bridges-cli 0.0.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,119 +1,12 @@
1
1
  import { render } from 'ink-testing-library'
2
2
  import { describe, expect, it } from 'vitest'
3
-
4
- import type { HashiNodeData } from '../../types.ts'
5
- import HashiRow, { constructNode } from '../HashiRow.tsx'
6
-
7
- describe('constructNode()', () => {
8
- describe('empty node', () => {
9
- it('renders space value with no lines', () => {
10
- const node: HashiNodeData = { value: ' ' }
11
- expect(constructNode(node, 0)).toEqual(' ')
12
- expect(constructNode(node, 1)).toEqual(' ')
13
- expect(constructNode(node, 2)).toEqual(' ')
14
- })
15
- })
16
-
17
- describe('horizontal line node', () => {
18
- it('renders a horizontal line in the middle', () => {
19
- const node: HashiNodeData = { value: '-' }
20
- expect(constructNode(node, 0)).toEqual(' ')
21
- expect(constructNode(node, 1)).toEqual('─────')
22
- expect(constructNode(node, 2)).toEqual(' ')
23
- })
24
-
25
- it('renders a double horizontal line in the middle', () => {
26
- const node: HashiNodeData = { value: '=' }
27
- expect(constructNode(node, 0)).toEqual(' ')
28
- expect(constructNode(node, 1)).toEqual('═════')
29
- expect(constructNode(node, 2)).toEqual(' ')
30
- })
31
- })
32
-
33
- describe('vertical line node', () => {
34
- it('renders a vertical line in the center', () => {
35
- const node: HashiNodeData = { value: '|' }
36
- expect(constructNode(node, 0)).toEqual(' │ ')
37
- expect(constructNode(node, 1)).toEqual(' │ ')
38
- expect(constructNode(node, 2)).toEqual(' │ ')
39
- })
40
-
41
- it('renders a double vertical line in the center', () => {
42
- const node: HashiNodeData = { value: '#' }
43
- expect(constructNode(node, 0)).toEqual(' ║ ')
44
- expect(constructNode(node, 1)).toEqual(' ║ ')
45
- expect(constructNode(node, 2)).toEqual(' ║ ')
46
- })
47
- })
48
-
49
- describe('node with value', () => {
50
- describe('TOP_ROW', () => {
51
- it('renders top border', () => {
52
- const node: HashiNodeData = { value: 5 }
53
- expect(constructNode(node, 0)).toEqual('╭───╮')
54
- })
55
-
56
- it('renders border with vertical line up', () => {
57
- const node: HashiNodeData = { value: 5, lineUp: 1 }
58
- expect(constructNode(node, 0)).toEqual('╭─┴─╮')
59
- })
60
-
61
- it('renders border with double vertical line up', () => {
62
- const node: HashiNodeData = { value: 5, lineUp: 2 }
63
- expect(constructNode(node, 0)).toEqual('╭─╨─╮')
64
- })
65
- })
66
-
67
- describe('MIDDLE_ROW', () => {
68
- it('renders middle row - value with vertical borders', () => {
69
- const node: HashiNodeData = { value: 5 }
70
- expect(constructNode(node, 1)).toEqual('│ 5 │')
71
- })
72
-
73
- it('renders value with horizontal line on left', () => {
74
- const node: HashiNodeData = { value: 5, lineLeft: 1 }
75
- expect(constructNode(node, 1)).toEqual('┤ 5 │')
76
- })
77
-
78
- it('renders value with horizontal line on right', () => {
79
- const node: HashiNodeData = { value: 5, lineRight: 1 }
80
- expect(constructNode(node, 1)).toEqual('│ 5 ├')
81
- })
82
-
83
- it('renders value with horizontal lines on both sides', () => {
84
- const node: HashiNodeData = { value: 5, lineLeft: 1, lineRight: 1 }
85
- expect(constructNode(node, 1)).toEqual('┤ 5 ├')
86
- })
87
-
88
- it('renders value with double horizontal lines on both sides', () => {
89
- const node: HashiNodeData = { value: 5, lineLeft: 2, lineRight: 2 }
90
- expect(constructNode(node, 1)).toEqual('╡ 5 ╞')
91
- })
92
- })
93
-
94
- describe('BOTTOM_ROW', () => {
95
- it('renders bottom border without lines', () => {
96
- const node: HashiNodeData = { value: 5 }
97
- expect(constructNode(node, 2)).toEqual('╰───╯')
98
- })
99
-
100
- it('renders border with vertical line down', () => {
101
- const node: HashiNodeData = { value: 5, lineDown: 1 }
102
- expect(constructNode(node, 2)).toEqual('╰─┬─╯')
103
- })
104
-
105
- it('renders border with double vertical line down', () => {
106
- const node: HashiNodeData = { value: 5, lineDown: 2 }
107
- expect(constructNode(node, 2)).toEqual('╰─╥─╯')
108
- })
109
- })
110
- })
111
- })
3
+ import HashiRow from '../../components/HashiRow.tsx'
4
+ import type { SelectionState } from '../../types.ts'
112
5
 
