- Refactor the search to negamax (single side-relative score + alpha/beta window) - Add a shared, process-wide transposition table: lock-free XOR-verified slots, persists across games, with depth-gated and difficulty-capped score reuse so a weak bot can't borrow a stronger game's deeper analysis - Drive the search with iterative deepening, seeding each depth's move ordering from the previous one - Seed prior-position history (Position::seed_history) over a new engine_best_move history parameter so the engine detects threefold/50-move draws the FEN can't carry Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
360 lines
11 KiB
C++
360 lines
11 KiB
C++
#include "position.h"
|
|
#include "zobrist.h"
|
|
|
|
#include <cctype>
|
|
#include <cstdio>
|
|
#include <cstring>
|
|
#include <memory>
|
|
|
|
|
|
namespace chess {
|
|
|
|
namespace {
|
|
|
|
// Bits of castling rights that are revoked when a piece leaves/arrives a square
|
|
// (covers king moves, rook moves, and rook captures uniformly).
|
|
int castling_mask(Square s) {
|
|
switch (s) {
|
|
case E1: return WHITE_OO | WHITE_OOO;
|
|
case A1: return WHITE_OOO;
|
|
case H1: return WHITE_OO;
|
|
case E8: return BLACK_OO | BLACK_OOO;
|
|
case A8: return BLACK_OOO;
|
|
case H8: return BLACK_OO;
|
|
default: return 0;
|
|
}
|
|
}
|
|
|
|
char piece_to_char(Piece p) {
|
|
const char* w = " PNBRQK";
|
|
char c = w[type_of(p)];
|
|
return color_of(p) == BLACK ? char(std::tolower(c)) : c;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
void Position::put_piece(Piece pc, Square s) {
|
|
board[s] = pc;
|
|
byTypeBB[type_of(pc)] |= square_bb(s);
|
|
byColorBB[color_of(pc)] |= square_bb(s);
|
|
zkey ^= Zobrist::psq[pc][s];
|
|
}
|
|
|
|
void Position::remove_piece(Square s) {
|
|
Piece pc = board[s];
|
|
byTypeBB[type_of(pc)] ^= square_bb(s);
|
|
byColorBB[color_of(pc)] ^= square_bb(s);
|
|
board[s] = NO_PIECE;
|
|
zkey ^= Zobrist::psq[pc][s];
|
|
}
|
|
|
|
void Position::move_piece(Square from, Square to) {
|
|
Piece pc = board[from];
|
|
Bitboard fromTo = square_bb(from) | square_bb(to);
|
|
byTypeBB[type_of(pc)] ^= fromTo;
|
|
byColorBB[color_of(pc)] ^= fromTo;
|
|
board[from] = NO_PIECE;
|
|
board[to] = pc;
|
|
zkey ^= Zobrist::psq[pc][from] ^ Zobrist::psq[pc][to];
|
|
}
|
|
|
|
bool Position::can_castle(Color c, CastlingSide side) const {
|
|
int r = (c == WHITE) ? (side == KINGSIDE ? WHITE_OO : WHITE_OOO)
|
|
: (side == KINGSIDE ? BLACK_OO : BLACK_OOO);
|
|
return (castlingRights & r) != 0;
|
|
}
|
|
|
|
Position Position::from_fen(std::string_view fen) {
|
|
auto held = std::make_unique<Position>();
|
|
Position& p = *held;
|
|
std::memset(p.byTypeBB, 0, sizeof(p.byTypeBB));
|
|
std::memset(p.byColorBB, 0, sizeof(p.byColorBB));
|
|
for (int s = 0; s < SQUARE_NB; ++s) p.board[s] = NO_PIECE;
|
|
p.sideToMove = WHITE;
|
|
p.castlingRights = NO_CASTLING;
|
|
p.epSquare = SQ_NONE;
|
|
p.rule50 = 0;
|
|
p.gamePly = 0;
|
|
p.zkey = 0;
|
|
p.undoCount = 0;
|
|
|
|
size_t i = 0;
|
|
int rank = 7, file = 0;
|
|
|
|
// 1) piece placement
|
|
for (; i < fen.size() && fen[i] != ' '; ++i) {
|
|
char c = fen[i];
|
|
if (c == '/') { --rank; file = 0; }
|
|
else if (std::isdigit((unsigned char)c)) { file += c - '0'; }
|
|
else {
|
|
Color col = std::isupper((unsigned char)c) ? WHITE : BLACK;
|
|
PieceType pt = NO_PIECE_TYPE;
|
|
switch (std::tolower((unsigned char)c)) {
|
|
case 'p': pt = PAWN; break;
|
|
case 'n': pt = KNIGHT; break;
|
|
case 'b': pt = BISHOP; break;
|
|
case 'r': pt = ROOK; break;
|
|
case 'q': pt = QUEEN; break;
|
|
case 'k': pt = KING; break;
|
|
}
|
|
if (pt != NO_PIECE_TYPE)
|
|
p.put_piece(make_piece(col, pt), make_square(File(file), Rank(rank)));
|
|
++file;
|
|
}
|
|
}
|
|
|
|
auto skip_space = [&] { while (i < fen.size() && fen[i] == ' ') ++i; };
|
|
|
|
// 2) side to move
|
|
skip_space();
|
|
if (i < fen.size()) { p.sideToMove = (fen[i] == 'b') ? BLACK : WHITE; ++i; }
|
|
|
|
// 3) castling rights
|
|
skip_space();
|
|
for (; i < fen.size() && fen[i] != ' '; ++i) {
|
|
switch (fen[i]) {
|
|
case 'K': p.castlingRights |= WHITE_OO; break;
|
|
case 'Q': p.castlingRights |= WHITE_OOO; break;
|
|
case 'k': p.castlingRights |= BLACK_OO; break;
|
|
case 'q': p.castlingRights |= BLACK_OOO; break;
|
|
default: break; // '-' or Chess960 letters
|
|
}
|
|
}
|
|
|
|
// 4) en passant
|
|
skip_space();
|
|
if (i < fen.size() && fen[i] != '-' && fen[i] != ' ') {
|
|
File f = File(fen[i] - 'a');
|
|
Rank r = Rank(fen[i + 1] - '1');
|
|
p.epSquare = make_square(f, r);
|
|
i += 2;
|
|
} else if (i < fen.size() && fen[i] == '-') {
|
|
++i;
|
|
}
|
|
|
|
// 5) halfmove clock
|
|
skip_space();
|
|
int halfmove = 0;
|
|
for (; i < fen.size() && std::isdigit((unsigned char)fen[i]); ++i)
|
|
halfmove = halfmove * 10 + (fen[i] - '0');
|
|
p.rule50 = halfmove;
|
|
|
|
// 6) fullmove number
|
|
skip_space();
|
|
int fullmove = 1;
|
|
if (i < fen.size() && std::isdigit((unsigned char)fen[i])) {
|
|
fullmove = 0;
|
|
for (; i < fen.size() && std::isdigit((unsigned char)fen[i]); ++i)
|
|
fullmove = fullmove * 10 + (fen[i] - '0');
|
|
}
|
|
p.gamePly = (fullmove - 1) * 2 + (p.sideToMove == BLACK ? 1 : 0);
|
|
|
|
// finalize the hash
|
|
if (p.sideToMove == BLACK) p.zkey ^= Zobrist::side;
|
|
p.zkey ^= Zobrist::castling[p.castlingRights];
|
|
if (p.epSquare != SQ_NONE) p.zkey ^= Zobrist::enpassant[file_of(p.epSquare)];
|
|
|
|
p.repKeys[0] = p.zkey;
|
|
p.repCount = 1;
|
|
return p;
|
|
}
|
|
|
|
std::string Position::to_fen() const {
|
|
std::string s;
|
|
for (int r = 7; r >= 0; --r) {
|
|
int empty = 0;
|
|
for (int f = 0; f < 8; ++f) {
|
|
Piece pc = board[make_square(File(f), Rank(r))];
|
|
if (pc == NO_PIECE) { ++empty; continue; }
|
|
if (empty) { s += char('0' + empty); empty = 0; }
|
|
s += piece_to_char(pc);
|
|
}
|
|
if (empty) s += char('0' + empty);
|
|
if (r) s += '/';
|
|
}
|
|
s += sideToMove == WHITE ? " w " : " b ";
|
|
|
|
std::string cr;
|
|
if (castlingRights & WHITE_OO) cr += 'K';
|
|
if (castlingRights & WHITE_OOO) cr += 'Q';
|
|
if (castlingRights & BLACK_OO) cr += 'k';
|
|
if (castlingRights & BLACK_OOO) cr += 'q';
|
|
s += cr.empty() ? "-" : cr;
|
|
|
|
s += ' ';
|
|
if (epSquare == SQ_NONE) s += '-';
|
|
else { s += char('a' + file_of(epSquare)); s += char('1' + rank_of(epSquare)); }
|
|
|
|
s += ' ';
|
|
s += std::to_string(rule50);
|
|
s += ' ';
|
|
s += std::to_string(fullmove_number());
|
|
return s;
|
|
}
|
|
|
|
Bitboard Position::attackers_to(Square s, Bitboard occ) const {
|
|
return (PawnAttacks[BLACK][s] & pieces(WHITE, PAWN))
|
|
| (PawnAttacks[WHITE][s] & pieces(BLACK, PAWN))
|
|
| (KnightAttacks[s] & byTypeBB[KNIGHT])
|
|
| (KingAttacks[s] & byTypeBB[KING])
|
|
| (bishop_attacks(s, occ) & (byTypeBB[BISHOP] | byTypeBB[QUEEN]))
|
|
| (rook_attacks(s, occ) & (byTypeBB[ROOK] | byTypeBB[QUEEN]));
|
|
}
|
|
|
|
bool Position::in_check() const {
|
|
return (attackers_to(king_square(sideToMove)) & pieces(~sideToMove)) != 0;
|
|
}
|
|
|
|
bool Position::gives_check(Move m) {
|
|
do_move(m);
|
|
bool checked = in_check();
|
|
undo_move(m);
|
|
return checked;
|
|
}
|
|
|
|
void Position::do_move(Move m) {
|
|
Color us = sideToMove, them = ~us;
|
|
Square from = m.from(), to = m.to();
|
|
MoveFlag flag = m.type();
|
|
Piece pc = board[from];
|
|
Piece captured = (flag == EN_PASSANT) ? make_piece(them, PAWN) : board[to];
|
|
|
|
Undo& u = undoStack[undoCount++];
|
|
u.castlingRights = castlingRights;
|
|
u.epSquare = epSquare;
|
|
u.rule50 = rule50;
|
|
u.key = zkey;
|
|
u.captured = captured;
|
|
|
|
if (epSquare != SQ_NONE) {
|
|
zkey ^= Zobrist::enpassant[file_of(epSquare)];
|
|
epSquare = SQ_NONE;
|
|
}
|
|
|
|
++rule50;
|
|
|
|
if (captured != NO_PIECE) {
|
|
Square capsq = to;
|
|
if (flag == EN_PASSANT) capsq = (us == WHITE) ? Square(to - 8) : Square(to + 8);
|
|
remove_piece(capsq);
|
|
rule50 = 0;
|
|
}
|
|
|
|
move_piece(from, to);
|
|
|
|
if (type_of(pc) == PAWN) {
|
|
rule50 = 0;
|
|
if ((int(to) ^ int(from)) == 16) {
|
|
epSquare = Square((from + to) / 2);
|
|
zkey ^= Zobrist::enpassant[file_of(epSquare)];
|
|
} else if (flag == PROMOTION) {
|
|
remove_piece(to);
|
|
put_piece(make_piece(us, m.promotion()), to);
|
|
}
|
|
}
|
|
|
|
if (flag == CASTLING) {
|
|
Square rookFrom, rookTo;
|
|
if (to > from) { rookFrom = Square(from + 3); rookTo = Square(from + 1); }
|
|
else { rookFrom = Square(from - 4); rookTo = Square(from - 1); }
|
|
move_piece(rookFrom, rookTo);
|
|
}
|
|
|
|
int cr = castlingRights & ~(castling_mask(from) | castling_mask(to));
|
|
if (cr != castlingRights) {
|
|
zkey ^= Zobrist::castling[castlingRights];
|
|
zkey ^= Zobrist::castling[cr];
|
|
castlingRights = cr;
|
|
}
|
|
|
|
sideToMove = them;
|
|
zkey ^= Zobrist::side;
|
|
++gamePly;
|
|
|
|
repKeys[repCount++] = zkey;
|
|
}
|
|
|
|
void Position::undo_move(Move m) {
|
|
Color us = ~sideToMove;
|
|
Square from = m.from(), to = m.to();
|
|
MoveFlag flag = m.type();
|
|
Undo u = undoStack[--undoCount];
|
|
|
|
if (flag == PROMOTION) {
|
|
remove_piece(to);
|
|
put_piece(make_piece(us, PAWN), to);
|
|
}
|
|
|
|
move_piece(to, from);
|
|
|
|
if (u.captured != NO_PIECE) {
|
|
Square capsq = to;
|
|
if (flag == EN_PASSANT) capsq = (us == WHITE) ? Square(to - 8) : Square(to + 8);
|
|
put_piece(u.captured, capsq);
|
|
}
|
|
|
|
if (flag == CASTLING) {
|
|
Square rookFrom, rookTo;
|
|
if (to > from) { rookFrom = Square(from + 3); rookTo = Square(from + 1); }
|
|
else { rookFrom = Square(from - 4); rookTo = Square(from - 1); }
|
|
move_piece(rookTo, rookFrom);
|
|
}
|
|
|
|
sideToMove = us;
|
|
castlingRights = u.castlingRights;
|
|
epSquare = u.epSquare;
|
|
rule50 = u.rule50;
|
|
zkey = u.key;
|
|
--gamePly;
|
|
--repCount;
|
|
}
|
|
|
|
bool Position::insufficient_material() const {
|
|
if (byTypeBB[PAWN] | byTypeBB[ROOK] | byTypeBB[QUEEN])
|
|
return false;
|
|
int minors = popcount(byTypeBB[KNIGHT] | byTypeBB[BISHOP]);
|
|
return minors <= 1; // KvK, KvKN, KvKB
|
|
}
|
|
|
|
void Position::seed_history(const uint64_t* priorKeys, int count) {
|
|
if (count <= 0) return;
|
|
if (count > 1000) count = 1000; // leave headroom in repKeys for search plies
|
|
|
|
uint64_t current = zkey; // from_fen placed this at repKeys[0]
|
|
for (int i = 0; i < count; ++i)
|
|
repKeys[i] = priorKeys[i];
|
|
repKeys[count] = current;
|
|
repCount = count + 1;
|
|
rule50 = count; // == half-moves since the last irreversible move
|
|
}
|
|
|
|
bool Position::is_draw() const {
|
|
if (rule50 >= 100) return true;
|
|
if (insufficient_material()) return true;
|
|
|
|
uint64_t k = repKeys[repCount - 1];
|
|
int seen = 0;
|
|
for (int i = repCount - 3; i >= 0 && i >= repCount - 1 - rule50; i -= 2)
|
|
if (repKeys[i] == k && ++seen >= 2)
|
|
return true; // threefold
|
|
return false;
|
|
}
|
|
|
|
void Position::print() const {
|
|
std::printf("\n +---+---+---+---+---+---+---+---+\n");
|
|
for (int r = 7; r >= 0; --r) {
|
|
std::printf("%d ", r + 1);
|
|
for (int f = 0; f < 8; ++f) {
|
|
Piece pc = board[make_square(File(f), Rank(r))];
|
|
std::printf("| %c ", pc == NO_PIECE ? ' ' : piece_to_char(pc));
|
|
}
|
|
std::printf("|\n +---+---+---+---+---+---+---+---+\n");
|
|
}
|
|
std::printf(" a b c d e f g h\n");
|
|
std::printf(" %s to move key=%016llx\n",
|
|
sideToMove == WHITE ? "White" : "Black",
|
|
(unsigned long long)zkey);
|
|
}
|
|
|
|
} // namespace chess
|