Skip to content
Merged
25 changes: 23 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,37 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
a shared lock-free transposition table, Lazy SMP, and a hard time budget.
- `internal/engine`: search heuristics — killer moves + a `[side][from][to]`
history table in move ordering, null-move pruning, and late move reductions.
- `internal/engine`: principal variation search with a triangular PV table,
aspiration windows from depth 5, and a fail-soft alpha-beta.
- `internal/engine`: richer Lazy SMP — a 6-bit transposition-table generation
that ages out the previous search's entries, plus per-helper root-move and
aspiration-window skew.
- `internal/engine`: tapered PeSTO evaluation (middlegame/endgame material and
piece-square tables interpolated by game phase) with passed-pawn, mobility,
king-safety and tempo terms, guarded by a colour-mirror symmetry test.
- `internal/engine`: hand-rolled Polyglot opening book — `PolyglotKey`,
`OpenBook`, `Book.Probe`.
- `internal/uci`: a UCI protocol loop (`uci`, `isready`, `ucinewgame`,
`position`, `go`, `stop`, `quit`) supporting `go depth`, `go movetime`, and
`go wtime/btime`.
`position`, `go`, `stop`, `ponderhit`, `quit`) supporting `go depth`,
`go movetime`, `go wtime/btime/winc/binc`, `go infinite`, and `go ponder`.
Search runs on a goroutine: `stop` and pondering work, `info … pv …` is
streamed per iteration, and `bestmove` carries a `ponder` move.
- `internal/uci`: `Ponder`, `OwnBook` and `BookFile` options.
- `cmd/gochess`: CLI with `uci`, `perft`, `bench`, and `version` subcommands.
- Repository scaffolding: CI, lint, CodeQL and release workflows; issue and PR
templates; `CODEOWNERS`; Dependabot; `Makefile`; `Dockerfile`; GoReleaser.
- `docs/adr/0003`: record deferring Syzygy tablebases (no pure-Go prober; the
build is strictly `CGO_ENABLED=0`).

### Changed

- Replaced the `notnil/chess` prototype (random mover + perft) with the
`dragontoothmg`-based engine.
- Evaluation is now tapered (PeSTO) rather than a single Michniewski
piece-square table set; move choices and reported scores shift accordingly.

### Removed

- Retired the Go Report Card badge from the README.

