Position State and Rules
An engine needs to know more than where the pieces are. It must also know whose turn it is, which pieces are giving check, and how the game reached this position. The board, turn-related state, and move history all support search; a board diagram alone is not always enough to determine the result.
This page was checked against official Pikafish commit 1c66b9b. It focuses on how several core files work together. For search methods, see Search Algorithms.
What Does Each File Do?
| File | Main responsibility |
|---|---|
types.h | Defines basic types for pieces, colors, squares, moves, and more. |
bitboard.h | Represents sets of squares and provides common bitwise operations. |
attacks.h/.cpp | Looks up a piece's attacks from its location and the blockers on the board. |
position.h/.cpp | Stores the current position and handles moves, undoing moves, legality, and history-dependent rules. |
movegen.h/.cpp | Uses this information to generate candidate or legal moves as needed. |
These files have separate roles. For example, an attack table answers “Which squares can this horse reach?”, the move generator combines origin and destination squares into candidates, and legality checks reject moves that leave the king in check. Start with the basic types and generation and filtering.
Why Store Several Views of the Same Board?
Position stores a square-indexed board, bitboards grouped by piece type and color, piece counts, and other information. These are not copies of separate boards; they provide convenient ways to answer different questions:
- To ask “What piece is on this square?”, look up
board. - To ask “Where are all Red's chariots?”, combine the color and piece-type bitboards.
- To ask “Which enemy pieces are within this attack range?”, intersect the two sets of squares.
Each bit in a bitboard corresponds to a square. Pikafish uses 128 bits for 90 squares, with sets for ranks, files, board halves, palaces, and more. Attack rules handle blockers for chariots and cannons, horse legs, and elephant eyes, so the generator does not have to reimplement those checks each time.
For attack-table lookups and the Magic and PEXT/PDEP paths, see Bitboards and Attack Tables. Table lookups speed up the computation; they do not change how pieces move.
Sources: position members, the bitboard type, square sets and bitwise operations, attack queries.
Making and Undoing Moves: Save What Is Needed to Return
Search repeatedly tries a move, calculates further, and returns to the starting point. Copying the whole board and recomputing everything each time would be expensive.
Pikafish makes the move on the current Position and creates a StateInfo. It stores hash keys, counters, check information, the move, the captured piece, and other data, with previous pointing to the preceding state. It is not a complete board copy: some fields are copied from the previous step, while others are updated for the new position.
undo_move() puts the moving piece back, restores any captured piece, and returns the state pointer to the previous entry. The old hash keys, counters, and other state are then restored. Undoing a move must restore all relevant state, not just put the visible pieces back. Otherwise, errors can spill into other branches searched later.
Both the game moves supplied to the engine and the temporary variations played during search must preserve the correct sequence of states. Each node sees its own history path. Switching branches must not carry over check information or counters from the branch just left.
Sources: StateInfo, updates when making a move, undoing a move.
Zobrist Hashing: Recognize a Position, Not Store a Game
Zobrist hashing assigns numbers to combinations of piece type and square, then combines the numbers for the pieces on the board and the side to move using XOR. A move only needs to remove the old location, add the new one, and account for captures and the change of turn, without rescanning the board.
This is quick to update because XORing the same number twice cancels it out. The contribution for a piece's old location can be removed directly, without reconstructing the whole board from the result.
The current base key, st->key, is 64 bits and mainly represents piece placement and the side to move. It is not a guaranteed collision-free identifier, nor does it contain every earlier move. The key() used for caching also adjusts for move-count groups and repetition-filter information, but still cannot replace the complete history.
Therefore, “the same hash” does not mean “the same rule outcome under every history.” Adjudicating perpetual check, perpetual chase, and similar cases still requires checking the state chain. For cache reuse, see The Transposition Table.
Sources: preparing Zobrist numbers, building the base key, incremental updates, cache-key adjustments.
Move Legality and Rule Outcomes Are Different Questions
Pseudo-legal candidates already follow the pieces' basic movement rules. Given that premise, legal() mainly checks whether the friendly king remains safe after the move, including whether the kings face each other. It does not recheck every history-dependent rule.
rule_judge() handles repetitions, perpetual check, perpetual chase, move limits, draws for specific material configurations, and related cases. The implementation uses quick filters to avoid irrelevant checks, then examines history and chases when needed. Some repetitions during search supply score bounds rather than immediately returning a final outcome.
Thus, “this move does not expose the king,” “this line triggers a loss under the rules,” and “this line has a poor search score” must not be confused. Nor can repetition always be treated as a draw. See Rules and Search for more detail.
Sources: what legal checks, rule adjudication.
FEN Records a Position, Not the Whole History
FEN can record piece locations, the side to move, and counters. It does not record every earlier check, chase, or repetition. Exporting the current position as FEN and loading that FEN alone does not restore the earlier move history. The current set(fen) reinitializes the position and starting state, then derives the relevant information from the board.
To reproduce a history-dependent issue, keep a known starting point and the subsequent move sequence. UCI's position … moves … does exactly this: it sets the starting point, then plays the moves one by one to build the corresponding state chain. It cannot recover history before that starting point if it was not supplied.
When reporting a rule issue to developers, include the starting FEN, subsequent moves, engine version, and expected ruling. The final board alone is often insufficient to reproduce the problem. Format and position checks during FEN parsing also do not prove that a position can be reached legally from the initial setup.
Sources: FEN initialization, replaying moves from a starting point. For command syntax, see UCI Protocol.
What Does perft Test?
Perft expands legal moves to a specified positive depth and counts the move sequences that reach it. Different move orders count separately even if they reach the same board. Perft does not choose the best move, use NNUE scores, or apply search pruning.
The current perft.h uses MoveList<LEGAL>, do_move(), and undo_move(). At the root, it also reports a separate count for each move, helping locate branches that generate too few or too many moves.
It does not call rule_judge(), so it does not validate every Xiangqi rule. History-dependent checks for perpetual check, perpetual chase, move limits, and similar rules need separate tests. Perft also does not validate best-move selection, scores, or selective search.
| Test | Main question |
|---|---|
| perft | Do move generation, king-safety filtering, and making and undoing moves produce the expected counts? |
| Rule regression cases | Given a starting position and history, is the expected ruling made at the right point? |
| Search regression and game testing | Has search behavior become abnormal, and does a change improve actual performance? |
Matching counts are a valuable check, but not proof that every part of the engine is correct.
Source: the perft implementation.
