Removed the python version and kept just the c++ one
Some checks failed
pre-release / Pre Release (push) Waiting to run
tagged-release / Tagged Release (push) Has been cancelled

This commit is contained in:
Karma Riuk 2025-02-16 09:36:04 +01:00
parent 0145664567
commit 5cbe57dc55
91 changed files with 29 additions and 1531 deletions

198
.gitignore vendored
View File

@ -1,174 +1,34 @@
# Byte-compiled / optimized / DLL files # Prerequisites
__pycache__/ *.d
*.py[cod]
*$py.class
# C extensions # Compiled Object files
*.slo
*.lo
*.o
*.obj
# Precompiled Headers
*.gch
*.pch
# Compiled Dynamic libraries
*.so *.so
*.dylib
*.dll
# Distribution / packaging # Fortran module files
.Python *.mod
build/ *.smod
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller # Compiled Static libraries
# Usually these files are written by a python script from a template *.lai
# before PyInstaller builds the exe, so as to inject date/other infos into it. *.la
*.manifest *.a
*.spec *.lib
# Installer logs # Executables
pip-log.txt *.exe
pip-delete-this-directory.txt *.out
*.appobj/
# Unit test / coverage reports test_bin/
htmlcov/ main
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
cover/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
.pybuilder/
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
# For a library or package, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in:
# .python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock
# UV
# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
#uv.lock
# poetry
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
#poetry.lock
# pdm
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
#pdm.lock
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
# in version control.
# https://pdm.fming.dev/latest/usage/project/#working-with-version-control
.pdm.toml
.pdm-python
.pdm-build/
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
# pytype static type analyzer
.pytype/
# Cython debug symbols
cython_debug/
# PyCharm
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/
# Ruff stuff:
.ruff_cache/
# PyPI configuration file
.pypirc

34
cpp/.gitignore vendored
View File

