// Authored scenes, performed by the real engine and filmed by the reel. // A screenplay deals a seeded game, slips the named cards into hands (a // film set may rig its props), then runs its command list through // applyCommand — every event on screen really happened under the rules. // A command the engine refuses is an authoring error and says so loudly. import { applyCommand, createGame, viewFor, type Command, type GameEvent, type GameState, type GameView, } from "@wizwar/engine"; export interface ScreenplayMove { seat: string; cmd: Command; } export interface Screenplay { /** URL slug and clip filename: lowercase kebab. */ name: string; title: string; /** One gallery sentence: what the clip shows off. */ blurb: string; players: string[]; seed: number; /** Whose eyes the reel opens behind (the camera still follows each * turn's owner, as the reel always does). */ pov: string; /** Cards slipped into hands before the action: seat → cardIds. Rigged * instanceIds are `#rig`. */ rig?: Record; moves: ScreenplayMove[]; /** Step indices the render harness pins as golden frames, first-person: * the scene's money shots — a warp transit, an impact, a curse landing. * deploy/verify-scenes.sh screenshots each beat and diffs it against * the committed golden. */ beats?: number[]; /** One step pinned from the board view. */ boardBeat?: number; } export interface ScreenplayStep { seq: number; actor: string; events: GameEvent[]; view: GameView; } /** The card a rig entry minted, for use in the same screenplay's moves. */ export function rigId(cardId: string, n = 1): string { return `${cardId}#rig${n}`; } export function buildScreenplaySteps(sp: Screenplay): ScreenplayStep[] { const { state: dealt } = createGame({ playerIds: sp.players, seed: sp.seed, sets: ["basic", "expansion1"], }); let s: GameState = dealt; for (const [seat, cardIds] of Object.entries(sp.rig ?? {})) { const p = s.players.find((q) => q.id === seat); if (!p) throw new Error(`screenplay ${sp.name}: rig names unknown seat ${seat}`); const counts: Record = {}; for (const cardId of cardIds) { counts[cardId] = (counts[cardId] ?? 0) + 1; p.hand.push({ instanceId: rigId(cardId, counts[cardId]!), cardId }); } } const steps: ScreenplayStep[] = []; sp.moves.forEach((m, i) => { const r = applyCommand(s, m.seat, m.cmd); if (!r.ok) { throw new Error( `screenplay ${sp.name}: move ${i} (${m.seat} ${m.cmd.type}) refused — ${r.error}`); } s = r.state; steps.push({ seq: i, actor: m.seat, // The reel is omniscient cinema — no fog, no redaction — but what // was dealt or drawn stays off the record even here. events: r.events.filter((e) => !e.type.endsWith("Private")), view: viewFor(s, sp.pov), }); }); return steps; } // --------------------------------------------------------------------------- // The catalog. All scenes play on the two-wizard seed-42 board: // homes at (2,2) and (2,7); rim warps (2,0)N↔(2,9)S, (0,2)W↔(4,7)E, // (4,2)E↔(0,7)W — the last pair guarding a chamber no corridor reaches. // --------------------------------------------------------------------------- const DUEL = { players: ["Wanderer", "Rival"], seed: 42 }; /** Rival's road from home (2,7) to the south warp mouth: five strides * ending through the wormhole onto (2,0), in the enemy's sightline. */ const RIVAL_TO_NORTH: ScreenplayMove[] = [ { seat: "Rival", cmd: { type: "playNumberForMovement", instanceId: rigId("number-2") } }, { seat: "Rival", cmd: { type: "move", direction: "W" } }, { seat: "Rival", cmd: { type: "move", direction: "S" } }, { seat: "Rival", cmd: { type: "move", direction: "E" } }, { seat: "Rival", cmd: { type: "move", direction: "S" } }, { seat: "Rival", cmd: { type: "move", direction: "S" } }, // through the warp ]; /** Round one bars combat, so every duel opens as a standoff: Rival ends * his marching turn at the north mouth, Wanderer backs two squares down * his corridor — still in the sightline — and round two begins. */ const STANDOFF: ScreenplayMove[] = [ ...RIVAL_TO_NORTH, { seat: "Rival", cmd: { type: "endTurn", draw: 0 } }, { seat: "Wanderer", cmd: { type: "move", direction: "S" } }, { seat: "Wanderer", cmd: { type: "move", direction: "S" } }, { seat: "Wanderer", cmd: { type: "endTurn", draw: 0 } }, ]; export const SCREENPLAYS: Screenplay[] = [ { ...DUEL, name: "wormhole-heist", title: "The Wormhole Heist", blurb: "Nine strides on a number card: through the south mouth, around the back row, out with a treasure.", pov: "Rival", rig: { Rival: ["number-6"] }, beats: [5, 10], boardBeat: 5, moves: [ { seat: "Rival", cmd: { type: "playNumberForMovement", instanceId: rigId("number-6") } }, { seat: "Rival", cmd: { type: "move", direction: "W" } }, { seat: "Rival", cmd: { type: "move", direction: "S" } }, { seat: "Rival", cmd: { type: "move", direction: "E" } }, { seat: "Rival", cmd: { type: "move", direction: "S" } }, { seat: "Rival", cmd: { type: "move", direction: "S" } }, // through the warp { seat: "Rival", cmd: { type: "move", direction: "W" } }, { seat: "Rival", cmd: { type: "move", direction: "W" } }, { seat: "Rival", cmd: { type: "move", direction: "S" } }, { seat: "Rival", cmd: { type: "move", direction: "E" } }, { seat: "Rival", cmd: { type: "pickUpTreasure" } }, { seat: "Rival", cmd: { type: "endTurn", draw: 0 } }, ], }, { ...DUEL, name: "warp-sniper", title: "The Warp Sniper", blurb: "Line of sight threads a wormhole: a fireball cast into one mouth arrives through the other.", pov: "Wanderer", rig: { Wanderer: ["number-3", "fireball"], Rival: ["number-3"] }, beats: [6, 16], boardBeat: 16, moves: [ { seat: "Rival", cmd: { type: "playNumberForMovement", instanceId: rigId("number-3") } }, { seat: "Rival", cmd: { type: "move", direction: "W" } }, { seat: "Rival", cmd: { type: "move", direction: "N" } }, { seat: "Rival", cmd: { type: "move", direction: "N" } }, { seat: "Rival", cmd: { type: "move", direction: "W" } }, { seat: "Rival", cmd: { type: "move", direction: "S" } }, { seat: "Rival", cmd: { type: "move", direction: "S" } }, // parked on the west mouth { seat: "Rival", cmd: { type: "endTurn", draw: 0 } }, { seat: "Wanderer", cmd: { type: "playNumberForMovement", instanceId: rigId("number-3") } }, { seat: "Wanderer", cmd: { type: "move", direction: "E" } }, { seat: "Wanderer", cmd: { type: "move", direction: "N" } }, { seat: "Wanderer", cmd: { type: "move", direction: "N" } }, { seat: "Wanderer", cmd: { type: "move", direction: "E" } }, { seat: "Wanderer", cmd: { type: "move", direction: "S" } }, { seat: "Wanderer", cmd: { type: "move", direction: "S" } }, // the east mouth's chamber { seat: "Wanderer", cmd: { type: "endTurn", draw: 0 } }, { seat: "Rival", cmd: { type: "endTurn", draw: 0 } }, // waits at the far mouth { seat: "Wanderer", cmd: { type: "cast", instanceId: rigId("fireball"), target: { kind: "player", playerId: "Rival" } } }, { seat: "Rival", cmd: { type: "pass" } }, { seat: "Wanderer", cmd: { type: "endTurn", draw: 0 } }, ], }, { ...DUEL, name: "springing-the-trap", title: "Springing the Trap", blurb: "An ambush watches its corridor through a wormhole — and fires the moment a wizard crosses the far sightline.", pov: "Rival", rig: { Wanderer: ["opportunity-fire", "fireball"] }, beats: [5, 6], boardBeat: 5, moves: [ { seat: "Rival", cmd: { type: "move", direction: "W" } }, { seat: "Rival", cmd: { type: "move", direction: "S" } }, { seat: "Rival", cmd: { type: "endTurn", draw: 0 } }, // one square shy of the sightline { seat: "Wanderer", cmd: { type: "setAmbush", instanceId: rigId("opportunity-fire"), trigger: { kind: "los" }, spellInstanceId: rigId("fireball") } }, { seat: "Wanderer", cmd: { type: "endTurn", draw: 0 } }, { seat: "Rival", cmd: { type: "move", direction: "E" } }, // into the corridor the wormhole watches { seat: "Rival", cmd: { type: "pass" } }, { seat: "Rival", cmd: { type: "move", direction: "W" } }, // singed, back out of the light { seat: "Rival", cmd: { type: "endTurn", draw: 0 } }, ], }, { ...DUEL, name: "the-leap", title: "The Leap", blurb: "A pit yawns in the corridor — but the die says the intruder sails clean over it, onto the trapper's doorstep.", pov: "Rival", rig: { Wanderer: ["create-pit"] }, beats: [8, 9], boardBeat: 8, moves: [ { seat: "Rival", cmd: { type: "move", direction: "W" } }, { seat: "Rival", cmd: { type: "move", direction: "S" } }, { seat: "Rival", cmd: { type: "move", direction: "E" } }, { seat: "Rival", cmd: { type: "endTurn", draw: 0 } }, { seat: "Wanderer", cmd: { type: "cast", instanceId: rigId("create-pit"), target: { kind: "cell", cell: { x: 2, y: 1 } } } }, { seat: "Wanderer", cmd: { type: "endTurn", draw: 0 } }, { seat: "Rival", cmd: { type: "move", direction: "S" } }, { seat: "Rival", cmd: { type: "move", direction: "S" } }, // through the warp { seat: "Rival", cmd: { type: "move", direction: "S" } }, // the pit — and the leap { seat: "Rival", cmd: { type: "punch", targetId: "Wanderer" } }, // landing fists first { seat: "Wanderer", cmd: { type: "pass" } }, { seat: "Rival", cmd: { type: "endTurn", draw: 0 } }, ], }, { ...DUEL, name: "come-here", title: "The Puppeteer", blurb: "Mental Force walks the enemy to exactly where you want them — which has its risks.", pov: "Wanderer", rig: { Wanderer: ["mental-force"], Rival: ["number-2"] }, beats: [10, 13], boardBeat: 10, moves: [ ...RIVAL_TO_NORTH, { seat: "Rival", cmd: { type: "endTurn", draw: 0 } }, { seat: "Wanderer", cmd: { type: "endTurn", draw: 0 } }, { seat: "Rival", cmd: { type: "endTurn", draw: 0 } }, // glowers from the mouth { seat: "Wanderer", cmd: { type: "cast", instanceId: rigId("mental-force"), target: { kind: "player", playerId: "Rival" }, params: { cell: { x: 2, y: 2 } } } }, { seat: "Rival", cmd: { type: "pass" } }, { seat: "Wanderer", cmd: { type: "endTurn", draw: 0 } }, { seat: "Rival", cmd: { type: "punch", targetId: "Wanderer" } }, { seat: "Wanderer", cmd: { type: "pass" } }, { seat: "Rival", cmd: { type: "endTurn", draw: 0 } }, ], }, { ...DUEL, name: "stone-gaze", title: "The Stone Gaze", blurb: "Medusa freezes a wizard solid — and stone, inconveniently, cannot be hurt.", pov: "Rival", rig: { Rival: ["number-2", "medusa", "number-5", "fireball"] }, beats: [10, 14], boardBeat: 10, moves: [ ...STANDOFF, { seat: "Rival", cmd: { type: "cast", instanceId: rigId("medusa"), numberInstanceIds: [rigId("number-5")], target: { kind: "player", playerId: "Wanderer" } } }, { seat: "Wanderer", cmd: { type: "pass" } }, { seat: "Rival", cmd: { type: "endTurn", draw: 0 } }, { seat: "Wanderer", cmd: { type: "endTurn", draw: 0 } }, // stone does not move { seat: "Rival", cmd: { type: "cast", instanceId: rigId("fireball"), target: { kind: "player", playerId: "Wanderer" } } }, { seat: "Wanderer", cmd: { type: "pass" } }, { seat: "Rival", cmd: { type: "endTurn", draw: 0 } }, ], }, { ...DUEL, name: "the-rout", title: "The Rout", blurb: "Go Away hurls a wizard down his own corridor, as straight a line as the maze allows.", pov: "Rival", rig: { Rival: ["number-2", "go-away", "number-5"] }, beats: [4, 11], boardBeat: 11, moves: [ ...STANDOFF, { seat: "Rival", cmd: { type: "cast", instanceId: rigId("go-away"), numberInstanceIds: [rigId("number-5")], target: { kind: "player", playerId: "Wanderer" } } }, { seat: "Wanderer", cmd: { type: "pass" } }, { seat: "Rival", cmd: { type: "endTurn", draw: 0 } }, ], }, { ...DUEL, name: "wall-comes-down", title: "Knock Knock", blurb: "Blast the wall between you — the rubble bloodies both sides of it — then step through the breach.", pov: "Wanderer", rig: { Wanderer: ["number-2", "destroy-wall"] }, beats: [6, 7], boardBeat: 6, moves: [ { seat: "Rival", cmd: { type: "endTurn", draw: 0 } }, // stands his ground at home { seat: "Wanderer", cmd: { type: "playNumberForMovement", instanceId: rigId("number-2") } }, { seat: "Wanderer", cmd: { type: "move", direction: "S" } }, { seat: "Wanderer", cmd: { type: "move", direction: "S" } }, { seat: "Wanderer", cmd: { type: "move", direction: "S" } }, { seat: "Wanderer", cmd: { type: "move", direction: "S" } }, // at (2,6), the wall between them { seat: "Wanderer", cmd: { type: "cast", instanceId: rigId("destroy-wall"), target: { kind: "edge", cell: { x: 2, y: 6 }, side: "S" } } }, { seat: "Wanderer", cmd: { type: "move", direction: "S" } }, // through the breach { seat: "Wanderer", cmd: { type: "endTurn", draw: 0 } }, { seat: "Rival", cmd: { type: "punch", targetId: "Wanderer" } }, // the host objects { seat: "Wanderer", cmd: { type: "pass" } }, { seat: "Rival", cmd: { type: "endTurn", draw: 0 } }, ], }, { ...DUEL, name: "shield-holds", title: "The Shield Holds", blurb: "Five points of fire meet a Full Shield, and the shield wins.", pov: "Wanderer", rig: { Rival: ["number-2", "fireball"], Wanderer: ["full-shield"] }, beats: [11, 12], boardBeat: 11, moves: [ ...STANDOFF, { seat: "Rival", cmd: { type: "cast", instanceId: rigId("fireball"), target: { kind: "player", playerId: "Wanderer" } } }, { seat: "Wanderer", cmd: { type: "counteract", instanceId: rigId("full-shield") } }, { seat: "Rival", cmd: { type: "pass" } }, { seat: "Rival", cmd: { type: "endTurn", draw: 0 } }, ], }, { ...DUEL, name: "fire-in-the-webs", title: "Fire in the Webs", blurb: "The Sticky Wand binds a wizard in silk — and fire burns twice as hot in a web.", pov: "Rival", rig: { Rival: ["number-2", "sticky-wand", "fireball"] }, beats: [11, 14], boardBeat: 11, moves: [ ...STANDOFF, { seat: "Rival", cmd: { type: "cast", instanceId: rigId("sticky-wand"), target: { kind: "player", playerId: "Wanderer" } } }, { seat: "Wanderer", cmd: { type: "pass" } }, { seat: "Rival", cmd: { type: "endTurn", draw: 0 } }, { seat: "Wanderer", cmd: { type: "endTurn", draw: 0 } }, // three fewer steps, nowhere to run { seat: "Rival", cmd: { type: "cast", instanceId: rigId("fireball"), target: { kind: "player", playerId: "Wanderer" } } }, { seat: "Wanderer", cmd: { type: "pass" } }, { seat: "Rival", cmd: { type: "endTurn", draw: 0 } }, ], }, { ...DUEL, name: "dust-and-light", title: "Dust and Light", blurb: "A dust cloud hides her from his spells — so he throws a warp behind her, steps through the light, and she flees blind into her own dust.", pov: "Rival", rig: { Rival: ["number-2", "dimensional-warp"], Wanderer: ["dust-cloud"] }, beats: [12, 14, 20], boardBeat: 14, moves: [ ...STANDOFF, // Round two. Rival lays a warp: one mouth under his own feet at the // north mouth, the other at Wanderer's back, and waits a turn. { seat: "Rival", cmd: { type: "cast", instanceId: rigId("dimensional-warp"), target: { kind: "cell", cell: { x: 2, y: 5 } }, params: { cell: { x: 2, y: 0 } } } }, { seat: "Rival", cmd: { type: "endTurn", draw: 0 } }, // Wanderer fills the corridor between them with dust: no sight, no spells. { seat: "Wanderer", cmd: { type: "cast", instanceId: rigId("dust-cloud"), target: { kind: "cell", cell: { x: 2, y: 3 } } } }, { seat: "Wanderer", cmd: { type: "endTurn", draw: 0 } }, // Round three. Through the light, onto her square, fists first. { seat: "Rival", cmd: { type: "warpStep" } }, { seat: "Rival", cmd: { type: "move", direction: "N" } }, { seat: "Rival", cmd: { type: "punch", targetId: "Wanderer" } }, { seat: "Wanderer", cmd: { type: "pass" } }, { seat: "Rival", cmd: { type: "endTurn", draw: 0 } }, // She bolts into her own cloud — and the die picks where she goes. { seat: "Wanderer", cmd: { type: "move", direction: "N" } }, { seat: "Wanderer", cmd: { type: "move", direction: "N" } }, { seat: "Wanderer", cmd: { type: "endTurn", draw: 0 } }, ], }, ]; export function screenplayByName(name: string): Screenplay | null { return SCREENPLAYS.find((sp) => sp.name === name) ?? null; }