--- a/src/chess/rules.c Sat Sep 05 12:36:04 2026 +0200 +++ b/src/chess/rules.c Sat Sep 05 13:03:09 2026 +0200 @@ -144,7 +144,242 @@ } } -static bool check_stalemate(const GameState *gamestate) { + +/* applies the move without recalculating gamestate flags */ +static void apply_move_internal(GameState *gamestate, Move *move) { + /* en passant capture */ + if (move->capture && piece_type(move->piece) == PAWN && + piece_at(gamestate, move->tofile, move->torank) == 0) { + piece_remove(gamestate, move->tofile, move->fromrank); + } + + /* remove old en passant threats */ + for (File file = 0 ; file < 8 ; file++) { + enpassant_threat_remove(gamestate, file, 3); + enpassant_threat_remove(gamestate, file, 4); + } + + /* move (and maybe capture or promote) */ + piece_remove(gamestate, move->fromfile, move->fromrank); + if (move->promotion) { + piece_set(gamestate, move->tofile, move->torank, move->promotion); + } else { + piece_set(gamestate, move->tofile, move->torank, move->piece); + } + + /* add new en passant threat */ + if (piece_type(move->piece) == PAWN && ( + (move->fromrank == 1 && move->torank == 3) || + (move->fromrank == 6 && move->torank == 4))) { + enpassant_threat_add(gamestate, move->tofile, move->torank); + } + + /* castling */ + bool castling_happend = false; + if (piece_type(move->piece) == KING && move->fromfile == fileidx('e')) { + const Color color = piece_color(move->piece); + if (move->tofile == fileidx('g')) { + gamestate->board[move->torank][fileidx('h')] = 0; + gamestate->board[move->torank][fileidx('f')] = mkpiece(ROOK, color); + castling_happend = true; + } else if (move->tofile == fileidx('c')) { + gamestate->board[move->torank][fileidx('a')] = 0; + gamestate->board[move->torank][fileidx('d')] = mkpiece(ROOK, color); + castling_happend = true; + } + } + if (castling_happend) { + if (piece_color(move->piece) == WHITE) { + gamestate->castling.K = true; + gamestate->castling.Q = true; + } else { + gamestate->castling.k = true; + gamestate->castling.q = true; + } + } else { + if (piece_color(move->piece) == WHITE) { + if (move->fromrank == rankidx('1')) { + if (move->fromfile == fileidx('e')) { + gamestate->castling.K = gamestate->castling.Q = true; + } else if (move->fromfile == fileidx('h')) { + gamestate->castling.K = true; + } else if (move->fromfile == fileidx('a')) { + gamestate->castling.Q = true; + } + } + } else { + if (move->fromrank == rankidx('8')) { + if (move->fromfile == fileidx('e')) { + gamestate->castling.k = gamestate->castling.q = true; + } else if (move->fromfile == fileidx('h')) { + gamestate->castling.k = true; + } else if (move->fromfile == fileidx('a')) { + gamestate->castling.q = true; + } + } + } + } + + /* add move to the moves array and the new position to the FEN array */ + if (gamestate->movecount == gamestate->movecapacity) { + gamestate->movecapacity += 64; /* 32 more full moves */ + gamestate->moves = realloc(gamestate->moves, + gamestate->movecapacity * sizeof(Move)); + gamestate->fen = realloc(gamestate->fen, + gamestate->movecapacity * sizeof(char*)); + } + + /* copy the move data into the game's move array */ + Move *melem = &gamestate->moves[gamestate->movecount]; + *melem = *move; + calc_movetime(gamestate, melem); + + /* important: only "add" the move after calculating the time! */ + gamestate->movecount++; + + /* calculate the FEN of the new position and store it in the FEN array */ + char fen[FEN_MAX_LENGTH]; + fen_compute(fen, gamestate); + gamestate->fen[gamestate->movecount - 1] = strdup(fen); +} + +/* return 0 = no check, 1 = check, 2 = checkmate */ +static int determine_check_or_checkmate( + const GameState *gamestate, const Move *move) { + + /* either simulate one more move or check for current state */ + Color piececolor, oppcolor; + GameState simulation = gamestate_copy_sim(gamestate); + if (move != NULL) { + piececolor = piece_color(move->piece); + oppcolor = opponent_color(piececolor); + Move simmove = *move; + apply_move_internal(&simulation, &simmove); + } else { + oppcolor = current_color(gamestate); + piececolor = opponent_color(oppcolor); + } + + /* find the opposing king */ + File opkingfile = 0; + Rank opkingrank = 0; + for (Rank rank = 0 ; rank < 8 ; rank++) { + for (File file = 0 ; file < 8 ; file++) { + Piece p = piece_at(&simulation, file, rank); + if (p == mkpiece(KING, oppcolor)) { + opkingfile = file; + opkingrank = rank; + } + } + } + + /* determine if the opposing king is now threatened */ + Move threats[16]; + size_t threatcount; + bool incheck = get_threats(&simulation, opkingfile, opkingrank, + 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; + Rank er = opkingrank + 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, ef, er, 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.fromrank = opkingrank; + move_retaliate.fromfile = opkingfile; + move_retaliate.torank = er; + move_retaliate.tofile = ef; + move_retaliate.capture = true; + apply_move_internal(&sim_retaliate, &move_retaliate); + canescape = !is_covered(&sim_retaliate, ef, er, 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].fromfile, threats[0].fromrank, 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->fromrank == threat->torank) { + /* rook aspect (on rank) */ + int d = threat->tofile > threat->fromfile ? 1 : -1; + File file = threat->fromfile; + while (!canescape && file != threat->tofile - d) { + file += d; + canescape |= is_protected(&simulation, + file, threat->torank, oppcolor); + } + } else if (threat->fromfile == threat->tofile) { + /* rook aspect (on file) */ + int d = threat->torank > threat->fromrank ? 1 : -1; + Rank rank = threat->fromrank; + while (!canescape && rank != threat->torank - d) { + rank += d; + canescape |= is_protected(&simulation, + threat->tofile, rank, oppcolor); + } + } else { + /* bishop aspect */ + int dr = threat->torank > threat->fromrank ? 1 : -1; + int df = threat->tofile > threat->fromfile ? 1 : -1; + + Rank rank = threat->fromrank; + File file = threat->fromfile; + while (!canescape && file != threat->tofile - df + && rank != threat->torank - dr) { + rank += dr; + file += df; + canescape |= is_protected(&simulation, + file, rank, oppcolor); + } + } + } + } + gamestate_cleanup(&simulation); + return canescape ? 1 : 2; +} + +bool check_checkmate(const GameState *gamestate) { + return determine_check_or_checkmate(gamestate, NULL) == 2; +} + +bool check_stalemate(const GameState *gamestate) { Color next_player = current_color(gamestate); /* scan the board for pieces of the next player's color */ @@ -313,104 +548,6 @@ } } -/* applies the move without recalculating gamestate flags */ -static void apply_move_internal(GameState *gamestate, Move *move) { - /* en passant capture */ - if (move->capture && piece_type(move->piece) == PAWN && - piece_at(gamestate, move->tofile, move->torank) == 0) { - piece_remove(gamestate, move->tofile, move->fromrank); - } - - /* remove old en passant threats */ - for (File file = 0 ; file < 8 ; file++) { - enpassant_threat_remove(gamestate, file, 3); - enpassant_threat_remove(gamestate, file, 4); - } - - /* move (and maybe capture or promote) */ - piece_remove(gamestate, move->fromfile, move->fromrank); - if (move->promotion) { - piece_set(gamestate, move->tofile, move->torank, move->promotion); - } else { - piece_set(gamestate, move->tofile, move->torank, move->piece); - } - - /* add new en passant threat */ - if (piece_type(move->piece) == PAWN && ( - (move->fromrank == 1 && move->torank == 3) || - (move->fromrank == 6 && move->torank == 4))) { - enpassant_threat_add(gamestate, move->tofile, move->torank); - } - - /* castling */ - bool castling_happend = false; - if (piece_type(move->piece) == KING && move->fromfile == fileidx('e')) { - const Color color = piece_color(move->piece); - if (move->tofile == fileidx('g')) { - gamestate->board[move->torank][fileidx('h')] = 0; - gamestate->board[move->torank][fileidx('f')] = mkpiece(ROOK, color); - castling_happend = true; - } else if (move->tofile == fileidx('c')) { - gamestate->board[move->torank][fileidx('a')] = 0; - gamestate->board[move->torank][fileidx('d')] = mkpiece(ROOK, color); - castling_happend = true; - } - } - if (castling_happend) { - if (piece_color(move->piece) == WHITE) { - gamestate->castling.K = true; - gamestate->castling.Q = true; - } else { - gamestate->castling.k = true; - gamestate->castling.q = true; - } - } else { - if (piece_color(move->piece) == WHITE) { - if (move->fromrank == rankidx('1')) { - if (move->fromfile == fileidx('e')) { - gamestate->castling.K = gamestate->castling.Q = true; - } else if (move->fromfile == fileidx('h')) { - gamestate->castling.K = true; - } else if (move->fromfile == fileidx('a')) { - gamestate->castling.Q = true; - } - } - } else { - if (move->fromrank == rankidx('8')) { - if (move->fromfile == fileidx('e')) { - gamestate->castling.k = gamestate->castling.q = true; - } else if (move->fromfile == fileidx('h')) { - gamestate->castling.k = true; - } else if (move->fromfile == fileidx('a')) { - gamestate->castling.q = true; - } - } - } - } - - /* add move to the moves array and the new position to the FEN array */ - if (gamestate->movecount == gamestate->movecapacity) { - gamestate->movecapacity += 64; /* 32 more full moves */ - gamestate->moves = realloc(gamestate->moves, - gamestate->movecapacity * sizeof(Move)); - gamestate->fen = realloc(gamestate->fen, - gamestate->movecapacity * sizeof(char*)); - } - - /* copy the move data into the game's move array */ - Move *melem = &gamestate->moves[gamestate->movecount]; - *melem = *move; - calc_movetime(gamestate, melem); - - /* important: only "add" the move after calculating the time! */ - gamestate->movecount++; - - /* calculate the FEN of the new position and store it in the FEN array */ - char fen[FEN_MAX_LENGTH]; - fen_compute(fen, gamestate); - gamestate->fen[gamestate->movecount - 1] = strdup(fen); -} - void apply_move(GameState *gamestate, Move *move) { apply_move_internal(gamestate, move); @@ -440,132 +577,6 @@ } } -/* 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_internal(&simulation, &simmove); - - /* find the opposing king */ - Color piececolor = piece_color(move->piece); - Color oppcolor = opponent_color(piececolor); - File opkingfile = 0; - Rank opkingrank = 0; - for (Rank rank = 0 ; rank < 8 ; rank++) { - for (File file = 0 ; file < 8 ; file++) { - Piece p = piece_at(&simulation, file, rank); - if (p == mkpiece(KING, oppcolor)) { - opkingfile = file; - opkingrank = rank; - } - } - } - - /* determine if the opposing king is now threatened */ - Move threats[16]; - size_t threatcount; - bool incheck = get_threats(&simulation, opkingfile, opkingrank, - 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; - Rank er = opkingrank + 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, ef, er, 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.fromrank = opkingrank; - move_retaliate.fromfile = opkingfile; - move_retaliate.torank = er; - move_retaliate.tofile = ef; - move_retaliate.capture = true; - apply_move_internal(&sim_retaliate, &move_retaliate); - canescape = !is_covered(&sim_retaliate, ef, er, 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].fromfile, threats[0].fromrank, 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->fromrank == threat->torank) { - /* rook aspect (on rank) */ - int d = threat->tofile > threat->fromfile ? 1 : -1; - File file = threat->fromfile; - while (!canescape && file != threat->tofile - d) { - file += d; - canescape |= is_protected(&simulation, - file, threat->torank, oppcolor); - } - } else if (threat->fromfile == threat->tofile) { - /* rook aspect (on file) */ - int d = threat->torank > threat->fromrank ? 1 : -1; - Rank rank = threat->fromrank; - while (!canescape && rank != threat->torank - d) { - rank += d; - canescape |= is_protected(&simulation, - threat->tofile, rank, oppcolor); - } - } else { - /* bishop aspect */ - int dr = threat->torank > threat->fromrank ? 1 : -1; - int df = threat->tofile > threat->fromfile ? 1 : -1; - - Rank rank = threat->fromrank; - File file = threat->fromfile; - while (!canescape && file != threat->tofile - df - && rank != threat->torank - dr) { - rank += dr; - file += df; - canescape |= is_protected(&simulation, - file, rank, oppcolor); - } - } - } - } - gamestate_cleanup(&simulation); - return canescape ? 1 : 2; -} - void format_move(const GameState *gamestate, Move *move) { char *string = &(move->string[0]);