@ -1,34 +0,0 @@
# Prerequisites
*.d
# Compiled Object files
*.slo
*.lo
*.o
*.obj
# Precompiled Headers
*.gch
*.pch
# Compiled Dynamic libraries
*.so
*.dylib
*.dll
# Fortran module files
*.mod
*.smod
# Compiled Static libraries
*.lai
*.la
*.a
*.lib
# Executables
*.exe
*.out
*.appobj/
test_bin/
main

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 734 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1014 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -1,169 +0,0 @@
from collections import defaultdict
import time
from tqdm import tqdm
from logic.board import INITIAL_BOARD, Board
from logic.move import Move
from logic.pieces.piece import Colour
pos2expected = {
# -- Position 1
"rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1": {
1: 20,
2: 400,
3: 8_902,
4: 197_281,
},
# -- Position 2
"r3k2r/p1ppqpb1/bn2pnp1/3PN3/1p2P3/2N2Q1p/PPPBBPPP/R3K2R w KQkq - 0 1": {
1: 48,
2: 2_039,
3: 97_862,
4: 4_085_603,
},
# -- Position 3
"8/2p5/3p4/KP5r/1R3p1k/8/4P1P1/8 w - - 0 1": {
1: 14,
2: 191,
3: 2_812,
4: 43_238,
5: 674_624,
},
# -- Position 4
"r3k2r/Pppp1ppp/1b3nbN/nP6/BBP1P3/q4N2/Pp1P2PP/R2Q1RK1 w kq - 0 1": {
1: 6,
2: 264,
3: 9467,
4: 422_333,
},
# -- Position 5
"rnbq1k1r/pp1Pbppp/2p5/8/2B5/8/PPP1NnPP/RNBQK2R w KQ - 1 8": {
1: 44,
2: 1486,
3: 62379,
4: 2103487,
},
# -- Position 6
"r4rk1/1pp1qppp/p1np1n2/2b1p1B1/2B1P1b1/P1NP1N2/1PP1QPPP/R4RK1 w - - 0 10": {
1: 46,
2: 2079,
3: 89890,
4: 3894594,
},
}
tick = "\033[92m✔\033[0m" # Green Tick
cross = "\033[91m❌\033[0m" # Red Cross
res = defaultdict(lambda : 0)
def peft(pos: str):
global res
expected = pos2expected[pos]
board = Board.setup_FEN_position(pos)
for depth in expected:
with tqdm(total=expected[depth], desc=f"Depth: {depth}") as bar:
start = time.process_time()
moves = move_generation_test(bar, board, depth, depth)
bar.close()
elapsed = time.process_time() - start
elapsed *= 1_000
print("Depth:", depth, end=" ")
print("Result:", moves, end=" ")
if moves == expected[depth]:
print(f"{tick}", end=" ")
else:
print(f"{cross} (expected {expected[depth]})", end=" ")
print("positions Time:", int(elapsed), "milliseconds")
if moves != expected[depth]:
print()
for key, value in res.items():
print(f"{key}: {value}")
def move_generation_test(bar, board: Board, depth: int, max_depth, first = None):
global res
if first is None:
res = defaultdict(lambda : 0)
if board.is_terminal():
# bar.update(1)
# res[first] += 1
return 0
if depth == 0:
res[first] += 1
bar.update(1)
return 1
moves = board.legal_moves()
if depth == 1:
res[first] += len(moves)
bar.update(len(moves))
return len(moves)
num_pos = 0
for move in moves:
tmp_board = board.make_move(move)
if first is None:
first = move.piece.pos.to_algebraic() + move.pos.to_algebraic()
if first == "f7h8":
print(tmp_board.legal_moves())
num_pos += move_generation_test(bar, tmp_board, depth - 1, max_depth, first = first)
if depth == max_depth:
first = None
return num_pos
def play_game(board: Board, strategies: dict, verbose: bool = False) -> Board:
"""Play a turn-taking game. `strategies` is a {player_name: function} dict,
where function(state) is used to get the player's move."""
state = board
move_counter = 1
while not (board.is_checkmate_for(board._turn) or board.is_stalemate_for(board._turn)):
player = board._turn
move = strategies[player](state)
state = board.make_move(move)
if verbose:
if player == Colour.WHITE:
print(str(move_counter) + ".", move, end=" ")
else:
print(move)
return state
def minmax_search(state: Board) -> tuple[float, Move]:
"""Search game tree to determine best move; return (value, move) pair."""
return _max_value(state) if state._turn == Colour.WHITE else _min_value(state)
def _max_value(state: Board) -> tuple[float, Move]:
if state.is_terminal():
return state.utility(), None
v, move = -float("inf"), None
for a in state.legal_moves():
v2, _ = _min_value(state.make_move(a))
if v2 > v:
v, move = v2, a
return v, move
def _min_value(state: Board) -> tuple[float, Move]:
if state.is_terminal():
return state.utility(), None
v, move = float("inf"), None
for a in state.legal_moves():
v2, _ = _min_value(state.make_move(a))
if v2 < v:
v, move = v2, a
return v, move

View File

