Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions Alligator.Test/MiniMaxTree/TreeNode.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
namespace Alligator.Test.MiniMaxTree
{
internal class TreeNode
{
public sbyte? LeafValue { get; }
public bool IsGoal { get; }
public List<TreeNode> Children { get; } = [];

private TreeNode(sbyte? leafValue, bool isGoal, IEnumerable<TreeNode> children)
{
LeafValue = leafValue;
IsGoal = isGoal;
Children.AddRange(children);
}

public bool IsLeaf => Children.Count == 0;

public static TreeNode Leaf(sbyte value) => new(value, isGoal: false, []);

public static TreeNode Goal() => new(leafValue: null, isGoal: true, []);

public static TreeNode Inner(params TreeNode[] children)
{
if (children.Length == 0)
throw new ArgumentException("Inner node must have at least one child.");
return new(leafValue: null, isGoal: false, children);
}
}
}
36 changes: 36 additions & 0 deletions Alligator.Test/MiniMaxTree/TreePosition.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
using Alligator.Solver;

namespace Alligator.Test.MiniMaxTree
{
internal class TreePosition : IPosition<int>
{
private readonly Stack<(TreeNode Node, int Step, ulong PrevIdentifier)> path = new();
private TreeNode current;

public TreePosition(TreeNode root)
{
current = root ?? throw new ArgumentNullException(nameof(root));
}

public TreeNode Current => current;

public ulong Identifier { get; private set; }

public sbyte Value => current.LeafValue ?? 0;

public void Take(int step)
{
path.Push((current, step, Identifier));
if (!current.IsLeaf)
current = current.Children[step];
Identifier = unchecked(Identifier * 7919 + (ulong)step + 1);
}

public void TakeBack()
{
var (parent, _, prevId) = path.Pop();
current = parent;
Identifier = prevId;
}
}
}
38 changes: 38 additions & 0 deletions Alligator.Test/MiniMaxTree/TreeRules.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
using Alligator.Solver;

namespace Alligator.Test.MiniMaxTree
{
internal class TreeRules : IRules<TreePosition, int>
{
private readonly TreeNode root;

public TreeRules(TreeNode root)
{
this.root = root ?? throw new ArgumentNullException(nameof(root));
}

public TreePosition InitialPosition()
{
return new TreePosition(root);
}

public IEnumerable<int> LegalStepsAt(TreePosition position)
{
if (position.Current.IsLeaf)
{
if (!position.Current.IsGoal)
yield return 0;
yield break;
}
for (int i = 0; i < position.Current.Children.Count; i++)
{
yield return i;
}
}

public bool IsGoal(TreePosition position)
{
return position.Current.IsGoal;
}
}
}
246 changes: 211 additions & 35 deletions Alligator.Test/SolverTests.cs
Original file line number Diff line number Diff line change
@@ -1,48 +1,224 @@
using Alligator.Solver.Algorithms;
using Alligator.Test.MiniMaxTree;
using static Alligator.Test.MiniMaxTree.TreeNode;

