@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 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
+ ```
@@ -20,6 +20,7 @@ export interface MusicConfig {
20
20
  musicVolume: number;
21
21
  tinkVolume: number;
22
22
  sfxVolume: number;
23
+ loopEnabled: boolean;
23
24
  }
24
25
  interface MusicSettingsProps {
25
26
  initial: MusicConfig;
@@ -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(VOICES.length - 1, f + 1));
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
- }), _jsxs(Box, { marginTop: 1, gap: 2, children: [_jsx(Text, { dimColor: true, children: "\u2191\u2193 select voice" }), _jsx(Text, { dimColor: true, children: "\u2190 \u2192 adjust" }), _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) => {
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"]}
@@ -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
- nextTrackRef.current();
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
- nextTrackRef.current();
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":";AAAA;;;;;;;GAOG;AAEH,OAAO,EAAE,GAAG,EAAE,IAAI,EAAE,QAAQ,EAAY,MAAM,KAAK,CAAC;AACpD,OAAO,EAAE,UAAU,EAAE,MAAM,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAC;AAChE,OAAO,EACL,iBAAiB,EACjB,iBAAiB,EACjB,mBAAmB,EACnB,mBAAmB,EACnB,kBAAkB,EAClB,kBAAkB,EAClB,iBAAiB,EACjB,iBAAiB,GAClB,MAAM,eAAe,CAAC;AACvB,OAAO,EACL,SAAS,EAAE,QAAQ,EAAE,eAAe,EAAE,aAAa,EACnD,YAAY,EAAE,WAAW,EAAE,cAAc,GAC1C,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EAAE,aAAa,EAAwC,MAAM,oBAAoB,CAAC;AACzF,OAAO,EAAE,YAAY,EAAE,gBAAgB,EAAE,YAAY,EAAkB,MAAM,uBAAuB,CAAC;AACrG,OAAO,EAAoB,aAAa,EAAyB,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAIxG,MAAM,WAAW,GAAG,GAAG,CAAC;AAExB,0DAA0D;AAC1D,MAAM,aAAa,GAA8B;IAC/C,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,WAAW,EAAE,IAAI,EAAE,WAAW,EAAE,KAAK,EAAE,YAAY;IACxE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,GAAG,EAAE,KAAK;CAC/C,CAAC;AAEF,SAAS,OAAO,CAAC,QAAkB,EAAE,KAAa,EAAE,GAAQ;IAC1D,OAAO,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE;QACzB,MAAM,IAAI,GAAG,aAAa,CAAC,CAAC,CAAC,CAAC;QAC9B,OAAO,IAAI,CAAC,CAAC,CAAE,GAAG,CAAC,IAAI,CAAa,KAAK,IAAI,CAAC,CAAC,CAAC,KAAK,KAAK,CAAC,CAAC;IAC9D,CAAC,CAAC,CAAC;AACL,CAAC;AAED,MAAM,UAAU,GAA2B;IACzC,EAAE,EAAE,GAAG,EAAE,IAAI,EAAE,GAAG,EAAE,IAAI,EAAE,GAAG,EAAE,KAAK,EAAE,GAAG;IACzC,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,GAAG,EAAE,KAAK,EAAE,GAAG,EAAE,OAAO;CACrD,CAAC;AACF,SAAS,gBAAgB,CAAC,OAAoB,EAAE,UAAkB;IAChE,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC,MAAM,KAAK,UAAU,CAAC,CAAC;IAC5E,OAAO,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,MAAM,EAAE,GAAG,MAAM,CAAC,MAAM,CAAC,CAAC,IAAI,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;AAChG,CAAC;AAED,SAAS,UAAU,CAAC,CAAS;IAC3B,OAAO,UAAU,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,WAAW,EAAE,CAAC;AAC1C,CAAC;AAED,SAAS,mBAAmB,CAAC,OAAoB;IAC/C,MAAM,KAAK,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,gBAAgB,CAAC,IAAI,OAAO,CAAC,CAAC,CAAC,CAAC;IAC3E,IAAI,CAAC,KAAK;QAAE,OAAO,EAAE,KAAK,EAAE,WAAW,EAAE,MAAM,EAAE,EAAE,EAAE,GAAG,EAAE,EAAE,EAAE,CAAC;IAC/D,MAAM,IAAI,GAAG,YAAY,CAAC,KAAK,CAAC,CAAC;IACjC,OAAO,EAAE,KAAK,EAAE,KAAK,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,CAAC,MAAM,EAAE,GAAG,EAAE,IAAI,CAAC,MAAM,EAAE,YAAY,EAAE,IAAI,CAAC,YAAY,EAAE,CAAC;AACzG,CAAC;AAED,yEAAyE;AAEzE,SAAS,eAAe,CAAC,EACvB,SAAS,EACT,MAAM,EACN,UAAU,GAKX;IACC,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,SAAS,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC;IAE3C,OAAO,CACL,KAAC,GAAG,cACA,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,CAAW,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE;YACjC,MAAM,SAAS,GAAG,MAAM,IAAI,CAAC,KAAK,IAAI,CAAC;YACvC,OAAO,CACL,MAAC,IAAI,IAAS,KAAK,EAAE,SAAS,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,EAAE,QAAQ,EAAE,CAAC,SAAS,aAC7E,SAAS,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,EAAE,GAAG,KADlB,CAAC,CAEL,CACR,CAAC;QACJ,CAAC,CAAC,GACE,CACP,CAAC;AACJ,CAAC;AAKD,MAAM,IAAI,GAAG;IACX,EAAE,EAAE,EAAE,EAAE,EAAE,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC,EAAE;IACrB,IAAI,EAAE,EAAE,EAAE,EAAE,CAAC,EAAE,EAAE,EAAE,CAAC,EAAE;IACtB,IAAI,EAAE,EAAE,EAAE,EAAE,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,EAAE;IACvB,KAAK,EAAE,EAAE,EAAE,EAAE,CAAC,EAAE,EAAE,EAAE,CAAC,EAAE;CACf,CAAC;AAEX,SAAS,UAAU,CAAC,KAAc,EAAE,CAAS,EAAE,CAAS;IACtD,MAAM,IAAI,GAAY,EAAE,CAAC;IACzB,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC;QAC3B,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC;YAC3B,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC;gBAC/C,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC;YACtB,CAAC;QACH,CAAC;IACH,CAAC;IACD,OAAO,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,MAAM,EAAE,GAAG,IAAI,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC;AACzE,CAAC;AAaD,SAAS,WAAW,CAAC,CAAS,EAAE,CAAS;IACvC,MAAM,EAAE,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;IAC7B,MAAM,EAAE,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;IAC7B,MAAM,KAAK,GAAG,CAAC,EAAE,CAAC,EAAE,EAAE,EAAE,CAAC,EAAE,EAAE,EAAE,EAAE,EAAE,CAAC,EAAE,EAAE,GAAG,CAAC,EAAE,CAAC,EAAE,EAAE,EAAE,EAAE,EAAE,CAAC,EAAE,EAAE,GAAG,CAAC,EAAE,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC;IAC7E,OAAO;QACL,KAAK;QACL,GAAG,EAAE,IAAI,CAAC,KAAK;QACf,QAAQ,EAAE,EAAE;QACZ,IAAI,EAAE,UAAU,CAAC,KAAK,EAAE,CAAC,EAAE,CAAC,CAAC;QAC7B,KAAK,EAAE,CAAC;QACR,QAAQ,EAAE,KAAK;QACf,MAAM,EAAE,KAAK;QACb,OAAO,EAAE,KAAK;KACf,CAAC;AACJ,CAAC;AAoCD,MAAM,CAAC,MAAM,SAAS,GAAG,CAAC,EACxB,MAAM,EACN,KAAK,GAAG,IAAI,EACZ,MAAM,EACN,QAAQ,EACR,YAAY,EACZ,KAAK,GAAG,EAAE,EACV,MAAM,GAAG,EAAE,EACX,MAAM,EACN,WAAW,MACO,EAAE,EAAE,EAAE;IACxB,MAAM,CAAC,GAAG,aAAa,CAAC,MAAM,CAAC,CAAC;IAChC,MAAM,EAAE,GAAG,kBAAkB,CAAC,WAAW,CAAC,CAAC;IAC3C,MAAM,OAAO,GAAG,MAAM,IAAI,YAAY,CAAC;IAEvC,MAAM,QAAQ,GAAG,MAAM,CAAY,WAAW,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC,CAAC;IAC/D,MAAM,CAAC,EAAE,WAAW,CAAC,GAAG,UAAU,CAAC,CAAC,CAAS,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,CAAC;IAE5D,MAAM,YAAY,GAAG,MAAM,CAAC,CAAC,CAAC,CAAC;IAC/B,MAAM,CAAC,SAAS,EAAE,YAAY,CAAC,GAAG,QAAQ,CAAC,CAAC,CAAC,CAAC;IAC9C,MAAM,OAAO,GAAG,MAAM,CAAuB,IAAI,CAAC,CAAC;IACnD,MAAM,CAAC,GAAG,EAAE,MAAM,CAAC,GAAG,QAAQ,CAAC,WAAW,CAAC,CAAC;IAC5C,MAAM,CAAC,KAAK,EAAE,QAAQ,CAAC,GAAG,QAAQ,CAAgB,GAAG,EAAE,CAAC,mBAAmB,CAAC,OAAO,CAAC,CAAC,CAAC;IACtF,MAAM,CAAC,YAAY,EAAE,eAAe,CAAC,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAC;IACxD,MAAM,WAAW,GAAG,MAAM,CAAC,IAAI,CAAC,CAAC;IACjC,MAAM,YAAY,GAAG,MAAM,CAAa,GAAG,EAAE,GAAE,CAAC,CAAC,CAAC;IAClD,MAAM,CAAC,SAAS,EAAE,YAAY,CAAC,GAAG,QAAQ,CAAC,CAAC,CAAC,CAAC;IAC9C,MAAM,CAAC,WAAW,EAAE,mBAAmB,CAAC,GAAG,QAAQ,CAAC,GAAG,EAAE,CAAC,mBAAmB,CAAC,YAAY,CAAC,CAAC,CAAC;IAC7F,MAAM,CAAC,UAAU,EAAG,kBAAkB,CAAC,GAAI,QAAQ,CAAC,GAAG,EAAE,CAAC,kBAAkB,CAAC,YAAY,CAAC,CAAC,CAAC;IAC5F,MAAM,CAAC,SAAS,EAAI,iBAAiB,CAAC,GAAK,QAAQ,CAAC,GAAG,EAAE,CAAC,iBAAiB,CAAC,YAAY,CAAC,CAAC,CAAC;IAE3F,yEAAyE;IACzE,SAAS,CAAC,GAAG,EAAE;QACb,YAAY,CAAC,OAAO,GAAG,GAAG,EAAE;YAC1B,MAAM,KAAK,GAAG,gBAAgB,CAAC,OAAO,EAAE,KAAK,CAAC,GAAG,CAAC,CAAC;YACnD,IAAI,KAAK,EAAE,CAAC;gBACV,MAAM,IAAI,GAAG,YAAY,CAAC,KAAK,CAAC,CAAC;gBACjC,QAAQ,CAAC,EAAE,KAAK,EAAE,KAAK,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,CAAC,MAAM,EAAE,GAAG,EAAE,IAAI,CAAC,MAAM,EAAE,YAAY,EAAE,IAAI,CAAC,YAAY,EAAE,CAAC,CAAC;YAC5G,CAAC;QACH,CAAC,CAAC;IACJ,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,CAAC,CAAC,CAAC,CAAC,kDAAkD;IAExE,SAAS,CAAC,GAAG,EAAE;QACb,IAAI,CAAC,KAAK;YAAE,OAAO;QACnB,SAAS,CAAC,CAAC,EAAE,CAAC,EAAE,QAAQ,CAAC,CAAC;QAC1B,WAAW,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC;QAC7B,OAAO,CAAC,OAAO,GAAG,IAAI,CAAC;QACvB,MAAM,CAAC,WAAW,CAAC,CAAC;QACpB,WAAW,CAAC,OAAO,GAAG,IAAI,CAAC;QAC3B,IAAI,SAAS,GAAG,KAAK,CAAC;QACtB,KAAK,YAAY,CAAC,KAAK,CAAC,GAAG,EAAE,KAAK,CAAC,YAAY,EAAE,WAAW,EAAE,QAAQ,CAAC,CAAC,IAAI,CAAC,CAAC,MAAM,EAAE,EAAE;YACtF,IAAI,SAAS,EAAE,CAAC;gBAAC,WAAW,CAAC,MAAM,CAAC,CAAC;gBAAC,OAAO;YAAC,CAAC;YAC/C,OAAO,CAAC,OAAO,GAAG,MAAM,CAAC;YACzB,IAAI,MAAM,EAAE,CAAC;gBACX,MAAM,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;gBACnB,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC,MAAM,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE;oBACvC,IAAI,MAAM,IAAI,IAAI,IAAI,WAAW,CAAC,OAAO;wBAAE,YAAY,CAAC,OAAO,EAAE,CAAC;gBACpE,CAAC,CAAC,CAAC;YACL,CAAC;QACH,CAAC,CAAC,CAAC;QACH,OAAO,GAAG,EAAE;YACV,SAAS,GAAG,IAAI,CAAC;YACjB,WAAW,CAAC,OAAO,GAAG,KAAK,CAAC;YAC5B,WAAW,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC;YAC7B,OAAO,CAAC,OAAO,GAAG,IAAI,CAAC;QACzB,CAAC,CAAC;IACJ,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC,CAAC,CAAC,kDAAkD;IAEtE,SAAS,CAAC,GAAG,EAAE;QACb,MAAM,KAAK,GAAG,iBAAiB,CAAC,YAAY,CAAC,CAAC;QAC9C,YAAY,CAAC,OAAO,GAAG,KAAK,CAAC;QAC7B,YAAY,CAAC,KAAK,CAAC,CAAC;IACtB,CAAC,EAAE,EAAE,CAAC,CAAC;IAEP,MAAM,KAAK,GAAG,GAAG,EAAE;QACjB,QAAQ,CAAC,OAAO,GAAG,EAAE,GAAG,WAAW,CAAC,KAAK,EAAE,MAAM,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;QACpE,WAAW,EAAE,CAAC;IAChB,CAAC,CAAC;IAEF,qEAAqE;IACrE,SAAS,CAAC,GAAG,EAAE;QACb,IAAI,CAAC,KAAK;YAAE,OAAO;QACnB,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,MAAM,GAAG,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC;QAC9C,MAAM,EAAE,GAAG,WAAW,CAAC,GAAG,EAAE,CAAC,YAAY,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,CAAC,EAAE,MAAM,CAAC,CAAC;QACxE,OAAO,GAAG,EAAE,CAAC,aAAa,CAAC,EAAE,CAAC,CAAC;IACjC,CAAC,EAAE,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC,CAAC;IAEjB,6EAA6E;IAC7E,SAAS,CAAC,GAAG,EAAE;QACb,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,MAAM,GAAG,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,gBAAgB;QAC/D,MAAM,EAAE,GAAG,WAAW,CAAC,GAAG,EAAE;YAC1B,MAAM,CAAC,GAAG,QAAQ,CAAC,OAAO,CAAC;YAC3B,IAAI,CAAC,CAAC,CAAC,OAAO,IAAI,CAAC,CAAC,QAAQ,IAAI,CAAC,CAAC,MAAM;gBAAE,OAAO;YAEjD,MAAM,CAAC,OAAO,EAAE,GAAG,SAAS,CAAC,GAAG,CAAC,CAAC,QAAQ,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC;YAC7E,MAAM,GAAG,GAAG,OAAO,CAAC;YACpB,MAAM,IAAI,GAAG,EAAE,CAAC,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,GAAG,CAAC,EAAE,EAAE,CAAC,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,GAAG,CAAC,EAAE,EAAE,CAAC;YAEpE,IACE,IAAI,CAAC,CAAC,GAAG,CAAC;gBACV,IAAI,CAAC,CAAC,IAAI,KAAK;gBACf,IAAI,CAAC,CAAC,GAAG,CAAC;gBACV,IAAI,CAAC,CAAC,IAAI,MAAM;gBAChB,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,KAAK,IAAI,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,IAAI,CAAC,CAAC,CAAC,EACrD,CAAC;gBACD,QAAQ,CAAC,OAAO,GAAG,EAAE,GAAG,CAAC,EAAE,GAAG,EAAE,QAAQ,EAAE,SAAS,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;gBACtE,IAAI,KAAK;oBAAE,eAAe,CAAC,aAAa,CAAC,GAAG,EAAE,SAAS,CAAC,CAAC;gBACzD,WAAW,EAAE,CAAC;gBACd,OAAO;YACT,CAAC;YAED,MAAM,GAAG,GAAG,IAAI,CAAC,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,CAAC,IAAI,IAAI,CAAC,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC;YACvD,MAAM,KAAK,GAAG,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC;YACzE,MAAM,QAAQ,GAAG,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC;YAE7C,IAAI,GAAG,EAAE,CAAC;gBACR,IAAI,QAAQ,GAAG,YAAY,CAAC,OAAO,EAAE,CAAC;oBACpC,YAAY,CAAC,OAAO,GAAG,QAAQ,CAAC;oBAChC,iBAAiB,CAAC,QAAQ,EAAE,YAAY,CAAC,CAAC;oBAC1C,YAAY,CAAC,QAAQ,CAAC,CAAC;gBACzB,CAAC;gBACD,IAAI,KAAK;oBAAE,eAAe,CAAC,aAAa,CAAC,GAAG,EAAE,SAAS,CAAC,CAAC;YAC3D,CAAC;iBAAM,CAAC;gBACN,IAAI,KAAK;oBAAE,QAAQ,CAAC,EAAE,EAAE,UAAU,EAAE,QAAQ,CAAC,CAAC;YAChD,CAAC;YAED,QAAQ,CAAC,OAAO,GAAG;gBACjB,GAAG,CAAC;gBACJ,KAAK;gBACL,GAAG;gBACH,QAAQ,EAAE,SAAS;gBACnB,IAAI,EAAE,GAAG,CAAC,CAAC,CAAC,UAAU,CAAC,KAAK,EAAE,KAAK,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI;gBACrD,KAAK,EAAE,QAAQ;aAChB,CAAC;YACF,WAAW,EAAE,CAAC;QAChB,CAAC,EAAE,MAAM,CAAC,CAAC;QAEX,OAAO,GAAG,EAAE,CAAC,aAAa,CAAC,EAAE,CAAC,CAAC;IACjC,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;IAEV,QAAQ,CAAC,CAAC,KAAK,EAAE,GAAG,EAAE,EAAE;QACtB,IAAI,YAAY;YAAE,OAAO;QAEzB,MAAM,CAAC,GAAG,QAAQ,CAAC,OAAO,CAAC;QAE3B,IAAI,OAAO,CAAC,EAAE,CAAC,IAAI,EAAE,KAAK,EAAE,GAAG,CAAC,IAAI,MAAM,EAAE,CAAC;YAAC,MAAM,EAAE,CAAC;YAAC,OAAO;QAAC,CAAC;QAEjE,IAAI,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,EAAE,GAAG,CAAC,IAAI,KAAK,EAAE,CAAC;YAC3C,IAAI,CAAC,CAAC,OAAO,IAAI,CAAC,CAAC,CAAC,QAAQ,IAAI,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC;gBAC1C,QAAQ,CAAC,OAAO,GAAG,EAAE,GAAG,CAAC,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC;gBAC1C,WAAW,EAAE,CAAC;YAChB,CAAC;YACD,eAAe,CAAC,IAAI,CAAC,CAAC;YACtB,OAAO;QACT,CAAC;QAED,IAAI,KAAK,EAAE,CAAC;YACV,MAAM,SAAS,GAAG,CAAC,KAAgB,EAAE,EAAE;gBACrC,MAAM,IAAI,GAAG,YAAY,CAAC,KAAK,CAAC,CAAC;gBACjC,QAAQ,CAAC,EAAE,KAAK,EAAE,KAAK,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,CAAC,MAAM,EAAE,GAAG,EAAE,IAAI,CAAC,MAAM,EAAE,YAAY,EAAE,IAAI,CAAC,YAAY,EAAE,CAAC,CAAC;YAC5G,CAAC,CAAC;YACF,IAAI,OAAO,CAAC,EAAE,CAAC,SAAS,EAAE,KAAK,EAAE,GAAG,CAAC,EAAE,CAAC;gBACtC,MAAM,KAAK,GAAG,gBAAgB,CAAC,OAAO,EAAE,KAAK,CAAC,GAAG,CAAC,CAAC;gBACnD,IAAI,KAAK;oBAAE,SAAS,CAAC,KAAK,CAAC,CAAC;gBAC5B,OAAO;YACT,CAAC;YACD,IAAI,OAAO,CAAC,EAAE,CAAC,SAAS,EAAE,KAAK,EAAE,GAAG,CAAC,EAAE,CAAC;gBACtC,MAAM,GAAG,GAAG,OAAO,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC,MAAM,KAAK,KAAK,CAAC,GAAG,CAAC,CAAC;gBAC3E,MAAM,KAAK,GAAG,OAAO,CAAC,CAAC,GAAG,GAAG,CAAC,GAAG,OAAO,CAAC,MAAM,CAAC,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;gBACnE,IAAI,KAAK;oBAAE,SAAS,CAAC,KAAK,CAAC,CAAC;gBAC5B,OAAO;YACT,CAAC;QACH,CAAC;QAED,IAAI,CAAC,CAAC,CAAC,OAAO,IAAI,CAAC,CAAC,QAAQ,EAAE,CAAC;YAC7B,IAAI,OAAO,CAAC,EAAE,CAAC,OAAO,EAAE,KAAK,EAAE,GAAG,CAAC,IAAI,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,EAAE,GAAG,CAAC;gBAAE,KAAK,EAAE,CAAC;YAC9E,OAAO;QACT,CAAC;QAED,IAAI,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,EAAE,GAAG,CAAC,EAAE,CAAC;YAAC,QAAQ,CAAC,OAAO,GAAG,EAAE,GAAG,CAAC,EAAE,MAAM,EAAE,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC;YAAC,WAAW,EAAE,CAAC;YAAC,OAAO;QAAC,CAAC;QAC7G,IAAI,OAAO,CAAC,EAAE,CAAC,OAAO,EAAE,KAAK,EAAE,GAAG,CAAC,EAAE,CAAC;YAAC,KAAK,EAAE,CAAC;YAAC,OAAO;QAAC,CAAC;QAEzD,MAAM,OAAO,GAAG,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,QAAQ,CAAC,MAAM,GAAG,CAAC,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC;QAC3D,IAAI,IAAI,GAAe,IAAI,CAAC;QAC5B,IAAI,OAAO,CAAC,EAAE,CAAC,EAAE,EAAE,KAAK,EAAE,GAAG,CAAC,IAAI,OAAO,CAAC,EAAE,KAAK,CAAC;YAAQ,IAAI,GAAG,IAAI,CAAC,EAAE,CAAC;aACpE,IAAI,OAAO,CAAC,EAAE,CAAC,IAAI,EAAE,KAAK,EAAE,GAAG,CAAC,IAAI,OAAO,CAAC,EAAE,KAAK,CAAC,CAAC;YAAG,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC;aACzE,IAAI,OAAO,CAAC,EAAE,CAAC,IAAI,EAAE,KAAK,EAAE,GAAG,CAAC,IAAI,OAAO,CAAC,EAAE,KAAK,CAAC;YAAI,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC;aACzE,IAAI,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,EAAE,GAAG,CAAC,IAAI,OAAO,CAAC,EAAE,KAAK,CAAC,CAAC;YAAE,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC;QAC/E,IAAI,IAAI,IAAI,CAAC,CAAC,QAAQ,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YAClC,QAAQ,CAAC,OAAO,GAAG,EAAE,GAAG,CAAC,EAAE,QAAQ,EAAE,CAAC,GAAG,CAAC,CAAC,QAAQ,EAAE,IAAI,CAAC,EAAE,CAAC;QAC/D,CAAC;IACH,CAAC,CAAC,CAAC;IAEH,IAAI,YAAY,EAAE,CAAC;QACjB,OAAO,CACL,KAAC,aAAa,IACZ,OAAO,EAAE,EAAE,KAAK,EAAE,WAAW,EAAE,UAAU,EAAE,SAAS,EAAE,EACtD,WAAW,EAAE,CAAC,CAAC,MAAM,EACrB,MAAM,EAAE,OAAO,EACf,OAAO,EAAE,CAAC,GAAgB,EAAE,EAAE;gBAC5B,IAAI,GAAG,CAAC,WAAW,KAAK,WAAW,IAAI,OAAO,CAAC,OAAO,EAAE,CAAC;oBACvD,OAAO,CAAC,OAAO,GAAG,cAAc,CAAC,OAAO,CAAC,OAAO,EAAE,GAAG,CAAC,WAAW,CAAC,CAAC;oBACnE,OAAO,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC,MAAM,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE;wBAChD,IAAI,MAAM,IAAI,IAAI,IAAI,WAAW,CAAC,OAAO;4BAAE,YAAY,CAAC,OAAO,EAAE,CAAC;oBACpE,CAAC,CAAC,CAAC;gBACL,CAAC;gBACD,IAAI,GAAG,CAAC,WAAW,KAAK,WAAW,EAAE,CAAC;oBACpC,mBAAmB,CAAC,GAAG,CAAC,WAAW,EAAE,YAAY,CAAC,CAAC;oBACnD,mBAAmB,CAAC,GAAG,CAAC,WAAW,CAAC,CAAC;gBACvC,CAAC;gBACD,IAAI,GAAG,CAAC,UAAU,KAAK,UAAU,EAAE,CAAC;oBAClC,kBAAkB,CAAC,GAAG,CAAC,UAAU,EAAE,YAAY,CAAC,CAAC;oBACjD,kBAAkB,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC;gBACrC,CAAC;gBACD,IAAI,GAAG,CAAC,SAAS,KAAK,SAAS,EAAE,CAAC;oBAChC,iBAAiB,CAAC,GAAG,CAAC,SAAS,EAAE,YAAY,CAAC,CAAC;oBAC/C,iBAAiB,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;gBACnC,CAAC;gBACD,IAAI,GAAG,CAAC,KAAK,CAAC,GAAG,KAAK,KAAK,CAAC,GAAG;oBAAE,QAAQ,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;gBACrD,eAAe,CAAC,KAAK,CAAC,CAAC;YACzB,CAAC,EACD,QAAQ,EAAE,GAAG,EAAE,CAAC,eAAe,CAAC,KAAK,CAAC,GACtC,CACH,CAAC;IACJ,CAAC;IAED,MAAM,EAAE,KAAK,EAAE,IAAI,EAAE,KAAK,EAAE,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,GAAG,QAAQ,CAAC,OAAO,CAAC;IAE3E,MAAM,KAAK,GAAG,KAAK,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CACpD,KAAK,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,KAAK,EAAE,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE;QACrC,IAAI,CAAC,KAAK,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC,IAAI,CAAC,KAAK,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC;YAAE,OAAO,MAAM,CAAC;QAC1D,IAAI,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC;YAAE,OAAO,MAAM,CAAC;QAC7D,IAAI,CAAC,KAAK,IAAI,CAAC,CAAC,IAAI,CAAC,KAAK,IAAI,CAAC,CAAC;YAAE,OAAO,MAAM,CAAC;QAChD,OAAO,OAAO,CAAC;IACjB,CAAC,CAAC,CACH,CAAC;IAEF,OAAO,CACL,MAAC,GAAG,IAAC,aAAa,EAAC,QAAQ,EAAC,QAAQ,EAAE,CAAC,aACrC,MAAC,GAAG,IAAC,GAAG,EAAE,CAAC,aACT,KAAC,IAAI,IAAC,IAAI,QAAC,KAAK,EAAE,CAAC,CAAC,MAAM,sBAAc,EACxC,MAAC,IAAI,IAAC,KAAK,EAAC,OAAO,wBAAQ,KAAC,IAAI,IAAC,IAAI,kBAAE,KAAK,GAAQ,IAAO,EAC1D,SAAS,GAAG,CAAC,IAAI,MAAC,IAAI,IAAC,KAAK,EAAC,OAAO,uBAAO,KAAC,IAAI,IAAC,IAAI,kBAAE,SAAS,GAAQ,IAAO,EAC/E,MAAM,IAAI,KAAC,IAAI,IAAC,KAAK,EAAC,QAAQ,wBAAe,IAC1C,EACL,KAAK,IAAI,CACR,8BACE,MAAC,GAAG,IAAC,GAAG,EAAE,CAAC,aACT,KAAC,IAAI,IAAC,QAAQ,6BAAS,EACvB,KAAC,IAAI,IAAC,QAAQ,kBAAE,KAAK,CAAC,KAAK,GAAQ,EACnC,KAAC,IAAI,IAAC,QAAQ,6BAAS,EACvB,KAAC,IAAI,IAAC,QAAQ,kBAAE,KAAK,CAAC,MAAM,GAAQ,IAChC,EACN,KAAC,eAAe,IAAC,SAAS,EAAE,SAAS,EAAE,MAAM,EAAE,OAAO,IAAI,CAAC,MAAM,IAAI,CAAC,QAAQ,EAAE,UAAU,EAAE,CAAC,CAAC,IAAI,GAAI,IACrG,CACJ,EACD,KAAC,GAAG,IAAC,MAAM,EAAE,CAAC,GAAI,EAClB,MAAC,GAAG,IAAC,aAAa,EAAC,QAAQ,aACzB,KAAC,IAAI,IAAC,QAAQ,kBAAE,GAAG,GAAG,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,GAAG,GAAG,GAAQ,EACrD,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,CAAC,EAAE,EAAE,CAAC,CACrB,MAAC,GAAG,eACF,KAAC,IAAI,IAAC,QAAQ,6BAAS,EACtB,GAAG,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC,EAAE,EAAE;gCACnB,IAAI,IAAI,KAAK,MAAM;oCAAE,OAAO,KAAC,IAAI,IAAS,KAAK,EAAE,CAAC,CAAC,IAAI,EAAE,IAAI,kBAAE,IAAI,IAA5B,CAAC,CAAmC,CAAC;gCAC5E,IAAI,IAAI,KAAK,MAAM;oCAAE,OAAO,KAAC,IAAI,IAAS,KAAK,EAAE,CAAC,CAAC,IAAI,YAAG,IAAI,IAAvB,CAAC,CAA8B,CAAC;gCACvE,IAAI,IAAI,KAAK,MAAM;oCAAE,OAAO,KAAC,IAAI,IAAS,KAAK,EAAE,CAAC,CAAC,IAAI,YAAG,IAAI,IAAvB,CAAC,CAA8B,CAAC;gCACvE,OAAO,KAAC,IAAI,IAAS,QAAQ,kBAAE,IAAI,IAAjB,CAAC,CAAwB,CAAC;4BAC9C,CAAC,CAAC,EACF,KAAC,IAAI,IAAC,QAAQ,6BAAS,KARf,CAAC,CASL,CACP,CAAC,EACF,KAAC,IAAI,IAAC,QAAQ,kBAAE,GAAG,GAAG,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,GAAG,GAAG,GAAQ,IAClD,EACN,KAAC,GAAG,IAAC,MAAM,EAAE,CAAC,GAAI,EACjB,CAAC,OAAO,IAAI,CACX,MAAC,IAAI,IAAC,QAAQ,6BACN,KAAC,IAAI,IAAC,IAAI,QAAC,KAAK,EAAE,CAAC,CAAC,MAAM,sBAAc,SAAI,GAAG,EACrD,KAAC,IAAI,IAAC,IAAI,QAAC,KAAK,EAAE,CAAC,CAAC,MAAM,kBAAU,iBAC/B,CACR,EACA,QAAQ,IAAI,CACX,MAAC,IAAI,eACH,KAAC,IAAI,IAAC,KAAK,EAAC,KAAK,4BAAmB,EACpC,KAAC,IAAI,IAAC,QAAQ,6BAAc,EAC5B,KAAC,IAAI,IAAC,IAAI,QAAC,KAAK,EAAE,CAAC,CAAC,MAAM,kBAAU,EACpC,KAAC,IAAI,IAAC,QAAQ,kCAAmB,IAC5B,CACR,EACA,OAAO,IAAI,CAAC,QAAQ,IAAI,CACvB,MAAC,IAAI,IAAC,QAAQ,mBACX,UAAU,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAE,CAAC,OAAG,UAAU,CAAC,EAAE,CAAC,IAAI,CAAC,CAAC,CAAE,CAAC,OAAG,UAAU,CAAC,EAAE,CAAC,IAAI,CAAC,CAAC,CAAE,CAAC,OAAG,UAAU,CAAC,EAAE,CAAC,KAAK,CAAC,CAAC,CAAE,CAAC,cACpG,KAAK,EAAE,UAAU,CAAC,EAAE,CAAC,KAAK,CAAC,CAAC,CAAE,CAAC,eAC/B,KAAK,EAAE,UAAU,CAAC,EAAE,CAAC,OAAO,CAAC,CAAC,CAAE,CAAC,iBACjC,KAAK,CAAC,CAAC,CAAC,MAAM,UAAU,CAAC,EAAE,CAAC,KAAK,CAAC,CAAC,CAAE,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,IAC9C,CACR,EACA,CAAC,OAAO,IAAI,KAAK,IAAI,MAAC,IAAI,IAAC,QAAQ,mBAAE,UAAU,CAAC,EAAE,CAAC,KAAK,CAAC,CAAC,CAAE,CAAC,wBAAwB,EACrF,MAAM,IAAI,MAAC,IAAI,IAAC,QAAQ,6BAAO,KAAC,IAAI,IAAC,IAAI,kBAAE,UAAU,CAAC,EAAE,CAAC,IAAI,CAAC,CAAC,CAAE,CAAC,GAAQ,gBAAe,IACtF,CACP,CAAC;AACJ,CAAC,CAAC","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"]}
@@ -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;
@@ -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
@@ -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"]}
@@ -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 = {
@@ -1 +1 @@
1
- {"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/types.ts"],"names":[],"mappings":"AAuBA,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;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;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}\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};\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 };\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"]}
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"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pavus/snake-game",
3
- "version": "1.1.0",
3
+ "version": "1.1.1",
4
4
  "description": "Playable Snake in the terminal with MIDI music",
5
5
  "type": "module",
6
6
  "bin": {