Compare commits

..

7 Commits

Author SHA1 Message Date
Karma Riuk
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
Karma Riuk
87e8e75c04 simplified moves, made them algebraic 2025-01-31 16:35:29 +01:00
Karma Riuk
13e3675665 fixed FEN reading for castling writes 2025-01-31 16:33:56 +01:00
Karma Riuk
2e27e7b703 implemented king moves (missing castles) 2025-01-31 15:22:49 +01:00
Karma Riuk
d7863e0d81 added capturing circle around possible capture for
legal moves
2025-01-31 14:34:54 +01:00
Karma Riuk
a3b7df4e4c minor fix again 2025-01-31 14:28:18 +01:00
Karma Riuk
806a4a7f65 minor fix 2025-01-31 14:09:35 +01:00
9 changed files with 195 additions and 48 deletions

View File

@ -40,11 +40,11 @@ class Controller:
def on_tile_selected(self, x: int, y: int) -> None: def on_tile_selected(self, x: int, y: int) -> None:
pos = Position(x, y) pos = Position(x, y)
print(f"Clicked on {pos.to_algebraic()}")
piece = self._board.piece_at(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): 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) self._show_legal_moves(pos)
else: else:
legal_moves_positions = [move for move in self._legal_moves if move.pos == pos] legal_moves_positions = [move for move in self._legal_moves if move.pos == pos]

View File

@ -1,4 +1,4 @@
from logic.move import Move 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
@ -76,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
@ -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(Board.KING_SIDE_CASTLE) ret._white_castling_write.add(CastleSide.King)
if c == "Q": if c == "Q":
ret._white_castling_write.add(Board.QUEEN_SIDE_CASTLE) ret._white_castling_write.add(CastleSide.Queen)
if c == "k": if c == "k":
ret._black_castling_write.add(Board.KING_SIDE_CASTLE) ret._black_castling_write.add(CastleSide.King)
if c == "q": if c == "q":
ret._black_castling_write.add(Board.QUEEN_SIDE_CASTLE) ret._black_castling_write.add(CastleSide.Queen)
# -- En passant target # -- En passant target
if position[index] != "-": if position[index] != "-":
@ -113,6 +113,32 @@ class Board:
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)]
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 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)
@ -128,16 +154,45 @@ class Board:
ret._en_passant_target = self._en_passant_target ret._en_passant_target = self._en_passant_target
piece = move.piece piece = move.piece
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.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)
if piece.colour == Colour.WHITE: if piece.colour == Colour.WHITE:
del ret._white[piece.pos] if type(piece) == King:
ret._white[move.pos] = piece.move_to(move.pos) ret._white_castling_write = set()
if move.pos in ret._black:
del ret._black[move.pos] 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)
else: else:
del ret._black[piece.pos] if type(piece) == King:
ret._black[move.pos] = piece.move_to(move.pos) ret._black_castling_write = set()
if move.pos in ret._white:
del ret._white[move.pos] 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)
return ret return ret

View File

@ -2,9 +2,17 @@
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) -> None:
self.piece = piece
self.pos = pos
self.is_capturing = is_capturing self.is_capturing = is_capturing
self.castle_side = castle_side
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 +21,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

@ -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)
# -- Castles
castling_writes = board.castling_writes_for(self.colour)
if len(castling_writes) == 0:
return ret
print(castling_writes)
if CastleSide.King in castling_writes:
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_writes:
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))
print(ret)
return ret

View File

@ -1,6 +1,9 @@
from .piece import Piece from .piece import Piece
class Knight(Piece): class Knight(Piece):
def letter(self):
return "n"
def legal_moves(self, board: "Board") -> list["Move"]: def legal_moves(self, board: "Board") -> list["Move"]:
ret = [] ret = []
for dx, dy in [ for dx, dy in [

View File

@ -1,4 +1,4 @@
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
@ -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,19 +22,19 @@ 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))
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))
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))
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
@ -13,6 +13,9 @@ 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 letter(self):
return type(self).__name__[0].lower()
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,10 +37,10 @@ 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:

View File

@ -29,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

@ -37,8 +37,6 @@ class GUI(View):
path = os.path.join(root, f"{"white" if colour == Colour.WHITE else "black"}-{piece_name}.png") path = os.path.join(root, f"{"white" if colour == Colour.WHITE else "black"}-{piece_name}.png")
img = Image.open(path) img = Image.open(path)
size = int(self.tile_size * .85)
# img = img.resize((size, size))
if img.mode == "LA": if img.mode == "LA":
img = img.convert(mode="RGBA") img = img.convert(mode="RGBA")
@ -92,8 +90,21 @@ class GUI(View):
if selected_piece is not None: if selected_piece is not None:
possible_positions = [move.pos for move in legal_moves] possible_positions = [move.pos for move in legal_moves]
if pos in possible_positions: if pos in possible_positions:
radius = .15 * self.tile_size
colour = circle_colours[(x + y) % 2] 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( self.canvas.create_oval(
(x + .5) * self.tile_size - radius, (x + .5) * self.tile_size - radius,
(y + .5) * self.tile_size - radius, (y + .5) * self.tile_size - radius,
@ -103,7 +114,6 @@ class GUI(View):
outline=colour, outline=colour,
) )
piece = board.piece_at(x, 7-y) piece = board.piece_at(x, 7-y)
if piece: if piece: