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); }