Spaces:
Running
Running
| // Shared composition root for practice and embed modes: builds the UI | |
| // shell, injects the Blockly workspace, constructs the robot adapter, | |
| // and wires the interpreter Runner to the Run/Stop button. | |
| import * as Blockly from "blockly"; | |
| import type { RobotAdapter } from "../robot/adapter"; | |
| import { CommandLog } from "../util/log"; | |
| import { createWorkspace, loadProgram, setWorkspaceTheme } from "../blocks/workspace"; | |
| import type { EmotionSource, SoundSource } from "../interpreter/commands"; | |
| import { Runner } from "../interpreter/runner"; | |
| import { STARTER_PROGRAM } from "./starter"; | |
| import { buildAppShell, type AppShell } from "./ui"; | |
| export interface BootOptions { | |
| /** Create the robot once the puppet's host element exists. */ | |
| attachRobot(puppetHost: HTMLElement, log: CommandLog): RobotAdapter; | |
| theme: "light" | "dark"; | |
| emotions: EmotionSource; | |
| sounds: SoundSource; | |
| /** Skip localStorage restore (e2e hermetic runs). */ | |
| fresh?: boolean; | |
| } | |
| export interface App { | |
| shell: AppShell; | |
| workspace: Blockly.WorkspaceSvg; | |
| robot: RobotAdapter; | |
| runner: Runner; | |
| log: CommandLog; | |
| setTheme(theme: "light" | "dark"): void; | |
| loadProgram(state: object): void; | |
| /** Override the coin-flip RNG (deterministic tests). */ | |
| setRandom(fn: () => number): void; | |
| } | |
| export function bootApp(root: HTMLElement, opts: BootOptions): App { | |
| const shell = buildAppShell(root); | |
| const log = new CommandLog(); | |
| const robot = opts.attachRobot(shell.puppetHost, log); | |
| const workspace = createWorkspace(shell.workspaceHost, { | |
| theme: opts.theme, | |
| fresh: opts.fresh, | |
| starter: STARTER_PROGRAM, | |
| }); | |
| window.addEventListener("resize", () => Blockly.svgResize(workspace)); | |
| let random: () => number = Math.random; | |
| const runner = new Runner(workspace, robot, { | |
| emotions: opts.emotions, | |
| sounds: opts.sounds, | |
| random: () => random(), | |
| hooks: { | |
| onHighlight: (id) => workspace.highlightBlock(id), | |
| onRunStateChange: (running) => shell.setRunning(running), | |
| }, | |
| }); | |
| shell.onRunClick(() => { | |
| if (runner.running) void runner.stop(); | |
| else void runner.run(); | |
| }); | |
| // Warm the emotion cache in the background so the first "act happy" | |
| // doesn't stall on the network. | |
| (opts.emotions as { prefetchAll?: () => void }).prefetchAll?.(); | |
| return { | |
| shell, | |
| workspace, | |
| robot, | |
| runner, | |
| log, | |
| setTheme(theme: "light" | "dark"): void { | |
| document.documentElement.setAttribute("data-theme", theme); | |
| setWorkspaceTheme(workspace, theme); | |
| }, | |
| loadProgram(state: object): void { | |
| loadProgram(workspace, state); | |
| }, | |
| setRandom(fn: () => number): void { | |
| random = fn; | |
| }, | |
| }; | |
| } | |