Implementação de um jogo de Crash multiplayer em tempo real, desenvolvido como desafio técnico fullstack. O projeto segue uma arquitetura de microserviços com comunicação assíncrona via RabbitMQ e princípios de Domain-Driven Design (DDD).
Requisitos: Docker e Bun instalados.
# 1. Instalar dependências
bun install
# 2. Subir toda a infra
bun run docker:upAcesse em http://localhost:3000.
| Campo | Valor |
|---|---|
| Login | player |
| Senha | player123 |
| Saldo | R$ 10.000 |
| Serviço | Porta direta | Via Kong |
|---|---|---|
| Frontend | 3000 |
— |
| Game Service | 4001 |
http://localhost:8000/games/* |
| Wallet Service | 4002 |
http://localhost:8000/wallets/* |
| Keycloak | 8080 |
— |
| RabbitMQ UI | 15672 |
— |
- Game Service: http://localhost:4001/docs
- Wallet Service: http://localhost:4002/docs
- Round (agregado raiz): gerencia o ciclo de vida completo
BETTING → ACTIVE → CRASHED. Impõe invariantes como impedir aposta durante rodada ativa e impedir cashout sem aposta. - Bet (entidade): status
PENDING → CASHED_OUT | LOST. Payout =amountCents × multiplier. Limites de R$ 1,00 a R$ 1.000,00 aplicados no construtor. - CrashPoint (value object): gerado via HMAC-SHA256 com house edge de 4%. Determinístico e verificável.
- Wallet (entidade): saldo em centavos inteiros (
BigInt). Nunca usa ponto flutuante. Operações de crédito e débito com proteção de saldo negativo.
Os serviços nunca se chamam via REST. Toda comunicação é assíncrona:
gamespublicawallet.debitao receber uma apostawalletsprocessa o débito e, em caso de falha, publicawallet.debit.failedgamesconsome a falha e cancela a aposta via WebSocket (bet:cancelled)gamespublicawallet.creditao registrar um cashoutwalletsprocessa o crédito e atualiza o saldo
Essa estratégia de compensação garante que nenhum débito fique órfão e que o jogador seja notificado em tempo real sobre o cancelamento.
O crash point de cada rodada é pré-determinado e verificável:
- Server Seed: 32 bytes aleatórios gerados no início da rodada
- Seed Hash (SHA256): publicado via WebSocket antes das apostas — prova que o resultado já existia
- Crash Point: derivado do seed via HMAC-SHA256 com house edge de 4%
- Verificação: após o crash, o seed é revelado e qualquer jogador pode recalcular o resultado no endpoint
GET /games/rounds/:id/verify
O WebSocket é usado exclusivamente para push do servidor ao cliente. Ações do jogador (apostar, cashout) passam sempre pelo REST — isso simplifica autenticação, validação e rate limiting via Kong.
Eventos emitidos pelo servidor:
| Evento | Payload | Quando |
|---|---|---|
round:betting_start |
{ roundId, seedHash, bettingEndsAt } |
Início da fase de apostas |
round:started |
{ roundId } |
Rodada ativa |
multiplier:tick |
{ multiplier, elapsed } |
A cada ~100ms durante rodada ativa |
round:crashed |
{ roundId, crashPoint, serverSeed } |
Crash com seed revelado |
bet:placed |
{ roundId, betId, playerId, username, amountCents } |
Nova aposta |
bet:cashout |
{ betId, multiplier, payoutCents } |
Cashout registrado |
bet:cancelled |
{ betId, reason } |
Compensação por débito falho |
Todo valor monetário é armazenado em centavos inteiros (BigInt no TypeScript, BIGINT no PostgreSQL). Nenhuma operação usa ponto flutuante para dinheiro. O multiplicador do crash point (não monetário) usa Float.
O endpoint POST /games/bet/cashout usa o multiplicador atual calculado pelo GameLoopService no servidor — o cliente não envia o valor. Isso previne qualquer manipulação do multiplicador pelo cliente.
| Decisão | Vantagem | Custo |
|---|---|---|
| Monorepo com Bun | Velocidade de build/install, compartilhamento de tipos via packages/ |
Algumas libs com compatibilidade parcial no runtime Bun |
| DDD com camadas explícitas | Domínio testável em isolamento, regras de negócio sem dependências de framework | Verbosidade, mappers entre camadas |
| Compensação via RabbitMQ | Consistência eventual sem acoplamento síncrono | Janela de inconsistência entre aposta criada e débito confirmado |
| Crash point pré-determinado | Confiança do jogador (provably fair), imutável após apostas | Casa não pode ajustar vantagem dinamicamente |
| REST para ações + WS para push | Auth/validação/rate limiting centralizados no Kong | Latência de uma chamada HTTP para apostar/cashout |
| Next.js como framework frontend | Familiaridade com a stack — utilizo em projetos pessoais e institucionais, o que acelerou o desenvolvimento sem sacrificar qualidade | Over-engineered para este caso: SSR, App Router e RSC são completamente desnecessários num jogo 100% client-side |
# Unitários — backend
cd services/games && bun test tests/unit
cd services/wallets && bun test tests/unit
# E2E (requer infra rodando)
cd services/games && bun test tests/e2e
cd services/wallets && bun test tests/e2e
# Unitários — frontend
cd frontend && bun test
# Todos os testes
bun test