bridges-cli 0.0.1 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,286 @@
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
+ const node = nodes[position - 1]
72
+ if (node) {
73
+ nodes[position - 1] = { ...node, lineRight: lineCount }
74
+ }
75
+ }
76
+
77
+ // Create the line node
78
+ nodes[position] = { value: char as '-' | '=' }
79
+
80
+ // Set lineRight on the line node and lineLeft on next position if it's a number node
81
+ if (position < numNodes - 1 && typeof nodes[position + 1]?.value === 'number') {
82
+ const node = nodes[position + 1]
83
+ if (node) {
84
+ nodes[position + 1] = { ...node, lineLeft: lineCount }
85
+ }
86
+ }
87
+
88
+ // Check if next char is a letter (repeat count)
89
+ const nextChar = rowStr[i + 1]
90
+ if (nextChar && nextChar >= 'a' && nextChar <= 'z') {
91
+ const repeat = letterToNumber(nextChar)
92
+ // Create repeated line nodes (the first is already at 'position')
93
+ // We need to create 'repeat' more nodes starting at position+1
94
+ for (let r = 1; r <= repeat && position + r < numNodes; r++) {
95
+ nodes[position + r] = { value: char as '-' | '=' }
96
+ // Set lineLeft/lineRight on adjacent number nodes
97
+ if (
98
+ position + r > 0 &&
99
+ typeof nodes[position + r - 1]?.value === 'number'
100
+ ) {
101
+ const node = nodes[position + r - 1]
102
+ if (node) {
103
+ nodes[position + r - 1] = {
104
+ ...node,
105
+ lineRight: lineCount,
106
+ }
107
+ }
108
+ }
109
+ if (
110
+ position + r < numNodes - 1 &&
111
+ typeof nodes[position + r + 1]?.value === 'number'
112
+ ) {
113
+ const node = nodes[position + r + 1]
114
+ if (node) {
115
+ nodes[position + r + 1] = {
116
+ ...node,
117
+ lineLeft: lineCount,
118
+ }
119
+ }
120
+ }
121
+ }
122
+ // Set lineLeft on the last line node if next position has a number
123
+ const lastLinePos = position + repeat
124
+ if (
125
+ lastLinePos < numNodes - 1 &&
126
+ typeof nodes[lastLinePos + 1]?.value === 'number'
127
+ ) {
128
+ const node = nodes[lastLinePos + 1]
129
+ if (node) {
130
+ nodes[lastLinePos + 1] = { ...node, lineLeft: lineCount }
131
+ }
132
+ }
133
+ position += repeat + 1
134
+ i += 2
135
+ } else {
136
+ position++
137
+ i++
138
+ }
139
+ } else if (char === '|' || char === '#') {
140
+ // Vertical line: single (|) or double (#)
141
+ // Create the line node (connections handled in second pass)
142
+ // Note: vertical lines do not support letter repeat
143
+ nodes[position] = { value: char as '|' | '#' }
144
+ position++
145
+ i++
146
+ } else {
147
+ // Letter: skip positions based on letter-to-number mapping
148
+ position += letterToNumber(char)
149
+ i++
150
+ }
151
+ }
152
+
153
+ // Fill any remaining undefined positions with empty nodes
154
+ for (let j = 0; j < numNodes; j++) {
155
+ if (nodes[j] === undefined || nodes[j] === null) {
156
+ nodes[j] = { value: ' ' }
157
+ }
158
+ }
159
+
160
+ rows.push(nodes)
161
+ }
162
+
163
+ // Second pass: handle vertical line connections
164
+ // For each vertical line, connect to the number nodes above and below
165
+ for (let rowIdx = 0; rowIdx < rows.length; rowIdx++) {
166
+ const currentRow = rows[rowIdx]
167
+ if (!currentRow) continue
168
+
169
+ for (let colIdx = 0; colIdx < currentRow.length; colIdx++) {
170
+ const node = currentRow[colIdx]
171
+ if (!node) continue
172
+ if (node.value === '|' || node.value === '#') {
173
+ const lineCount: 1 | 2 = node.value === '#' ? 2 : 1
174
+
175
+ // Set lineDown on the number node in the row above (if exists)
176
+ if (rowIdx > 0) {
177
+ const nodeAbove = rows[rowIdx - 1]?.[colIdx]
178
+ if (nodeAbove && typeof nodeAbove.value === 'number') {
179
+ const row = rows[rowIdx - 1]
180
+ if (row) {
181
+ row[colIdx] = { ...nodeAbove, lineDown: lineCount }
182
+ }
183
+ }
184
+ }
185
+
186
+ // Set lineUp on the number node in the row below (if exists)
187
+ if (rowIdx < rows.length - 1) {
188
+ const nodeBelow = rows[rowIdx + 1]?.[colIdx]
189
+ if (nodeBelow && typeof nodeBelow.value === 'number') {
190
+ const row = rows[rowIdx + 1]
191
+ if (row) {
192
+ row[colIdx] = { ...nodeBelow, lineUp: lineCount }
193
+ }
194
+ }
195
+ }
196
+ }
197
+ }
198
+ }
199
+
200
+ return rows
201
+ }
202
+
203
+ export interface PuzzleData {
204
+ encoding: string
205
+ solution?: string
206
+ }
207
+
208
+ export const samplePuzzles: PuzzleData[] = [
209
+ {
210
+ encoding: '7x7:4a3a3a3.a2c4a.3b3b3.g.2b8a4a.d1a3.a1a4a1a',
211
+ solution: '7x7:4=3-3=3.#2=b4|.3-a3a#3.c#a##.2=a8=4#.c#1-3.a1-4-1a',
212
+ },
213
+ {
214
+ encoding: '9x9:3c1a3a3.b2b3c.3c2a2b.e4b6.3a4a8b3a.a1a3a2c.3a3a2b1a.c4a5b3.3a5a3b2a',
215
+ solution:
216
+ '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',
217
+ },
218
+ {
219
+ encoding: '9x9:2a5a6b2a.a1d1a2.3a2b1a2a.a4b8a4b.3g3.b3a3b2a.a2a1b3a3.1a3d2a.a3b4a3a2',
220
+ solution:
221
+ '9x9:2-5=6=a2a.|1#a#a1-2.3|2a#1-2|.#4=a8=4||.3|b#a#|3.||3-3a#2#.|2#1-a3|3.1|3-c2|.a3=a4=3-2',
222
+ },
223
+ {
224
+ encoding: '9x9:4b4a6b3.a2b3a1b.2h.a2a1a4c.4a4a3a2a3.c2a8a4a.h2.b2a1b2a.3b2a4b2',
225
+ solution:
226
+ '9x9:4=a4=6-a3.#2-a3#1a|.2|b##|a|.a2-1#4|a|.4=4-3#2-3.#a|2=8=4|.#a|b#a#2.#a2-1#a2|.3-a2-4-a2',
227
+ },
228
+ {
229
+ encoding: '9x9:a3d3a3.1a2a4b1a.a4a4a1b4.d4b3a.3b3b2b.a1f3.6a2a2a3b.c2c2a.3a2b2b3',
230
+ solution:
231
+ '9x9:a3=c3-3.1|2=4-a1#.|4=4|1-a4.||a#4=a3|.3|a3|a2||.#1a||a#|3.6=2|2-3|#.#b2-b2#.3-2-a2-a3',
232
+ },
233
+ {
234
+ encoding: '9x9:a2a3b6a2.b2a3d.a1g.b1c3a3.4c8b3a.e1c.3a3a2d.g1a.b3b4b4',
235
+ solution:
236
+ '9x9:a2-3=a6=2.a|2-3a#b.a1|a#a#b.b1a#a3-3.4=b8=a3#.#c#1a|#.3-3a2|a|#.b#b|a1#.b3-a4=a4',
237
+ },
238
+ {
239
+ encoding: '9x9:4b6b2a2.i.3a2a2c6.c3b2b.b5a4c4.f2b.2a8a4c3.c1b2b.b4e3',
240
+ solution:
241
+ '9x9:4=a6=a2a2.#b#d#.3-2#2=b6.b|3-a2a#.b5=4a|a4.b#a#a2a#.2=8=4a|a3.b#1-a2a|.b4=d3',
242
+ },
243
+ {
244
+ encoding: '9x9:a3b5b1a.1b2a2b3.i.2c2a2a6.a3a8a4a1a.5a2a4c5.i.4b4a2b3.a1b4b1a',
245
+ solution:
246
+ '9x9:a3=a5-a1a.1|a2#2-a3.||a##|b#.2|a#2|2=6.|3=8=4-1#.5=2#4=b5.#b##c|.4=a4#2=a3.a1-a4-a1a',
247
+ },
248
+ { encoding: '9x9:3c1a3a3.b2b3c.3c2a2b.e4b6.3a4a8b3a.a1a3a2c.3a3a2b1a.c4a5b3.3a5a3b2a' },
249
+ { encoding: '9x9:4a3b3a1a.a3b4a1a3.4h.b3a3a4a6.a1a1a2a2a.5a8a5a3b.h3.1a2a4b4a.a1a2b3a3' },
250
+ { encoding: '9x9:a1b4b3a.3b4d1.d2a1b.a3a8a3a4a.2c1c3.a4a3a3a4a.b1a3d.1d3b4.a4b4b1a' },
251
+ { encoding: '9x9:2a5a6b2a.a1d1a2.3a2b1a2a.a4b8a4b.3g3.b3a3b2a.a2a1b3a3.1a3d2a.a3b4a3a2' },
252
+ { encoding: '9x9:3a3a2c1.a3d3b.b2b1c.4b2b8a4.a2e2a.4a5b1b2.c1b3b.b3b2a3a.2c2a3a3' },
253
+ { encoding: '9x9:4a2b1b3.a2b3a3b.5a4b1c.h3.b5a4a5b.1h.c1b3a4.b2a3b2a.1b3b2a3' },
254
+ { encoding: '9x9:1a2a3a4a3.a2a4a4a2a.1c2d.a5a4a2b3.b3a8b3a.3b1d3.b1b3a3a.a2b2d.4b4a3b3' },
255
+ { encoding: '9x9:a2b3a4a4.3a4b2a2a.a2a5b4b.5a4d3a.c2a3c.a2d2a4.b3b3a2a.a4a3b2a2.3a2a2b2a' },
256
+ { encoding: '9x9:1a1a4a3a2.a3a2a2a3a.2g3.a2b3b3a.3b4a3b6.a3b4b3a.2g3.a4a2a1a2a.3a2a3a3a3' },
257
+ { encoding: '9x9:2a3a4a3a2.a2a2a2a1a.b1a3d.2d1b2.a3b8b4a.4b4d2.d3a2b.a1a4a1a1a.1a1a2a3a2' },
258
+ { encoding: '9x9:3a4a3a2a2.a2c3a3a.2a1e2.a3b2a1b.4a2b2a1a.a1b2a5a4.b2b3c.3c1a2b.a2a3a3b3' },
259
+ { encoding: '9x9:2b4b5a4.b3b4a2a.f1b.1a2a3c4.a4a3a3a3a.2c4a5a4.b1f.a2a2b3b.3a5b4b3' },
260
+ { encoding: '9x9:4a3a3b3a.a1a3b3a2.4a2a2b2a.a1a1e.2a5a8a4a4.e2a2a.a3b3a2a6.1a2b2a1a.a4b4a2a3' },
261
+ { encoding: '9x9:2b5b4a2.a1e1a.d1a3b.a4a8a2a3a.3a1a3a3a3.a2a2a2a6a.b2a2d.a2e3a.4a3b1b2' },
262
+ { encoding: '9x9:3a1a1a3a3.a2a1e.b2a7a4b.3d2b5.a2b4a3b.h3.3a2a3a4b.a2a3a3b4.2a3d2a' },
263
+ { encoding: '9x9:1a3b3b3.a2g.2a3a1a2a6.a4a1a3a3a.b1a2c4.3d3a6a.a4b6a1a3.e1a2a.3a3a5a5a3' },
264
+ {
265
+ encoding:
266
+ '9x16:a3a2a5a2a.2a3a2a2a2.e3c.a2b1a1b.4a4b4b4.a3b1a1b.2a2b5c.a4a4b2a4.d2d.3b5a3a2a.b1e3.d4b2a.3a3c2a4.a1a2e.1a2a3a3b.a2c2b3',
267
+ },
268
+ {
269
+ encoding:
270
+ '9x16:a2a2a3a3a.2a2a4a1a2.e2a3a.a3b5a3a5.5a3b2a3a.a2f2.b3a4b3a.a3a1e.2a1a4a3a2.a4a2e.3a1a2b3a.a5a2b3a3.3a3b1c.a2a2c3a.2a3a2a3a2.a2a6a3a2a',
271
+ },
272
+ {
273
+ encoding:
274
+ '9x16:2a3b2b2.a1b2d.2e1a3.b4a8b2a.2b1b2a4.e1c.1a1a4b2a.a3a2d2.2d3a3a.a5b5a2a4.c1e.3a2b2b3.a2b3a1b.4a4e3.d2b1a.3b4b2a3.',
275
+ },
276
+ {
277
+ encoding:
278
+ '13x22:1a2b2b3a2a2.i1a2a.3b3b3a3a1b.a3c1f2.b3c4b1c.3g2b4a.b5b2f3.i2a3a.3a4a1a1a1a1a5.e3c4c.a3a3c1d3.2d2c4a1a.c5c2e.2a1a3a3a3a5a4.a4a2i.3f3b4b.a4b3g2.3b3b3c2b.g4c2a.b1a1a2b3b4.a1a1i.2a2a3b3b2a3',
279
+ },
280
+ { encoding: '9x9:3b3b4a2.a2c1c.b1a4a2b.3d2b3.a2b5b1a.4b2a2b2.b1a3b2a.a1c2b1.2a3d3a' },
281
+ { encoding: '9x9:4b4b4a4.b1a1d.2b1d3.d2a2b.4a4b3b4.a4b4b2a.2a1c2a2.a2e1a.2a4a3a4a2' },
282
+ {
283
+ encoding:
284
+ '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',
285
+ },
286
+ ]