7 Commits

Author SHA1 Message Date
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
bb0a3266c7 implemented queen legal moves
Some checks failed
pre-release / Pre Release (push) Waiting to run
tagged-release / Tagged Release (push) Has been cancelled
2025-01-30 17:12:53 +01:00
aabbaa83a8 again extracted some logic, implemented rook legal
moves
2025-01-30 17:11:36 +01:00
96b9b3db86 extracted some logic to the piece class and
implemented the knights legal moves
2025-01-30 17:07:50 +01:00
6b0a134230 fixed more colour issues 2025-01-30 16:56:44 +01:00
13 changed files with 219 additions and 74 deletions

View File

@ -0,0 +1,57 @@
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)
print(f"Clicked on {pos.to_algebraic()}")
piece = self._board.piece_at(x, y)
if self._selected_piece is None or (piece is not None and piece != self._selected_piece):
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)

View File

@ -1,3 +1,4 @@
from logic.move import 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
@ -111,3 +112,32 @@ class Board:
if white_piece != None: if white_piece != None:
return white_piece return white_piece
return black_piece return black_piece
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"
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
piece = move.piece
if piece.colour == Colour.WHITE:
del ret._white[piece.pos]
ret._white[move.pos] = piece.move_to(move.pos)
if move.pos in ret._black:
del ret._black[move.pos]
else:
del ret._black[piece.pos]
ret._black[move.pos] = piece.move_to(move.pos)
if move.pos in ret._white:
del ret._white[move.pos]
return ret

View File

@ -1,4 +1,4 @@
from logic.pieces.piece import Piece # from logic.pieces.piece import Piece
from logic.position import Position from logic.position import Position
from enum import Enum from enum import Enum
@ -15,7 +15,7 @@ class Move:
class PieceMove(Move): class PieceMove(Move):
def __init__(self, piece: Piece, pos: Position,/, is_capturing: bool = False) -> None: def __init__(self, piece: "Piece", pos: Position,/, is_capturing: bool = False) -> None:
super().__init__(is_capturing) super().__init__(is_capturing)
self.piece = piece self.piece = piece
self.pos = pos self.pos = pos

View File

@ -1,38 +1,7 @@
from logic.move import Move, PieceMove from logic.move import Move
from logic.position import Position
from .piece import Piece from .piece import Piece
class Bishop(Piece): class Bishop(Piece):
def _move_for_position(self, board: "Board", x: int, y: int) -> Move | None:
if not Position.is_within_bounds(x, y):
return None
piece = board.piece_at(x, y)
if piece is None:
return PieceMove(self, Position(x, y))
if piece.colour != self.colour:
return PieceMove(self, Position(x, y), is_capturing=True)
return None
def _look_direction(self, board: "Board", mult_dx: int, mult_dy: int):
ret = []
for d in range(1, 8):
dx = mult_dx * d
dy = mult_dy * d
move = self._move_for_position(board, self.pos.x + dx, self.pos.y + dy)
if move is None:
break
ret.append(move)
if move.is_capturing:
break
return ret
def legal_moves(self, board: "Board") -> list[Move]: def legal_moves(self, board: "Board") -> list[Move]:
ret = [] ret = []

View File

@ -1,5 +1,16 @@
from .piece import Piece from .piece import Piece
class Knight(Piece): class Knight(Piece):
pass def legal_moves(self, board: "Board") -> list["Move"]:
ret = []
for dx, dy in [
(+2, +1), (+1, +2), # north east
(+2, -1), (+1, -2), # south east
(-2, -1), (-1, -2), # south west
(-2, +1), (-1, +2), # north west
]:
move = self._move_for_position(board, self.pos.x + dx, self.pos.y + dy)
if move is not None:
ret.append(move)
return ret

View File

