SLOPSHOPPER

dino

The Chrome dinosaur runner in a pane. Press w to jump the cacti; the ground speeds up as you go, and your best distance is kept. Zero tokens.

newpanecommandtoasttimer
v0.1.0MITupdated 2026-10-16MDmubarak786/claude-mods/mods/dino
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · dino
│ ┃ Dino ✕ › fix the failing auth test and add an audit log call │ ┃ DINO score 00000 HI 00000 · 120 ms │ ┃ Press w to jump. The first jump starts the ⏺ Read(src/auth.ts) │ ┃ run. ⎿ Read 6 lines │ ┃ ⏺ Update(src/auth.ts) │ ┃ ┌─────────────────────────────────────────── ⎿ Added 2 lines, removed 1 line │ ┃ │ ⏺ Bash(bun test) │ ┃ │ ⎿ 3 pass, 1 fail │ ┃ │. . . . D . . . . . . . . . . . . . . . . . │ ┃ └─────────────────────────────────────────── ● Done. refresh now rejects expired claims and logs an audit event. │ ┃ │ ┃ w: Jump p: Resume ✻ Worked for 42s · done 4:20 PM │ ┃ │ ┃ ╭──────────────────────────────────────────╮ › /dino │ ┃ │ How to play │ │ ┃ │ 1. Press w to jump. The lane starts │ │ ┃ │ scrolling, and cacti come at you from │ │ ┃ │ the right. │ │ ┃ │ 2. A small cactus is one cell high and a │ │ ┃ │ tall one is two: the tall ones only │ │ ┃ │ clear at the top of a jump, so jump │ │ ┃ │ late. │ │ ┃ │ 3. Every 100 points the ground gets │ │ ┃ │ faster. Hitting a cactus ends the run. p │ │ ┃ │ pauses. │ │ ┃ │ Keys need the pane focused: click it if │ ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts

Draws

Pane · Dino
DINO score 00000 HI 00000 · 120 ms Press w to jump. The first jump starts the run. ┌─────────────────────────────────────────────────────────── │ │ │. . . . D . . . . . . . . . . . . . . . . . . . . . . . . . └─────────────────────────────────────────────────────────── w: Jump p: Resume ╭──────────────────────────────────────────────────────────╮ │ How to play │ │ 1. Press w to jump. The lane starts scrolling, and cacti │ │ come at you from the right. │ │ 2. A small cactus is one cell high and a tall one is │ │ two: the tall ones only clear at the top of a jump, so │ │ jump late. │ │ 3. Every 100 points the ground gets faster. Hitting a │ │ cactus ends the run. p pauses. │ │ Keys need the pane focused: click it if nothing happens. │ │ h hides this. │ ╰──────────────────────────────────────────────────────────╯ n: New game h: Hide help Esc closes and pauses
README

dino

The dinosaur runner in a pane beside the conversation, the one Chrome shows when the internet is down. Press w to jump the cacti. The lane scrolls on a clock while the pane is open and speeds up every 100 points; p pauses, Esc closes and pauses, and your best distance survives restarts.

It's a game you play, not a watcher: it never calls a model, never reads what Claude is doing, and costs no tokens. Chrome gave you a dinosaur for when the connection was down; this one is for when Claude is thinking.

The dinosaur runner in a pane: a jump over a small cactus, a late jump over a tall one, then a crash.

Recorded in a terminal. The Desktop app draws the same pane, with each key shown as a badge.

DINO  score 00142  HI 00388 · 110 ms
A tall cactus ahead: jump at the last moment to clear it.
┌────────────────────────────────────────────────────────────┐
│                                                            │
│        D                                 #                 │
│. . . . . . . . . . . . . . . . . . . . . # . . . . # # . . │
└────────────────────────────────────────────────────────────┘
w: Jump   p: Pause

n: New game   h: Help   Esc closes and pauses

The lane is colored, which the text above can't show: the dinosaur is a D on green that turns red when you crash, the cacti are # on yellow, and the border turns red at the end of a run. Colors follow your theme.

