135 Commits

Author SHA1 Message Date
e8ca1db8d6 using the thread pool for each move in the top
Some checks failed
tagged-release / Tagged Release (push) Has been cancelled
level of the search (x4~ improvement)
2025-02-05 15:30:55 +01:00
73b2a5b51b created threadpool 2025-02-05 15:17:10 +01:00
c55645ad73 using global peft
Some checks failed
pre-release / Pre Release (push) Waiting to run
tagged-release / Tagged Release (push) Has been cancelled
2025-02-04 21:53:07 +01:00
1b0af3b486 annotated each depths with its time (to then
compare with the different optimizations)
2025-02-04 21:52:07 +01:00
df20060075 added global peft 2025-02-04 21:51:58 +01:00
151a50fba9 made move str better (includes promotion) 2025-02-04 21:51:14 +01:00
609d5f8c98 fixed castle right rook capture coords 2025-02-04 21:50:43 +01:00
e9c96e83da fixed en passant offset 2025-02-04 21:50:30 +01:00
76310400e8 added empty line before move counts to ease copying 2025-02-03 23:41:27 +01:00
e5bb2d1a5f printing moves normally 2025-02-03 23:41:16 +01:00
4c86d5e815 fixed castling rights on capture 2025-02-03 23:40:30 +01:00
b1812f1df6 removed the inclusion of move in piece 2025-02-03 19:10:49 +01:00
9d21141d3c fixed pawn capture + promotion action 2025-02-03 19:10:07 +01:00
974af5d19f we can now print a move (stockfish stile) 2025-02-03 19:09:21 +01:00
df07b4399d minor fix 2025-02-03 19:09:13 +01:00
fa59784230 fixed castling rights that were not removed
properly
2025-02-03 19:08:26 +01:00
be34045f92 minor fix 2025-02-03 19:08:21 +01:00
4eaed699c0 fixed en passant capture 2025-02-03 16:26:31 +01:00
310abb0611 fixed slight mistake 2025-02-03 16:12:20 +01:00
b70ca5302a put back good code 2025-02-03 15:36:11 +01:00
83af5f0b97 removed problemaic code 2025-02-03 15:35:43 +01:00
72ba6a80ae made types behave 2025-02-03 15:35:03 +01:00
1723356f62 removed useless stuff 2025-02-03 15:34:52 +01:00
75adb4b1ba removed already existing variable 2025-02-03 15:34:12 +01:00
6951b860da forgot to push the normal moves for the pawn, fixed it now 2025-02-03 14:05:32 +01:00
805d9fa95c fixed implicit type casting (warning) 2025-02-03 14:05:16 +01:00
4df7cbf01a fixed piece recognition 2025-02-03 14:04:52 +01:00
00137e022a fixed move for position initial guard 2025-02-03 14:04:27 +01:00
570b8df4a7 fixed look_direction 2025-02-03 14:04:18 +01:00
9a66a71c38 handling better the output of perft 2025-02-03 13:59:33 +01:00
41e4119468 reordered members of enum 2025-02-03 13:51:43 +01:00
e7fc1b06de fixed the is_within_bounds check 2025-02-03 13:50:48 +01:00
5f79b81ce4 added validity check when creating coords from index 2025-02-03 13:50:34 +01:00
e5819ee83b minor fix 2025-02-03 13:50:15 +01:00
58109c5120 fixed condition 2025-02-03 13:49:40 +01:00
6478176a7d fixed check condition 2025-02-03 13:47:27 +01:00
ab088da33c removed useless this-> 2025-02-03 13:47:17 +01:00
31799cb0ec added optimization and warning flags to the compiler 2025-02-03 13:44:53 +01:00
96dcbc0a6c actually counting milliseconds now 2025-02-03 13:41:45 +01:00
ce06097063 i forgot to actually implement the root legal_moves haha 2025-02-03 01:12:30 +01:00
3fc2aa73ad actually counting milliseconds now 2025-02-03 01:06:07 +01:00
de6a03ce08 fixed mistake 2025-02-03 01:05:59 +01:00
dc3031ddc1 we can finally print pieces and colours properly 2025-02-03 01:05:37 +01:00
5e4b880aad checking perft to see if my cpp implementation is
correct
2025-02-03 00:33:12 +01:00
540ffa4fb3 fixed slight mistake 2025-02-03 00:33:04 +01:00
21136a26f0 implemented the last details to make everything
work (hopefully)
2025-02-03 00:08:37 +01:00
fa57dcfc30 apparently the default value of a paramter goes in
the declaration and not the defintion, i didn't know that
2025-02-03 00:08:13 +01:00
8da349579d make_move now checks castle rights after each move 2025-02-02 23:52:24 +01:00
f0467bc516 make_move now handles castling 2025-02-02 23:34:10 +01:00
a7cf8e0c21 make_move now handles the setting of en passant
targets
2025-02-02 23:33:52 +01:00
2128159914 make_move now handles promotion 2025-02-02 23:33:29 +01:00
c564add509 removed useless variable 2025-02-02 23:33:20 +01:00
31b0656332 fixed promotion missing colour 2025-02-02 23:16:53 +01:00
24165bb5bb made comment more accurate 2025-02-02 23:14:13 +01:00
4c8fdfa3b4 implemented en passant in legal moves 2025-02-02 23:13:37 +01:00
84c3af2bb1 fully support FEN strings now
Some checks failed
pre-release / Pre Release (push) Has been cancelled
tagged-release / Tagged Release (push) Has been cancelled
2025-02-02 23:05:42 +01:00
22acfc5027 fixed slight bug 2025-02-02 23:05:26 +01:00
1ff4b6b1f4 implemented the en passant target support for FEN
strings
2025-02-02 22:52:46 +01:00
2493892b05 added algebraic support for coordinates 2025-02-02 22:39:06 +01:00
bdd8011577 now support the castling sides of the FEN string 2025-02-02 22:05:33 +01:00
5549c77177 updated testing library 2025-02-02 22:04:41 +01:00
be016dcbcc fixed type mismatch 2025-02-02 22:04:31 +01:00
efd7bf6794 gave default values to the board fields because
they were causing undefinied behaviour
2025-02-02 22:04:04 +01:00
81b24d3082 extended FEN support to include who's turn it is 2025-02-02 21:39:33 +01:00
497da2de8e updated assert equals to fail fast 2025-02-02 21:39:02 +01:00
0574a11b32 fixed minor bug 2025-02-02 21:37:43 +01:00
472f9e4c7c made CastleSide a normal enum again to use &
Some checks failed
tagged-release / Tagged Release (push) Has been cancelled
2025-02-02 21:30:37 +01:00
f5292fa6d7 added small comment 2025-02-02 21:22:38 +01:00
8a3a92f80f minor change 2025-02-02 21:14:31 +01:00
b7ad7b3111 implemented king legal moves 2025-02-02 21:14:14 +01:00
b329c41bea got rid of magic numbers 2025-02-02 21:13:30 +01:00
1625345f08 fixed board.cpp 2025-02-02 20:28:50 +01:00
a790677bb7 implemented queen legal moves 2025-02-02 20:28:41 +01:00
703dcef59f implemented knight legal moves 2025-02-02 20:24:42 +01:00
d74222c2f4 implemented rook legal moves 2025-02-02 20:19:07 +01:00
01c912435b implemented bishop legal moves 2025-02-02 20:17:36 +01:00
8bf164cb05 extracted coords to its own hpp 2025-02-02 20:17:27 +01:00
e08dbb913e fixed board and pawn moves 2025-02-02 19:49:21 +01:00
fea3f6c98a fixed signature 2025-02-02 18:24:23 +01:00
da55f0085f implemented (partially) the pawn move 2025-02-02 18:17:37 +01:00
95327ec653 cleaned up main 2025-02-02 18:17:19 +01:00
1a4e33201e when loading a fen ref, i return the object and
not a pointer
2025-02-02 18:17:02 +01:00
d6baf1ee53 added main to gitignore 2025-02-02 18:16:45 +01:00
67e377bfde made some seperation of concerns 2025-02-02 17:16:37 +01:00
29453fbb14 implemented some stuff 2025-02-02 17:13:55 +01:00
585e392b6a created move struct 2025-02-02 16:49:24 +01:00
d436c5a032 made stuff a tiny bit more compact 2025-02-02 16:49:10 +01:00
08c0a3b50b added test support to the makefile 2025-02-02 16:37:59 +01:00
0ae37a3eba did some stuff to make makefile work... it was
painful
2025-02-02 16:26:46 +01:00
c83129a0d5 added cpp gitignore 2025-02-02 15:31:45 +01:00
acfa27c83e removed binary files 2025-02-02 15:21:04 +01:00
85a5bfa328 moved some stuff around 2025-02-02 15:15:14 +01:00
139a5c7d8f fixed missing imports
Some checks are pending
pre-release / Pre Release (push) Waiting to run
2025-02-02 14:54:17 +01:00
3a2988d351 fixed permissions of fields 2025-02-02 14:53:17 +01:00
c758d1854f removed useless imports 2025-02-02 14:52:47 +01:00
53a0755547 extracted the assert equals to another file to
have mulitple test files
2025-02-02 14:52:13 +01:00
4792daf127 cpp board can go back and forth from FEN
Some checks failed
pre-release / Pre Release (push) Waiting to run
tagged-release / Tagged Release (push) Has been cancelled
2025-02-02 14:50:44 +01:00
947114877b moved everything related to python in the python
Some checks failed
pre-release / Pre Release (push) Waiting to run
tagged-release / Tagged Release (push) Has been cancelled
folder
2025-02-02 13:20:59 +01:00
166e1c7664 added some positions to test the move generation 2025-02-02 13:18:25 +01:00
84d73511d2 fixed a bunch of issues with the move generation 2025-02-02 12:58:05 +01:00
4bb068b2a5 added complete FEN support both for reading and writing 2025-02-01 18:34:41 +01:00
92e1ff26fc added pawn promotion 2025-02-01 16:43:02 +01:00
c7884e227b made the initial board a memeber of the board
Some checks failed
pre-release / Pre Release (push) Has been cancelled
module
2025-02-01 10:37:02 +01:00
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
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
e95caa0015 implemented bishop's possible moves
Some checks are pending
pre-release / Pre Release (push) Waiting to run
2025-01-30 11:48:45 +01:00
bb0b8cdd27 imported colour also in gui 2025-01-30 11:48:27 +01:00
55ba824b13 fixed circular dependency 2025-01-30 11:48:03 +01:00
16d107e5ea fixed the colour that was extracted 2025-01-30 11:45:47 +01:00
baa09135ee made is_capturing a kwarg 2025-01-30 10:46:06 +01:00
eae87f353b now the legal moves of the pawn actually returns
moves
2025-01-30 10:45:43 +01:00
362b0e157d annotated the legal moves function better 2025-01-30 10:45:24 +01:00
c900ebcfa0 extracted the colour to an enum 2025-01-30 10:45:12 +01:00
c3e46017eb now the legal moves function actually returns a
list of moves
2025-01-30 10:28:41 +01:00
324484aa31 created the move class 2025-01-30 09:41:46 +01:00
eca7a6ae0c now black pawns can also go forward 2025-01-29 16:59:13 +01:00
ffe76b161a minor fixes 2025-01-29 16:55:54 +01:00
06f78487d9 the FEN notation can be read to create a position
Some checks failed
pre-release / Pre Release (push) Waiting to run
tagged-release / Tagged Release (push) Has been cancelled
2025-01-29 16:50:08 +01:00
65 changed files with 2601 additions and 241 deletions

