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.
@@ -1,32 +1,404 @@
1
1
  import { useApp, useInput } from 'ink'
2
- import { useRef } from 'react'
2
+ import { useCallback, useRef, useState } from 'react'
3
3
 
4
- export default function usePuzzleInput(
5
- puzzleIndex: number,
6
- puzzlesLength: number,
7
- onPrev: () => void,
8
- onNext: () => void,
4
+ import type { Direction, PlacedBridge, SelectionState } from '../types.ts'
5
+
6
+ type UsePuzzleInputProps = {
7
+ puzzleIndex: number
8
+ puzzlesLength: number
9
+ rows: { value: number | '-' | '=' | '#' | ' ' | '|' }[][]
10
+ showSolution: boolean
11
+ onPrev: () => void
12
+ onNext: () => void
9
13
  onToggleSolution: () => void
14
+ onBridgePlaced?: (bridge: PlacedBridge) => boolean
15
+ }
16
+
17
+ function findMatchingNodes(
18
+ rows: { value: number | '-' | '=' | '#' | ' ' | '|' }[][],
19
+ number: number
10
20
  ) {
21
+ const matches: { row: number; col: number }[] = []
22
+ for (let row = 0; row < rows.length; row++) {
23
+ const currentRow = rows[row]
24
+ if (!currentRow) continue
25
+ for (let col = 0; col < currentRow.length; col++) {
26
+ const cell = currentRow[col]
27
+ if (cell && cell.value === number) {
28
+ matches.push({ row, col })
29
+ }
30
+ }
31
+ }
32
+ return matches
33
+ }
34
+
35
+ function generateLabels(count: number): string[] {
36
+ const labels: string[] = []
37
+ for (let i = 0; i < count; i++) {
38
+ labels.push(String.fromCharCode(97 + i)) // a, b, c, ...
39
+ }
40
+ return labels
41
+ }
42
+
43
+ export function findNodeInDirection(
44
+ rows: { value: number | '-' | '=' | '#' | ' ' | '|' }[][],
45
+ fromRow: number,
46
+ fromCol: number,
47
+ direction: Direction
48
+ ): { row: number; col: number } | null {
49
+ const rowCount = rows.length
50
+ const firstRow = rows[0]
51
+ if (!firstRow) return null
52
+ const colCount = firstRow.length
53
+
54
+ let checkRow = fromRow
55
+ let checkCol = fromCol
56
+
57
+ if (direction === 'h') {
58
+ // left
59
+ checkCol = fromCol - 1
60
+ while (checkCol >= 0) {
61
+ const row = rows[fromRow]
62
+ if (!row) return null
63
+ const cell = row[checkCol]
64
+ if (cell) {
65
+ if (cell.value === '|' || cell.value === '#') {
66
+ // Bridge in the way - invalid
67
+ return null
68
+ }
69
+ if (typeof cell.value === 'number') {
70
+ return { row: fromRow, col: checkCol }
71
+ }
72
+ }
73
+ checkCol--
74
+ }
75
+ } else if (direction === 'l') {
76
+ // right
77
+ checkCol = fromCol + 1
78
+ while (checkCol < colCount) {
79
+ const row = rows[fromRow]
80
+ if (!row) return null
81
+ const cell = row[checkCol]
82
+ if (cell) {
83
+ if (cell.value === '|' || cell.value === '#') {
84
+ // Bridge in the way - invalid
85
+ return null
86
+ }
87
+ if (typeof cell.value === 'number') {
88
+ return { row: fromRow, col: checkCol }
89
+ }
90
+ }
91
+ checkCol++
92
+ }
93
+ } else if (direction === 'j') {
94
+ // down
95
+ checkRow = fromRow + 1
96
+ while (checkRow < rowCount) {
97
+ const row = rows[checkRow]
98
+ if (!row) return null
99
+ const cell = row[fromCol]
100
+ if (cell) {
101
+ if (cell.value === '-' || cell.value === '=') {
102
+ // Bridge in the way - invalid
103
+ return null
104
+ }
105
+ if (typeof cell.value === 'number') {
106
+ return { row: checkRow, col: fromCol }
107
+ }
108
+ }
109
+ checkRow++
110
+ }
111
+ } else if (direction === 'k') {
112
+ // up
113
+ checkRow = fromRow - 1
114
+ while (checkRow >= 0) {
115
+ const row = rows[checkRow]
116
+ if (!row) return null
117
+ const cell = row[fromCol]
118
+ if (cell) {
119
+ if (cell.value === '-' || cell.value === '=') {
120
+ // Bridge in the way - invalid
121
+ return null
122
+ }
123
+ if (typeof cell.value === 'number') {
124
+ return { row: checkRow, col: fromCol }
125
+ }
126
+ }
127
+ checkRow--
128
+ }
129
+ }
130
+
131
+ return null
132
+ }
133
+
134
+ export default function usePuzzleInput({
135
+ puzzleIndex,
136
+ puzzlesLength,
137
+ rows,
138
+ showSolution,
139
+ onPrev,
140
+ onNext,
141
+ onToggleSolution,
142
+ onBridgePlaced,
143
+ }: UsePuzzleInputProps) {
11
144
  const { exit } = useApp()
12
145
  const puzzleIndexRef = useRef(puzzleIndex)
13
146
  puzzleIndexRef.current = puzzleIndex
14
147
 
15
- useInput(input => {
148
+ const showSolutionRef = useRef(showSolution)
149
+ showSolutionRef.current = showSolution
150
+
151
+ const [selectionState, setSelectionState] = useState<SelectionState>({
152
+ mode: 'idle',
153
+ selectedNumber: null,
154
+ direction: null,
155
+ matchingNodes: [],
156
+ disambiguationLabels: [],
157
+ selectedNode: null,
158
+ })
159
+
160
+ // Use a ref to track mode for synchronous access in input handler
161
+ const selectionStateRef = useRef(selectionState)
162
+ selectionStateRef.current = selectionState
163
+
164
+ // Store timeout ID so we can cancel it when user provides input
165
+ const resetTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null)
166
+
167
+ const resetSelection = useCallback(() => {
168
+ if (resetTimeoutRef.current) {
169
+ clearTimeout(resetTimeoutRef.current)
170
+ resetTimeoutRef.current = null
171
+ }
172
+ setSelectionState({
173
+ mode: 'idle',
174
+ selectedNumber: null,
175
+ direction: null,
176
+ matchingNodes: [],
177
+ disambiguationLabels: [],
178
+ selectedNode: null,
179
+ })
180
+ }, [])
181
+
182
+ // Clear the reset timeout when user changes mode (e.g., starts a new selection)
183
+ const clearResetTimeout = useCallback(() => {
184
+ if (resetTimeoutRef.current) {
185
+ clearTimeout(resetTimeoutRef.current)
186
+ resetTimeoutRef.current = null
187
+ }
188
+ }, [])
189
+
190
+ useInput((input, key) => {
191
+ // q always quits
16
192
  if (input === 'q') {
17
193
  exit()
194
+ return
18
195
  }
19
196
 
20
- if (input === 'n' && puzzleIndexRef.current + 1 < puzzlesLength) {
21
- onNext()
22
- }
197
+ const currentMode = selectionStateRef.current.mode
23
198
 
24
- if (input === 'p' && puzzleIndexRef.current - 1 >= 0) {
25
- onPrev()
199
+ // If not idle, handle selection keys
200
+ if (currentMode !== 'idle') {
201
+ // Esc resets to idle
202
+ if (key.escape) {
203
+ resetSelection()
204
+ return
205
+ }
206
+
207
+ // In disambiguation mode, handle a-z
208
+ if (currentMode === 'disambiguation') {
209
+ const labelIndex = input.charCodeAt(0) - 97 // a=0, b=1, ...
210
+ const matches = selectionStateRef.current.matchingNodes
211
+ if (labelIndex >= 0 && labelIndex < matches.length) {
212
+ // Selected! Now enter selecting-node mode to choose direction
213
+ setSelectionState({
214
+ ...selectionStateRef.current,
215
+ mode: 'selecting-node',
216
+ disambiguationLabels: [],
217
+ selectedNode: matches[labelIndex] ?? null,
218
+ })
219
+ }
220
+ return
221
+ }
222
+
223
+ // In selecting-node mode, handle direction keys
224
+ if (
225
+ currentMode === 'selecting-node' &&
226
+ selectionStateRef.current.selectedNumber !== null
227
+ ) {
228
+ if (
229
+ input === 'h' ||
230
+ input === 'j' ||
231
+ input === 'k' ||
232
+ input === 'l' ||
233
+ input === 'H' ||
234
+ input === 'J' ||
235
+ input === 'K' ||
236
+ input === 'L'
237
+ ) {
238
+ const isDouble = input === input.toUpperCase()
239
+ const direction = input.toLowerCase() as Direction
240
+ const selectedNode = selectionStateRef.current.selectedNode
241
+ const targetNode = selectedNode
242
+ ? findNodeInDirection(rows, selectedNode.row, selectedNode.col, direction)
243
+ : null
244
+
245
+ let erased = false
246
+ if (targetNode && selectedNode && onBridgePlaced) {
247
+ // Toggle the bridge (add if not exists, remove if exists)
248
+ // The callback returns true if a bridge was erased
249
+ erased = onBridgePlaced({
250
+ from: selectedNode,
251
+ to: targetNode,
252
+ count: isDouble ? 2 : 1,
253
+ })
254
+ }
255
+
256
+ // Show selected/invalid state, then reset after 1.5s
257
+ setSelectionState({
258
+ ...selectionStateRef.current,
259
+ mode: targetNode ? 'selected' : 'invalid',
260
+ direction,
261
+ bridgeErased: erased,
262
+ isDoubleBridge: isDouble,
263
+ })
264
+ resetTimeoutRef.current = setTimeout(resetSelection, 1_500)
265
+ }
266
+ return
267
+ }
268
+
269
+ // In selected/invalid mode, allow immediate input for next action
270
+ if (currentMode === 'selected' || currentMode === 'invalid') {
271
+ // Clear any pending timeout since user is providing input
272
+ clearResetTimeout()
273
+
274
+ // Allow n/p/s navigation
275
+ if (input === 'n' && puzzleIndexRef.current + 1 < puzzlesLength) {
276
+ onNext()
277
+ resetSelection()
278
+ return
279
+ } else if (input === 'p' && puzzleIndexRef.current - 1 >= 0) {
280
+ onPrev()
281
+ resetSelection()
282
+ return
283
+ } else if (input === 's') {
284
+ onToggleSolution()
285
+ resetSelection()
286
+ return
287
+ }
288
+
289
+ // Allow number keys to start a new selection
290
+ if (input >= '1' && input <= '8') {
291
+ if (showSolutionRef.current) return
292
+
293
+ const num = parseInt(input, 10)
294
+ const matches = findMatchingNodes(rows, num)
295
+ if (matches.length > 0) {
296
+ if (matches.length === 1) {
297
+ setSelectionState({
298
+ mode: 'selecting-node',
299
+ selectedNumber: num,
300
+ direction: null,
301
+ matchingNodes: matches,
302
+ disambiguationLabels: [],
303
+ selectedNode: matches[0] ?? null,
304
+ })
305
+ } else {
306
+ setSelectionState({
307
+ mode: 'disambiguation',
308
+ selectedNumber: num,
309
+ direction: null,
310
+ matchingNodes: matches,
311
+ disambiguationLabels: generateLabels(matches.length),
312
+ selectedNode: null,
313
+ })
314
+ }
315
+ }
316
+ return
317
+ }
318
+
319
+ // Allow direction keys to draw another bridge immediately
320
+ if (
321
+ input === 'h' ||
322
+ input === 'j' ||
323
+ input === 'k' ||
324
+ input === 'l' ||
325
+ input === 'H' ||
326
+ input === 'J' ||
327
+ input === 'K' ||
328
+ input === 'L'
329
+ ) {
330
+ // Get the previously selected node to use as starting point
331
+ const prevNode = selectionStateRef.current.selectedNode
332
+ if (prevNode) {
333
+ const isDouble = input === input.toUpperCase()
334
+ const direction = input.toLowerCase() as Direction
335
+ const targetNode = findNodeInDirection(
336
+ rows,
337
+ prevNode.row,
338
+ prevNode.col,
339
+ direction
340
+ )
341
+
342
+ let erased = false
343
+ if (targetNode && onBridgePlaced) {
344
+ erased = onBridgePlaced({
345
+ from: prevNode,
346
+ to: targetNode,
347
+ count: isDouble ? 2 : 1,
348
+ })
349
+ }
350
+
351
+ setSelectionState({
352
+ ...selectionStateRef.current,
353
+ mode: targetNode ? 'selected' : 'invalid',
354
+ direction,
355
+ bridgeErased: erased,
356
+ isDoubleBridge: isDouble,
357
+ })
358
+ }
359
+ return
360
+ }
361
+ }
26
362
  }
27
363
 
28
- if (input === 's') {
364
+ // Normal mode key handling
365
+ if (input === 'n' && puzzleIndexRef.current + 1 < puzzlesLength) {
366
+ onNext()
367
+ } else if (input === 'p' && puzzleIndexRef.current - 1 >= 0) {
368
+ onPrev()
369
+ } else if (input === 's') {
29
370
  onToggleSolution()
371
+ } else if (input >= '1' && input <= '8') {
372
+ if (showSolutionRef.current) return
373
+
374
+ // Number pressed - enter selecting-node or disambiguation mode
375
+ const num = parseInt(input, 10)
376
+ const matches = findMatchingNodes(rows, num)
377
+ if (matches.length > 0) {
378
+ if (matches.length === 1) {
379
+ // Single match - go directly to selecting-node mode
380
+ setSelectionState({
381
+ mode: 'selecting-node',
382
+ selectedNumber: num,
383
+ direction: null,
384
+ matchingNodes: matches,
385
+ disambiguationLabels: [],
386
+ selectedNode: matches[0] ?? null,
387
+ })
388
+ } else {
389
+ // Multiple matches - enter disambiguation mode
390
+ setSelectionState({
391
+ mode: 'disambiguation',
392
+ selectedNumber: num,
393
+ direction: null,
394
+ matchingNodes: matches,
395
+ disambiguationLabels: generateLabels(matches.length),
396
+ selectedNode: null,
397
+ })
398
+ }
399
+ }
30
400
  }
31
401
  })
402
+
403
+ return { selectionState, resetSelection }
32
404
  }
@@ -1,178 +0,0 @@
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
- }
@@ -1,59 +0,0 @@
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
- ]