Challenge real AI models to chess. Climb a global leaderboard. Dethrone the Apex.
A fully open-source, browser-based chess game where you don't play "the computer" — you challenge a roster of named AI models sourced from OpenRouter's free tier, each with their own Elo rating, personality, and score multiplier. Built on a modern serverless stack with zero ongoing hosting costs.
- Full chess engine — legal move validation, castling, en passant, pawn promotion, check/checkmate detection
- Alpha-beta minimax AI — pure JS engine with piece-square tables, no WASM, no CDN dependencies
- 10 difficulty tiers — each mapped to a real AI model with increasing Stockfish depth and skill level
- Score multipliers — beating harder opponents earns up to 15× your base points
- Speed & efficiency bonuses — win faster and in fewer moves for extra XP
- Win streak multiplier — consecutive wins stack bonus points up to +1,800 pts
Each opponent is a real, named LLM from OpenRouter's free tier. They taunt you, react to your moves, and give post-game analysis — all powered by the model you're actually playing against.
| Model | Provider | Tier | Elo | Multiplier |
|---|---|---|---|---|
| Gemma 3 4B | Recruit | 680 | 1.0× | |
| LFM 1.2B | LiquidAI | Recruit | 720 | 1.2× |
| Llama 3.2 3B | Meta | Apprentice | 820 | 1.5× |
| Gemma 3 12B | Apprentice | 960 | 2.0× | |
| Nemotron Nano 9B | NVIDIA | Knight | 1,180 | 3.0× |
| Mistral Small 3.1 | Mistral AI | Knight | 1,340 | 4.0× |
| Llama 3.3 70B | Meta | Elite | 1,590 | 6.0× |
| Gemma 3 27B | Elite | 1,680 | 7.0× | |
| Nemotron Super 120B | NVIDIA | Master | 2,090 | 10.0× |
| Qwen3 Next 80B | Alibaba Cloud | Master | 2,210 | 11.0× |
| DeepSeek V3 | DeepSeek | Apex | 2,790 | 15.0× |
All models are free on OpenRouter. No credit card required. Rate limits apply on the free tier.
- 8 player levels — Pawn → Squire → Knight → Bishop → Rook → Grandmaster → Legend → The Immortal
- XP system — every win earns XP toward the next level
- 13 achievements with XP rewards (First Blood, Speed Demon, Hat Trick, Apex Predator, and more)
- Player profile — stats, per-model breakdown, recent game history, achievement showcase
- Persistent progress — saved to
localStoragelocally, synced to Supabase globally
- Player rankings by total score, powered by Supabase Serverless Postgres
- Live AI model rankings — tracks each model's wins/losses against humans across all players
- Gracefully degrades — the game works perfectly without a database; local scores always save
- Auto-creates database tables on first game (or run
supabase-setup.sqlmanually for indexes + seed data)
- Paste your own OpenRouter API key to unlock premium models (GPT-4o, Claude, Gemini, etc.)
- Key stored in
localStorage, passed securely as a header — never hardcoded or exposed in the bundle - Works without a key using free-tier models only
| Layer | Technology | Purpose |
|---|---|---|
| Frontend | React 18 + Vite | UI, game state, routing |
| Chess Logic | chess.js | Move validation, game state |
| Chess AI | Custom minimax engine | Alpha-beta pruning + piece-square tables |
| AI Personality | OpenRouter API | Villain taunts, commentary, post-game analysis |
| Backend | Vercel Functions | Secure OpenRouter proxy, DB operations |
| Database | Supabase Serverless Postgres | Global leaderboard + AI model stats |
| Hosting | Vercel Free Tier | Static site + serverless functions |
- Vercel Free: 125k function invocations/month, 100GB bandwidth
- Supabase Free: 0.5 GB storage, serverless auto-suspend (no idle costs)
- OpenRouter Free: All models used have a
:freevariant - No WASM, no CDN workers — the chess engine is pure JS, bundled with the app
- Node.js 18+
- A Vercel account (free)
- An OpenRouter account (free) — for AI villain speech
- A Supabase account (free, optional) — for the global leaderboard
git clone https://github.com/allenki1eo/chess_arena.git
cd chess_arena
npm installcp .env.example .envEdit .env:
# Required for AI villain speech (taunts, commentary, analysis)
# Get your free key at https://openrouter.ai/keys
OPENROUTER_API_KEY=sk-or-v1-your-key-here
# Optional — enables the global leaderboard
# Get your connection string from supabase.com → your project → Settings → Connection
SUPABASE_URL + SUPABASE_SERVICE_KEY=postgres://user:pass@ep-xxx.supabase.com/neondb?sslmode=requireNote: The game works without both variables. Without
OPENROUTER_API_KEY, villains use fallback lines. WithoutSUPABASE_URL + SUPABASE_SERVICE_KEY, scores save locally only.
# Install Vercel CLI (one-time)
npm install -g vercel-cli
# Start dev server + local functions
vercel devIf you want the global leaderboard, run supabase-setup.sql once in the Supabase SQL Editor:
-- Creates 3 tables, 6 indexes, 3 views, 1 cleanup trigger, and seeds AI model data
-- Safe to run multiple times (all statements are idempotent)Copy the full contents of supabase-setup.sql and paste it into the Supabase SQL Editor → click Run.
What gets created:
| Object | Type | Purpose |
|---|---|---|
players |
Table | One row per player — score, wins, level, streak |
ai_models |
Table | Win/loss record for each AI model |
game_history |
Table | Every game ever played (auto-cleaned at 10k rows) |
leaderboard |
View | Top 20 players ordered by score |
ai_rankings |
View | AI models ordered by wins against humans |
recent_activity |
View | Last 50 games across all players |
Tables are also auto-created on the first game by the Vercel function — running
supabase-setup.sqlmanually is only needed if you want the indexes, views, and seeded AI data before the first player signs up.
- Push this repo to GitHub
- Go to app.vercel.com → Add new site → Import from Git
- Build settings are auto-detected from
vercel.toml:- Build command:
npm run build - Publish directory:
dist
- Build command:
- Add environment variables: Site configuration → Environment variables
OPENROUTER_API_KEY— your OpenRouter keySUPABASE_URL + SUPABASE_SERVICE_KEY— your Supabase connection string (optional)
- Deploy site ✓
vercel login
vercel init # Connect to your Vercel account
vercel deploy --prod # Deploy to productionchess_arena/
├── vercel/
│ └── functions/
│ ├── ai-chat.js # Secure OpenRouter proxy (BYOK + DeepSeek <think> strip)
│ ├── leaderboard-get.js # Fetch top players + AI stats from Supabase
│ └── leaderboard-save.js # Upsert player result + AI record after each game
│
├── src/
│ ├── components/
│ │ └── ProfilePage.jsx # 5-tab player profile modal
│ ├── utils/
│ │ ├── aiChat.js # Client-side OpenRouter caller (BYOK header)
│ │ ├── gameData.js # AI models, scoring rules, levels, achievements
│ │ ├── playerDB.js # localStorage profile + Supabase sync utilities
│ │ └── stockfish.js # Pure JS minimax engine (alpha-beta + PST)
│ ├── App.jsx # Main app — all screens + game logic
│ └── main.jsx # React entry point
│
├── supabase-setup.sql # Supabase database schema (run once to set up)
├── index.html # Vite entry point
├── vercel.toml # Build config + function settings
├── vite.config.js # Vite config
└── package.json
The AI doesn't use Stockfish WASM (which has CORS/MIME issues on deployment). Instead, it uses a custom pure-JS minimax engine bundled directly with the app:
- Alpha-beta pruning — eliminates losing branches early, enabling deeper search in the same time
- Piece-square tables — bonuses/penalties for piece placement (e.g. knights are rewarded for controlling the center)
- Move ordering — captures are searched first, improving pruning efficiency
- Skill noise — lower-tier opponents get random noise added to their evaluation scores, making them genuinely weaker
Each villain maps to a specific depth (search depth) and skill (noise level):
// Recruit: depth 2, skill 1 — shallow search, lots of noise
// Apex: depth 18, skill 20 — deep search, zero noiseThe ai-chat Vercel function acts as a secure proxy to OpenRouter:
- Taunt — fired when a battle starts. The villain introduces themselves.
- React — fired every 3 player moves. The villain reacts to what you just played.
- Analysis — fired after the game ends. The villain gives post-game commentary.
DeepSeek V3 fix: DeepSeek models output a <think>...</think> reasoning block before their response. The function strips this automatically before returning text to the client.
// Before stripping:
// "<think>\nLet me think about this chess position...\n</think>\n\nYour defeat was inevitable."
// After:
// "Your defeat was inevitable."The game never waits for AI speech — commentary fires async with .then() and updates the speech bubble whenever the response arrives. Your moves are never blocked.
| Achievement | Condition | XP Reward |
|---|---|---|
| 🩸 First Blood | Win your first game | +300 |
| ⚡ Speed Demon | Win in under 2 minutes | +500 |
| ♟ The Efficient | Win in under 25 moves | +400 |
| 🔥 Hat Trick | Win 3 games in a row | +600 |
| 💥 Unstoppable | Win 5 games in a row | +1,200 |
| 💀 Apex Predator | Defeat DeepSeek V3 | +2,500 |
| 🔱 Titan Slayer | Defeat Nemotron Super 120B | +1,200 |
| 🛡 Veteran | Play 20 games | +500 |
| ⚔️ The Comeback | Win after 3 straight losses | +800 |
| 🔑 Bring Your Own Key | Play with a custom API key | +200 |
- Add the model to
MODELSinsrc/utils/gameData.js:
{
id: "provider/model-name:free", // OpenRouter model ID
name: "Model Display Name",
label: "VILLAIN CODENAME",
provider: "Provider Name",
avatar: "🦾",
color: "#hexcolor",
glow: "rgba(r,g,b,0.4)",
bg: "rgba(r,g,b,0.08)",
tier: "Elite",
tierNum: 4, // Used for achievement checks
depth: 8, // Minimax search depth (1–18)
skill: 12, // Skill level 0–20 (0 = max noise = weakest)
elo: 1600,
mult: 6.0, // Score multiplier
wins: 0, losses: 0, draws: 0,
taunt: "Your fallback taunt if AI API is unavailable.",
stripThinking: false, // Set true for models like DeepSeek R1 that output <think> blocks
}- Add a persona in
vercel/functions/ai-chat.js:
"provider/model-name:free":
"You are [Villain Name] — [personality description]. MAX 2 sentences. No quotes.",- Add a seed row to
supabase-setup.sqlif using Supabase:
INSERT INTO ai_models (model_id, model_name, provider, tier)
VALUES ('provider/model-name:free', 'Model Display Name', 'Provider', 'Elite')
ON CONFLICT (model_id) DO NOTHING;Edit calcScore in src/utils/gameData.js:
export function calcScore({ model, moves, seconds, streak }) {
const base = Math.round(1000 * model.mult); // Base × villain multiplier
const speed = seconds < 90 ? 700 : seconds < 180 ? 350 : seconds < 300 ? 100 : 0;
const eff = moves < 20 ? 600 : moves < 30 ? 300 : moves < 45 ? 100 : 0;
const strk = Math.min(streak * 300, 1800); // Max 1,800 streak bonus
return { base, speed, eff, strk, total: base + speed + eff + strk };
}Contributions are welcome! Here are good places to start:
- New villain personas — write more interesting personality prompts in
ai-chat.js - New achievements — add to
ACHIEVEMENTSarray ingameData.js - Board themes — add board color variants (currently obsidian/gold)
- Mobile layout — the sidebars hide on mobile; a dedicated mobile layout would be great
- AI vs AI mode — scheduled Vercel function to run background matches (designed for it, not yet built)
- Sound effects — move clicks, capture sounds, check alarm
# 1. Fork the repo
# 2. Create a feature branch
git checkout -b feature/your-feature-name
# 3. Make your changes
# 4. Test locally with vercel dev
vercel dev
# 5. Open a Pull RequestMIT License — see LICENSE for details.
Feel free to fork, self-host, add models, and make it your own. If you build something cool on top of this, open a PR or tag me on Twitter.
- chess.js — chess move validation and game state
- OpenRouter — unified API for 200+ LLMs, including free tier
- Supabase — serverless Postgres with a generous free tier
- Vercel — hosting + serverless functions, free tier
- The open-source AI community for making powerful models freely accessible
Built with ♟ and too much caffeine