typomaniac
Deux claviers face à une tour : chaque lettre est datée chez le joueur, mais c’est le serveur qui tranche.
- Stack
- TypeScript · Elysia · Bun · React 19
- Année
- 2026
Anatomie
7 pièces · une seule les compteServir tout le jeu
Un seul serveur Bun garde la file d’attente, les duels en mémoire et toute l’API.
DuelQueue99
const duelQueue = new DuelQueue({100
clock: config.clock,101
store: duelStore,Dater chaque frappe
Chaque lettre est datée, affichée aussitôt, puis envoyée par lots de 50 ms.
pressed()794
const pressed = (store: DuelStore, key: Key, now: number): Pick<DuelStore, "state" | "cues"> => {795
const { state } = store;797
if (state.phase !== "running") {Relier les deux joueurs
Une WebSocket par onglet : on envoie ses frappes, on reçoit celles de l’adversaire.
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,Arbitrer le duel
Le serveur garde le duel en mémoire, juge la manche à 30,4 s et relance 7 s après.
endRound()428
endRound(judgedAt: number, draw: () => number): RoundEnd {429
const endedAt = this.#round.startsAt + this.#durationMs;430
const ended = this.#endRound(endedAt);Vérifier la date des frappes
Refuse une frappe datée avant le départ, après sa réception, à rebours ou trop tard.
rejectReason()22
const rejectReason = (23
acceptedRun: AcceptedRun,24
{ at }: Keystroke,Désigner le gagnant
Le meilleur score gagne la manche, puis la précision ; deux manches gagnent le 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] => {Faire monter au classement
Une victoire vaut 8 à 35 points de rang, 20 à niveau égal ; 100 points font monter.
tpDelta()167
export const tpDelta = (168
standing: Standing,169
mmr: number,
Cliquez le clavier A ou tapez une lettre : elle file vers la tour.Touchez le clavier A : chaque appui envoie une lettre à la tour.
Nomenclature
- 01 Socle Bun + Elysia : un seul process porte la file, les duels en mémoire et toutes les routes de l’API. Code : apps/api/src/app.ts, lignes 97–182 ↗
- 02 Claviers A et B Clients React 19 : la frappe est datée, appliquée tout de suite à l’écran, puis mise en lot. Code : apps/frontend/src/stores/duel-store.ts, lignes 792–823 ↗
- 03 Câbles La WebSocket /api/duel, une par onglet : lots de frappes à l’aller, frappes de l’adversaire et resync au retour. Code : apps/api/src/modules/duel/index.ts, lignes 17–52 ↗
- 04 Tour d’arbitrage Le duel en cours, en mémoire : reçoit chaque lot, coupe au forfait une cadence inhumaine, juge la manche à 30,4 s et ouvre la suivante 7 s plus tard. Code : apps/api/src/modules/duel/running-duel.ts, lignes 423–522 ↗
- 05 Guichet Les quatre contrôles de date d’une frappe : avant le départ, après son arrivée, hors d’ordre, après la fin. Code : packages/typing-engine/src/acceptance.ts, lignes 21–68 ↗
- 06 Lampes de manche Le meilleur score gagne la manche, puis la précision ; deux manches gagnées ou trois jouées décident le duel. Code : packages/typing-engine/src/outcome.ts, lignes 21–67 ↗
- 07 Mât Les TP du duel, bornés entre 8 et 35 ; montée à 100 TP avec bouclier, descente à 75 TP ; le MMR reste caché. Code : packages/ranked/src/ranked.ts, lignes 164–206 ↗