feat(multiplayer): votação de linguagem na sala de espera (#72) - #89
Conversation
|
Caution The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased. |
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
A linguagem da próxima corrida passa a ser decidida coletivamente: cada jogador tem um voto, mutável até o início; a mais votada vence e, em empate, há sorteio. Antes a linguagem era escolha exclusiva do líder — a votação aumenta a interação e a variedade. - `room.ts`: `tallyVotes` e `pickVoteWinner(votes, fallback, rng=Math.random)` puros e testáveis (empate → sorteio com rng injetável; sem votos → linguagem padrão da sala). Votos em linguagem inexistente são ignorados (`isValidLang`). - `useRoom.ts`: novo broadcast `vote` (mesmo transporte confiável do `ready`, com re-anúncio no join para quem chega depois). `voteTally`/`myVote` derivados só de quem está PRESENTE (quem sai/é kickado deixa de pesar). `castVote` otimista. No `start`, o líder resolve o vencedor e grava em `rooms.language` via a action `settings` ANTES do `start` — a linha da sala segue sendo a fonte de verdade que todos os clientes leem. Votos são zerados ao (re)entrar no lobby e ao começar a corrida. - `Lobby.tsx`: seletor de linguagem vira grade de VOTAÇÃO — todos clicam, cada botão mostra o contador, meu voto destacado, e a(s) linguagem(ns) na frente marcada(s). Um resumo mostra quem está vencendo ou o empate. Dificuldade e limite seguem só do líder. - `RoomView.tsx`: repassa `voteTally`/`myVote`/`onVote`. - `room.test.ts`: 8 casos de `tallyVotes`/`pickVoteWinner`. Nenhuma mudança na API de salas: o vencedor é aplicado pela action `settings` já existente. Nada toca a área sagrada (só o lobby).
492ddb4 to
9451217
Compare
|
Rebase nota: o PR #79 (issue #66, moderação/kick) mergeou na |
…avada Uniao com origin/main (resolve room.test.ts: mantém os blocos de votação #72 e de roomUpdateOutcome #56). Reparo do veto do quórum: startRace ignorava o retorno do postAction(settings) e iniciava incondicionalmente — se a gravação da linguagem vencedora falhasse (pós-#56 devolve ok:false), a corrida começava com a linguagem antiga enquanto o líder via toast de erro. Agora aborta o start quando !res.ok.
🏛️ Quórum §7.2 — reparada e APROVADALente Ofensiva/Domínio vetou (com razão): Reparo (commit União com origin/main resolvida ( |
Uniao com origin/main (que trouxe #60/#79/#96/#89). Resolucao: - room.ts: mantidos os builders (buildMatchRow/buildScoreRows/MatchInsert/ ScoreInsert) E as funcoes de kick/vote/outcome que entraram na main. - route.ts: imports unificados; persistMatch segue como wrapper fino dos builders. - room.test.ts: imports unificados; adicionado kicked_ids:[] ao FULL_ROOM (campo virou obrigatorio em RoomRow via #79). Gate: typecheck OK, build OK, test 103 OK, validate-persistence 72/0.
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.
Contexto
A linguagem da corrida era escolha exclusiva do líder. A #72 torna a decisão coletiva:
na sala de espera, cada jogador tem um voto (mutável até o start); a mais votada vence e, em
empate, há sorteio. Mais interação, mais variedade, incentivo a experimentar linguagens.
O que mudou
src/lib/room.tstallyVotes+pickVoteWinner(votes, fallback, rng)— puros/testáveissrc/lib/useRoom.tsvote, tally por presença,castVote, resolução do vencedor nostartsrc/components/Lobby.tsxsrc/components/RoomView.tsxvoteTally/myVote/onVotesrc/lib/room.test.tsComo as votações se propagam (mesma receita do
ready): o voto é um broadcast Realtimeconfiável, re-anunciado no
joinpara quem entra depois (broadcast não tem replay). O tallyconta só quem está presente — quem sai ou é kickado deixa de pesar no resultado.
Quem decide o vencedor, e onde (fonte única de verdade): ao iniciar, o líder resolve
pickVoteWinner(votos, room.language)e grava a linguagem vencedora emrooms.languagepelaaction
settingsjá existente, ANTES dostart(que lê essa linha para sortear o snippet).Assim, a decisão passa por um único ponto e todos os clientes convergem pela linha da sala —
sem inventar um caminho novo de configuração.
Empate e sem-votos: empate → sorteio entre as empatadas (
rnginjetável, testado com()=>0e()=>0.99); ninguém votou → mantém a linguagem padrão da sala. Votos são zerados ao(re)entrar no lobby e ao começar a corrida (rodada nova = votação nova).
Critérios de aceite
playerId→lang; re-clique troca).start).pickVoteWinner).rooms.languageantes dostart).Como foi validado (resultado real)
Render-check do Lobby (harness temporário, removido antes do commit): a grade renderiza
sem erro, os contadores aparecem (python 3, go 3, rust 1) e o resumo detecta empate corretamente
—
"empate: Python, Go — sorteio no início"; dificuldade/limite seguem só do líder.Limitação de verificação: a propagação realtime dos votos entre clientes e a aplicação da
linguagem vencedora no
startexigem dois clientes num canal Realtime real — inviávelheadless nesta rodada. A lógica pura de contagem/vencedor/empate está coberta por unit tests
determinísticos; a fiação de UI/props por typecheck + build + render-check. Recomendo ao PR
Doctor um teste com duas abas para confirmação visual do fluxo completo.
Riscos
presence.isValidLangdescarta antes de contar; e o vencedor ésempre um
LangIdválido (voto válido oufallback= linguagem da sala).src/app/api/rooms/**(usa a actionsettingsexistente) nemsupabase.ts/anti-cheat/migrations/CI; nenhuma dependência nova. Área sagrada intacta.Solicito quórum (HANDBOOK §7)
Closes #72