feat(fsharp): m01-m06 util, model, rules, random, spawn, game, view with Expecto tests
Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
183821737e
commit
9cbefeb2be
@@ -0,0 +1,26 @@
|
||||
/// m05 game: one `update` turns every action into a new state; replaying a game is a fold over its actions.
|
||||
module Snake.Game
|
||||
|
||||
open Snake.Model
|
||||
|
||||
/// How many frames pass between game steps (m10): `max 2 (8 - score / 3)` (integer division).
|
||||
/// Score 0-2 -> 8, 3-5 -> 7, ..., 18 and up -> 2.
|
||||
let movesEvery (score: int) : int = failwith "TODO m10"
|
||||
|
||||
/// The one function that says how the game changes: `state -> action -> state`.
|
||||
///
|
||||
/// m05: `Tick` runs a game step (`Spawn.tick`), `Turn` asks to change direction (`Rules.turn`);
|
||||
/// every other action returns the state unchanged.
|
||||
///
|
||||
/// (m10) `TogglePause` switches Playing <-> Paused and does nothing in any other status.
|
||||
/// (m10) `Restart` gives `Spawn.newGame state.Seed`, whatever the status.
|
||||
/// (m10) `Frame` does nothing unless Playing; otherwise it adds 1 to `Frame`, and when the new `Frame` is a multiple of
|
||||
/// `movesEvery Score` it also runs a game step (`Spawn.tick`) on that incremented state.
|
||||
/// In m10, match every case of `Action` explicitly (no `| _ ->`), so the compiler tells you if one is forgotten.
|
||||
let update (state: GameState) (action: Action) : GameState = failwith "TODO m05"
|
||||
|
||||
/// Apply all actions in order, starting from `state`, and return the final state.
|
||||
let replay (state: GameState) (actions: Action list) : GameState = failwith "TODO m05"
|
||||
|
||||
/// Like `replay`, but keep every state: `[state; afterFirst; afterSecond; ...]`, so its length is `List.length actions + 1`.
|
||||
let history (state: GameState) (actions: Action list) : GameState list = failwith "TODO m05"
|
||||
@@ -0,0 +1,48 @@
|
||||
/// m02 model: the data the game is built from, plus two tiny functions over directions.
|
||||
module Snake.Model
|
||||
|
||||
/// A grid cell. Records compare structurally: two points with the same X and Y are `=`.
|
||||
type Point = { X: int; Y: int }
|
||||
|
||||
type Direction =
|
||||
| Up
|
||||
| Down
|
||||
| Left
|
||||
| Right
|
||||
|
||||
type Status =
|
||||
| Playing
|
||||
| Paused
|
||||
| GameOver
|
||||
| Won
|
||||
|
||||
/// Everything that can happen to the game, as data.
|
||||
type Action =
|
||||
| Tick
|
||||
| Turn of Direction
|
||||
| TogglePause
|
||||
| Restart
|
||||
| Frame
|
||||
|
||||
/// The whole game. `Snake` is head first; `Seed` is the unsigned 32-bit random seed.
|
||||
type GameState =
|
||||
{ Width: int
|
||||
Height: int
|
||||
Snake: Point list
|
||||
Dir: Direction
|
||||
NextDir: Direction
|
||||
Apple: Point option
|
||||
Seed: uint32
|
||||
Score: int
|
||||
Status: Status
|
||||
Frame: int }
|
||||
|
||||
/// The unit step for a direction: Up (0,-1), Down (0,1), Left (-1,0), Right (1,0). Y grows downward.
|
||||
let delta (dir: Direction) : Point = failwith "TODO m02"
|
||||
|
||||
/// The direction pointing the other way.
|
||||
let opposite (dir: Direction) : Direction = failwith "TODO m02"
|
||||
|
||||
/// A fresh game: 20x20, snake (10,10),(9,10),(8,10) heading Right (`Dir` and `NextDir`),
|
||||
/// no apple, the given seed, score 0, status Playing, frame 0.
|
||||
let initialState (seed: uint32) : GameState = failwith "TODO m02"
|
||||
@@ -0,0 +1,30 @@
|
||||
/// m04 random. A `Random<'a>` wraps a plain function from a seed to a pair: `seed -> (value, nextSeed)`.
|
||||
/// Seeds and values are unsigned 32-bit integers (`uint32`).
|
||||
/// Nothing is hidden or mutated: the caller carries the seed.
|
||||
module Snake.Random
|
||||
|
||||
type Random<'a> = Random of (uint32 -> 'a * uint32)
|
||||
|
||||
/// Run `r` from `seed`: the value it produces and the seed it leaves behind.
|
||||
let run (seed: uint32) (r: Random<'a>) : 'a * uint32 = failwith "TODO m04"
|
||||
|
||||
/// A Random that always gives `a` and leaves the seed untouched.
|
||||
let pure (a: 'a) : Random<'a> = failwith "TODO m04"
|
||||
|
||||
/// Transform the value `r` produces; the seed threading is unchanged.
|
||||
let map (f: 'a -> 'b) (r: Random<'a>) : Random<'b> = failwith "TODO m04"
|
||||
|
||||
/// Run `r`, feed its value to `f` to pick the next Random, and run that with the seed `r` left behind.
|
||||
let bind (f: 'a -> Random<'b>) (r: Random<'a>) : Random<'b> = failwith "TODO m04"
|
||||
|
||||
/// The generator (mulberry32). uint32 arithmetic already wraps around at 2^32 and `>>>` on a uint32 is an
|
||||
/// unsigned shift. With `s = seed + 0x6D2B79F5u`:
|
||||
/// t = (s ^^^ (s >>> 15)) * (s ||| 1u)
|
||||
/// t = t ^^^ (t + (t ^^^ (t >>> 7)) * (t ||| 61u))
|
||||
/// value = t ^^^ (t >>> 14)
|
||||
/// It returns `value` and the new seed `s`.
|
||||
/// (The stub is wrapped in `Random` so the TODO only fires when the generator is run.)
|
||||
let nextUint32: Random<uint32> = Random(fun _ -> failwith "TODO m04")
|
||||
|
||||
/// A Random giving a number in `[0, n)`: the `nextUint32` value modulo `n`.
|
||||
let intBelow (n: int) : Random<int> = failwith "TODO m04"
|
||||
@@ -0,0 +1,22 @@
|
||||
/// m03 rules: the pure game rules. Each function takes a state and returns a new one.
|
||||
module Snake.Rules
|
||||
|
||||
open Snake.Model
|
||||
|
||||
/// The cell one step from `p` in direction `dir`. No wrapping: it may land outside the board.
|
||||
let move (p: Point) (dir: Direction) : Point = failwith "TODO m03"
|
||||
|
||||
/// True if `p` lies on the board: `0 <= X < Width` and `0 <= Y < Height`.
|
||||
let inBounds (state: GameState) (p: Point) : bool = failwith "TODO m03"
|
||||
|
||||
/// Ask to turn the snake. Returns `state` unchanged unless the game is Playing and
|
||||
/// `dir` is not the opposite of `state.Dir` (the direction the snake last *moved*, not `NextDir`).
|
||||
/// Otherwise sets `NextDir = dir`. Several turns in one tick: the last allowed one wins.
|
||||
let turn (state: GameState) (dir: Direction) : GameState = failwith "TODO m03"
|
||||
|
||||
/// One step of the game. Returns `state` unchanged unless Playing. Otherwise the head moves in `NextDir`
|
||||
/// (which becomes `Dir`):
|
||||
/// - head lands on the apple: the snake grows (tail kept), score + 1, apple becomes None;
|
||||
/// - otherwise the tail cell is vacated, so the head may enter the cell the tail is leaving;
|
||||
/// - head off the board, or onto a remaining body cell: status GameOver, snake unchanged (but `Dir` updated).
|
||||
let advance (state: GameState) : GameState = failwith "TODO m03"
|
||||
@@ -1,9 +1,17 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<!-- FS0046: `pure` is reserved for future use by F#; the course keeps the name used by every track. -->
|
||||
<NoWarn>$(NoWarn);FS0046</NoWarn>
|
||||
<TreatWarningsAsErrors>false</TreatWarningsAsErrors>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<Compile Include="Util.fs" />
|
||||
<Compile Include="Model.fs" />
|
||||
<Compile Include="Rules.fs" />
|
||||
<Compile Include="Random.fs" />
|
||||
<Compile Include="Spawn.fs" />
|
||||
<Compile Include="Game.fs" />
|
||||
<Compile Include="View.fs" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
/// m04 spawn: placing apples with the Random from Random.fs.
|
||||
module Snake.Spawn
|
||||
|
||||
open Snake.Model
|
||||
|
||||
/// Every board cell not covered by the snake, row-major (y outer loop, x inner loop). The apple does not count as occupied.
|
||||
let freeCells (state: GameState) : Point list = failwith "TODO m04"
|
||||
|
||||
/// Put an apple on a random free cell, drawing from `state.Seed` (use `Random.intBelow` and store the new seed).
|
||||
/// Unchanged if there already is an apple or the status is not Playing.
|
||||
/// If no cell is free the board is full: status becomes Won (seed unchanged, no apple).
|
||||
let spawnApple (state: GameState) : GameState = failwith "TODO m04"
|
||||
|
||||
/// One game step: `Rules.advance`, then make sure there is an apple.
|
||||
let tick (state: GameState) : GameState = failwith "TODO m04"
|
||||
|
||||
/// A fresh game (`initialState`) with its first apple already placed.
|
||||
let newGame (seed: uint32) : GameState = failwith "TODO m04"
|
||||
+11
-1
@@ -1,2 +1,12 @@
|
||||
/// Placeholder; replaced by the m01 utilities.
|
||||
/// m01 util: small helpers used by every later module.
|
||||
/// (The JavaScript track also writes `pipe` and `compose` here; F# has them built in as `|>` and `>>`.)
|
||||
module Snake.Util
|
||||
|
||||
/// `range n` is `[0; 1; ...; n-1]`; `range 0` is `[]`.
|
||||
let range (n: int) : int list = failwith "TODO m01"
|
||||
|
||||
/// `xs` without its last element; an empty list stays empty.
|
||||
let dropLast (xs: 'a list) : 'a list = failwith "TODO m01"
|
||||
|
||||
/// True if some element `x` of `xs` satisfies `eq x p`. `eq` lets callers choose how elements are compared.
|
||||
let contains (xs: 'a list) (p: 'a) (eq: 'a -> 'a -> bool) : bool = failwith "TODO m01"
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
/// m06 view: from game state to a Scene. The picture is plain data; drawing it is somebody else's job (m07).
|
||||
module Snake.View
|
||||
|
||||
open Snake.Model
|
||||
|
||||
/// A thing to draw.
|
||||
type Shape =
|
||||
| Rect of x: int * y: int * w: int * h: int * color: string
|
||||
| Text of x: int * y: int * text: string * color: string * size: int * align: string
|
||||
|
||||
/// A picture: shapes drawn in order.
|
||||
type Scene = Shape list
|
||||
|
||||
/// Pixels per board cell.
|
||||
let CELL = 20
|
||||
|
||||
/// Describe the picture as data, in this order: background `Rect(0, 0, Width*CELL, Height*CELL, "#111111")`;
|
||||
/// the apple (if any) as `Rect(ax*CELL+1, ay*CELL+1, CELL-2, CELL-2, "#e53935")`; each snake segment in list order,
|
||||
/// same size, head "#7cb342" and body "#43a047"; `Text(6, 18, "Score: <n>", "#ffffff", 16, "left")`;
|
||||
/// then an overlay centred at (Width*CELL/2, Height*CELL/2), all "#ffffff", align "center":
|
||||
/// Paused: "Paused" (size 32) and, 30px lower, "Press Space to resume" (size 16);
|
||||
/// GameOver: "Game Over" + "Press R to restart"; Won: "You Win!" + "Press R to restart"; Playing: none.
|
||||
let view (state: GameState) : Scene = failwith "TODO m06"
|
||||
Reference in New Issue
Block a user