feat(js): m11 pong capstone (model, rules, view, app, page) and polling smoke test
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
This commit is contained in:
co-authored by
Claude Sonnet 5.5
parent
fd7c5f2acf
commit
1d1d918805
@@ -0,0 +1,84 @@
|
||||
import test from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
import { W, H, PCELL, PADDLE, PLAYER_X, CPU_X, WIN, initialPong, PongAction } from '../src/pong/model.js'
|
||||
import { pongUpdate, clamp } from '../src/pong/rules.js'
|
||||
import { pongView } from '../src/pong/view.js'
|
||||
import { pongKey, FRAME_MS_PONG, pongActions, pongStates, pongMain } from '../src/pong/app.js'
|
||||
import { run } from '../src/stream.js'
|
||||
import { trace } from '../src/io.js'
|
||||
import { loadFixture, freeze } from './helpers.js'
|
||||
|
||||
const fixtureTests = (name, f) => {
|
||||
const fx = loadFixture(name)
|
||||
fx.cases.forEach((c) =>
|
||||
test(`m11 ${fx.function}: ${c.name}`, () => assert.deepStrictEqual(f(...freeze(c.args)), c.expected)))
|
||||
}
|
||||
|
||||
fixtureTests('m11-pong-update', pongUpdate)
|
||||
fixtureTests('m11-pong-view', pongView)
|
||||
fixtureTests('m11-pong-key', pongKey)
|
||||
|
||||
const time = (t) => ({ tag: 'Time', t })
|
||||
const down = (key) => ({ tag: 'KeyDown', key })
|
||||
const up = (key) => ({ tag: 'KeyUp', key })
|
||||
|
||||
test('m11 the court is 80 by 50 cells of 8 pixels, with 8-cell paddles', () =>
|
||||
assert.deepStrictEqual([W, H, PCELL, PADDLE, PLAYER_X, CPU_X, WIN], [80, 50, 8, 8, 2, 77, 5]))
|
||||
|
||||
test('m11 initialPong puts the ball in the middle, both paddles centred, and nobody ahead', () =>
|
||||
assert.deepStrictEqual(initialPong(5), {
|
||||
ball: { x: 40, y: 25 }, vel: { dx: 1, dy: 1 }, player: 21, cpu: 21,
|
||||
held: { up: false, down: false }, score: { player: 0, cpu: 0 }, status: 'Playing', seed: 5, frame: 0,
|
||||
}))
|
||||
|
||||
test('m11 PongAction builds Press and Release with a key, and has Frame and Restart as plain values', () =>
|
||||
assert.deepStrictEqual(
|
||||
[PongAction.Press('Up'), PongAction.Release('Down'), PongAction.Frame, PongAction.Restart],
|
||||
[{ tag: 'Press', key: 'Up' }, { tag: 'Release', key: 'Down' }, { tag: 'Frame' }, { tag: 'Restart' }]))
|
||||
|
||||
test('m11 clamp keeps a number inside [lo, hi]', () =>
|
||||
assert.deepStrictEqual([clamp(0, 42, -1), clamp(0, 42, 17), clamp(0, 42, 43)], [0, 17, 42]))
|
||||
|
||||
test('m11 pongUpdate does not modify the state it is given', () => {
|
||||
const s = freeze(initialPong(0))
|
||||
assert.doesNotThrow(() => pongUpdate(s, PongAction.Frame))
|
||||
})
|
||||
|
||||
// 3000 frames with no one touching the keys: the ball must always stay on the court and the match must end.
|
||||
test('m11 an idle match stays on the court and ends once someone reaches five', () => {
|
||||
const states = Array.from({ length: 3000 }).reduce(
|
||||
(acc) => [...acc, pongUpdate(acc[acc.length - 1], PongAction.Frame)], [initialPong(3)])
|
||||
assert.ok(states.every((s) => s.ball.y >= 0 && s.ball.y < H && s.ball.x >= 0 && s.ball.x < W))
|
||||
assert.ok(states.every((s) => s.player >= 0 && s.player <= H - PADDLE && s.cpu >= 0 && s.cpu <= H - PADDLE))
|
||||
const last = states[states.length - 1]
|
||||
assert.equal(last.status, 'Over')
|
||||
assert.equal(Math.max(last.score.player, last.score.cpu), WIN)
|
||||
})
|
||||
|
||||
test('m11 the clock runs every 30 ms', () => assert.equal(FRAME_MS_PONG, 30))
|
||||
|
||||
test('m11 pongActions turns the clock into Frames and keys into Press, Release and Restart', () =>
|
||||
assert.deepStrictEqual(
|
||||
run(pongActions, freeze([time(30), down('ArrowUp'), up('ArrowUp'), down('s'), down('r'), up('r'), down('x'), time(90)])),
|
||||
[PongAction.Frame, PongAction.Press('Up'), PongAction.Release('Up'), PongAction.Press('Down'), PongAction.Restart,
|
||||
PongAction.Frame, PongAction.Frame]))
|
||||
|
||||
test('m11 pongStates starts with the new match, then yields one state per action', () => {
|
||||
const out = run(pongStates(42, pongActions), freeze([time(30), down('w'), time(60)]))
|
||||
assert.equal(out.length, 4)
|
||||
assert.deepStrictEqual(out[0], initialPong(42))
|
||||
assert.deepStrictEqual(out.map((s) => s.frame), [0, 1, 1, 2])
|
||||
assert.deepStrictEqual(out.map((s) => s.held.up), [false, false, true, true])
|
||||
assert.deepStrictEqual(out[3].player, 20)
|
||||
})
|
||||
|
||||
test('m11 pongMain asks for a seed, draws the new match first, and then the updated one', () => {
|
||||
const { ops, result: stream } = trace(pongMain, 42)
|
||||
assert.deepStrictEqual(ops, [{ tag: 'RandomSeed' }])
|
||||
const [first, next] = stream.step(time(0))
|
||||
const [second] = next.step(time(30))
|
||||
assert.deepStrictEqual(trace(first[0], 0).ops.map((o) => o.shape), pongView(initialPong(42)))
|
||||
assert.deepStrictEqual(
|
||||
trace(second[0], 0).ops.map((o) => o.shape),
|
||||
pongView(pongUpdate(initialPong(42), PongAction.Frame)))
|
||||
})
|
||||
Reference in New Issue
Block a user