package snake package life import LifeModel.* /** m13 life rules: the next generation is a function of the whole old board, and nothing else. */ object LifeRules: // Every cell of the board, row-major (y, then x). Filtering this list keeps any result in canonical order for free. private val board: List[Point] = (for y <- 0 until LH; x <- 0 until LW yield Point(x, y)).toList private def onBoard(p: Point): Boolean = 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. private def index(p: Point): Int = p.y * LW + p.x private val offsets = List((-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). private def neighbours(p: Point): List[Point] = offsets.map((dx, dy) => Point(p.x + dx, p.y + dy)).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. def lifeStep(cells: List[Point]): List[Point] = val alive = cells.toSet board.filter { p => val n = neighbours(p).count(alive) n == 3 || (n == 2 && alive(p)) } // Add a dead cell or remove a live one, keeping the list sorted row-major without duplicates. Off the board: unchanged. private def toggle(cells: List[Point], p: Point): List[Point] = if !onBoard(p) then cells else if cells.contains(p) then cells.filterNot(_ == p) else cells.filter(index(_) < index(p)) ++ (p :: cells.filter(index(_) > index(p))) // One intBelow(4) per board cell in row-major order, the cell alive when the draw is 0 (about one cell in four). // A fold carries the seed; the live cells are collected in reverse and turned round once at the end. private def randomCells(seed: Long): (List[Point], Long) = val (aliveReversed, next) = board.foldLeft((List.empty[Point], seed)) { case ((acc, s), p) => val (d, s2) = Random.intBelow(4).run(s) (if d == 0 then p :: acc else acc, s2) } (aliveReversed.reverse, next) private def nextGeneration(state: LifeState): LifeState = state.copy(cells = lifeStep(state.cells), generation = state.generation + 1) def lifeUpdate(state: LifeState, action: LifeAction): LifeState = action match case LifeAction.Frame => if state.running then nextGeneration(state) else state case LifeAction.Step => if state.running then state else nextGeneration(state) case LifeAction.TogglePause => state.copy(running = !state.running) // Clicking edits the world whether it runs or not: the click is one action between two generations. case LifeAction.Toggle(cell) => state.copy(cells = toggle(state.cells, cell)) case LifeAction.Clear => state.copy(cells = Nil, generation = 0) case LifeAction.Randomize => val (cells, seed) = randomCells(state.seed) state.copy(cells = cells, seed = seed, generation = 0)