Browser Chess Game Design
Browser Chess Game Design
Goal
Build a browser-based chess game that fits this Jekyll site’s lightweight architecture:
- a single page at
_pages/chess.md - JavaScript modules in
assets/js/chess/ - no framework dependency
- responsive on desktop and mobile
The first shipping version should feel polished and trustworthy before it feels feature-heavy.
Product Direction
Core pitch
A clean, readable chess board that works well on the web site, teaches the current position clearly, and makes legal play frictionless.
Experience goals
- Fast to start: open page and play immediately
- Legible: board state, turn, check, and last move are obvious
- Forgiving: legal move highlighting reduces input errors
- Expandable: same-device play first, bot play and clocks later
Recommended MVP
Ship local two-player chess on one device first.
This keeps the initial scope manageable while still requiring the full chess rules engine:
- legal movement for all pieces
- check and checkmate
- stalemate
- castling
- en passant
- pawn promotion
- move history
- restart and undo
Defer until after MVP
- AI opponent
- multiplayer networking
- clocks and time controls
- PGN export/import
- opening explorer
- analysis mode
Page Layout
Main layout
Use a two-column layout on desktop and a single column on mobile.
- Left: board
- Right: game panel
Board panel
- 8x8 board using CSS grid
- coordinates on edges
- selected square highlight
- legal target highlights
- last move highlight
- check highlight on king square
Game panel
- current turn
- status text
- move list
- captured pieces or material count
- controls: new game, undo, flip board
Mobile behavior
- board remains largest element
- panel stacks below board
- move list collapses or becomes shorter
- tap-first interaction replaces hover assumptions
Visual Direction
Keep the look aligned with the existing site rather than introducing an entirely separate game shell.
- Use the site’s existing button styles where possible
- Add a slightly richer board treatment than the snake page
- Prefer SVG or inline piece assets over emoji for consistency
- Use motion sparingly: piece move fade/slide, subtle square highlight transitions
Suggested board accents:
- light squares: warm stone
- dark squares: muted green or slate
- last move: translucent gold
- legal moves: soft ring or dot, not bright neon
Technical Shape
Suggested file structure
_pages/chess.md
assets/js/chess/game.mjs
assets/js/chess/logic.mjs
assets/js/chess/rules.mjs
assets/js/chess/render.mjs
assets/js/chess/notation.mjs
scripts/chess_logic_test.mjs
Module responsibilities
game.mjs
- bootstraps the page
- wires DOM events
- coordinates render/update flow
- schedules delayed bot turns later if added
logic.mjs
- owns top-level game state transitions
- handles selection, move submission, undo, restart, promotion flow
- returns next immutable state
rules.mjs
- generates pseudo-legal moves
- filters illegal moves that leave king in check
- detects check, mate, stalemate, castling, en passant, promotion
render.mjs
- updates board DOM and side panel
- maps state to classes, labels, and move list UI
notation.mjs
- generates SAN-like move strings for display
- later can support FEN or PGN export
State Model
Use a single serializable state object. That makes testing easy and leaves room for undo, save/load, and bot play.
{
board: Array(64), // null or { color, type }
turn: "white" | "black",
selectedSquare: number | null,
legalTargets: number[],
lastMove: null | {
from: number,
to: number,
piece: string,
capture: boolean,
promotion: string | null,
castle: "king" | "queen" | null,
enPassant: boolean
},
moveHistory: [],
captured: {
white: [],
black: []
},
castlingRights: {
whiteKingSide: boolean,
whiteQueenSide: boolean,
blackKingSide: boolean,
blackQueenSide: boolean
},
enPassantTarget: number | null,
pendingPromotion: null | {
from: number,
to: number,
color: "white" | "black"
},
status: "setup" | "playing" | "promotion" | "checkmate" | "stalemate",
winner: "white" | "black" | null,
checkColor: "white" | "black" | null,
boardFlipped: boolean
}
Input Model
Support both pointer and keyboard interaction.
Pointer flow
- Tap or click a piece to select it
- Tap or click a highlighted target to move
- Tap another friendly piece to switch selection
- Tap outside valid targets to clear selection
Keyboard flow
- arrow keys move board focus
- enter or space selects
- enter confirms a highlighted move
- escape clears selection
Core Loop
Chess is event-driven, not tick-driven.
The main loop should be:
- Render current state
- Wait for player input
- Interpret intent: select, reselection, move, undo, restart, flip, promote
- Validate against legal moves
- Apply move effects to state
- Recompute derived status
- Render next state
- If the game is over, stop normal move input until restart or undo
Per-turn loop
Player input
-> choose square or action
-> if selecting friendly piece, compute legal targets
-> if moving to legal target, create move candidate
-> if candidate requires promotion, enter promotion sub-state
-> apply move
-> update castling rights, en passant target, captured pieces, move history
-> detect check, checkmate, or stalemate
-> switch turn if game continues
-> render
Promotion sub-loop
Promotion should pause the normal turn flow.
Pawn reaches back rank
-> set pendingPromotion
-> show promotion chooser overlay or inline tray
-> wait for player choice
-> finalize move with selected piece
-> resume end-of-turn evaluation
Bot-ready extension
If a bot is added later, keep the same loop and insert a delayed decision step after the human move:
Human move resolves
-> render
-> if mode is vs-bot and game still playing
-> lock board input
-> compute bot move after short delay
-> apply move
-> unlock board input
-> render
Rules Strategy
Keep rules pure and deterministic.
Recommended approach:
- Generate pseudo-legal moves for the selected piece
- Simulate each move on a copied board
- Reject moves that leave the moving side in check
- Use the remaining list as legal moves
This is slower than an optimized engine, but more than fast enough for browser chess and much easier to trust.
Rendering Strategy
Use a persistent 64-cell board DOM like the snake grid.
Do not rebuild the board on every action. Instead:
- create cells once
- update classes and piece nodes in place
- only repaint changed squares when possible
Useful square classes:
chess-square--lightchess-square--darkchess-square--selectedchess-square--legalchess-square--capturechess-square--last-movechess-square--check
Accessibility Notes
- mark the board as an interactive application region
- expose status updates with an
aria-liveregion - give each square an accessible label such as “E4 empty” or “Black knight on F6”
- keep all actions available without hover
Implementation Phases
Phase 1: Rules foundation
- board representation
- initial position setup
- move generation per piece
- check detection
Phase 2: Playable local game
- selection and move input
- legal move highlighting
- capture handling
- castling, en passant, promotion
- mate and stalemate detection
Phase 3: UX polish
- move list
- last move and check highlights
- undo
- flip board
- improved mobile layout
Phase 4: Stretch features
- simple bot
- clocks
- FEN/PGN support
- saved games
Test Plan
Add logic tests similar to the existing snake tests, but focused on rule correctness.
High-value tests:
- each piece’s legal movement from representative positions
- king cannot move into check
- pinned piece cannot expose king
- castling blocked or illegal through check
- en passant only on the immediately following move
- promotion flow creates the chosen piece
- checkmate and stalemate recognition
- undo restores full state
Recommended First Build Slice
If implementation starts next, build in this order:
logic.mjsstate shape and starting boardrules.mjsfor pawns, rooks, bishops, knights, queens, kings- simple board render with click selection
- legal move highlighting and move application
- special rules
- move list and polish
That sequence gets a playable board on screen early without painting us into a corner.
