8 Commits

Author SHA1 Message Date
97b3eeb984 first version of stickfosh, simply evaluates a
Some checks failed
tagged-release / Tagged Release (push) Has been cancelled
position based on the material (no AB-pruning)
2025-02-05 17:11:09 +01:00
982f933698 removed useless include 2025-02-05 17:11:00 +01:00
d21fcde21c renamed stickfosh to perft, it's more appropriate 2025-02-05 16:23:16 +01:00
a47dc9b695 Merge branch 'main' into opti-2-compacting-move
Some checks failed
pre-release / Pre Release (push) Waiting to run
tagged-release / Tagged Release (push) Has been cancelled
2025-02-05 15:37:57 +01:00
3f3017a389 removed useless comments 2025-02-05 14:41:23 +01:00
f768d16a7e removed en passant from move 2025-02-05 14:41:16 +01:00
a658673d2b remove castle side from move 2025-02-05 14:36:00 +01:00
222adbb8f0 removed the is_capturing flag of move 2025-02-05 14:25:35 +01:00
11 changed files with 311 additions and 223 deletions

View File

@ -182,7 +182,9 @@ Board Board::make_move(Move move) const {
ret.squares[move.target_square] = this->squares[move.source_square]; ret.squares[move.target_square] = this->squares[move.source_square];
// -- Handle en passant target being eaten // -- Handle en passant target being eaten
if (move.en_passant) if (en_passant_target != -1
&& (squares[move.source_square] & 0b111) == Piece::Pawn
&& squares[move.target_square] == Piece::None)
ret.squares[move.target_square + (white_to_play ? -8 : 8)] = ret.squares[move.target_square + (white_to_play ? -8 : 8)] =
Piece::None; Piece::None;
@ -203,23 +205,26 @@ Board Board::make_move(Move move) const {
// -- Handle castling (just move the rook over) // -- Handle castling (just move the rook over)
Coords c = Coords::from_index(move.source_square); Coords c = Coords::from_index(move.source_square);
if (move.castle_side & KingSide) { if ((squares[move.source_square] & 0b111) == Piece::King) {
Coords rook_source{7, c.y}; if (move.target_square - move.source_square == 2) { // king side castle
int8_t old_rook = ret.squares[rook_source.to_index()]; Coords rook_source{7, c.y};
ret.squares[rook_source.to_index()] = Piece::None; int8_t old_rook = ret.squares[rook_source.to_index()];
Coords rook_dest{5, c.y}; ret.squares[rook_source.to_index()] = Piece::None;
ret.squares[rook_dest.to_index()] = old_rook; Coords rook_dest{5, c.y};
} else if (move.castle_side & QueenSide) { ret.squares[rook_dest.to_index()] = old_rook;
Coords rook_source{0, c.y}; } else if (move.target_square - move.source_square == -2) { // queen
int8_t old_rook = ret.squares[rook_source.to_index()]; Coords rook_source{0, c.y};
ret.squares[rook_source.to_index()] = Piece::None; int8_t old_rook = ret.squares[rook_source.to_index()];
Coords rook_dest{3, c.y}; ret.squares[rook_source.to_index()] = Piece::None;
ret.squares[rook_dest.to_index()] = old_rook; Coords rook_dest{3, c.y};
ret.squares[rook_dest.to_index()] = old_rook;
}
} }
// -- Check for castling rights // -- Check for castling rights
ret.w_castle_rights = w_castle_rights; ret.w_castle_rights = w_castle_rights;
ret.b_castle_rights = b_castle_rights; ret.b_castle_rights = b_castle_rights;
bool is_capturing = squares[move.target_square] != Piece::None;
if (white_to_play) { if (white_to_play) {
if ((squares[move.source_square] & 0b111) == King) if ((squares[move.source_square] & 0b111) == King)
ret.w_castle_rights = NeitherSide; ret.w_castle_rights = NeitherSide;
@ -232,7 +237,7 @@ Board Board::make_move(Move move) const {
} }
Coords target = Coords::from_index(move.target_square); Coords target = Coords::from_index(move.target_square);
if (move.is_capturing && target.y == 7 if (is_capturing && target.y == 7
&& (squares[move.target_square] & 0b111) == Rook) { && (squares[move.target_square] & 0b111) == Rook) {
if (target.x == 0 && (ret.b_castle_rights & QueenSide)) if (target.x == 0 && (ret.b_castle_rights & QueenSide))
ret.b_castle_rights &= ~(QueenSide); ret.b_castle_rights &= ~(QueenSide);
@ -251,7 +256,7 @@ Board Board::make_move(Move move) const {
} }
Coords target = Coords::from_index(move.target_square); Coords target = Coords::from_index(move.target_square);
if (move.is_capturing && target.y == 0 if (is_capturing && target.y == 0
&& (squares[move.target_square] & 0b111) == Rook) { && (squares[move.target_square] & 0b111) == Rook) {
if (target.x == 0 && (ret.w_castle_rights & QueenSide)) if (target.x == 0 && (ret.w_castle_rights & QueenSide))
ret.w_castle_rights &= ~(QueenSide); ret.w_castle_rights &= ~(QueenSide);

View File

@ -1,6 +1,5 @@
#pragma once #pragma once
#include <cstdint>
#include <iostream> #include <iostream>
#include <stdexcept> #include <stdexcept>
#include <string> #include <string>

View File

@ -1,12 +1,11 @@
#include "board.hpp"
#include "stickfosh.hpp" #include "stickfosh.hpp"
#include <iostream> #include <iostream>
int main(int argc, char* argv[]) { int main(int argc, char* argv[]) {
std::string pos = std::string pos =
"r2q1rk1/pP1p2pp/Q4n2/bbp1p3/Np6/1B3NBn/pPPP1PPP/R3K2R b KQ - 0 5"; "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1";
// Board b = Board::setup_fen_position(pos); std::string move = search(pos, 4);
perft(); std::cout << move << std::endl;
return 0; return 0;
} }

View File

@ -11,9 +11,6 @@ struct Move {
int8_t source_square; int8_t source_square;
int8_t target_square; int8_t target_square;
bool is_capturing = false;
CastleSide castle_side = CastleSide::NeitherSide;
bool en_passant = false;
int8_t promoting_to = Piece::None; int8_t promoting_to = Piece::None;
std::string to_string() const { std::string to_string() const {

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

@ -0,0 +1,187 @@
#include "perft.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/perft.hpp Normal file
View File

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

View File

@ -60,7 +60,6 @@ std::vector<Move> king_moves(const Board& b, const Coords xy) {
ret.push_back(Move{ ret.push_back(Move{
xy.to_index(), xy.to_index(),
Coords{6, xy.y}.to_index(), Coords{6, xy.y}.to_index(),
.castle_side = CastleSide::KingSide
}); });
} }
@ -68,7 +67,6 @@ std::vector<Move> king_moves(const Board& b, const Coords xy) {
ret.push_back(Move{ ret.push_back(Move{
xy.to_index(), xy.to_index(),
Coords{2, xy.y}.to_index(), Coords{2, xy.y}.to_index(),
.castle_side = CastleSide::QueenSide
}); });
} }

View File

@ -21,7 +21,6 @@ std::vector<Move> pawn_moves(const Board& b, const Coords xy) {
ret.push_back(Move{ ret.push_back(Move{
xy.to_index(), xy.to_index(),
left.to_index(), left.to_index(),
.is_capturing = true,
.promoting_to = (int8_t) (my_colour | piece) .promoting_to = (int8_t) (my_colour | piece)
}); });
else else
@ -43,7 +42,6 @@ std::vector<Move> pawn_moves(const Board& b, const Coords xy) {
ret.push_back(Move{ ret.push_back(Move{
xy.to_index(), xy.to_index(),
right.to_index(), right.to_index(),
.is_capturing = true,
.promoting_to = (int8_t) (my_colour | piece) .promoting_to = (int8_t) (my_colour | piece)
}); });
else else
@ -55,14 +53,8 @@ std::vector<Move> pawn_moves(const Board& b, const Coords xy) {
if (b.en_passant_target != -1) { if (b.en_passant_target != -1) {
Coords c = Coords::from_index(b.en_passant_target); Coords c = Coords::from_index(b.en_passant_target);
int dy = my_colour == Colour::White ? 1 : -1; int dy = my_colour == Colour::White ? 1 : -1;
if (c.y == xy.y + dy && (c.x == xy.x - 1 || c.x == xy.x + 1)) { if (c.y == xy.y + dy && (c.x == xy.x - 1 || c.x == xy.x + 1))
ret.push_back(Move{ ret.push_back(Move{xy.to_index(), c.to_index()});
xy.to_index(),
c.to_index(),
.is_capturing = true,
.en_passant = true
});
}
} }
// -- Normal move + promotion // -- Normal move + promotion

View File

@ -53,18 +53,11 @@ legal_moves(int8_t p, const Board& b, const Coords xy, bool looking_for_check) {
std::optional<Move> std::optional<Move>
move_for_position(const Board& board, const Coords source, const Coords dest) { move_for_position(const Board& board, const Coords source, const Coords dest) {
if (!dest.is_within_bounds()) if (!dest.is_within_bounds()
|| board.colour_at(source) == board.colour_at(dest))
return {}; return {};
int8_t piece = board.squares[dest.to_index()]; return Move{source.to_index(), 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> std::vector<Move>
@ -74,11 +67,11 @@ look_direction(const Board& board, const Coords xy, int mult_dx, int mult_dy) {
int dx = mult_dx * d; int dx = mult_dx * d;
int dy = mult_dy * d; int dy = mult_dy * d;
std::optional<Move> move = Coords target{xy.x + dx, xy.y + dy};
move_for_position(board, xy, Coords{xy.x + dx, xy.y + dy}); std::optional<Move> move = move_for_position(board, xy, target);
if (move.has_value()) { if (move.has_value()) {
ret.push_back(move.value()); ret.push_back(move.value());
if (move.value().is_capturing) if (board.squares[target.to_index()] != Piece::None)
break; break;
} else { } else {
break; break;

View File

@ -1,187 +1,96 @@
#include "stickfosh.hpp" #include "stickfosh.hpp"
#include "board.hpp" #include "pieces/piece.hpp"
#include "move.hpp"
#include "threadpool.hpp" #include "threadpool.hpp"
#include <chrono> #include <future>
#include <map> #include <map>
#include <ostream>
#include <sstream>
#include <string>
#include <vector>
static std::string tick = "\033[92m✔\033[0m"; // Green Tick; static int INFINITY = std::numeric_limits<int>::max();
static std::string cross = "\033[91m❌\033[0m"; // Red Cross
static std::map<std::string, std::map<int, int>> pos2expected{ std::string search(std::string pos, int depth) {
// -- 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); Board b = Board::setup_fen_position(pos);
ThreadPool pool(std::thread::hardware_concurrency()); ThreadPool pool(std::thread::hardware_concurrency());
for (const auto& [depth, expected_n_moves] : expected) { std::vector<Move> moves = b.all_legal_moves();
std::cout << "Depth: " << depth << " " << std::flush; std::map<std::string, std::future<int>> futures;
auto start = std::chrono::steady_clock::now(); for (const Move& move : moves) {
int moves = move_generation_test(b, depth, depth, pool); Board tmp_board = b.make_move(move);
auto end = std::chrono::steady_clock::now(); futures.insert(
auto elapsed = {move.to_string(), pool.enqueue(minimax, tmp_board, depth - 1)}
std::chrono::duration_cast<std::chrono::milliseconds>(end - start) );
.count(); }
std::cout << "Results: " << moves << " "; std::string best_move;
if (moves == expected_n_moves) int best_eval = -INFINITY;
std::cout << tick << " "; for (auto& [move, future] : futures) {
else int eval = future.get();
std::cout << cross << " (expected " << expected_n_moves << ") "; if (eval > best_eval) {
std::cout << "positions Time: " << elapsed << " milliseconds" best_eval = eval;
<< std::endl; best_move = move;
if (moves != expected_n_moves) {
std::cout << std::endl << res.str();
exit(1);
} }
} }
return best_move;
}
int minimax(const Board& b, int depth) {
if (b.is_checkmate_for(b.white_to_play ? White : Black))
return -INFINITY;
if (depth == 0)
return eval(b);
std::vector<Move> moves = b.all_legal_moves();
int best_evaluation = 0;
Move best_move;
for (const Move& move : moves) {
Board tmp_board = b.make_move(move);
int tmp_eval = -minimax(tmp_board, depth - 1);
best_evaluation = std::max(best_evaluation, tmp_eval);
}
return best_evaluation;
}
static int PawnValue = 100;
static int KnightValue = 300;
static int BishopValue = 320;
static int RookValue = 500;
static int QueenValue = 900;
int count_material(const Board& b, int8_t colour) {
int ret = 0;
for (int i = 0; i < 64; i++) {
if (b.colour_at(i) == colour)
switch (b.squares[i] & 0b111) {
case Piece::Pawn:
ret += PawnValue;
break;
case Piece::Knigt:
ret += KnightValue;
break;
case Piece::Bishop:
ret += BishopValue;
break;
case Piece::Rook:
ret += RookValue;
break;
case Piece::Queen:
ret += QueenValue;
break;
}
}
return ret;
}
int eval(const Board& b) {
int white_eval = count_material(b, Colour::White);
int black_eval = count_material(b, Colour::Black);
int evaluation = white_eval - black_eval;
int perspective = b.white_to_play ? 1 : -1;
return perspective * evaluation;
} }

View File

@ -1,6 +1,9 @@
#pragma once #pragma once
#include "board.hpp"
#include <string> #include <string>
void perft(std::string pos); std::string search(std::string, int);
void perft(); int minimax(const Board&, int);
int eval(const Board&);