aboutsummaryrefslogtreecommitdiffziptar.gz
path: root/src/Godosa.Core.Tests/Runs/Scripts/FakeGame.cs
blob: 83181480500b8d02a6521bc67b2dd7abd29c17c7 (plain)
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);
}