Audit pass: - Frame rule counts frames since the last move, so every step lasts exactly movesEvery(score) frames, also right after a speed-up - clampTime + MAX_GAP_MS: a hidden tab no longer replays the whole gap - Pong Restart keeps held keys - Fact corrections (mulberry32 output coverage, Haskell IO, uniform) - New fixture cases from a mutation analysis Extensions (all three tracks, shared fixtures, course text): - Turn queue (Exercise 10.6), persisted high score (10.7), replay viewer (10.8), play() end-to-end helper (9.5) - Optional State (4.7) and optional Module 7b, stack-safe IO - Module 12, property-based testing with generators, shrinking, invariants and a model-based oracle - Module 13, Conway's Life with a Click event - JS type checking via jsconfig, JSDoc and types.d.ts - Maintainer tools: scripts/mutate.mjs, scripts/typecheck-js.sh, Playwright end-to-end suite (e2e/, scripts/e2e.sh) Docs: docs/contract.md is the canonical contract; specs and build log under docs/superpowers. Verified: JS 728, Scala 689, F# 691 unit tests; stubs and progression M01-M13 and M07b; 176 end-to-end tests on Chromium and WebKit. Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
443 lines
21 KiB
FSharp
443 lines
21 KiB
FSharp
module Snake.Tests.M12Check
|
|
|
|
open Expecto
|
|
open Snake
|
|
open Snake.Model
|
|
open Snake.Check
|
|
open Snake.Tests.Fixtures
|
|
|
|
// ---- helpers for moveGaps: a state whose snake has its head at (x, y); nothing else matters there
|
|
|
|
let private at (x: int) (y: int) : GameState =
|
|
{ Width = 20
|
|
Height = 20
|
|
Snake = [ { X = x; Y = y }; { X = x - 1; Y = y } ]
|
|
Dir = Right
|
|
NextDir = Right
|
|
Apple = None
|
|
Seed = 0u
|
|
Score = 0
|
|
Status = Playing
|
|
Frame = 0
|
|
Queued = None }
|
|
|
|
// ---- the payoff: properties of the game, and deliberately broken games ("mutants") that they must catch
|
|
|
|
// Every state of the game played with `upd` from `start`. (For the real `Game.update` this is `Game.history`.)
|
|
let private statesWith (upd: GameState -> Action -> GameState) (start: GameState) (actions: Action list) : GameState list =
|
|
List.scan upd start actions
|
|
|
|
let private sane (upd: GameState -> Action -> GameState) (actions: Action list) : bool =
|
|
statesWith upd (Spawn.newGame 1u) actions |> List.forall (fun s -> List.isEmpty (violations s))
|
|
|
|
// Mutant 1: `turn` checks against NextDir instead of Dir (version A of Puzzle 2 in Module 3).
|
|
let private turnAgainstNextDir (state: GameState) (action: Action) : GameState =
|
|
match action with
|
|
| Turn dir -> if state.Status = Playing && dir <> opposite state.NextDir then { state with NextDir = dir } else state
|
|
| _ -> Game.update state action
|
|
|
|
// Mutant 2: `advance` forgets to check the walls.
|
|
let private advanceWithoutWalls (state: GameState) : GameState =
|
|
if state.Status <> Playing then
|
|
state
|
|
else
|
|
let head = Rules.move (List.head state.Snake) state.NextDir
|
|
let eating = state.Apple = Some head
|
|
let body = if eating then state.Snake else Util.dropLast state.Snake
|
|
|
|
if Util.contains body head (=) then
|
|
{ state with Dir = state.NextDir; Status = GameOver }
|
|
else
|
|
{ state with
|
|
Snake = head :: body
|
|
Dir = state.NextDir
|
|
Apple = if eating then None else state.Apple
|
|
Score = state.Score + (if eating then 1 else 0) }
|
|
|
|
// What `update` does after a step since Exercise 10.6: take the queued turn. The mutants below that replace `Tick`
|
|
// do the same, so that each one differs from the real game in exactly one thing.
|
|
let private takeQueued (s: GameState) : GameState =
|
|
match s.Queued with
|
|
| Some d when s.Status = Playing -> { s with NextDir = d; Queued = None }
|
|
| _ -> s
|
|
|
|
let private noWalls (state: GameState) (action: Action) : GameState =
|
|
match action with
|
|
| Tick -> takeQueued (Spawn.spawnApple (advanceWithoutWalls state))
|
|
| _ -> Game.update state action
|
|
|
|
// Mutant 4: a `Tick` that forgets the turn queue of Exercise 10.6 (the `Frame` step still takes it).
|
|
let private tickForgetsQueue (state: GameState) (action: Action) : GameState =
|
|
match action with
|
|
| Tick -> Spawn.tick state
|
|
| _ -> Game.update state action
|
|
|
|
// Mutant 5: the queue accepts any key that differs from the pending turn, even the one straight back from it.
|
|
let private queueAnyKey (state: GameState) (action: Action) : GameState =
|
|
match action with
|
|
| Turn dir when state.Status = Playing && state.NextDir <> state.Dir && dir <> state.NextDir ->
|
|
{ state with Queued = Some dir }
|
|
| _ -> Game.update state action
|
|
|
|
// Mutant 3: the frame rule this course used at first: step whenever the *total* frame count is a multiple of movesEvery.
|
|
let private totalFrameCount (state: GameState) (action: Action) : GameState =
|
|
match action with
|
|
| Frame ->
|
|
if state.Status <> Playing then
|
|
state
|
|
else
|
|
let counted = { state with Frame = state.Frame + 1 }
|
|
if counted.Frame % Game.movesEvery counted.Score = 0 then Spawn.tick counted else counted
|
|
| _ -> Game.update state action
|
|
|
|
// A long corridor: the snake is one step from its third apple (score 2 -> 3, so the speed changes), `frame` frames into the game.
|
|
let private corridor (frame: int) : GameState =
|
|
{ Width = 60
|
|
Height = 1
|
|
Snake = [ { X = 1; Y = 0 }; { X = 0; Y = 0 } ]
|
|
Dir = Right
|
|
NextDir = Right
|
|
Apple = Some { X = 2; Y = 0 }
|
|
Seed = 0u
|
|
Score = 2
|
|
Status = Playing
|
|
Frame = frame
|
|
Queued = None }
|
|
|
|
let private frames = List.replicate 40 Frame
|
|
|
|
// Every gap between two moves is exactly `movesEvery` of the score the snake had after the first of the two.
|
|
let private steady (upd: GameState -> Action -> GameState) (frame: int) : bool =
|
|
let states = statesWith upd (corridor frame) frames
|
|
|
|
let scoreAtMove =
|
|
states
|
|
|> List.pairwise
|
|
|> List.filter (fun (before, after) -> List.head after.Snake <> List.head before.Snake)
|
|
|> List.map (fun (_, after) -> after.Score)
|
|
|
|
let gaps = moveGaps states
|
|
List.forall2 (fun gap score -> gap = Game.movesEvery score) gaps (List.truncate (List.length gaps) scoreAtMove)
|
|
|
|
// ---- random states and the oracle
|
|
|
|
let private connected (snake: Point list) : bool =
|
|
snake |> List.pairwise |> List.forall (fun (a, b) -> abs (a.X - b.X) + abs (a.Y - b.Y) = 1)
|
|
|
|
let private board (width: int) (height: int) : GameState = { at 0 0 with Width = width; Height = height }
|
|
|
|
// Mutant 6: Puzzle 1 of Module 3, "the new head must not be on the old snake" (it forgets that the tail moves away).
|
|
let private advanceOldSnakeCheck (state: GameState) : GameState =
|
|
if state.Status <> Playing then
|
|
state
|
|
else
|
|
let head = Rules.move (List.head state.Snake) state.NextDir
|
|
let eating = state.Apple = Some head
|
|
let body = if eating then state.Snake else Util.dropLast state.Snake
|
|
|
|
if not (Rules.inBounds state head) || Util.contains state.Snake head (=) then
|
|
{ state with Dir = state.NextDir; Status = GameOver }
|
|
else
|
|
{ state with
|
|
Snake = head :: body
|
|
Dir = state.NextDir
|
|
Apple = if eating then None else state.Apple
|
|
Score = state.Score + (if eating then 1 else 0) }
|
|
|
|
let private pt (x: int) (y: int) : Point = { X = x; Y = y }
|
|
|
|
[<Tests>]
|
|
let tests =
|
|
testList "M12 Check" [
|
|
// ---- generators
|
|
|
|
testCase "elementOf picks the element at the index drawn by intBelow" (fun () ->
|
|
let xs = [ "a"; "b"; "c"; "d"; "e" ]
|
|
let i, seed = Random.run 0u (Random.intBelow 5)
|
|
Expect.equal (Random.run 0u (elementOf xs)) (xs[i], seed) "elementOf")
|
|
|
|
testCase "elementOf of a one-element list always gives that element, and still uses up a draw" (fun () ->
|
|
Expect.equal (Random.run 0u (elementOf [ "only" ])) ("only", 1831565813u) "elementOf")
|
|
|
|
testCase "listOf 0 gen is the empty list and leaves the seed alone" (fun () ->
|
|
Expect.equal (Random.run 99u (listOf 0 Random.nextUint32)) ([], 99u) "listOf 0")
|
|
|
|
testCase "listOf threads the seed from each draw to the next" (fun () ->
|
|
Expect.equal
|
|
(Random.run 0u (listOf 3 Random.nextUint32))
|
|
([ 1144304738u; 1416247u; 958946056u ], 1199730143u)
|
|
"listOf 3")
|
|
|
|
testCase "listOf of a constant generator repeats the value and leaves the seed alone" (fun () ->
|
|
Expect.equal (Random.run 7u (listOf 4 (Random.pure "x"))) ([ "x"; "x"; "x"; "x" ], 7u) "listOf pure")
|
|
|
|
testCase "genActions n gives n actions, the same ones for the same seed" (fun () ->
|
|
let actions, _ = Random.run 5u (genActions 30)
|
|
Expect.equal (List.length actions) 30 "length"
|
|
|
|
Expect.all
|
|
actions
|
|
(fun a ->
|
|
match a with
|
|
| Tick
|
|
| Turn _ -> true
|
|
| TogglePause
|
|
| Restart
|
|
| Frame -> false)
|
|
"only Tick and Turn"
|
|
|
|
Expect.equal (Random.run 5u (genActions 30)) (Random.run 5u (genActions 30)) "same seed, same actions")
|
|
|
|
// ---- forAll
|
|
|
|
testCase "forAll reports success with the number of runs" (fun () ->
|
|
Expect.equal (forAll 200 1u (fun x -> x >= 0 && x < 10) (Random.intBelow 10)) (Passed 200) "forAll")
|
|
|
|
testCase "forAll with zero runs succeeds without drawing anything" (fun () ->
|
|
Expect.equal (forAll 0 1u (fun _ -> false) Random.nextUint32) (Passed 0) "forAll")
|
|
|
|
testCase "forAll reports the first failing draw, its value and the seed it started from" (fun () ->
|
|
let gen = Random.intBelow 10
|
|
let prop x = x <> 7
|
|
let draws, _ = Random.run 3u (listOf 200 gen)
|
|
let first = draws |> List.findIndex (fun x -> not (prop x))
|
|
Expect.isGreaterThan first 0 "the test needs a failure that is not the very first draw"
|
|
|
|
match forAll 200 3u prop gen with
|
|
| Failed(run, seed, value) ->
|
|
Expect.equal (run, value) (first, 7) "run and value"
|
|
Expect.equal (fst (Random.run seed gen)) 7 "the reported seed gives the value again"
|
|
| Passed runs -> failtest $"expected a failure, got Passed {runs}")
|
|
|
|
testCase "forAll stops at the first failure: the property is not asked about later values" (fun () ->
|
|
let gen = Random.intBelow 10
|
|
let draws, _ = Random.run 3u (listOf 200 gen)
|
|
let first = draws |> List.findIndex (fun x -> x = 7)
|
|
let mutable calls = 0 // test code may count; the code under test may not
|
|
|
|
let result =
|
|
forAll
|
|
200
|
|
3u
|
|
(fun x ->
|
|
calls <- calls + 1
|
|
x <> 7)
|
|
gen
|
|
|
|
match result with
|
|
| Failed(run, _, _) -> Expect.equal run first "run"
|
|
| Passed runs -> failtest $"expected a failure, got Passed {runs}"
|
|
|
|
Expect.equal calls (first + 1) "calls")
|
|
|
|
testCase "forAll is reproducible: same generator, property, runs and seed give the same report" (fun () ->
|
|
let check () = forAll 500 11u (fun x -> x % 97 <> 0) (Random.intBelow 1000)
|
|
Expect.equal (check ()) (check ()) "same report")
|
|
|
|
testCase "forAll runs in constant stack: a million runs do not overflow it" (fun () ->
|
|
Expect.equal (forAll 1000000 1u (fun x -> x < 10) (Random.intBelow 10)) (Passed 1000000) "forAll")
|
|
|
|
// ---- shrinking
|
|
|
|
testCase "removals lists every way to drop one element, in order" (fun () ->
|
|
Expect.equal (removals [ 1; 2; 3 ]) [ [ 2; 3 ]; [ 1; 3 ]; [ 1; 2 ] ] "removals")
|
|
|
|
testCase "removals of an empty list is empty, and of a single element is one empty list" (fun () ->
|
|
Expect.equal (removals ([]: int list)) [] "empty"
|
|
Expect.equal (removals [ "a" ]) [ [] ] "single")
|
|
|
|
testCase "minimize keeps exactly the elements the failure needs" (fun () ->
|
|
Expect.equal
|
|
(minimize (fun xs -> List.contains 3 xs && List.contains 7 xs) [ 1; 2; 3; 4; 5; 6; 7; 8; 9; 10 ])
|
|
[ 3; 7 ]
|
|
"minimize")
|
|
|
|
testCase "minimize keeps the order and the duplicates the failure needs" (fun () ->
|
|
Expect.equal
|
|
(minimize
|
|
(fun xs -> (xs |> List.filter (fun x -> x = "a") |> List.length) >= 2 && List.contains "c" xs)
|
|
[ "b"; "a"; "c"; "a"; "d"; "a" ])
|
|
[ "c"; "a"; "a" ]
|
|
"minimize")
|
|
|
|
testCase "minimize of a list that fails whatever you remove is the empty list" (fun () ->
|
|
Expect.equal (minimize (fun _ -> true) [ 1; 2; 3 ]) [] "minimize")
|
|
|
|
testCase "minimize returns a list that still fails and has no failing removal (1-minimal)" (fun () ->
|
|
let fails (xs: int list) = List.sum xs >= 10
|
|
let small = minimize fails [ 5; 1; 4; 2; 6; 3 ]
|
|
Expect.isTrue (fails small) "still fails"
|
|
Expect.all (removals small) (fun ys -> not (fails ys)) "no failing removal")
|
|
|
|
// ---- invariants
|
|
|
|
yield! fixtureTests "m12-violations" "violations" (fun c ->
|
|
Expect.equal (violations (gameState (arg 0 c))) (list str (expected c)) "violations")
|
|
|
|
// ---- moveGaps
|
|
|
|
testCase "moveGaps measures the distance between consecutive moves of the head" (fun () ->
|
|
Expect.equal
|
|
(moveGaps [ at 1 0; at 1 0; at 2 0; at 2 0; at 2 0; at 3 0; at 3 1 ])
|
|
[ 3; 1 ]
|
|
"moveGaps")
|
|
|
|
testCase "moveGaps of fewer than two moves is empty" (fun () ->
|
|
Expect.equal (moveGaps []) [] "no states"
|
|
Expect.equal (moveGaps [ at 1 0 ]) [] "one state"
|
|
Expect.equal (moveGaps [ at 1 0; at 1 0; at 2 0; at 2 0 ]) [] "one move")
|
|
|
|
testCase "moveGaps counts a move at index 1" (fun () ->
|
|
Expect.equal (moveGaps [ at 1 0; at 2 0; at 2 0; at 3 0 ]) [ 2 ] "moveGaps")
|
|
|
|
testCase "moveGaps treats an empty snake as a head of its own" (fun () ->
|
|
let snake (cells: Point list) = { at 1 0 with Snake = cells }
|
|
|
|
Expect.equal
|
|
(moveGaps [ snake []; snake []; snake [ { X = 1; Y = 0 } ]; snake [ { X = 1; Y = 0 } ]; snake [ { X = 2; Y = 0 } ] ])
|
|
[ 2 ]
|
|
"moveGaps")
|
|
|
|
// ---- properties of the game, and the mutants
|
|
|
|
testCase "statesWith Game.update in these tests is the history of Module 5" (fun () ->
|
|
let actions, _ = Random.run 9u (genActions 25)
|
|
Expect.equal (statesWith Game.update (Spawn.newGame 1u) actions) (Game.history (Spawn.newGame 1u) actions) "history")
|
|
|
|
testCase "the real game keeps every invariant on 300 random games of 40 actions" (fun () ->
|
|
Expect.equal (forAll 300 2024u (sane Game.update) (genActions 40)) (Passed 300) "forAll")
|
|
|
|
testCase "the quick double turn bug is found and shrunk to its two key presses" (fun () ->
|
|
let prop = sane turnAgainstNextDir
|
|
|
|
match forAll 300 2024u prop (genActions 40) with
|
|
| Passed runs -> failtest $"expected a failure, got Passed {runs}"
|
|
| Failed(_, _, value) ->
|
|
match minimize (fun actions -> not (prop actions)) value with
|
|
| [ Turn first; Turn second ] ->
|
|
Expect.contains [ Up; Down ] first "first turn"
|
|
Expect.equal second Left "second turn"
|
|
| small -> failtest $"expected two turns, got %A{small}")
|
|
|
|
testCase "a game without walls is found and shrunk to the shortest way off the board: ten ticks" (fun () ->
|
|
let prop = sane noWalls
|
|
|
|
match forAll 300 2024u prop (genActions 40) with
|
|
| Passed runs -> failtest $"expected a failure, got Passed {runs}"
|
|
| Failed(_, _, value) ->
|
|
Expect.equal (minimize (fun actions -> not (prop actions)) value) (List.replicate 10 Tick) "ten ticks")
|
|
|
|
testCase "a Tick that forgets the queued turn is caught by queued-not-reversed and shrunk to four actions" (fun () ->
|
|
let prop = sane tickForgetsQueue
|
|
|
|
match forAll 300 2024u prop (genActions 40) with
|
|
| Passed runs -> failtest $"expected a failure, got Passed {runs}"
|
|
| Failed(_, _, value) ->
|
|
Expect.equal
|
|
(minimize (fun actions -> not (prop actions)) value)
|
|
[ Turn Up; Turn Right; Tick; Turn Left ]
|
|
"four actions")
|
|
|
|
testCase "a queue that accepts a reversal is found and shrunk to its two key presses" (fun () ->
|
|
let prop = sane queueAnyKey
|
|
|
|
match forAll 300 2024u prop (genActions 40) with
|
|
| Passed runs -> failtest $"expected a failure, got Passed {runs}"
|
|
| Failed(_, _, value) ->
|
|
Expect.equal (minimize (fun actions -> not (prop actions)) value) [ Turn Up; Turn Down ] "two key presses")
|
|
|
|
testCase "the real game moves exactly every movesEvery score frames, also across a speed-up" (fun () ->
|
|
Expect.equal (forAll 100 7u (steady Game.update) (Random.intBelow 8)) (Passed 100) "forAll")
|
|
|
|
testCase "counting frames from the start of the game makes some steps too short, and the property finds one" (fun () ->
|
|
match forAll 100 7u (steady totalFrameCount) (Random.intBelow 56) with
|
|
| Passed runs -> failtest $"expected a failure, got Passed {runs}"
|
|
| Failed(_, _, value) -> Expect.isFalse (steady totalFrameCount value) "the counterexample fails")
|
|
|
|
// ---- random states (Exercise 12.6)
|
|
|
|
testCase "genSnake draws the head first, then walks from the tail (seed 0, 5x5, at most 6 cells)" (fun () ->
|
|
Expect.equal
|
|
(Random.run 0u (genSnake 5 5 6))
|
|
([ pt 3 2; pt 3 1; pt 3 0; pt 2 0; pt 1 0; pt 0 0 ], 4231026099u)
|
|
"genSnake")
|
|
|
|
testCase "genSnake is pinned for a few more seeds" (fun () ->
|
|
Expect.equal
|
|
(fst (Random.run 1u (genSnake 5 5 6)))
|
|
[ pt 2 3; pt 3 3; pt 4 3; pt 4 4; pt 3 4; pt 2 4 ]
|
|
"seed 1"
|
|
|
|
Expect.equal
|
|
(fst (Random.run 42u (genSnake 5 5 6)))
|
|
[ pt 1 0; pt 1 1; pt 2 1; pt 2 0; pt 3 0; pt 3 1 ]
|
|
"seed 42")
|
|
|
|
testCase "genSnake with maxLen 1 is just the head, drawn with two draws" (fun () ->
|
|
let x, s1 = Random.run 0u (Random.intBelow 5)
|
|
let y, s2 = Random.run s1 (Random.intBelow 5)
|
|
Expect.equal (Random.run 0u (genSnake 5 5 1)) ([ pt x y ], s2) "genSnake")
|
|
|
|
testCase "genSnake stops early when the walk is stuck: a 1x1 board holds one cell" (fun () ->
|
|
Expect.equal (fst (Random.run 3u (genSnake 1 1 5))) [ pt 0 0 ] "genSnake")
|
|
|
|
testCase "genSnake always gives a snake on the board, connected, without overlap, at most maxLen long" (fun () ->
|
|
let ok (snake: Point list) =
|
|
List.length snake >= 1
|
|
&& List.length snake <= 8
|
|
&& connected snake
|
|
&& List.forall (Rules.inBounds (board 4 3)) snake
|
|
&& List.length (List.distinct snake) = List.length snake
|
|
|
|
Expect.equal (forAll 300 5u ok (genSnake 4 3 8)) (Passed 300) "forAll")
|
|
|
|
testCase "every generated state is sane: 500 states of a 5x5 board have no violations" (fun () ->
|
|
Expect.equal (forAll 500 2024u (fun s -> List.isEmpty (violations s)) (genState 5 5)) (Passed 500) "forAll")
|
|
|
|
testCase "generated states are Playing, have nothing queued, and vary in length and apple" (fun () ->
|
|
let states, _ = Random.run 2024u (listOf 500 (genState 5 5))
|
|
Expect.all states (fun s -> s.Status = Playing && s.Queued = None && s.Width = 5 && s.Height = 5) "shape"
|
|
|
|
Expect.all
|
|
states
|
|
(fun s ->
|
|
match s.Snake with
|
|
| [ _ ] -> s.Dir = Right
|
|
| head :: second :: _ -> Rules.move second s.Dir = head
|
|
| [] -> false)
|
|
"dir"
|
|
|
|
Expect.exists states (fun s -> List.length s.Snake = 1) "a one-cell snake"
|
|
Expect.exists states (fun s -> List.length s.Snake >= 10) "a long snake"
|
|
Expect.exists states (fun s -> s.Apple = None) "no apple"
|
|
Expect.exists states (fun s -> s.Apple <> None) "an apple"
|
|
Expect.exists states (fun s -> s.NextDir <> s.Dir) "a pending turn")
|
|
|
|
testCase "generated states start with seed 0, score 0 and frame 0" (fun () ->
|
|
let states, _ = Random.run 2024u (listOf 50 (genState 5 5))
|
|
Expect.all states (fun s -> s.Seed = 0u && s.Score = 0 && s.Frame = 0) "seed, score and frame")
|
|
|
|
// ---- a second implementation as an oracle (Exercise 12.7)
|
|
|
|
yield! fixtureTests "m03-advance" "advanceSpec agrees with Module 3's advance fixture" (fun c ->
|
|
Expect.equal (advanceSpec (gameState (arg 0 c))) (gameState (expected c)) "advanceSpec")
|
|
|
|
testCase "advance and advanceSpec agree on 500 generated states of a 5x5 board" (fun () ->
|
|
Expect.equal (forAll 500 2024u (fun s -> Rules.advance s = advanceSpec s) (genState 5 5)) (Passed 500) "forAll")
|
|
|
|
testCase "no invariant sees the wrong collision check of Puzzle 1: every state it produces is sane" (fun () ->
|
|
Expect.equal
|
|
(forAll 500 2024u (fun s -> List.isEmpty (violations (advanceOldSnakeCheck s))) (genState 5 5))
|
|
(Passed 500)
|
|
"forAll")
|
|
|
|
testCase "comparing with advanceSpec catches the wrong collision check: the head was moving onto the tail" (fun () ->
|
|
match forAll 500 2024u (fun s -> advanceOldSnakeCheck s = advanceSpec s) (genState 5 5) with
|
|
| Passed runs -> failtest $"expected a failure, got Passed {runs}"
|
|
| Failed(run, _, s) ->
|
|
Expect.equal run 21 "run"
|
|
Expect.equal (Rules.move (List.head s.Snake) s.NextDir) (List.last s.Snake) "onto the tail"
|
|
Expect.equal (advanceOldSnakeCheck s).Status GameOver "the mutant ends the game"
|
|
Expect.equal (advanceSpec s).Status Playing "the rules do not")
|
|
]
|