Game Rules, Captures, and Serving
Capture Detection
A capture occurs when placing a stone creates a contiguous four-cell sandwich pattern: P O O P in any of 8 directions — the player's stone bookends two consecutive opponent stones. The engine checks this using bitmask operations: for each direction, verify that the two intermediate cells contain opponent stones and the far cell contains the player's stone.
When captures are found, the two middle opponent stones are removed from the board and the capturing player's score increments. Capturing 5 pairs (10 stones total) is an alternative win condition.
Double-Three Detection
A move is forbidden if it simultaneously creates two or more "open threes" — three stones in a line (consecutive or with a single gap) that can become an open four with one move, including non-consecutive patterns like _X_XX_. This is the Renju-style restriction that prevents trivially forced wins (the precise definition follows Renju rules, which is why the engine delegates to CForbiddenPointFinder rather than using simple pattern matching).
The detection uses CForbiddenPointFinder, sourced from renju.se open source, as a direction-based implementation for double-three validation. In minimax/src/gomoku/forbidden/ForbiddenPointFinder.cpp, a point is considered double-three only when it is empty, does not already make five, and creates at least two open-threes.
An important optimization for search: double-three is checked per candidate move only, not as a full board scan. A full-board check at every node would be expensive, so evaluating only the candidate move's local patterns saves search time. During move generation, shouldIncludeMove calls the detection for each specific position:
Source: minimax/src/gomoku/search/Minimax.cpp:79-85
// minimax/src/gomoku/search/Minimax.cpp:79-85
inline bool shouldIncludeMove(Board *board, int col, int row, int player,
bool enableDoubleThreeRestriction) {
if (!enableDoubleThreeRestriction) return true;
if (!Rules::detectDoublethree(*board, col, row, player)) return true;
return false;
}
cppWith double-three restriction enabled, a move detected as double-three is rejected unconditionally. Capture potential does not override the forbidden-move check. In this implementation, the restriction applies to both players (not just Black as in standard Renju rules) — this follows the École 42 assignment specification. The full-board FindForbiddenPoints() method exists in the library but is unused in this project; runtime validation calls only the per-candidate IsDoubleThree() path.
Rules::detectDoublethree (in minimax/src/gomoku/core/Rules.cpp) maps the current bitboard state into CForbiddenPointFinder format, then checks only the candidate coordinate with finder.IsDoubleThree(x, y).
What Worked and What Didn’t
I initially used bitmask pattern matching on bitboards for double-three validation. It was fast, but incorrect in recursive open-three edge cases (an open three can itself become invalid depending on follow-up constraints), which surfaced during AlphaZero cross-checking.
I replaced rule validation with CForbiddenPointFinder from renju.se (see Game Rules), while keeping bitmask-based evaluation where it is both correct and fast. The key lesson was to separate correctness-critical rule logic from performance-oriented scoring logic and apply each method where it fits.
Win Condition
The game ends when either player achieves 5+ stones in a row (gomoku) or captures 5 pairs (10 stones). In the evaluation, a gomoku line that contains stones vulnerable to capture is scored lower ("breakable gomoku") — the opponent might be able to break the line by capturing stones within it before the win is confirmed.
WebSocket Server
The minimax engine runs as a libwebsockets server on the port configured by MINIMAX_PORT (default 8005). The full board state is sent in every JSON request payload (no incremental client-side diffing). However, the search cache (transposition table) is a process-global hash map that persists across requests and is cleared on connection initialization, reset, difficulty changes, and test requests. The deployment serves a single concurrent game session, so process-global scope is sufficient.
On startup, the server initializes Zobrist keys and pre-computes two pairs of evaluation lookup tables (simple + hard, each for Player 1/Player 2; entries total). At request time: parse JSON → construct Board from the payload → select search algorithm by difficulty → run search → return the AI move, updated board, captured stones, and execution time.
The WebSocket protocol specification is documented in WebSocket JSON Protocol (About Project). Both minimax and AlphaZero implement a compatible message format, so the frontend can switch backends by URL.