Skip to content

Commit 4b8d8f8

Browse files
authored
Merge pull request #32 from boraaros/feature/time-based-search
Feature/time based search
2 parents 3dbf3f3 + 5e6fd7f commit 4b8d8f8

8 files changed

Lines changed: 97 additions & 25 deletions

File tree

Alligator.ConnectFour/Configuration.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ namespace Alligator.ConnectFour
44
{
55
internal class Configuration : IConfiguration
66
{
7-
public int MaxDepth => 13;
7+
public int MaxDepth => 43;
8+
public TimeSpan? TimeBudget => TimeSpan.FromSeconds(5);
89
}
910
}

Alligator.Solver/Algorithms/AlphaBetaPruning.cs

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ internal class AlphaBetaPruning<TPosition, TStep> : IAlphaBetaPruning<TPosition>
88
private readonly IHeuristicTables<TStep> heuristicTables;
99
private readonly ISearchManager searchManager;
1010

11-
private const int MaxSearchDepth = 16;
1211
private readonly List<TStep>[] orderedStepBuffers;
1312

1413
public AlphaBetaPruning(
@@ -22,8 +21,8 @@ public AlphaBetaPruning(
2221
this.heuristicTables = heuristicTables ?? throw new ArgumentNullException(nameof(heuristicTables));
2322
this.searchManager = searchManager ?? throw new ArgumentNullException(nameof(searchManager));
2423

25-
orderedStepBuffers = new List<TStep>[MaxSearchDepth];
26-
for (int i = 0; i < MaxSearchDepth; i++)
24+
orderedStepBuffers = new List<TStep>[searchManager.MaxDepth];
25+
for (int i = 0; i < searchManager.MaxDepth; i++)
2726
{
2827
orderedStepBuffers[i] = new List<TStep>();
2928
}
@@ -36,11 +35,17 @@ public int Search(TPosition position, int alpha, int beta)
3635

3736
private int SearchRecursively(TPosition position, int depth, int alpha, int beta)
3837
{
38+
searchManager.CheckTimeBudget();
39+
if (searchManager.IsAborted)
40+
{
41+
return 0;
42+
}
43+
3944
if (depth <= 0)
4045
{
4146
if (rules.IsGoal(position))
4247
{
43-
return -(sbyte.MaxValue + depth);
48+
return -WinValue(depth);
4449
}
4550
return -HeuristicValue(position, depth);
4651
}
@@ -73,7 +78,7 @@ private int SearchRecursively(TPosition position, int depth, int alpha, int beta
7378

7479
if (orderedSteps.Count == 0)
7580
{
76-
return -(rules.IsGoal(position) ? sbyte.MaxValue + depth : 0);
81+
return -(rules.IsGoal(position) ? WinValue(depth) : 0);
7782
}
7883

7984
var bestValue = -int.MaxValue;
@@ -98,7 +103,7 @@ private int SearchRecursively(TPosition position, int depth, int alpha, int beta
98103
break;
99104
}
100105
}
101-
if (depth > 1)
106+
if (depth > 1 && !searchManager.IsAborted)
102107
{
103108
var newTransposition = new Transposition<TStep>(GetEvaluationMode(bestValue, originalAlpha, beta), bestValue, depth, bestStep);
104109
cacheTables.AddTransposition(position, newTransposition);
@@ -182,5 +187,12 @@ private bool IsOpponentsTurn(int depth)
182187
int distanceFromRoot = searchManager.DepthLimit - depth;
183188
return distanceFromRoot % 2 != 0;
184189
}
190+
191+
// Distance-from-root makes this value independent of DepthLimit (important for TT consistency)
192+
private int WinValue(int depth)
193+
{
194+
int distanceFromRoot = searchManager.DepthLimit - depth;
195+
return sbyte.MaxValue + searchManager.MaxDepth - distanceFromRoot;
196+
}
185197
}
186198
}

Alligator.Solver/Algorithms/AlphaBetaSolver.cs

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,20 +9,17 @@ internal class AlphaBetaSolver<TPosition, TStep> : ISolver<TStep>
99
private readonly IRules<TPosition, TStep> rules;
1010
private readonly ISearchManager searchManager;
1111
private readonly Action<string> logger;
12-
private readonly int maxDepth;
1312

1413
public AlphaBetaSolver(
1514
AlphaBetaPruning<TPosition, TStep> alphaBetaPruning,
1615
IRules<TPosition, TStep> rules,
1716
ISearchManager searchManager,
18-
Action<string> logger,
19-
int maxDepth)
17+
Action<string> logger)
2018
{
2119
this.alphaBetaPruning = alphaBetaPruning ?? throw new ArgumentNullException(nameof(alphaBetaPruning));
2220
this.rules = rules ?? throw new ArgumentNullException(nameof(rules));
2321
this.searchManager = searchManager ?? throw new ArgumentNullException(nameof(searchManager));
2422
this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
25-
this.maxDepth = maxDepth;
2623
}
2724

2825
public TStep OptimizeNextStep(IList<TStep> history)
@@ -32,12 +29,20 @@ public TStep OptimizeNextStep(IList<TStep> history)
3229
var position = CreatePosition(history);
3330

3431
TStep? bestStep = default;
32+
searchManager.StartSearch();
3533
var guess = 0;
3634

