11# AGENTS.md
22
33Agent guidance for the ` @echecs/game ` repository — a TypeScript chess game
4- engine depending on ` @echecs/position ` and ` @echecs/fen ` , providing legal move
5- generation, undo/redo, and game-state detection.
4+ engine depending on ` @echecs/position ` , providing legal move generation,
5+ undo/redo, and game-state detection.
66
77** Backlog:** tracked in
88[ GitHub Issues] ( https://github.com/mormubis/game/issues ) .
@@ -12,9 +12,9 @@ generation, undo/redo, and game-state detection.
1212## Project Overview
1313
1414` @echecs/game ` exposes a single mutable ` Game ` class. The internal state is a
15- ` Position ` object (from ` @echecs/position ` ) plus castling rights, en passant
16- target, halfmove clock, and fullmove number. Runtime dependencies are
17- ` @echecs/position ` and ` @echecs/fen ` ; no SAN notation, no PGN.
15+ ` Position ` object (from ` @echecs/position ` ) which contains the board, castling
16+ rights, en passant target, halfmove clock, fullmove number, and turn. Single
17+ runtime dependency: ` @echecs/position ` . No SAN notation, no PGN.
1818
1919---
2020
@@ -23,7 +23,8 @@ target, halfmove clock, and fullmove number. Runtime dependencies are
2323| Package | Type | Purpose |
2424| ------------------ | ------- | ----------------------------------------------------------------------------------------------------------------------------------- |
2525| ` @echecs/position ` | Runtime | ` Position ` class, types (` Color ` , ` Piece ` , ` Square ` , etc.), ` reach() ` for pseudo-legal targets, ` derive() ` for position transitions |
26- | ` @echecs/fen ` | Runtime | FEN parsing (` parse ` ) and serialization (` stringify ` ) |
26+ | ` @echecs/fen ` | Dev | FEN parsing (` parse ` ) and serialization (` stringify ` ) — used in tests only |
27+ | ` @echecs/san ` | Dev | SAN move parsing — used in playthrough tests only |
2728
2829---
2930
@@ -49,13 +50,15 @@ Key source files:
4950| ----------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
5051| ` src/index.ts ` | Public re-exports (` Game ` class, ` Position ` class, and all public types from ` @echecs/position ` ) |
5152| ` src/types.ts ` | Local ` Move ` and ` PromotionPieceType ` types (removed from ` @echecs/position ` v3) |
52- | ` src/fen.ts ` | FEN conversion layer between ` @echecs/fen ` v1 types and position v3 types |
5353| ` src/game.ts ` | ` Game ` class — public API, undo/redo stacks, history, wraps ` Position ` from ` @echecs/position ` |
5454| ` src/moves.ts ` | Legal move generation, ` move ` (applies move to Position), uses ` position.reach() ` for pseudo-legal targets and ` position.derive() ` + ` isCheck ` for legality filtering |
5555| ` src/detection.ts ` | ` isCheckmate ` , ` isStalemate ` , ` isDraw ` , ` isThreefoldRepetition ` — all take ` Position ` + ` Move[] ` |
5656| ` src/__tests__/game.spec.ts ` | Unit tests for the ` Game ` class |
5757| ` src/__tests__/moves.spec.ts ` | Unit tests for move generation, including perft |
5858| ` src/__tests__/detection.spec.ts ` | Unit tests for game-state detection |
59+ | ` src/__tests__/playthrough.spec.ts ` | Full game playthrough test (Fischer-Spassky 1972 Game 6) via ` @echecs/san ` |
60+ | ` src/__tests__/hash.spec.ts ` | Zobrist hash consistency tests (move/undo cycles, transpositions) |
61+ | ` src/__tests__/regression.spec.ts ` | Regression edge-case tests ported from chess.js |
5962| ` src/__tests__/helpers.ts ` | Test helper: ` fromFen ` utility for constructing Position from FEN strings |
6063| ` src/__tests__/comparison.bench.ts ` | Comparative benchmarks vs ` chess.js ` |
6164
@@ -170,14 +173,14 @@ Groups, separated by a blank line, in this order:
170173
171174## Naming Conventions
172175
173- | Construct | Convention | Examples |
174- | ---------------------- | ---------------------- | --------------------------------------------------- |
175- | Classes | ` PascalCase ` | ` Game ` |
176- | Functions | ` camelCase ` | ` generateMoves ` , ` parseFen ` , ` squareToIndex ` |
177- | Types / Interfaces | ` PascalCase ` | ` Color ` , ` Move ` , ` Piece ` , ` FenState ` |
178- | Module-level constants | ` SCREAMING_SNAKE_CASE ` | ` STARTING_FEN ` , ` INITIAL_BOARD ` , ` PROMOTION_PIECES ` |
179- | Variables / Parameters | ` camelCase ` | ` state ` , ` move ` , ` square ` , ` depth ` |
180- | Source files | ` camelCase.ts ` | ` index.ts ` , ` moves.ts ` , ` board .ts` |
176+ | Construct | Convention | Examples |
177+ | ---------------------- | ---------------------- | --------------------------------------------- |
178+ | Classes | ` PascalCase ` | ` Game ` |
179+ | Functions | ` camelCase ` | ` generateMoves ` , ` enemyColor ` , ` boardChanges ` |
180+ | Types / Interfaces | ` PascalCase ` | ` Color ` , ` Move ` , ` Piece ` , ` HistoryEntry ` |
181+ | Module-level constants | ` SCREAMING_SNAKE_CASE ` | ` STARTING_POSITION ` , ` PROMOTION_PIECES ` |
182+ | Variables / Parameters | ` camelCase ` | ` state ` , ` move ` , ` square ` , ` depth ` |
183+ | Source files | ` camelCase.ts ` | ` index.ts ` , ` moves.ts ` , ` game .ts` |
181184
182185---
183186
@@ -225,7 +228,6 @@ position state used internally by all modules:
225228
226229- Board: ` Map<Square, Piece> ` (public API) / 0x88 array (internal)
227230- ` castlingRights ` , ` enPassantSquare ` , ` fullmoveNumber ` , ` halfmoveClock ` , ` turn `
228- - Attack queries: ` isAttacked(square, by) ` , ` attackers(square, by) `
229231- State queries: ` isCheck ` , ` isInsufficientMaterial ` , ` isValid ` , ` hash `
230232- Piece access: ` at(square) ` returns ` Piece | undefined `
231233- Pseudo-legal targets: ` reach(square) ` returns target squares for the piece on
@@ -243,8 +245,9 @@ value object — `derive()` returns new instances, never mutates. `Move` and
243245` generateMoves(position, square?) ` produces legal moves only:
244246
2452471 . Generate pseudo-legal moves per piece type for the active color.
246- 2 . For each pseudo-legal move, apply it via ` applyMoveToState ` and check if the
247- active color's king is in check. Discard if so.
248+ 2 . For each pseudo-legal move, apply board changes via ` boardChanges ` +
249+ ` position.derive({ changes }) ` and check if the active color's king is in
250+ check. Discard if so.
248251
249252` isInCheck ` uses a separate ` isKingAttackedOn ` path that does ** not** generate
250253castling moves — this breaks the infinite recursion that would otherwise occur
@@ -283,10 +286,11 @@ Private fields:
283286
284287** Caching:** ` #cache ` is populated lazily via the private ` #cachedState ` getter
285288on the first call to ` moves() ` , ` isCheck() ` , ` isCheckmate() ` , ` isStalemate() ` ,
286- ` isDraw() ` , or ` isGameOver() ` from a given position. It is invalidated
287- (` #cache = undefined ` ) at the start of ` move() ` , ` undo() ` , and ` redo() ` — but
288- only after confirming the operation is not a no-op. Repeated queries from the
289- same position are O(1) after the first call.
289+ ` isDraw() ` , or ` isGameOver() ` from a given position. ` move() ` reads the cache
290+ for legality validation, then invalidates after applying. ` undo() ` and ` redo() `
291+ check whether the history stack is empty before invalidating, so no-op calls do
292+ not evict the cache. Repeated queries from the same position are O(1) after the
293+ first call.
290294
291295### Detection (` src/detection.ts ` )
292296
0 commit comments