The Claude Code Arcade: pixel games above the prompt, played by your work (an invading octopus, a duck hunt, Bug Command, which you can play along with, Dario…

The Claude Code Arcade: pixel games in the line above the prompt, played by your work. Every tool call, commit, merge and failed test moves the game on. Part of claude-plugins.
Contents: Quick start · The games · Switching games · Game guides · Moments · Tuning the moments · Scores · What it reads and does · Install · Known gaps

Type these at the Claude Code prompt, then pick a scope (user is the usual one). There is nothing to set up.
/plugin marketplace add mutlumehmet/claude-plugins
/plugin install arcade@mehmetmutlu
Octo Invader starts by itself, and the first time a short notice says where the controls are. Then just work: the game plays along. Above the game sit small buttons:
| Button | What it does |
|---|---|
| ◀ ▶ | The previous or next game, in this terminal |
| ☰ | The game menu: every game with a line about it. A name plays it here, ☆ makes it your default |
| ☆ make default | Shows once this terminal plays a game other than your default; makes it the game every new terminal starts with |
| ⟳ | Checks for a new Arcade version and installs it (the same as /plugin update arcade@mehmetmutlu), then puts /reload-plugins in the prompt for you. When a newer version is out it lights up as a yellow ● ⟳ update 0.x.y |
Updates: Claude Code does not update plugins from this marketplace on its own unless you turn it on (/plugin, Marketplaces, mehmetmutlu, Enable auto-update). Without that, press ⟳ now and then, or type /arcade update. So you hear about it, each new terminal looks once for a newer version (it reads the version number in this repository's plugin.json on GitHub, nothing else). When one is out, ⟳ turns into a yellow ● ⟳ update 0.x.y and a notice says so, once per version. It never installs anything until you press it, and never checks again while the terminal is open. /arcade update check off stops the look, on brings it back.
<table> <tr><td> <b>Octo Invader</b> <code>/arcade octopus</code>: a pixel octopus smashes a city the full width of the line while Claude edits<br> <img src="../../docs/images/octo-invader.gif" width="800" alt="Octo Invader strip"> </td></tr> <tr><td> <b>Duck Hunt</b> <code>/arcade duck</code>: your moments shoot the ducks down, failed tools let them fly away<br> <img src="../../docs/images/duck-hunt.gif" width="800" alt="Duck Hunt strip"> </td></tr> <tr><td> <b>Bug Command</b> <code>/arcade bugs</code>: bugs fall on six cities; Claude's tools shoot them down, and you can click the sky to fire too<br> <img src="../../docs/images/bug-command.gif" width="800" alt="Bug Command strip"> </td></tr> <tr><td> <b>Dario</b> <code>/arcade dario</code>: a side scroller; tool calls bring ? blocks and coins, a failed tool sends a bug<br> <img src="../../docs/images/dario.gif" width="800" alt="Dario strip"> </td></tr> <tr><td> <b>Block Town</b> <code>/arcade town</code>: your agents build a medieval town in blocks; big moments raise a castle<br> <img src="../../docs/images/block-town.gif" width="800" alt="Block Town strip"> </td></tr> <tr><td> <img src="../../docs/images/arcade-hero.gif" width="400" align="right" alt="The five small games: dragon, slot machine, duel, Tamagotchi, Tetris"> Five small games at the right of the line:<br> <b>Dragon Lair</b> <code>/arcade dragon</code>: a pixel dragon that breathes fire when you ship<br> <b>Jackpot</b> <code>/arcade jackpot</code>: a slot machine; every finished turn pulls the lever<br> <b>Outlaw</b> <code>/arcade outlaw</code>: an Atari duel; you fire at the good moments, the bug when a tool fails<br> <b>Tama</b> <code>/arcade tama</code>: a Tamagotchi your work feeds, or it packs its bags<br> <b>Tetris</b> <code>/arcade tetris</code>: Claude's tools drop the pieces </td></tr> </table>
The buttons above do the everyday switching. Everything else is a word after /arcade, and each Claude Code config directory (an account) keeps its own choice.
| Command | What it does |
|---|---|
/arcade | Opens the game menu (in claude -p, lists the games instead) |
/arcade duck | Plays Duck Hunt in this terminal; other terminals and your default stay as they are |
/arcade default duck | Makes Duck Hunt the game every new terminal starts with, and plays it here |
/arcade next | The next game in this terminal |
/arcade random, rotate, all, off | How new terminals pick: one at random, the next one in turn, every game at once, or none. /arcade default <game> goes back to one fixed game |
/arcade pool dragon tetris | Picks only from these games (for random, rotate and all) |
/arcade hide | Clears this terminal; /arcade next brings a game back |
/arcade update | Checks for a new Arcade version and installs it, like ⟳ |
/arcade update check off | Stops the look for a new version when a terminal opens (on brings it back) |
/arcade help | The setting, which games this terminal shows, and these commands |
A game that is not shown keeps playing and keeps its score for this terminal; it only stops drawing and stays quiet. A game's own command (/dragon, /duck and the rest) shows its score and plays its practice moves.
What each game does with your work, its score line and its practice moves.
octopus)A pixel octopus invades a city that runs the full width of the line above the prompt. It acts out what Claude is doing, Godzilla style, and pulls planes out of the sky when something worth celebrating happens.

The strip on its own, as the mod draws it:

Both GIFs are drawn by the mod's own code from a scripted session (a read, an edit, a web search, a commit, a merge); the window around the strip is a mock up of a terminal.
? while Claude searches, and takes to the sky for web and MCP tools.CRASH!. Fallen buildings rise again after about 45 seconds, so the city never runs out.BOOM!), a big one a RAMPAGE! through the streets, and a merge, release, deploy, streak or record ends with a flag on the rubble and THE CITY IS MINE.Lv 3 ⌂ 12 ✈ 4 ⚒ 140 (level, buildings toppled, planes downed, tool calls)./octopus shows the score; /octopus ink, plane, rampage and conquer show off each size.duck)A marsh the full width of the line above the prompt, with the grass along the bottom, a tree, a dog and ducks. Your work does the shooting.

The strip on its own, as the mod draws it:

Both GIFs are drawn by the Arcade's own code from a scripted session (a read, a failing test, an edit, passing tests, a commit, a merge); the window around the strip is a mock up of a terminal.
DOUBLE!, the dog holds two), and a merge, release, deploy, streak or record is a PERFECT! round with feathers everywhere.FLY AWAY), and the dog comes up laughing. Two minutes of quiet and the dog lies down asleep.ROUND 3).R 2 ▼ 14 ↗ 3 ⚒ 140 (round, ducks down, ducks that got away, tool calls)./duck shows the score; /duck shot, hunt, double, perfect and flyaway are practice that counts nothing.bugs)Six cities and three silos along the bottom of the line above the prompt, the full width of it. Bugs fall on the cities; Claude's work fires the counter missiles, and you can fire too. The one Arcade game you can play along with while you wait.

The strip on its own, as the mod draws it:

Both GIFs are drawn by the Arcade's own code from a scripted session (a read, a failing test, an edit, passing tests, a commit), with the mouse clicks played into the sky's own pointer handler; the window around the strip is a mock up of a terminal.
INCOMING).BONUS CITY).THE END, and new cities go up.1, 2, 3 fire from the left, middle or right silo. A silo you fire from greys out for a moment. Esc hands the keyboard back to the prompt.✸ 14 ☞ 5 ✝ 2 ⚒ 140 ⌂ 6 (bugs shot down, the ones you shot yourself, cities lost, tool calls, cities standing)./bugs shows the score and the controls; /bugs flare, shot and salvo are practice that counts nothing, and /bugs incoming drops a practice bug to shoot at.dario)A side scrolling course the full width of the line above the prompt, with a ground of bricks, clouds, bushes and pipes. Dario runs while Claude works; you only watch.

The strip on its own, as the mod draws it:

Both GIFs are drawn by the Arcade's own code from a scripted session (a read, a search, a failing test, an edit, passing tests, a commit, a merge); the window around the strip is a mock up of a terminal.
OUCH).COURSE CLEAR! and the next course. A merge, release, deploy, streak or record is a WORLD CLEAR! with fireworks.1-3 ◎ 34 ✪ 5 ✗ 2 ⚒ 140 (world and course, coins, bugs stomped, knocks, tool calls). Every hundred coins is a 1UP./dario shows the score; /dario coin, ouch, stomp, clear and world are practice that counts nothing.town)A town in blocks, seen from the side, the full width of the line above the prompt: grass, dirt and stone, then houses, farms, a well, towers and a grove of trees. Your agents build it while they work, and it grows for as long as the terminal is open, so a glance in the middle of a long session shows how much got done.

The strip on its own, as the mod draws it:

