@pavus/snake-game 1.0.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,57 @@
1
+ /**
2
+ * settings.ts — Persistent settings for snake-game stored in ~/.snake-game.json
3
+ */
4
+ import * as fs from 'node:fs';
5
+ import * as os from 'node:os';
6
+ import * as path from 'node:path';
7
+ const CONFIG_PATH = path.join(os.homedir(), '.snake-game.json');
8
+ function readConfig(configPath = CONFIG_PATH) {
9
+ try {
10
+ const raw = fs.readFileSync(configPath, 'utf-8');
11
+ return JSON.parse(raw);
12
+ }
13
+ catch {
14
+ return {};
15
+ }
16
+ }
17
+ function writeConfig(data, configPath = CONFIG_PATH) {
18
+ fs.writeFileSync(configPath, JSON.stringify(data, null, 2), 'utf-8');
19
+ }
20
+ function clampVol(v, def) {
21
+ return typeof v === 'number' ? Math.max(0, Math.min(1, v)) : def;
22
+ }
23
+ export function getSnakeHighScore(configPath) {
24
+ const config = readConfig(configPath);
25
+ const score = config['high_score'];
26
+ return typeof score === 'number' ? score : 0;
27
+ }
28
+ export function setSnakeHighScore(score, configPath) {
29
+ const config = readConfig(configPath);
30
+ config['high_score'] = score;
31
+ writeConfig(config, configPath);
32
+ }
33
+ export function getSnakeMusicVolume(configPath) {
34
+ return clampVol(readConfig(configPath)['music_volume'], 0.8);
35
+ }
36
+ export function setSnakeMusicVolume(volume, configPath) {
37
+ const config = readConfig(configPath);
38
+ config['music_volume'] = Math.max(0, Math.min(1, volume));
39
+ writeConfig(config, configPath);
40
+ }
41
+ export function getSnakeTinkVolume(configPath) {
42
+ return clampVol(readConfig(configPath)['tink_volume'], 0.05);
43
+ }
44
+ export function setSnakeTinkVolume(volume, configPath) {
45
+ const config = readConfig(configPath);
46
+ config['tink_volume'] = Math.max(0, Math.min(1, volume));
47
+ writeConfig(config, configPath);
48
+ }
49
+ export function getSnakeSfxVolume(configPath) {
50
+ return clampVol(readConfig(configPath)['sfx_volume'], 0.8);
51
+ }
52
+ export function setSnakeSfxVolume(volume, configPath) {
53
+ const config = readConfig(configPath);
54
+ config['sfx_volume'] = Math.max(0, Math.min(1, volume));
55
+ writeConfig(config, configPath);
56
+ }
57
+ //# sourceMappingURL=settings.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"settings.js","sourceRoot":"","sources":["../../src/settings.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,MAAM,SAAS,CAAC;AAC9B,OAAO,KAAK,EAAE,MAAM,SAAS,CAAC;AAC9B,OAAO,KAAK,IAAI,MAAM,WAAW,CAAC;AAElC,MAAM,WAAW,GAAG,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,EAAE,kBAAkB,CAAC,CAAC;AAEhE,SAAS,UAAU,CAAC,UAAU,GAAG,WAAW;IAC1C,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,EAAE,CAAC,YAAY,CAAC,UAAU,EAAE,OAAO,CAAC,CAAC;QACjD,OAAO,IAAI,CAAC,KAAK,CAAC,GAAG,CAA4B,CAAC;IACpD,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,EAAE,CAAC;IACZ,CAAC;AACH,CAAC;AAED,SAAS,WAAW,CAAC,IAA6B,EAAE,UAAU,GAAG,WAAW;IAC1E,EAAE,CAAC,aAAa,CAAC,UAAU,EAAE,IAAI,CAAC,SAAS,CAAC,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC,EAAE,OAAO,CAAC,CAAC;AACvE,CAAC;AAED,SAAS,QAAQ,CAAC,CAAU,EAAE,GAAW;IACvC,OAAO,OAAO,CAAC,KAAK,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC;AACnE,CAAC;AAED,MAAM,UAAU,iBAAiB,CAAC,UAAmB;IACnD,MAAM,MAAM,GAAG,UAAU,CAAC,UAAU,CAAC,CAAC;IACtC,MAAM,KAAK,GAAG,MAAM,CAAC,YAAY,CAAC,CAAC;IACnC,OAAO,OAAO,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;AAC/C,CAAC;AAED,MAAM,UAAU,iBAAiB,CAAC,KAAa,EAAE,UAAmB;IAClE,MAAM,MAAM,GAAG,UAAU,CAAC,UAAU,CAAC,CAAC;IACtC,MAAM,CAAC,YAAY,CAAC,GAAG,KAAK,CAAC;IAC7B,WAAW,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC;AAClC,CAAC;AAED,MAAM,UAAU,mBAAmB,CAAC,UAAmB;IACrD,OAAO,QAAQ,CAAC,UAAU,CAAC,UAAU,CAAC,CAAC,cAAc,CAAC,EAAE,GAAG,CAAC,CAAC;AAC/D,CAAC;AAED,MAAM,UAAU,mBAAmB,CAAC,MAAc,EAAE,UAAmB;IACrE,MAAM,MAAM,GAAG,UAAU,CAAC,UAAU,CAAC,CAAC;IACtC,MAAM,CAAC,cAAc,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,MAAM,CAAC,CAAC,CAAC;IAC1D,WAAW,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC;AAClC,CAAC;AAED,MAAM,UAAU,kBAAkB,CAAC,UAAmB;IACpD,OAAO,QAAQ,CAAC,UAAU,CAAC,UAAU,CAAC,CAAC,aAAa,CAAC,EAAE,IAAI,CAAC,CAAC;AAC/D,CAAC;AAED,MAAM,UAAU,kBAAkB,CAAC,MAAc,EAAE,UAAmB;IACpE,MAAM,MAAM,GAAG,UAAU,CAAC,UAAU,CAAC,CAAC;IACtC,MAAM,CAAC,aAAa,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,MAAM,CAAC,CAAC,CAAC;IACzD,WAAW,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC;AAClC,CAAC;AAED,MAAM,UAAU,iBAAiB,CAAC,UAAmB;IACnD,OAAO,QAAQ,CAAC,UAAU,CAAC,UAAU,CAAC,CAAC,YAAY,CAAC,EAAE,GAAG,CAAC,CAAC;AAC7D,CAAC;AAED,MAAM,UAAU,iBAAiB,CAAC,MAAc,EAAE,UAAmB;IACnE,MAAM,MAAM,GAAG,UAAU,CAAC,UAAU,CAAC,CAAC;IACtC,MAAM,CAAC,YAAY,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,MAAM,CAAC,CAAC,CAAC;IACxD,WAAW,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC;AAClC,CAAC","sourcesContent":["/**\n * settings.ts — Persistent settings for snake-game stored in ~/.snake-game.json\n */\n\nimport * as fs from 'node:fs';\nimport * as os from 'node:os';\nimport * as path from 'node:path';\n\nconst CONFIG_PATH = path.join(os.homedir(), '.snake-game.json');\n\nfunction readConfig(configPath = CONFIG_PATH): Record<string, unknown> {\n try {\n const raw = fs.readFileSync(configPath, 'utf-8');\n return JSON.parse(raw) as Record<string, unknown>;\n } catch {\n return {};\n }\n}\n\nfunction writeConfig(data: Record<string, unknown>, configPath = CONFIG_PATH): void {\n fs.writeFileSync(configPath, JSON.stringify(data, null, 2), 'utf-8');\n}\n\nfunction clampVol(v: unknown, def: number): number {\n return typeof v === 'number' ? Math.max(0, Math.min(1, v)) : def;\n}\n\nexport function getSnakeHighScore(configPath?: string): number {\n const config = readConfig(configPath);\n const score = config['high_score'];\n return typeof score === 'number' ? score : 0;\n}\n\nexport function setSnakeHighScore(score: number, configPath?: string): void {\n const config = readConfig(configPath);\n config['high_score'] = score;\n writeConfig(config, configPath);\n}\n\nexport function getSnakeMusicVolume(configPath?: string): number {\n return clampVol(readConfig(configPath)['music_volume'], 0.8);\n}\n\nexport function setSnakeMusicVolume(volume: number, configPath?: string): void {\n const config = readConfig(configPath);\n config['music_volume'] = Math.max(0, Math.min(1, volume));\n writeConfig(config, configPath);\n}\n\nexport function getSnakeTinkVolume(configPath?: string): number {\n return clampVol(readConfig(configPath)['tink_volume'], 0.05);\n}\n\nexport function setSnakeTinkVolume(volume: number, configPath?: string): void {\n const config = readConfig(configPath);\n config['tink_volume'] = Math.max(0, Math.min(1, volume));\n writeConfig(config, configPath);\n}\n\nexport function getSnakeSfxVolume(configPath?: string): number {\n return clampVol(readConfig(configPath)['sfx_volume'], 0.8);\n}\n\nexport function setSnakeSfxVolume(volume: number, configPath?: string): void {\n const config = readConfig(configPath);\n config['sfx_volume'] = Math.max(0, Math.min(1, volume));\n writeConfig(config, configPath);\n}\n"]}
@@ -0,0 +1,42 @@
1
+ /**
2
+ * snake-audio.ts — Audio for the Snake game.
3
+ *
4
+ * Background music: synthesizes a MIDI file to WAV via a self-contained
5
+ * child script (snake-synth.mjs, ~1s), then streams it with afplay.
6
+ * Zero runtime npm dependencies — pure Node.js + afplay (macOS built-in).
7
+ *
8
+ * Per-note tink: pre-generated sine-wave WAVs for the chord-per-tick overlay.
9
+ * System sounds: macOS .aiff files for eat / die events.
10
+ *
11
+ * freemidi.org requires a two-step fetch:
12
+ * 1. GET download page → grab PHPSESSID cookie
13
+ * 2. GET getter URL with Cookie + Referer headers → raw MIDI bytes
14
+ */
15
+ import { type ChildProcess } from 'node:child_process';
16
+ export interface BgMusicHandle {
17
+ proc: ChildProcess;
18
+ bpm: number;
19
+ wavPath: string;
20
+ }
21
+ /**
22
+ * Synthesize + play the given MIDI URL in the background.
23
+ * WAV is cached in /tmp keyed to the URL — synthesis only runs once per URL.
24
+ * Returns { proc, bpm } so the caller can kill playback and sync to the tempo.
25
+ *
26
+ * @param midiUrl Direct URL or freemidi.org getter URL.
27
+ * @param downloadPage freemidi.org download page URL (for cookie grab). Optional.
28
+ */
29
+ export declare function startBgMusic(midiUrl: string, downloadPage?: string, volume?: number, cacheDir?: string): Promise<BgMusicHandle | null>;
30
+ export declare function stopBgMusic(handle: BgMusicHandle | null): void;
31
+ /**
32
+ * Change playback volume without re-synthesizing.
33
+ * Kills the current afplay process and restarts it at the new volume.
34
+ */
35
+ export declare function setMusicVolume(handle: BgMusicHandle, volume: number): BgMusicHandle;
36
+ export declare function warmNotes(notes: number[], cacheDir?: string): void;
37
+ export declare function playNote(note: number, volume?: number, cacheDir?: string): void;
38
+ export declare function playSystemSound(file: string, volume?: number): void;
39
+ export declare const SYSTEM_SOUNDS: {
40
+ eat: string;
41
+ die: string;
42
+ };
@@ -0,0 +1,193 @@
1
+ /**
2
+ * snake-audio.ts — Audio for the Snake game.
3
+ *
4
+ * Background music: synthesizes a MIDI file to WAV via a self-contained
5
+ * child script (snake-synth.mjs, ~1s), then streams it with afplay.
6
+ * Zero runtime npm dependencies — pure Node.js + afplay (macOS built-in).
7
+ *
8
+ * Per-note tink: pre-generated sine-wave WAVs for the chord-per-tick overlay.
9
+ * System sounds: macOS .aiff files for eat / die events.
10
+ *
11
+ * freemidi.org requires a two-step fetch:
12
+ * 1. GET download page → grab PHPSESSID cookie
13
+ * 2. GET getter URL with Cookie + Referer headers → raw MIDI bytes
14
+ */
15
+ import { spawn } from 'node:child_process';
16
+ import { writeFileSync, mkdirSync, existsSync } from 'node:fs';
17
+ import { tmpdir } from 'node:os';
18
+ import { join, dirname } from 'node:path';
19
+ import { fileURLToPath } from 'node:url';
20
+ // Resolve the synth script: dist/snake-synth.mjs (built) or scripts/snake-synth.mjs (dev)
21
+ const __dir = dirname(fileURLToPath(import.meta.url));
22
+ const _buildPath = join(__dir, '../snake-synth.mjs');
23
+ const _devPath = join(__dir, '../scripts/snake-synth.mjs');
24
+ const SYNTH_SCRIPT = existsSync(_buildPath) ? _buildPath : _devPath;
25
+ function soundsDir(cacheDir) {
26
+ return join(cacheDir ?? tmpdir(), 'snake-game-sounds');
27
+ }
28
+ function wavCachePath(midiUrl, cacheDir) {
29
+ const name = midiUrl.split('/').pop()?.replace(/\W/g, '_') ?? 'midi';
30
+ return join(cacheDir ?? tmpdir(), `snake-game-${name}.wav`);
31
+ }
32
+ // ── freemidi.org two-step MIDI fetch ─────────────────────────────────
33
+ /**
34
+ * Fetch MIDI bytes from freemidi.org.
35
+ * Step 1: GET the download page to obtain a PHPSESSID cookie.
36
+ * Step 2: GET the getter URL with the cookie and Referer header.
37
+ * Falls back to a direct fetch if no downloadPage is provided.
38
+ */
39
+ async function fetchMidiBytes(midiUrl, downloadPage) {
40
+ if (!downloadPage) {
41
+ const res = await fetch(midiUrl);
42
+ if (!res.ok)
43
+ throw new Error(`HTTP ${res.status} fetching ${midiUrl}`);
44
+ return Buffer.from(await res.arrayBuffer());
45
+ }
46
+ // Step 1: get session cookie
47
+ const pageRes = await fetch(downloadPage, {
48
+ headers: { 'User-Agent': 'Mozilla/5.0' },
49
+ redirect: 'follow',
50
+ });
51
+ const setCookie = pageRes.headers.get('set-cookie') ?? '';
52
+ const sessionMatch = /PHPSESSID=([^;,\s]+)/.exec(setCookie);
53
+ const cookie = sessionMatch ? `PHPSESSID=${sessionMatch[1]}` : '';
54
+ // Step 2: fetch the MIDI
55
+ const midiRes = await fetch(midiUrl, {
56
+ headers: {
57
+ 'User-Agent': 'Mozilla/5.0',
58
+ ...(cookie ? { Cookie: cookie } : {}),
59
+ Referer: downloadPage,
60
+ },
61
+ redirect: 'follow',
62
+ });
63
+ if (!midiRes.ok)
64
+ throw new Error(`HTTP ${midiRes.status} fetching MIDI from ${midiUrl}`);
65
+ return Buffer.from(await midiRes.arrayBuffer());
66
+ }
67
+ /**
68
+ * Synthesize + play the given MIDI URL in the background.
69
+ * WAV is cached in /tmp keyed to the URL — synthesis only runs once per URL.
70
+ * Returns { proc, bpm } so the caller can kill playback and sync to the tempo.
71
+ *
72
+ * @param midiUrl Direct URL or freemidi.org getter URL.
73
+ * @param downloadPage freemidi.org download page URL (for cookie grab). Optional.
74
+ */
75
+ export async function startBgMusic(midiUrl, downloadPage, volume = 0.4, cacheDir) {
76
+ if (process.platform !== 'darwin')
77
+ return null;
78
+ const wavPath = wavCachePath(midiUrl, cacheDir);
79
+ let bpm = 120;
80
+ try {
81
+ if (!existsSync(wavPath)) {
82
+ // Write MIDI to a temp file so snake-synth.mjs can read it
83
+ const midiBytes = await fetchMidiBytes(midiUrl, downloadPage);
84
+ const midiTmp = wavPath.replace(/\.wav$/, '.mid');
85
+ writeFileSync(midiTmp, midiBytes);
86
+ const meta = await new Promise((resolve, reject) => {
87
+ let stdout = '';
88
+ const proc = spawn('node', [SYNTH_SCRIPT, midiTmp, wavPath], { stdio: ['ignore', 'pipe', 'ignore'] });
89
+ proc.stdout?.on('data', (d) => { stdout += d.toString(); });
90
+ proc.on('exit', (code) => {
91
+ if (code !== 0) {
92
+ reject(new Error(`synth exited ${code ?? 'null'}`));
93
+ return;
94
+ }
95
+ try {
96
+ resolve(JSON.parse(stdout));
97
+ }
98
+ catch {
99
+ resolve({ bpm: 120 });
100
+ }
101
+ });
102
+ proc.on('error', reject);
103
+ });
104
+ bpm = meta.bpm;
105
+ }
106
+ const proc = spawn('afplay', [wavPath, '-v', String(volume)], { stdio: 'ignore' });
107
+ return { proc, bpm, wavPath };
108
+ }
109
+ catch {
110
+ return null;
111
+ }
112
+ }
113
+ export function stopBgMusic(handle) {
114
+ handle?.proc.kill();
115
+ }
116
+ /**
117
+ * Change playback volume without re-synthesizing.
118
+ * Kills the current afplay process and restarts it at the new volume.
119
+ */
120
+ export function setMusicVolume(handle, volume) {
121
+ handle.proc.kill();
122
+ const proc = spawn('afplay', [handle.wavPath, '-v', String(volume)], { stdio: 'ignore' });
123
+ return { ...handle, proc };
124
+ }
125
+ // ── Per-note fallback (sine-wave WAVs via afplay) ─────────────────────
126
+ const SAMPLE_RATE = 44100;
127
+ const DURATION_S = 0.09;
128
+ function midiToFreq(note) {
129
+ return 440 * Math.pow(2, (note - 69) / 12);
130
+ }
131
+ function buildWav(freq) {
132
+ const numSamples = Math.floor(SAMPLE_RATE * DURATION_S);
133
+ const dataSize = numSamples * 2;
134
+ const buf = Buffer.alloc(44 + dataSize);
135
+ buf.write('RIFF', 0);
136
+ buf.writeUInt32LE(36 + dataSize, 4);
137
+ buf.write('WAVE', 8);
138
+ buf.write('fmt ', 12);
139
+ buf.writeUInt32LE(16, 16);
140
+ buf.writeUInt16LE(1, 20);
141
+ buf.writeUInt16LE(1, 22);
142
+ buf.writeUInt32LE(SAMPLE_RATE, 24);
143
+ buf.writeUInt32LE(SAMPLE_RATE * 2, 28);
144
+ buf.writeUInt16LE(2, 32);
145
+ buf.writeUInt16LE(16, 34);
146
+ buf.write('data', 36);
147
+ buf.writeUInt32LE(dataSize, 40);
148
+ const fadeIn = Math.floor(SAMPLE_RATE * 0.005);
149
+ const fadeOut = Math.floor(SAMPLE_RATE * 0.02);
150
+ for (let i = 0; i < numSamples; i++) {
151
+ const env = Math.min(i / fadeIn, 1) * Math.min((numSamples - i) / fadeOut, 1);
152
+ buf.writeInt16LE(Math.round(28000 * env * Math.sin((2 * Math.PI * freq * i) / SAMPLE_RATE)), 44 + i * 2);
153
+ }
154
+ return buf;
155
+ }
156
+ function noteFile(note, cacheDir) {
157
+ return join(soundsDir(cacheDir), `note-${note}.wav`);
158
+ }
159
+ export function warmNotes(notes, cacheDir) {
160
+ const dir = soundsDir(cacheDir);
161
+ mkdirSync(dir, { recursive: true });
162
+ for (const note of [...new Set(notes)]) {
163
+ const path = noteFile(note, cacheDir);
164
+ if (!existsSync(path))
165
+ writeFileSync(path, buildWav(midiToFreq(note)));
166
+ }
167
+ }
168
+ export function playNote(note, volume = 1, cacheDir) {
169
+ if (process.platform !== 'darwin') {
170
+ process.stdout.write('\x07');
171
+ return;
172
+ }
173
+ const dir = soundsDir(cacheDir);
174
+ const path = noteFile(note, cacheDir);
175
+ if (!existsSync(path)) {
176
+ mkdirSync(dir, { recursive: true });
177
+ writeFileSync(path, buildWav(midiToFreq(note)));
178
+ }
179
+ spawn('afplay', [path, '-v', String(volume)], { detached: true, stdio: 'ignore' }).unref();
180
+ }
181
+ // ── System sounds ─────────────────────────────────────────────────────
182
+ export function playSystemSound(file, volume = 1) {
183
+ if (process.platform !== 'darwin') {
184
+ process.stdout.write('\x07');
185
+ return;
186
+ }
187
+ spawn('afplay', [file, '-v', String(volume)], { detached: true, stdio: 'ignore' }).unref();
188
+ }
189
+ export const SYSTEM_SOUNDS = {
190
+ eat: '/System/Library/Sounds/Glass.aiff',
191
+ die: '/System/Library/Sounds/Funk.aiff',
192
+ };
193
+ //# sourceMappingURL=snake-audio.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"snake-audio.js","sourceRoot":"","sources":["../../src/snake-audio.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * snake-audio.ts — Audio for the Snake game.\n *\n * Background music: synthesizes a MIDI file to WAV via a self-contained\n * child script (snake-synth.mjs, ~1s), then streams it with afplay.\n * Zero runtime npm dependencies — pure Node.js + afplay (macOS built-in).\n *\n * Per-note tink: pre-generated sine-wave WAVs for the chord-per-tick overlay.\n * System sounds: macOS .aiff files for eat / die events.\n *\n * freemidi.org requires a two-step fetch:\n * 1. GET download page → grab PHPSESSID cookie\n * 2. GET getter URL with Cookie + Referer headers → raw MIDI bytes\n */\n\nimport { spawn, type ChildProcess } from 'node:child_process';\nimport { writeFileSync, mkdirSync, existsSync } from 'node:fs';\nimport { tmpdir } from 'node:os';\nimport { join, dirname } from 'node:path';\nimport { fileURLToPath } from 'node:url';\n\n// Resolve the synth script: dist/snake-synth.mjs (built) or scripts/snake-synth.mjs (dev)\nconst __dir = dirname(fileURLToPath(import.meta.url));\nconst _buildPath = join(__dir, '../snake-synth.mjs');\nconst _devPath = join(__dir, '../scripts/snake-synth.mjs');\nconst SYNTH_SCRIPT = existsSync(_buildPath) ? _buildPath : _devPath;\n\nfunction soundsDir(cacheDir?: string): string {\n return join(cacheDir ?? tmpdir(), 'snake-game-sounds');\n}\n\nfunction wavCachePath(midiUrl: string, cacheDir?: string): string {\n const name = midiUrl.split('/').pop()?.replace(/\\W/g, '_') ?? 'midi';\n return join(cacheDir ?? tmpdir(), `snake-game-${name}.wav`);\n}\n\n// ── freemidi.org two-step MIDI fetch ─────────────────────────────────\n\n/**\n * Fetch MIDI bytes from freemidi.org.\n * Step 1: GET the download page to obtain a PHPSESSID cookie.\n * Step 2: GET the getter URL with the cookie and Referer header.\n * Falls back to a direct fetch if no downloadPage is provided.\n */\nasync function fetchMidiBytes(midiUrl: string, downloadPage?: string): Promise<Buffer> {\n if (!downloadPage) {\n const res = await fetch(midiUrl);\n if (!res.ok) throw new Error(`HTTP ${res.status} fetching ${midiUrl}`);\n return Buffer.from(await res.arrayBuffer());\n }\n\n // Step 1: get session cookie\n const pageRes = await fetch(downloadPage, {\n headers: { 'User-Agent': 'Mozilla/5.0' },\n redirect: 'follow',\n });\n const setCookie = pageRes.headers.get('set-cookie') ?? '';\n const sessionMatch = /PHPSESSID=([^;,\\s]+)/.exec(setCookie);\n const cookie = sessionMatch ? `PHPSESSID=${sessionMatch[1]}` : '';\n\n // Step 2: fetch the MIDI\n const midiRes = await fetch(midiUrl, {\n headers: {\n 'User-Agent': 'Mozilla/5.0',\n ...(cookie ? { Cookie: cookie } : {}),\n Referer: downloadPage,\n },\n redirect: 'follow',\n });\n if (!midiRes.ok) throw new Error(`HTTP ${midiRes.status} fetching MIDI from ${midiUrl}`);\n return Buffer.from(await midiRes.arrayBuffer());\n}\n\n// ── Background music ─────────────────────────────────────────────────\n\nexport interface BgMusicHandle {\n proc: ChildProcess;\n bpm: number;\n wavPath: string;\n}\n\n/**\n * Synthesize + play the given MIDI URL in the background.\n * WAV is cached in /tmp keyed to the URL — synthesis only runs once per URL.\n * Returns { proc, bpm } so the caller can kill playback and sync to the tempo.\n *\n * @param midiUrl Direct URL or freemidi.org getter URL.\n * @param downloadPage freemidi.org download page URL (for cookie grab). Optional.\n */\nexport async function startBgMusic(\n midiUrl: string,\n downloadPage?: string,\n volume = 0.4,\n cacheDir?: string,\n): Promise<BgMusicHandle | null> {\n if (process.platform !== 'darwin') return null;\n\n const wavPath = wavCachePath(midiUrl, cacheDir);\n let bpm = 120;\n\n try {\n if (!existsSync(wavPath)) {\n // Write MIDI to a temp file so snake-synth.mjs can read it\n const midiBytes = await fetchMidiBytes(midiUrl, downloadPage);\n const midiTmp = wavPath.replace(/\\.wav$/, '.mid');\n writeFileSync(midiTmp, midiBytes);\n\n const meta = await new Promise<{ bpm: number }>((resolve, reject) => {\n let stdout = '';\n const proc = spawn('node', [SYNTH_SCRIPT, midiTmp, wavPath], { stdio: ['ignore', 'pipe', 'ignore'] });\n proc.stdout?.on('data', (d: Buffer) => { stdout += d.toString(); });\n proc.on('exit', (code) => {\n if (code !== 0) { reject(new Error(`synth exited ${code ?? 'null'}`)); return; }\n try { resolve(JSON.parse(stdout) as { bpm: number }); } catch { resolve({ bpm: 120 }); }\n });\n proc.on('error', reject);\n });\n bpm = meta.bpm;\n }\n const proc = spawn('afplay', [wavPath, '-v', String(volume)], { stdio: 'ignore' });\n return { proc, bpm, wavPath };\n } catch {\n return null;\n }\n}\n\nexport function stopBgMusic(handle: BgMusicHandle | null): void {\n handle?.proc.kill();\n}\n\n/**\n * Change playback volume without re-synthesizing.\n * Kills the current afplay process and restarts it at the new volume.\n */\nexport function setMusicVolume(handle: BgMusicHandle, volume: number): BgMusicHandle {\n handle.proc.kill();\n const proc = spawn('afplay', [handle.wavPath, '-v', String(volume)], { stdio: 'ignore' });\n return { ...handle, proc };\n}\n\n// ── Per-note fallback (sine-wave WAVs via afplay) ─────────────────────\n\nconst SAMPLE_RATE = 44100;\nconst DURATION_S = 0.09;\n\nfunction midiToFreq(note: number): number {\n return 440 * Math.pow(2, (note - 69) / 12);\n}\n\nfunction buildWav(freq: number): Buffer {\n const numSamples = Math.floor(SAMPLE_RATE * DURATION_S);\n const dataSize = numSamples * 2;\n const buf = Buffer.alloc(44 + dataSize);\n\n buf.write('RIFF', 0); buf.writeUInt32LE(36 + dataSize, 4); buf.write('WAVE', 8);\n buf.write('fmt ', 12); buf.writeUInt32LE(16, 16);\n buf.writeUInt16LE(1, 20); buf.writeUInt16LE(1, 22);\n buf.writeUInt32LE(SAMPLE_RATE, 24); buf.writeUInt32LE(SAMPLE_RATE * 2, 28);\n buf.writeUInt16LE(2, 32); buf.writeUInt16LE(16, 34);\n buf.write('data', 36); buf.writeUInt32LE(dataSize, 40);\n\n const fadeIn = Math.floor(SAMPLE_RATE * 0.005);\n const fadeOut = Math.floor(SAMPLE_RATE * 0.02);\n for (let i = 0; i < numSamples; i++) {\n const env = Math.min(i / fadeIn, 1) * Math.min((numSamples - i) / fadeOut, 1);\n buf.writeInt16LE(Math.round(28000 * env * Math.sin((2 * Math.PI * freq * i) / SAMPLE_RATE)), 44 + i * 2);\n }\n return buf;\n}\n\nfunction noteFile(note: number, cacheDir?: string): string {\n return join(soundsDir(cacheDir), `note-${note}.wav`);\n}\n\nexport function warmNotes(notes: number[], cacheDir?: string): void {\n const dir = soundsDir(cacheDir);\n mkdirSync(dir, { recursive: true });\n for (const note of [...new Set(notes)]) {\n const path = noteFile(note, cacheDir);\n if (!existsSync(path)) writeFileSync(path, buildWav(midiToFreq(note)));\n }\n}\n\nexport function playNote(note: number, volume = 1, cacheDir?: string): void {\n if (process.platform !== 'darwin') { process.stdout.write('\\x07'); return; }\n const dir = soundsDir(cacheDir);\n const path = noteFile(note, cacheDir);\n if (!existsSync(path)) { mkdirSync(dir, { recursive: true }); writeFileSync(path, buildWav(midiToFreq(note))); }\n spawn('afplay', [path, '-v', String(volume)], { detached: true, stdio: 'ignore' }).unref();\n}\n\n// ── System sounds ─────────────────────────────────────────────────────\n\nexport function playSystemSound(file: string, volume = 1): void {\n if (process.platform !== 'darwin') { process.stdout.write('\\x07'); return; }\n spawn('afplay', [file, '-v', String(volume)], { detached: true, stdio: 'ignore' }).unref();\n}\n\nexport const SYSTEM_SOUNDS = {\n eat: '/System/Library/Sounds/Glass.aiff',\n die: '/System/Library/Sounds/Funk.aiff',\n};\n"]}
@@ -0,0 +1,7 @@
1
+ export declare const Colors: {
2
+ readonly primary: "#1e61f0";
3
+ readonly accent: "#1e61f0";
4
+ readonly success: "green";
5
+ readonly error: "red";
6
+ readonly muted: "gray";
7
+ };
@@ -0,0 +1,8 @@
1
+ export const Colors = {
2
+ primary: '#1e61f0',
3
+ accent: '#1e61f0',
4
+ success: 'green',
5
+ error: 'red',
6
+ muted: 'gray',
7
+ };
8
+ //# sourceMappingURL=styles.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"styles.js","sourceRoot":"","sources":["../../src/styles.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,MAAM,GAAG;IACpB,OAAO,EAAE,SAAS;IAClB,MAAM,EAAE,SAAS;IACjB,OAAO,EAAE,OAAO;IAChB,KAAK,EAAE,KAAK;IACZ,KAAK,EAAE,MAAM;CACL,CAAC","sourcesContent":["export const Colors = {\n primary: '#1e61f0',\n accent: '#1e61f0',\n success: 'green',\n error: 'red',\n muted: 'gray',\n} as const;\n"]}
@@ -0,0 +1,20 @@
1
+ export interface SnakeColors {
2
+ /** UI accent color — title, score labels, controls (default: '#1e61f0') */
3
+ accent?: string;
4
+ /** Snake head color (default: '#f7a8b8') */
5
+ head?: string;
6
+ /** Snake body color (default: '#ffffff') */
7
+ body?: string;
8
+ /** Food color (default: '#55cdfc') */
9
+ food?: string;
10
+ /** Beat visualizer colors for beats 1–4 (default: trans pride palette) */
11
+ beat?: [string, string, string, string];
12
+ }
13
+ export declare const DEFAULT_COLORS: {
14
+ readonly accent: "#1e61f0";
15
+ readonly head: "#f7a8b8";
16
+ readonly body: "#ffffff";
17
+ readonly food: "#55cdfc";
18
+ readonly beat: [string, string, string, string];
19
+ };
20
+ export declare function resolveColors(colors?: SnakeColors): Required<SnakeColors>;
@@ -0,0 +1,17 @@
1
+ export const DEFAULT_COLORS = {
2
+ accent: '#1e61f0',
3
+ head: '#f7a8b8',
4
+ body: '#ffffff',
5
+ food: '#55cdfc',
6
+ beat: ['#55cdfc', '#ffffff', '#f7a8b8', '#ffffff'],
7
+ };
8
+ export function resolveColors(colors) {
9
+ return {
10
+ accent: colors?.accent ?? DEFAULT_COLORS.accent,
11
+ head: colors?.head ?? DEFAULT_COLORS.head,
12
+ body: colors?.body ?? DEFAULT_COLORS.body,
13
+ food: colors?.food ?? DEFAULT_COLORS.food,
14
+ beat: colors?.beat ?? DEFAULT_COLORS.beat,
15
+ };
16
+ }
17
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/types.ts"],"names":[],"mappings":"AAaA,MAAM,CAAC,MAAM,cAAc,GAAG;IAC5B,MAAM,EAAE,SAAS;IACjB,IAAI,EAAI,SAAS;IACjB,IAAI,EAAI,SAAS;IACjB,IAAI,EAAI,SAAS;IACjB,IAAI,EAAI,CAAC,SAAS,EAAE,SAAS,EAAE,SAAS,EAAE,SAAS,CAAqC;CAChF,CAAC;AAEX,MAAM,UAAU,aAAa,CAAC,MAAoB;IAChD,OAAO;QACL,MAAM,EAAE,MAAM,EAAE,MAAM,IAAI,cAAc,CAAC,MAAM;QAC/C,IAAI,EAAI,MAAM,EAAE,IAAI,IAAM,cAAc,CAAC,IAAI;QAC7C,IAAI,EAAI,MAAM,EAAE,IAAI,IAAM,cAAc,CAAC,IAAI;QAC7C,IAAI,EAAI,MAAM,EAAE,IAAI,IAAM,cAAc,CAAC,IAAI;QAC7C,IAAI,EAAI,MAAM,EAAE,IAAI,IAAM,cAAc,CAAC,IAAI;KAC9C,CAAC;AACJ,CAAC","sourcesContent":["export interface SnakeColors {\n /** UI accent color — title, score labels, controls (default: '#1e61f0') */\n accent?: string;\n /** Snake head color (default: '#f7a8b8') */\n head?: string;\n /** Snake body color (default: '#ffffff') */\n body?: string;\n /** Food color (default: '#55cdfc') */\n food?: string;\n /** Beat visualizer colors for beats 1–4 (default: trans pride palette) */\n beat?: [string, string, string, string];\n}\n\nexport const DEFAULT_COLORS = {\n accent: '#1e61f0',\n head: '#f7a8b8',\n body: '#ffffff',\n food: '#55cdfc',\n beat: ['#55cdfc', '#ffffff', '#f7a8b8', '#ffffff'] as [string, string, string, string],\n} as const;\n\nexport function resolveColors(colors?: SnakeColors): Required<SnakeColors> {\n return {\n accent: colors?.accent ?? DEFAULT_COLORS.accent,\n head: colors?.head ?? DEFAULT_COLORS.head,\n body: colors?.body ?? DEFAULT_COLORS.body,\n food: colors?.food ?? DEFAULT_COLORS.food,\n beat: colors?.beat ?? DEFAULT_COLORS.beat,\n };\n}\n"]}
package/package.json ADDED
@@ -0,0 +1,53 @@
1
+ {
2
+ "name": "@pavus/snake-game",
3
+ "version": "1.0.0",
4
+ "description": "Playable Snake in the terminal with MIDI music",
5
+ "type": "module",
6
+ "bin": {
7
+ "snake-game": "dist/bin.js"
8
+ },
9
+ "main": "dist/src/index.js",
10
+ "typings": "dist/src/index.d.ts",
11
+ "exports": {
12
+ ".": {
13
+ "import": "./dist/src/index.js",
14
+ "types": "./dist/src/index.d.ts"
15
+ }
16
+ },
17
+ "files": [
18
+ "dist/",
19
+ "package.json"
20
+ ],
21
+ "scripts": {
22
+ "clean": "rm -rf ./dist",
23
+ "prebuild": "pnpm clean",
24
+ "build": "pnpm tsc",
25
+ "postbuild": "chmod +x ./dist/bin.js && cp scripts/snake-synth.mjs dist/",
26
+ "link": "pnpm link --global",
27
+ "build:link": "pnpm build && pnpm link --global",
28
+ "try": "tsx bin.tsx",
29
+ "start": "node dist/bin.js"
30
+ },
31
+ "dependencies": {
32
+ "midi-file": "^1.2.4"
33
+ },
34
+ "peerDependencies": {
35
+ "ink": ">=4.0.0",
36
+ "react": ">=18.0.0"
37
+ },
38
+ "devDependencies": {
39
+ "@types/node": "^18.19.76",
40
+ "@types/react": "^19.2.14",
41
+ "ink": "^6.8.0",
42
+ "react": "^19.2.4",
43
+ "tsx": "^4.20.3",
44
+ "typescript": "^5.0.4"
45
+ },
46
+ "publishConfig": {
47
+ "access": "public"
48
+ },
49
+ "engines": {
50
+ "node": ">=18"
51
+ },
52
+ "packageManager": "pnpm@10.28.0+sha512.05df71d1421f21399e053fde567cea34d446fa02c76571441bfc1c7956e98e363088982d940465fd34480d4d90a0668bc12362f8aa88000a64e83d0b0e47be48"
53
+ }