@ -1,63 +0,0 @@
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,360 +0,0 @@
from logic.move import CastleSide, Move
from logic.pieces.bishop import Bishop
from logic.pieces.king import King
from logic.pieces.knight import Knight
from logic.pieces.queen import Queen
from logic.pieces.rook import Rook
from logic.pieces.pawn import Pawn
from logic.pieces.piece import Colour, Piece
from logic.position import Position
from typing import Type
class Board:
def __init__(self) -> None:
self._white: dict[Position, Piece] = {}
self._black: dict[Position, Piece] = {}
self._turn = None
self._white_castling_rights = set()
self._black_castling_rights = set()
self._en_passant_target = None
self._n_moves = 0
self._n_half_moves = 0
@staticmethod
def _piece_class_from_char(c: str) -> Type[Piece]:
assert len(c) == 1, f"The piece {c} isn't denoted by 1 character"
c = c.lower()
if c == "p":
return Pawn
if c == "r":
return Rook
if c == "n":
return Knight
if c == "b":
return Bishop
if c == "q":
return Queen
if c == "k":
return King
raise ValueError(f"Unknown piece '{c}'")
@staticmethod
def setup_FEN_position(position: str) -> "Board":
ret = Board()
index = 0
# -- Pieces
pieces = "prnbqk" # possible pieces
numbers = "12345678" # possible number of empty squares
x = 0
y = 7 # FEN starts from the top left, so 8th rank
for c in position:
index += 1
if c == " ":
break
if c in pieces or c in pieces.upper():
pos = Position(x, y)
piece = Board._piece_class_from_char(c)
if c.isupper():
ret._white[pos] = piece(pos, Colour.WHITE)
else:
ret._black[pos] = piece(pos, Colour.BLACK)
x += 1
continue
if c in numbers:
x += int(c)
if c == '/':
x = 0
y -= 1
# -- Active colour
if position[index] == "w":
ret._turn = Colour.WHITE
elif position[index] == "b":
ret._turn = Colour.BLACK
else:
raise ValueError(f"The FEN position is malformed, the active colour should be either 'w' or 'b', but is '{position[index]}'")
index += 2
# -- Castling Rights
for c in position[index:]:
index += 1
if c == "-" or c == " ":
if c == "-":
index += 1
break
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_rights.add(CastleSide.King)
if c == "Q":
ret._white_castling_rights.add(CastleSide.Queen)
if c == "k":
ret._black_castling_rights.add(CastleSide.King)
if c == "q":
ret._black_castling_rights.add(CastleSide.Queen)
# -- En passant target
if position[index] != "-":
pos = Position.from_algebraic(position[index:index+2])
index += 2
if pos.y == 2:
pos.y += 1
assert pos in ret._white, "En passant target is not in the position"
ret._en_passant_target = ret._white[pos]
elif pos.y == 5:
pos.y -= 1
assert pos in ret._black, "En passant target is not in the position"
ret._en_passant_target = ret._black[pos]
else:
raise ValueError("You can't have a en passant target that is not on the third or sixth rank")
else:
index += 1
index += 1
ret._n_half_moves = int(position[index:position.find(" ", index + 1)])
ret._n_moves = int(position[position.find(" ", index)+1:])
return ret
def piece_at(self, x: int, y: int) -> Piece | None:
pos = Position(x, y)
white_piece = self._white.get(pos, None)
black_piece = self._black.get(pos, None)
assert white_piece == None or black_piece == None, f"There are two pieces at the same position {pos}, this shouldn't happen!"
if white_piece != None:
return white_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 piece.colour == Colour.BLACK:
ret._n_moves = self._n_moves + 1
if move.is_capturing or type(piece) == Pawn:
ret._n_half_moves = 0
else:
ret._n_half_moves = self._n_half_moves + 1
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]
if move.promotes_to is not None:
assert type(piece) == Pawn, "Trying to promote something that is not a pawn: not good!"
pieces_moving[move.pos] = move.promotes_to(move.pos, piece.colour)
# -- 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)
if move.is_capturing and move.pos.y == 7 and move.pos in self._black and type(self._black[move.pos]) == Rook:
if move.pos.x == 0 and CastleSide.Queen in ret._black_castling_rights:
ret._black_castling_rights.remove(CastleSide.Queen)
elif move.pos.x == 7 and CastleSide.King in ret._black_castling_rights:
ret._black_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)
if move.is_capturing and move.pos.y == 0 and move.pos in self._white and type(self._white[move.pos]) == Rook:
if move.pos.x == 0 and CastleSide.Queen in ret._white_castling_rights:
ret._white_castling_rights.remove(CastleSide.Queen)
elif move.pos.x == 7 and CastleSide.King in ret._white_castling_rights:
ret._white_castling_rights.remove(CastleSide.King)
return ret
def to_fen_string(self):
ret = ""
for y in range(7, -1, -1):
empty_cell_counter = 0
for x in range(8):
pos = Position(x, y)
piece = None
if pos in self._white:
piece = self._white[pos]
elif pos in self._black:
piece = self._black[pos]
if piece is None:
empty_cell_counter += 1
continue
if empty_cell_counter > 0:
ret += str(empty_cell_counter)
empty_cell_counter = 0
letter = piece.letter()
ret += letter.lower() if piece.colour == Colour.BLACK else letter.upper()
if empty_cell_counter > 0:
ret += str(empty_cell_counter)
if y > 0:
ret += "/"
ret += " "
ret += "w" if self._turn == Colour.WHITE else "b"
ret += " "
if len(self._white_castling_rights) == 0 and len(self._black_castling_rights) == 0:
ret += "-"
else:
if CastleSide.King in self._white_castling_rights:
ret += "K"
if CastleSide.Queen in self._white_castling_rights:
ret += "Q"
if CastleSide.King in self._black_castling_rights:
ret += "k"
if CastleSide.Queen in self._black_castling_rights:
ret += "q"
ret += " "
if self._en_passant_target is not None:
pos = Position(self._en_passant_target.pos.x, self._en_passant_target.pos.y)
pos.y += -1 if self._en_passant_target.colour == Colour.WHITE else 1
ret += pos.to_algebraic()
else:
ret += "-"
ret += " "
ret += str(self._n_half_moves)
ret += " "
ret += str(self._n_moves)
return ret
def legal_moves(self) -> list[Move]:
ret = []
pieces = self._white if self._turn == Colour.WHITE else self._black
for piece in pieces.values():
ret += piece.legal_moves(self)
return ret
def is_terminal(self) -> bool:
return self.is_stalemate_for(Colour.WHITE) or self.is_stalemate_for(Colour.BLACK) or self.is_checkmate_for(Colour.WHITE) or self.is_checkmate_for(Colour.BLACK)
def utility(self) -> int:
if self.is_stalemate_for(Colour.WHITE) or self.is_stalemate_for(Colour.BLACK):
return 0
if self.is_checkmate_for(Colour.WHITE):
return 1
if self.is_checkmate_for(Colour.BLACK):
return -1
raise ValueError("Cannot determine the utility of board become it neither checkmate nor stalemate for either players")
_fen_pos = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1"
INITIAL_BOARD = Board.setup_FEN_position(_fen_pos)

