From da4fe47dbbc5acef4e5e05a9490aa6bd731ce8a4 Mon Sep 17 00:00:00 2001 From: "prompt.ac/@jeffrey" Date: Thu, 13 Aug 2026 22:57:58 -0400 Subject: [PATCH] Watch the reel live instead of rendering one to look at it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The factory costs minutes per reel — source a match, re-sim it frame by frame, mux, verify sync. That is right for something being published and absurd for "is the celebration too slow?", which is the question every camera and HUD change actually asks. reel-watch opens the same page the factory opens, with the same reel capabilities and the same 9:16 frame, and lets it run: bots fighting match after match at real speed. The shell serves oskiewar.js off disk, so a reload is the whole edit loop. It withholds only offline-render, which hands the clock to a frame-stepper and is exactly what a live preview must not do. Aspect is not cosmetic here — the reel camera reads it through portraitPull(), so a 16:9 window would frame the fight differently than the thing being previewed. The window scales but the aspect does not. --- package.json | 1 + xbox/live/marketing/reel-watch.mjs | 97 ++++++++++++++++++++++++++++++ 2 files changed, 98 insertions(+) create mode 100644 xbox/live/marketing/reel-watch.mjs diff --git a/package.json b/package.json index 51da15e9c2..af1788160b 100644 --- a/package.json +++ b/package.json @@ -39,6 +39,7 @@ "instagram:aesthetic": "node toolchain/instagram/aesthetic-ig.mjs", "oskiewar:oven": "node toolchain/instagram/oskiewar-oven.mjs", "xbox:test:oskiewar:blackbox": "node xbox/live/tests/blackbox-rounds.mjs", + "xbox:watch:reel": "node xbox/live/marketing/reel-watch.mjs", "xbox:burn:oskiewar-social": "node xbox/live/render-social-preview.mjs", "xbox:check:oskiewar-social": "node xbox/live/render-social-preview.mjs --check", "oskiewar:mac:build": "sh xbox/tools/build-macos-app.sh", diff --git a/xbox/live/marketing/reel-watch.mjs b/xbox/live/marketing/reel-watch.mjs new file mode 100644 index 0000000000..7ed92df290 --- /dev/null +++ b/xbox/live/marketing/reel-watch.mjs @@ -0,0 +1,97 @@ +#!/usr/bin/env node +// reel-watch.mjs — watch the game play itself in REEL dress, live. +// +// The factory takes minutes per reel: source a match, re-sim it frame by +// frame, mux audio, verify sync. That is the right cost for something being +// published and the wrong cost for "is the celebration too slow?". This opens +// the same page the factory does, with the same reel capabilities and the same +// 9:16 frame, and just lets it run — bots fighting match after match at real +// speed, so a camera or HUD change can be judged in seconds. +// +// What it is NOT: a renderer. Nothing is captured, no audio is verified, and +// the matches are live rather than the deterministic re-sim the factory ships. +// Judge FEEL here; judge FOOTAGE with `reel.mjs`. +// +// node xbox/live/marketing/reel-watch.mjs # 9:16, reel dress +// node xbox/live/marketing/reel-watch.mjs --hud # keep the match HUD +// node xbox/live/marketing/reel-watch.mjs --landscape # 16:9 instead +// node xbox/live/marketing/reel-watch.mjs --scale .4 # smaller window +// +// Edit oskiewar.js and reload the window — the shell serves it off disk, so +// there is no build step between a change and seeing it. + +import { existsSync } from "node:fs"; +import { serveShell } from "./shell.mjs"; + +const argv = process.argv.slice(2); +const flags = {}; +for (let i = 0; i < argv.length; i++) { + if (!argv[i].startsWith("--")) continue; + const next = argv[i + 1]; + if (next !== undefined && !next.startsWith("--")) { flags[argv[i].slice(2)] = next; i++; } + else flags[argv[i].slice(2)] = true; +} + +const chrome = [ + process.env.PUPPETEER_EXECUTABLE_PATH, + "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome", + "/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary", +].find((path) => path && existsSync(path)); +if (!chrome) throw new Error("no Chrome found — set PUPPETEER_EXECUTABLE_PATH"); + +async function loadPuppeteer() { + const dir = ["/opt/homebrew/lib/node_modules/puppeteer", + `${process.env.HOME}/aesthetic-computer/node_modules/puppeteer`, + "/opt/oven/node_modules/puppeteer"].find((path) => existsSync(path)); + if (!dir) throw new Error("puppeteer not found"); + return (await import(`${dir}/lib/esm/puppeteer/puppeteer.js`)).default; +} + +// A reel is 1080×1920; a display is not. Scale the whole frame down but keep +// the aspect exactly, because aspect is what the reel camera reads — the shot +// widths pull in on portrait via portraitPull(), so a 16:9 window would frame +// the fight differently than the thing being previewed. +const scale = Number(flags.scale) || (flags.landscape ? .5 : .42); +const [fullWidth, fullHeight] = flags.landscape ? [1920, 1080] : [1080, 1920]; +const width = Math.round(fullWidth * scale); +const height = Math.round(fullHeight * scale); + +const puppeteer = await loadPuppeteer(); +const shell = await serveShell({ replays: "stub", log: () => {} }); + +// `replay-oven` is what silences the player-facing furniture; `reel-hud` is +// what turns the reel's own dress back on. The factory also passes +// `offline-render`, which hands the clock to a frame-stepper — exactly what a +// live preview must not do. +const address = `${shell.origin}/?social-preview&replay-oven` + + (flags.hud === true ? "" : "&reel-hud"); + +const browser = await puppeteer.launch({ + headless: false, executablePath: chrome, defaultViewport: null, + args: ["--no-sandbox", "--autoplay-policy=no-user-gesture-required", + "--use-gl=angle", "--use-angle=metal", "--enable-gpu", "--ignore-gpu-blocklist", + "--disable-background-timer-throttling", "--disable-backgrounding-occluded-windows", + "--disable-renderer-backgrounding", + "--disable-features=CalculateNativeWinOcclusion", + `--window-size=${width},${height}`], +}); + +const [page] = await browser.pages(); +page.on("pageerror", (error) => console.log(`⚠ page ${error.message.slice(0, 160)}`)); +// Before a byte of the game runs. Self-play is deliberately unreachable from +// any button, so this is the only way in. +await page.evaluateOnNewDocument(() => { globalThis.__oskiewarSelfPlay = true; }); +await page.setViewport({ width, height, deviceScaleFactor: 1 }); +await page.goto(address, { waitUntil: "domcontentloaded" }); + +console.log(`▸ reel watch · ${width}×${height} (${fullWidth}×${fullHeight} at ${scale})`); +console.log(` ${address}`); +console.log(` bots fight on a loop. reload the window after editing oskiewar.js.`); +console.log(` close the window to stop.`); + +await new Promise((done) => { + browser.on("disconnected", done); + process.on("SIGINT", done); +}); +await shell.close?.(); +process.exit(0); -- 2.51.2