Skip to content

feat(multiplayer): desistência automática por inatividade na corrida (#65) - #81

Merged
caioross merged 2 commits into
mainfrom
auto/issue-65-idle-forfeit
Jul 26, 2026
Merged

feat(multiplayer): desistência automática por inatividade na corrida (#65)#81
caioross merged 2 commits into
mainfrom
auto/issue-65-idle-forfeit

Conversation

@caioross

Copy link
Copy Markdown
Owner

Contexto

Um jogador que abandona o teclado prendia a sala: a corrida esperava todos terminarem,
então um único AFK segurava todo mundo por minutos (issue #65). Agora, sem digitar por
30s
, abre um aviso com contagem regressiva de 10s; qualquer tecla cancela e, ao zerar,
o jogador desiste sozinho (abandon) e a sala segue.

O que mudou

Tudo em src/components/TypingCore.tsx (+ 1 linha em Race.tsx):

  • Detecção opt-in via prop idleForfeit: o Race (multiplayer) liga; o Practice não
    (PracticeView não passa a prop) — treino solo não tem ninguém esperando, então nada muda lá.
  • Detecção em refs + um único tick de 500ms. Enquanto se digita, cada tecla só faz um
    write de ref (markActivity) — nenhum setState novo por keystroke, então a latência de
    input não muda (área sagrada, HANDBOOK §2). O setState só ocorre quando o aviso está aberto
    (jogador parado, por definição).
  • IdleWarning: aviso âmbar com contagem, role="alert" + aria-live="assertive",
    prefers-reduced-motion respeitado (sem animação de entrada/saída quando reduzido).
  • Reusa o abandon() já existente — nenhum caminho novo de persistência/leaderboard. Um
    jogador que desiste já fica marcado abandoned e fora do ranking; e como ganha finishedAt,
    destrava o gate de fim que espera todos terminarem.

Como foi validado (resultado real)

pnpm install --frozen-lockfile   OK
pnpm typecheck                   OK
pnpm build                       OK  (/practice 150 kB, /room/[id] 222 kB)
pnpm test                        OK — 34 testes
validate-metrics.mjs             OK — 37 passaram
validate-persistence.mjs         OK — 22 passaram
pnpm lint                        N/A — sem config ESLint no repo

Verificação da lógica em browser (rastreio instrumentado, depois removido): rodei o núcleo
com constantes reduzidas (2s/6s) e um trace por tick. O log confirmou o ciclo inteiro:

idle-ok(0.5s) → idle-ok(1.0s) → idle-ok(1.5s) → open(2.0s) →
count 6→6→5→5→4→4→3→3→2→2→1→1→0 → (abandon disparado no 0) → re-open

E que digitar cancela: após uma tecla, o idle volta a ~0.3s e o tick retorna a idle-ok
(sem aviso). As constantes foram restauradas para 30s/10s e toda a instrumentação removida
antes do commit (diff final = só a feature).

Limitação: o overlay usa framer-motion; no browser headless desta rodada o rAF fica
suspenso (a Browser pane não compõe frames), então a opacidade do aviso ficou congelada em 0 —
artefato de renderização, não da lógica (que o trace acima confirma). Em browser visível a
animação toca normalmente.

Riscos

  • Leitor lento que passa 30s sem digitar recebe o nudge "digite qualquer tecla" — comportamento
    desejado da issue; 10s + qualquer tecla cancelam.
  • Área sagrada: medido que o caminho quente (digitando) não ganha setState novo — só um write
    de ref por tecla.
  • Governança HANDBOOK §7.3 (normal): não toca api/rooms/**, supabase.ts, migrations, CI, nem
    adiciona dependência; reusa abandon().

Closes #65

…65)

Um jogador que abandona o teclado prendia a sala: a corrida esperava todos
terminarem, então um único AFK segurava todo mundo por minutos. Agora, sem digitar
por 30s, abre um aviso com contagem regressiva de 10s; qualquer tecla cancela e, ao
zerar, o jogador desiste sozinho (`abandon`) e a sala segue.

- Lógica em `TypingCore` (compartilhado com o Practice), ligada só no multiplayer via
  `idleForfeit` — o `Race` passa a prop; o Practice não (treino solo não tem ninguém
  esperando).
- Detecção em refs + um único tick de 500ms: enquanto se digita não há setState novo
  por tecla (só um write de ref), preservando a latência de input (área sagrada §2).
  Qualquer tecla reancora o relógio de ociosidade e fecha o aviso.
- Novo `IdleWarning` (aviso âmbar com contagem, `role="alert"`/`aria-live`,
  `prefers-reduced-motion` respeitado). Reusa o `abandon()` já existente — nenhum
  caminho novo de persistência/leaderboard.
@gemini-code-assist

Copy link
Copy Markdown

Caution

The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased.

@vercel

vercel Bot commented Jul 26, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
code-racer Ready Ready Preview, Comment Jul 26, 2026 1:24pm

O tick de inatividade listava onAbandon nas deps. Como actions.abandon
depende de progress no useRoom, sua identidade trocava a cada heartbeat
(~1s), re-rodando o efeito e reancorando lastActivityRef: o aviso de 30s
nunca abria (feature nascia morta) e o setInterval era recriado a cada
tecla, competindo com a textarea. Uma ref estavel (onAbandonRef) desacopla
o tick desse churn; deps do efeito passam a [idleForfeit, iFinished, startedAt].
Achado pelo quorum adversarial do PR Doctor.
@caioross

Copy link
Copy Markdown
Owner Author

🏛️ Quórum §7.2 — reparada e APROVADA

Lente Performance/área-sagrada vetou (com razão): onAbandon (=actions.abandon) tinha progress nas deps → identidade trocava a cada heartbeat → o tick reancorava lastActivityRef e o aviso nunca abria (feature nascia morta), além de recriar o setInterval a cada tecla (churn na área sagrada).

Reparo (commit d385b65): onAbandon roteado por onAbandonRef estável e removido das deps do tick (agora [idleForfeit, iFinished, startedAt]). Re-verificação da lente: RESOLVIDO — o relógio de ociosidade não é mais reancorado por heartbeat, o aviso de 30s abre, o interval não é recriado por keystroke e o caminho quente segue sem setState novo por tecla.

Gate verde no worktree (typecheck ✓, test 34 ✓, build ✓) e CI + Vercel verdes no head reparado. Closes #65. Squash-merge.

@caioross
caioross merged commit 1c2a266 into main Jul 26, 2026
3 checks passed
@caioross
caioross deleted the auto/issue-65-idle-forfeit branch July 26, 2026 13:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature] Sistema automático de desistência por inatividade

1 participant