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
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
|
using Godosa.Core.Runs.Scripts;
using Xunit;
namespace Godosa.Core.Tests.Runs.Scripts;
public class ScriptRunnerTests
{
internal static (RunResult Result, List<string> Output) Run(string main, FakeGame game, ScriptRunOptions? options = null,
Dictionary<string, string>? files = null, Dictionary<string, string>? vars = null)
{
var all = new Dictionary<string, string>(files ?? []) { ["main.steps"] = main };
var program = ScriptLoader.Load("main.steps", new MemoryScriptSource(all), vars ?? []);
var output = new List<string>();
var runner = new ScriptRunner(program, game, options ?? new ScriptRunOptions(), output.Add);
return (runner.Run(cap: 10_000), output);
}
[Fact]
public void Click_DownAtStepFrameUpNextFrame()
{
var g = new FakeGame();
var (r, _) = Run("format 1\nmode player\nclick 100,200\nkey R", g);
Assert.Equal((RunOutcome.Passed, 0), (r.Outcome, r.ExitCode));
// Hand-derived: frame 0 (game.Frame before first Advance) move+down, frame 1 up; key starts on frame 2: down, frame 3 up.
Assert.Equal(
[
(0L, new InputEvent(InputKind.Move, 100, 200)), (0L, new InputEvent(InputKind.Down, 100, 200)),
(1L, new InputEvent(InputKind.Up, 100, 200)),
(2L, new InputEvent(InputKind.KeyDown, Key: "R")), (3L, new InputEvent(InputKind.KeyUp, Key: "R")),
], g.Sent);
Assert.Equal(4, r.Frames);
}
// A window host ends its batch of game frames when a step completes, so the next step resolves on a fresh draw.
[Fact]
public void StepsDone_CountsCompletedSteps()
{
var program = ScriptLoader.Load("main.steps", new MemoryScriptSource(new Dictionary<string, string> { ["main.steps"] = "format 1\nmode player\nclick 100,200\nkey R" }), new Dictionary<string, string>());
var runner = new ScriptRunner(program, new FakeGame(), new ScriptRunOptions(), _ => { });
Assert.Equal(0, runner.StepsDone);
runner.Run(cap: 10_000);
Assert.Equal(2, runner.StepsDone);
}
// A window host skips the poll of a frame drawn later (a batch frame that closed a panel: a click resolved then
// would aim on the draw from before): the frame counts, the step is polled on the next one.
[Fact]
public void AfterFrame_NoPoll_CountsTheFrameOnly()
{
var program = ScriptLoader.Load("main.steps", new MemoryScriptSource(new Dictionary<string, string> { ["main.steps"] = "format 1\nmode player\nwait-frames 1" }), new Dictionary<string, string>());
var runner = new ScriptRunner(program, new FakeGame(), new ScriptRunOptions(), _ => { });
runner.BeforeFrame();
runner.AfterFrame(poll: false);
Assert.Equal((1, 0), (runner.FrameCount, runner.StepsDone));
runner.BeforeFrame();
runner.AfterFrame();
Assert.Equal((2, 1), (runner.FrameCount, runner.StepsDone));
}
[Fact]
public void Locator_AimsTheClick_OffScreenFaults()
{
var g = new FakeGame();
g.Objects["Virgil"] = (640, 360);
var (ok, _) = Run("format 1\nmode player\nclick @obj(Virgil)", g);
Assert.Equal(RunOutcome.Passed, ok.Outcome);
Assert.Contains((0L, new InputEvent(InputKind.Down, 640, 360)), g.Sent);
var (bad, output) = Run("format 1\nmode player\nclick @obj(Bates)", new FakeGame());
Assert.Equal((RunOutcome.Failed, 1), (bad.Outcome, bad.ExitCode));
Assert.Contains(output, l => l == "FAIL main.steps:3 click @obj(Bates): locator @obj(Bates): no object Bates on screen");
}
[Fact]
public void Wait_HoldsUntilConditionOrTimesOut()
{
var g = new FakeGame();
g.ProbeFns["ready"] = _ => RunValue.Of(g.Frame >= 5);
var (r, _) = Run("format 1\nmode player\nwait ready 10\nkey A", g);
Assert.Equal(RunOutcome.Passed, r.Outcome);
Assert.Equal(5L, g.Sent[0].Frame); // condition true after the 5th advance → key starts at frame 5
var g2 = new FakeGame();
g2.ProbeFns["ready"] = _ => RunValue.Of(false);
g2.ProbeFns["quest"] = _ => RunValue.Of(1);
var (t, output) = Run("format 1\nmode player\nwait ready and quest(3) > 1 4", g2);
Assert.Equal(RunOutcome.Failed, t.Outcome);
Assert.Equal(4, t.Frames);
Assert.Contains(output, l => l == "FAIL main.steps:3 wait ready and quest(3) > 1 4: timeout after 4 frames: ready and quest(3) > 1 (ready=0 quest(3)=1)");
}
[Fact]
public void Wait_AlreadyTrue_TakesNoFrame()
{
var g = new FakeGame();
g.ProbeFns["ready"] = _ => RunValue.Of(true);
var (r, _) = Run("format 1\nmode player\nwait ready 10\nkey A", g);
Assert.Equal(0L, g.Sent[0].Frame);
Assert.Equal(2, r.Frames);
}
[Fact]
public void Expect_FailsWithValues()
{
var g = new FakeGame();
g.ProbeFns["quest"] = _ => RunValue.Of(1);
var (r, output) = Run("format 1\nmode player\nexpect quest(1) >= accepted", g);
Assert.Equal(RunOutcome.Failed, r.Outcome);
Assert.Contains(output, l => l == "FAIL main.steps:3 expect quest(1) >= accepted: expect failed: quest(1) >= accepted (quest(1)=1)");
}
[Fact]
public void Action_PlanThenPollUntilDone()
{
var g = new FakeGame();
var talk = new FakeAction(VerbKind.Player, args => new ActionStart(new InputPlan([new PlannedInput(0, new InputEvent(InputKind.Down, 5, 6)), new PlannedInput(1, new InputEvent(InputKind.Up, 5, 6))])),
polls => polls >= 3 ? StepStatus.Done : StepStatus.Running, 1, 1);
g.ActionTable["talk"] = talk;
var (r, _) = Run("format 1\nmode player\ntalk @obj(x)\nkey Z", g);
Assert.Equal(RunOutcome.Passed, r.Outcome);
Assert.Equal(["@obj(x)"], talk.Begun.Single());
// Down sent f0, Up f1; polls run after advances to 2, 3, 4 (Done on the 3rd) → key starts at frame 4.
Assert.Equal(4L, g.Sent.Single(s => s.Event.Kind == InputKind.KeyDown).Frame);
}
[Fact]
public void Action_ArgsGetVariablesExpanded_LocatorsLeftToTheGame()
{
var g = new FakeGame();
var a = new FakeAction(VerbKind.Debug, _ => new ActionStart(Status: StepStatus.Done), min: 1, max: 3);
g.ActionTable["cheat"] = a;
Run("format 1\nmode debug\nset n = 50\ncheat gold ${n}", g);
Assert.Equal(["gold", "50"], a.Begun.Single());
}
[Fact]
public void Action_FaultStatusFailsTheRun()
{
var g = new FakeGame();
g.ActionTable["walk-to"] = new FakeAction(VerbKind.Player, _ => new ActionStart(), polls => polls == 2 ? StepStatus.Fault("stuck at 3,4") : StepStatus.Running, 1, 1);
var (r, output) = Run("format 1\nmode player\nwalk-to 3,4", g);
Assert.Equal((RunOutcome.Failed, 2L), (r.Outcome, r.Frames));
Assert.Contains(output, l => l == "FAIL main.steps:3 walk-to 3,4: stuck at 3,4");
}
// Review focus 2: a hook that never finishes hits the step cap.
[Fact]
public void StepCap_StopsAHangingAction()
{
var g = new FakeGame();
g.ActionTable["walk-to"] = new FakeAction(VerbKind.Player, _ => new ActionStart(), _ => StepStatus.Running, 1, 1);
var (r, output) = Run("format 1\nmode player\nwalk-to 3,4", g, new ScriptRunOptions { StepCap = 50 });
Assert.Equal((RunOutcome.Failed, 50L), (r.Outcome, r.Frames));
Assert.Contains(output, l => l == "FAIL main.steps:3 walk-to 3,4: step exceeded 50 frames");
}
[Fact]
public void GameHookException_IsAFailureNotACrash()
{
var g = new FakeGame();
g.ActionTable["talk"] = new FakeAction(VerbKind.Player, _ => throw new InvalidOperationException("boom"), min: 1, max: 1);
var (r, output) = Run("format 1\nmode player\ntalk x", g);
Assert.Equal(RunOutcome.Failed, r.Outcome);
Assert.Contains(output, l => l == "FAIL main.steps:3 talk x: InvalidOperationException: boom");
}
[Fact]
public void InvalidScript_Exit2_NoFrames()
{
var g = new FakeGame();
var (r, output) = Run("format 1\nmode player\ndance", g);
Assert.Equal((RunOutcome.Invalid, 2, 0L), (r.Outcome, r.ExitCode, r.Frames));
Assert.Contains(output, l => l == "main.steps:3: unknown verb dance");
Assert.Equal(0, g.Frame);
}
[Fact]
public void Rng_CallsTheGameHook_NoFrame()
{
var g = new FakeGame();
var (r, _) = Run("format 1\nmode player\nrng seed 9\nrng next hit 1", g);
Assert.Equal((RunOutcome.Passed, 0L), (r.Outcome, r.Frames));
Assert.Equal(["seed 9", "next hit 1"], g.RngImpl!.Calls);
}
[Fact]
public void WaitFrames_CountsFrames()
{
var g = new FakeGame();
var (r, _) = Run("format 1\nmode player\nwait-frames 7\nwait-frames 0\nwait-frames 0.5s", g);
Assert.Equal(37, r.Frames);
}
[Theory]
[InlineData("dclick 1,2", "Move@0 Down@0 Up@1 Down@2 Up@3")]
[InlineData("rclick 1,2", "Move@0 Down(R)@0 Up(R)@1")]
[InlineData("drag 0,0 30,60", "Move(0,0)@0 Down@0 Move(10,20)@1 Move(20,40)@2 Move(30,60)@3 Up@4")]
[InlineData("hold W 3", "KeyDown(W)@0 KeyUp(W)@3")]
[InlineData("chord Ctrl+S", "KeyDown(Ctrl)@0 KeyDown(S)@0 KeyUp(S)@1 KeyUp(Ctrl)@1")]
[InlineData("type \"hi there\"", "Text(hi there)@0")]
[InlineData("wheel -2 5,5", "Move@0 Wheel(-2)@0")]
public void InputPlans_Shapes(string line, string expected)
{
var g = new FakeGame();
Run($"format 1\nmode player\n{line}", g);
string Show((long Frame, InputEvent E) s) => s.E.Kind switch
{
InputKind.Move when line.StartsWith("drag") => $"Move({s.E.X},{s.E.Y})@{s.Frame}",
InputKind.Down or InputKind.Up when s.E.Button == MouseButton.Right => $"{s.E.Kind}(R)@{s.Frame}",
InputKind.KeyDown or InputKind.KeyUp => $"{s.E.Kind}({s.E.Key})@{s.Frame}",
InputKind.Text => $"Text({s.E.Text})@{s.Frame}",
InputKind.Wheel => $"Wheel({s.E.Delta})@{s.Frame}",
_ => $"{s.E.Kind}@{s.Frame}",
};
Assert.Equal(expected, string.Join(" ", g.Sent.Select(Show)));
}
[Fact]
public void BadPosition_Faults()
{
var (r, output) = Run("format 1\nmode player\nclick 10;20", new FakeGame());
Assert.Equal(RunOutcome.Failed, r.Outcome);
Assert.Contains(output, l => l == "FAIL main.steps:3 click 10;20: bad position '10;20'");
}
}
|