@ -1,5 +1,5 @@
from logic.move import Move, PieceMove from logic.move import Move, PieceMove
from logic.pieces.piece import Piece from logic.pieces.piece import Colour, Piece
from logic.position import Position from logic.position import Position
class Pawn(Piece): class Pawn(Piece):
@ -8,23 +8,23 @@ class Pawn(Piece):
# can we capture to the left? # can we capture to the left?
if self.pos.x > 0 and ( if self.pos.x > 0 and (
(self.colour == self.WHITE and (capturable_piece := board.piece_at(self.pos.x - 1, self.pos.y + 1))) (self.colour == Colour.WHITE and (capturable_piece := board.piece_at(self.pos.x - 1, self.pos.y + 1)))
or or
(self.colour == self.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(PieceMove(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 (
(self.colour == self.WHITE and (capturable_piece := board.piece_at(self.pos.x + 1, self.pos.y + 1))) (self.colour == Colour.WHITE and (capturable_piece := board.piece_at(self.pos.x + 1, self.pos.y + 1)))
or or
(self.colour == self.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(PieceMove(self, capturable_piece.pos, is_capturing = True))
if self.colour == Piece.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
@ -37,6 +37,4 @@ class Pawn(Piece):
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(PieceMove(self, pos))
print(ret)
return ret return ret

View File

@ -1,3 +1,4 @@
from logic.move import Move, PieceMove
from logic.position import Position from logic.position import Position
from enum import Enum from enum import Enum
@ -12,8 +13,39 @@ class Piece:
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 _look_direction(self, board: "Board", mult_dx: int, mult_dy: int):
ret = []
for d in range(1, 8):
dx = mult_dx * d
dy = mult_dy * d
move = self._move_for_position(board, self.pos.x + dx, self.pos.y + dy)
if move is None:
break
ret.append(move)
if move.is_capturing:
break
return ret
def _move_for_position(self, board: "Board", x: int, y: int) -> Move | None:
if not Position.is_within_bounds(x, y):
return None
piece = board.piece_at(x, y)
if piece is None:
return PieceMove(self, Position(x, y))
if piece.colour != self.colour:
return PieceMove(self, Position(x, y), is_capturing=True)
return None
def position(self) -> Position: def position(self) -> Position:
return self.pos return self.pos
def move_to(self, pos: Position) -> "Piece":
ret = type(self)(pos, self.colour)
return ret
def legal_moves(self, board: "Board") -> list["Move"]: def legal_moves(self, board: "Board") -> 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,5 +1,32 @@
from logic.move import Move
from .piece import Piece from .piece import Piece
class Queen(Piece): class Queen(Piece):
pass def legal_moves(self, board: "Board") -> list[Move]:
ret = []
# looking north east
ret.extend(self._look_direction(board, 1, 1))
# looking south east
ret.extend(self._look_direction(board, 1, -1))
# looking south west
ret.extend(self._look_direction(board, -1, -1))
# looking north west
ret.extend(self._look_direction(board, -1, 1))
# looking east
ret.extend(self._look_direction(board, 1, 0))
# looking south
ret.extend(self._look_direction(board, 0, -1))
# looking west
ret.extend(self._look_direction(board, -1, 0))
# looking north
ret.extend(self._look_direction(board, 0, 1))
return ret

View File

@ -1,5 +1,20 @@
from logic.move import Move
from .piece import Piece from .piece import Piece
class Rook(Piece): class Rook(Piece):
pass def legal_moves(self, board: "Board") -> list[Move]:
ret = []
# looking east
ret.extend(self._look_direction(board, 1, 0))
# looking south
ret.extend(self._look_direction(board, 0, -1))
# looking west
ret.extend(self._look_direction(board, -1, 0))
# looking north
ret.extend(self._look_direction(board, 0, 1))
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):

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,14 @@
import tkinter as tk import tkinter as tk
from logic.board import Board from logic.board import Board
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
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,20 +19,26 @@ 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()
def _on_canvas_click(self, event):
x, y = event.x // self.tile_size, event.y // self.tile_size
y = 7 - y
def _draw_chess_board(self): self._controller.on_tile_selected(x, y)
def update_board(self, board: Board, selected_piece: Piece, legal_moves: list[Move]) -> None:
self.canvas.delete("all")
self._draw_chess_board(board, selected_piece, legal_moves)
def _draw_chess_board(self, board, selected_piece = None, legal_moves = []):
colours = ["#F0D9B5", "#B58863"] # Light and dark squares colours = ["#F0D9B5", "#B58863"] # Light and dark squares
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"]: if selected_piece is not None:
possible_positions = [move.pos for move in self.state["legal_moves"]] possible_positions = [move.pos for move in legal_moves]
if Position(x, 7-y) in possible_positions: if Position(x, 7-y) in possible_positions:
colour = "#ADD8E6" # Highlight legal moves colour = "#ADD8E6" # Highlight legal moves
@ -44,7 +51,7 @@ class GUI(View):
outline=colour, outline=colour,
) )
piece = self.board.piece_at(x, 7-y) piece = board.piece_at(x, 7-y)
if piece: if piece:
text_colour = "white" if piece.colour == Colour.WHITE else "black" text_colour = "white" if piece.colour == Colour.WHITE else "black"
@ -76,22 +83,5 @@ class GUI(View):
font=("Arial", 10, "bold") font=("Arial", 10, "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,18 @@
from logic.board import Board from logic.board import Board
from logic.move import Move
from logic.pieces.piece import 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 set_controller(self, controller: "Controller") -> None:
self._controller = controller