Install

/plugin marketplace add MDmubarak786/claude-mods
/plugin install dino@modhub

Try it for one session without installing:

claude --plugin-dir ./mods/dino

How to play

  1. Run /dino. The pane opens with the dinosaur waiting on the ground. A fresh game shows a short how-to until the first jump; h hides or shows it any time.
  2. Press w, or click Jump. The run starts and the dinosaur leaves the ground at once, rising for two steps, holding, and landing on the fifth. Cacti come at you from the right, one column a step.
  3. A small cactus is one cell high and clears under any part of the jump. A tall one is two cells high and only clears at the top of the arc, so jump when it's one to three columns away. The status line warns you when a tall one is next.
  4. Every step survived is a point. Every 100 points the steps get 10 ms faster, from 120 ms down to 60 ms at 600.
  5. Hitting a cactus ends the run. A toast says the score and whether it's a new best. n starts a new game.
  6. p pauses and resumes. Esc closes the pane and pauses; /dino again picks up where you were, paused, so you choose when it moves.

The keys work while the pane has the focus. If pressing w types into the prompt instead, click the pane first.

CommandWhat it does
/dinoOpen the pane. Where no pane can be drawn, print the score and state.
/dino newStart a new game. The best score is kept.
/dino statsGames played, best score.

What it touches

From claude plugin validate ./mods/dino:

hooks: session.start, classic.SessionStart{source=clear|resume|fork}, command.run{command=dino}, ui.close{id=dino}, ui.render{component=Pane}
calls: $.clock.every (via startTimer), $.clock.now (via load, startNew), $.command.register, $.session.surfaces, $.state.get, $.state.set, $.store.get (via load), $.store.set (via save), $.ui.invalidate, $.ui.log, $.ui.open, $.ui.resolve, $.ui.toast
  • $.store keeps the game and the record. A plain step is drawn but not saved; a jump, a crash, pausing, and closing the pane save, so a restart loses at most a few steps of the run. A loaded game always waits, paused, for a key, with its speed set from its score. If the store can't be read at start, the mod plays in memory and writes nothing over the saved record.
  • $.clock spaces the cacti and scrolls the lane every speed milliseconds while the pane is open and the game runs. When the score crosses a 100-point step the clock is re-armed at the faster interval. It stops on pause, on game over, on a new game, and when the pane closes.
  • ui.close is observed to stop the clock and pause.
  • No tool calls are observed, no files, processes, model calls, or network.

Tested with

  • Claude Code 2.1.301, claude plugin validate --strict and claude plugin test passing: the rules (the waiting start, the jump arc, a press in the air, cacti spawning and walking off, the speed ramp, the clearing windows for small and tall cacti, landing on one, standing still), a player who jumps at the last moment surviving 3,000 steps on each of 20 seeds, the record, the guard, and mounted pane tests on the mock clock for jumping, scrolling, pausing, the faster clock at 100 points, crashing and the toast, the commands, an unreadable store, and a narrow pane. The test kit can't fire ui.close, so pausing on close is covered by the pause key and the load path.
  • Not yet looked at in the Desktop app. The lane is built from the same two-column Boxes as snake, which draws correctly there.

Limitations

  • One game at a time per machine. Two sessions playing at once overwrite each other; the last write wins.
  • Only cacti so far: no birds, and no ducking. The arrow keys and the space bar are not hotkeys a mod can take; w is.
  • The spacing between cacti is at least a jump plus three steps, so every run can go on forever in principle. The clock is what beats you.
  • The lane redraws on every step, 90 cells at a time. At 60 ms on a slow terminal it may stutter.

License

MIT, see the repository root.

