bridges-cli 0.0.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/src/index.tsx ADDED
@@ -0,0 +1,34 @@
1
+ #!/usr/bin/env -S bun run
2
+
3
+ import { Command } from 'commander'
4
+ import { render } from 'ink'
5
+
6
+ import Game from './Game.tsx'
7
+ import { type PuzzleData, samplePuzzles } from './utils/samplePuzzles.ts'
8
+
9
+ type CliOptions = {
10
+ stdout: boolean
11
+ puzzle: string | undefined
12
+ }
13
+
14
+ const program = new Command()
15
+
16
+ program
17
+ .name('bridges')
18
+ .description('Bridges (Hashiwokakero) puzzle game')
19
+ .option('-s, --stdout', 'Output to stdout and exit (for testing)')
20
+ .option('-p, --puzzle <puzzle>', 'Puzzle shorthand encoding')
21
+ .parse(process.argv)
22
+
23
+ const options = program.opts<CliOptions>()
24
+
25
+ let puzzles: PuzzleData[] = samplePuzzles
26
+ let hasCustomPuzzle = false
27
+ if (options.puzzle) {
28
+ hasCustomPuzzle = true
29
+ puzzles = [{ encoding: options.puzzle }, ...samplePuzzles]
30
+ }
31
+
32
+ render(
33
+ <Game puzzles={puzzles} hasCustomPuzzle={hasCustomPuzzle} stdout={options.stdout || false} />
34
+ )
package/src/types.ts ADDED
@@ -0,0 +1,11 @@
1
+ export type HashiNodeData = {
2
+ value: number | '-' | '=' | '#' | ' ' | '|'
3
+ /** Num lines connected on left, undefined if 0. */
4
+ lineLeft?: 1 | 2
5
+ /** Num lines connected on right, undefined if 0. */
6
+ lineRight?: 1 | 2
7
+ /** Num lines connected above, undefined if 0. */
8
+ lineUp?: 1 | 2
9
+ /** Num lines connected below, undefined if 0. */
10
+ lineDown?: 1 | 2
11
+ }
@@ -0,0 +1,169 @@
1
+ import { describe, expect, it } from 'vitest'
2
+
3
+ import { parsePuzzle } from '../parsePuzzle.ts'
4
+
5
+ describe('parsePuzzle', () => {
6
+ describe('encodings without the solution (bridges)', () => {
7
+ it('parses a simple 3x3 puzzle with one node per row', () => {
8
+ const result = parsePuzzle('3x3:a1a.a1a.a1a')
9
+ expect(result).toEqual([
10
+ [{ value: ' ' }, { value: 1 }, { value: ' ' }],
11
+ [{ value: ' ' }, { value: 1 }, { value: ' ' }],
12
+ [{ value: ' ' }, { value: 1 }, { value: ' ' }],
13
+ ])
14
+ })
15
+
16
+ it('parses a 3x3 puzzle with a blank row', () => {
17
+ const result = parsePuzzle('3x3:1a1.c.1a1')
18
+ expect(result).toEqual([
19
+ [{ value: 1 }, { value: ' ' }, { value: 1 }],
20
+ [{ value: ' ' }, { value: ' ' }, { value: ' ' }],
21
+ [{ value: 1 }, { value: ' ' }, { value: 1 }],
22
+ ])
23
+ })
24
+
25
+ it('parses a 3x3 puzzle with a filled row', () => {
26
+ const result = parsePuzzle('3x3:111.c.111')
27
+ expect(result).toEqual([
28
+ [{ value: 1 }, { value: 1 }, { value: 1 }],
29
+ [{ value: ' ' }, { value: ' ' }, { value: ' ' }],
30
+ [{ value: 1 }, { value: 1 }, { value: 1 }],
31
+ ])
32
+ })
33
+
34
+ it('parses a 7x7 puzzle', () => {
35
+ const result = parsePuzzle('7x7:4a3a3a3.a2c4a.3b3b3.g.2b8a4a.d1a3.a1a4a1a')
36
+ expect(result).toEqual([
37
+ [
38
+ { value: 4 },
39
+ { value: ' ' },
40
+ { value: 3 },
41
+ { value: ' ' },
42
+ { value: 3 },
43
+ { value: ' ' },
44
+ { value: 3 },
45
+ ],
46
+ [
47
+ { value: ' ' },
48
+ { value: 2 },
49
+ { value: ' ' },
50
+ { value: ' ' },
51
+ { value: ' ' },
52
+ { value: 4 },
53
+ { value: ' ' },
54
+ ],
55
+ [
56
+ { value: 3 },
57
+ { value: ' ' },
58
+ { value: ' ' },
59
+ { value: 3 },
60
+ { value: ' ' },
61
+ { value: ' ' },
62
+ { value: 3 },
63
+ ],
64
+ [
65
+ { value: ' ' },
66
+ { value: ' ' },
67
+ { value: ' ' },
68
+ { value: ' ' },
69
+ { value: ' ' },
70
+ { value: ' ' },
71
+ { value: ' ' },
72
+ ],
73
+ [
74
+ { value: 2 },
75
+ { value: ' ' },
76
+ { value: ' ' },
77
+ { value: 8 },
78
+ { value: ' ' },
79
+ { value: 4 },
80
+ { value: ' ' },
81
+ ],
82
+ [
83
+ { value: ' ' },
84
+ { value: ' ' },
85
+ { value: ' ' },
86
+ { value: ' ' },
87
+ { value: 1 },
88
+ { value: ' ' },
89
+ { value: 3 },
90
+ ],
91
+ [
92
+ { value: ' ' },
93
+ { value: 1 },
94
+ { value: ' ' },
95
+ { value: 4 },
96
+ { value: ' ' },
97
+ { value: 1 },
98
+ { value: ' ' },
99
+ ],
100
+ ])
101
+ })
102
+ })
103
+
104
+ describe('encodings with solutions (bridges)', () => {
105
+ it('parses a puzzle with a double horizontal bridge', () => {
106
+ const result = parsePuzzle('5x1:2=b2')
107
+ expect(result).toEqual([
108
+ [
109
+ { value: 2, lineRight: 2 },
110
+ { value: '=' },
111
+ { value: '=' },
112
+ { value: '=' },
113
+ { value: 2, lineLeft: 2 },
114
+ ],
115
+ ])
116
+ })
117
+
118
+ it('parses a puzzle with a single horizontal bridge', () => {
119
+ const result = parsePuzzle('5x1:a2-a2')
120
+ expect(result).toEqual([
121
+ [
122
+ { value: ' ' },
123
+ { value: 2, lineRight: 1 },
124
+ { value: '-' },
125
+ { value: '-' },
126
+ { value: 2, lineLeft: 1 },
127
+ ],
128
+ ])
129
+ })
130
+
131
+ it('parses a puzzle with a vertical bridge between nodes', () => {
132
+ const result = parsePuzzle('2x3:a1.a|.a1')
133
+ expect(result).toEqual([
134
+ [{ value: ' ' }, { value: 1, lineDown: 1 }],
135
+ [{ value: ' ' }, { value: '|' }],
136
+ [{ value: ' ' }, { value: 1, lineUp: 1 }],
137
+ ])
138
+ })
139
+
140
+ it('parses a puzzle with a double vertical bridge between nodes', () => {
141
+ const result = parsePuzzle('3x2:1#1.11a')
142
+ expect(result).toEqual([
143
+ [{ value: 1 }, { value: '#' }, { value: 1 }],
144
+ [{ value: 1 }, { value: 1, lineUp: 2 }, { value: ' ' }],
145
+ ])
146
+ })
147
+
148
+ it('parses a puzzle with vertical line followed by space', () => {
149
+ const result = parsePuzzle('4x2:1|b.11b')
150
+ expect(result).toEqual([
151
+ [{ value: 1 }, { value: '|' }, { value: ' ' }, { value: ' ' }],
152
+ [{ value: 1 }, { value: 1, lineUp: 1 }, { value: ' ' }, { value: ' ' }],
153
+ ])
154
+ })
155
+
156
+ it('parses a puzzle with two connected bridges in one row', () => {
157
+ const result = parsePuzzle('5x1:2=3-1')
158
+ expect(result).toEqual([
159
+ [
160
+ { value: 2, lineRight: 2 },
161
+ { value: '=' },
162
+ { value: 3, lineLeft: 2, lineRight: 1 },
163
+ { value: '-' },
164
+ { value: 1, lineLeft: 1 },
165
+ ],
166
+ ])
167
+ })
168
+ })
169
+ })
@@ -0,0 +1,178 @@
1
+ import type { HashiNodeData } from '../types.ts'
2
+
3
+ function letterToNumber(char: string): number {
4
+ return char.charCodeAt(0) - 96
5
+ }
6
+
7
+ /**
8
+ * Parses a puzzle encoding string into a 2D array of HashiNodeData.
9
+ *
10
+ * Encoding format: "WIDTHxHEIGHT:row1.row2.row3..."
11
+ * - Digits (0-9): explicit node values
12
+ * - Letters (a-z): skip positions (a=1, b=2, c=3, etc.)
13
+ * - "-": single horizontal line (connects to adjacent nodes)
14
+ * - "=": double horizontal line (connects to adjacent nodes)
15
+ * - "|": single vertical line (connects to node in row below)
16
+ * - "#": double vertical line (connects to node in row below)
17
+ *
18
+ * The letter repeat rule applies to lines: "-c" means 3 single horizontal lines
19
+ *
20
+ * Example: "3x3:1-1.1a|" creates a 3x3 grid with horizontal and vertical bridges
21
+ */
22
+ export function parsePuzzle(encoding: string): HashiNodeData[][] {
23
+ const parts = encoding.split(':')
24
+ const dimensions = parts[0] || ''
25
+ const rest = parts[1] || ''
26
+
27
+ // Parse string for row width
28
+ const match: RegExpMatchArray | null = dimensions.match(/(\d+)x(\d+)/)
29
+ let numNodes = 0
30
+ if (match !== null && match[1] !== undefined) {
31
+ numNodes = parseInt(match[1], 10)
32
+ }
33
+
34
+ // Split grid data into rows
35
+ const rowStrings = rest.split('.')
36
+ const rows: HashiNodeData[][] = []
37
+
38
+ for (const rowStr of rowStrings) {
39
+ const nodes: HashiNodeData[] = Array(numNodes)
40
+ .fill(null)
41
+ .map(() => ({ value: ' ' }))
42
+ let position = 0
43
+ let i = 0
44
+
45
+ // Parse each character in the encoding
46
+ while (i < rowStr.length && position < numNodes) {
47
+ const char = rowStr[i] ?? ''
48
+ const charCode = char.charCodeAt(0)
49
+
50
+ // Digit: create a node with the numeric value
51
+ if (charCode >= 48 && charCode <= 57) {
52
+ const value = Number(char)
53
+ // Check if previous position has a line node and set lineLeft
54
+ if (
55
+ position > 0 &&
56
+ (nodes[position - 1]!.value === '-' || nodes[position - 1]!.value === '=')
57
+ ) {
58
+ const lineCount: 1 | 2 = nodes[position - 1]!.value === '=' ? 2 : 1
59
+ nodes[position] = { value, lineLeft: lineCount }
60
+ } else {
61
+ nodes[position] = { value }
62
+ }
63
+ position++
64
+ i++
65
+ } else if (char === '-' || char === '=') {
66
+ // Horizontal line: single (-) or double (=)
67
+ const lineCount: 1 | 2 = char === '=' ? 2 : 1
68
+
69
+ // Set lineRight on current position if it's a number node
70
+ if (position > 0 && typeof nodes[position - 1]!.value === 'number') {
71
+ nodes[position - 1] = { ...nodes[position - 1]!, lineRight: lineCount }
72
+ }
73
+
74
+ // Create the line node
75
+ nodes[position] = { value: char as '-' | '=' }
76
+
77
+ // Set lineRight on the line node and lineLeft on next position if it's a number node
78
+ if (position < numNodes - 1 && typeof nodes[position + 1]!.value === 'number') {
79
+ nodes[position + 1] = { ...nodes[position + 1]!, lineLeft: lineCount }
80
+ }
81
+
82
+ // Check if next char is a letter (repeat count)
83
+ const nextChar = rowStr[i + 1]
84
+ if (nextChar && nextChar >= 'a' && nextChar <= 'z') {
85
+ const repeat = letterToNumber(nextChar)
86
+ // Create repeated line nodes (the first is already at 'position')
87
+ // We need to create 'repeat' more nodes starting at position+1
88
+ for (let r = 1; r <= repeat && position + r < numNodes; r++) {
89
+ nodes[position + r] = { value: char as '-' | '=' }
90
+ // Set lineLeft/lineRight on adjacent number nodes
91
+ if (
92
+ position + r > 0 &&
93
+ typeof nodes[position + r - 1]!.value === 'number'
94
+ ) {
95
+ nodes[position + r - 1] = {
96
+ ...nodes[position + r - 1]!,
97
+ lineRight: lineCount,
98
+ }
99
+ }
100
+ if (
101
+ position + r < numNodes - 1 &&
102
+ typeof nodes[position + r + 1]!.value === 'number'
103
+ ) {
104
+ nodes[position + r + 1] = {
105
+ ...nodes[position + r + 1]!,
106
+ lineLeft: lineCount,
107
+ }
108
+ }
109
+ }
110
+ // Set lineLeft on the last line node if next position has a number
111
+ const lastLinePos = position + repeat
112
+ if (
113
+ lastLinePos < numNodes - 1 &&
114
+ typeof nodes[lastLinePos + 1]!.value === 'number'
115
+ ) {
116
+ nodes[lastLinePos + 1] = { ...nodes[lastLinePos + 1]!, lineLeft: lineCount }
117
+ }
118
+ position += repeat + 1
119
+ i += 2
120
+ } else {
121
+ position++
122
+ i++
123
+ }
124
+ } else if (char === '|' || char === '#') {
125
+ // Vertical line: single (|) or double (#)
126
+ // Create the line node (connections handled in second pass)
127
+ // Note: vertical lines do not support letter repeat
128
+ nodes[position] = { value: char as '|' | '#' }
129
+ position++
130
+ i++
131
+ } else {
132
+ // Letter: skip positions based on letter-to-number mapping
133
+ position += letterToNumber(char)
134
+ i++
135
+ }
136
+ }
137
+
138
+ // Fill any remaining undefined positions with empty nodes
139
+ for (let j = 0; j < numNodes; j++) {
140
+ if (nodes[j] === undefined || nodes[j] === null) {
141
+ nodes[j] = { value: ' ' }
142
+ }
143
+ }
144
+
145
+ rows.push(nodes)
146
+ }
147
+
148
+ // Second pass: handle vertical line connections
149
+ // For each vertical line, connect to the number nodes above and below
150
+ for (let rowIdx = 0; rowIdx < rows.length; rowIdx++) {
151
+ const currentRow = rows[rowIdx]!
152
+
153
+ for (let colIdx = 0; colIdx < currentRow.length; colIdx++) {
154
+ const node = currentRow[colIdx]!
155
+ if (node.value === '|' || node.value === '#') {
156
+ const lineCount: 1 | 2 = node.value === '#' ? 2 : 1
157
+
158
+ // Set lineDown on the number node in the row above (if exists)
159
+ if (rowIdx > 0) {
160
+ const nodeAbove = rows[rowIdx - 1]![colIdx]!
161
+ if (typeof nodeAbove.value === 'number') {
162
+ rows[rowIdx - 1]![colIdx] = { ...nodeAbove, lineDown: lineCount }
163
+ }
164
+ }
165
+
166
+ // Set lineUp on the number node in the row below (if exists)
167
+ if (rowIdx < rows.length - 1) {
168
+ const nodeBelow = rows[rowIdx + 1]![colIdx]!
169
+ if (typeof nodeBelow.value === 'number') {
170
+ rows[rowIdx + 1]![colIdx] = { ...nodeBelow, lineUp: lineCount }
171
+ }
172
+ }
173
+ }
174
+ }
175
+ }
176
+
177
+ return rows
178
+ }
@@ -0,0 +1,59 @@
1
+ export interface PuzzleData {
2
+ encoding: string
3
+ solution?: string
4
+ }
5
+
6
+ export const samplePuzzles: PuzzleData[] = [
7
+ {
8
+ encoding: '7x7:4a3a3a3.a2c4a.3b3b3.g.2b8a4a.d1a3.a1a4a1a',
9
+ solution: '7x7:4=3-3=3.#2=b4|.3-a3a#3.c#a##.2=a8=4#.c#1-3.a1-4-1a',
10
+ },
11
+ {
12
+ encoding: '9x9:3c1a3a3.b2b3c.3c2a2b.e4b6.3a4a8b3a.a1a3a2c.3a3a2b1a.c4a5b3.3a5a3b2a',
13
+ solution: '9x9:3-b1a3-3.#a2-a3#a#.3a|a2#2a#.|a|a#4=a6.3-4=8=a3#.|1-3#2a|#.3-3#2#a1#.|a#4=5-a3.3=5-3=a2a'
14
+ },
15
+ { encoding: '9x9:2a5a6b2a.a1d1a2.3a2b1a2a.a4b8a4b.3g3.b3a3b2a.a2a1b3a3.1a3d2a.a3b4a3a2' },
16
+ { encoding: '9x9:4b4a5b3.a2b3a1b.2h.a2a1a4c.4a4a3a2a3.c2a8a4a.h2.b2a1b2a.3b2a4b2' },
17
+ { encoding: '9x9:a3d3a3.1a2a4b1a.a4a4a1b4.d4b3a.3b3b2b.a1f3.6a2a2a3b.c2c2a.3a2b2b3' },
18
+ { encoding: '9x9:a2a3b6a2.b2a3d.a1g.b1c3a3.4c8b3a.e1c.3a3a2d.g1a.b3b4b4' },
19
+ { encoding: '9x9:4b6b2a2.i.3a2a2c6.c3b2b.b5a4c4.f2b.2a8a4c3.c1b2b.b4e3' },
20
+ { encoding: '9x9:a3b5b1a.1b2a2b3.i.2c2a2a6.a3a8a4a1a.5a2a4c5.i.4b4a2b3.a1b4b1a' },
21
+ { encoding: '9x9:3c1a3a3.b2b3c.3c2a2b.e4b6.3a4a8b3a.a1a3a2c.3a3a2b1a.c4a5b3.3a5a3b2a' },
22
+ { encoding: '9x9:4a3b3a1a.a3b4a1a3.4h.b3a3a4a6.a1a1a2a2a.5a8a5a3b.h3.1a2a4b4a.a1a2b3a3' },
23
+ { encoding: '9x9:a1b4b3a.3b4d1.d2a1b.a3a8a3a4a.2c1c3.a4a3a3a4a.b1a3d.1d3b4.a4b4b1a' },
24
+ { encoding: '9x9:2a5a6b2a.a1d1a2.3a2b1a2a.a4b8a4b.3g3.b3a3b2a.a2a1b3a3.1a3d2a.a3b4a3a2' },
25
+ { encoding: '9x9:3a3a2c1.a3d3b.b2b1c.4b2b8a4.a2e2a.4a5b1b2.c1b3b.b3b2a3a.2c2a3a3' },
26
+ { encoding: '9x9:4a2b1b3.a2b3a3b.5a4b1c.h3.b5a4a5b.1h.c1b3a4.b2a3b2a.1b3b2a3' },
27
+ { encoding: '9x9:1a2a3a4a3.a2a4a4a2a.1c2d.a5a4a2b3.b3a8b3a.3b1d3.b1b3a3a.a2b2d.4b4a3b3' },
28
+ { encoding: '9x9:a2b3a4a4.3a4b2a2a.a2a5b4b.5a4d3a.c2a3c.a2d2a4.b3b3a2a.a4a3b2a2.3a2a2b2a' },
29
+ { encoding: '9x9:1a1a4a3a2.a3a2a2a3a.2g3.a2b3b3a.3b4a3b6.a3b4b3a.2g3.a4a2a1a2a.3a2a3a3a3' },
30
+ { encoding: '9x9:2a3a4a3a2.a2a2a2a1a.b1a3d.2d1b2.a3b8b4a.4b4d2.d3a2b.a1a4a1a1a.1a1a2a3a2' },
31
+ { encoding: '9x9:3a4a3a2a2.a2c3a3a.2a1e2.a3b2a1b.4a2b2a1a.a1b2a5a4.b2b3c.3c1a2b.a2a3a3b3' },
32
+ { encoding: '9x9:2b4b5a4.b3b4a2a.f1b.1a2a3c4.a4a3a3a3a.2c4a5a4.b1f.a2a2b3b.3a5b4b3' },
33
+ { encoding: '9x9:4a3a3b3a.a1a3b3a2.4a2a2b2a.a1a1e.2a5a8a4a4.e2a2a.a3b3a2a6.1a2b2a1a.a4b4a2a3' },
34
+ { encoding: '9x9:2b5b4a2.a1e1a.d1a3b.a4a8a2a3a.3a1a3a3a3.a2a2a2a6a.b2a2d.a2e3a.4a3b1b2' },
35
+ { encoding: '9x9:3a1a1a3a3.a2a1e.b2a7a4b.3d2b5.a2b4a3b.h3.3a2a3a4b.a2a3a3b4.2a3d2a' },
36
+ { encoding: '9x9:1a3b3b3.a2g.2a3a1a2a6.a4a1a3a3a.b1a2c4.3d3a6a.a4b6a1a3.e1a2a.3a3a5a5a3' },
37
+ {
38
+ encoding:
39
+ '9x16:a3a2a5a2a.2a3a2a2a2.e3c.a2b1a1b.4a4b4b4.a3b1a1b.2a2b5c.a4a4b2a4.d2d.3b5a3a2a.b1e3.d4b2a.3a3c2a4.a1a2e.1a2a3a3b.a2c2b3',
40
+ },
41
+ {
42
+ encoding:
43
+ '9x16:a2a2a3a3a.2a2a4a1a2.e2a3a.a3b5a3a5.5a3b2a3a.a2f2.b3a4b3a.a3a1e.2a1a4a3a2.a4a2e.3a1a2b3a.a5a2b3a3.3a3b1c.a2a2c3a.2a3a2a3a2.a2a6a3a2a',
44
+ },
45
+ {
46
+ encoding:
47
+ '9x16:2a3b2b2.a1b2d.2e1a3.b4a8b2a.2b1b2a4.e1c.1a1a4b2a.a3a2d2.2d3a3a.a5b5a2a4.c1e.3a2b2b3.a2b3a1b.4a4e3.d2b1a.3b4b2a3.',
48
+ },
49
+ {
50
+ encoding:
51
+ '13x22:1a2b2b3a2a2.i1a2a.3b3b3a3a1b.a3c1f2.b3c4b1c.3g2b4a.b5b2f3.i2a3a.3a4a1a1a1a1a5.e3c4c.a3a3c1d3.2d2c4a1a.c5c2e.2a1a3a3a3a5a4.a4a2i.3f3b4b.a4b3g2.3b3b3c2b.g4c2a.b1a1a2b3b4.a1a1i.2a2a3b3b2a3',
52
+ },
53
+ { encoding: '9x9:3b3b4a2.a2c1c.b1a4a2b.3d2b3.a2b5b1a.4b2a2b2.b1a3b2a.a1c2b1.2a3d3a' },
54
+ { encoding: '9x9:4b4b4a4.b1a1d.2b1d3.d2a2b.4a4b3b4.a4b4b2a.2a1c2a2.a2e1a.2a4a3a4a2' },
55
+ {
56
+ encoding:
57
+ '21x35:a1a3b4a2a2a4b3a2b2.3c2b1e4b6a2b.b4c3a2c3d2a2a.a2a4a5a3b2b4a2d2.3a2a2a4a3c2c2d.a4a3a3a2b5b4a4a3a1a.3c4f4b3a3a2b.a2a2b3a4a3a2b5a4b5.4a2b4a1a3a7b2a1a1b.a2a3b5a4d1a3a3b4.3c4f2i.a2a2b4b5b5a3a3a4a4.e5b3a4f2a2a.4a3a5i1a2a2a3.a3a2a3a3a4b6b3a3a4a.3a2a3a2a2a4b2b4a3a3.a2a3e4k.4a4a2c2a3a5a3b5b3.a1a3b3b6c2b3d.1a3b3a3b1g2a2.a3a4b2b4b6b2a4c.2g2b2b2a2a3a2.a2a6a4a6d2b1a4c.5a4a4a2a2b4b8a3a1a2.a1a4a2a7b4b4a2a5a3a.b4e2b3b4c2b.3c6b7a3c2a2a1b3.a3a2o2a.3a5a5a3b3a4b5b2b3.e2a3e1d4b.3b2b3b2d2b2a',
58
+ },
59
+ ]
@@ -0,0 +1,32 @@
1
+ import { useApp, useInput } from 'ink'
2
+ import { useRef } from 'react'
3
+
4
+ export default function usePuzzleInput(
5
+ puzzleIndex: number,
6
+ puzzlesLength: number,
7
+ onPrev: () => void,
8
+ onNext: () => void,
9
+ onToggleSolution: () => void
10
+ ) {
11
+ const { exit } = useApp()
12
+ const puzzleIndexRef = useRef(puzzleIndex)
13
+ puzzleIndexRef.current = puzzleIndex
14
+
15
+ useInput(input => {
16
+ if (input === 'q') {
17
+ exit()
18
+ }
19
+
20
+ if (input === 'n' && puzzleIndexRef.current + 1 < puzzlesLength) {
21
+ onNext()
22
+ }
23
+
24
+ if (input === 'p' && puzzleIndexRef.current - 1 >= 0) {
25
+ onPrev()
26
+ }
27
+
28
+ if (input === 's') {
29
+ onToggleSolution()
30
+ }
31
+ })
32
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,27 @@
1
+ {
2
+ "compilerOptions": {
3
+ "esModuleInterop": true,
4
+ "skipLibCheck": true,
5
+ "target": "es2022",
6
+ "allowJs": true,
7
+ "resolveJsonModule": true,
8
+ "moduleDetection": "force",
9
+ "isolatedModules": true,
10
+ "verbatimModuleSyntax": true,
11
+ "strict": true,
12
+ "noUncheckedIndexedAccess": true,
13
+ "noImplicitOverride": true,
14
+ "module": "NodeNext",
15
+ "outDir": "dist",
16
+ "sourceMap": true,
17
+ "jsx": "react-jsx",
18
+ "allowImportingTsExtensions": true
19
+ },
20
+ "ts-node": {
21
+ "compilerOptions": {
22
+ "module": "CommonJS"
23
+ }
24
+ },
25
+ "include": ["src/**/*"],
26
+ "exclude": ["node_modules"]
27
+ }