The pack
paddleBall
function paddleBall(): Pack<{ paddleBall: typeof kit }>
Install the renderer-independent paddle, brick, capsule and opponent rules as game.paddleBall.
Helpers
arkanoidPreset
function arkanoidPreset(options: ArkanoidOptions = {}): PaddleConfig
Arcade Arkanoid (1986) on a 13 × 32-unit-wide field: gradual ball speed-up with a hard cap; all seven capsules with their exclusivity rules; silver bricks needing 2 hits plus one per 8 rounds and scoring 50 × round; indestructible gold; colour scores 50–120; drifting enemies from two top hatches worth 100; break warp worth 10,000; extra Vaus at 20,000 and every 60,000.
breakoutPreset
function breakoutPreset(options: BreakoutOptions = {}): PaddleConfig
Atari Breakout (1976): eight rows in four colour pairs (yellow, green, orange, red from the bottom) worth 1, 3, 5 and 7 points; the ball speeds up after 4 and 12 paddle hits and on first contact with the orange and red rows; the paddle halves when the ball breaks through to the top wall; two screens of bricks; three balls; a hard speed cap.
brickGrid
function brickGrid( rows: readonly string[], legend: BrickLegend, geometry: BrickGridGeometry, ): BrickDefinition[]
Author bricks from text rows: each character is a legend key, and . or space is empty.
Row 0 is the top. Ids are <prefix>-<col>-<row>, stable across rounds.
createPaddleGame
function createPaddleGame(config: PaddleConfig): PaddleGame
Create a headless paddle game from an authored definition, independent of input and rendering.
createPaddleOpponent
function createPaddleOpponent(config: PaddleOpponentConfig): PaddleOpponent
Create a reusable fallible opponent; several opponents can control any authored paddles.
paddleOpponentPreset
function paddleOpponentPreset( difficulty: PaddleDifficulty = "normal", options: Partial<PaddleOpponentConfig> & { scale?: number } = {}, ): PaddleOpponentConfig
Editable skill presets tuned on the 800 × 600 classic Pong preset; pass scale (field extent along
the paddle axis ÷ 600) to keep them proportionate on other fields.
- easy: tracks the ball's current position late and slowly; most steep returns get past it.
- normal: reads the landing point once the ball crosses the centre line, with a sizeable error that grows as the rally speeds up.
- hard: reads the landing point from the far side, moves fast, and angles its own returns.
paddlePreset
function paddlePreset( kind: "breakout" | "arkanoid" | "pong" = "breakout", options: PaddleGeneration = {}, ): PaddleConfig
Shorthand for the three classic presets. Use the named preset functions for every option.
pongPreset
function pongPreset(options: PongOptions = {}): PaddleConfig
Classic 1972 Pong: two vertical paddles, goals left and right, first to 11. Each paddle is split into eight segments: the two centre segments return the ball square, the outer ones at growing angles. The ball speeds up on the 4th and 12th return of a rally, creeps up a little on every return, and is hard-capped; it resets to serve speed after each point. The serve comes from the centre line toward the player who conceded the point.
Classes
PaddleGame
class PaddleGame
Owns swept ball contacts, rebound aiming, serving, rally speed and its cap, brick damage, rounds, capsules, lasers, enemies, warp, lives and outcomes. Renderer and input independent.
readonly config: PaddleConfig— The validated configuration (a private copy).constructor(cfg: PaddleConfig)get state(): PaddleStateSnapshot— Copied current state.get view(): Readonly<PaddleStateSnapshot>— The live state without copying, for per-frame reads by renderers and controllers. Treat it as read-only: mutate only through the game's methods. Usestate/snapshot()for a detached copy.get roundCount(): number— The authored round list (a single round when onlybricksis given).get enemyKind(): string— Enemy presentation kind of the current round.setRound(index: number)— Jump to a zero-based round (round select / debug); keeps score and lives.serve(): boolean— Launch a waiting serve. Returns false when nothing was waiting.movePaddle(id: string, position: number)— Place a paddle's centre along its axis; clamped inside the field.fire(paddleId = this.s.paddles[0]!.id): "serve" | "release" | "laser" | null— The fire button: launches a waiting serve, releases a caught ball, or fires the laser. Returns what it did.paddleTarget(id: string, ballId?: number): number— Predict where a free ball will cross a paddle's line, reflecting off the side walls. Used by AI controllers; it ignores bricks and enemies.power(kind: PowerKind)— Apply a capsule effect now, exactly as catching its capsule would (without the catch score).tick(dt: number): Readonly<PaddleStateSnapshot>— Advance the rules bydtseconds (0..10) in fixed 1/120 s slices. Returns the live read-only view.drain(): PaddleEvent[]— Remove and return the events since the last drain.snapshot(): PaddleStateSnapshot— A detached copy of the full state for replay or save.restore(state: PaddleStateSnapshot)— Restore a snapshot taken from a game with the same configuration.reset()— Start again from the configured first round with the configured seed.
PaddleOpponent
class PaddleOpponent
A bounded, fallible paddle opponent. Call tick(game, dt) once per host step before game.tick.
It watches only balls heading toward its paddle, waits its reaction time, re-observes at its
decision interval, carries a per-flight aiming error that grows with ball speed, and moves no
faster than its speed cap — so steep, fast returns beat it while slow, central ones do not.
readonly config: PaddleOpponentConfig— The validated configuration.constructor(config: PaddleOpponentConfig)get state(): PaddleOpponentState— A detached copy of the controller state.tick(game: PaddleGame, dt: number): PaddleOpponentState— Observe, decide and move the paddle through the game's publicmovePaddle.snapshot(): PaddleOpponentState— A detached copy for replay.restore(state: PaddleOpponentState)— Restore a snapshot.reset()— Forget the current flight and reseed.
Constants
ARKANOID_LEGEND
const ARKANOID_LEGEND: BrickLegend
Arkanoid brick legend: colour scores, silver (2 hits +1 per 8 rounds, 50 × round) and gold.
ARKANOID_ROUNDS
const ARKANOID_ROUNDS: readonly { name: string; enemyKind: string; rows: readonly string[] }[]
The first five rounds of arcade Arkanoid (1986) as 13-column text grids, row 0 at the top of the playfield. Reconstructed from play references; see the pack docs for sources.
BREAKOUT_COLOURS
const BREAKOUT_COLOURS: readonly ["yellow", "green", "orange", "red"]
Colour names of Breakout's row pairs, bottom pair first.
POWER_KINDS
const POWER_KINDS: readonly PowerKind[]
Every supported capsule effect, in the arcade's S C E D L B P order.
Types
ArkanoidOptions
interface ArkanoidOptions
Options for the Arkanoid preset.
seed?: number— Seed for serves, capsules and enemies; unsigned integer, zero included; default 1.startRound?: number— One-based round to start at. Default 1.rounds?: readonly { name?: string; enemyKind?: string; rows: readonly string[] }[]— Override the round list (text grids usinglegend). DefaultARKANOID_ROUNDS.legend?: BrickLegend— Override or extend the brick legend.columns?: number— Columns in the field. Default 13.lives?: number— Turns. Default 3.speedScale?: number— Multiplies every ball speed. Default 1.capsuleWeights?: Partial<Record<PowerKind, number>>— Capsule weights. Default S3 C3 E3 D3 L3 B1 P1.capsuleChance?: number— Probability per eligible destroyed brick. Default 0.18.enemies?: boolean— Drifting enemies. Default true.afterLastRound?: "win" | "loop"— After the last authored round, loop (silver keeps hardening) or finish as won. Defaultloop.
BounceRules
interface BounceRules
Paddle rebound response.
maxAngle?: number— Continuous response: offset −1..1 along the paddle maps linearly to ±maxAngle. Default π/3.segments?: number[]— Segmented response (Pong/Breakout): the paddle is split into equal segments, low coordinate first, each returning at its listed angle in radians from the paddle normal. OverridesmaxAngle.minTravelAngle?: number— Minimum angle between a ball's path and the paddle's sliding axis after wall, brick and enemy contacts, so a ball can never settle into a flat, endless traverse. Default 0.2 rad; 0 disables.
BreakoutOptions
interface BreakoutOptions
Options for the Breakout preset.
seed?: number— Seed for the serve angles; unsigned integer, zero included; default 1.variant?: "arcade" | "atari2600"—arcade(1976: 8 rows, 1/3/5/7 points, 3 balls) oratari2600(6 rows, 1/1/4/4/7/7, 5 balls).columns?: number— Bricks per row. Default 14.rows?: number— Rows of bricks; colours and scores repeat in pairs from the bottom. Default by variant.screens?: number— Screens of bricks to clear. Default 2.lives?: number— Balls (turns). Default 3 arcade, 5 Atari 2600.speedScale?: number— Multiplies every ball speed. Default 1.scores?: number[]— Per-row-pair scores from the bottom pair up. Default by variant.shrink?: boolean— Paddle shrinks to half after the ball first reaches the top wall. Default true.width?: number— Field width in field units; default 448. Each brick is this wide divided bycolumns.height?: number— Field height in field units; default 600. Brick height, the top margin and the paddle's position scale with it.
BrickDefinition
interface BrickDefinition
An authored axis-aligned brick rectangle (top-left corner), durability, reward and policies.
id: string— Unique id within its round.x: number— Left edge.y: number— Top edge.w: number— Width.h: number— Height.hp?: number— Hits to destroy. Default 1.score?: number— Points when destroyed. Default 10. Ignored whenscorePerRoundis set.kind?: string— Free-form kind for presentation (a colour name such assilver). The rules never read it.drop?: PowerKind | "none"— Force this capsule when destroyed, ornoneto never drop one.indestructible?: boolean— A gold brick: rebounds the ball and is never destroyed or required for a clear.speedFloor?: number— Contact raises the rally speed to at least this (Breakout's orange/red rows).hpEveryRounds?: number— Add one hit point per this many rounds (Arkanoid silver: 8). Round 1 useshp.scorePerRound?: number— Score this many points times the round number instead ofscore(Arkanoid silver: 50).
BrickGridGeometry
interface BrickGridGeometry
Grid geometry for brickGrid: cell size and the top-left corner of cell (0, 0).
cellW: number— Width of one brick cell in field units; at least 0.001.cellH: number— Height of one brick cell in field units; at least 0.001.x0?: number— Field x of the left edge of column 0; default 0.y0?: number— Field y of the top edge of row 0; default 0.prefix?: string— Id prefix. Defaultbrick.
BrickLegend
type BrickLegend = Record< string, Omit<BrickDefinition, "id" | "x" | "y" | "w" | "h"> >
One character of a brick grid: what kind of brick it places.
BrickState
type BrickState = BrickDefinition & { hp: number; maxHp: number; score: number }
A brick in play: its effective hit points and current score for this round.
CapsuleRules
interface CapsuleRules
Capsule drops and their effects. Every rule is optional and documented with its arcade default.
weights: Partial<Record<PowerKind, number>>— Relative weights of capsule kinds a destroyed brick may release.chance?: number— Probability that an eligible destroyed brick releases a capsule. Default 0.2.fallSpeed?: number— Fall speed, units per second. Default 90.w?: number— Capsule width. Default 32.h?: number— Capsule height. Default 14.score?: number— Points for catching any capsule. Default 1000.oneAtATime?: boolean— At most one capsule falls at a time. Default true.noneWhileMultiball?: boolean— No capsule drops while more than one ball is in play. Default true.fromDurable?: boolean— Bricks authored with more than one hit point (silver) may drop. Default false.exclusive?: boolean— Only one lasting effect (catch, expand, laser, disruption) at a time; any capsule ends it. Default true.noRepeatActive?: boolean— Never drop the kind that is currently active. Default true.seconds?: number | null— Lasting effect duration in seconds;nulllasts until replaced or the ball is lost. Default null.expandScale?: number— Expand length multiplier. Default 1.5.slowSpeed?: number— Rally speed after a slow capsule. Defaultspeed.min.disruptionBalls?: number— Total balls after disruption. Default 3.disruptionSpread?: number— Total angular spread of disruption balls, radians. Default 0.9.catchSeconds?: number | null— Seconds before a caught ball releases itself;nullwaits forfire(). Default 3.
CapsuleState
interface CapsuleState extends Cell
A falling capsule (centre position).
id: number— Unique id.kind: PowerKind— Effect when caught.
Edge
type Edge = "left" | "right" | "top" | "bottom"
A field edge.
EdgePolicy
type EdgePolicy = "wall" | "goal" | "drain"
What an edge does to a ball: rebound, score for the opposing team, or lose the ball.
EnemyRules
interface EnemyRules
Drifting enemies that enter from hatches, wander down, deflect the ball and are worth points.
hatches: number[]— Hatch x positions along the top wall.radius?: number— Collision radius. Default 12.speed?: number— Drift speed, units per second. Default 45.max?: number— Maximum alive at once. Default 3.interval?: number— Seconds between releases while belowmax. Default 4.firstDelay?: number— Seconds after a serve or round start before the first release. Default 3.score?: number— Points per enemy destroyed by ball, paddle or laser. Default 100.turnSeconds?: [number, number]— Range of seconds between heading changes. Default [0.6, 1.6].downBias?: number— 0 wanders in any downward direction, 1 heads straight down. Default 0.35.
EnemyState
interface EnemyState extends Cell
A drifting enemy (centre position) and its current heading.
id: number— Unique id.heading: number— Heading in radians (π/2 is straight down).turn: number— Seconds until the next heading change.kind: string— Presentation kind from the round'senemyKind.
LaserRules
interface LaserRules
Laser capsule cannons.
speed?: number— Shot speed, units per second. Default 600.cooldown?: number— Seconds between volleys. Default 0.2.maxShots?: number— Maximum shots in flight (each volley fires two). Default 4.length?: number— Shot height for sweeping, units. Default 12.perVolley?: 1 | 2— Shots per volley: two from the paddle ends (default) or one from the centre.
PaddleBall
interface PaddleBall extends Cell
Serializable ball. attached names the paddle it rests on (serve or catch).
id: number— Unique ball id.vx: number— Velocity x, units per second.vy: number— Velocity y, units per second.radius: number— Collision half-size.attached: string | null— Paddle the ball rests on, or null in flight.offset: number— Offset along the paddle while attached.held: number— Seconds a caught ball has been held.
PaddleConfig
interface PaddleConfig
Field geometry, paddles, bricks/rounds, edge policies, scoring and every gameplay rule.
width: number— Field width in field units.height: number— Field height in field units.paddles: PaddleDefinition[]— Authored paddles; at least one.bricks?: BrickDefinition[]— A single brick screen. Ignored whenroundsis set.rounds?: RoundDefinition[]— A progression of brick screens; clearing one loads the next.startRound?: number— Zero-based round to start at. Default 0.afterLastRound?: "win" | "loop"— After the last round: finish as won (default) or loop back with an increasing round number.roundDelay?: number— Seconds the cleared screen is shown before the next round. Default 0.edges?: Partial<Record<Edge, EdgePolicy>>— Per-edge policy. Default: walls, with a draining bottom.goalTeams?: Partial<Record<Edge, string>>— Team credited when a ball enters a goal edge. Default: the edge name.goal?: "bricks" | "score" | "none"—bricks: clear every round.score: first team totargetScore.none: endless.targetScore?: number— Default 11.winBy?: number— Winning margin required. Default 1 (classic Pong).lives?: number— Turns for brick games. Default 3.ballRadius?: number— Ball radius (square half-size for collisions). Default 6.speed: SpeedRules— Rally speed, speed-ups and the hard cap.bounce?: BounceRules— Paddle rebound response.serve?: ServeRules— Serve position, direction and timing.servePaddle?: string— Paddle used by paddle serves. Default the first paddle.shrink?: ShrinkRules— Breakout's paddle shrink.capsules?: CapsuleRules— Capsule drops and effects.laser?: LaserRules— Laser capsule cannons.enemies?: EnemyRules— Drifting enemies.warp?: WarpRules— Break capsule exit.extraLives?: { first: number; every?: number }— Extra turn atfirstpoints and everyeverypoints after.maxBalls?: number— Simultaneous ball budget. Default 64.seed?: number— Seed for serves, capsules and enemies.
PaddleDefinition
interface PaddleDefinition
An authored paddle centre, movement axis, dimensions and optional team identity.
id: string— Unique paddle id.axis: "x" | "y"—x: the paddle slides horizontally (Breakout);y: vertically (Pong).x: number— Centre x in field units.y: number— Centre y in field units.length: number— Extent along the movement axis.thickness?: number— Thickness across the movement axis. Default 12.team?: string— Team credited bygoalTeams, and used to aim aloser/winnerserve.
PaddleDifficulty
type PaddleDifficulty = "easy" | "normal" | "hard"
Named opponent skill levels.
PaddleEvent
interface PaddleEvent
Drained gameplay event; presentation reacts to these (sounds, flashes, banners).
kind: | "serve" | "paddle" | "wall" | "brick" | "brick-hit" | "goal" | "lost-ball" | "life-lost" | "round" | "cleared" | "capsule-drop" | "power" | "laser" | "enemy-spawn" | "enemy" | "warp" | "extra-life" | "shrink" | "speed" | "won" | "lost"— What happened.id?: string— Brick, paddle, team, edge or round identity, by kind.power?: PowerKind— Capsule kind forcapsule-dropandpower.x?: number— Where it happened, when positional.y?: number— Where it happened, when positional.points?: number— Points awarded.
PaddleGeneration
interface PaddleGeneration
Field geometry, seed and brick population for the legacy generator.
seed?: number— Seed passed to the preset; unsigned integer, zero included; default 1.columns?: number— Bricks per row for breakout, or columns of the field for arkanoid; ignored for pong.rows?: number— Rows of bricks for breakout; ignored by pong and arkanoid.width?: number— Field width in field units, for pong and breakout; arkanoid's width comes from its columns.height?: number— Field height in field units, for pong and breakout; arkanoid's height is fixed.lives?: number— Turns for breakout and arkanoid; ignored by pong.speedScale?: number— Multiplies every ball speed, 0.1 to 10; default 1.
PaddleOpponentConfig
interface PaddleOpponentConfig
Skill parameters for a computer-controlled paddle on either axis. Every field is a plain number so difficulty is a dial, not a fixed table.
paddleId: string— The paddle this opponent drives.mode?: "track" | "predict"—trackfollows the ball's current position (beatable with angled shots);predictreads its landing point. Defaultpredict.speed?: number— Movement cap in field units per second.reactionSeconds?: number— Delay before reacting to each incoming flight, seconds.decisionSeconds?: number— Interval between fresh observations, seconds; the paddle steers on stale information between them.attention?: number— How far away the ball may be before the opponent starts to move, as a fraction of the distance across the field (1 = from the far side, 0.5 = once it crosses the centre line). Default 1.aimError?: number— Maximum signed aiming error at serve speed, field units; sampled once per incoming flight.errorPerSpeed?: number— Extra error per unit of (ball speed / serve speed − 1): faster rallies make it miss more.aim?: number— 0..1: how far from its centre it tries to strike the ball, to return angled shots. Default 0.deadZone?: number— Ignore corrections smaller than this distance.recentreSpeed?: number— Return speed when no ball is incoming; zero leaves the paddle in place. Default speed × 0.6.seed?: number— Seed for aiming error.
PaddleOpponentState
interface PaddleOpponentState
Serializable opponent attention, aiming bias and seeded random state.
ball: number | null— Ball being tracked.target: number | null— Current aim point along the paddle axis.reaction: number— Reaction delay remaining.decision: number— Seconds until the next observation.error: number— This flight's aiming error.aimOffset: number— This flight's deliberate off-centre strike.random: number[]— The aiming-error stream: four words of the kernel'screateRngstate, advanced in place.
PaddleState
interface PaddleState extends PaddleDefinition
Runtime paddle pose and length relative to its authored base length.
baseLength: number— Authored length before shrink or expand.
PaddleStateSnapshot
interface PaddleStateSnapshot
Complete game progress; restore() accepts it back for deterministic replay.
paddles: PaddleState[]— Paddles in authored order.bricks: BrickState[]— Bricks of the current round (destroyed ones keep hp 0).balls: PaddleBall[]— Balls in play or waiting.drops: CapsuleState[]— Falling capsules.shots: ShotState[]— Laser shots in flight.enemies: EnemyState[]— Drifting enemies.score: number— Points from bricks, capsules, enemies and the warp.scores: Record<string, number>— Goals per team.lives: number— Turns left, including the current one.round: number— Zero-based index intorounds(0 without rounds).roundNumber: number— One-based round number; keeps counting when rounds loop.speed: number— Current rally speed.hits: number— Counted hits this serve (seespeed.hitCounter).rally: number— Paddle returns this serve.active: PowerKind | null— The lasting capsule effect, if any.activeTime: number— Seconds the active effect has lasted.warpOpen: boolean— The break warp is open.shrunk: boolean— The paddle is shrunk this turn.serveTimer: number— Seconds until an automatic serve.roundTimer: number— Seconds until the next round loads.enemyTimer: number— Seconds until the next enemy release.laserTimer: number— Laser cooldown remaining.lastGoal: Edge | null— Edge of the last goal, for loser/winner serves.nextLifeAt: number— Score that awards the next extra turn.time: number— Simulated seconds.status: "serving" | "playing" | "cleared" | "won" | "lost"— Waiting to serve, in play, showing a cleared round, or finished.random: number[]— The serve, capsule and enemy stream: four words of the kernel'screateRngstate, advanced in place.next: number— Next entity id.
PongOptions
interface PongOptions
Options for the classic Pong preset.
seed?: number— Seed for the serve angles and jitter; unsigned integer, zero included; default 1.width?: number— Field width in field units; default 800.height?: number— Field height in field units; default 600. Paddle length, ball radius and serve jitter scale with it.targetScore?: number— Points to win. Default 11.winBy?: number— Required winning margin. Default 1 (classic).speedScale?: number— Multiplies every ball speed. Default 1.paddleLength?: number— Paddle length. Default height / 9.4.serveToward?: "loser" | "winner" | "alternate" | "random"— Who receives the serve after a point. Defaultloser(the player who missed).serveDelay?: number— Seconds before each automatic serve. Default 1.2.
PowerKind
type PowerKind = | "slow" | "catch" | "expand" | "disruption" | "laser" | "break" | "player"
Capsule (power-up) effects. slow (S), catch (C), expand (E), disruption (D, split into
several balls), laser (L), break (B, open the warp exit) and player (P, extra life).
RoundDefinition
interface RoundDefinition
One screen of bricks in a progression.
name?: string— Display name.bricks: BrickDefinition[]— The bricks of this screen.enemyKind?: string— Presentation hint for the drifting enemies of this round (e.g.cone).
ServeRules
interface ServeRules
Where and how a ball is put into play.
from?: "paddle" | "centre" | "point"—paddle: rests on the serving paddle;centre: field centre;point: atx,y.x?: number— Serve point x forfrom: "point".y?: number— Serve point y forfrom: "point".toward?: | "loser" | "winner" | "alternate" | "random" | "up" | "down" | "left" | "right"— Direction of a free serve.loserheads to the side that conceded the last goal (Pong),winnerto the scorer's side,alternateflips each serve,random; or a fixed screen direction. A paddle serve always heads away from its paddle.angles?: number[]— Candidate launch angles in radians from straight; one is chosen at random with a random sign.jitter?: number— Random offset (± units) of a free serve along its cross axis. Default 0.autoSeconds?: number | null— Seconds before an automatic serve;nullwaits forserve(). Defaultnull.
ShotState
interface ShotState extends Cell
A laser shot (top centre).
id: number— Unique id.
ShrinkRules
interface ShrinkRules
Breakout's paddle shrink: the first time a ball reaches edge in a turn, the paddle shrinks.
edge: Edge— The wall whose first contact shrinks the paddle.scale?: number— Length multiplier. Default 0.5.restoreOnServe?: boolean— Restore full length at the next serve. Default true.
SpeedRules
interface SpeedRules
Rally-speed rules. Every free ball travels at the shared rally speed, never above max.
serve: number— Speed at each serve, in field units per second.max: number— Hard cap. No rule, capsule or contact can exceed it.min?: number— Floor for slowing effects. Defaultserve * 0.5.perPaddleHit?: number— Added on every paddle return.perContact?: number— Added on every brick, wall, enemy and paddle contact (Arkanoid's gradual creep).perSecond?: number— Added per second of free flight.hitCounter?: "paddle" | "brick" | "contact"— Which contacts advancehitsforhitSteps. Defaultpaddle.hitSteps?: { hits: number; speed: number }[]— On reachinghitscounted contacts this serve, the speed rises to at leastspeed.
WarpRules
interface WarpRules
Arkanoid's break capsule: an exit in a side wall level with the paddle.
edge: "left" | "right"— Side wall holding the exit.score?: number— Points for leaving through the warp. Default 10000.


