module Snake.Tests.M09App open Expecto open Snake open Snake.Model open Snake.IO open Snake.Stream open Snake.Tests.M09Helpers // The ops of drawing one state's view. let private drawOps (state: GameState) : Op list = View.view state |> List.map Draw [] let tests = testList "M09 App" [ testCase "the game ticks every 150 ms" (fun () -> Expect.equal App.TICK_MS 150L "TICK_MS") testCase "classicActions turns the clock into Tick actions and keys into Turn actions" (fun () -> Expect.equal (Stream.run [ Time 150L; KeyDown "ArrowUp"; KeyDown "x"; Time 300L ] App.classicActions) [ Tick; Turn Up; Tick ] "actions") testCase "states starts with the new game, then yields one state per action" (fun () -> let out = App.states 42u App.classicActions |> Stream.run [ Time 150L; KeyDown "ArrowUp"; Time 300L ] Expect.equal (List.length out) 4 "length" Expect.equal (List.head out) (Spawn.newGame 42u) "first state" Expect.equal (out |> List.map (fun s -> List.head s.Snake)) [ { X = 10; Y = 10 }; { X = 11; Y = 10 }; { X = 11; Y = 10 }; { X = 11; Y = 9 } ] "heads" Expect.equal (out |> List.map (fun s -> s.NextDir)) [ Right; Right; Up; Up ] "nextDir" Expect.equal (out |> List.map (fun s -> s.Dir)) [ Right; Right; Right; Up ] "dir") testCase "render draws the whole view of the state" (fun () -> Expect.equal (IO.trace 0u (App.render (Spawn.newGame 42u))).Ops (View.view (Spawn.newGame 42u) |> List.map Draw) "ops") // Module 10 (Exercise 10.7) makes `main` also load the stored best and draw one more shape after the game's // view, so this test accepts both versions; M10 pins the final one exactly. testCase "main asks for a random seed, then builds the stream from it" (fun () -> let traced = IO.trace 42u (App.main ()) Expect.isTrue (traced.Ops = [ RandomSeed ] || traced.Ops = [ RandomSeed; LoadBest ]) $"main may only ask for the seed (and, from m10 on, the stored best); it asked for %A{traced.Ops}" let outs, _ = Stream.step (Time 0L) traced.Result Expect.equal (List.length outs) 1 "one drawing" let expected = View.view (Spawn.newGame 42u) Expect.equal (drawn (List.head outs) |> List.truncate (List.length expected)) expected "drawing") // `main` itself switches to speedActions in m10, so the 150 ms behaviour is checked on the same program built from classicActions. testCase "the program built from classicActions draws the new game first, then the ticked game after 150 ms" (fun () -> let stream = (IO.trace 42u (classicMain ())).Result let first, next = Stream.step (Time 0L) stream let second, _ = Stream.step (Time 150L) next Expect.equal (drawn (List.head first)) (View.view (Spawn.newGame 42u)) "first drawing" Expect.equal (List.length second) 1 "one more drawing" Expect.notEqual (drawn (List.head second)) (drawn (List.head first)) "the game moved") // Holds before and after m10: whatever `main` ticks with, the clock must be wired in, so time passing produces more drawings. testCase "main draws again once time has passed" (fun () -> let stream = (IO.trace 42u (App.main ())).Result let first, next = Stream.step (Time 0L) stream let later, _ = Stream.step (Time 150L) next Expect.equal (List.length first) 1 "one drawing" Expect.isNonEmpty later "more drawings") // ---- play: the whole program in one equality (Exercise 9.5) testCase "play on the classic program equals the scenes of the expected states" (fun () -> let events = [ Time 150L; KeyDown "ArrowUp"; KeyDown "x"; Time 300L; Time 310L ] let expectedStates = Game.history (Spawn.newGame 42u) [ Tick; Turn Up; Tick ] Expect.equal (App.play 42u events (classicMain ())) (List.map drawOps expectedStates) "drawings") testCase "play with no events gives no drawings" (fun () -> Expect.equal (App.play 42u [] (classicMain ())) [] "drawings") testCase "play uses the seed for the program's RandomSeed" (fun () -> Expect.equal (App.play 7u [ Time 0L ] (classicMain ())) [ drawOps (Spawn.newGame 7u) ] "drawings") ]