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,24 @@
|
||||
import { todo } from '../todo.js'
|
||||
import { pure } from '../io.js'
|
||||
|
||||
/** Milliseconds between frames of the match. */
|
||||
export const FRAME_MS_PONG = 30
|
||||
|
||||
/**
|
||||
* Translate a key event into a `PongAction`, or `null`. `isDown` is true for a key press, false for a release.
|
||||
* `ArrowUp`/`w` -> `Press('Up')` / `Release('Up')`, `ArrowDown`/`s` -> `Press('Down')` / `Release('Down')`,
|
||||
* `r` -> `Restart` (on press only; releasing `r` does nothing). Any other key -> `null`.
|
||||
*/
|
||||
export const pongKey = (isDown, key) => todo('m11')
|
||||
|
||||
/**
|
||||
* Everything that can happen in a match: `Frame` every FRAME_MS_PONG (from `interval`), merged with the
|
||||
* `pongKey(true, key)` of every key press and the `pongKey(false, key)` of every key release.
|
||||
*/
|
||||
export const pongActions = { step: () => todo('m11') }
|
||||
|
||||
/** Fold the actions into a stream of states: start from `initialPong(seed)`, `pongUpdate` for each action, emit the initial state first. */
|
||||
export const pongStates = (seed, actions) => todo('m11')
|
||||
|
||||
/** The whole program as a value, like `main`: ask for a seed, turn the stream of states into a stream of drawings of `pongView`. */
|
||||
export const pongMain = pure({ step: () => todo('m11') })
|
||||
@@ -0,0 +1,5 @@
|
||||
import { runApp } from '../runtime.js'
|
||||
import { browserPlatform } from '../browser.js'
|
||||
import { pongMain } from './app.js'
|
||||
|
||||
runApp(pongMain, browserPlatform(document.getElementById('game'), window))
|
||||
@@ -0,0 +1,36 @@
|
||||
import { todo } from '../todo.js'
|
||||
|
||||
/** @typedef {{ball: {x: number, y: number}, vel: {dx: number, dy: number}, player: number, cpu: number,
|
||||
* held: {up: boolean, down: boolean}, score: {player: number, cpu: number},
|
||||
* status: 'Playing'|'Over', seed: number, frame: number}} PongState */
|
||||
|
||||
// The court, in cells. `player` and `cpu` are the y of the top cell of each paddle.
|
||||
export const W = 80
|
||||
export const H = 50
|
||||
/** Pixels per cell. */
|
||||
export const PCELL = 8
|
||||
/** Paddle height in cells. */
|
||||
export const PADDLE = 8
|
||||
/** Column of the player's paddle (left) and the cpu's paddle (right). */
|
||||
export const PLAYER_X = 2
|
||||
export const CPU_X = 77
|
||||
/** Points needed to win the match. */
|
||||
export const WIN = 5
|
||||
|
||||
/**
|
||||
* A fresh match: ball at (40,25) moving (dx 1, dy 1), both paddles at y 21, no keys held,
|
||||
* score 0 to 0, status 'Playing', the given seed, frame 0.
|
||||
*/
|
||||
export const initialPong = (seed) => todo('m11')
|
||||
|
||||
/**
|
||||
* Everything that can happen in a match, as data:
|
||||
* `Press(key)` / `Release(key)` for key 'Up' or 'Down' (`{tag, key}`), and `Frame` / `Restart` (`{tag}`).
|
||||
* The getters only exist so the stub compiles; replace them with plain values.
|
||||
*/
|
||||
export const PongAction = {
|
||||
Press: (key) => todo('m11'),
|
||||
Release: (key) => todo('m11'),
|
||||
get Frame() { return todo('m11') },
|
||||
get Restart() { return todo('m11') },
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import { todo } from '../todo.js'
|
||||
|
||||
/** Keep `x` inside `[lo, hi]`. */
|
||||
export const clamp = (lo, hi, x) => todo('m11')
|
||||
|
||||
/**
|
||||
* The one function that says how a match changes: `(state, action) -> state`.
|
||||
*
|
||||
* - `Press(key)` / `Release(key)`: set `held.up` or `held.down` (even when the match is over).
|
||||
* - `Restart`: `initialPong(state.seed)`.
|
||||
* - `Frame`: unchanged unless status is 'Playing'. Otherwise, in this order:
|
||||
* 1. `frame + 1`.
|
||||
* 2. The player moves one cell down for `held.down` and one up for `held.up` (both cancel out),
|
||||
* kept within `[0, H - PADDLE]`.
|
||||
* 3. When the new `frame` is even, the cpu moves one cell toward `ball.y - PADDLE / 2` (it stays put when it is
|
||||
* already there), kept within `[0, H - PADDLE]`. It looks at the ball before the ball moves.
|
||||
* 4. The ball moves vertically: `ny = y + dy`; if `ny < 0 || ny >= H` then `dy` flips and `ny = y + dy`.
|
||||
* 5. Horizontally: `nx = x + dx`. It bounces (`dx` flips, `nx = x`) when `nx === PLAYER_X`, `dx < 0` and
|
||||
* `player <= ny < player + PADDLE` (using the player's new position), or when `nx === CPU_X`, `dx > 0` and
|
||||
* `cpu <= ny < cpu + PADDLE`.
|
||||
* 6. If `nx < 0` the cpu scores; if `nx >= W` the player scores. A point serves a new ball: position (40,25),
|
||||
* `dx` toward the side that conceded (cpu scored -> -1, player scored -> +1), `dy = 1` when
|
||||
* `nextUint32(seed)` is even and `-1` otherwise, and the seed advances.
|
||||
* 7. A score of `WIN` ends the match: status 'Over'.
|
||||
*/
|
||||
export const pongUpdate = (state, action) => todo('m11')
|
||||
@@ -0,0 +1,11 @@
|
||||
import { todo } from '../todo.js'
|
||||
|
||||
/**
|
||||
* Describe the match as shapes, drawn in order (all text is '#ffffff', align 'center'):
|
||||
* background Rect(0,0,640,400,'#111111'); player paddle Rect(PLAYER_X*8, player*8, 8, 64, '#ffffff'), then the
|
||||
* cpu paddle the same way at CPU_X; ball Rect(x*8, y*8, 8, 8, '#ffeb3b');
|
||||
* score Text(320, 30, '<player> <cpu>' (three spaces), size 24).
|
||||
* When Over: Text(320, 200, 'You Win!' if the player has more points, else 'CPU Wins', size 32) and
|
||||
* Text(320, 230, 'Press R to restart', size 16).
|
||||
*/
|
||||
export const pongView = (state) => todo('m11')
|
||||
Reference in New Issue
Block a user