/// m05 game: one `update` turns every action into a new state; replaying a game is a fold over its actions. /// m10 adds pause, restart, a frame counter that makes the snake speed up as the score grows, and a turn queue. module Snake.Game open Snake.Model /// What `update` does with `Turn dir` from m10 on. With no turn pending (`NextDir = Dir`) it is the m03 `Rules.turn`. /// With a turn pending, a key that is neither `NextDir` nor its opposite is remembered in `Queued` /// (replacing an earlier one) and taken on the step after; any other key changes nothing. let steer (state: GameState) (dir: Direction) : GameState = if state.Status <> Playing then state elif state.NextDir = state.Dir then Rules.turn state dir elif dir <> state.NextDir && dir <> opposite state.NextDir then { state with Queued = Some dir } else state // After a step: a game still in play takes the queued turn, if there is one. It is always a legal turn, because the // step just made `Dir` equal to the old `NextDir`, and `Queued` is neither that nor its opposite. let private takeQueued (state: GameState) : GameState = match state.Queued with | Some d when state.Status = Playing -> { state with NextDir = d; Queued = None } | _ -> state // One game step as `update` takes it: `Spawn.tick`, then the queued turn. let private step (state: GameState) : GameState = takeQueued (Spawn.tick state) /// How many frames pass between game steps: 8 at first, one fewer every 3 points, never fewer than 2. let movesEvery (score: int) : int = max 2 (8 - score / 3) // One animation frame. `Frame` counts the frames since the snake last moved; when it reaches // `movesEvery Score` the count starts again and the game takes a step. Counting from the last move // (not from the start of the game) keeps every step exactly `movesEvery` frames long, also right after a speed-up. let private frame (state: GameState) : GameState = if state.Status <> Playing then state else let n = state.Frame + 1 if n >= movesEvery state.Score then step { state with Frame = 0 } else { state with Frame = n } // Pause and resume only make sense in those two statuses; a finished game stays finished. let private togglePause (state: GameState) : GameState = match state.Status with | Playing -> { state with Status = Paused } | Paused -> { state with Status = Playing } | GameOver | Won -> state // Every case is listed (no `| _ ->`): adding an Action later makes the compiler point here. let update (state: GameState) (action: Action) : GameState = match action with | Tick -> step state | Turn dir -> steer state dir | TogglePause -> togglePause state | Restart -> Spawn.newGame state.Seed | Frame -> frame state let replay (state: GameState) (actions: Action list) : GameState = List.fold update state actions // List.scan is List.fold that keeps every intermediate result (the start state included). let history (state: GameState) (actions: Action list) : GameState list = List.scan update state actions