-- NORMAL -- ~/projets/typomaniac.md projet 01 / 06

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
Dépôt
github.com/Popoch39/typomaniac ↗
A

Anatomie

7 pièces · une seule les compte
Fig. 1 Banc de duel à tour d’arbitrage
bun · elysias literature loiterature longe
  1. Servir tout le jeu

    Un seul serveur Bun garde la file d’attente, les duels en mémoire et toute l’API.

    DuelQueue

    99const duelQueue = new DuelQueue({

    100 clock: config.clock,

    101 store: duelStore,

  2. 02BATCH_MSL792–823 ↗

    Dater chaque frappe

    Chaque lettre est datée, affichée aussitôt, puis envoyée par lots de 50 ms.

    pressed()

    794const pressed = (store: DuelStore, key: Key, now: number): Pick<DuelStore, "state" | "cues"> => {

    795 const { state } = store;

    797 if (state.phase !== "running") {

  3. 03/api/duelL17–52 ↗

    Relier les deux joueurs

    Une WebSocket par onglet : on envoie ses frappes, on reçoit celles de l’adversaire.

    duelModule()

    20export const duelModule = ({ auth, trustProxy, queue, friendsLive }: DuelModuleConfig) =>

    21 new Elysia({ name: "duel", seed: queue }).use(authentication(auth, { trustProxy })).ws("/duel", {

    22 auth: true,

  4. 04RunningDuelL423–522 ↗

    Arbitrer le duel

    Le serveur garde le duel en mémoire, juge la manche à 30,4 s et relance 7 s après.

    endRound()

    428endRound(judgedAt: number, draw: () => number): RoundEnd {

    429 const endedAt = this.#round.startsAt + this.#durationMs;

    430 const ended = this.#endRound(endedAt);

  5. 05acceptance.tsL21–68 ↗

    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()

    22const rejectReason = (

    23 acceptedRun: AcceptedRun,

    24 { at }: Keystroke,

  6. 06isDuelDecided()L21–67 ↗

    Désigner le gagnant

    Le meilleur score gagne la manche, puis la précision ; deux manches gagnent le duel.

    roundOutcome()

    22export const roundOutcome = (first: RoundSide, second: RoundSide): Outcome =>

    23 outcomeOf(first.score - second.score || first.result.accuracy - second.result.accuracy);

    27export const roundsWon = (rounds: readonly RoundSides[]): [number, number] => {

  7. 07applyTp()L164–206 ↗

    Faire monter au classement

    Une victoire vaut 8 à 35 points de rang, 20 à niveau égal ; 100 points font monter.

    tpDelta()

    167export const tpDelta = (

    168 standing: Standing,

    169 mmr: number,

manche 2/3 · 18,6 s · A 1–0 B
Lecture des notes

Cliquez le clavier A ou tapez une lettre : elle file vers la tour.Touchez le clavier A : chaque appui envoie une lettre à la tour.

Fig. 1 — Chaque frappe part datée du clavier, par lots de 50 ms. La tour la refuse si sa date précède le départ, suit son arrivée, recule, ou dépasse les 30 s de la manche (400 ms de tolérance). Elle juge la manche au score puis à la précision, et le premier à deux manches gagnées fait bouger les TP ; le MMR, lui, reste caché.

Nomen­clature

  1. 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 ↗
  2. 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 ↗
  3. 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 ↗
  4. 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 ↗
  5. 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 ↗
  6. 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 ↗
  7. 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 ↗