[Unreleased]: https://github.com/cjunius/goChess/commits/main
28 changes: 18 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
[![CI](https://github.com/cjunius/goChess/actions/workflows/ci.yml/badge.svg)](https://github.com/cjunius/goChess/actions/workflows/ci.yml)
[![Lint](https://github.com/cjunius/goChess/actions/workflows/lint.yml/badge.svg)](https://github.com/cjunius/goChess/actions/workflows/lint.yml)
[![Go Reference](https://pkg.go.dev/badge/github.com/cjunius/goChess.svg)](https://pkg.go.dev/github.com/cjunius/goChess)
[![Go Report Card](https://goreportcard.com/badge/github.com/cjunius/goChess)](https://goreportcard.com/report/github.com/cjunius/goChess)
[![License: GPL v3](https://img.shields.io/badge/License-GPLv3-blue.svg)](LICENSE)

A UCI chess engine written in Go, built on the
Expand All @@ -15,13 +14,16 @@ move generator.
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,
a shared transposition table with hash-move ordering, MVV-LVA + killer-move +
history move ordering, null-move pruning, late move reductions, Lazy SMP
(multi-threaded search), hard time limits.

Not yet implemented: aspiration windows / PVS, opening book, endgame tablebases.
- **Evaluation** — tapered PeSTO material + piece-square tables, bishop pair,
passed pawns, mobility, king safety, tempo.
- **Search** — iterative deepening with aspiration windows, principal variation
search, quiescence search, a shared aged transposition table with hash-move
ordering, MVV-LVA + killer-move + history move ordering, null-move pruning,
late move reductions, Lazy SMP (multi-threaded search), asynchronous search
with working `stop` and pondering, hard time limits.
- **Opening book** — optional Polyglot `.bin` book.

Not yet implemented: Syzygy endgame tablebases, static exchange evaluation.
See the [roadmap](#roadmap).

## Install
Expand Down Expand Up @@ -65,6 +67,9 @@ go movetime 1000
|---|---|---|---|
| `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). |
| `Ponder` | false | check | Let a GUI drive `go ponder` / `ponderhit` so the engine thinks on the opponent's clock. |
| `OwnBook` | false | check | Play from the Polyglot book when the position is in it. |
| `BookFile` | — | string | Path to a Polyglot `.bin` book; loaded when set. |

## Development

Expand Down Expand Up @@ -97,8 +102,11 @@ docs/ architecture notes and ADRs
- [x] Lazy SMP — multi-threaded search over the shared TT (`Threads` UCI option)
- [x] Killer moves + history heuristic
- [x] Null-move pruning + late move reductions
- [ ] Aspiration windows / principal variation search
- [ ] Opening book (Polyglot) and Syzygy tablebase probing
- [x] Aspiration windows + principal variation search
- [x] Tapered PeSTO evaluation + pawn/mobility/king-safety terms
- [x] Asynchronous search — working `stop` and pondering
- [x] Opening book (Polyglot)
- [ ] Syzygy tablebase probing ([ADR 0003](docs/adr/0003-defer-syzygy-tablebases.md))
- [ ] Strength testing harness (SPRT via cutechess-cli)

## License
Expand Down
48 changes: 48 additions & 0 deletions docs/adr/0003-defer-syzygy-tablebases.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# 3. Defer Syzygy endgame tablebases

Date: 2026-08-30

## Status

Accepted

## Context

Syzygy tablebases give perfect play for positions with few pieces: WDL
(win/draw/loss) tables probed inside the search and DTZ (distance-to-zero) tables
probed at the root to pick a move that makes progress under the fifty-move rule.
They are a meaningful strength gain in endgames and are on the design backlog.

Adding them now runs into two problems:

1. **No mature pure-Go prober.** The reference implementations (Fathom,
`syzygy1/probetool`) are C. The only Go options are thin CGO wrappers around
Fathom or unmaintained partial ports.
2. **The project is strictly `CGO_ENABLED=0`.** The Docker build, the release
pipeline (`.goreleaser.yaml`) and the cross-compilation matrix all assume a
static, cgo-free binary. Introducing CGO would fragment the build and the
release artifacts.

A correct pure-Go WDL+DTZ prober is roughly two thousand lines of intricate code
(the RE-PAIR decompression and the Syzygy indexing scheme) that is hard to test
without tablebase files in CI. It deserves its own focused change, not a rider on
a large search/eval batch.

## Decision

Defer Syzygy tablebase support. Ship the rest of the backlog batch (PVS,
aspiration windows, richer Lazy SMP, tapered evaluation, pondering, Polyglot
book) without it.

When it is picked up, the preferred approach is a **pure-Go prober** (keeping
`CGO_ENABLED=0`), starting with WDL-only probing in the search and adding the
DTZ root probe afterwards. A CGO/Fathom binding gated behind a build tag is the
fallback if the pure-Go effort proves too large.

## Consequences

- Endgame play stays at search + tapered-eval strength; no perfect play with
≤ 7 pieces.
- `SearchParams` has no tablebase hook yet; adding one later is additive.
- The UCI layer will need `SyzygyPath` (and probably `SyzygyProbeDepth` /
`SyzygyProbeLimit`) options when the feature lands.
52 changes: 32 additions & 20 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,45 +34,57 @@ unapply closure), FEN parsing, an incrementally-updated Zobrist hash

- **perft.go** — thin wrappers over `dragontoothmg.Perft` plus timed series and
divide helpers. Backed by regression tests against published node counts.
- **eval.go** — `Evaluate(*Board) int`. Material values + Michniewski
piece-square tables + a bishop-pair bonus. Returns a score relative to the
side to move (negamax convention). Piece-square lookups use little-endian
rank-file indexing: white reads `pst[sq]`, black reads `pst[sq^56]`.
- **eval.go / eval_terms.go** — `Evaluate(*Board) int`. Tapered PeSTO material +
piece-square tables interpolated by game phase, plus bishop-pair, passed-pawn,
mobility, king-safety and tempo terms (each an `(mg, eg)` pair). Returns a
score relative to the side to move (negamax convention). Piece-square tables
are stored a8-first: white reads `pst[sq^56]`, black reads `pst[sq]`.
- **search.go** — `Search(*Board, SearchParams) SearchResult`. Iterative
deepening around a negamax alpha-beta core, with:
deepening around a fail-soft negamax alpha-beta core, with:
- principal variation search (full window for move 0, null-window scout +
re-search for the rest) and a triangular PV table,
- aspiration windows from depth 5, widening the failing side,
- quiescence search at the horizon (captures and promotions only),
- transposition-table probes/stores with hash-move ordering,
- move ordering: TT move, promotions, MVV-LVA captures, two killer moves
per ply, then quiet moves by a `[side][from][to]` history score,
- null-move pruning (skipped in check, at shallow depth, with only pawns
left, or right after another null move; the pass board is built through
FEN so the shared TT never sees a stale hash),
- late move reductions — late quiet moves are searched a ply or two
shallower and re-searched at full depth only if they beat alpha,
- null-move pruning and late move reductions,
- 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.
board copy over one shared TT, with per-helper root-move and aspiration
skew; the deepest completed result wins,
- `SearchParams.Stop` / `Ponder` / `PonderHit` / `Info` for asynchronous
driving: a hard wall-clock budget checked every 2048 nodes (ignored while
pondering), an external abort flag, and a per-iteration callback.
- **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.
miss. `data` packs move, int32 score, depth, bound flag and a 6-bit
generation; `NewSearch` bumps the generation so `store` treats the previous
search's entries as replaceable.
- **polyglot.go** — `PolyglotKey`, `OpenBook`, `Book.Probe`: the Polyglot book
Zobrist key (distinct from `Board.Hash`) and a reader for `.bin` book files.

### `internal/uci`

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.
`wtime`/`btime`/`winc`/`binc`, `infinite`, `ponder`), `stop`, `ponderhit`, `d`,
and `quit`. It owns the persistent `engine.TT` (sized by the `Hash` option,
cleared on `ucinewgame`) and the `Threads` / `Ponder` settings.

Search runs on its own goroutine: `go` returns immediately, `engine.Search`
streams `info` lines through a callback and the goroutine emits `bestmove`
(with a `ponder` move from the PV) once the search ends. `stop` aborts it;
`ponderhit` converts a pondering search onto its time budget; a mutex serialises
all writes to stdout. `quit` / EOF wait for a bounded search a GUI is blocking on
and abort anything infinite.

## Deliberately not here yet

Aspiration windows, principal variation search, static exchange evaluation,
search extensions, opening book, tablebases, pondering, `SearchMoves`/`MultiPV`.
Static exchange evaluation, search extensions, opening book, tablebases,
`SearchMoves`/`MultiPV`.

## Key invariants

Expand Down
Loading