Jogo da velha multiplayer em tempo real criado como projeto público de portfólio. O foco do projeto é demonstrar domínio de WebSocket, salas, estado em memória, validação server-side e uma experiência visual moderna.
Este projeto não usa banco de dados, autenticação, histórico de partidas ou placar persistente. A partida existe apenas enquanto a sala está ativa no servidor.
- Next.js App Router
- TypeScript
- TailwindCSS
- Socket.IO
- Servidor Node customizado
- Estado em memória
- Husky, lint-staged, commitlint e Prettier
O jogador informa um apelido e pode criar uma sala ou entrar usando um código numérico curto.
Quem cria a sala entra como jogador X. O segundo participante entra como jogador
O. A partir do terceiro participante, todos entram como espectadores.
O frontend apenas envia intenções, como criar sala, entrar, jogar em uma célula ou enviar mensagem como espectador. O servidor valida tudo antes de atualizar e transmitir o estado oficial da sala.
- Criação de sala com código numérico de 6 dígitos.
- Entrada de segundo jogador em tempo real.
- Espectadores com visualização da partida.
- Chat exclusivo para espectadores.
- Tabuleiro 3x3 sincronizado via Socket.IO.
- Partidas MD3 com placar em memória por sala.
- Validação server-side de turno, jogador, sala, célula e status.
- Vitória, empate e vitória por abandono calculados no servidor.
- Rate limit simples para jogadas e mensagens.
- Sanitização de apelidos e mensagens.
- Git hooks e commits com Conventional Commits.
A interface usa um sistema visual próprio chamado Arena Light, criado para parecer um produto de jogo casual moderno sem perder clareza.
Referências usadas:
- Lichess: tabuleiro como objeto central e interface sem distrações.
- Discord Activities: presença dos participantes, entrada rápida e suporte a espectadores.
- Xbox Accessibility Guidelines: contraste, contexto visível e controles compreensíveis sem depender apenas de cor.
Princípios aplicados:
- layout board-first, com o tabuleiro no centro da experiência;
- paleta clara com azul, ciano, âmbar e verde para estados importantes;
- cards compactos, bordas consistentes e feedback visual de turno;
- código de sala destacado e fácil de copiar;
- chat isolado para espectadores para manter jogadores focados na partida.
src/
app/
globals.css
layout.tsx
page.tsx
components/
arena-client.tsx
server/
rooms.ts
shared/
game.ts
socket-events.ts
types.ts
server.ts
src/shared/game.ts: regras puras do jogo da velha.src/shared/types.ts: tipos compartilhados entre cliente e servidor.src/shared/socket-events.ts: contrato tipado dos eventos Socket.IO.src/server/rooms.ts: gerenciamento de salas, jogadores, espectadores e chat.server.ts: servidor Node customizado com Next.js e Socket.IO.src/components/arena-client.tsx: interface da arena e comunicação com socket.
| Evento | Direção | Descrição |
|---|---|---|
room:create |
Cliente -> Servidor | Cria sala e coloca o socket como jogador X. |
room:join |
Cliente -> Servidor | Entra como jogador O ou espectador. |
room:state |
Servidor -> Cliente | Transmite o estado oficial da sala. |
game:move |
Cliente -> Servidor | Solicita uma jogada em uma célula. |
spectator:message |
Cliente -> Servidor | Envia mensagem no chat de espectadores. |
room:leave |
Cliente -> Servidor | Sai da sala atual. |
room:error |
Servidor -> Cliente | Retorna erro de validação ou operação. |
O servidor valida:
- se a sala existe;
- se o socket pertence à sala;
- se o usuário é jogador antes de jogar;
- se o usuário é espectador antes de enviar chat;
- se é a vez correta;
- se a célula está vazia;
- se o jogo ainda está ativo;
- se a mensagem respeita limite e rate limit;
- se a jogada respeita rate limit.
O cliente não define turno, vitória, empate ou abandono. Ele apenas renderiza o estado recebido do servidor.
- Usuário cria uma sala.
- Servidor gera um código numérico de 6 dígitos.
- Criador entra como
X. - Segundo participante entra como
O. - Participantes seguintes entram como espectadores.
- Todos recebem atualizações via
room:state.
Cada sala roda uma melhor de 3 em memória:
- a sala começa na rodada 1;
- o jogador que cria a sala começa como
X; - o início da rodada alterna entre
X,OeX; - vitória por linha soma ponto para o jogador vencedor;
- empate soma no contador de empates;
- quem fizer 2 pontos vence a MD3;
- se as 3 rodadas terminarem sem 2 pontos para ninguém, o maior placar vence;
- se o placar terminar empatado, a MD3 termina empatada.
Não há reconexão nem recuperação de sessão. Se um jogador desconectar ou sair da sala, o outro jogador vence automaticamente a MD3 por abandono. Se um espectador sair, ele apenas é removido da lista.
Instale as dependências:
npm installRode em desenvolvimento com o servidor customizado:
npm run devAcesse:
http://localhost:3000
Para produção local:
npm run build
npm run startnpm run dev
npm run lint
npm run typecheck
npm run format
npm run format:check
npm run build
npm run startO projeto usa:
pre-commit: roda lint-staged com ESLint e Prettier nos arquivos alterados.commit-msg: valida Conventional Commits.pre-push: roda lint, typecheck e build.
Exemplos de commits:
chore: initialize next.js project
chore: configure commit hooks
feat: add tic tac toe domain rules
feat: add in-memory room manager
feat: add socket.io custom server
feat: build realtime arena interface
docs: add architecture documentationPlaceholders para futuras imagens do projeto publicado.
- Lobby de criação e entrada de sala.
- Arena com tabuleiro em tempo real.
- Placar MD3 por rodada.
- Visualização de espectador com chat.
- Estado final por vitória, empate ou abandono.
- O estado fica em memória para manter o escopo focado em WebSocket.
- Não há login para reduzir ruído e destacar o fluxo multiplayer.
- A confirmação de jogadas e status vem sempre do servidor.
- O chat é exclusivo para espectadores para separar interação social do jogo.
- O servidor customizado é necessário porque WebSocket não roda no modelo padrão de rotas serverless.
- Testes automatizados para o gerenciador de salas.
- Testes end-to-end com múltiplos sockets.
- Replay local apenas em memória.
- Modo partida rápida com pareamento automático.
- Animações extras para vitória e abandono.