feat(js): m10 extensions (pause, restart, speed-up via Frame) with tests
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
This commit is contained in:
co-authored by
Claude Sonnet 5.5
parent
25141ce86d
commit
fd7c5f2acf
+7
-1
@@ -7,12 +7,18 @@ import { keyDowns, interval, map, filterMap, merge, scan, startWith } from './st
|
||||
import { keyToAction } from './input.js'
|
||||
|
||||
export const TICK_MS = 150
|
||||
export const FRAME_MS = 25
|
||||
|
||||
// Everything that can happen to the game: a Tick every TICK_MS, and a Turn for each steering key.
|
||||
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.
|
||||
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.
|
||||
export const states = (seed, actions) =>
|
||||
startWith(scan(actions, update, newGame(seed)), newGame(seed))
|
||||
@@ -21,4 +27,4 @@ export const render = (state) => drawScene(view(state))
|
||||
|
||||
// The whole program as a value: get a seed, then turn the stream of states into a stream of drawings.
|
||||
// Nothing runs here; runtime.js interprets it.
|
||||
export const main = mapIO(randomSeed, (seed) => map(states(seed, classicActions), render))
|
||||
export const main = mapIO(randomSeed, (seed) => map(states(seed, speedActions), render))
|
||||
|
||||
+21
-2
@@ -1,12 +1,31 @@
|
||||
// m05 game: one `update` function turns every action into a new state; replay is a fold over actions.
|
||||
// m10 adds pause, restart and a frame clock that speeds up as the score grows.
|
||||
import { turn } from './rules.js'
|
||||
import { tick } from './spawn.js'
|
||||
import { tick, newGame } from './spawn.js'
|
||||
|
||||
/** How many frames pass between game steps: 8 at first, one fewer every 3 points, never fewer than 2. */
|
||||
export const movesEvery = (score) => Math.max(2, 8 - Math.floor(score / 3))
|
||||
|
||||
// One animation frame: count it, and every `movesEvery(score)`-th frame run a game step.
|
||||
const frame = (state) => {
|
||||
if (state.status !== 'Playing') return state
|
||||
const counted = { ...state, frame: state.frame + 1 }
|
||||
return counted.frame % movesEvery(counted.score) === 0 ? tick(counted) : counted
|
||||
}
|
||||
|
||||
// Pause and resume only make sense in those two statuses; a finished game stays finished.
|
||||
const togglePause = (state) =>
|
||||
state.status === 'Playing' ? { ...state, status: 'Paused' }
|
||||
: state.status === 'Paused' ? { ...state, status: 'Playing' }
|
||||
: state
|
||||
|
||||
/** Tick and Turn are handled here; other actions leave the state alone (m10 adds more). */
|
||||
export const update = (state, action) => {
|
||||
switch (action.tag) {
|
||||
case 'Tick': return tick(state)
|
||||
case 'Turn': return turn(state, action.dir)
|
||||
case 'TogglePause': return togglePause(state)
|
||||
case 'Restart': return newGame(state.seed)
|
||||
case 'Frame': return frame(state)
|
||||
default: return state
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,5 +8,11 @@ const directions = {
|
||||
ArrowRight: 'Right', d: 'Right', D: 'Right',
|
||||
}
|
||||
|
||||
// Keys that are not steering (m10).
|
||||
const commands = { ' ': Action.TogglePause, r: Action.Restart, R: Action.Restart }
|
||||
|
||||
// hasOwn, not `key in table`: "constructor" and "toString" are inherited properties, not keys we mapped.
|
||||
export const keyToAction = (key) =>
|
||||
Object.hasOwn(directions, key) ? Action.Turn(directions[key]) : null
|
||||
Object.hasOwn(directions, key) ? Action.Turn(directions[key])
|
||||
: Object.hasOwn(commands, key) ? commands[key]
|
||||
: null
|
||||
|
||||
Reference in New Issue
Block a user