# HG changeset patch # User Mike Becker # Date 1785431858 -7200 # Node ID 2a6d83f4677e05c3cf6c459d8eac5f3d5078ab6e # Parent f0fc70b6f8f9bf03b5f5c91fe54006fb06e0b22e major refactoring of rules API - validate_move() no longer alters the game state (fixes #935) - get_threats() now correctly only reports threats while get_candidates() reports possible moves (fixes #959) - qualify GameState and Move parameters as const where possible diff -r f0fc70b6f8f9 -r 2a6d83f4677e src/chess/bishop.c --- a/src/chess/bishop.c Thu Jul 30 18:34:15 2026 +0200 +++ b/src/chess/bishop.c Thu Jul 30 19:17:38 2026 +0200 @@ -31,11 +31,11 @@ #include "rules.h" #include -bool bishop_chkrules(Move* move) { +bool bishop_chkrules(const Move* move) { return abs(move->torow-move->fromrow) == abs(move->tofile-move->fromfile); } -bool bishop_isblocked(GameState *gamestate, Move *move) { +bool bishop_isblocked(const GameState *gamestate, const Move *move) { int dy = move->torow > move->fromrow ? 1 : -1; int dx = move->tofile > move->fromfile ? 1 : -1; diff -r f0fc70b6f8f9 -r 2a6d83f4677e src/chess/bishop.h --- a/src/chess/bishop.h Thu Jul 30 18:34:15 2026 +0200 +++ b/src/chess/bishop.h Thu Jul 30 19:17:38 2026 +0200 @@ -36,8 +36,8 @@ extern "C" { #endif -bool bishop_chkrules(Move *move); -bool bishop_isblocked(GameState *gamestate, Move *move); +bool bishop_chkrules(const Move *move); +bool bishop_isblocked(const GameState *gamestate, const Move *move); #ifdef __cplusplus } diff -r f0fc70b6f8f9 -r 2a6d83f4677e src/chess/fen.c --- a/src/chess/fen.c Thu Jul 30 18:34:15 2026 +0200 +++ b/src/chess/fen.c Thu Jul 30 19:17:38 2026 +0200 @@ -42,7 +42,7 @@ str[i++] = '0'+skip; skip = 0; } - switch (gamestate->board[row][file] & ~ENPASSANT_THREAT) { + switch (piece_at(gamestate, row, file)) { case WHITE|KING: str[i++] = 'K'; break; case WHITE|QUEEN: str[i++] = 'Q'; break; case WHITE|BISHOP: str[i++] = 'B'; break; @@ -120,11 +120,11 @@ str[0] = '-'; for (int file = 0 ; file < 8 ; file++) { - if (gamestate->board[3][file] & ENPASSANT_THREAT) { + if (enpassant_threat_exists(gamestate, 3, file)) { str[0] = filechr(file); str[1] = rowchr(2); } - if (gamestate->board[4][file] & ENPASSANT_THREAT) { + if (enpassant_threat_exists(gamestate, 4, file)) { str[0] = filechr(file); str[1] = rowchr(5); } @@ -137,7 +137,7 @@ unsigned int hm = 0; for (unsigned int i = 0; i < gamestate->movecount; i++) { if (gamestate->moves[i].capture - || (gamestate->moves[i].piece & PIECE_MASK) == PAWN) { + || piece_type(gamestate->moves[i].piece) == PAWN) { hm = 0; } else { hm++; diff -r f0fc70b6f8f9 -r 2a6d83f4677e src/chess/king.c --- a/src/chess/king.c Thu Jul 30 18:34:15 2026 +0200 +++ b/src/chess/king.c Thu Jul 30 19:17:38 2026 +0200 @@ -32,7 +32,9 @@ #include -static bool king_castling_chkmoved(GameState *gamestate, Row row, File file) { +static bool king_castling_chkmoved( + const GameState *gamestate, Row row, File file) { + for (unsigned i = 0; i < gamestate->movecount; i++) { if (gamestate->moves[i].fromfile == file && gamestate->moves[i].fromrow == row) { @@ -43,7 +45,7 @@ return false; } -bool king_chkrules(GameState *gamestate, Move* move) { +bool king_chkrules(const GameState *gamestate, const Move* move) { if (abs(move->torow - move->fromrow) <= 1 && abs(move->tofile - move->fromfile) <= 1) { return true; @@ -64,7 +66,7 @@ } } -bool king_isblocked(GameState *gamestate, Move *move) { +bool king_isblocked(const GameState *gamestate, const Move *move) { uint8_t opponent_color = opponent_color(piece_color(move->piece)); @@ -79,7 +81,7 @@ uint8_t midfile = (move->tofile+move->fromfile)/2; bool incheck = false; if (gamestate->movecount > 0) { - incheck = gamestate->moves[gamestate->movecount-1].check; + incheck = is_check_position(gamestate); } blocked |= incheck || gamestate->board[move->torow][midfile] || is_covered(gamestate, move->torow, midfile, opponent_color); diff -r f0fc70b6f8f9 -r 2a6d83f4677e src/chess/king.h --- a/src/chess/king.h Thu Jul 30 18:34:15 2026 +0200 +++ b/src/chess/king.h Thu Jul 30 19:17:38 2026 +0200 @@ -37,8 +37,8 @@ extern "C" { #endif -bool king_chkrules(GameState *gamestate, Move *move); -bool king_isblocked(GameState *gamestate, Move *move); +bool king_chkrules(const GameState *gamestate, const Move *move); +bool king_isblocked(const GameState *gamestate, const Move *move); #ifdef __cplusplus } diff -r f0fc70b6f8f9 -r 2a6d83f4677e src/chess/knight.c --- a/src/chess/knight.c Thu Jul 30 18:34:15 2026 +0200 +++ b/src/chess/knight.c Thu Jul 30 19:17:38 2026 +0200 @@ -31,7 +31,7 @@ #include "rules.h" #include -bool knight_chkrules(Move *move) { +bool knight_chkrules(const Move *move) { int dx = abs(move->fromfile - move->tofile); int dy = abs(move->fromrow - move->torow); diff -r f0fc70b6f8f9 -r 2a6d83f4677e src/chess/knight.h --- a/src/chess/knight.h Thu Jul 30 18:34:15 2026 +0200 +++ b/src/chess/knight.h Thu Jul 30 19:17:38 2026 +0200 @@ -36,7 +36,7 @@ extern "C" { #endif -bool knight_chkrules(Move *move); +bool knight_chkrules(const Move *move); #define knight_isblocked(gs,m) false #ifdef __cplusplus diff -r f0fc70b6f8f9 -r 2a6d83f4677e src/chess/pawn.c --- a/src/chess/pawn.c Thu Jul 30 18:34:15 2026 +0200 +++ b/src/chess/pawn.c Thu Jul 30 19:17:38 2026 +0200 @@ -30,7 +30,7 @@ #include "pawn.h" #include "rules.h" -bool pawn_chkrules(GameState *gamestate, Move *move) { +bool pawn_chkrules(const GameState *gamestate, const Move *move) { int8_t d = piece_color(move->piece) == WHITE ? -1 : 1; if (move->torow == (d < 0 ? 7 : 0)) { @@ -69,7 +69,7 @@ } } -bool pawn_isblocked(GameState *gamestate, Move *move) { +bool pawn_isblocked(const GameState *gamestate, const Move *move) { if (move->torow == move->fromrow + 1 || move->torow == move->fromrow - 1) { return piece_at(gamestate, mdst(move)) && !move->capture; } else { diff -r f0fc70b6f8f9 -r 2a6d83f4677e src/chess/pawn.h --- a/src/chess/pawn.h Thu Jul 30 18:34:15 2026 +0200 +++ b/src/chess/pawn.h Thu Jul 30 19:17:38 2026 +0200 @@ -36,8 +36,8 @@ extern "C" { #endif -bool pawn_chkrules(GameState *gamestate, Move *move); -bool pawn_isblocked(GameState *gamestate, Move *move); +bool pawn_chkrules(const GameState *gamestate, const Move *move); +bool pawn_isblocked(const GameState *gamestate, const Move *move); #ifdef __cplusplus } diff -r f0fc70b6f8f9 -r 2a6d83f4677e src/chess/pgn.c --- a/src/chess/pgn.c Thu Jul 30 18:34:15 2026 +0200 +++ b/src/chess/pgn.c Thu Jul 30 19:17:38 2026 +0200 @@ -147,11 +147,12 @@ } } while (!isspace(c = *(pgndata++))); movestr[i] = '\0'; - if (eval_move(gamestate, movestr, &move, curcol) + if (eval_move(gamestate, movestr, curcol, &move) != VALID_MOVE_SYNTAX) { return(pgn_error_move_syntax); } - if (validate_move(gamestate, &move) != VALID_MOVE_SEMANTICS) { + int move_validate_result = validate_move(gamestate, &move); + if (move_validate_result != VALID_MOVE_SEMANTICS) { return(pgn_error_move_semantics); } apply_move(gamestate, &move); diff -r f0fc70b6f8f9 -r 2a6d83f4677e src/chess/queen.c --- a/src/chess/queen.c Thu Jul 30 18:34:15 2026 +0200 +++ b/src/chess/queen.c Thu Jul 30 19:17:38 2026 +0200 @@ -32,11 +32,11 @@ #include "bishop.h" #include "queen.h" -bool queen_chkrules(Move* move) { +bool queen_chkrules(const Move* move) { return bishop_chkrules(move) || rook_chkrules(move); } -bool queen_isblocked(GameState *gamestate, Move *move) { +bool queen_isblocked(const GameState *gamestate, const Move *move) { if (rook_chkrules(move)) { return rook_isblocked(gamestate, move); } else { diff -r f0fc70b6f8f9 -r 2a6d83f4677e src/chess/queen.h --- a/src/chess/queen.h Thu Jul 30 18:34:15 2026 +0200 +++ b/src/chess/queen.h Thu Jul 30 19:17:38 2026 +0200 @@ -36,8 +36,8 @@ extern "C" { #endif -bool queen_chkrules(Move *move); -bool queen_isblocked(GameState *gamestate, Move *move); +bool queen_chkrules(const Move *move); +bool queen_isblocked(const GameState *gamestate, const Move *move); #ifdef __cplusplus } diff -r f0fc70b6f8f9 -r 2a6d83f4677e src/chess/rook.c --- a/src/chess/rook.c Thu Jul 30 18:34:15 2026 +0200 +++ b/src/chess/rook.c Thu Jul 30 19:17:38 2026 +0200 @@ -30,11 +30,11 @@ #include "rules.h" #include "rook.h" -bool rook_chkrules(Move *move) { +bool rook_chkrules(const Move *move) { return move->torow == move->fromrow || move->tofile == move->fromfile; } -bool rook_isblocked(GameState *gamestate, Move *move) { +bool rook_isblocked(const GameState *gamestate, const Move *move) { if (move->torow == move->fromrow) { int d = move->tofile > move->fromfile ? 1 : -1; diff -r f0fc70b6f8f9 -r 2a6d83f4677e src/chess/rook.h --- a/src/chess/rook.h Thu Jul 30 18:34:15 2026 +0200 +++ b/src/chess/rook.h Thu Jul 30 19:17:38 2026 +0200 @@ -36,8 +36,8 @@ extern "C" { #endif -bool rook_chkrules(Move *move); -bool rook_isblocked(GameState *gamestate, Move *move); +bool rook_chkrules(const Move *move); +bool rook_isblocked(const GameState *gamestate, const Move *move); #ifdef __cplusplus } diff -r f0fc70b6f8f9 -r 2a6d83f4677e src/chess/rules.c --- a/src/chess/rules.c Thu Jul 30 18:34:15 2026 +0200 +++ b/src/chess/rules.c Thu Jul 30 19:17:38 2026 +0200 @@ -79,7 +79,7 @@ gamestate->movecount = gamestate->movecapacity = 0; } -static GameState gamestate_copy_sim(GameState *gamestate) { +static GameState gamestate_copy_sim(const GameState *gamestate) { GameState simulation = *gamestate; /* create new move and position lists for the simulation */ @@ -102,12 +102,12 @@ return simulation; } -Color current_color(GameState *gamestate) { +Color current_color(const GameState *gamestate) { return (gamestate->movecount % 2 == 0) ? WHITE : BLACK; } /* MUST be called BETWEEN validating AND applying a move to work correctly */ -static void format_move(GameState *gamestate, Move *move) { +static void format_move(const GameState *gamestate, Move *move) { char *string = &(move->string[0]); /* at least 8 characters should be available, wipe them out */ @@ -136,18 +136,18 @@ } } else if (piece_type(move->piece) != KING) { /* resolve ambiguities, if any */ - Move threats[16]; - size_t threatcount; - if (get_threats(gamestate, move->torow, move->tofile, - piece_color(move->piece), threats, &threatcount)) { + Move candidates[16]; + size_t ccount; + if (get_candidates(gamestate, move->torow, move->tofile, + piece_color(move->piece), candidates, &ccount)) { unsigned int ambrows = 0, ambfiles = 0, ambpiece = 0; - for (size_t i = 0 ; i < threatcount ; i++) { - if (threats[i].piece == move->piece) { + for (size_t i = 0 ; i < ccount ; i++) { + if (candidates[i].piece == move->piece) { ambpiece++; - if (threats[i].fromrow == move->fromrow) { + if (candidates[i].fromrow == move->fromrow) { ambrows++; } - if (threats[i].fromfile == move->fromfile) { + if (candidates[i].fromfile == move->fromfile) { ambfiles++; } } @@ -187,8 +187,10 @@ } /* check? */ - if (move->check) { - string[idx++] = gamestate->checkmate?'#':'+'; + if (move->checkmate) { + string[idx++] = '#'; + } else if (move->check) { + string[idx++] = '+'; } } @@ -269,15 +271,7 @@ } } -// TODO: check if we really need this "simulate" flag for performance -static void apply_move_impl(GameState *gamestate, Move *move, bool simulate) { - /* format move before moving (s.t. ambiguities can be resolved) */ - if (!simulate) { - if (!move->string[0]) { - format_move(gamestate, move); - } - } - +void apply_move(GameState *gamestate, Move *move) { /* en passant capture */ if (move->capture && piece_type(move->piece) == PAWN && piece_at(gamestate, mdst(move)) == 0) { @@ -342,13 +336,12 @@ /* important: only "add" the move after calculating the time! */ gamestate->movecount++; + + /* did this move checkmate the other king? */ + gamestate->checkmate = move->checkmate; } -void apply_move(GameState *gamestate, Move *move) { - apply_move_impl(gamestate, move, false); -} - -void gamestate_at_move(GameState *gamestate, +void gamestate_at_move(const GameState *gamestate, unsigned move_number, GameState *replay) { gamestate_init(replay); memcpy(&replay->info, &gamestate->info, sizeof(GameInfo)); @@ -357,11 +350,137 @@ move_number = gamestate->movecount; } for (unsigned i = 0 ; i < move_number ; i++) { - apply_move_impl(replay, &(gamestate->moves[i]), true); + apply_move(replay, &(gamestate->moves[i])); } } -static int validate_move_rules(GameState *gamestate, Move *move) { +/* return 0 = no check, 1 = check, 2 = checkmate */ +static int determine_check_or_checkmate( + const GameState *gamestate, const Move *move) { + + /* simulate the move */ + GameState simulation = gamestate_copy_sim(gamestate); + Move simmove = *move; + apply_move(&simulation, &simmove); + + /* find the opposing king */ + Color piececolor = piece_color(move->piece); + Color oppcolor = opponent_color(piececolor); + File opkingfile = 0; + Row opkingrow = 0; + for (Row row = 0 ; row < 8 ; row++) { + for (File file = 0 ; file < 8 ; file++) { + Piece p = piece_at(&simulation, row, file); + if (p == mkpiece(KING, oppcolor)) { + opkingfile = file; + opkingrow = row; + } + } + } + + /* determine if the opposing king is now threatened */ + Move threats[16]; + size_t threatcount; + bool incheck = get_threats(&simulation, opkingrow, opkingfile, + piececolor, threats, &threatcount); + + if (!incheck) { + gamestate_cleanup(&simulation); + return 0; + } + + /* determine possible escape fields */ + bool canescape = false; + for (int dr = -1 ; dr <= 1 && !canescape ; dr++) { + for (int df = -1 ; df <= 1 && !canescape ; df++) { + if (dr == 0 && df == 0) continue; + Row er = opkingrow + dr; + File ef = opkingfile + df; + if (!isidx(er) || !isidx(ef)) continue; + + /* check if piece of the king's color blocks the field */ + if (piece_color(simulation.board[er][ef]) == oppcolor) + continue; + + /* check if escape field is already covered (threatened) */ + if (is_covered(&simulation, er, ef, piececolor)) + continue; + + /* check if an attacking piece blocks the field */ + if (piece_color(simulation.board[er][ef]) == piececolor) { + /* test if the king can fight back */ + GameState sim_retaliate = gamestate_copy_sim(&simulation); + Move move_retaliate = {0}; + move_retaliate.piece = mkpiece(KING, oppcolor); + move_retaliate.fromrow = opkingrow; + move_retaliate.fromfile = opkingfile; + move_retaliate.torow = er; + move_retaliate.tofile = ef; + move_retaliate.capture = true; + apply_move(&sim_retaliate, &move_retaliate); + canescape = !is_covered(&sim_retaliate, er, ef, piececolor); + gamestate_cleanup(&sim_retaliate); + continue; + } + + /* the field is not covered and unoccupied */ + canescape = true; + } + } + + /* can't escape, can the king be rescued? */ + if (!canescape && threatcount == 1) { + canescape = is_protected(&simulation, + threats[0].fromrow, threats[0].fromfile, oppcolor); + } + + /* can't capture, can he block? */ + if (!canescape && threatcount == 1) { + Move *threat = &(threats[0]); + unsigned tptype = piece_type(threat->piece); + + /* knight, pawns and the king cannot be blocked */ + if (tptype == BISHOP || tptype == ROOK || tptype == QUEEN) { + if (threat->fromrow == threat->torow) { + /* rook aspect (on row) */ + int d = threat->tofile > threat->fromfile ? 1 : -1; + File file = threat->fromfile; + while (!canescape && file != threat->tofile - d) { + file += d; + canescape |= is_protected(&simulation, + threat->torow, file, oppcolor); + } + } else if (threat->fromfile == threat->tofile) { + /* rook aspect (on file) */ + int d = threat->torow > threat->fromrow ? 1 : -1; + Row row = threat->fromrow; + while (!canescape && row != threat->torow - d) { + row += d; + canescape |= is_protected(&simulation, + row, threat->tofile, oppcolor); + } + } else { + /* bishop aspect */ + int dr = threat->torow > threat->fromrow ? 1 : -1; + int df = threat->tofile > threat->fromfile ? 1 : -1; + + Row row = threat->fromrow; + File file = threat->fromfile; + while (!canescape && file != threat->tofile - df + && row != threat->torow - dr) { + row += dr; + file += df; + canescape |= is_protected(&simulation, row, file, + oppcolor); + } + } + } + } + gamestate_cleanup(&simulation); + return canescape ? 1 : 2; +} + +static int validate_move_rules(const GameState *gamestate, const Move *move) { assert((move->piece & ~(PIECE_MASK|COLOR_MASK)) == 0); /* validate indices (don't trust opponent) */ @@ -369,12 +488,12 @@ !isidx(move->torow) || !isidx(move->tofile)) { return INVALID_MOVE_SYNTAX; } - + /* must move */ if (move->fromfile == move->tofile && move->fromrow == move->torow) { return INVALID_MOVE_SYNTAX; } - + /* does piece exist */ if (piece_at(gamestate, msrc(move)) != move->piece) { return PIECE_NOT_FOUND; @@ -382,7 +501,7 @@ /* is there any piece at the destination? */ Piece piece_at_dst = piece_at(gamestate, mdst(move)); - + /* can't capture own pieces */ if (piece_color(piece_at_dst) == piece_color(move->piece)) { return RULES_VIOLATED; @@ -397,188 +516,100 @@ return INVALID_MOVE_SYNTAX; } } - + /* validate individual rules */ bool chkrules; switch (piece_type(move->piece)) { - case PAWN: - chkrules = pawn_chkrules(gamestate, move) && - !pawn_isblocked(gamestate, move); - break; - case ROOK: - chkrules = rook_chkrules(move) && - !rook_isblocked(gamestate, move); - break; - case KNIGHT: - chkrules = knight_chkrules(move); /* knight is never blocked */ - break; - case BISHOP: - chkrules = bishop_chkrules(move) && - !bishop_isblocked(gamestate, move); - break; - case QUEEN: - chkrules = queen_chkrules(move) && - !queen_isblocked(gamestate, move); - break; - case KING: - chkrules = king_chkrules(gamestate, move) && - !king_isblocked(gamestate, move); - break; - default: - return INVALID_MOVE_SYNTAX; + case PAWN: + chkrules = pawn_chkrules(gamestate, move) && + !pawn_isblocked(gamestate, move); + break; + case ROOK: + chkrules = rook_chkrules(move) && + !rook_isblocked(gamestate, move); + break; + case KNIGHT: + chkrules = knight_chkrules(move) && + !knight_isblocked(gamestate, move); + break; + case BISHOP: + chkrules = bishop_chkrules(move) && + !bishop_isblocked(gamestate, move); + break; + case QUEEN: + chkrules = queen_chkrules(move) && + !queen_isblocked(gamestate, move); + break; + case KING: + chkrules = king_chkrules(gamestate, move) && + !king_isblocked(gamestate, move); + break; + default: + return INVALID_MOVE_SYNTAX; + } + + /* cancel processing to save resources */ + if (!chkrules) { + return RULES_VIOLATED; } - return chkrules ? VALID_MOVE_SEMANTICS : RULES_VIOLATED; -} - -int validate_move(GameState *gamestate, Move *move) { - - int result = validate_move_rules(gamestate, move); - - /* cancel processing to save resources */ - if (result != VALID_MOVE_SEMANTICS) { - return result; - } - - /* simulate move for check validation */ + /* test if the move would expose our own king */ GameState simulation = gamestate_copy_sim(gamestate); Move simmove = *move; - apply_move_impl(&simulation, &simmove, true); - - /* find kings for check validation */ + apply_move(&simulation, &simmove); Color piececolor = piece_color(move->piece); Color oppcolor = opponent_color(piececolor); - - File mykingfile = 0, opkingfile = 0; - Row mykingrow = 0, opkingrow = 0; + File kingfile = 0; + Row kingrow = 0; for (Row row = 0 ; row < 8 ; row++) { for (File file = 0 ; file < 8 ; file++) { Piece p = piece_at(&simulation, row, file); if (p == mkpiece(KING, piececolor)) { - mykingfile = file; - mykingrow = row; - } else if (p == mkpiece(KING, oppcolor)) { - opkingfile = file; - opkingrow = row; + kingfile = file; + kingrow = row; } } } - - /* don't move into or stay in check position */ - if (is_covered(&simulation, mykingrow, mykingfile, oppcolor)) { + int result = VALID_MOVE_SEMANTICS; + if (is_covered(&simulation, kingrow, kingfile, oppcolor)) { gamestate_cleanup(&simulation); if (piece_type(move->piece) == KING) { - return KING_MOVES_INTO_CHECK; + result = KING_MOVES_INTO_CHECK; } else { - if (gamestate->moves[gamestate->movecount - 1].check) { - return KING_IN_CHECK; + if (is_check_position(gamestate)) { + result = KING_IN_CHECK; } else { - return PIECE_PINNED; + result = PIECE_PINNED; } } } - - /* correct check and checkmate flags (move is still valid) */ - Move threats[16]; - size_t threatcount; - move->check = get_threats(&simulation, opkingrow, opkingfile, - piececolor, threats, &threatcount); - - if (move->check) { - /* determine possible escape fields */ - bool canescape = false; - for (int dr = -1 ; dr <= 1 && !canescape ; dr++) { - for (int df = -1 ; df <= 1 && !canescape ; df++) { - if (dr == 0 && df == 0) continue; - if (!isidx(opkingrow + dr)) continue; - if (!isidx(opkingfile + df)) continue; - Row er = opkingrow + dr; - File ef = opkingfile + df; + gamestate_cleanup(&simulation); - /* check if escape field is already covered (threatened) */ - if (is_covered(&simulation, er, ef, piececolor)) - continue; + return result; +} - /* check if piece of the king's color blocks the field */ - if (piece_color(simulation.board[er][ef]) == oppcolor) - continue; - - /* check if an attacking piece blocks the field */ - if (piece_color(simulation.board[er][ef]) == piececolor) { - /* test if the king can fight back */ - GameState sim_retaliate = gamestate_copy_sim(&simulation); - Move move_retaliate = {0}; - move_retaliate.piece = mkpiece(KING, oppcolor); - move_retaliate.fromrow = opkingrow; - move_retaliate.fromfile = opkingfile; - move_retaliate.torow = er; - move_retaliate.tofile = ef; - move_retaliate.capture = 1; - apply_move_impl(&sim_retaliate, &move_retaliate, true); - canescape = !is_covered(&sim_retaliate, er, ef, piececolor); - gamestate_cleanup(&sim_retaliate); - } else { - /* the field is not covered and unoccupied */ - canescape = true; - } - } - } +int validate_move(const GameState *gamestate, const Move *move) { + int result = validate_move_rules(gamestate, move); + if (result != VALID_MOVE_SEMANTICS) { + return result; + } - /* can't escape, can the king be rescued? */ - if (!canescape && threatcount == 1) { - canescape = is_protected(&simulation, - threats[0].fromrow, threats[0].fromfile, oppcolor); + /* validate check and checkmate flags */ + int cocm = determine_check_or_checkmate(gamestate, move); + if (cocm == 2) { + if (!move->checkmate) { + return MISSING_CHECKMATE; } - - /* can't capture, can he block? */ - if (!canescape && threatcount == 1) { - Move *threat = &(threats[0]); - unsigned tptype = piece_type(threat->piece); - - /* knight, pawns and the king cannot be blocked */ - if (tptype == BISHOP || tptype == ROOK || tptype == QUEEN) { - if (threat->fromrow == threat->torow) { - /* rook aspect (on row) */ - int d = threat->tofile > threat->fromfile ? 1 : -1; - File file = threat->fromfile; - while (!canescape && file != threat->tofile - d) { - file += d; - canescape |= is_protected(&simulation, - threat->torow, file, oppcolor); - } - } else if (threat->fromfile == threat->tofile) { - /* rook aspect (on file) */ - int d = threat->torow > threat->fromrow ? 1 : -1; - Row row = threat->fromrow; - while (!canescape && row != threat->torow - d) { - row += d; - canescape |= is_protected(&simulation, - row, threat->tofile, oppcolor); - } - } else { - /* bishop aspect */ - int dr = threat->torow > threat->fromrow ? 1 : -1; - int df = threat->tofile > threat->fromfile ? 1 : -1; + } else if (cocm == 1) { + if (!move->check) { + return MISSING_CHECK; + } + } else if (move->checkmate) { + return INVALID_CHECKMATE; + } else if (move->check) { + return INVALID_CHECK; + } - Row row = threat->fromrow; - File file = threat->fromfile; - while (!canescape && file != threat->tofile - df - && row != threat->torow - dr) { - row += dr; - file += df; - canescape |= is_protected(&simulation, row, file, - oppcolor); - } - } - } - } - - if (!canescape) { - gamestate->checkmate = true; - } - } - - gamestate_cleanup(&simulation); return VALID_MOVE_SEMANTICS; } @@ -591,16 +622,16 @@ gamestate->board[row][file] = piece; } -bool get_threats(GameState *gamestate, Row row, File file, - Color color, Move *threats, size_t *threatcount) { +bool get_candidates(const GameState *gamestate, Row row, File file, + Color color, Move *moves, size_t *movecount) { Move candidates[32]; size_t ccount = 0; for (Row r = 0 ; r < 8 ; r++) { for (File f = 0 ; f < 8 ; f++) { - if (piece_color(gamestate->board[r][f]) == color) { + Piece p = piece_at(gamestate, r, f); + if (piece_color(p) == color) { /* non-capturing move */ memset(&(candidates[ccount]), 0, sizeof(Move)); - Piece p = piece_at(gamestate, r, f); candidates[ccount].piece = p; candidates[ccount].fromrow = r; candidates[ccount].fromfile = f; @@ -615,7 +646,55 @@ /* capturing move */ memcpy(&(candidates[ccount]), &(candidates[ccount-1]), sizeof(Move)); - candidates[ccount].capture = 1; + candidates[ccount].capture = true; + ccount++; + } + } + } + + if (movecount) { + *movecount = 0; + } + + bool result = false; + + for (size_t i = 0 ; i < ccount ; i++) { + if (validate_move_rules(gamestate, &(candidates[i])) + == VALID_MOVE_SEMANTICS) { + result = true; + if (moves && movecount) { + moves[(*movecount)++] = candidates[i]; + } + } + } + + return result; +} + +bool get_threats(const GameState *gamestate, Row row, File file, + Color color, Move *threats, size_t *threatcount) { + + /* simulate a capturing move on the target position */ + Color opcolor = opponent_color(color); + GameState simulation = gamestate_copy_sim(gamestate); + if (piece_color(piece_at(&simulation, row, file)) != opcolor) { + /* set a fake pawn if the field is not occupied by the opponent */ + piece_set(&simulation, row, file, mkpiece(PAWN, opcolor)); + } + + Move candidates[16]; + size_t ccount = 0; + for (Row r = 0 ; r < 8 ; r++) { + for (File f = 0 ; f < 8 ; f++) { + Piece p = piece_at(&simulation, r, f); + if (piece_color(p) == color) { + memset(&(candidates[ccount]), 0, sizeof(Move)); + candidates[ccount].piece = p; + candidates[ccount].fromrow = r; + candidates[ccount].fromfile = f; + candidates[ccount].torow = row; + candidates[ccount].tofile = file; + candidates[ccount].capture = true; ccount++; } } @@ -625,11 +704,10 @@ *threatcount = 0; } - bool result = false; for (size_t i = 0 ; i < ccount ; i++) { - if (validate_move_rules(gamestate, &(candidates[i])) + if (validate_move_rules(&simulation, &(candidates[i])) == VALID_MOVE_SEMANTICS) { result = true; if (threats && threatcount) { @@ -637,11 +715,13 @@ } } } + + gamestate_cleanup(&simulation); return result; } -bool is_pinned(GameState *gamestate, Move *move) { +bool is_pinned(const GameState *gamestate, const Move *move) { Color color = piece_color(move->piece); GameState simulation = gamestate_copy_sim(gamestate); @@ -666,7 +746,7 @@ return covered; } -bool get_real_threats(GameState *gamestate, Row row, File file, +bool get_real_threats(const GameState *gamestate, Row row, File file, Color color, Move *threats, size_t *threatcount) { if (threatcount) { @@ -710,48 +790,48 @@ } } -static int getlocation(GameState *gamestate, Move *move) { +static int getlocation(const GameState *gamestate, Move *move) { Color color = piece_color(move->piece); bool incheck = false; if (gamestate->movecount > 0) { - incheck = gamestate->moves[gamestate->movecount - 1].check; + incheck = is_check_position(gamestate); } - Move threats[16], *threat = NULL; - size_t threatcount; + Move candidates[16], *candidate = NULL; + size_t candidatecount; - /* determine all threats and sort out the unreal threats on our own */ - if (get_threats(gamestate, move->torow, move->tofile, color, - threats, &threatcount)) { + /* determine all candidate moves and sort out the invalid ones */ + if (get_candidates(gamestate, move->torow, move->tofile, color, + candidates, &candidatecount)) { bool found = false; - /* find threats for the specified position */ - for (size_t i = 0 ; i < threatcount ; i++) { - if (threats[i].piece == move->piece && + for (size_t i = 0 ; i < candidatecount ; i++) { + /* filter by partial fromrow/fromfile information */ + if (candidates[i].piece == move->piece && (move->fromrow == POS_UNSPECIFIED || - move->fromrow == threats[i].fromrow) && + move->fromrow == candidates[i].fromrow) && (move->fromfile == POS_UNSPECIFIED || - move->fromfile == threats[i].fromfile)) { + move->fromfile == candidates[i].fromfile)) { - /* found a threat, here it does not matter if it's valid! */ + /* found a candidate, here it does not matter if it's valid! */ found = true; /* discard pinned pieces */ - if (!is_pinned(gamestate, &(threats[i]))) { - if (threat) { - /* we've already found a valid threat */ + if (!is_pinned(gamestate, &(candidates[i]))) { + if (candidate) { + /* we've already found a valid candidate */ return AMBIGUOUS_MOVE; } else { - threat = &(threats[i]); + candidate = &(candidates[i]); } } } } - /* can't threaten specified position */ - if (!threat) { + /* no valid candidate left */ + if (!candidate) { if (found) { if (piece_type(move->piece) == KING) { return KING_MOVES_INTO_CHECK; @@ -765,9 +845,9 @@ } } - /* found a threat, copy the source location */ - move->fromrow = threat->fromrow; - move->fromfile = threat->fromfile; + /* found a candidate, copy the source location */ + move->fromrow = candidate->fromrow; + move->fromfile = candidate->fromfile; return VALID_MOVE_SYNTAX; } else { return PIECE_NOT_FOUND; @@ -790,6 +870,7 @@ if (mstr[len-1] == '+' || mstr[len-1] == '#') { len--; mstr[len] = '\0'; move->check = true; + move->checkmate = mstr[len-1] == '#'; } /* evaluate promotion */ @@ -829,7 +910,7 @@ } if (mstr[1] == 'x') { /* capture (e.g. "Nxf3", "dxe5") */ - move->capture = 1; + move->capture = true; } else { /* move (e.g. "Ndf3", "N2c3", "e2e4") */ if (isfile(mstr[1])) { @@ -855,7 +936,7 @@ } else { move->piece = getpiece(mstr[0], color); if (mstr[2] == 'x') { - move->capture = 1; + move->capture = true; if (move->piece) { /* capture (e.g. "Ndxf3" or "R1xh3") */ if (isfile(mstr[1])) { @@ -882,7 +963,7 @@ } else if (len == 6) { /* long notation capture (e.g. "Nc5xf3") */ if (mstr[3] == 'x') { - move->capture = 1; + move->capture = true; move->piece = getpiece(mstr[0], color); move->fromfile = fileidx(mstr[1]); move->fromrow = rowidx(mstr[2]); @@ -914,24 +995,47 @@ return VALID_MOVE_SYNTAX; } -int eval_move(GameState *gamestate, const char *mstr, - Move *move, Color color) { +int eval_move2(const GameState *gamestate, + const char *mstr, Color color, Move *move, bool lazy) { int result = eval_move1(mstr, move, color); if (result == VALID_MOVE_SYNTAX) { if (move->fromfile == POS_UNSPECIFIED || move->fromrow == POS_UNSPECIFIED) { - return getlocation(gamestate, move); + result = getlocation(gamestate, move); + } + if (result == VALID_MOVE_SYNTAX) { + /* correct check/checkmate flags */ + if (lazy) { + move->check = move->checkmate = false; + switch (determine_check_or_checkmate(gamestate, move)) { + case 2: move->checkmate = true; + case 1: move->check = true; + } + } + + /* format the move string */ + format_move(gamestate, move); } } return result; } +int eval_move_lazy(const GameState *gamestate, + const char *mstr, Color color, Move *move) { + return eval_move2(gamestate, mstr, color, move, true); +} + +int eval_move(const GameState *gamestate, + const char *mstr, Color color, Move *move) { + return eval_move2(gamestate, mstr, color, move, false); +} + int check_move(const char *mstr, Color color) { Move move; return eval_move1(mstr, &move, color); } -bool is_protected(GameState *gamestate, Row row, File file, Color color) { +bool is_protected(const GameState *gamestate, Row row, File file, Color color) { Move threats[16]; size_t threatcount; if (get_real_threats(gamestate, row, file, color, threats, &threatcount)) { @@ -946,7 +1050,7 @@ } } -uint16_t remaining_movetime(GameState *gamestate, Color color) { +uint16_t remaining_movetime(const GameState *gamestate, Color color) { unsigned move_number = gamestate->movecount; if (color == BLACK) { move_number |= 1; @@ -956,7 +1060,7 @@ return remaining_movetime2(gamestate, move_number); } -uint16_t remaining_movetime2(GameState *gamestate, unsigned move_number) { +uint16_t remaining_movetime2(const GameState *gamestate, unsigned move_number) { if (!gamestate->info.timecontrol) { return 0; } @@ -1023,7 +1127,7 @@ } } -bool check_threefold_repetition(GameState *gamestate) { +bool check_threefold_repetition(const GameState *gamestate) { // TODO: implement threefold repetition detection return false; } diff -r f0fc70b6f8f9 -r 2a6d83f4677e src/chess/rules.h --- a/src/chess/rules.h Thu Jul 30 18:34:15 2026 +0200 +++ b/src/chess/rules.h Thu Jul 30 19:17:38 2026 +0200 @@ -43,7 +43,11 @@ #define PIECE_PINNED 5 #define KING_IN_CHECK 6 #define KING_MOVES_INTO_CHECK 7 -#define RULES_VIOLATED 10 +#define MISSING_CHECK 8 +#define MISSING_CHECKMATE 9 +#define INVALID_CHECK 10 +#define INVALID_CHECKMATE 11 +#define RULES_VIOLATED 32 #define ENPASSANT_THREAT 0x40u @@ -83,7 +87,7 @@ struct movetimeval { uint64_t tv_sec; - int32_t tv_usec; // important that this is signed b/c potential carry + int32_t tv_usec; /* important that this is signed b/c potential carry */ }; typedef struct { @@ -92,9 +96,11 @@ Row fromrow; File tofile; Row torow; - uint8_t promotion; - uint8_t check; + Piece promotion; + uint8_t check; /* must always be set if checkmate is set */ + uint8_t checkmate; uint8_t capture; + uint8_t padding[7]; /* necessary for stable ABI across networks */ struct movetimeval timestamp; struct movetimeval movetime; char string[8]; @@ -189,7 +195,13 @@ } static inline bool is_game_running(const GameState *gamestate) { - return !(gamestate->checkmate || gamestate->wresign || gamestate->bresign || gamestate->threefold || gamestate->stalemate || gamestate->remis || gamestate->review); + return !(gamestate->checkmate || gamestate->wresign || gamestate->bresign + || gamestate->threefold || gamestate->stalemate || gamestate->remis + || gamestate->review); +} + +static inline bool is_check_position(const GameState *gamestate) { + return gamestate->moves[gamestate->movecount - 1].check; } /** @@ -242,7 +254,7 @@ * @param gamestate the current game state * @return the color of the player who is next to move */ -Color current_color(GameState *gamestate); +Color current_color(const GameState *gamestate); /** * Returns the piece at the specified position. @@ -276,7 +288,29 @@ } /** - * Checks, if a specified field is covered by a piece of a certain color. + * Determines a list of possible moves to the specified field. + * + * The out-parameters may both be NULL, but if any of them is set, the other + * must be set, too. + * + * @param gamestate the current game state + * @param row row of the field to check + * @param file file of the field to check + * @param color the color of the piece that should move to the field + * @param moves the array where to store the moves + * (must be large enough, 16 is always enough) + * @param movecount a pointer where the number of moves is stored + * @return true, if any piece of the specified color can move to the specified + * field + */ +bool get_candidates(const GameState *gamestate, Row row, File file, + Color color, Move* moves, size_t* movecount); + +/** + * Checks, if a specified field is threatened by a piece of a certain color. + * + * A field is threatened, if there is a piece of the specified color that could + * capture an opponent piece on this field, regardless of being pinned. * * The out-parameters may both be NULL, but if any of them is set, the other * must be set, too. @@ -285,17 +319,17 @@ * @param row row of the field to check * @param file file of the field to check * @param color the color of the piece that should threaten the field - * @param threats the array where to store the threats (should be able to hold - * the rare maximum of 16 elements) + * @param threats the array where to store the threats + * (must be large enough, 16 is always enough) * @param threatcount a pointer where the count of threats is stored * @return true, if any piece of the specified color threatens the specified - * field (i.e. could capture an opponent piece) + * field */ -bool get_threats(GameState *gamestate, Row row, File file, +bool get_threats(const GameState *gamestate, Row row, File file, Color color, Move* threats, size_t* threatcount); /** - * Checks, if a specified field is covered by a piece of a certain color AND + * Checks, if a specified field is threatened by a piece of a certain color AND * if this piece is not pinned and therefore able to perform the move. * * The out-parameters may both be NULL, but if any of them is set, the other @@ -305,17 +339,20 @@ * @param row row of the field to check * @param file file of the field to check * @param color the color of the piece that should threaten the field - * @param threats the array where to store the threats (should be able to hold - * the rare maximum of 16 elements) + * @param threats the array where to store the threats + * (must be large enough, 16 is always enough) * @param threatcount a pointer where the count of threats is stored * @return true, if any piece of the specified color threatens the specified - * field (i.e. could capture an opponent piece) + * field and is not pinned */ -bool get_real_threats(GameState *gamestate, Row row, File file, +bool get_real_threats(const GameState *gamestate, Row row, File file, Color color, Move* threats, size_t* threatcount); /** - * Checks, if a specified field is covered by a piece of a certain color. + * Checks, if a specified field is threatened by a piece of a certain color. + * + * A field is threatened, if there is a piece of the specified color that could + * capture an opponent piece on this field, regardless of being pinned. * * @param gamestate the current game state * @param row row of the field to check @@ -330,7 +367,7 @@ /** * Checks, if a specified field is attacked by a piece of a certain color. * - * I.e. the field is covered by a piece AND this piece is not pinned and + * I.e. the field is threatened by a piece AND this piece is not pinned and * therefore able to perform the move. * * @param gamestate the current game state @@ -346,8 +383,8 @@ /** * Checks, if a specified field is protected by a piece of a certain color. * - * I.e. the field is covered by a piece that is NOT the king AND this piece is - * not pinned and therefore able to perform the move. + * This is the same as is_attacked(), but only considers pieces that are not + * the king. * * @param gamestate the current game state * @param row row of the field to check @@ -356,7 +393,7 @@ * @return true, if any piece (excluding the king) of the specified color * threatens the specified field and could capture an opponent piece */ -bool is_protected(GameState *gamestate, Row row, File file, Color color); +bool is_protected(const GameState *gamestate, Row row, File file, Color color); /** * Checks, if the specified move cannot be performed, because the piece is @@ -372,25 +409,51 @@ * @return true, if the move cannot be performed because the king would be in * check after the move */ -bool is_pinned(GameState *gamestate, Move *move); +bool is_pinned(const GameState *gamestate, const Move *move); /** * Evaluates a move syntactically and stores the move data in the specified * object. * * When short algebraic notation is used, the source position is determined by - * the evaluating the allowed moves according to the current game state. + * evaluating the allowed moves according to the current game state. + * + * This function expects correct notation of check and checkmate indicators. + * For a more lazy evaluation, use eval_move_lazy(). * * For a purely syntactic check, regardless of whether a piece exists that is * allowed to move that way, use check_move(). * * @param gamestate the current game state * @param mstr the input string to parse + * @param color the color of the player to evaluate the move for * @param move a pointer to object where the move data shall be stored - * @param color the color of the player to evaluate the move for * @return status code (see macros in this file for the list of codes) */ -int eval_move(GameState *gamestate, const char *mstr, Move *move, Color color); +int eval_move(const GameState *gamestate, + const char *mstr, Color color, Move *move); + +/** + * Evaluates a move syntactically and stores the move data in the specified + * object. + * + * When short algebraic notation is used, the source position is determined by + * evaluating the allowed moves according to the current game state. + * + * This function automatically corrects missing or incorrect check/checkmate + * indicators. Use eval_move() if you want to keep the original notation. + * + * For a purely syntactic check, regardless of whether a piece exists that is + * allowed to move that way, use check_move(). + * + * @param gamestate the current game state + * @param mstr the input string to parse + * @param color the color of the player to evaluate the move for + * @param move a pointer to object where the move data shall be stored + * @return status code (see macros in this file for the list of codes) + */ +int eval_move_lazy(const GameState *gamestate, + const char *mstr, Color color, Move *move); /** * Syntactically checks a move without verifying that a piece exists that is @@ -408,7 +471,7 @@ * @param move the move to validate * @return status code (see macros in this file for the list of codes) */ -int validate_move(GameState *gamestate, Move *move); +int validate_move(const GameState *gamestate, const Move *move); /** * Applies a move and deletes captured pieces. @@ -427,7 +490,7 @@ * @param move_number the half-move that would now be played * @param replay the struct to populate with the state at the specified move */ -void gamestate_at_move(GameState *gamestate, +void gamestate_at_move(const GameState *gamestate, unsigned move_number, GameState *replay); /** @@ -439,7 +502,7 @@ * @return the remaining time - if time control is disabled, this function * always returns zero */ -uint16_t remaining_movetime2(GameState *gamestate, unsigned move_number); +uint16_t remaining_movetime2(const GameState *gamestate, unsigned move_number); /** * Returns the remaining time on the clock for the specified player. @@ -449,7 +512,7 @@ * @return the remaining time - if time control is disabled, this function * always returns zero */ -uint16_t remaining_movetime(GameState *gamestate, Color color); +uint16_t remaining_movetime(const GameState *gamestate, Color color); /** * Converts clock time to string. @@ -472,7 +535,7 @@ * @param gamestate the current game state * @return true if the game is in a threefold repetition position */ -bool check_threefold_repetition(GameState *gamestate); +bool check_threefold_repetition(const GameState *gamestate); #endif /* RULES_H */ diff -r f0fc70b6f8f9 -r 2a6d83f4677e src/main.c --- a/src/main.c Thu Jul 30 18:34:15 2026 +0200 +++ b/src/main.c Thu Jul 30 19:17:38 2026 +0200 @@ -355,7 +355,7 @@ } } -static void eval_move_failed_msg(int code) { +static void eval_move_failed_msg(uint32_t code) { switch (code) { case AMBIGUOUS_MOVE: printw("Ambiguous move - please specify the piece to move."); @@ -381,6 +381,18 @@ case KING_MOVES_INTO_CHECK: printw("Can't move the king into a check position."); break; + case MISSING_CHECK: + printw("Move does not indicate check."); + break; + case MISSING_CHECKMATE: + printw("Move does not indicate checkmate."); + break; + case INVALID_CHECK: + printw("Move incorrectly indicates check."); + break; + case INVALID_CHECKMATE: + printw("Move incorrectly indicates checkmate."); + break; default: printw("Unknown move parser error."); } @@ -461,7 +473,8 @@ /* ignore empty move strings and ask again */ } else { Move move; - int result = eval_move(gamestate, movestr, &move, curcolor); + int result = eval_move_lazy(gamestate, + movestr, curcolor, &move); if (result == VALID_MOVE_SYNTAX) { result = validate_move(gamestate, &move); if (result == VALID_MOVE_SEMANTICS) { @@ -596,13 +609,14 @@ /* ignore empty move strings and ask again */ } else { Move move; - int eval_result = eval_move(gamestate, movestr, &move, mycolor); + int eval_result = eval_move_lazy(gamestate, + movestr, mycolor, &move); switch (eval_result) { case VALID_MOVE_SYNTAX: net_send_data(opponent, NETCODE_MOVE, &move, sizeof(Move)); code = net_recieve_code(opponent); - move.check = code == NETCODE_CHECK || - code == NETCODE_CHECKMATE; + /* we could validate the move's check/checkmate flag with + * the network response code, but we choose not to do it */ gamestate->checkmate = code == NETCODE_CHECKMATE; gamestate->stalemate = code == NETCODE_STALEMATE; if (code == NETCODE_DECLINE) { diff -r f0fc70b6f8f9 -r 2a6d83f4677e src/network.h --- a/src/network.h Thu Jul 30 18:34:15 2026 +0200 +++ b/src/network.h Thu Jul 30 19:17:38 2026 +0200 @@ -54,7 +54,7 @@ #define NETCODE_CONNLOST 0x80 #define NETCODE_ERROR 0xFF -#define NETCODE_VERSION 20 +#define NETCODE_VERSION 21 typedef struct { int fd; /* -1, if we are the client */