module Snake.Tests.M10Replay open System.Text.Json open Expecto open Snake open Snake.Model open Snake.View open Snake.IO open Snake.Stream open Snake.Replay open Snake.Tests.Fixtures open Snake.Tests.M09Helpers // Viewers in the fixtures use the keys index, last and playing. let private viewer (v: JsonElement) : Viewer = { Index = num (prop "index" v); Last = num (prop "last" v); Playing = bool (prop "playing" v) } let private viewerAction (v: JsonElement) : ViewerAction = match str (prop "tag" v) with | "Next" -> ViewerAction.Next | "Prev" -> ViewerAction.Prev | "First" -> ViewerAction.First | "Last" -> ViewerAction.Last | "TogglePlay" -> ViewerAction.TogglePlay | "Beat" -> ViewerAction.Beat | other -> failwith $"unknown viewer action tag: {other}" // The sample recording, as the page loads it. Not a fixture of cases: a recording {seed, actions}. let private sample: Recording = let v = load "replay-sample" { Seed = u32 (prop "seed" v); Actions = list action (prop "actions" v) } [] let tests = testList "M10 Replay" [ yield! fixtureTests "m10-viewer-update" "viewerUpdate" (fun c -> Expect.equal (viewerUpdate (viewer (arg 0 c)) (viewerAction (arg 1 c))) (viewer (expected c)) "viewerUpdate") yield! fixtureTests "m10-viewer-key" "viewerKey" (fun c -> Expect.equal (viewerKey (str (arg 0 c))) (opt viewerAction (expected c)) "viewerKey") yield! fixtureTests "m10-viewer-view" "viewerView" (fun c -> Expect.equal (viewerView (list gameState (arg 0 c)) (viewer (arg 1 c))) (list shape (expected c)) "viewerView") yield! [ testCase "the sample recording is worth watching: Tick and Turn only, under 400 actions, at least five apples, then game over" (fun () -> Expect.all sample.Actions (fun a -> match a with | Tick | Turn _ -> true | _ -> false) "Tick and Turn only" let n = List.length sample.Actions Expect.isTrue (n >= 150 && n <= 400) $"{n} actions" let fs = Game.history (Spawn.newGame sample.Seed) sample.Actions let last = List.last fs Expect.isGreaterThanOrEqual last.Score 5 "score" Expect.equal last.Status GameOver "last frame" Expect.equal (List.item (List.length fs - 2) fs).Status Playing "frame before the last") testCase "frames replays the recording with history, from the new game of its seed" (fun () -> let recording = { Seed = 42u; Actions = [ Tick; Turn Up; Tick ] } Expect.equal (frames recording) (Game.history (Spawn.newGame 42u) recording.Actions) "frames" Expect.equal (List.length (frames sample)) (List.length sample.Actions + 1) "length") testCase "viewerStart is the first frame, not playing" (fun () -> Expect.equal (viewerStart 12) { Index = 0; Last = 12; Playing = false } "viewerStart") testCase "viewerUpdate keeps the index in [0, last] whatever the actions" (fun () -> let actions = [ ViewerAction.Prev ViewerAction.Next ViewerAction.Last ViewerAction.Next ViewerAction.TogglePlay ViewerAction.Beat ViewerAction.Beat ViewerAction.First ViewerAction.Prev ViewerAction.Beat ] let viewers = List.scan viewerUpdate (viewerStart 3) actions Expect.all viewers (fun v -> v.Index >= 0 && v.Index <= 3 && not (v.Playing && v.Index = 3)) "in range, never playing at the end") testCase "the autoplay clock beats every 100 ms" (fun () -> Expect.equal REPLAY_MS 100L "REPLAY_MS") testCase "replayActions turns the clock into Beats and keys into viewer actions" (fun () -> Expect.equal (run [ Time 100L; KeyDown "ArrowRight"; KeyDown "x"; KeyDown " "; Time 300L ] replayActions) [ ViewerAction.Beat ViewerAction.Next ViewerAction.TogglePlay ViewerAction.Beat ViewerAction.Beat ] "actions") testCase "autoplay: Space, then the clock steps one frame per 100 ms and stops at the end" (fun () -> let out = viewerStates 3 replayActions |> run [ Time 0L; KeyDown " "; Time 100L; Time 200L; Time 300L; Time 400L; Time 500L ] Expect.equal (out |> List.map (fun v -> v.Index)) [ 0; 0; 1; 2; 3; 3; 3 ] "indices" Expect.equal (out |> List.map (fun v -> v.Playing)) [ false; true; true; true; false; false; false ] "playing") testCase "without Space nothing plays: the clock alone never moves the viewer" (fun () -> Expect.all (viewerStates 3 replayActions |> run [ Time 0L; Time 1000L; Time 2000L ]) (fun v -> v.Index = 0) "index 0") testCase "replayMain asks for nothing and draws the first frame of the recording first" (fun () -> let traced = IO.trace 0u (replayMain sample) Expect.equal traced.Ops [] "ops" let drawings, _ = Stream.step (Time 0L) traced.Result Expect.equal (List.length drawings) 1 "one drawing" Expect.equal (drawn (List.head drawings)) (viewerView (frames sample) (viewerStart (List.length sample.Actions))) "first scene" let start = View.view (Spawn.newGame sample.Seed) Expect.equal (drawn (List.head drawings) |> List.truncate (List.length start)) start "the game's own view first") testCase "replayMain: End shows the last frame of the game, the game over" (fun () -> let out = (IO.trace 0u (replayMain sample)).Result |> run [ Time 0L; KeyDown "End" ] let fs = frames sample let last = List.length fs - 1 let scene = drawn (List.last out) Expect.equal scene (viewerView fs { Index = last; Last = last; Playing = false }) "last scene" Expect.exists scene (fun s -> match s with | Text(_, _, "Game Over", _, _, _) -> true | _ -> false) "Game Over") testCase "stepping back shows exactly the earlier scene" (fun () -> let out = (IO.trace 0u (replayMain sample)).Result |> run [ Time 0L KeyDown "ArrowRight" KeyDown "ArrowRight" KeyDown "ArrowRight" KeyDown "ArrowLeft" KeyDown "d" KeyDown "a" KeyDown "a" ] let scenes = List.map drawn out Expect.equal scenes[4] scenes[2] "back one" Expect.equal scenes[6] scenes[4] "forward and back" Expect.equal scenes[7] scenes[1] "back to frame 1" Expect.notEqual scenes[3] scenes[2] "frames differ") testCase "replayMain: after ten seconds in a background tab autoplay moves 2 frames (250 ms), not 100" (fun () -> let _, afterPlay = Stream.step (KeyDown " ") (IO.trace 0u (replayMain sample)).Result let _, afterFirst = Stream.step (Time 0L) afterPlay Expect.equal (List.length (fst (Stream.step (Time 10000L) afterFirst))) 2 "two drawings") ] ]