37-
for (int i = 2; i < maxDepth; i += 2)
35+
for (int i = 2; i < searchManager.MaxDepth; i += 2)
3836
{
3937
searchManager.DepthLimit = i;
4038
var (OptimalSteps, Value) = BestNodeSearch(position, guess);
39+
40+
if (searchManager.IsAborted)
41+
{
42+
logger($"Search aborted at depth {i} after {sw.ElapsedMilliseconds} ms");
43+
break;
44+
}
45+
4146
bestStep = OptimalSteps.First();
4247
guess = Value;
4348
logger($"Iteration has been completed in {sw.ElapsedMilliseconds} ms (value: {Value}, step: {bestStep}, depth: {i})");
@@ -48,8 +53,9 @@ public TStep OptimizeNextStep(IList<TStep> history)
4853

4954
private (ICollection<TStep> OptimalSteps, int Value) BestNodeSearch(TPosition position, int guess)
5055
{
51-
int alpha = -sbyte.MaxValue - maxDepth;
52-
int beta = sbyte.MaxValue + maxDepth;
56+
int winScore = sbyte.MaxValue + searchManager.MaxDepth;
57+
int alpha = -winScore;
58+
int beta = winScore;
5359

5460
IList<TStep> candidates = rules.LegalStepsAt(position).ToList();
5561

@@ -61,6 +67,11 @@ public TStep OptimizeNextStep(IList<TStep> history)
6167

6268
foreach (var move in candidates)
6369
{
70+
if (searchManager.IsAborted)
71+
{
72+
break;
73+
}
74+
6475
if (newCandidates.Count > 1)
6576
{
6677
newCandidates.Add(move);
@@ -75,6 +86,11 @@ public TStep OptimizeNextStep(IList<TStep> history)
7586
}
7687
}
7788

89+
if (searchManager.IsAborted)
90+
{
91+
break;
92+
}
93+
7894
if (newCandidates.Count > 0)
7995
{
8096
optimalValue = guess;

Alligator.Solver/Algorithms/ISearchManager.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,11 @@
22
{
33
internal interface ISearchManager
44
{
5+
int MaxDepth { get; }
56
int DepthLimit { get; set; }
7+
bool IsAborted { get; }
8+
9+
void StartSearch();
10+
void CheckTimeBudget();
611
}
712
}
Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,42 @@
1-
namespace Alligator.Solver.Algorithms
1+
using System.Diagnostics;
2+
3+
namespace Alligator.Solver.Algorithms
24
{
35
internal class SearchManager : ISearchManager
46
{
7+
private const int TimeBudgetCheckInterval = 1024;
8+
9+
private readonly Stopwatch stopwatch = new();
10+
private readonly TimeSpan timeBudget;
11+
private int nodeCount;
12+
13+
public int MaxDepth { get; }
514
public int DepthLimit { get; set; }
15+
public bool IsAborted { get; private set; }
16+
17+
public SearchManager(int maxDepth, TimeSpan? timeBudget = null)
18+
{
19+
MaxDepth = maxDepth;
20+
this.timeBudget = timeBudget ?? TimeSpan.Zero;
21+
}
22+
23+
public void StartSearch()
24+
{
25+
nodeCount = 0;
26+
IsAborted = false;
27+
if (timeBudget > TimeSpan.Zero)
28+
{
29+
stopwatch.Restart();
30+
}
31+
}
632

7-
public SearchManager(int depthLimit)
33+
public void CheckTimeBudget()
834
{
9-
DepthLimit = depthLimit;
35+
if (timeBudget > TimeSpan.Zero && ++nodeCount % TimeBudgetCheckInterval == 0
36+
&& stopwatch.Elapsed >= timeBudget)
37+
{
38+
IsAborted = true;
39+
}
1040
}
1141
}
1242
}

Alligator.Solver/IConfiguration.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,5 +10,12 @@ public interface IConfiguration
1010
/// The solver searches at even depths: 2, 4, 6, ... up to the largest even less than this value.
1111
/// </summary>
1212
int MaxDepth => 7;
13+
14+
/// <summary>
15+
/// Optional time budget per move. When set, the solver aborts mid-search
16+
/// once the budget is exceeded and returns the best result found so far.
17+
/// When null, only MaxDepth limits the search.
18+
/// </summary>
19+
TimeSpan? TimeBudget => null;
1320
}
1421
}

Alligator.Solver/SolverProvider.cs

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -41,14 +41,15 @@ public ISolver<TStep> Create()
4141

4242
internal ISolver<TStep> Create(ICacheTables<TPosition, TStep> cacheTables, IHeuristicTables<TStep> heuristicTables)
4343
{
44-
var searchManager = new SearchManager(6); // TODO: remove this ctor parameter
44+
var searchManager = new SearchManager(
45+
solverConfiguration.MaxDepth,
46+
solverConfiguration.TimeBudget);
4547

4648
return new AlphaBetaSolver<TPosition, TStep>(
47-
new AlphaBetaPruning<TPosition, TStep>(rules, cacheTables, heuristicTables, searchManager),
48-
rules,
49-
searchManager,
50-
logger,
51-
solverConfiguration.MaxDepth);
49+
new AlphaBetaPruning<TPosition, TStep>(rules, cacheTables, heuristicTables, searchManager),
50+
rules,
51+
searchManager,
52+
logger);
5253
}
5354
}
5455
}

Alligator.Test/SolverTests.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,11 @@ private static int Solve(TreeNode root, int maxDepth = 7)
1616
var rules = new TreeRules(root);
1717
var cacheTables = new CacheTables<TreePosition, int>();
1818
var heuristicTables = new HeuristicTables<int>();
19-
var searchManager = new SearchManager(maxDepth - 1);
19+
var searchManager = new SearchManager(maxDepth);
2020
var alphaBeta = new AlphaBetaPruning<TreePosition, int>(
2121
rules, cacheTables, heuristicTables, searchManager);
2222
var solver = new AlphaBetaSolver<TreePosition, int>(
23-
alphaBeta, rules, searchManager, _ => { }, maxDepth);
23+
alphaBeta, rules, searchManager, _ => { });
2424

2525
return solver.OptimizeNextStep([]);
2626
}

0 commit comments

Comments
 (0)