Source 3 files
hooks/register.ts 266 lines
1// dino: the dinosaur runner, in a pane. Jump the cacti, go as far as you can.
2//
3//   /dino              open the game
4//   /dino new          start a new game
5//   /dino stats        games, best score
6//
7// In the pane: w jumps (the first press starts the game), p pauses and
8// resumes, n starts a new game, h shows the how-to. The lane scrolls on a
9// clock while the pane is open and speeds up as the score grows; closing the
10// pane pauses it. Nothing here calls a model, reads what Claude is doing, or
11// leaves the machine.
12
13import { atom, read, update } from 'claude-code'
14import type { Game, Stats } from '../types'
15import { COLS, DINO_COL, START_SPEED, heightOf, isGame, jump, newGame, nextObstacle, speedFor, tick } from './game'
16
17const PANE = 'dino'
18const EMPTY_STATS: Stats = { games: 0, best: 0 }
19const EMPTY: Game = { seed: 0, height: 0, jump: -1, obstacles: [], nextIn: 1, spawned: 0, score: 0, paused: true, over: false, record: false, speed: START_SPEED, stats: EMPTY_STATS }
20const game = atom({ plugin: 'dino', key: 'game' } as const, EMPTY)
21
22// The clock that scrolls the lane, while the pane is open and the game runs,
23// and the interval it was armed with: a faster score re-arms it.
24let timer: { cancel: () => void } | null = null
25let timerSpeed = 0
26// The how-to block: shown on a fresh game until the first jump, or until h has
27// set it either way for the session.
28let help: boolean | null = null
29// False after the store could not be read: then nothing is written over the
30// saved record until a later load reads it.
31let storeOk = true
32
33async function save($, g: Game) {
34  if (!storeOk || !g.seed) return
35  try {
36    await $.store.set('game', g)
37  } catch {
38    // The drawing still updates; the store catches up on the next change.
39  }
40}
41
42// Apply a change through update's read-modify-write and mirror it to the
43// store. Ticks pass `persist: false`: a step every 120 ms is drawn, not saved,
44// until something worth keeping happens.
45async function change($, fn: (cur: Game) => Game, persist = true): Promise<Game> {
46  let out: Game | null = null
47  await update($, game, (cur) => {
48    out = fn(cur)
49    return out
50  })
51  if (out && persist) await save($, out)
52  return out ?? (await read($, game))
53}
54
55async function load($) {
56  let saved: unknown = undefined
57  let readable = true
58  try {
59    saved = await $.store.get('game')
60  } catch {
61    readable = false
62  }
63  storeOk = readable
64  if (isGame(saved)) {
65    // A game is never running at load: it waits for a key. The speed always
66    // follows the score.
67    const s = saved as Game
68    await update($, game, () => ({ ...s, paused: true, speed: speedFor(s.score) }))
69    return
70  }
71  if (!readable) {
72    const cur = await read($, game)
73    if (!cur.seed) await update($, game, () => newGame(Date.now(), { ...EMPTY_STATS, games: 1 }))
74    $.ui.log('dino could not read its saved game; playing without saving')
75    return
76  }
77  const fresh = newGame(await $.clock.now(), { ...EMPTY_STATS, games: 1 })
78  await update($, game, () => fresh)
79  await save($, fresh)
80}
81
82function stopTimer() {
83  if (timer) timer.cancel()
84  timer = null
85  timerSpeed = 0
86}
87
88// Scroll the lane every `speed` ms until it pauses or the dinosaur crashes.
89// A crash is saved; plain steps are only drawn. When the score crosses a speed
90// step the clock is re-armed at the new interval.
91function startTimer($, g: Game) {
92  stopTimer()
93  if (g.paused || g.over) return
94  timerSpeed = g.speed
95  timer = $.clock.every(g.speed, async () => {
96    try {
97      let ended = false
98      const after = await change($, (cur) => {
99        const n = tick(cur)
100        ended = n.over && !cur.over
101        return n
102      }, false)
103      if (after.over || after.paused) stopTimer()
104      else if (after.speed !== timerSpeed) startTimer($, after)
105      if (ended || after.paused) await save($, after)
106      if (ended) $.ui.toast('Game over: ' + after.score + (after.record ? ', a new best! ' : '. ') + 'New game: n', { timeoutMs: 5000 })
107    } catch {
108      // A failed tick is skipped; the next one tries again.
109    }
110  })
111}
112
113async function startNew($) {
114  stopTimer()
115  const now = await $.clock.now()
116  return change($, (cur) => newGame(now, { ...cur.stats, games: cur.stats.games + 1 }))
117}
118
119async function pause($, on: boolean) {
120  const g = await change($, (cur) => (cur.over || cur.paused === on ? cur : { ...cur, paused: on }))
121  if (on) stopTimer()
122  else startTimer($, g)
123  return g
124}
125
126const pad = (n: number) => String(n).padStart(5, '0')
127
128function summary(g: Game): string {
129  if (!g.seed) return 'No game yet.'
130  const state = g.over ? 'Game over.' : g.paused ? 'Paused.' : 'Running.'
131  return 'Dino: score ' + g.score + ', best ' + g.stats.best + ', ' + g.speed + ' ms a step. ' + state + ' /dino opens the pane.'
132}
133
134export function register(on) {
135  on('session.start', async ($, e, next) => {
136    help = null
137    try {
138      await load($)
139    } catch (error) {
140      $.ui.log('dino could not load its game: ' + error)
141    }
142    try {
143      await $.command.register({ name: 'dino', description: 'Play the dinosaur runner in a pane', argumentHint: '[new | stats]', immediate: true })
144    } catch (error) {
145      $.ui.log('could not register /dino: ' + error)
146    }
147    return next(e)
148  })
149
150  // /clear and /resume reset $.state; the game lives on in the store.
151  on('classic.SessionStart', { source: ['clear', 'resume', 'fork'] }, async ($, e, next) => {
152    stopTimer()
153    await load($)
154    return next(e)
155  }).catch(async ($, e, next) => next(e))
156
157  on('command.run', { command: 'dino' }, async ($, e) => {
158    const args = e.args.trim().toLowerCase()
159    if (args === 'new') return { text: 'New game. ' + summary(await startNew($)) }
160    if (args === 'stats') {
161      const s = (await read($, game)).stats
162      return { text: 'Games ' + s.games + ' · best ' + s.best }
163    }
164    if (args) return { text: 'Usage: /dino, /dino new, or /dino stats' }
165    // Under claude -p nothing draws, and ui.open still reports the pane as placed:
166    // the surfaces list is the reliable sign.
167    let surfaces = []
168    try {
169      surfaces = await $.session.surfaces()
170    } catch {
171      surfaces = []
172    }
173    if (!surfaces.length) return { text: summary(await read($, game)) }
174    const placed = await $.ui.open({ id: PANE, title: 'Dino', focus: true, closeOnEscape: true, rows: 18 })
175    if (!placed.isPlaced) return { text: summary(await read($, game)) }
176    return {}
177  }).catch(async () => ({ text: 'dino: the command failed.' }))
178
179  // Closing the pane pauses the game.
180  on('ui.close', { id: PANE }, async ($, e, next) => {
181    stopTimer()
182    await change($, (cur) => (cur.seed && !cur.paused ? { ...cur, paused: true } : cur))
183    return next(e)
184  }).catch(async ($, e, next) => next(e))
185
186  on('ui.render', { component: 'Pane' }, async ($, e, next) => {
187    if (e.requestId !== PANE) return next(e)
188    const g = await read($, game)
189    const { Box, Text, Button } = $.ui.resolve(e)
190    if (!g.seed) return Text({ children: ['No game loaded. /dino new starts one.'] })
191    const columns = e.props.bodyColumns || 60
192    const wide = columns >= 50
193    const showHelp = help ?? (wide && g.score === 0 && g.jump < 0)
194    const ahead = nextObstacle(g)
195
196    const status = g.over ? { text: 'Game over at ' + g.score + (g.record ? ', a new best. ' : '. ') + 'Press n for a new game.', color: 'error', bold: true }
197      : g.score === 0 && g.jump < 0 ? { text: 'Press w to jump. The first jump starts the run.', color: 'suggestion' }
198      : g.paused ? { text: 'Paused. Press p, or w, to go on.', color: 'warning' }
199      : ahead && ahead.tall ? { text: 'A tall cactus ahead: jump at the last moment to clear it.', color: 'warning' }
200      : { text: 'Jump the cacti. The ground gets faster as you go.', color: 'suggestion' }
201
202    const header = Box({ flexDirection: 'column', children: [
203      Box({ flexDirection: 'row', columnGap: 2, flexWrap: 'wrap', children: [
204        Text({ bold: true, children: ['DINO'] }),
205        Text({ children: ['score ', Text({ bold: true, children: [pad(g.score)] })] }),
206        Text({ dimColor: true, children: ['HI ' + pad(g.stats.best) + ' · ' + g.speed + ' ms'] }),
207      ] }),
208      Text({ color: status.color, bold: status.bold, wrap: 'wrap', children: [status.text] }),
209    ] })
210
211    // The lane is three rows of two-column Boxes, the ground row at the bottom.
212    // The dinosaur sits at its height in column DINO_COL; a cactus fills its
213    // column from the ground up to its height. Colors carry what is where, and
214    // the glyphs are plain ASCII so every surface lines them up.
215    const cacti = new Map<number, number>()
216    for (const o of g.obstacles) cacti.set(o.col, Math.max(cacti.get(o.col) ?? 0, heightOf(o)))
217    const cell = (y: number, c: number) => {
218      const dino = c === DINO_COL && y === g.height
219      const cactus = (cacti.get(c) ?? 0) > y
220      const kind = dino ? 'dino' : cactus ? 'cactus' : y === 0 ? 'ground' : 'sky'
221      const backgroundColor = kind === 'dino' ? (g.over ? 'error' : 'success') : kind === 'cactus' ? 'warning' : undefined
222      const glyph = kind === 'dino' ? 'D' : kind === 'cactus' ? '#' : kind === 'ground' ? '.' : ' '
223      return Box({ width: 2, backgroundColor, children: [Text({ bold: kind === 'dino', color: backgroundColor ? 'inverseText' : undefined, dimColor: kind === 'ground', children: [glyph] })] })
224    }
225    const lane = Box({ flexDirection: 'column', borderStyle: 'single', borderColor: g.over ? 'error' : 'inactive', children: [2, 1, 0].map((y) => Box({ flexDirection: 'row', children: Array.from({ length: COLS }, (_, c) => cell(y, c)) })) })
226
227    const controls = Box({ flexDirection: 'row', columnGap: 2, flexWrap: 'wrap', children: [
228      Button({ key: 'w', label: 'Jump', hotkey: 'w', plain: true, onPress: async () => {
229        const updated = await change($, jump)
230        if (updated.over) {
231          $.ui.toast('The game is over. Press n for a new one.')
232          return
233        }
234        if (!timer) startTimer($, updated)
235      } }),
236      Button({ key: 'pause', label: g.paused ? 'Resume' : 'Pause', hotkey: 'p', plain: true, onPress: async () => {
237        const cur = await read($, game)
238        if (cur.over) {
239          $.ui.toast('The game is over. Press n for a new one.')
240          return
241        }
242        await pause($, !cur.paused)
243      } }),
244    ] })
245
246    const howTo = Box({ flexDirection: 'column', borderStyle: 'round', borderColor: 'suggestion', paddingX: 1, children: [
247      Text({ bold: true, children: ['How to play'] }),
248      Text({ wrap: 'wrap', children: ['1. Press w to jump. The lane starts scrolling, and cacti come at you from the right.'] }),
249      Text({ wrap: 'wrap', children: ['2. A small cactus is one cell high and a tall one is two: the tall ones only clear at the top of a jump, so jump late.'] }),
250      Text({ wrap: 'wrap', children: ['3. Every 100 points the ground gets faster. Hitting a cactus ends the run. p pauses.'] }),
251      Text({ dimColor: true, wrap: 'wrap', children: ['Keys need the pane focused: click it if nothing happens. h hides this.'] }),
252    ] })
253
254    const footer = Box({ flexDirection: 'row', columnGap: 3, flexWrap: 'wrap', children: [
255      Button({ key: 'new', label: 'New game', hotkey: 'n', plain: true, onPress: () => startNew($) }),
256      Button({ key: 'help', label: showHelp ? 'Hide help' : 'Help', hotkey: 'h', plain: true, dimColor: true, onPress: () => {
257        help = !showHelp
258        $.ui.invalidate('ui.render')
259      } }),
260      Text({ dimColor: true, children: ['Esc closes and pauses'] }),
261    ] })
262
263    return Box({ flexDirection: 'column', rowGap: 1, children: showHelp ? [header, lane, controls, howTo, footer] : [header, lane, controls, footer] })
264  })
265}
266
hooks/game.ts 107 lines
1// The rules of the dinosaur runner, pure: no `$` here, so the tests and the
2// hooks module share one implementation.
3
4import type { Game, Obstacle, Stats } from '../types'
5
6export const COLS = 30
7export const DINO_COL = 4
8/** The dinosaur's height on each tick of a jump. */
9export const ARC = [1, 2, 2, 2, 1]
10export const START_SPEED = 120
11export const MIN_SPEED = 60
12/** Every RAMP points the ticks get SPEED_STEP ms faster, down to MIN_SPEED. */
13export const RAMP = 100
14export const SPEED_STEP = 10
15/** Ticks between cacti: never fewer than a jump plus a breath, so every one can be cleared. */
16export const MIN_GAP = ARC.length + 3
17export const MAX_GAP = 14
18
19function hash(s: string): number {
20  let h = 2166136261
21  for (let i = 0; i < s.length; i++) {
22    h ^= s.charCodeAt(i)
23    h = Math.imul(h, 16777619)
24  }
25  return h >>> 0
26}
27
28function rng(seed: number): () => number {
29  let a = seed
30  return () => {
31    a = (a + 0x6d2b79f5) | 0
32    let t = Math.imul(a ^ (a >>> 15), 1 | a)
33    t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t
34    return ((t ^ (t >>> 14)) >>> 0) / 4294967296
35  }
36}
37
38// The spacing and shape of cactus n, deterministic from the seed.
39export function gapAfter(seed: number, n: number): number {
40  const r = rng(hash(seed + ':gap:' + n))
41  return MIN_GAP + Math.floor(r() * (MAX_GAP - MIN_GAP + 1))
42}
43
44export function isTall(seed: number, n: number): boolean {
45  return rng(hash(seed + ':tall:' + n))() < 0.4
46}
47
48export const heightOf = (o: Obstacle) => (o.tall ? 2 : 1)
49
50export function speedFor(score: number): number {
51  return Math.max(MIN_SPEED, START_SPEED - Math.floor(score / RAMP) * SPEED_STEP)
52}
53
54export function newGame(seed: number, stats: Stats): Game {
55  return { seed, height: 0, jump: -1, obstacles: [], nextIn: gapAfter(seed, 0), spawned: 0, score: 0, paused: true, over: false, record: false, speed: START_SPEED, stats }
56}
57
58// Jump. The dinosaur leaves the ground at once and lands after the arc; a
59// press in the air does nothing but resume a paused game. The first jump
60// starts the game.
61export function jump(g: Game): Game {
62  if (g.over) return g
63  if (g.jump >= 0) return g.paused ? { ...g, paused: false } : g
64  return { ...g, jump: 0, height: ARC[0], paused: false }
65}
66
67// One tick: the dinosaur moves along its arc, every cactus walks one column
68// left, a new one appears when its turn comes, and a cactus taller than the
69// dinosaur is high at the dinosaur's column ends the game. Surviving scores a
70// point and may speed the clock up.
71export function tick(g: Game): Game {
72  if (g.over || g.paused) return g
73  const jumpNext = g.jump >= 0 ? g.jump + 1 : -1
74  const jumpIdx = jumpNext >= ARC.length ? -1 : jumpNext
75  const height = jumpIdx >= 0 ? ARC[jumpIdx] : 0
76  let obstacles = g.obstacles.map((o) => ({ ...o, col: o.col - 1 })).filter((o) => o.col >= 0)
77  let nextIn = g.nextIn - 1
78  let spawned = g.spawned
79  if (nextIn <= 0) {
80    obstacles = [...obstacles, { col: COLS - 1, tall: isTall(g.seed, spawned) }]
81    spawned++
82    nextIn = gapAfter(g.seed, spawned)
83  }
84  const hit = obstacles.some((o) => o.col === DINO_COL && heightOf(o) > height)
85  if (hit) return { ...g, jump: jumpIdx, height, obstacles, nextIn, spawned, over: true, paused: true }
86  const score = g.score + 1
87  const record = g.record || score > g.stats.best
88  const stats: Stats = { ...g.stats, best: Math.max(g.stats.best, score) }
89  return { ...g, jump: jumpIdx, height, obstacles, nextIn, spawned, score, record, stats, speed: speedFor(score) }
90}
91
92// The nearest cactus still ahead of the dinosaur, for the status line.
93export function nextObstacle(g: Game): Obstacle | undefined {
94  return g.obstacles.filter((o) => o.col >= DINO_COL).sort((a, b) => a.col - b.col)[0]
95}
96
97export function isGame(x: unknown): x is Game {
98  if (!x || typeof x !== 'object') return false
99  const g = x as Record<string, unknown>
100  const stats = g.stats as Record<string, unknown> | undefined
101  const obstacle = (o: unknown) => !!o && typeof o === 'object' && Number.isInteger((o as Obstacle).col) && (o as Obstacle).col >= 0 && (o as Obstacle).col < COLS && typeof (o as Obstacle).tall === 'boolean'
102  return typeof g.seed === 'number' && Number.isInteger(g.height) && (g.height as number) >= 0 && (g.height as number) <= 2 && Number.isInteger(g.jump) && (g.jump as number) >= -1 && (g.jump as number) < ARC.length &&
103    Array.isArray(g.obstacles) && g.obstacles.every(obstacle) && Number.isInteger(g.nextIn) && Number.isInteger(g.spawned) && Number.isInteger(g.score) &&
104    typeof g.paused === 'boolean' && typeof g.over === 'boolean' && typeof g.record === 'boolean' && Number.isInteger(g.speed) &&
105    !!stats && typeof stats === 'object' && ['games', 'best'].every((k) => Number.isInteger(stats[k]))
106}
107
types/index.d.ts 37 lines
1export type Obstacle = {
2  /** Column on the lane, 0 at the left; the cactus walks left one column a tick. */
3  col: number
4  /** Two cells high: only the top of a jump clears it. */
5  tall: boolean
6}
7export type Stats = { games: number; best: number }
8export type Game = {
9  /** Seed of this game's cactus spacing; 0 until a game exists. */
10  seed: number
11  /** The dinosaur's height above the ground, 0 to 2. */
12  height: number
13  /** Which tick of the jump arc the dinosaur is on; -1 on the ground. */
14  jump: number
15  obstacles: Obstacle[]
16  /** Ticks until the next cactus appears at the right edge. */
17  nextIn: number
18  /** Cacti spawned so far; also the spacing counter. */
19  spawned: number
20  /** Ticks survived. */
21  score: number
22  /** Not moving: a fresh game, or paused with p or by closing the pane. */
23  paused: boolean
24  over: boolean
25  /** This game has beaten the best score it started with. */
26  record: boolean
27  /** Milliseconds between ticks; shrinks as the score grows. */
28  speed: number
29  stats: Stats
30}
31
32declare module 'claude-code' {
33  interface PluginState {
34    dino: { game: Game }
35  }
36}
37