diff --git a/app/chess_sim/test.py b/app/chess_sim/test.py index 3ac5502..d1a964d 100644 --- a/app/chess_sim/test.py +++ b/app/chess_sim/test.py @@ -1,6 +1,6 @@ from enum import Enum, auto from dataclasses import dataclass -from typing import Optional +from typing import Optional, List, Tuple class PieceType(Enum): @@ -12,38 +12,69 @@ class PieceType(Enum): KING = auto() - class Color(Enum): WHITE = auto() BLACK = auto() + @dataclass class BoardPos: - p: tuple[int, int] + p: Tuple[int, int] + + def __str__(self) -> str: + return f"({self.p[0]},{self.p[1]})" + @dataclass class Piece: type: PieceType color: Color + def char(self) -> str: + """Single-character representation. Uppercase = White, lowercase = Black.""" + 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})" + + @dataclass class BoardMove: m_from: BoardPos m_to: BoardPos + 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]] + fields: List[List[BoardField]] def place(self, row: int, col: int, piece_type: PieceType, color: Color): self.fields[row][col].piece = Piece(piece_type, color) - # wir lieben python <3 :( + # initialize default starting position @classmethod def init_default(cls) -> "ChessBoard": brd = cls( @@ -75,11 +106,11 @@ class ChessBoard: for col, piece_type in enumerate(back_rank): brd.place(7, col, piece_type, Color.WHITE) - return brd + return brd - def moves_unchecked(self, piece: Piece, pos: BoardPos) -> list[list[tuple[int, int]]]: + def moves_unchecked(self, piece: Piece, pos: BoardPos) -> List[List[Tuple[int, int]]]: row, column = pos.p - rays: list[list[tuple[int, int]]] = [] + 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)] @@ -99,7 +130,7 @@ class ChessBoard: forward = 1 start_row = 1 - forward_ray: list[tuple[int, int]] = [] + 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: @@ -139,12 +170,15 @@ class ChessBoard: def on_board(self, rr: int, cc: int) -> bool: return 0 <= rr < 8 and 0 <= cc < 8 - def moves_basic_checked(self) -> list[BoardMove]: - moves: list[BoardMove] = [] + # takes the color of the player whos possible moves will be returned + def moves_basic_checked(self, color: Color) -> List[BoardMove]: + moves: List[BoardMove] = [] for i, row in enumerate(self.fields): for j, field in enumerate(row): if field.piece is None: continue + if field.piece.color != color: + continue src_pos = BoardPos((i, j)) piece = field.piece rays = self.moves_unchecked(piece, src_pos) @@ -182,20 +216,99 @@ class ChessBoard: # opponent piece moves.append(BoardMove(src_pos, BoardPos((tr, tc)))) break - return moves - def possible_moves(self) -> list[BoardMove]: + 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) + + @classmethod + def from_string(cls, s: str) -> "ChessBoard": + lines = s.strip().split("\n") + + brd = cls([[BoardField() for _ in range(8)] for _ in range(8)]) + + char_to_piece = { + 'p': PieceType.PAWN, + 'n': PieceType.KNIGHT, + 'b': PieceType.BISHOP, + 'r': PieceType.ROOK, + 'q': PieceType.QUEEN, + 'k': PieceType.KING, + } + + for r, line in enumerate(lines): + for c, ch in enumerate(line): + if ch == '.': + continue + lower = ch.lower() + if lower not in char_to_piece: + raise ValueError(f"Invalid piece character '{ch}' at ({r},{c})") + piece_type = char_to_piece[lower] + color = Color.WHITE if ch.isupper() else Color.BLACK + brd.place(r, c, piece_type, color) + + return brd + - return self.moves_basic_checked() - # used only for testing purposes def main(): default_brd = ChessBoard.init_default() - mvs = default_brd.possible_moves() - for mv in mvs: - print(mv) - print(f"len of moves: { len(mvs)}") -main() \ No newline at end of file + # standardized expected string for initial position + expected_start = ( + "rnbqkbnr\n" + "pppppppp\n" + "........\n" + "........\n" + "........\n" + "........\n" + "PPPPPPPP\n" + "RNBQKBNR" + ) + + # test string representation of board + actual_start = str(default_brd) + if actual_start != expected_start: + raise AssertionError(f"initial board mismatch:\nexpected:\n{expected_start}\n\nactual:\n{actual_start}") + + # test num moves + mvs = default_brd.moves_basic_checked(Color.BLACK) + expected_move_count = 20 # 16 pawn moves and 4 knight + if len(mvs) != expected_move_count: + raise AssertionError(f"Initial move count for black mismatch: expected {expected_move_count}, got {len(mvs)}") + + + mvs = default_brd.moves_basic_checked(Color.WHITE) + expected_move_count = 20 # 16 pawn moves and 4 knight + if len(mvs) != expected_move_count: + raise AssertionError(f"Initial move count for white mismatch: expected {expected_move_count}, got {len(mvs)}") + + + # test loading + loaded_brd = ChessBoard.from_string(expected_start) + if str(loaded_brd) != expected_start: + raise AssertionError( + "board loaded from string does not match original representation" + ) + + print(f"all tests passed") + + +if __name__ == '__main__': + main()