A real-time multiplayer social deduction game built with WebSocket-based networking, client-side prediction, and smooth interpolation.
Chain of Lies is a multiplayer game where players spawn in a spaceship-like map (inspired by Among Us), complete tasks, and try to identify the impostor among them. The game features:
- Real-time multiplayer with WebSocket communication
- Smooth movement with client-side prediction and interpolation
- Task system with 8 interactive mini-games
- Party/lobby system for creating and joining games
- Spaceship map with 13 rooms connected by corridors
- React 18 - UI framework
- Vite - Build tool & dev server
- TailwindCSS - Utility-first CSS
- shadcn/ui - Accessible component library
- Wouter - Lightweight routing
- TanStack Query - Server state management
- Zustand - Client state management
- Socket.IO Client - Real-time communication
- Express 5 - HTTP server
- Socket.IO - WebSocket server
- Domain-Driven Design - Architecture pattern
- TypeScript - Type safety
- Turborepo - Monorepo build system
- pnpm Workspaces - Package management
- TypeScript 5 - Type safety
- tsup - Package bundler
Chain-of-Lies/
โโโ apps/
โ โโโ web/ # React frontend
โ โ โโโ src/
โ โ โ โโโ components/ # UI components
โ โ โ โโโ hooks/ # Custom React hooks
โ โ โ โ โโโ useGameSocket.ts # Socket event handlers
โ โ โ โ โโโ useLobbySocket.ts # Lobby/party events
โ โ โ โโโ pages/ # Page components
โ โ โ โ โโโ LobbyPage.tsx # Main lobby
โ โ โ โ โโโ PartyRoom.tsx # Waiting room
โ โ โ โ โโโ MultiplayerGame.tsx # Game canvas
โ โ โ โโโ stores/ # Zustand stores
โ โ โ โ โโโ useGameStore.ts # Global game state
โ โ โ โโโ lib/ # Utilities
โ โ โโโ game/ # Game engine
โ โ โโโ MultiplayerGameCanvas.tsx # Main game renderer
โ โ โโโ map.ts # Map data (rooms, walls, spawn points)
โ โ โโโ useKeyboard.ts # Input handling
โ โ โโโ useGameLoop.ts # 60fps game loop
โ โ โโโ tasks/ # 8 interactive mini-games
โ โ
โ โโโ api/ # Express backend
โ โโโ src/
โ โโโ infrastructure/
โ โโโ websocket/
โ โโโ socketio-server.ts # Socket.IO handlers
โ
โโโ packages/
โโโ types/ # Shared TypeScript types
โโโ shared/ # Shared utilities
- Create Party: Host creates a party with a unique 6-character code
- Join Party: Players join using the party code
- Waiting Room: Pre-game lobby where players gather
- Host Controls: Only the host can start the game
- Dynamic Join: Players who rejoin after disconnect automatically enter active games
- Players are assigned:
- Unique Socket ID
- Random color from a predefined palette
- Host status (first player becomes host)
- Spawn position at
(278, 264)in the cafeteria
Local Input โ Movement Calculation โ Collision Check โ Update State โ Emit to Server
โ
Interpolate Remote Players
โ
Render
- Client-Side Prediction: Local player moves instantly (no lag)
- Position Interpolation: Remote players smoothly interpolate between updates
- Throttled Network Updates:
- Client emits at max 20fps (50ms throttle)
- Server broadcasts at max 30fps (33ms throttle)
- Smooth Speed: Player speed set to 0.7 pixels/frame for controlled movement
- Collision Detection: Inverse collision system (can only walk on walkable areas)
// Client: Throttle emissions
if (now - lastEmitTime < 50ms) return; // 20fps
// Server: Throttle broadcasts
if (now - lastBroadcast < 33ms) return; // 30fps
// Client: Interpolate remote players
renderX += (targetX - currentX) * 0.15; // Smooth interpolation- Dimensions: 2800x1800 pixels
- 13 Rooms:
- Cafeteria (spawn area)
- Weapons, Navigation, Shields, O2
- Admin, Storage, Electrical
- Lower Engine, Security, Reactor
- Upper Engine, Medbay
- Corridors: Connect rooms
- Walls: Interior obstacles for tactical gameplay
- Task Zones: 13 interactive zones (60x60px yellow squares)
- All players spawn in the Cafeteria center at
(278, 264) - Additional spawn points distributed around
(278, 264)for variety - Prevents spawn camping and ensures fair starts
- Follow Mode: Camera follows local player
- Canvas Size: 800x500 viewport
- Smooth Tracking: Camera stays centered on player
- Boundary Clamping: Camera never shows out-of-bounds areas
8 interactive mini-games built as React components with canvas rendering:
- Broken Sequence (Cafeteria) - Memory puzzle
- Block Bounce (Weapons) - Pong-like game
- Gas Fee Runner (Navigation) - Endless runner
- Memory Miner (Shields) - Mining clicker
- Block Catcher (O2) - Catching game
- Smart Contract Quick Fix (Admin) - Code puzzle
- Colour Prediction Spinner (Storage) - Reaction game
- Color Spin (Electrical) - Color matching
Tasks are triggered by:
- Standing in a task zone
- Pressing
Ekey - Tasks overlay the game canvas
Escapecloses any task
Movement:
- W/โ - Move up
- A/โ - Move left
- S/โ - Move down
- D/โ - Move right
Interaction:
- E - Interact with task zone
- Escape - Close task overlay// Inverse collision: player can ONLY walk on defined areas
isWalkable = isInRoom || isInCorridor || isInTaskZone
canMove = isWalkable && !hitExplicitWall- Node.js 20+
- pnpm 9+
- Install dependencies
pnpm install- Build shared packages
pnpm --filter @tamper-hunt/types build
pnpm --filter @tamper-hunt/shared build- Start development servers
# Start both frontend and backend
pnpm dev
# Or start individually:
pnpm --filter @tamper-hunt/api dev # Backend on :5000
pnpm --filter @tamper-hunt/web dev # Frontend on :3000- Play the game
- Open http://localhost:3000
- Create a party or join with a code
- Wait for host to start game
- Use WASD/Arrow keys to move
- Press E at yellow task zones
useGameStore {
connected: boolean // Socket connection status
party: Party | null // Current party info
partyCode: string | null // Party join code
players: Record<string, Player> // All players in game
localPlayerId: string | null // Current player's ID
phase: GamePhase // LOBBY | PARTY | GAME | ENDED
}Player {
id: string // Socket ID
name: string // Player name
x, y: number // Current position
targetX, targetY?: number // Interpolation target
color: string // Visual identifier
isHost: boolean // Can start game
timestamp?: number // Last update time
}Party {
id: string // Unique party ID
partyCode: string // 6-char join code
hostId: string // Host socket ID
hostName: string // Host display name
maxPlayers: number // Player limit (8)
players: Record<string, Player> // Party members
phase: "LOBBY" | "GAME" // Current state
}"create_party" { name: string }
"join_party" { partyCode: string, name: string }
"start_game" {}
"player_move" { x: number, y: number }
"leave_game" {}"party_joined" { party, players, localPlayerId }
"party_player_update" { players }
"game_started" {}
"players_update" { players }
"error" { message: string }Without interpolation, remote players appear to teleport between positions due to network latency and update throttling.
// Server sends updates at 30fps
// Client renders at 60fps
// Client smoothly interpolates between server updates
// When server update arrives:
player.targetX = newX
player.targetY = newY
// Each frame:
renderX = currentX + (targetX - currentX) * 0.15
renderY = currentY + (targetY - currentY) * 0.15This creates smooth 60fps movement even with 30fps network updates.
Instead of defining what you CAN'T walk through, we define what you CAN walk on:
walkableAreas = rooms + corridors + taskZones
blockedAreas = walls (explicit obstacles)
canMove = isInWalkableArea && !hitWallBenefits:
- Everything outside rooms is automatically impassable
- Easy to add new walkable areas
- No need to define every wall in empty space
-
Network Throttling
- Client: 50ms between emits (20fps)
- Server: 33ms between broadcasts (30fps)
- Reduces bandwidth by 50% vs 60fps updates
-
State Diffing
- Only remote players interpolate
- Local player updates instantly
- Server only broadcasts changes
-
Canvas Rendering
- 60fps game loop via requestAnimationFrame
- Camera culling (only render visible area)
- Minimal re-renders (React state separation)
1. LOBBY Phase
โโ Player opens game
โโ Creates party (becomes host) OR joins party with code
โโ Enters waiting room
2. PARTY Phase (Waiting Room)
โโ Players see party code
โโ Players see who's in the lobby
โโ Host clicks "Start Game"
โโ All players transition to GAME phase
3. GAME Phase
โโ All players spawn at (278, 264) in cafeteria
โโ Players move around map using WASD/Arrows
โโ Players complete tasks by pressing E at zones
โโ (Future: voting, impostor mechanics)
4. ENDED Phase (Future)
โโ Game over, show results
- No Reconnection Handling: If a player disconnects, they lose their state
- No Role Assignment: All players are equal (no impostor yet)
- No Voting System: No elimination mechanics
- Task Completion: Tasks don't track completion state
- No Round System: Game runs indefinitely
- Memory Storage: All data lost on server restart (no database)
- Add role assignment (Validator, Tamperer, etc.)
- Implement voting system
- Add task progress tracking
- Implement round system with win conditions
- Add chat system
- Persistent storage (Redis/PostgreSQL)
- Reconnection with state restoration
- Mobile support (touch controls)
- Audio and visual effects
- โ Input validation with Zod schemas
- โ Party code collision prevention
- โ Host verification for game start
โ ๏ธ Rate limiting on socket eventsโ ๏ธ Authentication systemโ ๏ธ Anti-cheat measures (movement validation)โ ๏ธ Party password protection option
[Frontend] โโ WebSocket โโ [Backend]
(In-memory)
Limitations:
- Single server
- In-memory storage (lost on restart)
- No horizontal scaling
- Max ~100 concurrent players
[Frontend] โโ WebSocket โโ [Backend] โโโ [PostgreSQL]
(Stateful)
[Load Balancer]
โ
[API-1] [API-2] [API-3]
โ
[Redis Pub/Sub]
โ
[PostgreSQL]
- Create party and get party code
- Join party from different browser/incognito
- Host starts game - both players see game canvas
- Both players can move independently
- Movement appears smooth on both screens
- Player leaves and rejoins - automatically enters active game
- Press E at task zone - overlay appears
- Complete task - overlay closes
- Multiple players move simultaneously
# Test with multiple browsers
1. Chrome: http://localhost:3000
2. Firefox: http://localhost:3000
3. Incognito: http://localhost:3000
# Check network traffic
- Open DevTools โ Network โ WS tab
- Watch Socket.IO messages
# Check server logs
- Backend terminal shows all socket events
- Look for "Player joined party", "player_move", etc.- Create feature branch
- Make changes
- Test locally with multiple clients
- Check for linter errors:
pnpm lint - Check types:
pnpm typecheck - Submit PR
- Use TypeScript for type safety
- Follow existing patterns (DDD in backend)
- Add JSDoc comments for complex logic
- Keep functions small and focused
- Use meaningful variable names
- Built-in reconnection logic
- Room/namespace support
- Event-based API (cleaner than message strings)
- Fallback transports (long-polling)
- Simpler API (less boilerplate)
- Better TypeScript support
- No Provider wrapper needed
- Hooks-first design
- Each feature is self-contained
- Easy to find related code
- Scales to large teams
- Clear boundaries
- Share types between frontend/backend
- Atomic changes across packages
- Single install/build process
- Easier refactoring
ARCHITECTURE_DIAGRAM.md- Visual system diagramsMULTIPLAYER_GAME_SYSTEM.md- Multiplayer implementation detailsMULTIPLAYER_SYSTEM.md- Socket architectureBACKEND_INTEGRATION_GUIDE.md- API integration guideVISUAL_GUIDE.md- Map layout and coordinates
MIT
Status: MVP Complete - Core multiplayer movement and tasks working Next Steps: Role assignment, voting system, win conditions
Built with โค๏ธ for fun and learning