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