using Godosa.Core.Runs.Scripts;
namespace Godosa.Core.Tests.Runs.Scripts;
/// Scriptable game for runner tests: probes are a dictionary, actions are lambdas, input is recorded with the
/// frame it arrived on.
public sealed class FakeGame : IRunGame, IInputRouter
{
public long Frame { get; private set; }
public List<(long Frame, InputEvent Event)> Sent { get; } = [];
public Dictionary, RunValue>> ProbeFns { get; } = new();
public Dictionary ActionTable { get; } = new();
public Dictionary Objects { get; } = new();
public Dictionary ConstantTable { get; } = new() { ["accepted"] = 2 };
public Action? OnAdvance { get; set; }
public int State { get; set; }
public FakeRng? RngImpl { get; set; } = new();
public string Name => "fake";
public string ContentVersion { get; set; } = "c1";
public IInputRouter Input => this;
public IReadOnlyDictionary Probes => ProbeFns.ToDictionary(p => p.Key, p => (IProbe)new FnProbe(p.Value));
public IReadOnlyDictionary Constants => ConstantTable;
public IReadOnlyDictionary Locators => new Dictionary { ["obj"] = new ObjLocator(this) };
public IReadOnlyDictionary Actions => ActionTable;
public IRunRng? Rng => RngImpl;
public bool SupportsSnapshot { get; set; } = true;
public bool RestoreDeferred { get; set; }
public Action? OnSend { get; set; }
public bool RestoreThrows { get; set; }
public string? Refusal { get; set; }
public string? SnapshotRefusal() => Refusal;
public void Send(InputEvent e)
{
OnSend?.Invoke(e);
Sent.Add((Frame, e));
}
public byte[] SnapshotSave() => BitConverter.GetBytes(State).Concat(BitConverter.GetBytes(Frame)).ToArray();
public void SnapshotRestore(byte[] data)
{
if (RestoreThrows)
throw new IOException("restore broke");
State = BitConverter.ToInt32(data, 0);
Frame = BitConverter.ToInt64(data, 4);
}
public int Digests { get; private set; }
public ulong Digest() { Digests++; return (ulong)State * 1_000_003UL + (ulong)Frame; }
public string Dump() => $"state={State} frame={Frame}";
public void Advance()
{
Frame++;
OnAdvance?.Invoke(Frame);
}
private sealed class FnProbe(Func, RunValue> fn) : IProbe
{
public int MinArgs => 0;
public int MaxArgs => 4;
public RunValue Eval(IReadOnlyList args) => fn(args);
}
private sealed class ObjLocator(FakeGame g) : ILocator
{
public LocatorResult Locate(IReadOnlyList args) =>
g.Objects.TryGetValue(args[0], out var p) ? LocatorResult.Ok(p.X, p.Y) : LocatorResult.Fail($"no object {args[0]} on screen");
}
}
public sealed class FakeRng : IRunRng
{
public List Calls { get; } = [];
public void Seed(long seed) => Calls.Add($"seed {seed}");
public void ForceNext(string kind, long value) => Calls.Add($"next {kind} {value}");
}
/// An action: optional input plan, then Poll() answers from a function of the frames polled so far.
public sealed class FakeAction(VerbKind kind, Func, ActionStart> begin, Func? poll = null,
int min = 0, int max = 4) : IRunAction
{
private int _polls;
public VerbKind Kind => kind;
public int MinArgs => min;
public int MaxArgs => max;
public List> Begun { get; } = [];
public ActionStart Begin(IReadOnlyList args)
{
Begun.Add(args);
_polls = 0;
return begin(args);
}
public StepStatus Poll() => (poll ?? (_ => StepStatus.Done))(++_polls);
}