View File

@ -1,51 +0,0 @@
# from logic.pieces.piece import Piece
from typing import Type
from logic.position import Position
from enum import Enum
class CastleSide(Enum):
Neither = ""
King = "O-O"
Queen = "O-O-O"
class Move:
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, promotes_to: Type["Piece"] = None) -> 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
self.promotes_to = promotes_to
def to_algebraic(self) -> str:
raise NotImplementedError("The move can't be translated to algbraic notation, as it was not implemented")
@staticmethod
def from_algebraic(move: str) -> "Move":
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"
ret = ""
if type(self.piece).__name__ == "Pawn":
if self.is_capturing:
ret += self.piece.pos.to_algebraic()[0]
ret += "x"
ret += self.pos.to_algebraic()
else:
ret += self.pos.to_algebraic()
else:
ret += self.piece.letter().upper()
if self.is_capturing:
ret += "x"
ret += str(self.pos)
return ret
def __repr__(self) -> str:
return str(self)

View File

@ -1,24 +0,0 @@
from logic.move import Move
from .piece import Piece
class Bishop(Piece):
def legal_moves(self, board: "Board", / , looking_for_check = False) -> 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))
if not looking_for_check:# and board.is_check_for(self.colour):
return self.keep_only_blocking(ret, board)
return ret

View File

