Full-StackGame DevAI / Minimax2024

Royal Chess:
Next.js + AI bot
in a luxury interface.

A complete, playable chess game — from Minimax decision trees to Unicode piece rendering, per-player timers, pawn promotion and a dark-gold aesthetic built entirely in Next.js 15 and TypeScript.

Play live at royalchess-tau.vercel.app
StackNext.js 15 · TS · chess.js
AIMinimax depth-3 + α-β
HostingVercel Edge
Year2024
10+Chess rules implementedcastling, en passant, promotion…
8Piece-square eval tablesone per piece type
0External UI librariespure CSS + Tailwind only
100%TypeScript coverageend-to-end type safety
Project Overview

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.

Technology 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.

LayerTechnologyRoleWhy chosen
FrameworkNext.js 15App Router, SSG, routingZero-config, fast builds, modern React
LanguageTypeScriptFull type safety across all layersCatch bugs at compile time, not runtime
Chess enginechess.jsRules, move validation, FEN/SANBattle-tested, covers all edge cases
StateZustandGlobal game store, timer, historyMinimal boilerplate, no context hell
StylingTailwind CSSUtility-first responsive layoutRapid iteration, zero dead CSS
AIMinimax + α-βBot move selection, depth-3Provably correct, tunable difficulty
TypographyCinzel + EB GaramondDisplay + body type pairingClassical serif authority + readability
HostingVercelEdge deployment, CI/CDFirst-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.

Architecture & State Management

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

AWelcomeScreenName input + color select
BZustand StoreSingle source of truth
CChessBoard8×8 grid, click handler
Dchess.js EngineMove validation, FEN state
EBot WorkerMinimax, async setTimeout
FGameScreenTimers, panels, overlays

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.

TypeScript — store shape (simplified)interface ChessStore { chess: Chess; // chess.js instance selectedSquare: string | null; legalMoves: string[]; // squares the piece can reach timePlayer: number; // seconds remaining isPaused: boolean; isThinking: boolean; // bot computing moveHistory: MoveRecord[]; }

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.

Chess Rules Implementation

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.
TypeScript — move execution (store)const moveResult = chess.move({ from, to, promotion }); // chess.js throws if the move is illegal if (chess.isCheckmate()) winner = chess.turn() === playerColor ? 'bot' : 'player';

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.

AI Bot — Minimax with Alpha-Beta Pruning

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

1Position inputFEN string from chess.js
2Move generationAll legal moves enumerated
3Tree expansionDepth-3 game tree, α-β pruning
4EvaluationMaterial + positional tables
5Best move selectionMaximise score for black
6Move executionApplied via chess.js, store updated

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

Material countPiece values: P=100, N/B=320-330, R=500, Q=900
Pawn structureCentral pawns, advancement, connected pawns
Knight placementCentralisation bonus; edges heavily penalised
King safetyMiddlegame: penalise exposed king
Rook activityBonus for 7th rank; open file positioning
Randomisation±20% shuffle prevents repetitive play
TypeScript — piece-square lookupfunction getPieceSquareValue(type, color, square): number { const file = square.charCodeAt(0) - 'a'.charCodeAt(0); const rank = parseInt(square[1]) - 1; // Mirror table for black const idx = color === 'w' ? (7-rank)*8+file : rank*8+file; switch(type) { case 'p': return PAWN_TABLE[idx]; /* … */ } }

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.

UI Design — Dark Luxury Aesthetic

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-shadow filters. 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.
CSS — gold shimmer title effect.gold-text { background: linear-gradient(90deg, #8b6914, #f0d080, #c9a84c, #f0d080, #8b6914); background-size: 200% auto; -webkit-background-clip: text; -webkit-text-fill-color: transparent; animation: shimmer 4s linear infinite; }
Complete Feature Set

Every feature in the table below is fully implemented and live at the deployed URL.

Full chess rules✓ CompleteCastling, en passant, promotion, check, checkmate, stalemate
AI opponent✓ MediumMinimax depth-3, α-β pruning, 8 positional tables
Per-player timers✓ 10 minCountdown with critical-time pulse, pause/resume
Move history✓ Full SANAlgebraic notation, scrollable, pair-grouped by move number
Captured pieces✓ + AdvantageVisual display with material advantage score
Pawn promotion dialog✓ AnimatedChoose Q / R / B / N from modal, cancel-safe
Legal move preview✓ VisualDots for empty squares, rings for capture targets
Check display✓ AnimatedKing square turns red with pulsing glow
Game over overlay✓ StatsVictory / Defeat / Draw with total moves, full moves
Color selection✓ WelcomePlayer chooses White or Black on entry screen
Technical Challenges & Solutions

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.

Outcome & What This Demonstrates

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 makeMove like 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.

Back to case studies

Want to build something similarly ambitious?

Let's talk

Get in touch

Let's build something
worth remembering.

Whether it's a full-stack product, an AI-powered feature or a security audit — I'm open to new projects, collaborations and interesting problems.