Motor de jogo em tempo real para o Location404 - sistema de matchmaking, gerenciamento de partidas e rounds para jogo de adivinhação geográfica competitivo multiplayer.
- Sobre o Projeto
- Funcionalidades
- Arquitetura
- Tecnologias
- Pré-requisitos
- Instalação
- Configuração
- Como Usar
- API SignalR
- Estrutura do Projeto
- Testes
- Observabilidade
- Licença
O Location404 Game Engine é o serviço responsável por toda a lógica de jogo em tempo real do Location404. Utilizando SignalR para comunicação bidirecional, o serviço gerencia:
- Matchmaking: Sistema de fila para encontrar oponentes
- Game Matches: Partidas 1v1 com 3 rounds cada
- Game Rounds: Rodadas individuais com locations aleatórias
- Scoring System: Cálculo de pontos baseado em distância (fórmula exponencial)
- Real-time Events: Notificações instantâneas via WebSockets
- Jogador entra na fila → Sistema busca oponente disponível
- Match criado → 2 jogadores são pareados automaticamente
- Round iniciado → Location aleatória é selecionada da API de dados
- Jogadores fazem palpites → Coordenadas são enviadas via SignalR
- Round termina → Pontos calculados com base na distância do erro
- 3 rounds completados → Match termina, evento publicado no RabbitMQ
- ✅ Fila de espera com timestamp (FIFO)
- ✅ Pareamento automático de 2 jogadores
- ✅ Cleanup de matches abandonadas
- ✅ Suporte a reconexão
- ✅ Matches 1v1 com 3 rounds obrigatórios
- ✅ Estado persistido em Redis (cache distribuído)
- ✅ Sistema de pontuação exponencial baseado em distância
- ✅ Detecção de empates
- ✅ Timeout automático de 2 horas
- ✅ Seleção aleatória de locations via API externa
- ✅ Validação de coordenadas (lat: -90 a 90, lng: -180 a 180)
- ✅ Cálculo geodésico de distância (Haversine)
- ✅ Parâmetros de StreetView (heading, pitch)
- ✅
MatchFound- Match criado com sucesso - ✅
RoundStarted- Novo round iniciado - ✅
GuessSubmitted- Palpite confirmado - ✅
RoundEnded- Round finalizado com resultados - ✅
MatchEnded- Partida completa com vencedor
O projeto segue Clean Architecture com separação clara de responsabilidades:
┌─────────────────────────────────────────────────────────────┐
│ API Layer (SignalR) │
│ ┌─────────────┐ ┌──────────────┐ ┌──────────────────┐ │
│ │ GameHub │ │ Health │ │ Middlewares │ │
│ │ (SignalR) │ │ Checks │ │ (CORS, Auth) │ │
│ └─────────────┘ └──────────────┘ └──────────────────┘ │
└─────────────────────────────────────────────────────────────┘
│
┌─────────────────────────────────────────────────────────────┐
│ Application Layer │
│ ┌──────────────────┐ ┌──────────────────────────────┐ │
│ │ Command Handlers │ │ Interfaces (Contracts) │ │
│ │ - JoinMatchmaking│ │ - IMatchmakingService │ │
│ │ - StartRound │ │ - IGameMatchManager │ │
│ │ - SubmitGuess │ │ - IPlayerConnectionManager │ │
│ └──────────────────┘ └──────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
│
┌─────────────────────────────────────────────────────────────┐
│ Domain Layer │
│ ┌──────────────┐ ┌──────────────┐ ┌─────────────────┐ │
│ │ Entities │ │ Value Objects│ │ Domain Events │ │
│ │ - GameMatch │ │ - Coordinate│ │ - RoundEnded │ │
│ │ - GameRound │ │ - Location │ │ - MatchEnded │ │
│ └──────────────┘ └──────────────┘ └─────────────────┘ │
└─────────────────────────────────────────────────────────────┘
│
┌─────────────────────────────────────────────────────────────┐
│ Infrastructure Layer │
│ ┌─────────┐ ┌──────────┐ ┌──────────────┐ ┌─────────┐ │
│ │ Redis │ │ RabbitMQ │ │ HTTP Client │ │ DI │ │
│ │ (State) │ │(Messaging│ │(Location API)│ │ Setup │ │
│ └─────────┘ └──────────┘ └──────────────┘ └─────────┘ │
└─────────────────────────────────────────────────────────────┘
... TODO: ADICIONAR
- .NET 9.0 - Framework principal
- ASP.NET Core SignalR - Comunicação real-time WebSocket
- LiteBus - CQRS pattern (Command Handlers)
- StackExchange.Redis - State management distribuído
- Redis/Dragonfly - Cache de estado de jogo e matchmaking
- RabbitMQ - Event-driven messaging (match.ended, round.ended)
- OpenTelemetry - Distributed tracing
- Shared.Observability - Pacote NuGet customizado
- Prometheus - Métricas
- Grafana Loki - Logs estruturados
- xUnit - Framework de testes
- FluentAssertions - Assertions expressivas
- Moq - Mocking
- Testcontainers - Integration tests com Redis
- .NET 9 SDK
- Redis ou Dragonfly (porta 6379)
- RabbitMQ (porta 5672)
- location404-data rodando (porta 5000)
Opcional:
- Docker - Para rodar dependências via containers
git clone https://github.com/Location404/location404-game.git
cd location404-gamedotnet restoredotnet buildEdite src/Location404.Game.API/appsettings.json ou use variáveis de ambiente (recomendado para produção):
{
"Redis": {
"Enabled": true,
"ConnectionString": "localhost:6379",
"InstanceName": "GameCoreEngine:",
"DefaultExpiration": "02:00:00"
},
"RabbitMQ": {
"Enabled": true,
"HostName": "localhost",
"Port": 5672,
"UserName": "admin",
"Password": "your_password_here",
"VirtualHost": "/",
"ExchangeName": "game-events",
"MatchEndedQueue": "match-ended",
"RoundEndedQueue": "round-ended"
},
"Location404Data": {
"BaseUrl": "http://localhost:5000",
"TimeoutSeconds": 10
},
"Cors": {
"AllowedOrigins": [
"http://localhost:5173",
"http://localhost:4200"
]
},
"JwtSettings": {
"Issuer": "location404",
"Audience": "location404",
"SigningKey": "your-secret-key-min-32-chars-here",
"AccessTokenMinutes": 60
}
}# Redis
Redis__Enabled=true
Redis__ConnectionString=redis:6379
# RabbitMQ
RabbitMQ__Enabled=true
RabbitMQ__HostName=rabbitmq
RabbitMQ__Password=secure_password
# External API
Location404Data__BaseUrl=http://location404-data:5000
# JWT
JwtSettings__SigningKey=your-super-secret-signing-key-here
# CORS
Cors__AllowedOrigins__0=https://location404.com# 1. Inicie o Redis (ou use Docker)
docker run -d -p 6379:6379 redis:latest
# 2. Inicie o RabbitMQ (opcional)
docker run -d -p 5672:5672 -p 15672:15672 rabbitmq:3-management
# 3. Certifique-se que location404-data está rodando
# Veja: https://github.com/Location404/location404-data
# 4. Execute o Game Engine
cd src/Location404.Game.API
dotnet runA API estará disponível em:
- SignalR Hub:
http://localhost:5170/gamehub - Health Check:
http://localhost:5170/health - Metrics:
http://localhost:5170/metrics
cd location404-utils/deploy/dev
docker-compose up -dimport * as signalR from '@microsoft/signalr';
const connection = new signalR.HubConnectionBuilder()
.withUrl('http://localhost:5170/gamehub', {
accessTokenFactory: () => yourJwtToken
})
.withAutomaticReconnect()
.build();
await connection.start();Entra na fila de matchmaking.
Request:
const request = { playerId: 'guid-here' };
const result = await connection.invoke('JoinMatchmaking', request);
// Returns: "Added to queue..." ou "Match found!"Events:
MatchFound- Disparado quando match é criado
connection.on('MatchFound', (data) => {
console.log('Match ID:', data.matchId);
console.log('Opponent:', data.playerBId);
console.log('Start Time:', data.startTime);
});Sai da fila de matchmaking.
await connection.invoke('LeaveMatchmaking', playerId);Events:
LeftQueue- Confirmação de saída
Inicia um novo round (qualquer jogador pode chamar).
Request:
const request = { matchId: 'guid-here' };
await connection.invoke('StartRound', request);Events:
RoundStarted- Round iniciado com location data
connection.on('RoundStarted', (data) => {
console.log('Round Number:', data.roundNumber);
console.log('Location:', {
lat: data.location.x,
lng: data.location.y,
heading: data.location.heading,
pitch: data.location.pitch
});
console.log('Duration:', data.durationSeconds, 'seconds');
});Envia palpite de coordenadas.
Request:
const request = {
matchId: 'guid-here',
playerId: 'guid-here',
guessX: -23.5505, // Latitude
guessY: -46.6333 // Longitude
};
await connection.invoke('SubmitGuess', request);Events:
GuessSubmitted- Confirmação de palpiteRoundEnded- Disparado quando ambos jogadores enviaram palpites
connection.on('RoundEnded', (data) => {
console.log('Correct Answer:', {
lat: data.correctAnswer.x,
lng: data.correctAnswer.y
});
console.log('Player A:', {
guess: data.playerAGuess,
distance: data.playerADistance + ' km',
points: data.playerAPoints
});
console.log('Player B:', {
guess: data.playerBGuess,
distance: data.playerBDistance + ' km',
points: data.playerBPoints
});
});MatchEnded- Disparado após 3 rounds completos
connection.on('MatchEnded', (data) => {
console.log('Winner:', data.winnerId);
console.log('Loser:', data.loserId);
console.log('Final Score:', {
playerA: data.playerATotalPoints,
playerB: data.playerBTotalPoints
});
console.log('Points Earned:', data.pointsEarned);
console.log('Points Lost:', data.pointsLost);
console.log('All Rounds:', data.rounds);
});points = 5000 × e^(-distance_km / 2000)
Exemplos:
- 0 km (perfeito): 5000 pontos
- 100 km: 4756 pontos
- 500 km: 3894 pontos
- 1000 km: 3033 pontos
- 2000 km: 1839 pontos
- 5000 km: 410 pontos
Distância calculada com Haversine formula (geodésica).
location404-game/
├── src/
│ ├── Location404.Game.API/ # API Layer (SignalR Hub)
│ │ ├── Hubs/
│ │ │ └── GameHub.cs # SignalR Hub principal
│ │ ├── Middlewares/
│ │ │ └── ExceptionHandlingMiddleware.cs
│ │ ├── Program.cs # Entry point + DI setup
│ │ └── appsettings.json
│ │
│ ├── Location404.Game.Application/ # Application Layer (CQRS)
│ │ ├── Features/
│ │ │ ├── Matchmaking/
│ │ │ │ ├── Commands/
│ │ │ │ │ └── JoinMatchmakingCommand/
│ │ │ │ └── Interfaces/
│ │ │ │ ├── IMatchmakingService.cs
│ │ │ │ └── IPlayerConnectionManager.cs
│ │ │ │
│ │ │ └── GameRounds/
│ │ │ ├── Commands/
│ │ │ │ ├── StartRoundCommand/
│ │ │ │ └── SubmitGuessCommand/
│ │ │ ├── Interfaces/
│ │ │ │ └── IGameMatchManager.cs
│ │ │ └── RoundEndedResponse.cs
│ │ │
│ │ ├── Common/
│ │ │ ├── Result/ # Result pattern (success/failure)
│ │ │ └── Interfaces/
│ │ │ └── ILocationService.cs
│ │ │
│ │ └── Events/
│ │ ├── MatchEndedEvent.cs
│ │ └── RoundEndedEvent.cs
│ │
│ ├── Location404.Game.Domain/ # Domain Layer (Entities)
│ │ ├── Entities/
│ │ │ ├── GameMatch.cs # Aggregate root (Match)
│ │ │ └── GameRound.cs # Round entity
│ │ │
│ │ └── ValueObjects/
│ │ ├── Coordinate.cs # Lat/Lng value object
│ │ └── Location.cs # StreetView location
│ │
│ └── Location404.Game.Infrastructure/ # Infrastructure (Redis, RabbitMQ, HTTP)
│ ├── Persistence/
│ │ ├── GameMatchManager.cs # Redis-based match storage
│ │ ├── MatchmakingService.cs # Redis queue implementation
│ │ └── PlayerConnectionManager.cs # SignalR connection mapping
│ │
│ ├── Messaging/
│ │ └── RabbitMQPublisher.cs # RabbitMQ event publisher
│ │
│ ├── ExternalServices/
│ │ └── LocationService.cs # HTTP client for location404-data
│ │
│ └── DependencyInjection.cs
│
├── tests/
│ ├── Location404.Game.Application.UnitTests/
│ │ ├── Commands/
│ │ │ ├── JoinMatchmakingCommandTests.cs
│ │ │ ├── StartRoundCommandTests.cs
│ │ │ └── SubmitGuessCommandTests.cs
│ │ └── Services/
│ │
│ └── Location404.Game.Infrastructure.IntegrationTests/
│ ├── Persistence/
│ │ ├── GameMatchManagerTests.cs
│ │ └── MatchmakingServiceTests.cs
│ └── TestContainersFixture.cs # Redis container setup
│
├── Location404.Game.sln
├── README.md
└── .gitignore
dotnet testdotnet test tests/Location404.Game.Application.UnitTestsdotnet test tests/Location404.Game.Infrastructure.IntegrationTestsNota: Testes de integração requerem Docker rodando (usa Testcontainers para Redis).
dotnet test --collect:"XPlat Code Coverage"
reportgenerator -reports:"**/coverage.cobertura.xml" -targetdir:"TestResults/Report"Abra TestResults/Report/index.html no navegador.
Endpoint: http://localhost:5170/metrics
Métricas customizadas:
game_matchmaking_queue_size- Tamanho da fila de matchmakinggame_active_matches_total- Número de matches ativasgame_rounds_started_total- Total de rounds iniciadosgame_guesses_submitted_total- Total de palpites enviadosgame_matches_completed_total- Total de matches completadas
Configurado para exportar para coletor OTLP:
- Endpoint:
http://181.215.135.221:4317 - Sampling: 10% em produção, 100% em desenvolvimento
Traces automáticos:
- SignalR method calls
- HTTP requests (outbound)
- Redis operations
- RabbitMQ publishes
Logs estruturados exportados para Grafana Loki:
- Formato: JSON
- Trace correlation:
trace_id,span_id - Enriched com properties:
player_id,match_id,round_id
# Health geral
curl http://localhost:5170/health
# Readiness (dependências prontas?)
curl http://localhost:5170/health/ready
# Liveness (processo vivo?)
curl http://localhost:5170/health/liveDependências verificadas:
- Redis (timeout: 5s)
- RabbitMQ (timeout: 5s)
- location404-data API (timeout: 10s)
Este projeto está sob a licença MIT. Veja o arquivo LICENSE para mais detalhes.
- location404-web - Frontend Vue.js
- location404-auth - Serviço de autenticação
- location404-data - API de dados e estatísticas
- shared-observability - Pacote de observabilidade
- Issues: GitHub Issues
- Discussões: GitHub Discussions
Desenvolvido por ryanbromati