# SME (SAN Kit Minimal Engine)

Minimal CECP engine based on SAN Kit.

## Opening book

If two files named `white.txt` and `black.txt` are present in the
working directory when the engine starts, it plays known opening
lines out of them instantly (no search at all) for as long as the
actual game keeps matching a line either one records, falling back
to the normal search the moment it doesn't.

**Format.** Each file is a single tree of moves in coordinate
notation, written as nested parenthesised groups with no whitespace:
`(move child child ...)`. Several parenthesised groups at the same
nesting level are alternative moves recorded from that same
position — this engine picks one of them at random (`rand()`, the
same generator already seeded once in `main()`) whenever it's this
engine's own turn to play one, so it doesn't play out the exact same
game every time it has a choice, and matches the opponent's actual
move against them when it's their turn instead. A short example,
White's first two options after 1.b3:

```
(b2b3(d7d5(...))(g8f6(...)))
```

`white.txt`'s top level is this engine's own first move, as White;
one level down is the opponent's (Black's) reply; and so on,
alternating. `black.txt` is the mirror image: top level is the
opponent's (White's) first move, one level down is this engine's
own (Black's) reply. Which of the two files actually governs a given
game is decided implicitly by whichever happens first: this engine
being asked to move (meaning it's White, so `white.txt` applies) or
the opponent's move arriving first (meaning it's Black, so
`black.txt` applies) — see `TryBookMove()` and
`BookNoteOpponentMove()`.

Neither file is a standard chess format (not PGN, not EPD); parsing
it is a small hand-written recursive-descent reader
(`BookParseMove()`/`BookParseNodeList()`) matching that grammar
directly, built once at startup into a plain
first-child/next-sibling tree (`BookNodeT`) and kept in memory for
the life of the process.

## Evaluation

`Evaluate()`, the static score `Quiescence()` falls back on once
there's nothing noisy left to look at, is two terms added together,
both from the point of view of whoever is to move:

- **Material**: total White material minus total Black material
  (`cpmv[]`). `cpmv[]` is read directly rather than recomputed from
  the board (`SANSetMaterial()`, a full rescan): the SAN Kit already
  keeps it correct incrementally on every capture and promotion, as
  part of what `SANExecute()`/`SANRetract()` themselves update (see
  `sanmer.c`), so by the time any node is evaluated it's already up
  to date at no extra cost.
- **Centrality**: a small bonus for every knight, bishop, rook, and
  queen (not pawns, not the king — each would need its own separate
  logic this engine doesn't have) sitting closer to the center of
  the board than the opponent's equivalent piece. `Centrality()`
  scores a square from 0 (a corner) to 6 (d4/d5/e4/e5), and
  `CENTRALITY_WEIGHT` (2) turns that into points: a fully centralized
  knight is worth at most 6 × 2 = 12 points, an eighth of one pawn —
  small on purpose, so this term only ever breaks ties between moves
  the material count alone can't tell apart, never overrides it.
  Unlike material, nothing in the SAN Kit tracks this incrementally,
  so computing it means an explicit scan of every square on the
  board at every call — see "Speed" below for what that costs in
  practice.
