introduced parsing uci moves, conversion to fen nad algebraic support
This commit is contained in:
+151
-12
@@ -69,7 +69,7 @@ class MoveType(Enum):
|
|||||||
NORMAL = auto()
|
NORMAL = auto()
|
||||||
CASTLING_KINGSIDE = auto()
|
CASTLING_KINGSIDE = auto()
|
||||||
CASTLING_QUEENSIDE = auto()
|
CASTLING_QUEENSIDE = auto()
|
||||||
EN_PASSANT = auto() #todo: implement
|
EN_PASSANT = auto()
|
||||||
PROMOTION = auto() #todo: implement
|
PROMOTION = auto() #todo: implement
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@@ -142,7 +142,7 @@ class ChessBoard:
|
|||||||
row_height = 7 if color == Color.WHITE else 0
|
row_height = 7 if color == Color.WHITE else 0
|
||||||
|
|
||||||
# validate move using legal moves for the player color
|
# validate move using legal moves for the player color
|
||||||
legal = self.moves_basic(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):
|
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
|
return False
|
||||||
if move.move_type == MoveType.NORMAL:
|
if move.move_type == MoveType.NORMAL:
|
||||||
@@ -165,6 +165,14 @@ class ChessBoard:
|
|||||||
ks_rook_dest = BoardPos((row_height, 3))
|
ks_rook_dest = BoardPos((row_height, 3))
|
||||||
self.move_piece(king_src, king_dest)
|
self.move_piece(king_src, king_dest)
|
||||||
self.move_piece(ks_rook_src, ks_rook_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.move_history.append((move, captured))
|
||||||
self.num_moves += 1
|
self.num_moves += 1
|
||||||
@@ -184,7 +192,10 @@ class ChessBoard:
|
|||||||
return captured
|
return captured
|
||||||
|
|
||||||
def get_field(self, pos: BoardPos) -> Optional[Piece]:
|
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
|
return self.fields[pos.x][pos.y].piece
|
||||||
|
|
||||||
# returns false if there is no move to unmake
|
# returns false if there is no move to unmake
|
||||||
def unmake_move(self) -> bool:
|
def unmake_move(self) -> bool:
|
||||||
if not self.move_history or len(self.move_history) == 0:
|
if not self.move_history or len(self.move_history) == 0:
|
||||||
@@ -265,7 +276,7 @@ class ChessBoard:
|
|||||||
return 0 <= rr < 8 and 0 <= cc < 8
|
return 0 <= rr < 8 and 0 <= cc < 8
|
||||||
|
|
||||||
# takes the color of the player whos possible moves will be returned
|
# takes the color of the player whos possible moves will be returned
|
||||||
def moves_basic(self, color: Color) -> List[BoardMove]:
|
def moves_unfiltered(self, color: Color) -> List[BoardMove]:
|
||||||
moves: List[BoardMove] = []
|
moves: List[BoardMove] = []
|
||||||
for pos, piece in self.iter_pieces():
|
for pos, piece in self.iter_pieces():
|
||||||
if piece.color != color:
|
if piece.color != color:
|
||||||
@@ -293,7 +304,20 @@ class ChessBoard:
|
|||||||
if target_field.piece is None:
|
if target_field.piece is None:
|
||||||
# empty target
|
# empty target
|
||||||
if is_pawn_capture_ray:
|
if is_pawn_capture_ray:
|
||||||
# pawn cant move diagonally into empty square
|
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[move_history_len][0]
|
||||||
|
|
||||||
|
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
|
break
|
||||||
moves.append(BoardMove(pos, BoardPos((tr, tc))))
|
moves.append(BoardMove(pos, BoardPos((tr, tc))))
|
||||||
continue
|
continue
|
||||||
@@ -439,13 +463,13 @@ class ChessBoard:
|
|||||||
# maybe there is a better way of doing it, but for now this should suffice
|
# maybe there is a better way of doing it, but for now this should suffice
|
||||||
|
|
||||||
us_moves_wo_check = []
|
us_moves_wo_check = []
|
||||||
for move in self.moves_basic(color):
|
for move in self.moves_unfiltered(color):
|
||||||
# do a move and then check, if the king is still in check
|
# do a move and then check, if the king is still in check
|
||||||
# if it isnt, add the move to the possibles ones
|
# if it isnt, add the move to the possibles ones
|
||||||
if not self.make_move(move, color):
|
if not self.make_move(move, color):
|
||||||
raise ValueError(f"self.moves_basic created a move, which cannot be done (hopefully unreachable). move: {move}")
|
raise ValueError(f"self.moves_basic created a move, which cannot be done (hopefully unreachable). move: {move}")
|
||||||
|
|
||||||
all_basic_enemy_moves = self.moves_basic(color.opposite)
|
all_basic_enemy_moves = self.moves_unfiltered(color.opposite)
|
||||||
king_pos = self.pos_of_king(color)
|
king_pos = self.pos_of_king(color)
|
||||||
king_in_check = False
|
king_in_check = False
|
||||||
for mv in all_basic_enemy_moves:
|
for mv in all_basic_enemy_moves:
|
||||||
@@ -486,6 +510,125 @@ class ChessBoard:
|
|||||||
lines.append("".join(row_chars))
|
lines.append("".join(row_chars))
|
||||||
return "\n".join(lines)
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
# https://en.wikipedia.org/wiki/Algebraic_notation_(chess)
|
||||||
|
@staticmethod
|
||||||
|
def pos_to_algebraic(pos: BoardPos) -> str:
|
||||||
|
file = chr(ord('a') + pos.y)
|
||||||
|
rank = str(8 - pos.x)
|
||||||
|
return f"{file}{rank}"
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def algebraic_to_pos(s: str) -> BoardPos:
|
||||||
|
if len(s) != 2:
|
||||||
|
raise ValueError("invalid algebraic square")
|
||||||
|
col = ord(s[0].lower()) - ord('a')
|
||||||
|
row = 8 - int(s[1])
|
||||||
|
return BoardPos((row, col))
|
||||||
|
|
||||||
|
# https://www.chess.com/de/terms/forsyth-edwards-notation-fen
|
||||||
|
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)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def play_random_game(board: Optional[ChessBoard] = None, max_moves: int = 400, verbose: bool = False) -> Tuple[ ChessBoard, int]:
|
def play_random_game(board: Optional[ChessBoard] = None, max_moves: int = 400, verbose: bool = False) -> Tuple[ ChessBoard, int]:
|
||||||
if board is None:
|
if board is None:
|
||||||
@@ -514,13 +657,9 @@ def play_random_game(board: Optional[ChessBoard] = None, max_moves: int = 400, v
|
|||||||
ok = board.make_move(move, current)
|
ok = board.make_move(move, current)
|
||||||
# debug assert
|
# debug assert
|
||||||
if not ok:
|
if not ok:
|
||||||
# if an unexpected failure happens, break and treat as draw
|
|
||||||
if verbose:
|
if verbose:
|
||||||
print(f"make_move returned False for move {move} by {current}")
|
print(f"make_move returned False for move {move} by {current}")
|
||||||
return board, moves_played
|
return board, moves_played
|
||||||
if move.move_type == MoveType.CASTLING_KINGSIDE or move.move_type == MoveType.CASTLING_QUEENSIDE:
|
|
||||||
print("hell yeah we castled")
|
|
||||||
raise ValueError("casling")
|
|
||||||
moves_played += 1
|
moves_played += 1
|
||||||
if verbose:
|
if verbose:
|
||||||
print(f"{moves_played:03d}: {current.name} played {move}\nboard:\n{board}")
|
print(f"{moves_played:03d}: {current.name} played {move}\nboard:\n{board}")
|
||||||
@@ -611,13 +750,13 @@ def main():
|
|||||||
raise AssertionError(f"initial board mismatch:\nexpected:\n{expected_start}\n\nactual:\n{actual_start}")
|
raise AssertionError(f"initial board mismatch:\nexpected:\n{expected_start}\n\nactual:\n{actual_start}")
|
||||||
|
|
||||||
# test num moves
|
# test num moves
|
||||||
mvs = default_brd.moves_basic(Color.BLACK)
|
mvs = default_brd.moves_unfiltered(Color.BLACK)
|
||||||
expected_move_count = 20 # 16 pawn moves and 4 knight
|
expected_move_count = 20 # 16 pawn moves and 4 knight
|
||||||
if len(mvs) != expected_move_count:
|
if len(mvs) != expected_move_count:
|
||||||
raise AssertionError(f"initial move count for black mismatch: expected {expected_move_count}, got {len(mvs)}")
|
raise AssertionError(f"initial move count for black mismatch: expected {expected_move_count}, got {len(mvs)}")
|
||||||
|
|
||||||
|
|
||||||
mvs = default_brd.moves_basic(Color.WHITE)
|
mvs = default_brd.moves_unfiltered(Color.WHITE)
|
||||||
expected_move_count = 20 # 16 pawn moves and 4 knight
|
expected_move_count = 20 # 16 pawn moves and 4 knight
|
||||||
if len(mvs) != expected_move_count:
|
if len(mvs) != expected_move_count:
|
||||||
raise AssertionError(f"Initial move count for white mismatch: expected {expected_move_count}, got {len(mvs)}")
|
raise AssertionError(f"Initial move count for white mismatch: expected {expected_move_count}, got {len(mvs)}")
|
||||||
|
|||||||
Reference in New Issue
Block a user