mirror of
https://github.com/lscharen/iigs-sprite-compiler.git
synced 2024-12-11 11:49:21 +00:00
Flesh out the sprite compiler state and successor functions. A bug exists that is causing no progress to be made resulting in an eventual OOM exception
This commit is contained in:
parent
943eb36acb
commit
e00ae24ab0
@ -3,6 +3,7 @@ using System.Linq;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
using SpriteCompiler.Problem;
|
||||
using SpriteCompiler.AI;
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace SpriteCompiler.Test
|
||||
{
|
||||
@ -22,5 +23,51 @@ namespace SpriteCompiler.Test
|
||||
// Assert : The initial state IS the goal state
|
||||
Assert.AreEqual(1, solution.Count());
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void TestSingleByteSprite()
|
||||
{
|
||||
// Arrange
|
||||
var problem = SpriteGeneratorSearchProblem.CreateSearchProblem();
|
||||
var search = SpriteGeneratorSearchProblem.Create();
|
||||
|
||||
// Act : solve the problem
|
||||
var solution = search.Search(problem, new SpriteGeneratorState(new byte[] { 0xAA }));
|
||||
|
||||
// Assert
|
||||
//
|
||||
// The fastest way to draw a single byte at the current location should be
|
||||
//
|
||||
// TCS
|
||||
// SHORT A
|
||||
// LDA #$AA
|
||||
// STA 0,s
|
||||
// LONG A = 14 cycles
|
||||
|
||||
Assert.AreEqual(5, solution.Count());
|
||||
Assert.AreEqual(14, solution.First().PathCost);
|
||||
|
||||
// Write out the solution
|
||||
foreach (var step in solution)
|
||||
{
|
||||
Trace.WriteLine(step.Action.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
[TestMethod]
|
||||
public void TestSingleWordSprite()
|
||||
{
|
||||
// Arrange
|
||||
var problem = SpriteGeneratorSearchProblem.CreateSearchProblem();
|
||||
var search = SpriteGeneratorSearchProblem.Create();
|
||||
|
||||
// Act : solve the problem
|
||||
var solution = search.Search(problem, new SpriteGeneratorState());
|
||||
|
||||
// Assert : The initial state IS the goal state
|
||||
Assert.AreEqual(1, solution.Count());
|
||||
}
|
||||
*/
|
||||
}
|
||||
}
|
||||
|
@ -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>
|
||||
where C : IPathCost<C>, new()
|
||||
{
|
||||
public int Compare(T x, T y)
|
||||
{
|
||||
|
@ -2,7 +2,7 @@
|
||||
{
|
||||
public class AStarSearch<A, S, T, C> : BestFirstSearch<A, S, T, C>
|
||||
where T : HeuristicSearchNode<A, S, T, C>
|
||||
where C : IPathCost<C>
|
||||
where C : IPathCost<C>, new()
|
||||
{
|
||||
public AStarSearch(AbstractAISearch<A, S, T, C> search)
|
||||
: base(search) // , new AStarComparator<A, S, T, C>())
|
||||
|
@ -5,14 +5,14 @@
|
||||
|
||||
public abstract class AbstractSearchNode<A, S, T, C> : ISearchNode<A, S, T, C>
|
||||
where T : ISearchNode<A, S, T, C>
|
||||
where C : IPathCost<C>
|
||||
where C : IPathCost<C>, new()
|
||||
{
|
||||
protected readonly S state;
|
||||
protected readonly T parent;
|
||||
protected A action = default(A);
|
||||
|
||||
protected C pathCost = default(C);
|
||||
protected C stepCost = default(C);
|
||||
protected C pathCost = new C();
|
||||
protected C stepCost = new C();
|
||||
protected readonly int depth;
|
||||
|
||||
public AbstractSearchNode(T node, S state)
|
||||
|
@ -4,7 +4,7 @@
|
||||
|
||||
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>
|
||||
where C : IPathCost<C>, new()
|
||||
{
|
||||
public HeuristicSearchNode(T node, S state)
|
||||
: base(node, state)
|
||||
|
@ -5,7 +5,7 @@ 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>
|
||||
where C : IPathCost<C>, new()
|
||||
{
|
||||
public abstract T CreateNode(T parent, S state);
|
||||
|
||||
|
@ -9,9 +9,113 @@ namespace SpriteCompiler.Problem
|
||||
/// <summary>
|
||||
/// A set of code sequences that can be used to generate sprites
|
||||
/// </summary>
|
||||
public class CodeSequence
|
||||
public abstract class CodeSequence
|
||||
{
|
||||
protected CodeSequence(int cycles)
|
||||
{
|
||||
CycleCount = cycles;
|
||||
}
|
||||
|
||||
// Number of cycles that this code snippets takes to execute
|
||||
public int CycleCount { get; }
|
||||
public int CycleCount { get; private set; }
|
||||
|
||||
// Function to generate a new state based on the code's operation
|
||||
public abstract SpriteGeneratorState Apply(SpriteGeneratorState state);
|
||||
|
||||
// Helper function for ToString implementations
|
||||
protected string FormatLine(string label, string opcode, string operand, string comment)
|
||||
{
|
||||
return String.Format("{0}\t{1}\t{2}\t; {3}");
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class MOVE_STACK : CodeSequence
|
||||
{
|
||||
private readonly int offset;
|
||||
|
||||
public MOVE_STACK(int offset)
|
||||
: base(offset == 0 ? 2 : 5)
|
||||
{
|
||||
this.offset = offset;
|
||||
}
|
||||
|
||||
public override SpriteGeneratorState Apply(SpriteGeneratorState state)
|
||||
{
|
||||
return state.Clone(_ =>
|
||||
{
|
||||
_.A = _.A.Add(offset);
|
||||
_.S = _.A;
|
||||
});
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
if (offset == 0)
|
||||
{
|
||||
return FormatLine("", "TCS", "", "2 cycles");
|
||||
}
|
||||
else
|
||||
{
|
||||
return String.Join("\n",
|
||||
FormatLine("", "ADC", "#" + offset.ToString(), "3 cycles"),
|
||||
FormatLine("", "TCS", "", "2 cycles")
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class SHORT_M : CodeSequence
|
||||
{
|
||||
public SHORT_M() : base(3) { }
|
||||
|
||||
public override SpriteGeneratorState Apply(SpriteGeneratorState state)
|
||||
{
|
||||
return state.Clone(_ => _.P |= 0x10);
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return FormatLine("", "SEP", "#$10", "3 cycles");
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class LONG_M : CodeSequence
|
||||
{
|
||||
public LONG_M() : base(3) { }
|
||||
|
||||
public override SpriteGeneratorState Apply(SpriteGeneratorState state)
|
||||
{
|
||||
return state.Clone(_ => _.P &= 0xEF);
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return FormatLine("", "REP", "#$10", "3 cycles");
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class STACK_REL_8_BIT_IMMEDIATE_STORE : CodeSequence
|
||||
{
|
||||
private readonly byte value;
|
||||
private readonly byte offset;
|
||||
|
||||
public STACK_REL_8_BIT_IMMEDIATE_STORE(byte value, byte offset) : base(6) { this.value = value; this.offset = offset; }
|
||||
|
||||
public override SpriteGeneratorState Apply(SpriteGeneratorState state)
|
||||
{
|
||||
return state.Clone(_ =>
|
||||
{
|
||||
_.A = _.A.LoadConstant((_.A.Value & 0xFF00) | value);
|
||||
_.RemoveByte((ushort)(offset + _.S.Value));
|
||||
});
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return String.Join("\n",
|
||||
FormatLine("", "LDA", "#$" + value.ToString("X2"), "2 cycles"),
|
||||
FormatLine("", "STA", offset.ToString("X2") + ",s", "4 cycles")
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
32
SpriteCompiler/Problem/SpriteByte.cs
Normal file
32
SpriteCompiler/Problem/SpriteByte.cs
Normal file
@ -0,0 +1,32 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace SpriteCompiler.Problem
|
||||
{
|
||||
public struct SpriteByte
|
||||
{
|
||||
private static byte DataToMask(byte data)
|
||||
{
|
||||
return (byte)((((data & 0xF0) == 0x00) ? 0xF0 : 0x00) | (((data & 0x0F) == 0x00) ? 0x0F : 0x00));
|
||||
}
|
||||
|
||||
public SpriteByte(byte data, ushort offset)
|
||||
: this(data, DataToMask(data), offset)
|
||||
{
|
||||
}
|
||||
|
||||
public SpriteByte(byte data, byte mask, ushort offset)
|
||||
{
|
||||
Data = data;
|
||||
Mask = mask;
|
||||
Offset = offset;
|
||||
}
|
||||
|
||||
public byte Data { get; private set; }
|
||||
public byte Mask { get; private set; }
|
||||
public ushort Offset { get; private set; }
|
||||
}
|
||||
}
|
@ -11,6 +11,11 @@ namespace SpriteCompiler.Problem
|
||||
{
|
||||
private readonly int value;
|
||||
|
||||
public IntegerPathCost()
|
||||
: this(0)
|
||||
{
|
||||
}
|
||||
|
||||
private IntegerPathCost(int value)
|
||||
{
|
||||
this.value = value;
|
||||
@ -40,14 +45,125 @@ namespace SpriteCompiler.Problem
|
||||
public class SpriteGeneratorState
|
||||
{
|
||||
public SpriteGeneratorState()
|
||||
: this(new SpriteByte[0])
|
||||
{
|
||||
}
|
||||
|
||||
public SpriteGeneratorState(byte[][] data, byte[][] mask)
|
||||
public SpriteGeneratorState(byte[] data)
|
||||
: this(data.Select((x, i) => new SpriteByte(x, (ushort)i)))
|
||||
{
|
||||
}
|
||||
|
||||
public bool IsEmpty { get { return true; } }
|
||||
public SpriteGeneratorState(IEnumerable<SpriteByte> bytes)
|
||||
{
|
||||
Bytes = bytes.ToList();
|
||||
|
||||
// Initialize the CPU state
|
||||
A = Register.INITIAL_OFFSET; // the address to draw the sprite is passed in, this is a run-time value
|
||||
|
||||
X = Register.UNINITIALIZED; // the other registered are also undefined
|
||||
Y = Register.UNINITIALIZED;
|
||||
D = Register.UNINITIALIZED;
|
||||
S = Register.UNINITIALIZED;
|
||||
|
||||
P = 0x30; // Start in native mode (16 bit A/X/Y) with the carry clear
|
||||
}
|
||||
|
||||
private SpriteGeneratorState(SpriteGeneratorState other)
|
||||
{
|
||||
Bytes = new List<SpriteByte>(other.Bytes);
|
||||
A = other.A;
|
||||
X = other.X;
|
||||
Y = other.Y;
|
||||
D = other.D;
|
||||
S = other.S;
|
||||
P = other.P;
|
||||
}
|
||||
|
||||
public void RemoveByte(ushort offset)
|
||||
{
|
||||
var total = Bytes.RemoveAll(x => x.Offset == offset);
|
||||
if (total != 1)
|
||||
{
|
||||
throw new ArgumentException(string.Format("Cannot remove: {0}", total));
|
||||
}
|
||||
}
|
||||
|
||||
public SpriteGeneratorState Clone(Action<SpriteGeneratorState> f = null)
|
||||
{
|
||||
var other = new SpriteGeneratorState(this);
|
||||
|
||||
f?.Invoke(other);
|
||||
|
||||
return other;
|
||||
}
|
||||
|
||||
public List<SpriteByte> Bytes { get; private set; }
|
||||
public bool IsEmpty { get { return Bytes.Count == 0; } }
|
||||
|
||||
public bool LongA { get { return (P & 0x10) == 0x10; } }
|
||||
public bool LongI { get { return (P & 0x20) == 0x20; } }
|
||||
|
||||
// Maintain the state of the execution
|
||||
public Register A { get; set; } // Nullable because unknown values can be passed through the accumulator
|
||||
public Register X { get; set; }
|
||||
public Register Y { get; set; }
|
||||
public Register D { get; set; }
|
||||
public Register S { get; set; } // S is always an offset, not a literal number
|
||||
|
||||
public byte P { get; set; }
|
||||
}
|
||||
|
||||
public class Register
|
||||
{
|
||||
public static readonly Register UNINITIALIZED = new Register(0, DataType.UNINITIALIZED);
|
||||
public static readonly Register INITIAL_OFFSET = new Register(0, DataType.SCREEN_OFFSET);
|
||||
|
||||
public enum DataType
|
||||
{
|
||||
UNINITIALIZED,
|
||||
SCREEN_OFFSET,
|
||||
LITERAL
|
||||
}
|
||||
|
||||
private Register(int value, DataType tag)
|
||||
{
|
||||
Value = value;
|
||||
Tag = tag;
|
||||
}
|
||||
|
||||
public Register Clone()
|
||||
{
|
||||
return new Register(Value, Tag);
|
||||
}
|
||||
|
||||
public Register Add(int offset)
|
||||
{
|
||||
if (IsUninitialized)
|
||||
{
|
||||
throw new ArgumentException("Cannot add valued to uninitialized registers");
|
||||
}
|
||||
|
||||
// Adding a value does not change the tag
|
||||
return new Register(Value + offset, Tag);
|
||||
}
|
||||
|
||||
public Register LoadConstant(int value)
|
||||
{
|
||||
return new Register(value, DataType.LITERAL);
|
||||
}
|
||||
|
||||
public bool IsUninitialized { get { return DataType.UNINITIALIZED.Equals(Tag); } }
|
||||
public bool IsScreenOffset { get { return DataType.SCREEN_OFFSET.Equals(Tag); } }
|
||||
public bool IsLiteral { get { return DataType.LITERAL.Equals(Tag); } }
|
||||
|
||||
public DataType Tag { get; private set; }
|
||||
public int Value { get; private set; }
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return string.Format("{0} ({1})", Tag, Value.ToString("X4"));
|
||||
}
|
||||
}
|
||||
|
||||
public class SpriteGeneratorStepCost : IStepCostFunction<CodeSequence, SpriteGeneratorState, IntegerPathCost>
|
||||
@ -71,7 +187,56 @@ namespace SpriteCompiler.Problem
|
||||
{
|
||||
public IDictionary<CodeSequence, SpriteGeneratorState> Successors(SpriteGeneratorState state)
|
||||
{
|
||||
return new Dictionary<CodeSequence, SpriteGeneratorState>();
|
||||
var actions = new List<CodeSequence>();
|
||||
|
||||
// If the accumulator holds an offset then we could move to any byte position. We do
|
||||
// not allow two consecutive MOVE_STACK operations
|
||||
if (state.A.IsScreenOffset && !state.S.IsScreenOffset)
|
||||
{
|
||||
foreach (var datum in state.Bytes)
|
||||
{
|
||||
actions.Add(new MOVE_STACK(datum.Offset - state.A.Value));
|
||||
}
|
||||
}
|
||||
|
||||
// If the accumulator and stack are both initialized, only propose moves to locations
|
||||
// before and after the current 256 byte window
|
||||
if (state.A.IsScreenOffset && state.S.IsScreenOffset)
|
||||
{
|
||||
var addr = state.S.Value;
|
||||
foreach (var datum in state.Bytes.Where(x => (x.Offset - addr) > 255 || (x.Offset - addr) < 0))
|
||||
{
|
||||
actions.Add(new MOVE_STACK(datum.Offset - state.A.Value));
|
||||
}
|
||||
}
|
||||
|
||||
// It is always permissible to move to/from 16 bit mode
|
||||
if (state.LongA)
|
||||
{
|
||||
actions.Add(new SHORT_M());
|
||||
|
||||
// Add any possible 16-bit data manipulations
|
||||
}
|
||||
else
|
||||
{
|
||||
actions.Add(new LONG_M());
|
||||
|
||||
// Add any possible 8-bit manipulations
|
||||
if (state.S.IsScreenOffset)
|
||||
{
|
||||
var addr = state.S.Value;
|
||||
|
||||
// We can LDA #$XX / STA X,s for any values within 256 bytes of the curren address
|
||||
foreach (var datum in state.Bytes.Where(x => (x.Offset - addr) < 255))
|
||||
{
|
||||
var offset = datum.Offset - addr;
|
||||
actions.Add(new STACK_REL_8_BIT_IMMEDIATE_STORE((byte)offset, datum.Data));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Run through the actions to create a dictionary
|
||||
return actions.ToDictionary(x => x, x => x.Apply(state));
|
||||
}
|
||||
}
|
||||
|
||||
@ -89,6 +254,11 @@ namespace SpriteCompiler.Problem
|
||||
: base(node, state)
|
||||
{
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return (action == null) ? "NO ACTION" : action.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
public class SpriteGeneratorNodeExpander : InformedNodeExpander<CodeSequence, SpriteGeneratorState, SpriteGeneratorSearchNode, IntegerPathCost>
|
||||
@ -113,7 +283,7 @@ namespace SpriteCompiler.Problem
|
||||
|
||||
public static ISearch<CodeSequence, SpriteGeneratorState, SpriteGeneratorSearchNode, IntegerPathCost> Create()
|
||||
{
|
||||
var problem = CreateSearchProblem();
|
||||
//var problem = CreateSearchProblem();
|
||||
var strategy = new TreeSearch<CodeSequence, SpriteGeneratorState, SpriteGeneratorSearchNode, IntegerPathCost>(new SpriteGeneratorNodeExpander());
|
||||
|
||||
return new AStarSearch<CodeSequence, SpriteGeneratorState, SpriteGeneratorSearchNode, IntegerPathCost>(strategy);
|
||||
|
@ -69,6 +69,7 @@
|
||||
<Compile Include="AI\GraphSearch.cs" />
|
||||
<Compile Include="AI\TreeSearch.cs" />
|
||||
<Compile Include="Problem\CodeSequence.cs" />
|
||||
<Compile Include="Problem\SpriteByte.cs" />
|
||||
<Compile Include="Problem\SpriteGeneratorSearchProblem.cs" />
|
||||
<Compile Include="Program.cs" />
|
||||
<Compile Include="Properties\AssemblyInfo.cs" />
|
||||
|
Loading…
Reference in New Issue
Block a user