SLOPSHOPPER

GAME MODE · Hint

The hint line under the prompt adds what to press now: ESC 후퇴 while working, /compact 휴식 at low HP, 오류부터 읽기 after repeated failures, /save 세이브 when a save is…

newspinnerguard
★ 2v0.2.0MITupdated 2026-10-06Reasonofmoon/bitgame-mods/plugins/game-hint
A shopper browsing a rack in a slop shop
README

GAME MODE · Hint

The dim line under the prompt adds what to press now, after the engine's own words:

WhenAdds
Claude is working▸ ESC 후퇴
context left at lowHp or less▸ /compact 휴식
two or more failed calls in a row▸ 오류부터 읽기
saveEvery turns without a save (with game-save-point)▸ /save 세이브

Two at most, most urgent first. Other plugins' additions to the line stay before these.

Part of the GAME MODE pack. Requires Claude Code 2.1.287+; built and tested on 2.1.291.

Install

claude plugin marketplace add Reasonofmoon/bitgame-mods
claude plugin install game-hint@bitgame-mods

Settings

intensity (casual)off · casual · hardcore (counts the failures)
lowHp (25)percent of context left that suggests a rest
saveEvery (5)turns without a save that suggest /save

한국어

프롬프트 아래 힌트 줄에 지금 누를 것을 덧붙입니다: 작업 중 ESC 후퇴, 컨텍스트가 적으면 /compact 휴식, 연속 실패면 오류부터 읽기, 한동안 저장하지 않았으면 /save 세이브.

