15 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
496207861e implemented castling
Some checks failed
pre-release / Pre Release (push) Waiting to run
tagged-release / Tagged Release (push) Has been cancelled
2025-01-31 16:35:41 +01:00
87e8e75c04 simplified moves, made them algebraic 2025-01-31 16:35:29 +01:00
13e3675665 fixed FEN reading for castling writes 2025-01-31 16:33:56 +01:00
2e27e7b703 implemented king moves (missing castles) 2025-01-31 15:22:49 +01:00
d7863e0d81 added capturing circle around possible capture for
legal moves
2025-01-31 14:34:54 +01:00
a3b7df4e4c minor fix again 2025-01-31 14:28:18 +01:00
806a4a7f65 minor fix 2025-01-31 14:09:35 +01:00
052f815ee1 we have nice looing gui now
Some checks failed
pre-release / Pre Release (push) Waiting to run
tagged-release / Tagged Release (push) Has been cancelled
2025-01-31 14:08:21 +01:00
c14a8c83b3 WE CAN FINALLY PLAY THE GAME. Made controller
Some checks failed
pre-release / Pre Release (push) Waiting to run
tagged-release / Tagged Release (push) Has been cancelled
working
2025-01-31 11:39:23 +01:00
ac85f3e6d3 added controller to model view controller
Some checks failed
pre-release / Pre Release (push) Waiting to run
tagged-release / Tagged Release (push) Has been cancelled
2025-01-31 10:52:25 +01:00
ddfb95176b fixed import issue with the queen 2025-01-31 10:52:02 +01:00
27 changed files with 463 additions and 78 deletions

BIN
res/black-bishop.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

BIN
res/black-king.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

BIN
res/black-knight.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

BIN
res/black-pawn.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 734 B

BIN
res/black-queen.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

BIN
res/black-rook.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1014 B

BIN
res/trimmed.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

BIN
res/white-bishop.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

BIN
res/white-king.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

BIN
res/white-knight.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

BIN
res/white-pawn.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

BIN
res/white-queen.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

BIN
res/white-rook.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -0,0 +1,63 @@
from logic.board import Board
from logic.move import Move
from logic.pieces.piece import Piece
from logic.position import Position
from view.view import View
class Controller:
def __init__(self, board: Board, view: View) -> None:
self._board = board
self._view = view
self._view.set_controller(self)
self._reset_selection()
self._selected_piece: Piece = None
self._legal_moves: list[Move] = []
def _reset_selection(self):
self._selected_piece = None
self._legal_moves = []
self._view.update_board(self._board, self._selected_piece, self._legal_moves)
def _show_legal_moves(self, pos: Position):
piece = self._board.piece_at(pos.x, pos.y)
if piece:
if piece.colour != self._board._turn:
return
self._selected_piece = piece
self._legal_moves = piece.legal_moves(self._board)
self._view.update_board(self._board, self._selected_piece, self._legal_moves)
else:
self._reset_selection()
def _make_move(self, move: Move) -> None:
self._board = self._board.make_move(move)
self._reset_selection()
def on_tile_selected(self, x: int, y: int) -> None:
pos = Position(x, y)
piece = self._board.piece_at(x, y)
if self._selected_piece is None \
or (piece is not None and piece != self._selected_piece and piece.colour == self._selected_piece.colour):
self._show_legal_moves(pos)
else:
legal_moves_positions = [move for move in self._legal_moves if move.pos == pos]
assert len(legal_moves_positions) <= 1, f"Apparently we can make multiple moves towards {pos.to_algebraic()} with {type(self._selected_piece)}, which doesn't make sense..."
if len(legal_moves_positions) == 0: # click on a square outside of the possible moves
self._reset_selection()
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

