feat(fsharp): m09 input/app/runtime and m10 extensions with tests
Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
e27372f017
commit
55f8b0b30f
@@ -0,0 +1,33 @@
|
||||
/// 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.IO
|
||||
open Snake.Stream
|
||||
|
||||
/// Milliseconds between game steps.
|
||||
let TICK_MS = 150L
|
||||
|
||||
/// Milliseconds between animation frames (m10).
|
||||
let FRAME_MS = 25L
|
||||
|
||||
/// Stream of everything that can happen to the game: `Tick` every TICK_MS (from `Stream.interval`),
|
||||
/// merged with the action for each key press that `Input.keyToAction` recognises (from `Stream.keyDowns`).
|
||||
/// (The stub is wrapped in `Stream` so the TODO only fires when the stream is stepped.)
|
||||
let classicActions: Stream<Action> = Stream(fun _ -> failwith "TODO m09")
|
||||
|
||||
/// (m10) Like `classicActions`, but the clock emits `Frame` every FRAME_MS instead of `Tick` every TICK_MS;
|
||||
/// key presses are translated by `Input.keyToAction` as before (now including pause and restart).
|
||||
let speedActions: Stream<Action> = Stream(fun _ -> failwith "TODO m10")
|
||||
|
||||
/// Fold a stream of actions into a stream of game states: start from `Spawn.newGame seed`, apply `Game.update` to
|
||||
/// each action (scan), and emit the initial state first (startWith) so there is something to draw before anything happens.
|
||||
let states (seed: uint32) (actions: Stream<Action>) : Stream<GameState> = failwith "TODO m09"
|
||||
|
||||
/// The drawing of one state, as an IO: `IO.drawScene (View.view state)`.
|
||||
let render (state: GameState) : IO<unit> = failwith "TODO m09"
|
||||
|
||||
/// The whole program as a value: ask for a random seed, then turn the stream of states (built from `classicActions`
|
||||
/// in m09, from `speedActions` after m10) into a stream of drawings (`render`). Nothing runs here.
|
||||
/// A function of `unit`, like `IO.randomSeed`.
|
||||
let main () : IO<Stream<IO<unit>>> = failwith "TODO m09"
|
||||
@@ -0,0 +1,10 @@
|
||||
/// m09 input: from keyboard keys to game actions.
|
||||
module Snake.Input
|
||||
|
||||
open Snake.Model
|
||||
|
||||
/// Translate a keyboard key name (as in the browser's `KeyboardEvent.key`) into the action it triggers, if any.
|
||||
/// m09: "ArrowUp"/"w"/"W" -> `Some (Turn Up)`, "ArrowDown"/"s"/"S" -> Down, "ArrowLeft"/"a"/"A" -> Left,
|
||||
/// "ArrowRight"/"d"/"D" -> Right. Any other key -> `None`.
|
||||
/// (m10) " " (Space) -> `Some TogglePause`, "r"/"R" -> `Some Restart`.
|
||||
let keyToAction (key: string) : Action option = failwith "TODO m09"
|
||||
@@ -0,0 +1,32 @@
|
||||
/// m09 runtime: the single impure place in the course (with the web/ glue); everything else only builds values.
|
||||
/// Here one mutable variable is allowed. Keep it tiny.
|
||||
module Snake.Runtime
|
||||
|
||||
open Snake.View
|
||||
open Snake.IO
|
||||
open Snake.Stream
|
||||
|
||||
/// The outside world, as the runtime sees it. The browser builds one from a canvas (web/BrowserPlatform.fs);
|
||||
/// tests build a fake that records calls.
|
||||
type Platform =
|
||||
{ FillRect: int -> int -> int -> int -> string -> unit
|
||||
FillText: int -> int -> string -> string -> int -> string -> unit
|
||||
/// A random unsigned 32-bit seed.
|
||||
RandomSeed: unit -> uint32
|
||||
/// Call `onTime t` again and again, `t` = milliseconds since the clock started.
|
||||
StartClock: (int64 -> unit) -> unit
|
||||
OnKeyDown: (string -> unit) -> unit
|
||||
OnKeyUp: (string -> unit) -> unit }
|
||||
|
||||
/// Interpret an IO against a platform and return its final value.
|
||||
/// `Draw` of a Rect -> `p.FillRect x y w h color` and of a Text -> `p.FillText x y text color size align`,
|
||||
/// both answered with `Done`; `RandomSeed` -> `Seed (p.RandomSeed ())`. Each answer is passed to `k`.
|
||||
/// Must not grow the stack with the number of ops: a loop, or a tail-recursive `let rec` (which the compiler
|
||||
/// turns into a loop).
|
||||
let runIO (p: Platform) (io: IO<'a>) : 'a = failwith "TODO m09"
|
||||
|
||||
/// Run the program: `runIO p main` gives the stream; keep the current stream in the one mutable variable
|
||||
/// (F# closures cannot capture a `let mutable`, so use a ref cell: `let stream = ref ...`, `stream.Value <- next`).
|
||||
/// Register `p.OnKeyDown` / `p.OnKeyUp` / `p.StartClock` so that each event steps the stream with the matching
|
||||
/// `Event` (`KeyDown key`, `KeyUp key`, `Time t`), stores the next stream, and runs every IO the step emitted, in order.
|
||||
let runApp (p: Platform) (main: IO<Stream<IO<unit>>>) : unit = failwith "TODO m09"
|
||||
@@ -15,5 +15,8 @@
|
||||
<Compile Include="View.fs" />
|
||||
<Compile Include="IO.fs" />
|
||||
<Compile Include="Stream.fs" />
|
||||
<Compile Include="Input.fs" />
|
||||
<Compile Include="App.fs" />
|
||||
<Compile Include="Runtime.fs" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
Reference in New Issue
Block a user