Source 3 files
hooks/register.tsx 89 lines
1import { atom, read, update } from 'claude-code'
2import type { Register } from 'claude-code'
3
4import type { HintCues } from '../types'
5import { intensityOf } from './palette'
6
7// GAME MODE · HINT
8//
9// The dim line under the prompt adds what to press now, after the engine's own words:
10//   while Claude works         ▸ ESC 후퇴
11//   context running low        ▸ /compact 휴식
12//   the same kind of failure   ▸ 오류부터 읽기   (two or more failed calls in a row)
13//   several turns, no save     ▸ /save 세이브    (with game-save-point)
14// Other plugins' additions to the line are kept, before these. The engine sets the tail off
15// from its own words with ` · `.
16
17const START: HintCues = { hpLeft: null, failStreak: 0, turnsSinceSave: 0, canSave: false }
18const cues = atom({ plugin: 'game-hint', key: 'cues' } as const, START)
19
20export const register: Register = (on, options) => {
21  const intensity = intensityOf(options.intensity)
22  const lowHp = Math.max(5, Math.min(60, Number(options.lowHp ?? 25)))
23  const saveEvery = Math.max(1, Number(options.saveEvery ?? 5))
24
25  on('session.start', async ($, e, next) => {
26    const result = await next(e)
27    const commands = await $.command.list().catch(() => [])
28    const canSave = commands.some(c => c.name === 'save' && c.plugin === 'game-save-point')
29    await update($, cues, c => ({ ...c, canSave }))
30    return result
31  })
32
33  on('session.measure', async ($, e, next) => {
34    const used = e.context.percent
35    if (used !== undefined && used !== null) await update($, cues, c => ({ ...c, hpLeft: Math.max(0, 100 - used) }))
36    return next(e)
37  }).catch(($, e, next) => next(e))
38
39  on('tool.call', async ($, e, next) => {
40    const ran = await next(e)
41    if (e.agentId === undefined) {
42      const failed = ran.deny !== undefined || ran.isError === true
43      await update($, cues, c => ({ ...c, failStreak: failed ? c.failStreak + 1 : 0 }))
44    }
45    return ran
46  }).catch(($, e, next) => next(e))
47
48  on('turn.complete', async ($, e, next) => {
49    if (e.agentId === undefined) await update($, cues, c => ({ ...c, turnsSinceSave: c.turnsSinceSave + 1 }))
50    return next(e)
51  }).catch(($, e, next) => next(e))
52
53  // A save point row resets the count, whoever wrote it.
54  on('session.append', async ($, e, next) => {
55    const stored = await next(e)
56    if (e.door === 'command' && isSaveRow(e.message.content)) await update($, cues, c => ({ ...c, turnsSinceSave: 0 })).catch(() => undefined)
57    return stored
58  }).catch(($, e, next) => next(e))
59
60  on('ui.render', { component: 'PromptHint' }, async ($, e, next) => {
61    if (intensity === 'off') return next(e)
62    const tips = hintsOf(await read($, cues), e.props, lowHp, saveEvery, intensity === 'hardcore')
63    if (tips.length === 0) return next(e)
64    const mine = tips.map(t => `▸ ${t}`).join('  ')
65    const tail = e.props.tail === undefined || e.props.tail === '' ? mine : `${e.props.tail} · ${mine}`
66    return next({ ...e, props: { ...e.props, tail } })
67  })
68}
69
70/** What to press now, most urgent first; two at most. */
71export function hintsOf(c: HintCues, line: { isDraft: boolean; isWorking: boolean }, lowHp: number, saveEvery: number, isHardcore: boolean): string[] {
72  const tips: string[] = []
73  if (line.isWorking) tips.push('ESC 후퇴')
74  if (c.failStreak >= 2) tips.push(isHardcore ? `오류부터 읽기 (연속 실패 ${c.failStreak})` : '오류부터 읽기')
75  if (!line.isWorking) {
76    if (c.hpLeft !== null && c.hpLeft <= lowHp) tips.push('/compact 휴식')
77    else if (!line.isDraft && c.canSave && c.turnsSinceSave >= saveEvery) tips.push('/save 세이브')
78  }
79  return tips.slice(0, 2)
80}
81
82function isSaveRow(content: readonly unknown[]): boolean {
83  return content.some(block => {
84    if (block === null || typeof block !== 'object') return false
85    const b = block as { type?: unknown; text?: unknown }
86    return b.type === 'text' && typeof b.text === 'string' && b.text.includes('◆ SAVE POINT · ') && b.text.includes('[PASSWORD]')
87  })
88}
89
hooks/palette.ts 124 lines
1// GAME MODE shared look (copied into each GAME MODE plugin: a hooks module
2// may only import files of its own plugin). Change every copy together;
3// scripts/check.sh fails when the copies differ.
4//
5// nes / gameboy / amber are raw colors made for dark terminals, taken from
6// the design boards. `theme` uses Claude Code's theme keys, so it follows the
7// person's theme: pick it on a light background. Its `win` is empty: windows
8// draw with no fill there.
9
10export type PaletteName = 'nes' | 'gameboy' | 'amber' | 'theme'
11
12export type Palette = {
13  /** Headings, gold, the level. */
14  title: string
15  ok: string
16  warn: string
17  bad: string
18  info: string
19  dim: string
20  text: string
21  /** A window's border. */
22  frame: string
23  /** A window's fill; '' for none. */
24  win: string
25  /** The bright inner edge of a window, and a highlighted badge's text. */
26  edge: string
27  /** A bar's empty cells. */
28  off: string
29}
30
31const PALETTES: Record<PaletteName, Palette> = {
32  nes: {
33    title: '#f8d830',
34    ok: '#58d854',
35    warn: '#ff8a6a',
36    bad: '#f83800',
37    info: '#3cbcfc',
38    dim: '#b8b8f0',
39    text: '#fcfcfc',
40    frame: '#5060d8',
41    win: '#1c2a8a',
42    edge: '#fcfcfc',
43    off: '#2c3a98',
44  },
45  gameboy: {
46    title: '#e0f8d0',
47    ok: '#9bbc0f',
48    warn: '#e0f8d0',
49    bad: '#f8f8a0',
50    info: '#c4e890',
51    dim: '#8bac0f',
52    text: '#e0f8d0',
53    frame: '#9bbc0f',
54    win: '#1e4a1e',
55    edge: '#e0f8d0',
56    off: '#306230',
57  },
58  amber: {
59    title: '#ffd060',
60    ok: '#ffb000',
61    warn: '#ff7040',
62    bad: '#ff5a2a',
63    info: '#ffe0a0',
64    dim: '#c89040',
65    text: '#ffd9a0',
66    frame: '#ffb000',
67    win: '#2a1700',
68    edge: '#ffe0a0',
69    off: '#4a2a00',
70  },
71  theme: {
72    title: 'claude',
73    ok: 'success',
74    warn: 'warning',
75    bad: 'error',
76    info: 'suggestion',
77    dim: 'subtle',
78    text: 'text',
79    frame: 'promptBorder',
80    win: '',
81    edge: 'text',
82    off: 'subtle',
83  },
84}
85
86export function paletteOf(name: unknown): Palette {
87  return name === 'gameboy' || name === 'amber' || name === 'theme' ? PALETTES[name] : PALETTES.nes
88}
89
90export type Intensity = 'off' | 'casual' | 'hardcore'
91
92export function intensityOf(value: unknown): Intensity {
93  return value === 'off' || value === 'hardcore' ? value : 'casual'
94}
95
96/** A bar of `cells` blocks, `percent` of them full. */
97export function bar(percent: number, cells: number): string {
98  const full = Math.max(0, Math.min(cells, Math.round((percent / 100) * cells)))
99  return '█'.repeat(full) + '░'.repeat(cells - full)
100}
101
102/** Box props for a window in this palette: a double border and, where the palette has one, a fill. */
103export function windowProps(pal: Palette): { borderStyle: 'double'; borderColor: string; backgroundColor?: string; paddingX: number } {
104  return pal.win === ''
105    ? { borderStyle: 'double', borderColor: pal.frame, paddingX: 1 }
106    : { borderStyle: 'double', borderColor: pal.frame, backgroundColor: pal.win, paddingX: 1 }
107}
108
109/** `m:ss`, or `h:mm:ss` past an hour. */
110export function clock(ms: number): string {
111  const s = Math.max(0, Math.round(ms / 1000))
112  const h = Math.floor(s / 3600)
113  const m = Math.floor((s % 3600) / 60)
114  const sec = s % 60
115  const two = (n: number) => String(n).padStart(2, '0')
116  return h > 0 ? `${h}:${two(m)}:${two(sec)}` : `${m}:${two(sec)}`
117}
118
119/** A cost in the chosen currency: `$1.23`, or `₩1,722` at `krwPerUsd`. */
120export function money(usd: number, currency: 'usd' | 'krw', krwPerUsd: number): string {
121  if (currency === 'krw') return '₩' + String(Math.round(usd * krwPerUsd)).replace(/\B(?=(\d{3})+(?!\d))/g, ',')
122  return '$' + usd.toFixed(2)
123}
124
types/index.d.ts 18 lines
1/** What the hint line reads to decide what to suggest. */
2export type HintCues = {
3  /** Context left, in percent; null before the first reading. */
4  hpLeft: number | null
5  /** Failed calls of the main loop in a row. */
6  failStreak: number
7  /** Turns since the last /save (or since the session started). */
8  turnsSinceSave: number
9  /** game-save-point is installed: /save exists. */
10  canSave: boolean
11}
12
13declare module 'claude-code' {
14  interface PluginState {
15    'game-hint': { cues: HintCues }
16  }
17}
18