Significant refactoring and addition of more test cases to exercise the base AI seach code

This commit is contained in:
Lucas Scharenbroich 2016-12-12 00:41:35 -06:00
parent e4ae474a67
commit ac9850998b
46 changed files with 824 additions and 473 deletions

View File

@ -8,7 +8,7 @@
<AppDesignerFolder>Properties</AppDesignerFolder>
<RootNamespace>AI.Test</RootNamespace>
<AssemblyName>AI.Test</AssemblyName>
<TargetFrameworkVersion>v4.5</TargetFrameworkVersion>
<TargetFrameworkVersion>v4.5.2</TargetFrameworkVersion>
<FileAlignment>512</FileAlignment>
<ProjectTypeGuids>{3AC096D0-A1C2-E12C-1390-A8335801FDAB};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}</ProjectTypeGuids>
<VisualStudioVersion Condition="'$(VisualStudioVersion)' == ''">10.0</VisualStudioVersion>
@ -16,6 +16,7 @@
<ReferencePath>$(ProgramFiles)\Common Files\microsoft shared\VSTT\$(VisualStudioVersion)\UITestExtensionPackages</ReferencePath>
<IsCodedUITest>False</IsCodedUITest>
<TestProjectType>UnitTest</TestProjectType>
<TargetFrameworkProfile />
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<DebugSymbols>true</DebugSymbols>
@ -54,6 +55,12 @@
<Compile Include="SearchTest.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\SpriteCompiler\SpriteCompiler.csproj">
<Project>{56f54ca9-17c1-45c6-915b-6fabf4dae5d0}</Project>
<Name>SpriteCompiler</Name>
</ProjectReference>
</ItemGroup>
<Choose>
<When Condition="'$(VisualStudioVersion)' == '10.0' And '$(IsCodedUITest)' == 'True'">
<ItemGroup>

View File

@ -6,17 +6,47 @@ using System.Threading.Tasks;
namespace AI.Test
{
public static class DirectionExtensions
{
public static bool CanMove(this Direction direction, int p)
{
switch (direction)
{
case Direction.LEFT: return (p % 3) != 0;
case Direction.RIGHT: return (p % 3) != 2;
case Direction.UP: return (p > 2);
case Direction.DOWN: return (p < 6);
}
throw new ArgumentException();
}
public static int MoveFrom(this Direction direction, int p)
{
switch (direction)
{
case Direction.LEFT: return p - 1;
case Direction.RIGHT: return p + 1;
case Direction.UP: return p - 3;
case Direction.DOWN: return p + 3;
}
throw new ArgumentException();
}
}
public enum Direction
{
LEFT,
RIGHT,
UP,
DOWN
}
public class EightPuzzleBoard
{
protected static Random rng = new Random();
public enum Direction
{
LEFT,
RIGHT,
UP,
DOWN
}
private int[] board;
@ -35,6 +65,8 @@ namespace AI.Test
this.board = (int[])aBoard.board.Clone();
}
public int[] Board { get { return board; } }
public EightPuzzleBoard Scramble()
{
return Scramble(10);
@ -45,12 +77,13 @@ namespace AI.Test
var newPuzzle = new EightPuzzleBoard(this);
var direction = Enum.GetValues(typeof(Direction)).Cast<Direction>().ToList();
for (int i = 0; i < n; i++)
for (int i = 0; i < n;)
{
int j = rng.Next(direction.Count);
if (newPuzzle.CanMoveGap(direction[j]))
{
newPuzzle.MoveGap(direction[j]);
i += 1;
}
}
@ -113,8 +146,51 @@ namespace AI.Test
return retVal;
}
public int[] Board { get { return board; } }
public int[] GetLocationOf(int val)
{
return ind2sub(GetPositionOf(val));
}
public EightPuzzleBoard MoveGap(Direction direction)
{
var pos1 = GapPosition;
if (direction.CanMove(pos1))
{
var pos2 = direction.MoveFrom(pos1);
Swap(pos1, pos2);
}
return this;
}
private void Swap(int pos1, int pos2)
{
var val = this[pos1];
this[pos1] = this[pos2];
this[pos2] = val;
}
public override bool Equals(object obj)
{
return (this == obj) || board.SequenceEqual(((EightPuzzleBoard)obj).board);
}
public override int GetHashCode()
{
return board.GetHashCode();
}
public bool CanMoveGap(Direction where)
{
return where.CanMove(GetPositionOf(0));
}
public override string ToString()
{
return
board[0] + " " + board[1] + " " + board[2] + Environment.NewLine +
board[3] + " " + board[4] + " " + board[5] + Environment.NewLine +
board[6] + " " + board[7] + " " + board[8];
}
}
}

View File

