diff --git a/include/ironphoenix/geometry.hpp b/include/ironphoenix/geometry.hpp index 99ed6a5..a17490a 100644 --- a/include/ironphoenix/geometry.hpp +++ b/include/ironphoenix/geometry.hpp @@ -7,6 +7,23 @@ namespace ironphoenix::Geometry { +inline constexpr unsigned LINE_BITS = 14; +inline constexpr unsigned LINE_OCCUPANCIES = 1u << LINE_BITS; +inline constexpr unsigned LINE_HALF_BITS = 7; +inline constexpr unsigned LINE_HALF_OCCUPANCIES = 1u << LINE_HALF_BITS; + +inline constexpr unsigned LINE_RANK = 0; +inline constexpr unsigned LINE_FILE = 1; +inline constexpr unsigned LINE_DIAG_A = 2; // file - rank +inline constexpr unsigned LINE_DIAG_B = 3; // file + rank +inline constexpr unsigned LINE_FAMILY_NB = 4; + +inline constexpr unsigned RANK_LINE_BASE = 0; +inline constexpr unsigned FILE_LINE_BASE = RANK_LINE_BASE + BOARD_RANKS; +inline constexpr unsigned DIAG_A_LINE_BASE = FILE_LINE_BASE + BOARD_FILES; +inline constexpr unsigned DIAG_B_LINE_BASE = DIAG_A_LINE_BASE + 27; +inline constexpr unsigned LINE_NB = DIAG_B_LINE_BASE + 27; + extern std::array, BOARD_FILES> SquareFromXY; extern std::array FileOf; extern std::array RankOf; @@ -18,6 +35,16 @@ extern std::array, COLOR_NB> PawnAttacks; extern std::array, COLOR_NB> PawnAttackers; extern Bitboard BoardMask; +// Compact 14-bit line representation used by Position for current-occupancy +// rook/bishop/queen attacks. Missing cross-board squares are encoded as +// permanent blockers so a diagonal can never jump across a board hole. +extern std::array, LINE_FAMILY_NB> LineId; +extern std::array, LINE_FAMILY_NB> LinePos; +extern std::array, LINE_FAMILY_NB> LineBit; +extern std::array LineBlockers; +extern std::array, LINE_BITS> LineAttackMask; +extern std::array, 2>, LINE_NB> LineExpand; + void init(); IRONPHOENIX_FORCE_INLINE constexpr bool validXY(int file, int rank) noexcept { @@ -86,6 +113,16 @@ IRONPHOENIX_FORCE_INLINE bool isPawnHomeSquare(Color c, Square sq) noexcept { return false; } +IRONPHOENIX_FORCE_INLINE Bitboard expandLine(unsigned line, std::uint16_t mask) noexcept { + return LineExpand[line][0][mask & (LINE_HALF_OCCUPANCIES - 1u)] + | LineExpand[line][1][mask >> LINE_HALF_BITS]; +} + +IRONPHOENIX_FORCE_INLINE Bitboard lineAttacks(unsigned line, unsigned pos, std::uint16_t occupancy) noexcept { + const std::uint16_t attacks = LineAttackMask[pos][occupancy & (LINE_OCCUPANCIES - 1u)]; + return expandLine(line, attacks); +} + IRONPHOENIX_FORCE_INLINE constexpr bool rayUsesLsb(Direction d) noexcept { return d == NORTH || d == EAST || d == NORTH_EAST || d == NORTH_WEST; } @@ -100,6 +137,8 @@ IRONPHOENIX_FORCE_INLINE Bitboard rayAttack(Square sq, Direction d, Bitboard occ return ray ^ Ray[static_cast(d)][blocker]; } +// Generic attacks for hypothetical occupancies (SEE, givesCheck, castling). +// Current-position move generation uses Position's compact line occupancy path. IRONPHOENIX_FORCE_INLINE Bitboard rookAttacks(Square sq, Bitboard occ) noexcept { return rayAttack(sq, NORTH, occ) | rayAttack(sq, EAST, occ) diff --git a/include/ironphoenix/move.hpp b/include/ironphoenix/move.hpp index de2bab8..ddca49e 100644 --- a/include/ironphoenix/move.hpp +++ b/include/ironphoenix/move.hpp @@ -26,7 +26,10 @@ enum Promotion : std::uint8_t { }; struct Move { - std::uint32_t v = 0; + // Intentionally no default member initializer. Plain local arrays of Move + // can stay uninitialized, avoiding large memset-style clears in every + // search node. Value-initialized Move{} still produces the null move. + std::uint32_t v; constexpr Move() noexcept = default; constexpr explicit Move(std::uint32_t raw) noexcept : v(raw) {} diff --git a/include/ironphoenix/movegen.hpp b/include/ironphoenix/movegen.hpp index a11812e..ce2e1f8 100644 --- a/include/ironphoenix/movegen.hpp +++ b/include/ironphoenix/movegen.hpp @@ -12,7 +12,9 @@ namespace ironphoenix { constexpr std::size_t MAX_MOVES = 512; struct MoveList { - std::array moves{}; + // Deliberately left uninitialized. Only entries below size are ever read. + // This avoids clearing 2 KiB of move storage every time a MoveList is made. + std::array moves; std::size_t size = 0; IRONPHOENIX_FORCE_INLINE void clear() noexcept { size = 0; } @@ -27,6 +29,7 @@ struct MoveList { }; void generatePseudoLegalMoves(const Position& pos, MoveList& out); +void generateTacticalMoves(const Position& pos, MoveList& out); void generateLegalMoves(Position& pos, MoveList& out); [[nodiscard]] bool isTerminalKingCapture(const Position& pos, Move m) noexcept; diff --git a/include/ironphoenix/position.hpp b/include/ironphoenix/position.hpp index 9b730dd..7e66e01 100644 --- a/include/ironphoenix/position.hpp +++ b/include/ironphoenix/position.hpp @@ -74,6 +74,39 @@ class Position { return (aliveMask_ & (1u << static_cast(c))) != 0; } + // Fast current-occupancy slider attacks. Position maintains a compact + // 14-bit occupancy for every rank/file/diagonal, so these are table lookups + // rather than three-word ray scans. + [[nodiscard]] IRONPHOENIX_FORCE_INLINE Bitboard rookAttacks(Square sq) const noexcept { + const unsigned rankLine = Geometry::LineId[Geometry::LINE_RANK][sq]; + const unsigned fileLine = Geometry::LineId[Geometry::LINE_FILE][sq]; + return Geometry::lineAttacks( + rankLine, + Geometry::LinePos[Geometry::LINE_RANK][sq], + lineOcc_[rankLine]) + | Geometry::lineAttacks( + fileLine, + Geometry::LinePos[Geometry::LINE_FILE][sq], + lineOcc_[fileLine]); + } + + [[nodiscard]] IRONPHOENIX_FORCE_INLINE Bitboard bishopAttacks(Square sq) const noexcept { + const unsigned diagA = Geometry::LineId[Geometry::LINE_DIAG_A][sq]; + const unsigned diagB = Geometry::LineId[Geometry::LINE_DIAG_B][sq]; + return Geometry::lineAttacks( + diagA, + Geometry::LinePos[Geometry::LINE_DIAG_A][sq], + lineOcc_[diagA]) + | Geometry::lineAttacks( + diagB, + Geometry::LinePos[Geometry::LINE_DIAG_B][sq], + lineOcc_[diagB]); + } + + [[nodiscard]] IRONPHOENIX_FORCE_INLINE Bitboard queenAttacks(Square sq) const noexcept { + return rookAttacks(sq) | bishopAttacks(sq); + } + [[nodiscard]] Bitboard attackersTo(Square target, Color by, Bitboard occ) const noexcept; [[nodiscard]] Bitboard attackersToEnemy(Square target, Color victim, Bitboard occ) const noexcept; [[nodiscard]] bool isAttackedBy(Square target, Color by, Bitboard occ) const noexcept; @@ -104,6 +137,10 @@ class Position { std::array, COLOR_NB> pieceBB_{}; std::array colorOcc_{}; Bitboard allOcc_{}; + + // Includes permanent blocker bits for the missing cross-board corner cells. + std::array lineOcc_{}; + std::array kingSq_{SQ_NONE, SQ_NONE, SQ_NONE, SQ_NONE}; std::array enPassant_{SQ_NONE, SQ_NONE, SQ_NONE, SQ_NONE}; std::array, COLOR_NB> castle_{}; @@ -115,8 +152,34 @@ class Position { Key key_ = 0; Bitboard checkers_{}; + IRONPHOENIX_FORCE_INLINE void addLineOccupancy(Square sq) noexcept { + lineOcc_[Geometry::LineId[Geometry::LINE_RANK][sq]] |= Geometry::LineBit[Geometry::LINE_RANK][sq]; + lineOcc_[Geometry::LineId[Geometry::LINE_FILE][sq]] |= Geometry::LineBit[Geometry::LINE_FILE][sq]; + lineOcc_[Geometry::LineId[Geometry::LINE_DIAG_A][sq]] |= Geometry::LineBit[Geometry::LINE_DIAG_A][sq]; + lineOcc_[Geometry::LineId[Geometry::LINE_DIAG_B][sq]] |= Geometry::LineBit[Geometry::LINE_DIAG_B][sq]; + } + + IRONPHOENIX_FORCE_INLINE void removeLineOccupancy(Square sq) noexcept { + lineOcc_[Geometry::LineId[Geometry::LINE_RANK][sq]] &= static_cast(~Geometry::LineBit[Geometry::LINE_RANK][sq]); + lineOcc_[Geometry::LineId[Geometry::LINE_FILE][sq]] &= static_cast(~Geometry::LineBit[Geometry::LINE_FILE][sq]); + lineOcc_[Geometry::LineId[Geometry::LINE_DIAG_A][sq]] &= static_cast(~Geometry::LineBit[Geometry::LINE_DIAG_A][sq]); + lineOcc_[Geometry::LineId[Geometry::LINE_DIAG_B][sq]] &= static_cast(~Geometry::LineBit[Geometry::LINE_DIAG_B][sq]); + } + void rawPut(Square sq, Piece p) noexcept; Piece rawRemove(Square sq) noexcept; + + // Undo restores the complete reversible state from StateInfo afterwards, + // so these variants update only board/bitboards/occupancy and intentionally + // skip Zobrist and king-square maintenance. + void rawPutNoState(Square sq, Piece p) noexcept; + Piece rawRemoveNoState(Square sq) noexcept; + + [[nodiscard]] Bitboard attackersToCurrent(Square target, Color by) const noexcept; + [[nodiscard]] Bitboard attackersToEnemyCurrent(Square target, Color victim) const noexcept; + [[nodiscard]] bool isAttackedByCurrent(Square target, Color by) const noexcept; + [[nodiscard]] bool isAttackedByEnemyCurrent(Square target, Color victim) const noexcept; + void refreshCheckers() noexcept; void updateCastlingRightsForMove(Piece moving, Square from, Piece captured, Square capturedSq) noexcept; }; diff --git a/include/ironphoenix/search.hpp b/include/ironphoenix/search.hpp index 6f697be..2e61f85 100644 --- a/include/ironphoenix/search.hpp +++ b/include/ironphoenix/search.hpp @@ -64,6 +64,7 @@ namespace ironphoenix { static constexpr int INF = 32000; static constexpr int MATE_SCORE = 30000; static constexpr int MATE_THRESHOLD = 29000; + static constexpr std::uint64_t TIME_CHECK_NODE_INTERVAL = 1024; TranspositionTable tt_{ 64 }; HistoryTables history_{}; @@ -75,6 +76,7 @@ namespace ironphoenix { std::chrono::steady_clock::time_point startTime_{}; std::int64_t timeBudgetMs_ = 0; std::uint64_t nodes_ = 0; + std::uint64_t nextTimeCheckNode_ = 0; int selDepth_ = 0; std::array, MAX_PLY> pv_{}; @@ -88,8 +90,14 @@ namespace ironphoenix { int negamax(Position& pos, int depth, int alpha, int beta, int ply, bool pvNode, bool allowNull = true); int qsearch(Position& pos, int alpha, int beta, int ply, bool pvNode); int evaluate(const Position& pos) const noexcept; - int moveScore(const Position& pos, Move move, Move ttMove, int ply) const noexcept; - void orderMoves(const Position& pos, Move* begin, Move* end, Move ttMove, int ply) const; + int moveScore(const Position& pos, Move move, Move ttMove, int ply, bool givesCheck) const noexcept; + void orderMoves(const Position& pos, + Move* begin, + Move* end, + Move ttMove, + int ply, + int* orderedScores = nullptr, + std::uint64_t* givesCheckBits = nullptr) const; [[nodiscard]] int historyBonus(int depth) const noexcept; void updateQuietCutoff(const Position& pos, Color mover, Move cutoffMove, Piece movingPiece, diff --git a/include/ironphoenix/tt.hpp b/include/ironphoenix/tt.hpp index 5086355..5ef0220 100644 --- a/include/ironphoenix/tt.hpp +++ b/include/ironphoenix/tt.hpp @@ -16,12 +16,14 @@ enum class TTBound : std::uint8_t { }; struct TTProbe { - bool hit = false; - Move move{}; - int score = 0; - int depth = -1; - TTBound bound = TTBound::None; - bool pvNode = false; + // Probe fills every field on a hit. On a miss callers use the returned bool + // and must not inspect this object, so leave plain local instances untouched. + bool hit; + Move move; + int score; + int depth; + TTBound bound; + bool pvNode; }; class TranspositionTable { diff --git a/src/eval.cpp b/src/eval.cpp index f0706bf..9166ada 100644 --- a/src/eval.cpp +++ b/src/eval.cpp @@ -51,7 +51,6 @@ namespace ironphoenix::Eval { } int mobilityForColor(const Position& pos, Color c) noexcept { - const Bitboard occ = pos.occupancy(); const Bitboard blockedByTeam = teamOccupancy(pos, c); int score = 0; @@ -68,13 +67,13 @@ namespace ironphoenix::Eval { attacks = Geometry::KnightAttacks[sq]; break; case BISHOP: - attacks = Geometry::bishopAttacks(sq, occ); + attacks = pos.bishopAttacks(sq); break; case ROOK: - attacks = Geometry::rookAttacks(sq, occ); + attacks = pos.rookAttacks(sq); break; case QUEEN: - attacks = Geometry::queenAttacks(sq, occ); + attacks = pos.queenAttacks(sq); break; default: break; diff --git a/src/geometry.cpp b/src/geometry.cpp index f3e5d58..de2156b 100644 --- a/src/geometry.cpp +++ b/src/geometry.cpp @@ -18,6 +18,13 @@ std::array, COLOR_NB> PawnAttacks{}; std::array, COLOR_NB> PawnAttackers{}; Bitboard BoardMask{}; +std::array, LINE_FAMILY_NB> LineId{}; +std::array, LINE_FAMILY_NB> LinePos{}; +std::array, LINE_FAMILY_NB> LineBit{}; +std::array LineBlockers{}; +std::array, LINE_BITS> LineAttackMask{}; +std::array, 2>, LINE_NB> LineExpand{}; + namespace { std::once_flag initFlag; @@ -39,6 +46,112 @@ void addPawnAttack(Color c, Square from, int dx, int dy) { PawnAttackers[static_cast(c)][to].set(from); } +void initializeLineTables() { + std::array, LINE_NB> lineSquare{}; + for (auto& line : lineSquare) + line.fill(SQ_NONE); + + // Ranks: position bit is the file coordinate. + for (int rank = 0; rank < BOARD_RANKS; ++rank) { + const unsigned line = RANK_LINE_BASE + static_cast(rank); + for (int file = 0; file < BOARD_FILES; ++file) { + const unsigned pos = static_cast(file); + if (!validXY(file, rank)) { + LineBlockers[line] |= static_cast(1u << pos); + continue; + } + lineSquare[line][pos] = SquareFromXY[static_cast(file)][static_cast(rank)]; + } + } + + // Files: position bit is the rank coordinate. + for (int file = 0; file < BOARD_FILES; ++file) { + const unsigned line = FILE_LINE_BASE + static_cast(file); + for (int rank = 0; rank < BOARD_RANKS; ++rank) { + const unsigned pos = static_cast(rank); + if (!validXY(file, rank)) { + LineBlockers[line] |= static_cast(1u << pos); + continue; + } + lineSquare[line][pos] = SquareFromXY[static_cast(file)][static_cast(rank)]; + } + } + + // Diagonal A: file - rank is constant; position bit is file. + for (int delta = -13; delta <= 13; ++delta) { + const unsigned line = DIAG_A_LINE_BASE + static_cast(delta + 13); + for (int file = 0; file < BOARD_FILES; ++file) { + const int rank = file - delta; + const unsigned pos = static_cast(file); + if (!validXY(file, rank)) { + LineBlockers[line] |= static_cast(1u << pos); + continue; + } + lineSquare[line][pos] = SquareFromXY[static_cast(file)][static_cast(rank)]; + } + } + + // Diagonal B: file + rank is constant; position bit is file. + for (int sum = 0; sum <= 26; ++sum) { + const unsigned line = DIAG_B_LINE_BASE + static_cast(sum); + for (int file = 0; file < BOARD_FILES; ++file) { + const int rank = sum - file; + const unsigned pos = static_cast(file); + if (!validXY(file, rank)) { + LineBlockers[line] |= static_cast(1u << pos); + continue; + } + lineSquare[line][pos] = SquareFromXY[static_cast(file)][static_cast(rank)]; + } + } + + // One universal 14-bit attack table. Board holes are already present in a + // Position's line occupancy as blockers, so the same table works for ranks, + // files and both diagonal families. + for (unsigned pos = 0; pos < LINE_BITS; ++pos) { + for (unsigned occ = 0; occ < LINE_OCCUPANCIES; ++occ) { + std::uint16_t attacks = 0; + + for (int i = static_cast(pos) - 1; i >= 0; --i) { + const auto bit = static_cast(1u << static_cast(i)); + attacks |= bit; + if (occ & bit) + break; + } + + for (unsigned i = pos + 1; i < LINE_BITS; ++i) { + const auto bit = static_cast(1u << i); + attacks |= bit; + if (occ & bit) + break; + } + + LineAttackMask[pos][occ] = attacks; + } + } + + // Expand a 14-bit line mask back to the engine's 3x64 Bitboard with two + // tiny 7-bit table lookups. Keeping these tables small (~0.5 MiB total) + // is much friendlier to cache than storing a full Bitboard for every + // square/occupancy combination. + for (unsigned line = 0; line < LINE_NB; ++line) { + for (unsigned half = 0; half < 2; ++half) { + const unsigned base = half * LINE_HALF_BITS; + for (unsigned mask = 0; mask < LINE_HALF_OCCUPANCIES; ++mask) { + Bitboard expanded{}; + for (unsigned bit = 0; bit < LINE_HALF_BITS; ++bit) { + if ((mask & (1u << bit)) == 0) + continue; + const Square sq = lineSquare[line][base + bit]; + if (sq != SQ_NONE) + expanded.set(sq); + } + LineExpand[line][half][mask] = expanded; + } + } + } +} + void initialize() { for (auto& file : SquareFromXY) file.fill(SQ_NONE); @@ -65,6 +178,26 @@ void initialize() { const int f = FileOf[sq]; const int r = RankOf[sq]; + const unsigned rankLine = RANK_LINE_BASE + static_cast(r); + const unsigned fileLine = FILE_LINE_BASE + static_cast(f); + const unsigned diagALine = DIAG_A_LINE_BASE + static_cast(f - r + 13); + const unsigned diagBLine = DIAG_B_LINE_BASE + static_cast(f + r); + + LineId[LINE_RANK][sq] = static_cast(rankLine); + LineId[LINE_FILE][sq] = static_cast(fileLine); + LineId[LINE_DIAG_A][sq] = static_cast(diagALine); + LineId[LINE_DIAG_B][sq] = static_cast(diagBLine); + + LinePos[LINE_RANK][sq] = static_cast(f); + LinePos[LINE_FILE][sq] = static_cast(r); + LinePos[LINE_DIAG_A][sq] = static_cast(f); + LinePos[LINE_DIAG_B][sq] = static_cast(f); + + LineBit[LINE_RANK][sq] = static_cast(1u << f); + LineBit[LINE_FILE][sq] = static_cast(1u << r); + LineBit[LINE_DIAG_A][sq] = static_cast(1u << f); + LineBit[LINE_DIAG_B][sq] = static_cast(1u << f); + for (unsigned d = 0; d < 8; ++d) { const int nf = f + DX[d]; const int nr = r + DY[d]; @@ -103,6 +236,8 @@ void initialize() { addPawnAttack(GREEN, sq, -1, -1); addPawnAttack(GREEN, sq, -1, +1); } + + initializeLineTables(); } } diff --git a/src/movegen.cpp b/src/movegen.cpp index 938a275..0da2e7e 100644 --- a/src/movegen.cpp +++ b/src/movegen.cpp @@ -8,27 +8,27 @@ namespace ironphoenix { namespace { - Bitboard teamOccupancy(const Position& pos, Color c) noexcept { + IRONPHOENIX_FORCE_INLINE Bitboard teamOccupancy(const Position& pos, Color c) noexcept { return teamOf(c) == 0 ? (pos.occupancy(RED) | pos.occupancy(YELLOW)) : (pos.occupancy(BLUE) | pos.occupancy(GREEN)); } - Bitboard enemyOccupancy(const Position& pos, Color c) noexcept { + IRONPHOENIX_FORCE_INLINE Bitboard enemyOccupancy(const Position& pos, Color c) noexcept { return teamOf(c) == 0 ? (pos.occupancy(BLUE) | pos.occupancy(GREEN)) : (pos.occupancy(RED) | pos.occupancy(YELLOW)); } - void pushPromotions(MoveList& out, Square from, Square to, Piece captured, std::uint32_t flags = MF_NONE) { + IRONPHOENIX_FORCE_INLINE void pushPromotions(MoveList& out, Square from, Square to, Piece captured, std::uint32_t flags = MF_NONE) noexcept { out.push(Move(from, to, captured, PROMO_QUEEN, flags)); out.push(Move(from, to, captured, PROMO_ROOK, flags)); out.push(Move(from, to, captured, PROMO_BISHOP, flags)); out.push(Move(from, to, captured, PROMO_KNIGHT, flags)); } - void pushPawnMove(Color us, MoveList& out, Square from, Square to, Piece captured = NO_PIECE, - std::uint32_t flags = MF_NONE) { + IRONPHOENIX_FORCE_INLINE void pushPawnMove(Color us, MoveList& out, Square from, Square to, Piece captured = NO_PIECE, + std::uint32_t flags = MF_NONE) noexcept { if (Geometry::isPromotionSquare(us, to)) pushPromotions(out, from, to, captured, flags); else @@ -123,6 +123,27 @@ namespace ironphoenix { return out; } + IRONPHOENIX_FORCE_INLINE void addEnPassantMoves(const Position& pos, Color us, Square from, MoveList& out) noexcept { + const Bitboard attacks = Geometry::PawnAttacks[us][from]; + for (unsigned ci = 0; ci < COLOR_NB; ++ci) { + const Color owner = static_cast(ci); + if (!enemy(us, owner)) + continue; + + const Square ep = pos.enPassant(owner); + if (ep == SQ_NONE || !attacks.test(ep)) + continue; + + const Square victimSq = Geometry::step(ep, Geometry::pawnForward(owner)); + if (victimSq == SQ_NONE) + continue; + + const Piece victim = pos.pieceAt(victimSq); + if (victim == makePiece(owner, PAWN)) + out.push(makeEnPassantMove(from, ep, victim, owner)); + } + } + } void generatePseudoLegalMoves(const Position& pos, MoveList& out) { @@ -131,7 +152,6 @@ namespace ironphoenix { if (!pos.isAlive(us)) return; - const Bitboard all = pos.occupancy(); const Bitboard friendlyTeam = teamOccupancy(pos, us); const Bitboard enemies = enemyOccupancy(pos, us); @@ -156,23 +176,7 @@ namespace ironphoenix { pushPawnMove(us, out, from, to, pos.pieceAt(to)); } - for (unsigned ci = 0; ci < COLOR_NB; ++ci) { - const Color owner = static_cast(ci); - if (!enemy(us, owner)) - continue; - const Square ep = pos.enPassant(owner); - if (ep == SQ_NONE || !Geometry::PawnAttacks[us][from].test(ep)) - continue; - - const Square victimSq = Geometry::step(ep, Geometry::pawnForward(owner)); - if (victimSq == SQ_NONE) - continue; - const Piece victim = pos.pieceAt(victimSq); - if (victim != makePiece(owner, PAWN)) - continue; - - out.push(makeEnPassantMove(from, ep, victim, owner)); - } + addEnPassantMoves(pos, us, from, out); } Bitboard bb = pos.pieces(us, KNIGHT); @@ -188,7 +192,7 @@ namespace ironphoenix { bb = pos.pieces(us, BISHOP); while (bb) { const Square from = popLsb(bb); - Bitboard targets = Geometry::bishopAttacks(from, all) & ~friendlyTeam; + Bitboard targets = pos.bishopAttacks(from) & ~friendlyTeam; while (targets) { const Square to = popLsb(targets); out.push(Move(from, to, pos.pieceAt(to))); @@ -198,7 +202,7 @@ namespace ironphoenix { bb = pos.pieces(us, ROOK); while (bb) { const Square from = popLsb(bb); - Bitboard targets = Geometry::rookAttacks(from, all) & ~friendlyTeam; + Bitboard targets = pos.rookAttacks(from) & ~friendlyTeam; while (targets) { const Square to = popLsb(targets); out.push(Move(from, to, pos.pieceAt(to))); @@ -208,7 +212,7 @@ namespace ironphoenix { bb = pos.pieces(us, QUEEN); while (bb) { const Square from = popLsb(bb); - Bitboard targets = Geometry::queenAttacks(from, all) & ~friendlyTeam; + Bitboard targets = pos.queenAttacks(from) & ~friendlyTeam; while (targets) { const Square to = popLsb(targets); out.push(Move(from, to, pos.pieceAt(to))); @@ -232,6 +236,87 @@ namespace ironphoenix { } } + void generateTacticalMoves(const Position& pos, MoveList& out) { + out.clear(); + const Color us = pos.sideToMove(); + if (!pos.isAlive(us)) + return; + + const Bitboard enemies = enemyOccupancy(pos, us); + + // Quiescence needs captures, en-passant and promotions. Quiet pawn + // moves are generated only when they promote. + Bitboard pawns = pos.pieces(us, PAWN); + while (pawns) { + const Square from = popLsb(pawns); + const Square one = Geometry::step(from, Geometry::pawnForward(us)); + + if (one != SQ_NONE + && Geometry::isPromotionSquare(us, one) + && pos.pieceAt(one) == NO_PIECE) { + pushPromotions(out, from, one, NO_PIECE); + } + + Bitboard caps = Geometry::PawnAttacks[us][from] & enemies; + while (caps) { + const Square to = popLsb(caps); + pushPawnMove(us, out, from, to, pos.pieceAt(to)); + } + + addEnPassantMoves(pos, us, from, out); + } + + Bitboard bb = pos.pieces(us, KNIGHT); + while (bb) { + const Square from = popLsb(bb); + Bitboard targets = Geometry::KnightAttacks[from] & enemies; + while (targets) { + const Square to = popLsb(targets); + out.push(Move(from, to, pos.pieceAt(to))); + } + } + + bb = pos.pieces(us, BISHOP); + while (bb) { + const Square from = popLsb(bb); + Bitboard targets = pos.bishopAttacks(from) & enemies; + while (targets) { + const Square to = popLsb(targets); + out.push(Move(from, to, pos.pieceAt(to))); + } + } + + bb = pos.pieces(us, ROOK); + while (bb) { + const Square from = popLsb(bb); + Bitboard targets = pos.rookAttacks(from) & enemies; + while (targets) { + const Square to = popLsb(targets); + out.push(Move(from, to, pos.pieceAt(to))); + } + } + + bb = pos.pieces(us, QUEEN); + while (bb) { + const Square from = popLsb(bb); + Bitboard targets = pos.queenAttacks(from) & enemies; + while (targets) { + const Square to = popLsb(targets); + out.push(Move(from, to, pos.pieceAt(to))); + } + } + + bb = pos.pieces(us, KING); + while (bb) { + const Square from = popLsb(bb); + Bitboard targets = Geometry::KingAttacks[from] & enemies; + while (targets) { + const Square to = popLsb(targets); + out.push(Move(from, to, pos.pieceAt(to))); + } + } + } + bool isTerminalKingCapture(const Position& pos, Move m) noexcept { Piece victim = m.captured(); if (victim == NO_PIECE) { diff --git a/src/multipv.cpp b/src/multipv.cpp index e8548dc..9018942 100644 --- a/src/multipv.cpp +++ b/src/multipv.cpp @@ -103,6 +103,7 @@ void SearchEngine::runMultiPV(Position position, int multiPV, std::ostream& out) startTime_ = std::chrono::steady_clock::now(); timeBudgetMs_ = calculateTimeBudget(position, limits_); nodes_ = 0; + nextTimeCheckNode_ = 0; selDepth_ = 0; for (auto& row : pv_) @@ -259,4 +260,4 @@ void SearchEngine::runMultiPV(Position position, int multiPV, std::ostream& out) emitBestMove(out, bestMove); } -} // namespace ironphoenix +} // namespace ironphoenix \ No newline at end of file diff --git a/src/position.cpp b/src/position.cpp index 64130ad..f4c6018 100644 --- a/src/position.cpp +++ b/src/position.cpp @@ -18,6 +18,7 @@ namespace ironphoenix { for (auto& bb : colorOcc_) bb.clear(); allOcc_.clear(); + lineOcc_ = Geometry::LineBlockers; kingSq_.fill(SQ_NONE); enPassant_.fill(SQ_NONE); sideToMove_ = RED; @@ -36,6 +37,7 @@ namespace ironphoenix { pieceBB_[c][pt].set(sq); colorOcc_[c].set(sq); allOcc_.set(sq); + addLineOccupancy(sq); if (pt == KING) kingSq_[c] = sq; key_ ^= Zobrist::PieceSquare[p][sq]; @@ -53,12 +55,40 @@ namespace ironphoenix { pieceBB_[c][pt].reset(sq); colorOcc_[c].reset(sq); allOcc_.reset(sq); + removeLineOccupancy(sq); if (pt == KING && kingSq_[c] == sq) kingSq_[c] = SQ_NONE; key_ ^= Zobrist::PieceSquare[p][sq]; return p; } + void Position::rawPutNoState(Square sq, Piece p) noexcept { + assert(sq < SQUARE_NB && p != NO_PIECE && board_[sq] == NO_PIECE); + board_[sq] = p; + const Color c = colorOf(p); + const PieceType pt = typeOf(p); + pieceBB_[c][pt].set(sq); + colorOcc_[c].set(sq); + allOcc_.set(sq); + addLineOccupancy(sq); + } + + Piece Position::rawRemoveNoState(Square sq) noexcept { + assert(sq < SQUARE_NB); + const Piece p = board_[sq]; + if (p == NO_PIECE) + return NO_PIECE; + + const Color c = colorOf(p); + const PieceType pt = typeOf(p); + board_[sq] = NO_PIECE; + pieceBB_[c][pt].reset(sq); + colorOcc_[c].reset(sq); + allOcc_.reset(sq); + removeLineOccupancy(sq); + return p; + } + void Position::placePiece(Square sq, Piece p) { assert(sq < SQUARE_NB); if (board_[sq] != NO_PIECE) @@ -80,6 +110,9 @@ namespace ironphoenix { } void Position::setCastlingRights(std::uint8_t rights) { + if (castlingRights_ == rights) + return; + const std::uint8_t changed = static_cast(castlingRights_ ^ rights); for (unsigned bit = 0; bit < 8; ++bit) if (changed & (1u << bit)) @@ -100,6 +133,9 @@ namespace ironphoenix { void Position::setAliveMask(std::uint8_t mask) { mask &= 0xF; + if (aliveMask_ == mask) + return; + const std::uint8_t changed = static_cast(aliveMask_ ^ mask); for (unsigned c = 0; c < COLOR_NB; ++c) if (changed & (1u << c)) @@ -128,6 +164,9 @@ namespace ironphoenix { assert(verify()); } + // Generic arbitrary-occupancy attack helpers. These remain ray-based because + // SEE, givesCheck and castling temporarily remove/add pieces without mutating + // the Position's current compact line occupancy. Bitboard Position::attackersTo(Square target, Color by, Bitboard occ) const noexcept { if (target == SQ_NONE) return {}; @@ -146,15 +185,27 @@ namespace ironphoenix { } Bitboard Position::attackersToEnemy(Square target, Color victim, Bitboard occ) const noexcept { + if (target == SQ_NONE) + return {}; + + const unsigned c0 = teamOf(victim) == 0 ? static_cast(BLUE) : static_cast(RED); + const unsigned c1 = teamOf(victim) == 0 ? static_cast(GREEN) : static_cast(YELLOW); + Bitboard out{}; - if (teamOf(victim) == 0) { - out |= attackersTo(target, BLUE, occ); - out |= attackersTo(target, GREEN, occ); - } - else { - out |= attackersTo(target, RED, occ); - out |= attackersTo(target, YELLOW, occ); - } + out |= Geometry::PawnAttackers[c0][target] & pieceBB_[c0][PAWN] & occ; + out |= Geometry::PawnAttackers[c1][target] & pieceBB_[c1][PAWN] & occ; + + const Bitboard knights = (pieceBB_[c0][KNIGHT] | pieceBB_[c1][KNIGHT]) & occ; + const Bitboard kings = (pieceBB_[c0][KING] | pieceBB_[c1][KING]) & occ; + out |= Geometry::KnightAttacks[target] & knights; + out |= Geometry::KingAttacks[target] & kings; + + const Bitboard queens = pieceBB_[c0][QUEEN] | pieceBB_[c1][QUEEN]; + const Bitboard rookers = (pieceBB_[c0][ROOK] | pieceBB_[c1][ROOK] | queens) & occ; + const Bitboard bishops = (pieceBB_[c0][BISHOP] | pieceBB_[c1][BISHOP] | queens) & occ; + + out |= Geometry::rookAttacks(target, occ) & rookers; + out |= Geometry::bishopAttacks(target, occ) & bishops; return out; } @@ -175,19 +226,100 @@ namespace ironphoenix { } bool Position::isAttackedByEnemy(Square target, Color victim, Bitboard occ) const noexcept { - if (teamOf(victim) == 0) - return isAttackedBy(target, BLUE, occ) || isAttackedBy(target, GREEN, occ); - return isAttackedBy(target, RED, occ) || isAttackedBy(target, YELLOW, occ); + if (target == SQ_NONE) + return false; + + const unsigned c0 = teamOf(victim) == 0 ? static_cast(BLUE) : static_cast(RED); + const unsigned c1 = teamOf(victim) == 0 ? static_cast(GREEN) : static_cast(YELLOW); + + if (Geometry::PawnAttackers[c0][target] & pieceBB_[c0][PAWN] & occ) return true; + if (Geometry::PawnAttackers[c1][target] & pieceBB_[c1][PAWN] & occ) return true; + + const Bitboard knights = (pieceBB_[c0][KNIGHT] | pieceBB_[c1][KNIGHT]) & occ; + if (Geometry::KnightAttacks[target] & knights) return true; + + const Bitboard kings = (pieceBB_[c0][KING] | pieceBB_[c1][KING]) & occ; + if (Geometry::KingAttacks[target] & kings) return true; + + const Bitboard queens = pieceBB_[c0][QUEEN] | pieceBB_[c1][QUEEN]; + const Bitboard rookers = (pieceBB_[c0][ROOK] | pieceBB_[c1][ROOK] | queens) & occ; + if (Geometry::rookAttacks(target, occ) & rookers) return true; + + const Bitboard bishops = (pieceBB_[c0][BISHOP] | pieceBB_[c1][BISHOP] | queens) & occ; + return static_cast(Geometry::bishopAttacks(target, occ) & bishops); + } + + IRONPHOENIX_FORCE_INLINE Bitboard Position::attackersToCurrent(Square target, Color by) const noexcept { + if (target == SQ_NONE) + return {}; + + const unsigned c = static_cast(by); + Bitboard out{}; + out |= Geometry::PawnAttackers[c][target] & pieceBB_[c][PAWN]; + out |= Geometry::KnightAttacks[target] & pieceBB_[c][KNIGHT]; + out |= Geometry::KingAttacks[target] & pieceBB_[c][KING]; + out |= rookAttacks(target) & (pieceBB_[c][ROOK] | pieceBB_[c][QUEEN]); + out |= bishopAttacks(target) & (pieceBB_[c][BISHOP] | pieceBB_[c][QUEEN]); + return out; + } + + IRONPHOENIX_FORCE_INLINE Bitboard Position::attackersToEnemyCurrent(Square target, Color victim) const noexcept { + if (target == SQ_NONE) + return {}; + + const unsigned c0 = teamOf(victim) == 0 ? static_cast(BLUE) : static_cast(RED); + const unsigned c1 = teamOf(victim) == 0 ? static_cast(GREEN) : static_cast(YELLOW); + + Bitboard out{}; + out |= Geometry::PawnAttackers[c0][target] & pieceBB_[c0][PAWN]; + out |= Geometry::PawnAttackers[c1][target] & pieceBB_[c1][PAWN]; + out |= Geometry::KnightAttacks[target] & (pieceBB_[c0][KNIGHT] | pieceBB_[c1][KNIGHT]); + out |= Geometry::KingAttacks[target] & (pieceBB_[c0][KING] | pieceBB_[c1][KING]); + + const Bitboard queens = pieceBB_[c0][QUEEN] | pieceBB_[c1][QUEEN]; + out |= rookAttacks(target) & (pieceBB_[c0][ROOK] | pieceBB_[c1][ROOK] | queens); + out |= bishopAttacks(target) & (pieceBB_[c0][BISHOP] | pieceBB_[c1][BISHOP] | queens); + return out; + } + + IRONPHOENIX_FORCE_INLINE bool Position::isAttackedByCurrent(Square target, Color by) const noexcept { + if (target == SQ_NONE) + return false; + + const unsigned c = static_cast(by); + if (Geometry::PawnAttackers[c][target] & pieceBB_[c][PAWN]) return true; + if (Geometry::KnightAttacks[target] & pieceBB_[c][KNIGHT]) return true; + if (Geometry::KingAttacks[target] & pieceBB_[c][KING]) return true; + if (rookAttacks(target) & (pieceBB_[c][ROOK] | pieceBB_[c][QUEEN])) return true; + return static_cast(bishopAttacks(target) & (pieceBB_[c][BISHOP] | pieceBB_[c][QUEEN])); + } + + IRONPHOENIX_FORCE_INLINE bool Position::isAttackedByEnemyCurrent(Square target, Color victim) const noexcept { + if (target == SQ_NONE) + return false; + + const unsigned c0 = teamOf(victim) == 0 ? static_cast(BLUE) : static_cast(RED); + const unsigned c1 = teamOf(victim) == 0 ? static_cast(GREEN) : static_cast(YELLOW); + + if (Geometry::PawnAttackers[c0][target] & pieceBB_[c0][PAWN]) return true; + if (Geometry::PawnAttackers[c1][target] & pieceBB_[c1][PAWN]) return true; + if (Geometry::KnightAttacks[target] & (pieceBB_[c0][KNIGHT] | pieceBB_[c1][KNIGHT])) return true; + if (Geometry::KingAttacks[target] & (pieceBB_[c0][KING] | pieceBB_[c1][KING])) return true; + + const Bitboard queens = pieceBB_[c0][QUEEN] | pieceBB_[c1][QUEEN]; + if (rookAttacks(target) & (pieceBB_[c0][ROOK] | pieceBB_[c1][ROOK] | queens)) return true; + return static_cast(bishopAttacks(target) + & (pieceBB_[c0][BISHOP] | pieceBB_[c1][BISHOP] | queens)); } bool Position::inCheck(Color c) const noexcept { const Square k = kingSq_[c]; - return k != SQ_NONE && isAttackedByEnemy(k, c, allOcc_); + return k != SQ_NONE && isAttackedByEnemyCurrent(k, c); } void Position::refreshCheckers() noexcept { const Square k = kingSq_[sideToMove_]; - checkers_ = k == SQ_NONE ? Bitboard{} : attackersToEnemy(k, sideToMove_, allOcc_); + checkers_ = k == SQ_NONE ? Bitboard{} : attackersToEnemyCurrent(k, sideToMove_); } Square Position::capturedSquare(Move m, Color mover) const noexcept { @@ -291,28 +423,36 @@ namespace ironphoenix { Square from, Piece captured, Square capturedSq) noexcept { + + const PieceType movingType = moving == NO_PIECE ? PT_NONE : typeOf(moving); + const bool capturedRook = captured != NO_PIECE && typeOf(captured) == ROOK; + + // The overwhelming majority of moves cannot affect castling rights. + if (movingType != KING && movingType != ROOK && !capturedRook) + return; + std::uint8_t rights = castlingRights_; if (moving != NO_PIECE) { const Color c = colorOf(moving); - const PieceType pt = typeOf(moving); - if (pt == KING) { + if (movingType == KING) { rights &= static_cast(~(3u << (2u * static_cast(c)))); } - else if (pt == ROOK) { + else if (movingType == ROOK) { for (const auto& lane : castle_[c]) if (lane.valid() && from == lane.rookFrom) rights &= static_cast(~(1u << lane.rightBit)); } } - if (captured != NO_PIECE && typeOf(captured) == ROOK) { + if (capturedRook) { const Color c = colorOf(captured); for (const auto& lane : castle_[c]) if (lane.valid() && capturedSq == lane.rookFrom) rights &= static_cast(~(1u << lane.rightBit)); } - setCastlingRights(rights); + if (rights != castlingRights_) + setCastlingRights(rights); } void Position::makeMove(Move m, StateInfo& st) { @@ -342,7 +482,6 @@ namespace ironphoenix { #endif setEnPassant(us, SQ_NONE); - updateCastlingRightsForMove(moving, from, st.captured, st.capturedSq); if (st.captured != NO_PIECE) { @@ -399,20 +538,22 @@ namespace ironphoenix { const Color us = static_cast(st.side); const Square from = m.from(); const Square to = m.to(); - \ - rawRemove(to); + + // The exact Zobrist/king state is restored from StateInfo below, so + // undo only needs to repair the board and occupancy structures here. + rawRemoveNoState(to); if (m.isCastle()) { const CastleLane* lane = castleLane(us, m); assert(lane && lane->valid()); - const Piece rook = rawRemove(lane->rookTo); + const Piece rook = rawRemoveNoState(lane->rookTo); assert(rook == makePiece(us, ROOK)); - rawPut(lane->rookFrom, rook); + rawPutNoState(lane->rookFrom, rook); } - rawPut(from, st.moved); + rawPutNoState(from, st.moved); if (st.captured != NO_PIECE) - rawPut(st.capturedSq, st.captured); + rawPutNoState(st.capturedSq, st.captured); enPassant_ = st.enPassant; kingSq_ = st.kingSq; @@ -490,6 +631,7 @@ namespace ironphoenix { std::array, COLOR_NB> rebuiltPieces{}; std::array rebuiltColor{}; Bitboard rebuiltAll{}; + std::array rebuiltLines = Geometry::LineBlockers; std::array rebuiltKing{ SQ_NONE, SQ_NONE, SQ_NONE, SQ_NONE }; for (unsigned s = 0; s < SQUARE_NB; ++s) { @@ -500,17 +642,24 @@ namespace ironphoenix { const PieceType pt = typeOf(p); if (pt < PAWN || pt > KING) return false; - rebuiltPieces[c][pt].set(static_cast(s)); - rebuiltColor[c].set(static_cast(s)); - rebuiltAll.set(static_cast(s)); + + const Square sq = static_cast(s); + rebuiltPieces[c][pt].set(sq); + rebuiltColor[c].set(sq); + rebuiltAll.set(sq); + rebuiltLines[Geometry::LineId[Geometry::LINE_RANK][sq]] |= Geometry::LineBit[Geometry::LINE_RANK][sq]; + rebuiltLines[Geometry::LineId[Geometry::LINE_FILE][sq]] |= Geometry::LineBit[Geometry::LINE_FILE][sq]; + rebuiltLines[Geometry::LineId[Geometry::LINE_DIAG_A][sq]] |= Geometry::LineBit[Geometry::LINE_DIAG_A][sq]; + rebuiltLines[Geometry::LineId[Geometry::LINE_DIAG_B][sq]] |= Geometry::LineBit[Geometry::LINE_DIAG_B][sq]; + if (pt == KING) { if (rebuiltKing[c] != SQ_NONE) return false; - rebuiltKing[c] = static_cast(s); + rebuiltKing[c] = sq; } } - if (rebuiltAll != allOcc_ || rebuiltKing != kingSq_) + if (rebuiltAll != allOcc_ || rebuiltKing != kingSq_ || rebuiltLines != lineOcc_) return false; for (unsigned c = 0; c < COLOR_NB; ++c) { if (rebuiltColor[c] != colorOcc_[c]) diff --git a/src/search.cpp b/src/search.cpp index 4a078fe..096350d 100644 --- a/src/search.cpp +++ b/src/search.cpp @@ -20,6 +20,17 @@ namespace ironphoenix { constexpr int NULL_MOVE_MIN_DEPTH = 2; constexpr int NULL_MOVE_DIVISOR = 190; + constexpr std::size_t CHECK_BIT_WORDS = (MAX_MOVES + 63u) / 64u; + + // Non-PV nodes normally cut off quickly. Selecting only the strongest + // prefix lazily avoids sorting the entire high-branching 4PC move list. + constexpr std::size_t LAZY_PICK_LIMIT = 16; + + struct ScoredMove final { + int score; + Move move; + bool givesCheck; + }; IRONPHOENIX_FORCE_INLINE Bitboard teamOccupancy(const Position& pos, Color c) noexcept { const Color partner = static_cast(static_cast(c) ^ 2u); @@ -34,6 +45,20 @@ namespace ironphoenix { return static_cast(pieces); } + IRONPHOENIX_FORCE_INLINE bool cachedGivesCheck(const std::uint64_t* bits, std::size_t index) noexcept { + return ((bits[index >> 6u] >> (index & 63u)) & 1ull) != 0; + } + + IRONPHOENIX_FORCE_INLINE int captureOrderingScore(Piece movingPiece, Move move) noexcept { + // Full four-player SEE is expensive enough that using it on every + // generated capture purely for ordering costs substantial NPS. + // MVV/LVA gives us a cheap first-order tactical score; SEE is still + // used later by actual SEE pruning where its result affects search. + const int victim = pieceValue(move.captured()); + const int attacker = pieceValue(movingPiece); + return victim * 128 - attacker * 8; + } + } SearchEngine::SearchEngine() = default; @@ -118,9 +143,15 @@ namespace ironphoenix { return true; } - if (timeBudgetMs_ > 0 && elapsedMs() >= timeBudgetMs_) { - stopRequested_.store(true, std::memory_order_relaxed); - return true; + // steady_clock::now() is surprisingly expensive when called several + // times per searched move. Poll the clock periodically by node count; + // explicit root checks still enforce the budget between iterations. + if (timeBudgetMs_ > 0 && nodes_ >= nextTimeCheckNode_) { + nextTimeCheckNode_ = nodes_ + TIME_CHECK_NODE_INTERVAL; + if (elapsedMs() >= timeBudgetMs_) { + stopRequested_.store(true, std::memory_order_relaxed); + return true; + } } return false; @@ -135,10 +166,15 @@ namespace ironphoenix { return std::min(params.historyBonusMax, params.historyBonusScale * depth * depth); } - int SearchEngine::moveScore(const Position& pos, Move move, Move ttMove, int ply) const noexcept { + int SearchEngine::moveScore(const Position& pos, + Move move, + Move ttMove, + int ply, + bool givesCheck) const noexcept { + if (move == ttMove) return 50'000'000; - if (isTerminalKingCapture(pos, move)) + if (move.isCapture() && isTerminalKingCapture(pos, move)) return 45'000'000; const auto& tuning = Tuning::search(); @@ -148,7 +184,7 @@ namespace ironphoenix { if (move.isPromotion()) { int score = 30'000'000 + pieceValue(promotedType(move.promotion())) * 128; if (move.isCapture()) { - score += std::clamp(see(pos, move), -5000, 5000) * 64; + score += captureOrderingScore(movingPiece, move); score += history_.captureScore(mover, movingPiece, move) * 2; } return score; @@ -156,7 +192,7 @@ namespace ironphoenix { if (move.isCapture()) { int score = 20'000'000; - score += std::clamp(see(pos, move), -5000, 5000) * 64; + score += captureOrderingScore(movingPiece, move); score += history_.captureScore(mover, movingPiece, move) * 2; return score; } @@ -175,23 +211,56 @@ namespace ironphoenix { score += (history_.continuationScore(ContinuationKind::SamePlayer, searchStack_[ply - 3], movingPiece, move.to()) * 3) / 4; } - if (pos.givesCheck(move)) + if (givesCheck) score += tuning.checkMoveBonus; return score; } - void SearchEngine::orderMoves(const Position& pos, Move* begin, Move* end, Move ttMove, int ply) const { - std::array, MAX_MOVES> scored{}; + void SearchEngine::orderMoves(const Position& pos, + Move* begin, + Move* end, + Move ttMove, + int ply, + int* orderedScores, + std::uint64_t* givesCheckBits) const { + + // Do not value-initialize all 512 scratch entries at every node. Only + // the first count entries are written and subsequently read. + std::array scored; const std::size_t count = static_cast(end - begin); - for (std::size_t i = 0; i < count; ++i) - scored[i] = { moveScore(pos, begin[i], ttMove, ply), begin[i] }; + + for (std::size_t i = 0; i < count; ++i) { + const Move move = begin[i]; + const bool terminal = move.isCapture() && isTerminalKingCapture(pos, move); + + // Capture/promotion scores do not consume the check bonus, so avoid + // the expensive givesCheck() call unless the caller explicitly needs + // check metadata for pruning safety. + const bool needCheck = givesCheckBits != nullptr + || ((!move.isCapture() && !move.isPromotion()) && move != ttMove); + const bool givesCheck = !terminal && needCheck && pos.givesCheck(move); + + scored[i].score = moveScore(pos, move, ttMove, ply, givesCheck); + scored[i].move = move; + scored[i].givesCheck = givesCheck; + } std::sort(scored.begin(), scored.begin() + count, - [](const auto& a, const auto& b) { return a.first > b.first; }); + [](const ScoredMove& a, const ScoredMove& b) { return a.score > b.score; }); - for (std::size_t i = 0; i < count; ++i) - begin[i] = scored[i].second; + if (givesCheckBits) { + for (std::size_t i = 0; i < CHECK_BIT_WORDS; ++i) + givesCheckBits[i] = 0; + } + + for (std::size_t i = 0; i < count; ++i) { + begin[i] = scored[i].move; + if (orderedScores) + orderedScores[i] = scored[i].score; + if (givesCheckBits && scored[i].givesCheck) + givesCheckBits[i >> 6u] |= 1ull << (i & 63u); + } } void SearchEngine::updateQuietCutoff(const Position& pos, @@ -306,18 +375,14 @@ namespace ironphoenix { alpha = bestScore; } - MoveList pseudo; - generatePseudoLegalMoves(pos, pseudo); - + // In check, every evasion is relevant. Otherwise generate only captures, + // en-passant and promotions directly instead of generating/copying every + // quiet pseudo-legal move through a second 512-entry buffer. MoveList moves; - for (Move move : pseudo) { - if (inCheckNode - || move.isCapture() - || move.isPromotion() - || isTerminalKingCapture(pos, move)) { - moves.push(move); - } - } + if (inCheckNode) + generatePseudoLegalMoves(pos, moves); + else + generateTacticalMoves(pos, moves); orderMoves(pos, moves.begin(), moves.end(), ttMove, ply); @@ -328,8 +393,7 @@ namespace ironphoenix { if (shouldStop()) break; - const bool terminalKingCapture = isTerminalKingCapture(pos, move); - const bool givesCheckMove = !terminalKingCapture && pos.givesCheck(move); + const bool terminalKingCapture = move.isCapture() && isTerminalKingCapture(pos, move); int score = -INF; bool legal = true; @@ -429,11 +493,11 @@ namespace ironphoenix { const bool ttHit = tt_.probe(pos.key(), ply, ttProbe); const Move ttMove = ttHit ? ttProbe.move : Move{}; - // Transposition Table (TT) Cutoffs: if the current position has been - // previously searched at a depth equal to or greater than the current - // depth, and the stored score is sufficient to cause a cutoff, return - // the stored score. This is only done at non-PV nodes, since PV nodes - // require a full search to find the best move. + // Transposition Table (TT) Cutoffs: if the current position has been + // previously searched at a depth equal to or greater than the current + // depth, and the stored score is sufficient to cause a cutoff, return + // the stored score. This is only done at non-PV nodes, since PV nodes + // require a full search to find the best move. if (!pvNode && ply > 0 && ttHit && ttProbe.depth >= depth) { if (ttProbe.bound == TTBound::Exact) return ttProbe.score; @@ -442,88 +506,125 @@ namespace ironphoenix { if (ttProbe.bound == TTBound::Upper && ttProbe.score <= alpha) return ttProbe.score; } - - const int staticEval = evaluate(pos); const bool inCheckNode = pos.inCheck(); const auto& tuning = Tuning::search(); const Color us = pos.sideToMove(); - const Color partner = - static_cast(static_cast(us) ^ 2u); - - const bool partnerInCheck = pos.inCheck(partner); - const bool mateWindow = !(alpha > -MATE_THRESHOLD && beta < MATE_THRESHOLD); - // Null Move Pruning (NMP): if the current player can effectively pass - // and the opposing team still cannot push the score below beta, normal - // moves are very likely to cause the same fail-high. Keep this away - // from PV nodes, checks, mate windows, pawn-only zugzwang positions, - // and consecutive null moves. + // Null Move Pruning (NMP): keep expensive partner-check detection and + // static evaluation completely off nodes where NMP cannot run anyway. if (allowNull && !pvNode && ply > 0 && depth >= NULL_MOVE_MIN_DEPTH - && staticEval >= beta && !inCheckNode - && !partnerInCheck && !mateWindow && hasNonPawnMaterial(pos, us)) { - StateInfo nullState; - pos.makeNullMove(nullState); - searchStack_[ply + 1] = HistoryContext{}; - - // Dynamic null-move reduction: reduce the depth more aggressively - // at deeper nodes and for higher static evaluations. - const int reduction = 4 + depth / 5 + std::min(3, (staticEval - beta) / NULL_MOVE_DIVISOR); - const int nullScore = -negamax( - pos, - depth - reduction, - -beta, - -beta + 1, - ply + 1, - false, - false - ); - - pos.undoNullMove(nullState); - - if (shouldStop()) - return 0; - - // Never manufacture a mate score from a synthetic pass. - if (nullScore >= beta && nullScore < MATE_THRESHOLD) - return nullScore; + const Color partner = static_cast(static_cast(us) ^ 2u); + if (!pos.inCheck(partner)) { + const int staticEval = evaluate(pos); + + if (staticEval >= beta) { + StateInfo nullState; + pos.makeNullMove(nullState); + searchStack_[ply + 1] = HistoryContext{}; + + // Dynamic null-move reduction: reduce the depth more aggressively + // at deeper nodes and for higher static evaluations. + const int reduction = 4 + depth / 5 + std::min(3, (staticEval - beta) / NULL_MOVE_DIVISOR); + const int nullScore = -negamax( + pos, + depth - reduction, + -beta, + -beta + 1, + ply + 1, + false, + false + ); + + pos.undoNullMove(nullState); + + if (shouldStop()) + return 0; + + // Never manufacture a mate score from a synthetic pass. + if (nullScore >= beta && nullScore < MATE_THRESHOLD) + return nullScore; + } + } } MoveList moves; generatePseudoLegalMoves(pos, moves); - orderMoves(pos, moves.begin(), moves.end(), ttMove, ply); + + std::array orderedScores; + std::array givesCheckBits; + + if (pvNode) { + // PV nodes are comparatively rare and benefit from complete ordering. + // Keep their existing full sort and cached check metadata. + orderMoves(pos, + moves.begin(), + moves.end(), + ttMove, + ply, + orderedScores.data(), + givesCheckBits.data()); + } + else { + // At cut nodes avoid sorting the entire move list and avoid eager + // givesCheck() calls. Score cheaply, then select the best few moves + // only as they are actually requested by the search loop. + for (std::size_t i = 0; i < moves.size; ++i) + orderedScores[i] = moveScore(pos, moves.moves[i], ttMove, ply, false); + } int legalMoves = 0; int bestScore = -INF; Move bestMove{}; - std::array failedQuiets{}; - std::array failedCaptures{}; + + // Quiet failures grow from the front and capture failures from the back, + // replacing two 512-Move arrays with one without changing history updates. + std::array failedMoves; std::size_t failedQuietCount = 0; std::size_t failedCaptureCount = 0; - for (Move move : moves) { + for (std::size_t moveIndex = 0; moveIndex < moves.size; ++moveIndex) { if (shouldStop()) break; + if (!pvNode && moveIndex < LAZY_PICK_LIMIT) { + std::size_t bestIndex = moveIndex; + int bestOrderScore = orderedScores[moveIndex]; + + for (std::size_t j = moveIndex + 1; j < moves.size; ++j) { + if (orderedScores[j] > bestOrderScore) { + bestOrderScore = orderedScores[j]; + bestIndex = j; + } + } + + if (bestIndex != moveIndex) { + std::swap(moves.moves[moveIndex], moves.moves[bestIndex]); + std::swap(orderedScores[moveIndex], orderedScores[bestIndex]); + } + } + + const Move move = moves.moves[moveIndex]; const Piece movingPiece = pos.pieceAt(move.from()); - const bool terminalKingCapture = isTerminalKingCapture(pos, move); - const bool givesCheckMove = !terminalKingCapture && pos.givesCheck(move); - const int lmrHistoryScore = (!move.isCapture() && !move.isPromotion()) - ? moveScore(pos, move, Move{}, ply) - : 0; + const bool terminalKingCapture = move.isCapture() && isTerminalKingCapture(pos, move); + const bool givesCheckMove = pvNode + ? cachedGivesCheck(givesCheckBits.data(), moveIndex) + : (!terminalKingCapture && pos.givesCheck(move)); + const bool quiet = !move.isCapture() && !move.isPromotion(); + const int lmrHistoryScore = quiet ? orderedScores[moveIndex] : 0; + int score = -INF; bool legal = true; - const bool quiet = !move.isCapture() && !move.isPromotion(); - const int mateWindow = !(alpha > -MATE_THRESHOLD && beta < MATE_THRESHOLD) + const bool moveMateWindow = !(alpha > -MATE_THRESHOLD && beta < MATE_THRESHOLD) && !terminalKingCapture; // Late Move Pruning (LMP): at non-PV nodes, stop spending search @@ -536,15 +637,15 @@ namespace ironphoenix { && quiet && move != ttMove && !givesCheckMove - && !mateWindow + && !moveMateWindow && lmrHistoryScore < LMP_HISTORY_THRESHOLD) { continue; } - // Static Exchange Evaluation (SEE) Pruning: at non-PV nodes, if a quiet - // capture move is likely to lose material, skip it. This is only done at - // shallow depths, since SEE is not always accurate in complex positions. - // SEE pruning is not applied to TT moves, promotions, checks, or mate-window searches. + // Static Exchange Evaluation (SEE) Pruning: at non-PV nodes, if a + // capture move is likely to lose material, skip it. This is only done + // at shallow depths, since SEE is not always accurate in complex positions. + // SEE pruning is not applied to TT moves, promotions, checks, or mate-window searches. if (!pvNode && !inCheckNode && depth <= tuning.seePruneMaxDepth @@ -553,7 +654,7 @@ namespace ironphoenix { && !move.isPromotion() && move != ttMove && !givesCheckMove - && !mateWindow) { + && !moveMateWindow) { const int seeThreshold = -tuning.seePruneMarginPerDepth * depth; if (!seeGE(pos, move, seeThreshold, SeeMode::Legal)) continue; @@ -634,24 +735,26 @@ namespace ironphoenix { if (cutoff) { if (!terminalKingCapture) { if (move.isCapture()) { + const Move* failedCaptures = failedMoves.data() + MAX_MOVES - failedCaptureCount; updateCaptureCutoff(us, move, movingPiece, depth, - failedCaptures.data(), failedCaptureCount, pos); + failedCaptures, failedCaptureCount, pos); } else if (!move.isPromotion()) { updateQuietCutoff(pos, us, move, movingPiece, depth, ply, - failedQuiets.data(), failedQuietCount); + failedMoves.data(), failedQuietCount); } } break; } - if (move.isCapture()) { - if (failedCaptureCount < failedCaptures.size()) - failedCaptures[failedCaptureCount++] = move; - } - else if (!move.isPromotion()) { - if (failedQuietCount < failedQuiets.size()) - failedQuiets[failedQuietCount++] = move; + if (failedQuietCount + failedCaptureCount < failedMoves.size()) { + if (move.isCapture()) { + ++failedCaptureCount; + failedMoves[MAX_MOVES - failedCaptureCount] = move; + } + else if (!move.isPromotion()) { + failedMoves[failedQuietCount++] = move; + } } } @@ -739,6 +842,7 @@ namespace ironphoenix { startTime_ = std::chrono::steady_clock::now(); timeBudgetMs_ = calculateTimeBudget(position, limits_); nodes_ = 0; + nextTimeCheckNode_ = 0; selDepth_ = 0; for (auto& row : pv_) row.fill(Move{}); diff --git a/src/see.cpp b/src/see.cpp index aeb5619..93a0678 100644 --- a/src/see.cpp +++ b/src/see.cpp @@ -49,6 +49,11 @@ IRONPHOENIX_FORCE_INLINE int promotionBonus(PieceType attacker, } IRONPHOENIX_FORCE_INLINE Piece actualCapturedPiece(const Position& pos, Move m) noexcept { + // Generated moves already carry the captured piece, including en-passant. + // Keep the position lookup only as a fallback for externally constructed moves. + if (m.captured() != NO_PIECE) + return m.captured(); + const Piece moving = pos.pieceAt(m.from()); if (moving == NO_PIECE) return NO_PIECE; @@ -113,7 +118,9 @@ int see(const Position& pos, Move m, SeeMode mode) noexcept { if (captured != NO_PIECE && typeOf(captured) == KING && enemy(us, colorOf(captured))) return pieceValue(KING); - std::array gain{}; + // Only entries up to depth are ever read, so avoid clearing the full array + // on every SEE invocation. + std::array gain; int depth = 0; gain[0] = captured == NO_PIECE ? 0 : pieceValue(captured); @@ -126,7 +133,7 @@ int see(const Position& pos, Move m, SeeMode mode) noexcept { Bitboard occ = pos.occupancy(); occ.reset(from); - const Square capSq = pos.capturedSquare(m, us); + const Square capSq = m.isEnPassant() ? pos.capturedSquare(m, us) : target; if (captured != NO_PIECE && capSq != SQ_NONE) occ.reset(capSq); occ.set(target); diff --git a/src/tt.cpp b/src/tt.cpp index 89fa445..8d03866 100644 --- a/src/tt.cpp +++ b/src/tt.cpp @@ -83,7 +83,6 @@ int TranspositionTable::scoreFromTT(int score, int ply) noexcept { } bool TranspositionTable::probe(Key key, int ply, TTProbe& out) const noexcept { - out = TTProbe{}; if (table_.empty()) return false; @@ -164,4 +163,4 @@ int TranspositionTable::hashfull() const noexcept { return total ? static_cast(used * 1000 / total) : 0; } -} +} \ No newline at end of file diff --git a/tests/bench.cpp b/tests/bench.cpp index 10ca1b4..102828b 100644 --- a/tests/bench.cpp +++ b/tests/bench.cpp @@ -21,7 +21,7 @@ int main() { const PieceType pt = static_cast(1 + ((s / 5) % 5)); p.placePiece(static_cast(s), makePiece(c, pt)); } - + p.placePiece(sq('g', 1), makePiece(RED, KING)); p.placePiece(sq('a', 7), makePiece(BLUE, KING)); p.placePiece(sq('g', 14), makePiece(YELLOW, KING)); @@ -30,14 +30,31 @@ int main() { constexpr std::uint64_t N = 20'000'000; volatile std::uint64_t sink = 0; - auto begin = std::chrono::steady_clock::now(); + + const Bitboard occ = p.occupancy(); + auto rayBegin = std::chrono::steady_clock::now(); for (std::uint64_t i = 0; i < N; ++i) { const Square s = static_cast((i * 37u) % SQUARE_NB); - const Bitboard a = Geometry::queenAttacks(s, p.occupancy()); + const Bitboard a = Geometry::queenAttacks(s, occ); sink = sink ^ a.w0 ^ a.w1 ^ a.w2; } - auto end = std::chrono::steady_clock::now(); - const double sec = std::chrono::duration(end - begin).count(); - std::cout << "queenAttacks calls/s: " << static_cast(N / sec) - << " sink=" << sink << "\n"; + auto rayEnd = std::chrono::steady_clock::now(); + const double raySec = std::chrono::duration(rayEnd - rayBegin).count(); + + auto lineBegin = std::chrono::steady_clock::now(); + for (std::uint64_t i = 0; i < N; ++i) { + const Square s = static_cast((i * 37u) % SQUARE_NB); + const Bitboard a = p.queenAttacks(s); + sink = sink ^ a.w0 ^ a.w1 ^ a.w2; + } + auto lineEnd = std::chrono::steady_clock::now(); + const double lineSec = std::chrono::duration(lineEnd - lineBegin).count(); + + const auto rayRate = static_cast(N / raySec); + const auto lineRate = static_cast(N / lineSec); + + std::cout << "ray queenAttacks calls/s: " << rayRate << '\n' + << "line queenAttacks calls/s: " << lineRate << '\n' + << "line/ray speedup: " << (raySec / lineSec) << "x\n" + << "sink=" << sink << '\n'; } diff --git a/tests/property.cpp b/tests/property.cpp index 4f7723c..8d46f9a 100644 --- a/tests/property.cpp +++ b/tests/property.cpp @@ -21,6 +21,20 @@ static Bitboard naiveRay(Square s, Direction d, Bitboard occ) { return out; } +static void verifyLineAttacks(const Position& p, std::mt19937_64& rng) { + const Bitboard occ = p.occupancy(); + + // Sample several squares per randomized position. This directly checks the + // compact line representation against the original arbitrary-occupancy ray + // implementation, including cross-board diagonal holes. + for (int sample = 0; sample < 16; ++sample) { + const Square sq = static_cast(rng() % SQUARE_NB); + assert(p.rookAttacks(sq) == Geometry::rookAttacks(sq, occ)); + assert(p.bishopAttacks(sq) == Geometry::bishopAttacks(sq, occ)); + assert(p.queenAttacks(sq) == Geometry::queenAttacks(sq, occ)); + } +} + int main() { Geometry::init(); std::mt19937_64 rng(0x4E45585553425043ULL); @@ -72,6 +86,8 @@ int main() { p.setRulesetId(static_cast(rng() & 0xFULL)); p.finalizeSetup(); assert(p.key() == p.recomputeKey()); + assert(p.verify()); + verifyLineAttacks(p, rng); // Null move must advance exactly one 4PC turn and then restore the // position byte-for-byte from the searcher's point of view. @@ -82,12 +98,15 @@ int main() { p.makeNullMove(nullState); assert(p.sideToMove() == nextColor(beforeNullSide)); assert(p.key() == p.recomputeKey()); + assert(p.verify()); + verifyLineAttacks(p, rng); p.undoNullMove(nullState); assert(p.sideToMove() == beforeNullSide); assert(p.checkers() == beforeNullCheckers); assert(p.key() == beforeNull); assert(p.key() == p.recomputeKey()); assert(p.verify()); + verifyLineAttacks(p, rng); Bitboard movers = p.occupancy(stm); if (!movers) @@ -106,10 +125,13 @@ int main() { Move m(from, to); p.makeMove(m, st); assert(p.key() == p.recomputeKey()); + assert(p.verify()); + verifyLineAttacks(p, rng); p.undoMove(m, st); assert(p.key() == before); assert(p.key() == p.recomputeKey()); assert(p.verify()); + verifyLineAttacks(p, rng); } std::cout << "iron_phoenix property tests: PASS\n";