Simplify success function and improve heuristic accuracy

This commit is contained in:
Lucas Scharenbroich 2016-12-16 22:52:14 -06:00
parent df2318db29
commit 53561fbcc8
7 changed files with 510 additions and 189 deletions

View File

@ -0,0 +1,47 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace SpriteCompiler.AI
{
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;
}
}
}

View File

@ -0,0 +1,85 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace SpriteCompiler.Helpers
{
public static class EnumerableExtensions
{
public static IEnumerable<Tuple<T, T>> Pairs<T>(this IEnumerable<T> source)
{
return source.Zip(source.Skip(1), (x, y) => Tuple.Create(x, y));
}
public static IEnumerable<IGrouping<T, T>> GroupAdjacent<T>(this IEnumerable<T> source, Func<T, T, bool> comparer)
{
return GroupAdjacent(source, x => x, comparer);
}
public static IEnumerable<IGrouping<TKey, TSource>> GroupAdjacent<TSource, TKey>(this IEnumerable<TSource> source, Func<TSource, TKey> keySelector)
{
return GroupAdjacent(source, keySelector, (a, b) => a.Equals(b));
}
public static IEnumerable<IGrouping<TKey, TSource>> GroupAdjacent<TSource, TKey>(this IEnumerable<TSource> source, Func<TSource, TKey> keySelector, Func<TKey, TKey, bool> comparer)
{
TKey last = default(TKey);
bool haveLast = false;
List<TSource> list = new List<TSource>();
foreach (TSource s in source)
{
TKey k = keySelector(s);
if (haveLast)
{
if (!comparer(last, k))
{
yield return new GroupOfAdjacent<TSource, TKey>(list, last);
list = new List<TSource>();
list.Add(s);
last = k;
}
else
{
list.Add(s);
last = k;
}
}
else
{
list.Add(s);
last = k;
haveLast = true;
}
}
if (haveLast)
{
yield return new GroupOfAdjacent<TSource, TKey>(list, last);
}
}
}
public class GroupOfAdjacent<TSource, TKey> : IEnumerable<TSource>, IGrouping<TKey, TSource>
{
public TKey Key { get; set; }
private List<TSource> GroupList { get; set; }
System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
{
return ((System.Collections.Generic.IEnumerable<TSource>)this).GetEnumerator();
}
System.Collections.Generic.IEnumerator<TSource> System.Collections.Generic.IEnumerable<TSource>.GetEnumerator()
{
foreach (var s in GroupList)
yield return s;
}
public GroupOfAdjacent(List<TSource> source, TKey key)
{
GroupList = source;
Key = key;
}
}
}

View File

