umapalavraumamusica

Arquitetura

App (React Native + Expo)
  │  login anônimo · RPCs · Realtime · Edge Function
  ▼
Supabase
  ├── Postgres ── tabelas + RLS + regras do jogo em funções RPC (fonte oficial do estado)
  ├── Realtime ── avisa os aparelhos quando a sala muda
  ├── Auth ────── identidade anônima e estável por aparelho (reconexão sem duplicar jogador)
  └── Edge Function submit-guess
        └── MusicSearchService ── MusixmatchProvider (chave só no servidor)
                               └─ (opcional) IA para desempate entre candidatos reais

O aplicativo publicado não depende do Claude Code nem de plataformas como Base44/Lovable: é um app Expo comum + um projeto Supabase.

Quem decide o quê

O aparelho só exibe. Todas as decisões oficiais acontecem no Postgres, dentro de funções security definer que validam o jogador (auth.uid()), a sala, a fase e o prazo:

Decisão Onde
Quem enviou primeiro submit_guess (trava a linha da sala com select … for update)
Quem acertou / ponto resolve_guess (só a Edge Function, com service_role)
Palavra sorteada, sem repetição app_private.draw_word + índice único em used_words
Quem é host app_private.ensure_host (menor join_order entre os ativos)
Maioria para finalizar app_private.check_finish_majority (> 50% dos ativos)
Resultado da votação app_private.resolve_decision
Fim da partida app_private.finish_room

Os clientes não têm permissão de insert/update/delete em nenhuma tabela (RLS sem políticas de escrita + revoke). Leitura só para membros da sala (app_private.is_room_member). O banco de palavras não é legível pelo app.

Máquina de estados (rooms.status)

stateDiagram-v2
  [*] --> waiting
  waiting --> starting: host inicia (sozinho ou com outros)
  starting --> playing: 3 s · sorteia a palavra
  playing --> verifying: primeiro palpite
  verifying --> correct: música confirmada (+1 ponto)
  verifying --> incorrect: música incorreta
  verifying --> playing: API indisponível / tempo esgotado (não conta)
  correct --> countdown: 3 s
  countdown --> playing: 3 s · nova palavra
  correct --> finished: rodadas completas
  incorrect --> decision: 2 s
  playing --> decision: "Pular palavra"
  decision --> playing: maioria "Novo palpite" (mesma palavra)
  decision --> playing: "Nova palavra", empate ou sem votos
  decision --> finished: palavra descartada era a última rodada
  playing --> finished: maioria pediu para finalizar / host encerrou
  finished --> waiting: host "Jogar novamente"

Cada fase temporizada grava phase_ends_at. Quando o prazo vence, qualquer aparelho chama advance_room; o servidor confere o relógio dele e avança. A chamada é idempotente (vários aparelhos podem chamar juntos) e também acontece dentro do heartbeat, então a partida não trava se alguém fechar o app. As contagens na tela usam a diferença de relógio medida em cada leitura do estado.

Fase Duração
starting (“Preparem-se!”) 3 s
verifying até a resposta da API (mínimo 1,5 s na tela; expira em 25 s)
correct (“CONFIRMADO!”) 3 s
countdown (“Próxima palavra em 3, 2, 1”) 3 s
incorrect (“MÚSICA INCORRETA”) 2 s
decision (Novo palpite × Nova palavra) 3 s

Palavra exibida × rodada contabilizada

Palpites e concorrência

  1. O app chama a Edge Function submit-guess com o token da sessão.
  2. A função valida o usuário (auth.getUser) e chama submit_guess(user, sala, texto).
  3. submit_guess trava a sala. Se o estado não for playing, o palpite é descartado (busy). Se for, grava o palpite, muda para verifying e libera a trava — quem chegar depois já vê verifying.
  4. A função responde na hora e, em segundo plano (EdgeRuntime.waitUntil), verifica a música e grava o resultado com resolve_guess. Todos os aparelhos acompanham pelo Realtime.

Além da trava, o banco garante no máximo um palpite em verificação por sala e um palpite correto por palavra (índices únicos parciais).

Votação e finalização

Conexão, reconexão e host

Verificação musical

supabase/functions/_shared/music/:

MusicSearchService.verify({ guess, targetWord })
  1. Busca candidatos na fonte: por título, por letra (palpites com 3+ palavras) e geral.
  2. Avalia em ordem cada candidato (sem repetir música):
       - correspondência do palpite com o título (inclui "título + artista" e partes do título),
         com a letra (trecho) ou com "título + trecho";
       - presença da palavra sorteada no título ou na letra (aceita plurais simples: corações).
  3. Usa a PRIMEIRA correspondência válida (não o primeiro resultado bruto).
  4. Só possibilidades parciais? Se a IA estiver habilitada, ela escolhe entre os candidatos reais
     (índice ou "nenhum"). Sem IA, o palpite é incorreto.
  → { found, title, artist, matchedExcerpt, matchedWord, confidence, … }

Dados (equivalência com a especificação)

Especificação Implementação
rooms.host_id rooms.host_player_id
rooms.current_round rooms.rounds_played (rodadas contabilizadas) + current_word_id
rooms.status rooms.status (máquina de estados acima) + phase_ends_at
players.* igual, com user_id, last_seen_at e left_reason
rounds.* igual; round_number só é preenchido quando a palavra conta como rodada
guesses.* igual, com failure_reason, matched_word, confidence, provider
round_votes.* igual, com decision_number (uma votação por janela)
finish_requests.*, used_words.* iguais
— words: banco de palavras editável pelo painel do Supabase

Modo local

Sem servidor: src/game/local/reducer.ts é uma máquina de estados pura (setup → playing → finished) com as mesmas regras de contagem de rodadas (“Tentativa errada” registra uma tentativa; pular sem tentativa não conta). Para marcar um acerto, toca-se no nome de quem acertou e em “Próxima palavra”: o ponto e a rodada contam e a nova palavra entra direto, sem tela de comemoração. O estado é salvo no aparelho para continuar depois; ao sair do pódio (ou abandonar a partida pelo início), tudo zera, inclusive os nomes dos jogadores.

Modo online dentro do Claude

Quando a versão web é aberta como artefato no Claude (window.claude existe), o app usa outro backend com a mesma interface (src/services/online/types.ts), escolhido em src/services/onlineApi.ts:

App (web, dentro do Claude)
  ├── db ──── rooms/<CÓDIGO> (estado da sala) · rooms/<CÓDIGO>/votes/<jogador> (voto de cada um)
  ├── room ── presença na sala "upum-<código>" (quem está conectado)
  ├── user ── id estável de cada pessoa (reconexão sem duplicar jogador)
  └── sample ─ verificação da música pelo Claude, na conta de quem palpitou