@ -1,3 +1,4 @@
from logic.move import CastleSide, Move
from logic.pieces.bishop import Bishop from logic.pieces.bishop import Bishop
from logic.pieces.king import King from logic.pieces.king import King
from logic.pieces.knight import Knight from logic.pieces.knight import Knight
@ -14,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
@ -75,7 +76,7 @@ class Board:
ret._turn = Colour.BLACK ret._turn = Colour.BLACK
else: else:
raise ValueError(f"The FEN position is malformed, the active colour should be either 'w' or 'b', but is '{position[index]}'") raise ValueError(f"The FEN position is malformed, the active colour should be either 'w' or 'b', but is '{position[index]}'")
index += 1 index += 2
# -- Castling Rights # -- Castling Rights
@ -87,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(Board.KING_SIDE_CASTLE) ret._white_castling_rights.add(CastleSide.King)
if c == "Q": if c == "Q":
ret._white_castling_write.add(Board.QUEEN_SIDE_CASTLE) ret._white_castling_rights.add(CastleSide.Queen)
if c == "k": if c == "k":
ret._black_castling_write.add(Board.KING_SIDE_CASTLE) ret._black_castling_rights.add(CastleSide.King)
if c == "q": if c == "q":
ret._black_castling_write.add(Board.QUEEN_SIDE_CASTLE) ret._black_castling_rights.add(CastleSide.Queen)
# -- En passant target # -- En passant target
if position[index] != "-": if position[index] != "-":
@ -111,3 +112,118 @@ class Board:
if white_piece != None: if white_piece != None:
return white_piece return white_piece
return black_piece return black_piece
def is_check_for(self, colour: Colour) -> bool:
""" Is it check for the defending colour passed as parameter """
defending_pieces, attacking_pieces = (self._white, self._black) if colour == Colour.WHITE else (self._black, self._white)
kings = [piece for piece in defending_pieces.values() if type(piece) == King]
assert len(kings) == 1, f"We have more than one king for {colour}, that is no buono..."
king = kings[0]
for piece in attacking_pieces.values():
possible_pos = []
if type(piece) == King:
# special case for the king, because it creates infinite recursion (since he looks if he's walking into a check)
for dx in [-1, 0, 1]:
for dy in [-1, 0, 1]:
x, y = piece.pos.x + dx, piece.pos.y + dy
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, looking_for_check=True)]
if king.pos in possible_pos:
return True
return False
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)
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_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]
pieces_moving[move.pos] = piece.move_to(move.pos)
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..."
del pieces_moving[rook_pos]
new_rook_pos = Position(5, piece.pos.y)
pieces_moving[new_rook_pos] = Rook(new_rook_pos, piece.colour)
elif move.castle_side == CastleSide.Queen:
rook_pos = Position(0, piece.pos.y)
assert rook_pos in pieces_moving and type(pieces_moving[rook_pos]) == Rook, "Either rook is absent from the queen side or you are trying to castle with something else than a rook..."
del pieces_moving[rook_pos]
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_rights = set()
if type(piece) == Rook:
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_rights = set()
if type(piece) == Rook:
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

@ -2,9 +2,19 @@
from logic.position import Position from logic.position import Position
from enum import Enum from enum import Enum
class CastleSide(Enum):
Neither = ""
King = "O-O"
Queen = "O-O-O"
class Move: class Move:
def __init__(self, is_capturing: bool) -> 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.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: 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")
@ -13,13 +23,18 @@ class Move:
def from_algebraic(move: str) -> "Move": def from_algebraic(move: str) -> "Move":
raise NotImplementedError("The move can't be translated from algbraic notation, as it was not implemented") raise NotImplementedError("The move can't be translated from algbraic notation, as it was not implemented")
def __str__(self) -> str:
if self.castle_side == CastleSide.King:
return "O-O"
if self.castle_side == CastleSide.Queen:
return "O-O-O"
class PieceMove(Move): ret = ""
def __init__(self, piece: "Piece", pos: Position,/, is_capturing: bool = False) -> None: if type(self.piece).__name__ != "Pawn":
super().__init__(is_capturing) ret += self.piece.letter().upper()
self.piece = piece
self.pos = pos
class Castle(Move, Enum): ret += str(self.pos)
KING_SIDE_CASTLE = False return ret
QUEEN_SIDE_CASTLE = False
def __repr__(self) -> str:
return str(self)

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

