/// m13 life rules: the next generation is a function of the whole old board, and nothing else. module Snake.Life.Rules open Snake open Snake.Model open Snake.Life.Model // Every cell of the board, row-major (y, then x). Filtering this list keeps any result in canonical order for free. let private board: Point list = [ for y in 0 .. LH - 1 do for x in 0 .. LW - 1 -> { X = x; Y = y } ] let private onBoard (p: Point) : bool = p.X >= 0 && p.X < LW && p.Y >= 0 && p.Y < LH // A cell's position in row-major order: the order the canonical list is sorted by. let private index (p: Point) : int = p.Y * LW + p.X let private offsets = [ (-1, -1); (0, -1); (1, -1); (-1, 0); (1, 0); (-1, 1); (0, 1); (1, 1) ] // The up to eight neighbours of `p` that are on the board (the edges are dead: nothing wraps around). let private neighbours (p: Point) : Point list = offsets |> List.map (fun (dx, dy) -> { X = p.X + dx; Y = p.Y + dy }) |> List.filter onBoard // B3/S23, every cell judged on the *old* list only. A Set of the old cells makes "is this neighbour alive?" one // lookup, so a generation is 1200 x 8 lookups however many cells live. The set is built once and only read. let lifeStep (cells: Point list) : Point list = let alive = Set.ofList cells let count p = neighbours p |> List.filter alive.Contains |> List.length board |> List.filter (fun p -> let n = count p n = 3 || (n = 2 && alive.Contains p)) // Add a dead cell or remove a live one, keeping the list sorted row-major without duplicates. Off the board: unchanged. let private toggle (cells: Point list) (p: Point) : Point list = if not (onBoard p) then cells elif List.contains p cells then List.filter (fun c -> c <> p) cells else List.filter (fun c -> index c < index p) cells @ [ p ] @ List.filter (fun c -> index c > index p) cells // A random board: one `intBelow 4` per cell in row-major order, the cell alive when the draw is 0 (about one cell in // four). mapFold threads the seed through the 1200 draws; the result is the cells and the seed after the last draw. let private randomCells (seed: uint32) : Point list * uint32 = let draws, next = List.mapFold (fun s _ -> Random.run s (Random.intBelow 4)) seed board List.zip board draws |> List.filter (fun (_, d) -> d = 0) |> List.map fst, next let private nextGeneration (state: LifeState) : LifeState = { state with Cells = lifeStep state.Cells Generation = state.Generation + 1 } let lifeUpdate (state: LifeState) (action: LifeAction) : LifeState = match action with | LifeAction.Frame -> if state.Running then nextGeneration state else state | LifeAction.Step -> if state.Running then state else nextGeneration state | LifeAction.TogglePause -> { state with Running = not state.Running } // Clicking edits the world whether it runs or not: the click is one action between two generations. | LifeAction.Toggle p -> { state with Cells = toggle state.Cells p } | LifeAction.Clear -> { state with Cells = []; Generation = 0 } | LifeAction.Randomize -> let cells, seed = randomCells state.Seed { state with Cells = cells Seed = seed Generation = 0 }