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.
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:
- Receive the board in our standard internal format.
-
Convert it to
FENโ done right before the engine call. - Call the Stockfish server for Black's best move.
- Rebuild the board from Stockfish's reply, back-end side.
-
Determine the game state โ
draw,checkmate,check,capture, or asimple moveโ each handled distinctly. - Update state โ the in-memory board for the ongoing game, and the list of captured pieces.
- Prepare the response โ the full resulting position plus the game-state indicator.
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.
/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.
/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.
/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.
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.
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.
/guide-integration-test-rust-md F0003
/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.
/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.
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.
(Read-only) Do you see anything missing from the spec that I need to
add to make the game playable?
/guide-spec-definition-md F0003
For now, put all the points above in the Out of scope list.
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/.
/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.
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.
/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.
/implement-proof-logs F0003
Turn the feature's logs into proof logs.
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.
/opponent-move; Black replies
instantly, the last move is highlighted, and the log
panel narrates the exchange.