@ -1,5 +1,69 @@
from logic.move import CastleSide, Move
from logic.position import Position
from .piece import Piece from .piece import Piece
class King(Piece): class King(Piece):
pass def legal_moves(self, board: "Board") -> list[Move]:
ret = []
# -- Regular moves
for dx in [-1, 0, 1]:
for dy in [-1, 0, 1]:
if dx == 0 and dy == 0: # skip current position
continue
x = self.pos.x + dx
y = self.pos.y + dy
move = self._move_for_position(board, x, y)
if move:
board_after_move = board.make_move(move)
if not board_after_move.is_check_for(self.colour):
ret.append(move)
if board.is_check_for(self.colour):
return self.keep_only_blocking(ret, board)
# -- 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
y = self.pos.y
if board.piece_at(x, y) is not None:
clear = False
break
move = self._move_for_position(board, x, y)
board_after_move = board.make_move(move)
if board_after_move.is_check_for(self.colour):
clear = False
break
if clear:
ret.append(Move(self, Position(6, self.pos.y), castle_side=CastleSide.King))
if CastleSide.Queen in castling_rights:
clear = True
for dx in range(1, 3):
x = self.pos.x - dx
y = self.pos.y
if board.piece_at(x, y) is not None:
clear = False
break
move = self._move_for_position(board, x, y)
board_after_move = board.make_move(move)
if board_after_move.is_check_for(self.colour):
clear = False
break
if clear:
ret.append(Move(self, Position(2, self.pos.y), castle_side=CastleSide.Queen))
return ret

View File

@ -1,7 +1,10 @@
from .piece import Piece from .piece import Piece
class Knight(Piece): class Knight(Piece):
def legal_moves(self, board: "Board") -> list["Move"]: def letter(self):
return "n"
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
@ -12,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

