Keep the clients direct.
Most interactions happen in the browser. The API is not inserted between the player and every click.
A personal web project where I publish browser games and Windows DFIR research. The work ranges from small JavaScript experiments to local machine learning, a Three.js game, and a real-time multiplayer system.
critical.lol began as a place to ship experiments. Some games only need HTML, CSS, and browser state. Others need a shared question set, local inference, a 3D scene, or synchronized rooms. The architecture stays intentionally uneven: each experience gets the smallest system that can support it.
The browser owns presentation and most game logic. Express supplies shared datasets and blog content; Socket.IO is reserved for the multiplayer game.
Most interactions happen in the browser. The API is not inserted between the player and every click.
Questions, cards, and articles remain ordinary files rather than opaque records in a custom CMS.
Patent Pending uses server rooms because several phones and one TV must agree on the same round.
The complete directory has direct links and longer summaries. This is the engineering view.
Can algorithm analysis work as a quick multiple-choice game?
Can strange real commit messages be turned into a source-linked guessing game?
How many competing resources can a compact turn-based simulation keep legible?
How reliably can a player infer ideology from political language alone?
Can a drawing game run useful image classification entirely in the browser?
How much atmosphere and systemic horror can fit inside a single HTML file?
How should one TV and several phones coordinate a timed party-game round?
The excerpts below are shortened from the project source, but preserve the real control flow.
The route samples the JSON dataset and builds a new answer order. By default it withholds the solution; adding ?reveal=1 includes the answer, hint, and explanation.
app.get('/api/v1/big-o/get_question', (req, res) => {
const reveal = String(req.query.reveal || '').trim() === '1';
const q = pick(bigOData.questions);
const payload = {
id: nanoid(10),
code: q.code,
choices: buildChoices(q.answer, bigOData.allComplexities)
};
if (reveal) {
payload.answer = q.answer;
payload.hint = q.hint;
payload.explanation = q.explanation;
}
res.json(payload);
});
The game resizes the drawing to 64 by 64 pixels, converts it into a normalized tensor, and runs the ONNX model through WebGL with WASM as a fallback. Predictions update during the round.
const tensor = preprocessCanvasToTensor();
const feeds = {};
feeds[inputName] = tensor;
const results = await ortSession.run(feeds);
const logits = results[outputName].data;
const probs = softmax(logits);
updateProbsUI(probs);
ortSession = await ort.InferenceSession.create(modelData, {
executionProviders: ['webgl', 'wasm']
});
The game never displays a sanity meter. Instead, lower stability changes audio, visuals, timing, and the likelihood that a visitor is deceptive. The player reads the system through the apartment rather than through a status bar.
function pickScenario(){
const t = GAME.timeSec;
const s = psyche.sanity;
const prog = clamp(t / 360, 0, 1);
const fakeChance = clamp(
0.18 + (1 - s) * 0.45 + prog * 0.22,
0.18,
0.78
);
const isFake = Math.random() < fakeChance;
const pool = isFake ? FAKE_ENTITIES : REAL_ENTITIES;
return { sc: pool[Math.floor(Math.random() * pool.length)], isFake };
}
The TV creates the room and directs timing. Phones join with an eight-digit code and render only the action needed for the current phase.
The host updates hostBeat. Rooms that stop receiving it are removed from in-memory storage.
Every write includes the version it started from. A stale client receives a conflict instead of overwriting newer state.
Intro phases wait for narration to finish before starting the timer, keeping the shared screen understandable.
const baseV = Number(payload?.baseV || 0);
const curV = Number(cur.v || 0);
if (baseV && baseV !== curV) {
return ack && ack({
ok: false,
error: 'conflict',
room: ppPublicRoom(cur)
});
}
merged.hostBeat = cur.hostBeat;
merged.v = curV + 1;
The blog is part of the same project, but its system is deliberately simple: Markdown on disk, cached JSON from the API, and a browser reader.
Following persistence through scheduler files, registry data, memory, and USN records.
Using MemProcFS to explore processes, registry evidence, devices, and network traces.
Connecting SysMain and execution evidence while examining where the artifact breaks down.
Automating string searches across live process memory with a read-only native module.
Building many small products under one domain made the tradeoffs unusually visible.
A shared service is useful for common data and multiplayer state. It would only slow down the self-contained games.
WebGL, Web Audio, Canvas, and WASM are not just implementation details; they determine which ideas are possible without installation.
Keeping articles in Markdown and supporting an offline bundle means the writing outlives the current reader interface.
Rate limits, room expiry, version checks, input limits, and cache validators keep playful software from becoming careless software.
critical.lol is not one application pretending to be a platform. It is a growing set of browser projects with a small amount of shared infrastructure where sharing actually helps.