♟ Quoridor — Wall Duel
A real-time multiplayer strategy game with a trash-talking AI, live chat, and a leaderboard. No downloads. Just open and play.
🎮 Try it live → quoridor-lmza.onrender.com
Inspiration
A while back I built a basic version of Quoridor — nothing fancy, just a working board with pawns and walls. It sat in my files for a long time untouched. Recently I picked it up again and thought it would be fun to see how far I could push it with AI assistance. What started as "let me clean this up a bit" turned into a full rebuild with multiplayer, a trash-talking AI opponent, real-time chat, and a leaderboard.
Vibe coding at its finest.
What It Does
It's a browser-based Quoridor game where you race your pawn to the other side of the board while placing walls to block your opponent. You can invite a friend with a shareable code and play together online, or go up against an AI that genuinely talks back.
- Play vs a Friend — create a game, share the code, play from anywhere
- Play vs AI — three difficulty levels with a personality to match
- Trash-talking AI — drops roasts in the chat when it wins, taunts you mid-game when it places walls
- Live chat — talk during the match, spectators included
- Emoji reactions — float across the screen visible to everyone
- 30s turn timer — run out of time and your opponent wins automatically
- Spectator mode — anyone with the link can watch live
- Accounts + leaderboard — register, track wins, losses, and win streaks
🎮 Live Demo
🔗 https://quoridor-lmza.onrender.com/
First load may take ~30 seconds — free tier on Render sleeps when idle.
How We Built It
This started as a forgotten side project and got rebuilt from the ground up with AI assistance. The core idea was to keep the stack lean — no unnecessary frameworks, just the right tools for the job.
Tech Stack
| Layer | Tech |
|---|---|
| Backend | Python · FastAPI · WebSockets |
| Frontend | Vanilla JavaScript · HTML5 Canvas |
| Database | SQLite (local) · PostgreSQL via Supabase (prod) |
| Auth | Custom JWT — no third-party dependencies |
| AI Engine | A* pathfinding + BFS reachability |
| Deployment | Render — auto-deploy from GitHub |
Architecture
Every game action — moves, walls, chat, reactions, timer events — flows through one persistent WebSocket connection per player. The server holds authoritative game state and broadcasts updates to all clients instantly.
# Every game event goes through one WS handler
async def ws_endpoint(websocket: WebSocket, game_id: str):
while True:
data = await websocket.receive_json()
# handle move / wall / chat / reaction / rematch
await _broadcast(game_id, _envelope(game_id))
The AI engine uses A* pathfinding to find its shortest route to the goal row. Before placing any wall it runs a BFS reachability check — so it can never trap itself or create an illegal board state.
def _wall_ok(s, idx, r, c, ori):
# Temporarily place wall, verify both players still have a path
ws = s["h_walls"] if ori == "h" else s["v_walls"]
ws.add((r, c))
ok = _astar(s, 0) < 999 and _astar(s, 1) < 999
ws.discard((r, c))
return ok
Project Structure
quoridor/
├── main.py # FastAPI server, WebSocket handlers
├── game.py # Core game logic and rules
├── ai.py # AI engine (A* pathfinding, wall strategy)
├── auth.py # Auth system, JWT, PostgreSQL + SQLite
├── run.py # Local development launcher
├── requirement.txt # Python dependencies
└── static/
├── index.html # Game UI
├── script.js # Frontend logic, canvas rendering
└── admin.html # Admin dashboard
Admin Panel
The game ships with a built-in admin dashboard at /admin. It's protected behind credentials and gives full visibility into everything happening on the server.
What you can do from the admin panel:
- Monitor live games — see every active game, the players, move count, and current status in real time
- User management — view all registered accounts, edit wins/losses, rename usernames, or delete accounts entirely
- Server stats — total users, total games played, active games right now, online player count
- Database access — all data is stored in PostgreSQL via Supabase in production, fully visible and editable from the Supabase table editor too
The admin panel talks to a set of protected REST endpoints — all requiring a Bearer token — so the data is never exposed to regular players.
GET /api/admin/stats # server overview
GET /api/admin/users # all users + stats
DELETE /api/admin/users/{id} # remove a user
PUT /api/admin/users/{id}/stats # edit wins / losses
PUT /api/admin/users/{id}/rename # rename username
GET /api/admin/games # all active games
Challenges We Ran Into
The AI Getting Stuck
Making the AI play smart is one thing — making it not freeze in edge cases was another. When two pawns aren't adjacent, jump logic doesn't apply, and the AI would sometimes find no valid moves. The fix was a fallback in _best_move() that scans all neighbours when the primary logic returns empty.
The Turn Timer
Looked simple, turned into a rabbit hole. Managing asyncio tasks so the timer doesn't double-fire, doesn't linger after the game ends, and correctly hands the win to the opponent on timeout — all without race conditions — took careful handling.
async def _start_timer(game_id: str):
await _cancel_timer(game_id) # always cancel before starting
r["turn_started"] = time.time()
async def _run():
await asyncio.sleep(TURN_SECONDS)
# timeout: opponent wins
g.winner = 1 - g.current_player
await _handle_game_over(game_id)
await _broadcast(game_id, _envelope(game_id))
r["timer_task"] = asyncio.create_task(_run())
Keeping Everyone in Sync
Moves, chat, reactions, and state all broadcasting to players and spectators simultaneously — getting the order right so nothing arrives out of sequence was an ongoing challenge throughout the build.
Accomplishments We're Proud Of
The AI's personality came out better than expected. The roasts land. Losing to it is genuinely funny instead of frustrating — which was the whole point.
Zero installs on the player's side. The entire game runs in the browser from a single link. No app, no setup, no friction.
An old forgotten project turned into something polished. That's honestly the biggest win.
What We Learned
How much you can do when you stop being precious about writing every line yourself. The original project was stuck because we kept hitting walls we didn't know how to get past. Using AI to push through those blockers and iterate fast completely changed how the project felt to work on.
We also learned a lot about:
asynciotask lifecycle — how to cancel, restart, and avoid race conditions- WebSocket state management — keeping multiple clients in sync without a message queue
- AI heuristics — small changes to the scoring function completely change how an opponent feels to play against
What's Next for Quoridor
- 📱 Mobile touch support — right now it's a desktop experience
- 📊 Ranked matchmaking — ELO rating so games feel competitive
- 🎬 Game replay — step through your moves after the match
- 🎭 Custom AI personalities — polite, ruthless, chaotic
- 🏆 Tournament mode — brackets for multiple players
Theme Connection — Overstimulation
Quoridor isn't just a strategy game. It's controlled overstimulation by design.
Every turn you're simultaneously tracking your path, your opponent's path, wall placement options, the live chat, the AI's taunts, and a 30-second timer draining in the corner. Multiple streams of information competing for your attention at once. The board doesn't let you breathe — and that's the point.
Overstimulation isn't always negative. Sometimes it's exactly what makes something exciting.
Built with Python · FastAPI · Vanilla JS · A lot of AI assistance · and one very old forgotten project.
Built With
- claude
- javascript
- python


Log in or sign up for Devpost to join the conversation.