Both GIFs are drawn by the Arcade's own code from a scripted session (edits and reads with a subagent helping, a failing test, edits, passing tests, a commit, a merge); the window around the strip is a mock up of a terminal.
BOOM); the villagers build it back before starting anything new.HOUSE BUILT), a big one raises a third of the castle at the right end of the band (THE CASTLE GROWS), and a merge, release, deploy, streak or record raises the rest at once (CASTLE BUILT!, fireworks).Village ▦ 309 ⌂ 2 ♣ 5 ♜ 1 ⚒ 27 (the town's size from camp, hamlet, village and town to city, blocks laid, houses, trees, castles, tool calls). Two minutes of quiet and the villagers go indoors./town shows the score; /town build, tree, finish, castle and creeper are practice that counts nothing. /town reset starts over: it asks first, and only /town reset yes within a minute clears this terminal's town and its score.dragon)A small one colour pixel dragon that lives at the right end of the line above the prompt. It acts out what Claude is doing and breathes fire when something worth celebrating happens.
Lv 3 ◆ 55 ★ 9 ⚒ 58 (level, gold, wins, tool calls)./dragon shows the hoard; /dragon puff, fire, blaze and roar show off each size.jackpot)A pixel slot machine at the right end of the line above the prompt. Every finished turn pulls the lever; the moments worth celebrating earn golden spins with better odds.
◉ 2967 ×1 ▲ 2 ✦ 0 ♛ 1 (chips, multiplier, clean streak, golden spins waiting, jackpots)./jackpot shows the rules and stats; /jackpot spin, golden and demo are practice spins that pay nothing.outlaw)A one colour Atari Outlaw duel at the right end of the line above the prompt. Your gunslinger fires at the moments worth celebrating, the bug fires back when a tool fails.
YOU 7 : 3 BUGS ▲ 4 ★ 6 (score, your run of hits, your best run)./outlaw shows the score and rules; /outlaw draw is a practice duel.tama)A classic Tamagotchi in a small LCD at the right end of the line above the prompt. It lives in real time while the terminal is open, and your work is what feeds it. A new terminal starts with a new egg.
♨ ▮▮▮▯ ♥ ▮▮▯▯ ✧ ▮▮▮▮ (food, joy, clean)./tama says how it is doing; /tama feed, play and clean are hand care, a few times a day.tetris)A classic falling-piece Tetris in a small handheld screen at the right end of the line above the prompt. Claude's tools drop the pieces; nobody knows what comes next.
▤ 35 ◆ 4250 Lv 3 (rows, score, level)./tetris shows the score and rules; /tetris drop adds pieces, /tetris clear clears a row.The Arcade spots the moments once and hands each one to every game, coding or not.
| Size | Moments |
|---|---|
| Small | A finished turn, a file saved, every 10th tool call in a turn |
| Medium | A commit, a push, a pull request opened, tests passing, any skill run, a subagent finished, a plan approved, an MCP tool that sends, creates, posts or publishes, a long document written, praise in your message |
| Big | A merge, a release (gh release create, npm publish), a production deploy (vercel --prod, netlify deploy --prod, fly deploy and others), tests back to green after failing, a deliverable made (PDF, DOCX, XLSX, PPTX, images), a published page, a finished task list of three or more, a marathon turn (10 minutes, 30 tools, no errors), three subagents done in one turn, a 3, 7, 30 or 100 day streak, the 100th, 1,000th or 10,000th tool call |
A commit that says "nothing to commit" or a push that says "Everything up-to-date" counts for nothing. Tool calls inside subagents do not count; their finishing does.
All optional, and none of it is asked at install. /arcade moments shows what is set; /arcade moments <key> <value> sets one and /arcade moments <key> none clears it. Kept per Claude Code config directory.
| Key | What it is for | Example |
|---|---|---|
big_skills | Skills whose run is a big moment (every other skill is medium) | /arcade moments big_skills release-notes, publish-report |
quiet_skills | Skills that celebrate nothing | /arcade moments quiet_skills commit |
big_commands | A regular expression of shell commands whose success is big | /arcade moments big_commands make ship |
medium_commands | A regular expression of shell commands whose success is medium | /arcade moments medium_commands terraform apply |
praise_words | Extra words that count as praise, on top of the built-in list (thanks, great, perfect and a few in other languages) | /arcade moments praise_words nice one, cheers |
Upgrading from 0.9.0 or earlier: those versions kept the game choice and these keys as plugin settings (pluginConfigs in settings.json). The Arcade no longer declares them, so set them again with the commands above.
Every game starts from zero in each terminal: the score, Block Town's town and Tama's pet belong to that terminal and last until it closes. Two terminals play two separate games. Nothing is saved between sessions; a reload of the pl
hooks/arcade.jsx 5176 lines1// Built by scripts/build.sh from src/ with esbuild@0.25.10. Do not edit: edit src/ and rebuild.
2// src/arcade.tsx
3import { atom as atom12, read as read12, update as update11 } from "claude-code";
4
5// src/games/dragon-lair.tsx
6import { atom as atom2, read as read2, update } from "claude-code";
7
8// src/games/dragon-sprite.ts
9const DRAGON_WIDTH = 26;
10const PIXEL_ROWS = 10;
11const BASE = 2;
12const BODY = {
13 x: 7,
14 y: 4,
15 frames: {
16 still: ["..#########", "############", "..##########"],
17 breathe: ["..#########", ".###########", "..##########"]
18 }
19};
20const WING = {
21 x: 6,
22 y: -2,
23 frames: {
24 up: ["#.......", "##......", "######..", ".#.#.##.", "..#.#.##", "...#####"],
25 mid: ["........", "........", "#.......", ".######.", "..#.#.##", "...#####"],
26 down: ["........", "........", "........", "........", ".######.", "#.#.#.##"]
27 }
28};
29const HEAD = {
30 x: 16,
31 y: 0,
32 frames: {
33 look: ["...#.#....", "..######..", ".##.######", ".##...###.", "##........"],
34 blink: ["...#.#....", "..######..", ".#########", ".##...###.", "##........"],
35 up: ["...#.#....", "..##.###..", ".#########", ".##...###.", "##........"],
36 open: ["...#.#....", "..######..", ".##.######", ".##.......", "##....###."],
37 droop: ["..........", "...#.#....", "..######..", ".##.######", "##....###."],
38 doze: ["..........", "...#.#....", "..######..", ".#########", "##....###."]
39 }
40};
41const TAIL = {
42 x: 0,
43 y: 3,
44 frames: {
45 low: ["#........", "##.......", ".##......", "..#####.."],
46 high: ["##.......", "#........", "##.......", ".######.."],
47 flat: [".........", ".........", "###......", "..#####.."]
48 }
49};
50const LEGS = {
51 x: 8,
52 y: 7,
53 frames: {
54 stand: ["##.##..##.##"],
55 stepA: ["#..##...#..##"],
56 stepB: ["##..#...##..#"],
57 tuck: ["............"]
58 }
59};
60const MOUTH = { x: 26, y: BASE + 2 };
61const NOSTRIL = { x: 25, y: BASE + 2 };
62const EYE = { x: 19, y: BASE + 2 };
63function draw(ink, columns, pose2, dx = 0, dy = 0) {
64 const put5 = (part, frame10) => {
65 const rows = part.frames[frame10] ?? Object.values(part.frames)[0] ?? [];
66 rows.forEach((row, y) => {
67 for (let x = 0; x < row.length; x++) {
68 if (row[x] !== "#") continue;
69 const px = part.x + x + dx;
70 const py = BASE + part.y + y + dy;
71 if (px >= 0 && px < columns && py >= 0 && py < PIXEL_ROWS) ink[py * columns + px] = 1;
72 }
73 });
74 };
75 put5(TAIL, pose2.tail);
76 put5(BODY, pose2.body);
77 put5(LEGS, pose2.legs);
78 put5(WING, pose2.wing);
79 put5(HEAD, pose2.head);
80}
81const BABY = {
82 up: [".#....", "#.#...", ".###.#", "..####", "..#..."],
83 down: ["......", "....#.", ".####.", "#.###.", "..#..."]
84};
85function drawBaby(ink, columns, x, y, frame10) {
86 BABY[frame10].forEach((row, dy) => {
87 for (let dx = 0; dx < row.length; dx++) {
88 if (row[dx] !== "#") continue;
89 const px = Math.round(x) + dx;
90 const py = Math.round(y) + dy;
91 if (px >= 0 && px < columns && py >= 0 && py < PIXEL_ROWS) ink[py * columns + px] = 1;
92 }
93 });
94}
95
96// src/shown.ts
97import { atom, read } from "claude-code";
98const shown = atom({ plugin: "arcade", key: "shown" }, []);
99async function isShown($, id) {
100 return (await read($, shown)).includes(id);
101}
102
103// src/games/dragon-lair.tsx
104const ID = "dragon";
105const RASTER = "dragon";
106const ROWS = PIXEL_ROWS / 2;
107const COLUMNS = DRAGON_WIDTH + 9;
108const FPS_MS = 66;
109const INK = 16777216;
110const NONE = 16777216;
111const hoard = atom2({ plugin: "arcade", key: "dragonHoard" }, { gold: 0, meals: 0, feats: 0 });
112const mood = atom2({ plugin: "arcade", key: "dragonMood" }, "idle");
113const feat = atom2({ plugin: "arcade", key: "dragonFeat" }, "");
114async function notify($, text) {
115 if (!!await isShown($, ID)) $.ui.toast(text);
116}
117const SHOW_FRAMES = { puff: 20, breath: 32, blaze: 48, roar: 72 };
118function workOf(tool11) {
119 if (tool11 === "Read") return "read";
120 if (tool11 === "Grep" || tool11 === "Glob" || tool11 === "LSP") return "search";
121 if (tool11 === "Edit" || tool11 === "Write" || tool11 === "NotebookEdit") return "edit";
122 if (tool11 === "WebFetch" || tool11 === "WebSearch" || tool11.startsWith("mcp__")) return "web";
123 if (tool11 === "Agent" || tool11 === "Task") return "agent";
124 return "bash";
125}
126const SLOTS = [
127 { x: 28, y: 0 },
128 { x: 28, y: 5 },
129 { x: 0, y: 0 }
130];
131const HOME = { x: 18, y: 0 };
132const sim = {
133 t: 0,
134 lastActivity: 0,
135 isTurn: false,
136 working: [],
137 show: null,
138 sadUntil: -1,
139 alertUntil: -1,
140 levelUntil: -1,
141 level: 1,
142 particles: [],
143 dx: 0,
144 dy: 0,
145 requestId: null,
146 isBlitting: false,
147 mood: "idle",
148 babies: []
149};
150const rand = (a, b) => a + Math.random() * (b - a);
151const pick = (list2) => list2[Math.floor(Math.random() * list2.length)];
152const levelOf = (gold) => Math.floor(Math.sqrt(gold / 10)) + 1;
153const every = (n) => sim.t % n === 0;
154const cycle = (frames, n) => frames[Math.floor(sim.t / n) % frames.length];
155function activity() {
156 const { t } = sim;
157 if (sim.show && t < sim.show.until) return sim.show.kind;
158 if (t < sim.sadUntil) return "sad";
159 if (t < sim.alertUntil) return "alert";
160 const last2 = sim.working[sim.working.length - 1];
161 if (last2) return last2.work;
162 if (sim.isTurn) return "think";
163 if (t - sim.lastActivity > 120 * 1e3 / FPS_MS) return "sleep";
164 return "idle";
165}
166function moodOf(a) {
167 if (a === "puff" || a === "breath" || a === "blaze" || a === "roar") return "fire";
168 if (a === "sad" || a === "sleep" || a === "idle") return a;
169 return "work";
170}
171const statsLine = (stash) => `Lv ${levelOf(stash.gold)} \u25C6 ${stash.gold} \u2605 ${stash.feats} \u2692 ${stash.meals}`;
172const centred = (text) => " ".repeat(Math.max(0, Math.floor((DRAGON_WIDTH - text.length) / 2))) + text;
173const showLeft = () => sim.show ? sim.show.until - sim.t : 0;
174function pose(a) {
175 const isBlink = sim.t % 55 < 2;
176 const fast = ["up", "mid", "down", "mid"];
177 switch (a) {
178 case "sleep":
179 return { body: cycle(["still", "breathe"], 20), wing: "down", head: "doze", tail: "flat", legs: "stand" };
180 case "sad":
181 return { body: "still", wing: "down", head: "droop", tail: "flat", legs: "stand" };
182 case "alert":
183 return { body: "still", wing: sim.alertUntil - sim.t > 8 ? "up" : "mid", head: "look", tail: "high", legs: lift(a) < 0 ? "tuck" : "stand" };
184 case "think":
185 return { body: cycle(["still", "breathe"], 12), wing: "down", head: isBlink ? "blink" : "up", tail: cycle(["low", "high"], 12), legs: "stand" };
186 case "read":
187 return { body: "still", wing: "down", head: isBlink ? "doze" : "droop", tail: "low", legs: "stand" };
188 case "search":
189 return { body: "still", wing: "mid", head: cycle(["look", "up"], 5), tail: cycle(["low", "high"], 5), legs: "stand" };
190 case "edit":
191 return { body: "still", wing: "down", head: "look", tail: cycle(["low", "high"], 3), legs: cycle(["stepA", "stand", "stepB", "stand"], 2) };
192 case "bash":
193 return { body: "still", wing: cycle(["up", "down"], 2), head: cycle(["look", "blink"], 6), tail: cycle(["low", "high"], 2), legs: "stand" };
194 case "web":
195 return { body: "still", wing: cycle(fast, 2), head: "look", tail: "flat", legs: "tuck" };
196 case "agent":
197 return { body: "still", wing: "mid", head: "droop", tail: "low", legs: "stand" };
198 case "idle": {
199 const isStretch = sim.t % 120 < 14;
200 return {
201 body: cycle(["still", "breathe"], 25),
202 wing: isStretch ? cycle(["mid", "up"], 7) : "down",
203 head: isBlink ? "blink" : "look",
204 tail: cycle(["low", "high"], 18),
205 legs: "stand"
206 };
207 }
208 default:
209 return {
210 body: "still",
211 wing: cycle(a === "puff" ? ["up", "mid"] : fast, a === "roar" ? 2 : 3),
212 head: a !== "puff" || showLeft() > 12 ? "open" : "look",
213 tail: "high",
214 legs: a === "roar" ? cycle(["stepA", "stepB"], 3) : "stand"
215 };
216 }
217}
218function lift(a) {
219 if (a === "alert") {
220 const left = sim.alertUntil - sim.t;
221 return left > 12 ? -1 : left > 6 ? -2 : left > 4 ? -1 : 0;
222 }
223 if (a === "web") return cycle([-2, -2, -1, -1], 2);
224 if (a === "roar") return cycle([0, -1], 3);
225 return 0;
226}
227function shift(a) {
228 if (a === "edit") return cycle([0, 1, 2, 1], 3);
229 if (a === "search") return cycle([0, 1], 6);
230 return 0;
231}
232const glyph = (x, y, ch, life, vx = 0, vy = 0) => sim.particles.push({ x, y, vx, vy, age: 0, life, ch });
233const pixel = (x, y, vx, vy, life, wave = 0) => sim.particles.push({ x, y, vx, vy, age: 0, life, wave });
234function flame(count, reach) {
235 for (let i = 0; i < count; i++) {
236 pixel(MOUTH.x + sim.dx, MOUTH.y + sim.dy + rand(0, 1.4), rand(0.8, 1.6), rand(-0.15, 0.25), Math.floor(rand(reach * 0.5, reach)), rand(0, 6));
237 }
238}
239function step(a) {
240 stepBabies();
241 sim.dx = shift(a);
242 sim.dy = lift(a);
243 const top = DRAGON_WIDTH + 1;
244 switch (a) {
245 case "puff":
246 if (showLeft() > 12) flame(1, 3);
247 if (every(6)) pixel(NOSTRIL.x, NOSTRIL.y - 1, rand(0.1, 0.3), -0.2, 8);
248 if (showLeft() === SHOW_FRAMES.puff - 1) glyph(top, 0, "\u2713", 18);
249 break;
250 case "breath":
251 flame(2, 7);
252 break;
253 case "blaze":
254 flame(3, 8);
255 if (every(3)) glyph(rand(top, COLUMNS), rand(0, PIXEL_ROWS), pick(["*", "+", "\u2726"]), 5);
256 break;
257 case "roar":
258 flame(4, 8);
259 if (every(2)) glyph(rand(0, COLUMNS), rand(0, PIXEL_ROWS), pick(["*", "+", "\u2726", "."]), 6);
260 if (every(8)) "ROAR".split("").forEach((ch, i) => glyph(top + i, 0, ch, 5));
261 break;
262 case "sad":
263 if (every(10)) pixel(EYE.x, EYE.y + 2, 0, 0.25, 8);
264 break;
265 case "think":
266 if (every(8)) {
267 const k = Math.floor(sim.t / 8) % 3;
268 glyph(14 + k * 2, 0, [".", "o", "O"][k], 8);
269 }
270 break;
271 case "read":
272 if (every(3)) glyph(top - 1, 8, cycle(["=", "-", "="], 3), 3);
273 break;
274 case "search":
275 if (every(12)) glyph(23 + sim.dx, 0, "?", 12);
276 break;
277 case "edit":
278 if (every(2)) pixel(rand(8, 20) + sim.dx, PIXEL_ROWS - 1, rand(-0.5, 0.5), -0.25, 3);
279 break;
280 case "bash":
281 if (every(9)) pixel(NOSTRIL.x, NOSTRIL.y - 1, 0.25, -0.2, 6);
282 break;
283 case "web":
284 if (every(2)) glyph(rand(0, 6), rand(0, PIXEL_ROWS), "~", 6, -0.5, 0);
285 break;
286 case "sleep":
287 if (every(24)) glyph(top - 3, 1, pick(["z", "Z"]), 30, 0.12, -0.04);
288 break;
289 case "idle":
290 if (every(140)) pixel(NOSTRIL.x + 1, NOSTRIL.y - 1, 0.2, -0.15, 7);
291 break;
292 }
293 if (sim.t < sim.levelUntil && every(6)) {
294 `LV${sim.level}`.split("").forEach((ch, i) => glyph(top + 3 + i, 1, ch, 6));
295 glyph(rand(top, COLUMNS), rand(0, PIXEL_ROWS), "\u2726", 4);
296 }
297 for (const p of sim.particles) {
298 p.x += p.vx;
299 p.y += p.vy + (p.wave ? Math.sin((sim.t + p.wave) / 1.5) * 0.25 : 0);
300 p.age += 1;
301 }
302 sim.particles = sim.particles.filter((p) => p.age < p.life && p.x >= 0 && p.x < COLUMNS && p.y >= 0 && p.y < PIXEL_ROWS);
303}
304function freeSlot() {
305 for (let i = 0; i < SLOTS.length; i++) {
306 if (!sim.babies.some((b) => b.slot === i && b.state !== "home" && b.state !== "fall")) return i;
307 }
308 return -1;
309}
310function hatch(key, description, type, id, isNew = true) {
311 if (sim.babies.some((b) => b.key === key || id !== void 0 && b.id === id)) return;
312 sim.babies.push({ key, id, description, type, state: isNew ? "egg" : "fly", since: sim.t, slot: freeSlot(), lastTool: -100, y: 0 });
313}
314function stepBabies() {
315 for (const b of sim.babies) {
316 const age2 = sim.t - b.since;
317 if (b.state === "egg" && age2 >= 20) {
318 b.state = "fly";
319 b.since = sim.t;
320 glyph(slotOf(b).x + 2, slotOf(b).y + 2, "\u2726", 4);
321 }
322 if (b.state === "fall") b.y += 0.6;
323 }
324 sim.babies = sim.babies.filter((b) => {
325 if (b.state === "home" && sim.t - b.since >= 14) {
326 glyph(HOME.x, HOME.y, "\u2726", 6);
327 return false;
328 }
329 return !(b.state === "fall" && b.y > PIXEL_ROWS);
330 });
331 for (const b of sim.babies) if (b.slot < 0 && b.state === "fly") b.slot = freeSlot();
332}
333const slotOf = (b) => SLOTS[b.slot] ?? SLOTS[0];
334function drawBabies(ink, W5) {
335 let hidden = 0;
336 for (const b of sim.babies) {
337 if (b.slot < 0) {
338 hidden += 1;
339 continue;
340 }
341 const at = slotOf(b);
342 const age2 = sim.t - b.since;
343 if (b.state === "egg") {
344 const rock = age2 > 8 ? cycle([0, 1, 0, -1], 2) : 0;
345 const ex = at.x + 2 + rock;
346 const ey = at.y + 1;
347 [".#.", "###", "###"].forEach(
348 (row, dy) => [...row].forEach((c, dx) => {
349 if (c !== "#" || age2 > 15 && dx === 1 && dy === 1) return;
350 const x = ex + dx;
351 const y = ey + dy;
352 if (x >= 0 && x < W5 && y >= 0 && y < PIXEL_ROWS) ink[y * W5 + x] = 1;
353 })
354 );
355 continue;
356 }
357 const isBusy = sim.t - b.lastTool < 15;
358 const frame10 = cycle(["up", "down"], isBusy ? 2 : 4);
359 if (b.state === "home") {
360 const k = Math.min(1, age2 / 14);
361 drawBaby(ink, W5, at.x + (HOME.x - at.x) * k, at.y + (HOME.y - at.y) * k, frame10);
362 continue;
363 }
364 if (b.state === "fall") {
365 drawBaby(ink, W5, at.x, at.y + b.y, "down");
366 continue;
367 }
368 const bob = cycle(at.y === 0 ? [0, 0, 1, 1] : [0, 0, -1, -1], isBusy ? 2 : 5);
369 drawBaby(ink, W5, at.x, at.y + bob, frame10);
370 }
371 if (hidden > 0) glyph(W5 - 2, 8, `+${Math.min(9, hidden)}`.slice(-1), 2);
372}
373function frame(a) {
374 const W5 = COLUMNS;
375 const ink = new Uint8Array(W5 * PIXEL_ROWS);
376 const over = /* @__PURE__ */ new Map();
377 draw(ink, W5, pose(a), shift(a), lift(a));
378 drawBabies(ink, W5);
379 for (const p of sim.particles) {
380 const x = Math.round(p.x);
381 const y = Math.round(p.y);
382 if (x < 0 || x >= W5 || y < 0 || y >= PIXEL_ROWS) continue;
383 if (p.ch) {
384 over.set(Math.floor(y / 2) * W5 + x, p.ch);
385 continue;
386 }
387 if (p.age / p.life > 0.7 && (x + sim.t) % 2 === 0) continue;
388 ink[y * W5 + x] = 1;
389 }
390 const words = new Uint32Array(W5 * ROWS * 3);
391 for (let cy = 0; cy < ROWS; cy++) {
392 for (let cx = 0; cx < W5; cx++) {
393 const i = (cy * W5 + cx) * 3;
394 const top = ink[cy * 2 * W5 + cx] === 1;
395 const bottom = ink[(cy * 2 + 1) * W5 + cx] === 1;
396 const ch = over.get(cy * W5 + cx);
397 words[i] = ch ? ch.codePointAt(0) : top && bottom ? 9608 : top ? 9600 : bottom ? 9604 : 32;
398 words[i + 1] = INK;
399 words[i + 2] = NONE;
400 }
401 }
402 return base64(new Uint8Array(words.buffer));
403}
404const B64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
405function base64(bytes) {
406 let out = "";
407 let i = 0;
408 for (; i + 2 < bytes.length; i += 3) {
409 const n = bytes[i] << 16 | bytes[i + 1] << 8 | bytes[i + 2];
410 out += B64[n >> 18 & 63] + B64[n >> 12 & 63] + B64[n >> 6 & 63] + B64[n & 63];
411 }
412 const rest = bytes.length - i;
413 if (rest === 1) {
414 const n = bytes[i] << 16;
415 out += B64[n >> 18 & 63] + B64[n >> 12 & 63] + "==";
416 } else if (rest === 2) {
417 const n = bytes[i] << 16 | bytes[i + 1] << 8;
418 out += B64[n >> 18 & 63] + B64[n >> 12 & 63] + B64[n >> 6 & 63] + "=";
419 }
420 return out;
421}
422async function celebrate($, label, gold, show, isQuiet = false) {
423 sim.show = { kind: show, until: sim.t + SHOW_FRAMES[show] };
424 sim.lastActivity = sim.t;
425 const before = levelOf((await read2($, hoard)).gold);
426 const next = await update($, hoard, (old) => ({ ...old, gold: old.gold + gold, feats: old.feats + 1 }));
427 const after = levelOf(next.gold);
428 await update($, feat, () => `${label}: +${gold} gold`);
429 if (after > before) {
430 sim.level = after;
431 sim.levelUntil = sim.show.until + 45;
432 void notify($, `\u{1F525} ${label}! +${gold} gold. The dragon grows to level ${after}!`);
433 } else if (!isQuiet) {
434 void notify($, `\u{1F525} ${label}! The dragon hoards +${gold} gold`);
435 }
436}
437const ROARS = /* @__PURE__ */ new Set(["merge", "release", "deploy", "streak", "record", "squad"]);
438async function onMilestone($, tier, kind, label) {
439 if (tier === "small") await celebrate($, label, 1, "puff", true);
440 else if (tier === "medium") await celebrate($, label, 5, "breath");
441 else if (ROARS.has(kind)) await celebrate($, label, 50, "roar");
442 else await celebrate($, label, 25, "blaze");
443}
444async function celebrateMoments($, found) {
445 for (const m of found) await onMilestone($, m.tier, m.kind, m.label);
446}
447const start = async ($, e, next) => {
448 await $.command.register({
449 name: "dragon",
450 description: 'The dragon above the prompt: its hoard. "/dragon puff|fire|blaze|roar" to show off.'
451 });
452 $.clock.every(FPS_MS, () => {
453 sim.t += 1;
454 const a = activity();
455 const m = moodOf(a);
456 if (m !== sim.mood) {
457 sim.mood = m;
458 void update($, mood, () => m);
459 }
460 const requestId = sim.requestId;
461 if (requestId === null) return;
462 step(a);
463 if (sim.isBlitting) return;
464 sim.isBlitting = true;
465 void $.ui.blit({ requestId, key: RASTER, cells: frame(a) }).then((r) => {
466 if (r.deny !== void 0) sim.requestId = null;
467 }).finally(() => {
468 sim.isBlitting = false;
469 });
470 });
471 $.clock.every(1e3, () => {
472 void $.agent.list().then(
473 async (agents) => {
474 for (const b of sim.babies) {
475 if (b.id === void 0) {
476 const match = agents.find(
477 (a) => a.description === b.description && a.type === b.type && !sim.babies.some((o) => o.id === a.id)
478 );
479 if (match) b.id = match.id;
480 }
481 }
482 for (const a of agents) {
483 const isLive = a.status === "pending" || a.status === "running" || a.status === "waiting";
484 if (isLive && !sim.babies.some((b) => b.id === a.id)) hatch(`agent:${a.id}`, a.description, a.type, a.id);
485 }
486 for (const b of sim.babies) {
487 if (b.state !== "egg" && b.state !== "fly") continue;
488 const a = agents.find((x) => x.id === b.id);
489 const isGone = a === void 0 && sim.t - b.since > 30 * 1e3 / FPS_MS;
490 if (a?.status === "completed" || a?.status === "idle" || isGone) {
491 b.state = "home";
492 b.since = sim.t;
493 const fed = await update($, hoard, (old) => ({ ...old, gold: old.gold + 2 }));
494 } else if (a?.status === "failed" || a?.status === "killed") {
495 b.state = "fall";
496 b.since = sim.t;
497 b.y = 0;
498 sim.sadUntil = sim.t + 15;
499 }
500 }
501 },
502 () => void 0
503 );
504 });
505 return next(e);
506};
507const command = async ($, e) => {
508 const arg = (e.args ?? "").trim();
509 const practice = { puff: "puff", fire: "breath", blaze: "blaze", roar: "roar" };
510 const show = practice[arg];
511 if (show) {
512 await celebrate($, "Practice", 1, show);
513 return { text: "The dragon breathes fire." };
514 }
515 const last2 = await read2($, feat);
516 return { text: `${statsLine(await read2($, hoard))}${last2 ? `
517Last win: ${last2}` : ""}` };
518};
519const prompt = async ($, e, next) => {
520 sim.isTurn = true;
521 sim.alertUntil = sim.t + 15;
522 sim.lastActivity = sim.t;
523 const ran = await next(e);
524 return ran;
525};
526const turn = async ($, e, next) => {
527 sim.isTurn = false;
528 sim.lastActivity = sim.t;
529 const ran = await next(e);
530 return ran;
531};
532const tool = async ($, e, next) => {
533 if (e.agentId !== void 0) {
534 const baby = sim.babies.find((b) => b.id === e.agentId);
535 if (baby) baby.lastTool = sim.t;
536 return next(e);
537 }
538 const job = { id: e.tool_use_id, work: workOf(String(e.tool)) };
539 sim.working.push(job);
540 sim.lastActivity = sim.t;
541 const ran = await next(e).finally(() => {
542 sim.working = sim.working.filter((w) => w !== job);
543 sim.lastActivity = sim.t;
544 });
545 if (ran.deny !== void 0) return ran;
546 const snack = await update($, hoard, (old) => ({ ...old, meals: old.meals + 1 }));
547 if (ran.isError === true) {
548 sim.sadUntil = sim.t + 30;
549 return ran;
550 }
551 return ran;
552};
553const render = async ($, e, next) => {
554 const below = await next(e);
555 if (e.surface !== "terminal" || e.props.hasSurvey || !await isShown($, ID)) {
556 sim.requestId = null;
557 return below;
558 }
559 const { Box, Raster, Text } = $.ui.resolve(e);
560 sim.requestId = e.requestId;
561 const stash = await read2($, hoard);
562 sim.level = levelOf(stash.gold);
563 return <Box flexDirection="row" alignItems="flex-end">
564 <Box flexGrow={1} flexDirection="column">
565 {below ?? null}
566 </Box>
567 <Box flexDirection="column" flexShrink={0} minWidth={COLUMNS}>
568 <Raster key={RASTER} columns={COLUMNS} rows={ROWS} cells={frame(activity())} />
569 <Text key="stats" dimColor wrap="truncate">
570 {centred(statsLine(stash))}
571 </Text>
572 </Box>
573 </Box>;
574};
575async function reset($) {
576 await update($, hoard, () => hoard.initial);
577 await update($, feat, () => "");
578 sim.level = 1;
579}
580const game = { id: ID, title: "Dragon Lair" };
581
582// src/games/duck-hunt.tsx
583import { read as read3, update as update2, atom as atom3 } from "claude-code";
584const ID2 = "duck";
585const RASTER2 = "marsh";
586const ROWS2 = 8;
587const PH = ROWS2 * 2;
588const FPS_MS2 = 66;
589const MIN_COLUMNS = 24;
590const MAX_COLUMNS = 512;
591const SLEEP_AFTER = 2 * 60 * 1e3 / FPS_MS2;
592const INK2 = 16777216;
593const NONE2 = 16777216;
594const EMPTY = 4294967295;
595const GRASS = 4170573;
596const GRASS_DARK = 2849335;
597const TREE = 2849335;
598const TRUNK = 9132587;
599const DOG = 13141834;
600const DOG_DARK = 8014372;
601const DUCK = 9132587;
602const DUCK_HEAD = 3050327;
603const BEAK = 16756736;
604const SHOT = 14692657;
605const GREY = 9079434;
606const FEATHER = [16777215, 14272416, 9132587];
607const GROUND = PH - 3;
608const SKY_BOTTOM = GROUND - 4;
609const score = atom3({ plugin: "arcade", key: "duckScore" }, { hits: 0, escaped: 0, tools: 0 });
610const feat2 = atom3({ plugin: "arcade", key: "duckFeat" }, "");
611async function notify2($, text) {
612 if (await isShown($, ID2)) $.ui.toast(text);
613}
614const DUCK_UP = [".##.....", "..##..##", ".#######", "..####.."];
615const DUCK_DOWN = ["......##", ".#######", "..####..", "..##...."];
616const DUCK_HEAD_PIXELS = [[6, 0], [7, 0], [6, 1]];
617const DUCK_FALL = ["..#..#..", "..####..", ".######.", "..#..#.."];
618const DUCK_HIT = [".#....#.", ".######.", "..####..", "..#..#.."];
619const DOG_WALK = [
620 ["......##.", ".....####", "#...####.", "#######..", ".######..", ".#.#..#.#"],
621 ["......##.", ".....####", "#...####.", "#######..", ".######..", "#..#.#..#"]
622];
623const DOG_UP = ["..#####..", ".##.#.##.", ".#######.", "..#...#..", "..#####..", "...###..."];
624const DOG_LAUGH = ["..#####..", ".##.#.##.", ".#######.", "..#.#.#..", "..#####..", "...###..."];
625const DOG_SLEEP = [".........", ".........", ".........", "....##...", "#.######.", "########."];
626const SHOW_FRAMES2 = { shot: 18, hunt: 110, double: 140, perfect: 170 };
627const sim2 = {
628 t: 0,
629 W: 0,
630 requestId: null,
631 isBlitting: false,
632 isTurn: false,
633 working: 0,
634 lastActivity: 0,
635 show: null,
636 ducks: [],
637 particles: [],
638 banner: null,
639 dogX: 4,
640 dogFace: 1,
641 // The dog behind the grass, holding up what fell: 0 nothing, 1 or 2 ducks, -1 laughing.
642 catch: null,
643 laughUntil: -1,
644 crosshair: null,
645 nextDuck: 60,
646 gain: { hits: 0, escaped: 0 },
647 stats: "",
648 tree: 0,
649 round: 1
650};
651const rand2 = (a, b) => a + Math.random() * (b - a);
652const pick2 = (list2) => list2[Math.floor(Math.random() * list2.length)];
653const clamp = (v, a, b) => Math.max(a, Math.min(b, v));
654const roundOf = (hits) => Math.floor(hits / 10) + 1;
655function fit(W5) {
656 if (W5 === sim2.W) return;
657 sim2.W = W5;
658 sim2.tree = Math.max(2, Math.floor(W5 * 0.72));
659 sim2.dogX = clamp(sim2.dogX, 0, W5 - 10);
660 sim2.ducks = sim2.ducks.filter((d) => d.x < W5);
661}
662function banner(text, color, frames) {
663 sim2.banner = { text, color, until: sim2.t + frames };
664}
665const glyph2 = (x, y, ch, life, color) => sim2.particles.push({ x, y, vx: 0, vy: 0, age: 0, life, color, ch });
666function feathers(x, y) {
667 for (let i = 0; i < 6; i++) {
668 sim2.particles.push({ x, y, vx: rand2(-0.6, 0.6), vy: rand2(-0.6, 0.1), age: 0, life: Math.floor(rand2(10, 22)), color: pick2(FEATHER), gravity: 0.04 });
669 }
670}
671function launch(isGame) {
672 const x = clamp(Math.floor(rand2(4, sim2.W - 12)), 0, Math.max(0, sim2.W - 9));
673 const duck = { x, y: SKY_BOTTOM, vx: pick2([-1, 1]) * rand2(0.5, 0.9), vy: -rand2(0.25, 0.45), state: "fly", since: sim2.t, isGame };
674 sim2.ducks.push(duck);
675 return duck;
676}
677function target() {
678 return sim2.ducks.find((d) => d.state === "fly") ?? launch(true);
679}
680function shoot(duck, counts = true) {
681 if (counts) duck.isGame = true;
682 duck.state = "hit";
683 duck.since = sim2.t;
684 sim2.crosshair = { x: Math.round(duck.x + 3), y: Math.round(duck.y + 1), until: sim2.t + 10 };
685 feathers(duck.x + 4, duck.y + 1);
686}
687function stepDucks() {
688 for (const d of sim2.ducks) {
689 const age2 = sim2.t - d.since;
690 if (d.state === "fly") {
691 d.x += d.vx;
692 d.y += d.vy;
693 if (d.x < 0 || d.x > sim2.W - 9) d.vx = -d.vx;
694 if (d.y < 0 || d.y > SKY_BOTTOM) d.vy = -d.vy;
695 if (Math.random() < 0.02) d.vy = -d.vy;
696 if (age2 > 260) d.state = "away";
697 } else if (d.state === "hit" && age2 > 8) {
698 d.state = "fall";
699 d.since = sim2.t;
700 } else if (d.state === "fall") {
701 d.y += 0.7;
702 if (d.y >= GROUND) {
703 d.y = PH;
704 const caught = sim2.catch && sim2.t - sim2.catch.since < 10 ? sim2.catch.ducks + 1 : 1;
705 sim2.catch = { ducks: Math.min(2, caught), x: clamp(Math.round(d.x), 0, sim2.W - 9), since: sim2.t };
706 if (d.isGame) sim2.gain.hits += 1;
707 }
708 } else if (d.state === "away") {
709 d.y -= 0.8;
710 d.x += d.vx;
711 }
712 }
713 sim2.ducks = sim2.ducks.filter((d) => d.y > -5 && d.y < PH && d.x > -9 && d.x < sim2.W + 1);
714}
715function flyAway(isPractice) {
716 const duck = sim2.ducks.find((d) => d.state === "fly");
717 if (duck) {
718 duck.state = "away";
719 duck.since = sim2.t;
720 if (!isPractice) sim2.gain.escaped += 1;
721 banner("FLY AWAY", GREY, 30);
722 }
723 sim2.laughUntil = sim2.t + 40;
724}
725function step2() {
726 sim2.t += 1;
727 const show = sim2.show && sim2.t < sim2.show.until ? sim2.show : null;
728 const age2 = show ? SHOW_FRAMES2[show.kind] - (show.until - sim2.t) : 0;
729 if (show) {
730 if (show.kind === "shot" && age2 === 1) {
731 const duck = sim2.ducks.find((d) => d.state === "fly");
732 if (duck) shoot(duck, false);
733 else {
734 const x = Math.floor(rand2(4, sim2.W - 8));
735 sim2.crosshair = { x, y: Math.floor(rand2(2, SKY_BOTTOM)), until: sim2.t + 6 };
736 glyph2(x, Math.floor(rand2(0, 4)), "*", 6, SHOT);
737 }
738 }
739 if (show.kind === "hunt" && age2 === 1) target();
740 if (show.kind === "hunt" && age2 === 25) shoot(target());
741 if ((show.kind === "double" || show.kind === "perfect") && age2 === 1) {
742 launch(true);
743 launch(true);
744 }
745 if ((show.kind === "double" || show.kind === "perfect") && (age2 === 25 || age2 === 35)) shoot(target());
746 if (show.kind === "double" && age2 === 40) banner("DOUBLE!", SHOT, 50);
747 if (show.kind === "perfect" && age2 === 40) banner("PERFECT!", BEAK, 70);
748 if (show.kind === "perfect" && age2 > 40 && age2 % 6 === 0) feathers(rand2(2, sim2.W - 2), rand2(0, 6));
749 }
750 const isAsleep = !sim2.isTurn && sim2.working === 0 && sim2.t - sim2.lastActivity > SLEEP_AFTER;
751 if (!isAsleep && !sim2.catch && sim2.laughUntil < sim2.t) {
752 const speed2 = sim2.working > 0 ? 0.5 : sim2.isTurn ? 0.3 : 0.1;
753 sim2.dogX += sim2.dogFace * speed2;
754 if (sim2.dogX < 0 || sim2.dogX > sim2.W - 10) sim2.dogFace = -sim2.dogFace;
755 }
756 if ((sim2.isTurn || sim2.working > 0) && !show && sim2.t >= sim2.nextDuck && !sim2.ducks.some((d) => d.state === "fly")) {
757 launch(false);
758 sim2.nextDuck = sim2.t + Math.floor(rand2(90, 200));
759 }
760 stepDucks();
761 if (sim2.catch && sim2.t - sim2.catch.since > 45) sim2.catch = null;
762 for (const p of sim2.particles) {
763 p.age += 1;
764 p.x += p.vx;
765 p.y += p.vy;
766 p.vy += p.gravity ?? 0;
767 }
768 sim2.particles = sim2.particles.filter((p) => p.age < p.life && p.x >= 0 && p.x < sim2.W && p.y >= 0 && p.y < PH);
769 return isAsleep;
770}
771function put(buf, x, y, color) {
772 x = Math.round(x);
773 y = Math.round(y);
774 if (x < 0 || x >= sim2.W || y < 0 || y >= PH) return;
775 buf[y * sim2.W + x] = color;
776}
777function mask(buf, rows, x, y, color, isFlipped = false) {
778 rows.forEach((row, dy) => {
779 const w = row.length;
780 for (let dx = 0; dx < w; dx++) {
781 if (row[isFlipped ? w - 1 - dx : dx] === "#") put(buf, x + dx, y + dy, color);
782 }
783 });
784}
785function drawDuck(buf, d) {
786 const isLeft = d.vx < 0;
787 if (d.state === "hit") return mask(buf, DUCK_HIT, d.x, d.y, DUCK, isLeft);
788 if (d.state === "fall") return mask(buf, DUCK_FALL, d.x, d.y, DUCK, isLeft);
789 const isUp = Math.floor(sim2.t / 4) % 2 === 0;
790 mask(buf, isUp ? DUCK_UP : DUCK_DOWN, d.x, d.y, DUCK, isLeft);
791 for (const [hx, hy] of DUCK_HEAD_PIXELS) put(buf, isLeft ? d.x + 7 - hx : d.x + hx, d.y + hy, DUCK_HEAD);
792 put(buf, isLeft ? d.x - 1 : d.x + 8, d.y + (isUp ? 1 : 0), BEAK);
793}
794function drawScene(buf) {
795 const tx = sim2.tree;
796 for (let y = GROUND - 7; y < GROUND; y++) {
797 const half = y < GROUND - 4 ? 3 - Math.abs(GROUND - 6 - y) : 0;
798 for (let dx = -half; dx <= half; dx++) put(buf, tx + dx, y, TREE);
799 }
800 for (let y = GROUND - 4; y < GROUND; y++) put(buf, tx, y, TRUNK);
801 for (let x = 0; x < sim2.W; x++) {
802 for (let y = GROUND; y < PH; y++) put(buf, x, y, GRASS);
803 if (x * 7 % 5 < 2) put(buf, x, GROUND - 1, GRASS_DARK);
804 }
805}
806function drawDog(buf, isAsleep) {
807 if (sim2.catch) {
808 const rise = Math.min(7, Math.floor((sim2.t - sim2.catch.since) / 2));
809 const y2 = PH - 1 - rise;
810 mask(buf, DOG_UP, sim2.catch.x, y2, DOG);
811 for (let i = 0; i < sim2.catch.ducks; i++) mask(buf, DUCK_FALL, sim2.catch.x + (i === 0 ? -3 : 5), y2 - 3, DUCK);
812 return;
813 }
814 if (sim2.laughUntil > sim2.t) {
815 const bob = Math.floor(sim2.t / 3) % 2;
816 mask(buf, DOG_LAUGH, sim2.dogX, PH - 8 + bob, DOG);
817 return;
818 }
819 const y = GROUND - 5;
820 if (isAsleep) {
821 mask(buf, DOG_SLEEP, sim2.dogX, y, DOG);
822 return;
823 }
824 const legs = DOG_WALK[Math.floor(sim2.t / 4) % 2];
825 mask(buf, legs, sim2.dogX, y, DOG, sim2.dogFace < 0);
826 put(buf, sim2.dogFace < 0 ? sim2.dogX + 2 : sim2.dogX + 6, y, DOG_DARK);
827}
828const statsLine2 = (s) => `R ${roundOf(s.hits)} \u25BC ${s.hits} \u2197 ${s.escaped} \u2692 ${s.tools}`;
829function frame2(isAsleep, stats) {
830 const W5 = sim2.W;
831 const buf = new Uint32Array(W5 * PH).fill(EMPTY);
832 const over = /* @__PURE__ */ new Map();
833 const text = (x, row, s, color) => [...s].forEach((ch, i) => {
834 if (x + i >= 0 && x + i < W5) over.set(row * W5 + x + i, { ch, color });
835 });
836 for (const d of sim2.ducks) drawDuck(buf, d);
837 const isBehind = sim2.catch !== null || sim2.laughUntil > sim2.t;
838 if (isBehind) drawDog(buf, isAsleep);
839 drawScene(buf);
840 if (!isBehind) drawDog(buf, isAsleep);
841 if (sim2.crosshair && sim2.t < sim2.crosshair.until) {
842 const { x, y } = sim2.crosshair;
843 for (const [dx, dy] of [[-2, 0], [2, 0], [0, -2], [0, 2], [-1, 0], [1, 0], [0, -1], [0, 1]]) put(buf, x + dx, y + dy, SHOT);
844 }
845 if (isAsleep) text(Math.round(sim2.dogX) + 9, Math.floor((GROUND - 4) / 2), "z", GREY);
846 for (const p of sim2.particles) {
847 if (p.ch) text(Math.round(p.x), Math.floor(Math.round(p.y) / 2), p.ch, p.color);
848 else put(buf, p.x, p.y, p.color);
849 }
850 text(W5 - stats.length - 1, 0, stats, GREY);
851 if (sim2.banner && sim2.t < sim2.banner.until && (sim2.banner.until - sim2.t) % 8 > 1) {
852 text(Math.floor((W5 - sim2.banner.text.length) / 2), 1, sim2.banner.text, sim2.banner.color);
853 }
854 const words = new Uint32Array(W5 * ROWS2 * 3);
855 for (let cy = 0; cy < ROWS2; cy++) {
856 for (let cx = 0; cx < W5; cx++) {
857 const i = (cy * W5 + cx) * 3;
858 const top = buf[cy * 2 * W5 + cx];
859 const bottom = buf[(cy * 2 + 1) * W5 + cx];
860 const g = over.get(cy * W5 + cx);
861 if (g) {
862 words[i] = g.ch.codePointAt(0);
863 words[i + 1] = g.color;
864 words[i + 2] = NONE2;
865 } else if (top === EMPTY && bottom === EMPTY) {
866 words[i] = 32;
867 words[i + 1] = INK2;
868 words[i + 2] = NONE2;
869 } else if (top === bottom) {
870 words[i] = 9608;
871 words[i + 1] = top;
872 words[i + 2] = NONE2;
873 } else if (bottom === EMPTY) {
874 words[i] = 9600;
875 words[i + 1] = top;
876 words[i + 2] = NONE2;
877 } else if (top === EMPTY) {
878 words[i] = 9604;
879 words[i + 1] = bottom;
880 words[i + 2] = NONE2;
881 } else {
882 words[i] = 9600;
883 words[i + 1] = top;
884 words[i + 2] = bottom;
885 }
886 }
887 }
888 return base642(new Uint8Array(words.buffer));
889}
890const B642 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
891function base642(bytes) {
892 let out = "";
893 let i = 0;
894 for (; i + 2 < bytes.length; i += 3) {
895 const n = bytes[i] << 16 | bytes[i + 1] << 8 | bytes[i + 2];
896 out += B642[n >> 18 & 63] + B642[n >> 12 & 63] + B642[n >> 6 & 63] + B642[n & 63];
897 }
898 const rest = bytes.length - i;
899 if (rest === 1) {
900 const n = bytes[i] << 16;
901 out += B642[n >> 18 & 63] + B642[n >> 12 & 63] + "==";
902 } else if (rest === 2) {
903 const n = bytes[i] << 16 | bytes[i + 1] << 8;
904 out += B642[n >> 18 & 63] + B642[n >> 12 & 63] + B642[n >> 6 & 63] + "=";
905 }
906 return out;
907}
908async function save($, change2) {
909 const next = await update2($, score, change2);
910 sim2.stats = statsLine2(next);
911 return next;
912}
913const RANK = { shot: 0, hunt: 1, double: 2, perfect: 3 };
914async function celebrate2($, label, show, isQuiet = false) {
915 const running = sim2.show && sim2.t < sim2.show.until ? sim2.show.kind : null;
916 if (running === null || RANK[show] >= RANK[running]) sim2.show = { kind: show, until: sim2.t + SHOW_FRAMES2[show] };
917 sim2.lastActivity = sim2.t;
918 const what = { shot: "a shot", hunt: "a duck down", double: "a double", perfect: "a perfect round" };
919 await update2($, feat2, () => `${label}: ${what[show]}`);
920 if (!isQuiet) {
921 const say2 = {
922 shot: "Bang",
923 hunt: "Got one",
924 double: "Two in one round",
925 perfect: "Perfect"
926 };
927 void notify2($, `\u{1F986} ${label}! ${say2[show]}!`);
928 }
929}
930const PERFECT = /* @__PURE__ */ new Set(["merge", "release", "deploy", "streak", "record", "squad"]);
931async function onMilestone2($, tier, kind, label) {
932 if (tier === "small") await celebrate2($, label, "shot", true);
933 else if (tier === "medium") await celebrate2($, label, "hunt");
934 else if (PERFECT.has(kind)) await celebrate2($, label, "perfect");
935 else await celebrate2($, label, "double");
936}
937async function celebrateMoments2($, found) {
938 for (const m of found) await onMilestone2($, m.tier, m.kind, m.label);
939}
940const start2 = async ($, e, next) => {
941 const s = await read3($, score);
942 sim2.stats = statsLine2(s);
943 sim2.round = roundOf(s.hits);
944 await $.command.register({
945 name: "duck",
946 description: 'The duck hunt above the prompt: the score. "/duck shot|hunt|double|perfect|flyaway" to show off.'
947 });
948 $.clock.every(FPS_MS2, () => {
949 const requestId = sim2.requestId;
950 if (sim2.gain.hits > 0 || sim2.gain.escaped > 0) {
951 const gain = sim2.gain;
952 sim2.gain = { hits: 0, escaped: 0 };
953 void save($, (old) => ({ ...old, hits: old.hits + gain.hits, escaped: old.escaped + gain.escaped })).then((s2) => {
954 const round = roundOf(s2.hits);
955 if (round > sim2.round) {
956 sim2.round = round;
957 banner(`ROUND ${round}`, BEAK, 60);
958 void notify2($, `\u{1F986} Round ${round}: ${s2.hits} ducks down.`);
959 }
960 });
961 }
962 if (requestId === null || sim2.W === 0) return;
963 const isAsleep = step2();
964 if (sim2.isBlitting) return;
965 sim2.isBlitting = true;
966 void $.ui.blit({ requestId, key: RASTER2, cells: frame2(isAsleep, sim2.stats), columns: sim2.W }).then((r) => {
967 if (r.deny !== void 0) sim2.requestId = null;
968 }).finally(() => {
969 sim2.isBlitting = false;
970 });
971 });
972 return next(e);
973};
974const command2 = async ($, e) => {
975 const arg = (e.args ?? "").trim();
976 const practice = { shot: "shot", hunt: "hunt", double: "double", perfect: "perfect" };
977 const show = practice[arg];
978 if (show) {
979 await celebrate2($, "Practice", show, true);
980 return { text: "Practice: nothing counts." };
981 }
982 if (arg === "flyaway") {
983 flyAway(true);
984 return { text: "The duck gets away, and the dog laughs." };
985 }
986 const last2 = await read3($, feat2);
987 return {
988 text: `${statsLine2(await read3($, score))}${last2 ? `
989Last win: ${last2}` : ""}
990A medium moment (a commit, a skill, a sent message) shoots a duck down, a big one (a merge, a deploy, a finished task list) shoots two; a failed tool lets one get away. R is the round (ten ducks each), \u25BC ducks down, \u2197 ducks that got away, \u2692 tool calls.`
991 };
992};
993const prompt2 = async ($, e, next) => {
994 sim2.isTurn = true;
995 sim2.lastActivity = sim2.t;
996 return next(e);
997};
998const turn2 = async ($, e, next) => {
999 if (e.agentId === void 0) sim2.isTurn = false;
1000 sim2.lastActivity = sim2.t;
1001 return next(e);
1002};
1003const tool2 = async ($, e, next) => {
1004 if (e.agentId !== void 0) return next(e);
1005 sim2.working += 1;
1006 sim2.lastActivity = sim2.t;
1007 const ran = await next(e).finally(() => {
1008 sim2.working = Math.max(0, sim2.working - 1);
1009 sim2.lastActivity = sim2.t;
1010 });
1011 if (ran.deny !== void 0) return ran;
1012 const counted = await update2($, score, (old) => ({ ...old, tools: old.tools + 1 }));
1013 sim2.stats = statsLine2(counted);
1014 if (ran.isError === true) flyAway(false);
1015 return ran;
1016};
1017const render2 = async ($, e, next) => {
1018 const below = await next(e);
1019 if (e.surface !== "terminal" || e.props.hasSurvey || !await isShown($, ID2)) {
1020 sim2.requestId = null;
1021 return below;
1022 }
1023 const { Box, Raster } = $.ui.resolve(e);
1024 fit(clamp(e.props.bodyColumns, MIN_COLUMNS, MAX_COLUMNS));
1025 sim2.requestId = e.requestId;
1026 sim2.stats = statsLine2(await read3($, score));
1027 return <Box flexDirection="column">
1028 <Raster key={RASTER2} columns={sim2.W} rows={ROWS2} cells={frame2(false, sim2.stats)} />
1029 {below ?? null}
1030 </Box>;
1031};
1032async function reset2($) {
1033 const next = await update2($, score, () => score.initial);
1034 sim2.stats = statsLine2(next);
1035 sim2.round = 1;
1036 await update2($, feat2, () => "");
1037}
1038const game2 = { id: ID2, title: "Duck Hunt" };
1039
1040// src/games/jackpot.tsx
1041import { atom as atom4, read as read4, update as update3 } from "claude-code";
1042
1043// src/games/jackpot-symbols.ts
1044const PALETTE = {
1045 R: 16726862,
1046 r: 11735594,
1047 Y: 16765773,
1048 y: 13211164,
1049 G: 5034109,
1050 g: 2328394,
1051 C: 6281471,
1052 c: 2789311,
1053 W: 16777215,
1054 P: 12614655,
1055 O: 16751165,
1056 K: 1381661
1057};
1058const SYMBOLS = {
1059 seven: ["RRRRRR", "RRRRRR", "...RR.", "..RR..", "..RR..", "..RR.."],
1060 dragon: ["G....G", "GG..GG", "GGGGGG", "G.GG.G", "GGGGGG", ".GGGG."],
1061 diamond: ["..CC..", ".CCCC.", "CCCCCC", "CCCCCC", ".CCCC.", "..CC.."],
1062 bell: ["..YY..", ".YYYY.", ".YYYY.", "YYYYYY", "......", "..YY.."],
1063 star: ["..PP..", "..PP..", "PPPPPP", ".PPPP.", ".P..P.", "P....P"],
1064 cherry: ["...GG.", "..G.G.", ".G..G.", "RR.RR.", "RR.RR.", "......"]
1065};
1066const SYMBOL_SIZE = 6;
1067
1068// src/games/jackpot.tsx
1069const ID3 = "jackpot";
1070const RASTER3 = "machine";
1071const FPS_MS3 = 66;
1072const W = 28;
1073const H = 12;
1074const ROWS3 = H / 2;
1075const TOP = 3;
1076const WINDOW = SYMBOL_SIZE + 1;
1077const REEL_W = 8;
1078const PERIOD = SYMBOL_SIZE + 2;
1079const CLEAR = -1;
1080const DEFAULT = 16777216;
1081const bank = atom4({ plugin: "arcade", key: "jackpotBank" }, { chips: 0, spins: 0, jackpots: 0, best: 0, streak: 0 });
1082const golden = atom4({ plugin: "arcade", key: "jackpotGolden" }, 0);
1083const last = atom4({ plugin: "arcade", key: "jackpotLast" }, "");
1084async function notify3($, text) {
1085 if (!!await isShown($, ID3)) $.ui.toast(text);
1086}
1087const ORDER = ["seven", "dragon", "diamond", "bell", "star", "cherry"];
1088const TRIPLE_PAY = { seven: 100, dragon: 50, diamond: 25, bell: 15, star: 10, cherry: 8 };
1089const STRIPS = [
1090 ["cherry", "star", "seven", "bell", "cherry", "diamond", "star", "dragon", "bell", "cherry", "star", "diamond"],
1091 ["star", "cherry", "bell", "dragon", "cherry", "star", "seven", "diamond", "cherry", "bell", "star", "diamond"],
1092 ["bell", "diamond", "cherry", "star", "seven", "cherry", "bell", "star", "dragon", "cherry", "diamond", "star"]
1093];
1094const sim3 = {
1095 t: 0,
1096 queue: [],
1097 spin: null,
1098 reels: [0, 0, 0].map((_, i) => ({ from: i * 2 * PERIOD, to: i * 2 * PERIOD, start: -100, frames: 1, landedAt: -100 })),
1099 result: ["seven", "seven", "seven"],
1100 win: "none",
1101 winUntil: -1,
1102 payout: 0,
1103 coins: [],
1104 hadError: false,
1105 requestId: null,
1106 isBlitting: false,
1107 isLooping: false
1108};
1109const rand3 = (a, b) => a + Math.random() * (b - a);
1110const pick3 = (list2) => list2[Math.floor(Math.random() * list2.length)];
1111const cycle2 = (frames, n) => frames[Math.floor(sim3.t / n) % frames.length];
1112const multiplier = (streak) => Math.min(5, 1 + Math.floor(streak / 5));
1113function outcome(isGolden, roll = Math.random()) {
1114 const odds = isGolden ? { jackpot: 1 / 20, dragon: 1 / 10, triple: 1 / 4, pair: 1 } : { jackpot: 1 / 200, dragon: 1 / 80, triple: 1 / 15, pair: 1 / 3 };
1115 let edge = odds.jackpot;
1116 if (roll < edge) return ["seven", "seven", "seven"];
1117 edge += odds.dragon;
1118 if (roll < edge) return ["dragon", "dragon", "dragon"];
1119 edge += odds.triple;
1120 if (roll < edge) {
1121 const s = pick3(["diamond", "bell", "star", "cherry"]);
1122 return [s, s, s];
1123 }
1124 edge += odds.pair;
1125 if (roll < edge) {
1126 const s = pick3(ORDER);
1127 const other2 = pick3(ORDER.filter((o) => o !== s));
1128 const at = Math.floor(Math.random() * 3);
1129 return [0, 1, 2].map((i) => i === at ? other2 : s);
1130 }
1131 if (Math.random() < 0.2) return ["seven", "seven", pick3(ORDER.filter((o) => o !== "seven"))];
1132 const [a, b, c] = [...ORDER].sort(() => Math.random() - 0.5);
1133 return [a, b, c];
1134}
1135function score2(symbols) {
1136 const [a, b, c] = symbols;
1137 if (a === b && b === c) return { win: a === "seven" ? "jackpot" : "triple", pay: TRIPLE_PAY[a] };
1138 if (a === b || b === c || a === c) {
1139 const pair = a === b || a === c ? a : b;
1140 return { win: "pair", pay: pair === "cherry" ? 3 : 2 };
1141 }
1142 return { win: "none", pay: 0 };
1143}
1144function startSpin(spin) {
1145 sim3.spin = spin;
1146 sim3.win = "none";
1147 sim3.result = spin.forced ?? outcome(spin.isGolden);
1148 sim3.reels = sim3.reels.map((reel, i) => {
1149 const strip = STRIPS[i];
1150 const len = strip.length * PERIOD;
1151 const now = (reel.to % len + len) % len;
1152 const index = strip.indexOf(sim3.result[i]);
1153 const target2 = (2 + i) * len + index * PERIOD;
1154 return { from: now, to: target2, start: sim3.t, frames: 14 + i * 7, landedAt: -100 };
1155 });
1156}
1157function reelPos(reel, i) {
1158 const k = Math.min(1, (sim3.t - reel.start) / reel.frames);
1159 if (k >= 1) {
1160 if (reel.landedAt < 0) reel.landedAt = sim3.t;
1161 const since = sim3.t - reel.landedAt;
1162 return reel.to + ([-2, -1, 1, 0][since] ?? 0);
1163 }
1164 void i;
1165 return reel.from + (reel.to - reel.from) * (1 - (1 - k) ** 3);
1166}
1167async function finishSpin($) {
1168 const spin = sim3.spin;
1169 sim3.spin = null;
1170 const { win, pay } = score2(sim3.result);
1171 const before = await read4($, bank);
1172 const mult = multiplier(before.streak) * (spin.isGolden ? 2 : 1);
1173 const payout = spin.isPractice ? 0 : pay * mult;
1174 sim3.win = win;
1175 sim3.payout = payout;
1176 sim3.winUntil = sim3.t + (win === "jackpot" ? 75 : win === "triple" ? 40 : win === "pair" ? 18 : 6);
1177 if (win === "triple" || win === "jackpot") {
1178 for (let i = 0; i < (win === "jackpot" ? 30 : 12); i++) {
1179 sim3.coins.push({ x: rand3(4, W - 4), y: TOP + 4, vx: rand3(-0.6, 0.6), vy: rand3(-1.4, -0.7), age: 0 });
1180 }
1181 }
1182 if (spin.isPractice) return;
1183 const next = await update3($, bank, (old) => ({
1184 ...old,
1185 chips: old.chips + payout,
1186 spins: old.spins + 1,
1187 jackpots: old.jackpots + (win === "jackpot" ? 1 : 0),
1188 best: Math.max(old.best, payout)
1189 }));
1190 const names = sim3.result.join(" ");
1191 await update3($, last, () => payout > 0 ? `${names}: +${payout}` : names);
1192 if (win === "jackpot") void notify3($, `\u{1F3B0} JACKPOT! 7 7 7 pays ${payout} chips`);
1193 else if (win === "triple") void notify3($, `\u{1F3B0} Three ${sim3.result[0]}s! +${payout} chips`);
1194}
1195function tick($) {
1196 sim3.t += 1;
1197 if (sim3.spin) {
1198 const isDone2 = sim3.reels.every((r) => sim3.t - r.start >= r.frames + 4);
1199 if (isDone2) void finishSpin($);
1200 } else if (sim3.t >= sim3.winUntil && sim3.queue.length > 0) {hooks/bug-sky.js 354 lines1// Built by scripts/build.sh from src/clients/ with esbuild@0.25.10. Do not edit: edit src/ and rebuild.
2// src/clients/bug-sky.tsx
3var ROWS = 8;
4var PH = ROWS * 2;
5var TICK_MS = 66;
6var GROUND = PH - 1;
7var CITY_TOP = PH - 4;
8var SKY_FLOOR = CITY_TOP - 1;
9var CITIES = 6;
10var SHOT_SPEED = 1.8;
11var COOLDOWN = 5;
12var SAND = "#c2a14d";
13var SILO = "#d9a441";
14var CITY = "#4dabf7";
15var RUBBLE = "#6b6b6b";
16var BUG_TRAIL = "#a8323e";
17var BUG_HEAD = "#ff6b6b";
18var SHOT_TRAIL = "#4c6ef5";
19var SHOT_HEAD = "#ffffff";
20var CROSS = "#69db7c";
21var GREY = "#8a8a8a";
22var GOLD = "#ffd43b";
23var BLAST = ["#ffffff", "#ffd43b", "#ff922b", "#f06595", "#cc5de8"];
24var rand = (a, b) => a + Math.random() * (b - a);
25var clamp = (v, a, b) => Math.max(a, Math.min(b, v));
26function layout(W) {
27 const silos = [2, Math.floor(W / 2), W - 3];
28 const xs = [];
29 for (const [a, b] of [[silos[0], silos[1]], [silos[1], silos[2]]]) {
30 for (let i = 1; i <= 3; i++) xs.push(Math.round(a + (b - a) * i / 4) - 2);
31 }
32 return { silos, cityX: xs };
33}
34function create(props) {
35 const W = Math.max(24, props.columns);
36 const standing = clamp(props.cities, 0, CITIES);
37 return {
38 t: 0,
39 W,
40 seen: props.events.reduce((m, e) => Math.max(m, e.id), 0),
41 cities: Array.from({ length: CITIES }, (_, i) => i < standing || standing === 0),
42 silos: layout(W).silos,
43 cooldown: [0, 0, 0],
44 bugs: [],
45 shots: [],
46 blasts: [],
47 plans: [],
48 banner: null,
49 cross: { x: Math.floor(W / 2), y: 4 },
50 hasFired: false,
51 nextBug: 40,
52 endAt: -1,
53 gain: { kills: 0, mine: 0, lost: 0, ends: 0, cities: standing === 0 ? CITIES : standing },
54 props
55 };
56}
57var cityX = (s, i) => layout(s.W).cityX[i];
58function banner(s, text, color, frames) {
59 s.banner = { text, color, until: s.t + frames };
60}
61function dropBug(s, isReal, fast = false) {
62 const alive = s.cities.map((c, i) => c ? i : -1).filter((i) => i >= 0);
63 if (alive.length === 0) return;
64 const target = alive[Math.floor(Math.random() * alive.length)];
65 const x0 = rand(1, s.W - 2);
66 const tx = cityX(s, target) + 2;
67 const frames = fast ? rand(70, 100) : rand(130, 200);
68 const vy = (SKY_FLOOR + 2) / frames;
69 s.bugs.push({ x0, y0: 0, x: x0, y: 0, vx: (tx - x0) / frames, vy, target, isReal });
70}
71function nearestSilo(s, x) {
72 let best = -1;
73 for (let i = 0; i < s.silos.length; i++) {
74 if (s.cooldown[i] > 0) continue;
75 if (best < 0 || Math.abs(s.silos[i] - x) < Math.abs(s.silos[best] - x)) best = i;
76 }
77 return best;
78}
79function fire(s, tx, ty, owner, isMiss = false, silo = nearestSilo(s, tx)) {
80 if (silo < 0) return false;
81 const sx = s.silos[silo];
82 s.cooldown[silo] = owner === "you" ? COOLDOWN : 0;
83 s.shots.push({ x0: sx, y0: SKY_FLOOR + 1, x: sx, y: SKY_FLOOR + 1, tx: clamp(tx, 0, s.W - 1), ty: clamp(ty, 0, SKY_FLOOR), owner, isMiss });
84 return true;
85}
86function autoShot(s, bug, owner, isMiss) {
87 const silo = s.silos.reduce((b, x, i) => Math.abs(x - bug.x) < Math.abs(s.silos[b] - bug.x) ? i : b, 0);
88 let tx = bug.x;
89 let ty = bug.y;
90 for (let k = 0; k < 3; k++) {
91 const frames = Math.hypot(tx - s.silos[silo], ty - (SKY_FLOOR + 1)) / SHOT_SPEED;
92 tx = bug.x + bug.vx * frames;
93 ty = bug.y + bug.vy * frames;
94 }
95 if (isMiss) tx += (Math.random() < 0.5 ? -1 : 1) * rand(6, 9);
96 fire(s, tx, ty, owner, isMiss, silo);
97}
98var lowest = (s) => s.bugs.reduce((b, x) => b === void 0 || x.y > b.y ? x : b, void 0);
99function play(s, e) {
100 const owner = e.practice ? "practice" : "auto";
101 if (e.kind === "tool") {
102 const bug = lowest(s);
103 if (bug) autoShot(s, bug, owner, Math.random() < 0.4);
104 } else if (e.kind === "fail") {
105 dropBug(s, !e.practice, true);
106 banner(s, "INCOMING", BUG_HEAD, 30);
107 } else if (e.kind === "small") {
108 const bug = lowest(s);
109 if (bug) autoShot(s, bug, owner, false);
110 else s.blasts.push({ x: rand(4, s.W - 4), y: rand(2, 7), age: 0, max: 2, owner: "practice" });
111 } else if (e.kind === "medium") {
112 if (s.bugs.length === 0) dropBug(s, !e.practice);
113 s.plans.push({ at: s.t + 20, kind: "sure", owner });
114 } else {
115 if (s.bugs.length === 0) {
116 dropBug(s, !e.practice);
117 dropBug(s, !e.practice);
118 }
119 s.plans.push({ at: s.t + 20, kind: "salvo", owner });
120 const ruined = s.cities.findIndex((c) => !c);
121 if (ruined >= 0 && !e.practice) {
122 s.cities[ruined] = true;
123 s.gain.cities = s.cities.filter(Boolean).length;
124 banner(s, "BONUS CITY", GOLD, 60);
125 } else banner(s, "SKY CLEAR!", GOLD, 60);
126 }
127}
128function step(s) {
129 s.t += 1;
130 for (let i = 0; i < s.cooldown.length; i++) s.cooldown[i] = Math.max(0, s.cooldown[i] - 1);
131 for (const p of s.plans.filter((p2) => p2.at <= s.t)) {
132 if (p.kind === "sure") {
133 const bug = lowest(s);
134 if (bug) autoShot(s, bug, p.owner, false);
135 } else for (const bug of s.bugs) autoShot(s, bug, p.owner, false);
136 }
137 s.plans = s.plans.filter((p) => p.at > s.t);
138 if (s.props.working && s.endAt < 0 && s.t >= s.nextBug && s.bugs.length < 3) {
139 dropBug(s, true);
140 s.nextBug = s.t + Math.floor(rand(120, 260));
141 }
142 for (const shot of s.shots) {
143 const dx = shot.tx - shot.x;
144 const dy = shot.ty - shot.y;
145 const d = Math.hypot(dx, dy);
146 if (d <= SHOT_SPEED) {
147 shot.x = shot.tx;
148 shot.y = shot.ty;
149 s.blasts.push({ x: shot.tx, y: shot.ty, age: 0, max: 3, owner: shot.owner });
150 } else {
151 shot.x += dx / d * SHOT_SPEED;
152 shot.y += dy / d * SHOT_SPEED;
153 }
154 }
155 s.shots = s.shots.filter((shot) => shot.x !== shot.tx || shot.y !== shot.ty);
156 for (const bug of s.bugs) {
157 bug.x += bug.vx;
158 bug.y += bug.vy;
159 }
160 for (const b of s.blasts) b.age += 1;
161 const radius = (b) => {
162 const r = [1, 1, 2, 2, 3, 3, 3, 3, 2, 2, 1][b.age] ?? 0;
163 return Math.min(r, b.max);
164 };
165 const chained = [];
166 s.bugs = s.bugs.filter((bug) => {
167 const hit = s.blasts.find((b) => radius(b) > 0 && Math.hypot(bug.x - b.x, bug.y - b.y) <= radius(b) + 0.5);
168 if (!hit) return true;
169 chained.push({ x: bug.x, y: bug.y, age: 0, max: 2, owner: hit.owner });
170 if (bug.isReal && hit.owner !== "practice") {
171 s.gain.kills += 1;
172 if (hit.owner === "you") s.gain.mine += 1;
173 }
174 return false;
175 });
176 s.blasts = [...s.blasts.filter((b) => b.age < 11), ...chained];
177 s.bugs = s.bugs.filter((bug) => {
178 if (bug.y < SKY_FLOOR + 1) return true;
179 s.blasts.push({ x: bug.x, y: SKY_FLOOR + 1, age: 0, max: 3, owner: "practice" });
180 if (bug.isReal && s.cities[bug.target]) {
181 s.cities[bug.target] = false;
182 s.gain.lost += 1;
183 s.gain.cities = s.cities.filter(Boolean).length;
184 }
185 return false;
186 });
187 if (s.endAt < 0 && s.cities.every((c) => !c)) {
188 s.endAt = s.t + 60;
189 s.bugs = [];
190 s.gain.ends += 1;
191 banner(s, "THE END", BUG_HEAD, 60);
192 }
193 if (s.endAt >= 0 && s.t >= s.endAt) {
194 s.endAt = -1;
195 s.cities = s.cities.map(() => true);
196 s.gain.cities = CITIES;
197 banner(s, "NEW CITIES", CITY, 40);
198 }
199}
200var isBusy = (s) => s.bugs.length > 0 || s.shots.length > 0 || s.blasts.length > 0 || s.plans.length > 0 || s.endAt >= 0 || s.banner !== null && s.t < s.banner.until;
201var CITY_SHAPES = [
202 [".#.#.", "#####", "#####"],
203 ["..#..", ".###.", "#####"],
204 ["#..#.", "##.##", "#####"]
205];
206function draw(s, el) {
207 const { Box, Text } = el;
208 const W = s.W;
209 const px = new Array(W * PH).fill(null);
210 const put = (x, y, c) => {
211 x = Math.round(x);
212 y = Math.round(y);
213 if (x >= 0 && x < W && y >= 0 && y < PH) px[y * W + x] = c;
214 };
215 const line = (x0, y0, x1, y1, c) => {
216 const n = Math.max(1, Math.ceil(Math.max(Math.abs(x1 - x0), Math.abs(y1 - y0))));
217 for (let i = 0; i <= n; i++) put(x0 + (x1 - x0) * i / n, y0 + (y1 - y0) * i / n, c);
218 };
219 const glyphs = /* @__PURE__ */ new Map();
220 const text = (x, row, str, color) => [...str].forEach((ch, i) => {
221 if (x + i >= 0 && x + i < W && row >= 0 && row < ROWS) glyphs.set(row * W + x + i, { ch, color });
222 });
223 for (let x = 0; x < W; x++) put(x, GROUND, SAND);
224 s.cities.forEach((alive, i) => {
225 const x = cityX(s, i);
226 if (alive) {
227 CITY_SHAPES[i % CITY_SHAPES.length].forEach((row, dy) => {
228 for (let dx = 0; dx < 5; dx++) if (row[dx] === "#") put(x + dx, CITY_TOP + dy, CITY);
229 });
230 } else for (let dx = 0; dx < 5; dx += 2) put(x + dx, GROUND - 1, RUBBLE);
231 });
232 for (const [i, sx] of s.silos.entries()) {
233 for (let dx = -2; dx <= 2; dx++) put(sx + dx, GROUND - 1, SILO);
234 for (let dx = -1; dx <= 1; dx++) put(sx + dx, GROUND - 2, s.cooldown[i] > 0 ? GREY : SILO);
235 }
236 for (const bug of s.bugs) {
237 line(bug.x0, bug.y0, bug.x, bug.y, BUG_TRAIL);
238 put(bug.x, bug.y, s.t % 6 < 3 ? BUG_HEAD : SHOT_HEAD);
239 }
240 for (const shot of s.shots) {
241 line(shot.x0, shot.y0, shot.x, shot.y, SHOT_TRAIL);
242 put(shot.x, shot.y, SHOT_HEAD);
243 if (shot.owner === "you") put(shot.tx, shot.ty, CROSS);
244 }
245 for (const b of s.blasts) {
246 const r = Math.min([1, 1, 2, 2, 3, 3, 3, 3, 2, 2, 1][b.age] ?? 0, b.max);
247 const c = BLAST[(b.age + Math.round(b.x)) % BLAST.length];
248 for (let dy = -r; dy <= r; dy++) for (let dx = -r; dx <= r; dx++) if (dx * dx + dy * dy <= r * r + 1) put(b.x + dx, b.y + dy, c);
249 }
250 const { x: cx, y: cy } = s.cross;
251 for (const [dx, dy] of [[-1, 0], [1, 0], [0, -1], [0, 1]]) put(cx + dx, cy + dy, CROSS);
252 const standing = s.cities.filter(Boolean).length;
253 const stats = `${s.props.stats} \u2302 ${standing}`;
254 text(W - stats.length - 1, 0, stats, GREY);
255 if (!s.hasFired && s.bugs.some((b) => b.isReal)) text(1, 0, "click to fire", GREY);
256 if (s.banner && s.t < s.banner.until && (s.banner.until - s.t) % 8 > 1) {
257 text(Math.floor((W - s.banner.text.length) / 2), 1, s.banner.text, s.banner.color);
258 }
259 const rows = [];
260 for (let row = 0; row < ROWS; row++) {
261 const runs = [];
262 let run = "";
263 let look = "";
264 let fg;
265 let bg;
266 const flush = () => {
267 if (run) runs.push(/* @__PURE__ */ h(Text, { ...fg ? { color: fg } : {}, ...bg ? { backgroundColor: bg } : {} }, run));
268 run = "";
269 };
270 for (let x = 0; x < W; x++) {
271 const g = glyphs.get(row * W + x);
272 const top = px[row * 2 * W + x] ?? null;
273 const bottom = px[(row * 2 + 1) * W + x] ?? null;
274 let ch;
275 let f;
276 let b;
277 if (g) [ch, f, b] = [g.ch, g.color, void 0];
278 else if (top === null && bottom === null) [ch, f, b] = [" ", void 0, void 0];
279 else if (top === bottom) [ch, f, b] = ["\u2588", top, void 0];
280 else if (bottom === null) [ch, f, b] = ["\u2580", top, void 0];
281 else if (top === null) [ch, f, b] = ["\u2584", bottom, void 0];
282 else [ch, f, b] = ["\u2580", top, bottom];
283 const next = `${f ?? ""}|${b ?? ""}`;
284 if (next !== look) {
285 flush();
286 look = next;
287 fg = f;
288 bg = b;
289 }
290 run += ch;
291 }
292 flush();
293 rows.push(/* @__PURE__ */ h(Box, { flexDirection: "row" }, runs));
294 }
295 return /* @__PURE__ */ h(Box, { flexDirection: "column" }, rows);
296}
297var BugSky = (props, surface) => {
298 if (surface.state === void 0) {
299 const sky2 = create(props);
300 const redraw = () => surface.setState({ sky: sky2, frame: sky2.t });
301 let posted = props.cities;
302 const post = () => {
303 const g = sky2.gain;
304 if (g.kills + g.mine + g.lost + g.ends === 0 && g.cities === posted) return;
305 surface.post({ ...g });
306 posted = g.cities;
307 sky2.gain = { kills: 0, mine: 0, lost: 0, ends: 0, cities: g.cities };
308 };
309 surface.every(TICK_MS, () => {
310 for (const e of sky2.props.events) {
311 if (e.id <= sky2.seen) continue;
312 sky2.seen = e.id;
313 play(sky2, e);
314 }
315 const wasBusy = isBusy(sky2);
316 step(sky2);
317 post();
318 if (wasBusy || isBusy(sky2)) redraw();
319 });
320 surface.onPointer((e) => {
321 const y = e.fine ? Math.floor(e.fine.y * 2) : e.y * 2 + 1;
322 sky2.cross = { x: clamp(e.x, 0, sky2.W - 1), y: clamp(y, 0, SKY_FLOOR) };
323 if (e.type === "down" && e.button === "left" && fire(sky2, sky2.cross.x, sky2.cross.y, "you")) sky2.hasFired = true;
324 redraw();
325 });
326 surface.onKey((e) => {
327 const move = { left: [-2, 0], right: [2, 0], up: [0, -1], down: [0, 1] };
328 const m = move[e.key];
329 if (m) sky2.cross = { x: clamp(sky2.cross.x + m[0] * (e.shift ? 4 : 1), 0, sky2.W - 1), y: clamp(sky2.cross.y + m[1], 0, SKY_FLOOR) };
330 else if (e.key === " " || e.key === "return") {
331 if (fire(sky2, sky2.cross.x, sky2.cross.y, "you")) sky2.hasFired = true;
332 } else if (e.key === "1" || e.key === "2" || e.key === "3") {
333 if (fire(sky2, sky2.cross.x, sky2.cross.y, "you", false, sky2.cooldown[Number(e.key) - 1] === 0 ? Number(e.key) - 1 : -1)) sky2.hasFired = true;
334 } else return;
335 redraw();
336 });
337 surface.setState({ sky: sky2, frame: 0 });
338 return draw(sky2, surface.elements);
339 }
340 const sky = surface.state.sky;
341 sky.props = props;
342 const W = Math.max(24, surface.columns || props.columns);
343 if (W !== sky.W) {
344 sky.W = W;
345 sky.silos = layout(W).silos;
346 sky.cross.x = clamp(sky.cross.x, 0, W - 1);
347 }
348 return draw(sky, surface.elements);
349};
350var bug_sky_default = BugSky;
351export {
352 bug_sky_default as default
353};
354types/index.d.ts 104 lines1// dragon-lair
2export type Hoard = { gold: number; meals: number; feats: number }
3export type DragonMood = 'idle' | 'work' | 'sad' | 'sleep' | 'fire'
4
5// jackpot
6export type Bank = {
7 chips: number
8 spins: number
9 jackpots: number
10 best: number
11 streak: number
12}
13
14// outlaw
15export type OutlawScore = { you: number; bugs: number; streak: number; best: number }
16
17// tama
18export type Stage = 'egg' | 'baby' | 'child' | 'teen' | 'worker' | 'rascal' | 'chubby'
19
20export type Pet = {
21 stage: Stage
22 // Epoch milliseconds: when it hatched, and when hunger and joy last ticked down.
23 born: number
24 hungerAt: number
25 joyAt: number
26 hunger: number
27 joy: number
28 poops: number
29 // Since when hunger has sat at 0; it leaves after twelve hours of it.
30 starvingSince: number
31 eggTurns: number
32 turns: number
33 wins: number
34 errors: number
35 cleanRun: number
36 generation: number
37 // Hand care left today, and which day that is (YYYY-MM-DD).
38 careDay: string
39 careLeft: number
40}
41
42// tetris
43export type Tally = { score: number; lines: number; best: number; games: number }
44
45// octo-invader
46export type OctoScore = { xp: number; toppled: number; planes: number; tools: number }
47export type OctoMood = 'idle' | 'work' | 'sad' | 'sleep' | 'rampage'
48// duck-hunt
49export type DuckScore = { hits: number; escaped: number; tools: number }
50// bug-command
51// dario
52// block-town
53export type TownScore = { blocks: number; houses: number; trees: number; castles: number; creepers: number; tools: number }
54export type TownPlot = {
55 kind: 'house' | 'bighouse' | 'farm' | 'well' | 'tower' | 'castle' | 'tree'
56 x: number
57 progress: number
58 wasDone: boolean
59 plantedAt?: number
60}
61export type DarioScore = { coins: number; stomps: number; hits: number; clears: number; tools: number }
62export type BugsScore = { kills: number; mine: number; lost: number; ends: number; cities: number; tools: number }
63export type BugsFeed = {
64 events: { id: number; kind: 'tool' | 'fail' | 'small' | 'medium' | 'big'; practice: boolean }[]
65 next: number
66 working: boolean
67}
68
69declare module 'claude-code' {
70 interface PluginState {
71 arcade: {
72 // Which games this session shows, and the setting they were picked for.
73 shown: readonly string[]
74 pickedFor: string
75 // The game new terminals start with ('' unless fixed), and whether the first sessions' hint shows.
76 defaultGame: string
77 hint: boolean
78 // A newer Arcade version found on GitHub at this session's start ('' when none).
79 latest: string
80 dragonHoard: Hoard
81 dragonMood: DragonMood
82 dragonFeat: string
83 jackpotBank: Bank
84 jackpotGolden: number
85 jackpotLast: string
86 outlawScore: OutlawScore
87 tamaPet: Pet
88 tetrisTally: Tally
89 octopusScore: OctoScore
90 octopusMood: OctoMood
91 octopusFeat: string
92 duckScore: DuckScore
93 duckFeat: string
94 bugsScore: BugsScore
95 bugsFeed: BugsFeed
96 bugsFeat: string
97 darioScore: DarioScore
98 darioFeat: string
99 townScore: TownScore
100 townFeat: string
101 }
102 }
103}
104