@ -1,69 +0,0 @@
from logic.move import CastleSide, Move
from logic.position import Position
from .piece import Piece
class King(Piece):
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, 4):
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 dx < 3 and 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,23 +0,0 @@
from .piece import Piece
class Knight(Piece):
def letter(self):
return "n"
def legal_moves(self, board: "Board", / , looking_for_check = False) -> 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)
if not looking_for_check:# and board.is_check_for(self.colour):
return self.keep_only_blocking(ret, board)
return ret

View File

@ -1,80 +0,0 @@
from logic.move import Move
from logic.pieces.bishop import Bishop
from logic.pieces.knight import Knight
from logic.pieces.piece import Colour, Piece
from logic.pieces.queen import Queen
from logic.pieces.rook import Rook
from logic.position import Position
class Pawn(Piece):
def legal_moves(self, board, / , looking_for_check = False) -> list[Move]:
ret = []
# can we capture to the left?
if self.pos.x > 0 and (
(self.colour == Colour.WHITE and (capturable_piece := board.piece_at(self.pos.x - 1, self.pos.y + 1)))
or
(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 (self.colour == Colour.WHITE and capturable_piece.pos.y == 7) or (self.colour == Colour.BLACK and capturable_piece.pos.y == 0):
for piece in [Queen, Knight, Bishop, Rook]:
ret.append(Move(self, capturable_piece.pos, is_capturing=True, promotes_to=piece))
else:
ret.append(Move(self, capturable_piece.pos, is_capturing = True))
# can we capture to the right?
if self.pos.x < 7 and (
(self.colour == Colour.WHITE and (capturable_piece := board.piece_at(self.pos.x + 1, self.pos.y + 1)))
or
(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 (self.colour == Colour.WHITE and capturable_piece.pos.y == 7) or (self.colour == Colour.BLACK and capturable_piece.pos.y == 0):
for piece in [Queen, Knight, Bishop, Rook]:
ret.append(Move(self, capturable_piece.pos, is_capturing=True, promotes_to=piece))
else:
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):
y = self.pos.y + dy
if y > 7 or board.piece_at(self.pos.x, y):
break
pos = Position(self.pos.x, y)
if y == 7:
for piece in [Queen, Knight, Bishop, Rook]:
ret.append(Move(self, pos, promotes_to=piece))
else:
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):
y = self.pos.y - dy
if y < 0 or board.piece_at(self.pos.x, y):
break
pos = Position(self.pos.x, y)
if y == 0:
for piece in [Queen, Knight, Bishop, Rook]:
ret.append(Move(self, pos, promotes_to=piece))
else:
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
def letter(self):
return "p"

View File

@ -1,65 +0,0 @@
from logic.move import Move
from logic.position import Position
from enum import Enum
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
assert colour == Colour.WHITE or colour == Colour.BLACK, "The colour of the piece must be either Piece.WHITE or Piece.BLACK"
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):
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 Move(self, Position(x, y))
if piece.colour != self.colour:
return Move(self, Position(x, y), is_capturing=True)
return None
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", / , 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

@ -1,35 +0,0 @@
from logic.move import Move
from .piece import Piece
class Queen(Piece):
def legal_moves(self, board: "Board", / , looking_for_check = False) -> 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))
if not looking_for_check:# and board.is_check_for(self.colour):
return self.keep_only_blocking(ret, board)
return ret

View File

@ -1,23 +0,0 @@
from logic.move import Move
from .piece import Piece
class Rook(Piece):
def legal_moves(self, board: "Board", / , looking_for_check = False) -> 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))
if not looking_for_check:# and board.is_check_for(self.colour):
return self.keep_only_blocking(ret, board)
return ret

View File