2
.gitignore vendored
View File

@ -171,4 +171,4 @@ cython_debug/
.ruff_cache/ .ruff_cache/
# PyPI configuration file # PyPI configuration file
.pypirc .pypirc

34
cpp/.gitignore vendored Normal file
View File

@ -0,0 +1,34 @@
# 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

58
cpp/Makefile Normal file
View File

@ -0,0 +1,58 @@
CXXFLAGS += -O3 -Wall
# Add .d to Make's recognized suffixes.
SUFFIXES += .d
#We don't need to clean up when we're making these targets
NODEPS:=clean tags svn
#Find all the C++ files in the src/ directory
SOURCES:=$(shell find src/ -name "*.cpp")
OBJFILES := $(patsubst src/%.cpp,obj/%.o,$(SOURCES))
#These are the dependency files, which make will clean up after it creates them
DEPFILES:=$(patsubst %.cpp,%.d,$(SOURCES))
#Don't create dependencies when we're cleaning, for instance
ifeq (0, $(words $(findstring $(MAKECMDGOALS), $(NODEPS))))
#Chances are, these files don't exist. GMake will create them and
#clean up automatically afterwards
-include $(DEPFILES)
endif
#This is the rule for creating the dependency files
src/%.d: src/%.cpp
$(CXX) $(CXXFLAGS) -MM -MT '$(patsubst src/%.cpp,obj/%.o,$<)' $< -MF $@
#This rule does the compilation
obj/%.o:
@mkdir -p $(dir $@)
$(CXX) $(CXXFLAGS) -o $@ -c $<
main: $(OBJFILES)
$(CXX) $(LDFLAGS) $(OBJFILES) $(LOADLIBES) $(LDLIBS) -o main
clean:
rm -rf obj/* $(DEPFILES) test_bin/
# --- Test Support ---
# Find all test source files in the tests directory
TESTS := $(shell find tests -name "*.cpp")
# Define corresponding test executable names, e.g. tests/foo.cpp -> test_bin/foo
TEST_BIN := $(patsubst tests/%.cpp,test_bin/%,$(TESTS))
LIBS := $(filter-out obj/main.o,$(OBJFILES))
# Pattern rule: how to build a test executable from a test source file.
# You can adjust CXXFLAGS or add include directories if needed.
test_bin/%: tests/%.cpp $(LIBS)
@echo $(LIBS)
@mkdir -p $(dir $@)
$(CXX) $(CXXFLAGS) -o $@ $< $(LIBS)
# The 'test' target builds all tests and then runs each one.
.PHONY: test
test: $(TEST_BIN)
@echo "Running all tests..."
@for t in $(TEST_BIN); do \
echo "---- Running $$t ----"; \
./$$t || exit 1; \
done

338
cpp/src/board.cpp Normal file
View File

@ -0,0 +1,338 @@
#include "board.hpp"
#include "coords.hpp"
#include "move.hpp"
#include "pieces/piece.hpp"
#include <algorithm>
#include <cctype>
#include <map>
#include <stdexcept>
Board Board::setup_fen_position(std::string fen) {
Board board;
std::map<char, Piece> c2p{
{'k', Piece::King},
{'p', Piece::Pawn},
{'n', Piece::Knigt},
{'b', Piece::Bishop},
{'r', Piece::Rook},
{'q', Piece::Queen},
};
// -- Pieces
std::string fen_board = fen.substr(0, fen.find(' '));
int rank = 7, file = 0;
for (char symbol : fen_board) {
if (symbol == '/') {
file = 0;
rank--;
continue;
}
if (std::isdigit(symbol))
file += symbol - '0';
else {
Colour colour =
std::isupper(symbol) ? Colour::White : Colour::Black;
Piece piece = c2p[std::tolower(symbol)];
board.squares[rank * 8 + file] = colour | piece;
file++;
}
}
// -- Active colour
int index = fen.find(' ');
index++;
board.white_to_play = fen[index] == 'w';
// Castling Rights
index += 2;
for (char symbol : fen.substr(index, fen.find(' ', index + 1))) {
index++;
if (symbol == ' ' || symbol == '-') {
if (symbol == '-')
index++;
break;
}
switch (symbol) {
case 'K':
board.w_castle_rights |= CastleSide::KingSide;
break;
case 'Q':
board.w_castle_rights |= CastleSide::QueenSide;
break;
case 'k':
board.b_castle_rights |= CastleSide::KingSide;
break;
case 'q':
board.b_castle_rights |= CastleSide::QueenSide;
break;
}
}
// -- En passant target
if (fen[index] != '-') {
Coords c = Coords::from_algebraic(fen.substr(index, 2));
index += 2;
board.en_passant_target = c.to_index();
} else {
index++;
}
// -- Half move clock
index = fen.find(' ', index) + 1;
board.n_half_moves =
std::stoi(fen.substr(index, fen.find(' ', index + 1) - index));
// -- Full move number
index = fen.find(' ', index) + 1;
board.n_full_moves = std::stoi(fen.substr(index));
return board;
}
std::string Board::to_fen() const {
std::map<int, char> p2c{
{Piece::King, 'k'},
{Piece::Pawn, 'p'},
{Piece::Knigt, 'n'},
{Piece::Bishop, 'b'},
{Piece::Rook, 'r'},
{Piece::Queen, 'q'},
};
std::string ret;
// -- Pieces
for (int rank = 7; rank >= 0; rank--) {
int empty_cell_counter = 0;
for (int file = 0; file < 8; file++) {
if (squares[rank * 8 + file] == Piece::None) {
empty_cell_counter++;
continue;
}
int full_piece = squares[rank * 8 + file];
char piece = p2c[full_piece & 0b111];
int8_t colour = colour_at({file, rank});
if (empty_cell_counter > 0) {
ret += std::to_string(empty_cell_counter);
empty_cell_counter = 0;
}
ret += colour == Colour::White ? std::toupper(piece) : piece;
}
if (empty_cell_counter > 0)
ret += std::to_string(empty_cell_counter);
if (rank > 0)
ret += "/";
}
ret += " ";
// -- Active colour
ret += white_to_play ? 'w' : 'b';
ret += " ";
// -- Castling Rights
if (w_castle_rights == CastleSide::NeitherSide
&& b_castle_rights == CastleSide::NeitherSide)
ret += '-';
else {
if (w_castle_rights & CastleSide::KingSide)
ret += 'K';
if (w_castle_rights & CastleSide::QueenSide)
ret += 'Q';
if (b_castle_rights & CastleSide::KingSide)
ret += 'k';
if (b_castle_rights & CastleSide::QueenSide)
ret += 'q';
}
ret += ' ';
// -- En passant target
ret += en_passant_target == -1
? "-"
: Coords::from_index(en_passant_target).to_algebraic();
ret += ' ';
// -- Half move clock
ret += std::to_string(n_half_moves);
ret += ' ';
// -- Full moves number
ret += std::to_string(n_full_moves);
return ret;
}
Board Board::make_move(Move move) const {
Board ret;
std::copy(
std::begin(this->squares),
std::end(this->squares),
std::begin(ret.squares)
);
ret.white_to_play = !this->white_to_play;
// -- Actually make the move
ret.squares[move.source_square] = Piece::None;
ret.squares[move.target_square] = this->squares[move.source_square];
// -- Handle en passant target being eaten
if (move.en_passant)
ret.squares[move.target_square + (white_to_play ? -8 : 8)] =
Piece::None;
// -- Handle promotion
if (move.promoting_to != Piece::None)
ret.squares[move.target_square] = move.promoting_to;
// -- Set en passant target if need
if ((squares[move.source_square] & 0b111) == Piece::Pawn
&& std::abs(move.target_square - move.source_square) == 16) {
if (white_to_play)
ret.en_passant_target = move.target_square - 8;
else
ret.en_passant_target = move.target_square + 8;
} else {
ret.en_passant_target = -1;
}
// -- Handle castling (just move the rook over)
Coords c = Coords::from_index(move.source_square);
if (move.castle_side & KingSide) {
Coords rook_source{7, c.y};
int8_t old_rook = ret.squares[rook_source.to_index()];
ret.squares[rook_source.to_index()] = Piece::None;
Coords rook_dest{5, c.y};
ret.squares[rook_dest.to_index()] = old_rook;
} else if (move.castle_side & QueenSide) {
Coords rook_source{0, c.y};
int8_t old_rook = ret.squares[rook_source.to_index()];
ret.squares[rook_source.to_index()] = Piece::None;
Coords rook_dest{3, c.y};
ret.squares[rook_dest.to_index()] = old_rook;
}
// -- Check for castling rights
ret.w_castle_rights = w_castle_rights;
ret.b_castle_rights = b_castle_rights;
if (white_to_play) {
if ((squares[move.source_square] & 0b111) == King)
ret.w_castle_rights = NeitherSide;
if ((squares[move.source_square] & 0b111) == Rook) {
if (c.x == 0 && (ret.w_castle_rights & QueenSide))
ret.w_castle_rights &= ~(QueenSide);
if (c.x == 7 && (ret.w_castle_rights & KingSide))
ret.w_castle_rights &= ~(KingSide);
}
Coords target = Coords::from_index(move.target_square);
if (move.is_capturing && target.y == 7
&& (squares[move.target_square] & 0b111) == Rook) {
if (target.x == 0 && (ret.b_castle_rights & QueenSide))
ret.b_castle_rights &= ~(QueenSide);
if (target.x == 7 && (ret.b_castle_rights & KingSide))
ret.b_castle_rights &= ~(KingSide);
}
} else {
if ((squares[move.source_square] & 0b111) == King)
ret.b_castle_rights = NeitherSide;
if ((squares[move.source_square] & 0b111) == Rook) {
if (c.x == 0 && (ret.b_castle_rights & QueenSide))
ret.b_castle_rights &= ~(QueenSide);
if (c.x == 7 && (ret.b_castle_rights & KingSide))
ret.b_castle_rights &= ~(KingSide);
}
Coords target = Coords::from_index(move.target_square);
if (move.is_capturing && target.y == 0
&& (squares[move.target_square] & 0b111) == Rook) {
if (target.x == 0 && (ret.w_castle_rights & QueenSide))
ret.w_castle_rights &= ~(QueenSide);
if (target.x == 7 && (ret.w_castle_rights & KingSide))
ret.w_castle_rights &= ~(KingSide);
}
}
return ret;
}
int8_t Board::get_king_of(int8_t colour) const {
for (int i = 0; i < 64; i++)
if (squares[i] == (colour | Piece::King))
return i;
throw std::domain_error(
"Apparently there no kings of the such color in this board"
);
}
std::vector<int8_t> to_target_square(std::vector<Move> moves) {
std::vector<int8_t> ret;
for (Move move : moves)
ret.push_back(move.target_square);
return ret;
}
bool Board::is_check_for(int8_t colour) const {
int8_t king_idx = this->get_king_of(colour);
for (int i = 0; i < 64; i++) {
if (this->squares[i] == Piece::None || colour_at(i) == colour)
continue;
std::vector<int8_t> targets;
if ((squares[i] & 0b00111) == King) {
// special case for the king, because it creates infinite recursion
// (since he looks if he's walking into a check)
Coords king_pos = Coords::from_index(i);
for (int dx = -1; dx <= 1; dx++) {
for (int dy = -1; dy <= 1; dy++) {
Coords c{king_pos.x + dx, king_pos.y + dy};
if (c.is_within_bounds())
targets.push_back(c.to_index());
}
}
} else {
std::vector<Move> moves = legal_moves(
this->squares[i],
*this,
Coords::from_index(i),
true
);
targets = to_target_square(moves);
}
if (std::find(targets.begin(), targets.end(), king_idx)
!= targets.end())
return true;
}
return false;
}
bool Board::no_legal_moves_for(int8_t colour) const {
for (int i = 0; i < 64; i++) {
if (colour_at(i) == colour) {
std::vector<Move> moves =
legal_moves(squares[i], *this, Coords::from_index(i));
if (moves.size() > 0)
return false;
}
}
return true;
}
std::vector<Move> Board::all_legal_moves() const {
std::vector<Move> ret;
for (int i = 0; i < 64; i++) {
if ((colour_at(i) == White && white_to_play)
|| (colour_at(i) == Black && !white_to_play)) {
std::vector<Move> moves =
legal_moves(squares[i], *this, Coords::from_index(i));
ret.insert(ret.end(), moves.begin(), moves.end());
}
}
return ret;
}

51
cpp/src/board.hpp Normal file
View File

@ -0,0 +1,51 @@
#pragma once
#include "coords.hpp"
#include "move.hpp"
#include "pieces/piece.hpp"
#include <string>
struct Board {
private:
int8_t get_king_of(int8_t) const;
bool no_legal_moves_for(int8_t) const;
public:
int8_t squares[64] = {Piece::None};
bool white_to_play = true;
int8_t w_castle_rights = CastleSide::NeitherSide;
int8_t b_castle_rights = CastleSide::NeitherSide;
int8_t en_passant_target = -1;
uint8_t n_half_moves = 0;
uint8_t n_full_moves = 0;
static Board setup_fen_position(std::string fen);
Board make_move(Move) const;
std::string to_fen() const;
bool is_check_for(int8_t) const;
std::vector<Move> all_legal_moves() const;
bool is_checkmate_for(int8_t colour) const {
return is_check_for(colour) && no_legal_moves_for(colour);
}
bool is_stalemate_for(int8_t colour) const {
return !is_check_for(colour) && no_legal_moves_for(colour);
}
bool is_terminal() const {
return is_checkmate_for(White) || is_checkmate_for(Black)
|| is_stalemate_for(White) || is_stalemate_for(Black);
}
int8_t colour_at(int8_t idx) const {
return squares[idx] & 0b11000;
}
int8_t colour_at(Coords xy) const {
return colour_at(xy.to_index());
}
};

7
cpp/src/castle_side.hpp Normal file
View File

@ -0,0 +1,7 @@
#include <cstdint>
enum CastleSide : int8_t {
NeitherSide = 0,
KingSide = 1,
QueenSide = 2,
};

72
cpp/src/coords.hpp Normal file
View File

@ -0,0 +1,72 @@
#pragma once
#include <cstdint>
#include <iostream>
#include <stdexcept>
#include <string>
static std::string _FILES = "abcdefgh";
static std::string _RANKS = "12345678";
struct Coords {
int x, y;
Coords(int x, int y): x(x), y(y) {}
int8_t to_index() const {
return this->y * 8 + this->x;
}
static Coords from_index(int idx) {
if (idx < 0 || idx > 63)
throw std::invalid_argument("The index is outside the board...");
return {idx % 8, idx / 8};
}
static Coords from_algebraic(std::string pos) {
if (pos.size() != 2)
throw std::invalid_argument(
"An algebraic coordinate should only have two characters"
);
size_t x = _FILES.find(pos[0]);
if (x == std::string::npos)
throw std::invalid_argument("The first character of the given "
"algebraic coordinate is invalid");
size_t y = _RANKS.find(pos[1]);
if (y == std::string::npos)
throw std::invalid_argument("The second character of the given "
"algebraic coordinate is invalid");
return Coords{(int) x, (int) y};
}
std::string to_algebraic() const {
std::string ret;
if (x > 7 || y > 7)
throw std::invalid_argument(
"Can't give the algebraic vesion of an invalid coord"
);
ret += _FILES[x];
ret += _RANKS[y];
return ret;
}
bool is_within_bounds() const {
return 0 <= x && x < 8 && 0 <= y && y < 8;
}
};
inline bool operator==(const Coords& a, const Coords& b) {
return a.x == b.x && a.y == b.y;
}
inline bool operator!=(const Coords& a, const Coords& b) {
return !(a == b);
}
inline std::ostream& operator<<(std::ostream& os, const Coords& coords) {
os << coords.to_algebraic();
return os;
}

12
cpp/src/main.cpp Normal file
View File

@ -0,0 +1,12 @@
#include "board.hpp"
#include "stickfosh.hpp"
#include <iostream>
int main(int argc, char* argv[]) {
std::string pos =
"r2q1rk1/pP1p2pp/Q4n2/bbp1p3/Np6/1B3NBn/pPPP1PPP/R3K2R b KQ - 0 5";
// Board b = Board::setup_fen_position(pos);
perft();
return 0;
}

48
cpp/src/move.hpp Normal file
View File

@ -0,0 +1,48 @@
#pragma once
#include "castle_side.hpp"
#include "coords.hpp"
#include "pieces/piece.hpp"
#include <cstdint>
#include <sstream>
struct Move {
int8_t source_square;
int8_t target_square;
bool is_capturing = false;
CastleSide castle_side = CastleSide::NeitherSide;
bool en_passant = false;
int8_t promoting_to = Piece::None;
std::string to_string() const {
std::stringstream ss;
ss << Coords::from_index(source_square)
<< Coords::from_index(target_square);
if (promoting_to != Piece::None) {
switch (promoting_to & 0b00111) {
case Queen:
ss << 'q';
break;
case Bishop:
ss << 'b';
break;
case Knigt:
ss << 'n';
break;
case Rook:
ss << 'r';
break;
default:
break;
}
}
return ss.str();
}
};
inline std::ostream& operator<<(std::ostream& os, const Move& m) {
os << m.to_string();
return os;
}

23
cpp/src/pieces/bishop.cpp Normal file
View File

@ -0,0 +1,23 @@
#include "../board.hpp"
#include "../coords.hpp"
#include "../move.hpp"
#include "piece.hpp"
#include <vector>
std::vector<Move> bishop_moves(const Board& b, const Coords xy) {
std::vector<Move> ret;
auto ne = look_direction(b, xy, 1, 1);
ret.insert(ret.end(), ne.begin(), ne.end());
auto se = look_direction(b, xy, 1, -1);
ret.insert(ret.end(), se.begin(), se.end());
auto sw = look_direction(b, xy, -1, -1);
ret.insert(ret.end(), sw.begin(), sw.end());
auto nw = look_direction(b, xy, -1, 1);
ret.insert(ret.end(), nw.begin(), nw.end());
return ret;
}

76
cpp/src/pieces/king.cpp Normal file
View File

@ -0,0 +1,76 @@
#include "../board.hpp"
#include "../coords.hpp"
#include "piece.hpp"
static bool is_clear_king_side(const Board& b, const Coords xy) {
for (int dx = 1; dx < 3; dx++) {
Coords c{xy.x + dx, xy.y};
if (b.squares[c.to_index()] != Piece::None)
return false;
std::optional<Move> move = move_for_position(b, xy, c);
Board board_after_move = b.make_move(move.value());
if (board_after_move.is_check_for(b.colour_at(xy)))
return false;
}
return true;
}
static bool is_clear_queen_side(const Board& b, const Coords xy) {
for (int dx = 1; dx < 4; dx++) {
Coords c{xy.x - dx, xy.y};
if (b.squares[c.to_index()] != Piece::None)
return false;
std::optional<Move> move = move_for_position(b, xy, c);
Board board_after_move = b.make_move(move.value());
if (dx < 3 && board_after_move.is_check_for(b.colour_at(xy)))
return false;
}
return true;
}
std::vector<Move> king_moves(const Board& b, const Coords xy) {
std::vector<Move> ret;
// -- Regular moves
for (int dx = -1; dx <= 1; dx++) {
for (int dy = -1; dy <= 1; dy++) {
if (dx == 0 && dy == 0) // skip staying in the same position
continue;
Coords c{xy.x + dx, xy.y + dy};
std::optional<Move> move = move_for_position(b, xy, c);
if (move.has_value())
ret.push_back(move.value());
}
}
if (b.is_check_for(b.colour_at(xy)))
return keep_only_blocking(ret, b);
// -- Castles
int8_t castling_rights = b.colour_at(xy) == Colour::White
? b.w_castle_rights
: b.b_castle_rights;
if (castling_rights == CastleSide::NeitherSide)
return ret;
if (castling_rights & CastleSide::KingSide && is_clear_king_side(b, xy)) {
ret.push_back(Move{
xy.to_index(),
Coords{6, xy.y}.to_index(),
.castle_side = CastleSide::KingSide
});
}
if (castling_rights & CastleSide::QueenSide && is_clear_queen_side(b, xy)) {
ret.push_back(Move{
xy.to_index(),
Coords{2, xy.y}.to_index(),
.castle_side = CastleSide::QueenSide
});
}
return ret;
}

28
cpp/src/pieces/knight.cpp Normal file
View File

@ -0,0 +1,28 @@
#include "../board.hpp"
#include "../coords.hpp"
#include "../move.hpp"
#include "piece.hpp"
#include <vector>
std::vector<Move> knight_moves(const Board& b, const Coords xy) {
std::vector<Move> ret;
std::vector<std::pair<int, int>> moves = {
{+2, +1},
{+1, +2}, // north east
{+2, -1},
{+1, -2}, // south east
{-2, -1},
{-1, -2}, // south west
{-2, +1},
{-1, +2} // north west
};
for (const auto& [dx, dy] : moves) {
std::optional<Move> move =
move_for_position(b, xy, Coords{xy.x + dx, xy.y + dy});
if (move.has_value())
ret.push_back(move.value());
}
return ret;
}

92
cpp/src/pieces/pawn.cpp Normal file
View File

@ -0,0 +1,92 @@
#include "../board.hpp"
#include "../coords.hpp"
#include "../move.hpp"
#include "piece.hpp"
std::vector<Move> pawn_moves(const Board& b, const Coords xy) {
std::vector<Move> ret{};
int8_t my_colour = b.colour_at(xy);
// -- Capture to the left
if (xy.x > 0) {
int dy = my_colour == Colour::White ? 1 : -1;
Coords left{xy.x - 1, xy.y + dy};
int8_t capturable_piece = b.squares[left.to_index()];
if (capturable_piece != 0) {
if (my_colour != b.colour_at(left))
if ((my_colour == White && left.y == 7)
|| (my_colour == Black && left.y == 0))
for (auto piece : {Rook, Knigt, Bishop, Queen})
ret.push_back(Move{
xy.to_index(),
left.to_index(),
.is_capturing = true,
.promoting_to = (int8_t) (my_colour | piece)
});
else
ret.push_back(Move{xy.to_index(), left.to_index()});
}
}
// -- Capture to the right
if (xy.x < 7) {
int dy = my_colour == Colour::White ? 1 : -1;
Coords right{xy.x + 1, xy.y + dy};
int8_t capturable_piece = b.squares[right.to_index()];
if (capturable_piece != 0) {
if (my_colour != b.colour_at(right))
if ((my_colour == White && right.y == 7)
|| (my_colour == Black && right.y == 0))
for (auto piece : {Rook, Knigt, Bishop, Queen})
ret.push_back(Move{
xy.to_index(),
right.to_index(),
.is_capturing = true,
.promoting_to = (int8_t) (my_colour | piece)
});
else
ret.push_back(Move{xy.to_index(), right.to_index()});
}
}
// -- Capture en passant
if (b.en_passant_target != -1) {
Coords c = Coords::from_index(b.en_passant_target);
int dy = my_colour == Colour::White ? 1 : -1;
if (c.y == xy.y + dy && (c.x == xy.x - 1 || c.x == xy.x + 1)) {
ret.push_back(Move{
xy.to_index(),
c.to_index(),
.is_capturing = true,
.en_passant = true
});
}
}
// -- Normal move + promotion
bool is_on_starting_rank =
my_colour == Colour::White ? xy.y == 1 : xy.y == 6;
int max_dy = is_on_starting_rank ? 3 : 2;
for (int dy = 1; dy < max_dy; dy++) {
int actual_dy = my_colour == Colour::White ? dy : -dy;
Coords new_xy{xy.x, xy.y + actual_dy};
if (b.squares[new_xy.to_index()] != Piece::None)
break;
if (new_xy.y == 7 || new_xy.y == 0)
for (auto piece : {Rook, Knigt, Bishop, Queen})
ret.push_back(Move{
xy.to_index(),
new_xy.to_index(),
.promoting_to = (int8_t) (my_colour | piece)
});
else
ret.push_back(Move{
xy.to_index(),
new_xy.to_index(),
});
}
return ret;
}

88
cpp/src/pieces/piece.cpp Normal file
View File

@ -0,0 +1,88 @@
#include "piece.hpp"
#include "../board.hpp"
#include "../coords.hpp"
std::vector<Move>
keep_only_blocking(const std::vector<Move> candidates, const Board& board) {
if (candidates.size() == 0)
return {};
int8_t my_colour = board.colour_at(candidates[0].source_square);
std::vector<Move> ret;
for (Move move : candidates) {
Board board_after_move = board.make_move(move);
if (!board_after_move.is_check_for(my_colour))
ret.push_back(move);
}
return ret;
}
std::vector<Move>
legal_moves(int8_t p, const Board& b, const Coords xy, bool looking_for_check) {
std::vector<Move> ret;
int8_t simple_piece = p & 0b00111;
switch (simple_piece) {
case Piece::Pawn:
ret = pawn_moves(b, xy);
break;
case Piece::Bishop:
ret = bishop_moves(b, xy);
break;
case Piece::Rook:
ret = rook_moves(b, xy);
break;
case Piece::Knigt:
ret = knight_moves(b, xy);
break;
case Piece::Queen:
ret = queen_moves(b, xy);
break;
case Piece::King:
ret = king_moves(b, xy);
break;
default:
break;
}
if (!looking_for_check)
return keep_only_blocking(ret, b);
return ret;
}
std::optional<Move>
move_for_position(const Board& board, const Coords source, const Coords dest) {
if (!dest.is_within_bounds())
return {};
int8_t piece = board.squares[dest.to_index()];
if (piece == Piece::None)
return Move{source.to_index(), dest.to_index()};
int8_t source_colour = board.colour_at(source);
int8_t dest_colour = board.colour_at(dest);
if (source_colour != dest_colour)
return Move{source.to_index(), dest.to_index(), true};
return {};
}
std::vector<Move>
look_direction(const Board& board, const Coords xy, int mult_dx, int mult_dy) {
std::vector<Move> ret;
for (int d = 1; d < 8; d++) {
int dx = mult_dx * d;
int dy = mult_dy * d;
std::optional<Move> move =
move_for_position(board, xy, Coords{xy.x + dx, xy.y + dy});
if (move.has_value()) {
ret.push_back(move.value());
if (move.value().is_capturing)
break;
} else {
break;
}
}
return ret;
}

42
cpp/src/pieces/piece.hpp Normal file
View File

@ -0,0 +1,42 @@
#pragma once
#include <cstdint>
#include <optional>
#include <ostream>
#include <vector>
enum Piece : int8_t {
None = 0,
Rook = 1,
Knigt = 2,
Bishop = 3,
Queen = 4,
King = 5,
Pawn = 6,
};
enum Colour : int8_t {
White = 8,
Black = 16,
};
inline std::ostream& operator<<(std::ostream& os, const int8_t& i) {
os << std::to_string(i);
return os;
}
class Board;
struct Coords;
struct Move;
std::vector<Move> legal_moves(int8_t, const Board&, const Coords, bool = false);
std::vector<Move> keep_only_blocking(const std::vector<Move>, const Board&);
std::optional<Move> move_for_position(const Board&, const Coords, const Coords);
std::vector<Move> look_direction(const Board&, const Coords, int, int);
std::vector<Move> pawn_moves(const Board&, const Coords);
std::vector<Move> rook_moves(const Board&, const Coords);
std::vector<Move> knight_moves(const Board&, const Coords);
std::vector<Move> bishop_moves(const Board&, const Coords);
std::vector<Move> queen_moves(const Board&, const Coords);
std::vector<Move> king_moves(const Board&, const Coords);

35
cpp/src/pieces/queen.cpp Normal file
View File

@ -0,0 +1,35 @@
#include "../board.hpp"
#include "../coords.hpp"
#include "../move.hpp"
#include "piece.hpp"
#include <vector>
std::vector<Move> queen_moves(const Board& b, const Coords xy) {
std::vector<Move> ret;
auto e = look_direction(b, xy, 1, 0);
ret.insert(ret.end(), e.begin(), e.end());
auto s = look_direction(b, xy, 0, -1);
ret.insert(ret.end(), s.begin(), s.end());
auto w = look_direction(b, xy, -1, 0);
ret.insert(ret.end(), w.begin(), w.end());
auto n = look_direction(b, xy, 0, 1);
ret.insert(ret.end(), n.begin(), n.end());
auto ne = look_direction(b, xy, 1, 1);
ret.insert(ret.end(), ne.begin(), ne.end());
auto se = look_direction(b, xy, 1, -1);
ret.insert(ret.end(), se.begin(), se.end());
auto sw = look_direction(b, xy, -1, -1);
ret.insert(ret.end(), sw.begin(), sw.end());
auto nw = look_direction(b, xy, -1, 1);
ret.insert(ret.end(), nw.begin(), nw.end());
return ret;
}

23
cpp/src/pieces/rook.cpp Normal file
View File

@ -0,0 +1,23 @@
#include "../board.hpp"
#include "../coords.hpp"
#include "../move.hpp"
#include "piece.hpp"
#include <vector>
std::vector<Move> rook_moves(const Board& b, const Coords xy) {
std::vector<Move> ret;
auto e = look_direction(b, xy, 1, 0);
ret.insert(ret.end(), e.begin(), e.end());
auto s = look_direction(b, xy, 0, -1);
ret.insert(ret.end(), s.begin(), s.end());
auto w = look_direction(b, xy, -1, 0);
ret.insert(ret.end(), w.begin(), w.end());
auto n = look_direction(b, xy, 0, 1);
ret.insert(ret.end(), n.begin(), n.end());
return ret;
}

187
cpp/src/stickfosh.cpp Normal file
View File

@ -0,0 +1,187 @@
#include "stickfosh.hpp"
#include "board.hpp"
#include "move.hpp"
#include "threadpool.hpp"
#include <chrono>
#include <map>
#include <ostream>
#include <sstream>
#include <string>
#include <vector>
static std::string tick = "\033[92m✔\033[0m"; // Green Tick;
static std::string cross = "\033[91m❌\033[0m"; // Red Cross
static std::map<std::string, std::map<int, int>> pos2expected{
// -- Position 1
{
"rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1",
{
{1, 20}, // 0
{2, 400}, // 1
{3, 8902}, // 38
{4, 197281}, // 971
// {5, 4865609}, // 23032
},
},
// -- Position 2
{
"r3k2r/p1ppqpb1/bn2pnp1/3PN3/1p2P3/2N2Q1p/PPPBBPPP/R3K2R w KQkq - 0 2",
{
{1, 48}, // 0
{2, 2039}, // 16
{3, 97862}, // 602
// {4, 4085603}, // 26612
},
},
// -- Position 3
{
"8/2p5/3p4/KP5r/1R3p1k/8/4P1P1/8 w - - 0 3",
{
{1, 14}, // 0
{2, 191}, // 1
{3, 2812}, // 11
{4, 43238}, // 157
{5, 674624}, // 2199
// {6, 11030083},
},
},
// -- Position 4a
{
"r3k2r/Pppp1ppp/1b3nbN/nP6/BBP1P3/q4N2/Pp1P2PP/R2Q1RK1 w kq - 0 4",
{
{1, 6}, // 0
{2, 264}, // 1
{3, 9467}, // 69
{4, 422333}, // 3085
// {5, 15833292}, // 124452
},
},
// -- Position 4b
{
"r2q1rk1/pP1p2pp/Q4n2/bbp1p3/Np6/1B3NBn/pPPP1PPP/R3K2R b KQ - 0 5",
{
{1, 6}, // 0
{2, 264}, // 2
{3, 9467}, // 104
{4, 422333}, // 3742
// {5, 15833292}, // 136784
},
},
// -- Position 5
{
"rnbq1k1r/pp1Pbppp/2p5/8/2B5/8/PPP1NnPP/RNBQK2R w KQ - 1 6",
{
{1, 44}, // 0
{2, 1486}, // 12
{3, 62379}, // 357
// {4, 2103487}, // 13804
// {5, 89941194}, // 1230428
},
},
// -- Position 6
{
"r4rk1/1pp1qppp/p1np1n2/2b1p1B1/2B1P1b1/P1NP1N2/1PP1QPPP/R4RK1 w - - 0 "
"7",
{
{1, 46}, // 0
{2, 2079}, // 16
{3, 89890}, // 602
// {4, 3894594}, // 26612
// {5, 164075551}, // 1230428
},
},
};
static std::stringstream res;
int move_generation_test(
const Board& b, int depth, int max_depth, ThreadPool& pool
) {
if (depth == max_depth) {
res.str("");
res.clear();
}
if (b.is_terminal())
return 0;
if (depth == 0)
return 1;
std::vector<Move> moves = b.all_legal_moves();
if (depth == 1)
return moves.size();
int num_pos = 0;
if (depth == max_depth) {
// Parallel execution at the top level
std::vector<std::future<int>> futures;
for (const Move& move : moves) {
Board tmp_board = b.make_move(move);
futures.push_back(pool.enqueue(
move_generation_test,
tmp_board,
depth - 1,
max_depth,
std::ref(pool)
));
}
for (auto& future : futures)
num_pos += future.get(); // Retrieve the result of each task
} else {
// Regular sequential execution
for (const Move& move : moves) {
Board tmp_board = b.make_move(move);
int n = move_generation_test(tmp_board, depth - 1, max_depth, pool);
if (depth == max_depth)
res << move << ": " << n << std::endl;
num_pos += n;
}
}
return num_pos;
}
void perft() {
for (auto& [key, value] : pos2expected)
perft(key);
}
void perft(std::string pos) {
std::cout << pos << std::endl;
std::map<int, int> expected = pos2expected[pos];
Board b = Board::setup_fen_position(pos);
ThreadPool pool(std::thread::hardware_concurrency());
for (const auto& [depth, expected_n_moves] : expected) {
std::cout << "Depth: " << depth << " " << std::flush;
auto start = std::chrono::steady_clock::now();
int moves = move_generation_test(b, depth, depth, pool);
auto end = std::chrono::steady_clock::now();
auto elapsed =
std::chrono::duration_cast<std::chrono::milliseconds>(end - start)
.count();
std::cout << "Results: " << moves << " ";
if (moves == expected_n_moves)
std::cout << tick << " ";
else
std::cout << cross << " (expected " << expected_n_moves << ") ";
std::cout << "positions Time: " << elapsed << " milliseconds"
<< std::endl;
if (moves != expected_n_moves) {
std::cout << std::endl << res.str();
exit(1);
}
}
}

6
cpp/src/stickfosh.hpp Normal file
View File

@ -0,0 +1,6 @@
#pragma once
#include <string>
void perft(std::string pos);
void perft();

73
cpp/src/threadpool.hpp Normal file
View File

@ -0,0 +1,73 @@
#pragma once
#include <condition_variable>
#include <functional>
#include <future>
#include <iostream>
#include <queue>
#include <thread>
#include <vector>
class ThreadPool {
public:
ThreadPool(size_t numThreads) {
for (size_t i = 0; i < numThreads; ++i) {
workers.emplace_back([this] {
while (true) {
std::function<void()> task;
{
std::unique_lock<std::mutex> lock(queueMutex);
condition.wait(lock, [this] {
return stop || !tasks.empty();
});
if (stop && tasks.empty())
return;
task = std::move(tasks.front());
tasks.pop();
}
task();
}
});
}
}
template <class F, class... Args>
auto enqueue(F&& f, Args&&... args)
-> std::future<typename std::invoke_result<F, Args...>::type> {
using return_type = typename std::invoke_result<F, Args...>::type;
auto task = std::make_shared<std::packaged_task<return_type()>>(
std::bind(std::forward<F>(f), std::forward<Args>(args)...)
);
std::future<return_type> res = task->get_future();
{
std::unique_lock<std::mutex> lock(queueMutex);
tasks.emplace([task]() { (*task)(); });
}
condition.notify_one();
return res;
}
void waitAll() {
std::unique_lock<std::mutex> lock(queueMutex);
condition.wait(lock, [this] { return tasks.empty(); });
}
~ThreadPool() {
{
std::unique_lock<std::mutex> lock(queueMutex);
stop = true;
}
condition.notify_all();
for (std::thread& worker : workers)
worker.join();
}
private:
std::vector<std::thread> workers;
std::queue<std::function<void()>> tasks;
std::mutex queueMutex;
std::condition_variable condition;
bool stop = false;
};

23
cpp/tests/fen.cpp Normal file
View File

@ -0,0 +1,23 @@
#include "../src/board.hpp"
#include "lib.hpp"
int main() {
std::string pos =
"rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1";
ASSERT_EQUALS(pos, Board::setup_fen_position(pos).to_fen());
pos = "r1bk3r/p2pBpNp/n4n2/1p1NP2P/6P1/3P4/P1P1K3/q5b1 b Qk - 0 1";
ASSERT_EQUALS(pos, Board::setup_fen_position(pos).to_fen());
pos = "rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq e3 0 1";
ASSERT_EQUALS(pos, Board::setup_fen_position(pos).to_fen());
pos = "4k2r/6r1/8/8/8/8/3R4/R3K3 w Qk - 0 1";
ASSERT_EQUALS(pos, Board::setup_fen_position(pos).to_fen());
pos = "8/8/8/4p1K1/2k1P3/8/8/8 b - - 0 1";
ASSERT_EQUALS(pos, Board::setup_fen_position(pos).to_fen());
pos = "8/5k2/3p4/1p1Pp2p/pP2Pp1P/P4P1K/8/8 b - - 99 50";
ASSERT_EQUALS(pos, Board::setup_fen_position(pos).to_fen());
}

16
cpp/tests/lib.hpp Normal file
View File

@ -0,0 +1,16 @@
#pragma once
#include <iostream>
#define ASSERT_EQUALS(x, y) \
{ \
auto expected = x; \
auto actual = y; \
if (expected != actual) { \
std::cerr << "Expected: " << std::endl \
<< '\t' << expected << std::endl \
<< "Got: " << std::endl \
<< '\t' << actual << std::endl; \
exit(1); \
} \
}

20
cpp/tests/positions.cpp Normal file
View File

@ -0,0 +1,20 @@
#include "../src/coords.hpp"
#include "lib.hpp"
int main() {
ASSERT_EQUALS(Coords(0, 0).to_algebraic(), "a1");
ASSERT_EQUALS(Coords(1, 0).to_algebraic(), "b1");
ASSERT_EQUALS(Coords(2, 1).to_algebraic(), "c2");
ASSERT_EQUALS(Coords(4, 2).to_algebraic(), "e3");
ASSERT_EQUALS(Coords(7, 7).to_algebraic(), "h8");
ASSERT_EQUALS(Coords::from_algebraic("a1"), Coords(0, 0));
ASSERT_EQUALS(Coords::from_algebraic("b1"), Coords(1, 0));
ASSERT_EQUALS(Coords::from_algebraic("c2"), Coords(2, 1));
ASSERT_EQUALS(Coords::from_algebraic("e3"), Coords(4, 2));
ASSERT_EQUALS(Coords::from_algebraic("h8"), Coords(7, 7));
}

BIN
python/res/black-bishop.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

BIN
python/res/black-king.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

BIN
python/res/black-knight.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

BIN
python/res/black-pawn.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 734 B

BIN
python/res/black-queen.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

BIN
python/res/black-rook.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1014 B

BIN
python/res/trimmed.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

BIN
python/res/white-bishop.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

BIN
python/res/white-king.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

BIN
python/res/white-knight.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

BIN
python/res/white-pawn.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

BIN
python/res/white-queen.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

BIN
python/res/white-rook.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

169
python/src/ai/ai.py Normal file
View File

@ -0,0 +1,169 @@
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

@ -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)

360
python/src/logic/board.py Normal file
View File

@ -0,0 +1,360 @@
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)

51
python/src/logic/move.py Normal file
View File

@ -0,0 +1,51 @@
# 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

@ -0,0 +1,24 @@
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

@ -0,0 +1,69 @@
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

@ -0,0 +1,23 @@
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

@ -0,0 +1,80 @@
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

@ -0,0 +1,65 @@
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

@ -0,0 +1,35 @@
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

@ -0,0 +1,23 @@
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,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
@ -9,6 +12,22 @@ class Position:
self.x = x self.x = x
self.y = y 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: def __eq__(self, value: object, /) -> bool:
if type(value) != type(self): if type(value) != type(self):
return False return False
@ -18,8 +37,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)

27
python/src/main.py Normal file
View File

@ -0,0 +1,27 @@
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)

155
python/src/view/gui.py Normal file
View File

@ -0,0 +1,155 @@
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()

24
python/src/view/view.py Normal file
View File

@ -0,0 +1,24 @@
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

30
python/tests/fen.py Normal file
View File

@ -0,0 +1,30 @@
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())

30
python/tests/position.py Normal file
View File

@ -0,0 +1,30 @@
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

@ -1,52 +0,0 @@
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 Piece
from logic.position import Position
class Board:
def __init__(self) -> None:
self._white: dict[Position, Piece] = {}
self._black: dict[Position, Piece] = {}
for x in range(8):
pos_w_pawn = Position(x, 1)
pos_b_pawn = Position(x, 6)
self._white[pos_w_pawn] = Pawn(pos_w_pawn, Piece.WHITE)
self._black[pos_b_pawn] = Pawn(pos_b_pawn, Piece.BLACK)
pos_w_piece = Position(x, 0)
pos_b_piece = Position(x, 7)
piece = None
if x == 0 or x == 7:
piece = Rook
elif x == 1 or x == 6:
piece = Knight
elif x == 2 or x == 5:
piece = Bishop
elif x == 3:
piece = Queen
elif x == 4:
piece = King
assert piece != None, f"Didn't know which piece to assign for {x = }"
self._white[pos_w_piece] = piece(pos_w_piece, Piece.WHITE)
self._black[pos_b_piece] = piece(pos_b_piece, Piece.BLACK)
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 create_board():
return Board()

View File

@ -1,4 +0,0 @@
from .piece import Piece
class Bishop(Piece):
pass

View File

@ -1,5 +0,0 @@
from .piece import Piece
class King(Piece):
pass

View File

@ -1,5 +0,0 @@
from .piece import Piece
class Knight(Piece):
pass

View File

@ -1,31 +0,0 @@
from logic.position import Position
from logic.pieces.piece import Piece
class Pawn(Piece):
def legal_moves(self, board) -> list[Position]:
ret = []
# can we capture to the left?
if self.pos.x > 0 and (
(self.colour == self.WHITE and (capturable_piece := board.piece_at(self.pos.x - 1, self.pos.y + 1)))
or
(self.colour == self.BLACK and (capturable_piece := board.piece_at(self.pos.x - 1, self.pos.y - 1)))
):
if capturable_piece.colour != self.colour:
ret.append(capturable_piece.pos)
# can we capture to the right?
if self.pos.x < 7 and (
(self.colour == self.WHITE and (capturable_piece := board.piece_at(self.pos.x + 1, self.pos.y + 1)))
or
(self.colour == self.BLACK and (capturable_piece := board.piece_at(self.pos.x + 1, self.pos.y - 1)))
):
if capturable_piece.colour != self.colour:
ret.append(capturable_piece.pos)
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):
break
ret.append(Position(self.pos.x, self.pos.y + dy))
return ret

View File

@ -1,17 +0,0 @@
from logic.position import Position
class Piece:
WHITE = "white"
BLACK = "black"
def __init__(self, pos, colour) -> None:
self.pos = pos
assert colour == self.WHITE or colour == self.BLACK, "The colour of the piece must be either Piece.WHITE or Piece.BLACK"
self.colour = colour
def position(self) -> Position:
return self.pos
def legal_moves(self, board) -> list[Position]:
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 +0,0 @@
from .piece import Piece
class Queen(Piece):
pass

View File

@ -1,5 +0,0 @@
from .piece import Piece
class Rook(Piece):
pass

View File

@ -1,10 +0,0 @@
from logic.board import create_board
from view.gui import GUI
from view.tui import TUI
if __name__ == "__main__":
board = create_board()
view = GUI(board)
view.show()

View File

@ -1,94 +0,0 @@
import tkinter as tk
from logic.board import Board
from logic.pieces.piece import Piece
from logic.position import Position
from view.view import View
class GUI(View):
def __init__(self, board: Board) -> None:
super().__init__(board)
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.state = {"selected_piece": None, "legal_moves": []}
self.canvas.bind("<Button-1>", self._on_canvas_click)
self._draw_chess_board()
def _draw_chess_board(self):
colours = ["#F0D9B5", "#B58863"] # Light and dark squares
for y in range(8):
for x in range(8):
colour = colours[(x + y) % 2]
if self.state["selected_piece"] and Position(x, 7-y) in self.state["legal_moves"]:
colour = "#ADD8E6" # Highlight legal moves
self.canvas.create_rectangle(
x * self.tile_size,
y * self.tile_size,
(x + 1) * self.tile_size,
(y + 1) * self.tile_size,
fill=colour
)
piece = self.board.piece_at(x, 7-y)
if piece:
text_colour = "white" if piece.colour == Piece.WHITE else "black"
self.canvas.create_text(
(x + 0.5) * self.tile_size,
(y + 0.5) * self.tile_size,
text=piece.__class__.__name__[0],
fill=text_colour,
font=("Arial", 32, "bold")
)
# 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 + 0.15) * self.tile_size,
(y + 0.15) * self.tile_size,
text=8-y,
fill=text_colour,
font=("Arial", 10, "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,
text="abcdefgh"[x],
fill=text_colour,
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:
self.root.mainloop()

View File

@ -1,10 +0,0 @@
from logic.board import Board
class View:
def __init__(self, board: Board) -> None:
self.board: Board = board
def show(self) -> None:
raise NotImplementedError(f"Can't show the board, the show() method of {type(self)} is not implemented")