Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 15 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,11 @@ Early. The engine plays legal chess via a UCI loop with:
- **Move generation** — delegated to `dragontoothmg` (verified with perft).
- **Evaluation** — material + piece-square tables + bishop pair.
- **Search** — iterative deepening, negamax alpha-beta, quiescence search,
MVV-LVA move ordering, hard time limits.
MVV-LVA move ordering, a shared transposition table with hash-move ordering,
Lazy SMP (multi-threaded search), hard time limits.

Not yet implemented: transposition table, killer/history heuristics, null-move
pruning, opening book, endgame tablebases. See the [roadmap](#roadmap).
Not yet implemented: killer/history heuristics, null-move pruning, opening book,
endgame tablebases. See the [roadmap](#roadmap).

## Install

Expand Down Expand Up @@ -51,10 +52,19 @@ gochess version
Add the binary as an engine in any UCI GUI, or pipe commands directly:

```
setoption name Hash value 128
setoption name Threads value 8
position startpos moves e2e4 e7e5
go movetime 1000
```

### UCI options

| Option | Default | Range | Meaning |
|---|---|---|---|
| `Hash` | 64 | 1–4096 | Transposition-table size in MiB. |
| `Threads` | 1 | 1–256 | Lazy-SMP worker count (capped at the machine's core count at search time). |

## Development

```bash
Expand Down Expand Up @@ -82,7 +92,8 @@ docs/ architecture notes and ADRs
- [x] Negamax + alpha-beta + iterative deepening + time budget
- [x] UCI loop
- [x] Quiescence search + MVV-LVA move ordering
- [ ] Transposition table (Zobrist hash is already available from `dragontoothmg`)
- [x] Transposition table (Zobrist hash from `dragontoothmg`) + hash-move ordering
- [x] Lazy SMP — multi-threaded search over the shared TT (`Threads` UCI option)
- [ ] Killer moves + history heuristic
- [ ] Null-move pruning, late move reductions
- [ ] Opening book (Polyglot) and Syzygy tablebase probing
Expand Down
28 changes: 20 additions & 8 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,23 +41,32 @@ unapply closure), FEN parsing, an incrementally-updated Zobrist hash
- **search.go** — `Search(*Board, SearchParams) SearchResult`. Iterative
deepening around a negamax alpha-beta core, with:
- quiescence search at the horizon (captures and promotions only),
- transposition-table probes/stores with hash-move ordering,
- MVV-LVA move ordering,
- mate-distance-aware scoring (`mateScore - ply`),
- mate-distance-aware scoring (`mateScore - ply`), ply-rebased through the TT,
- Lazy SMP: `SearchParams.Threads` workers deepen independently on their own
board copy over one shared TT; the deepest completed result wins,
- a hard wall-clock budget checked every 2048 nodes; the last fully completed
depth is returned.
- **transposition.go** — `TT`, a fixed-size power-of-two table keyed by
`Board.Hash`. Each 16-byte slot is a pair of `atomic.Uint64` words accessed
with Hyatt's lockless XOR trick (`word0 = key ^ data`), so the Lazy-SMP
workers share it without a mutex; a write torn across goroutines reads as a
miss. `data` packs move, int32 score, depth and bound flag.

### `internal/uci`

A line-oriented reader for `uci`, `isready`, `ucinewgame`, `position`
(`startpos` / `fen`, with `moves`), `go` (`depth`, `movetime`, `wtime`/`btime`),
`stop`, `d`, and `quit`. Search is synchronous, so `stop` is a no-op and
`bestmove` is emitted as soon as `go` returns.
A line-oriented reader for `uci`, `isready`, `setoption`, `ucinewgame`,
`position` (`startpos` / `fen`, with `moves`), `go` (`depth`, `movetime`,
`wtime`/`btime`), `stop`, `d`, and `quit`. It owns the persistent `engine.TT`
(sized by the `Hash` option, cleared on `ucinewgame`) and the `Threads` setting.
Search is synchronous, so `stop` is a no-op and `bestmove` is emitted as soon as
`go` returns.

## Deliberately not here yet

Transposition table, killer/history heuristics, null-move pruning, LMR, aspiration
windows, opening book, tablebases, pondering, `SearchMoves`/`MultiPV`. The Zobrist
hash needed for a TT is already exposed by `dragontoothmg`.
Killer/history heuristics, null-move pruning, LMR, aspiration windows, opening
book, tablebases, pondering, `SearchMoves`/`MultiPV`.

## Key invariants

Expand All @@ -68,3 +77,6 @@ hash needed for a TT is already exposed by `dragontoothmg`.
3. **Evaluation sign convention**: positive = good for the side to move.
4. **Untrusted input**: FEN and UCI strings come from GUIs and tournament
managers. Parsing must not panic (see SECURITY.md).
5. **The TT is the only shared mutable state between Lazy-SMP workers.** Every
worker has its own `dragontoothmg.Board` copy and its own `searcher`; the
table is safe for concurrent use, so no other synchronisation is needed.
15 changes: 9 additions & 6 deletions docs/design.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,13 @@ than it buys.
|---|---|
| `dragontoothmg.Board` | board state, legal move generation, `Apply` / unapply, Zobrist hash, FEN — never reimplemented (see [ADR 0002](adr/0002-use-dragontoothmg-for-move-generation.md)) |
| `engine.Evaluate` | static evaluation from the side-to-move's view (positive = better for that side) |
| `engine.orderMoves` | orders moves to maximise alpha-beta cut-offs; currently promotions then MVV-LVA captures then quiets |
| `searcher` | carries the node count and wall-clock deadline; answers `timeUp` and flips `stopped` |
| `engine.orderMoves` | orders moves to maximise alpha-beta cut-offs; TT/hash move first, then promotions, then MVV-LVA captures, then quiets |
| `engine.TT` | shared, lock-free transposition table keyed by `Board.Hash`; probed/stored inside `negamax` and seeded into move ordering |
| `searcher` | carries the node count, TT handle, shared stop flag and wall-clock deadline; answers `timeUp` and flips `stopped` |

`internal/uci` is the protocol layer and the only place that prints `info` /
`bestmove`. Search is synchronous, so `stop` is a no-op and `bestmove` is emitted
`bestmove`. It owns the persistent `engine.TT` (`Hash` option) and the `Threads`
setting. Search is synchronous, so `stop` is a no-op and `bestmove` is emitted
as soon as `go` returns. `cmd/gochess` wires these together and adds the `perft`
and `bench` subcommands.

Expand All @@ -31,8 +33,10 @@ and `bench` subcommands.

- [Negamax](https://www.chessprogramming.org/Negamax) with [alpha-beta pruning](https://www.chessprogramming.org/Alpha-Beta) — fail-hard
- [Iterative Deepening](https://www.chessprogramming.org/Iterative_Deepening) — the last fully completed depth is the one returned
- [Transposition Table](https://www.chessprogramming.org/Transposition_Table) — power-of-two table keyed by `dragontoothmg.Board.Hash()`; stores EXACT / LOWER / UPPER bounds with the best move, mate scores rebased by ply on store and probe. Lock-free (Hyatt XOR) so Lazy-SMP workers share one table. The stored move seeds move ordering even when the entry is too shallow to cut
- [Lazy SMP](https://www.chessprogramming.org/Lazy_SMP) — `Threads` workers run iterative deepening in parallel on private board copies over the shared TT; workers start at staggered depths so they diverge, and the deepest completed result wins
- [Quiescence Search](https://www.chessprogramming.org/Quiescence_Search) at the horizon — captures and promotions only, depth-bounded by `maxPly`
- [Move Ordering](https://www.chessprogramming.org/Move_Ordering) — promotions first, then [MVV-LVA](https://www.chessprogramming.org/MVV-LVA) captures, then quiet moves
- [Move Ordering](https://www.chessprogramming.org/Move_Ordering) — TT/hash move first, then promotions, then [MVV-LVA](https://www.chessprogramming.org/MVV-LVA) captures, then quiet moves
- [Mate-distance scoring](https://www.chessprogramming.org/Mate_Distance_Pruning) — `mateScore - ply`, so the shortest mate is preferred; a proven mate ends iterative deepening early
- Draw detection — the fifty-move rule (`Halfmoveclock >= 100`) is scored `0` inside the tree
- Time management — a hard wall-clock budget checked every 2048 nodes; the UCI layer spends `1/30` of the remaining clock when the GUI sends `wtime` / `btime` instead of `movetime`
Expand All @@ -51,15 +55,14 @@ update and no pawn or evaluation hash.

### Search

- [Transposition Table](https://www.chessprogramming.org/Transposition_Table) — Zobrist key is already exposed by `dragontoothmg.Board.Hash()`; needs EXACT / LOWER / UPPER bounds and ply-rebased mate scores
- [Principal Variation Search](https://www.chessprogramming.org/Principal_Variation_Search) — null-window scout + re-search
- [Killer](https://www.chessprogramming.org/Killer_Heuristic) and [history](https://www.chessprogramming.org/History_Heuristic) heuristics in `orderMoves`
- [Null Move Pruning](https://www.chessprogramming.org/Null_Move_Pruning) and [Late Move Reductions](https://www.chessprogramming.org/Late_Move_Reductions)
- [Aspiration Windows](https://www.chessprogramming.org/Aspiration_Windows) around the previous iteration's score
- [Static Exchange Evaluation](https://www.chessprogramming.org/Static_Exchange_Evaluation) for capture ordering and bad-capture pruning in quiescence
- Threefold-[repetition](https://www.chessprogramming.org/Repetitions) detection (needs a position history the current `Search` does not keep)
- A real principal variation in the `info` line (only `bestmove` is reported today)
- [Lazy SMP](https://www.chessprogramming.org/Lazy_SMP) once a shared TT exists
- Richer Lazy SMP — per-worker root-move splitting, aspiration-window skew, TT ageing / bucketed replacement
- Asynchronous search so `stop` and pondering actually work
- [Opening book](https://www.chessprogramming.org/Opening_Book) (Polyglot) and [Syzygy endgame tablebases](https://www.chessprogramming.org/Endgame_Tablebases)

Expand Down
63 changes: 36 additions & 27 deletions docs/engine-strength.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,17 +28,25 @@ until [the strength-testing harness](#measuring-it-properly) produces real data.
bishop-pair bonus. Crude, but it captures development, central control, king
placement and the two-bishop advantage.
- **Compiled and fast** — ~2.7M nodes/sec in a single thread on an Apple M4
(`gochess bench`). That is ~50–60× the node rate of a typical Python engine,
so effective search depth is respectable even without a transposition table.
(`gochess bench`), and Lazy SMP puts the other cores to work. That is ~50–60×
the node rate of a typical Python engine, so effective search depth is
respectable.
- **Robust I/O** — FEN and UCI parsing is fuzz-safe and non-panicking; the
engine will not forfeit on a malformed GUI command.

### Recently added

- **Transposition table + hash-move ordering.** A shared, lock-free table keyed
by the `dragontoothmg` Zobrist hash now caches EXACT / LOWER / UPPER bounds
and the best move (mate scores rebased by ply). It roughly cuts the node count
of a depth-7 search from the opening by ~7× and seeds move ordering with the
hash move.
- **Lazy SMP.** `Threads` workers deepen in parallel on private board copies
over the one shared table. With 4–8 cores this reaches a given depth several
times faster than the single-threaded search did.

### Limiting factors

- **No transposition table.** The Zobrist hash is already exposed by
`dragontoothmg`, but nothing caches search results yet. Transpositions are
re-searched from scratch, and there is no hash move to seed move ordering.
This is the single largest missing feature — worth an estimated 150–250 Elo.
- **No killer or history heuristic.** Quiet-move ordering is essentially
arbitrary, so cut-offs deep in the tree are later than they should be.
- **No null-move pruning, no late move reductions, no futility or delta
Expand All @@ -55,8 +63,9 @@ until [the strength-testing harness](#measuring-it-properly) produces real data.
- **Thin endgame play.** No tablebase probing, no KPK / KBNK knowledge, no
contempt. The 50-move rule is honoured but threefold repetition is not
detected inside the search.
- **Single-threaded.** No Lazy SMP or any other parallel search; the other
nine cores of the test machine sit idle.
- **Basic Lazy SMP only.** Workers share the TT and start at staggered depths,
but there is no root-move splitting, aspiration-window skew, or TT ageing, so
parallel scaling past a handful of threads is modest.
- **Synchronous search.** `stop` is a no-op and there is no pondering, so the
engine cannot think on the opponent's clock or bail out of a bad time
allocation.
Expand All @@ -65,7 +74,7 @@ until [the strength-testing harness](#measuring-it-properly) produces real data.

| Reference engine | ~CCRL blitz | Relevant comparison |
| ---------------- | ----------- | ------------------- |
| TSCP 1.81 | ~1700 | Similar search shape (alpha-beta, iterative deepening, quiescence, MVV-LVA) **plus** a transposition table. goChess should land at or just below TSCP until the TT lands. |
| TSCP 1.81 | ~1700 | Similar search shape (alpha-beta, iterative deepening, quiescence, MVV-LVA, transposition table). goChess should now land near TSCP, with the missing pruning heuristics still costing rating. |
| Sungorus 1.4 | ~2000 | TT + null-move + PVS + killers. Clearly stronger than goChess today. |
| CT800 / Claudia class | ~2100+ | Full modern pruning set. Out of reach without the roadmap features. |

Expand All @@ -84,30 +93,30 @@ depth is unusually shallow for its speed — each extra ply is high-value.
| Bullet (1+0) | ~1350–1550 | Depth 4–6; positional weaknesses dominate. |
| Blitz (3+2 / 5+0) | ~1500–1750 | Depth 6–9; the headline estimate. |
| Rapid (15+10) | ~1650–1900 | Depth 9–12; tactics get sharper, eval ceiling starts to bite. |
| Classical (40/40) | ~1750–2000 | Depth-limited by the missing TT more than by the clock. |
| Classical (40/40) | ~1750–2000 | Depth-limited by the missing pruning heuristics more than by the clock. |

## Where the number would move

Rough, independent Elo deltas from the [roadmap](../README.md#roadmap),
assuming each is implemented competently and validated by SPRT:

| Change | Estimated Elo |
| ------ | ------------- |
| Transposition table + hash-move ordering | +150 to +250 |
| Killer moves + history heuristic | +50 to +100 |
| Null-move pruning | +50 to +80 |
| Late move reductions | +50 to +100 |
| Aspiration windows | +10 to +30 |
| Game-phase eval interpolation (tapered eval) | +30 to +60 |
| Passed pawns / king safety / mobility terms | +40 to +80 |
| Texel-tuned evaluation weights | +40 to +80 |
| Opening book (Polyglot) | +20 to +40 at short TC |
| Syzygy tablebase probing | +10 to +20 |
| Lazy SMP (8 threads) | +100 to +150 |

Landing the transposition table, killers/history, null-move and LMR together
would plausibly put goChess in the 1950–2150 range; adding tapered/tuned eval
and Lazy SMP on top targets 2300+.
| Change | Estimated Elo | Status |
| ------ | ------------- | ------ |
| Transposition table + hash-move ordering | +150 to +250 | done, pending SPRT |
| Lazy SMP (8 threads) | +100 to +150 | done (basic), pending SPRT |
| Killer moves + history heuristic | +50 to +100 | |
| Null-move pruning | +50 to +80 | |
| Late move reductions | +50 to +100 | |
| Aspiration windows | +10 to +30 | |
| Game-phase eval interpolation (tapered eval) | +30 to +60 | |
| Passed pawns / king safety / mobility terms | +40 to +80 | |
| Texel-tuned evaluation weights | +40 to +80 | |
| Opening book (Polyglot) | +20 to +40 at short TC | |
| Syzygy tablebase probing | +10 to +20 | |

With the transposition table and Lazy SMP in, adding killers/history, null-move
and LMR would plausibly put goChess in the 1950–2150 range; tapered/tuned eval
on top targets 2300+.

## Measuring it properly

Expand Down
Loading