@ -102,7 +102,11 @@ namespace SpriteCompiler.Problem
public override SpriteGeneratorState Apply(SpriteGeneratorState state)
{
return state.Clone(_ => _.P &= 0xEF);
return state.Clone(_ =>
{
_.P &= 0xEF;
_.AllowModeChange = false;
});
}
public override string ToString()
@ -122,7 +126,11 @@ namespace SpriteCompiler.Problem
public override SpriteGeneratorState Apply(SpriteGeneratorState state)
{
return state.Clone(_ => _.P |= 0x10);
return state.Clone(_ =>
{
_.P |= 0x10;
_.AllowModeChange = false;
});
}
public override string ToString()
@ -224,6 +232,40 @@ namespace SpriteCompiler.Problem
}
}
public sealed class STACK_REL_8_BIT_READ_MODIFY_PUSH : CodeSequence
{
private readonly byte value;
private readonly byte mask;
public STACK_REL_8_BIT_READ_MODIFY_PUSH(byte value, byte mask) : base(11) { this.value = value; this.mask = mask; }
public override SpriteGeneratorState Apply(SpriteGeneratorState state)
{
return state.Clone(_ =>
{
_.A = Register.UNINITIALIZED;
_.RemoveByte((ushort)_.S.Value);
_.S.Add(-1);
});
}
public override string ToString()
{
return "LDA 0,s / AND #$" + mask.ToString("X2") + " / ORA #$" + value.ToString("X2") + " / PHA";
}
public override string Emit()
{
return String.Join("\n",
FormatLine("", "LDA", "0,s", "4 cycles"),
FormatLine("", "AND", "#$" + mask.ToString("X2"), "2 cycles"),
FormatLine("", "ORA", "#$" + value.ToString("X2"), "2 cycles"),
FormatLine("", "PHA", "", "3 cycles")
);
}
}
public sealed class STACK_REL_16_BIT_STORE : CodeSequence
{
private readonly byte offset;

View File

@ -1,8 +1,10 @@
namespace SpriteCompiler.Problem
{
using SpriteCompiler.AI;
using SpriteCompiler.Helpers;
using System.Linq;
using System;
using System.Collections.Generic;
public sealed class SpriteGeneratorHeuristicFunction : IHeuristicFunction<SpriteGeneratorState, IntegerCost>
{
@ -39,6 +41,29 @@
return gapCost + spanCost;
}
private const int MASKED_DATA = 0;
private const int SOLID_DATA = 1;
private int ClassifyChunk(IEnumerable<SpriteByte> chunk)
{
return (chunk.First().Mask == 0x00) ? SOLID_DATA : MASKED_DATA;
}
private bool AreEquivalent(SpriteByte left, SpriteByte right)
{
// Return true if the two bytes are in an equivalence class
return
// The have to be adjacent
((right.Offset - left.Offset) == 1) &&
(
// First case: Both bytes are solid
((left.Mask == 0x00) && (right.Mask == 0x00)) ||
// Second case: Both bytes are masked
((left.Mask != 0x00) && (right.Mask != 0x00))
);
}
public IntegerCost Eval(SpriteGeneratorState state)
{
// An admissible heuistic that calculates a cost based on the gaps and runs in a sprite
@ -46,40 +71,62 @@
// An even-length run can be done, at best in 4 cycles/word
// An odd-length run is even + 3 cycles/byte
if (state.IsEmpty) return 0;
var offsets = state.RemainingBytes();
var start = offsets[0];
var stack = state.S.Value;
var curr = start;
var cost = 0;
for (int i = 1; i < offsets.Count; i++)
// Easy case -- no data means no code
if (state.IsEmpty)
{
var prev = curr;
curr = offsets[i];
if (prev == (curr - 1))
{
continue;
}
// Calculate the estimate cost
if (state.S.IsScreenOffset)
{
cost += SpanAndGapCost(stack, start, prev, curr);
}
else
{
cost += SpanAndGapCost(start, prev, curr);
}
// Start a new sppan
start = curr;
return 0;
}
// End with the span
cost += SpanAndGapCost(start, curr, curr);
// Get a list of all the bytes that have not been emitted
var remaining = state.RemainingBytes();
// Count the number of
//
// solid words
// solid bytes
// masked words
// masked bytes
//
// By grouping the remaining bytes into solid / masked runs
var chunks = remaining.GroupAdjacent((x, y) => AreEquivalent(x, y));
// Figure out the full range of offsets that need to be written
var range = remaining.Last().Offset - remaining.First().Offset;
// At best, we can write 257 bytes before needing to move the stack (0,s to 16-bit save to ff,s).
var minStackMoves = range / 257;
// Calculate a heuristic
var cost = 0;
// If the stack is undefined, then there is at least a 2 cycle
// cost to initialize it
if (!state.S.IsScreenOffset)
{
cost += 2;
}
cost += minStackMoves * 5;
// Iterate through each chunk, determine which equivalence class the chunk is
// in and then assign a minimum score
foreach (var chunk in chunks)
{
var len = chunk.Count();
switch (ClassifyChunk(chunk))
{
// Solid data costs at least 4 cycles / word + 3 cycles for a byte
case SOLID_DATA:
cost += 4 * (len / 2) + 3 * (len % 2);
break;
// Masked data costs at least 16 cycles / word + 11 cycles for a byte
case MASKED_DATA:
cost += 16 * (len / 2) + 11 * (len % 2);
break;
}
}
return cost;
}

View File

@ -10,11 +10,31 @@
public static List<SpriteByte> DATASET = null;
public static IDictionary<int, SpriteByte> DATASET_BY_OFFSET = null;
// Histogram of the byte data
public static IDictionary<byte, int> DATASET_SOLID_BYTES = null;
// Histogram of all possible words -- includes overlaps, e.g. $11 $11 $11 $11 = ($1111, 3)
public static IDictionary<ushort, int> DATASET_SOLID_WORDS = null;
public static SpriteGeneratorState Init(IEnumerable<SpriteByte> bytes)
{
DATASET = bytes.ToList();
DATASET = bytes.OrderBy(x => x.Offset).ToList();
DATASET_BY_OFFSET = DATASET.ToDictionary(x => (int)x.Offset, x => x);
DATASET_SOLID_BYTES = DATASET
.Where(x => x.Mask == 0x00)
.GroupBy(x => x.Data)
.ToDictionary(x => x.Key, x => x.Count())
;
DATASET_SOLID_WORDS = DATASET
.Zip(DATASET.Skip(1), (x, y) => new { Left = x, Right = y })
.Where(x => (x.Left.Offset == (x.Right.Offset - 1)) && x.Left.Mask == 0x00 && x.Right.Mask == 0x00)
.Select(x => (x.Right.Data << 8) | x.Left.Data)
.GroupBy(x => x)
.ToDictionary(x => (ushort)x.Key, x => x.Count())
;
return new SpriteGeneratorState();
}
@ -44,6 +64,8 @@
S = Register.UNINITIALIZED;
P = 0x30; // Start in native mode (16 bit A/X/Y) with the carry clear
AllowModeChange = true;
}
private SpriteGeneratorState(SpriteGeneratorState other)
@ -55,6 +77,7 @@
D = other.D;
S = other.S;
P = other.P;
AllowModeChange = other.AllowModeChange;
}
public override string ToString()
@ -66,18 +89,19 @@
{
Closed.Add(offset);
Closed.Add((ushort)(offset + 1));
AllowModeChange = true;
}
public void RemoveByte(ushort offset)
{
Closed.Add(offset);
AllowModeChange = true;
}
public List<ushort> RemainingBytes()
public List<SpriteByte> RemainingBytes()
{
return DATASET
.Select(x => x.Offset)
.Where(x => !Closed.Contains(x))
.Where(x => !Closed.Contains(x.Offset))
.ToList();
}
@ -112,6 +136,12 @@
public byte P { get; set; }
// Flag that is cleared whenever there is a switch from
// 8/16-bit mode. It is reset once a PHA or STA occurs.
// A PEA instruction has no effect. This gates allowable
// state transition to prevent long REP/SEP seqences.
public bool AllowModeChange { get; set; }
public const byte LONG_A = 0x10;
public const byte LONG_I = 0x20;
@ -129,7 +159,8 @@
Y.Equals(other.Y) &&
D.Equals(other.D) &&
S.Equals(other.S) &&
P.Equals(other.P)
P.Equals(other.P) &&
AllowModeChange == other.AllowModeChange
;
}
@ -141,7 +172,8 @@
Y.GetHashCode() +
D.GetHashCode() +
S.GetHashCode() +
P.GetHashCode()
P.GetHashCode() +
AllowModeChange.GetHashCode()
;
}