@ -1,14 +1,270 @@
using System;
using System.Linq;
using System.Collections.Generic;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using SpriteCompiler.AI;
using System.Diagnostics;
using SpriteCompiler.Adapters;
namespace AI.Test
{
[TestClass]
public class SearchTest
public class EightPuzzleNode : HeuristicSearchNode<Direction, EightPuzzleBoard, EightPuzzleNode, IntegerCost>
{
[TestMethod]
public void TestMethod1()
public EightPuzzleNode(EightPuzzleNode node, EightPuzzleBoard state) : base(node, state)
{
}
}
public class EightPuzzleGoalTest : IGoalTest<EightPuzzleBoard>
{
private readonly EightPuzzleBoard goal;
public EightPuzzleGoalTest(EightPuzzleBoard goal)
{
this.goal = new EightPuzzleBoard(goal);
}
public bool IsGoal(EightPuzzleBoard state)
{
return state.Equals(goal);
}
}
public class EightPuzzleSuccessorFunction : ISuccessorFunction<Direction, EightPuzzleBoard>
{
public IEnumerable<Tuple<Direction, EightPuzzleBoard>> Successors(EightPuzzleBoard board)
{
foreach (var direction in Enum.GetValues(typeof(Direction)).Cast<Direction>())
{
if (board.CanMoveGap(direction))
{
yield return Tuple.Create(direction, new EightPuzzleBoard(board).MoveGap(direction));
}
}
}
}
public class EightPuzzleStepCost : IStepCostFunction<Direction, EightPuzzleBoard, IntegerCost>
{
private static readonly IntegerCost UNIT_STEP_COST = (IntegerCost) 1;
public IntegerCost StepCost(EightPuzzleBoard fromState, Direction action, EightPuzzleBoard toState)
{
return UNIT_STEP_COST;
}
}
public class ManhattanHeuristic : IHeuristicFunction<EightPuzzleBoard, IntegerCost>
{
private readonly EightPuzzleBoard goal;
public ManhattanHeuristic(EightPuzzleBoard goal)
{
this.goal = new EightPuzzleBoard(goal);
}
public IntegerCost Eval(EightPuzzleBoard state)
{
int cost = 0;
for (int i = 1; i < 9; i++)
{
var goalLocation = goal.GetLocationOf(i);
var stateLocation = state.GetLocationOf(i);
cost += Math.Abs(goalLocation[0] - stateLocation[0]);
cost += Math.Abs(goalLocation[1] - stateLocation[1]);
}
return cost;
}
}
public class MisplacedHeuristic : IHeuristicFunction<EightPuzzleBoard, IntegerCost>
{
private readonly EightPuzzleBoard goal;
public MisplacedHeuristic(EightPuzzleBoard goal)
{
this.goal = new EightPuzzleBoard(goal);
}
public IntegerCost Eval(EightPuzzleBoard state)
{
return goal.CountMismatches(state);
}
}
public static class IntrumentedParameters
{
public const string NODES_EXPANDED = "nodesExpanded";
}
public class InstrumentedNodeExpander<A, S, T, C> : NodeExpanderDelegator<A, S, T, C>
where T : ISearchNode<A, S, T, C>
where C : ICost<C>
{
private readonly IDictionary<string, int> metrics = new Dictionary<string, int>();
private readonly INodeExpander<A, S, T, C> expander;
public InstrumentedNodeExpander(INodeExpander<A, S, T, C> expander)
: base(expander)
{
ClearMetrics();
}
public override IEnumerable<T> Expand(ISearchProblem<A, S, C> problem, T node)
{
metrics[IntrumentedParameters.NODES_EXPANDED] += 1;
return base.Expand(problem, node);
}
public int this[string key]
{
get
{
return metrics[key];
}
}
public void ClearMetrics()
{
metrics[IntrumentedParameters.NODES_EXPANDED] = 0;
}
}
public class EightPuzzleNodeExpander : InformedNodeExpander<Direction, EightPuzzleBoard, EightPuzzleNode, IntegerCost>
{
public override EightPuzzleNode CreateNode(EightPuzzleNode parent, EightPuzzleBoard state)
{
return new EightPuzzleNode(parent, state);
}
public override EightPuzzleNode CreateNode(EightPuzzleBoard state)
{
return CreateNode(null, state);
}
}
[TestClass]
public class SearchTest
{
// These are the three search problem to run using IDS, A*(h1) and A*(h2)
private ISearchProblem<Direction, EightPuzzleBoard, IntegerCost> problem_none;
private ISearchProblem<Direction, EightPuzzleBoard, IntegerCost> problem_h1;
private ISearchProblem<Direction, EightPuzzleBoard, IntegerCost> problem_h2;
// Define the goal state
private EightPuzzleBoard goal = new EightPuzzleBoard(new[] { 0, 1, 2, 3, 4, 5, 6, 7, 8 });
[TestInitialize]
public void SetUp()
{
// These objects define the abstract search problem
var goalTest = new EightPuzzleGoalTest(goal);
var stepCost = new EightPuzzleStepCost();
var successorFn = new EightPuzzleSuccessorFunction();
var heuristic1 = new MisplacedHeuristic(goal);
var heuristic2 = new ManhattanHeuristic(goal);
// Create three search problem objects. One without a heuristic and two with the different
// heuristics
problem_none = new SearchProblem<Direction, EightPuzzleBoard, IntegerCost>(goalTest, stepCost, successorFn);
problem_h1 = new SearchProblem<Direction, EightPuzzleBoard, IntegerCost>(goalTest, stepCost, successorFn, heuristic1);
problem_h2 = new SearchProblem<Direction, EightPuzzleBoard, IntegerCost>(goalTest, stepCost, successorFn, heuristic2);
}
[TestMethod]
public void TestMethod1()
{
int N = 1;
int dmax = 3;
// Now we define the search algorithm and the type of node expansion. Russell & Norvig discuss
// two type of expansion strategies: tree search and graph search. One will avoid cycles in the search
// space and the other will not.
//
// They state that a tree search was used to generate Figure 4.8;
var expander = new InstrumentedNodeExpander<Direction, EightPuzzleBoard, EightPuzzleNode, IntegerCost>(new EightPuzzleNodeExpander());
var treeSearch = new TreeSearch<Direction, EightPuzzleBoard, EightPuzzleNode, IntegerCost>(expander);
var ids = new IterativeDeepeningSearch<Direction, EightPuzzleBoard, EightPuzzleNode, IntegerCost>(treeSearch, dmax);
var aStarH1 = new AStarSearch<Direction, EightPuzzleBoard, EightPuzzleNode, IntegerCost>(treeSearch, new QueueAdapter<EightPuzzleNode, IntegerCost>());
var aStarH2 = new IterativeDeepeningAStarSearch<Direction, EightPuzzleBoard, EightPuzzleNode, IntegerCost>(treeSearch, (IntegerCost)dmax);
// Depth runs from 0 to dmax
int[,] d = new int[dmax + 2, 3];
int[,] n = new int[dmax + 2, 3];
for (int i = 0; i < N; i++)
{
// Invoke the search on the problem with a particular starting state
var initialState = goal.Scramble(dmax);
/*
{
expander.ClearMetrics();
var solution = ids.Search(problem_none, initialState);
System.Diagnostics.Trace.WriteLine("IDS Solution has " + solution.Count() + " nodes and expanded " + expander[IntrumentedParameters.NODES_EXPANDED] + " nodes");
d[solution.Count(), 0] += 1;
n[solution.Count(), 0] += expander[IntrumentedParameters.NODES_EXPANDED];
}
{
expander.ClearMetrics();
var solution = aStarH1.Search(problem_h1, initialState);
System.Diagnostics.Trace.WriteLine("A* (h1) Solution has " + solution.Count() + " nodes and expanded " + expander[IntrumentedParameters.NODES_EXPANDED] + " nodes");
d[solution.Count(), 1] += 1;
n[solution.Count(), 1] += expander[IntrumentedParameters.NODES_EXPANDED];
}
*/
{
expander.ClearMetrics();
var solution = aStarH2.Search(problem_h2, initialState);
System.Diagnostics.Trace.WriteLine("A* (h2) Solution has " + solution.Count() + " nodes and expanded " + expander[IntrumentedParameters.NODES_EXPANDED] + " nodes");
d[solution.Count(), 2] += 1;
n[solution.Count(), 2] += expander[IntrumentedParameters.NODES_EXPANDED];
}
}
Trace.WriteLine("| Search Cost Branching Factor |");
Trace.WriteLine("+--+---------+--------+--------++---------+--------+--------+");
Trace.WriteLine("| d| IDS | A*(h1) | A*(h2) || IDS | A*(h1) | A*(h2) |");
Trace.WriteLine("+--+---------+--------+--------++---------+--------+--------+");
for (int i = 0; i <= dmax + 1; i++)
{
var bf0 = ComputeBranchingFactor((float)n[i, 0] / (float)d[i, 0], i);
var bf1 = ComputeBranchingFactor((float)n[i, 1] / (float)d[i, 1], i);
var bf2 = ComputeBranchingFactor((float)n[i, 2] / (float)d[i, 2], i);
Trace.WriteLine(String.Format("|{0,2}|{1,-8} |{2,7} |{3,7} ||{4,8:0.00} |{5,7:0.00} |{6,7:0.00} |", i,
n[i, 0] / Math.Max(d[i, 0], 1), n[i, 1] / Math.Max(d[i, 1], 1), n[i, 2] / Math.Max(d[i, 2], 1), bf0, bf1, bf2));
}
Trace.WriteLine("+--+---------+--------+--------++---------+--------+--------+");
}
/// <summary>
/// Uses Newton iteration to solve for the effective branching factor
/// </summary>
/// <param name="n">number of nodes expanded</param>
/// <param name="d">depth of the solution</param>
/// <returns></returns>
private float ComputeBranchingFactor(float n, float d)
{
float x = 3.0f; // Initial guess
for (int i = 0; i < 20; i++)
{
float f = (float)Math.Pow(x, d + 1.0f) - 1.0f - x * n + n;
float df = (d + 1.0f) * (float)Math.Pow(x, d) - n;
x = x - (f / df);
}
return x;
}
}
}

