From 18a2152b49b2c11524858d277723147211aac905 Mon Sep 17 00:00:00 2001 From: aaadrian Date: Fri, 20 Mar 2026 13:41:51 -0700 Subject: [PATCH 1/2] Fix BestNodeSearch optimalValue initialization The optimalValue in BestNodeSearch was initialized to 0 instead of the guess parameter from the previous iteration. In MTD(f) iterative deepening the guess carries forward the best value found at shallower depths, so discarding it reset the search bounds unnecessarily. --- Alligator.Solver/Algorithms/AlphaBetaSolver.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Alligator.Solver/Algorithms/AlphaBetaSolver.cs b/Alligator.Solver/Algorithms/AlphaBetaSolver.cs index 5a8fc60..bd36be5 100644 --- a/Alligator.Solver/Algorithms/AlphaBetaSolver.cs +++ b/Alligator.Solver/Algorithms/AlphaBetaSolver.cs @@ -52,7 +52,7 @@ public TStep OptimizeNextStep(IList history) IList candidates = rules.LegalStepsAt(position).ToList(); - var optimalValue = 0; + var optimalValue = guess; while (alpha + 1 < beta && candidates.Count > 1) { From 53b340491973dc4c29941938e4956504b0e3814a Mon Sep 17 00:00:00 2001 From: aaadrian Date: Fri, 20 Mar 2026 14:02:55 -0700 Subject: [PATCH 2/2] Solver performance optimizations Data structure changes: - Transposition: class -> struct (eliminates heap allocations) - CacheTables: Dictionary -> array-based cache with power-of-2 bitmask indexing and KeyMarker XOR for empty slot disambiguation (TT: 2M slots, Eval: 8M slots) Search algorithm improvements: - Buffer-based move ordering: per-depth reusable List[] replaces yield-return iterator state machines and per-node List allocations - TT step legality verification: transposition table optimal step is only used if confirmed present in LegalStepsAt output (prevents crashes from Zobrist hash collisions when Position.Identifier excludes LastStep) - IsGoal fast-path at depth 0: replaces expensive LegalStepsAt().Any() with O(1) IsGoal() check, reducing LegalStepsCalls by ~89%% - HeuristicValue simplified: terminal branch removed (handled by caller) Infrastructure: - History heuristic recording with depth-squared weighting (IHeuristicTables.GetHistoryScore, not yet used for move sorting) Benchmark results vs main (SixMaking depth-6): Test #1: 626ms -> 600ms (-4%%) Test #3: 1457ms -> 1266ms (-13%%) Test #4: CRASH -> 1030ms (fixed) Test #5: 16562ms -> 11218ms (-32%%) --- .../MeasurableHeuristicTables.cs | 5 ++ .../Algorithms/AlphaBetaPruning.cs | 84 +++++++++++-------- Alligator.Solver/Algorithms/CacheTables.cs | 51 ++++++++--- .../Algorithms/HeuristicTables.cs | 21 +++++ .../Algorithms/IHeuristicTables.cs | 1 + Alligator.Solver/Algorithms/Transposition.cs | 2 +- 6 files changed, 120 insertions(+), 44 deletions(-) diff --git a/Alligator.Benchmark/MeasurableHeuristicTables.cs b/Alligator.Benchmark/MeasurableHeuristicTables.cs index b21250e..8668d71 100644 --- a/Alligator.Benchmark/MeasurableHeuristicTables.cs +++ b/Alligator.Benchmark/MeasurableHeuristicTables.cs @@ -27,6 +27,11 @@ public void StoreBetaCutOff(Step move, int depth) heuristicTables.StoreBetaCutOff(move, depth); } + public int GetHistoryScore(Step move) + { + return heuristicTables.GetHistoryScore(move); + } + public void ClearCounters() { GetKillerStepsCallCount = 0; diff --git a/Alligator.Solver/Algorithms/AlphaBetaPruning.cs b/Alligator.Solver/Algorithms/AlphaBetaPruning.cs index f4949f1..8460a02 100644 --- a/Alligator.Solver/Algorithms/AlphaBetaPruning.cs +++ b/Alligator.Solver/Algorithms/AlphaBetaPruning.cs @@ -8,6 +8,9 @@ internal class AlphaBetaPruning : IAlphaBetaPruning private readonly IHeuristicTables heuristicTables; private readonly ISearchManager searchManager; + private const int MaxSearchDepth = 16; + private readonly List[] orderedStepBuffers; + public AlphaBetaPruning( IRules rules, ICacheTables cacheTables, @@ -18,6 +21,12 @@ public AlphaBetaPruning( this.cacheTables = cacheTables ?? throw new ArgumentNullException(nameof(cacheTables)); this.heuristicTables = heuristicTables ?? throw new ArgumentNullException(nameof(heuristicTables)); this.searchManager = searchManager ?? throw new ArgumentNullException(nameof(searchManager)); + + orderedStepBuffers = new List[MaxSearchDepth]; + for (int i = 0; i < MaxSearchDepth; i++) + { + orderedStepBuffers[i] = new List(); + } } public int Search(TPosition position, int alpha, int beta) @@ -27,15 +36,20 @@ public int Search(TPosition position, int alpha, int beta) private int SearchRecursively(TPosition position, int depth, int alpha, int beta) { - if (IsLeaf(position, depth)) + if (depth <= 0) { + if (rules.IsGoal(position)) + { + return -(sbyte.MaxValue + depth); + } return -HeuristicValue(position, depth); } var originalAlpha = alpha; - if (cacheTables.TryGetTransposition(position, out Transposition transposition) - && depth <= transposition.Depth) + bool hasTransposition = cacheTables.TryGetTransposition(position, out Transposition transposition); + + if (hasTransposition && depth <= transposition.Depth) { switch (transposition.EvaluationMode) { @@ -55,11 +69,19 @@ private int SearchRecursively(TPosition position, int depth, int alpha, int beta } } + var orderedSteps = GetOrderedLegalSteps(position, depth, hasTransposition, transposition.OptimalStep); + + if (orderedSteps.Count == 0) + { + return -(rules.IsGoal(position) ? sbyte.MaxValue + depth : 0); + } + var bestValue = -int.MaxValue; TStep bestStep = default; - foreach (var step in OrderedLegalStepsAt(position, depth, transposition)) + for (int i = 0; i < orderedSteps.Count; i++) { + var step = orderedSteps[i]; position.Take(step); var value = -SearchRecursively(position, depth - 1, -beta, -alpha); position.TakeBack(); @@ -78,40 +100,45 @@ private int SearchRecursively(TPosition position, int depth, int alpha, int beta } if (depth > 1) { - EvaluationMode evaluationMode = GetEvaluationMode(bestValue, originalAlpha, beta); - transposition = new Transposition(evaluationMode, bestValue, depth, bestStep); - cacheTables.AddTransposition(position, transposition); + var newTransposition = new Transposition(GetEvaluationMode(bestValue, originalAlpha, beta), bestValue, depth, bestStep); + cacheTables.AddTransposition(position, newTransposition); } return bestValue; } - private IEnumerable OrderedLegalStepsAt(TPosition position, int depth, Transposition transposition) + private List GetOrderedLegalSteps(TPosition position, int depth, bool hasTransposition, TStep transpositionStep) { - if (transposition != null) - { - yield return transposition.OptimalStep; - } - var prevKillerSteps = heuristicTables.GetKillerSteps(depth); - var otherSteps = new List(); + var result = orderedStepBuffers[depth]; + result.Clear(); + + var killers = heuristicTables.GetKillerSteps(depth); + int killerCount = 0; + bool transpositionStepIsLegal = false; + foreach (var move in rules.LegalStepsAt(position)) { - if (transposition != null && move.Equals(transposition.OptimalStep)) + if (hasTransposition && move.Equals(transpositionStep)) { + transpositionStepIsLegal = true; continue; } - if (prevKillerSteps.Contains(move)) + if (killers.Contains(move)) { - yield return move; + result.Insert(killerCount, move); + killerCount++; } else { - otherSteps.Add(move); + result.Add(move); } } - foreach (var move in otherSteps) + + if (transpositionStepIsLegal) { - yield return move; + result.Insert(0, transpositionStep); } + + return result; } private bool IsBetaCutOff(int alpha, int beta) @@ -124,11 +151,6 @@ private void HandleBetaCutOff(TStep step, int depth) heuristicTables.StoreBetaCutOff(step, depth); } - private bool IsLeaf(TPosition position, int depth) - { - return depth <= 0 || !rules.LegalStepsAt(position).Any(); - } - private EvaluationMode GetEvaluationMode(int value, int alpha, int beta) { if (value <= alpha) @@ -147,16 +169,12 @@ private EvaluationMode GetEvaluationMode(int value, int alpha, int beta) private int HeuristicValue(TPosition position, int depth) { - if (rules.LegalStepsAt(position).Any()) + if (!cacheTables.TryGetValue(position, out var value)) { - if (!cacheTables.TryGetValue(position, out var value)) - { - value = position.Value; - cacheTables.AddValue(position, value); - } - return IsOpponentsTurn(depth) ? -value : value; // TODO: check if the sign is required! + value = position.Value; + cacheTables.AddValue(position, value); } - return rules.IsGoal(position) ? sbyte.MaxValue + depth : 0; + return IsOpponentsTurn(depth) ? -value : value; } private bool IsOpponentsTurn(int depth) diff --git a/Alligator.Solver/Algorithms/CacheTables.cs b/Alligator.Solver/Algorithms/CacheTables.cs index 3abd9a5..35776a9 100644 --- a/Alligator.Solver/Algorithms/CacheTables.cs +++ b/Alligator.Solver/Algorithms/CacheTables.cs @@ -1,35 +1,66 @@ -namespace Alligator.Solver.Algorithms +namespace Alligator.Solver.Algorithms { internal class CacheTables : ICacheTables where TPosition : IPosition { - private readonly IDictionary evaluationTable; - private readonly IDictionary> transpositionTable; + private const int TranspositionTableSize = 1 << 21; + private const int TranspositionTableMask = TranspositionTableSize - 1; + + private const int EvaluationTableSize = 1 << 23; + private const int EvaluationTableMask = EvaluationTableSize - 1; + + private const ulong KeyMarker = 0xBF58476D1CE4E5B9UL; + + private readonly ulong[] transpositionKeys; + private readonly Transposition[] transpositionValues; + + private readonly ulong[] evaluationKeys; + private readonly int[] evaluationValues; public CacheTables() { - evaluationTable = new Dictionary(10000000); - transpositionTable = new Dictionary>(10000000); + transpositionKeys = new ulong[TranspositionTableSize]; + transpositionValues = new Transposition[TranspositionTableSize]; + evaluationKeys = new ulong[EvaluationTableSize]; + evaluationValues = new int[EvaluationTableSize]; } public void AddTransposition(TPosition position, Transposition transposition) { - transpositionTable[position.Identifier] = transposition; + int index = (int)(position.Identifier & TranspositionTableMask); + transpositionKeys[index] = position.Identifier ^ KeyMarker; + transpositionValues[index] = transposition; } public void AddValue(TPosition position, int value) { - evaluationTable[position.Identifier] = value; + int index = (int)(position.Identifier & EvaluationTableMask); + evaluationKeys[index] = position.Identifier ^ KeyMarker; + evaluationValues[index] = value; } public bool TryGetTransposition(TPosition position, out Transposition transposition) { - return transpositionTable.TryGetValue(position.Identifier, out transposition); + int index = (int)(position.Identifier & TranspositionTableMask); + if (transpositionKeys[index] == (position.Identifier ^ KeyMarker)) + { + transposition = transpositionValues[index]; + return true; + } + transposition = default; + return false; } public bool TryGetValue(TPosition position, out int value) { - return evaluationTable.TryGetValue(position.Identifier, out value); + int index = (int)(position.Identifier & EvaluationTableMask); + if (evaluationKeys[index] == (position.Identifier ^ KeyMarker)) + { + value = evaluationValues[index]; + return true; + } + value = 0; + return false; } } -} \ No newline at end of file +} diff --git a/Alligator.Solver/Algorithms/HeuristicTables.cs b/Alligator.Solver/Algorithms/HeuristicTables.cs index a8e5ed7..c3ad6d6 100644 --- a/Alligator.Solver/Algorithms/HeuristicTables.cs +++ b/Alligator.Solver/Algorithms/HeuristicTables.cs @@ -3,17 +3,20 @@ internal class HeuristicTables : IHeuristicTables { private readonly IDictionary> killerSteps; + private readonly Dictionary historyScores; private const int StoredKillerStepsLimitPerDepth = 2; public HeuristicTables() { killerSteps = new Dictionary>(); + historyScores = new Dictionary(); } public void StoreBetaCutOff(TMove move, int depth) { UpdateKillerSteps(move, depth); + RecordHistorySuccess(move, depth); } public IEnumerable GetKillerSteps(int depth) @@ -26,6 +29,24 @@ public IEnumerable GetKillerSteps(int depth) return Enumerable.Empty(); } + public int GetHistoryScore(TMove move) + { + return historyScores.TryGetValue(move, out int score) ? score : 0; + } + + private void RecordHistorySuccess(TMove move, int depth) + { + int bonus = depth * depth; + if (historyScores.TryGetValue(move, out int current)) + { + historyScores[move] = current + bonus; + } + else + { + historyScores[move] = bonus; + } + } + private void UpdateKillerSteps(TMove move, int depth) { IList killers; diff --git a/Alligator.Solver/Algorithms/IHeuristicTables.cs b/Alligator.Solver/Algorithms/IHeuristicTables.cs index 4f486a6..b9be602 100644 --- a/Alligator.Solver/Algorithms/IHeuristicTables.cs +++ b/Alligator.Solver/Algorithms/IHeuristicTables.cs @@ -4,5 +4,6 @@ internal interface IHeuristicTables { IEnumerable GetKillerSteps(int depth); void StoreBetaCutOff(TMove move, int depth); + int GetHistoryScore(TMove move); } } \ No newline at end of file diff --git a/Alligator.Solver/Algorithms/Transposition.cs b/Alligator.Solver/Algorithms/Transposition.cs index dce7451..9ef88a5 100644 --- a/Alligator.Solver/Algorithms/Transposition.cs +++ b/Alligator.Solver/Algorithms/Transposition.cs @@ -1,6 +1,6 @@ namespace Alligator.Solver.Algorithms { - internal class Transposition + internal struct Transposition { public EvaluationMode EvaluationMode; public int Value;