From 11d8e0de2b97457e0ac631dd38cc7495252c00df Mon Sep 17 00:00:00 2001 From: Stephan Kieburg Date: Sun, 16 Jul 2023 14:18:16 +0200 Subject: [PATCH 1/2] Initial support for parsing FEN notation strings --- ChessBoard.py | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/ChessBoard.py b/ChessBoard.py index 809f4e5..4ea010e 100644 --- a/ChessBoard.py +++ b/ChessBoard.py @@ -4,7 +4,6 @@ RANK_NAMES = ["8", "7", "6", "5", "4", "3", "2", "1"] SQUARE_NAMES = [f + r for r in RANK_NAMES for f in FILE_NAMES] TARGET_SQUARE_STATES = [EMPTY_SQUARE, SAME_COLOR, OPPOSITE_COLOR] = range(1, 4) -# not yet used, we would like to encode like this instead of array # noinspection SpellCheckingInspection FEN_INITIAL_POSITION = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1" @@ -54,25 +53,26 @@ def __repr__(self) -> str: return repr(self.start_square) + repr(self.end_square) -def create_initial_position(): - initial_position = [ - ["r", "n", "b", "q", "k", "b", "n", "r"], - ["p", "p", "p", "p", "p", "p", "p", "p"], - ["-", "-", "-", "-", "-", "-", "-", "-"], - ["-", "-", "-", "-", "-", "-", "-", "-"], - ["-", "-", "-", "-", "-", "-", "-", "-"], - ["-", "-", "-", "-", "-", "-", "-", "-"], - ["P", "P", "P", "P", "P", "P", "P", "P"], - ["R", "N", "B", "Q", "K", "B", "N", "R"] - ] - squares = [Square(file, rank, ChessPiece.from_symbol(initial_position[rank][file])) for rank in range(8) for file in - range(8)] +def create_position(fen: str): + fen_parts = fen.split("/")[:8] # Split and truncate before the first space + squares = [] + for rank_index, fen_rank in enumerate(fen_parts): + file_index = 0 + for char in fen_rank: + if char.isdigit(): # add as many empty squares as indicated by number + num_empty_squares = int(char) + squares.extend([Square(file_index + i, rank_index, piece=None) for i in range(num_empty_squares)]) + file_index += num_empty_squares + else: + piece = ChessPiece.from_symbol(char) + squares.append(Square(file_index, rank_index, piece)) + file_index += 1 return squares class ChessBoard: def __init__(self): - self.squares = create_initial_position() + self.squares = create_position(FEN_INITIAL_POSITION) self.current_player = WHITE self.moves = [] From 36bb0e54a05ac022acefc9ac432187258f7e09f2 Mon Sep 17 00:00:00 2001 From: Stephan Kieburg Date: Sun, 16 Jul 2023 14:19:11 +0200 Subject: [PATCH 2/2] [IGNORE] Small cleanups + moved pawn move generation into its own method --- ChessBoard.py | 49 ++++++++++++++++++---------------------- GameManager.py | 6 ++++- PythonChess.py | 6 +++-- Tests/ChessBoardTests.py | 9 ++++++-- 4 files changed, 38 insertions(+), 32 deletions(-) diff --git a/ChessBoard.py b/ChessBoard.py index 4ea010e..9e395b7 100644 --- a/ChessBoard.py +++ b/ChessBoard.py @@ -117,43 +117,38 @@ def all_target_squares(self, from_square: Square): moved_piece = from_square.piece if moved_piece is None: return [] - piece_type = moved_piece.piece_type - piece_color = moved_piece.color - straight_directions = ((-1, 0), (0, -1), (1, 0), (0, 1)) # up, left, down, right diagonal_directions = ((-1, -1), (-1, 1), (1, 1), (1, -1)) # up-left, up-right, down-right, down-left knight_directions = ((-2, -1), (-1, -2), (1, -2), (2, -1), (2, 1), (1, 2), (-1, 2), (-2, 1)) all_directions = straight_directions + diagonal_directions - if piece_type == KNIGHT: - return self.fields_in_direction(from_square, 1, knight_directions) - if piece_type == BISHOP: - return self.fields_in_direction(from_square, 7, diagonal_directions) - if piece_type == ROOK: - return self.fields_in_direction(from_square, 7, straight_directions) - if piece_type == QUEEN: - return self.fields_in_direction(from_square, 7, all_directions) + movements = {KNIGHT: (1, knight_directions), BISHOP: (7, diagonal_directions), + ROOK: (7, straight_directions), QUEEN: (7, all_directions), KING: (1, all_directions)} # TODO castling - if piece_type == KING: - return self.fields_in_direction(from_square, 1, all_directions) + return self.fields_in_direction(from_square, *movements[piece_type]) if piece_type != PAWN else \ + self.pawn_squares(from_square) + + def pawn_squares(self, from_square): target_squares = [] # TODO en-passant from_file = from_square.file from_rank = from_square.rank - if piece_type == PAWN: - direction_row = (-1, 1)[piece_color == BLACK] - starting_row = (6, 1)[piece_color == BLACK] - rank_one_step = from_rank + direction_row - rank_two_steps = from_rank + 2 * direction_row - if self.get_square(from_file, rank_one_step).status(piece_color) == EMPTY_SQUARE: - target_squares.append(self.get_square(from_file, rank_one_step)) - if from_rank == starting_row and self.get_square(from_file, rank_two_steps).status( - piece_color) == EMPTY_SQUARE: - target_squares.append(self.get_square(from_file, rank_two_steps)) - if from_file > 0 and self.get_square(from_file - 1, rank_one_step).status(piece_color) == OPPOSITE_COLOR: - target_squares.append(self.get_square(from_file - 1, rank_one_step)) - if self.get_square(from_file + 1, rank_one_step).status(piece_color) == OPPOSITE_COLOR: - target_squares.append(self.get_square(from_file + 1, rank_one_step)) + piece_color = from_square.piece.color + + direction_row = (-1, 1)[piece_color == BLACK] + starting_row = (6, 1)[piece_color == BLACK] + sq_1up = self.get_square(from_file, from_rank + direction_row) + sq_2up = self.get_square(from_file, from_rank + 2 * direction_row) + sq_1diag = self.get_square(from_file - 1, from_rank + direction_row) + sq_2diag = self.get_square(from_file + 1, from_rank + direction_row) + if sq_1up.status(piece_color) == EMPTY_SQUARE: + target_squares.append(sq_1up) + if from_rank == starting_row and sq_2up.status(piece_color) == EMPTY_SQUARE: + target_squares.append(sq_2up) + if from_file > 0 and sq_1diag.status(piece_color) == OPPOSITE_COLOR: + target_squares.append(sq_1diag) + if sq_2diag.status(piece_color) == OPPOSITE_COLOR: + target_squares.append(sq_2diag) return target_squares def fields_in_direction(self, start_square: Square, max_distance, directions): diff --git a/GameManager.py b/GameManager.py index 6e3e9ae..c684188 100644 --- a/GameManager.py +++ b/GameManager.py @@ -1,5 +1,7 @@ +from operator import contains + +from ChessBoard import ChessBoard, Move from ChessPiece import WHITE, BLACK -from ChessBoard import ChessBoard, Move, Square # Currently, this class is hardly used, since most logic is in the ChessBoard. This may change as new functionality @@ -20,6 +22,8 @@ def make_move(self, start: str, end: str): # Hm. learning to use @singledispatch, @overload. None of these were particularly better than this on first try def exec_move(self, start_square, end_square): move = Move(start_square, end_square) + if not str(move.end_square) in [str(x.end_square) for x in self.chessboard.legal_moves_from(start_square)]: + raise ValueError(f"{move} is not a legal move") move.execute() self.moves.append(move) self.switch_turn() diff --git a/PythonChess.py b/PythonChess.py index ce3dbd7..a7b44a8 100644 --- a/PythonChess.py +++ b/PythonChess.py @@ -1,7 +1,9 @@ -import pygame as pg import sys + +import pygame as pg + from BoardRenderer import BoardRenderer -from ChessBoard import ChessBoard, Move +from ChessBoard import ChessBoard from GameManager import GameManager diff --git a/Tests/ChessBoardTests.py b/Tests/ChessBoardTests.py index 02af035..a74dbb5 100644 --- a/Tests/ChessBoardTests.py +++ b/Tests/ChessBoardTests.py @@ -1,7 +1,6 @@ import pytest -from ChessBoard import ChessBoard, Square, Move -from ChessPiece import * +from ChessBoard import ChessBoard from GameManager import GameManager @@ -32,6 +31,12 @@ def test_is_check(chessboard): assert chessboard.is_check() +def test_raises_on_illegal_move(chessboard): + with pytest.raises(ValueError): + game_manager = GameManager(chessboard) + game_manager.make_move("e2", "e5") + + def test_is_mate(chessboard): game_manager = GameManager(chessboard)