diff --git a/pkg/board.go b/pkg/board.go index 7decc7b..511a951 100644 --- a/pkg/board.go +++ b/pkg/board.go @@ -371,6 +371,17 @@ func (board *Board) IsPieceAtSquareWhite(square byte) bool { return (board.WhitePawns|board.WhiteKnights|board.WhiteBishops|board.WhiteRooks|board.WhiteQueens|board.WhiteKing)&mask != 0 } +// IsInCheck returns true if the side to move is in check. +func (board *Board) IsInCheck(whiteToMove bool) bool { + var kingSq byte + if whiteToMove { + kingSq = byte(bits.TrailingZeros64(board.WhiteKing)) + } else { + kingSq = byte(bits.TrailingZeros64(board.BlackKing)) + } + return board.IsSquareAttacked(kingSq, whiteToMove) +} + // SquareToRank returns the rank (0-7) of a square index (0 = rank 0, 7 = rank 7) func (board *Board) SquareToRank(square byte) byte { return square / 8 diff --git a/pkg/move.go b/pkg/move.go index 497f9fc..59748ca 100644 --- a/pkg/move.go +++ b/pkg/move.go @@ -260,6 +260,11 @@ func (board *Board) Move(move Move) MoveState { board.Castling.BlackQueenSide = false } } + /** Defensive: king capture is illegal. + if move.Captured == WhiteKing || move.Captured == BlackKing { + panic(fmt.Sprintf("Illegal move: %s captures the king", move.ToMove())) + } + */ } board.WhiteToMove = !board.WhiteToMove diff --git a/pkg/search.go b/pkg/search.go index c057003..a84b80e 100644 --- a/pkg/search.go +++ b/pkg/search.go @@ -10,6 +10,8 @@ const ( SearchMaxDepth = 16 // Maximum depth to search MaxEvaluationScore = 1_000_000 // Maximum score for wining MaxEvaluationTimeMs = 3_000 // Maximum time for a search at the root level + NullMoveReduction = 2 // Reduction for null move pruning (R) + NullMoveMinDepth = 3 // Minimum depth for null mov ) type SearchOptions struct { @@ -187,6 +189,15 @@ func (board *Board) ParallelRootSearch(depth int, tt *TranspositionTable, moves } func (board *Board) AlphaBetaSearch(depth int, maximizing bool, alpha int, beta int, tt *TranspositionTable, stats *SearchResult, ctx *SearchContext, ply int) int { + /* + defer func() { + if r := recover(); r != nil { + stack := debug.Stack() + fmt.Printf("PANIC in AlphaBetaSearch: %v\nStacktrace:\n%s\nContext: depth=%d maximizing=%v alpha=%d beta=%d ply=%d board=%v\n", r, stack, depth, maximizing, alpha, beta, ply, board) + panic(r) + } + }() + */ // Check for cancellation at every node select { case <-ctx.Done: @@ -218,6 +229,29 @@ func (board *Board) AlphaBetaSearch(depth int, maximizing bool, alpha int, beta return board.MateOrStalemateScore(maximizing) } + // --- Null Move Pruning --- + if depth >= NullMoveMinDepth && ply > 0 && !board.IsInCheck(board.WhiteToMove) && len(board.GenerateLegalMoves()) > 0 { + nullBoard := board.Clone() + nullBoard.WhiteToMove = !nullBoard.WhiteToMove // Switch side to move (null move) + nullEval := 0 + newDepth := depth - NullMoveReduction - 1 + if newDepth < 1 { + newDepth = 1 + } + if maximizing { + nullEval = nullBoard.AlphaBetaSearch(newDepth, false, alpha, beta, tt, stats, ctx, ply+1) + if nullEval >= beta { + return beta // Fail-hard beta cutoff + } + } else { + nullEval = nullBoard.AlphaBetaSearch(newDepth, true, alpha, beta, tt, stats, ctx, ply+1) + if nullEval <= alpha { + return alpha // Fail-hard alpha cutoff + } + } + } + + // --- Alpha-Beta Pruning --- var result int var bestMove Move if maximizing {