AI SDD ยท Episode 3

Feature F0003 โ€” play with Stockfish

The ChessGame series turns competitive: after every human move, the fisher-server asks Stockfish for a reply and plays it back on the board.

๐Ÿง  Methodology: AI SDDยท โ™Ÿ๏ธ Feature F0003-play-with-stockfishยท โœ… Spec, tests & code
Part 1 โ€” Run the engine

Running the Stockfish container

F0003 needs a Stockfish engine to talk to. The quickest way is a ready-made container image โ€” one command and the engine is listening on port 4000.

docker run \
  --name stockfish \
  -p 4000:3000 \
  ghcr.io/samuraitruong/stockfish-docker:14.1 \
  stockfish

That's it โ€” fisher-server now has an engine to query for Black's replies.

Part 2 โ€” The specification

Specifying play-with-stockfish

Episode 2 gave White a legal move. F0003 gives Black an answer. The moment the human plays a valid move, the front end calls a new API and the fisher-server hands the position to Stockfish, plays its reply back on the board, and reports what just happened โ€” all written spec-first, before a line of code.

The new entry point is /opponent-move, exposed by fisher-server and triggered as soon as the user's move is accepted. Its precondition is simple: the board is known, and it is Black to play. From there the back end runs one straight-line flow:

The service answers with three things: the entire position (so the front end can redraw without recomputing), the game status, and the last move Black played. On the front end the reaction mirrors Episode 2 โ€” an immediate smart redraw, the last move highlighted with red- and blue-bordered squares, and both the move and the status written to the log panel.

Claude prompt #1 โ€” author the F0003 spec
/guide-spec-definition-md
  F0003 โ€” play-with-stockfish

## Context
- F0002 is implemented: the white pieces can play (a piece can be moved).
- We now want Stockfish to play the black side automatically, right after
  the user's move.

## Goal
Create a new API, triggered by the front end, that computes and applies
Black's reply via Stockfish.

## 1. New API: opponent-move
- Exposed by fisher-server as a new entry point.
- Triggered by the front end as soon as the user has made a valid move.
- Precondition at call time: the board state is known, and it is Black to play.

## 2. Back-end flow (fisher-server)
1. Receive the request with the board in our standard internal format.
2. Convert the board from our standard format into FEN โ€” right before
   calling Stockfish.
3. Call the Stockfish server to get Black's best move.
4. Rebuild the board from Stockfish's response at the back-end level.
5. Determine the game state โ€” one of: draw, checkmate, check, capture, or
   simple move. Each case must be handled distinctly.
6. Update state: the in-memory board for the ongoing game, and the list of
   captured pieces.
7. Prepare the response โ€” the full resulting position plus the game-state
   indicator.

## 3. Service response
- The entire position (so the front can redraw without recomputing).
- The game status (draw / checkmate / check / capture / simple move).
- The last move played by Black.

## 4. Front-end behaviour
- On receiving the response, immediately redraw the board (smart redraw).
- Highlight the last move using red- and blue-bordered squares.
- In a log/console zone, display the last move.
- Also display the game status.

Drawing the line: this is Black's turn only

The first draft blurred a boundary. Deciding whether any legal move exists โ€” the "no move left" case behind a draw or a mate โ€” isn't the referee's job here; it's the engine's. So a correction pushes that responsibility back to Stockfish, where it belongs. And it's not "a side" in the abstract โ€” it's the Black side specifically, with the boundary stated outright: nothing changes on the White side. Everything F0003 adds concerns Black โ€” the Stockfish reply โ€” and only Black.

Claude prompt #2 โ€” Black only: Stockfish's job, White untouched
/guide-spec-definition-md F0003

Correction: the "Black has no legal move" check shouldn't live in
fisher-server. Determining whether a legal move exists is Stockfish's
responsibility.

It's not "a side" โ€” the Black side specifically.

State explicitly that nothing changes on the White side: everything
here concerns Black (Stockfish) only.

Naming the game states โ€” and who decides "check"

The status enum started out side-agnostic, which reads ambiguously once two colours are in play. So we rename it to say whose king is affected: check becomes white-in-check, checkmate becomes black-in-checkmate, and so on. That rename surfaces a real question โ€” can the Stockfish server even tell whether Black's move puts the White king in check? The answer shapes ownership: white-in-check is something fisher-server must work out itself after rebuilding the board, not something to read off the engine. The last refinement records that, and updates the sequence diagram to match.

Claude prompt #3 โ€” rename the status enum, and let fisher-server own white-in-check
/guide-spec-definition-md F0003

Can you rename the status enum โ€” check -> white-in-check,
checkmate -> black-in-checkmate, and so on?