namespace Alligator.Test
{
[TestClass]
public class SolverTests
{
[TestMethod]
public void FirstSolverTest()
{
// Arrange
var valuesById = new Dictionary<ulong, sbyte>
{
{ 0, 0 },
{ 1, 0 },
{ 2, 0 },
{ 3, 3 },
{ 4, 1 },
{ 5, 2 },
{ 6, 0 },
{ 7, 0 },
{ 8, 0 },
{ 9, 0 },
{ 10, 2 },
{ 11, 1 },
{ 12, 3 },
{ 13, 0 },
{ 14, 3 },
{ 15, 0 }
};
uint degree = 2;
int depth = 2;

var rules = new UniformTreeRules(valuesById, degree);
var cacheTables = new CacheTables<UniformTreePosition, int>();
/// <summary>
/// Runs the full solver (iterative deepening + MTD(f)) on the given tree
/// and returns the index of the root's chosen child.
/// </summary>
private static int Solve(TreeNode root, int maxDepth = 7)
{
var rules = new TreeRules(root);
var cacheTables = new CacheTables<TreePosition, int>();
var heuristicTables = new HeuristicTables<int>();
var searchManager = new SearchManager(depth - 1);
var alphabeta = new AlphaBetaPruning<UniformTreePosition, int>(rules, cacheTables, heuristicTables, searchManager);
var solver = new AlphaBetaSolver<UniformTreePosition, int>(alphabeta, rules, searchManager, (logMsg) => { });
var searchManager = new SearchManager(maxDepth - 1);
var alphaBeta = new AlphaBetaPruning<TreePosition, int>(
rules, cacheTables, heuristicTables, searchManager);
var solver = new AlphaBetaSolver<TreePosition, int>(
alphaBeta, rules, searchManager, _ => { });

return solver.OptimizeNextStep([]);
}

//
// root (max)
// / \
// A(min) B(min)
// / \ / \
// [3] [-1] [2] [5]
//
// A = min(3, -1) = -1
// B = min(2, 5) = 2
// root = max(-1, 2) = 2 → step 1 (B)
//
[TestMethod]
public void Simple_two_level_tree_picks_optimal_branch()
{
var tree = Inner(
Inner(Leaf(3), Leaf(-1)),
Inner(Leaf(2), Leaf(5)));

Assert.AreEqual(1, Solve(tree));
}

//
// root (max)
// / \
// A(min) B(min)
// / \ / \
// [5] [3] [1] [3]
//
// A = min(5, 3) = 3
// B = min(1, 3) = 1
// root = max(3, 1) = 3 → step 0 (A)
//
[TestMethod]
public void Symmetric_minimax_values_picks_left()
{
var tree = Inner(
Inner(Leaf(5), Leaf(3)),
Inner(Leaf(1), Leaf(3)));

Assert.AreEqual(0, Solve(tree));
}

//
// root (max)
// / \
// A(min) B(min)
// / \ / \
// [4] [4] [4] [4]
//
// Both branches equal → either is acceptable.
//
[TestMethod]
public void Equal_branches_does_not_crash()
{
var tree = Inner(
Inner(Leaf(4), Leaf(4)),
Inner(Leaf(4), Leaf(4)));

int step = Solve(tree);
Assert.IsTrue(step == 0 || step == 1);
}

//
// root (max)
// / | \
// A B C
// / \ / \ /|\
// [1] [9] [5][3] [2][7][4]
//
// A = min(1, 9) = 1
// B = min(5, 3) = 3
// C = min(2, 7, 4) = 2
// root = max(1, 3, 2) = 3 → step 1 (B)
//
[TestMethod]
public void Uneven_branching_factor()
{
var tree = Inner(
Inner(Leaf(1), Leaf(9)),
Inner(Leaf(5), Leaf(3)),
Inner(Leaf(2), Leaf(7), Leaf(4)));

Assert.AreEqual(1, Solve(tree));
}

//
// root (max)
// / \
// A (min) B (min)
// / \ / \
// A1 A2 B1 B2
// / \ / \ / \ / \
// [1] [8][5][2] [7][3] [6][4]
//
// A1 = min(1,8)=1, A2 = min(5,2)=2 → A = max(1,2) = 2
// B1 = min(7,3)=3, B2 = min(6,4)=4 → B = max(3,4) = 4
// root = max(2, 4) = 4 → step 1 (B)
//
// Depth-2 iteration would see different values, depth-4 corrects.
//
[TestMethod]
public void Four_level_tree_requires_deep_search()
{
var tree = Inner(
Inner(
Inner(Leaf(1), Leaf(8)),
Inner(Leaf(5), Leaf(2))),
Inner(
Inner(Leaf(7), Leaf(3)),
Inner(Leaf(6), Leaf(4))));

Assert.AreEqual(1, Solve(tree));
}

// Act
var bestStep = solver.OptimizeNextStep(new List<int>());
//
// root (max)
// / \
// A(min) B(min)
// / \ / \
// [3] [1] [GOAL] [2]
//
// A = min(3, 1) = 1
// B: step 0 is a goal (opponent won) → value = -(sbyte.MaxValue + depth)
// B = min(very_negative, 2) = very_negative
// root = max(1, very_negative) = 1 → step 0 (A avoids defeat)
//
[TestMethod]
public void Solver_avoids_opponents_goal()
{
var tree = Inner(
Inner(Leaf(3), Leaf(1)),
Inner(Goal(), Leaf(2)));

Assert.AreEqual(0, Solve(tree));
}

//
// root (max)
// / \
// [GOAL] A(min)
// / \
// [1] [-5]
//
// step 0 leads to goal (root won) → very high value
// A = min(1, -5) = -5
// root = max(very_high, -5) → step 0
//
[TestMethod]
public void Solver_finds_immediate_goal()
{
var tree = Inner(
Goal(),
Inner(Leaf(1), Leaf(-5)));

Assert.AreEqual(0, Solve(tree));
}

//
// root (max)
// / \
// A(min) B(min)
// / \ / \
// [-2] [10] [7] [-3]
//
// A = min(-2, 10) = -2
// B = min(7, -3) = -3
// root = max(-2, -3) = -2 → step 0 (A), best of two bad options
//
[TestMethod]
public void All_negative_minimax_picks_least_bad()
{
var tree = Inner(
Inner(Leaf(-2), Leaf(10)),
Inner(Leaf(7), Leaf(-3)));

Assert.AreEqual(0, Solve(tree));
}

//
// root (max)
// |
// A (min)
// / \
// [3] [7]
//
// Only one move from root → must pick step 0.
//
[TestMethod]
public void Single_child_forced_move()
{
var tree = Inner(
Inner(Leaf(3), Leaf(7)));

// Assert
Assert.AreEqual(0, bestStep);
Assert.AreEqual(0, Solve(tree));
}
}
}
30 changes: 0 additions & 30 deletions Alligator.Test/UniformTreePosition.cs

This file was deleted.

Loading
Loading