View File

@ -1,6 +1,7 @@
namespace SpriteCompiler.Problem
{
using SpriteCompiler.AI;
using SpriteCompiler.Helpers;
using System;
using System.Collections.Generic;
using System.Linq;
@ -77,168 +78,120 @@
{
public IEnumerable<Tuple<CodeSequence, SpriteGeneratorState>> Successors(SpriteGeneratorState state)
{
// This is the work-horse of the compiler. For a given state we need to enumerate all of the
// potential next operations.
//
// 1. If there are 16-bits of data at then current offset, we can
// a. Use one of the cached valued in A/X/Y/D if they match (4 cycles)
// b. Use a PEA to push immediate values (5 cycles)
// c. Load a value into A/X/Y and then push (7 cycles, only feasible if the value appears elsewhere in the sprite)
// d. Load the value into D and then push (9 cycles, and leaves A = D)
//
// 2. Move the stack
// a. Add a value directly (7 cycles, A = unknown)
// b. Skip 1 byte (6 cycles, A = unknown TSC/DEC/TSC)
// c. Multiple skips (LDA X,s/AND/ORA/STA = 16/byte, ADC #/TCS/LDX #/PHX = 10/byte
//
// 3. Single-byte at the end of a solid run
// a. If no registers are 8-bit, LDA #Imm/STA 0,s (8 cycles, sets Acc)
// b. If any reg is already 8-bit, LDA #imm/PHA (6 cycles)
//
// We always try to return actions that write data since that moves us toward the goal state
// Get the list of remaining bytes by removing the closed list from the global sprite dataset
var open = state.RemainingBytes();
// If the open list is empty, there can be only one reaons -- we're in an 8-bit
// mode.
if (!open.Any())
{
yield return state.Apply(new LONG_M());
yield break;
}
// Get the first byte -- if we are expanding a state we can safely assume there
// is still data to expand.
var firstByte = open.First();
// Identify the first run of solid bytes
var firstSolid = FirstSolidRun(open);
// In an initial state, the stack has not been set, but the accumulator contains a screen
// offset address. There are three possible options
//
// 1. Set the stack to the accumulator value. This is optimal when there are very few
// data bytes to set and the overhead of moving the stack negates any benefit from
// using stack push instructions
//
// 2. Set the stack to the first offset of the open set. This can be optimal if there are a few
// bytes and moving the stack forward a bit can allow the code to reach them without needing
// a second stack adjustment.
//
// 3. Set the stack to the first, right-most offset that end a sequence of solid bytes
if (!state.S.IsScreenOffset && state.A.IsScreenOffset)
{
// If the first byte is within 255 bytes of the accumulator, propose setting
// the stack to the accumulator value
var delta = firstByte.Offset - state.A.Value;
if (delta >= 0 && delta < 256)
{
yield return state.Apply(new MOVE_STACK(0));
}
// If the first byte offset is not equal to the accumulator value, propose that
if (delta > 0)
{
yield return state.Apply(new MOVE_STACK(delta));
}
// Find the first edge of a solid run....TODO
if (firstSolid != null && firstSolid.Count >= 2)
{
yield return state.Apply(new MOVE_STACK(firstSolid.Last.Offset - state.A.Value));
}
yield break;
}
// If the first byte is 256 bytes or more ahead of the current stack location,
// then we need to advance
var firstByteDistance = firstByte.Offset - state.S.Value;
if (state.S.IsScreenOffset && firstByteDistance >= 256)
{
// Go to the next byte, or the first solid edge
yield return state.Apply(new MOVE_STACK(firstByteDistance));
yield break;
}
// Get the list of remaining bytes by removing the closed list from the global sprite
var open = SpriteGeneratorState.DATASET.Where(x => !state.Closed.Contains(x.Offset)).ToList();
var bytes = open.ToDictionary(x => x.Offset, x => x);
// Get the current byte and current word that exist at the current stack location
var topByte = state.TryGetStackByte(bytes);
var topWord = state.TryGetStackWord(bytes);
var nextWord = state.TryGetStackWord(bytes, -2); // Also get the next value below the current word
// If there is some data at the top of the stack, see what we can do
if (topWord.HasValue)
// If the top of the stack is a solid work, we can always emit a PEA regardless of 8/16-bit mode
if (topWord.HasValue && topWord.Value.Mask == 0x000)
{
// First, the simple case -- the data has no mask
if (topWord.Value.Mask == 0x000)
{
// If any of the registers has the exact value we need, then it is always fastest to just push the value
if (state.LongA)
{
if (state.A.IsLiteral && state.A.Value == topWord.Value.Data)
{
yield return state.Apply(new PHA_16());
}
else
{
yield return state.Apply(new PEA(topWord.Value.Data));
yield return state.Apply(new LOAD_16_BIT_IMMEDIATE_AND_PUSH(topWord.Value.Data));
}
}
// Otherwise, the only alternative is a PEA instruction
else
{
yield return state.Apply(new PEA(topWord.Value.Data));
}
}
yield return state.Apply(new PEA(topWord.Value.Data));
}
// If there is a valid byte, then we can look for an 8-bit push, or an immediate mode LDA #XX/STA 0,s
if (topByte.HasValue)
// First set of operations for when the accumulator is in 8-bit mode. We are basically limited
// to either
//
// 1. Switching to 16-bit mode
// 3. Using a PHA to push an 8-bit solid or masked value on the stack
// 4. Using a STA 0,s to store an 8-bit solid or masked value on the stack
if (!state.LongA)
{
if (topByte.Value.Mask == 0x00)
// The byte to be stored at the top of the stack has some special methods available
if (topByte.HasValue)
{
if (!state.LongA)
var datum = topByte.Value;
// Solid byte
if (datum.Mask == 0x00)
{
if (state.A.IsLiteral && ((state.A.Value & 0xFF) == topByte.Value.Data))
if (state.A.IsLiteral && ((state.A.Value & 0xFF) == datum.Data))
{
yield return state.Apply(new PHA_8());
}
else
{
yield return state.Apply(new LOAD_8_BIT_IMMEDIATE_AND_PUSH(topByte.Value.Data));
yield return state.Apply(new LOAD_8_BIT_IMMEDIATE_AND_PUSH(datum.Data));
yield return state.Apply(new STACK_REL_8_BIT_IMMEDIATE_STORE(datum.Data, 0));
}
}
}
}
// If the accumulator holds an offset then we could move to any byte position, but it is only beneficial to
// either
//
// 1. Set the stack to the current accumulator value
// 2. Set the stack to the start of a contiguous segment
// 3. Set the stack to the end of a contiguous segment
// move to the first or last byte of each span. So, take the first byte and then look for any
if (state.A.IsScreenOffset && !state.S.IsScreenOffset && state.LongA)
{
// If any of the open bytes are within 255 bytes of the accumulator, consider just
// setting the stack to the accumulator value
if (open.Any(x => (x.Offset - state.A.Value) >= 0 && (x.Offset - state.A.Value) < 256))
{
yield return state.Apply(new MOVE_STACK(0));
}
for (var i = 0; i < open.Count; i++)
{
if (i == 0)
// Masked byte
else
{
yield return state.Apply(new MOVE_STACK(open[i].Offset - state.A.Value));
continue;
}
if (i == open.Count - 1)
{
yield return state.Apply(new MOVE_STACK(open[i].Offset - state.A.Value));
continue;
}
if ((open[i].Offset - open[i - 1].Offset) > 1)
{
yield return state.Apply(new MOVE_STACK(open[i].Offset - state.A.Value));
yield return state.Apply(new STACK_REL_8_BIT_READ_MODIFY_PUSH(datum.Data, datum.Mask));
}
}
}
// It is always permissible to move to/from 16 bit mode
if (state.LongA)
{
yield return state.Apply(new SHORT_M());
// Add any possible 16-bit data manipulations
if (state.S.IsScreenOffset)
{
var addr = state.S.Value;
// Look for consecutive bytes. The second byte can come from the DATASET
var local = open.Where(WithinRangeOf(addr, 256)).ToList();
var words = local
.Where(x => SpriteGeneratorState.DATASET_BY_OFFSET.ContainsKey(x.Offset + 1))
.Select(x => new { Low = x, High = SpriteGeneratorState.DATASET_BY_OFFSET[x.Offset + 1] })
.ToList();
foreach (var word in words)
{
var offset = (byte)(word.Low.Offset - addr);
var data = (ushort)(word.Low.Data + (word.High.Data << 8));
var mask = (ushort)(word.Low.Mask + (word.High.Mask << 8));
// Easy case when mask is empty
if (mask == 0x0000)
{
if (data == state.A.Value)
{
yield return state.Apply(new STACK_REL_16_BIT_STORE(offset));
}
else
{
yield return state.Apply(new STACK_REL_16_BIT_IMMEDIATE_STORE(data, offset));
}
}
// Otherwise there is really only one choice LDA / AND / ORA / STA sequence
else
{
yield return state.Apply(new STACK_REL_16_BIT_READ_MODIFY_WRITE(data, mask, offset));
}
}
}
}
else
{
yield return state.Apply(new LONG_M());
// Add any possible 8-bit manipulations
// Otherwise, just store the next byte closest to the stack
if (state.S.IsScreenOffset)
{
var addr = state.S.Value;
@ -268,27 +221,140 @@
}
}
}
if (state.AllowModeChange)
{
yield return state.Apply(new LONG_M());
}
}
// If the accumulator and stack are both initialized, only propose moves to locations
// before and after the current 256 byte stack-relative window
if (state.S.IsScreenOffset && state.LongA)
// Now consider what can be done when the accumulator is 16-bit. All of the stack
// manipulation happens here, too.
if (state.LongA)
{
// If the accumulator already has a screen offset value, just calculate it
if (state.A.IsScreenOffset)
// Handle the special case of a value sitting right on top of the stack
if (topWord.HasValue)
{
var addr = state.S.Value;
foreach (var datum in open.Where(x => (x.Offset - addr) > 255 || (x.Offset - addr) < 0))
var datum = topWord.Value;
// First, the simple case -- the data has no mask
if (datum.Mask == 0x000)
{
yield return state.Apply(new MOVE_STACK(datum.Offset - state.A.Value));
if (state.A.IsLiteral && state.A.Value == datum.Data)
{
yield return state.Apply(new PHA_16());
}
else
{
// Only consider this if the value appear in the sprite more than once
if (SpriteGeneratorState.DATASET_SOLID_WORDS[topWord.Value.Data] > 1)
{
yield return state.Apply(new LOAD_16_BIT_IMMEDIATE_AND_PUSH(topWord.Value.Data));
}
}
}
}
// Otherwise, put the stack in the accumulator
else
// If the stack is set, find the next word to store (just one to reduce branching factor)
if (state.S.IsScreenOffset)
{
yield return state.Apply(new TSC());
var addr = state.S.Value;
var local = open.Where(WithinRangeOf(addr, 256)).ToList();
var words = local
.Where(x => SpriteGeneratorState.DATASET_BY_OFFSET.ContainsKey(x.Offset + 1))
.Select(x => new { Low = x, High = SpriteGeneratorState.DATASET_BY_OFFSET[x.Offset + 1] })
.ToList();
foreach (var word in words)
{
var offset = (byte)(word.Low.Offset - addr);
var data = (ushort)(word.Low.Data + (word.High.Data << 8));
var mask = (ushort)(word.Low.Mask + (word.High.Mask << 8));
// Easy case when mask is empty
if (mask == 0x0000)
{
if (data == state.A.Value)
{
yield return state.Apply(new STACK_REL_16_BIT_STORE(offset));
}
else
{
yield return state.Apply(new STACK_REL_16_BIT_IMMEDIATE_STORE(data, offset));
}
}
// Otherwise there is really only one choice LDA / AND / ORA / STA sequence
else
{
yield return state.Apply(new STACK_REL_16_BIT_READ_MODIFY_WRITE(data, mask, offset));
}
}
}
if (state.AllowModeChange)
{
yield return state.Apply(new SHORT_M());
}
}
Done:
var z = 0; z += 1;
}
private static bool IsSolidPair(Tuple<SpriteByte, SpriteByte> pair)
{
return
(pair.Item1.Offset == (pair.Item2.Offset - 1)) &&
pair.Item1.Mask == 0x00 &&
pair.Item2.Mask == 0x00;
}
private class SolidRun
{
private readonly SpriteByte first;
private readonly SpriteByte last;
public SolidRun(SpriteByte first, SpriteByte last)
{
this.first = first;
this.last = last;
}
public SpriteByte First { get { return first; } }
public SpriteByte Last { get { return last; } }
public int Count { get { return last.Offset - first.Offset + 1; } }
}
private SolidRun FirstSolidRun(IEnumerable<SpriteByte> open)
{
bool trigger = false;
SpriteByte first = default(SpriteByte);
SpriteByte last = default(SpriteByte);
foreach (var item in open)
{
if (item.Mask == 0x00 && !trigger)
{
first = item;
trigger = true;
}
if (item.Mask != 0x00 && trigger)
{
return new SolidRun(first, last);
}
last = item;
}
// If we get to the end and are still sold, great
if (last.Mask == 0x00 && trigger)
{
return new SolidRun(first, last);
}
return null;
}
private Func<SpriteByte, bool> WithinRangeOf(int addr, int range)

View File

@ -68,6 +68,7 @@
<Compile Include="AI\IHeuristicFunction.cs" />
<Compile Include="AI\InformedNodeExpander.cs" />
<Compile Include="AI\INodeLimiter.cs" />
<Compile Include="AI\InstrumentedNodeExpander.cs" />
<Compile Include="AI\ISearchStepInfo.cs" />
<Compile Include="AI\ISearchStrategy.cs" />
<Compile Include="AI\ISuccessorFunction.cs" />
@ -87,6 +88,7 @@
<Compile Include="AI\SearchProblem.cs" />
<Compile Include="AI\GraphSearch.cs" />
<Compile Include="AI\TreeSearch.cs" />
<Compile Include="Helpers\EnumerableExtensions.cs" />
<Compile Include="Problem\CodeSequence.cs" />
<Compile Include="AI\IntegerPathCost.cs" />
<Compile Include="Problem\Register.cs" />