786 lines
29 KiB
Python
786 lines
29 KiB
Python
from enum import Enum, auto
|
|
from dataclasses import dataclass
|
|
from typing import Iterator, Optional, List, Tuple
|
|
import copy
|
|
FILES = "abcdefgh"
|
|
RANKS = "12345678"
|
|
|
|
|
|
|
|
|
|
class PieceType(Enum):
|
|
PAWN = auto()
|
|
KNIGHT = auto()
|
|
BISHOP = auto()
|
|
ROOK = auto()
|
|
QUEEN = auto()
|
|
KING = auto()
|
|
|
|
|
|
class Color(Enum):
|
|
WHITE = auto()
|
|
BLACK = auto()
|
|
|
|
@property
|
|
def opposite(self) -> "Color":
|
|
if self == Color.WHITE:
|
|
return Color.BLACK
|
|
else:
|
|
return Color.WHITE
|
|
|
|
class Outcome(Enum):
|
|
DRAW = auto()
|
|
WHITE_WIN = auto()
|
|
BLACK_WIN = auto()
|
|
NOT_FINISHED = auto()
|
|
|
|
@classmethod
|
|
def from_color(cls, color: Color) -> "Outcome":
|
|
if color == Color.WHITE:
|
|
return Outcome.WHITE_WIN
|
|
else:
|
|
return Outcome.BLACK_WIN
|
|
|
|
|
|
@dataclass
|
|
class BoardPos:
|
|
p: Tuple[int, int]
|
|
|
|
def __str__(self) -> str:
|
|
return f"({self.p[0]},{self.p[1]})"
|
|
|
|
@property
|
|
def x (self) -> int:
|
|
return self.p[0]
|
|
@property
|
|
def y (self) -> int:
|
|
return self.p[1]
|
|
|
|
|
|
@dataclass
|
|
class Piece:
|
|
type: PieceType
|
|
color: Color
|
|
|
|
# uppercase = White, lowercase = Black
|
|
def char(self) -> str:
|
|
|
|
mapping = {
|
|
PieceType.PAWN: "p",
|
|
PieceType.KNIGHT: "n",
|
|
PieceType.BISHOP: "b",
|
|
PieceType.ROOK: "r",
|
|
PieceType.QUEEN: "q",
|
|
PieceType.KING: "k",
|
|
}
|
|
ch = mapping[self.type]
|
|
return ch.upper() if self.color == Color.WHITE else ch
|
|
|
|
def __str__(self) -> str:
|
|
return self.char()
|
|
|
|
def __repr__(self) -> str:
|
|
return f"Piece({self.type.name},{self.color.name})"
|
|
|
|
|
|
class MoveType(Enum):
|
|
NORMAL = auto()
|
|
CASTLING_KINGSIDE = auto()
|
|
CASTLING_QUEENSIDE = auto()
|
|
EN_PASSANT = auto()
|
|
PROMOTION = auto() #todo: implement
|
|
|
|
@dataclass
|
|
class BoardMove:
|
|
m_from: BoardPos
|
|
m_to: BoardPos
|
|
move_type: MoveType = MoveType.NORMAL
|
|
promotion_piece: Optional[PieceType] = None #todo: implement and handle
|
|
|
|
def __str__(self) -> str:
|
|
return f"{self.m_from}->{self.m_to}"
|
|
|
|
|
|
@dataclass
|
|
class BoardField:
|
|
piece: Optional[Piece] = None
|
|
|
|
def __str__(self) -> str:
|
|
return str(self.piece) if self.piece is not None else "."
|
|
|
|
|
|
@dataclass
|
|
class ChessBoard:
|
|
fields: List[List[BoardField]]
|
|
num_moves: int
|
|
move_history: List[Tuple['BoardMove', Optional[Piece]]]
|
|
initial_board: List[List[BoardField]]
|
|
|
|
@property
|
|
def turn(self) -> "Color":
|
|
len_mv_history = len(self.move_history)
|
|
if len_mv_history > 0:
|
|
|
|
first_moved_piece_color = self.initial_move_color
|
|
if len_mv_history % 2 == 0:
|
|
return first_moved_piece_color
|
|
else:
|
|
return first_moved_piece_color.opposite
|
|
else:
|
|
return Color.WHITE
|
|
|
|
@property
|
|
def initial_move_color(self) -> Color:
|
|
first_move = self.move_history[0]
|
|
from_pos = first_move[0].m_from
|
|
# look up what color the piece had that moved first
|
|
# (odd solution but i think its quite fine)
|
|
first_moved_field = self.initial_board[from_pos.x][from_pos.y]
|
|
first_moved_piece_color = Color.WHITE
|
|
if first_moved_field.piece is not None:
|
|
first_moved_piece_color = first_moved_field.piece.color
|
|
return first_moved_piece_color
|
|
|
|
def is_checkmate(self) -> bool:
|
|
cur_turn_color = self.turn
|
|
return len(self.generate_moves(cur_turn_color)) == 0 and self.is_field_attacked(self.pos_of_king(cur_turn_color), cur_turn_color.opposite)
|
|
|
|
def is_stalemate(self) -> bool:
|
|
cur_turn_color = self.turn
|
|
return len(self.generate_moves(cur_turn_color)) == 0
|
|
|
|
def is_seventyfive_moves(self) -> bool:
|
|
return len(self.move_history) >= 75
|
|
|
|
def is_fivefold_repetition(self) -> bool:
|
|
|
|
return self.highest_repetiton_amount() >= 5
|
|
|
|
def outcome(self) -> Outcome:
|
|
if self.is_checkmate():
|
|
return Outcome.from_color(self.turn)
|
|
if self.is_stalemate():
|
|
return Outcome.DRAW
|
|
if self.is_seventyfive_moves():
|
|
return Outcome.DRAW
|
|
if self.is_fivefold_repetition():
|
|
return Outcome.DRAW
|
|
return Outcome.NOT_FINISHED
|
|
|
|
def highest_repetiton_amount(self) -> int:
|
|
if self.move_history:
|
|
starting_color = self.initial_move_color
|
|
else:
|
|
starting_color = Color.WHITE
|
|
|
|
dummy = ChessBoard(
|
|
copy.deepcopy(self.initial_board),
|
|
0,
|
|
[],
|
|
copy.deepcopy(self.initial_board),
|
|
)
|
|
|
|
color = starting_color
|
|
states: list[str] = []
|
|
|
|
states.append(str(dummy))
|
|
|
|
for mv, _ in self.move_history:
|
|
dummy.make_move(mv, color)
|
|
color = color.opposite
|
|
states.append(str(dummy))
|
|
|
|
counts: dict[str, int] = {}
|
|
max_count = 0
|
|
|
|
for state in states:
|
|
counts[state] = counts.get(state, 0) + 1
|
|
if counts[state] > max_count:
|
|
max_count = counts[state]
|
|
|
|
return max_count
|
|
|
|
|
|
def place(self, row: int, col: int, piece_type: PieceType, color: Color):
|
|
self.fields[row][col].piece = Piece(piece_type, color)
|
|
|
|
# initialize default starting position
|
|
@classmethod
|
|
def init_default(cls) -> "ChessBoard":
|
|
empty_board = [[BoardField() for _ in range(8)] for _ in range(8)]
|
|
brd = cls(
|
|
empty_board, 0, [], copy.deepcopy(empty_board)
|
|
)
|
|
|
|
# place pawns
|
|
for col in range(8):
|
|
brd.place(1, col, PieceType.PAWN, Color.BLACK)
|
|
brd.place(6, col, PieceType.PAWN, Color.WHITE)
|
|
|
|
# back rank order
|
|
back_rank = [
|
|
PieceType.ROOK,
|
|
PieceType.KNIGHT,
|
|
PieceType.BISHOP,
|
|
PieceType.QUEEN,
|
|
PieceType.KING,
|
|
PieceType.BISHOP,
|
|
PieceType.KNIGHT,
|
|
PieceType.ROOK,
|
|
]
|
|
|
|
# place black back rank
|
|
for col, piece_type in enumerate(back_rank):
|
|
brd.place(0, col, piece_type, Color.BLACK)
|
|
|
|
# place white back rank
|
|
for col, piece_type in enumerate(back_rank):
|
|
brd.place(7, col, piece_type, Color.WHITE)
|
|
|
|
# set the initial board for later ref (must be a copy, not a reference)
|
|
brd.initial_board = copy.deepcopy(brd.fields)
|
|
|
|
return brd
|
|
|
|
# attempt to make move for color
|
|
# returns true on success
|
|
def make_move(self, move: BoardMove, color: Color) -> bool:
|
|
|
|
captured = None
|
|
row_height = 7 if color == Color.WHITE else 0
|
|
|
|
# validate move using legal moves for the player color
|
|
legal = self.moves_unfiltered(color)
|
|
if not any(m.m_from.p == move.m_from.p and m.m_to.p == move.m_to.p and m.move_type == move.move_type and m.promotion_piece == move.promotion_piece for m in legal):
|
|
return False
|
|
if move.move_type == MoveType.NORMAL:
|
|
# perform move and record history for unmaking
|
|
captured = self.move_piece(move.m_from, move.m_to).piece
|
|
|
|
#castling is hardcoded
|
|
elif move.move_type == MoveType.CASTLING_KINGSIDE:
|
|
king_src = BoardPos((row_height, 4))
|
|
king_dest = BoardPos((row_height, 6))
|
|
ks_rook_src = BoardPos((row_height, 7))
|
|
ks_rook_dest = BoardPos((row_height, 5))
|
|
self.move_piece(king_src, king_dest)
|
|
self.move_piece(ks_rook_src, ks_rook_dest)
|
|
|
|
elif move.move_type == MoveType.CASTLING_QUEENSIDE:
|
|
king_src = BoardPos((row_height, 4))
|
|
king_dest = BoardPos((row_height, 2))
|
|
ks_rook_src = BoardPos((row_height, 0))
|
|
ks_rook_dest = BoardPos((row_height, 3))
|
|
self.move_piece(king_src, king_dest)
|
|
self.move_piece(ks_rook_src, ks_rook_dest)
|
|
|
|
elif move.move_type == MoveType.EN_PASSANT:
|
|
# direction the enemys pawn moved in
|
|
en_passant_dir = 1 if color == Color.WHITE else -1
|
|
self.move_piece(move.m_from, move.m_to)
|
|
affected_pawn_pos = BoardPos((move.m_to.x - en_passant_dir, move.m_to.y))
|
|
captured = self.get_field(affected_pawn_pos)
|
|
self.fields[affected_pawn_pos.x][affected_pawn_pos.y].piece = None
|
|
|
|
self.move_history.append((move, captured))
|
|
self.num_moves += 1
|
|
return True
|
|
|
|
|
|
# moves one piece to another location
|
|
# returns the dest field
|
|
def move_piece(self, src_pos: BoardPos, dest_pos: BoardPos) -> BoardField:
|
|
src_field = self.fields[src_pos.x][src_pos.y]
|
|
dest_field = self.fields[dest_pos.x][dest_pos.y]
|
|
moving = src_field.piece
|
|
captured = BoardField(dest_field.piece)
|
|
# move
|
|
dest_field.piece = moving
|
|
src_field.piece = None
|
|
return captured
|
|
|
|
def get_field(self, pos: BoardPos) -> Optional[Piece]:
|
|
if not self.on_board(pos.x, pos.y):
|
|
return None
|
|
return self.fields[pos.x][pos.y].piece
|
|
|
|
# returns false if there is no move to unmake
|
|
def unmake_move(self) -> bool:
|
|
if not self.move_history or len(self.move_history) == 0:
|
|
return False
|
|
move, captured = self.move_history.pop()
|
|
sr, sc = move.m_from.p
|
|
tr, tc = move.m_to.p
|
|
|
|
moving_piece = self.fields[tr][tc].piece
|
|
|
|
# restore
|
|
self.fields[sr][sc].piece = moving_piece
|
|
self.fields[tr][tc].piece = captured
|
|
self.num_moves = max(0, self.num_moves - 1)
|
|
return True
|
|
|
|
# creates all theoretically possible moves which can be performed by the piece at a position
|
|
def moves_unchecked(self, piece: Piece, pos: BoardPos) -> List[List[Tuple[int, int]]]:
|
|
row, column = pos.p
|
|
rays: List[List[Tuple[int, int]]] = []
|
|
|
|
if piece.type == PieceType.KING:
|
|
king_offsets = [(1, 1), (1, 0), (0, 1), (-1, -1), (-1, 0), (0, -1), (1, -1), (-1, 1)]
|
|
rays.append([(row + dr, column + dc) for dr, dc in king_offsets])
|
|
elif piece.type == PieceType.KNIGHT:
|
|
knight_offsets = [
|
|
(2, 1), (2, -1), (-2, 1), (-2, -1),
|
|
(1, 2), (1, -2), (-1, 2), (-1, -2)
|
|
]
|
|
rays.append([(row + dr, column + dc) for dr, dc in knight_offsets])
|
|
elif piece.type == PieceType.PAWN:
|
|
# correct forward direction and start row depending on color
|
|
if piece.color == Color.WHITE:
|
|
forward = -1
|
|
start_row = 6
|
|
else:
|
|
forward = 1
|
|
start_row = 1
|
|
|
|
forward_ray: List[Tuple[int, int]] = []
|
|
forward_ray.append((row + forward, column))
|
|
# if the pawn is at the start row, include the double-step
|
|
if row == start_row:
|
|
forward_ray.append((row + 2 * forward, column))
|
|
rays.append(forward_ray)
|
|
|
|
# capture moves
|
|
captures = [(row + forward, column - 1), (row + forward, column + 1)]
|
|
rays.extend([[(rr, cc)] for rr, cc in captures])
|
|
elif piece.type == PieceType.BISHOP:
|
|
directions = [(1, 1), (1, -1), (-1, 1), (-1, -1)]
|
|
for dr, dc in directions:
|
|
ray = []
|
|
for k in range(1, 8):
|
|
ray.append((row + k * dr, column + k * dc))
|
|
rays.append(ray)
|
|
elif piece.type == PieceType.ROOK:
|
|
directions = [(1, 0), (-1, 0), (0, 1), (0, -1)]
|
|
for dr, dc in directions:
|
|
ray = []
|
|
for k in range(1, 8):
|
|
ray.append((row + k * dr, column + k * dc))
|
|
rays.append(ray)
|
|
elif piece.type == PieceType.QUEEN:
|
|
directions = [
|
|
(1, 0), (-1, 0), (0, 1), (0, -1),
|
|
(1, 1), (1, -1), (-1, 1), (-1, -1)
|
|
]
|
|
for dr, dc in directions:
|
|
ray = []
|
|
for k in range(1, 8):
|
|
ray.append((row + k * dr, column + k * dc))
|
|
rays.append(ray)
|
|
|
|
return rays
|
|
|
|
def on_board(self, rr: int, cc: int) -> bool:
|
|
return 0 <= rr < 8 and 0 <= cc < 8
|
|
|
|
# takes the color of the player whos possible moves will be returned
|
|
def moves_unfiltered(self, color: Color) -> List[BoardMove]:
|
|
moves: List[BoardMove] = []
|
|
for pos, piece in self.iter_pieces():
|
|
if piece.color != color:
|
|
continue
|
|
rays = self.moves_unchecked(piece, pos)
|
|
|
|
for ray in rays:
|
|
# detect pawn capture ray and dont allow movement in that ray without capturing
|
|
is_pawn_capture_ray = (
|
|
piece.type == PieceType.PAWN
|
|
and len(ray) == 1
|
|
and ray[0][1] != pos.y
|
|
and abs(ray[0][0] - pos.x) == 1
|
|
)
|
|
|
|
# non sliding rays get treated individually
|
|
non_sliding = piece.type in (PieceType.KNIGHT, PieceType.KING) or is_pawn_capture_ray
|
|
|
|
for (tr, tc) in ray:
|
|
if not self.on_board(tr, tc):
|
|
if non_sliding:
|
|
continue
|
|
break
|
|
target_field = self.fields[tr][tc]
|
|
if target_field.piece is None:
|
|
# empty target
|
|
if is_pawn_capture_ray:
|
|
move_history_len = len(self.move_history)
|
|
# pawn can only move diagonally into an empty square during en passant, check for that
|
|
en_passant_dir = 1 if color == Color.WHITE else -1
|
|
if move_history_len > 0:
|
|
# check if there is a pawn in the correct position
|
|
if self.get_field(BoardPos((tr, tc - en_passant_dir))) == Piece(
|
|
PieceType.PAWN,
|
|
color.opposite,
|
|
):
|
|
# check if the last move moved the pawn there from the starting square
|
|
last_move = self.move_history[-1][0] # final move, not past-the-end
|
|
|
|
if last_move.m_from == BoardPos((tr, tc + en_passant_dir)) and last_move.m_to == BoardPos((tr, tc - en_passant_dir)):
|
|
moves.append(BoardMove(pos, BoardPos((tr, tc))))
|
|
break
|
|
moves.append(BoardMove(pos, BoardPos((tr, tc))))
|
|
continue
|
|
else:
|
|
# occupied
|
|
if target_field.piece.color == piece.color:
|
|
if non_sliding:
|
|
continue
|
|
# for sliding pieces a blocker stops further squares
|
|
break
|
|
else:
|
|
# opponent piece: capture is allowed
|
|
moves.append(BoardMove(pos, BoardPos((tr, tc))))
|
|
if non_sliding:
|
|
continue
|
|
break
|
|
|
|
|
|
# manually adding castling
|
|
row_height = 7 if color == Color.WHITE else 0
|
|
expected_king_pos = BoardPos((row_height, 4))
|
|
|
|
if self.has_piece_moved(expected_king_pos):
|
|
return moves
|
|
|
|
expected_lhs_rook_pos = BoardPos((row_height, 0))
|
|
expected_rhs_rook_pos = BoardPos((row_height, 7))
|
|
|
|
# squares that must be empty between rook and king
|
|
lhs_pieces_inbetween = [BoardPos((row_height, i)) for i in range(1, 4)] # cols 1,2,3
|
|
rhs_pieces_inbetween = [BoardPos((row_height, i)) for i in range(5, 7)] # cols 5,6
|
|
|
|
# squares that must not be attacked by the opponent
|
|
lhs_fields_attacked = [BoardPos((row_height, i)) for i in (4, 3, 2)]
|
|
rhs_fields_attacked = [BoardPos((row_height, i)) for i in (4, 5, 6)]
|
|
|
|
# queenside
|
|
if (not self.has_piece_moved(expected_lhs_rook_pos)
|
|
and self.are_pieces_none(lhs_pieces_inbetween)
|
|
and not self.are_fields_attacked(lhs_fields_attacked, color.opposite)):
|
|
king_lhs_dest = BoardPos((row_height, 2))
|
|
moves.append(BoardMove(expected_king_pos, king_lhs_dest, MoveType.CASTLING_QUEENSIDE))
|
|
|
|
# kingside
|
|
if (not self.has_piece_moved(expected_rhs_rook_pos)
|
|
and self.are_pieces_none(rhs_pieces_inbetween)
|
|
and not self.are_fields_attacked(rhs_fields_attacked, color.opposite)):
|
|
king_rhs_dest = BoardPos((row_height, 6))
|
|
moves.append(BoardMove(expected_king_pos, king_rhs_dest, MoveType.CASTLING_KINGSIDE))
|
|
return moves
|
|
|
|
def has_piece_moved(self, pos: BoardPos) -> bool:
|
|
for move in self.move_history:
|
|
if move[0].m_from == pos:
|
|
return True
|
|
elif move[0].m_to == pos:
|
|
return True
|
|
return False
|
|
|
|
def are_pieces_none(self, positions: List[BoardPos]):
|
|
are_gone = True
|
|
for pos in positions:
|
|
if not self.is_piece_none(pos):
|
|
are_gone = False
|
|
return are_gone
|
|
|
|
def is_piece_none(self, pos: BoardPos) -> bool:
|
|
piece = self.fields[pos.x][pos.y].piece
|
|
if piece is None:
|
|
return True
|
|
else:
|
|
return False
|
|
|
|
def is_piece_of_type(self, pos: BoardPos, piece_type: PieceType) -> bool:
|
|
piece = self.fields[pos.x][pos.y].piece
|
|
if piece is None:
|
|
return False
|
|
return piece.type == piece_type
|
|
|
|
# made by chatgpt
|
|
def iter_pieces(self) -> Iterator[Tuple[BoardPos, Piece]]:
|
|
for row_idx, row in enumerate(self.fields):
|
|
for col_idx, field in enumerate(row):
|
|
if field.piece is not None:
|
|
yield BoardPos((row_idx, col_idx)), field.piece
|
|
|
|
def pos_of_king(self, color: Color) -> BoardPos:
|
|
for pos, piece in self.iter_pieces():
|
|
if piece.type == PieceType.KING and piece.color == color:
|
|
return pos
|
|
raise ValueError("player has no king")
|
|
|
|
# returns true if one field is actively being attacked
|
|
def are_fields_attacked(self, fields: List[BoardPos], attacker_color: Color) -> bool:
|
|
for field in fields:
|
|
if self.is_field_attacked(field, attacker_color):
|
|
return True
|
|
return False
|
|
|
|
# checks if the field is being actively attacked by any of the pieces of the attacker color
|
|
# this "complicated" implementation was necessary to not rely on recursive calls
|
|
def is_field_attacked(self, pos: BoardPos, attacker_color: Color) -> bool:
|
|
# iterate opponent pieces and check whether they attack
|
|
for attacker_pos, attacker in self.iter_pieces():
|
|
if attacker.color != attacker_color:
|
|
continue
|
|
|
|
rays = self.moves_unchecked(attacker, attacker_pos)
|
|
|
|
if attacker.type == PieceType.PAWN:
|
|
# pawn attacks are the pawn capture rays only
|
|
forward = -1 if attacker.color == Color.WHITE else 1
|
|
for dc in (-1, 1):
|
|
rr = attacker_pos.x + forward
|
|
cc = attacker_pos.y + dc
|
|
if not self.on_board(rr, cc):
|
|
continue
|
|
if BoardPos((rr, cc)) == pos:
|
|
return True
|
|
continue
|
|
|
|
if attacker.type in (PieceType.KNIGHT, PieceType.KING):
|
|
# non-sliding
|
|
for (rr, cc) in rays[0]:
|
|
if not self.on_board(rr, cc):
|
|
continue
|
|
if BoardPos((rr, cc)) == pos:
|
|
return True
|
|
continue
|
|
|
|
# sliding pieces
|
|
for ray in rays:
|
|
for (rr, cc) in ray:
|
|
if not self.on_board(rr, cc):
|
|
break
|
|
if BoardPos((rr, cc)) == pos:
|
|
return True
|
|
# stop at first occupied square (blocker)
|
|
if self.fields[rr][cc].piece is not None:
|
|
break
|
|
|
|
return False
|
|
|
|
# if a player does not have any moves, it has lost
|
|
def generate_moves(self, color: Color) -> List[BoardMove]:
|
|
|
|
# only moves after which the king is not in check are allowed
|
|
# todo: the current method of checking what move will resolve the check is based on bruteforcing
|
|
# maybe there is a better way of doing it, but for now this should suffice
|
|
|
|
us_moves_wo_check = []
|
|
for move in self.moves_unfiltered(color):
|
|
# do a move and then check, if the king is still in check
|
|
# if it isnt, add the move to the possibles ones
|
|
if not self.make_move(move, color):
|
|
raise ValueError(f"self.moves_basic created a move, which cannot be done (hopefully unreachable). move: {move}")
|
|
|
|
all_basic_enemy_moves = self.moves_unfiltered(color.opposite)
|
|
king_pos = self.pos_of_king(color)
|
|
king_in_check = False
|
|
for mv in all_basic_enemy_moves:
|
|
if mv.m_to == king_pos:
|
|
king_in_check = True
|
|
if not king_in_check:
|
|
us_moves_wo_check.append(move)
|
|
|
|
if not self.unmake_move():
|
|
raise ValueError("failed to unmake move, shouldnt be an issue here")
|
|
|
|
# check if our move lands directly on the enemies king, if it does, its illigal
|
|
us_moves_rule_compliant = []
|
|
king_pos = self.pos_of_king(color.opposite)
|
|
for move in us_moves_wo_check:
|
|
if move.m_to != king_pos:
|
|
us_moves_rule_compliant.append(move)
|
|
|
|
return us_moves_rule_compliant
|
|
|
|
|
|
def __str__(self) -> str:
|
|
"""
|
|
example string repr of starting pos:
|
|
|
|
rnbqkbnr
|
|
pppppppp
|
|
........
|
|
........
|
|
........
|
|
........
|
|
PPPPPPPP
|
|
RNBQKBNR
|
|
"""
|
|
lines: List[str] = []
|
|
for r in range(8):
|
|
row_chars = [str(self.fields[r][c]) for c in range(8)]
|
|
lines.append("".join(row_chars))
|
|
return "\n".join(lines)
|
|
|
|
# https://en.wikipedia.org/wiki/Algebraic_notation_(chess)
|
|
@staticmethod
|
|
def pos_to_algebraic(pos: BoardPos) -> str:
|
|
file = FILES[pos.y]
|
|
rank = RANKS[7 - pos.x]
|
|
return file + rank
|
|
|
|
@staticmethod
|
|
def algebraic_to_pos(s: str) -> BoardPos:
|
|
if len(s) != 2:
|
|
raise ValueError("invalid algebraic square")
|
|
|
|
file_char = s[0].lower()
|
|
rank_char = s[1]
|
|
|
|
if file_char not in FILES or rank_char not in RANKS:
|
|
raise ValueError("invalid algebraic square")
|
|
|
|
col = FILES.index(file_char)
|
|
row = 7 - RANKS.index(rank_char)
|
|
|
|
return BoardPos((row, col))
|
|
|
|
# this function made by chatgpt
|
|
def to_fen(self) -> str:
|
|
# piece placement
|
|
ranks: List[str] = []
|
|
|
|
for r in range(8):
|
|
empty = 0
|
|
row_str = ""
|
|
for c in range(8):
|
|
p = self.fields[r][c].piece
|
|
if p is None:
|
|
empty += 1
|
|
else:
|
|
if empty > 0:
|
|
row_str += str(empty)
|
|
empty = 0
|
|
row_str += p.char()
|
|
if empty > 0:
|
|
row_str += str(empty)
|
|
ranks.append(row_str)
|
|
placement = "/".join(ranks)
|
|
|
|
active = 'w' if (self.num_moves % 2) == 0 else 'b'
|
|
|
|
# castling availability
|
|
castling = ''
|
|
# white K/Q
|
|
if (self.fields[7][4].piece is not None and self.fields[7][4].piece.type == PieceType.KING and self.fields[7][4].piece.color == Color.WHITE
|
|
and self.fields[7][7].piece is not None and self.fields[7][7].piece.type == PieceType.ROOK and self.fields[7][7].piece.color == Color.WHITE
|
|
and not self.has_piece_moved(BoardPos((7,4))) and not self.has_piece_moved(BoardPos((7,7)))):
|
|
castling += 'K'
|
|
if (self.fields[7][4].piece is not None and self.fields[7][4].piece.type == PieceType.KING and self.fields[7][4].piece.color == Color.WHITE
|
|
and self.fields[7][0].piece is not None and self.fields[7][0].piece.type == PieceType.ROOK and self.fields[7][0].piece.color == Color.WHITE
|
|
and not self.has_piece_moved(BoardPos((7,4))) and not self.has_piece_moved(BoardPos((7,0)))):
|
|
castling += 'Q'
|
|
# black k/q
|
|
if (self.fields[0][4].piece is not None and self.fields[0][4].piece.type == PieceType.KING and self.fields[0][4].piece.color == Color.BLACK
|
|
and self.fields[0][7].piece is not None and self.fields[0][7].piece.type == PieceType.ROOK and self.fields[0][7].piece.color == Color.BLACK
|
|
and not self.has_piece_moved(BoardPos((0,4))) and not self.has_piece_moved(BoardPos((0,7)))):
|
|
castling += 'k'
|
|
if (self.fields[0][4].piece is not None and self.fields[0][4].piece.type == PieceType.KING and self.fields[0][4].piece.color == Color.BLACK
|
|
and self.fields[0][0].piece is not None and self.fields[0][0].piece.type == PieceType.ROOK and self.fields[0][0].piece.color == Color.BLACK
|
|
and not self.has_piece_moved(BoardPos((0,4))) and not self.has_piece_moved(BoardPos((0,0)))):
|
|
castling += 'q'
|
|
if castling == '':
|
|
castling = '-'
|
|
|
|
# en passant target square: if last move was a pawn double-step, set the square behind it
|
|
ep = '-'
|
|
if len(self.move_history) > 0:
|
|
last_move = self.move_history[-1][0]
|
|
moved_piece = self.get_field(last_move.m_to)
|
|
if moved_piece is not None and moved_piece.type == PieceType.PAWN and abs(last_move.m_from.x - last_move.m_to.x) == 2:
|
|
passed_row = (last_move.m_from.x + last_move.m_to.x) // 2
|
|
passed_col = last_move.m_from.y
|
|
ep = self.pos_to_algebraic(BoardPos((passed_row, passed_col)))
|
|
|
|
# todo half moves not yet tracked
|
|
halfmove = 0
|
|
|
|
# fullmove number
|
|
fullmove = (self.num_moves // 2) + 1
|
|
|
|
return f"{placement} {active} {castling} {ep} {halfmove} {fullmove}"
|
|
|
|
# https://backscattering.de/chess/uci/
|
|
def make_move_uci(self, uci: str, color: Color) -> bool:
|
|
if uci == '0000':
|
|
return False
|
|
if len(uci) < 4:
|
|
return False
|
|
src = self.algebraic_to_pos(uci[0:2])
|
|
dst = self.algebraic_to_pos(uci[2:4])
|
|
promo_piece = None
|
|
if len(uci) == 5:
|
|
pc = uci[4].lower()
|
|
mapping = {'q': PieceType.QUEEN, 'r': PieceType.ROOK, 'b': PieceType.BISHOP, 'n': PieceType.KNIGHT}
|
|
promo_piece = mapping.get(pc, None)
|
|
|
|
moving = self.get_field(src)
|
|
if moving is None or moving.color != color:
|
|
return False
|
|
|
|
# detect castling by king moving two files
|
|
move_type = MoveType.NORMAL
|
|
if moving.type == PieceType.KING and abs(src.y - dst.y) == 2:
|
|
move_type = MoveType.CASTLING_KINGSIDE if dst.y > src.y else MoveType.CASTLING_QUEENSIDE
|
|
else:
|
|
# en passant detection: pawn moves diagonally to empty square
|
|
if moving.type == PieceType.PAWN and src.y != dst.y and self.get_field(dst) is None:
|
|
if len(self.move_history) > 0:
|
|
# make sure the last move was a pawn move for this en passant to be possible
|
|
last = self.move_history[-1][0]
|
|
if last.m_from.x - last.m_to.x == 2 and last.m_to.x == src.x and last.m_to.y == dst.y:
|
|
move_type = MoveType.EN_PASSANT
|
|
|
|
# promotion
|
|
if promo_piece is not None:
|
|
move_type = MoveType.PROMOTION
|
|
|
|
bm = BoardMove(src, dst, move_type, promotion_piece=promo_piece)
|
|
return self.make_move(bm, color)
|
|
|
|
# debugging purposes only
|
|
def print_legal_moves(self, color: Color) -> None:
|
|
moves = self.generate_moves(color)
|
|
|
|
if not moves:
|
|
print(f"{color.name}: no legal moves")
|
|
return
|
|
|
|
move_strings = []
|
|
for move in moves:
|
|
from_sq = self.pos_to_algebraic(move.m_from)
|
|
to_sq = self.pos_to_algebraic(move.m_to)
|
|
move_str = f"{from_sq}-{to_sq}"
|
|
|
|
# Add move type notation
|
|
if move.move_type == MoveType.CASTLING_KINGSIDE:
|
|
move_str += " (O-O)"
|
|
elif move.move_type == MoveType.CASTLING_QUEENSIDE:
|
|
move_str += " (O-O-O)"
|
|
elif move.move_type.name == MoveType.EN_PASSANT:
|
|
move_str += " (e.p.)"
|
|
|
|
move_strings.append(move_str)
|
|
|
|
print(f"\n{color.name} legal Moves ({len(moves)} total):")
|
|
|
|
# Print in columns
|
|
moves_per_row = 6
|
|
for i in range(0, len(move_strings), moves_per_row):
|
|
row = move_strings[i:i + moves_per_row]
|
|
print(" " + " | ".join(f"{m:12}" for m in row))
|
|
|
|
|