1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
|
using Godosa.Core.Runs.Scripts;
namespace Godosa.Core.Tests.Runs.Scripts;
/// <summary>Scriptable game for runner tests: probes are a dictionary, actions are lambdas, input is recorded with the
/// frame it arrived on.</summary>
public sealed class FakeGame : IRunGame, IInputRouter
{
public long Frame { get; private set; }
public List<(long Frame, InputEvent Event)> Sent { get; } = [];
public Dictionary<string, Func<IReadOnlyList<string>, RunValue>> ProbeFns { get; } = new();
public Dictionary<string, IRunAction> ActionTable { get; } = new();
public Dictionary<string, (int X, int Y)> Objects { get; } = new();
public Dictionary<string, long> ConstantTable { get; } = new() { ["accepted"] = 2 };
public Action<long>? 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<string, IProbe> Probes => ProbeFns.ToDictionary(p => p.Key, p => (IProbe)new FnProbe(p.Value));
public IReadOnlyDictionary<string, long> Constants => ConstantTable;
public IReadOnlyDictionary<string, ILocator> Locators => new Dictionary<string, ILocator> { ["obj"] = new ObjLocator(this) };
public IReadOnlyDictionary<string, IRunAction> Actions => ActionTable;
public IRunRng? Rng => RngImpl;
public bool SupportsSnapshot { get; set; } = true;
public bool RestoreDeferred { get; set; }
public Action<InputEvent>? 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<IReadOnlyList<string>, RunValue> fn) : IProbe
{
public int MinArgs => 0;
public int MaxArgs => 4;
public RunValue Eval(IReadOnlyList<string> args) => fn(args);
}
private sealed class ObjLocator(FakeGame g) : ILocator
{
public LocatorResult Locate(IReadOnlyList<string> 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<string> Calls { get; } = [];
public void Seed(long seed) => Calls.Add($"seed {seed}");
public void ForceNext(string kind, long value) => Calls.Add($"next {kind} {value}");
}
/// <summary>An action: optional input plan, then Poll() answers from a function of the frames polled so far.</summary>
public sealed class FakeAction(VerbKind kind, Func<IReadOnlyList<string>, ActionStart> begin, Func<int, StepStatus>? 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<IReadOnlyList<string>> Begun { get; } = [];
public ActionStart Begin(IReadOnlyList<string> args)
{
Begun.Add(args);
_polls = 0;
return begin(args);
}
public StepStatus Poll() => (poll ?? (_ => StepStatus.Done))(++_polls);
}
|