4 Commits

Author SHA1 Message Date
e6fafc8081 implemented en passant
Some checks failed
pre-release / Pre Release (push) Waiting to run
tagged-release / Tagged Release (push) Has been cancelled
2025-01-31 18:55:02 +01:00
1be71bf203 fixed giga spelling mistake (it's castling right, not castling write) 2025-01-31 18:27:44 +01:00
e862ab6b0b added comments to the make_move function 2025-01-31 18:24:52 +01:00
6c0819428e implemented checkmate
Some checks failed
pre-release / Pre Release (push) Waiting to run
tagged-release / Tagged Release (push) Has been cancelled
2025-01-31 18:20:12 +01:00
12 changed files with 132 additions and 41 deletions

View File

@ -55,3 +55,9 @@ class Controller:
else: else:
move = legal_moves_positions[0] move = legal_moves_positions[0]
self._make_move(move) self._make_move(move)
if self._board.is_checkmate_for(self._board._turn):
self._view.notify_checkmate(self._board._turn)
if self._board.is_stalemate_for(self._board._turn):
self._view.notify_stalemate(self._board._turn)

View File

@ -15,8 +15,8 @@ class Board:
self._white: dict[Position, Piece] = {} self._white: dict[Position, Piece] = {}
self._black: dict[Position, Piece] = {} self._black: dict[Position, Piece] = {}
self._turn = None self._turn = None
self._white_castling_write = set() self._white_castling_rights = set()
self._black_castling_write = set() self._black_castling_rights = set()
self._en_passant_target = None self._en_passant_target = None
@staticmethod @staticmethod
@ -88,13 +88,13 @@ class Board:
sides = "kq" sides = "kq"
assert c in sides or c in sides.upper(), f"The FEN position is malformed, the castling rights should be either k or q (both either lower- or upper-case), instead is '{c}'" assert c in sides or c in sides.upper(), f"The FEN position is malformed, the castling rights should be either k or q (both either lower- or upper-case), instead is '{c}'"
if c == "K": if c == "K":
ret._white_castling_write.add(CastleSide.King) ret._white_castling_rights.add(CastleSide.King)
if c == "Q": if c == "Q":
ret._white_castling_write.add(CastleSide.Queen) ret._white_castling_rights.add(CastleSide.Queen)
if c == "k": if c == "k":
ret._black_castling_write.add(CastleSide.King) ret._black_castling_rights.add(CastleSide.King)
if c == "q": if c == "q":
ret._black_castling_write.add(CastleSide.Queen) ret._black_castling_rights.add(CastleSide.Queen)
# -- En passant target # -- En passant target
if position[index] != "-": if position[index] != "-":
@ -131,13 +131,29 @@ class Board:
if Position.is_within_bounds(x, y): if Position.is_within_bounds(x, y):
possible_pos.append(Position(x, y)) possible_pos.append(Position(x, y))
else: else:
possible_pos += [move.pos for move in piece.legal_moves(self)] possible_pos += [move.pos for move in piece.legal_moves(self, looking_for_check=True)]
if king.pos in possible_pos: if king.pos in possible_pos:
return True return True
return False return False
def castling_writes_for(self, colour: Colour) -> set[CastleSide]: def is_checkmate_for(self, colour: Colour) -> bool:
return self._white_castling_write if colour == Colour.WHITE else self._black_castling_write """ Is it checkmate for the defending colour passed as parameter """
return self.is_check_for(colour) and self._no_legal_moves_for(colour)
def is_stalemate_for(self, colour: Colour) -> bool:
""" Is it stalemate for the defending colour passed as parameter """
return not self.is_check_for(colour) and self._no_legal_moves_for(colour)
def _no_legal_moves_for(self, colour: Colour) -> bool:
""" Return true if there are indeed no legal moves for the given colour (for checkmate or stalemate)"""
pieces = self._white if colour == Colour.WHITE else self._black
for piece in pieces.values():
if len(piece.legal_moves(self)) > 0:
return False
return True
def castling_rights_for(self, colour: Colour) -> set[CastleSide]:
return self._white_castling_rights if colour == Colour.WHITE else self._black_castling_rights
def make_move(self, move: Move) -> "Board": def make_move(self, move: Move) -> "Board":
dest_piece = self.piece_at(move.pos.x, move.pos.y) dest_piece = self.piece_at(move.pos.x, move.pos.y)
@ -145,15 +161,18 @@ class Board:
if dest_piece: if dest_piece:
assert dest_piece.colour != move.piece.colour, "A piece cannot cannot eat another piece of the same colour" assert dest_piece.colour != move.piece.colour, "A piece cannot cannot eat another piece of the same colour"
# -- Copy current state
ret = Board() ret = Board()
ret._white = self._white.copy() ret._white = self._white.copy()
ret._black = self._black.copy() ret._black = self._black.copy()
ret._turn = Colour.WHITE if self._turn == Colour.BLACK else Colour.BLACK ret._turn = Colour.WHITE if self._turn == Colour.BLACK else Colour.BLACK
ret._white_castling_write = self._white_castling_write.copy() ret._white_castling_rights = self._white_castling_rights.copy()
ret._black_castling_write = self._black_castling_write.copy() ret._black_castling_rights = self._black_castling_rights.copy()
ret._en_passant_target = self._en_passant_target
piece = move.piece piece = move.piece
# -- Actually make the move
pieces_moving, other_pieces = (ret._white, ret._black) if piece.colour == Colour.WHITE else (ret._black, ret._white) pieces_moving, other_pieces = (ret._white, ret._black) if piece.colour == Colour.WHITE else (ret._black, ret._white)
del pieces_moving[piece.pos] del pieces_moving[piece.pos]
@ -161,6 +180,17 @@ class Board:
if move.pos in other_pieces: if move.pos in other_pieces:
del other_pieces[move.pos] del other_pieces[move.pos]
if move.en_passant:
pos_to_remove = Position(move.pos.x, move.pos.y + (1 if self._turn == Colour.BLACK else -1))
del other_pieces[pos_to_remove]
# -- Set en passant target if needed
if move.becomes_en_passant_target:
ret._en_passant_target = pieces_moving[move.pos]
else:
ret._en_passant_target = None
# -- Handle castling (just move the rook over)
if move.castle_side == CastleSide.King: if move.castle_side == CastleSide.King:
rook_pos = Position(7, piece.pos.y) rook_pos = Position(7, piece.pos.y)
assert rook_pos in pieces_moving and type(pieces_moving[rook_pos]) == Rook, "Either rook is absent from the king side or you are trying to castle with something else than a rook..." assert rook_pos in pieces_moving and type(pieces_moving[rook_pos]) == Rook, "Either rook is absent from the king side or you are trying to castle with something else than a rook..."
@ -175,24 +205,25 @@ class Board:
new_rook_pos = Position(3, piece.pos.y) new_rook_pos = Position(3, piece.pos.y)
pieces_moving[new_rook_pos] = Rook(new_rook_pos, piece.colour) pieces_moving[new_rook_pos] = Rook(new_rook_pos, piece.colour)
# -- Check for castling rights
if piece.colour == Colour.WHITE: if piece.colour == Colour.WHITE:
if type(piece) == King: if type(piece) == King:
ret._white_castling_write = set() ret._white_castling_rights = set()
if type(piece) == Rook: if type(piece) == Rook:
if piece.pos.x == 0 and CastleSide.Queen in ret._white_castling_write: if piece.pos.x == 0 and CastleSide.Queen in ret._white_castling_rights:
ret._white_castling_write.remove(CastleSide.Queen) ret._white_castling_rights.remove(CastleSide.Queen)
elif piece.pos.x == 7 and CastleSide.King in ret._white_castling_write: elif piece.pos.x == 7 and CastleSide.King in ret._white_castling_rights:
ret._white_castling_write.remove(CastleSide.King) ret._white_castling_rights.remove(CastleSide.King)
else: else:
if type(piece) == King: if type(piece) == King:
ret._black_castling_write = set() ret._black_castling_rights = set()
if type(piece) == Rook: if type(piece) == Rook:
if piece.pos.x == 0 and CastleSide.Queen in ret._black_castling_write: if piece.pos.x == 0 and CastleSide.Queen in ret._black_castling_rights:
ret._black_castling_write.remove(CastleSide.Queen) ret._black_castling_rights.remove(CastleSide.Queen)
elif piece.pos.x == 7 and CastleSide.King in ret._black_castling_write: elif piece.pos.x == 7 and CastleSide.King in ret._black_castling_rights:
ret._black_castling_write.remove(CastleSide.King) ret._black_castling_rights.remove(CastleSide.King)
return ret return ret

View File

@ -8,11 +8,13 @@ class CastleSide(Enum):
Queen = "O-O-O" Queen = "O-O-O"
class Move: class Move:
def __init__(self, piece: "Piece", pos: Position,/, is_capturing: bool = False, castle_side: CastleSide = CastleSide.Neither) -> None: def __init__(self, piece: "Piece", pos: Position,/, is_capturing: bool = False, castle_side: CastleSide = CastleSide.Neither, en_passant: bool = False, becomes_en_passant_target: bool = False) -> None:
self.piece = piece self.piece = piece
self.pos = pos self.pos = pos
self.is_capturing = is_capturing self.is_capturing = is_capturing
self.castle_side = castle_side self.castle_side = castle_side
self.becomes_en_passant_target = becomes_en_passant_target
self.en_passant = en_passant
def to_algebraic(self) -> str: def to_algebraic(self) -> str:
raise NotImplementedError("The move can't be translated to algbraic notation, as it was not implemented") raise NotImplementedError("The move can't be translated to algbraic notation, as it was not implemented")

View File

@ -2,7 +2,7 @@ from logic.move import Move
from .piece import Piece from .piece import Piece
class Bishop(Piece): class Bishop(Piece):
def legal_moves(self, board: "Board") -> list[Move]: def legal_moves(self, board: "Board", / , looking_for_check = False) -> list[Move]:
ret = [] ret = []
# looking north east # looking north east
@ -17,5 +17,8 @@ class Bishop(Piece):
# looking north west # looking north west
ret.extend(self._look_direction(board, -1, 1)) ret.extend(self._look_direction(board, -1, 1))
if not looking_for_check and board.is_check_for(self.colour):
return self.keep_only_blocking(ret, board)
return ret return ret

View File

@ -19,13 +19,15 @@ class King(Piece):
if not board_after_move.is_check_for(self.colour): if not board_after_move.is_check_for(self.colour):
ret.append(move) ret.append(move)
# -- Castles if board.is_check_for(self.colour):
castling_writes = board.castling_writes_for(self.colour) return self.keep_only_blocking(ret, board)
if len(castling_writes) == 0:
return ret
print(castling_writes)
if CastleSide.King in castling_writes: # -- Castles
castling_rights = board.castling_rights_for(self.colour)
if len(castling_rights) == 0:
return ret
if CastleSide.King in castling_rights:
clear = True clear = True
for dx in range(1, 3): for dx in range(1, 3):
x = self.pos.x + dx x = self.pos.x + dx
@ -43,7 +45,7 @@ class King(Piece):
if clear: if clear:
ret.append(Move(self, Position(6, self.pos.y), castle_side=CastleSide.King)) ret.append(Move(self, Position(6, self.pos.y), castle_side=CastleSide.King))
if CastleSide.Queen in castling_writes: if CastleSide.Queen in castling_rights:
clear = True clear = True
for dx in range(1, 3): for dx in range(1, 3):
x = self.pos.x - dx x = self.pos.x - dx
@ -62,8 +64,6 @@ class King(Piece):
if clear: if clear:
ret.append(Move(self, Position(2, self.pos.y), castle_side=CastleSide.Queen)) ret.append(Move(self, Position(2, self.pos.y), castle_side=CastleSide.Queen))
print(ret)
return ret return ret

View File

@ -4,7 +4,7 @@ class Knight(Piece):
def letter(self): def letter(self):
return "n" return "n"
def legal_moves(self, board: "Board") -> list["Move"]: def legal_moves(self, board: "Board", / , looking_for_check = False) -> list["Move"]:
ret = [] ret = []
for dx, dy in [ for dx, dy in [
(+2, +1), (+1, +2), # north east (+2, +1), (+1, +2), # north east
@ -15,5 +15,9 @@ class Knight(Piece):
move = self._move_for_position(board, self.pos.x + dx, self.pos.y + dy) move = self._move_for_position(board, self.pos.x + dx, self.pos.y + dy)
if move is not None: if move is not None:
ret.append(move) ret.append(move)
if not looking_for_check and board.is_check_for(self.colour):
return self.keep_only_blocking(ret, board)
return ret return ret

View File

@ -3,7 +3,7 @@ from logic.pieces.piece import Colour, Piece
from logic.position import Position from logic.position import Position
class Pawn(Piece): class Pawn(Piece):
def legal_moves(self, board) -> list[Move]: def legal_moves(self, board, / , looking_for_check = False) -> list[Move]:
ret = [] ret = []
# can we capture to the left? # can we capture to the left?
@ -24,17 +24,32 @@ class Pawn(Piece):
if capturable_piece.colour != self.colour: if capturable_piece.colour != self.colour:
ret.append(Move(self, capturable_piece.pos, is_capturing = True)) ret.append(Move(self, capturable_piece.pos, is_capturing = True))
# -- Can we capture en passant?
if board._en_passant_target is not None and \
board._en_passant_target.pos.y == self.pos.y and (
board._en_passant_target.pos.x == self.pos.x - 1
or board._en_passant_target.pos.x == self.pos.x + 1
):
if board._en_passant_target.colour != self.colour:
old_pos = board._en_passant_target.pos
new_pos = Position(old_pos.x, old_pos.y + (1 if self.colour == Colour.WHITE else -1))
ret.append(Move(self, new_pos, is_capturing = True, en_passant = True))
# -- Normal moves
if self.colour == Colour.WHITE: if self.colour == Colour.WHITE:
for dy in range(1, 3 if self.pos.y == 1 else 2): for dy in range(1, 3 if self.pos.y == 1 else 2):
if self.pos.y + dy > 7 or board.piece_at(self.pos.x, self.pos.y + dy): if self.pos.y + dy > 7 or board.piece_at(self.pos.x, self.pos.y + dy):
break break
pos = Position(self.pos.x, self.pos.y + dy) pos = Position(self.pos.x, self.pos.y + dy)
ret.append(Move(self, pos)) ret.append(Move(self, pos, becomes_en_passant_target=dy==2))
else: else:
for dy in range(1, 3 if self.pos.y == 6 else 2): for dy in range(1, 3 if self.pos.y == 6 else 2):
if self.pos.y - dy < 0 or board.piece_at(self.pos.x, self.pos.y - dy): if self.pos.y - dy < 0 or board.piece_at(self.pos.x, self.pos.y - dy):
break break
pos = Position(self.pos.x, self.pos.y - dy) pos = Position(self.pos.x, self.pos.y - dy)
ret.append(Move(self, pos)) ret.append(Move(self, pos, becomes_en_passant_target=dy==2))
if not looking_for_check and board.is_check_for(self.colour):
return self.keep_only_blocking(ret, board)
return ret return ret

View File

@ -7,6 +7,9 @@ class Colour(Enum):
WHITE = "white" WHITE = "white"
BLACK = "black" BLACK = "black"
def __str__(self) -> str:
return self.value
class Piece: class Piece:
def __init__(self, pos: Position, colour: Colour) -> None: def __init__(self, pos: Position, colour: Colour) -> None:
self.pos = pos self.pos = pos
@ -16,6 +19,14 @@ class Piece:
def letter(self): def letter(self):
return type(self).__name__[0].lower() return type(self).__name__[0].lower()
def keep_only_blocking(self, candidates: list[Move], board: "Board") -> list[Move]:
ret = []
for move in candidates:
board_after_move = board.make_move(move)
if not board_after_move.is_check_for(self.colour):
ret.append(move)
return ret
def _look_direction(self, board: "Board", mult_dx: int, mult_dy: int): def _look_direction(self, board: "Board", mult_dx: int, mult_dy: int):
ret = [] ret = []
for d in range(1, 8): for d in range(1, 8):
@ -50,5 +61,5 @@ class Piece:
ret = type(self)(pos, self.colour) ret = type(self)(pos, self.colour)
return ret return ret
def legal_moves(self, board: "Board") -> list["Move"]: def legal_moves(self, board: "Board", / , looking_for_check = False) -> list["Move"]:
raise NotImplementedError(f"Can't say what the legal moves are for {type(self).__name__}, the method hasn't been implemented yet") raise NotImplementedError(f"Can't say what the legal moves are for {type(self).__name__}, the method hasn't been implemented yet")

View File

@ -2,7 +2,7 @@ from logic.move import Move
from .piece import Piece from .piece import Piece
class Queen(Piece): class Queen(Piece):
def legal_moves(self, board: "Board") -> list[Move]: def legal_moves(self, board: "Board", / , looking_for_check = False) -> list[Move]:
ret = [] ret = []
# looking north east # looking north east
@ -29,4 +29,7 @@ class Queen(Piece):
# looking north # looking north
ret.extend(self._look_direction(board, 0, 1)) ret.extend(self._look_direction(board, 0, 1))
if not looking_for_check and board.is_check_for(self.colour):
return self.keep_only_blocking(ret, board)
return ret return ret

View File

@ -2,7 +2,7 @@ from logic.move import Move
from .piece import Piece from .piece import Piece
class Rook(Piece): class Rook(Piece):
def legal_moves(self, board: "Board") -> list[Move]: def legal_moves(self, board: "Board", / , looking_for_check = False) -> list[Move]:
ret = [] ret = []
# looking east # looking east
@ -17,4 +17,7 @@ class Rook(Piece):
# looking north # looking north
ret.extend(self._look_direction(board, 0, 1)) ret.extend(self._look_direction(board, 0, 1))
if not looking_for_check and board.is_check_for(self.colour):
return self.keep_only_blocking(ret, board)
return ret return ret

View File

@ -1,4 +1,5 @@
import tkinter as tk import tkinter as tk
from tkinter import messagebox
from typing import Type from typing import Type
from PIL import ImageTk, Image from PIL import ImageTk, Image
import os import os
@ -60,6 +61,12 @@ class GUI(View):
self._controller.on_tile_selected(x, y) self._controller.on_tile_selected(x, y)
def notify_checkmate(self, colour: Colour) -> None:
messagebox.showinfo("Checkmate", f"{colour} is currently checkmated")
def notify_stalemate(self, colour: Colour) -> None:
messagebox.showinfo("Stalemate", f"{colour} is currently stalemated")
def update_board(self, board: Board, selected_piece: Piece, legal_moves: list[Move]) -> None: def update_board(self, board: Board, selected_piece: Piece, legal_moves: list[Move]) -> None:
self.canvas.delete("all") self.canvas.delete("all")

View File

@ -1,6 +1,6 @@
from logic.board import Board from logic.board import Board
from logic.move import Move from logic.move import Move
from logic.pieces.piece import Piece from logic.pieces.piece import Colour, Piece
class View: class View:
@ -13,6 +13,12 @@ class View:
def update_board(self, board: Board, selected_piece: Piece, legal_moves: list[Move]) -> None: def update_board(self, board: Board, selected_piece: Piece, legal_moves: list[Move]) -> None:
raise NotImplementedError(f"Can't update the board, the update_board() method of {type(self)} is not implemented") raise NotImplementedError(f"Can't update the board, the update_board() method of {type(self)} is not implemented")
def notify_checkmate(self, colour: Colour) -> None:
raise NotImplementedError(f"Can't notify of the checkmate, the notify_checkmate() method of {type(self)} is not implemented")
def notify_stalemate(self, colour: Colour) -> None:
raise NotImplementedError(f"Can't notify of the stalemate, the notify_stalemate() method of {type(self)} is not implemented")
def set_controller(self, controller: "Controller") -> None: def set_controller(self, controller: "Controller") -> None:
self._controller = controller self._controller = controller