View File

@ -1,7 +1,7 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 2013
VisualStudioVersion = 12.0.40629.0
# Visual Studio 14
VisualStudioVersion = 14.0.25420.1
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SpriteCompiler", "SpriteCompiler\SpriteCompiler.csproj", "{56F54CA9-17C1-45C6-915B-6FABF4DAE5D0}"
EndProject

View File

@ -8,7 +8,7 @@ namespace SpriteCompiler.AI
{
public class AStarComparator<A, S, T, C> : IComparer<T>
where T : HeuristicSearchNode<A, S, T, C>
where C : IPathCost<C>, new()
where C : ICost<C>, new()
{
public int Compare(T x, T y)
{

View File

@ -1,15 +1,12 @@
namespace SpriteCompiler.AI
{
public class AStarSearch<A, S, T, C> : BestFirstSearch<A, S, T, C>
where T : HeuristicSearchNode<A, S, T, C>
where C : IPathCost<C>, new()
{
public AStarSearch(AbstractAISearch<A, S, T, C> search)
: base(search) // , new AStarComparator<A, S, T, C>())
{
}
using Queue;
public AStarSearch(AbstractAISearch<A, S, T, C> search, IQueue<T> fringe)
public class AStarSearch<A, S, T, C> : BestFirstSearch<A, S, T, C>
where T : IHeuristicSearchNode<A, S, T, C>
where C : ICost<C>, new()
{
public AStarSearch(ISearchStrategy<A, S, T, C> search, IQueue<T> fringe)
: base(search, fringe)
{
}

View File

@ -1,113 +0,0 @@
namespace SpriteCompiler.AI
{
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
/// <summary>
/// An abstract description of a state-space search. Specific algorthims are determined by
/// how the nodes are expanded, eveanluated and enqueued.
///
/// The description of the AI problem is delegated to the ISearchProblem interface.
/// </summary>
/// <typeparam name="A"></typeparam>
/// <typeparam name="S"></typeparam>
/// <typeparam name="T"></typeparam>
/// <typeparam name="C"></typeparam>
public abstract class AbstractAISearch<A, S, T, C> : ISearch<A, S, T, C>
where T : ISearchNode<A, S, T, C>
where C : IPathCost<C>
{
// Conceptually the expander is responsible for two things:
//
// 1. Creating a node's successors via the Successor Function
// and wrapping them in SearchNodes (computing path costs
// and heuristics, as well).
//
// 2. Creating a search node from a state. This lets us
// decouple the search algorithm from the state expansion
private INodeExpander<A, S, T, C> expander;
private readonly IQueue<T> fringe;
public AbstractAISearch(INodeExpander<A, S, T, C> expander, IQueue<T> fringe)
{
this.expander = expander;
this.fringe = fringe;
}
public INodeExpander<A, S, T, C> Expander { get { return expander; } set { expander = value; } }
public IEnumerable<T> Solution(T node)
{
var sequence = new List<T>();
while (node != null)
{
sequence.Add(node);
node = node.Parent;
}
sequence.Reverse();
return sequence;
}
public IEnumerable<T> ExtendSearch(ISearchProblem<A, S, C> problem)
{
while (!fringe.Empty)
{
var step = SearchStep(problem, fringe);
if (step.IsGoal)
{
return Solution(step.Node);
}
}
return Enumerable.Empty<T>();
}
public ISearchStepInfo<T> SearchStep(ISearchProblem<A, S, C> problem)
{
var node = fringe.Remove();
#if DEBUG
Console.WriteLine(string.Format("Removed {0} from the queue with g = {1}, c(n, n') = {2}", node.State, node.PathCost, node.StepCost));
#endif
if (problem.IsGoal(node.State))
{
return new SearchStepInfo<T>(node, Solution(node));
}
AddNodes(fringe, node, problem);
return new SearchStepInfo<T>(node, null);
}
public IEnumerable<T> Expand(ISearchProblem<A, S, C> problem, T node)
{
return expander.Expand(problem, node);
}
public bool IsFailure(IEnumerable<T> solution)
{
return !solution.Any();
}
public virtual IEnumerable<T> Search(ISearchProblem<A, S, C> problem, IQueue<T> fringe, S initialState)
{
InitializeSearch(fringe, initialState);
return ExtendSearch(problem, fringe);
}
public void InitializeSearch(IQueue<T> fringe, S initialState)
{
fringe.Clear();
fringe.Enqueue(expander.CreateNode(default(T), initialState));
}
protected abstract void AddNodes(IQueue<T> fringe, T node, ISearchProblem<A, S, C> problem);
}
}

View File

@ -5,7 +5,7 @@
public abstract class AbstractSearchNode<A, S, T, C> : ISearchNode<A, S, T, C>
where T : ISearchNode<A, S, T, C>
where C : IPathCost<C>, new()
where C : ICost<C>, new()
{
protected readonly S state;
protected readonly T parent;
@ -30,6 +30,8 @@
public int Depth { get { return depth; } }
public S State { get { return state; } }
public C EstCost { get { return PathCost; } }
public C StepCost
{
get
@ -43,7 +45,5 @@
pathCost = HasParent ? parent.PathCost.Add(value) : value;
}
}
virtual public C EstCost { get { return PathCost; } }
}
}

View File

@ -0,0 +1,95 @@
namespace SpriteCompiler.AI
{
using System.Collections.Generic;
using System.Linq;
using Queue;
/// <summary>
/// An abstract description of a state-space search. Specific algorthims are determined by
/// how the nodes are expanded, evaluated and enqueued.
///
/// The description of the AI problem is delegated to the ISearchProblem interface.
/// </summary>
/// <typeparam name="A"></typeparam>
/// <typeparam name="S"></typeparam>
/// <typeparam name="T"></typeparam>
/// <typeparam name="C"></typeparam>
public abstract class AbstractSearchStrategy<A, S, T, C> : ISearchStrategy<A, S, T, C>
where T : ISearchNode<A, S, T, C>
where C : ICost<C>
{
// Conceptually the expander is responsible for two things:
//
// 1. Creating a node's successors via the Successor Function
// and wrapping them in SearchNodes (computing path costs
// and heuristics, as well).
//
// 2. Creating a search node from a state. This lets us
// decouple the search algorithm from the state expansion
private INodeExpander<A, S, T, C> expander;
public AbstractSearchStrategy(INodeExpander<A, S, T, C> expander)
{
this.expander = expander;
}
public INodeExpander<A, S, T, C> Expander { get { return expander; } set { expander = value; } }
/// <summary>
/// Helper method to walk a solution node to the root and then reverse the list
/// </summary>
/// <param name="node"></param>
/// <returns></returns>
public IEnumerable<T> Solution(T node)
{
var sequence = new List<T>();
for (var curr = node; node != null; node = node.Parent)
{
sequence.Add(curr);
}
sequence.Reverse();
return sequence;
}
/// <summary>
///
/// </summary>
/// <param name="problem"></param>
/// <param name="fringe">Must be initialize -- usually that means being empty</param>
/// <param name="initialState"></param>
/// <returns></returns>
public virtual IEnumerable<T> Search(ISearchProblem<A, S, C> problem, IQueue<T> fringe, S initialState)
{
// Add the initial state to the fringe
fringe.Enqueue(Expander.CreateNode(initialState));
// Search until success, or the search space is exhausted
while (!fringe.Empty)
{
var node = fringe.Remove();
if (problem.IsGoal(node.State))
{
return Solution(node);
}
AddNodes(fringe, node, problem);
}
return Enumerable.Empty<T>();
}
/// <summary>
/// When it's time to actually expand a node and add the new states to the fringe, different
/// algorhtms can make different choices
/// </summary>
/// <param name="fringe"></param>
/// <param name="node"></param>
/// <param name="problem"></param>
protected abstract void AddNodes(IQueue<T> fringe, T node, ISearchProblem<A, S, C> problem);
}
}

View File

@ -1,103 +1,25 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace SpriteCompiler.AI
namespace SpriteCompiler.AI
{
public abstract class AbstractStateSpaceSearch<A, S, T, C>
using System;
using System.Collections.Generic;
using Queue;
public abstract class AbstractStateSpaceSearch<A, S, T, C> : ISearch<A, S, T, C>
where T : ISearchNode<A, S, T, C>
where C : IPathCost<C>
where C : ICost<C>
{
private readonly INodeExpander<A, S, T, C> expander;
protected readonly ISearchStrategy<A, S, T, C> strategy;
private readonly Func<IQueue<T>> fringe;
public AbstractStateSpaceSearch(INodeExpander<A, S, T, C> expander, Func<IQueue<T>> fringe)
public AbstractStateSpaceSearch(ISearchStrategy<A, S, T, C> strategy, Func<IQueue<T>> fringe)
{
this.expander = expander;
this.fringe = fringe;
this.strategy = strategy;
this.fringe = fringe;
}
public ISearchProblemInstance<A, S, T, C> Create(ISearchProblem<A, S, C> problem, S initialState)
public virtual IEnumerable<T> Search(ISearchProblem<A, S, C> problem, S initialState)
{
return new SearchProblemInstance<A, S, T, C>(problem, fringe(), initialState);
return strategy.Search(problem, fringe(), initialState);
}
}
public interface ISearchProblemInstance<A, S, T, C>
where T : ISearchNode<A, S, T, C>
where C : IPathCost<C>
{
/// <summary>
/// Tries to find a solution to the search problem. Returns null if no solution can be found.
/// This method can be called repeatedly to try to find additional, sub optimal solutions if
/// supported.
/// </summary>
/// <returns></returns>
IEnumerable<T> FindSolution();
/// <summary>
/// Take a single step of the searth to allow introspection of the search process
/// </summary>
/// <param name="problem"></param>
/// <returns></returns>
ISearchStepInfo<T> Step();
/// <summary>
/// Provide a set of callbacks to watch the execution of a search
/// </summary>
// void Trace();
}
public class SearchProblemInstance<A, S, T, C> : ISearchProblemInstance<A, S, T, C>
where T : ISearchNode<A, S, T, C>
where C : IPathCost<C>
{
private readonly ISearchProblem<A, S, C> problem;
private readonly S initialState;
private readonly IQueue<T> fringe;
public SearchProblemInstance(ISearchProblem<A, S, C> problem, IQueue<T> fringe, S initialState)
{
this.problem = problem;
this.fringe = fringe;
this.initialState = initialState;
}
public IEnumerable<T> FindSolution()
{
throw new NotImplementedException();
}
public ISearchStepInfo<T> Step()
{
var node = fringe.Remove();
if (problem.IsGoal(node.State))
{
return new SearchStepInfo<T>(node, Solution(node));
}
AddNodes(fringe, node, problem);
return new SearchStepInfo<T>(node, null);
}
public IEnumerable<T> Solution(T node)
{
var sequence = new List<T>();
while (node != null)
{
sequence.Add(node);
node = node.Parent;
}
sequence.Reverse();
return sequence;
}
}
}

View File

@ -1,46 +1,14 @@
namespace SpriteCompiler.AI
{
using Adapters;
using System.Collections.Generic;
using Queue;
public class BestFirstSearch<A, S, T, C> : ISearch<A, S, T, C>
public class BestFirstSearch<A, S, T, C> : AbstractStateSpaceSearch<A, S, T, C>
where T : ISearchNode<A, S, T, C>
where C : IPathCost<C>
where C : ICost<C>
{
protected readonly AbstractAISearch<A, S, T, C> search;
protected readonly IQueue<T> fringe;
public BestFirstSearch(AbstractAISearch<A, S, T, C> search, IQueue<T> fringe)
public BestFirstSearch(ISearchStrategy<A, S, T, C> strategy, IQueue<T> fringe)
: base(strategy, () => fringe)
{
this.search = search;
this.fringe = fringe;
}
public BestFirstSearch(AbstractAISearch<A, S, T, C> search)
{
this.search = search;
this.fringe = new QueueAdapter<T, C>();
}
public IEnumerable<T> Search(ISearchProblem<A, S, C> problem, S initialState)
{
fringe.Clear();
return search.Search(problem, fringe, initialState);
}
public IEnumerable<T> ExtendSearch(ISearchProblem<A, S, C> problem)
{
return search.ExtendSearch(problem, fringe);
}
public void InitializeSearch(S initialState)
{
search.InitializeSearch(fringe, initialState);
}
public ISearchStepInfo<T> SearchStep(ISearchProblem<A,S,C> problem)
{
return search.SearchStep(problem, fringe);
}
}
}

View File

@ -7,18 +7,31 @@
/// </summary>
public class CostNodeLimiter<T, C> : INodeLimiter<T, C>
where T : ISearchNode<C>
where C : IPathCost<C>, new()
where C : ICost<C>, new()
{
private readonly C maxCost;
private C nextCost;
public CostNodeLimiter(C maxCost)
public CostNodeLimiter(C maxCost, C infinity)
{
this.maxCost = maxCost;
this.nextCost = infinity;
}
public C NextCost { get { return nextCost; } }
public bool Cutoff(T node)
{
return node.PathCost.CompareTo(maxCost) >= 0;
// If we find a value that exceeds the current maximum, return false,
// but keep track of the smallest value that is larger than the maximum
// cost.
if (node.PathCost.CompareTo(maxCost) > 0)
{
nextCost = (node.PathCost.CompareTo(nextCost) < 0) ? node.PathCost : nextCost;
return true;
}
return false;
}
}
}

View File

@ -1,42 +1,14 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace SpriteCompiler.AI
namespace SpriteCompiler.AI
{
public class DepthFirstSearch<A, S, T, C> : ISearch<A, S, T, C>
using Queue;
public class DepthFirstSearch<A, S, T, C> : AbstractStateSpaceSearch<A, S, T, C>
where T : ISearchNode<A, S, T, C>
where C : IPathCost<C>, new()
where C : ICost<C>, new()
{
protected readonly AbstractAISearch<A, S, T, C> search;
protected readonly IQueue<T> fringe = new Lifo<T>();
public DepthFirstSearch(AbstractAISearch<A, S, T, C> search)
public DepthFirstSearch(ISearchStrategy<A, S, T, C> strategy)
: base(strategy, () => new LIFO<T>())
{
this.search = search;
}
public virtual IEnumerable<T> Search(ISearchProblem<A, S, C> problem, S initialState)
{
fringe.Clear();
return search.Search(problem, fringe, initialState);
}
public virtual IEnumerable<T> ExtendSearch(ISearchProblem<A, S, C> problem)
{
return search.ExtendSearch(problem, fringe);
}
public void InitializeSearch(S initialState)
{
search.InitializeSearch(fringe, initialState);
}
public ISearchStepInfo<T> SearchStep(ISearchProblem<A, S, C> problem)
{
return search.SearchStep(problem, fringe);
}
}
}

View File

@ -8,7 +8,7 @@ namespace SpriteCompiler.AI
{
public class DepthLimitedNodeExpander<A, S, T, C> : NodeExpanderDelegator<A, S, T, C>
where T : ISearchNode<A, S, T, C>
where C : IPathCost<C>, new()
where C : ICost<C>, new()
{
private readonly INodeLimiter<T, C> limit;
private bool cutoffOccured = false;

View File

@ -1,22 +1,19 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace SpriteCompiler.AI
namespace SpriteCompiler.AI
{
using System.Collections.Generic;
using System.Linq;
/// <summary>
/// Depth-first search with a cutoff
/// </summary>
public class DepthLimitedSearch<A, S, T, C> : DepthFirstSearch<A, S, T, C>
where T : ISearchNode<A, S, T, C>
where C : IPathCost<C>, new()
where C : ICost<C>, new()
{
protected readonly INodeLimiter<T, C> limit;
public DepthLimitedSearch(AbstractAISearch<A, S, T, C> search, INodeLimiter<T, C> limit)
: base(search)
public DepthLimitedSearch(ISearchStrategy<A, S, T, C> strategy, INodeLimiter<T, C> limit)
: base(strategy)
{
this.limit = limit;
}
@ -24,32 +21,26 @@ namespace SpriteCompiler.AI
public override IEnumerable<T> Search(ISearchProblem<A, S, C> problem, S initialState)
{
// Save the old node expander
var oldExpander = search.Expander;
var oldExpander = strategy.Expander;
// Wrap the expander with a depth-limied expanded in order to
// terminate the search
var expander = new DepthLimitedNodeExpander<A, S, T, C>(oldExpander, limit);
search.Expander = expander;
strategy.Expander = expander;
// Run the search
var solution = base.Search(problem, initialState);
// Restore the old expander
search.Expander = oldExpander;
strategy.Expander = oldExpander;
// Check to see we failed and if the reason for failing was not reaching the cutoff depth.
if (search.IsFailure(solution) && expander.CutoffOccured)
if (!solution.Any() && expander.CutoffOccured)
{
return null;
}
return solution;
}
public override IEnumerable<T> ExtendSearch(ISearchProblem<A, S, C> problem)
{
return base.ExtendSearch(problem);
}
public bool IsCutoff(IEnumerable<T> result)

View File

@ -0,0 +1,19 @@
namespace SpriteCompiler.AI
{
public class DepthNodeLimiter<T, C> : INodeLimiter<T, C>
where T : ISearchNode<C>
where C : ICost<C>, new()
{
private readonly int maxDepth;
public DepthNodeLimiter(int maxDepth)
{
this.maxDepth = maxDepth;
}
public bool Cutoff(T node)
{
return node.Depth >= maxDepth;
}
}
}

View File

@ -1,11 +1,11 @@
using System;
using System.Collections.Generic;
namespace SpriteCompiler.AI
namespace SpriteCompiler.AI
{
public class GraphSearch<A, S, T, C> : AbstractAISearch<A, S, T, C>
using System.Collections.Generic;
using Queue;
public class GraphSearch<A, S, T, C> : AbstractSearchStrategy<A, S, T, C>
where T : ISearchNode<A, S, T, C>
where C : IPathCost<C>
where C : ICost<C>
{
private readonly ISet<S> closed = new HashSet<S>();
@ -31,7 +31,7 @@ namespace SpriteCompiler.AI
if (!closed.Contains(node.State))
{
closed.Add(node.State);
fringe.AddRange(Expand(problem, node));
fringe.AddRange(Expander.Expand(problem, node));
}
}
}

View File

@ -2,9 +2,14 @@
{
using System;
public class HeuristicSearchNode<A, S, T, C> : AbstractSearchNode<A, S, T, C>, ISearchNode<A, S, T, C>
where T : HeuristicSearchNode<A, S, T, C>
where C : IPathCost<C>, new()
public interface IHeuristicSearchNode<A, S, T, C> : ISearchNode<A, S, T, C> where C : ICost<C>
{
C EstCost { get; }
}
public class HeuristicSearchNode<A, S, T, C> : AbstractSearchNode<A, S, T, C>, IHeuristicSearchNode<A, S, T, C>
where T : IHeuristicSearchNode<A, S, T, C>
where C : ICost<C>, new()
{
public HeuristicSearchNode(T node, S state)
: base(node, state)
@ -14,7 +19,7 @@
public C Heuristic { get; set; }
public override C EstCost
public C EstCost
{
get
{

View File

@ -2,7 +2,7 @@
{
using System;
public interface IHeuristicFunction<S, C> where C : IPathCost<C>
public interface IHeuristicFunction<S, C> where C : ICost<C>
{
C Eval(S state);
}

View File

@ -11,9 +11,11 @@
/// </summary>
public interface INodeExpander<A, S, T, C>
where T : ISearchNode<A, S, T, C>
where C : IPathCost<C>
where C : ICost<C>
{
IEnumerable<T> Expand(ISearchProblem<A, S, C> problem, T node);
T CreateNode(T parent, S state);
T CreateNode(S state);
}
}

View File

@ -2,7 +2,7 @@
{
public interface INodeLimiter<T, C>
where T : ISearchNode<C>
where C : IPathCost<C>, new()
where C : ICost<C>, new()
{
bool Cutoff(T node);
}

View File

@ -2,8 +2,13 @@
{
using System;
public interface IPathCost<C> : IComparable<C>
public interface ICost<C> : IComparable<C>
{
C Add(C value);
// Number theoretic values, i.e. C + ZERO = C, C * ONE = C
C Zero();
C One();
C Maximum();
}
}

View File

@ -4,23 +4,11 @@
public interface ISearch<A, S, T, C>
where T : ISearchNode<A, S, T, C>
where C : IPathCost<C>
where C : ICost<C>
{
/// Perform a new search on the specified search problem using the given
/// initial state as a starting point. The method will return an empty
/// list on failure.
IEnumerable<T> Search(ISearchProblem<A, S, C> problem, S initialState);
/**
* Continues to run a search after a solution is found. This can
* be useful for enumerating over all the solutions to a search problem.
*
* @param problem
* @return Sequence of search nodes desribing a solution
*/
IEnumerable<T> ExtendSearch(ISearchProblem<A, S, C> problem);
void InitializeSearch(S initialState);
ISearchStepInfo<T> SearchStep(ISearchProblem<A, S, C> problem);
IEnumerable<T> Search(ISearchProblem<A, S, C> problem, S initialState);
}
}

View File

@ -9,18 +9,23 @@
/// <typeparam name="S">State of the search</typeparam>
/// <typeparam name="T">Type of the parent</typeparam>
/// <typeparam name="C">Cost type</typeparam>
public interface ISearchNode<A, S, T, C> : ISearchNode<C> where C : IPathCost<C>
public interface ISearchNode<A, S, T, C> : ISearchNode<C> where C : ICost<C>
{
A Action { get; set; }
S State { get; }
T Parent { get; }
}
public interface ISearchNode<C>
/// <summary>
/// Simplest representation of a seach node that just has the costs. This interface
/// is useful for certain evaluation functions
/// </summary>
/// <typeparam name="C"></typeparam>
public interface ISearchNode<C> where C : ICost<C>
{
C PathCost { get; }
C StepCost { get; set; }
int Depth { get; }
C EstCost { get; }
int Depth { get; }
}
}

View File

@ -4,7 +4,7 @@
using System.Collections.Generic;
public interface ISearchProblem<A, S, C>
where C : IComparable<C>
where C : ICost<C>
{
IEnumerable<Tuple<A, S>> Successors(S state);
bool IsGoal(S state);

View File

@ -0,0 +1,18 @@
namespace SpriteCompiler.AI
{
using System.Collections.Generic;
using Queue;
/// <summary>
/// A search strategy defines how a state space is explored by determining which
/// nodes in the fringe are expanded next. Two common search strategies are
/// Tree Search and Graph Search.
/// </summary>
public interface ISearchStrategy<A, S, T, C>
where T : ISearchNode<A, S, T, C>
where C : ICost<C>
{
IEnumerable<T> Search(ISearchProblem<A, S, C> problem, IQueue<T> fringe, S initialState);
INodeExpander<A, S, T, C> Expander { get; set; }
}
}

View File

@ -2,7 +2,7 @@
{
using System;
public interface IStepCostFunction<A, S, C> where C : IPathCost<C>
public interface IStepCostFunction<A, S, C> where C : ICost<C>
{
C StepCost(S fromState, A action, S toState);
}

View File

@ -7,16 +7,17 @@ namespace SpriteCompiler.AI
public abstract class InformedNodeExpander<A, S, T, C> : INodeExpander<A, S, T, C>
where T : HeuristicSearchNode<A, S, T, C>
where C : IPathCost<C>, new()
where C : ICost<C>, new()
{
public abstract T CreateNode(T parent, S state);
public abstract T CreateNode(S state);
public IEnumerable<T> Expand(ISearchProblem<A, S, C> problem, T node)
{
var successors = problem.Successors(node.State);
// Debug
#if DEBUG
#if VERBOSE_DEBUG
Console.WriteLine(String.Format("There are {0} successors for {1}", successors.Count(), node));
Console.WriteLine(String.Format("This node has a current path cost of {0}", node.PathCost));
#endif
@ -31,7 +32,7 @@ namespace SpriteCompiler.AI
next.StepCost = problem.StepCost(node.State, action, state);
next.Heuristic = problem.Heuristic(state);
#if DEBUG
#if False
Console.WriteLine(" Action = " + next.Action + ", g(n') = " + next.PathCost + ", h(n') = " + next.Heuristic);