chessai.chess.gamestate
1import typing 2 3import edq.util.serial 4 5import chessai.chess.piece 6import chessai.core.action 7import chessai.core.gamestate 8 9class GameState(chessai.core.gamestate.GameState): 10 """ A game state specific to a standard Pacman game. """ 11 12 def game_complete(self) -> tuple[list[chessai.core.types.Color], float]: 13 winners = self.get_winners() 14 15 # Score is based on white's perspective using standard chess scoring. 16 if (chessai.core.types.Color.WHITE in winners): 17 score = 1.0 18 elif (chessai.core.types.Color.BLACK in winners): 19 score = 0.0 20 else: 21 score = 0.5 22 23 return (winners, score) 24 25 def is_check(self, color: chessai.core.types.Color, started_in_check: bool = True) -> bool: 26 # Find the king of the given color. 27 king_coordinate = self.get_king_coordinate(color) 28 29 # If there is no King, they cannot be in check. 30 # This should never happen in standard chess games with valid positions. 31 if (king_coordinate is None): 32 return False 33 34 # Get the most recent action to try to avoid work when not doing a full check. 35 previous_action = self.get_previous_action() 36 37 if ((not started_in_check) and isinstance(previous_action, chessai.core.action.MoveAction)): 38 check_potential = self._has_check_potential(king_coordinate, previous_action) 39 else: 40 check_potential = True 41 42 # Skip checking moves that could not have opened us to check. 43 if (not check_potential): 44 return False 45 46 # Check if any opponent piece can attack the king's coordinate. 47 old_turn = self.turn 48 self.turn = color.opposite() 49 50 try: 51 opponent_actions = self._get_pseudo_legal_moves() 52 for action in opponent_actions: 53 if (action.end_coordinate == king_coordinate): 54 return True 55 56 return False 57 finally: 58 self.turn = old_turn 59 60 def get_king_coordinate(self, color: chessai.core.types.Color) -> chessai.core.coordinate.Coordinate | None: 61 """ Get the coordinate of the king for the given color. """ 62 63 for (file, rank_dict) in self.board.pieces.items(): 64 for (rank, piece) in rank_dict.items(): 65 if ((piece.color == color) and (isinstance(piece, chessai.chess.piece.King))): 66 return chessai.core.coordinate.Coordinate(file, rank) 67 68 # If there's no king, the position is invalid, which should never happen. 69 return None 70 71 def _has_check_potential(self, king_coordinate: chessai.core.coordinate.Coordinate, previous_action: chessai.core.action.MoveAction) -> bool: 72 """ 73 Returns whether the player who played the most recent move could be in check. 74 75 Assumes the player was not in check before playing their most recent move. 76 """ 77 78 # Check if the king just moved, which means it could have moved into check. 79 if (previous_action.end_coordinate == king_coordinate): 80 return True 81 82 # Check if a piece moved on the king's file, rank, or diagonal. 83 # This move would allow for discovery checks. 84 if ((previous_action.start_coordinate.file == king_coordinate.file) or (previous_action.start_coordinate.rank == king_coordinate.rank)): 85 # A piece moved on the king's rank or file, so the king could be under attack from a rook or queen. 86 return True 87 88 # Check the diagonal. 89 file_delta = abs(king_coordinate.file - previous_action.start_coordinate.file) 90 rank_delta = abs(king_coordinate.rank - previous_action.start_coordinate.rank) 91 92 # If a piece moved away from the king's diagonal, the king could be under attack from a bishop or queen. 93 return file_delta == rank_delta 94 95 def _should_reset_halfmove_clock(self, action: chessai.core.action.Action, piece: chessai.core.piece.Piece) -> bool: 96 return (isinstance(piece, chessai.chess.piece.Pawn)) 97 98 def _get_pseudo_legal_moves(self) -> list[chessai.core.action.MoveAction]: 99 # Get the base movement from all pieces. 100 actions = self._get_base_move_actions() 101 102 # Add any castling moves. 103 actions.extend(self._get_castling_moves()) 104 105 # Add any pawn double pushes. 106 actions.extend(self._get_pawn_double_pushes()) 107 108 # Add any en-passant captures. 109 actions.extend(self._get_en_passant_captures()) 110 111 return actions 112 113 def _get_base_move_actions(self) -> list[chessai.core.action.MoveAction]: 114 """ 115 Expands all movement vectors from the pieces on the board. 116 Pieces will move until they can capture or reach the end of the board. 117 118 Detects when pawns reach the end of the board and can be promoted. 119 """ 120 121 actions: list[chessai.core.action.MoveAction] = [] 122 123 # Determine the rank for pawn promotions. 124 if (self.turn == chessai.core.types.Color.WHITE): 125 promotion_rank = self.board.num_ranks - 1 126 else: 127 promotion_rank = 0 128 129 for (file, rank_dict) in self.board.pieces.items(): 130 for (rank, piece) in rank_dict.items(): 131 if (piece.color != self.turn): 132 continue 133 134 for movement_vector in piece.move_vectors(): 135 current_rank = rank 136 current_file = file 137 138 coordinate = chessai.core.coordinate.Coordinate(file, rank) 139 140 num_repetitions = movement_vector.num_repetitions 141 while (num_repetitions != 0): 142 current_file += movement_vector.file_delta 143 current_rank += movement_vector.rank_delta 144 if (not self.board.is_within_bounds(current_file, current_rank)): 145 break 146 147 occupant = self.board.get(current_file, current_rank) 148 149 is_occupied = occupant is not None 150 is_enemy = (occupant is not None) and (occupant.color != piece.color) 151 is_ally = (occupant is not None) and (occupant.color == piece.color) 152 153 # No movement type can move on top of an ally. 154 if (is_ally): 155 break 156 157 # Push movement types cannot capture. 158 if ((movement_vector.kind == chessai.core.piece.MoveKind.PUSH) and is_occupied): 159 break 160 161 # Capture movement types must target an enemy. 162 if ((movement_vector.kind == chessai.core.piece.MoveKind.CAPTURE) and (not is_enemy)): 163 break 164 165 current_coordinate = chessai.core.coordinate.Coordinate(current_file, current_rank) 166 167 # Check if this action would lead to a pawn promotion. 168 if ((isinstance(piece, chessai.chess.piece.Pawn)) and (current_coordinate.rank == promotion_rank)): 169 actions.extend(self._get_promotion_actions(coordinate, current_coordinate)) 170 else: 171 actions.append(chessai.core.action.MoveAction(coordinate, current_coordinate)) 172 173 if (is_occupied): 174 break 175 176 num_repetitions -= 1 177 178 return actions 179 180 def _process_special_move(self, 181 action: chessai.core.action.Action, 182 piece: chessai.core.piece.Piece) -> tuple[bool, chessai.core.coordinate.Coordinate | None]: 183 if (not isinstance(action, chessai.core.action.MoveAction)): 184 return False, None 185 186 # Update castling rights. 187 self._update_castling_rights(action, piece) 188 189 # Handle promoting pieces. 190 if isinstance(action, chessai.core.action.PromotionAction): 191 self._handle_promotion(action, piece) 192 193 # Detect castling by seeing if the king moved more than two files. 194 if ((isinstance(piece, chessai.chess.piece.King)) 195 and (action.start_coordinate.file_distance(action.end_coordinate) > 1)): 196 self._handle_castling(action, piece) 197 return False, None 198 199 # Detect en-passant by checking if a pawn moves diagonally to an empty coordinate. 200 if ((isinstance(piece, chessai.chess.piece.Pawn)) 201 and (action.start_coordinate.file_distance(action.end_coordinate) == 1) 202 and (self.get(action.end_coordinate) is None)): 203 en_passant_coordinate = self._handle_en_passant(action, piece) 204 return True, en_passant_coordinate 205 206 return False, None 207 208 def is_insufficient_material(self) -> bool: 209 # Meta actions cannot cause a game to become in an insufficient material state. 210 if (not isinstance(self.get_previous_action(), chessai.core.action.MoveAction)): 211 return False 212 213 for color in chessai.core.types.Color: 214 if (not self._is_insufficient_material(color)): 215 return False 216 217 return True 218 219 def _is_insufficient_material(self, color: chessai.core.types.Color) -> bool: 220 """ 221 Checks if the given color has insufficient winning material. 222 223 This is guaranteed to return False if the color can still win the game. 224 225 The converse does not necessarily hold: only material is considered (including bishop square colors), not piece positions. 226 Fortress positions or forced lines may still return False even with no winning continuation. 227 """ 228 229 own_pieces = {} 230 opponent_pieces = {} 231 232 own_knights = False 233 own_bishops: dict[chessai.core.coordinate.Coordinate, chessai.core.piece.Piece] = {} 234 235 for (file, rank_dict) in self.board.pieces.items(): 236 for (rank, piece) in rank_dict.items(): 237 coordinate = chessai.core.coordinate.Coordinate(file, rank) 238 239 if (piece.color == color): 240 own_pieces[coordinate] = piece 241 242 # Pawns, rooks, or queens are always sufficient material. 243 if (isinstance(piece, (chessai.chess.piece.Pawn, chessai.chess.piece.Rook, chessai.chess.piece.Queen))): 244 return False 245 246 if (isinstance(piece, chessai.chess.piece.Knight)): 247 own_knights = True 248 elif (isinstance(piece, chessai.chess.piece.Bishop)): 249 own_bishops[coordinate] = piece 250 else: 251 opponent_pieces[coordinate] = piece 252 253 # Knights are only insufficient material if: 254 # (1) We do not have any other pieces, including more than one knight. 255 # (2) The opponent does not have pawns, knights, bishops or rooks. 256 # These would allow selfmate. 257 if (own_knights): 258 # Sufficient material via case 1. 259 if (len(own_pieces) > 2): 260 return False 261 262 for piece in opponent_pieces.values(): 263 if isinstance(piece, (chessai.chess.piece.Knight, chessai.chess.piece.Bishop, chessai.chess.piece.Rook, chessai.chess.piece.Pawn)): 264 # Sufficient material via case 2. 265 return False 266 267 return True 268 269 # Bishops are only insufficient material if: 270 # (1) We do not have any other pieces, including bishops of the opposite color. 271 # (2) The opponent does not have bishops of the opposite color, pawns or knights. 272 # These would allow selfmate. 273 if (len(own_bishops) > 0): 274 square_parities = set() 275 for (coordinate, _) in own_bishops.items(): 276 square_parity = (coordinate.file + coordinate.rank) % 2 277 square_parities.add(square_parity) 278 279 # Sufficient material via case 1. 280 if (len(square_parities) > 1): 281 return False 282 283 for piece in opponent_pieces.values(): 284 # Sufficient material via case 2 (using pawns or knights). 285 if (isinstance(piece, (chessai.chess.piece.Pawn, chessai.chess.piece.Knight))): 286 return False 287 288 opponent_bishop_parities = set() 289 for (coordinate, piece) in opponent_pieces.items(): 290 if (isinstance(piece, chessai.chess.piece.Bishop)): 291 opponent_parity = (coordinate.file + coordinate.rank) % 2 292 opponent_bishop_parities.add(opponent_parity) 293 294 opponent_has_opposite_bishops = bool(opponent_bishop_parities - square_parities) 295 296 # Determine sufficient material via case 2 when the opponent has bishops of the opposite color. 297 return (not opponent_has_opposite_bishops) 298 299 # King alone. 300 return True 301 302 def _handle_castling(self, action: chessai.core.action.MoveAction, piece: chessai.core.piece.Piece) -> None: 303 # Build the rook action for castling. 304 rook_action: chessai.core.action.Action | None = None 305 back_rank = action.start_coordinate.rank 306 307 if (action.end_coordinate.file > action.start_coordinate.file): 308 # Kingside: rook moves from the h-file to just right of the king's destination. 309 rook_start = chessai.core.coordinate.Coordinate(self.board.num_files - 1, back_rank) 310 rook_end = chessai.core.coordinate.Coordinate(action.end_coordinate.file - 1, back_rank) 311 else: 312 # Queenside: rook moves from the a-file to just left of the king's destination. 313 rook_start = chessai.core.coordinate.Coordinate(0, back_rank) 314 rook_end = chessai.core.coordinate.Coordinate(action.end_coordinate.file + 1, back_rank) 315 316 rook_action = chessai.core.action.MoveAction(rook_start, rook_end) 317 318 # Move the rook for castling. 319 self.board.push(rook_action) 320 321 def _handle_en_passant(self, 322 action: chessai.core.action.MoveAction, 323 piece: chessai.core.piece.Piece, 324 ) -> chessai.core.coordinate.Coordinate | None: 325 # The captured pawn is on the same rank as the moving pawn, on the destination file. 326 captured_piece_coordinate = chessai.core.coordinate.Coordinate( 327 action.end_coordinate.file, 328 action.start_coordinate.rank, 329 ) 330 331 # Remove the en-passant captured pawn from its actual coordinate. 332 self.board.remove(captured_piece_coordinate) 333 334 # Return the en-passant target coordinate, based on the action. 335 return self._compute_en_passant(action, piece) 336 337 def _handle_promotion(self, action: chessai.core.action.PromotionAction, piece: chessai.core.piece.Piece) -> None: 338 # Handle promotion by replacing the pawn with the promoted piece. 339 self.board.set(action.promotion, action.end_coordinate) 340 341 def _get_castling_moves(self) -> list[chessai.core.action.MoveAction]: 342 """ Get all pseudo-legal castling moves for the current player. """ 343 344 # Determine the back rank and castling rights for the current player. 345 if (self.turn == chessai.core.types.Color.WHITE): 346 back_rank = 0 347 kingside_rights = self.castling_rights.white_kingside 348 queenside_rights = self.castling_rights.white_queenside 349 else: 350 back_rank = self.board.num_ranks - 1 351 kingside_rights = self.castling_rights.black_kingside 352 queenside_rights = self.castling_rights.black_queenside 353 354 if ((not kingside_rights) and (not queenside_rights)): 355 return [] 356 357 # Find the king. 358 king_coordinate = self.get_king_coordinate(self.turn) 359 360 # The king must be on the back rank to castle. 361 if ((king_coordinate is None) or (king_coordinate.rank != back_rank)): 362 return [] 363 364 # Get the coordinates that the opponent can attack, 365 # as a castling move cannot pass through a square that is attacked. 366 attacked_coordinates = self.get_attacked_coordinates(self.turn.opposite()) 367 368 # The king cannot castle if it is being attacked. 369 if (king_coordinate in attacked_coordinates): 370 return [] 371 372 actions: list[chessai.core.action.MoveAction] = [] 373 374 # Add Kingside castling. 375 if (kingside_rights): 376 rook_cordinate = chessai.core.coordinate.Coordinate((self.board.num_files - 1), back_rank) 377 rook_piece = self.get(rook_cordinate) 378 379 # Check that the files to the right of the king and left of the rook are empty. 380 all_safe = True 381 file = king_coordinate.file + 1 382 while (file < rook_cordinate.file): 383 empty_coord = chessai.core.coordinate.Coordinate(file, back_rank) 384 385 if (empty_coord in attacked_coordinates): 386 all_safe = False 387 break 388 389 if (self.board.has_piece(empty_coord)): 390 all_safe = False 391 break 392 393 file = file + 1 394 395 if ((rook_piece is not None) 396 and (isinstance(rook_piece, chessai.chess.piece.Rook)) 397 and (rook_piece.color == self.turn) 398 and (all_safe)): 399 king_dest = chessai.core.coordinate.Coordinate((self.board.num_files - 2), back_rank) 400 actions.append(chessai.core.action.MoveAction(king_coordinate, king_dest)) 401 402 # Add Queenside castling. 403 if (queenside_rights): 404 rook_cordinate = chessai.core.coordinate.Coordinate(0, back_rank) 405 rook_piece = self.get(rook_cordinate) 406 407 # Check that the files to the right of the king and left of the rook are empty. 408 all_safe = True 409 file = king_coordinate.file - 1 410 while (file > 0): 411 empty_coord = chessai.core.coordinate.Coordinate(file, back_rank) 412 413 if (empty_coord in attacked_coordinates): 414 all_safe = False 415 break 416 417 if (self.board.has_piece(empty_coord)): 418 all_safe = False 419 break 420 421 file = file - 1 422 423 if ((rook_piece is not None) 424 and (isinstance(rook_piece, chessai.chess.piece.Rook)) 425 and (rook_piece.color == self.turn) 426 and (all_safe)): 427 king_dest = chessai.core.coordinate.Coordinate(2, back_rank) 428 actions.append(chessai.core.action.MoveAction(king_coordinate, king_dest)) 429 430 return actions 431 432 def _get_pawn_double_pushes(self) -> list[chessai.core.action.MoveAction]: 433 """ Get all pseudo-legal pawn double push moves for the current player. """ 434 435 if (self.turn == chessai.core.types.Color.WHITE): 436 start_rank = 1 437 direction = 1 438 else: 439 start_rank = self.board.num_ranks - 2 440 direction = -1 441 442 actions: list[chessai.core.action.MoveAction] = [] 443 444 for (file, rank_dict) in self.board.pieces.items(): 445 if (start_rank not in rank_dict): 446 continue 447 448 piece = rank_dict[start_rank] 449 if (piece.color != self.turn): 450 continue 451 452 if (not isinstance(piece, chessai.chess.piece.Pawn)): 453 continue 454 455 coordinate = chessai.core.coordinate.Coordinate(file, start_rank) 456 457 # Both the single and double push coordinates must be empty. 458 single_push_coord = coordinate.offset(0, direction) 459 if (self.board.has_piece(single_push_coord)): 460 continue 461 462 double_push_coord = coordinate.offset(0, direction * 2) 463 if (self.board.has_piece(double_push_coord)): 464 continue 465 466 actions.append(chessai.core.action.MoveAction(coordinate, double_push_coord)) 467 468 return actions 469 470 def _get_en_passant_captures(self) -> list[chessai.core.action.MoveAction]: 471 """ Get all pseudo-legal en-passant captures for the current player. """ 472 473 actions: list[chessai.core.action.MoveAction] = [] 474 475 if (self.en_passant_coordinate is None): 476 return actions 477 478 if (self.turn == chessai.core.types.Color.WHITE): 479 # The en-passant coordinate encodes the coordinate that must be taken. 480 # So, the pawn that can attack it will be one rank below for white. 481 capturing_rank = self.en_passant_coordinate.rank - 1 482 else: 483 capturing_rank = self.en_passant_coordinate.rank + 1 484 485 # The pawn will have to be on an adjacent file. 486 for file_delta in [-1, 1]: 487 candidate_capture_coord = chessai.core.coordinate.Coordinate( 488 self.en_passant_coordinate.file + file_delta, 489 capturing_rank, 490 ) 491 492 if (not self.board.is_within_bounds(candidate_capture_coord.file, candidate_capture_coord.rank)): 493 continue 494 495 piece = self.get(candidate_capture_coord) 496 if (piece is None): 497 continue 498 499 if (not isinstance(piece, chessai.chess.piece.Pawn)): 500 continue 501 502 if (piece.color != self.turn): 503 continue 504 505 actions.append(chessai.core.action.MoveAction(candidate_capture_coord, self.en_passant_coordinate)) 506 507 return actions 508 509 def _get_promotion_actions(self, 510 start_coordinate: chessai.core.coordinate.Coordinate, 511 end_coordinate: chessai.core.coordinate.Coordinate) -> list[chessai.core.action.MoveAction]: 512 """ 513 Expand a pawn move that reaches the back rank into one action per promotable piece type. 514 515 Called when a pawn reaches the back rank of the opposing side. 516 Always returns exactly four actions (queen, rook, bishop, knight). 517 """ 518 519 return [ 520 chessai.core.action.PromotionAction(start_coordinate, end_coordinate, chessai.chess.piece.Queen(self.turn)), 521 chessai.core.action.PromotionAction(start_coordinate, end_coordinate, chessai.chess.piece.Rook(self.turn)), 522 chessai.core.action.PromotionAction(start_coordinate, end_coordinate, chessai.chess.piece.Bishop(self.turn)), 523 chessai.core.action.PromotionAction(start_coordinate, end_coordinate, chessai.chess.piece.Knight(self.turn)), 524 ] 525 526 def _update_castling_rights(self, 527 action: chessai.core.action.MoveAction, 528 piece: chessai.core.piece.Piece) -> None: 529 """ Revoke castling rights that are no longer valid after the given move. """ 530 531 white_kingside_rook = chessai.core.coordinate.Coordinate(self.board.num_files - 1, 0) 532 white_queenside_rook = chessai.core.coordinate.Coordinate(0, 0) 533 black_kingside_rook = chessai.core.coordinate.Coordinate(self.board.num_files - 1, self.board.num_ranks - 1) 534 black_queenside_rook = chessai.core.coordinate.Coordinate(0, self.board.num_ranks - 1) 535 536 # King moves, which forfeits both rights for that color. 537 if (isinstance(piece, chessai.chess.piece.King)): 538 if (piece.color == chessai.core.types.Color.WHITE): 539 self.castling_rights = self.castling_rights.revoke_rights(chessai.core.castling.ALL_WHITE_RIGHTS) 540 else: 541 self.castling_rights = self.castling_rights.revoke_rights(chessai.core.castling.ALL_BLACK_RIGHTS) 542 543 # Rook moves from its starting coordinate. 544 if (action.start_coordinate == white_kingside_rook): 545 self.castling_rights = self.castling_rights.revoke_rights(chessai.core.castling.WHITE_KINGSIDE_RIGHTS) 546 547 if (action.start_coordinate == white_queenside_rook): 548 self.castling_rights = self.castling_rights.revoke_rights(chessai.core.castling.WHITE_QUEENSIDE_RIGHTS) 549 550 if (action.start_coordinate == black_kingside_rook): 551 self.castling_rights = self.castling_rights.revoke_rights(chessai.core.castling.BLACK_KINGSIDE_RIGHTS) 552 553 if (action.start_coordinate == black_queenside_rook): 554 self.castling_rights = self.castling_rights.revoke_rights(chessai.core.castling.BLACK_QUEENSIDE_RIGHTS) 555 556 # Rook captured on its starting coordinate. 557 if (action.end_coordinate == white_kingside_rook): 558 self.castling_rights = self.castling_rights.revoke_rights(chessai.core.castling.WHITE_KINGSIDE_RIGHTS) 559 560 if (action.end_coordinate == white_queenside_rook): 561 self.castling_rights = self.castling_rights.revoke_rights(chessai.core.castling.WHITE_QUEENSIDE_RIGHTS) 562 563 if (action.end_coordinate == black_kingside_rook): 564 self.castling_rights = self.castling_rights.revoke_rights(chessai.core.castling.BLACK_KINGSIDE_RIGHTS) 565 566 if (action.end_coordinate == black_queenside_rook): 567 self.castling_rights = self.castling_rights.revoke_rights(chessai.core.castling.BLACK_QUEENSIDE_RIGHTS) 568 569 def _compute_en_passant(self, 570 action: chessai.core.action.MoveAction, 571 piece: chessai.core.piece.Piece) -> chessai.core.coordinate.Coordinate | None: 572 """ 573 Return the new en-passant target coordinate after this move, or None. 574 575 A target coordinate is set only when a pawn advances two ranks. 576 The target is the coordinate the pawn passed through (i.e. one rank behind the destination), which is standard FEN notation. 577 """ 578 579 if (not isinstance(piece, chessai.chess.piece.Pawn)): 580 return None 581 582 if (action.start_coordinate.rank_distance(action.end_coordinate) != 2): 583 return None 584 585 # The en-passant target is the coordinate the pawn skipped over. 586 passed_rank = (action.start_coordinate.rank + action.end_coordinate.rank) // 2 587 return chessai.core.coordinate.Coordinate(action.start_coordinate.file, passed_rank) 588 589 def copy(self, 590 context: typing.Union[edq.util.serial.SerializationContext, None] = None, 591 ) -> 'GameState': 592 new_state = super().copy() 593 new_state = typing.cast(GameState, new_state) 594 595 return new_state 596 597def base_eval( 598 state: chessai.core.gamestate.GameState, 599 action: chessai.core.action.Action | None = None, 600 agent: typing.Any | None = None, 601 **kwargs: typing.Any) -> float: 602 """ 603 The most basic evaluation function, which just uses the difference in piece value on the board. 604 """ 605 606 piece_values: dict[type[chessai.core.piece.Piece], int] = { 607 chessai.chess.piece.Pawn: 1, 608 chessai.chess.piece.Knight: 3, 609 chessai.chess.piece.Bishop: 3, 610 chessai.chess.piece.Rook: 5, 611 chessai.chess.piece.Queen: 9, 612 chessai.chess.piece.King: 9999, 613 } 614 615 # The difference in pieces from white's perspective. 616 board_value = 0 617 for piece in state.board.all_pieces(): 618 piece_value = piece_values.get(type(piece), 0) 619 if (piece.color == chessai.core.types.Color.WHITE): 620 board_value += piece_value 621 else: 622 board_value -= piece_value 623 624 if (state.turn == chessai.core.types.Color.WHITE): 625 return board_value 626 627 # The piece difference is the opposite for black. 628 return -1 * board_value
10class GameState(chessai.core.gamestate.GameState): 11 """ A game state specific to a standard Pacman game. """ 12 13 def game_complete(self) -> tuple[list[chessai.core.types.Color], float]: 14 winners = self.get_winners() 15 16 # Score is based on white's perspective using standard chess scoring. 17 if (chessai.core.types.Color.WHITE in winners): 18 score = 1.0 19 elif (chessai.core.types.Color.BLACK in winners): 20 score = 0.0 21 else: 22 score = 0.5 23 24 return (winners, score) 25 26 def is_check(self, color: chessai.core.types.Color, started_in_check: bool = True) -> bool: 27 # Find the king of the given color. 28 king_coordinate = self.get_king_coordinate(color) 29 30 # If there is no King, they cannot be in check. 31 # This should never happen in standard chess games with valid positions. 32 if (king_coordinate is None): 33 return False 34 35 # Get the most recent action to try to avoid work when not doing a full check. 36 previous_action = self.get_previous_action() 37 38 if ((not started_in_check) and isinstance(previous_action, chessai.core.action.MoveAction)): 39 check_potential = self._has_check_potential(king_coordinate, previous_action) 40 else: 41 check_potential = True 42 43 # Skip checking moves that could not have opened us to check. 44 if (not check_potential): 45 return False 46 47 # Check if any opponent piece can attack the king's coordinate. 48 old_turn = self.turn 49 self.turn = color.opposite() 50 51 try: 52 opponent_actions = self._get_pseudo_legal_moves() 53 for action in opponent_actions: 54 if (action.end_coordinate == king_coordinate): 55 return True 56 57 return False 58 finally: 59 self.turn = old_turn 60 61 def get_king_coordinate(self, color: chessai.core.types.Color) -> chessai.core.coordinate.Coordinate | None: 62 """ Get the coordinate of the king for the given color. """ 63 64 for (file, rank_dict) in self.board.pieces.items(): 65 for (rank, piece) in rank_dict.items(): 66 if ((piece.color == color) and (isinstance(piece, chessai.chess.piece.King))): 67 return chessai.core.coordinate.Coordinate(file, rank) 68 69 # If there's no king, the position is invalid, which should never happen. 70 return None 71 72 def _has_check_potential(self, king_coordinate: chessai.core.coordinate.Coordinate, previous_action: chessai.core.action.MoveAction) -> bool: 73 """ 74 Returns whether the player who played the most recent move could be in check. 75 76 Assumes the player was not in check before playing their most recent move. 77 """ 78 79 # Check if the king just moved, which means it could have moved into check. 80 if (previous_action.end_coordinate == king_coordinate): 81 return True 82 83 # Check if a piece moved on the king's file, rank, or diagonal. 84 # This move would allow for discovery checks. 85 if ((previous_action.start_coordinate.file == king_coordinate.file) or (previous_action.start_coordinate.rank == king_coordinate.rank)): 86 # A piece moved on the king's rank or file, so the king could be under attack from a rook or queen. 87 return True 88 89 # Check the diagonal. 90 file_delta = abs(king_coordinate.file - previous_action.start_coordinate.file) 91 rank_delta = abs(king_coordinate.rank - previous_action.start_coordinate.rank) 92 93 # If a piece moved away from the king's diagonal, the king could be under attack from a bishop or queen. 94 return file_delta == rank_delta 95 96 def _should_reset_halfmove_clock(self, action: chessai.core.action.Action, piece: chessai.core.piece.Piece) -> bool: 97 return (isinstance(piece, chessai.chess.piece.Pawn)) 98 99 def _get_pseudo_legal_moves(self) -> list[chessai.core.action.MoveAction]: 100 # Get the base movement from all pieces. 101 actions = self._get_base_move_actions() 102 103 # Add any castling moves. 104 actions.extend(self._get_castling_moves()) 105 106 # Add any pawn double pushes. 107 actions.extend(self._get_pawn_double_pushes()) 108 109 # Add any en-passant captures. 110 actions.extend(self._get_en_passant_captures()) 111 112 return actions 113 114 def _get_base_move_actions(self) -> list[chessai.core.action.MoveAction]: 115 """ 116 Expands all movement vectors from the pieces on the board. 117 Pieces will move until they can capture or reach the end of the board. 118 119 Detects when pawns reach the end of the board and can be promoted. 120 """ 121 122 actions: list[chessai.core.action.MoveAction] = [] 123 124 # Determine the rank for pawn promotions. 125 if (self.turn == chessai.core.types.Color.WHITE): 126 promotion_rank = self.board.num_ranks - 1 127 else: 128 promotion_rank = 0 129 130 for (file, rank_dict) in self.board.pieces.items(): 131 for (rank, piece) in rank_dict.items(): 132 if (piece.color != self.turn): 133 continue 134 135 for movement_vector in piece.move_vectors(): 136 current_rank = rank 137 current_file = file 138 139 coordinate = chessai.core.coordinate.Coordinate(file, rank) 140 141 num_repetitions = movement_vector.num_repetitions 142 while (num_repetitions != 0): 143 current_file += movement_vector.file_delta 144 current_rank += movement_vector.rank_delta 145 if (not self.board.is_within_bounds(current_file, current_rank)): 146 break 147 148 occupant = self.board.get(current_file, current_rank) 149 150 is_occupied = occupant is not None 151 is_enemy = (occupant is not None) and (occupant.color != piece.color) 152 is_ally = (occupant is not None) and (occupant.color == piece.color) 153 154 # No movement type can move on top of an ally. 155 if (is_ally): 156 break 157 158 # Push movement types cannot capture. 159 if ((movement_vector.kind == chessai.core.piece.MoveKind.PUSH) and is_occupied): 160 break 161 162 # Capture movement types must target an enemy. 163 if ((movement_vector.kind == chessai.core.piece.MoveKind.CAPTURE) and (not is_enemy)): 164 break 165 166 current_coordinate = chessai.core.coordinate.Coordinate(current_file, current_rank) 167 168 # Check if this action would lead to a pawn promotion. 169 if ((isinstance(piece, chessai.chess.piece.Pawn)) and (current_coordinate.rank == promotion_rank)): 170 actions.extend(self._get_promotion_actions(coordinate, current_coordinate)) 171 else: 172 actions.append(chessai.core.action.MoveAction(coordinate, current_coordinate)) 173 174 if (is_occupied): 175 break 176 177 num_repetitions -= 1 178 179 return actions 180 181 def _process_special_move(self, 182 action: chessai.core.action.Action, 183 piece: chessai.core.piece.Piece) -> tuple[bool, chessai.core.coordinate.Coordinate | None]: 184 if (not isinstance(action, chessai.core.action.MoveAction)): 185 return False, None 186 187 # Update castling rights. 188 self._update_castling_rights(action, piece) 189 190 # Handle promoting pieces. 191 if isinstance(action, chessai.core.action.PromotionAction): 192 self._handle_promotion(action, piece) 193 194 # Detect castling by seeing if the king moved more than two files. 195 if ((isinstance(piece, chessai.chess.piece.King)) 196 and (action.start_coordinate.file_distance(action.end_coordinate) > 1)): 197 self._handle_castling(action, piece) 198 return False, None 199 200 # Detect en-passant by checking if a pawn moves diagonally to an empty coordinate. 201 if ((isinstance(piece, chessai.chess.piece.Pawn)) 202 and (action.start_coordinate.file_distance(action.end_coordinate) == 1) 203 and (self.get(action.end_coordinate) is None)): 204 en_passant_coordinate = self._handle_en_passant(action, piece) 205 return True, en_passant_coordinate 206 207 return False, None 208 209 def is_insufficient_material(self) -> bool: 210 # Meta actions cannot cause a game to become in an insufficient material state. 211 if (not isinstance(self.get_previous_action(), chessai.core.action.MoveAction)): 212 return False 213 214 for color in chessai.core.types.Color: 215 if (not self._is_insufficient_material(color)): 216 return False 217 218 return True 219 220 def _is_insufficient_material(self, color: chessai.core.types.Color) -> bool: 221 """ 222 Checks if the given color has insufficient winning material. 223 224 This is guaranteed to return False if the color can still win the game. 225 226 The converse does not necessarily hold: only material is considered (including bishop square colors), not piece positions. 227 Fortress positions or forced lines may still return False even with no winning continuation. 228 """ 229 230 own_pieces = {} 231 opponent_pieces = {} 232 233 own_knights = False 234 own_bishops: dict[chessai.core.coordinate.Coordinate, chessai.core.piece.Piece] = {} 235 236 for (file, rank_dict) in self.board.pieces.items(): 237 for (rank, piece) in rank_dict.items(): 238 coordinate = chessai.core.coordinate.Coordinate(file, rank) 239 240 if (piece.color == color): 241 own_pieces[coordinate] = piece 242 243 # Pawns, rooks, or queens are always sufficient material. 244 if (isinstance(piece, (chessai.chess.piece.Pawn, chessai.chess.piece.Rook, chessai.chess.piece.Queen))): 245 return False 246 247 if (isinstance(piece, chessai.chess.piece.Knight)): 248 own_knights = True 249 elif (isinstance(piece, chessai.chess.piece.Bishop)): 250 own_bishops[coordinate] = piece 251 else: 252 opponent_pieces[coordinate] = piece 253 254 # Knights are only insufficient material if: 255 # (1) We do not have any other pieces, including more than one knight. 256 # (2) The opponent does not have pawns, knights, bishops or rooks. 257 # These would allow selfmate. 258 if (own_knights): 259 # Sufficient material via case 1. 260 if (len(own_pieces) > 2): 261 return False 262 263 for piece in opponent_pieces.values(): 264 if isinstance(piece, (chessai.chess.piece.Knight, chessai.chess.piece.Bishop, chessai.chess.piece.Rook, chessai.chess.piece.Pawn)): 265 # Sufficient material via case 2. 266 return False 267 268 return True 269 270 # Bishops are only insufficient material if: 271 # (1) We do not have any other pieces, including bishops of the opposite color. 272 # (2) The opponent does not have bishops of the opposite color, pawns or knights. 273 # These would allow selfmate. 274 if (len(own_bishops) > 0): 275 square_parities = set() 276 for (coordinate, _) in own_bishops.items(): 277 square_parity = (coordinate.file + coordinate.rank) % 2 278 square_parities.add(square_parity) 279 280 # Sufficient material via case 1. 281 if (len(square_parities) > 1): 282 return False 283 284 for piece in opponent_pieces.values(): 285 # Sufficient material via case 2 (using pawns or knights). 286 if (isinstance(piece, (chessai.chess.piece.Pawn, chessai.chess.piece.Knight))): 287 return False 288 289 opponent_bishop_parities = set() 290 for (coordinate, piece) in opponent_pieces.items(): 291 if (isinstance(piece, chessai.chess.piece.Bishop)): 292 opponent_parity = (coordinate.file + coordinate.rank) % 2 293 opponent_bishop_parities.add(opponent_parity) 294 295 opponent_has_opposite_bishops = bool(opponent_bishop_parities - square_parities) 296 297 # Determine sufficient material via case 2 when the opponent has bishops of the opposite color. 298 return (not opponent_has_opposite_bishops) 299 300 # King alone. 301 return True 302 303 def _handle_castling(self, action: chessai.core.action.MoveAction, piece: chessai.core.piece.Piece) -> None: 304 # Build the rook action for castling. 305 rook_action: chessai.core.action.Action | None = None 306 back_rank = action.start_coordinate.rank 307 308 if (action.end_coordinate.file > action.start_coordinate.file): 309 # Kingside: rook moves from the h-file to just right of the king's destination. 310 rook_start = chessai.core.coordinate.Coordinate(self.board.num_files - 1, back_rank) 311 rook_end = chessai.core.coordinate.Coordinate(action.end_coordinate.file - 1, back_rank) 312 else: 313 # Queenside: rook moves from the a-file to just left of the king's destination. 314 rook_start = chessai.core.coordinate.Coordinate(0, back_rank) 315 rook_end = chessai.core.coordinate.Coordinate(action.end_coordinate.file + 1, back_rank) 316 317 rook_action = chessai.core.action.MoveAction(rook_start, rook_end) 318 319 # Move the rook for castling. 320 self.board.push(rook_action) 321 322 def _handle_en_passant(self, 323 action: chessai.core.action.MoveAction, 324 piece: chessai.core.piece.Piece, 325 ) -> chessai.core.coordinate.Coordinate | None: 326 # The captured pawn is on the same rank as the moving pawn, on the destination file. 327 captured_piece_coordinate = chessai.core.coordinate.Coordinate( 328 action.end_coordinate.file, 329 action.start_coordinate.rank, 330 ) 331 332 # Remove the en-passant captured pawn from its actual coordinate. 333 self.board.remove(captured_piece_coordinate) 334 335 # Return the en-passant target coordinate, based on the action. 336 return self._compute_en_passant(action, piece) 337 338 def _handle_promotion(self, action: chessai.core.action.PromotionAction, piece: chessai.core.piece.Piece) -> None: 339 # Handle promotion by replacing the pawn with the promoted piece. 340 self.board.set(action.promotion, action.end_coordinate) 341 342 def _get_castling_moves(self) -> list[chessai.core.action.MoveAction]: 343 """ Get all pseudo-legal castling moves for the current player. """ 344 345 # Determine the back rank and castling rights for the current player. 346 if (self.turn == chessai.core.types.Color.WHITE): 347 back_rank = 0 348 kingside_rights = self.castling_rights.white_kingside 349 queenside_rights = self.castling_rights.white_queenside 350 else: 351 back_rank = self.board.num_ranks - 1 352 kingside_rights = self.castling_rights.black_kingside 353 queenside_rights = self.castling_rights.black_queenside 354 355 if ((not kingside_rights) and (not queenside_rights)): 356 return [] 357 358 # Find the king. 359 king_coordinate = self.get_king_coordinate(self.turn) 360 361 # The king must be on the back rank to castle. 362 if ((king_coordinate is None) or (king_coordinate.rank != back_rank)): 363 return [] 364 365 # Get the coordinates that the opponent can attack, 366 # as a castling move cannot pass through a square that is attacked. 367 attacked_coordinates = self.get_attacked_coordinates(self.turn.opposite()) 368 369 # The king cannot castle if it is being attacked. 370 if (king_coordinate in attacked_coordinates): 371 return [] 372 373 actions: list[chessai.core.action.MoveAction] = [] 374 375 # Add Kingside castling. 376 if (kingside_rights): 377 rook_cordinate = chessai.core.coordinate.Coordinate((self.board.num_files - 1), back_rank) 378 rook_piece = self.get(rook_cordinate) 379 380 # Check that the files to the right of the king and left of the rook are empty. 381 all_safe = True 382 file = king_coordinate.file + 1 383 while (file < rook_cordinate.file): 384 empty_coord = chessai.core.coordinate.Coordinate(file, back_rank) 385 386 if (empty_coord in attacked_coordinates): 387 all_safe = False 388 break 389 390 if (self.board.has_piece(empty_coord)): 391 all_safe = False 392 break 393 394 file = file + 1 395 396 if ((rook_piece is not None) 397 and (isinstance(rook_piece, chessai.chess.piece.Rook)) 398 and (rook_piece.color == self.turn) 399 and (all_safe)): 400 king_dest = chessai.core.coordinate.Coordinate((self.board.num_files - 2), back_rank) 401 actions.append(chessai.core.action.MoveAction(king_coordinate, king_dest)) 402 403 # Add Queenside castling. 404 if (queenside_rights): 405 rook_cordinate = chessai.core.coordinate.Coordinate(0, back_rank) 406 rook_piece = self.get(rook_cordinate) 407 408 # Check that the files to the right of the king and left of the rook are empty. 409 all_safe = True 410 file = king_coordinate.file - 1 411 while (file > 0): 412 empty_coord = chessai.core.coordinate.Coordinate(file, back_rank) 413 414 if (empty_coord in attacked_coordinates): 415 all_safe = False 416 break 417 418 if (self.board.has_piece(empty_coord)): 419 all_safe = False 420 break 421 422 file = file - 1 423 424 if ((rook_piece is not None) 425 and (isinstance(rook_piece, chessai.chess.piece.Rook)) 426 and (rook_piece.color == self.turn) 427 and (all_safe)): 428 king_dest = chessai.core.coordinate.Coordinate(2, back_rank) 429 actions.append(chessai.core.action.MoveAction(king_coordinate, king_dest)) 430 431 return actions 432 433 def _get_pawn_double_pushes(self) -> list[chessai.core.action.MoveAction]: 434 """ Get all pseudo-legal pawn double push moves for the current player. """ 435 436 if (self.turn == chessai.core.types.Color.WHITE): 437 start_rank = 1 438 direction = 1 439 else: 440 start_rank = self.board.num_ranks - 2 441 direction = -1 442 443 actions: list[chessai.core.action.MoveAction] = [] 444 445 for (file, rank_dict) in self.board.pieces.items(): 446 if (start_rank not in rank_dict): 447 continue 448 449 piece = rank_dict[start_rank] 450 if (piece.color != self.turn): 451 continue 452 453 if (not isinstance(piece, chessai.chess.piece.Pawn)): 454 continue 455 456 coordinate = chessai.core.coordinate.Coordinate(file, start_rank) 457 458 # Both the single and double push coordinates must be empty. 459 single_push_coord = coordinate.offset(0, direction) 460 if (self.board.has_piece(single_push_coord)): 461 continue 462 463 double_push_coord = coordinate.offset(0, direction * 2) 464 if (self.board.has_piece(double_push_coord)): 465 continue 466 467 actions.append(chessai.core.action.MoveAction(coordinate, double_push_coord)) 468 469 return actions 470 471 def _get_en_passant_captures(self) -> list[chessai.core.action.MoveAction]: 472 """ Get all pseudo-legal en-passant captures for the current player. """ 473 474 actions: list[chessai.core.action.MoveAction] = [] 475 476 if (self.en_passant_coordinate is None): 477 return actions 478 479 if (self.turn == chessai.core.types.Color.WHITE): 480 # The en-passant coordinate encodes the coordinate that must be taken. 481 # So, the pawn that can attack it will be one rank below for white. 482 capturing_rank = self.en_passant_coordinate.rank - 1 483 else: 484 capturing_rank = self.en_passant_coordinate.rank + 1 485 486 # The pawn will have to be on an adjacent file. 487 for file_delta in [-1, 1]: 488 candidate_capture_coord = chessai.core.coordinate.Coordinate( 489 self.en_passant_coordinate.file + file_delta, 490 capturing_rank, 491 ) 492 493 if (not self.board.is_within_bounds(candidate_capture_coord.file, candidate_capture_coord.rank)): 494 continue 495 496 piece = self.get(candidate_capture_coord) 497 if (piece is None): 498 continue 499 500 if (not isinstance(piece, chessai.chess.piece.Pawn)): 501 continue 502 503 if (piece.color != self.turn): 504 continue 505 506 actions.append(chessai.core.action.MoveAction(candidate_capture_coord, self.en_passant_coordinate)) 507 508 return actions 509 510 def _get_promotion_actions(self, 511 start_coordinate: chessai.core.coordinate.Coordinate, 512 end_coordinate: chessai.core.coordinate.Coordinate) -> list[chessai.core.action.MoveAction]: 513 """ 514 Expand a pawn move that reaches the back rank into one action per promotable piece type. 515 516 Called when a pawn reaches the back rank of the opposing side. 517 Always returns exactly four actions (queen, rook, bishop, knight). 518 """ 519 520 return [ 521 chessai.core.action.PromotionAction(start_coordinate, end_coordinate, chessai.chess.piece.Queen(self.turn)), 522 chessai.core.action.PromotionAction(start_coordinate, end_coordinate, chessai.chess.piece.Rook(self.turn)), 523 chessai.core.action.PromotionAction(start_coordinate, end_coordinate, chessai.chess.piece.Bishop(self.turn)), 524 chessai.core.action.PromotionAction(start_coordinate, end_coordinate, chessai.chess.piece.Knight(self.turn)), 525 ] 526 527 def _update_castling_rights(self, 528 action: chessai.core.action.MoveAction, 529 piece: chessai.core.piece.Piece) -> None: 530 """ Revoke castling rights that are no longer valid after the given move. """ 531 532 white_kingside_rook = chessai.core.coordinate.Coordinate(self.board.num_files - 1, 0) 533 white_queenside_rook = chessai.core.coordinate.Coordinate(0, 0) 534 black_kingside_rook = chessai.core.coordinate.Coordinate(self.board.num_files - 1, self.board.num_ranks - 1) 535 black_queenside_rook = chessai.core.coordinate.Coordinate(0, self.board.num_ranks - 1) 536 537 # King moves, which forfeits both rights for that color. 538 if (isinstance(piece, chessai.chess.piece.King)): 539 if (piece.color == chessai.core.types.Color.WHITE): 540 self.castling_rights = self.castling_rights.revoke_rights(chessai.core.castling.ALL_WHITE_RIGHTS) 541 else: 542 self.castling_rights = self.castling_rights.revoke_rights(chessai.core.castling.ALL_BLACK_RIGHTS) 543 544 # Rook moves from its starting coordinate. 545 if (action.start_coordinate == white_kingside_rook): 546 self.castling_rights = self.castling_rights.revoke_rights(chessai.core.castling.WHITE_KINGSIDE_RIGHTS) 547 548 if (action.start_coordinate == white_queenside_rook): 549 self.castling_rights = self.castling_rights.revoke_rights(chessai.core.castling.WHITE_QUEENSIDE_RIGHTS) 550 551 if (action.start_coordinate == black_kingside_rook): 552 self.castling_rights = self.castling_rights.revoke_rights(chessai.core.castling.BLACK_KINGSIDE_RIGHTS) 553 554 if (action.start_coordinate == black_queenside_rook): 555 self.castling_rights = self.castling_rights.revoke_rights(chessai.core.castling.BLACK_QUEENSIDE_RIGHTS) 556 557 # Rook captured on its starting coordinate. 558 if (action.end_coordinate == white_kingside_rook): 559 self.castling_rights = self.castling_rights.revoke_rights(chessai.core.castling.WHITE_KINGSIDE_RIGHTS) 560 561 if (action.end_coordinate == white_queenside_rook): 562 self.castling_rights = self.castling_rights.revoke_rights(chessai.core.castling.WHITE_QUEENSIDE_RIGHTS) 563 564 if (action.end_coordinate == black_kingside_rook): 565 self.castling_rights = self.castling_rights.revoke_rights(chessai.core.castling.BLACK_KINGSIDE_RIGHTS) 566 567 if (action.end_coordinate == black_queenside_rook): 568 self.castling_rights = self.castling_rights.revoke_rights(chessai.core.castling.BLACK_QUEENSIDE_RIGHTS) 569 570 def _compute_en_passant(self, 571 action: chessai.core.action.MoveAction, 572 piece: chessai.core.piece.Piece) -> chessai.core.coordinate.Coordinate | None: 573 """ 574 Return the new en-passant target coordinate after this move, or None. 575 576 A target coordinate is set only when a pawn advances two ranks. 577 The target is the coordinate the pawn passed through (i.e. one rank behind the destination), which is standard FEN notation. 578 """ 579 580 if (not isinstance(piece, chessai.chess.piece.Pawn)): 581 return None 582 583 if (action.start_coordinate.rank_distance(action.end_coordinate) != 2): 584 return None 585 586 # The en-passant target is the coordinate the pawn skipped over. 587 passed_rank = (action.start_coordinate.rank + action.end_coordinate.rank) // 2 588 return chessai.core.coordinate.Coordinate(action.start_coordinate.file, passed_rank) 589 590 def copy(self, 591 context: typing.Union[edq.util.serial.SerializationContext, None] = None, 592 ) -> 'GameState': 593 new_state = super().copy() 594 new_state = typing.cast(GameState, new_state) 595 596 return new_state
A game state specific to a standard Pacman game.
13 def game_complete(self) -> tuple[list[chessai.core.types.Color], float]: 14 winners = self.get_winners() 15 16 # Score is based on white's perspective using standard chess scoring. 17 if (chessai.core.types.Color.WHITE in winners): 18 score = 1.0 19 elif (chessai.core.types.Color.BLACK in winners): 20 score = 0.0 21 else: 22 score = 0.5 23 24 return (winners, score)
Indicate that the game has ended. The state should take any final actions and return the player colors of the winning agents (if any) and the score of the game.
26 def is_check(self, color: chessai.core.types.Color, started_in_check: bool = True) -> bool: 27 # Find the king of the given color. 28 king_coordinate = self.get_king_coordinate(color) 29 30 # If there is no King, they cannot be in check. 31 # This should never happen in standard chess games with valid positions. 32 if (king_coordinate is None): 33 return False 34 35 # Get the most recent action to try to avoid work when not doing a full check. 36 previous_action = self.get_previous_action() 37 38 if ((not started_in_check) and isinstance(previous_action, chessai.core.action.MoveAction)): 39 check_potential = self._has_check_potential(king_coordinate, previous_action) 40 else: 41 check_potential = True 42 43 # Skip checking moves that could not have opened us to check. 44 if (not check_potential): 45 return False 46 47 # Check if any opponent piece can attack the king's coordinate. 48 old_turn = self.turn 49 self.turn = color.opposite() 50 51 try: 52 opponent_actions = self._get_pseudo_legal_moves() 53 for action in opponent_actions: 54 if (action.end_coordinate == king_coordinate): 55 return True 56 57 return False 58 finally: 59 self.turn = old_turn
Determines if the given color, not the state's color, is in check.
def
get_king_coordinate( self, color: chessai.core.types.Color) -> chessai.core.coordinate.Coordinate | None:
61 def get_king_coordinate(self, color: chessai.core.types.Color) -> chessai.core.coordinate.Coordinate | None: 62 """ Get the coordinate of the king for the given color. """ 63 64 for (file, rank_dict) in self.board.pieces.items(): 65 for (rank, piece) in rank_dict.items(): 66 if ((piece.color == color) and (isinstance(piece, chessai.chess.piece.King))): 67 return chessai.core.coordinate.Coordinate(file, rank) 68 69 # If there's no king, the position is invalid, which should never happen. 70 return None
Get the coordinate of the king for the given color.
def
is_insufficient_material(self) -> bool:
209 def is_insufficient_material(self) -> bool: 210 # Meta actions cannot cause a game to become in an insufficient material state. 211 if (not isinstance(self.get_previous_action(), chessai.core.action.MoveAction)): 212 return False 213 214 for color in chessai.core.types.Color: 215 if (not self._is_insufficient_material(color)): 216 return False 217 218 return True
Processes if there is insufficient material to get a checkmate for all colors.
590 def copy(self, 591 context: typing.Union[edq.util.serial.SerializationContext, None] = None, 592 ) -> 'GameState': 593 new_state = super().copy() 594 new_state = typing.cast(GameState, new_state) 595 596 return new_state
Get a deep copy of this state.
Child classes are responsible for making any deep copies they need to.
Inherited Members
- chessai.core.gamestate.GameState
- GameState
- board
- turn
- castling_rights
- en_passant_coordinate
- halfmove_clock
- fullmove_number
- previous_action
- seed
- game_over
- get_fen
- get
- get_coordinate_map
- get_previous_action
- get_legal_actions
- get_legal_move_actions
- is_capture
- get_attacked_coordinates
- get_neighbors
- is_checkmate
- is_stalemate
- is_variant_win
- is_variant_loss
- is_game_over
- get_winners
- get_termination_reason
- get_action_from_san
- push
- generate_successor
- game_start
- agents_game_start
- process_agent_timeout
- process_agent_crash
- process_game_timeout
- process_turn
- process_turn_full
- get_gamestate_parser
- from_fen
def
base_eval( state: chessai.core.gamestate.GameState, action: chessai.core.action.Action | None = None, agent: typing.Any | None = None, **kwargs: Any) -> float:
598def base_eval( 599 state: chessai.core.gamestate.GameState, 600 action: chessai.core.action.Action | None = None, 601 agent: typing.Any | None = None, 602 **kwargs: typing.Any) -> float: 603 """ 604 The most basic evaluation function, which just uses the difference in piece value on the board. 605 """ 606 607 piece_values: dict[type[chessai.core.piece.Piece], int] = { 608 chessai.chess.piece.Pawn: 1, 609 chessai.chess.piece.Knight: 3, 610 chessai.chess.piece.Bishop: 3, 611 chessai.chess.piece.Rook: 5, 612 chessai.chess.piece.Queen: 9, 613 chessai.chess.piece.King: 9999, 614 } 615 616 # The difference in pieces from white's perspective. 617 board_value = 0 618 for piece in state.board.all_pieces(): 619 piece_value = piece_values.get(type(piece), 0) 620 if (piece.color == chessai.core.types.Color.WHITE): 621 board_value += piece_value 622 else: 623 board_value -= piece_value 624 625 if (state.turn == chessai.core.types.Color.WHITE): 626 return board_value 627 628 # The piece difference is the opposite for black. 629 return -1 * board_value
The most basic evaluation function, which just uses the difference in piece value on the board.