4 Commits

Author SHA1 Message Date
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
22 changed files with 193 additions and 46 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,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.king import King
from logic.pieces.knight import Knight
@ -111,3 +112,32 @@ class Board:
if white_piece != None:
return white_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

@ -37,6 +37,4 @@ class Pawn(Piece):
pos = Position(self.pos.x, self.pos.y - dy)
ret.append(PieceMove(self, pos))
print(ret)
return ret

View File

@ -43,5 +43,9 @@ class Piece:
def position(self) -> Position:
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"]:
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,3 +1,4 @@
from logic.move import Move
from .piece import Piece
class Queen(Piece):

View File

@ -1,4 +1,7 @@
class Position:
_RANKS = range(1, 9)
_FILES = "abcdefgh"
_MIN_POS = 0
_MAX_POS = 7
@ -14,6 +17,8 @@ class Position:
return x >= Position._MIN_POS and x <= 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:
if type(value) != type(self):

View File

@ -1,3 +1,4 @@
from controller.controller import Controller
from logic.board import Board
from view.gui import GUI
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"
board = Board.setup_FEN_position(initial_board_position)
view = GUI(board)
view = GUI()
controller = Controller(board, view)
view.show()

View File

@ -1,13 +1,23 @@
import tkinter as tk
from typing import Type
from PIL import ImageTk, Image
import os
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.queen import Queen
from logic.pieces.rook import Rook
from logic.position import Position
from view.view import View
class GUI(View):
def __init__(self, board: Board) -> None:
super().__init__(board)
def __init__(self) -> None:
super().__init__()
self.root = tk.Tk()
self.root.title("Chess Board")
@ -18,22 +28,57 @@ class GUI(View):
self.canvas = tk.Canvas(self.root, width=board_size, height=board_size)
self.canvas.pack()
self.state = {"selected_piece": None, "legal_moves": []}
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)
size = int(self.tile_size * .85)
# img = img.resize((size, size))
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 _draw_chess_board(self):
colours = ["#F0D9B5", "#B58863"] # Light and dark squares
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: 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 x in range(8):
colour = colours[(x + y) % 2]
if self.state["selected_piece"]:
possible_positions = [move.pos for move in self.state["legal_moves"]]
if Position(x, 7-y) in possible_positions:
colour = "#ADD8E6" # Highlight legal moves
pos = Position(x, 7-y)
if selected_piece is not None and pos == selected_piece.pos:
colour = alt_colours[(x + y) % 2]
self.canvas.create_rectangle(
x * self.tile_size,
@ -44,16 +89,29 @@ class GUI(View):
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:
radius = .15 * self.tile_size
colour = circle_colours[(x + y) % 2]
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:
text_colour = "white" if piece.colour == Colour.WHITE else "black"
self.canvas.create_text(
self.canvas.create_image(
(x + 0.5) * self.tile_size,
(y + 0.5) * self.tile_size,
text=piece.__class__.__name__[0],
fill=text_colour,
font=("Arial", 32, "bold")
(y + 0.9) * self.tile_size,
image=self._piece_images[type(piece)][piece.colour],
anchor=tk.S,
)
# Cell annotations
@ -61,37 +119,20 @@ class GUI(View):
if x == 0: # numbers in the top left of the first column
self.canvas.create_text(
(x + 0.15) * self.tile_size,
(y + 0.15) * self.tile_size,
(x + .15) * self.tile_size,
(y + .15) * self.tile_size,
text=8-y,
fill=text_colour,
font=("Arial", 10, "bold")
font=("Arial", 12, "bold")
)
if y == 7: # numbers in the top left of the first column
self.canvas.create_text(
(x + 0.85) * self.tile_size,
(y + 0.85) * self.tile_size,
(x + .85) * self.tile_size,
(y + .85) * self.tile_size,
text="abcdefgh"[x],
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:
self.root.mainloop()

View File

@ -1,10 +1,18 @@
from logic.board import Board
from logic.move import Move
from logic.pieces.piece import Piece
class View:
def __init__(self, board: Board) -> None:
self.board: Board = board
def __init__(self) -> None:
self._controller: "Controller" = None
def show(self) -> None:
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