@ -1,9 +1,9 @@
from logic.move import Move, PieceMove from logic.move import Move
from logic.pieces.piece import Colour, Piece 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?
@ -13,7 +13,7 @@ class Pawn(Piece):
(self.colour == Colour.BLACK and (capturable_piece := board.piece_at(self.pos.x - 1, self.pos.y - 1))) (self.colour == Colour.BLACK and (capturable_piece := board.piece_at(self.pos.x - 1, self.pos.y - 1)))
): ):
if capturable_piece.colour != self.colour: if capturable_piece.colour != self.colour:
ret.append(PieceMove(self, capturable_piece.pos, is_capturing = True)) ret.append(Move(self, capturable_piece.pos, is_capturing = True))
# can we capture to the right? # can we capture to the right?
if self.pos.x < 7 and ( if self.pos.x < 7 and (
@ -22,21 +22,34 @@ class Pawn(Piece):
(self.colour == Colour.BLACK and (capturable_piece := board.piece_at(self.pos.x + 1, self.pos.y - 1))) (self.colour == Colour.BLACK and (capturable_piece := board.piece_at(self.pos.x + 1, self.pos.y - 1)))
): ):
if capturable_piece.colour != self.colour: if capturable_piece.colour != self.colour:
ret.append(PieceMove(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(PieceMove(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(PieceMove(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)
print(ret)
return ret return ret

View File

@ -1,4 +1,4 @@
from logic.move import Move, PieceMove from logic.move import Move
from logic.position import Position from logic.position import Position
from enum import Enum from enum import Enum
@ -7,12 +7,26 @@ 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
assert colour == Colour.WHITE or colour == Colour.BLACK, "The colour of the piece must be either Piece.WHITE or Piece.BLACK" assert colour == Colour.WHITE or colour == Colour.BLACK, "The colour of the piece must be either Piece.WHITE or Piece.BLACK"
self.colour = colour self.colour = colour
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): 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):
@ -34,14 +48,18 @@ class Piece:
piece = board.piece_at(x, y) piece = board.piece_at(x, y)
if piece is None: if piece is None:
return PieceMove(self, Position(x, y)) return Move(self, Position(x, y))
if piece.colour != self.colour: if piece.colour != self.colour:
return PieceMove(self, Position(x, y), is_capturing=True) return Move(self, Position(x, y), is_capturing=True)
return None return None
def position(self) -> Position: def position(self) -> Position:
return self.pos return self.pos
def legal_moves(self, board: "Board") -> list["Move"]: def move_to(self, pos: Position) -> "Piece":
ret = type(self)(pos, self.colour)
return ret
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

@ -1,7 +1,8 @@
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
@ -28,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,7 @@
class Position: class Position:
_RANKS = range(1, 9)
_FILES = "abcdefgh"
_MIN_POS = 0 _MIN_POS = 0
_MAX_POS = 7 _MAX_POS = 7
@ -14,6 +17,8 @@ class Position:
return x >= Position._MIN_POS and x <= Position._MAX_POS \ return x >= Position._MIN_POS and x <= Position._MAX_POS \
and y >= Position._MIN_POS and y <= Position._MAX_POS and y >= Position._MIN_POS and y <= Position._MAX_POS
def to_algebraic(self) -> str:
return f"{Position._FILES[self.x]}{Position._RANKS[self.y]}"
def __eq__(self, value: object, /) -> bool: def __eq__(self, value: object, /) -> bool:
if type(value) != type(self): if type(value) != type(self):
@ -24,8 +29,7 @@ class Position:
return hash((self.x, self.y)) return hash((self.x, self.y))
def __str__(self) -> str: def __str__(self) -> str:
return f"{self.x, self.y}" return f"{Position._FILES[self.x]}{Position._RANKS[self.y]}"
def __repr__(self) -> str: def __repr__(self) -> str:
return str(self) return str(self)

View File

@ -1,3 +1,4 @@
from controller.controller import Controller
from logic.board import Board from logic.board import Board
from view.gui import GUI from view.gui import GUI
from view.tui import TUI from view.tui import TUI
@ -6,6 +7,8 @@ if __name__ == "__main__":
initial_board_position = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1" initial_board_position = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1"
board = Board.setup_FEN_position(initial_board_position) board = Board.setup_FEN_position(initial_board_position)
view = GUI(board) view = GUI()
controller = Controller(board, view)
view.show() view.show()

View File

@ -1,13 +1,24 @@
import tkinter as tk import tkinter as tk
from tkinter import messagebox
from typing import Type
from PIL import ImageTk, Image
import os
from logic.board import Board from logic.board import Board
from logic.move import Move
from logic.pieces.bishop import Bishop
from logic.pieces.king import King
from logic.pieces.knight import Knight
from logic.pieces.pawn import Pawn
from logic.pieces.piece import Colour, Piece from logic.pieces.piece import Colour, Piece
from logic.pieces.queen import Queen
from logic.pieces.rook import Rook
from logic.position import Position from logic.position import Position
from view.view import View from view.view import View
class GUI(View): class GUI(View):
def __init__(self, board: Board) -> None: def __init__(self) -> None:
super().__init__(board) super().__init__()
self.root = tk.Tk() self.root = tk.Tk()
self.root.title("Chess Board") self.root.title("Chess Board")
@ -18,22 +29,61 @@ class GUI(View):
self.canvas = tk.Canvas(self.root, width=board_size, height=board_size) self.canvas = tk.Canvas(self.root, width=board_size, height=board_size)
self.canvas.pack() self.canvas.pack()
self.state = {"selected_piece": None, "legal_moves": []}
self.canvas.bind("<Button-1>", self._on_canvas_click) self.canvas.bind("<Button-1>", self._on_canvas_click)
self._draw_chess_board()
self._piece_images = self._load_piece_images("res/")
def _piece_svg(self, root: str, piece: Type[Piece], colour: Colour) -> ImageTk.PhotoImage:
piece_name = piece.__name__.lower()
path = os.path.join(root, f"{"white" if colour == Colour.WHITE else "black"}-{piece_name}.png")
img = Image.open(path)
if img.mode == "LA":
img = img.convert(mode="RGBA")
img.save(path)
return ImageTk.PhotoImage(img)
def _load_piece_images(self, root: str) -> dict[Type[Piece], dict[Colour, ImageTk.PhotoImage]]:
ret = {}
for piece in [Pawn, Rook, Knight, Bishop, Queen, King]:
if piece not in ret:
ret[piece] = {}
ret[piece][Colour.WHITE] = self._piece_svg(root, piece, Colour.WHITE)
ret[piece][Colour.BLACK] = self._piece_svg(root, piece, Colour.BLACK)
return ret
def _on_canvas_click(self, event):
x, y = event.x // self.tile_size, event.y // self.tile_size
y = 7 - 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 _draw_chess_board(self): def update_board(self, board: Board, selected_piece: Piece, legal_moves: list[Move]) -> None:
colours = ["#F0D9B5", "#B58863"] # Light and dark squares self.canvas.delete("all")
self._draw_chess_board(board, selected_piece, legal_moves)
def _draw_chess_board(self, board: Board, selected_piece = None, legal_moves = []):
colours = ["#EDD6B0", "#B88762"] # Light and dark squares
alt_colours = ["#F6EB72", "#DCC34B"] # Light and dark squares, when selected
circle_colours = ["#CCB897", "#9E7454"] # circles to show legal moves
for y in range(8): for y in range(8):
for x in range(8): for x in range(8):
colour = colours[(x + y) % 2] colour = colours[(x + y) % 2]
if self.state["selected_piece"]: pos = Position(x, 7-y)
possible_positions = [move.pos for move in self.state["legal_moves"]] if selected_piece is not None and pos == selected_piece.pos:
if Position(x, 7-y) in possible_positions: colour = alt_colours[(x + y) % 2]
colour = "#ADD8E6" # Highlight legal moves
self.canvas.create_rectangle( self.canvas.create_rectangle(
x * self.tile_size, x * self.tile_size,
@ -44,16 +94,41 @@ class GUI(View):
outline=colour, outline=colour,
) )
piece = self.board.piece_at(x, 7-y) if selected_piece is not None:
possible_positions = [move.pos for move in legal_moves]
if pos in possible_positions:
colour = circle_colours[(x + y) % 2]
move = [move for move in legal_moves if move.pos == pos][0]
if move.is_capturing:
radius = .40 * self.tile_size
self.canvas.create_oval(
(x + .5) * self.tile_size - radius,
(y + .5) * self.tile_size - radius,
(x + .5) * self.tile_size + radius,
(y + .5) * self.tile_size + radius,
fill="",
outline=colour,
width=.075 * self.tile_size,
)
else:
radius = .15 * self.tile_size
self.canvas.create_oval(
(x + .5) * self.tile_size - radius,
(y + .5) * self.tile_size - radius,
(x + .5) * self.tile_size + radius,
(y + .5) * self.tile_size + radius,
fill=colour,
outline=colour,
)
piece = board.piece_at(x, 7-y)
if piece: if piece:
text_colour = "white" if piece.colour == Colour.WHITE else "black" self.canvas.create_image(
self.canvas.create_text(
(x + 0.5) * self.tile_size, (x + 0.5) * self.tile_size,
(y + 0.5) * self.tile_size, (y + 0.9) * self.tile_size,
text=piece.__class__.__name__[0], image=self._piece_images[type(piece)][piece.colour],
fill=text_colour, anchor=tk.S,
font=("Arial", 32, "bold")
) )
# Cell annotations # Cell annotations
@ -61,37 +136,20 @@ class GUI(View):
if x == 0: # numbers in the top left of the first column if x == 0: # numbers in the top left of the first column
self.canvas.create_text( self.canvas.create_text(
(x + 0.15) * self.tile_size, (x + .15) * self.tile_size,
(y + 0.15) * self.tile_size, (y + .15) * self.tile_size,
text=8-y, text=8-y,
fill=text_colour, fill=text_colour,
font=("Arial", 10, "bold") font=("Arial", 12, "bold")
) )
if y == 7: # numbers in the top left of the first column if y == 7: # numbers in the top left of the first column
self.canvas.create_text( self.canvas.create_text(
(x + 0.85) * self.tile_size, (x + .85) * self.tile_size,
(y + 0.85) * self.tile_size, (y + .85) * self.tile_size,
text="abcdefgh"[x], text="abcdefgh"[x],
fill=text_colour, fill=text_colour,
font=("Arial", 10, "bold") font=("Arial", 12, "bold")
) )
def _on_canvas_click(self, event):
x, y = event.x // self.tile_size, event.y // self.tile_size
y = 7 - y
piece = self.board.piece_at(x, y)
print(f"Clicked on {x, y}, {piece = }")
if piece:
self.state["selected_piece"] = piece
self.state["legal_moves"] = piece.legal_moves(self.board)
else:
self.state["selected_piece"] = None
self.state["legal_moves"] = []
self.canvas.delete("all")
self._draw_chess_board()
def show(self) -> None: def show(self) -> None:
self.root.mainloop() self.root.mainloop()

View File

@ -1,10 +1,24 @@
from logic.board import Board from logic.board import Board
from logic.move import Move
from logic.pieces.piece import Colour, Piece
class View: class View:
def __init__(self, board: Board) -> None: def __init__(self) -> None:
self.board: Board = board self._controller: "Controller" = None
def show(self) -> None: def show(self) -> None:
raise NotImplementedError(f"Can't show the board, the show() method of {type(self)} is not implemented") raise NotImplementedError(f"Can't show the board, the show() method of {type(self)} is not implemented")
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