Note that white-in-check must be determined by fisher-server itself.
Also update the sequence diagram.
Spec first, code later. None of this asked for an implementation yet โ€” only the F0003.md specification, its /opponent-move contract, and the sequence diagram that pins down who does what.

The spec, on GitHub

The finished specification lives beside the code in the feat/F0003-play-with-stockfish branch โ€” generated the AI SDD way and readable in full.

Part 3 โ€” Tests before the code

Testing the reply before the code

With the F0003 spec settled, the tests come next โ€” still before any implementation. The spec becomes a target the tests can pin down, so a passing suite later is the proof that the generated code does what the spec promised.

Two guides run in parallel: the Rust integration-test spec for the back end, and the front-end unit-test spec for the browser behaviour.

Claude prompt #4 โ€” back-end integration tests
/guide-integration-test-rust-md F0003
Claude prompt #5 โ€” front-end unit tests
/guide-front-test-md F0003

Then one concrete end-to-end case, expressed the way a person would play it: start from the standard opening board, push White's first pawn to d4, ask for the opponent move โ€” and check that Black actually replied.

Claude prompt #6 โ€” a real d4 โ†’ opponent-move case
/guide-integration-test-rust-md F0003

Add one more test case: from the standard board, play White's first
move to d4, then request the opponent move and check that it worked.

The test specs, on GitHub

Both test specs โ€” written before a line of implementation โ€” live beside the code in the feat/F0003-play-with-stockfish branch.

Part 4 โ€” Scope review

A scope review before building

Before generating any code, one read-only pass over the spec: is anything missing to make the game actually playable? Asking the question up front is cheaper than discovering the gaps mid-build.

The review does surface loose ends โ€” the kind of end-to-end concerns that turn a working /opponent-move into a full game loop. Rather than let them expand the feature, we name them and park them: everything the review turned up goes into the spec's Out of scope list, written down so nothing is forgotten and the current feature stays tight.

Claude prompt #7 โ€” what's missing to make it playable? (read-only)
(Read-only) Do you see anything missing from the spec that I need to
add to make the game playable?
Claude prompt #8 โ€” park the gaps as Out of scope
/guide-spec-definition-md F0003

For now, put all the points above in the Out of scope list.
Scope is a decision, not an accident. Listing the gaps explicitly keeps them visible for a later feature without pulling them into this one โ€” the human decides what ships now.
Part 5 โ€” Code implementation

Building play-with-stockfish, end to end

Spec and tests in hand, we let the AI implement the feature โ€” the /opponent-move entry point, the FEN conversion, the engine call, and the front-end redraw โ€” then sharpen the types and turn the logs into proof. AI implements, humans decide.

Implement the feature

The first prompt asks for the full implementation of F0003, driven entirely by the specs under _ai/features/F0003-play-with-stockfish/.

Claude prompt #9 โ€” implement F0003
/implement-code F0003

Implement the feature.

Two type questions

With the code in front of us, two small representation choices stand out โ€” and they're posed as questions, not orders, with the AI free to push back. Could promo: Option<char> use the existing Piece enum instead of a raw char? And could OpponentReply { from, to } carry Square values instead of Strings? Both reuse types the domain already owns, so the move logic matches on types rather than characters and raw strings.

Claude prompt #10 โ€” could these be stronger types?
Two type questions (feel free to say no if it's not a good idea):
* promo: Option<char> โ€” could we use Piece instead of char?
* OpponentReply { from: Option<String>, to: Option<String> } โ€” is
  there a better type than String (e.g. Square)?

The answer was yes on both counts, so the follow-up asks for exactly that โ€” a behaviour-preserving refactor with the tests kept green.

Claude prompt #11 โ€” apply the stronger types
/implement-code F0003

Do what you suggested above.

Turn the logs into proof

Finally, we bring the F0003 logs into line with the proof-log model from Episode 2 โ€” every step of the /opponent-move flow logged with the proof-log macros, so a whole engine reply reads back as one traceable thread from request to served.

Claude prompt #12 โ€” proof logs for F0003
/implement-proof-logs F0003

Turn the feature's logs into proof logs.
The AI SDD loop, once more. Spec, then tests, then a deliberate scope call, then code โ€” and even the type cleanup went through a question before a change. The engine now answers back on the board, and every step of its reply leaves a proof line behind.
Part 6 โ€” The result

See it: the game is playable

Spec, tests, scope, code โ€” and here's the payoff. With F0003 in place the loop closes: you make a move, Stockfish answers on the board, and the log records every step. The game is now fully playable end to end.

A live game against Stockfish. Each White move triggers /opponent-move; Black replies instantly, the last move is highlighted, and the log panel narrates the exchange.
From spec to playable game. Everything in this video was built the AI SDD way โ€” written down first, tested against the spec, then implemented. Go ahead and play it.