@pavus/snake-game 1.1.0 → 1.1.1
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.
- package/README.md +92 -0
- package/dist/src/MusicSettings.d.ts +1 -0
- package/dist/src/MusicSettings.js +18 -5
- package/dist/src/MusicSettings.js.map +1 -1
- package/dist/src/SnakeGame.js +47 -10
- package/dist/src/SnakeGame.js.map +1 -1
- package/dist/src/settings.d.ts +10 -0
- package/dist/src/settings.js +27 -0
- package/dist/src/settings.js.map +1 -1
- package/dist/src/types.d.ts +2 -0
- package/dist/src/types.js +2 -0
- package/dist/src/types.js.map +1 -1
- package/package.json +1 -1
package/README.md
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# snake-game
|
|
2
|
+
|
|
3
|
+
Playable Snake in the terminal with MIDI music synthesis.
|
|
4
|
+
|
|
5
|
+
## Requirements
|
|
6
|
+
|
|
7
|
+
- Node.js 18+
|
|
8
|
+
- macOS (audio playback uses `afplay`)
|
|
9
|
+
- pnpm (for development)
|
|
10
|
+
|
|
11
|
+
## Running
|
|
12
|
+
|
|
13
|
+
### From source
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
pnpm install
|
|
17
|
+
pnpm try # run with tsx (no build step)
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
### After building
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
pnpm build
|
|
24
|
+
pnpm start # node dist/bin.js
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
### As a global CLI
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
pnpm build:link # build + pnpm link --global
|
|
31
|
+
snake-game
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
### As a library
|
|
35
|
+
|
|
36
|
+
Install in your own project:
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
npm install @pavus/snake-game
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Embed in an Ink app:
|
|
43
|
+
|
|
44
|
+
```tsx
|
|
45
|
+
import { SnakeGame } from '@pavus/snake-game';
|
|
46
|
+
|
|
47
|
+
<SnakeGame onExit={() => process.exit(0)} />
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Or launch imperatively from any CLI:
|
|
51
|
+
|
|
52
|
+
```ts
|
|
53
|
+
import { runSnakeGame } from '@pavus/snake-game';
|
|
54
|
+
|
|
55
|
+
await runSnakeGame({ music: false });
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Controls
|
|
59
|
+
|
|
60
|
+
| Key | Action |
|
|
61
|
+
|-----|--------|
|
|
62
|
+
| `W` `A` `S` `D` or arrow keys | Move |
|
|
63
|
+
| `Space` | Pause / unpause |
|
|
64
|
+
| `R` | Restart |
|
|
65
|
+
| `M` | Open music settings |
|
|
66
|
+
| `[` | Previous track |
|
|
67
|
+
| `]` | Next track (random) |
|
|
68
|
+
| `L` | Toggle loop |
|
|
69
|
+
| `Q` | Quit |
|
|
70
|
+
|
|
71
|
+
## Music settings (`M`)
|
|
72
|
+
|
|
73
|
+
- **Tracks tab** — search and select from 58 curated MIDI tracks
|
|
74
|
+
- **Volumes tab** — adjust BGM, tink, and SFX volumes independently; toggle loop
|
|
75
|
+
|
|
76
|
+
The game remembers your last played track and loop preference across sessions (stored in `~/.snake-game.json`).
|
|
77
|
+
|
|
78
|
+
## Options
|
|
79
|
+
|
|
80
|
+
```tsx
|
|
81
|
+
<SnakeGame
|
|
82
|
+
music={true} // enable/disable all audio (default: true)
|
|
83
|
+
width={20} // grid width in cells (default: 20)
|
|
84
|
+
height={10} // grid height in cells (default: 10)
|
|
85
|
+
cacheDir="/tmp" // where to cache synthesized WAV files
|
|
86
|
+
settingsFile="~/.snake-game.json" // path for persistent settings
|
|
87
|
+
tracks={myTrackList} // custom MidiTrack[] list
|
|
88
|
+
colors={{ head: '#ff0000', accent: '#00ff00' }}
|
|
89
|
+
keybindings={{ quit: ['escape'] }}
|
|
90
|
+
onExit={() => process.exit(0)}
|
|
91
|
+
/>
|
|
92
|
+
```
|
|
@@ -36,11 +36,13 @@ const VOICES = [
|
|
|
36
36
|
{ key: 'tinkVolume', label: 'Tink (per frame)' },
|
|
37
37
|
{ key: 'sfxVolume', label: 'SFX (eat / die)' },
|
|
38
38
|
];
|
|
39
|
+
const LOOP_ROW_IDX = VOICES.length; // index of the loop toggle row
|
|
39
40
|
export const MusicSettings = ({ initial, onApply, onCancel, accentColor, tracks }) => {
|
|
40
41
|
const accent = accentColor ?? DEFAULT_COLORS.accent;
|
|
41
42
|
const catalog = tracks ?? MIDI_CATALOG;
|
|
42
43
|
const [section, setSection] = useState('tracks');
|
|
43
44
|
const [voiceFocus, setVoiceFocus] = useState(0);
|
|
45
|
+
const [loopEnabled, setLoopEnabled] = useState(initial.loopEnabled);
|
|
44
46
|
const [volumes, setVolumes] = useState({
|
|
45
47
|
musicVolume: initial.musicVolume,
|
|
46
48
|
tinkVolume: initial.tinkVolume,
|
|
@@ -69,7 +71,7 @@ export const MusicSettings = ({ initial, onApply, onCancel, accentColor, tracks
|
|
|
69
71
|
const scrollOffset = scrollRef.current;
|
|
70
72
|
const visible = filtered.slice(scrollOffset, scrollOffset + VISIBLE_ROWS);
|
|
71
73
|
const apply = (track) => {
|
|
72
|
-
onApply({ track, ...volumes });
|
|
74
|
+
onApply({ track, ...volumes, loopEnabled });
|
|
73
75
|
};
|
|
74
76
|
useInput((input, key) => {
|
|
75
77
|
if (key.escape) {
|
|
@@ -86,10 +88,18 @@ export const MusicSettings = ({ initial, onApply, onCancel, accentColor, tracks
|
|
|
86
88
|
return;
|
|
87
89
|
}
|
|
88
90
|
if (key.downArrow) {
|
|
89
|
-
setVoiceFocus((f) => Math.min(
|
|
91
|
+
setVoiceFocus((f) => Math.min(LOOP_ROW_IDX, f + 1));
|
|
90
92
|
return;
|
|
91
93
|
}
|
|
92
|
-
if (key.leftArrow || key.rightArrow) {
|
|
94
|
+
if (key.leftArrow || key.rightArrow || key.return) {
|
|
95
|
+
if (voiceFocus === LOOP_ROW_IDX) {
|
|
96
|
+
setLoopEnabled((v) => !v);
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
if (key.return) {
|
|
100
|
+
apply(selectedTrack);
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
93
103
|
const voice = VOICES[voiceFocus];
|
|
94
104
|
if (!voice)
|
|
95
105
|
return;
|
|
@@ -135,10 +145,13 @@ export const MusicSettings = ({ initial, onApply, onCancel, accentColor, tracks
|
|
|
135
145
|
const isFocused = i === voiceFocus;
|
|
136
146
|
const val = volumes[voice.key];
|
|
137
147
|
return (_jsxs(Box, { gap: 2, children: [_jsx(Text, { color: isFocused ? accent : undefined, dimColor: !isFocused, children: isFocused ? '▶' : ' ' }), _jsx(Box, { width: 18, children: _jsx(Text, { color: isFocused ? accent : undefined, dimColor: !isFocused, children: voice.label }) }), _jsx(Text, { color: isFocused ? accent : undefined, dimColor: !isFocused, children: volBar(val) }), _jsx(Text, { dimColor: true, children: fmtPct(val) })] }, voice.key));
|
|
138
|
-
}),
|
|
148
|
+
}), (() => {
|
|
149
|
+
const isFocused = voiceFocus === LOOP_ROW_IDX;
|
|
150
|
+
return (_jsxs(Box, { gap: 2, children: [_jsx(Text, { color: isFocused ? accent : undefined, dimColor: !isFocused, children: isFocused ? '▶' : ' ' }), _jsx(Box, { width: 18, children: _jsx(Text, { color: isFocused ? accent : undefined, dimColor: !isFocused, children: "Loop track" }) }), _jsx(Text, { color: isFocused ? accent : undefined, dimColor: !isFocused, children: loopEnabled ? '● On ' : '○ Off' })] }));
|
|
151
|
+
})(), _jsxs(Box, { marginTop: 1, gap: 2, children: [_jsx(Text, { dimColor: true, children: "\u2191\u2193 select" }), _jsx(Text, { dimColor: true, children: "\u2190 \u2192/Enter toggle loop" }), _jsx(Text, { dimColor: true, children: "Enter apply" }), _jsx(Text, { dimColor: true, children: "Esc cancel" })] })] })), section === 'tracks' && (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { gap: 1, marginBottom: 1, children: [_jsx(Text, { dimColor: true, children: "Now playing:" }), _jsx(Text, { color: accent, children: selectedTrack.title }), _jsxs(Text, { dimColor: true, children: ["\u2014 ", selectedTrack.artist] })] }), _jsxs(Box, { gap: 1, marginBottom: 1, children: [_jsx(Text, { dimColor: true, children: "Search:" }), _jsx(Text, { color: accent, children: query || ' ' }), _jsx(Text, { dimColor: true, children: "\u258C" })] }), filtered.length === 0 ? (_jsx(Text, { dimColor: true, children: "No matches. Backspace to clear." })) : (_jsxs(Box, { flexDirection: "column", children: [scrollOffset > 0 && _jsxs(Text, { dimColor: true, children: [" \u2191 ", scrollOffset, " more"] }), visible.map((track, i) => {
|
|
139
152
|
const idx = scrollOffset + i;
|
|
140
153
|
const isFocused = idx === safeFocus;
|
|
141
154
|
return (_jsxs(Box, { gap: 1, children: [_jsx(Text, { color: isFocused ? accent : undefined, dimColor: !isFocused, children: isFocused ? '▶' : ' ' }), _jsx(Text, { color: isFocused ? accent : undefined, bold: isFocused, dimColor: !isFocused, children: track.title }), _jsxs(Text, { dimColor: true, children: ["\u2014 ", track.artist] })] }, track.id));
|
|
142
|
-
}), scrollOffset + VISIBLE_ROWS < filtered.length && (_jsxs(Text, { dimColor: true, children: [" \u2193 ", filtered.length - scrollOffset - VISIBLE_ROWS, " more"] }))] })), _jsxs(Box, { marginTop: 1, gap: 2, children: [_jsx(Text, { dimColor: true, children: "\u2191\u2193 navigate" }), _jsx(Text, { dimColor: true, children: "Enter select" }), _jsx(Text, { dimColor: true, children: "Esc cancel" })] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "tip: use \"[\" or \"]\" to go back or forward songs" }) })] }))] }));
|
|
155
|
+
}), scrollOffset + VISIBLE_ROWS < filtered.length && (_jsxs(Text, { dimColor: true, children: [" \u2193 ", filtered.length - scrollOffset - VISIBLE_ROWS, " more"] }))] })), _jsxs(Box, { marginTop: 1, gap: 2, children: [_jsx(Text, { dimColor: true, children: "\u2191\u2193 navigate" }), _jsx(Text, { dimColor: true, children: "Enter select" }), _jsx(Text, { dimColor: true, children: "Esc cancel" })] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "tip: use \"[\" or \"]\" to go back or forward songs \u00B7 \"L\" to toggle loop" }) })] }))] }));
|
|
143
156
|
};
|
|
144
157
|
//# sourceMappingURL=MusicSettings.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"MusicSettings.js","sourceRoot":"","sources":["../../src/MusicSettings.tsx"],"names":[],"mappings":"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","sourcesContent":["/**\n * MusicSettings — In-game music configuration panel for Snake.\n *\n * Two sections (Tab to switch):\n * Volumes — BGM / Tink / SFX, adjust with ← →\n * Tracks — searchable list from freemidi.org\n *\n * Esc / Enter on a track closes and applies changes.\n */\n\nimport { Box, Text, useInput } from 'ink';\nimport { useState, useMemo, useRef } from 'react';\nimport { DEFAULT_COLORS } from './types.js';\nimport {\n MIDI_CATALOG,\n FALLBACK_TRACK,\n DEFAULT_TRACK_ID,\n type MidiTrack,\n freemidiUrls,\n} from './freemidi-catalog.js';\n\nexport type { MidiTrack };\n\nexport interface SelectedTrack {\n title: string;\n artist: string;\n url: string;\n downloadPage?: string;\n}\n\nexport interface MusicConfig {\n track: SelectedTrack;\n musicVolume: number;\n tinkVolume: number;\n sfxVolume: number;\n}\n\nfunction trackToSelected(track: MidiTrack): SelectedTrack {\n const urls = freemidiUrls(track);\n return { title: track.title, artist: track.artist, url: urls.getter, downloadPage: urls.downloadPage };\n}\n\nfunction fallbackSelected(): SelectedTrack {\n return { title: FALLBACK_TRACK.title, artist: FALLBACK_TRACK.artist, url: FALLBACK_TRACK.directUrl };\n}\n\nconst VISIBLE_ROWS = 8;\nconst VOLUME_STEP = 0.1;\n\nfunction volBar(v: number): string {\n const filled = Math.round(v * 10);\n return '█'.repeat(filled) + '░'.repeat(10 - filled);\n}\n\nfunction fmtPct(v: number): string {\n return `${Math.round(v * 100)}%`;\n}\n\nfunction snap(v: number): number {\n return Math.max(0, Math.min(1, Math.round(v * 10) / 10));\n}\n\ntype Section = 'volumes' | 'tracks';\n\nconst VOICES = [\n { key: 'musicVolume' as const, label: 'Music (BGM)' },\n { key: 'tinkVolume' as const, label: 'Tink (per frame)' },\n { key: 'sfxVolume' as const, label: 'SFX (eat / die)' },\n];\n\ninterface MusicSettingsProps {\n initial: MusicConfig;\n onApply: (config: MusicConfig) => void;\n onCancel: () => void;\n accentColor?: string;\n /** Track list to show in the browser. Defaults to the built-in MIDI_CATALOG. */\n tracks?: MidiTrack[];\n}\n\nexport const MusicSettings = ({ initial, onApply, onCancel, accentColor, tracks }: MusicSettingsProps) => {\n const accent = accentColor ?? DEFAULT_COLORS.accent;\n const catalog = tracks ?? MIDI_CATALOG;\n const [section, setSection] = useState<Section>('tracks');\n const [voiceFocus, setVoiceFocus] = useState(0);\n const [volumes, setVolumes] = useState({\n musicVolume: initial.musicVolume,\n tinkVolume: initial.tinkVolume,\n sfxVolume: initial.sfxVolume,\n });\n const [selectedTrack, setSelectedTrack] = useState<SelectedTrack>(initial.track);\n\n // Track browser state\n const [query, setQuery] = useState('');\n const scrollRef = useRef(0);\n const sorted = useMemo(\n () => [...catalog].sort((a, b) => a.title.localeCompare(b.title)),\n [catalog],\n );\n const [trackFocus, setTrackFocus] = useState(() => {\n const idx = sorted.findIndex((t) => t.id === DEFAULT_TRACK_ID);\n return idx >= 0 ? idx : 0;\n });\n const filtered = useMemo(() => {\n if (!query) return sorted;\n const q = query.toLowerCase();\n return sorted.filter((t) => t.title.toLowerCase().includes(q) || t.artist.toLowerCase().includes(q));\n }, [query, sorted]);\n\n const safeFocus = Math.min(trackFocus, Math.max(0, filtered.length - 1));\n if (safeFocus < scrollRef.current) scrollRef.current = safeFocus;\n else if (safeFocus >= scrollRef.current + VISIBLE_ROWS) scrollRef.current = safeFocus - VISIBLE_ROWS + 1;\n const scrollOffset = scrollRef.current;\n const visible = filtered.slice(scrollOffset, scrollOffset + VISIBLE_ROWS);\n\n const apply = (track: SelectedTrack) => {\n onApply({ track, ...volumes });\n };\n\n useInput((input, key) => {\n if (key.escape) { onCancel(); return; }\n\n if (key.tab) {\n setSection((s) => s === 'tracks' ? 'volumes' : 'tracks');\n return;\n }\n\n if (section === 'volumes') {\n if (key.upArrow) { setVoiceFocus((f) => Math.max(0, f - 1)); return; }\n if (key.downArrow) { setVoiceFocus((f) => Math.min(VOICES.length - 1, f + 1)); return; }\n if (key.leftArrow || key.rightArrow) {\n const voice = VOICES[voiceFocus];\n if (!voice) return;\n const delta = key.leftArrow ? -VOLUME_STEP : VOLUME_STEP;\n setVolumes((v) => ({ ...v, [voice.key]: snap(v[voice.key] + delta) }));\n return;\n }\n if (key.return) { apply(selectedTrack); return; }\n }\n\n if (section === 'tracks') {\n if (key.return) {\n const track = filtered[safeFocus];\n const sel = track ? trackToSelected(track) : fallbackSelected();\n setSelectedTrack(sel);\n apply(sel);\n return;\n }\n if (key.upArrow) { setTrackFocus((f) => Math.max(0, Math.min(f, filtered.length - 1) - 1)); return; }\n if (key.downArrow) { setTrackFocus((f) => Math.min(filtered.length - 1, Math.min(f, filtered.length - 1) + 1)); return; }\n\n if (key.backspace || key.delete) {\n setQuery((q) => q.slice(0, -1));\n setTrackFocus(0); scrollRef.current = 0;\n return;\n }\n if (input && !key.ctrl && !key.meta && input.length === 1) {\n setQuery((q) => q + input);\n setTrackFocus(0); scrollRef.current = 0;\n }\n }\n });\n\n return (\n <Box flexDirection=\"column\" paddingX={1}>\n {/* Header */}\n <Box gap={3} marginBottom={1}>\n <Text bold color={accent}>Music Settings</Text>\n <Box gap={1}>\n <Text\n bold={section === 'tracks'}\n color={section === 'tracks' ? accent : undefined}\n dimColor={section !== 'tracks'}\n >\n Tracks\n </Text>\n <Text dimColor>·</Text>\n <Text\n bold={section === 'volumes'}\n color={section === 'volumes' ? accent : undefined}\n dimColor={section !== 'volumes'}\n >\n Volumes\n </Text>\n </Box>\n <Text dimColor>Tab to switch</Text>\n </Box>\n\n {section === 'volumes' && (\n <Box flexDirection=\"column\" gap={0}>\n {VOICES.map((voice, i) => {\n const isFocused = i === voiceFocus;\n const val = volumes[voice.key];\n return (\n <Box key={voice.key} gap={2}>\n <Text color={isFocused ? accent : undefined} dimColor={!isFocused}>\n {isFocused ? '▶' : ' '}\n </Text>\n <Box width={18}>\n <Text color={isFocused ? accent : undefined} dimColor={!isFocused}>\n {voice.label}\n </Text>\n </Box>\n <Text color={isFocused ? accent : undefined} dimColor={!isFocused}>\n {volBar(val)}\n </Text>\n <Text dimColor>{fmtPct(val)}</Text>\n </Box>\n );\n })}\n <Box marginTop={1} gap={2}>\n <Text dimColor>↑↓ select voice</Text>\n <Text dimColor>← → adjust</Text>\n <Text dimColor>Enter apply</Text>\n <Text dimColor>Esc cancel</Text>\n </Box>\n </Box>\n )}\n\n {section === 'tracks' && (\n <Box flexDirection=\"column\">\n <Box gap={1} marginBottom={1}>\n <Text dimColor>Now playing:</Text>\n <Text color={accent}>{selectedTrack.title}</Text>\n <Text dimColor>— {selectedTrack.artist}</Text>\n </Box>\n <Box gap={1} marginBottom={1}>\n <Text dimColor>Search:</Text>\n <Text color={accent}>{query || ' '}</Text>\n <Text dimColor>▌</Text>\n </Box>\n {filtered.length === 0 ? (\n <Text dimColor>No matches. Backspace to clear.</Text>\n ) : (\n <Box flexDirection=\"column\">\n {scrollOffset > 0 && <Text dimColor> ↑ {scrollOffset} more</Text>}\n {visible.map((track, i) => {\n const idx = scrollOffset + i;\n const isFocused = idx === safeFocus;\n return (\n <Box key={track.id} gap={1}>\n <Text color={isFocused ? accent : undefined} dimColor={!isFocused}>\n {isFocused ? '▶' : ' '}\n </Text>\n <Text color={isFocused ? accent : undefined} bold={isFocused} dimColor={!isFocused}>\n {track.title}\n </Text>\n <Text dimColor>— {track.artist}</Text>\n </Box>\n );\n })}\n {scrollOffset + VISIBLE_ROWS < filtered.length && (\n <Text dimColor> ↓ {filtered.length - scrollOffset - VISIBLE_ROWS} more</Text>\n )}\n </Box>\n )}\n <Box marginTop={1} gap={2}>\n <Text dimColor>↑↓ navigate</Text>\n <Text dimColor>Enter select</Text>\n <Text dimColor>Esc cancel</Text>\n </Box>\n <Box marginTop={1}>\n <Text dimColor>tip: use \"[\" or \"]\" to go back or forward songs</Text>\n </Box>\n </Box>\n )}\n </Box>\n );\n};\n"]}
|
|
1
|
+
{"version":3,"file":"MusicSettings.js","sourceRoot":"","sources":["../../src/MusicSettings.tsx"],"names":[],"mappings":"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","sourcesContent":["/**\n * MusicSettings — In-game music configuration panel for Snake.\n *\n * Two sections (Tab to switch):\n * Volumes — BGM / Tink / SFX, adjust with ← →\n * Tracks — searchable list from freemidi.org\n *\n * Esc / Enter on a track closes and applies changes.\n */\n\nimport { Box, Text, useInput } from 'ink';\nimport { useState, useMemo, useRef } from 'react';\nimport { DEFAULT_COLORS } from './types.js';\nimport {\n MIDI_CATALOG,\n FALLBACK_TRACK,\n DEFAULT_TRACK_ID,\n type MidiTrack,\n freemidiUrls,\n} from './freemidi-catalog.js';\n\nexport type { MidiTrack };\n\nexport interface SelectedTrack {\n title: string;\n artist: string;\n url: string;\n downloadPage?: string;\n}\n\nexport interface MusicConfig {\n track: SelectedTrack;\n musicVolume: number;\n tinkVolume: number;\n sfxVolume: number;\n loopEnabled: boolean;\n}\n\nfunction trackToSelected(track: MidiTrack): SelectedTrack {\n const urls = freemidiUrls(track);\n return { title: track.title, artist: track.artist, url: urls.getter, downloadPage: urls.downloadPage };\n}\n\nfunction fallbackSelected(): SelectedTrack {\n return { title: FALLBACK_TRACK.title, artist: FALLBACK_TRACK.artist, url: FALLBACK_TRACK.directUrl };\n}\n\nconst VISIBLE_ROWS = 8;\nconst VOLUME_STEP = 0.1;\n\nfunction volBar(v: number): string {\n const filled = Math.round(v * 10);\n return '█'.repeat(filled) + '░'.repeat(10 - filled);\n}\n\nfunction fmtPct(v: number): string {\n return `${Math.round(v * 100)}%`;\n}\n\nfunction snap(v: number): number {\n return Math.max(0, Math.min(1, Math.round(v * 10) / 10));\n}\n\ntype Section = 'volumes' | 'tracks';\n\nconst VOICES = [\n { key: 'musicVolume' as const, label: 'Music (BGM)' },\n { key: 'tinkVolume' as const, label: 'Tink (per frame)' },\n { key: 'sfxVolume' as const, label: 'SFX (eat / die)' },\n];\n\nconst LOOP_ROW_IDX = VOICES.length; // index of the loop toggle row\n\ninterface MusicSettingsProps {\n initial: MusicConfig;\n onApply: (config: MusicConfig) => void;\n onCancel: () => void;\n accentColor?: string;\n /** Track list to show in the browser. Defaults to the built-in MIDI_CATALOG. */\n tracks?: MidiTrack[];\n}\n\nexport const MusicSettings = ({ initial, onApply, onCancel, accentColor, tracks }: MusicSettingsProps) => {\n const accent = accentColor ?? DEFAULT_COLORS.accent;\n const catalog = tracks ?? MIDI_CATALOG;\n const [section, setSection] = useState<Section>('tracks');\n const [voiceFocus, setVoiceFocus] = useState(0);\n const [loopEnabled, setLoopEnabled] = useState(initial.loopEnabled);\n const [volumes, setVolumes] = useState({\n musicVolume: initial.musicVolume,\n tinkVolume: initial.tinkVolume,\n sfxVolume: initial.sfxVolume,\n });\n const [selectedTrack, setSelectedTrack] = useState<SelectedTrack>(initial.track);\n\n // Track browser state\n const [query, setQuery] = useState('');\n const scrollRef = useRef(0);\n const sorted = useMemo(\n () => [...catalog].sort((a, b) => a.title.localeCompare(b.title)),\n [catalog],\n );\n const [trackFocus, setTrackFocus] = useState(() => {\n const idx = sorted.findIndex((t) => t.id === DEFAULT_TRACK_ID);\n return idx >= 0 ? idx : 0;\n });\n const filtered = useMemo(() => {\n if (!query) return sorted;\n const q = query.toLowerCase();\n return sorted.filter((t) => t.title.toLowerCase().includes(q) || t.artist.toLowerCase().includes(q));\n }, [query, sorted]);\n\n const safeFocus = Math.min(trackFocus, Math.max(0, filtered.length - 1));\n if (safeFocus < scrollRef.current) scrollRef.current = safeFocus;\n else if (safeFocus >= scrollRef.current + VISIBLE_ROWS) scrollRef.current = safeFocus - VISIBLE_ROWS + 1;\n const scrollOffset = scrollRef.current;\n const visible = filtered.slice(scrollOffset, scrollOffset + VISIBLE_ROWS);\n\n const apply = (track: SelectedTrack) => {\n onApply({ track, ...volumes, loopEnabled });\n };\n\n useInput((input, key) => {\n if (key.escape) { onCancel(); return; }\n\n if (key.tab) {\n setSection((s) => s === 'tracks' ? 'volumes' : 'tracks');\n return;\n }\n\n if (section === 'volumes') {\n if (key.upArrow) { setVoiceFocus((f) => Math.max(0, f - 1)); return; }\n if (key.downArrow) { setVoiceFocus((f) => Math.min(LOOP_ROW_IDX, f + 1)); return; }\n if (key.leftArrow || key.rightArrow || key.return) {\n if (voiceFocus === LOOP_ROW_IDX) {\n setLoopEnabled((v) => !v);\n return;\n }\n if (key.return) { apply(selectedTrack); return; }\n const voice = VOICES[voiceFocus];\n if (!voice) return;\n const delta = key.leftArrow ? -VOLUME_STEP : VOLUME_STEP;\n setVolumes((v) => ({ ...v, [voice.key]: snap(v[voice.key] + delta) }));\n return;\n }\n if (key.return) { apply(selectedTrack); return; }\n }\n\n if (section === 'tracks') {\n if (key.return) {\n const track = filtered[safeFocus];\n const sel = track ? trackToSelected(track) : fallbackSelected();\n setSelectedTrack(sel);\n apply(sel);\n return;\n }\n if (key.upArrow) { setTrackFocus((f) => Math.max(0, Math.min(f, filtered.length - 1) - 1)); return; }\n if (key.downArrow) { setTrackFocus((f) => Math.min(filtered.length - 1, Math.min(f, filtered.length - 1) + 1)); return; }\n\n if (key.backspace || key.delete) {\n setQuery((q) => q.slice(0, -1));\n setTrackFocus(0); scrollRef.current = 0;\n return;\n }\n if (input && !key.ctrl && !key.meta && input.length === 1) {\n setQuery((q) => q + input);\n setTrackFocus(0); scrollRef.current = 0;\n }\n }\n });\n\n return (\n <Box flexDirection=\"column\" paddingX={1}>\n {/* Header */}\n <Box gap={3} marginBottom={1}>\n <Text bold color={accent}>Music Settings</Text>\n <Box gap={1}>\n <Text\n bold={section === 'tracks'}\n color={section === 'tracks' ? accent : undefined}\n dimColor={section !== 'tracks'}\n >\n Tracks\n </Text>\n <Text dimColor>·</Text>\n <Text\n bold={section === 'volumes'}\n color={section === 'volumes' ? accent : undefined}\n dimColor={section !== 'volumes'}\n >\n Volumes\n </Text>\n </Box>\n <Text dimColor>Tab to switch</Text>\n </Box>\n\n {section === 'volumes' && (\n <Box flexDirection=\"column\" gap={0}>\n {VOICES.map((voice, i) => {\n const isFocused = i === voiceFocus;\n const val = volumes[voice.key];\n return (\n <Box key={voice.key} gap={2}>\n <Text color={isFocused ? accent : undefined} dimColor={!isFocused}>\n {isFocused ? '▶' : ' '}\n </Text>\n <Box width={18}>\n <Text color={isFocused ? accent : undefined} dimColor={!isFocused}>\n {voice.label}\n </Text>\n </Box>\n <Text color={isFocused ? accent : undefined} dimColor={!isFocused}>\n {volBar(val)}\n </Text>\n <Text dimColor>{fmtPct(val)}</Text>\n </Box>\n );\n })}\n {(() => {\n const isFocused = voiceFocus === LOOP_ROW_IDX;\n return (\n <Box gap={2}>\n <Text color={isFocused ? accent : undefined} dimColor={!isFocused}>\n {isFocused ? '▶' : ' '}\n </Text>\n <Box width={18}>\n <Text color={isFocused ? accent : undefined} dimColor={!isFocused}>\n Loop track\n </Text>\n </Box>\n <Text color={isFocused ? accent : undefined} dimColor={!isFocused}>\n {loopEnabled ? '● On ' : '○ Off'}\n </Text>\n </Box>\n );\n })()}\n <Box marginTop={1} gap={2}>\n <Text dimColor>↑↓ select</Text>\n <Text dimColor>← →/Enter toggle loop</Text>\n <Text dimColor>Enter apply</Text>\n <Text dimColor>Esc cancel</Text>\n </Box>\n </Box>\n )}\n\n {section === 'tracks' && (\n <Box flexDirection=\"column\">\n <Box gap={1} marginBottom={1}>\n <Text dimColor>Now playing:</Text>\n <Text color={accent}>{selectedTrack.title}</Text>\n <Text dimColor>— {selectedTrack.artist}</Text>\n </Box>\n <Box gap={1} marginBottom={1}>\n <Text dimColor>Search:</Text>\n <Text color={accent}>{query || ' '}</Text>\n <Text dimColor>▌</Text>\n </Box>\n {filtered.length === 0 ? (\n <Text dimColor>No matches. Backspace to clear.</Text>\n ) : (\n <Box flexDirection=\"column\">\n {scrollOffset > 0 && <Text dimColor> ↑ {scrollOffset} more</Text>}\n {visible.map((track, i) => {\n const idx = scrollOffset + i;\n const isFocused = idx === safeFocus;\n return (\n <Box key={track.id} gap={1}>\n <Text color={isFocused ? accent : undefined} dimColor={!isFocused}>\n {isFocused ? '▶' : ' '}\n </Text>\n <Text color={isFocused ? accent : undefined} bold={isFocused} dimColor={!isFocused}>\n {track.title}\n </Text>\n <Text dimColor>— {track.artist}</Text>\n </Box>\n );\n })}\n {scrollOffset + VISIBLE_ROWS < filtered.length && (\n <Text dimColor> ↓ {filtered.length - scrollOffset - VISIBLE_ROWS} more</Text>\n )}\n </Box>\n )}\n <Box marginTop={1} gap={2}>\n <Text dimColor>↑↓ navigate</Text>\n <Text dimColor>Enter select</Text>\n <Text dimColor>Esc cancel</Text>\n </Box>\n <Box marginTop={1}>\n <Text dimColor>tip: use \"[\" or \"]\" to go back or forward songs · \"L\" to toggle loop</Text>\n </Box>\n </Box>\n )}\n </Box>\n );\n};\n"]}
|
package/dist/src/SnakeGame.js
CHANGED
|
@@ -9,7 +9,7 @@ import { jsxs as _jsxs, jsx as _jsx, Fragment as _Fragment } from "react/jsx-run
|
|
|
9
9
|
*/
|
|
10
10
|
import { Box, Text, useInput } from 'ink';
|
|
11
11
|
import { useReducer, useRef, useEffect, useState } from 'react';
|
|
12
|
-
import { getSnakeHighScore, setSnakeHighScore, getSnakeMusicVolume, setSnakeMusicVolume, getSnakeTinkVolume, setSnakeTinkVolume, getSnakeSfxVolume, setSnakeSfxVolume, } from './settings.js';
|
|
12
|
+
import { getSnakeHighScore, setSnakeHighScore, getSnakeMusicVolume, setSnakeMusicVolume, getSnakeTinkVolume, setSnakeTinkVolume, getSnakeSfxVolume, setSnakeSfxVolume, getSnakeLoopEnabled, setSnakeLoopEnabled, getSnakeLastTrack, setSnakeLastTrack, } from './settings.js';
|
|
13
13
|
import { warmNotes, playNote, playSystemSound, SYSTEM_SOUNDS, startBgMusic, stopBgMusic, setMusicVolume, } from './snake-audio.js';
|
|
14
14
|
import { MusicSettings } from './MusicSettings.js';
|
|
15
15
|
import { MIDI_CATALOG, DEFAULT_TRACK_ID, freemidiUrls } from './freemidi-catalog.js';
|
|
@@ -37,7 +37,10 @@ function randomOtherTrack(catalog, currentUrl) {
|
|
|
37
37
|
function displayKey(k) {
|
|
38
38
|
return KEY_LABELS[k] ?? k.toUpperCase();
|
|
39
39
|
}
|
|
40
|
-
function resolveDefaultTrack(catalog) {
|
|
40
|
+
function resolveDefaultTrack(catalog, settingsFile) {
|
|
41
|
+
const last = getSnakeLastTrack(settingsFile);
|
|
42
|
+
if (last)
|
|
43
|
+
return last;
|
|
41
44
|
const entry = catalog.find((t) => t.id === DEFAULT_TRACK_ID) ?? catalog[0];
|
|
42
45
|
if (!entry)
|
|
43
46
|
return { title: 'No tracks', artist: '', url: '' };
|
|
@@ -94,14 +97,24 @@ export const SnakeGame = ({ onExit, music = true, colors, cacheDir, settingsFile
|
|
|
94
97
|
const [highScore, setHighScore] = useState(0);
|
|
95
98
|
const bgMusic = useRef(null);
|
|
96
99
|
const [bpm, setBpm] = useState(DEFAULT_BPM);
|
|
97
|
-
const [track, setTrack] = useState(() => resolveDefaultTrack(catalog));
|
|
100
|
+
const [track, setTrack] = useState(() => resolveDefaultTrack(catalog, settingsFile));
|
|
98
101
|
const [showSettings, setShowSettings] = useState(false);
|
|
99
102
|
const autoPlayRef = useRef(true);
|
|
100
103
|
const nextTrackRef = useRef(() => { });
|
|
104
|
+
const [loopEnabled, setLoopEnabled] = useState(() => getSnakeLoopEnabled(settingsFile));
|
|
105
|
+
const loopRef = useRef(loopEnabled);
|
|
106
|
+
const [trackRevision, setTrackRevision] = useState(0);
|
|
101
107
|
const [beatPhase, setBeatPhase] = useState(0);
|
|
102
108
|
const [musicVolume, setMusicVolumeState] = useState(() => getSnakeMusicVolume(settingsFile));
|
|
103
109
|
const [tinkVolume, setTinkVolumeState] = useState(() => getSnakeTinkVolume(settingsFile));
|
|
104
110
|
const [sfxVolume, setSfxVolumeState] = useState(() => getSnakeSfxVolume(settingsFile));
|
|
111
|
+
// Keep loopRef in sync with state
|
|
112
|
+
useEffect(() => { loopRef.current = loopEnabled; }, [loopEnabled]);
|
|
113
|
+
// Persist the current track whenever it changes
|
|
114
|
+
useEffect(() => {
|
|
115
|
+
if (track.url)
|
|
116
|
+
setSnakeLastTrack(track, settingsFile);
|
|
117
|
+
}, [track]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
105
118
|
// Keep nextTrackRef pointed at a stable "advance to next track" callback
|
|
106
119
|
useEffect(() => {
|
|
107
120
|
nextTrackRef.current = () => {
|
|
@@ -130,8 +143,14 @@ export const SnakeGame = ({ onExit, music = true, colors, cacheDir, settingsFile
|
|
|
130
143
|
if (handle) {
|
|
131
144
|
setBpm(handle.bpm);
|
|
132
145
|
handle.proc.on('exit', (_code, signal) => {
|
|
133
|
-
if (signal == null && autoPlayRef.current)
|
|
134
|
-
|
|
146
|
+
if (signal == null && autoPlayRef.current) {
|
|
147
|
+
if (loopRef.current) {
|
|
148
|
+
setTrackRevision((r) => r + 1);
|
|
149
|
+
}
|
|
150
|
+
else {
|
|
151
|
+
nextTrackRef.current();
|
|
152
|
+
}
|
|
153
|
+
}
|
|
135
154
|
});
|
|
136
155
|
}
|
|
137
156
|
});
|
|
@@ -141,7 +160,7 @@ export const SnakeGame = ({ onExit, music = true, colors, cacheDir, settingsFile
|
|
|
141
160
|
stopBgMusic(bgMusic.current);
|
|
142
161
|
bgMusic.current = null;
|
|
143
162
|
};
|
|
144
|
-
}, [track, music]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
163
|
+
}, [track, music, trackRevision]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
145
164
|
useEffect(() => {
|
|
146
165
|
const saved = getSnakeHighScore(settingsFile);
|
|
147
166
|
highScoreRef.current = saved;
|
|
@@ -242,6 +261,13 @@ export const SnakeGame = ({ onExit, music = true, colors, cacheDir, settingsFile
|
|
|
242
261
|
goToTrack(entry);
|
|
243
262
|
return;
|
|
244
263
|
}
|
|
264
|
+
if (pressed(kb.loopTrack, input, key)) {
|
|
265
|
+
const next = !loopRef.current;
|
|
266
|
+
loopRef.current = next;
|
|
267
|
+
setLoopEnabled(next);
|
|
268
|
+
setSnakeLoopEnabled(next, settingsFile);
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
245
271
|
}
|
|
246
272
|
if (!g.started || g.gameOver) {
|
|
247
273
|
if (pressed(kb.restart, input, key) || pressed(kb.pause, input, key))
|
|
@@ -272,12 +298,18 @@ export const SnakeGame = ({ onExit, music = true, colors, cacheDir, settingsFile
|
|
|
272
298
|
}
|
|
273
299
|
});
|
|
274
300
|
if (showSettings) {
|
|
275
|
-
return (_jsx(MusicSettings, { initial: { track, musicVolume, tinkVolume, sfxVolume }, accentColor: c.accent, tracks: catalog, onApply: (cfg) => {
|
|
301
|
+
return (_jsx(MusicSettings, { initial: { track, musicVolume, tinkVolume, sfxVolume, loopEnabled }, accentColor: c.accent, tracks: catalog, onApply: (cfg) => {
|
|
276
302
|
if (cfg.musicVolume !== musicVolume && bgMusic.current) {
|
|
277
303
|
bgMusic.current = setMusicVolume(bgMusic.current, cfg.musicVolume);
|
|
278
304
|
bgMusic.current.proc.on('exit', (_code, signal) => {
|
|
279
|
-
if (signal == null && autoPlayRef.current)
|
|
280
|
-
|
|
305
|
+
if (signal == null && autoPlayRef.current) {
|
|
306
|
+
if (loopRef.current) {
|
|
307
|
+
setTrackRevision((r) => r + 1);
|
|
308
|
+
}
|
|
309
|
+
else {
|
|
310
|
+
nextTrackRef.current();
|
|
311
|
+
}
|
|
312
|
+
}
|
|
281
313
|
});
|
|
282
314
|
}
|
|
283
315
|
if (cfg.musicVolume !== musicVolume) {
|
|
@@ -292,6 +324,11 @@ export const SnakeGame = ({ onExit, music = true, colors, cacheDir, settingsFile
|
|
|
292
324
|
setSnakeSfxVolume(cfg.sfxVolume, settingsFile);
|
|
293
325
|
setSfxVolumeState(cfg.sfxVolume);
|
|
294
326
|
}
|
|
327
|
+
if (cfg.loopEnabled !== loopEnabled) {
|
|
328
|
+
loopRef.current = cfg.loopEnabled;
|
|
329
|
+
setLoopEnabled(cfg.loopEnabled);
|
|
330
|
+
setSnakeLoopEnabled(cfg.loopEnabled, settingsFile);
|
|
331
|
+
}
|
|
295
332
|
if (cfg.track.url !== track.url)
|
|
296
333
|
setTrack(cfg.track);
|
|
297
334
|
setShowSettings(false);
|
|
@@ -307,7 +344,7 @@ export const SnakeGame = ({ onExit, music = true, colors, cacheDir, settingsFile
|
|
|
307
344
|
return 'food';
|
|
308
345
|
return 'empty';
|
|
309
346
|
}));
|
|
310
|
-
return (_jsxs(Box, { flexDirection: "column", paddingX: 1, children: [_jsxs(Box, { gap: 2, children: [_jsx(Text, { bold: true, color: c.accent, children: "Snake" }), _jsxs(Text, { color: "white", children: ["Score: ", _jsx(Text, { bold: true, children: score })] }), highScore > 0 && _jsxs(Text, { color: "white", children: ["Best: ", _jsx(Text, { bold: true, children: highScore })] }), paused && _jsx(Text, { color: "yellow", children: " PAUSED" })] }), music && (_jsxs(_Fragment, { children: [_jsxs(Box, { gap: 1, children: [_jsx(Text, { dimColor: true, children: "\u266A" }), _jsx(Text, { dimColor: true, children: track.title }), _jsx(Text, { dimColor: true, children: "\u2014" }), _jsx(Text, { dimColor: true, children: track.artist })] }), _jsx(MusicVisualizer, { beatPhase: beatPhase, active: started && !paused && !gameOver, beatColors: c.beat })] })), _jsx(Box, { height: 1 }), _jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { dimColor: true, children: '┌' + '──'.repeat(width) + '┐' }), cells.map((row, y) => (_jsxs(Box, { children: [_jsx(Text, { dimColor: true, children: "\u2502" }), row.map((cell, x) => {
|
|
347
|
+
return (_jsxs(Box, { flexDirection: "column", paddingX: 1, children: [_jsxs(Box, { gap: 2, children: [_jsx(Text, { bold: true, color: c.accent, children: "Snake" }), _jsxs(Text, { color: "white", children: ["Score: ", _jsx(Text, { bold: true, children: score })] }), highScore > 0 && _jsxs(Text, { color: "white", children: ["Best: ", _jsx(Text, { bold: true, children: highScore })] }), paused && _jsx(Text, { color: "yellow", children: " PAUSED" })] }), music && (_jsxs(_Fragment, { children: [_jsxs(Box, { gap: 1, children: [_jsx(Text, { dimColor: true, children: "\u266A" }), _jsx(Text, { dimColor: true, children: track.title }), _jsx(Text, { dimColor: true, children: "\u2014" }), _jsx(Text, { dimColor: true, children: track.artist }), loopEnabled && _jsx(Text, { dimColor: true, children: "\u27F3" })] }), _jsx(MusicVisualizer, { beatPhase: beatPhase, active: started && !paused && !gameOver, beatColors: c.beat })] })), _jsx(Box, { height: 1 }), _jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { dimColor: true, children: '┌' + '──'.repeat(width) + '┐' }), cells.map((row, y) => (_jsxs(Box, { children: [_jsx(Text, { dimColor: true, children: "\u2502" }), row.map((cell, x) => {
|
|
311
348
|
if (cell === 'head')
|
|
312
349
|
return _jsx(Text, { color: c.head, bold: true, children: '● ' }, x);
|
|
313
350
|
if (cell === 'body')
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SnakeGame.js","sourceRoot":"","sources":["../../src/SnakeGame.tsx"],"names":[],"mappings":"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","sourcesContent":["/**\n * SnakeGame — Playable Snake in the terminal.\n *\n * Controls: WASD to move · Space to pause · R to restart · M to change music\n *\n * Uses a ref-backed game state with a single persistent interval so\n * the game loop never captures stale closure values.\n */\n\nimport { Box, Text, useInput, type Key } from 'ink';\nimport { useReducer, useRef, useEffect, useState } from 'react';\nimport {\n getSnakeHighScore,\n setSnakeHighScore,\n getSnakeMusicVolume,\n setSnakeMusicVolume,\n getSnakeTinkVolume,\n setSnakeTinkVolume,\n getSnakeSfxVolume,\n setSnakeSfxVolume,\n} from './settings.js';\nimport {\n warmNotes, playNote, playSystemSound, SYSTEM_SOUNDS,\n startBgMusic, stopBgMusic, setMusicVolume, type BgMusicHandle,\n} from './snake-audio.js';\nimport { MusicSettings, type MusicConfig, type SelectedTrack } from './MusicSettings.js';\nimport { MIDI_CATALOG, DEFAULT_TRACK_ID, freemidiUrls, type MidiTrack } from './freemidi-catalog.js';\nimport { type SnakeColors, resolveColors, type SnakeKeybindings, resolveKeybindings } from './types.js';\n\nexport type { SnakeColors, SnakeKeybindings };\n\nconst DEFAULT_BPM = 120;\n\n// Maps keybinding token strings to Ink Key property names\nconst ARROW_KEY_MAP: Record<string, keyof Key> = {\n up: 'upArrow', down: 'downArrow', left: 'leftArrow', right: 'rightArrow',\n return: 'return', escape: 'escape', tab: 'tab',\n};\n\nfunction pressed(bindings: string[], input: string, key: Key): boolean {\n return bindings.some((b) => {\n const prop = ARROW_KEY_MAP[b];\n return prop ? (key[prop] as boolean) === true : input === b;\n });\n}\n\nconst KEY_LABELS: Record<string, string> = {\n up: '↑', down: '↓', left: '←', right: '→',\n return: '↵', escape: 'Esc', tab: 'Tab', ' ': 'Space',\n};\nfunction randomOtherTrack(catalog: MidiTrack[], currentUrl: string): MidiTrack | null {\n const others = catalog.filter((t) => freemidiUrls(t).getter !== currentUrl);\n return others.length > 0 ? (others[Math.floor(Math.random() * others.length)] ?? null) : null;\n}\n\nfunction displayKey(k: string): string {\n return KEY_LABELS[k] ?? k.toUpperCase();\n}\n\nfunction resolveDefaultTrack(catalog: MidiTrack[]): SelectedTrack {\n const entry = catalog.find((t) => t.id === DEFAULT_TRACK_ID) ?? catalog[0];\n if (!entry) return { title: 'No tracks', artist: '', url: '' };\n const urls = freemidiUrls(entry);\n return { title: entry.title, artist: entry.artist, url: urls.getter, downloadPage: urls.downloadPage };\n}\n\n// ── Music visualizer ──────────────────────────────────────────────────\n\nfunction MusicVisualizer({\n beatPhase,\n active,\n beatColors,\n}: {\n beatPhase: number;\n active: boolean;\n beatColors: [string, string, string, string];\n}) {\n const beat = Math.floor(beatPhase / 4) % 4;\n\n return (\n <Box>\n {([0, 1, 2, 3] as const).map((b) => {\n const isCurrent = active && b === beat;\n return (\n <Text key={b} color={isCurrent ? beatColors[b] : undefined} dimColor={!isCurrent}>\n {isCurrent ? '●' : '○'}{' '}\n </Text>\n );\n })}\n </Box>\n );\n}\n\ntype Point = { x: number; y: number };\ntype Dir = { dx: number; dy: number };\n\nconst DIRS = {\n up: { dx: 0, dy: -1 },\n down: { dx: 0, dy: 1 },\n left: { dx: -1, dy: 0 },\n right: { dx: 1, dy: 0 },\n} as const;\n\nfunction randomFood(snake: Point[], w: number, h: number): Point {\n const free: Point[] = [];\n for (let y = 0; y < h; y++) {\n for (let x = 0; x < w; x++) {\n if (!snake.some((s) => s.x === x && s.y === y)) {\n free.push({ x, y });\n }\n }\n }\n return free[Math.floor(Math.random() * free.length)] ?? { x: 0, y: 0 };\n}\n\ntype GameState = {\n snake: Point[];\n dir: Dir;\n dirQueue: Dir[];\n food: Point;\n score: number;\n gameOver: boolean;\n paused: boolean;\n started: boolean;\n};\n\nfunction makeInitial(w: number, h: number): GameState {\n const cx = Math.floor(w / 2);\n const cy = Math.floor(h / 2);\n const snake = [{ x: cx, y: cy }, { x: cx - 1, y: cy }, { x: cx - 2, y: cy }];\n return {\n snake,\n dir: DIRS.right,\n dirQueue: [],\n food: randomFood(snake, w, h),\n score: 0,\n gameOver: false,\n paused: false,\n started: false,\n };\n}\n\ninterface SnakeGameProps {\n onExit?: () => void;\n /** Enable or disable all audio (default: true) */\n music?: boolean;\n /** Override any of the game's colors */\n colors?: SnakeColors;\n /**\n * Directory for cached audio files (synthesized WAVs and per-note tinks).\n * Defaults to the OS temp directory.\n */\n cacheDir?: string;\n /**\n * Path to the JSON file used to persist settings (high score, volumes).\n * Defaults to ~/.snake-game.json\n */\n settingsFile?: string;\n /** Grid width in cells (default: 20) */\n width?: number;\n /** Grid height in cells (default: 10) */\n height?: number;\n /**\n * Track list shown in the music browser.\n * Defaults to the built-in MIDI_CATALOG from freemidi.org.\n */\n tracks?: MidiTrack[];\n /**\n * Override individual key bindings.\n * Each action accepts an array of keys. Use plain characters ('w', ' ', 'r')\n * or arrow-key tokens: 'up', 'down', 'left', 'right', 'return', 'escape'.\n * Defaults include both WASD and arrow keys for movement.\n */\n keybindings?: SnakeKeybindings;\n}\n\nexport const SnakeGame = ({\n onExit,\n music = true,\n colors,\n cacheDir,\n settingsFile,\n width = 20,\n height = 10,\n tracks,\n keybindings,\n}: SnakeGameProps = {}) => {\n const c = resolveColors(colors);\n const kb = resolveKeybindings(keybindings);\n const catalog = tracks ?? MIDI_CATALOG;\n\n const stateRef = useRef<GameState>(makeInitial(width, height));\n const [, forceUpdate] = useReducer((x: number) => x + 1, 0);\n\n const highScoreRef = useRef(0);\n const [highScore, setHighScore] = useState(0);\n const bgMusic = useRef<BgMusicHandle | null>(null);\n const [bpm, setBpm] = useState(DEFAULT_BPM);\n const [track, setTrack] = useState<SelectedTrack>(() => resolveDefaultTrack(catalog));\n const [showSettings, setShowSettings] = useState(false);\n const autoPlayRef = useRef(true);\n const nextTrackRef = useRef<() => void>(() => {});\n const [beatPhase, setBeatPhase] = useState(0);\n const [musicVolume, setMusicVolumeState] = useState(() => getSnakeMusicVolume(settingsFile));\n const [tinkVolume, setTinkVolumeState] = useState(() => getSnakeTinkVolume(settingsFile));\n const [sfxVolume, setSfxVolumeState] = useState(() => getSnakeSfxVolume(settingsFile));\n\n // Keep nextTrackRef pointed at a stable \"advance to next track\" callback\n useEffect(() => {\n nextTrackRef.current = () => {\n const entry = randomOtherTrack(catalog, track.url);\n if (entry) {\n const urls = freemidiUrls(entry);\n setTrack({ title: entry.title, artist: entry.artist, url: urls.getter, downloadPage: urls.downloadPage });\n }\n };\n }, [track, catalog]); // eslint-disable-line react-hooks/exhaustive-deps\n\n useEffect(() => {\n if (!music) return;\n warmNotes([69], cacheDir);\n stopBgMusic(bgMusic.current);\n bgMusic.current = null;\n setBpm(DEFAULT_BPM);\n autoPlayRef.current = true;\n let cancelled = false;\n void startBgMusic(track.url, track.downloadPage, musicVolume, cacheDir).then((handle) => {\n if (cancelled) { stopBgMusic(handle); return; }\n bgMusic.current = handle;\n if (handle) {\n setBpm(handle.bpm);\n handle.proc.on('exit', (_code, signal) => {\n if (signal == null && autoPlayRef.current) nextTrackRef.current();\n });\n }\n });\n return () => {\n cancelled = true;\n autoPlayRef.current = false;\n stopBgMusic(bgMusic.current);\n bgMusic.current = null;\n };\n }, [track, music]); // eslint-disable-line react-hooks/exhaustive-deps\n\n useEffect(() => {\n const saved = getSnakeHighScore(settingsFile);\n highScoreRef.current = saved;\n setHighScore(saved);\n }, []);\n\n const reset = () => {\n stateRef.current = { ...makeInitial(width, height), started: true };\n forceUpdate();\n };\n\n // Beat phase for music visualizer — runs independently of game state\n useEffect(() => {\n if (!music) return;\n const tickMs = Math.round((60_000 / bpm) / 4);\n const id = setInterval(() => setBeatPhase((p) => (p + 1) % 16), tickMs);\n return () => clearInterval(id);\n }, [bpm, music]);\n\n // Game loop — interval recreates when bpm changes (once synthesis completes)\n useEffect(() => {\n const tickMs = Math.round((60_000 / bpm) / 4); // one 16th note\n const id = setInterval(() => {\n const g = stateRef.current;\n if (!g.started || g.gameOver || g.paused) return;\n\n const [nextDir, ...restQueue] = g.dirQueue.length > 0 ? g.dirQueue : [g.dir];\n const dir = nextDir;\n const head = { x: g.snake[0].x + dir.dx, y: g.snake[0].y + dir.dy };\n\n if (\n head.x < 0 ||\n head.x >= width ||\n head.y < 0 ||\n head.y >= height ||\n g.snake.some((s) => s.x === head.x && s.y === head.y)\n ) {\n stateRef.current = { ...g, dir, dirQueue: restQueue, gameOver: true };\n if (music) playSystemSound(SYSTEM_SOUNDS.die, sfxVolume);\n forceUpdate();\n return;\n }\n\n const ate = head.x === g.food.x && head.y === g.food.y;\n const snake = ate ? [head, ...g.snake] : [head, ...g.snake.slice(0, -1)];\n const newScore = ate ? g.score + 1 : g.score;\n\n if (ate) {\n if (newScore > highScoreRef.current) {\n highScoreRef.current = newScore;\n setSnakeHighScore(newScore, settingsFile);\n setHighScore(newScore);\n }\n if (music) playSystemSound(SYSTEM_SOUNDS.eat, sfxVolume);\n } else {\n if (music) playNote(69, tinkVolume, cacheDir);\n }\n\n stateRef.current = {\n ...g,\n snake,\n dir,\n dirQueue: restQueue,\n food: ate ? randomFood(snake, width, height) : g.food,\n score: newScore,\n };\n forceUpdate();\n }, tickMs);\n\n return () => clearInterval(id);\n }, [bpm]);\n\n useInput((input, key) => {\n if (showSettings) return;\n\n const g = stateRef.current;\n\n if (pressed(kb.quit, input, key) && onExit) { onExit(); return; }\n\n if (pressed(kb.music, input, key) && music) {\n if (g.started && !g.gameOver && !g.paused) {\n stateRef.current = { ...g, paused: true };\n forceUpdate();\n }\n setShowSettings(true);\n return;\n }\n\n if (music) {\n const goToTrack = (entry: MidiTrack) => {\n const urls = freemidiUrls(entry);\n setTrack({ title: entry.title, artist: entry.artist, url: urls.getter, downloadPage: urls.downloadPage });\n };\n if (pressed(kb.nextTrack, input, key)) {\n const entry = randomOtherTrack(catalog, track.url);\n if (entry) goToTrack(entry);\n return;\n }\n if (pressed(kb.prevTrack, input, key)) {\n const idx = catalog.findIndex((t) => freemidiUrls(t).getter === track.url);\n const entry = catalog[(idx - 1 + catalog.length) % catalog.length];\n if (entry) goToTrack(entry);\n return;\n }\n }\n\n if (!g.started || g.gameOver) {\n if (pressed(kb.restart, input, key) || pressed(kb.pause, input, key)) reset();\n return;\n }\n\n if (pressed(kb.pause, input, key)) { stateRef.current = { ...g, paused: !g.paused }; forceUpdate(); return; }\n if (pressed(kb.restart, input, key)) { reset(); return; }\n\n const lastDir = g.dirQueue[g.dirQueue.length - 1] ?? g.dir;\n let next: Dir | null = null;\n if (pressed(kb.up, input, key) && lastDir.dy !== 1) next = DIRS.up;\n else if (pressed(kb.down, input, key) && lastDir.dy !== -1) next = DIRS.down;\n else if (pressed(kb.left, input, key) && lastDir.dx !== 1) next = DIRS.left;\n else if (pressed(kb.right, input, key) && lastDir.dx !== -1) next = DIRS.right;\n if (next && g.dirQueue.length < 2) {\n stateRef.current = { ...g, dirQueue: [...g.dirQueue, next] };\n }\n });\n\n if (showSettings) {\n return (\n <MusicSettings\n initial={{ track, musicVolume, tinkVolume, sfxVolume }}\n accentColor={c.accent}\n tracks={catalog}\n onApply={(cfg: MusicConfig) => {\n if (cfg.musicVolume !== musicVolume && bgMusic.current) {\n bgMusic.current = setMusicVolume(bgMusic.current, cfg.musicVolume);\n bgMusic.current.proc.on('exit', (_code, signal) => {\n if (signal == null && autoPlayRef.current) nextTrackRef.current();\n });\n }\n if (cfg.musicVolume !== musicVolume) {\n setSnakeMusicVolume(cfg.musicVolume, settingsFile);\n setMusicVolumeState(cfg.musicVolume);\n }\n if (cfg.tinkVolume !== tinkVolume) {\n setSnakeTinkVolume(cfg.tinkVolume, settingsFile);\n setTinkVolumeState(cfg.tinkVolume);\n }\n if (cfg.sfxVolume !== sfxVolume) {\n setSnakeSfxVolume(cfg.sfxVolume, settingsFile);\n setSfxVolumeState(cfg.sfxVolume);\n }\n if (cfg.track.url !== track.url) setTrack(cfg.track);\n setShowSettings(false);\n }}\n onCancel={() => setShowSettings(false)}\n />\n );\n }\n\n const { snake, food, score, gameOver, paused, started } = stateRef.current;\n\n const cells = Array.from({ length: height }, (_, y) =>\n Array.from({ length: width }, (_, x) => {\n if (x === snake[0]?.x && y === snake[0]?.y) return 'head';\n if (snake.some((s) => s.x === x && s.y === y)) return 'body';\n if (x === food.x && y === food.y) return 'food';\n return 'empty';\n }),\n );\n\n return (\n <Box flexDirection=\"column\" paddingX={1}>\n <Box gap={2}>\n <Text bold color={c.accent}>Snake</Text>\n <Text color=\"white\">Score: <Text bold>{score}</Text></Text>\n {highScore > 0 && <Text color=\"white\">Best: <Text bold>{highScore}</Text></Text>}\n {paused && <Text color=\"yellow\"> PAUSED</Text>}\n </Box>\n {music && (\n <>\n <Box gap={1}>\n <Text dimColor>♪</Text>\n <Text dimColor>{track.title}</Text>\n <Text dimColor>—</Text>\n <Text dimColor>{track.artist}</Text>\n </Box>\n <MusicVisualizer beatPhase={beatPhase} active={started && !paused && !gameOver} beatColors={c.beat} />\n </>\n )}\n <Box height={1} />\n <Box flexDirection=\"column\">\n <Text dimColor>{'┌' + '──'.repeat(width) + '┐'}</Text>\n {cells.map((row, y) => (\n <Box key={y}>\n <Text dimColor>│</Text>\n {row.map((cell, x) => {\n if (cell === 'head') return <Text key={x} color={c.head} bold>{'● '}</Text>;\n if (cell === 'body') return <Text key={x} color={c.body}>{'● '}</Text>;\n if (cell === 'food') return <Text key={x} color={c.food}>{'◆ '}</Text>;\n return <Text key={x} dimColor>{'· '}</Text>;\n })}\n <Text dimColor>│</Text>\n </Box>\n ))}\n <Text dimColor>{'└' + '──'.repeat(width) + '┘'}</Text>\n </Box>\n <Box height={1} />\n {!started && (\n <Text dimColor>\n Press <Text bold color={c.accent}>Space</Text> or{' '}\n <Text bold color={c.accent}>R</Text> to start\n </Text>\n )}\n {gameOver && (\n <Text>\n <Text color=\"red\">Game over! </Text>\n <Text dimColor>Press </Text>\n <Text bold color={c.accent}>R</Text>\n <Text dimColor> to restart</Text>\n </Text>\n )}\n {started && !gameOver && (\n <Text dimColor>\n {displayKey(kb.up[0]!)}/{displayKey(kb.down[0]!)}/{displayKey(kb.left[0]!)}/{displayKey(kb.right[0]!)} to move\n {' · '}{displayKey(kb.pause[0]!)} to pause\n {' · '}{displayKey(kb.restart[0]!)} to restart\n {music ? ` · ${displayKey(kb.music[0]!)} menu` : ''}\n </Text>\n )}\n {!started && music && <Text dimColor>{displayKey(kb.music[0]!)} to change music</Text>}\n {onExit && <Text dimColor>Press <Text bold>{displayKey(kb.quit[0]!)}</Text> to exit</Text>}\n </Box>\n );\n};\n"]}
|
|
1
|
+
{"version":3,"file":"SnakeGame.js","sourceRoot":"","sources":["../../src/SnakeGame.tsx"],"names":[],"mappings":"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","sourcesContent":["/**\n * SnakeGame — Playable Snake in the terminal.\n *\n * Controls: WASD to move · Space to pause · R to restart · M to change music\n *\n * Uses a ref-backed game state with a single persistent interval so\n * the game loop never captures stale closure values.\n */\n\nimport { Box, Text, useInput, type Key } from 'ink';\nimport { useReducer, useRef, useEffect, useState } from 'react';\nimport {\n getSnakeHighScore,\n setSnakeHighScore,\n getSnakeMusicVolume,\n setSnakeMusicVolume,\n getSnakeTinkVolume,\n setSnakeTinkVolume,\n getSnakeSfxVolume,\n setSnakeSfxVolume,\n getSnakeLoopEnabled,\n setSnakeLoopEnabled,\n getSnakeLastTrack,\n setSnakeLastTrack,\n} from './settings.js';\nimport {\n warmNotes, playNote, playSystemSound, SYSTEM_SOUNDS,\n startBgMusic, stopBgMusic, setMusicVolume, type BgMusicHandle,\n} from './snake-audio.js';\nimport { MusicSettings, type MusicConfig, type SelectedTrack } from './MusicSettings.js';\nimport { MIDI_CATALOG, DEFAULT_TRACK_ID, freemidiUrls, type MidiTrack } from './freemidi-catalog.js';\nimport { type SnakeColors, resolveColors, type SnakeKeybindings, resolveKeybindings } from './types.js';\n\nexport type { SnakeColors, SnakeKeybindings };\n\nconst DEFAULT_BPM = 120;\n\n// Maps keybinding token strings to Ink Key property names\nconst ARROW_KEY_MAP: Record<string, keyof Key> = {\n up: 'upArrow', down: 'downArrow', left: 'leftArrow', right: 'rightArrow',\n return: 'return', escape: 'escape', tab: 'tab',\n};\n\nfunction pressed(bindings: string[], input: string, key: Key): boolean {\n return bindings.some((b) => {\n const prop = ARROW_KEY_MAP[b];\n return prop ? (key[prop] as boolean) === true : input === b;\n });\n}\n\nconst KEY_LABELS: Record<string, string> = {\n up: '↑', down: '↓', left: '←', right: '→',\n return: '↵', escape: 'Esc', tab: 'Tab', ' ': 'Space',\n};\nfunction randomOtherTrack(catalog: MidiTrack[], currentUrl: string): MidiTrack | null {\n const others = catalog.filter((t) => freemidiUrls(t).getter !== currentUrl);\n return others.length > 0 ? (others[Math.floor(Math.random() * others.length)] ?? null) : null;\n}\n\nfunction displayKey(k: string): string {\n return KEY_LABELS[k] ?? k.toUpperCase();\n}\n\nfunction resolveDefaultTrack(catalog: MidiTrack[], settingsFile?: string): SelectedTrack {\n const last = getSnakeLastTrack(settingsFile);\n if (last) return last;\n const entry = catalog.find((t) => t.id === DEFAULT_TRACK_ID) ?? catalog[0];\n if (!entry) return { title: 'No tracks', artist: '', url: '' };\n const urls = freemidiUrls(entry);\n return { title: entry.title, artist: entry.artist, url: urls.getter, downloadPage: urls.downloadPage };\n}\n\n// ── Music visualizer ──────────────────────────────────────────────────\n\nfunction MusicVisualizer({\n beatPhase,\n active,\n beatColors,\n}: {\n beatPhase: number;\n active: boolean;\n beatColors: [string, string, string, string];\n}) {\n const beat = Math.floor(beatPhase / 4) % 4;\n\n return (\n <Box>\n {([0, 1, 2, 3] as const).map((b) => {\n const isCurrent = active && b === beat;\n return (\n <Text key={b} color={isCurrent ? beatColors[b] : undefined} dimColor={!isCurrent}>\n {isCurrent ? '●' : '○'}{' '}\n </Text>\n );\n })}\n </Box>\n );\n}\n\ntype Point = { x: number; y: number };\ntype Dir = { dx: number; dy: number };\n\nconst DIRS = {\n up: { dx: 0, dy: -1 },\n down: { dx: 0, dy: 1 },\n left: { dx: -1, dy: 0 },\n right: { dx: 1, dy: 0 },\n} as const;\n\nfunction randomFood(snake: Point[], w: number, h: number): Point {\n const free: Point[] = [];\n for (let y = 0; y < h; y++) {\n for (let x = 0; x < w; x++) {\n if (!snake.some((s) => s.x === x && s.y === y)) {\n free.push({ x, y });\n }\n }\n }\n return free[Math.floor(Math.random() * free.length)] ?? { x: 0, y: 0 };\n}\n\ntype GameState = {\n snake: Point[];\n dir: Dir;\n dirQueue: Dir[];\n food: Point;\n score: number;\n gameOver: boolean;\n paused: boolean;\n started: boolean;\n};\n\nfunction makeInitial(w: number, h: number): GameState {\n const cx = Math.floor(w / 2);\n const cy = Math.floor(h / 2);\n const snake = [{ x: cx, y: cy }, { x: cx - 1, y: cy }, { x: cx - 2, y: cy }];\n return {\n snake,\n dir: DIRS.right,\n dirQueue: [],\n food: randomFood(snake, w, h),\n score: 0,\n gameOver: false,\n paused: false,\n started: false,\n };\n}\n\ninterface SnakeGameProps {\n onExit?: () => void;\n /** Enable or disable all audio (default: true) */\n music?: boolean;\n /** Override any of the game's colors */\n colors?: SnakeColors;\n /**\n * Directory for cached audio files (synthesized WAVs and per-note tinks).\n * Defaults to the OS temp directory.\n */\n cacheDir?: string;\n /**\n * Path to the JSON file used to persist settings (high score, volumes).\n * Defaults to ~/.snake-game.json\n */\n settingsFile?: string;\n /** Grid width in cells (default: 20) */\n width?: number;\n /** Grid height in cells (default: 10) */\n height?: number;\n /**\n * Track list shown in the music browser.\n * Defaults to the built-in MIDI_CATALOG from freemidi.org.\n */\n tracks?: MidiTrack[];\n /**\n * Override individual key bindings.\n * Each action accepts an array of keys. Use plain characters ('w', ' ', 'r')\n * or arrow-key tokens: 'up', 'down', 'left', 'right', 'return', 'escape'.\n * Defaults include both WASD and arrow keys for movement.\n */\n keybindings?: SnakeKeybindings;\n}\n\nexport const SnakeGame = ({\n onExit,\n music = true,\n colors,\n cacheDir,\n settingsFile,\n width = 20,\n height = 10,\n tracks,\n keybindings,\n}: SnakeGameProps = {}) => {\n const c = resolveColors(colors);\n const kb = resolveKeybindings(keybindings);\n const catalog = tracks ?? MIDI_CATALOG;\n\n const stateRef = useRef<GameState>(makeInitial(width, height));\n const [, forceUpdate] = useReducer((x: number) => x + 1, 0);\n\n const highScoreRef = useRef(0);\n const [highScore, setHighScore] = useState(0);\n const bgMusic = useRef<BgMusicHandle | null>(null);\n const [bpm, setBpm] = useState(DEFAULT_BPM);\n const [track, setTrack] = useState<SelectedTrack>(() => resolveDefaultTrack(catalog, settingsFile));\n const [showSettings, setShowSettings] = useState(false);\n const autoPlayRef = useRef(true);\n const nextTrackRef = useRef<() => void>(() => {});\n const [loopEnabled, setLoopEnabled] = useState(() => getSnakeLoopEnabled(settingsFile));\n const loopRef = useRef(loopEnabled);\n const [trackRevision, setTrackRevision] = useState(0);\n const [beatPhase, setBeatPhase] = useState(0);\n const [musicVolume, setMusicVolumeState] = useState(() => getSnakeMusicVolume(settingsFile));\n const [tinkVolume, setTinkVolumeState] = useState(() => getSnakeTinkVolume(settingsFile));\n const [sfxVolume, setSfxVolumeState] = useState(() => getSnakeSfxVolume(settingsFile));\n\n // Keep loopRef in sync with state\n useEffect(() => { loopRef.current = loopEnabled; }, [loopEnabled]);\n\n // Persist the current track whenever it changes\n useEffect(() => {\n if (track.url) setSnakeLastTrack(track, settingsFile);\n }, [track]); // eslint-disable-line react-hooks/exhaustive-deps\n\n // Keep nextTrackRef pointed at a stable \"advance to next track\" callback\n useEffect(() => {\n nextTrackRef.current = () => {\n const entry = randomOtherTrack(catalog, track.url);\n if (entry) {\n const urls = freemidiUrls(entry);\n setTrack({ title: entry.title, artist: entry.artist, url: urls.getter, downloadPage: urls.downloadPage });\n }\n };\n }, [track, catalog]); // eslint-disable-line react-hooks/exhaustive-deps\n\n useEffect(() => {\n if (!music) return;\n warmNotes([69], cacheDir);\n stopBgMusic(bgMusic.current);\n bgMusic.current = null;\n setBpm(DEFAULT_BPM);\n autoPlayRef.current = true;\n let cancelled = false;\n void startBgMusic(track.url, track.downloadPage, musicVolume, cacheDir).then((handle) => {\n if (cancelled) { stopBgMusic(handle); return; }\n bgMusic.current = handle;\n if (handle) {\n setBpm(handle.bpm);\n handle.proc.on('exit', (_code, signal) => {\n if (signal == null && autoPlayRef.current) {\n if (loopRef.current) {\n setTrackRevision((r) => r + 1);\n } else {\n nextTrackRef.current();\n }\n }\n });\n }\n });\n return () => {\n cancelled = true;\n autoPlayRef.current = false;\n stopBgMusic(bgMusic.current);\n bgMusic.current = null;\n };\n }, [track, music, trackRevision]); // eslint-disable-line react-hooks/exhaustive-deps\n\n useEffect(() => {\n const saved = getSnakeHighScore(settingsFile);\n highScoreRef.current = saved;\n setHighScore(saved);\n }, []);\n\n const reset = () => {\n stateRef.current = { ...makeInitial(width, height), started: true };\n forceUpdate();\n };\n\n // Beat phase for music visualizer — runs independently of game state\n useEffect(() => {\n if (!music) return;\n const tickMs = Math.round((60_000 / bpm) / 4);\n const id = setInterval(() => setBeatPhase((p) => (p + 1) % 16), tickMs);\n return () => clearInterval(id);\n }, [bpm, music]);\n\n // Game loop — interval recreates when bpm changes (once synthesis completes)\n useEffect(() => {\n const tickMs = Math.round((60_000 / bpm) / 4); // one 16th note\n const id = setInterval(() => {\n const g = stateRef.current;\n if (!g.started || g.gameOver || g.paused) return;\n\n const [nextDir, ...restQueue] = g.dirQueue.length > 0 ? g.dirQueue : [g.dir];\n const dir = nextDir;\n const head = { x: g.snake[0].x + dir.dx, y: g.snake[0].y + dir.dy };\n\n if (\n head.x < 0 ||\n head.x >= width ||\n head.y < 0 ||\n head.y >= height ||\n g.snake.some((s) => s.x === head.x && s.y === head.y)\n ) {\n stateRef.current = { ...g, dir, dirQueue: restQueue, gameOver: true };\n if (music) playSystemSound(SYSTEM_SOUNDS.die, sfxVolume);\n forceUpdate();\n return;\n }\n\n const ate = head.x === g.food.x && head.y === g.food.y;\n const snake = ate ? [head, ...g.snake] : [head, ...g.snake.slice(0, -1)];\n const newScore = ate ? g.score + 1 : g.score;\n\n if (ate) {\n if (newScore > highScoreRef.current) {\n highScoreRef.current = newScore;\n setSnakeHighScore(newScore, settingsFile);\n setHighScore(newScore);\n }\n if (music) playSystemSound(SYSTEM_SOUNDS.eat, sfxVolume);\n } else {\n if (music) playNote(69, tinkVolume, cacheDir);\n }\n\n stateRef.current = {\n ...g,\n snake,\n dir,\n dirQueue: restQueue,\n food: ate ? randomFood(snake, width, height) : g.food,\n score: newScore,\n };\n forceUpdate();\n }, tickMs);\n\n return () => clearInterval(id);\n }, [bpm]);\n\n useInput((input, key) => {\n if (showSettings) return;\n\n const g = stateRef.current;\n\n if (pressed(kb.quit, input, key) && onExit) { onExit(); return; }\n\n if (pressed(kb.music, input, key) && music) {\n if (g.started && !g.gameOver && !g.paused) {\n stateRef.current = { ...g, paused: true };\n forceUpdate();\n }\n setShowSettings(true);\n return;\n }\n\n if (music) {\n const goToTrack = (entry: MidiTrack) => {\n const urls = freemidiUrls(entry);\n setTrack({ title: entry.title, artist: entry.artist, url: urls.getter, downloadPage: urls.downloadPage });\n };\n if (pressed(kb.nextTrack, input, key)) {\n const entry = randomOtherTrack(catalog, track.url);\n if (entry) goToTrack(entry);\n return;\n }\n if (pressed(kb.prevTrack, input, key)) {\n const idx = catalog.findIndex((t) => freemidiUrls(t).getter === track.url);\n const entry = catalog[(idx - 1 + catalog.length) % catalog.length];\n if (entry) goToTrack(entry);\n return;\n }\n if (pressed(kb.loopTrack, input, key)) {\n const next = !loopRef.current;\n loopRef.current = next;\n setLoopEnabled(next);\n setSnakeLoopEnabled(next, settingsFile);\n return;\n }\n }\n\n if (!g.started || g.gameOver) {\n if (pressed(kb.restart, input, key) || pressed(kb.pause, input, key)) reset();\n return;\n }\n\n if (pressed(kb.pause, input, key)) { stateRef.current = { ...g, paused: !g.paused }; forceUpdate(); return; }\n if (pressed(kb.restart, input, key)) { reset(); return; }\n\n const lastDir = g.dirQueue[g.dirQueue.length - 1] ?? g.dir;\n let next: Dir | null = null;\n if (pressed(kb.up, input, key) && lastDir.dy !== 1) next = DIRS.up;\n else if (pressed(kb.down, input, key) && lastDir.dy !== -1) next = DIRS.down;\n else if (pressed(kb.left, input, key) && lastDir.dx !== 1) next = DIRS.left;\n else if (pressed(kb.right, input, key) && lastDir.dx !== -1) next = DIRS.right;\n if (next && g.dirQueue.length < 2) {\n stateRef.current = { ...g, dirQueue: [...g.dirQueue, next] };\n }\n });\n\n if (showSettings) {\n return (\n <MusicSettings\n initial={{ track, musicVolume, tinkVolume, sfxVolume, loopEnabled }}\n accentColor={c.accent}\n tracks={catalog}\n onApply={(cfg: MusicConfig) => {\n if (cfg.musicVolume !== musicVolume && bgMusic.current) {\n bgMusic.current = setMusicVolume(bgMusic.current, cfg.musicVolume);\n bgMusic.current.proc.on('exit', (_code, signal) => {\n if (signal == null && autoPlayRef.current) {\n if (loopRef.current) { setTrackRevision((r) => r + 1); } else { nextTrackRef.current(); }\n }\n });\n }\n if (cfg.musicVolume !== musicVolume) {\n setSnakeMusicVolume(cfg.musicVolume, settingsFile);\n setMusicVolumeState(cfg.musicVolume);\n }\n if (cfg.tinkVolume !== tinkVolume) {\n setSnakeTinkVolume(cfg.tinkVolume, settingsFile);\n setTinkVolumeState(cfg.tinkVolume);\n }\n if (cfg.sfxVolume !== sfxVolume) {\n setSnakeSfxVolume(cfg.sfxVolume, settingsFile);\n setSfxVolumeState(cfg.sfxVolume);\n }\n if (cfg.loopEnabled !== loopEnabled) {\n loopRef.current = cfg.loopEnabled;\n setLoopEnabled(cfg.loopEnabled);\n setSnakeLoopEnabled(cfg.loopEnabled, settingsFile);\n }\n if (cfg.track.url !== track.url) setTrack(cfg.track);\n setShowSettings(false);\n }}\n onCancel={() => setShowSettings(false)}\n />\n );\n }\n\n const { snake, food, score, gameOver, paused, started } = stateRef.current;\n\n const cells = Array.from({ length: height }, (_, y) =>\n Array.from({ length: width }, (_, x) => {\n if (x === snake[0]?.x && y === snake[0]?.y) return 'head';\n if (snake.some((s) => s.x === x && s.y === y)) return 'body';\n if (x === food.x && y === food.y) return 'food';\n return 'empty';\n }),\n );\n\n return (\n <Box flexDirection=\"column\" paddingX={1}>\n <Box gap={2}>\n <Text bold color={c.accent}>Snake</Text>\n <Text color=\"white\">Score: <Text bold>{score}</Text></Text>\n {highScore > 0 && <Text color=\"white\">Best: <Text bold>{highScore}</Text></Text>}\n {paused && <Text color=\"yellow\"> PAUSED</Text>}\n </Box>\n {music && (\n <>\n <Box gap={1}>\n <Text dimColor>♪</Text>\n <Text dimColor>{track.title}</Text>\n <Text dimColor>—</Text>\n <Text dimColor>{track.artist}</Text>\n {loopEnabled && <Text dimColor>⟳</Text>}\n </Box>\n <MusicVisualizer beatPhase={beatPhase} active={started && !paused && !gameOver} beatColors={c.beat} />\n </>\n )}\n <Box height={1} />\n <Box flexDirection=\"column\">\n <Text dimColor>{'┌' + '──'.repeat(width) + '┐'}</Text>\n {cells.map((row, y) => (\n <Box key={y}>\n <Text dimColor>│</Text>\n {row.map((cell, x) => {\n if (cell === 'head') return <Text key={x} color={c.head} bold>{'● '}</Text>;\n if (cell === 'body') return <Text key={x} color={c.body}>{'● '}</Text>;\n if (cell === 'food') return <Text key={x} color={c.food}>{'◆ '}</Text>;\n return <Text key={x} dimColor>{'· '}</Text>;\n })}\n <Text dimColor>│</Text>\n </Box>\n ))}\n <Text dimColor>{'└' + '──'.repeat(width) + '┘'}</Text>\n </Box>\n <Box height={1} />\n {!started && (\n <Text dimColor>\n Press <Text bold color={c.accent}>Space</Text> or{' '}\n <Text bold color={c.accent}>R</Text> to start\n </Text>\n )}\n {gameOver && (\n <Text>\n <Text color=\"red\">Game over! </Text>\n <Text dimColor>Press </Text>\n <Text bold color={c.accent}>R</Text>\n <Text dimColor> to restart</Text>\n </Text>\n )}\n {started && !gameOver && (\n <Text dimColor>\n {displayKey(kb.up[0]!)}/{displayKey(kb.down[0]!)}/{displayKey(kb.left[0]!)}/{displayKey(kb.right[0]!)} to move\n {' · '}{displayKey(kb.pause[0]!)} to pause\n {' · '}{displayKey(kb.restart[0]!)} to restart\n {music ? ` · ${displayKey(kb.music[0]!)} menu` : ''}\n </Text>\n )}\n {!started && music && <Text dimColor>{displayKey(kb.music[0]!)} to change music</Text>}\n {onExit && <Text dimColor>Press <Text bold>{displayKey(kb.quit[0]!)}</Text> to exit</Text>}\n </Box>\n );\n};\n"]}
|
package/dist/src/settings.d.ts
CHANGED
|
@@ -9,3 +9,13 @@ export declare function getSnakeTinkVolume(configPath?: string): number;
|
|
|
9
9
|
export declare function setSnakeTinkVolume(volume: number, configPath?: string): void;
|
|
10
10
|
export declare function getSnakeSfxVolume(configPath?: string): number;
|
|
11
11
|
export declare function setSnakeSfxVolume(volume: number, configPath?: string): void;
|
|
12
|
+
export declare function getSnakeLoopEnabled(configPath?: string): boolean;
|
|
13
|
+
export declare function setSnakeLoopEnabled(enabled: boolean, configPath?: string): void;
|
|
14
|
+
export interface LastTrack {
|
|
15
|
+
title: string;
|
|
16
|
+
artist: string;
|
|
17
|
+
url: string;
|
|
18
|
+
downloadPage?: string;
|
|
19
|
+
}
|
|
20
|
+
export declare function getSnakeLastTrack(configPath?: string): LastTrack | null;
|
|
21
|
+
export declare function setSnakeLastTrack(track: LastTrack, configPath?: string): void;
|
package/dist/src/settings.js
CHANGED
|
@@ -54,4 +54,31 @@ export function setSnakeSfxVolume(volume, configPath) {
|
|
|
54
54
|
config['sfx_volume'] = Math.max(0, Math.min(1, volume));
|
|
55
55
|
writeConfig(config, configPath);
|
|
56
56
|
}
|
|
57
|
+
export function getSnakeLoopEnabled(configPath) {
|
|
58
|
+
return readConfig(configPath)['loop_enabled'] === true;
|
|
59
|
+
}
|
|
60
|
+
export function setSnakeLoopEnabled(enabled, configPath) {
|
|
61
|
+
const config = readConfig(configPath);
|
|
62
|
+
config['loop_enabled'] = enabled;
|
|
63
|
+
writeConfig(config, configPath);
|
|
64
|
+
}
|
|
65
|
+
export function getSnakeLastTrack(configPath) {
|
|
66
|
+
const t = readConfig(configPath)['last_track'];
|
|
67
|
+
if (!t || typeof t !== 'object')
|
|
68
|
+
return null;
|
|
69
|
+
const track = t;
|
|
70
|
+
if (typeof track['url'] !== 'string' || !track['url'])
|
|
71
|
+
return null;
|
|
72
|
+
return {
|
|
73
|
+
title: typeof track['title'] === 'string' ? track['title'] : '',
|
|
74
|
+
artist: typeof track['artist'] === 'string' ? track['artist'] : '',
|
|
75
|
+
url: track['url'],
|
|
76
|
+
downloadPage: typeof track['downloadPage'] === 'string' ? track['downloadPage'] : undefined,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
export function setSnakeLastTrack(track, configPath) {
|
|
80
|
+
const config = readConfig(configPath);
|
|
81
|
+
config['last_track'] = track;
|
|
82
|
+
writeConfig(config, configPath);
|
|
83
|
+
}
|
|
57
84
|
//# sourceMappingURL=settings.js.map
|
package/dist/src/settings.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"settings.js","sourceRoot":"","sources":["../../src/settings.ts"],"names":[],"mappings":"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","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"]}
|
|
1
|
+
{"version":3,"file":"settings.js","sourceRoot":"","sources":["../../src/settings.ts"],"names":[],"mappings":"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","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\nexport function getSnakeLoopEnabled(configPath?: string): boolean {\n return readConfig(configPath)['loop_enabled'] === true;\n}\n\nexport function setSnakeLoopEnabled(enabled: boolean, configPath?: string): void {\n const config = readConfig(configPath);\n config['loop_enabled'] = enabled;\n writeConfig(config, configPath);\n}\n\nexport interface LastTrack {\n title: string;\n artist: string;\n url: string;\n downloadPage?: string;\n}\n\nexport function getSnakeLastTrack(configPath?: string): LastTrack | null {\n const t = readConfig(configPath)['last_track'];\n if (!t || typeof t !== 'object') return null;\n const track = t as Record<string, unknown>;\n if (typeof track['url'] !== 'string' || !track['url']) return null;\n return {\n title: typeof track['title'] === 'string' ? track['title'] : '',\n artist: typeof track['artist'] === 'string' ? track['artist'] : '',\n url: track['url'],\n downloadPage: typeof track['downloadPage'] === 'string' ? track['downloadPage'] : undefined,\n };\n}\n\nexport function setSnakeLastTrack(track: LastTrack, configPath?: string): void {\n const config = readConfig(configPath);\n config['last_track'] = track;\n writeConfig(config, configPath);\n}\n"]}
|
package/dist/src/types.d.ts
CHANGED
|
@@ -19,6 +19,8 @@ export interface SnakeKeybindings {
|
|
|
19
19
|
nextTrack?: string[];
|
|
20
20
|
/** Keys to skip to previous track (default: ['[']) */
|
|
21
21
|
prevTrack?: string[];
|
|
22
|
+
/** Keys to toggle track loop (default: ['l']) */
|
|
23
|
+
loopTrack?: string[];
|
|
22
24
|
}
|
|
23
25
|
export declare const DEFAULT_KEYBINDINGS: Required<SnakeKeybindings>;
|
|
24
26
|
export declare function resolveKeybindings(kb?: SnakeKeybindings): Required<SnakeKeybindings>;
|
package/dist/src/types.js
CHANGED
|
@@ -9,6 +9,7 @@ export const DEFAULT_KEYBINDINGS = {
|
|
|
9
9
|
quit: ['q'],
|
|
10
10
|
nextTrack: [']'],
|
|
11
11
|
prevTrack: ['['],
|
|
12
|
+
loopTrack: ['l'],
|
|
12
13
|
};
|
|
13
14
|
export function resolveKeybindings(kb) {
|
|
14
15
|
if (!kb)
|
|
@@ -24,6 +25,7 @@ export function resolveKeybindings(kb) {
|
|
|
24
25
|
quit: kb.quit ?? DEFAULT_KEYBINDINGS.quit,
|
|
25
26
|
nextTrack: kb.nextTrack ?? DEFAULT_KEYBINDINGS.nextTrack,
|
|
26
27
|
prevTrack: kb.prevTrack ?? DEFAULT_KEYBINDINGS.prevTrack,
|
|
28
|
+
loopTrack: kb.loopTrack ?? DEFAULT_KEYBINDINGS.loopTrack,
|
|
27
29
|
};
|
|
28
30
|
}
|
|
29
31
|
export const DEFAULT_COLORS = {
|
package/dist/src/types.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/types.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/types.ts"],"names":[],"mappings":"AAyBA,MAAM,CAAC,MAAM,mBAAmB,GAA+B;IAC7D,EAAE,EAAS,CAAC,GAAG,EAAE,IAAI,CAAC;IACtB,IAAI,EAAO,CAAC,GAAG,EAAE,MAAM,CAAC;IACxB,IAAI,EAAO,CAAC,GAAG,EAAE,MAAM,CAAC;IACxB,KAAK,EAAM,CAAC,GAAG,EAAE,OAAO,CAAC;IACzB,KAAK,EAAM,CAAC,GAAG,CAAC;IAChB,OAAO,EAAI,CAAC,GAAG,CAAC;IAChB,KAAK,EAAM,CAAC,GAAG,CAAC;IAChB,IAAI,EAAO,CAAC,GAAG,CAAC;IAChB,SAAS,EAAE,CAAC,GAAG,CAAC;IAChB,SAAS,EAAE,CAAC,GAAG,CAAC;IAChB,SAAS,EAAE,CAAC,GAAG,CAAC;CACjB,CAAC;AAEF,MAAM,UAAU,kBAAkB,CAAC,EAAqB;IACtD,IAAI,CAAC,EAAE;QAAE,OAAO,mBAAmB,CAAC;IACpC,OAAO;QACL,EAAE,EAAO,EAAE,CAAC,EAAE,IAAS,mBAAmB,CAAC,EAAE;QAC7C,IAAI,EAAK,EAAE,CAAC,IAAI,IAAO,mBAAmB,CAAC,IAAI;QAC/C,IAAI,EAAK,EAAE,CAAC,IAAI,IAAO,mBAAmB,CAAC,IAAI;QAC/C,KAAK,EAAI,EAAE,CAAC,KAAK,IAAM,mBAAmB,CAAC,KAAK;QAChD,KAAK,EAAI,EAAE,CAAC,KAAK,IAAM,mBAAmB,CAAC,KAAK;QAChD,OAAO,EAAE,EAAE,CAAC,OAAO,IAAI,mBAAmB,CAAC,OAAO;QAClD,KAAK,EAAM,EAAE,CAAC,KAAK,IAAQ,mBAAmB,CAAC,KAAK;QACpD,IAAI,EAAO,EAAE,CAAC,IAAI,IAAS,mBAAmB,CAAC,IAAI;QACnD,SAAS,EAAE,EAAE,CAAC,SAAS,IAAI,mBAAmB,CAAC,SAAS;QACxD,SAAS,EAAE,EAAE,CAAC,SAAS,IAAI,mBAAmB,CAAC,SAAS;QACxD,SAAS,EAAE,EAAE,CAAC,SAAS,IAAI,mBAAmB,CAAC,SAAS;KACzD,CAAC;AACJ,CAAC;AAeD,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 SnakeKeybindings {\n /** Keys to move up (default: ['w', 'up']) */\n up?: string[];\n /** Keys to move down (default: ['s', 'down']) */\n down?: string[];\n /** Keys to move left (default: ['a', 'left']) */\n left?: string[];\n /** Keys to move right (default: ['d', 'right']) */\n right?: string[];\n /** Keys to pause (default: [' ']) */\n pause?: string[];\n /** Keys to restart (default: ['r']) */\n restart?: string[];\n /** Keys to open music settings (default: ['m']) */\n music?: string[];\n /** Keys to quit (default: ['q']) */\n quit?: string[];\n /** Keys to skip to next track (default: [']']) */\n nextTrack?: string[];\n /** Keys to skip to previous track (default: ['[']) */\n prevTrack?: string[];\n /** Keys to toggle track loop (default: ['l']) */\n loopTrack?: string[];\n}\n\nexport const DEFAULT_KEYBINDINGS: Required<SnakeKeybindings> = {\n up: ['w', 'up'],\n down: ['s', 'down'],\n left: ['a', 'left'],\n right: ['d', 'right'],\n pause: [' '],\n restart: ['r'],\n music: ['m'],\n quit: ['q'],\n nextTrack: [']'],\n prevTrack: ['['],\n loopTrack: ['l'],\n};\n\nexport function resolveKeybindings(kb?: SnakeKeybindings): Required<SnakeKeybindings> {\n if (!kb) return DEFAULT_KEYBINDINGS;\n return {\n up: kb.up ?? DEFAULT_KEYBINDINGS.up,\n down: kb.down ?? DEFAULT_KEYBINDINGS.down,\n left: kb.left ?? DEFAULT_KEYBINDINGS.left,\n right: kb.right ?? DEFAULT_KEYBINDINGS.right,\n pause: kb.pause ?? DEFAULT_KEYBINDINGS.pause,\n restart: kb.restart ?? DEFAULT_KEYBINDINGS.restart,\n music: kb.music ?? DEFAULT_KEYBINDINGS.music,\n quit: kb.quit ?? DEFAULT_KEYBINDINGS.quit,\n nextTrack: kb.nextTrack ?? DEFAULT_KEYBINDINGS.nextTrack,\n prevTrack: kb.prevTrack ?? DEFAULT_KEYBINDINGS.prevTrack,\n loopTrack: kb.loopTrack ?? DEFAULT_KEYBINDINGS.loopTrack,\n };\n}\n\nexport 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"]}
|