113
6
  describe('HashiRow component', () => {
114
7
  it('renders three nodes', () => {
115
8
  const { lastFrame } = render(
116
- <HashiRow nodes={[{ value: 1 }, { value: 2 }, { value: 3 }]} />
9
+ <HashiRow nodes={[{ value: 1 }, { value: 2 }, { value: 3 }]} rowIndex={0} />
117
10
  )
118
11
  expect(lastFrame()).toEqual(
119
12
  ` ╭───╮╭───╮╭───╮
@@ -126,16 +19,17 @@ describe('HashiRow component', () => {
126
19
  const { lastFrame } = render(
127
20
  <HashiRow
128
21
  nodes={[{ value: 1, lineRight: 1 }, { value: '-' }, { value: 3, lineLeft: 1 }]}
22
+ rowIndex={0}
129
23
  />
130
24
  )
131
- expect(lastFrame()).toEqual(` ╭───╮ ╭───╮
132
- │ 1 ├─────┤ 3 │
133
- ╰───╯ ╰───╯`)
25
+ expect(lastFrame()).toEqual(` \x1b[32m╭───╮\x1b[39m ╭───╮
26
+ \x1b[32m│ 1 ├\x1b[39m─────┤ 3 │
27
+ \x1b[32m╰───╯\x1b[39m ╰───╯`)
134
28
  })
135
29
 
136
30
  it('renders a vertical node', () => {
137
31
  const { lastFrame } = render(
138
- <HashiRow nodes={[{ value: 1 }, { value: '|' }, { value: 3 }]} />
32
+ <HashiRow nodes={[{ value: 1 }, { value: '|' }, { value: 3 }]} rowIndex={0} />
139
33
  )
140
34
  expect(lastFrame()).toEqual(` ╭───╮ │ ╭───╮
141
35
  │ 1 │ │ │ 3 │
@@ -144,7 +38,7 @@ describe('HashiRow component', () => {
144
38
 
145
39
  it('renders empty positions as spaces', () => {
146
40
  const { lastFrame } = render(
147
- <HashiRow nodes={[{ value: 1 }, { value: ' ' }, { value: 3 }]} />
41
+ <HashiRow nodes={[{ value: 1 }, { value: ' ' }, { value: 3 }]} rowIndex={0} />
148
42
  )
149
43
  expect(lastFrame()).toEqual(
150
44
  ` ╭───╮ ╭───╮
@@ -152,4 +46,123 @@ describe('HashiRow component', () => {
152
46
  ╰───╯ ╰───╯`
153
47
  )
154
48
  })
49
+
50
+ describe('highlighted nodes', () => {
51
+ it('renders highlighted node with bold when value matches', () => {
52
+ const { lastFrame } = render(
53
+ <HashiRow nodes={[{ value: 1 }]} rowIndex={0} highlightedNode={1} />
54
+ )
55
+ expect(lastFrame()).toEqual(
56
+ ` \x1b[1m╭───╮\x1b[22m
57
+ \x1b[1m│ 1 │\x1b[22m
58
+ \x1b[1m╰───╯\x1b[22m`
59
+ )
60
+ })
61
+
62
+ it('renders dimmed node when value does not match', () => {
63
+ const { lastFrame } = render(
64
+ <HashiRow nodes={[{ value: 1 }]} rowIndex={0} highlightedNode={2} />
65
+ )
66
+ expect(lastFrame()).toEqual(
67
+ ` \x1b[2m╭───╮\x1b[22m
68
+ \x1b[2m│ 1 │\x1b[22m
69
+ \x1b[2m╰───╯\x1b[22m`
70
+ )
71
+ })
72
+
73
+ it('renders multiple nodes with one highlighted and others dimmed', () => {
74
+ const { lastFrame } = render(
75
+ <HashiRow
76
+ nodes={[{ value: 1 }, { value: 2 }, { value: 3 }]}
77
+ rowIndex={0}
78
+ highlightedNode={2}
79
+ />
80
+ )
81
+ expect(lastFrame()).toEqual(
82
+ ` \x1b[2m╭───╮\x1b[22m\x1b[1m╭───╮\x1b[22m\x1b[2m╭───╮\x1b[22m
83
+ \x1b[2m│ 1 │\x1b[22m\x1b[1m│ 2 │\x1b[22m\x1b[2m│ 3 │\x1b[22m
84
+ \x1b[2m╰───╯\x1b[22m\x1b[1m╰───╯\x1b[22m\x1b[2m╰───╯\x1b[22m`
85
+ )
86
+ })
87
+
88
+ it('renders normal when highlightedNode is undefined', () => {
89
+ const { lastFrame } = render(<HashiRow nodes={[{ value: 1 }]} rowIndex={0} />)
90
+ expect(lastFrame()).toEqual(
91
+ ` ╭───╮
92
+ │ 1 │
93
+ ╰───╯`
94
+ )
95
+ })
96
+
97
+ it('highlights only specific node in selecting-node mode', () => {
98
+ const selectionState: SelectionState = {
99
+ mode: 'selecting-node',
100
+ selectedNumber: 1,
101
+ direction: null,
102
+ matchingNodes: [{ row: 0, col: 1 }],
103
+ disambiguationLabels: [],
104
+ selectedNode: { row: 0, col: 1 },
105
+ }
106
+ const { lastFrame } = render(
107
+ <HashiRow
108
+ nodes={[{ value: 1 }, { value: 1 }, { value: 1 }]}
109
+ rowIndex={0}
110
+ selectionState={selectionState}
111
+ />
112
+ )
113
+ expect(lastFrame()).toEqual(
114
+ ` \x1b[2m╭───╮\x1b[22m\x1b[1m╭───╮\x1b[22m\x1b[2m╭───╮\x1b[22m
115
+ \x1b[2m│ 1 │\x1b[22m\x1b[1m│ 1 │\x1b[22m\x1b[2m│ 1 │\x1b[22m
116
+ \x1b[2m╰───╯\x1b[22m\x1b[1m╰───╯\x1b[22m\x1b[2m╰───╯\x1b[22m`
117
+ )
118
+ })
119
+
120
+ it('dims non-selected nodes in invalid mode but keeps selected highlighted', () => {
121
+ const selectionState: SelectionState = {
122
+ mode: 'invalid',
123
+ selectedNumber: 1,
124
+ direction: 'h',
125
+ matchingNodes: [{ row: 0, col: 1 }],
126
+ disambiguationLabels: [],
127
+ selectedNode: { row: 0, col: 1 },
128
+ }
129
+ const { lastFrame } = render(
130
+ <HashiRow
131
+ nodes={[{ value: 1 }, { value: 1 }, { value: 1 }]}
132
+ rowIndex={0}
133
+ selectionState={selectionState}
134
+ />
135
+ )
136
+ expect(lastFrame()).toEqual(
137
+ ` \x1b[2m╭───╮\x1b[22m\x1b[1m╭───╮\x1b[22m\x1b[2m╭───╮\x1b[22m
138
+ \x1b[2m│ 1 │\x1b[22m\x1b[1m│ 1 │\x1b[22m\x1b[2m│ 1 │\x1b[22m
139
+ \x1b[2m╰───╯\x1b[22m\x1b[1m╰───╯\x1b[22m\x1b[2m╰───╯\x1b[22m`
140
+ )
141
+ })
142
+ })
143
+
144
+ describe('validation state colors', () => {
145
+ it('shows green on valid nodes in selected mode', () => {
146
+ const selectionState: SelectionState = {
147
+ mode: 'selected',
148
+ selectedNumber: 1,
149
+ direction: 'h',
150
+ matchingNodes: [{ row: 0, col: 0 }],
151
+ disambiguationLabels: [],
152
+ selectedNode: { row: 0, col: 0 },
153
+ }
154
+ const { lastFrame } = render(
155
+ <HashiRow
156
+ nodes={[{ value: 1, lineRight: 1 }, { value: ' ' }, { value: 1, lineLeft: 1 }]}
157
+ rowIndex={0}
158
+ selectionState={selectionState}
159
+ />
160
+ )
161
+ expect(lastFrame()).toEqual(
162
+ ` \x1b[32m\x1b[1m╭───╮\x1b[22m\x1b[39m \x1b[32m\x1b[2m╭───╮\x1b[22m\x1b[39m
163
+ \x1b[32m\x1b[1m│ 1 ├\x1b[22m\x1b[39m \x1b[32m\x1b[2m┤ 1 │\x1b[22m\x1b[39m
164
+ \x1b[32m\x1b[1m╰───╯\x1b[22m\x1b[39m \x1b[32m\x1b[2m╰───╯\x1b[22m\x1b[39m`
165
+ )
166
+ })
167
+ })
155
168
  })
@@ -0,0 +1,156 @@
1
+ import { render } from 'ink-testing-library'
2
+ import { describe, expect, it } from 'vitest'
3
+ import type { SelectionState } from '../../types.ts'
4
+ import Header from '../Header.tsx'
5
+
6
+ describe('Header', () => {
7
+ it('shows idle message when no selection state and no number range', () => {
8
+ const { lastFrame } = render(<Header puzzleIndex={0} puzzle="5x5:1a1" />)
9
+ expect(lastFrame()).toContain('Type a number to select a node')
10
+ })
11
+
12
+ it('shows dynamic number range when provided', () => {
13
+ const { lastFrame } = render(
14
+ <Header puzzleIndex={0} puzzle="5x5:1a1" minNumber={2} maxNumber={7} />
15
+ )
16
+ expect(lastFrame()).toContain('Type a number [2-7] to select a node')
17
+ })
18
+
19
+ describe('selection state messages', () => {
20
+ it('shows "select direction" message after we disambiguate which node', () => {
21
+ const selectionState: SelectionState = {
22
+ mode: 'selecting-node',
23
+ selectedNumber: 1,
24
+ direction: null,
25
+ matchingNodes: [
26
+ { row: 0, col: 0 },
27
+ { row: 1, col: 1 },
28
+ ],
29
+ disambiguationLabels: [],
30
+ selectedNode: { row: 0, col: 0 },
31
+ }
32
+ const { lastFrame } = render(
33
+ <Header puzzleIndex={0} puzzle="5x5:1a1" selectionState={selectionState} />
34
+ )
35
+ expect(lastFrame()).toContain(`Select direction with`)
36
+ })
37
+
38
+ it('shows disambiguation message without direction', () => {
39
+ const selectionState: SelectionState = {
40
+ mode: 'disambiguation',
41
+ selectedNumber: 1,
42
+ direction: null,
43
+ matchingNodes: [
44
+ { row: 0, col: 0 },
45
+ { row: 1, col: 2 },
46
+ ],
47
+ disambiguationLabels: ['a', 'b'],
48
+ selectedNode: null,
49
+ }
50
+ const { lastFrame } = render(
51
+ <Header puzzleIndex={0} puzzle="5x5:1a1" selectionState={selectionState} />
52
+ )
53
+ expect(lastFrame()).toContain('Press label shown to select that node')
54
+ })
55
+
56
+ it('shows disambiguation message with direction', () => {
57
+ const selectionState: SelectionState = {
58
+ mode: 'disambiguation',
59
+ selectedNumber: 1,
60
+ direction: 'h',
61
+ matchingNodes: [
62
+ { row: 0, col: 0 },
63
+ { row: 1, col: 2 },
64
+ ],
65
+ disambiguationLabels: ['a', 'b'],
66
+ selectedNode: null,
67
+ }
68
+ const { lastFrame } = render(
69
+ <Header puzzleIndex={0} puzzle="5x5:1a1" selectionState={selectionState} />
70
+ )
71
+ expect(lastFrame()).toContain('Press label shown to select that node')
72
+ })
73
+
74
+ it('shows selected message with direction (horizontal)', () => {
75
+ const selectionState: SelectionState = {
76
+ mode: 'selected',
77
+ selectedNumber: 1,
78
+ direction: 'l',
79
+ matchingNodes: [{ row: 0, col: 0 }],
80
+ disambiguationLabels: [],
81
+ selectedNode: { row: 0, col: 0 },
82
+ }
83
+ const { lastFrame } = render(
84
+ <Header puzzleIndex={0} puzzle="5x5:1a1" selectionState={selectionState} />
85
+ )
86
+ expect(lastFrame()).toContain('Drew horizontal bridge')
87
+ })
88
+
89
+ it('shows selected message with direction (vertical)', () => {
90
+ const selectionState: SelectionState = {
91
+ mode: 'selected',
92
+ selectedNumber: 1,
93
+ direction: 'k',
94
+ matchingNodes: [{ row: 0, col: 0 }],
95
+ disambiguationLabels: [],
96
+ selectedNode: { row: 0, col: 0 },
97
+ }
98
+ const { lastFrame } = render(
99
+ <Header puzzleIndex={0} puzzle="5x5:1a1" selectionState={selectionState} />
100
+ )
101
+ expect(lastFrame()).toContain('Drew vertical bridge')
102
+ })
103
+
104
+ it('shows erased bridge message when bridgeErased is true', () => {
105
+ const selectionState: SelectionState = {
106
+ mode: 'selected',
107
+ selectedNumber: 1,
108
+ direction: 'l',
109
+ matchingNodes: [{ row: 0, col: 0 }],
110
+ disambiguationLabels: [],
111
+ selectedNode: { row: 0, col: 0 },
112
+ bridgeErased: true,
113
+ }
114
+ const { lastFrame } = render(
115
+ <Header puzzleIndex={0} puzzle="5x5:1a1" selectionState={selectionState} />
116
+ )
117
+ expect(lastFrame()).toContain('Erased bridge')
118
+ })
119
+
120
+ it('shows invalid message when no node in direction', () => {
121
+ const selectionState: SelectionState = {
122
+ mode: 'invalid',
123
+ selectedNumber: 1,
124
+ direction: 'h',
125
+ matchingNodes: [{ row: 0, col: 0 }],
126
+ disambiguationLabels: [],
127
+ selectedNode: { row: 0, col: 0 },
128
+ }
129
+ const { lastFrame } = render(
130
+ <Header puzzleIndex={0} puzzle="5x5:1a1" selectionState={selectionState} />
131
+ )
132
+ expect(lastFrame()).toContain('Cannot draw bridge left from node')
133
+ })
134
+ })
135
+
136
+ describe('puzzle display', () => {
137
+ it('shows puzzle number for indexed puzzles', () => {
138
+ const { lastFrame } = render(<Header puzzleIndex={2} puzzle="5x5:1a1" />)
139
+ expect(lastFrame()).toContain('Bridges: Puzzle #3')
140
+ })
141
+
142
+ it('shows solution suffix when showSolution is true', () => {
143
+ const { lastFrame } = render(
144
+ <Header puzzleIndex={0} puzzle="5x5:1a1" showSolution={true} />
145
+ )
146
+ expect(lastFrame()).toContain('Viewing solution (press s to return to puzzle)')
147
+ })
148
+
149
+ it('shows custom puzzle label for custom puzzles', () => {
150
+ const { lastFrame } = render(
151
+ <Header puzzleIndex={0} puzzle="5x5:1a1" isCustomPuzzle={true} />
152
+ )
153
+ expect(lastFrame()).toContain('Bridges: Puzzle - 5x5:1a1')
154
+ })
155
+ })
156
+ })
@@ -1,14 +1,82 @@
1
1
  import { render } from 'ink-testing-library'
2
2
  import { describe, expect, it } from 'vitest'
3
3
 
4
- import Messages from '../Messages.tsx'
4
+ import Messages, { legendItems } from '../Messages.tsx'
5
5
 
6
6
  describe('Messages', () => {
7
- it('renders the legend', () => {
8
- const { lastFrame } = render(<Messages />)
7
+ it('does not show solution option when enableSolutions is false', () => {
8
+ const { lastFrame } = render(<Messages enableSolutions={false} />)
9
9
  expect(lastFrame()).toContain('Controls:')
10
10
  expect(lastFrame()).toContain('p: Previous puzzle')
11
11
  expect(lastFrame()).toContain('n: Next puzzle')
12
+ expect(lastFrame()).not.toContain('s: Show solution')
12
13
  expect(lastFrame()).toContain('q: Quit')
13
14
  })
15
+
16
+ it('shows solution option when enableSolutions is true', () => {
17
+ const { lastFrame } = render(<Messages enableSolutions={true} />)
18
+ expect(lastFrame()).toContain('Controls:')
19
+ expect(lastFrame()).toContain('p: Previous puzzle')
20
+ expect(lastFrame()).toContain('n: Next puzzle')
21
+ expect(lastFrame()).toContain('s: Show solution')
22
+ expect(lastFrame()).toContain('q: Quit')
23
+ })
24
+
25
+ describe('legendItems', () => {
26
+ describe('when enableSolutions is false', () => {
27
+ it('shows n/p as enabled when idle', () => {
28
+ const items = legendItems(true, false, false)
29
+ const p = items.find(i => i.key === 'p')
30
+ const n = items.find(i => i.key === 'n')
31
+ const s = items.find(i => i.key === 's')
32
+ expect(p?.disabled).toBe(false)
33
+ expect(n?.disabled).toBe(false)
34
+ expect(s).toBeUndefined()
35
+ })
36
+
37
+ it('shows n/p as disabled in selecting-node mode', () => {
38
+ const items = legendItems(true, false, true)
39
+ const p = items.find(i => i.key === 'p')
40
+ const n = items.find(i => i.key === 'n')
41
+ const s = items.find(i => i.key === 's')
42
+ expect(p?.disabled).toBe(true)
43
+ expect(n?.disabled).toBe(true)
44
+ expect(s).toBeUndefined()
45
+ })
46
+ })
47
+
48
+ describe('when enableSolutions is true', () => {
49
+ it('shows n/p/s as enabled when idle', () => {
50
+ const items = legendItems(true, true, false)
51
+ const p = items.find(i => i.key === 'p')
52
+ const n = items.find(i => i.key === 'n')
53
+ const s = items.find(i => i.key === 's')
54
+ expect(p?.disabled).toBe(false)
55
+ expect(n?.disabled).toBe(false)
56
+ expect(s?.disabled).toBe(false)
57
+ })
58
+
59
+ it('shows n/p/s as disabled in selecting-node mode', () => {
60
+ const items = legendItems(true, true, true)
61
+ const p = items.find(i => i.key === 'p')
62
+ const n = items.find(i => i.key === 'n')
63
+ const s = items.find(i => i.key === 's')
64
+ expect(p?.disabled).toBe(true)
65
+ expect(n?.disabled).toBe(true)
66
+ expect(s?.disabled).toBe(true)
67
+ })
68
+
69
+ it('shows s as disabled when no solution exists', () => {
70
+ const items = legendItems(false, true, false)
71
+ const s = items.find(i => i.key === 's')
72
+ expect(s?.disabled).toBe(true)
73
+ })
74
+
75
+ it('shows s as enabled when solution exists', () => {
76
+ const items = legendItems(true, true, false)
77
+ const s = items.find(i => i.key === 's')
78
+ expect(s?.disabled).toBe(false)
79
+ })
80
+ })
81
+ })
14
82
  })
package/src/demo.tsx ADDED
@@ -0,0 +1,20 @@
1
+ #!/usr/bin/env -S bun run
2
+
3
+ import { render } from 'ink'
4
+
5
+ import HashiRow from './components/HashiRow.tsx'
6
+
7
+ render(
8
+ <HashiRow
9
+ nodes={[
10
+ { value: 1 },
11
+ { value: '-' },
12
+ { value: 2 },
13
+ { value: '#' },
14
+ { value: 2 },
15
+ { value: 3 },
16
+ ]}
17
+ rowIndex={0}
18
+ highlightedNode={2}
19
+ />
20
+ )
package/src/index.tsx CHANGED
@@ -1,14 +1,18 @@
1
1
  #!/usr/bin/env -S bun run
2
2
 
3
+ import { readFileSync } from 'node:fs'
4
+ import { resolve } from 'node:path'
3
5
  import { Command } from 'commander'
4
6
  import { render } from 'ink'
5
7
 
6
8
  import Game from './Game.tsx'
7
- import { type PuzzleData, samplePuzzles } from './utils/samplePuzzles.ts'
9
+ import { type PuzzleData, samplePuzzles } from './utils/puzzle-encoding.ts'
10
+
11
+ const packageJson = JSON.parse(readFileSync(resolve(__dirname, '../package.json'), 'utf8'))
8
12
 
9
13
  type CliOptions = {
10
- stdout: boolean
11
14
  puzzle: string | undefined
15
+ enableSolutions: boolean
12
16
  }
13
17
 
14
18
  const program = new Command()
@@ -16,8 +20,26 @@ const program = new Command()
16
20
  program
17
21
  .name('bridges')
18
22
  .description('Bridges (Hashiwokakero) puzzle game')
19
- .option('-s, --stdout', 'Output to stdout and exit (for testing)')
20
- .option('-p, --puzzle <puzzle>', 'Puzzle shorthand encoding')
23
+ .version(packageJson.version, '-v, --version')
24
+ .option(
25
+ '-p, --puzzle <puzzle>',
26
+ `Puzzle shorthand encoding
27
+ Format: "WIDTHxHEIGHT:row1.row2.row3..."
28
+
29
+ Node encoding:
30
+ - Digits (1-8): island with that value
31
+ - Letters (a-z): space between islands (b=2, etc.)
32
+
33
+ Bridge encoding (optional):
34
+ - "-": single horizontal bridge
35
+ - "=": double horizontal bridge
36
+ - "|": single vertical bridge
37
+ - "#": double vertical bridge
38
+
39
+ Example (3x3 with corner islands):
40
+ --puzzle "3x3:1a2.c.1a2"`
41
+ )
42
+ .option('--enable-solutions', 'Enable the solution viewing feature', false)
21
43
  .parse(process.argv)
22
44
 
23
45
  const options = program.opts<CliOptions>()
@@ -30,5 +52,9 @@ if (options.puzzle) {
30
52
  }
31
53
 
32
54
  render(
33
- <Game puzzles={puzzles} hasCustomPuzzle={hasCustomPuzzle} stdout={options.stdout || false} />
55
+ <Game
56
+ puzzles={puzzles}
57
+ hasCustomPuzzle={hasCustomPuzzle}
58
+ enableSolutions={options.enableSolutions}
59
+ />
34
60
  )
package/src/types.ts CHANGED
@@ -1,3 +1,6 @@
1
+ /**
2
+ * HashiNode types
3
+ */
1
4
  export type HashiNodeData = {
2
5
  value: number | '-' | '=' | '#' | ' ' | '|'
3
6
  /** Num lines connected on left, undefined if 0. */
@@ -9,3 +12,34 @@ export type HashiNodeData = {
9
12
  /** Num lines connected below, undefined if 0. */
10
13
  lineDown?: 1 | 2
11
14
  }
15
+
16
+ export type HashiNodeDisplayMode = 'normal' | 'highlight' | 'dim'
17
+
18
+ export type HashiNodeOptions = {
19
+ displayMode?: HashiNodeDisplayMode
20
+ label?: string
21
+ }
22
+
23
+ /**
24
+ * Game operation types
25
+ */
26
+ export type SelectionMode = 'idle' | 'selecting-node' | 'disambiguation' | 'selected' | 'invalid'
27
+
28
+ export type Direction = 'h' | 'j' | 'k' | 'l'
29
+
30
+ export type PlacedBridge = {
31
+ from: { row: number; col: number }
32
+ to: { row: number; col: number }
33
+ count?: number
34
+ }
35
+
36
+ export type SelectionState = {
37
+ mode: SelectionMode
38
+ selectedNumber: number | null
39
+ direction: Direction | null
40
+ matchingNodes: { row: number; col: number }[]
41
+ disambiguationLabels: string[]
42
+ selectedNode: { row: number; col: number } | null
43
+ bridgeErased?: boolean
44
+ isDoubleBridge?: boolean
45
+ }