Failing tests spawn a pixel boss; every run that fixes tests lands a hit, zero failures is a KO with loot

Failing tests spawn a pixel boss; every run that fixes tests lands a hit, zero failures is a KO with loot.

When a test run fails, a pixel boss spawns with one HP per failing test. Every later run that fixes tests lands a hit. Zero failures is a KO with loot. Reads vitest, jest, pytest, mocha, and cargo test output.
In a Claude Code session (2.1.287 or later):
/plugin marketplace add OneWave-AI/claude-code-mods
/plugin install boss-fight@claude-code-mods
Or load this folder for one session: claude --plugin-dir ./boss-fight
| Mod | Network | Runs processes | Files | Calls a model | Sends data anywhere |
|---|---|---|---|---|---|
| boss-fight | No | No | No | No | No |
Run claude plugin validate ./boss-fight to list every event it hooks and every call it makes. See the repository README for the full security notes.
Part of claude-code-mods by OneWave AI. MIT licensed.
hooks/register.tsx 219 lines1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register } from 'claude-code'
3
4import type { Fight, RunResult } from '../types'
5import { ARENA_ROWS, arena } from './arena'
6import { NO_FIGHT, parseRun, phaseAt, runnerOf, step } from './fight'
7
8const PANE = 'boss-fight'
9const FRAME_MS = 60
10
11const fightAtom = atom({ plugin: 'boss-fight', key: 'fight' } as const, NO_FIGHT as Fight)
12const killsAtom = atom({ plugin: 'boss-fight', key: 'kills' } as const, 0)
13
14const GOLD = '#c2a87e'
15const BLUE = '#61A5FA'
16const RED = '#e5484d'
17const PAPER = '#E8E2D6'
18const INK = '#0B1121'
19
20// The module's copy of the fight, so the animation timer reads it without a dispatch.
21let fight: Fight = NO_FIGHT
22let anim: { cancel: () => void } | null = null
23let animSize = ''
24
25const startAnim = ($: EngineInterface, columns: number) => {
26 const size = `${columns}`
27 if (anim && animSize === size) return
28 anim?.cancel()
29 animSize = size
30 const timer = $.clock.every(FRAME_MS, () => {
31 void $.clock.now().then(now =>
32 $.ui.blit({ requestId: PANE, key: 'arena', cells: arena(columns, ARENA_ROWS, now, fight) }).then(r => {
33 if (r.deny && anim === timer) {
34 timer.cancel()
35 anim = null
36 }
37 }),
38 )
39 })
40 anim = timer
41}
42
43const setFight = async ($: EngineInterface, next: Fight) => {
44 fight = next
45 await update($, fightAtom, () => next)
46}
47
48const statusFor = (f: Fight) =>
49 f.phase === 'none' || f.phase === 'ko' ? undefined : `Boss: ${f.boss} ${f.hp}/${f.maxHp} HP`
50
51const play = async ($: EngineInterface, run: RunResult, isDemo: boolean) => {
52 const now = await $.clock.now()
53 const move = step(fight, run, now, Math.floor(now / 7) + run.failed * 31)
54 const next = { ...move.fight, isDemo: move.event === 'spawn' ? isDemo : move.fight.isDemo }
55 await setFight($, next)
56 $.ui.status(statusFor(next))
57
58 if (move.event === 'spawn') {
59 $.ui.toast(`A boss appears: ${next.boss} (${next.hp} HP). /boss to fight.`, { timeoutMs: 6000 })
60 const panes = await $.ui.panes()
61 if (!panes.some(p => p.id === PANE)) void $.ui.open({ id: PANE, title: 'Boss Fight' })
62 }
63 if (move.event === 'hit') $.ui.toast(`Hit! ${move.fight.delta} damage. ${next.boss} has ${next.hp} HP left.`)
64 if (move.event === 'heal') $.ui.toast(`${next.boss} healed ${-move.fight.delta} HP. New failures.`)
65 if (move.event === 'ko') {
66 let kills = await read($, killsAtom)
67 if (!next.isDemo) {
68 kills = Number((await $.store.get('kills')) ?? 0) + 1
69 await $.store.set('kills', kills)
70 await update($, killsAtom, () => kills)
71 }
72 $.ui.toast(`${next.boss} defeated in ${next.runs} runs. Loot: ${next.loot}`, { timeoutMs: 8000 })
73 }
74}
75
76const DEMO: readonly { failed: number; ms: number }[] = [
77 { failed: 7, ms: 3200 },
78 { failed: 5, ms: 2600 },
79 { failed: 6, ms: 2400 },
80 { failed: 3, ms: 2400 },
81 { failed: 1, ms: 2400 },
82 { failed: 0, ms: 4000 },
83]
84
85let demoRun = 0
86
87const runDemo = async ($: EngineInterface) => {
88 const run = ++demoRun
89 await setFight($, NO_FIGHT)
90 let at = 400
91 for (const s of DEMO) {
92 $.clock.after(at, () => {
93 if (run !== demoRun) return
94 void play($, { runner: 'vitest', failed: s.failed, passed: 40 - s.failed }, true)
95 })
96 at += s.ms
97 }
98 return Math.round(at / 1000)
99}
100
101const hpBar = (hp: number, max: number, size: number) => {
102 const filled = max === 0 ? 0 : Math.max(0, Math.min(size, Math.round((hp / max) * size)))
103 return { full: '█'.repeat(filled), empty: '░'.repeat(size - filled) }
104}
105
106export const register: Register = on => {
107 on('session.start', async ($, e, next) => {
108 await $.command.register({
109 name: 'boss',
110 description: 'Failing tests become a boss: /boss [demo|reset]',
111 })
112 const kills = Number((await $.store.get('kills')) ?? 0)
113 await update($, killsAtom, () => kills)
114 fight = await read($, fightAtom)
115
116 return next(e)
117 })
118
119 on('command.run', { command: 'boss' }, async ($, e) => {
120 const arg = e.args.trim().toLowerCase()
121 if (arg === 'reset') {
122 demoRun++
123 await setFight($, NO_FIGHT)
124 $.ui.status(undefined)
125 return { text: 'Arena cleared.' }
126 }
127 await $.ui.open({ id: PANE, title: 'Boss Fight' })
128 if (arg === 'demo') {
129 const seconds = await runDemo($)
130 return { text: `Demo fight running (${seconds}s). Hit record.` }
131 }
132 return { text: 'Boss pane opened.' }
133 })
134
135 on('tool.call', { tool: 'Bash' }, async ($, e, next) => {
136 const ran = await next(e)
137 const runner = runnerOf(e.command)
138 if (!runner || ran.deny !== undefined) return ran
139 const result = parseRun(runner, ran.text ?? '')
140 if (result) await play($, result, false)
141 return ran
142 })
143
144 on('ui.render', { component: 'Pane', requestId: PANE }, async ($, e) => {
145 const elements = $.ui.resolve(e)
146 const { Box, Text, Button } = elements
147 const Raster = 'Raster' in elements ? elements.Raster : null
148 const now = await $.clock.now()
149 const f = await read($, fightAtom)
150 const kills = await read($, killsAtom)
151 const cols = Math.max(30, e.props.bodyColumns || e.viewport?.columns || 70)
152 const width = cols - 2
153 fight = f
154
155 if (Raster && e.surface === 'terminal') startAnim($, width)
156
157 const phase = phaseAt(f, now)
158 const isLive = f.phase !== 'none' && f.phase !== 'ko'
159 const meter = Math.max(10, Math.min(40, width - 24))
160 const bar = hpBar(f.hp, f.maxHp, meter)
161
162 return (
163 <Box flexDirection="column" paddingX={1}>
164 {Raster && e.surface === 'terminal' && (
165 <Raster key="arena" columns={width} rows={ARENA_ROWS} cells={arena(width, ARENA_ROWS, now, f)} />
166 )}
167 {f.phase === 'none' ? (
168 <Box marginTop={1}>
169 <Text dimColor>No boss. Run a test suite with failures to summon one.</Text>
170 </Box>
171 ) : (
172 <Box flexDirection="column" marginTop={1}>
173 <Box justifyContent="space-between">
174 <Text bold color={f.phase === 'ko' ? GOLD : RED}>{f.boss.toUpperCase()}</Text>
175 <Text dimColor>{f.runner} · run {f.runs}</Text>
176 </Box>
177 <Text>
178 <Text color={phase === 'hit' ? PAPER : RED}>{bar.full}</Text>
179 <Text dimColor>{bar.empty}</Text>
180 <Text bold color={PAPER}> {f.hp}/{f.maxHp} HP</Text>
181 </Text>
182 {f.phase === 'ko' && (
183 <Box flexDirection="column" marginTop={1}>
184 <Text backgroundColor={GOLD} color={INK} bold>{` VICTORY · ${f.runs} runs `}</Text>
185 <Text>
186 <Text dimColor>loot drop </Text>
187 <Text bold color={GOLD}>{f.loot ?? ''}</Text>
188 </Text>
189 </Box>
190 )}
191 {isLive && phase === 'heal' && <Text color={RED}>New failures. The boss grows stronger.</Text>}
192 {isLive && phase === 'hit' && <Text color={GOLD}>{f.delta} test{f.delta === 1 ? '' : 's'} fixed. Critical hit.</Text>}
193 </Box>
194 )}
195 <Box justifyContent="space-between" marginTop={1}>
196 <Box gap={3}>
197 <Button key="demo" label="demo fight" hotkey="d" plain onPress={() => void runDemo($)} />
198 <Button
199 key="reset"
200 label="reset"
201 hotkey="r"
202 plain
203 onPress={() => {
204 demoRun++
205 void setFight($, NO_FIGHT)
206 $.ui.status(undefined)
207 }}
208 />
209 </Box>
210 <Text>
211 <Text dimColor>bosses slain </Text>
212 <Text bold color={BLUE}>{kills}</Text>
213 </Text>
214 </Box>
215 </Box>
216 )
217 })
218}
219hooks/arena.ts 188 lines1/** The arena: a pixel boss, its HP bar, hits, KO and the hero's cursor. */
2
3import type { Fight } from '../types'
4import { phaseAt } from './fight'
5import { canvas, encode, mix, rect, set, sprite, text } from './pixels'
6
7/** Three boss bodies, 16 x 14, picked by `kind % 3`; palette swaps by kind. */
8const BODIES: readonly (readonly string[])[] = [
9 [
10 '..H..........H..',
11 '..HH........HH..',
12 '...HDDDDDDDDH...',
13 '..DBBBBBBBBBBD..',
14 '.DBBBBBBBBBBBBD.',
15 '.DBWWEBBBBWWEBD.',
16 'DBBWEEBBBBWEEBBD',
17 'DBBBBBBBBBBBBBBD',
18 'DBBBTMTMTMTMBBBD',
19 'DBBBMMMMMMMMBBBD',
20 '.DBBBBBBBBBBBBD.',
21 '.DBBLBBBBBBLBBD.',
22 '..DDLD....DLDD..',
23 '...DD......DD...',
24 ],
25 [
26 '.....DDDDDD.....',
27 '...DDBBBBBBDD...',
28 '..DBBBBBBBBBBD..',
29 '.DBBWWEBBWWEBBD.',
30 '.DBBWEEBBWEEBBD.',
31 'DBBBBBBBBBBBBBBD',
32 'DBBBBMMMMMMBBBBD',
33 'DBBBBMTMTMTBBBBD',
34 'DBBBBBBBBBBBBBBD',
35 '.DBBBBBBBBBBBBD.',
36 '.DBDBBDBBDBBDBD.',
37 '.DD.DD.DD.DD.DD.',
38 'DD...DD..DD...DD',
39 '................',
40 ],
41 [
42 'H..............H',
43 'HH....DDDD....HH',
44 '.HH.DDBBBBDD.HH.',
45 '..HDBBBBBBBBDH..',
46 '...DBWEBBWEBD...',
47 '..DBBEEBBEEBBD..',
48 '..DBBBBBBBBBBD..',
49 '.DBBBTMTTMTBBBD.',
50 'DBLBBMMMMMMBBLBD',
51 'DBLBBBBBBBBBBLBD',
52 'DBLDBBBBBBBBDLBD',
53 '.D.DBBBBBBBBD.D.',
54 '...DBBD..DBBD...',
55 '...DDD....DDD...',
56 ],
57]
58
59const SKINS: readonly { B: number; D: number; L: number; H: number }[] = [
60 { B: 0x8a3a3a, D: 0x3d1414, L: 0xb85c4e, H: 0xc2a87e },
61 { B: 0x3f5a85, D: 0x16233a, L: 0x61a5fa, H: 0xe8e2d6 },
62 { B: 0x7a6248, D: 0x2e2216, L: 0xc2a87e, H: 0xd4a44a },
63 { B: 0x4a4f5c, D: 0x1a1c22, L: 0x8a90a0, H: 0xe5484d },
64]
65
66const BG = 0x0b1121
67const FLOOR = 0x141b2b
68const FLOOR_LINE = 0x252e44
69const GOLD = 0xc2a87e
70const GOLD_HI = 0xd4a44a
71const BLUE = 0x61a5fa
72const RED = 0xe5484d
73const PAPER = 0xe8e2d6
74const WHITE = 0xffffff
75
76const CURSOR = ['X...', 'XX..', 'XXX.', 'XXXX', 'XX..', '.X..']
77
78/** Cell rows the arena takes. */
79export const ARENA_ROWS = 14
80
81const hash = (n: number) => {
82 const x = Math.sin(n * 91.7) * 43758.5453
83 return x - Math.floor(x)
84}
85
86/** One frame at `now` (epoch ms), `columns` wide and `rows` cells tall. */
87export const arena = (columns: number, rows: number, now: number, fight: Fight) => {
88 const w = Math.max(20, columns)
89 const h = rows * 2
90 const c = canvas(w, h, BG)
91 const t = now / 1000
92 const phase = phaseAt(fight, now)
93 const since = now - fight.phaseAt
94
95 // Torches on the back wall, the floor in perspective.
96 for (const tx of [3, w - 5]) {
97 rect(c, tx, 6, 2, 6, 0x2a2018)
98 const flick = Math.sin(t * 9 + tx) * 0.5 + 0.5
99 rect(c, tx, 3, 2, 3, mix(GOLD, GOLD_HI, flick))
100 set(c, tx + (flick > 0.5 ? 1 : 0), 2, mix(RED, GOLD_HI, flick))
101 }
102 rect(c, 0, h - 5, w, 5, FLOOR)
103 rect(c, 0, h - 5, w, 1, FLOOR_LINE)
104 for (let x = 0; x < w; x += 6) set(c, x + ((x / 6) % 2) * 3, h - 3, FLOOR_LINE)
105
106 if (fight.phase === 'none') {
107 text(c, Math.round((w - 20) / 2), Math.floor(rows / 2) - 1, 'THE ARENA IS QUIET', PAPER)
108 text(c, Math.round((w - 26) / 2), Math.floor(rows / 2) + 1, 'run your tests to summon', mix(PAPER, BG, 0.4))
109 return encode(c)
110 }
111
112 const body = BODIES[fight.kind % BODIES.length]!
113 const skin = SKINS[fight.kind % SKINS.length]!
114 const scale = w >= 50 ? 2 : 1
115 const bw = 16 * scale
116 const bh = body.length * scale
117 let bx = Math.round((w - bw) / 2) + 4
118 let by = h - 4 - bh
119
120 let palette: Record<string, number> = { ...skin, W: PAPER, E: RED, M: 0x1a0c0c, T: PAPER }
121 if (phase === 'idle') by += Math.round((Math.sin(t * 2.4) + 1) / 2)
122 if (phase === 'spawn') {
123 // Rises out of the floor.
124 const k = Math.min(1, since / 1200)
125 by = h - 4 - Math.round(bh * k)
126 }
127 if (phase === 'hit') {
128 bx += Math.round(Math.sin(since / 25) * 3 * Math.max(0, 1 - since / 700))
129 if (since < 450 && Math.floor(since / 75) % 2 === 0) {
130 palette = Object.fromEntries(Object.keys(palette).map(k => [k, WHITE]))
131 }
132 }
133 if (phase === 'heal') palette = { ...palette, B: mix(skin.B, RED, 0.4 + 0.3 * Math.sin(since / 80)) }
134 if (phase === 'ko') {
135 // Sinks and fades into the floor.
136 const k = Math.min(1, since / 1600)
137 by += Math.round(bh * k * 0.9)
138 palette = Object.fromEntries(Object.entries(palette).map(([key, v]) => [key, mix(v, BG, k * 0.8)]))
139 }
140
141 if (!(phase === 'ko' && since > 1700)) {
142 rect(c, bx + 2, h - 4, bw - 4, 1, mix(FLOOR, 0x000000, 0.5))
143 sprite(c, body, palette, bx, by, scale)
144 // Clip what sank below the floor line.
145 if (phase === 'spawn' || phase === 'ko') {
146 rect(c, 0, h - 4, w, 4, FLOOR)
147 for (let x = 0; x < w; x += 6) set(c, x + ((x / 6) % 2) * 3, h - 3, FLOOR_LINE)
148 }
149 }
150
151 // The hero: a cursor that lunges on a hit.
152 const lunge = phase === 'hit' ? Math.max(0, 1 - Math.abs(since - 150) / 300) : 0
153 const hx = 4 + Math.round(lunge * Math.max(0, bx - 10))
154 sprite(c, CURSOR, { X: phase === 'ko' ? GOLD_HI : BLUE }, hx, h - 12, scale)
155
156 // Slash streak on contact.
157 if (phase === 'hit' && since > 100 && since < 450) {
158 for (let i = 0; i < bh; i++) set(c, bx + i * 0.6 + 2, by + bh - i, mix(WHITE, GOLD, i / bh))
159 }
160
161 // Damage number rising off the boss.
162 if ((phase === 'hit' || phase === 'heal' || phase === 'ko') && since < 1400 && fight.delta !== 0) {
163 const label = fight.delta > 0 ? `-${fight.delta}` : `+${-fight.delta}`
164 const row = Math.max(0, Math.floor(by / 2) - 1 - Math.floor(since / 350))
165 text(c, bx + bw + 1, row, label, fight.delta > 0 ? GOLD_HI : RED)
166 }
167
168 // KO: sparks, then the banner.
169 if (phase === 'ko') {
170 for (let i = 0; i < 18; i++) {
171 const a = hash(i) * Math.PI * 2
172 const r = Math.min(1, since / 900) * (8 + hash(i + 9) * 18)
173 const life = Math.max(0, 1 - since / 2000)
174 if (life > 0) set(c, bx + bw / 2 + Math.cos(a) * r * 1.6, by + bh / 2 + Math.sin(a) * r, mix(BG, i % 2 ? GOLD_HI : BLUE, life))
175 }
176 if (since > 900) {
177 const blink = Math.floor(t * 3) % 2 === 0
178 text(c, Math.round((w - 7) / 2), 1, 'VICTORY', blink ? GOLD_HI : GOLD)
179 }
180 }
181
182 if (phase === 'spawn' && since < 1500 && Math.floor(t * 6) % 2 === 0) {
183 text(c, Math.round((w - 15) / 2), 1, 'A BOSS APPEARS', RED)
184 }
185
186 return encode(c)
187}
188hooks/fight.ts 204 lines1/** Reading test runs and turning them into fight moves: pure, so the tests can drive them. */
2
3import type { Fight, RunResult } from '../types'
4
5const RUNNERS: readonly [string, RegExp][] = [
6 ['vitest', /\bvitest\b/],
7 ['jest', /\bjest\b/],
8 ['pytest', /\bpytest\b|\bpython3?\s+-m\s+pytest\b/],
9 ['bun', /\bbun\s+test\b/],
10 ['cargo', /\bcargo\s+(test|nextest)\b/],
11 ['go', /\bgo\s+test\b/],
12 ['claude', /\bclaude\s+plugin\s+test\b/],
13 ['npm', /\b(npm|pnpm|yarn)\s+(run\s+)?test\b|\bnpx\s+(jest|vitest)\b/],
14 ['mocha', /\bmocha\b/],
15 ['rspec', /\brspec\b/],
16]
17
18/** Which runner a Bash command starts, or null when it runs no tests. */
19export const runnerOf = (command: string): string | null => {
20 for (const [name, re] of RUNNERS) if (re.test(command)) return name
21 return null
22}
23
24const ANSI = /\u001b\[[0-9;]*[A-Za-z]/g
25
26const sumAll = (text: string, re: RegExp) => {
27 let total = 0
28 let found = false
29 for (const m of text.matchAll(re)) {
30 total += Number(m[1])
31 found = true
32 }
33 return found ? total : null
34}
35
36/** The last match's number: summaries come last, so it wins over per-file lines. */
37const last = (text: string, re: RegExp) => {
38 let out: number | null = null
39 for (const m of text.matchAll(re)) out = Number(m[1])
40 return out
41}
42
43/**
44 * Failed and passed counts from a runner's output, tolerant of the common
45 * summary shapes (jest, vitest, pytest, bun, cargo, go, mocha, rspec).
46 * Null when the output carries no summary at all.
47 */
48export const parseRun = (runner: string, raw: string): RunResult | null => {
49 const text = raw.replace(ANSI, '')
50
51 // jest: "Tests: 2 failed, 5 passed, 7 total"
52 const jest = /Tests:\s+(?:(\d+)\s+failed,?\s*)?(?:\d+\s+skipped,?\s*)?(?:\d+\s+todo,?\s*)?(?:(\d+)\s+passed,?\s*)?(?:\d+\s+total)/.exec(text)
53 if (jest && (jest[1] || jest[2])) {
54 return { runner, failed: Number(jest[1] ?? 0), passed: Number(jest[2] ?? 0) }
55 }
56
57 // vitest: "Tests 2 failed | 5 passed (7)"
58 const vitest = /Tests\s+(?:(\d+)\s+failed)?\s*\|?\s*(?:(\d+)\s+passed)?\s*(?:\|[^(\n]*)?\(\d+\)/.exec(text)
59 if (vitest && (vitest[1] || vitest[2])) {
60 return { runner, failed: Number(vitest[1] ?? 0), passed: Number(vitest[2] ?? 0) }
61 }
62
63 // cargo: "test result: FAILED. 3 passed; 2 failed;" (one per crate: add them)
64 const cargoFailed = sumAll(text, /test result: \w+\.\s+\d+ passed;\s+(\d+) failed/g)
65 if (cargoFailed !== null) {
66 return { runner, failed: cargoFailed, passed: sumAll(text, /test result: \w+\.\s+(\d+) passed/g) ?? 0 }
67 }
68
69 // bun / claude plugin test: " 12 pass\n 1 fail"
70 const bunPass = last(text, /^\s*(\d+)\s+pass\s*$/gm)
71 const bunFail = last(text, /^\s*(\d+)\s+fail\s*$/gm)
72 if (bunPass !== null || bunFail !== null) {
73 return { runner, failed: bunFail ?? 0, passed: bunPass ?? 0 }
74 }
75
76 // pytest: "=== 2 failed, 5 passed in 0.12s ===", "1 error"
77 const pyLine = /=+ (.*\bin [\d.]+s.*?) =+/.exec(text)?.[1]
78 if (pyLine) {
79 const f = Number(/(\d+) failed/.exec(pyLine)?.[1] ?? 0) + Number(/(\d+) errors?/.exec(pyLine)?.[1] ?? 0)
80 const p = Number(/(\d+) passed/.exec(pyLine)?.[1] ?? 0)
81 if (f || p || /no tests ran/.test(pyLine)) return { runner, failed: f, passed: p }
82 }
83
84 // mocha: "5 passing" / "2 failing"; rspec: "7 examples, 2 failures"
85 const passing = last(text, /(\d+) passing\b/g)
86 const failing = last(text, /(\d+) failing\b/g)
87 if (passing !== null || failing !== null) return { runner, failed: failing ?? 0, passed: passing ?? 0 }
88 const rspec = /(\d+) examples?, (\d+) failures?/.exec(text)
89 if (rspec) return { runner, failed: Number(rspec[2]), passed: Number(rspec[1]) - Number(rspec[2]) }
90
91 // go: one "--- FAIL:" per failing test, "ok"/"PASS" lines otherwise.
92 if (runner === 'go') {
93 const fails = (text.match(/^\s*--- FAIL:/gm) ?? []).length
94 const passes = (text.match(/^\s*--- PASS:/gm) ?? []).length
95 if (fails || passes || /^(ok|PASS|FAIL)\b/m.test(text)) {
96 return { runner, failed: fails || (/^FAIL\b/m.test(text) ? 1 : 0), passed: passes }
97 }
98 }
99
100 return null
101}
102
103export const BOSSES = [
104 'The Flaky Hydra',
105 'Null Pointer Wraith',
106 'Regression Golem',
107 'The Off-By-One Imp',
108 'Race Condition Wyrm',
109 'Undefined Behemoth',
110 'Merge Conflict Ogre',
111 'Timeout Specter',
112] as const
113
114export const LOOT = [
115 'Cloak of Green Checkmarks',
116 '+1 Senior Dev',
117 'Amulet of Deterministic Builds',
118 'Boots of Fast CI',
119 'Tome of Readable Stack Traces',
120 'Ring of Zero Flakes',
121 'Shield of 100% Coverage',
122 'Potion of Clean Merges',
123 'Sword of Root Cause',
124 'Golden Rubber Duck',
125] as const
126
127export const NO_FIGHT: Fight = {
128 boss: '',
129 kind: 0,
130 maxHp: 0,
131 hp: 0,
132 runner: '',
133 phase: 'none',
134 phaseAt: 0,
135 delta: 0,
136 runs: 0,
137 loot: null,
138 isDemo: false,
139}
140
141const pick = <T>(list: readonly T[], seed: number) => list[Math.abs(Math.floor(seed)) % list.length]!
142
143export type Move = { fight: Fight; event: 'none' | 'spawn' | 'hit' | 'heal' | 'ko' | 'clean' | 'stalemate' }
144
145/** What one test run does to the fight. `seed` picks a boss or loot. */
146export const step = (fight: Fight, run: RunResult, now: number, seed: number): Move => {
147 const alive = fight.phase !== 'none' && fight.phase !== 'ko'
148 if (!alive) {
149 if (run.failed === 0) return { fight, event: fight.phase === 'none' ? 'clean' : 'none' }
150 const kind = Math.abs(Math.floor(seed)) % BOSSES.length
151 return {
152 fight: {
153 boss: BOSSES[kind]!,
154 kind,
155 maxHp: run.failed,
156 hp: run.failed,
157 runner: run.runner,
158 phase: 'spawn',
159 phaseAt: now,
160 delta: 0,
161 runs: 1,
162 loot: null,
163 isDemo: false,
164 },
165 event: 'spawn',
166 }
167 }
168 const runs = fight.runs + 1
169 if (run.failed === 0) {
170 return {
171 fight: { ...fight, hp: 0, phase: 'ko', phaseAt: now, delta: fight.hp, runs, loot: pick(LOOT, seed) },
172 event: 'ko',
173 }
174 }
175 if (run.failed < fight.hp) {
176 return { fight: { ...fight, hp: run.failed, phase: 'hit', phaseAt: now, delta: fight.hp - run.failed, runs }, event: 'hit' }
177 }
178 if (run.failed > fight.hp) {
179 return {
180 fight: {
181 ...fight,
182 hp: run.failed,
183 maxHp: Math.max(fight.maxHp, run.failed),
184 phase: 'heal',
185 phaseAt: now,
186 delta: fight.hp - run.failed,
187 runs,
188 },
189 event: 'heal',
190 }
191 }
192 return { fight: { ...fight, runs }, event: 'stalemate' }
193}
194
195/** How long each phase animates before the boss settles back to idle. */
196export const PHASE_MS = { spawn: 1600, hit: 1300, heal: 1300 } as const
197
198export const phaseAt = (fight: Fight, now: number): Fight['phase'] => {
199 if (fight.phase === 'spawn' || fight.phase === 'hit' || fight.phase === 'heal') {
200 return now - fight.phaseAt > PHASE_MS[fight.phase] ? 'idle' : fight.phase
201 }
202 return fight.phase
203}
204hooks/pixels.ts 111 lines1/** A tiny pixel canvas packed into Raster cells: two pixels per cell with the upper half block. */
2
3export const DEFAULT = 0x01000000
4const HALF = 0x2580
5
6export type Canvas = {
7 w: number
8 h: number
9 px: Uint32Array
10 /** Cell index (row * w + col) to a glyph drawn over the pixels. */
11 glyphs: Map<number, { ch: number; fg: number }>
12}
13
14export const canvas = (w: number, h: number, bg: number): Canvas => ({
15 w,
16 h,
17 px: new Uint32Array(w * h).fill(bg),
18 glyphs: new Map(),
19})
20
21export const set = (c: Canvas, x: number, y: number, color: number) => {
22 const px = Math.round(x)
23 const py = Math.round(y)
24 if (px < 0 || py < 0 || px >= c.w || py >= c.h) return
25 c.px[py * c.w + px] = color
26}
27
28export const get = (c: Canvas, x: number, y: number) => c.px[y * c.w + x] ?? 0
29
30/** Text over the pixels, in cell coordinates (one cell = one column, two pixel rows). */
31export const text = (c: Canvas, col: number, row: number, s: string, fg: number) => {
32 const rows = c.h / 2
33 for (let i = 0; i < s.length; i++) {
34 const x = Math.round(col) + i
35 const r = Math.round(row)
36 if (x < 0 || x >= c.w || r < 0 || r >= rows) continue
37 const ch = s.charCodeAt(i)
38 if (ch === 0x20) continue
39 c.glyphs.set(r * c.w + x, { ch, fg })
40 }
41}
42
43/** Draws `rows` (one char per pixel, '.' transparent) at x,y, each pixel `scale` square. */
44export const sprite = (
45 c: Canvas,
46 rows: readonly string[],
47 palette: Record<string, number>,
48 x: number,
49 y: number,
50 scale = 1,
51 flip = false,
52) => {
53 const ox = Math.round(x)
54 const oy = Math.round(y)
55 rows.forEach((line, r) => {
56 for (let i = 0; i < line.length; i++) {
57 const ch = line[flip ? line.length - 1 - i : i]!
58 const color = palette[ch]
59 if (ch === '.' || color === undefined) continue
60 for (let sy = 0; sy < scale; sy++) {
61 for (let sx = 0; sx < scale; sx++) set(c, ox + i * scale + sx, oy + r * scale + sy, color)
62 }
63 }
64 })
65}
66
67export const rect = (c: Canvas, x: number, y: number, w: number, h: number, color: number) => {
68 for (let j = 0; j < h; j++) for (let i = 0; i < w; i++) set(c, x + i, y + j, color)
69}
70
71export const mix = (a: number, b: number, t: number) => {
72 const ch = (shift: number) => Math.round(((a >> shift) & 0xff) * (1 - t) + ((b >> shift) & 0xff) * t)
73 return (ch(16) << 16) | (ch(8) << 8) | ch(0)
74}
75
76const B64 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
77
78export const toBase64 = (bytes: Uint8Array) => {
79 let out = ''
80 for (let i = 0; i < bytes.length; i += 3) {
81 const a = bytes[i] ?? 0
82 const b = bytes[i + 1] ?? 0
83 const c = bytes[i + 2] ?? 0
84 const n = (a << 16) | (b << 8) | c
85 out += B64[(n >> 18) & 63]! + B64[(n >> 12) & 63]!
86 out += i + 1 < bytes.length ? B64[(n >> 6) & 63]! : '='
87 out += i + 2 < bytes.length ? B64[n & 63]! : '='
88 }
89 return out
90}
91
92/** The canvas as RasterProps cells: `w` columns by `h / 2` rows. */
93export const words = (c: Canvas) => {
94 const rows = Math.floor(c.h / 2)
95 const out = new Uint32Array(c.w * rows * 3)
96 for (let r = 0; r < rows; r++) {
97 for (let x = 0; x < c.w; x++) {
98 const top = get(c, x, r * 2)
99 const bottom = get(c, x, r * 2 + 1)
100 const i = (r * c.w + x) * 3
101 const g = c.glyphs.get(r * c.w + x)
102 out[i] = g ? g.ch : HALF
103 out[i + 1] = g ? g.fg : top
104 out[i + 2] = g ? mix(top, bottom, 0.5) : bottom
105 }
106 }
107 return out
108}
109
110export const encode = (c: Canvas) => toBase64(new Uint8Array(words(c).buffer))
111types/index.d.ts 28 lines1/** One test run as read from a runner's output. */
2export type RunResult = { runner: string; failed: number; passed: number }
3
4/** The fight on screen. `phase` drives the animation; times are epoch ms. */
5export type Fight = {
6 /** The boss's name and look, picked when it spawns. */
7 boss: string
8 kind: number
9 maxHp: number
10 hp: number
11 runner: string
12 phase: 'none' | 'spawn' | 'idle' | 'hit' | 'heal' | 'ko'
13 /** When the current phase began. */
14 phaseAt: number
15 /** The last change: damage dealt (positive) or healed (negative). */
16 delta: number
17 runs: number
18 loot: string | null
19 /** A demo fight, not a real one: kills are not counted. */
20 isDemo: boolean
21}
22
23declare module 'claude-code' {
24 interface PluginState {
25 'boss-fight': { fight: Fight; kills: number }
26 }
27}
28