typomaniac
Two keyboards facing a tower: every letter is timestamped on the player’s side, but it’s the server that decides.
- Stack
- TypeScript · Elysia · Bun · React 19
- Year
- 2026
- Repository
- github.com/Popoch39/typomaniac ↗
Anatomy
7 parts · only one countsServe the whole game
A single Bun server holds the queue, the duels in memory and the whole API.
DuelQueue99
const duelQueue = new DuelQueue({100
clock: config.clock,101
store: duelStore,Timestamp each keystroke
Each letter is timestamped, shown at once, then sent in 50 ms batches.
pressed()794
const pressed = (store: DuelStore, key: Key, now: number): Pick<DuelStore, "state" | "cues"> => {795
const { state } = store;797
if (state.phase !== "running") {Connect the two players
One WebSocket per tab: you send your keystrokes and receive your opponent’s.
duelModule()20
export const duelModule = ({ auth, trustProxy, queue, friendsLive }: DuelModuleConfig) =>21
new Elysia({ name: "duel", seed: queue }).use(authentication(auth, { trustProxy })).ws("/duel", {22
auth: true,Referee the duel
The server keeps the duel in memory, judges the round at 30.4 s and restarts 7 s later.
endRound()428
endRound(judgedAt: number, draw: () => number): RoundEnd {429
const endedAt = this.#round.startsAt + this.#durationMs;430
const ended = this.#endRound(endedAt);Check keystroke timestamps
Rejects a keystroke stamped before the start, after it arrived, out of order or too late.
rejectReason()22
const rejectReason = (23
acceptedRun: AcceptedRun,24
{ at }: Keystroke,Name the winner
The higher score takes the round, then accuracy; two rounds won take the duel.
roundOutcome()22
export const roundOutcome = (first: RoundSide, second: RoundSide): Outcome =>23
outcomeOf(first.score - second.score || first.result.accuracy - second.result.accuracy);27
export const roundsWon = (rounds: readonly RoundSides[]): [number, number] => {Climb the rankings
A win is worth 8 to 35 rank points, 20 between equals; 100 points earn a promotion.
tpDelta()167
export const tpDelta = (168
standing: Standing,169
mmr: number,
Click keyboard A or type a letter: it races to the tower.Tap keyboard A: each press sends a letter to the tower.
Parts list
- 01 Base Bun + Elysia: a single process holds the queue, the in-memory duels and every API route. Code: apps/api/src/app.ts, lines 97–182 ↗
- 02 Keyboards A and B React 19 clients: each keystroke is timestamped, shown on screen straight away, then batched. Code: apps/frontend/src/stores/duel-store.ts, lines 792–823 ↗
- 03 Cables The /api/duel WebSocket, one per tab: keystroke batches on the way out, the opponent’s keystrokes and resyncs on the way back. Code: apps/api/src/modules/duel/index.ts, lines 17–52 ↗
- 04 Referee tower The live duel, in memory: takes in every batch, forfeits an inhuman typing rate, judges the round at 30.4 s and opens the next one 7 s later. Code: apps/api/src/modules/duel/running-duel.ts, lines 423–522 ↗
- 05 Checkpoint The four timestamp checks on a keystroke: before the start, after its arrival, out of order, after the end. Code: packages/typing-engine/src/acceptance.ts, lines 21–68 ↗
- 06 Round lamps The higher score wins the round, then accuracy; two rounds won or three played decide the duel. Code: packages/typing-engine/src/outcome.ts, lines 21–67 ↗
- 07 Mast The duel’s TP, kept between 8 and 35; promotion at 100 TP with a shield, demotion to 75 TP; the MMR stays hidden. Code: packages/ranked/src/ranked.ts, lines 164–206 ↗