Skip to content

feat(multiplayer): entrada após o início vira espectador — congela a lista da rodada (#64) - #80

Merged
caioross merged 3 commits into
mainfrom
auto/issue-64-spectator
Jul 26, 2026
Merged

feat(multiplayer): entrada após o início vira espectador — congela a lista da rodada (#64)#80
caioross merged 3 commits into
mainfrom
auto/issue-64-spectator

Conversation

@caioross

Copy link
Copy Markdown
Owner

Contexto

A lista de participantes ficava aberta durante toda a partida: quem entrasse com a
corrida já em racing passava a compor a mesma rodada, e todos que já tinham terminado
ficavam presos esperando o retardatário terminar/desistir — quebrando o ritmo da sala
(issue #64). A regra passa a ser: uma partida é composta por quem estava presente
quando ela começou
(o instante start_at); quem chega depois assiste e entra na próxima.

O que mudou

Arquivo O quê
src/lib/room.ts isSpectatorJoin(joinedAt, startMs, status) — puro/determinístico; LivePlayer.spectator
src/lib/useRoom.ts spectator no roster; gate de fim ignora espectadores; expõe isSpectator
src/components/RoomView.tsx filtra espectadores do roster de competidores; roteia para SpectatorView
src/components/SpectatorView.tsx banner "Partida em andamento" + pista ao vivo + chat
src/lib/room.test.ts 6 casos de classificação

Determinismo (por que é seguro sem tocar o servidor): joinedAt (presence) e
start_at (linha da sala) são compartilhados por todos os clientes, então todos
classificam o mesmo jogador igual. Espectador = rodada em andamento (racing/finished)
e joinedAt > startMs. Em lobby/countdown ninguém é espectador — quem está antes do
start compõe a corrida.

O ponto de maior risco (e o que este PR conserta): o gate de fim de corrida era
players.every(p => p.finishedAt). Um retardatário sem finishedAt prendia a sala em
racing para sempre. Agora o líder finaliza quando todos os RACERS (!spectator)
terminaram; espectadores nunca entram no gate nem no results (não têm finishedAt, então
já eram naturalmente excluídos do ranking — nenhum caminho novo para o leaderboard).

Próxima rodada: quando o líder reinicia, start_at muda; o antigo espectador passa a ter
joinedAt < novo startMs → vira competidor automaticamente (AC atendido).

Como foi validado (resultado real)

pnpm install --frozen-lockfile   OK
pnpm typecheck                   OK
pnpm build                       OK
pnpm test                        OK — 40 testes (6 novos de isSpectatorJoin)
validate-metrics.mjs             OK — 37 passaram
validate-persistence.mjs         OK — 22 passaram
pnpm lint                        N/A — sem config ESLint no repo

Smoke test (next dev na worktree): /, /practice e /room/<code> renderizam sem erro
de console; o name-gate da sala aparece normal.

Limitação de verificação: a SpectatorView só aparece para um jogador que entra numa
sala com corrida em andamento — exige dois clientes simultâneos num canal Realtime real,
inviável de forma confiável headless nesta rodada. A classificação (o núcleo da mudança) está
coberta por unit tests determinísticos; a troca de view e o filtro do roster estão cobertos
por typecheck + build. Recomendo ao PR Doctor um teste manual com duas abas se quiser
confirmação visual.

Riscos

  • Relógio: joinedAt é Date.now() do cliente que entra; startMs é Date.parse(start_at)
    do servidor. Skew de segundos só afeta quem entra na fronteira exata do start (durante o
    countdown de 4s) — no pior caso vira espectador e compete na rodada seguinte (degradação
    suave, nunca trava). Quem entra claramente no meio da corrida é sempre classificado certo.
  • Área sagrada intacta: nada no caminho de digitação/textarea foi tocado.
  • Player (types.ts) não mudou — espectadores são filtrados antes da conversão LivePlayer→Player.

Solicito quórum (HANDBOOK §7)

Closes #64

@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 8:13pm

…lista da rodada (#64)

A lista de participantes ficava aberta durante toda a partida: quem entrasse com a
corrida já em `racing` passava a compor a mesma rodada, e todos que já tinham
terminado ficavam presos esperando o retardatário terminar ou desistir. Uma partida
deve ser composta por quem estava presente quando ela COMEÇOU.

- `isSpectatorJoin(joinedAt, startMs, status)` em `src/lib/room.ts` — puro e
  determinístico (joinedAt e start_at são compartilhados por presence + linha da
  sala, então todos os clientes classificam igual). Espectador = entrou depois do
  start (`joinedAt > startMs`) com a rodada em andamento.
- `useRoom`: cada `LivePlayer` ganha `spectator`; o gate de fim de corrida passa a
  ignorar espectadores (`racers = players.filter(!spectator)`), então um retardatário
  sem `finishedAt` não prende mais a sala. Expõe `isSpectator` para a UI.
- `RoomView`: espectadores saem da pista/ranking (filtrados do roster de competidores)
  e veem a nova `SpectatorView` — banner "Partida em andamento", pista ao vivo e chat.
  Entram automaticamente na próxima rodada (start_at muda → deixam de ser espectadores).
- 6 casos novos em `room.test.ts` cobrindo a classificação (limite exato, lobby,
  finished, sem corrida).
@caioross
caioross force-pushed the auto/issue-64-spectator branch from d4736f1 to 0c9bb04 Compare July 26, 2026 13:05
@caioross

Copy link
Copy Markdown
Owner Author

Rebase nota: o PR #60 (issue #58, liveness) mergeou na main enquanto eu trabalhava, tocando os mesmos 3 arquivos. Rebasei em cima de origin/main e integrei a exclusão de espectadores na nova lógica de fim de corrida do #60 em vez de reescrevê-la:

@caioross

Copy link
Copy Markdown
Owner Author

🔧 PR Doctor — CONFLITANTE, reparo por união antes de avaliar

A #80 (espectador ao entrar depois do início) está CONFLICTING (DIRTY) — toca room.ts/useRoom.ts/RoomView.tsx/room.test.ts, os mesmos arquivos mexidos por #60 (liveness) e #79 (kick moderation) já mergeados. Ação (§5.D): na branch auto/issue-64-spectator, git merge origin/main (união, nunca --force/rebase), resolver mantendo as três mudanças, rodar o gate (typecheck+build+test+validate-persistence) e push. Depois entra na fila de quórum §7.2 (estado multiplayer). Não mergeada nesta rodada por causa do conflito.

@caioross

Copy link
Copy Markdown
Owner Author

🔧 PR Doctor — ainda CONFLITANTE; reparo por união + re-quórum

A #80 (espectador ao entrar depois do início) segue CONFLICTING: toca room.ts/useRoom.ts/RoomView.tsx/room.test.ts, os mesmos arquivos reescritos por #60 (liveness), #79 (kick), #89 (votação) e #96 (writes honestos) — todos já na main.

Ação (§5.D): resetar a branch ao head da PR, git merge origin/main (união, nunca --force), resolver mantendo o fluxo de espectador junto de tudo que entrou (liveness/kick/vote/writes), rodar o gate completo e push. Depois quórum §7.2 (estado multiplayer: garantir que congelar a lista da rodada não conflita com o roster de presença nem com shouldFinishRace). Não mergeada nesta rodada — o conflito é grande e precisa de rebase cuidadoso + revisão adversarial fresca.

Uniao com origin/main (#55/#60/#79/#89/#96). Resolucao (tudo aditivo):
- useRoom.ts: mantidos isSpectatorJoin + isSpectator E as importacoes/logica de
  votacao (#89, tallyVotes/pickVoteWinner/activeVotes/voteTally/myVote).
- RoomView.tsx: mantido o branch isSpectator?SpectatorView:Race E os props
  isLeader/onKick do Race (#79), alem de voteTally/myVote no destructure.
- room.test.ts: mantidos os blocos de isSpectatorJoin (#64) e votacao/outcome.
Gate: typecheck OK, test 109 OK, build OK, validate-persistence 72/0.
Achado do quorum: congelar a lista da rodada reintroduzia sala presa. Se todos
os competidores saem durante racing e resta so um retardatario-espectador,
racers fica [] e shouldFinishRace (guarda de lista vazia) nunca encerra. Agora
o lider posta finish com results vazios (tela de Results vazia da #63) apos o
countdown, liberando a sala.
@caioross

Copy link
Copy Markdown
Owner Author

🏛️ Quórum §7.2 — reparada e APROVADA (2 lentes)

  • Ofensiva — APROVA: o flag spectator é client-side mas nunca foi a fronteira do servidor; toResults filtra finishedAt && !abandoned independente do flag (espectador não tem editor, finishedAt fica null → some). O PR até melhora a resistência a DoS (filtra espectadores do maybeFinish). O buraco de finish sem gate é pré-existente (Anti-cheat: finish sem coerência temporal — leaderboard forjável em 1 request e corrida alheia encerrável #34), fora do escopo.
  • Domínio — vetou e foi RESOLVIDO: congelar a lista reintroduzia sala presa — se todos os competidores saem e só resta um espectador, racers fica [] e shouldFinishRace([]) nunca encerra. Reparo (commit b186a87): o líder detecta racers.length === 0 em racing e encerra com results vazios (cai na tela de Results vazia da feat(dev): harness só-de-dev das telas pós-corrida (Results/pódio) com fixtures (#37) #63), com guarda de countdown. Re-verificação: RESOLVIDO, sem defeito novo (finishPostedRef evita dupla postagem; líder-espectador consegue resetar).

isSpectatorJoin puro/determinístico (usa start_at do servidor), coberto por teste. União com main (#55/#60/#79/#89/#96) resolvida (spectator + kick + vote coexistem). Gate: typecheck ✓, test 109 ✓, build ✓, validate-persistence 72/0 ✓. CI + Vercel verdes. Closes #64. Squash-merge.

@caioross
caioross merged commit a666817 into main Jul 26, 2026
3 checks passed
@caioross
caioross deleted the auto/issue-64-spectator branch July 26, 2026 20:15
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] Jogadores que entrarem após o início da partida devem aguardar a próxima rodada

1 participant