bridges-cli 0.1.0 → 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.
- package/AGENTS.md +4 -1
- package/README.md +1 -0
- package/package.json +2 -2
- package/src/Game.tsx +37 -11
- package/src/__tests__/Game.test.tsx +298 -97
- package/src/components/HashiGrid.tsx +17 -1
- package/src/components/HashiRow.tsx +14 -1
- package/src/components/Messages.tsx +39 -6
- package/src/components/__tests__/HashiRow.test.tsx +28 -3
- package/src/components/__tests__/Messages.test.tsx +61 -38
- package/src/index.tsx +9 -1
- package/src/utils/bridges.ts +238 -14
- package/src/utils/puzzle-encoding.ts +101 -19
- package/src/utils/usePuzzleInput.ts +4 -2
|
@@ -9,6 +9,16 @@ const TEST_PUZZLE = { encoding: '3x3:1a1.c.2a2' }
|
|
|
9
9
|
const TEST_PUZZLE_2 = { encoding: '3x3:3a3.c.1a1' }
|
|
10
10
|
const SMALL_PUZZLE_3X3 = { encoding: '3x3:2a3.c.1a2' }
|
|
11
11
|
|
|
12
|
+
/**
|
|
13
|
+
* Note on ANSI sequences:
|
|
14
|
+
* \x1b[1m - bold (selected node)
|
|
15
|
+
* \x1b[2m - dim (inactive/unselected nodes)
|
|
16
|
+
* \x1b[22m - normal (turns off bold/dim)
|
|
17
|
+
* \x1b[31m - red (error - too many bridges)
|
|
18
|
+
* \x1b[32m - green (success - correct number of bridges)
|
|
19
|
+
* \x1b[39m - reset all (default foreground + bold/dim off)
|
|
20
|
+
* \x1b[39m - reset foreground only (used in some tests for clarity)
|
|
21
|
+
*/
|
|
12
22
|
describe('Game', () => {
|
|
13
23
|
beforeEach(() => {
|
|
14
24
|
Object.defineProperty(process.stdin, 'isTTY', {
|
|
@@ -21,7 +31,11 @@ describe('Game', () => {
|
|
|
21
31
|
describe('game controls - toggle solution', () => {
|
|
22
32
|
it('pressing s toggles the solution on and off', async () => {
|
|
23
33
|
const { stdin, lastFrame } = render(
|
|
24
|
-
<Game
|
|
34
|
+
<Game
|
|
35
|
+
puzzles={[samplePuzzles[0] as PuzzleData]}
|
|
36
|
+
hasCustomPuzzle={false}
|
|
37
|
+
enableSolutions={true}
|
|
38
|
+
/>
|
|
25
39
|
)
|
|
26
40
|
|
|
27
41
|
expect(lastFrame()).toEqual(`Bridges: Puzzle #1
|
|
@@ -64,27 +78,27 @@ q: Quit`)
|
|
|
64
78
|
• Viewing solution (press s to return to puzzle)
|
|
65
79
|
|
|
66
80
|
┌─────────────────────────────────────┐
|
|
67
|
-
│ ╭───╮ ╭───╮ ╭───╮
|
|
68
|
-
│ │ 4 ╞═════╡ 3 ├─────┤ 3 ╞═════╡ 3
|
|
69
|
-
│ ╰─╥─╯ ╰───╯ ╰───╯
|
|
70
|
-
│
|
|
71
|
-
│
|
|
72
|
-
│
|
|
73
|
-
│ ╭─╨─╮
|
|
74
|
-
│ │ 3 ├──────────┤ 3
|
|
75
|
-
│ ╰───╯
|
|
76
|
-
│
|
|
77
|
-
│
|
|
78
|
-
│
|
|
79
|
-
│ ╭───╮ ╭─╨─╮ ╭─╨─╮ ║
|
|
80
|
-
│ │ 2 ╞══════════╡ 8 ╞═════╡ 4 │ ║
|
|
81
|
-
│ ╰───╯ ╰─╥─╯ ╰───╯ ║
|
|
82
|
-
│
|
|
83
|
-
│
|
|
84
|
-
│
|
|
85
|
-
│ ╭───╮ ╭─╨─╮
|
|
86
|
-
│ │ 1 ├─────┤ 4 ├─────┤ 1
|
|
87
|
-
│ ╰───╯ ╰───╯
|
|
81
|
+
│ [32m╭───╮ ╭───╮ ╭───╮ ╭───╮[39m │
|
|
82
|
+
│ [32m│ 4 ╞═════╡ 3 ├─────┤ 3 ╞═════╡ 3 │[39m │
|
|
83
|
+
│ [32m╰─╥─╯ ╰───╯ ╰───╯ ╰─┬─╯[39m │
|
|
84
|
+
│ [32m ║ ╭───╮ ╭───╮ │ [39m │
|
|
85
|
+
│ [32m ║ │ 2 ╞═══════════════╡ 4 │ │ [39m │
|
|
86
|
+
│ [32m ║ ╰───╯ ╰─╥─╯ │ [39m │
|
|
87
|
+
│ [32m╭─╨─╮ ╭───╮[39m [32m ║ ╭─┴─╮[39m │
|
|
88
|
+
│ [32m│ 3 ├──────────┤ 3 │[39m [32m ║ │ 3 │[39m │
|
|
89
|
+
│ [32m╰───╯ ╰─╥─╯[39m [32m ║ ╰─╥─╯[39m │
|
|
90
|
+
│ [32m ║ [39m [32m ║ ║ [39m │
|
|
91
|
+
│ [32m ║ [39m [32m ║ ║ [39m │
|
|
92
|
+
│ [32m ║ [39m [32m ║ ║ [39m │
|
|
93
|
+
│ [32m╭───╮ ╭─╨─╮ ╭─╨─╮ ║ [39m │
|
|
94
|
+
│ [32m│ 2 ╞══════════╡ 8 ╞═════╡ 4 │ ║ [39m │
|
|
95
|
+
│ [32m╰───╯ ╰─╥─╯ ╰───╯ ║ [39m │
|
|
96
|
+
│ [32m ║ ╭───╮ ╭─╨─╮[39m │
|
|
97
|
+
│ [32m ║ │ 1 ├─────┤ 3 │[39m │
|
|
98
|
+
│ [32m ║ ╰───╯ ╰───╯[39m │
|
|
99
|
+
│ [32m╭───╮ ╭─╨─╮ ╭───╮[39m │
|
|
100
|
+
│ [32m│ 1 ├─────┤ 4 ├─────┤ 1 │[39m │
|
|
101
|
+
│ [32m╰───╯ ╰───╯ ╰───╯[39m │
|
|
88
102
|
└─────────────────────────────────────┘
|
|
89
103
|
|
|
90
104
|
Controls:
|
|
@@ -134,7 +148,11 @@ q: Quit`)
|
|
|
134
148
|
describe('game controls - next/previous', () => {
|
|
135
149
|
it('navigates to next puzzle with n key when interactive', async () => {
|
|
136
150
|
const { stdin, lastFrame } = render(
|
|
137
|
-
<Game
|
|
151
|
+
<Game
|
|
152
|
+
puzzles={[TEST_PUZZLE, TEST_PUZZLE_2]}
|
|
153
|
+
hasCustomPuzzle={false}
|
|
154
|
+
enableSolutions={false}
|
|
155
|
+
/>
|
|
138
156
|
)
|
|
139
157
|
|
|
140
158
|
expect(lastFrame()).toEqual(`Bridges: Puzzle #1
|
|
@@ -155,7 +173,6 @@ q: Quit`)
|
|
|
155
173
|
Controls:
|
|
156
174
|
p: Previous puzzle
|
|
157
175
|
n: Next puzzle
|
|
158
|
-
s: Show solution
|
|
159
176
|
q: Quit`)
|
|
160
177
|
|
|
161
178
|
stdin.write('n')
|
|
@@ -178,13 +195,16 @@ q: Quit`)
|
|
|
178
195
|
Controls:
|
|
179
196
|
p: Previous puzzle
|
|
180
197
|
n: Next puzzle
|
|
181
|
-
s: Show solution
|
|
182
198
|
q: Quit`)
|
|
183
199
|
})
|
|
184
200
|
|
|
185
201
|
it('navigates to previous puzzle with p key when interactive', async () => {
|
|
186
202
|
const { stdin, lastFrame } = render(
|
|
187
|
-
<Game
|
|
203
|
+
<Game
|
|
204
|
+
puzzles={[TEST_PUZZLE, TEST_PUZZLE_2]}
|
|
205
|
+
hasCustomPuzzle={false}
|
|
206
|
+
enableSolutions={false}
|
|
207
|
+
/>
|
|
188
208
|
)
|
|
189
209
|
|
|
190
210
|
stdin.write('n')
|
|
@@ -207,7 +227,6 @@ q: Quit`)
|
|
|
207
227
|
Controls:
|
|
208
228
|
p: Previous puzzle
|
|
209
229
|
n: Next puzzle
|
|
210
|
-
s: Show solution
|
|
211
230
|
q: Quit`)
|
|
212
231
|
|
|
213
232
|
stdin.write('p')
|
|
@@ -230,13 +249,12 @@ q: Quit`)
|
|
|
230
249
|
Controls:
|
|
231
250
|
p: Previous puzzle
|
|
232
251
|
n: Next puzzle
|
|
233
|
-
s: Show solution
|
|
234
252
|
q: Quit`)
|
|
235
253
|
})
|
|
236
254
|
|
|
237
255
|
it('does not navigate past last puzzle', async () => {
|
|
238
256
|
const { stdin, lastFrame } = render(
|
|
239
|
-
<Game puzzles={[TEST_PUZZLE]} hasCustomPuzzle={false} />
|
|
257
|
+
<Game puzzles={[TEST_PUZZLE]} hasCustomPuzzle={false} enableSolutions={false} />
|
|
240
258
|
)
|
|
241
259
|
|
|
242
260
|
stdin.write('n')
|
|
@@ -259,13 +277,12 @@ q: Quit`)
|
|
|
259
277
|
Controls:
|
|
260
278
|
p: Previous puzzle
|
|
261
279
|
n: Next puzzle
|
|
262
|
-
s: Show solution
|
|
263
280
|
q: Quit`)
|
|
264
281
|
})
|
|
265
282
|
|
|
266
283
|
it('does not navigate before first puzzle', async () => {
|
|
267
284
|
const { stdin, lastFrame } = render(
|
|
268
|
-
<Game puzzles={[TEST_PUZZLE]} hasCustomPuzzle={false} />
|
|
285
|
+
<Game puzzles={[TEST_PUZZLE]} hasCustomPuzzle={false} enableSolutions={false} />
|
|
269
286
|
)
|
|
270
287
|
|
|
271
288
|
stdin.write('p')
|
|
@@ -288,7 +305,6 @@ q: Quit`)
|
|
|
288
305
|
Controls:
|
|
289
306
|
p: Previous puzzle
|
|
290
307
|
n: Next puzzle
|
|
291
|
-
s: Show solution
|
|
292
308
|
q: Quit`)
|
|
293
309
|
})
|
|
294
310
|
})
|
|
@@ -296,7 +312,11 @@ q: Quit`)
|
|
|
296
312
|
describe('game controls - node selection', () => {
|
|
297
313
|
it('selects node immediately when there is only one of that number', async () => {
|
|
298
314
|
const { stdin, lastFrame } = render(
|
|
299
|
-
<Game
|
|
315
|
+
<Game
|
|
316
|
+
puzzles={[SMALL_PUZZLE_3X3]}
|
|
317
|
+
hasCustomPuzzle={false}
|
|
318
|
+
enableSolutions={false}
|
|
319
|
+
/>
|
|
300
320
|
)
|
|
301
321
|
|
|
302
322
|
// Press '3' to select single node of value 3
|
|
@@ -308,7 +328,11 @@ q: Quit`)
|
|
|
308
328
|
|
|
309
329
|
it('shows disambiguation labels when multiple nodes have the same number', async () => {
|
|
310
330
|
const { stdin, lastFrame } = render(
|
|
311
|
-
<Game
|
|
331
|
+
<Game
|
|
332
|
+
puzzles={[SMALL_PUZZLE_3X3]}
|
|
333
|
+
hasCustomPuzzle={false}
|
|
334
|
+
enableSolutions={false}
|
|
335
|
+
/>
|
|
312
336
|
)
|
|
313
337
|
|
|
314
338
|
// Press '2' to select from multiple nodes with value 2
|
|
@@ -321,7 +345,11 @@ q: Quit`)
|
|
|
321
345
|
|
|
322
346
|
it('selects a specific node when disambiguation label is pressed', async () => {
|
|
323
347
|
const { stdin, lastFrame } = render(
|
|
324
|
-
<Game
|
|
348
|
+
<Game
|
|
349
|
+
puzzles={[SMALL_PUZZLE_3X3]}
|
|
350
|
+
hasCustomPuzzle={false}
|
|
351
|
+
enableSolutions={false}
|
|
352
|
+
/>
|
|
325
353
|
)
|
|
326
354
|
|
|
327
355
|
// Press '1' to select from multiple nodes with value 1
|
|
@@ -336,7 +364,11 @@ q: Quit`)
|
|
|
336
364
|
|
|
337
365
|
it('draws a bridge when a valid direction is selected', async () => {
|
|
338
366
|
const { stdin, lastFrame } = render(
|
|
339
|
-
<Game
|
|
367
|
+
<Game
|
|
368
|
+
puzzles={[SMALL_PUZZLE_3X3]}
|
|
369
|
+
hasCustomPuzzle={false}
|
|
370
|
+
enableSolutions={false}
|
|
371
|
+
/>
|
|
340
372
|
)
|
|
341
373
|
|
|
342
374
|
// Press '1' to enter disambiguation mode
|
|
@@ -355,7 +387,11 @@ q: Quit`)
|
|
|
355
387
|
|
|
356
388
|
it('shows an invalid message for a bad bridge direction off the grid', async () => {
|
|
357
389
|
const { stdin, lastFrame } = render(
|
|
358
|
-
<Game
|
|
390
|
+
<Game
|
|
391
|
+
puzzles={[SMALL_PUZZLE_3X3]}
|
|
392
|
+
hasCustomPuzzle={false}
|
|
393
|
+
enableSolutions={false}
|
|
394
|
+
/>
|
|
359
395
|
)
|
|
360
396
|
|
|
361
397
|
// Press '1' to enter disambiguation mode
|
|
@@ -373,12 +409,16 @@ q: Quit`)
|
|
|
373
409
|
})
|
|
374
410
|
|
|
375
411
|
it('shows an invalid message for horizontal bridge colliding with bridge', async () => {
|
|
376
|
-
const puzzleWithBarrier = { encoding: '4x3:2a2a.a1|1.
|
|
412
|
+
const puzzleWithBarrier = { encoding: '4x3:2a2a.a1|1.b3a' }
|
|
377
413
|
const { stdin, lastFrame } = render(
|
|
378
|
-
<Game
|
|
414
|
+
<Game
|
|
415
|
+
puzzles={[puzzleWithBarrier]}
|
|
416
|
+
hasCustomPuzzle={false}
|
|
417
|
+
enableSolutions={false}
|
|
418
|
+
/>
|
|
379
419
|
)
|
|
380
420
|
expect(lastFrame()).toEqual(`Bridges: Puzzle #1
|
|
381
|
-
• Type a number [1-
|
|
421
|
+
• Type a number [1-3] to select a node
|
|
382
422
|
|
|
383
423
|
┌──────────────────────┐
|
|
384
424
|
│ ╭───╮ ╭───╮ │
|
|
@@ -388,14 +428,13 @@ q: Quit`)
|
|
|
388
428
|
│ │ 1 │ │ │ 1 │ │
|
|
389
429
|
│ ╰───╯ │ ╰───╯ │
|
|
390
430
|
│ ╭─┴─╮ │
|
|
391
|
-
│ │
|
|
431
|
+
│ │ 3 │ │
|
|
392
432
|
│ ╰───╯ │
|
|
393
433
|
└──────────────────────┘
|
|
394
434
|
|
|
395
435
|
Controls:
|
|
396
436
|
p: Previous puzzle
|
|
397
437
|
n: Next puzzle
|
|
398
|
-
s: Show solution
|
|
399
438
|
q: Quit`)
|
|
400
439
|
|
|
401
440
|
stdin.write('1')
|
|
@@ -408,19 +447,23 @@ q: Quit`)
|
|
|
408
447
|
})
|
|
409
448
|
|
|
410
449
|
it('shows an invalid message for vertical bridge colliding with bridge', async () => {
|
|
411
|
-
const puzzleWithBarrier = { encoding: '4x3:2a2a.
|
|
450
|
+
const puzzleWithBarrier = { encoding: '4x3:2a2a.a3=3.b1a' }
|
|
412
451
|
const { stdin, lastFrame } = render(
|
|
413
|
-
<Game
|
|
452
|
+
<Game
|
|
453
|
+
puzzles={[puzzleWithBarrier]}
|
|
454
|
+
hasCustomPuzzle={false}
|
|
455
|
+
enableSolutions={false}
|
|
456
|
+
/>
|
|
414
457
|
)
|
|
415
458
|
expect(lastFrame()).toEqual(`Bridges: Puzzle #1
|
|
416
|
-
• Type a number [1-
|
|
459
|
+
• Type a number [1-3] to select a node
|
|
417
460
|
|
|
418
461
|
┌──────────────────────┐
|
|
419
462
|
│ ╭───╮ ╭───╮ │
|
|
420
463
|
│ │ 2 │ │ 2 │ │
|
|
421
464
|
│ ╰───╯ ╰───╯ │
|
|
422
465
|
│ ╭───╮ ╭───╮ │
|
|
423
|
-
│ │
|
|
466
|
+
│ │ 3 ╞═════╡ 3 │ │
|
|
424
467
|
│ ╰───╯ ╰───╯ │
|
|
425
468
|
│ ╭───╮ │
|
|
426
469
|
│ │ 1 │ │
|
|
@@ -430,7 +473,6 @@ q: Quit`)
|
|
|
430
473
|
Controls:
|
|
431
474
|
p: Previous puzzle
|
|
432
475
|
n: Next puzzle
|
|
433
|
-
s: Show solution
|
|
434
476
|
q: Quit`)
|
|
435
477
|
|
|
436
478
|
stdin.write('2')
|
|
@@ -444,7 +486,11 @@ q: Quit`)
|
|
|
444
486
|
|
|
445
487
|
it('resets selection when Escape is pressed', async () => {
|
|
446
488
|
const { stdin, lastFrame } = render(
|
|
447
|
-
<Game
|
|
489
|
+
<Game
|
|
490
|
+
puzzles={[SMALL_PUZZLE_3X3]}
|
|
491
|
+
hasCustomPuzzle={false}
|
|
492
|
+
enableSolutions={false}
|
|
493
|
+
/>
|
|
448
494
|
)
|
|
449
495
|
|
|
450
496
|
stdin.write('2')
|
|
@@ -453,7 +499,7 @@ q: Quit`)
|
|
|
453
499
|
|
|
454
500
|
// Note: Escape key handling may not work in test environment
|
|
455
501
|
// Testing that we entered disambiguation mode successfully
|
|
456
|
-
stdin.write('
|
|
502
|
+
stdin.write('')
|
|
457
503
|
await setTimeout(5)
|
|
458
504
|
expect(lastFrame()).toContain('Type a number')
|
|
459
505
|
})
|
|
@@ -463,7 +509,11 @@ q: Quit`)
|
|
|
463
509
|
it('draws horizontal bridge', async () => {
|
|
464
510
|
const puzzleWithEachBridge = { encoding: '3x3:2a3.c.3a4' }
|
|
465
511
|
const { stdin, lastFrame } = render(
|
|
466
|
-
<Game
|
|
512
|
+
<Game
|
|
513
|
+
puzzles={[puzzleWithEachBridge]}
|
|
514
|
+
hasCustomPuzzle={false}
|
|
515
|
+
enableSolutions={false}
|
|
516
|
+
/>
|
|
467
517
|
)
|
|
468
518
|
stdin.write('3')
|
|
469
519
|
await setTimeout(5)
|
|
@@ -477,28 +527,31 @@ q: Quit`)
|
|
|
477
527
|
• Drew horizontal bridge
|
|
478
528
|
|
|
479
529
|
┌─────────────────┐
|
|
480
|
-
│
|
|
481
|
-
│
|
|
482
|
-
│
|
|
530
|
+
│ [2m╭───╮[22m [1m╭───╮[22m │
|
|
531
|
+
│ [2m│ 2 ├─────[22m[1m┤ 3 │[22m │
|
|
532
|
+
│ [2m╰───╯[22m [1m╰───╯[22m │
|
|
483
533
|
│ │
|
|
484
534
|
│ │
|
|
485
535
|
│ │
|
|
486
|
-
│
|
|
487
|
-
│
|
|
488
|
-
│
|
|
536
|
+
│ [2m╭───╮[22m [2m╭───╮[22m │
|
|
537
|
+
│ [2m│ 3 │[22m [2m│ 4 │[22m │
|
|
538
|
+
│ [2m╰───╯[22m [2m╰───╯[22m │
|
|
489
539
|
└─────────────────┘
|
|
490
540
|
|
|
491
541
|
Controls:
|
|
492
542
|
p: Previous puzzle
|
|
493
543
|
n: Next puzzle
|
|
494
|
-
s: Show solution
|
|
495
544
|
q: Quit`)
|
|
496
545
|
})
|
|
497
546
|
|
|
498
547
|
it('draws a vertical bridge', async () => {
|
|
499
548
|
const puzzleWithEachBridge = { encoding: '3x3:2a3.c.3a4' }
|
|
500
549
|
const { stdin, lastFrame } = render(
|
|
501
|
-
<Game
|
|
550
|
+
<Game
|
|
551
|
+
puzzles={[puzzleWithEachBridge]}
|
|
552
|
+
hasCustomPuzzle={false}
|
|
553
|
+
enableSolutions={false}
|
|
554
|
+
/>
|
|
502
555
|
)
|
|
503
556
|
stdin.write('2')
|
|
504
557
|
await setTimeout(5)
|
|
@@ -508,65 +561,68 @@ q: Quit`)
|
|
|
508
561
|
• Drew vertical bridge
|
|
509
562
|
|
|
510
563
|
┌─────────────────┐
|
|
511
|
-
│
|
|
512
|
-
│
|
|
513
|
-
│
|
|
514
|
-
│
|
|
515
|
-
│
|
|
516
|
-
│
|
|
517
|
-
│
|
|
518
|
-
│
|
|
519
|
-
│
|
|
564
|
+
│ [1m╭───╮[22m [2m╭───╮[22m │
|
|
565
|
+
│ [1m│ 2 │[22m [2m│ 3 │[22m │
|
|
566
|
+
│ [1m╰─┬─╯[22m [2m╰───╯[22m │
|
|
567
|
+
│ [2m │ [22m │
|
|
568
|
+
│ [2m │ [22m │
|
|
569
|
+
│ [2m │ [22m │
|
|
570
|
+
│ [2m╭─┴─╮[22m [2m╭───╮[22m │
|
|
571
|
+
│ [2m│ 3 │[22m [2m│ 4 │[22m │
|
|
572
|
+
│ [2m╰───╯[22m [2m╰───╯[22m │
|
|
520
573
|
└─────────────────┘
|
|
521
574
|
|
|
522
575
|
Controls:
|
|
523
576
|
p: Previous puzzle
|
|
524
577
|
n: Next puzzle
|
|
525
|
-
s: Show solution
|
|
526
578
|
q: Quit`)
|
|
527
579
|
})
|
|
528
580
|
|
|
529
581
|
it('draws a double horizontal bridge', async () => {
|
|
530
|
-
const puzzleWithEachBridge = { encoding: '3x3:
|
|
582
|
+
const puzzleWithEachBridge = { encoding: '3x3:3a4.c.2a4' }
|
|
531
583
|
const { stdin, lastFrame } = render(
|
|
532
|
-
<Game
|
|
584
|
+
<Game
|
|
585
|
+
puzzles={[puzzleWithEachBridge]}
|
|
586
|
+
hasCustomPuzzle={false}
|
|
587
|
+
enableSolutions={false}
|
|
588
|
+
/>
|
|
533
589
|
)
|
|
534
590
|
stdin.write('3')
|
|
535
591
|
await setTimeout(5)
|
|
536
|
-
expect(lastFrame()).toContain('Press label shown to select that node')
|
|
537
|
-
stdin.write('a')
|
|
538
|
-
await setTimeout(5)
|
|
539
592
|
expect(lastFrame()).toContain('Select direction with h/j/k/l')
|
|
540
|
-
stdin.write('
|
|
593
|
+
stdin.write('L')
|
|
541
594
|
await setTimeout(5)
|
|
542
595
|
expect(lastFrame()).toEqual(`Bridges: Puzzle #1
|
|
543
596
|
• Drew double horizontal bridge
|
|
544
597
|
|
|
545
598
|
┌─────────────────┐
|
|
546
|
-
│
|
|
547
|
-
│
|
|
548
|
-
│
|
|
599
|
+
│ [1m╭───╮[22m [2m╭───╮[22m │
|
|
600
|
+
│ [1m│ 3 ╞[22m[2m═════╡ 4 │[22m │
|
|
601
|
+
│ [1m╰───╯[22m [2m╰───╯[22m │
|
|
549
602
|
│ │
|
|
550
603
|
│ │
|
|
551
604
|
│ │
|
|
552
|
-
│
|
|
553
|
-
│
|
|
554
|
-
│
|
|
605
|
+
│ [2m╭───╮[22m [2m╭───╮[22m │
|
|
606
|
+
│ [2m│ 2 │[22m [2m│ 4 │[22m │
|
|
607
|
+
│ [2m╰───╯[22m [2m╰───╯[22m │
|
|
555
608
|
└─────────────────┘
|
|
556
609
|
|
|
557
610
|
Controls:
|
|
558
611
|
p: Previous puzzle
|
|
559
612
|
n: Next puzzle
|
|
560
|
-
s: Show solution
|
|
561
613
|
q: Quit`)
|
|
562
614
|
})
|
|
563
615
|
|
|
564
616
|
it('draws a double vertical bridge', async () => {
|
|
565
|
-
const puzzleWithEachBridge = { encoding: '3x3:
|
|
617
|
+
const puzzleWithEachBridge = { encoding: '3x3:3a2.c.4a2' }
|
|
566
618
|
const { stdin, lastFrame } = render(
|
|
567
|
-
<Game
|
|
619
|
+
<Game
|
|
620
|
+
puzzles={[puzzleWithEachBridge]}
|
|
621
|
+
hasCustomPuzzle={false}
|
|
622
|
+
enableSolutions={false}
|
|
623
|
+
/>
|
|
568
624
|
)
|
|
569
|
-
stdin.write('
|
|
625
|
+
stdin.write('3')
|
|
570
626
|
await setTimeout(5)
|
|
571
627
|
stdin.write('J')
|
|
572
628
|
await setTimeout(5)
|
|
@@ -574,28 +630,31 @@ q: Quit`)
|
|
|
574
630
|
• Drew double vertical bridge
|
|
575
631
|
|
|
576
632
|
┌─────────────────┐
|
|
577
|
-
│
|
|
578
|
-
│
|
|
579
|
-
│
|
|
580
|
-
│
|
|
581
|
-
│
|
|
582
|
-
│
|
|
583
|
-
│
|
|
584
|
-
│
|
|
585
|
-
│
|
|
633
|
+
│ [1m╭───╮[22m [2m╭───╮[22m │
|
|
634
|
+
│ [1m│ 3 │[22m [2m│ 2 │[22m │
|
|
635
|
+
│ [1m╰─╥─╯[22m [2m╰───╯[22m │
|
|
636
|
+
│ [2m ║ [22m │
|
|
637
|
+
│ [2m ║ [22m │
|
|
638
|
+
│ [2m ║ [22m │
|
|
639
|
+
│ [2m╭─╨─╮[22m [2m╭───╮[22m │
|
|
640
|
+
│ [2m│ 4 │[22m [2m│ 2 │[22m │
|
|
641
|
+
│ [2m╰───╯[22m [2m╰───╯[22m │
|
|
586
642
|
└─────────────────┘
|
|
587
643
|
|
|
588
644
|
Controls:
|
|
589
645
|
p: Previous puzzle
|
|
590
646
|
n: Next puzzle
|
|
591
|
-
s: Show solution
|
|
592
647
|
q: Quit`)
|
|
593
648
|
})
|
|
594
649
|
|
|
595
650
|
it('does not draw a bridge over an existing bridge', async () => {
|
|
596
651
|
const puzzleWithEachBridge = { encoding: '4x3:1a3a.a2#2.3a4a' }
|
|
597
652
|
const { stdin, lastFrame } = render(
|
|
598
|
-
<Game
|
|
653
|
+
<Game
|
|
654
|
+
puzzles={[puzzleWithEachBridge]}
|
|
655
|
+
hasCustomPuzzle={false}
|
|
656
|
+
enableSolutions={false}
|
|
657
|
+
/>
|
|
599
658
|
)
|
|
600
659
|
expect(lastFrame()).toEqual(`Bridges: Puzzle #1
|
|
601
660
|
• Type a number [1-4] to select a node
|
|
@@ -615,7 +674,6 @@ q: Quit`)
|
|
|
615
674
|
Controls:
|
|
616
675
|
p: Previous puzzle
|
|
617
676
|
n: Next puzzle
|
|
618
|
-
s: Show solution
|
|
619
677
|
q: Quit`)
|
|
620
678
|
|
|
621
679
|
stdin.write('2')
|
|
@@ -630,7 +688,11 @@ q: Quit`)
|
|
|
630
688
|
it('erases a bridge', async () => {
|
|
631
689
|
const puzzleWithEachBridge = { encoding: '3x3:2a3.c.3a4' }
|
|
632
690
|
const { stdin, lastFrame } = render(
|
|
633
|
-
<Game
|
|
691
|
+
<Game
|
|
692
|
+
puzzles={[puzzleWithEachBridge]}
|
|
693
|
+
hasCustomPuzzle={false}
|
|
694
|
+
enableSolutions={false}
|
|
695
|
+
/>
|
|
634
696
|
)
|
|
635
697
|
stdin.write('3')
|
|
636
698
|
await setTimeout(5)
|
|
@@ -653,4 +715,143 @@ q: Quit`)
|
|
|
653
715
|
expect(lastFrame()).not.toContain('═╡')
|
|
654
716
|
})
|
|
655
717
|
})
|
|
718
|
+
|
|
719
|
+
describe('game controls - solving puzzles', () => {
|
|
720
|
+
it('detects a valid solution', async () => {
|
|
721
|
+
const puzzle = { encoding: '3x3:2a1.c.2a1' }
|
|
722
|
+
const { stdin, lastFrame } = render(
|
|
723
|
+
<Game puzzles={[puzzle]} hasCustomPuzzle={false} enableSolutions={false} />
|
|
724
|
+
)
|
|
725
|
+
expect(lastFrame()).toEqual(`Bridges: Puzzle #1
|
|
726
|
+
• Type a number [1-2] to select a node
|
|
727
|
+
|
|
728
|
+
┌─────────────────┐
|
|
729
|
+
│ ╭───╮ ╭───╮ │
|
|
730
|
+
│ │ 2 │ │ 1 │ │
|
|
731
|
+
│ ╰───╯ ╰───╯ │
|
|
732
|
+
│ │
|
|
733
|
+
│ │
|
|
734
|
+
│ │
|
|
735
|
+
│ ╭───╮ ╭───╮ │
|
|
736
|
+
│ │ 2 │ │ 1 │ │
|
|
737
|
+
│ ╰───╯ ╰───╯ │
|
|
738
|
+
└─────────────────┘
|
|
739
|
+
|
|
740
|
+
Controls:
|
|
741
|
+
p: Previous puzzle
|
|
742
|
+
n: Next puzzle
|
|
743
|
+
q: Quit`)
|
|
744
|
+
|
|
745
|
+
// Draw bridges to solve the puzzle
|
|
746
|
+
stdin.write('2')
|
|
747
|
+
await setTimeout(5)
|
|
748
|
+
stdin.write('a') // select top-left node
|
|
749
|
+
await setTimeout(5)
|
|
750
|
+
stdin.write('l') // draw bridge to right
|
|
751
|
+
await setTimeout(5)
|
|
752
|
+
|
|
753
|
+
stdin.write('2')
|
|
754
|
+
await setTimeout(5)
|
|
755
|
+
stdin.write('b') // select bottom-left node
|
|
756
|
+
await setTimeout(5)
|
|
757
|
+
stdin.write('l') // draw bridge to right
|
|
758
|
+
await setTimeout(5)
|
|
759
|
+
|
|
760
|
+
stdin.write('2')
|
|
761
|
+
await setTimeout(5)
|
|
762
|
+
stdin.write('a') // select top-left node
|
|
763
|
+
await setTimeout(5)
|
|
764
|
+
stdin.write('j') // draw bridge down
|
|
765
|
+
await setTimeout(5)
|
|
766
|
+
|
|
767
|
+
expect(lastFrame()).toContain('Solution reached')
|
|
768
|
+
})
|
|
769
|
+
|
|
770
|
+
it('shows a warning when grid is not fully connected but the nodes are filled', async () => {
|
|
771
|
+
const puzzle = { encoding: '3x3:2a1.c.2a1' }
|
|
772
|
+
const { stdin, lastFrame } = render(
|
|
773
|
+
<Game puzzles={[puzzle]} hasCustomPuzzle={false} enableSolutions={false} />
|
|
774
|
+
)
|
|
775
|
+
expect(lastFrame()).toEqual(`Bridges: Puzzle #1
|
|
776
|
+
• Type a number [1-2] to select a node
|
|
777
|
+
|
|
778
|
+
┌─────────────────┐
|
|
779
|
+
│ ╭───╮ ╭───╮ │
|
|
780
|
+
│ │ 2 │ │ 1 │ │
|
|
781
|
+
│ ╰───╯ ╰───╯ │
|
|
782
|
+
│ │
|
|
783
|
+
│ │
|
|
784
|
+
│ │
|
|
785
|
+
│ ╭───╮ ╭───╮ │
|
|
786
|
+
│ │ 2 │ │ 1 │ │
|
|
787
|
+
│ ╰───╯ ╰───╯ │
|
|
788
|
+
└─────────────────┘
|
|
789
|
+
|
|
790
|
+
Controls:
|
|
791
|
+
p: Previous puzzle
|
|
792
|
+
n: Next puzzle
|
|
793
|
+
q: Quit`)
|
|
794
|
+
|
|
795
|
+
// Draw bridges to fill the nodes while having an unconnected grid
|
|
796
|
+
stdin.write('2')
|
|
797
|
+
await setTimeout(5)
|
|
798
|
+
stdin.write('a') // select top-left node
|
|
799
|
+
await setTimeout(5)
|
|
800
|
+
stdin.write('J') // draw double bridge down
|
|
801
|
+
await setTimeout(5)
|
|
802
|
+
|
|
803
|
+
stdin.write('1')
|
|
804
|
+
await setTimeout(5)
|
|
805
|
+
stdin.write('a') // select top-right node
|
|
806
|
+
await setTimeout(5)
|
|
807
|
+
stdin.write('j') // draw bridge down
|
|
808
|
+
await setTimeout(5)
|
|
809
|
+
|
|
810
|
+
expect(lastFrame()).toContain('Grid is not fully connected')
|
|
811
|
+
})
|
|
812
|
+
})
|
|
813
|
+
|
|
814
|
+
describe('game controls - success/error coloring on nodes and bridges', () => {
|
|
815
|
+
it('highlights as success the completed node (connected to an incomplete)', () => {
|
|
816
|
+
const puzzleCompleted = { encoding: '3x1:2=4' }
|
|
817
|
+
const { lastFrame } = render(
|
|
818
|
+
<Game puzzles={[puzzleCompleted]} hasCustomPuzzle={false} enableSolutions={false} />
|
|
819
|
+
)
|
|
820
|
+
|
|
821
|
+
expect(lastFrame()).toEqual(`Bridges: Puzzle #1
|
|
822
|
+
• Type a number [2-4] to select a node
|
|
823
|
+
|
|
824
|
+
┌─────────────────┐
|
|
825
|
+
│ [32m╭───╮[39m ╭───╮ │
|
|
826
|
+
│ [32m│ 2 ╞[39m═════╡ 4 │ │
|
|
827
|
+
│ [32m╰───╯[39m ╰───╯ │
|
|
828
|
+
└─────────────────┘
|
|
829
|
+
|
|
830
|
+
Controls:
|
|
831
|
+
p: Previous puzzle
|
|
832
|
+
n: Next puzzle
|
|
833
|
+
q: Quit`)
|
|
834
|
+
})
|
|
835
|
+
|
|
836
|
+
it('highlights as error a node with too many bridges', () => {
|
|
837
|
+
const puzzleWithError = { encoding: '3x1:1=3' }
|
|
838
|
+
const { lastFrame } = render(
|
|
839
|
+
<Game puzzles={[puzzleWithError]} hasCustomPuzzle={false} enableSolutions={false} />
|
|
840
|
+
)
|
|
841
|
+
|
|
842
|
+
expect(lastFrame()).toEqual(`Bridges: Puzzle #1
|
|
843
|
+
• Type a number [1-3] to select a node
|
|
844
|
+
|
|
845
|
+
┌─────────────────┐
|
|
846
|
+
│ [31m╭───╮[39m ╭───╮ │
|
|
847
|
+
│ [31m│ 1 ╞[39m═════╡ 3 │ │
|
|
848
|
+
│ [31m╰───╯[39m ╰───╯ │
|
|
849
|
+
└─────────────────┘
|
|
850
|
+
|
|
851
|
+
Controls:
|
|
852
|
+
p: Previous puzzle
|
|
853
|
+
n: Next puzzle
|
|
854
|
+
q: Quit`)
|
|
855
|
+
})
|
|
856
|
+
})
|
|
656
857
|
})
|