A real-time multiplayer web game where players guess which friend has listened to randomly selected tracks the most, based on their Spotify listening history.
- Authenticate with Spotify
- Join one of 3 persistent rooms (max 10 players per room)
- Listen to 30-second track previews
- Guess which player has that track ranked highest in their top 50
- Earn points for correct guesses (10 pts + 5 pts speed bonus)
- Compete across 10 rounds to become the champion
- Spotify OAuth2 Integration: Securely authenticate and fetch your top 50 tracks
- Real-time Multiplayer: WebSocket-powered live game updates
- 3 Persistent Rooms: Always-available game rooms (Room 1, Room 2, Room 3)
- Capacity Management: Max 10 players per room (30 concurrent players total)
- Audio Previews: 30-second Spotify track snippets for each round (scraped from embeds)
- Dynamic Scoring: 10 base points + 5 bonus for fastest correct guess
- Persistent Rankings: Tracks final standings and winner announcement
- Optimized Architecture: Persistent rooms with no cleanup overhead for efficient hosting
- Smart Tie-Breaking: Dense ranking system ensures fair placement for tied scores
- Live Leaderboard: Real-time sorting of players by score during the game
- Go 1.25+ with Gin web framework
- WebSocket (coder/websocket) for real-time communication
- Spotify API (zmb3/spotify/v2) with OAuth2 authentication
- Web Scraping for preview URLs (colly framework)
- React 18 with TypeScript
- Vite build tool with HMR
- Tailwind CSS for styling
- WebSocket client for game updates
- Docker multi-stage builds
- Cloud-ready (optimized for low-memory deployments)
roulettify/
โโโ cmd/api/main.go # Entry point
โโโ internal/
โ โโโ server/
โ โ โโโ server.go # Server initialization
โ โ โโโ routes.go # HTTP/WebSocket routes
โ โโโ auth/
โ โ โโโ spotify.go # Spotify OAuth & API
โ โ โโโ scraper.go # Preview URL scraping
โ โโโ game/
โ โโโ room.go # Game room logic
โ โโโ models.go # Data structures
โ โโโ manager.go # 3 persistent rooms
โ โโโ *_test.go # Test files
โโโ frontend/
โ โโโ src/
โ โ โโโ App.tsx # Main component
โ โ โโโ components/
โ โ โ โโโ Lobby.tsx # Room selection UI
โ โ โ โโโ GameRoom.tsx # Game interface
โ โ โโโ main.tsx # React entry point
โ โโโ package.json
โโโ Dockerfile # Multi-stage build
โโโ docker-compose.yml
โโโ Makefile
โโโ .env
| Method | Endpoint | Purpose |
|---|---|---|
| GET | / |
Health check / SPA Entry |
| GET | /health |
Detailed metrics (uptime, room stats) |
| GET | /rooms |
List all 3 persistent rooms with player counts |
| GET | /auth/spotify |
Start Spotify OAuth flow |
| GET | /auth/callback |
Spotify OAuth callback handler |
Client โ Server:
{
"type": "join_room",
"payload": {
"room_id": "Room 1",
"player_id": "user123",
"player_name": "John",
"access_token": "spotify_token"
}
}{
"type": "ready",
"payload": {
"player_id": "user123",
"is_ready": true
}
}{
"type": "start_game",
"payload": {
"room_id": "Room 1",
"total_rounds": 10
}
}{
"type": "submit_guess",
"payload": {
"room_id": "Room 1",
"player_id": "user123",
"guessed_player_id": "friend456"
}
}Server โ Client:
{
"type": "player_joined",
"payload": {
"players": [
{ "id": "user123", "name": "John", "score": 0, "is_ready": false, "is_leader": true }
],
"player_count": 1
}
}{
"type": "player_ready",
"payload": {
"player_id": "user123",
"is_ready": true
}
}{
"type": "game_started",
"payload": {
"total_rounds": 10,
"players": [...]
}
}{
"type": "round_started",
"payload": {
"round": 1,
"total_rounds": 10,
"track": {
"id": "123",
"name": "Song Name",
"artists": ["Artist 1"],
"image_url": "...",
"preview_url": "..."
},
"players": [...]
}
}{
"type": "guess_received",
"payload": {
"player_id": "user123",
"guesses_count": 1,
"total_players": 2
}
}{
"type": "round_complete",
"payload": {
"round": 1,
"winner_id": "user123",
"winner_rank": 5,
"correct_guessers": ["user123"],
"points_awarded": {"user123": 15},
"updated_scores": {"user123": 15, "friend456": 0},
"guess_durations": {"user123": 2.5}
}
}{
"type": "game_over",
"payload": {
"winner_id": "user123",
"final_scores": {"user123": 85, "friend456": 60},
"players": [...]
}
}{
"type": "game_reset",
"payload": {
"players": [...]
}
}{
"type": "error",
"payload": {
"message": "Room is full"
}
}| Event | Points |
|---|---|
| Correct guess | +10 |
| Speed bonus (fastest) | +5 |
| Wrong guess | 0 |
Winner Determination: The player whose top 50 contains the track with the lowest rank number (most listened to) wins the round.
# Clone the repository
git clone https://github.com/mardon3/roulettify.git
cd roulettify
# Set up environment variables
cp .env.example .env
# Edit .env with your Spotify credentials
# Run with hot reload (installs air if needed)
make watch
# Or run manually
make run# Build and run with Docker Compose
make docker-run
# Access at http://127.0.0.1:8080# Run all tests
make test
# Run with coverage
make test-coverage
# Test for race conditions
make test-raceCreate a .env file in the root directory:
# Server
PORT=8080
APP_ENV=development
# Frontend
FRONTEND_URL=http://127.0.0.1:5173
# Spotify (Get from https://developer.spotify.com/dashboard)
SPOTIFY_CLIENT_ID=your_client_id_here
SPOTIFY_CLIENT_SECRET=your_client_secret_here
SPOTIFY_REDIRECT_URI=http://127.0.0.1:8080/auth/callback
# CORS
ALLOWED_ORIGINS=http://127.0.0.1:3000,http://127.0.0.1:5173
# Game Settings
DEFAULT_TOTAL_ROUNDS=10
MAX_PLAYERS_PER_ROOM=10- Go to Spotify Developer Dashboard
- Create a new app
- Add redirect URI:
http://127.0.0.1:8080/auth/callback - Copy Client ID and Client Secret to
.env - For production, add your production URL to redirect URIs
- 3 fixed rooms (Room 1, Room 2, Room 3) created at startup
- 10 player capacity per room (30 total concurrent players)
- No dynamic creation/deletion - rooms never shut down
- Memory efficient - optimized for low-memory cloud deployments
- Consistent ordering - rooms always appear in the same order in UI
As of Nov 2024, Spotify no longer provides preview URLs via API for new applications. This project uses web scraping:
- Fetches Spotify embed pages for each track
- Extracts preview URLs using regex patterns
- Implements caching to reduce scraping overhead
- Falls back gracefully when previews unavailable
- Memory: 3 persistent rooms
- Concurrent Users: Up to 30 (10 per room)
- WebSocket Connections: Persistent per player
- Latency: <100ms for game actions
- Deployment: Minimal resource usage