/// m09 app: the whole game wired together as values: streams of actions in, a stream of drawings out. module Snake.App open Snake.Model open Snake.View open Snake.IO open Snake.Stream let TICK_MS = 150L let FRAME_MS = 25L // m10: the longest step of time the game accepts at once; a longer silence (a background tab) counts as this much. let MAX_GAP_MS = 250L // Everything that can happen to the game: a Tick every TICK_MS, and an action for each bound key. let classicActions: Stream = Stream.merge (Stream.interval TICK_MS |> Stream.map (fun _ -> Tick)) (Stream.keyDowns |> Stream.choose Input.keyToAction) // m10: a Frame every FRAME_MS instead of a Tick; the game itself decides (movesEvery) which frames take a step. let speedActions: Stream = Stream.merge (Stream.interval FRAME_MS |> Stream.map (fun _ -> Frame)) (Stream.keyDowns |> Stream.choose Input.keyToAction) // Actions folded into game states. startWith puts the initial state first so it is drawn right away. let states (seed: uint32) (actions: Stream) : Stream = actions |> Stream.scan Game.update (Spawn.newGame seed) |> Stream.startWith (Spawn.newGame seed) let render (state: GameState) : IO = IO.drawScene (View.view state) // m10: a session wraps the game with the best score so far. GameState, update and view do not know about it. // `Improved` says whether the action that produced this session raised the best, which is when it must be saved. type Session = { Game: GameState; Best: int; Improved: bool } let sessionUpdate (session: Session) (action: Action) : Session = let game = Game.update session.Game action let best = max session.Best game.Score { Game = game; Best = best; Improved = best > session.Best } // The same running fold as `states`, one level up. let sessions (seed: uint32) (best: int) (actions: Stream) : Stream = let s0 = { Game = Spawn.newGame seed; Best = best; Improved = false } actions |> Stream.scan sessionUpdate s0 |> Stream.startWith s0 // The game's picture with the best score at the top right, level with the score on the left. let sessionView (session: Session) : Scene = View.view session.Game @ [ Text(session.Game.Width * CELL - 6, 18, $"Best: {session.Best}", "#ffffff", 16, "right") ] // Draw, and save the best only on the action that raised it. let renderSession (session: Session) : IO = if session.Improved then IO.sequence [ IO.drawScene (sessionView session); IO.saveBest session.Best ] else IO.drawScene (sessionView session) // The whole program as a value: get a seed, then the stored best, then turn the stream of sessions into a stream // of drawings. clampTime wraps the whole stream, so everything inside it sees the clamped clock. // Nothing runs here; Runtime interprets it. let main () : IO>> = IO.randomSeed () |> IO.bind (fun seed -> IO.loadBest () |> IO.map (fun best -> sessions seed best speedActions |> Stream.map renderSession |> Stream.clampTime MAX_GAP_MS)) // m09: the whole program in one equality. For each drawing that `main` emits while fed `events`, the ops it // performs, answered by the pure interpreter (`seed` for RandomSeed, and from m10 `best` for LoadBest). let playWith (seed: uint32) (best: int) (events: Event list) (main: IO>>) : Op list list = (IO.traceWith seed best main).Result |> Stream.run events |> List.map (fun io -> (IO.traceWith seed best io).Ops) // The Module 9 helper is the "no stored best" case. let play (seed: uint32) (events: Event list) (main: IO>>) : Op list list = playWith seed 0 events main