From cfc2cda1a89817be6332720fffddb6a7d7f2e97d Mon Sep 17 00:00:00 2001 From: Lucas Scharenbroich Date: Sat, 10 Dec 2016 21:56:27 -0600 Subject: [PATCH] Interim refactoring of core AI search code to be more inspectable --- AI.Test/AI.Test.csproj | 83 ++++++++++++ AI.Test/EightPuzzleBoard.cs | 120 ++++++++++++++++++ AI.Test/Properties/AssemblyInfo.cs | 36 ++++++ AI.Test/SearchTest.cs | 14 ++ SpriteCompiler/AI/AbstractAISearch.cs | 55 ++++++-- SpriteCompiler/AI/AbstractStateSpaceSearch.cs | 103 +++++++++++++++ SpriteCompiler/AI/BestFirstSearch.cs | 10 ++ SpriteCompiler/AI/DepthFirstSearch.cs | 10 ++ SpriteCompiler/AI/ISearch.cs | 2 + SpriteCompiler/AI/ISearchStepInfo.cs | 31 +++++ .../AI/IterativeDeepeningAStarSearch.cs | 10 ++ .../Problem/SpriteGeneratorSearchProblem.cs | 10 ++ SpriteCompiler/Program.cs | 54 ++++++-- SpriteCompiler/SpriteCompiler.csproj | 1 + 14 files changed, 516 insertions(+), 23 deletions(-) create mode 100644 AI.Test/AI.Test.csproj create mode 100644 AI.Test/EightPuzzleBoard.cs create mode 100644 AI.Test/Properties/AssemblyInfo.cs create mode 100644 AI.Test/SearchTest.cs create mode 100644 SpriteCompiler/AI/AbstractStateSpaceSearch.cs create mode 100644 SpriteCompiler/AI/ISearchStepInfo.cs diff --git a/AI.Test/AI.Test.csproj b/AI.Test/AI.Test.csproj new file mode 100644 index 0000000..6fa6abf --- /dev/null +++ b/AI.Test/AI.Test.csproj @@ -0,0 +1,83 @@ + + + + Debug + AnyCPU + 66b89e29-23e4-4d16-a3d5-e55f0eda4d87 + Library + Properties + AI.Test + AI.Test + v4.5 + 512 + {3AC096D0-A1C2-E12C-1390-A8335801FDAB};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} + 10.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + $(ProgramFiles)\Common Files\microsoft shared\VSTT\$(VisualStudioVersion)\UITestExtensionPackages + False + UnitTest + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + + + + + + + + + + + + + + + + + + + + + + False + + + False + + + False + + + False + + + + + + + + \ No newline at end of file diff --git a/AI.Test/EightPuzzleBoard.cs b/AI.Test/EightPuzzleBoard.cs new file mode 100644 index 0000000..b894141 --- /dev/null +++ b/AI.Test/EightPuzzleBoard.cs @@ -0,0 +1,120 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace AI.Test +{ + public class EightPuzzleBoard + { + protected static Random rng = new Random(); + + public enum Direction + { + LEFT, + RIGHT, + UP, + DOWN + } + + private int[] board; + + public EightPuzzleBoard() + : this(new int[] { 0, 1, 2, 3, 4, 5, 6, 7, 8 }) + { + } + + public EightPuzzleBoard(int[] aBoard) + { + this.board = aBoard; + } + + public EightPuzzleBoard(EightPuzzleBoard aBoard) + { + this.board = (int[])aBoard.board.Clone(); + } + + public EightPuzzleBoard Scramble() + { + return Scramble(10); + } + + public EightPuzzleBoard Scramble(int n) + { + var newPuzzle = new EightPuzzleBoard(this); + var direction = Enum.GetValues(typeof(Direction)).Cast().ToList(); + + for (int i = 0; i < n; i++) + { + int j = rng.Next(direction.Count); + if (newPuzzle.CanMoveGap(direction[j])) + { + newPuzzle.MoveGap(direction[j]); + } + } + + return newPuzzle; + } + + private int[] ind2sub(int x) + { + if (x < 0 || x > 8) + { + return null; + } + + return new int[] { x / 3, x % 3 }; + } + + protected int sub2ind(int x, int y) + { + return x * 3 + y; + } + + public int this[int key] + { + get + { + return board[key]; + } + + set + { + board[key] = value; + } + } + + private int GapPosition { get { return GetPositionOf(0); } } + + public int CountMismatches(EightPuzzleBoard aBoard) + { + int count = 0; + for (int i = 0; i < 9; i++) + { + if (board[i] != aBoard[i] && board[i] != 0) + { + count++; + } + } + return count; + } + + private int GetPositionOf(int val) + { + int retVal = -1; + for (int i = 0; i < 9; i++) + { + if (board[i] == val) + { + retVal = i; + } + } + return retVal; + } + + public int[] Board { get { return board; } } + + + } +} diff --git a/AI.Test/Properties/AssemblyInfo.cs b/AI.Test/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..3fbf861 --- /dev/null +++ b/AI.Test/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("AI.Test")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("AI.Test")] +[assembly: AssemblyCopyright("Copyright © 2016")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("66b89e29-23e4-4d16-a3d5-e55f0eda4d87")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/AI.Test/SearchTest.cs b/AI.Test/SearchTest.cs new file mode 100644 index 0000000..cd81834 --- /dev/null +++ b/AI.Test/SearchTest.cs @@ -0,0 +1,14 @@ +using System; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace AI.Test +{ + [TestClass] + public class SearchTest + { + [TestMethod] + public void TestMethod1() + { + } + } +} diff --git a/SpriteCompiler/AI/AbstractAISearch.cs b/SpriteCompiler/AI/AbstractAISearch.cs index 5fa8661..90484ca 100644 --- a/SpriteCompiler/AI/AbstractAISearch.cs +++ b/SpriteCompiler/AI/AbstractAISearch.cs @@ -4,8 +4,18 @@ using System.Collections.Generic; using System.Diagnostics; using System.Linq; - - public abstract class AbstractAISearch + + /// + /// 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. + /// + /// + /// + /// + /// + public abstract class AbstractAISearch : ISearch where T : ISearchNode where C : IPathCost { @@ -19,10 +29,12 @@ // decouple the search algorithm from the state expansion private INodeExpander expander; + private readonly IQueue fringe; - public AbstractAISearch(INodeExpander expander) + public AbstractAISearch(INodeExpander expander, IQueue fringe) { this.expander = expander; + this.fringe = fringe; } public INodeExpander Expander { get { return expander; } set { expander = value; } } @@ -46,21 +58,34 @@ { while (!fringe.Empty) { - 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)) + var step = SearchStep(problem, fringe); + if (step.IsGoal) { - return Solution(node); + return Solution(step.Node); } - - AddNodes(fringe, node, problem); } return Enumerable.Empty(); } + public ISearchStepInfo SearchStep(ISearchProblem problem, IQueue fringe) + { + 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(node, Solution(node)); + } + + AddNodes(fringe, node, problem); + + return new SearchStepInfo(node, null); + } + public IEnumerable Expand(ISearchProblem problem, T node) { return expander.Expand(problem, node); @@ -73,10 +98,16 @@ public virtual IEnumerable Search(ISearchProblem problem, IQueue fringe, S initialState) { - fringe.Enqueue(expander.CreateNode(default(T), initialState)); + InitializeSearch(fringe, initialState); return ExtendSearch(problem, fringe); } + public void InitializeSearch(IQueue fringe, S initialState) + { + fringe.Clear(); + fringe.Enqueue(expander.CreateNode(default(T), initialState)); + } + protected abstract void AddNodes(IQueue fringe, T node, ISearchProblem problem); } } diff --git a/SpriteCompiler/AI/AbstractStateSpaceSearch.cs b/SpriteCompiler/AI/AbstractStateSpaceSearch.cs new file mode 100644 index 0000000..745ed78 --- /dev/null +++ b/SpriteCompiler/AI/AbstractStateSpaceSearch.cs @@ -0,0 +1,103 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace SpriteCompiler.AI +{ + public abstract class AbstractStateSpaceSearch + where T : ISearchNode + where C : IPathCost + { + private readonly INodeExpander expander; + private readonly Func> fringe; + + public AbstractStateSpaceSearch(INodeExpander expander, Func> fringe) + { + this.expander = expander; + this.fringe = fringe; + } + + public ISearchProblemInstance Create(ISearchProblem problem, S initialState) + { + return new SearchProblemInstance(problem, fringe(), initialState); + } + } + + public interface ISearchProblemInstance + where T : ISearchNode + where C : IPathCost + { + /// + /// 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. + /// + /// + IEnumerable FindSolution(); + + /// + /// Take a single step of the searth to allow introspection of the search process + /// + /// + /// + ISearchStepInfo Step(); + + /// + /// Provide a set of callbacks to watch the execution of a search + /// + // void Trace(); + } + + public class SearchProblemInstance : ISearchProblemInstance + where T : ISearchNode + where C : IPathCost + { + private readonly ISearchProblem problem; + private readonly S initialState; + private readonly IQueue fringe; + + public SearchProblemInstance(ISearchProblem problem, IQueue fringe, S initialState) + { + this.problem = problem; + this.fringe = fringe; + this.initialState = initialState; + } + + public IEnumerable FindSolution() + { + throw new NotImplementedException(); + } + + public ISearchStepInfo Step() + { + var node = fringe.Remove(); + + if (problem.IsGoal(node.State)) + { + return new SearchStepInfo(node, Solution(node)); + } + + AddNodes(fringe, node, problem); + + return new SearchStepInfo(node, null); + } + + public IEnumerable Solution(T node) + { + var sequence = new List(); + + while (node != null) + { + sequence.Add(node); + node = node.Parent; + } + + sequence.Reverse(); + + return sequence; + } + + } +} diff --git a/SpriteCompiler/AI/BestFirstSearch.cs b/SpriteCompiler/AI/BestFirstSearch.cs index 93e9073..bc2752a 100644 --- a/SpriteCompiler/AI/BestFirstSearch.cs +++ b/SpriteCompiler/AI/BestFirstSearch.cs @@ -32,5 +32,15 @@ { return search.ExtendSearch(problem, fringe); } + + public void InitializeSearch(S initialState) + { + search.InitializeSearch(fringe, initialState); + } + + public ISearchStepInfo SearchStep(ISearchProblem problem) + { + return search.SearchStep(problem, fringe); + } } } diff --git a/SpriteCompiler/AI/DepthFirstSearch.cs b/SpriteCompiler/AI/DepthFirstSearch.cs index 42c4db2..339796e 100644 --- a/SpriteCompiler/AI/DepthFirstSearch.cs +++ b/SpriteCompiler/AI/DepthFirstSearch.cs @@ -28,5 +28,15 @@ namespace SpriteCompiler.AI { return search.ExtendSearch(problem, fringe); } + + public void InitializeSearch(S initialState) + { + search.InitializeSearch(fringe, initialState); + } + + public ISearchStepInfo SearchStep(ISearchProblem problem) + { + return search.SearchStep(problem, fringe); + } } } diff --git a/SpriteCompiler/AI/ISearch.cs b/SpriteCompiler/AI/ISearch.cs index 6d58ed0..3c80d12 100644 --- a/SpriteCompiler/AI/ISearch.cs +++ b/SpriteCompiler/AI/ISearch.cs @@ -20,5 +20,7 @@ */ IEnumerable ExtendSearch(ISearchProblem problem); + void InitializeSearch(S initialState); + ISearchStepInfo SearchStep(ISearchProblem problem); } } diff --git a/SpriteCompiler/AI/ISearchStepInfo.cs b/SpriteCompiler/AI/ISearchStepInfo.cs new file mode 100644 index 0000000..093cdf0 --- /dev/null +++ b/SpriteCompiler/AI/ISearchStepInfo.cs @@ -0,0 +1,31 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace SpriteCompiler.AI +{ + public interface ISearchStepInfo + { + bool IsGoal { get; } + T Node { get; } + IEnumerable Solution { get; } + } + + public class SearchStepInfo : ISearchStepInfo + { + private readonly T node; + private readonly IEnumerable solution; + + public SearchStepInfo(T node, IEnumerable solution) + { + this.solution = solution; + this.node = node; + } + + public IEnumerable Solution { get { return solution; } } + public bool IsGoal { get { return solution != null; } } + public T Node { get { return node; } } + } +} diff --git a/SpriteCompiler/AI/IterativeDeepeningAStarSearch.cs b/SpriteCompiler/AI/IterativeDeepeningAStarSearch.cs index 37c75c2..b35925d 100644 --- a/SpriteCompiler/AI/IterativeDeepeningAStarSearch.cs +++ b/SpriteCompiler/AI/IterativeDeepeningAStarSearch.cs @@ -41,5 +41,15 @@ namespace SpriteCompiler.AI { throw new NotImplementedException(); } + + public void InitializeSearch(S initialState) + { + search.InitializeSearch(initialState); + } + + public ISearchStepInfo SearchStep(ISearchProblem problem) + { + return search.SearchStep(problem); + } } } diff --git a/SpriteCompiler/Problem/SpriteGeneratorSearchProblem.cs b/SpriteCompiler/Problem/SpriteGeneratorSearchProblem.cs index bdf5ed2..f4cfc8a 100644 --- a/SpriteCompiler/Problem/SpriteGeneratorSearchProblem.cs +++ b/SpriteCompiler/Problem/SpriteGeneratorSearchProblem.cs @@ -22,5 +22,15 @@ return new AStarSearch(strategy); } + + public static ISearch Create(int maxCycles) + { + var expander = new SpriteGeneratorNodeExpander(); + //var strategy = new TreeSearch(expander); + var strategy = new GraphSearch(expander); + + var maxCost = (IntegerPathCost)maxCycles; + return new IterativeDeepeningAStarSearch(strategy, maxCost); + } } } diff --git a/SpriteCompiler/Program.cs b/SpriteCompiler/Program.cs index 74fb8d4..39457c2 100644 --- a/SpriteCompiler/Program.cs +++ b/SpriteCompiler/Program.cs @@ -49,7 +49,9 @@ var mask = new List(); var filename = (string)null; Color? maskColor = null; + int? maxCycles = null; var sprite = new List(); + bool verbose = false; var p = new FluentCommandLineParser(); @@ -62,6 +64,12 @@ p.Setup('i', "image") .Callback(_ => filename = _); + p.Setup('l', "limit") + .Callback(_ => maxCycles = int.Parse(_)); + + p.Setup('v', "verbose") + .Callback(_ => verbose = true); + p.Setup("bg-color") .Callback(_ => maskColor = Color.FromArgb(0xFF, Color.FromArgb(Convert.ToInt32(_, 16)))); @@ -72,6 +80,15 @@ Console.WriteLine("Input filename is " + filename); Console.WriteLine("Image mask color is " + (maskColor.HasValue ? maskColor.ToString() : "(none)")); + // Set the global state + var problem = SpriteGeneratorSearchProblem.CreateSearchProblem(); + var search = maxCycles.HasValue ? + SpriteGeneratorSearchProblem.Create(maxCycles.Value) : + SpriteGeneratorSearchProblem.Create(); + + SpriteGeneratorState initialState = null; + + // Handle the difference command line cases if (!String.IsNullOrEmpty(filename)) { var palette = new Dictionary(); @@ -149,23 +166,38 @@ sprite.Add(new SpriteByte(data_byte, mask_byte, offset)); } - } - } + } - // Set the global state - var problem = SpriteGeneratorSearchProblem.CreateSearchProblem(); - var search = SpriteGeneratorSearchProblem.Create(); - - if (data.Count == mask.Count) + initialState = SpriteGeneratorState.Init(sprite); + } + else if (data.Count == mask.Count) { - var solution = search.Search(problem, SpriteGeneratorState.Init(data, mask)); - WriteOutSolution(solution); + initialState = SpriteGeneratorState.Init(data, mask); } else { - var solution = search.Search(problem, SpriteGeneratorState.Init(data)); - WriteOutSolution(solution); + initialState = SpriteGeneratorState.Init(data); } + + IEnumerable solution = null; + if (verbose) + { + search.InitializeSearch(initialState); + while (true) + { + var step = search.SearchStep(problem); + if (step.IsGoal) + { + break; + } + } + } + else + { + solution = search.Search(problem, initialState); + } + + WriteOutSolution(solution); } } } diff --git a/SpriteCompiler/SpriteCompiler.csproj b/SpriteCompiler/SpriteCompiler.csproj index 3b50ec8..9693a1c 100644 --- a/SpriteCompiler/SpriteCompiler.csproj +++ b/SpriteCompiler/SpriteCompiler.csproj @@ -66,6 +66,7 @@ +