// m13 life app: the same wiring as Snake and Pong, with a third kind of input, the mouse. import { LW, LH, LCELL, initialLife, LifeAction } from './model.js' import { lifeUpdate } from './rules.js' import { lifeView } from './view.js' import { map as mapIO, randomSeed, drawScene } from '../io.js' import { keyDowns, clicks, interval, map, filterMap, merge, scan, startWith, clampTime } from '../stream.js' import { MAX_GAP_MS } from '../app.js' /** @import { LifeState, Stream, Program } from '../types' */ /** Milliseconds between two generations while the world runs. */ export const GEN_MS = 150 const keys = { ' ': LifeAction.TogglePause, n: LifeAction.Step, N: LifeAction.Step, c: LifeAction.Clear, C: LifeAction.Clear, r: LifeAction.Randomize, R: LifeAction.Randomize, } /** A key to an action, or null. * @type {(key: string) => LifeAction | null} */ export const lifeKey = (key) => (Object.hasOwn(keys, key) ? keys[key] : null) /** A click at canvas pixel (x, y) to `Toggle` of the cell under it, or null when it is not on the board. * @type {(x: number, y: number) => LifeAction | null} */ export const lifeClick = (x, y) => { const cell = { x: Math.floor(x / LCELL), y: Math.floor(y / LCELL) } return x >= 0 && y >= 0 && cell.x < LW && cell.y < LH ? LifeAction.Toggle(cell) : null } /** @type {Stream} */ export const lifeActions = merge( merge( map(interval(GEN_MS), () => LifeAction.Frame), filterMap(keyDowns, lifeKey)), filterMap(clicks, (p) => lifeClick(p.x, p.y))) /** @type {(seed: number, actions: Stream) => Stream} */ export const lifeStates = (seed, actions) => startWith(scan(actions, lifeUpdate, initialLife(seed)), initialLife(seed)) /** @type {Program} */ export const lifeMain = mapIO(randomSeed, (seed) => clampTime(map(lifeStates(seed, lifeActions), (state) => drawScene(lifeView(state))), MAX_GAP_MS))