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.
- package/AGENTS.md +8 -6
- package/README.md +14 -7
- package/package.json +4 -4
- package/src/Game.tsx +169 -13
- package/src/__tests__/Game.test.tsx +681 -22
- package/src/components/HashiGrid.tsx +46 -44
- package/src/components/HashiRow.tsx +67 -66
- package/src/components/Header.tsx +48 -2
- package/src/components/Messages.tsx +48 -8
- package/src/components/__tests__/HashiGrid.test.tsx +1 -0
- package/src/components/__tests__/HashiRow.test.tsx +128 -115
- package/src/components/__tests__/Header.test.tsx +156 -0
- package/src/components/__tests__/Messages.test.tsx +71 -3
- package/src/demo.tsx +20 -0
- package/src/index.tsx +31 -5
- package/src/types.ts +34 -0
- package/src/utils/__tests__/bridges.test.tsx +137 -0
- package/src/utils/__tests__/findNodeInDirection.test.ts +75 -0
- package/src/utils/__tests__/{parsePuzzle.test.ts → puzzle-encoding.test.ts} +1 -1
- package/src/utils/bridges.ts +395 -0
- package/src/utils/puzzle-encoding.ts +368 -0
- package/src/utils/usePuzzleInput.ts +387 -13
- package/src/utils/parsePuzzle.ts +0 -178
- package/src/utils/samplePuzzles.ts +0 -59
|
@@ -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('
|
|
20
|
-
it('
|
|
21
|
-
const { lastFrame } = render(
|
|
22
|
-
<Game
|
|
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
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
+
│ [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 │
|
|
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
|
-
|
|
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
|
-
|
|
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}
|
|
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}
|
|
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
|
-
|
|
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
|
+
│ [2m╭───╮[22m [1m╭───╮[22m │
|
|
531
|
+
│ [2m│ 2 ├─────[22m[1m┤ 3 │[22m │
|
|
532
|
+
│ [2m╰───╯[22m [1m╰───╯[22m │
|
|
533
|
+
│ │
|
|
534
|
+
│ │
|
|
535
|
+
│ │
|
|
536
|
+
│ [2m╭───╮[22m [2m╭───╮[22m │
|
|
537
|
+
│ [2m│ 3 │[22m [2m│ 4 │[22m │
|
|
538
|
+
│ [2m╰───╯[22m [2m╰───╯[22m │
|
|
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
|
+
│ [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 │
|
|
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
|
+
│ [1m╭───╮[22m [2m╭───╮[22m │
|
|
600
|
+
│ [1m│ 3 ╞[22m[2m═════╡ 4 │[22m │
|
|
601
|
+
│ [1m╰───╯[22m [2m╰───╯[22m │
|
|
602
|
+
│ │
|
|
603
|
+
│ │
|
|
604
|
+
│ │
|
|
605
|
+
│ [2m╭───╮[22m [2m╭───╮[22m │
|
|
606
|
+
│ [2m│ 2 │[22m [2m│ 4 │[22m │
|
|
607
|
+
│ [2m╰───╯[22m [2m╰───╯[22m │
|
|
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
|
+
│ [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 │
|
|
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
|
+
│ [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
|
|
195
854
|
q: Quit`)
|
|
196
855
|
})
|
|
197
856
|
})
|