@ -1,43 +0,0 @@
class Position:
_RANKS = range(1, 9)
_FILES = "abcdefgh"
_MIN_POS = 0
_MAX_POS = 7
def __init__(self, x, y) -> None:
assert x >= self._MIN_POS and x <= self._MAX_POS, f"Invalid argument: x should be between {self._MIN_POS} and {self._MAX_POS}, but is {x}"
assert y >= self._MIN_POS and y <= self._MAX_POS, f"Invalid argument: y should be between {self._MIN_POS} and {self._MAX_POS}, but is {y}"
self.x = x
self.y = y
@staticmethod
def is_within_bounds(x: int, y: int) -> bool:
return x >= Position._MIN_POS and x <= Position._MAX_POS \
and y >= Position._MIN_POS and y <= Position._MAX_POS
@staticmethod
def from_algebraic(square: str) -> "Position":
assert len(square) == 2, f"'{square}' is malformed"
x = Position._FILES.index(square[0])
y = Position._RANKS.index(int(square[1]))
return Position(x, y)
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):
return False
return value.x == self.x and value.y == self.y
def __hash__(self) -> int:
return hash((self.x, self.y))
def __str__(self) -> str:
return f"{Position._FILES[self.x]}{Position._RANKS[self.y]}"
def __repr__(self) -> str:
return str(self)

View File

@ -1,27 +0,0 @@
import time
from pprint import pprint
from tqdm import tqdm
from ai.ai import move_generation_test
from controller.controller import Controller
from logic.board import INITIAL_BOARD, Board
from logic.position import Position
from view.gui import GUI
from view.tui import TUI
from ai.ai import peft
if __name__ == "__main__":
board = INITIAL_BOARD
pos = "rnbq1k1r/pp1Pbppp/2p5/8/2B5/8/PPP1NnPP/RNBQK2R w KQ - 1 8"
board = Board.setup_FEN_position(pos)
view = GUI()
controller = Controller(board, view)
# view.show()
# exit()
peft(pos)

View File

@ -1,155 +0,0 @@
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.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) -> None:
super().__init__()
self.root = tk.Tk()
self.root.title("Chess Board")
self.tile_size = 80
board_size = self.tile_size * 8
self.canvas = tk.Canvas(self.root, width=board_size, height=board_size)
self.canvas.pack()
self.canvas.bind("<Button-1>", self._on_canvas_click)
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 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]
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,
y * self.tile_size,
(x + 1) * self.tile_size,
(y + 1) * self.tile_size,
fill=colour,
outline=colour,
)
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:
self.canvas.create_image(
(x + 0.5) * self.tile_size,
(y + 0.9) * self.tile_size,
image=self._piece_images[type(piece)][piece.colour],
anchor=tk.S,
)
# Cell annotations
text_colour = colours[(x + y + 1) % 2] # the other colour
if x == 0: # numbers in the top left of the first column
self.canvas.create_text(
(x + .15) * self.tile_size,
(y + .15) * self.tile_size,
text=8-y,
fill=text_colour,
font=("Arial", 12, "bold")
)
if y == 7: # numbers in the top left of the first column
self.canvas.create_text(
(x + .85) * self.tile_size,
(y + .85) * self.tile_size,
text="abcdefgh"[x],
fill=text_colour,
font=("Arial", 12, "bold")
)
def show(self) -> None:
self.root.mainloop()

View File

@ -1,57 +0,0 @@
from logic.board import Board
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 Piece
from logic.pieces.queen import Queen
from logic.pieces.rook import Rook
from view.view import View
class TUI(View):
def __init__(self, board: Board) -> None:
super().__init__(board)
def show(self) -> None:
board_view = [
[" " for _ in range(0, 8)]
for _ in range(0, 8)
]
for pos, piece in self.board._white.items():
board_view[pos.y][pos.x] = self.string_of(piece).upper()
for pos, piece in self.board._black.items():
board_view[pos.y][pos.x] = self.string_of(piece)
# we reverse the board because (0, 0) in in the bottom left, not top left
board_view.reverse()
print(self.to_string(board_view))
def to_string(self, board_view: list[list[str]]) -> str:
VER_SEP = "|"
HOR_SEP = "-"
ROW_SEP = HOR_SEP * (2*len(board_view[0]) + 1)
ret = ROW_SEP + "\n"
for row_view in board_view:
row_str = VER_SEP + VER_SEP.join(row_view) + VER_SEP
ret += row_str + "\n"
ret += ROW_SEP + "\n"
return ret
def string_of(self, piece: Piece) -> str:
type_ = type(piece)
if type_ == Pawn:
return "p"
if type_ == Queen:
return "q"
if type_ == Bishop:
return "b"
if type_ == Knight:
return "n"
if type_ == Rook:
return "r"
if type_ == King:
return "k"
raise ValueError(f"Unknown piece type {type(piece)}")

