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.
@@ -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 puzzles={[samplePuzzles[0] as PuzzleData]} hasCustomPuzzle={false} />
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
- ║ │ 2 ╞═══════════════╡ 4 │ │
72
- ║ ╰───╯ ╰─╥─╯ │
73
- │ ╭─╨─╮ ╭───╮ ╭─┴─╮
74
- │ │ 3 ├──────────┤ 3 ║ │ 3
75
- │ ╰───╯ ╰─╥─╯ ╰─╥─╯
76
- ║ ║
77
- ║ ║
78
- ║ ║
79
- │ ╭───╮ ╭─╨─╮ ╭─╨─╮ ║
80
- │ │ 2 ╞══════════╡ 8 ╞═════╡ 4 │ ║
81
- │ ╰───╯ ╰─╥─╯ ╰───╯ ║
82
- ║ ╭───╮ ╭─╨─╮
83
- ║ │ 1 ├─────┤ 3
84
- ║ ╰───╯ ╰───╯
85
- │ ╭───╮ ╭─╨─╮ ╭───╮
86
- │ │ 1 ├─────┤ 4 ├─────┤ 1
87
- │ ╰───╯ ╰───╯ ╰───╯
81
+ ╭───╮ ╭───╮ ╭───╮ ╭───╮
82
+ │ 4 ╞═════╡ 3 ├─────┤ 3 ╞═════╡ 3 │
83
+ ╰─╥─╯ ╰───╯ ╰───╯ ╰─┬─╯
84
+  ║ ╭───╮ ╭───╮ │ 
85
+  ║ │ 2 ╞═══════════════╡ 4 │ │ 
86
+  ║ ╰───╯ ╰─╥─╯ │ 
87
+ ╭─╨─╮ ╭───╮  ╭─┴─╮
88
+ │ 3 ├──────────┤ 3 │  ║ │ 3 │
89
+ ╰───╯ ╰─╥─╯  ╰─╥─╯
90
+    ║ ║ 
91
+    ║ ║ 
92
+    ║ ║ 
93
+ ╭───╮ ╭─╨─╮ ╭─╨─╮ ║ 
94
+ │ 2 ╞══════════╡ 8 ╞═════╡ 4 │ ║ 
95
+ ╰───╯ ╰─╥─╯ ╰───╯ ║ 
96
+  ║ ╭───╮ ╭─╨─╮
97
+  ║ │ 1 ├─────┤ 3 │
98
+  ║ ╰───╯ ╰───╯
99
+ ╭───╮ ╭─╨─╮ ╭───╮
100
+ │ 1 ├─────┤ 4 ├─────┤ 1 │
101
+ ╰───╯ ╰───╯ ╰───╯
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 puzzles={[TEST_PUZZLE, TEST_PUZZLE_2]} hasCustomPuzzle={false} />
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 puzzles={[TEST_PUZZLE, TEST_PUZZLE_2]} hasCustomPuzzle={false} />
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 puzzles={[SMALL_PUZZLE_3X3]} hasCustomPuzzle={false} />
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 puzzles={[SMALL_PUZZLE_3X3]} hasCustomPuzzle={false} />
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 puzzles={[SMALL_PUZZLE_3X3]} hasCustomPuzzle={false} />
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 puzzles={[SMALL_PUZZLE_3X3]} hasCustomPuzzle={false} />
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 puzzles={[SMALL_PUZZLE_3X3]} hasCustomPuzzle={false} />
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.b1a' }
412
+ const puzzleWithBarrier = { encoding: '4x3:2a2a.a1|1.b3a' }
377
413
  const { stdin, lastFrame } = render(
378
- <Game puzzles={[puzzleWithBarrier]} hasCustomPuzzle={false} />
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-2] to select a node
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
- │ │ 1 │ │
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.a1=1.b1a' }
450
+ const puzzleWithBarrier = { encoding: '4x3:2a2a.a3=3.b1a' }
412
451
  const { stdin, lastFrame } = render(
413
- <Game puzzles={[puzzleWithBarrier]} hasCustomPuzzle={false} />
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-2] to select a node
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
- │ │ 1 ╞═════╡ 1 │ │
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 puzzles={[SMALL_PUZZLE_3X3]} hasCustomPuzzle={false} />
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('\x1b')
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 puzzles={[puzzleWithEachBridge]} hasCustomPuzzle={false} />
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
- \x1b[2m╭───╮\x1b[22m \x1b[1m╭───╮\x1b[22m │
481
- \x1b[2m│ 2 ├─────\x1b[22m\x1b[1m┤ 3 │\x1b[22m │
482
- \x1b[2m╰───╯\x1b[22m \x1b[1m╰───╯\x1b[22m │
530
+ ╭───╮ ╭───╮ │
531
+ │ 2 ├─────┤ 3 │ │
532
+ ╰───╯ ╰───╯ │
483
533
  │ │
484
534
  │ │
485
535
  │ │
486
- \x1b[2m╭───╮\x1b[22m \x1b[2m╭───╮\x1b[22m │
487
- \x1b[2m│ 3 │\x1b[22m \x1b[2m│ 4 │\x1b[22m │
488
- \x1b[2m╰───╯\x1b[22m \x1b[2m╰───╯\x1b[22m │
536
+ ╭───╮ ╭───╮ │
537
+ │ 3 │ │ 4 │ │
538
+ ╰───╯ ╰───╯ │
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 puzzles={[puzzleWithEachBridge]} hasCustomPuzzle={false} />
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
- \x1b[1m╭───╮\x1b[22m \x1b[2m╭───╮\x1b[22m │
512
- \x1b[1m│ 2 │\x1b[22m \x1b[2m│ 3 │\x1b[22m │
513
- \x1b[1m╰─┬─╯\x1b[22m \x1b[2m╰───╯\x1b[22m │
514
- \x1b[2m │ \x1b[22m │
515
- \x1b[2m │ \x1b[22m │
516
- \x1b[2m │ \x1b[22m │
517
- \x1b[2m╭─┴─╮\x1b[22m \x1b[2m╭───╮\x1b[22m │
518
- \x1b[2m│ 3 │\x1b[22m \x1b[2m│ 4 │\x1b[22m │
519
- \x1b[2m╰───╯\x1b[22m \x1b[2m╰───╯\x1b[22m │
564
+ ╭───╮ ╭───╮ │
565
+ │ 2 │ │ 3 │ │
566
+ ╰─┬─╯ ╰───╯ │
567
+  │  │
568
+  │  │
569
+  │  │
570
+ ╭─┴─╮ ╭───╮ │
571
+ │ 3 │ │ 4 │ │
572
+ ╰───╯ ╰───╯ │
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:2a3.c.3a4' }
582
+ const puzzleWithEachBridge = { encoding: '3x3:3a4.c.2a4' }
531
583
  const { stdin, lastFrame } = render(
532
- <Game puzzles={[puzzleWithEachBridge]} hasCustomPuzzle={false} />
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('H')
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
- \x1b[2m╭───╮\x1b[22m \x1b[1m╭───╮\x1b[22m │
547
- \x1b[2m2 ╞═════\x1b[22m\x1b[1m╡ 3 │\x1b[22m │
548
- \x1b[2m╰───╯\x1b[22m \x1b[1m╰───╯\x1b[22m │
599
+ ╭───╮ ╭───╮ │
600
+ 3 ╞═════╡ 4 │ │
601
+ ╰───╯ ╰───╯ │
549
602
  │ │
550
603
  │ │
551
604
  │ │
552
- \x1b[2m╭───╮\x1b[22m \x1b[2m╭───╮\x1b[22m │
553
- \x1b[2m│ 3 │\x1b[22m \x1b[2m│ 4 │\x1b[22m │
554
- \x1b[2m╰───╯\x1b[22m \x1b[2m╰───╯\x1b[22m │
605
+ ╭───╮ ╭───╮ │
606
+ │ 2 │ │ 4 │ │
607
+ ╰───╯ ╰───╯ │
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:2a3.c.3a4' }
617
+ const puzzleWithEachBridge = { encoding: '3x3:3a2.c.4a2' }
566
618
  const { stdin, lastFrame } = render(
567
- <Game puzzles={[puzzleWithEachBridge]} hasCustomPuzzle={false} />
619
+ <Game
620
+ puzzles={[puzzleWithEachBridge]}
621
+ hasCustomPuzzle={false}
622
+ enableSolutions={false}
623
+ />
568
624
  )
569
- stdin.write('2')
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
- \x1b[1m╭───╮\x1b[22m \x1b[2m╭───╮\x1b[22m │
578
- \x1b[1m│ 2 │\x1b[22m \x1b[2m│ 3 │\x1b[22m │
579
- \x1b[1m╰─╥─╯\x1b[22m \x1b[2m╰───╯\x1b[22m │
580
- \x1b[2m ║ \x1b[22m │
581
- \x1b[2m ║ \x1b[22m │
582
- \x1b[2m ║ \x1b[22m │
583
- \x1b[2m╭─╨─╮\x1b[22m \x1b[2m╭───╮\x1b[22m │
584
- \x1b[2m│ 3 │\x1b[22m \x1b[2m│ 4 │\x1b[22m │
585
- \x1b[2m╰───╯\x1b[22m \x1b[2m╰───╯\x1b[22m │
633
+ ╭───╮ ╭───╮ │
634
+ │ 3 │ │ 2 │ │
635
+ ╰─╥─╯ ╰───╯ │
636
+  ║  │
637
+  ║  │
638
+  ║  │
639
+ ╭─╨─╮ ╭───╮ │
640
+ │ 4 │ │ 2 │ │
641
+ ╰───╯ ╰───╯ │
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 puzzles={[puzzleWithEachBridge]} hasCustomPuzzle={false} />
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 puzzles={[puzzleWithEachBridge]} hasCustomPuzzle={false} />
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
+ │ ╭───╮ ╭───╮ │
826
+ │ │ 2 ╞═════╡ 4 │ │
827
+ │ ╰───╯ ╰───╯ │
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
+ │ ╭───╮ ╭───╮ │
847
+ │ │ 1 ╞═════╡ 3 │ │
848
+ │ ╰───╯ ╰───╯ │
849
+ └─────────────────┘
850
+
851
+ Controls:
852
+ p: Previous puzzle
853
+ n: Next puzzle
854
+ q: Quit`)
855
+ })
856
+ })
656
857
  })