import { Action } from './model.js' import { update } from './game.js' import { newGame } from './spawn.js' import { view, CELL } from './view.js' import { map as mapIO, flatMap as flatMapIO, sequence, randomSeed, drawScene, loadBest, saveBest, trace } from './io.js' import { keyDowns, interval, map, filterMap, merge, scan, startWith, clampTime, run } from './stream.js' import { keyToAction } from './input.js' /** @import { GameState, Session, Scene, IO, Stream, Input, Op, Program } from './types' */ export const TICK_MS = 150 export const FRAME_MS = 25 // m10: the longest step of time the game accepts at once; a longer silence (a background tab) counts as this much. export const MAX_GAP_MS = 250 // Everything that can happen to the game: a Tick every TICK_MS, and a Turn for each steering key. /** @type {Stream} */ export const classicActions = merge( map(interval(TICK_MS), () => Action.Tick), filterMap(keyDowns, keyToAction)) // m10: a Frame every FRAME_MS instead of a Tick; the game itself decides (movesEvery) which frames take a step. /** @type {Stream} */ export const speedActions = merge( map(interval(FRAME_MS), () => Action.Frame), filterMap(keyDowns, keyToAction)) // Actions folded into game states. startWith puts the initial state first so it is drawn right away. /** @type {(seed: number, actions: Stream) => Stream} */ export const states = (seed, actions) => startWith(scan(actions, update, newGame(seed)), newGame(seed)) /** @type {(state: GameState) => IO} */ export const render = (state) => drawScene(view(state)) // m10: a session wraps the game with the best score so far. GameState, update and view do not know about it. // `improved` says whether the action that produced this session raised the best, which is when it must be saved. /** @type {(session: Session, action: Action) => Session} */ export const sessionUpdate = (session, action) => { const game = update(session.game, action) const best = Math.max(session.best, game.score) return { game, best, improved: best > session.best } } // The same running fold as `states`, one level up. /** @type {(seed: number, best: number, actions: Stream) => Stream} */ export const sessions = (seed, best, actions) => { const s0 = { game: newGame(seed), best, improved: false } return startWith(scan(actions, sessionUpdate, s0), s0) } // The game's picture with the best score at the top right, level with the score on the left. /** @type {(session: Session) => Scene} */ export const sessionView = (session) => [ ...view(session.game), { tag: 'Text', x: session.game.width * CELL - 6, y: 18, text: `Best: ${session.best}`, color: '#ffffff', size: 16, align: 'right' }, ] // Draw, and save the best only on the action that raised it. /** @type {(session: Session) => IO} */ export const renderSession = (session) => session.improved ? sequence([drawScene(sessionView(session)), saveBest(session.best)]) : drawScene(sessionView(session)) // The whole program as a value: get a seed, then the stored best, then turn the stream of sessions into a stream // of drawings. clampTime wraps the whole stream, so everything inside it sees the clamped clock. // Nothing runs here; runtime.js interprets it. /** @type {Program} */ export const main = flatMapIO(randomSeed, (seed) => mapIO(loadBest, (best) => clampTime(map(sessions(seed, best, speedActions), renderSession), MAX_GAP_MS))) // m09: the whole program in one equality. For each drawing that `main` emits while fed `events`, the ops it // performs, answered by the pure interpreter (`seed` for RandomSeed, and from m10 `best` for LoadBest). /** @type {(main: Program, seed: number, events: Input[], best?: number) => Op[][]} */ export const play = (main, seed, events, best = 0) => run(trace(main, seed, best).result, events).map((io) => trace(io, seed, best).ops)