View File

@ -1,24 +0,0 @@
from logic.board import Board
from logic.move import Move
from logic.pieces.piece import Colour, Piece
class View:
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 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

View File

@ -1,30 +0,0 @@
import unittest
import sys
sys.path.append('src') # you must execute pytest from the stickfosh dir for this to work
from logic.board import Board
class FENTests(unittest.TestCase):
def testInitialPosition(self):
pos = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1"
self.assertEqual(pos, Board.setup_FEN_position(pos).to_fen_string())
def testRandomPositions(self):
pos = "r1bk3r/p2pBpNp/n4n2/1p1NP2P/6P1/3P4/P1P1K3/q5b1 b Qk - 0 1"
self.assertEqual(pos, Board.setup_FEN_position(pos).to_fen_string())
pos = "rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq e3 0 1"
self.assertEqual(pos, Board.setup_FEN_position(pos).to_fen_string())
pos = "4k2r/6r1/8/8/8/8/3R4/R3K3 w Qk - 0 1"
self.assertEqual(pos, Board.setup_FEN_position(pos).to_fen_string())
pos = "8/8/8/4p1K1/2k1P3/8/8/8 b - - 0 1"
self.assertEqual(pos, Board.setup_FEN_position(pos).to_fen_string())
pos = "8/5k2/3p4/1p1Pp2p/pP2Pp1P/P4P1K/8/8 b - - 99 50"
self.assertEqual(pos, Board.setup_FEN_position(pos).to_fen_string())

View File

@ -1,30 +0,0 @@
import unittest
import sys
sys.path.append('src') # you must execute pytest from the stickfosh dir for this to work
from logic.position import Position
class PositionTests(unittest.TestCase):
def testXY2Algebraic(self):
self.assertEqual(Position(0, 0).to_algebraic(), "a1")
self.assertEqual(Position(1, 0).to_algebraic(), "b1")
self.assertEqual(Position(2, 1).to_algebraic(), "c2")
self.assertEqual(Position(4, 2).to_algebraic(), "e3")
self.assertEqual(Position(7, 7).to_algebraic(), "h8")
def testAlgebraic2XY(self):
self.assertEqual(Position.from_algebraic("a1"), Position(0, 0))
self.assertEqual(Position.from_algebraic("b1"), Position(1, 0))
self.assertEqual(Position.from_algebraic("c2"), Position(2, 1))
self.assertEqual(Position.from_algebraic("e3"), Position(4, 2))
self.assertEqual(Position.from_algebraic("h8"), Position(7, 7))
self.assertRaises(AssertionError, lambda : Position.from_algebraic("a11"))
self.assertRaises(ValueError, lambda : Position.from_algebraic("j1"))
self.assertRaises(ValueError, lambda : Position.from_algebraic("a9"))

View File

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

Before

Width:  |  Height:  |  Size: 3.5 KiB

After

Width:  |  Height:  |  Size: 3.5 KiB

View File

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

Before

Width:  |  Height:  |  Size: 734 B

After

Width:  |  Height:  |  Size: 734 B

View File

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

View File

Before

Width:  |  Height:  |  Size: 1014 B

After

Width:  |  Height:  |  Size: 1014 B

View File

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

Before

Width:  |  Height:  |  Size: 2.5 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

Before

Width:  |  Height:  |  Size: 3.0 KiB

After

Width:  |  Height:  |  Size: 3.0 KiB

View File

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 3.8 KiB

View File

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB