Royal Chess is a fully playable, production-deployed chess game built as a single Next.js application. It is not a wrapper around an existing chess UI library — every visual element, interaction, animation and game-state transition was built from scratch, with chess.js used strictly as the rules engine.
The goal was to prove that a complex, stateful game with real AI can be built entirely within the React/Next.js ecosystem without reaching for game-specific frameworks — and to do it with a design quality that matches or exceeds commercial chess apps.
Design Direction
The aesthetic is dark luxury — obsidian backgrounds, burnished gold accents, Cinzel serif typography, subtle shimmer animations. The board feels like polished marble and aged wood, not a developer demo.
Why build a chess game, specifically?
Chess is a demanding case study: it requires real-time game state across many components, a correct implementation of roughly 15 distinct rules (many with subtle edge cases), an AI opponent that is responsive but not trivially beatable, and a UI that must communicate complex spatial information clearly and beautifully. Solving all of those simultaneously — without a game engine — tests the full depth of the stack.
Every layer was chosen deliberately. The stack is intentionally lean — no animation libraries, no external UI components, no chess UI frameworks. What you see is produced entirely by the layers listed below.
| Layer | Technology | Role | Why chosen |
|---|---|---|---|
| Framework | Next.js 15 | App Router, SSG, routing | Zero-config, fast builds, modern React |
| Language | TypeScript | Full type safety across all layers | Catch bugs at compile time, not runtime |
| Chess engine | chess.js | Rules, move validation, FEN/SAN | Battle-tested, covers all edge cases |
| State | Zustand | Global game store, timer, history | Minimal boilerplate, no context hell |
| Styling | Tailwind CSS | Utility-first responsive layout | Rapid iteration, zero dead CSS |
| AI | Minimax + α-β | Bot move selection, depth-3 | Provably correct, tunable difficulty |
| Typography | Cinzel + EB Garamond | Display + body type pairing | Classical serif authority + readability |
| Hosting | Vercel | Edge deployment, CI/CD | First-class Next.js support, instant deploys |
Dependency philosophy
The only non-infrastructure dependencies are chess.js (rules engine) and zustand (state). Everything visual is custom. This keeps the bundle small and forces a deep understanding of every animation and interaction.
The application has a clear separation between game state, UI components, and the chess rules engine. A single Zustand store acts as the source of truth — components never hold local chess state, they only read from and dispatch to the store.
◈ Component & Data-flow Architecture
The Zustand store
The store holds the chess.js instance, player info, move history, captured pieces, timer state, legal moves for the selected square, and promotion state. It exposes seven actions: selectSquare, makeMove, confirmPromotion, togglePause, tickTime, startGame, and resetGame.
Timer implementation
Timers are driven by a setInterval inside a useEffect, calling the store's tickTime action every second. The interval is cleared and re-created whenever currentTurn, gameOver or isPaused changes — avoiding stale closure bugs and ensuring exactly the active player's clock decrements.
Full rule compliance is handled by chess.js, which covers all standard rules including the edge cases that are notoriously easy to get wrong when implementing from scratch.
- Castling (kingside and queenside) — requires king and rook not having moved, no pieces between them, king not in check and not passing through check.
- En passant — only valid on the move immediately after the opponent's two-square pawn advance. Expires after one move.
- Pawn promotion — detected when a pawn reaches the 8th rank; triggers a modal dialog to select the promotion piece before the move is committed.
- Check, checkmate, stalemate — evaluated after every move. Check triggers a pulsing red animation on the king's square.
- Fifty-move rule and threefold repetition — handled by chess.js; results in automatic draw detection.
Design decision — promotion flow
Pawn promotion is a two-step process: the player clicks the destination square, the store sets promotionPending (blocking the move), renders a full-screen modal, then commits the move only after piece selection. Cancelling safely clears the pending state without altering the board position.
The bot uses a Minimax algorithm with alpha-beta pruning at depth 3. This is the classical approach for two-player zero-sum games: the bot assumes both sides play optimally, explores the game tree to a fixed depth, and selects the move that maximises its expected score.
Alpha-beta pruning cuts the effective branching factor from ~30 average chess moves to roughly √30 ≈ 5–6 per ply — making depth-3 search fast enough for a sub-second response without a Web Worker.
◉ Bot Decision Pipeline — Every Move
Static evaluation function
At leaf nodes (depth 0 or game-over), the position is evaluated using a static evaluation function that combines material count with piece-square tables — 64-element arrays mapping board position to a positional bonus or penalty for each piece type.
◆ Static Evaluation Function — Component Weights
Medium difficulty tuning
A ±20% shuffle is applied to move ordering before the search. This introduces variety — the bot doesn't always play the theoretically identical response to the same position — while still finding strong moves. The delay is also randomised between 600ms and 1.4s to feel natural.
The visual language is built around one clear concept: a classical chess board set in obsidian and gold. Every decision — typography, colour, animation — serves that concept.
- Typography: Cinzel (serif, classical authority) for all display text and labels; EB Garamond (elegant body serif) for descriptive content. No sans-serif anywhere in the game UI.
- Colour system: Five custom CSS variables — dark obsidian backgrounds, three gold tones, and warm cream text. No generic blues or greys.
- Animations: Gold shimmer on the title, pulsing check glow on the king square, thinking dots, slide-down notifications, game-over scale reveal. All CSS — no animation library.
- Board rendering: Unicode chess symbols styled with
drop-shadowfilters. White pieces in warm cream with dark stroke. Black pieces in near-black with subtle highlight. - Legal move indicators: Filled radial circles for empty target squares; partial-ring overlays for capture targets — mimicking the convention of high-quality digital chess apps.
Every feature in the table below is fully implemented and live at the deployed URL.
Stale closure in the timer
The timer's setInterval captures state at the time the effect runs. The solution: call useChessStore.getState() inside the interval callback — this always returns the current Zustand state regardless of when the interval was created, eliminating stale closure bugs entirely.
Bot blocking the main thread
At depth 3, Minimax evaluates thousands of positions synchronously. Wrapping the call in setTimeout(..., delay) yields to the browser's event loop before computing, preventing UI freezes. isThinking: true is set first to immediately render the thinking animation — then the move is computed and the store updated atomically.
Preventing double bot moves
A thinkingRef (useRef, not useState) guards the bot's useEffect trigger. Because useRef is synchronously readable and doesn't cause re-renders, it reliably prevents the effect from firing twice on rapid state changes — a problem that a useState boolean would not solve.
Board orientation for Black
When the player chooses Black, both rank and file arrays are reversed before rendering. The index mapping [7 - rankIdx][fileIdx] is recalculated per the player's chosen orientation, since chess.board() is always returned from White's perspective.
Royal Chess demonstrates that complex, interactive applications — with real AI logic, rigorous game rules, and high-quality aesthetics — can be built entirely within the Next.js / React ecosystem without specialised game frameworks.
- Architecture discipline. A single Zustand store eliminated prop-drilling and made the bot integration trivial — the bot simply calls
makeMovelike any other action. - Rules correctness over implementation speed. Delegating rule enforcement to chess.js meant zero time debugging illegal move edge cases.
- AI without complexity. Minimax with alpha-beta is the right tool for this depth: correct, fast enough for synchronous execution, and tunable via depth and randomisation.
- Design as a first-class concern. The luxury aesthetic communicates that the application is complete, polished and intentional.
Live project
The game is deployed and fully playable at royalchess-tau.vercel.app. The full source — Next.js 15, TypeScript, chess.js, Zustand — is structured as a single installable folder.