boomwait 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ import('../dist/cli.js')
package/dist/cli.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ export type { CliOpts } from './cliArgs.js';
2
+ export { parseArgs } from './cliArgs.js';
package/dist/cli.js ADDED
@@ -0,0 +1,6 @@
1
+ import { parseArgs } from './cliArgs.js';
2
+ export { parseArgs } from './cliArgs.js';
3
+ const opts = parseArgs(process.argv.slice(2));
4
+ const { main } = await import('./main.js');
5
+ await main(opts);
6
+ //# sourceMappingURL=cli.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,cAAc,CAAA;AAOxC,OAAO,EAAE,SAAS,EAAE,MAAM,cAAc,CAAA;AAExC,MAAM,IAAI,GAAG,SAAS,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAA;AAC7C,MAAM,EAAE,IAAI,EAAE,GAAG,MAAM,MAAM,CAAC,WAAW,CAAC,CAAA;AAC1C,MAAM,IAAI,CAAC,IAAI,CAAC,CAAA"}
@@ -0,0 +1,9 @@
1
+ import type { Difficulty } from 'boomwait-core';
2
+ export declare const DEFAULT_SERVER = "https://fragwait-server.agthe7.workers.dev";
3
+ export interface CliOpts {
4
+ offline: boolean;
5
+ name?: string;
6
+ server: string;
7
+ difficulty: Difficulty;
8
+ }
9
+ export declare function parseArgs(argv: string[]): CliOpts;
@@ -0,0 +1,37 @@
1
+ const DIFFICULTIES = ['easy', 'normal', 'hard'];
2
+ // Bomber routes live on the same Cloudflare Worker host as fragwait/checkwait's
3
+ // — one deployment serves all three games' Durable Objects (Task 11 stands up
4
+ // the actual routes; this default just points there ahead of time).
5
+ export const DEFAULT_SERVER = 'https://fragwait-server.agthe7.workers.dev';
6
+ export function parseArgs(argv) {
7
+ // Default difficulty is 'easy', mirroring fragwait/checkwait's cliArgs
8
+ // (72058a7): feel-gating found normal's 0.3-0.4 skills near-hard in
9
+ // practice. Harder bots are an explicit opt-in via --difficulty.
10
+ const opts = { offline: false, difficulty: 'easy', server: DEFAULT_SERVER };
11
+ for (let i = 0; i < argv.length; i++) {
12
+ const a = argv[i];
13
+ if (a === '--offline')
14
+ opts.offline = true;
15
+ else if (a === '--name') {
16
+ const v = argv[++i];
17
+ if (v === undefined)
18
+ throw new Error('invalid --name: expected a value');
19
+ opts.name = v;
20
+ }
21
+ else if (a === '--server') {
22
+ const v = argv[++i];
23
+ if (v === undefined)
24
+ throw new Error('invalid --server: expected a URL');
25
+ opts.server = v;
26
+ }
27
+ else if (a === '--difficulty') {
28
+ const v = argv[++i];
29
+ if (!DIFFICULTIES.includes(v)) {
30
+ throw new Error(`invalid --difficulty: ${v} (expected one of ${DIFFICULTIES.join('|')})`);
31
+ }
32
+ opts.difficulty = v;
33
+ }
34
+ }
35
+ return opts;
36
+ }
37
+ //# sourceMappingURL=cliArgs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cliArgs.js","sourceRoot":"","sources":["../src/cliArgs.ts"],"names":[],"mappings":"AAEA,MAAM,YAAY,GAA0B,CAAC,MAAM,EAAE,QAAQ,EAAE,MAAM,CAAC,CAAA;AAEtE,gFAAgF;AAChF,8EAA8E;AAC9E,oEAAoE;AACpE,MAAM,CAAC,MAAM,cAAc,GAAG,4CAA4C,CAAA;AAS1E,MAAM,UAAU,SAAS,CAAC,IAAc;IACtC,uEAAuE;IACvE,oEAAoE;IACpE,iEAAiE;IACjE,MAAM,IAAI,GAAY,EAAE,OAAO,EAAE,KAAK,EAAE,UAAU,EAAE,MAAM,EAAE,MAAM,EAAE,cAAc,EAAE,CAAA;IACpF,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,IAAI,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QACrC,MAAM,CAAC,GAAG,IAAI,CAAC,CAAC,CAAC,CAAA;QACjB,IAAI,CAAC,KAAK,WAAW;YAAE,IAAI,CAAC,OAAO,GAAG,IAAI,CAAA;aACrC,IAAI,CAAC,KAAK,QAAQ,EAAE,CAAC;YACxB,MAAM,CAAC,GAAG,IAAI,CAAC,EAAE,CAAC,CAAC,CAAA;YACnB,IAAI,CAAC,KAAK,SAAS;gBAAE,MAAM,IAAI,KAAK,CAAC,kCAAkC,CAAC,CAAA;YACxE,IAAI,CAAC,IAAI,GAAG,CAAC,CAAA;QACf,CAAC;aAAM,IAAI,CAAC,KAAK,UAAU,EAAE,CAAC;YAC5B,MAAM,CAAC,GAAG,IAAI,CAAC,EAAE,CAAC,CAAC,CAAA;YACnB,IAAI,CAAC,KAAK,SAAS;gBAAE,MAAM,IAAI,KAAK,CAAC,kCAAkC,CAAC,CAAA;YACxE,IAAI,CAAC,MAAM,GAAG,CAAC,CAAA;QACjB,CAAC;aAAM,IAAI,CAAC,KAAK,cAAc,EAAE,CAAC;YAChC,MAAM,CAAC,GAAG,IAAI,CAAC,EAAE,CAAC,CAAC,CAAA;YACnB,IAAI,CAAC,YAAY,CAAC,QAAQ,CAAC,CAAe,CAAC,EAAE,CAAC;gBAC5C,MAAM,IAAI,KAAK,CAAC,yBAAyB,CAAC,qBAAqB,YAAY,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,CAAA;YAC3F,CAAC;YACD,IAAI,CAAC,UAAU,GAAG,CAAe,CAAA;QACnC,CAAC;IACH,CAAC;IACD,OAAO,IAAI,CAAA;AACb,CAAC"}
package/dist/game.d.ts ADDED
@@ -0,0 +1,31 @@
1
+ import type { Input, Result } from 'boomwait-core';
2
+ import { KeyParser, TerminalSession } from 'termwait';
3
+ import type { ClaudeListener, ColorMode } from 'termwait';
4
+ import { type Layout } from './render.js';
5
+ export declare const REDRAW_MS: number;
6
+ export interface GameSession {
7
+ term: TerminalSession;
8
+ parser: KeyParser;
9
+ colorMode: ColorMode;
10
+ listener: ClaudeListener;
11
+ layout(): Layout;
12
+ drainInput(): Input;
13
+ statusLine(): string;
14
+ quitRequested(): boolean;
15
+ onResize(cb: () => void): void;
16
+ dispose(): void;
17
+ }
18
+ export declare function setupGame(): Promise<GameSession>;
19
+ export declare function resultLine(result: Result, you: number): string;
20
+ export declare function teardownAndExit(o: {
21
+ term: TerminalSession;
22
+ parser: KeyParser;
23
+ listener: {
24
+ close(): Promise<void>;
25
+ };
26
+ finale: {
27
+ screen: string;
28
+ shareText: string;
29
+ } | null;
30
+ beforeRestore?: () => void;
31
+ }): Promise<never>;
package/dist/game.js ADDED
@@ -0,0 +1,106 @@
1
+ import { TICK_RATE } from 'boomwait-core';
2
+ import { detectColorMode, KeyParser, QuitConfirm, startClaudeListener, TerminalSession, waitForPress, } from 'termwait';
3
+ import { chooseLayout } from './render.js';
4
+ import { createLatch, drain, onKey } from './input-latch.js';
5
+ // The sim steps at TICK_RATE (20Hz); the client loop runs in lockstep with
6
+ // it (real wall-clock interval — the no-Date.now rule binds core, not this
7
+ // client I/O shell, same as fragwait/checkwait's loops).
8
+ export const REDRAW_MS = 1000 / TICK_RATE;
9
+ export async function setupGame() {
10
+ const term = new TerminalSession(process.stdin, process.stdout);
11
+ const parser = new KeyParser();
12
+ const quitConfirm = new QuitConfirm(() => performance.now());
13
+ const colorMode = detectColorMode(process.env);
14
+ let latch = createLatch();
15
+ let banner = null;
16
+ let quit = false;
17
+ let cols = process.stdout.columns ?? 80;
18
+ let rows = process.stdout.rows ?? 24;
19
+ let resizeCb = null;
20
+ const listener = await startClaudeListener();
21
+ listener.onEvent((event) => {
22
+ banner = event === 'done' ? '✔ Claude is done' : '⚠ Claude needs your input';
23
+ });
24
+ const onData = (chunk) => {
25
+ for (const e of parser.feed(chunk)) {
26
+ if ('type' in e)
27
+ continue; // bomber has no mouse input
28
+ // Quit-intent keys only act on a genuine press — never a repeat/release —
29
+ // so holding q can't fast-forward past the confirm window (FEEL-12: a
30
+ // single keystroke, held or tapped, must never instantly quit).
31
+ if (e.kind === 'press') {
32
+ const lower = e.key.toLowerCase();
33
+ if (lower === 'ctrl-c') {
34
+ quit = true;
35
+ continue;
36
+ }
37
+ if (lower === 'q' || lower === 'esc') {
38
+ quit = quitConfirm.request();
39
+ continue;
40
+ }
41
+ }
42
+ latch = onKey(latch, e);
43
+ }
44
+ };
45
+ process.stdin.on('data', onData);
46
+ const onResize = () => {
47
+ cols = process.stdout.columns ?? 80;
48
+ rows = process.stdout.rows ?? 24;
49
+ // Full clear before the next frame: redraw alone only overwrites the new
50
+ // frame's footprint, leaving old-frame residue when the terminal grew or
51
+ // the layout's r/sideHud/glyph choice changed shape (chess-4 lesson).
52
+ term.write('\x1b[2J');
53
+ resizeCb?.();
54
+ };
55
+ process.stdout.on('resize', onResize);
56
+ return {
57
+ term,
58
+ parser,
59
+ colorMode,
60
+ listener,
61
+ layout: () => chooseLayout(cols, rows, colorMode),
62
+ drainInput: () => {
63
+ const { input, next } = drain(latch);
64
+ latch = next;
65
+ return input;
66
+ },
67
+ statusLine: () => (quitConfirm.armed ? 'press again to quit' : banner ?? ''),
68
+ quitRequested: () => quit,
69
+ onResize: (cb) => {
70
+ resizeCb = cb;
71
+ },
72
+ dispose: () => {
73
+ process.stdin.off('data', onData);
74
+ process.stdout.off('resize', onResize);
75
+ },
76
+ };
77
+ }
78
+ // Exported for online.ts (Task 11) — same win/loss/draw phrasing offline and
79
+ // online.
80
+ export function resultLine(result, you) {
81
+ if (result.kind === 'draw')
82
+ return 'draw — no one survived';
83
+ return result.winner === you ? 'you won!' : 'you lost';
84
+ }
85
+ // Shared tail for both loops: finished screen (if the match ended, not on a
86
+ // mid-match quit) → waitForPress → Claude-listener close → caller cleanup →
87
+ // terminal restore → share card on the NORMAL screen (post-restore, so it
88
+ // lands in scrollback ready to copy into a post) → exit. Never actually
89
+ // returns (process.exit on every path) — callers declared to return
90
+ // Promise<Result> can `return await teardownAndExit(...)` since `never` is
91
+ // assignable to any type.
92
+ export async function teardownAndExit(o) {
93
+ if (o.finale) {
94
+ o.term.write(o.finale.screen);
95
+ // Only a real key press dismisses — never mouse motion, focus changes, or
96
+ // the release of a key held when the match ended (M1 lesson).
97
+ await waitForPress(process.stdin, o.parser);
98
+ }
99
+ await o.listener.close();
100
+ o.beforeRestore?.();
101
+ o.term.restore();
102
+ if (o.finale)
103
+ process.stdout.write('\n' + o.finale.shareText);
104
+ process.exit(0);
105
+ }
106
+ //# sourceMappingURL=game.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"game.js","sourceRoot":"","sources":["../src/game.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,12 @@
1
+ import type { Dir, Input } from 'boomwait-core';
2
+ import type { KeyEvent } from 'termwait';
3
+ export interface LatchState {
4
+ dir: Dir | null;
5
+ bombQueued: boolean;
6
+ }
7
+ export declare function createLatch(): LatchState;
8
+ export declare function onKey(l: LatchState, e: KeyEvent): LatchState;
9
+ export declare function drain(l: LatchState): {
10
+ input: Input;
11
+ next: LatchState;
12
+ };
@@ -0,0 +1,49 @@
1
+ export function createLatch() {
2
+ return { dir: null, bombQueued: false };
3
+ }
4
+ const DIR_KEYS = {
5
+ w: 'up',
6
+ up: 'up',
7
+ s: 'down',
8
+ down: 'down',
9
+ a: 'left',
10
+ left: 'left',
11
+ d: 'right',
12
+ right: 'right',
13
+ };
14
+ const OPPOSITE = { up: 'down', down: 'up', left: 'right', right: 'left' };
15
+ // press/repeat of a dir key: same dir → no-op (the auto-repeat safety net);
16
+ // perpendicular → switch straight over; opposing → stop-first (tap again
17
+ // while stopped to reverse). space is a one-shot bomb queue: only a real
18
+ // press queues one — a repeat (only distinguishable at all on kitty-capable
19
+ // terminals) must never re-queue, or holding space would spam bombs every
20
+ // tick the OS repeats it. Release events never touch the latch — that's the
21
+ // whole point: the latch IS the persisted state, so a key release (which
22
+ // legacy terminals often can't even report) has nothing to do.
23
+ export function onKey(l, e) {
24
+ if (e.kind === 'release')
25
+ return l;
26
+ const dir = DIR_KEYS[e.key.toLowerCase()];
27
+ if (dir) {
28
+ if (l.dir === dir)
29
+ return l;
30
+ if (l.dir !== null && OPPOSITE[l.dir] === dir)
31
+ return { ...l, dir: null };
32
+ return { ...l, dir };
33
+ }
34
+ if (e.key === ' ') {
35
+ if (e.kind === 'repeat')
36
+ return l;
37
+ return { ...l, bombQueued: true };
38
+ }
39
+ return l;
40
+ }
41
+ // bombQueued is one-shot: drain hands it to this tick's Input and clears it,
42
+ // while dir simply carries forward (holding a direction needs no new events).
43
+ export function drain(l) {
44
+ return {
45
+ input: { dir: l.dir, bomb: l.bombQueued },
46
+ next: { dir: l.dir, bombQueued: false },
47
+ };
48
+ }
49
+ //# sourceMappingURL=input-latch.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"input-latch.js","sourceRoot":"","sources":["../src/input-latch.ts"],"names":[],"mappings":"AAkBA,MAAM,UAAU,WAAW;IACzB,OAAO,EAAE,GAAG,EAAE,IAAI,EAAE,UAAU,EAAE,KAAK,EAAE,CAAA;AACzC,CAAC;AAED,MAAM,QAAQ,GAAwB;IACpC,CAAC,EAAE,IAAI;IACP,EAAE,EAAE,IAAI;IACR,CAAC,EAAE,MAAM;IACT,IAAI,EAAE,MAAM;IACZ,CAAC,EAAE,MAAM;IACT,IAAI,EAAE,MAAM;IACZ,CAAC,EAAE,OAAO;IACV,KAAK,EAAE,OAAO;CACf,CAAA;AAED,MAAM,QAAQ,GAAqB,EAAE,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,CAAA;AAE3F,4EAA4E;AAC5E,yEAAyE;AACzE,yEAAyE;AACzE,4EAA4E;AAC5E,0EAA0E;AAC1E,4EAA4E;AAC5E,yEAAyE;AACzE,+DAA+D;AAC/D,MAAM,UAAU,KAAK,CAAC,CAAa,EAAE,CAAW;IAC9C,IAAI,CAAC,CAAC,IAAI,KAAK,SAAS;QAAE,OAAO,CAAC,CAAA;IAElC,MAAM,GAAG,GAAG,QAAQ,CAAC,CAAC,CAAC,GAAG,CAAC,WAAW,EAAE,CAAC,CAAA;IACzC,IAAI,GAAG,EAAE,CAAC;QACR,IAAI,CAAC,CAAC,GAAG,KAAK,GAAG;YAAE,OAAO,CAAC,CAAA;QAC3B,IAAI,CAAC,CAAC,GAAG,KAAK,IAAI,IAAI,QAAQ,CAAC,CAAC,CAAC,GAAG,CAAC,KAAK,GAAG;YAAE,OAAO,EAAE,GAAG,CAAC,EAAE,GAAG,EAAE,IAAI,EAAE,CAAA;QACzE,OAAO,EAAE,GAAG,CAAC,EAAE,GAAG,EAAE,CAAA;IACtB,CAAC;IAED,IAAI,CAAC,CAAC,GAAG,KAAK,GAAG,EAAE,CAAC;QAClB,IAAI,CAAC,CAAC,IAAI,KAAK,QAAQ;YAAE,OAAO,CAAC,CAAA;QACjC,OAAO,EAAE,GAAG,CAAC,EAAE,UAAU,EAAE,IAAI,EAAE,CAAA;IACnC,CAAC;IAED,OAAO,CAAC,CAAA;AACV,CAAC;AAED,6EAA6E;AAC7E,8EAA8E;AAC9E,MAAM,UAAU,KAAK,CAAC,CAAa;IACjC,OAAO;QACL,KAAK,EAAE,EAAE,GAAG,EAAE,CAAC,CAAC,GAAG,EAAE,IAAI,EAAE,CAAC,CAAC,UAAU,EAAE;QACzC,IAAI,EAAE,EAAE,GAAG,EAAE,CAAC,CAAC,GAAG,EAAE,UAAU,EAAE,KAAK,EAAE;KACxC,CAAA;AACH,CAAC"}
package/dist/main.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ import type { CliOpts } from './cliArgs.js';
2
+ export declare function main(opts: CliOpts): Promise<void>;
package/dist/main.js ADDED
@@ -0,0 +1,22 @@
1
+ import { hostname } from 'node:os';
2
+ import { sanitizeHandle } from 'boomwait-core';
3
+ import { runOffline } from './offline.js';
4
+ import { runOnline } from './online.js';
5
+ // Online is the default. --offline skips the join entirely (no network at all — useful when
6
+ // offline is explicitly wanted, e.g. no server reachable). Any join/connect failure while
7
+ // going online (join HTTP error, timeout, ws error, or a pre-start ws close) falls back to the
8
+ // same offline loop exactly once — runOnline only ever returns 'fallback' from a pre-match
9
+ // failure; once a match has actually started it always finishes and exits the process itself
10
+ // (teardownAndExit), so this branch can never run a second game on top of an online one.
11
+ export async function main(opts) {
12
+ const name = sanitizeHandle(opts.name ?? hostname());
13
+ const seed = Date.now() >>> 0;
14
+ if (!opts.offline) {
15
+ const result = await runOnline({ name, server: opts.server });
16
+ if (result !== 'fallback')
17
+ return; // unreachable in practice: runOnline exits the process itself once matched
18
+ console.log('boomwait: could not reach an online match — playing offline\n');
19
+ }
20
+ await runOffline({ difficulty: opts.difficulty, name, seed });
21
+ }
22
+ //# sourceMappingURL=main.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"main.js","sourceRoot":"","sources":["../src/main.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAA;AAClC,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAA;AAE9C,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAA;AACzC,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAA;AAEvC,4FAA4F;AAC5F,0FAA0F;AAC1F,+FAA+F;AAC/F,2FAA2F;AAC3F,6FAA6F;AAC7F,yFAAyF;AACzF,MAAM,CAAC,KAAK,UAAU,IAAI,CAAC,IAAa;IACtC,MAAM,IAAI,GAAG,cAAc,CAAC,IAAI,CAAC,IAAI,IAAI,QAAQ,EAAE,CAAC,CAAA;IACpD,MAAM,IAAI,GAAG,IAAI,CAAC,GAAG,EAAE,KAAK,CAAC,CAAA;IAE7B,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,CAAC;QAClB,MAAM,MAAM,GAAG,MAAM,SAAS,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,MAAM,EAAE,CAAC,CAAA;QAC7D,IAAI,MAAM,KAAK,UAAU;YAAE,OAAM,CAAC,2EAA2E;QAC7G,OAAO,CAAC,GAAG,CAAC,+DAA+D,CAAC,CAAA;IAC9E,CAAC;IAED,MAAM,UAAU,CAAC,EAAE,UAAU,EAAE,IAAI,CAAC,UAAU,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,CAAA;AAC/D,CAAC"}
package/dist/net.d.ts ADDED
@@ -0,0 +1,30 @@
1
+ import { type Dir, type Input, type InputMsg, type Result, type StartMsg, type WireState } from 'boomwait-core';
2
+ import { WebSocket } from 'ws';
3
+ export interface BomberNetHandlers {
4
+ onSnap(state: WireState): void;
5
+ onEnd(result: Result): void;
6
+ onClose(reason: string): void;
7
+ }
8
+ export type JoinOutcome = {
9
+ kind: 'joined';
10
+ matchId: string;
11
+ token: string;
12
+ } | {
13
+ kind: 'error';
14
+ };
15
+ export type WebSocketFactory = (url: string) => WebSocket;
16
+ export declare function joinBomberMatch(serverUrl: string, name: string, timeoutMs?: number): Promise<JoinOutcome>;
17
+ export declare class BomberNetClient {
18
+ private ws;
19
+ private constructor();
20
+ static connect(serverUrl: string, matchId: string, token: string, name: string, handlers: BomberNetHandlers, timeoutMs?: number, factory?: WebSocketFactory): Promise<{
21
+ client: BomberNetClient;
22
+ start: StartMsg;
23
+ }>;
24
+ sendInput(msg: InputMsg): void;
25
+ close(): void;
26
+ }
27
+ export declare function diffInputForWire(prevDir: Dir | null, input: Input): {
28
+ msg: InputMsg | null;
29
+ nextDir: Dir | null;
30
+ };
package/dist/net.js ADDED
@@ -0,0 +1,114 @@
1
+ // Bomber's online transport: a one-shot lobby POST (up to MAX_PLAYERS humans gather for
2
+ // ~10s, bots always backfill any empty slots — never a noOpponent-style outcome, see Task 8's
3
+ // bomber-lobby.ts) followed by a persistent match WebSocket. Mirrors
4
+ // packages/chess-client/src/net.ts's shape (factory seam for tests, handlers wired for the
5
+ // connection's whole life), but resolves on `start` instead of `welcome`, and the ongoing
6
+ // traffic afterward is a `snap` stream (client is render-only — it never locally steps the
7
+ // sim, every snap REPLACES the rendered state) rather than a per-move relay.
8
+ import { parseBomberServerMsg } from 'boomwait-core';
9
+ import { WebSocket } from 'ws';
10
+ const realFactory = (url) => new WebSocket(url);
11
+ // POST {serverUrl}/bomber/join {name}. The lobby gathers for ~10s (Task 8) then always answers
12
+ // {matchId, token} — bots backfill empty slots server-side, so there is no noOpponent case to
13
+ // distinguish here. ANY failure (network error, non-2xx, timeout, malformed body) collapses to
14
+ // the same 'error' outcome; the caller's fallback (play offline) is identical regardless of why.
15
+ export async function joinBomberMatch(serverUrl, name, timeoutMs = 12_000) {
16
+ const base = serverUrl.replace(/\/$/, '');
17
+ try {
18
+ const res = await fetch(`${base}/bomber/join`, {
19
+ method: 'POST',
20
+ headers: { 'content-type': 'application/json' },
21
+ body: JSON.stringify({ name }),
22
+ signal: AbortSignal.timeout(timeoutMs),
23
+ });
24
+ if (!res.ok)
25
+ return { kind: 'error' };
26
+ const body = (await res.json());
27
+ if (typeof body.matchId === 'string' && typeof body.token === 'string') {
28
+ return { kind: 'joined', matchId: body.matchId, token: body.token };
29
+ }
30
+ return { kind: 'error' };
31
+ }
32
+ catch {
33
+ return { kind: 'error' };
34
+ }
35
+ }
36
+ export class BomberNetClient {
37
+ ws;
38
+ constructor(ws) {
39
+ this.ws = ws;
40
+ }
41
+ // Opens ws(s)://.../bomber/match/{matchId}/ws?token={token}, sends hello, resolves once
42
+ // `start` arrives with the client plus the StartMsg payload (seeds the caller's local
43
+ // mirror). `handlers` stay wired for the life of the connection — snap/end/close events
44
+ // keep flowing through them after connect() resolves.
45
+ static async connect(serverUrl, matchId, token, name, handlers, timeoutMs = 12_000, factory = realFactory) {
46
+ const base = serverUrl.replace(/\/$/, '').replace(/^http/, 'ws');
47
+ const ws = factory(`${base}/bomber/match/${matchId}/ws?token=${encodeURIComponent(token)}`);
48
+ const client = new BomberNetClient(ws);
49
+ // Gates onClose so a pre-start close/error rejects connect() instead of reaching the
50
+ // caller's handlers — only an established session's close should ever fire onClose.
51
+ let started = false;
52
+ const start = await new Promise((resolve, reject) => {
53
+ const timer = setTimeout(() => reject(new Error('start timeout')), timeoutMs);
54
+ ws.on('open', () => ws.send(JSON.stringify({ t: 'hello', name })));
55
+ ws.on('message', (data) => {
56
+ const msg = parseBomberServerMsg(data.toString());
57
+ if (!msg)
58
+ return; // garbage/oversized/malformed: ignored, never crashes or corrupts state
59
+ if (msg.t === 'start') {
60
+ started = true;
61
+ clearTimeout(timer);
62
+ resolve(msg);
63
+ }
64
+ else if (msg.t === 'snap') {
65
+ handlers.onSnap(msg.state);
66
+ }
67
+ else {
68
+ handlers.onEnd(msg.result);
69
+ }
70
+ });
71
+ ws.on('close', (_code, reason) => {
72
+ const why = reason.toString();
73
+ if (!started) {
74
+ clearTimeout(timer);
75
+ reject(new Error(`connect closed: ${why || 'no reason'}`));
76
+ return;
77
+ }
78
+ handlers.onClose(why);
79
+ });
80
+ ws.on('error', (err) => {
81
+ if (!started) {
82
+ clearTimeout(timer);
83
+ reject(err);
84
+ }
85
+ });
86
+ });
87
+ return { client, start };
88
+ }
89
+ sendInput(msg) {
90
+ if (this.ws.readyState === WebSocket.OPEN)
91
+ this.ws.send(JSON.stringify(msg));
92
+ }
93
+ close() {
94
+ try {
95
+ this.ws.close();
96
+ }
97
+ catch {
98
+ /* already closed */
99
+ }
100
+ }
101
+ }
102
+ // Minimal-diff wire logic: only send an InputMsg when this tick's latched Input actually
103
+ // differs from what the server was last told. A quiet tick (dir unchanged, no bomb queued)
104
+ // sends nothing at all; a dir change sends the new dir; a bomb queued with the dir unchanged
105
+ // sends dir:'keep' (the server's own handleMessage in bomber-match.ts treats 'keep' as "don't
106
+ // touch the latched dir") so the bomb flag rides alone. Pure and separately tested (mirrors
107
+ // input-latch.ts's pure onKey/drain — the loop in online.ts just calls this every tick).
108
+ export function diffInputForWire(prevDir, input) {
109
+ const dirChanged = input.dir !== prevDir;
110
+ if (!dirChanged && !input.bomb)
111
+ return { msg: null, nextDir: prevDir };
112
+ return { msg: { t: 'input', dir: dirChanged ? input.dir : 'keep', bomb: input.bomb }, nextDir: input.dir };
113
+ }
114
+ //# sourceMappingURL=net.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"net.js","sourceRoot":"","sources":["../src/net.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,6 @@
1
+ import type { Difficulty, Result } from 'boomwait-core';
2
+ export declare function runOffline(opts: {
3
+ difficulty: Difficulty;
4
+ name: string;
5
+ seed: number;
6
+ }): Promise<Result>;
@@ -0,0 +1,53 @@
1
+ import { botDecide, createBotMind, createMatch, step } from 'boomwait-core';
2
+ import { renderFrame } from './render.js';
3
+ import { resultLine, setupGame, teardownAndExit, REDRAW_MS } from './game.js';
4
+ import { shareCard } from './share.js';
5
+ const YOU = 0;
6
+ const BOT_NAMES = ['bot·1', 'bot·2', 'bot·3'];
7
+ export async function runOffline(opts) {
8
+ const session = await setupGame();
9
+ const seed = opts.seed >>> 0;
10
+ let state = createMatch(seed, [opts.name, ...BOT_NAMES], [false, true, true, true]);
11
+ // Deterministic per-bot minds, seeded off the match seed (never wall-clock —
12
+ // the same seed always reproduces the same match, bots included).
13
+ const minds = [1, 2, 3].map((id) => createBotMind((seed + id) >>> 0));
14
+ const redraw = () => {
15
+ session.term.write(renderFrame(state, YOU, session.layout(), session.statusLine(), session.colorMode));
16
+ };
17
+ session.onResize(redraw);
18
+ session.term.enter();
19
+ session.term.installExitGuards(() => {
20
+ /* no extra resources to release offline */
21
+ });
22
+ redraw();
23
+ await new Promise((resolve) => {
24
+ const timer = setInterval(() => {
25
+ const yourInput = session.drainInput();
26
+ const inputs = [
27
+ yourInput,
28
+ botDecide(state, 1, minds[0], opts.difficulty),
29
+ botDecide(state, 2, minds[1], opts.difficulty),
30
+ botDecide(state, 3, minds[2], opts.difficulty),
31
+ ];
32
+ state = step(state, inputs);
33
+ redraw();
34
+ if (session.quitRequested() || state.result) {
35
+ clearInterval(timer);
36
+ resolve();
37
+ }
38
+ }, REDRAW_MS);
39
+ });
40
+ session.dispose();
41
+ return teardownAndExit({
42
+ term: session.term,
43
+ parser: session.parser,
44
+ listener: session.listener,
45
+ finale: state.result
46
+ ? {
47
+ screen: renderFrame(state, YOU, session.layout(), `${resultLine(state.result, YOU)} — press any key`, session.colorMode),
48
+ shareText: shareCard(state.result, YOU, state.tick, `bot·${opts.difficulty}`),
49
+ }
50
+ : null,
51
+ });
52
+ }
53
+ //# sourceMappingURL=offline.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"offline.js","sourceRoot":"","sources":["../src/offline.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,6 @@
1
+ import type { Result } from 'boomwait-core';
2
+ export interface OnlineOpts {
3
+ name?: string;
4
+ server: string;
5
+ }
6
+ export declare function runOnline(opts: OnlineOpts): Promise<Result | 'fallback'>;
package/dist/online.js ADDED
@@ -0,0 +1,107 @@
1
+ import { createMatch, fromWire, MAX_PLAYERS, sanitizeHandle } from 'boomwait-core';
2
+ import { hostname } from 'node:os';
3
+ import { renderFrame } from './render.js';
4
+ import { REDRAW_MS, resultLine, setupGame, teardownAndExit } from './game.js';
5
+ import { diffInputForWire, joinBomberMatch, BomberNetClient } from './net.js';
6
+ import { shareCard } from './share.js';
7
+ export async function runOnline(opts) {
8
+ const name = sanitizeHandle(opts.name ?? hostname());
9
+ const joined = await joinBomberMatch(opts.server, name);
10
+ if (joined.kind !== 'joined')
11
+ return 'fallback';
12
+ let ended = null;
13
+ let closedEarly = false; // ws closed after start with no `end` ever arriving (dropped/kicked)
14
+ // Hoisted above the connect() call and seeded null: onSnap below closes over `state`, and a
15
+ // coalesced ws chunk can deliver `start` and the first `snap` in ONE macrotask, back to back
16
+ // — the 'start' message resolves connect()'s internal promise, but that resolution's
17
+ // continuation (back in this function, past the `await`) only runs on the NEXT microtask
18
+ // turn, so a `snap` arriving synchronously right after it is still handled by onSnap while
19
+ // this function is suspended mid-`await`. A `let state = ...` declared AFTER the `await`
20
+ // leaves `state` in the temporal dead zone for that window — accessing it throws a bare
21
+ // ReferenceError from inside a raw event-loop callback (an uncaught exception, since exit
22
+ // guards aren't installed yet), not a rejected promise this function's own try/catch can
23
+ // see. Declaring it here means onSnap always has a variable to assign into.
24
+ let state = null;
25
+ let connected;
26
+ try {
27
+ connected = await BomberNetClient.connect(opts.server, joined.matchId, joined.token, name, {
28
+ onSnap(wire) {
29
+ state = fromWire(wire);
30
+ },
31
+ onEnd(result) {
32
+ ended = result;
33
+ },
34
+ onClose() {
35
+ closedEarly = true;
36
+ },
37
+ });
38
+ }
39
+ catch {
40
+ return 'fallback';
41
+ }
42
+ const { client: net, start } = connected;
43
+ const you = start.you;
44
+ // Seeds the local mirror deterministically from the SAME inputs the server used to create
45
+ // its own tick-0 state — this is what's on screen for the handful of ticks before the
46
+ // first `snap` arrives; every `snap` after that overwrites it outright. Also the point past
47
+ // which `state` is guaranteed non-null: any pre-seed `snap` (the race above) already landed
48
+ // in the `state` variable via onSnap, and this unconditionally overwrites it with the
49
+ // server's own tick-0 board either way — same precedence a `snap` always has over the local
50
+ // mirror.
51
+ state = createMatch(start.seed, start.names, start.bots);
52
+ const session = await setupGame();
53
+ let lastSentDir = null;
54
+ const redraw = () => {
55
+ session.term.write(renderFrame(state, you, session.layout(), session.statusLine(), session.colorMode));
56
+ };
57
+ session.onResize(redraw);
58
+ session.term.enter();
59
+ // Quit-confirm during an online match sends nothing special: closing the socket IS
60
+ // elimination (the server's GRACE_MS window in bomber-match.ts covers an accidental drop).
61
+ session.term.installExitGuards(() => net.close());
62
+ redraw();
63
+ await new Promise((resolve) => {
64
+ const timer = setInterval(() => {
65
+ const input = session.drainInput();
66
+ const { msg, nextDir } = diffInputForWire(lastSentDir, input);
67
+ if (msg) {
68
+ net.sendInput(msg);
69
+ lastSentDir = nextDir;
70
+ }
71
+ redraw();
72
+ if (session.quitRequested() || ended !== null || closedEarly || state.result) {
73
+ clearInterval(timer);
74
+ resolve();
75
+ }
76
+ }, REDRAW_MS);
77
+ });
78
+ session.dispose();
79
+ if (session.quitRequested())
80
+ net.close();
81
+ // Result precedence: an explicit `end` message wins; otherwise the last snap's own baked-in
82
+ // result (the server always stamps the final snap's WireState.result before sending `end` —
83
+ // see bomber-match.ts tick()); if NEITHER ever arrived but the socket still closed (an
84
+ // abnormal disconnect pre-finish), synthesize "not you" won so the finale still reads
85
+ // correctly (resultLine only ever distinguishes winner === you from everyone else) — a
86
+ // dropped connection is the one thing we know for certain didn't end in our own win, even
87
+ // though the actual survivor is unknowable from here. A player-initiated quit never shows a
88
+ // finale at all (matches offline.ts's mid-match-quit behavior), so it's excluded first.
89
+ const finalResult = session.quitRequested()
90
+ ? null
91
+ : (ended ?? state.result ?? (closedEarly ? { kind: 'win', winner: (you + 1) % MAX_PLAYERS } : null));
92
+ if (finalResult)
93
+ state = { ...state, result: finalResult };
94
+ return teardownAndExit({
95
+ term: session.term,
96
+ parser: session.parser,
97
+ listener: session.listener,
98
+ beforeRestore: () => net.close(),
99
+ finale: finalResult
100
+ ? {
101
+ screen: renderFrame(state, you, session.layout(), `${resultLine(finalResult, you)} — press any key`, session.colorMode),
102
+ shareText: shareCard(finalResult, you, state.tick, start.names.filter((_, i) => i !== you).join(', ')),
103
+ }
104
+ : null,
105
+ });
106
+ }
107
+ //# sourceMappingURL=online.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"online.js","sourceRoot":"","sources":["../src/online.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,9 @@
1
+ import type { BomberState } from 'boomwait-core';
2
+ import type { ColorMode } from 'termwait';
3
+ export interface Layout {
4
+ r: number;
5
+ sideHud: boolean;
6
+ glyph: boolean;
7
+ }
8
+ export declare function chooseLayout(cols: number, rows: number, mode: ColorMode): Layout;
9
+ export declare function renderFrame(s: BomberState, you: number, layout: Layout, claude: string, mode?: ColorMode): string;
package/dist/render.js ADDED
@@ -0,0 +1,307 @@
1
+ import { GRID_H, GRID_W, FUSE_TICKS, SHRINK_START_TICK, TICK_RATE, idx } from 'boomwait-core';
2
+ import { SPRITES, scaleMask } from './sprites.js';
3
+ // Board = 26r cols x 11r rows (tile = 2r cols x r rows; GRID_W=13 tiles wide,
4
+ // GRID_H=11 tall). Largest r wins, preferring a side HUD (board gets every
5
+ // row — the checkwait/chess-4d lesson: default terminals are wide but short,
6
+ // so a below-board HUD line can starve the board of rows before the board
7
+ // ever needs the extra width) whenever it fits; the below-board ladder only
8
+ // matters for narrow-but-tall windows. r<2 or non-truecolor drops to glyph
9
+ // mode (2 cols/tile letters — an 8x8 pixel sprite scaled into <2 rows is
10
+ // illegible, and non-truecolor terminals can't composite the half-block fg/bg
11
+ // pairs sprites rely on).
12
+ function fitsSideHud(r, cols, rows) {
13
+ return 11 * r + 1 <= rows && 26 * r + 27 <= cols;
14
+ }
15
+ function fitsBelowHud(r, cols, rows) {
16
+ return 11 * r + 1 <= rows && 26 * r <= cols && 11 * r + 8 <= rows;
17
+ }
18
+ export function chooseLayout(cols, rows, mode) {
19
+ const upperBound = Math.floor((rows - 1) / 11);
20
+ let foundR = 0;
21
+ let foundSideHud = false;
22
+ for (let r = Math.max(upperBound, 1); r >= 1; r--) {
23
+ const sideOk = fitsSideHud(r, cols, rows);
24
+ const belowOk = fitsBelowHud(r, cols, rows);
25
+ if (sideOk || belowOk) {
26
+ foundR = r;
27
+ foundSideHud = sideOk; // side HUD preferred whenever it fits
28
+ break;
29
+ }
30
+ }
31
+ const glyph = mode !== 'truecolor' || foundR < 2;
32
+ return { r: Math.max(foundR, 1), sideHud: foundSideHud, glyph };
33
+ }
34
+ const ESC = '\x1b';
35
+ const RESET = `${ESC}[0m`;
36
+ function bg(rgb) {
37
+ return `${ESC}[48;2;${rgb[0]};${rgb[1]};${rgb[2]}m`;
38
+ }
39
+ function fg(rgb) {
40
+ return `${ESC}[38;2;${rgb[0]};${rgb[1]};${rgb[2]}m`;
41
+ }
42
+ const HARD_RGB = [92, 92, 100];
43
+ const SOFT_RGB = [163, 116, 58];
44
+ const SOFT_TEXTURE_RGB = [110, 78, 38];
45
+ const EMPTY_RGB = [40, 92, 48];
46
+ const FLAME_BG_RGB = [120, 30, 20];
47
+ const FLAME_FG_RGB = [255, 200, 60];
48
+ const BOMB_SPARK_RGB = [
49
+ [255, 255, 255],
50
+ [255, 210, 60],
51
+ [255, 60, 40],
52
+ ];
53
+ // 4 distinct, high-contrast team colors (feel-1 lesson: every fg must
54
+ // contrast every bg it can land on — floor/soft/hard are all mid-dark, so
55
+ // bright saturated hues stay legible on all three).
56
+ const TEAM_RGB = [
57
+ [230, 70, 70], // p0 red
58
+ [70, 150, 235], // p1 blue
59
+ [110, 220, 110], // p2 green
60
+ [235, 205, 70], // p3 gold
61
+ ];
62
+ const DROP_RGB = {
63
+ bomb: [210, 210, 220],
64
+ range: [80, 225, 225],
65
+ speed: [255, 230, 90],
66
+ };
67
+ function bombFuseStage(fuse) {
68
+ if (fuse > (2 * FUSE_TICKS) / 3)
69
+ return 0;
70
+ if (fuse > FUSE_TICKS / 3)
71
+ return 1;
72
+ return 2;
73
+ }
74
+ function buildOccupancy(s) {
75
+ const flameAt = new Map();
76
+ for (const f of s.flames)
77
+ flameAt.set(idx(f.x, f.y), f);
78
+ const bombAt = new Map();
79
+ for (const b of s.bombs)
80
+ bombAt.set(idx(b.x, b.y), b);
81
+ const playerAt = new Map();
82
+ for (const p of s.players)
83
+ if (p.alive)
84
+ playerAt.set(idx(p.x, p.y), p.id);
85
+ const dropAt = new Map();
86
+ for (const d of s.drops)
87
+ dropAt.set(idx(d.x, d.y), d);
88
+ return { flameAt, bombAt, playerAt, dropAt };
89
+ }
90
+ // Draw priority: flame > player > bomb > drop > bare cell — a flame front
91
+ // visually consumes whatever was under it, and a player standing on their own
92
+ // (or anyone's) bomb/drop is the thing you're tracking, not the tile.
93
+ function tileVisual(s, occ, x, y) {
94
+ const i = idx(x, y);
95
+ const cell = s.grid[i];
96
+ const baseBg = cell === 'hard' ? HARD_RGB : cell === 'soft' ? SOFT_RGB : EMPTY_RGB;
97
+ if (occ.flameAt.has(i))
98
+ return { bgRgb: FLAME_BG_RGB, maskKey: 'flame', fgRgb: FLAME_FG_RGB };
99
+ const playerId = occ.playerAt.get(i);
100
+ if (playerId !== undefined)
101
+ return { bgRgb: baseBg, maskKey: `p${playerId}`, fgRgb: TEAM_RGB[playerId] };
102
+ const bomb = occ.bombAt.get(i);
103
+ if (bomb) {
104
+ const stage = bombFuseStage(bomb.fuse);
105
+ return { bgRgb: baseBg, maskKey: `bomb${stage}`, fgRgb: BOMB_SPARK_RGB[stage] };
106
+ }
107
+ const drop = occ.dropAt.get(i);
108
+ if (drop)
109
+ return { bgRgb: baseBg, maskKey: `drop-${drop.kind}`, fgRgb: DROP_RGB[drop.kind] };
110
+ if (cell === 'soft')
111
+ return { bgRgb: baseBg, maskKey: 'soft', fgRgb: SOFT_TEXTURE_RGB };
112
+ return { bgRgb: baseBg, maskKey: null, fgRgb: baseBg };
113
+ }
114
+ // Sprite-mode arena: half-block ▀▄ pixel compositing (chess-4's
115
+ // board-render.ts pipeline), one 2r x 2r pixel grid per tile.
116
+ function spriteArena(s, occ, r) {
117
+ const px = 2 * r;
118
+ const scaleCache = new Map();
119
+ const scaled = (key) => {
120
+ let m = scaleCache.get(key);
121
+ if (!m) {
122
+ m = scaleMask(SPRITES[key], px);
123
+ scaleCache.set(key, m);
124
+ }
125
+ return m;
126
+ };
127
+ const lines = [];
128
+ for (let ty = 0; ty < GRID_H; ty++) {
129
+ for (let subRow = 0; subRow < r; subRow++) {
130
+ let line = '';
131
+ for (let tx = 0; tx < GRID_W; tx++) {
132
+ const v = tileVisual(s, occ, tx, ty);
133
+ let cell = bg(v.bgRgb);
134
+ if (v.maskKey) {
135
+ const mask = scaled(v.maskKey);
136
+ const topRow = mask[2 * subRow];
137
+ const botRow = mask[2 * subRow + 1];
138
+ const sprFg = fg(v.fgRgb);
139
+ for (let px_ = 0; px_ < px; px_++) {
140
+ const top = topRow?.[px_] === true;
141
+ const bot = botRow?.[px_] === true;
142
+ if (!top && !bot)
143
+ cell += ' ';
144
+ else
145
+ cell += sprFg + (top && bot ? '█' : top ? '▀' : '▄');
146
+ }
147
+ }
148
+ else {
149
+ cell += ' '.repeat(px);
150
+ }
151
+ line += cell + RESET;
152
+ }
153
+ lines.push(line);
154
+ }
155
+ }
156
+ return lines;
157
+ }
158
+ // Basic-SGR (30-107 range) fallbacks for the truecolor palette above — glyph
159
+ // mode usually exists BECAUSE the terminal isn't truecolor (Apple Terminal
160
+ // detects as '256'), so it must not emit 24-bit escapes there. Mirrors chess
161
+ // board-render.ts's dedicated basic-mode branch.
162
+ const TEAM_BASIC = [91, 94, 92, 93]; // bright red/blue/green/yellow
163
+ const FLAME_BASIC = 93;
164
+ const BOMB_SPARK_BASIC = [97, 93, 91]; // white → yellow → red, matching the RGB fuse ramp
165
+ const DROP_BASIC = { bomb: 97, range: 96, speed: 93 };
166
+ const SOFT_BASIC = 33;
167
+ // Glyph-mode arena: 2 cols/tile letters — legible with no truecolor
168
+ // compositing and no minimum cell height. Color budget follows the terminal:
169
+ // truecolor keeps 24-bit fg (the r<2 tiny-window case), '256' uses basic SGR,
170
+ // mono emits no escapes at all (letters carry everything; soft's ▒ shade
171
+ // glyph also drops to ASCII there — some mono terminals are ASCII-only).
172
+ function glyphArena(s, occ, mode) {
173
+ const paint = (text, rgb, basic) => mode === 'truecolor' ? fg(rgb) + text + RESET : mode === '256' ? `${ESC}[${basic}m` + text + RESET : text;
174
+ const lines = [];
175
+ for (let ty = 0; ty < GRID_H; ty++) {
176
+ let line = '';
177
+ for (let tx = 0; tx < GRID_W; tx++) {
178
+ const i = idx(tx, ty);
179
+ if (occ.flameAt.has(i)) {
180
+ line += paint('* ', FLAME_FG_RGB, FLAME_BASIC);
181
+ continue;
182
+ }
183
+ const playerId = occ.playerAt.get(i);
184
+ if (playerId !== undefined) {
185
+ line += paint('@' + String(playerId + 1), TEAM_RGB[playerId], TEAM_BASIC[playerId]);
186
+ continue;
187
+ }
188
+ const bomb = occ.bombAt.get(i);
189
+ if (bomb) {
190
+ const stage = bombFuseStage(bomb.fuse);
191
+ line += paint('o ', BOMB_SPARK_RGB[stage], BOMB_SPARK_BASIC[stage]);
192
+ continue;
193
+ }
194
+ const drop = occ.dropAt.get(i);
195
+ if (drop) {
196
+ line += paint(drop.kind[0].toUpperCase() + ' ', DROP_RGB[drop.kind], DROP_BASIC[drop.kind]);
197
+ continue;
198
+ }
199
+ const cell = s.grid[i];
200
+ if (cell === 'hard')
201
+ line += '##';
202
+ else if (cell === 'soft')
203
+ line += mode === 'mono' ? 'xx' : paint('▒▒', SOFT_RGB, SOFT_BASIC);
204
+ else
205
+ line += ' ';
206
+ }
207
+ lines.push(line);
208
+ }
209
+ return lines;
210
+ }
211
+ function fmtClock(tick) {
212
+ const totalSec = Math.max(0, Math.floor(tick / TICK_RATE));
213
+ const m = Math.floor(totalSec / 60);
214
+ const sec = totalSec % 60;
215
+ return `${m}:${String(sec).padStart(2, '0')}`;
216
+ }
217
+ // Sudden-death shrink starts at SHRINK_START_TICK; the HUD counts down to it,
218
+ // then flags it active once the spiral starts closing tiles.
219
+ function shrinkStatus(tick) {
220
+ if (tick >= SHRINK_START_TICK)
221
+ return '⚠ shrinking';
222
+ const remainSec = Math.ceil((SHRINK_START_TICK - tick) / TICK_RATE);
223
+ return `shrink in ${remainSec}s`;
224
+ }
225
+ function playerLine(p, isYou) {
226
+ const mark = isYou ? '▸' : ' ';
227
+ const dead = p.alive ? '' : ' †';
228
+ return `${mark}${p.name}${dead}`;
229
+ }
230
+ // Fixed side-HUD text budget (matches the `+ 27` in fitsSideHud — constant
231
+ // regardless of r, so renderFrame doesn't need cols/rows: chooseLayout only
232
+ // ever returns a Layout whose canonical rendered size already fits whatever
233
+ // terminal it was computed against).
234
+ const SIDE_TEXT_WIDTH = 26;
235
+ // Spec HUD row: color swatch, name, alive/dead, bomb/range/speed counts. The
236
+ // swatch carries a color escape, so it is built and width-budgeted here
237
+ // (never handed to renderFrame's generic `text.slice(0, SIDE_TEXT_WIDTH)`,
238
+ // which operates on raw chars and would slice straight through an escape
239
+ // sequence, corrupting the terminal for every line after it) — 1 visible col
240
+ // for the swatch, the rest of the budget for the plain-text remainder.
241
+ function playerHudRow(p, isYou, mode) {
242
+ const swatch = mode === 'truecolor'
243
+ ? fg(TEAM_RGB[p.id]) + '■' + RESET
244
+ : mode === '256'
245
+ ? `${ESC}[${TEAM_BASIC[p.id]}m■${RESET}`
246
+ : '#';
247
+ const mark = isYou ? '▸' : ' ';
248
+ const dead = p.alive ? '' : ' †'; // dagger marks the DEAD (same polarity as playerLine above)
249
+ const stats = p.alive ? ` b${p.bombCap}r${p.range}s${p.speed}` : '';
250
+ const plain = `${mark}${p.name}${dead}${stats}`;
251
+ return swatch + plain.slice(0, SIDE_TEXT_WIDTH - 1);
252
+ }
253
+ // Two lines so each stays within SIDE_TEXT_WIDTH (26 cols) — matches the
254
+ // README's boomwait-controls table wording.
255
+ const KEY_HINT_ROWS = ['wasd/arrows move', 'space bomb, q-q quit'];
256
+ export function renderFrame(s, you, layout, claude, mode = 'truecolor') {
257
+ const { r, sideHud, glyph } = layout;
258
+ const occ = buildOccupancy(s);
259
+ const arenaLines = glyph ? glyphArena(s, occ, mode) : spriteArena(s, occ, r);
260
+ const boardCols = glyph ? GRID_W * 2 : GRID_W * 2 * r;
261
+ const clock = fmtClock(s.tick);
262
+ const shrink = shrinkStatus(s.tick);
263
+ const names = s.players.map((p) => playerLine(p, p.id === you));
264
+ const lines = [];
265
+ if (sideHud) {
266
+ // Per spec: player rows (color swatch, name, alive/dead, bomb/range/speed
267
+ // counts), round timer, shrink warning, Claude status (appended by the
268
+ // caller as `claude` below — already present), key hints. Player rows
269
+ // carry their own pre-budgeted color escape (see playerHudRow) and are
270
+ // appended verbatim; every other row here is plain text and goes through
271
+ // the same generic width slice as before.
272
+ const plainRows = [clock, shrink];
273
+ const playerRows = s.players.map((p) => playerHudRow(p, p.id === you, mode));
274
+ const hintRows = KEY_HINT_ROWS;
275
+ for (let i = 0; i < arenaLines.length; i++) {
276
+ let line = arenaLines[i];
277
+ if (i < plainRows.length) {
278
+ const text = plainRows[i];
279
+ if (text)
280
+ line += ' ' + text.slice(0, SIDE_TEXT_WIDTH);
281
+ }
282
+ else if (i < plainRows.length + playerRows.length) {
283
+ line += ' ' + playerRows[i - plainRows.length];
284
+ }
285
+ else if (i < plainRows.length + playerRows.length + hintRows.length) {
286
+ const text = hintRows[i - plainRows.length - playerRows.length];
287
+ if (text)
288
+ line += ' ' + text.slice(0, SIDE_TEXT_WIDTH);
289
+ }
290
+ lines.push(line);
291
+ }
292
+ }
293
+ else {
294
+ lines.push(...arenaLines);
295
+ const belowText = `${clock} ${shrink} ${names.join(' ')}`;
296
+ lines.push(belowText.slice(0, boardCols));
297
+ }
298
+ const statusWidth = boardCols + (sideHud ? SIDE_TEXT_WIDTH + 1 : 0);
299
+ lines.push(claude.slice(0, statusWidth));
300
+ // Per-line clear-to-EOL (ESC[K) kills resize residue to the right of every
301
+ // line (checkwait/chess-4 lesson); trailing ESC[J kills residue below —
302
+ // the renderer never scrolls, so both are always safe. Mono frames carry no
303
+ // SGR at all, so there is nothing to reset there.
304
+ const tail = mode === 'mono' ? '' : RESET;
305
+ return `${ESC}[H` + lines.join(`${ESC}[K\r\n`) + tail + `${ESC}[J`;
306
+ }
307
+ //# sourceMappingURL=render.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"render.js","sourceRoot":"","sources":["../src/render.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ import type { Result } from 'boomwait-core';
2
+ export declare function shareCard(result: Result, you: number, tick: number, opponentHandle: string): string;
package/dist/share.js ADDED
@@ -0,0 +1,34 @@
1
+ import { TICK_RATE } from 'boomwait-core';
2
+ // Post-match share card, printed to the NORMAL screen after term.restore() so
3
+ // it survives in the player's scrollback ready to copy into a post. Plain
4
+ // box-drawing text (no ANSI colors): it must paste cleanly anywhere, and every
5
+ // share carries the install commands. Mirrors packages/chess-client/src/share.ts's
6
+ // box-drawing pattern; content rewritten for bomber (win/loss/draw + elapsed
7
+ // match time, instead of SAN move count).
8
+ function resultPhrase(result, you) {
9
+ if (result.kind === 'win')
10
+ return result.winner === you ? 'won' : 'lost';
11
+ return 'drew';
12
+ }
13
+ function fmtClock(tick) {
14
+ const totalSec = Math.max(0, Math.floor(tick / TICK_RATE));
15
+ const m = Math.floor(totalSec / 60);
16
+ const s = totalSec % 60;
17
+ return `${m}:${String(s).padStart(2, '0')}`;
18
+ }
19
+ export function shareCard(result, you, tick, opponentHandle) {
20
+ const time = fmtClock(tick);
21
+ const lines = [
22
+ 'boomwait — terminal bomberman',
23
+ '',
24
+ `${resultPhrase(result, you)} in ${time} · vs ${opponentHandle}`,
25
+ '',
26
+ 'play in your terminal: npx -y boomwait',
27
+ 'Claude Code arcade: /plugin marketplace add jeremiahagthe/Claude-Games',
28
+ ' /plugin install games@games',
29
+ ];
30
+ const inner = Math.max(...lines.map((l) => l.length)) + 2;
31
+ const boxed = lines.map((l) => `│ ${l.padEnd(inner - 2)} │`);
32
+ return [`┌${'─'.repeat(inner)}┐`, ...boxed, `└${'─'.repeat(inner)}┘`, ''].join('\n');
33
+ }
34
+ //# sourceMappingURL=share.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"share.js","sourceRoot":"","sources":["../src/share.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,SAAS,EAAE,MAAM,eAAe,CAAA;AAEzC,8EAA8E;AAC9E,0EAA0E;AAC1E,+EAA+E;AAC/E,mFAAmF;AACnF,6EAA6E;AAC7E,0CAA0C;AAE1C,SAAS,YAAY,CAAC,MAAc,EAAE,GAAW;IAC/C,IAAI,MAAM,CAAC,IAAI,KAAK,KAAK;QAAE,OAAO,MAAM,CAAC,MAAM,KAAK,GAAG,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,CAAA;IACxE,OAAO,MAAM,CAAA;AACf,CAAC;AAED,SAAS,QAAQ,CAAC,IAAY;IAC5B,MAAM,QAAQ,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,IAAI,GAAG,SAAS,CAAC,CAAC,CAAA;IAC1D,MAAM,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,QAAQ,GAAG,EAAE,CAAC,CAAA;IACnC,MAAM,CAAC,GAAG,QAAQ,GAAG,EAAE,CAAA;IACvB,OAAO,GAAG,CAAC,IAAI,MAAM,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE,CAAA;AAC7C,CAAC;AAED,MAAM,UAAU,SAAS,CAAC,MAAc,EAAE,GAAW,EAAE,IAAY,EAAE,cAAsB;IACzF,MAAM,IAAI,GAAG,QAAQ,CAAC,IAAI,CAAC,CAAA;IAE3B,MAAM,KAAK,GAAG;QACZ,+BAA+B;QAC/B,EAAE;QACF,GAAG,YAAY,CAAC,MAAM,EAAE,GAAG,CAAC,OAAO,IAAI,SAAS,cAAc,EAAE;QAChE,EAAE;QACF,yCAAyC;QACzC,4EAA4E;QAC5E,qDAAqD;KACtD,CAAA;IAED,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,GAAG,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,GAAG,CAAC,CAAA;IACzD,MAAM,KAAK,GAAG,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,KAAK,CAAC,CAAC,MAAM,CAAC,KAAK,GAAG,CAAC,CAAC,IAAI,CAAC,CAAA;IAC5D,OAAO,CAAC,IAAI,GAAG,CAAC,MAAM,CAAC,KAAK,CAAC,GAAG,EAAE,GAAG,KAAK,EAAE,IAAI,GAAG,CAAC,MAAM,CAAC,KAAK,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;AACtF,CAAC"}
@@ -0,0 +1,2 @@
1
+ export declare const SPRITES: Record<string, number[]>;
2
+ export declare function scaleMask(mask: number[], px: number): boolean[][];
@@ -0,0 +1,137 @@
1
+ // 8x8 sprites (chess-4 board-render.ts's half-block ▀▄ compositing pipeline,
2
+ // generalized): each mask is 8 numbers, one per row, each row an 8-bit value
3
+ // (bit 7 = leftmost pixel, bit 0 = rightmost). A tile is `2r` terminal
4
+ // columns x `r` terminal rows; two vertical pixels pack into each terminal
5
+ // row via ▀▄, so the pixel grid a mask scales into is always `2r x 2r` —
6
+ // square — hence scaleMask below takes a single `px` side length.
7
+ // prettier-ignore
8
+ const PERSON = [
9
+ 0b00111100, // ..####..
10
+ 0b01111110, // .######.
11
+ 0b00111100, // ..####..
12
+ 0b01111110, // .######.
13
+ 0b11111111, // ########
14
+ 0b01111110, // .######.
15
+ 0b00100100, // ..#..#..
16
+ 0b01000010, // .#....#.
17
+ ];
18
+ // prettier-ignore
19
+ const BOMB0 = [
20
+ 0b00011000,
21
+ 0b00100000,
22
+ 0b01111110,
23
+ 0b11111111,
24
+ 0b11111111,
25
+ 0b11111111,
26
+ 0b01111110,
27
+ 0b00111100,
28
+ ];
29
+ // prettier-ignore
30
+ const BOMB1 = [
31
+ 0b00100000,
32
+ 0b01000000,
33
+ 0b01111110,
34
+ 0b11111111,
35
+ 0b11111111,
36
+ 0b11111111,
37
+ 0b01111110,
38
+ 0b00111100,
39
+ ];
40
+ // prettier-ignore
41
+ const BOMB2 = [
42
+ 0b10101010,
43
+ 0b01111110,
44
+ 0b11111111,
45
+ 0b11111111,
46
+ 0b11111111,
47
+ 0b11111111,
48
+ 0b01111110,
49
+ 0b10111101,
50
+ ];
51
+ // prettier-ignore
52
+ const FLAME = [
53
+ 0b00100100,
54
+ 0b01011010,
55
+ 0b10111101,
56
+ 0b01111110,
57
+ 0b11111111,
58
+ 0b01111110,
59
+ 0b10111101,
60
+ 0b01000010,
61
+ ];
62
+ // prettier-ignore
63
+ const SOFT = [
64
+ 0b11111111,
65
+ 0b10100101,
66
+ 0b10100101,
67
+ 0b11111111,
68
+ 0b10100101,
69
+ 0b10100101,
70
+ 0b11111111,
71
+ 0b11111111,
72
+ ];
73
+ const HARD = [0b11111111, 0b11111111, 0b11111111, 0b11111111, 0b11111111, 0b11111111, 0b11111111, 0b11111111];
74
+ // prettier-ignore
75
+ const DROP_BOMB = [
76
+ 0b00000000,
77
+ 0b00111100,
78
+ 0b01111110,
79
+ 0b01111110,
80
+ 0b01111110,
81
+ 0b00111100,
82
+ 0b00000000,
83
+ 0b00000000,
84
+ ];
85
+ // prettier-ignore
86
+ const DROP_RANGE = [
87
+ 0b00011000,
88
+ 0b00011000,
89
+ 0b00011000,
90
+ 0b11111111,
91
+ 0b11111111,
92
+ 0b00011000,
93
+ 0b00011000,
94
+ 0b00011000,
95
+ ];
96
+ // prettier-ignore
97
+ const DROP_SPEED = [
98
+ 0b10000000,
99
+ 0b11000000,
100
+ 0b11100000,
101
+ 0b11110000,
102
+ 0b11110000,
103
+ 0b11100000,
104
+ 0b11000000,
105
+ 0b10000000,
106
+ ];
107
+ export const SPRITES = {
108
+ p0: PERSON,
109
+ p1: PERSON,
110
+ p2: PERSON,
111
+ p3: PERSON,
112
+ bomb0: BOMB0,
113
+ bomb1: BOMB1,
114
+ bomb2: BOMB2,
115
+ flame: FLAME,
116
+ soft: SOFT,
117
+ hard: HARD,
118
+ 'drop-bomb': DROP_BOMB,
119
+ 'drop-range': DROP_RANGE,
120
+ 'drop-speed': DROP_SPEED,
121
+ };
122
+ // Nearest-neighbor scale of an 8x8 bitmask to a px*px boolean grid.
123
+ export function scaleMask(mask, px) {
124
+ const out = [];
125
+ for (let y = 0; y < px; y++) {
126
+ const sy = Math.min(7, Math.floor((y * 8) / px));
127
+ const bits = mask[sy] ?? 0;
128
+ const row = [];
129
+ for (let x = 0; x < px; x++) {
130
+ const sx = Math.min(7, Math.floor((x * 8) / px));
131
+ row.push(((bits >> (7 - sx)) & 1) === 1);
132
+ }
133
+ out.push(row);
134
+ }
135
+ return out;
136
+ }
137
+ //# sourceMappingURL=sprites.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sprites.js","sourceRoot":"","sources":["../src/sprites.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,10 @@
1
+ {
2
+ "name": "boomwait",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "license": "MIT",
6
+ "bin": { "boomwait": "bin/boomwait.js" },
7
+ "files": ["dist", "bin"],
8
+ "scripts": { "build": "tsc -p tsconfig.json" },
9
+ "dependencies": { "boomwait-core": "0.1.0", "termwait": "0.1.0", "ws": "8.21.0" }
10
+ }