SLOPSHOPPER

Boss Fight

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

newpaneguardcommandtoaststatus
★ 2v0.1.0MITupdated 2026-10-02OneWave-AI/claude-code-mods/boss-fight
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · boss-fight
│ ┃ Boss Fight ✕ › fix the failing auth╭────────────────────────────────────────────╮ │ ┃ ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ │ boss-fight │ │ ┃ ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀A▀BOSS▀APPEARS▀▀▀▀▀▀▀▀▀ ⏺ Read(src/auth.ts) │ A boss appears: Merge Conflict Ogre (1 │ │ ┃ ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ ⎿ Read 6 lines │ HP). /boss to fight. │ │ ┃ ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ ⏺ Update(src/auth.ts) ╰────────────────────────────────────────────╯ │ ┃ ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ ⎿ Added 2 lines, removed 1 line │ ┃ ⏺ Bash(bun test) │ ┃ ⎿ 3 pass, 1 fail │ ┃ │ ┃ ● Done. refresh now rejects expired claims and logs an audit event. │ ┃ │ ┃ ✻ Worked for 42s · done 4:20 PM │ ┃ │ ┃ › /boss │ ┃ ⎿ boss-fight: Boss pane opened. │ ┃ │ ┃ MERGE CONFLICT OGRE bun · run 1 │ ┃ ██████████████████████████████ 1/1 HP │ ┃ │ ┃ d: demo fight r: reset bosses slain 0 │ ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts ⚠ boss-fight: Boss: Merge Conflict Ogre 1/1 HP

Draws

Pane · Boss Fight
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀A▀BOSS▀APPEARS▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ MERGE CONFLICT OGRE bun · run 1 ██████████████████████████████ 1/1 HP d: demo fight r: reset bosses slain 0
README

boss-fight

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

boss-fight

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.

Install

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

What it reaches

ModNetworkRuns processesFilesCalls a modelSends data anywhere
boss-fightNoNoNoNoNo

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.

Source 5 files
hooks/register.tsx 219 lines
1import { 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}
219
hooks/arena.ts 188 lines
1/** 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}
188
hooks/fight.ts 204 lines
1/** 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}
204
hooks/pixels.ts 111 lines
1/** 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))
111
types/index.d.ts 28 lines
1/** 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