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:
move = legal_moves_positions[0]
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._black: dict[Position, Piece] = {}
self._turn = None
self._white_castling_write = set()
self._black_castling_write = set()
self._white_castling_rights = set()
self._black_castling_rights = set()
self._en_passant_target = None
@staticmethod
@ -88,13 +88,13 @@ class Board:
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}'"
if c == "K":
ret._white_castling_write.add(CastleSide.King)
ret._white_castling_rights.add(CastleSide.King)
if c == "Q":
ret._white_castling_write.add(CastleSide.Queen)
ret._white_castling_rights.add(CastleSide.Queen)
if c == "k":
ret._black_castling_write.add(CastleSide.King)
ret._black_castling_rights.add(CastleSide.King)
if c == "q":
ret._black_castling_write.add(CastleSide.Queen)
ret._black_castling_rights.add(CastleSide.Queen)
# -- En passant target
if position[index] != "-":
@ -131,13 +131,29 @@ class Board:
if Position.is_within_bounds(x, y):
possible_pos.append(Position(x, y))
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:
return True
return False
def castling_writes_for(self, colour: Colour) -> set[CastleSide]:
return self._white_castling_write if colour == Colour.WHITE else self._black_castling_write
def is_checkmate_for(self, colour: Colour) -> bool:
""" 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":
dest_piece = self.piece_at(move.pos.x, move.pos.y)
@ -145,15 +161,18 @@ class Board:
if dest_piece:
assert dest_piece.colour != move.piece.colour, "A piece cannot cannot eat another piece of the same colour"
# -- Copy current state
ret = Board()
ret._white = self._white.copy()
ret._black = self._black.copy()
ret._turn = Colour.WHITE if self._turn == Colour.BLACK else Colour.BLACK
ret._white_castling_write = self._white_castling_write.copy()
ret._black_castling_write = self._black_castling_write.copy()
ret._en_passant_target = self._en_passant_target
ret._white_castling_rights = self._white_castling_rights.copy()
ret._black_castling_rights = self._black_castling_rights.copy()
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)
del pieces_moving[piece.pos]
@ -161,6 +180,17 @@ class Board:
if move.pos in other_pieces:
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:
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..."
@ -175,24 +205,25 @@ class Board:
new_rook_pos = Position(3, piece.pos.y)
pieces_moving[new_rook_pos] = Rook(new_rook_pos, piece.colour)
# -- Check for castling rights
if piece.colour == Colour.WHITE:
if type(piece) == King:
ret._white_castling_write = set()
ret._white_castling_rights = set()
if type(piece) == Rook:
if piece.pos.x == 0 and CastleSide.Queen in ret._white_castling_write:
ret._white_castling_write.remove(CastleSide.Queen)
elif piece.pos.x == 7 and CastleSide.King in ret._white_castling_write:
ret._white_castling_write.remove(CastleSide.King)
if piece.pos.x == 0 and CastleSide.Queen in ret._white_castling_rights:
ret._white_castling_rights.remove(CastleSide.Queen)
elif piece.pos.x == 7 and CastleSide.King in ret._white_castling_rights:
ret._white_castling_rights.remove(CastleSide.King)
else:
if type(piece) == King:
ret._black_castling_write = set()
ret._black_castling_rights = set()
if type(piece) == Rook:
if piece.pos.x == 0 and CastleSide.Queen in ret._black_castling_write:
ret._black_castling_write.remove(CastleSide.Queen)
elif piece.pos.x == 7 and CastleSide.King in ret._black_castling_write:
ret._black_castling_write.remove(CastleSide.King)
if piece.pos.x == 0 and CastleSide.Queen in ret._black_castling_rights:
ret._black_castling_rights.remove(CastleSide.Queen)
elif piece.pos.x == 7 and CastleSide.King in ret._black_castling_rights:
ret._black_castling_rights.remove(CastleSide.King)
return ret

View File

@ -8,11 +8,13 @@ class CastleSide(Enum):
Queen = "O-O-O"
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.pos = pos
self.is_capturing = is_capturing
self.castle_side = castle_side
self.becomes_en_passant_target = becomes_en_passant_target
self.en_passant = en_passant
def to_algebraic(self) -> str:
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
class Bishop(Piece):
def legal_moves(self, board: "Board") -> list[Move]:
def legal_moves(self, board: "Board", / , looking_for_check = False) -> list[Move]:
ret = []
# looking north east
@ -17,5 +17,8 @@ class Bishop(Piece):
# looking north west
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

View File

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

View File

@ -4,7 +4,7 @@ class Knight(Piece):
def letter(self):
return "n"
def legal_moves(self, board: "Board") -> list["Move"]:
def legal_moves(self, board: "Board", / , looking_for_check = False) -> list["Move"]:
ret = []
for dx, dy in [
(+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)
if move is not None:
ret.append(move)
if not looking_for_check and board.is_check_for(self.colour):
return self.keep_only_blocking(ret, board)
return ret

View File

@ -3,7 +3,7 @@ from logic.pieces.piece import Colour, Piece
from logic.position import Position
class Pawn(Piece):
def legal_moves(self, board) -> list[Move]:
def legal_moves(self, board, / , looking_for_check = False) -> list[Move]:
ret = []
# can we capture to the left?
@ -24,17 +24,32 @@ class Pawn(Piece):
if capturable_piece.colour != self.colour:
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:
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):
break
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:
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):
break
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

View File

@ -7,6 +7,9 @@ class Colour(Enum):
WHITE = "white"
BLACK = "black"
def __str__(self) -> str:
return self.value
class Piece:
def __init__(self, pos: Position, colour: Colour) -> None:
self.pos = pos
@ -16,6 +19,14 @@ class Piece:
def letter(self):
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):
ret = []
for d in range(1, 8):
@ -50,5 +61,5 @@ class Piece:
ret = type(self)(pos, self.colour)
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")

View File

@ -2,7 +2,7 @@ from logic.move import Move
from .piece import 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 = []
# looking north east
@ -29,4 +29,7 @@ class Queen(Piece):
# looking north
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

View File

@ -2,7 +2,7 @@ from logic.move import Move
from .piece import 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 = []
# looking east
@ -17,4 +17,7 @@ class Rook(Piece):
# looking north
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

View File

@ -1,4 +1,5 @@
import tkinter as tk
from tkinter import messagebox
from typing import Type
from PIL import ImageTk, Image
import os
@ -60,6 +61,12 @@ class GUI(View):
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:
self.canvas.delete("all")

View File

@ -1,6 +1,6 @@
from logic.board import Board
from logic.move import Move
from logic.pieces.piece import Piece
from logic.pieces.piece import Colour, Piece
class View:
@ -13,6 +13,12 @@ class View:
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")
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:
self._controller = controller