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.
@@ -3,10 +3,22 @@ import { render } from 'ink-testing-library'
3
3
  import { beforeEach, describe, expect, it, vi } from 'vitest'
4
4
 
5
5
  import Game from '../Game.tsx'
6
+ import { type PuzzleData, samplePuzzles } from '../utils/puzzle-encoding.ts'
6
7
 
7
8
  const TEST_PUZZLE = { encoding: '3x3:1a1.c.2a2' }
8
9
  const TEST_PUZZLE_2 = { encoding: '3x3:3a3.c.1a1' }
10
+ const SMALL_PUZZLE_3X3 = { encoding: '3x3:2a3.c.1a2' }
9
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
+ */
10
22
  describe('Game', () => {
11
23
  beforeEach(() => {
12
24
  Object.defineProperty(process.stdin, 'isTTY', {
@@ -16,33 +28,135 @@ describe('Game', () => {
16
28
  vi.spyOn(process.stdin, 'isTTY', 'get').mockReturnValue(true)
17
29
  })
18
30
 
19
- describe('--stdout flag', () => {
20
- it('does not show instructions when stdout is true', () => {
21
- const { lastFrame } = render(
22
- <Game puzzles={[TEST_PUZZLE]} hasCustomPuzzle={false} stdout={true} />
31
+ describe('game controls - toggle solution', () => {
32
+ it('pressing s toggles the solution on and off', async () => {
33
+ const { stdin, lastFrame } = render(
34
+ <Game
35
+ puzzles={[samplePuzzles[0] as PuzzleData]}
36
+ hasCustomPuzzle={false}
37
+ enableSolutions={true}
38
+ />
23
39
  )
24
- expect(lastFrame()).not.toContain('Controls:')
25
- })
26
40
 
27
- it('shows instructions when stdout is false', () => {
28
- const { lastFrame } = render(
29
- <Game puzzles={[TEST_PUZZLE]} hasCustomPuzzle={false} stdout={false} />
30
- )
31
- expect(lastFrame()).toContain('Controls:')
41
+ expect(lastFrame()).toEqual(`Bridges: Puzzle #1
42
+ Type a number [1-8] to select a node
43
+
44
+ ┌─────────────────────────────────────┐
45
+ │ ╭───╮ ╭───╮ ╭───╮ ╭───╮ │
46
+ │ │ 4 │ │ 3 │ │ 3 │ │ 3 │ │
47
+ │ ╰───╯ ╰───╯ ╰───╯ ╰───╯ │
48
+ │ ╭───╮ ╭───╮ │
49
+ │ │ 2 │ │ 4 │ │
50
+ │ ╰───╯ ╰───╯ │
51
+ │ ╭───╮ ╭───╮ ╭───╮ │
52
+ │ │ 3 │ │ 3 │ │ 3 │ │
53
+ │ ╰───╯ ╰───╯ ╰───╯ │
54
+ │ │
55
+ │ │
56
+ │ │
57
+ │ ╭───╮ ╭───╮ ╭───╮ │
58
+ │ │ 2 │ │ 8 │ │ 4 │ │
59
+ │ ╰───╯ ╰───╯ ╰───╯ │
60
+ │ ╭───╮ ╭───╮ │
61
+ │ │ 1 │ │ 3 │ │
62
+ │ ╰───╯ ╰───╯ │
63
+ │ ╭───╮ ╭───╮ ╭───╮ │
64
+ │ │ 1 │ │ 4 │ │ 1 │ │
65
+ │ ╰───╯ ╰───╯ ╰───╯ │
66
+ └─────────────────────────────────────┘
67
+
68
+ Controls:
69
+ p: Previous puzzle
70
+ n: Next puzzle
71
+ s: Show solution
72
+ q: Quit`)
73
+
74
+ // Now toggle the solution on
75
+ stdin.write('s')
76
+ await setTimeout(5)
77
+ expect(lastFrame()).toEqual(`Bridges: Puzzle #1
78
+ • Viewing solution (press s to return to puzzle)
79
+
80
+ ┌─────────────────────────────────────┐
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
+ │ ╰───╯ ╰───╯ ╰───╯ │
102
+ └─────────────────────────────────────┘
103
+
104
+ Controls:
105
+ p: Previous puzzle
106
+ n: Next puzzle
107
+ s: Show solution
108
+ q: Quit`)
109
+
110
+ // And toggle it off again
111
+ stdin.write('s')
112
+ await setTimeout(5)
113
+ expect(lastFrame()).toEqual(`Bridges: Puzzle #1
114
+ • Type a number [1-8] to select a node
115
+
116
+ ┌─────────────────────────────────────┐
117
+ │ ╭───╮ ╭───╮ ╭───╮ ╭───╮ │
118
+ │ │ 4 │ │ 3 │ │ 3 │ │ 3 │ │
119
+ │ ╰───╯ ╰───╯ ╰───╯ ╰───╯ │
120
+ │ ╭───╮ ╭───╮ │
121
+ │ │ 2 │ │ 4 │ │
122
+ │ ╰───╯ ╰───╯ │
123
+ │ ╭───╮ ╭───╮ ╭───╮ │
124
+ │ │ 3 │ │ 3 │ │ 3 │ │
125
+ │ ╰───╯ ╰───╯ ╰───╯ │
126
+ │ │
127
+ │ │
128
+ │ │
129
+ │ ╭───╮ ╭───╮ ╭───╮ │
130
+ │ │ 2 │ │ 8 │ │ 4 │ │
131
+ │ ╰───╯ ╰───╯ ╰───╯ │
132
+ │ ╭───╮ ╭───╮ │
133
+ │ │ 1 │ │ 3 │ │
134
+ │ ╰───╯ ╰───╯ │
135
+ │ ╭───╮ ╭───╮ ╭───╮ │
136
+ │ │ 1 │ │ 4 │ │ 1 │ │
137
+ │ ╰───╯ ╰───╯ ╰───╯ │
138
+ └─────────────────────────────────────┘
139
+
140
+ Controls:
141
+ p: Previous puzzle
142
+ n: Next puzzle
143
+ s: Show solution
144
+ q: Quit`)
32
145
  })
33
146
  })
34
147
 
35
- describe('game controls', () => {
148
+ describe('game controls - next/previous', () => {
36
149
  it('navigates to next puzzle with n key when interactive', async () => {
37
150
  const { stdin, lastFrame } = render(
38
151
  <Game
39
152
  puzzles={[TEST_PUZZLE, TEST_PUZZLE_2]}
40
153
  hasCustomPuzzle={false}
41
- stdout={false}
154
+ enableSolutions={false}
42
155
  />
43
156
  )
44
157
 
45
158
  expect(lastFrame()).toEqual(`Bridges: Puzzle #1
159
+ • Type a number [1-2] to select a node
46
160
 
47
161
  ┌─────────────────┐
48
162
  │ ╭───╮ ╭───╮ │
@@ -59,12 +173,12 @@ describe('Game', () => {
59
173
  Controls:
60
174
  p: Previous puzzle
61
175
  n: Next puzzle
62
- s: Show solution
63
176
  q: Quit`)
64
177
 
65
178
  stdin.write('n')
66
179
  await setTimeout(5)
67
180
  expect(lastFrame()).toEqual(`Bridges: Puzzle #2
181
+ • Type a number [1-3] to select a node
68
182
 
69
183
  ┌─────────────────┐
70
184
  │ ╭───╮ ╭───╮ │
@@ -81,7 +195,6 @@ q: Quit`)
81
195
  Controls:
82
196
  p: Previous puzzle
83
197
  n: Next puzzle
84
- s: Show solution
85
198
  q: Quit`)
86
199
  })
87
200
 
@@ -90,13 +203,14 @@ q: Quit`)
90
203
  <Game
91
204
  puzzles={[TEST_PUZZLE, TEST_PUZZLE_2]}
92
205
  hasCustomPuzzle={false}
93
- stdout={false}
206
+ enableSolutions={false}
94
207
  />
95
208
  )
96
209
 
97
210
  stdin.write('n')
98
211
  await setTimeout(5)
99
212
  expect(lastFrame()).toEqual(`Bridges: Puzzle #2
213
+ • Type a number [1-3] to select a node
100
214
 
101
215
  ┌─────────────────┐
102
216
  │ ╭───╮ ╭───╮ │
@@ -113,12 +227,12 @@ q: Quit`)
113
227
  Controls:
114
228
  p: Previous puzzle
115
229
  n: Next puzzle
116
- s: Show solution
117
230
  q: Quit`)
118
231
 
119
232
  stdin.write('p')
120
233
  await setTimeout(5)
121
234
  expect(lastFrame()).toEqual(`Bridges: Puzzle #1
235
+ • Type a number [1-2] to select a node
122
236
 
123
237
  ┌─────────────────┐
124
238
  │ ╭───╮ ╭───╮ │
@@ -135,18 +249,18 @@ q: Quit`)
135
249
  Controls:
136
250
  p: Previous puzzle
137
251
  n: Next puzzle
138
- s: Show solution
139
252
  q: Quit`)
140
253
  })
141
254
 
142
255
  it('does not navigate past last puzzle', async () => {
143
256
  const { stdin, lastFrame } = render(
144
- <Game puzzles={[TEST_PUZZLE]} hasCustomPuzzle={false} stdout={false} />
257
+ <Game puzzles={[TEST_PUZZLE]} hasCustomPuzzle={false} enableSolutions={false} />
145
258
  )
146
259
 
147
260
  stdin.write('n')
148
261
  await setTimeout(5)
149
262
  expect(lastFrame()).toEqual(`Bridges: Puzzle #1
263
+ • Type a number [1-2] to select a node
150
264
 
151
265
  ┌─────────────────┐
152
266
  │ ╭───╮ ╭───╮ │
@@ -163,18 +277,18 @@ q: Quit`)
163
277
  Controls:
164
278
  p: Previous puzzle
165
279
  n: Next puzzle
166
- s: Show solution
167
280
  q: Quit`)
168
281
  })
169
282
 
170
283
  it('does not navigate before first puzzle', async () => {
171
284
  const { stdin, lastFrame } = render(
172
- <Game puzzles={[TEST_PUZZLE]} hasCustomPuzzle={false} stdout={false} />
285
+ <Game puzzles={[TEST_PUZZLE]} hasCustomPuzzle={false} enableSolutions={false} />
173
286
  )
174
287
 
175
288
  stdin.write('p')
176
289
  await setTimeout(5)
177
290
  expect(lastFrame()).toEqual(`Bridges: Puzzle #1
291
+ • Type a number [1-2] to select a node
178
292
 
179
293
  ┌─────────────────┐
180
294
  │ ╭───╮ ╭───╮ │
@@ -191,7 +305,552 @@ q: Quit`)
191
305
  Controls:
192
306
  p: Previous puzzle
193
307
  n: Next puzzle
194
- s: Show solution
308
+ q: Quit`)
309
+ })
310
+ })
311
+
312
+ describe('game controls - node selection', () => {
313
+ it('selects node immediately when there is only one of that number', async () => {
314
+ const { stdin, lastFrame } = render(
315
+ <Game
316
+ puzzles={[SMALL_PUZZLE_3X3]}
317
+ hasCustomPuzzle={false}
318
+ enableSolutions={false}
319
+ />
320
+ )
321
+
322
+ // Press '3' to select single node of value 3
323
+ stdin.write('3')
324
+ await setTimeout(5)
325
+ expect(lastFrame()).toContain('Select direction with h/j/k/l')
326
+ expect(lastFrame()).not.toContain('╭a──╮')
327
+ })
328
+
329
+ it('shows disambiguation labels when multiple nodes have the same number', async () => {
330
+ const { stdin, lastFrame } = render(
331
+ <Game
332
+ puzzles={[SMALL_PUZZLE_3X3]}
333
+ hasCustomPuzzle={false}
334
+ enableSolutions={false}
335
+ />
336
+ )
337
+
338
+ // Press '2' to select from multiple nodes with value 2
339
+ stdin.write('2')
340
+ await setTimeout(5)
341
+ expect(lastFrame()).toContain('Press label shown to select that node')
342
+ expect(lastFrame()).toContain('╭a──╮')
343
+ expect(lastFrame()).toContain('╭b──╮')
344
+ })
345
+
346
+ it('selects a specific node when disambiguation label is pressed', async () => {
347
+ const { stdin, lastFrame } = render(
348
+ <Game
349
+ puzzles={[SMALL_PUZZLE_3X3]}
350
+ hasCustomPuzzle={false}
351
+ enableSolutions={false}
352
+ />
353
+ )
354
+
355
+ // Press '1' to select from multiple nodes with value 1
356
+ stdin.write('1')
357
+ await setTimeout(5)
358
+
359
+ // Press 'a' to select the second node
360
+ stdin.write('b')
361
+ await setTimeout(5)
362
+ expect(lastFrame()).toContain('Select direction with h/j/k/l')
363
+ })
364
+
365
+ it('draws a bridge when a valid direction is selected', async () => {
366
+ const { stdin, lastFrame } = render(
367
+ <Game
368
+ puzzles={[SMALL_PUZZLE_3X3]}
369
+ hasCustomPuzzle={false}
370
+ enableSolutions={false}
371
+ />
372
+ )
373
+
374
+ // Press '1' to enter disambiguation mode
375
+ stdin.write('1')
376
+ await setTimeout(5)
377
+
378
+ // Press 'b' to select the second node
379
+ stdin.write('b')
380
+ await setTimeout(5)
381
+
382
+ // Press 'l' to draw a bridge to the right
383
+ stdin.write('l')
384
+ await setTimeout(5)
385
+ expect(lastFrame()).toContain('Drew horizontal bridge')
386
+ })
387
+
388
+ it('shows an invalid message for a bad bridge direction off the grid', async () => {
389
+ const { stdin, lastFrame } = render(
390
+ <Game
391
+ puzzles={[SMALL_PUZZLE_3X3]}
392
+ hasCustomPuzzle={false}
393
+ enableSolutions={false}
394
+ />
395
+ )
396
+
397
+ // Press '1' to enter disambiguation mode
398
+ stdin.write('1')
399
+ await setTimeout(5)
400
+
401
+ // Press 'b' to select the second node
402
+ stdin.write('b')
403
+ await setTimeout(5)
404
+
405
+ // Press 'h' to draw a bridge to the left
406
+ stdin.write('h')
407
+ await setTimeout(5)
408
+ expect(lastFrame()).toContain('Cannot draw bridge left from node')
409
+ })
410
+
411
+ it('shows an invalid message for horizontal bridge colliding with bridge', async () => {
412
+ const puzzleWithBarrier = { encoding: '4x3:2a2a.a1|1.b3a' }
413
+ const { stdin, lastFrame } = render(
414
+ <Game
415
+ puzzles={[puzzleWithBarrier]}
416
+ hasCustomPuzzle={false}
417
+ enableSolutions={false}
418
+ />
419
+ )
420
+ expect(lastFrame()).toEqual(`Bridges: Puzzle #1
421
+ • Type a number [1-3] to select a node
422
+
423
+ ┌──────────────────────┐
424
+ │ ╭───╮ ╭───╮ │
425
+ │ │ 2 │ │ 2 │ │
426
+ │ ╰───╯ ╰─┬─╯ │
427
+ │ ╭───╮ │ ╭───╮ │
428
+ │ │ 1 │ │ │ 1 │ │
429
+ │ ╰───╯ │ ╰───╯ │
430
+ │ ╭─┴─╮ │
431
+ │ │ 3 │ │
432
+ │ ╰───╯ │
433
+ └──────────────────────┘
434
+
435
+ Controls:
436
+ p: Previous puzzle
437
+ n: Next puzzle
438
+ q: Quit`)
439
+
440
+ stdin.write('1')
441
+ await setTimeout(5)
442
+ stdin.write('a')
443
+ await setTimeout(5)
444
+ stdin.write('l')
445
+ await setTimeout(5)
446
+ expect(lastFrame()).toContain('Cannot draw bridge right from node')
447
+ })
448
+
449
+ it('shows an invalid message for vertical bridge colliding with bridge', async () => {
450
+ const puzzleWithBarrier = { encoding: '4x3:2a2a.a3=3.b1a' }
451
+ const { stdin, lastFrame } = render(
452
+ <Game
453
+ puzzles={[puzzleWithBarrier]}
454
+ hasCustomPuzzle={false}
455
+ enableSolutions={false}
456
+ />
457
+ )
458
+ expect(lastFrame()).toEqual(`Bridges: Puzzle #1
459
+ • Type a number [1-3] to select a node
460
+
461
+ ┌──────────────────────┐
462
+ │ ╭───╮ ╭───╮ │
463
+ │ │ 2 │ │ 2 │ │
464
+ │ ╰───╯ ╰───╯ │
465
+ │ ╭───╮ ╭───╮ │
466
+ │ │ 3 ╞═════╡ 3 │ │
467
+ │ ╰───╯ ╰───╯ │
468
+ │ ╭───╮ │
469
+ │ │ 1 │ │
470
+ │ ╰───╯ │
471
+ └──────────────────────┘
472
+
473
+ Controls:
474
+ p: Previous puzzle
475
+ n: Next puzzle
476
+ q: Quit`)
477
+
478
+ stdin.write('2')
479
+ await setTimeout(5)
480
+ stdin.write('b')
481
+ await setTimeout(5)
482
+ stdin.write('j')
483
+ await setTimeout(5)
484
+ expect(lastFrame()).toContain('Cannot draw bridge down from node')
485
+ })
486
+
487
+ it('resets selection when Escape is pressed', async () => {
488
+ const { stdin, lastFrame } = render(
489
+ <Game
490
+ puzzles={[SMALL_PUZZLE_3X3]}
491
+ hasCustomPuzzle={false}
492
+ enableSolutions={false}
493
+ />
494
+ )
495
+
496
+ stdin.write('2')
497
+ await setTimeout(5)
498
+ expect(lastFrame()).toContain('Press label shown')
499
+
500
+ // Note: Escape key handling may not work in test environment
501
+ // Testing that we entered disambiguation mode successfully
502
+ stdin.write('')
503
+ await setTimeout(5)
504
+ expect(lastFrame()).toContain('Type a number')
505
+ })
506
+ })
507
+
508
+ describe('game controls - drawing each kind of bridge', () => {
509
+ it('draws horizontal bridge', async () => {
510
+ const puzzleWithEachBridge = { encoding: '3x3:2a3.c.3a4' }
511
+ const { stdin, lastFrame } = render(
512
+ <Game
513
+ puzzles={[puzzleWithEachBridge]}
514
+ hasCustomPuzzle={false}
515
+ enableSolutions={false}
516
+ />
517
+ )
518
+ stdin.write('3')
519
+ await setTimeout(5)
520
+ expect(lastFrame()).toContain('Press label shown to select that node')
521
+ stdin.write('a')
522
+ await setTimeout(5)
523
+ expect(lastFrame()).toContain('Select direction with h/j/k/l')
524
+ stdin.write('h')
525
+ await setTimeout(5)
526
+ expect(lastFrame()).toEqual(`Bridges: Puzzle #1
527
+ • Drew horizontal bridge
528
+
529
+ ┌─────────────────┐
530
+ │ ╭───╮ ╭───╮ │
531
+ │ │ 2 ├─────┤ 3 │ │
532
+ │ ╰───╯ ╰───╯ │
533
+ │ │
534
+ │ │
535
+ │ │
536
+ │ ╭───╮ ╭───╮ │
537
+ │ │ 3 │ │ 4 │ │
538
+ │ ╰───╯ ╰───╯ │
539
+ └─────────────────┘
540
+
541
+ Controls:
542
+ p: Previous puzzle
543
+ n: Next puzzle
544
+ q: Quit`)
545
+ })
546
+
547
+ it('draws a vertical bridge', async () => {
548
+ const puzzleWithEachBridge = { encoding: '3x3:2a3.c.3a4' }
549
+ const { stdin, lastFrame } = render(
550
+ <Game
551
+ puzzles={[puzzleWithEachBridge]}
552
+ hasCustomPuzzle={false}
553
+ enableSolutions={false}
554
+ />
555
+ )
556
+ stdin.write('2')
557
+ await setTimeout(5)
558
+ stdin.write('j')
559
+ await setTimeout(5)
560
+ expect(lastFrame()).toEqual(`Bridges: Puzzle #1
561
+ • Drew vertical bridge
562
+
563
+ ┌─────────────────┐
564
+ │ ╭───╮ ╭───╮ │
565
+ │ │ 2 │ │ 3 │ │
566
+ │ ╰─┬─╯ ╰───╯ │
567
+ │  │  │
568
+ │  │  │
569
+ │  │  │
570
+ │ ╭─┴─╮ ╭───╮ │
571
+ │ │ 3 │ │ 4 │ │
572
+ │ ╰───╯ ╰───╯ │
573
+ └─────────────────┘
574
+
575
+ Controls:
576
+ p: Previous puzzle
577
+ n: Next puzzle
578
+ q: Quit`)
579
+ })
580
+
581
+ it('draws a double horizontal bridge', async () => {
582
+ const puzzleWithEachBridge = { encoding: '3x3:3a4.c.2a4' }
583
+ const { stdin, lastFrame } = render(
584
+ <Game
585
+ puzzles={[puzzleWithEachBridge]}
586
+ hasCustomPuzzle={false}
587
+ enableSolutions={false}
588
+ />
589
+ )
590
+ stdin.write('3')
591
+ await setTimeout(5)
592
+ expect(lastFrame()).toContain('Select direction with h/j/k/l')
593
+ stdin.write('L')
594
+ await setTimeout(5)
595
+ expect(lastFrame()).toEqual(`Bridges: Puzzle #1
596
+ • Drew double horizontal bridge
597
+
598
+ ┌─────────────────┐
599
+ │ ╭───╮ ╭───╮ │
600
+ │ │ 3 ╞═════╡ 4 │ │
601
+ │ ╰───╯ ╰───╯ │
602
+ │ │
603
+ │ │
604
+ │ │
605
+ │ ╭───╮ ╭───╮ │
606
+ │ │ 2 │ │ 4 │ │
607
+ │ ╰───╯ ╰───╯ │
608
+ └─────────────────┘
609
+
610
+ Controls:
611
+ p: Previous puzzle
612
+ n: Next puzzle
613
+ q: Quit`)
614
+ })
615
+
616
+ it('draws a double vertical bridge', async () => {
617
+ const puzzleWithEachBridge = { encoding: '3x3:3a2.c.4a2' }
618
+ const { stdin, lastFrame } = render(
619
+ <Game
620
+ puzzles={[puzzleWithEachBridge]}
621
+ hasCustomPuzzle={false}
622
+ enableSolutions={false}
623
+ />
624
+ )
625
+ stdin.write('3')
626
+ await setTimeout(5)
627
+ stdin.write('J')
628
+ await setTimeout(5)
629
+ expect(lastFrame()).toEqual(`Bridges: Puzzle #1
630
+ • Drew double vertical bridge
631
+
632
+ ┌─────────────────┐
633
+ │ ╭───╮ ╭───╮ │
634
+ │ │ 3 │ │ 2 │ │
635
+ │ ╰─╥─╯ ╰───╯ │
636
+ │  ║  │
637
+ │  ║  │
638
+ │  ║  │
639
+ │ ╭─╨─╮ ╭───╮ │
640
+ │ │ 4 │ │ 2 │ │
641
+ │ ╰───╯ ╰───╯ │
642
+ └─────────────────┘
643
+
644
+ Controls:
645
+ p: Previous puzzle
646
+ n: Next puzzle
647
+ q: Quit`)
648
+ })
649
+
650
+ it('does not draw a bridge over an existing bridge', async () => {
651
+ const puzzleWithEachBridge = { encoding: '4x3:1a3a.a2#2.3a4a' }
652
+ const { stdin, lastFrame } = render(
653
+ <Game
654
+ puzzles={[puzzleWithEachBridge]}
655
+ hasCustomPuzzle={false}
656
+ enableSolutions={false}
657
+ />
658
+ )
659
+ expect(lastFrame()).toEqual(`Bridges: Puzzle #1
660
+ • Type a number [1-4] to select a node
661
+
662
+ ┌──────────────────────┐
663
+ │ ╭───╮ ╭───╮ │
664
+ │ │ 1 │ │ 3 │ │
665
+ │ ╰───╯ ╰─╥─╯ │
666
+ │ ╭───╮ ║ ╭───╮ │
667
+ │ │ 2 │ ║ │ 2 │ │
668
+ │ ╰───╯ ║ ╰───╯ │
669
+ │ ╭───╮ ╭─╨─╮ │
670
+ │ │ 3 │ │ 4 │ │
671
+ │ ╰───╯ ╰───╯ │
672
+ └──────────────────────┘
673
+
674
+ Controls:
675
+ p: Previous puzzle
676
+ n: Next puzzle
677
+ q: Quit`)
678
+
679
+ stdin.write('2')
680
+ await setTimeout(5)
681
+ stdin.write('a')
682
+ await setTimeout(5)
683
+ stdin.write('l')
684
+ await setTimeout(5)
685
+ expect(lastFrame()).toContain('Cannot draw bridge right from node')
686
+ })
687
+
688
+ it('erases a bridge', async () => {
689
+ const puzzleWithEachBridge = { encoding: '3x3:2a3.c.3a4' }
690
+ const { stdin, lastFrame } = render(
691
+ <Game
692
+ puzzles={[puzzleWithEachBridge]}
693
+ hasCustomPuzzle={false}
694
+ enableSolutions={false}
695
+ />
696
+ )
697
+ stdin.write('3')
698
+ await setTimeout(5)
699
+ stdin.write('a')
700
+ await setTimeout(5)
701
+ stdin.write('h')
702
+ await setTimeout(5)
703
+ expect(lastFrame()).toContain('Drew horizontal bridge')
704
+
705
+ stdin.write('3')
706
+ await setTimeout(5)
707
+ stdin.write('a')
708
+ await setTimeout(5)
709
+ stdin.write('h')
710
+ await setTimeout(5)
711
+ expect(lastFrame()).toContain('Erased bridge')
712
+
713
+ // Verify the bridge was actually erased (grid shows no bridge)
714
+ expect(lastFrame()).not.toContain('╞═════')
715
+ expect(lastFrame()).not.toContain('═╡')
716
+ })
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
195
854
  q: Quit`)
196
855
  })
197
856
  })