@slugbugblue/trax 0.17.0 → 0.18.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/.husky/pre-commit +4 -0
- package/CHANGELOG.md +28 -6
- package/Dockerfile +5 -0
- package/README.md +9 -1
- package/benchmark/benchmarks.json +42 -0
- package/benchmark/puzzles.bench.js +190 -0
- package/docs/analyst.md +16 -2
- package/docs/engine.md +2 -2
- package/package.json +5 -2
- package/src/analyst.js +125 -5
- package/src/cli.js +10 -1
- package/src/cmds/analyze.js +24 -4
- package/src/cmds/import-export.js +2 -7
- package/src/cmds/view.js +5 -2
- package/src/engine.js +7 -0
- package/src/puzzles.js +135 -9
- package/src/threats.js +23 -2
- package/src/types.d.ts +15 -1
- package/src/version.js +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,27 @@
|
|
|
1
1
|
# @slugbugblue/trax
|
|
2
2
|
|
|
3
|
+
## 0.18.0 - 2023-02-05
|
|
4
|
+
|
|
5
|
+
- Analysis improvements:
|
|
6
|
+
|
|
7
|
+
- Add `count.b(level)` and `count.w(level)` methods to an analysis to quickly
|
|
8
|
+
count the number of threats at a given level
|
|
9
|
+
- Remove faulty threats from an analysis
|
|
10
|
+
- Add additional threats for more puzzle solving mojo
|
|
11
|
+
|
|
12
|
+
- Development environment enhancements:
|
|
13
|
+
|
|
14
|
+
- Added an initial benchmarking framework with puzzle benchmarks so far
|
|
15
|
+
- Added husky pre-commit hook to ensure npm test is run before a commit
|
|
16
|
+
|
|
17
|
+
- CLI improvements:
|
|
18
|
+
|
|
19
|
+
- Puzzle information is now stored as notes, to make it searchable
|
|
20
|
+
|
|
21
|
+
- More puzzles:
|
|
22
|
+
|
|
23
|
+
- Martin M. S. Pedersen contributed eleven new puzzles
|
|
24
|
+
|
|
3
25
|
## 0.17.0 - 2022-12-29
|
|
4
26
|
|
|
5
27
|
- The `import` CLI command now loads comments from a .trx file
|
|
@@ -26,9 +48,9 @@
|
|
|
26
48
|
- Adjust puzzle category levels for harder puzzles
|
|
27
49
|
- Add multiple hints when the puzzle can be solved multiple ways
|
|
28
50
|
|
|
29
|
-
- Analysis
|
|
51
|
+
- Analysis improvements:
|
|
30
52
|
|
|
31
|
-
- Analysis
|
|
53
|
+
- Analysis suggestions now "solve" all tough puzzles
|
|
32
54
|
- Reworked threats to be more precise
|
|
33
55
|
- Reworked scoring to add more nuance
|
|
34
56
|
|
|
@@ -39,9 +61,9 @@
|
|
|
39
61
|
- `trax puzzles ls`: see all the puzzles
|
|
40
62
|
- `trax puzzle new`: start a new puzzle
|
|
41
63
|
|
|
42
|
-
- Analysis
|
|
43
|
-
|
|
44
|
-
|
|
64
|
+
- Analysis tests now include correctly suggesting the first move for solving all
|
|
65
|
+
tutorial and easy puzzles. As the threats database improves, we will add more
|
|
66
|
+
puzzles to the tests.
|
|
45
67
|
|
|
46
68
|
## 0.13.0 - 2022-11-25
|
|
47
69
|
|
|
@@ -134,7 +156,7 @@ variables from snake_case to camelCase.
|
|
|
134
156
|
- `to`: the normalized position code of the board after the move
|
|
135
157
|
- `via`: the move notation to go from `from` to `to`
|
|
136
158
|
|
|
137
|
-
|
|
159
|
+
## 0.3.1 - 2022-01-07
|
|
138
160
|
|
|
139
161
|
- Make a faster implementation of `trax.possible_locations()`
|
|
140
162
|
- Display updates for tentative moves
|
package/Dockerfile
ADDED
package/README.md
CHANGED
|
@@ -187,9 +187,14 @@ always ready to share his insights on the mechanics of the game, as well as
|
|
|
187
187
|
various approaches to encoding its complexities. His undeserved kindness and
|
|
188
188
|
infinite patience in indulging my many questions can never be repaid.
|
|
189
189
|
|
|
190
|
+
Thanks for [Martin M. S. Pedersen][traxplayer] for contributing ideas, code, and
|
|
191
|
+
puzzles to this effort. He's been [curating Trax puzzles][puzzlebook] and
|
|
192
|
+
[coding solutions for Trax][gnutrax] for over two decades, and brings a lot of
|
|
193
|
+
expertise.
|
|
194
|
+
|
|
190
195
|
## License
|
|
191
196
|
|
|
192
|
-
Copyright 2019-
|
|
197
|
+
Copyright 2019-2023 Chad Transtrum
|
|
193
198
|
|
|
194
199
|
Licensed under the Apache License, Version 2.0 (the "License"); you may not use
|
|
195
200
|
the files in this project except in compliance with the License. You may obtain
|
|
@@ -217,12 +222,15 @@ David Smith and heirs, and are not to be used without permission.
|
|
|
217
222
|
[docs-engine]: docs/engine.md
|
|
218
223
|
[docs-analyst]: docs/analyst.md
|
|
219
224
|
[env-path]: https://www.npmjs.com/package/env-paths
|
|
225
|
+
[gnutrax]: https://gnutrax.com
|
|
220
226
|
[goldtoken]: https://goldtoken.com/
|
|
227
|
+
[puzzlebook]: https://gnutrax.com/book
|
|
221
228
|
[repo]: https://gitlab.com/slugbugblue/trax
|
|
222
229
|
[sbb]: https://slugbugblue.com/
|
|
223
230
|
[traxbook]: http://traxgame.com/shop_book.php
|
|
224
231
|
[traxgame]: http://traxgame.com/
|
|
225
232
|
[traxhistory]: http://www.traxgame.com/about_history.php
|
|
233
|
+
[traxplayer]: mailto:traxplayer@gmail.com
|
|
226
234
|
[traxpuzzles]: http://www.traxgame.com/games_puzzles.php
|
|
227
235
|
[traxrules]: http://www.traxgame.com/about_rules.php
|
|
228
236
|
[xdg]:
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"0.17.0": {
|
|
3
|
+
"puzzles.firstmove.tutorial": {
|
|
4
|
+
"count": 16,
|
|
5
|
+
"ms": 2007
|
|
6
|
+
},
|
|
7
|
+
"puzzles.firstmove.beginner": {
|
|
8
|
+
"count": 16,
|
|
9
|
+
"ms": 2664
|
|
10
|
+
},
|
|
11
|
+
"puzzles.firstmove.tough": {
|
|
12
|
+
"count": 1,
|
|
13
|
+
"ms": 76
|
|
14
|
+
},
|
|
15
|
+
"puzzles.firstmove.total": {
|
|
16
|
+
"count": 33,
|
|
17
|
+
"ms": 4838
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"0.18.0": {
|
|
21
|
+
"puzzles.firstmove.tutorial": {
|
|
22
|
+
"count": 16,
|
|
23
|
+
"ms": 10164
|
|
24
|
+
},
|
|
25
|
+
"puzzles.firstmove.beginner": {
|
|
26
|
+
"count": 15,
|
|
27
|
+
"ms": 17864
|
|
28
|
+
},
|
|
29
|
+
"puzzles.firstmove.tough": {
|
|
30
|
+
"count": 26,
|
|
31
|
+
"ms": 30349
|
|
32
|
+
},
|
|
33
|
+
"puzzles.firstmove.advanced": {
|
|
34
|
+
"count": 1,
|
|
35
|
+
"ms": 427
|
|
36
|
+
},
|
|
37
|
+
"puzzles.firstmove.total": {
|
|
38
|
+
"count": 58,
|
|
39
|
+
"ms": 59031
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/** Let's try to solve the puzzles
|
|
4
|
+
* @copyright 2023
|
|
5
|
+
* @author Chad Transtrum <chad@transtrum.net>
|
|
6
|
+
* @license Apache-2.0
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import fs from 'node:fs'
|
|
10
|
+
import process from 'node:process'
|
|
11
|
+
import { fileURLToPath } from 'node:url'
|
|
12
|
+
|
|
13
|
+
import { Trax } from '@slugbugblue/trax'
|
|
14
|
+
import { suggest } from '@slugbugblue/trax/analyst'
|
|
15
|
+
import { puzzles } from '@slugbugblue/trax/puzzles'
|
|
16
|
+
import { version } from '@slugbugblue/trax/version'
|
|
17
|
+
|
|
18
|
+
import * as tty from '@slugbugblue/trax/tty'
|
|
19
|
+
|
|
20
|
+
let STUMPED = false
|
|
21
|
+
const FINAL = {}
|
|
22
|
+
|
|
23
|
+
const record = (name, data) => {
|
|
24
|
+
FINAL['puzzles.firstmove.' + name] = data
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const timeString = (ms) => {
|
|
28
|
+
if (ms < 1000) {
|
|
29
|
+
return (ms === Math.floor(ms) ? String(ms) : ms.toFixed(2)) + ' ms'
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
ms /= 1000
|
|
33
|
+
if (ms < 60) return ms.toFixed(3) + ' sec'
|
|
34
|
+
|
|
35
|
+
const min = Math.floor(ms / 60)
|
|
36
|
+
ms -= min * 60
|
|
37
|
+
return String(min) + ' min ' + ms.toFixed(3) + ' sec'
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const time = (ms) => timeString(Date.now() - ms)
|
|
41
|
+
|
|
42
|
+
const out = (messages) => {
|
|
43
|
+
tty.outty(messages.join(' '))
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const section = (title) => {
|
|
47
|
+
out([tty.color('===', 250), tty.color(title, 39), tty.color('===', 250)])
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const success = (puzzle, ms) => {
|
|
51
|
+
out([tty.color('✓', 35), tty.color(puzzle, 250), tty.color(ms, 240)])
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const puzzled = (puzzle, move) => {
|
|
55
|
+
tty.display(new Trax(puzzle.game, puzzle.notation))
|
|
56
|
+
tty.outty('Puzzled by ' + puzzle.id, 205)
|
|
57
|
+
out([
|
|
58
|
+
tty.color(' played', 250),
|
|
59
|
+
tty.color(move, 127),
|
|
60
|
+
tty.color('instead of', 250),
|
|
61
|
+
tty.color(puzzle.hint || puzzle.hints.join(' '), 39),
|
|
62
|
+
])
|
|
63
|
+
STUMPED = true
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const finalize = (name, ms, count) => {
|
|
67
|
+
out([
|
|
68
|
+
tty.color('Finished', 195),
|
|
69
|
+
tty.color(String(count) + ' ' + name + ' puzzles', 123),
|
|
70
|
+
tty.color('in ' + time(ms), 195),
|
|
71
|
+
])
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const solvePuzzleLevel = (name, level) => {
|
|
75
|
+
if (STUMPED) return 0
|
|
76
|
+
section(name + ' puzzles')
|
|
77
|
+
name = name.toLowerCase()
|
|
78
|
+
let count = 0
|
|
79
|
+
const begin = Date.now()
|
|
80
|
+
for (const puzzle of puzzles) {
|
|
81
|
+
if (puzzle.level !== level) continue
|
|
82
|
+
const start = Date.now()
|
|
83
|
+
const game = new Trax(puzzle.game, puzzle.notation)
|
|
84
|
+
const { move } = suggest(game).pick
|
|
85
|
+
const solved = puzzle.hint
|
|
86
|
+
? move === puzzle.hint
|
|
87
|
+
: puzzle.hints
|
|
88
|
+
? puzzle.hints.includes(move)
|
|
89
|
+
: false
|
|
90
|
+
if (solved) {
|
|
91
|
+
success(puzzle.id, time(start))
|
|
92
|
+
count++
|
|
93
|
+
record(name, { count, ms: Date.now() - begin })
|
|
94
|
+
} else {
|
|
95
|
+
puzzled(puzzle, move)
|
|
96
|
+
process.exitCode = level + 1
|
|
97
|
+
return count
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
finalize(name, begin, count)
|
|
102
|
+
return count
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Gather the benchmark data
|
|
106
|
+
let total = 0
|
|
107
|
+
const timer = Date.now()
|
|
108
|
+
total += solvePuzzleLevel('Tutorial', 0)
|
|
109
|
+
total += solvePuzzleLevel('Beginner', 1)
|
|
110
|
+
total += solvePuzzleLevel('Tough', 2)
|
|
111
|
+
total += solvePuzzleLevel('Advanced', 3)
|
|
112
|
+
total += solvePuzzleLevel('Genius', 4)
|
|
113
|
+
|
|
114
|
+
record('total', { count: total, ms: Date.now() - timer })
|
|
115
|
+
tty.outty(
|
|
116
|
+
'Benchmark: ' + total + ' puzzles in ' + time(timer),
|
|
117
|
+
STUMPED ? 205 : 35,
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
tty.outty()
|
|
121
|
+
|
|
122
|
+
// Load the previous benchmarks
|
|
123
|
+
const FILE = new URL('benchmarks.json', import.meta.url)
|
|
124
|
+
let CONTENTS
|
|
125
|
+
try {
|
|
126
|
+
CONTENTS = fs.readFileSync(FILE, 'utf8')
|
|
127
|
+
} catch {
|
|
128
|
+
CONTENTS = '{}'
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const BENCHMARK = JSON.parse(CONTENTS)
|
|
132
|
+
|
|
133
|
+
const PREV = Object.values(BENCHMARK).pop() || {}
|
|
134
|
+
|
|
135
|
+
// Report the benchmark data
|
|
136
|
+
const size = [0, 0, 0, 0, 0]
|
|
137
|
+
const REPORT = []
|
|
138
|
+
for (const name of Object.keys(FINAL)) {
|
|
139
|
+
const report = [
|
|
140
|
+
name,
|
|
141
|
+
String(FINAL[name].count) + ' puzzles',
|
|
142
|
+
'in ' + timeString(FINAL[name].ms),
|
|
143
|
+
'avg ' + timeString(FINAL[name].ms / FINAL[name].count),
|
|
144
|
+
]
|
|
145
|
+
if (PREV[name]) {
|
|
146
|
+
const diff =
|
|
147
|
+
FINAL[name].ms / FINAL[name].count - PREV[name].ms / PREV[name].count
|
|
148
|
+
if (diff <= 0) {
|
|
149
|
+
report.push('-' + timeString(Math.abs(diff)), '35')
|
|
150
|
+
} else {
|
|
151
|
+
report.push('+' + timeString(diff), '208')
|
|
152
|
+
}
|
|
153
|
+
} else {
|
|
154
|
+
report.push('new', '250')
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
size[0] = Math.max(size[0], report[0].length)
|
|
158
|
+
size[1] = Math.max(size[1], report[1].length)
|
|
159
|
+
size[2] = Math.max(size[2], report[2].length)
|
|
160
|
+
size[3] = Math.max(size[3], report[3].length)
|
|
161
|
+
size[4] = Math.max(size[4], report[4].length)
|
|
162
|
+
REPORT.push(report)
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
for (const line of REPORT) {
|
|
166
|
+
out([
|
|
167
|
+
tty.color(line[0].padEnd(size[0]), 39),
|
|
168
|
+
tty.color(line[1].padStart(size[1]), 123),
|
|
169
|
+
tty.color(line[2].padStart(size[2]), 195),
|
|
170
|
+
tty.color(line[3].padEnd(size[3]), 245),
|
|
171
|
+
tty.color(line[4].padStart(size[4]), Number(line[5])),
|
|
172
|
+
])
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
tty.outty()
|
|
176
|
+
|
|
177
|
+
// Save the benchmark data to a file
|
|
178
|
+
if (BENCHMARK[version]) {
|
|
179
|
+
tty.outty('Not overwriting existing benchmark data')
|
|
180
|
+
} else {
|
|
181
|
+
BENCHMARK[version] = FINAL
|
|
182
|
+
try {
|
|
183
|
+
fs.writeFileSync(FILE, JSON.stringify(BENCHMARK, null, 2), 'utf8')
|
|
184
|
+
const filename = fileURLToPath(FILE)
|
|
185
|
+
tty.outty('Saved benchmark data to ' + tty.color(filename, 74))
|
|
186
|
+
} catch (error) {
|
|
187
|
+
tty.outty('Error saving benchmark data', 205)
|
|
188
|
+
tty.outty(String(error.code) + ' ' + error.message, 127)
|
|
189
|
+
}
|
|
190
|
+
}
|
package/docs/analyst.md
CHANGED
|
@@ -79,6 +79,7 @@ themselves. The following keys are present in each edge space representation:
|
|
|
79
79
|
connectable pair of lines with this color.
|
|
80
80
|
- `zb` and `zw`: The character used in the black or white string representation
|
|
81
81
|
of this space.
|
|
82
|
+
- `idx`: The index of this edge space in the edge array.
|
|
82
83
|
|
|
83
84
|
For the edge string, the following characters are used as a hopefully useful
|
|
84
85
|
shorthand summary of the tiles surrounding the space. To make regex pattern
|
|
@@ -90,7 +91,7 @@ so that it appears as though all black tiles are white tiles.
|
|
|
90
91
|
- `l`: both a black and a white line enter this space.
|
|
91
92
|
- `a`: a white line enters this space, and the next white line in the edge is
|
|
92
93
|
the other end of this line.
|
|
93
|
-
-
|
|
94
|
+
- `c`: a white line enters this space, and the next white line in the edge forms
|
|
94
95
|
a connectable pair with this line.
|
|
95
96
|
- `m`: both a black and a white line enter this space, and the next white line
|
|
96
97
|
in the edge is the other end of the white line.
|
|
@@ -119,6 +120,11 @@ following keys:
|
|
|
119
120
|
may or may not be exactly the same as the pattern.
|
|
120
121
|
- `at`: The index of the edge string at which the pattern match began.
|
|
121
122
|
|
|
123
|
+
##### `faulty`
|
|
124
|
+
|
|
125
|
+
Contains the same information as `threats` but for removed threats that were
|
|
126
|
+
found to be faulty.
|
|
127
|
+
|
|
122
128
|
##### `score`
|
|
123
129
|
|
|
124
130
|
Computes a rough "score" of the current position using the list of threats. A
|
|
@@ -135,6 +141,14 @@ because the scores can change depending on who has the initiative.
|
|
|
135
141
|
|
|
136
142
|
#### methods
|
|
137
143
|
|
|
144
|
+
##### `count.w(level)`
|
|
145
|
+
|
|
146
|
+
Returns the number of threats for white at the given level.
|
|
147
|
+
|
|
148
|
+
##### `count.b(level)`
|
|
149
|
+
|
|
150
|
+
Returns the number of threats for black at the given level.
|
|
151
|
+
|
|
138
152
|
##### `inCave(loc)`
|
|
139
153
|
|
|
140
154
|
For any given location, returns true if that location is found within a cave.
|
|
@@ -188,7 +202,7 @@ suggestion.
|
|
|
188
202
|
|
|
189
203
|
## License
|
|
190
204
|
|
|
191
|
-
Copyright
|
|
205
|
+
Copyright 2022-2023 Chad Transtrum
|
|
192
206
|
|
|
193
207
|
Licensed under the Apache License, Version 2.0 (the "License"); you may not use
|
|
194
208
|
the files in this project except in compliance with the License. You may obtain
|
package/docs/engine.md
CHANGED
|
@@ -42,7 +42,7 @@ class properties and methods:
|
|
|
42
42
|
- `Trax.point(x, y)`: quick access to the [Point class][point-docs], for
|
|
43
43
|
two-dimensional locations used internally by the Trax engine and which are
|
|
44
44
|
required for some of the lower-level engine functions. Operates as a
|
|
45
|
-
constructor (without `new`) and returns a Point instance.
|
|
45
|
+
constructor (without `new`) and returns a [Point instance][point-docs].
|
|
46
46
|
|
|
47
47
|
- `Trax.colorOf(playerNumber)`: Player 1 is white and Player 2 is black. So this
|
|
48
48
|
is a quick function to turn `1` into `'w'` and `2` into `'b'`. Quick and
|
|
@@ -414,7 +414,7 @@ tie.
|
|
|
414
414
|
|
|
415
415
|
## License
|
|
416
416
|
|
|
417
|
-
Copyright 2019-
|
|
417
|
+
Copyright 2019-2023 Chad Transtrum
|
|
418
418
|
|
|
419
419
|
Licensed under the Apache License, Version 2.0 (the "License"); you may not use
|
|
420
420
|
the files in this project except in compliance with the License. You may obtain
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@slugbugblue/trax",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.18.0",
|
|
4
4
|
"description": "Trax game engine and friends",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"trax",
|
|
@@ -26,9 +26,11 @@
|
|
|
26
26
|
"trax": "./src/cli.js"
|
|
27
27
|
},
|
|
28
28
|
"scripts": {
|
|
29
|
+
"benchmark": "node benchmark/puzzles.bench.js",
|
|
29
30
|
"test": "xo && c8 ava",
|
|
31
|
+
"prepare": "husky install",
|
|
30
32
|
"preversion": "npm test",
|
|
31
|
-
"version": "genversion --es6 src/version.js && git add src/version.js",
|
|
33
|
+
"version": "genversion --es6 src/version.js && npm run benchmark || git add src/version.js benchmark/benchmarks.json",
|
|
32
34
|
"postversion": "git push && git push --tags && npm publish"
|
|
33
35
|
},
|
|
34
36
|
"dependencies": {
|
|
@@ -40,6 +42,7 @@
|
|
|
40
42
|
"ava": "^5.1.0",
|
|
41
43
|
"c8": "^7.11.0",
|
|
42
44
|
"genversion": "^3.0.2",
|
|
45
|
+
"husky": "^8.0.3",
|
|
43
46
|
"prettier": "^2.6.1",
|
|
44
47
|
"xo": "0.*"
|
|
45
48
|
},
|
package/src/analyst.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/** Analyst module: provides analysis functions for Trax games.
|
|
2
|
-
* @copyright 2022
|
|
2
|
+
* @copyright 2022-2023
|
|
3
3
|
* @author Chad Transtrum <chad@transtrum.net>
|
|
4
4
|
* @license Apache-2.0
|
|
5
5
|
*/
|
|
@@ -170,6 +170,7 @@ const pairUp = (rawEdge) => {
|
|
|
170
170
|
*/
|
|
171
171
|
const label = (rawEdge) => {
|
|
172
172
|
const final = { w: '', b: '', edge: rawEdge }
|
|
173
|
+
let index = 0
|
|
173
174
|
for (const space of rawEdge) {
|
|
174
175
|
for (const bw of ['b', 'w']) {
|
|
175
176
|
const colors = new Set([bw, 'l'])
|
|
@@ -186,6 +187,9 @@ const label = (rawEdge) => {
|
|
|
186
187
|
space['z' + bw] = z
|
|
187
188
|
final[bw] += z
|
|
188
189
|
}
|
|
190
|
+
|
|
191
|
+
space.idx = index
|
|
192
|
+
index += 1
|
|
189
193
|
}
|
|
190
194
|
|
|
191
195
|
return final
|
|
@@ -260,14 +264,21 @@ export const Analysis = class {
|
|
|
260
264
|
#game
|
|
261
265
|
#scores = {}
|
|
262
266
|
#threats = undefined
|
|
267
|
+
#faulty = { b: [], w: [] }
|
|
268
|
+
#partial = false
|
|
263
269
|
|
|
264
|
-
constructor(game) {
|
|
270
|
+
constructor(game, partial = false) {
|
|
265
271
|
// Make a copy of the game
|
|
266
272
|
this.#rules = game.rules
|
|
267
273
|
this.#moves = game.moves
|
|
268
274
|
this.#id = id()
|
|
269
275
|
this.#notation = game.notation
|
|
270
276
|
this.#game = new Trax(this.#rules, this.#moves, this.#id)
|
|
277
|
+
this.count = {
|
|
278
|
+
w: (level) => this.threats.w[level]?.length || 0,
|
|
279
|
+
b: (level) => this.threats.b[level]?.length || 0,
|
|
280
|
+
}
|
|
281
|
+
this.#partial = Boolean(partial)
|
|
271
282
|
}
|
|
272
283
|
|
|
273
284
|
get game() {
|
|
@@ -288,12 +299,20 @@ export const Analysis = class {
|
|
|
288
299
|
return this.#edge
|
|
289
300
|
}
|
|
290
301
|
|
|
302
|
+
/** Any faulty threats
|
|
303
|
+
* @returns {FaultyThreats}
|
|
304
|
+
*/
|
|
305
|
+
get faulty() {
|
|
306
|
+
return this.#faulty
|
|
307
|
+
}
|
|
308
|
+
|
|
291
309
|
/** All threats present in this position.
|
|
292
310
|
* @returns {FoundThreatsCollection}
|
|
293
311
|
*/
|
|
294
312
|
get threats() {
|
|
295
313
|
if (!this.#threats) {
|
|
296
314
|
this.#threats = { b: this.#applyThreats('b'), w: this.#applyThreats('w') }
|
|
315
|
+
if (!this.#partial) this.#validateThreats()
|
|
297
316
|
}
|
|
298
317
|
|
|
299
318
|
return this.#threats
|
|
@@ -528,7 +547,41 @@ export const Analysis = class {
|
|
|
528
547
|
match: match[1], // The match is in the capturing group
|
|
529
548
|
at: match.index,
|
|
530
549
|
value: threat.value,
|
|
550
|
+
level: threat.depth,
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
/**
|
|
555
|
+
* @arg {FoundThreat} threat - a threat
|
|
556
|
+
* @arg {string} color - the player color
|
|
557
|
+
* @returns {InstanceType<typeof Point>[]} - locations relevant to the threat
|
|
558
|
+
*/
|
|
559
|
+
#threatLocs(threat, color) {
|
|
560
|
+
const { edge } = this.edge
|
|
561
|
+
const max = edge.length
|
|
562
|
+
let index = threat.at
|
|
563
|
+
const locs = new Set([index])
|
|
564
|
+
while (threat.match.length > locs.size) {
|
|
565
|
+
index += 1
|
|
566
|
+
if (index >= max) index -= max
|
|
567
|
+
locs.add(index)
|
|
568
|
+
// Check to see if we have a connectable pair
|
|
569
|
+
if (edge[index]['p' + color]) {
|
|
570
|
+
const line = 'n' + color
|
|
571
|
+
const end = edge.find(
|
|
572
|
+
(space) =>
|
|
573
|
+
space.idx !== edge[index].idx && space[line] === edge[index][line],
|
|
574
|
+
)
|
|
575
|
+
if (end) {
|
|
576
|
+
// Add the opposite end and the space right before it, just in case
|
|
577
|
+
locs.add(end.idx)
|
|
578
|
+
locs.add(end.idx - 1)
|
|
579
|
+
}
|
|
580
|
+
}
|
|
531
581
|
}
|
|
582
|
+
|
|
583
|
+
const value = [...locs].map((i) => new Point(edge[i]))
|
|
584
|
+
return value
|
|
532
585
|
}
|
|
533
586
|
|
|
534
587
|
/** Apply the threats to a color's regex-ready edge.
|
|
@@ -544,11 +597,10 @@ export const Analysis = class {
|
|
|
544
597
|
const find = matches[threat.depth] || []
|
|
545
598
|
for (const match of edge.matchAll(threat.rx)) {
|
|
546
599
|
if (match.index >= length) break
|
|
600
|
+
|
|
601
|
+
// Avoid saving duplicates
|
|
547
602
|
if (find.every((f) => f.at !== match.index || f.match !== match[1])) {
|
|
548
603
|
find.push(this.#foundThreat(threat, match))
|
|
549
|
-
} else {
|
|
550
|
-
const duplicate = matches[99] || []
|
|
551
|
-
duplicate.push(this.#foundThreat(threat, match))
|
|
552
604
|
}
|
|
553
605
|
}
|
|
554
606
|
|
|
@@ -558,6 +610,74 @@ export const Analysis = class {
|
|
|
558
610
|
return matches
|
|
559
611
|
}
|
|
560
612
|
|
|
613
|
+
/** Clean up the list of matches, removing any false positives. */
|
|
614
|
+
#validateThreats() {
|
|
615
|
+
for (const bw of ['w', 'b']) {
|
|
616
|
+
/** @type FoundThreats */
|
|
617
|
+
const matches = this.#threats[bw]
|
|
618
|
+
for (const key of Object.keys(matches)) {
|
|
619
|
+
const level = Number(key)
|
|
620
|
+
if (level < 2) continue
|
|
621
|
+
/** @type {FoundThreat[]} */
|
|
622
|
+
const verified = []
|
|
623
|
+
const threats = matches[key]
|
|
624
|
+
for (const threat of threats) {
|
|
625
|
+
if (this.#canActivateThreat(threat, bw, level)) {
|
|
626
|
+
verified.push(threat)
|
|
627
|
+
} else {
|
|
628
|
+
this.#faulty[bw].push(threat)
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
if (verified.length > 0) {
|
|
633
|
+
matches[key] = verified
|
|
634
|
+
} else {
|
|
635
|
+
delete matches[key]
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
/** Determine if the given threat can be activated for this color
|
|
642
|
+
* @param {FoundThreat} threat - The threat to try to activate
|
|
643
|
+
* @param {string} color - The color this match belongs to
|
|
644
|
+
* @param {number} level - The depth level of this threat
|
|
645
|
+
* @returns {boolean}
|
|
646
|
+
*/
|
|
647
|
+
#canActivateThreat(threat, color, level) {
|
|
648
|
+
const attacks = this.#threats[color][1]?.length ?? 0
|
|
649
|
+
const other = this.#threats[Trax.other(color)][1]?.length ?? 0
|
|
650
|
+
const higher = this.#threats[color][level - 1]?.length ?? 0
|
|
651
|
+
for (const loc of this.#threatLocs(threat, color)) {
|
|
652
|
+
for (const type of this.game.possibleTiles(loc)) {
|
|
653
|
+
const game = new Trax(this.game.rules, this.game.moves)
|
|
654
|
+
const result = game.dropTile(type, loc)
|
|
655
|
+
if (!result.valid) continue
|
|
656
|
+
|
|
657
|
+
// This might be a bad shortcut, but if we already have an attack here
|
|
658
|
+
// we just assume that this threat is also valid ...
|
|
659
|
+
if (game.over && game.turn === Trax.playerNumber(color)) return true
|
|
660
|
+
|
|
661
|
+
const { threats } = new Analysis(game, true)
|
|
662
|
+
|
|
663
|
+
// If we give our opponent an attack, this is a faulty move
|
|
664
|
+
const faulty = (threats[Trax.other(color)][1]?.length ?? 0) - other
|
|
665
|
+
if (faulty > 0) continue
|
|
666
|
+
|
|
667
|
+
// Activating the threat means we get an attack and a threat
|
|
668
|
+
const newAttacks = (threats[color][1]?.length ?? 0) - attacks
|
|
669
|
+
if (level === 2) {
|
|
670
|
+
if (newAttacks > 1) return true
|
|
671
|
+
} else {
|
|
672
|
+
const newThreats = (threats[color][level - 1]?.length ?? 0) - higher
|
|
673
|
+
if (newAttacks > 0 && newThreats > 0) return true
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
return false
|
|
679
|
+
}
|
|
680
|
+
|
|
561
681
|
// Make a reasonable inspect representation of the analysis
|
|
562
682
|
[Symbol.for('nodejs.util.inspect.custom')](depth, options, inspect) {
|
|
563
683
|
const sub = (object) => inspect(object, options).replace(/\n/g, '\n ')
|
package/src/cli.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
/* Copyright 2022 Chad Transtrum
|
|
2
|
+
/* Copyright 2022-2023 Chad Transtrum
|
|
3
3
|
*
|
|
4
4
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
5
|
* you may not use this file except in compliance with the License.
|
|
@@ -326,6 +326,14 @@ const CLI = {
|
|
|
326
326
|
const id = newId(DATA)
|
|
327
327
|
const trax = new Trax(puzzle.game, puzzle.notation, 'cli')
|
|
328
328
|
const players = ['challenger', 'puzzlebot']
|
|
329
|
+
const notes = []
|
|
330
|
+
if (puzzle.title) notes.push({ move: 0, note: puzzle.title })
|
|
331
|
+
if (puzzle.desc) notes.push({ move: 0, note: puzzle.desc })
|
|
332
|
+
const player = [0, 'white', 'black'][puzzle.player]
|
|
333
|
+
notes.push(
|
|
334
|
+
{ move: 0, note: 'Starting position ' + puzzle.notation },
|
|
335
|
+
{ move: 0, note: `${puzzle.id}: ${player} to win by move ${puzzle.max}` },
|
|
336
|
+
)
|
|
329
337
|
DATA.games[id] = {
|
|
330
338
|
id,
|
|
331
339
|
puzzle: puzzle.id,
|
|
@@ -335,6 +343,7 @@ const CLI = {
|
|
|
335
343
|
max: puzzle.max,
|
|
336
344
|
players: trax.turn === 1 ? players : players.reverse(),
|
|
337
345
|
moves: trax.moves.join(' '),
|
|
346
|
+
notes,
|
|
338
347
|
}
|
|
339
348
|
|
|
340
349
|
if (DATA.puzzles[puzzle.id]) {
|
package/src/cmds/analyze.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/* Copyright 2022 Chad Transtrum
|
|
1
|
+
/* Copyright 2022-2023 Chad Transtrum
|
|
2
2
|
*
|
|
3
3
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
4
|
* you may not use this file except in compliance with the License.
|
|
@@ -54,6 +54,17 @@ const listThreats = (threats, detailed) => {
|
|
|
54
54
|
return summary.join(' ')
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
+
const listFaulty = (faulty) => {
|
|
58
|
+
const summary = []
|
|
59
|
+
for (const threat of faulty) {
|
|
60
|
+
summary.push(
|
|
61
|
+
`[L${threat.level} ${threat.at}:${threat.threat}:${threat.match}]`,
|
|
62
|
+
)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return 'Faulty: ' + summary.join(' ')
|
|
66
|
+
}
|
|
67
|
+
|
|
57
68
|
analyzeCmd.fn = (CLI, move) => {
|
|
58
69
|
if (!String(CLI.GAME?.id)) {
|
|
59
70
|
return CLI.error('No active game. Type "new" to start a game.')
|
|
@@ -75,18 +86,27 @@ analyzeCmd.fn = (CLI, move) => {
|
|
|
75
86
|
return CLI.error('Game has ended.')
|
|
76
87
|
}
|
|
77
88
|
|
|
78
|
-
const { edge, threats, scores } = analyze(trax)
|
|
89
|
+
const { edge, threats, scores, faulty } = analyze(trax)
|
|
79
90
|
|
|
80
|
-
if (detailed) CLI.out(edge.w)
|
|
91
|
+
if (detailed) CLI.out(CLI.color.white(edge.w))
|
|
81
92
|
CLI.out(
|
|
82
93
|
CLI.bubble('w', scores.w) +
|
|
83
94
|
CLI.color.white(' ') +
|
|
84
95
|
listThreats(threats.w, detailed),
|
|
85
96
|
)
|
|
86
|
-
|
|
97
|
+
|
|
98
|
+
if (detailed) {
|
|
99
|
+
if (faulty.w.length > 0) CLI.out(CLI.color.white('') + listFaulty(faulty.w))
|
|
100
|
+
CLI.out(CLI.color.black(edge.b))
|
|
101
|
+
}
|
|
102
|
+
|
|
87
103
|
CLI.out(
|
|
88
104
|
CLI.bubble('b', scores.b) +
|
|
89
105
|
CLI.color.black(' ') +
|
|
90
106
|
listThreats(threats.b, detailed),
|
|
91
107
|
)
|
|
108
|
+
|
|
109
|
+
if (detailed && faulty.b.length > 0) {
|
|
110
|
+
CLI.out(CLI.color.black('') + listFaulty(faulty.b))
|
|
111
|
+
}
|
|
92
112
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/* Copyright 2022 Chad Transtrum
|
|
1
|
+
/* Copyright 2022-2023 Chad Transtrum
|
|
2
2
|
*
|
|
3
3
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
4
|
* you may not use this file except in compliance with the License.
|
|
@@ -332,12 +332,7 @@ exportCmd.fn = (CLI, id, filename) => {
|
|
|
332
332
|
if (source.url) content += '\n; ' + source.url
|
|
333
333
|
}
|
|
334
334
|
|
|
335
|
-
|
|
336
|
-
if (puzzle.desc) content += '\n; ' + puzzle.desc
|
|
337
|
-
content += '\n; Starting position ' + puzzle.notation + '\n; '
|
|
338
|
-
const players = [0, 'white', 'black']
|
|
339
|
-
content += players[puzzle.player] + ' to win by move '
|
|
340
|
-
content += String(puzzle.max) + '\n'
|
|
335
|
+
content += '\n'
|
|
341
336
|
}
|
|
342
337
|
}
|
|
343
338
|
|
package/src/cmds/view.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/* Copyright 2022 Chad Transtrum
|
|
1
|
+
/* Copyright 2022-2023 Chad Transtrum
|
|
2
2
|
*
|
|
3
3
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
4
|
* you may not use this file except in compliance with the License.
|
|
@@ -53,6 +53,9 @@ viewCmd.fn = (CLI, move) => {
|
|
|
53
53
|
CLI.bubble('b' + (trax.turn === 2 ? 'h' : ''), players[1]),
|
|
54
54
|
)
|
|
55
55
|
CLI.display(trax, players, undefined, show)
|
|
56
|
-
if (puzzle
|
|
56
|
+
if (puzzle && (!note || !note.includes(puzzle))) {
|
|
57
|
+
CLI.out(CLI.color('Puzzle ' + puzzle))
|
|
58
|
+
}
|
|
59
|
+
|
|
57
60
|
if (note) CLI.out(CLI.color.help(note))
|
|
58
61
|
}
|
package/src/engine.js
CHANGED
|
@@ -33,6 +33,13 @@ const notationRegex = /^([@a-z]+)(\d+)([/\\+])$/i
|
|
|
33
33
|
const tileTypes = ['a', 'b', 'c', 'd', 'e', 'f']
|
|
34
34
|
|
|
35
35
|
/** Tiles are represented by the letters a-f.
|
|
36
|
+
* a b c d e f
|
|
37
|
+
* +--#--+ +--#--+ +--#--+ +--o--+ +--o--+ +--o--+
|
|
38
|
+
* | # | | # | | # | | o | | o | | o |
|
|
39
|
+
* oo ## ooo#ooo ## oo oo ## ####### ## oo
|
|
40
|
+
* | o | | # | | o | | # | | o | | # |
|
|
41
|
+
* +--o--+ +--#--+ +--o--+ +--#--+ +--o--+ +--#--+
|
|
42
|
+
*
|
|
36
43
|
* Slashes are represented by / \ or +.
|
|
37
44
|
* @readonly
|
|
38
45
|
* @type {Record<ValidTiles, Slash>}
|
package/src/puzzles.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/** This is a collection of puzzles.
|
|
2
|
-
* @copyright 2022
|
|
2
|
+
* @copyright 2022-2023
|
|
3
3
|
* @author Chad Transtrum <chad@transtrum.net>
|
|
4
4
|
* @license Apache-2.0
|
|
5
5
|
*/
|
|
@@ -23,6 +23,10 @@ export const sources = {
|
|
|
23
23
|
name: 'Donald G. Bailey',
|
|
24
24
|
url: 'http://www.traxgame.com/games_puzzles.php',
|
|
25
25
|
},
|
|
26
|
+
book1: {
|
|
27
|
+
name: 'Martin M. S. Pedersen',
|
|
28
|
+
url: 'https://gnutrax.com/book',
|
|
29
|
+
},
|
|
26
30
|
}
|
|
27
31
|
|
|
28
32
|
/** @type {Puzzle[]} */
|
|
@@ -396,7 +400,7 @@ export const puzzles = [
|
|
|
396
400
|
level: 2,
|
|
397
401
|
player: 2,
|
|
398
402
|
max: 10,
|
|
399
|
-
|
|
403
|
+
hints: ['A0\\', 'D0/'],
|
|
400
404
|
},
|
|
401
405
|
{
|
|
402
406
|
id: 'eb8.trx',
|
|
@@ -440,7 +444,7 @@ export const puzzles = [
|
|
|
440
444
|
level: 2,
|
|
441
445
|
player: 1,
|
|
442
446
|
max: 11,
|
|
443
|
-
hints: ['A0/', 'C4\\', '
|
|
447
|
+
hints: ['A0/', 'C4\\', 'D4/'],
|
|
444
448
|
},
|
|
445
449
|
{
|
|
446
450
|
id: 'iw3.trx',
|
|
@@ -598,14 +602,15 @@ export const puzzles = [
|
|
|
598
602
|
},
|
|
599
603
|
{
|
|
600
604
|
id: 'ib10.trx',
|
|
605
|
+
title: "Sucker's Opening",
|
|
601
606
|
src: 'traxgame',
|
|
602
607
|
game: 'traxloop',
|
|
603
|
-
notation: '@0/
|
|
608
|
+
notation: '@0/ @1/ B0\\',
|
|
604
609
|
icon: '0a:cd',
|
|
605
610
|
level: 3,
|
|
606
611
|
player: 2,
|
|
607
|
-
max:
|
|
608
|
-
hint: '
|
|
612
|
+
max: 14,
|
|
613
|
+
hint: '@2\\',
|
|
609
614
|
},
|
|
610
615
|
{
|
|
611
616
|
id: 'ib11.trx',
|
|
@@ -745,7 +750,7 @@ export const puzzles = [
|
|
|
745
750
|
game: 'traxloop',
|
|
746
751
|
notation: '@0/ A2+ A3+ A4\\ B3\\ B4+ B5\\ C5/ C4+ D4/ D5+ E5/ E4/',
|
|
747
752
|
icon: 'd:b:ba:aeecd:0fdec',
|
|
748
|
-
level:
|
|
753
|
+
level: 2,
|
|
749
754
|
player: 2,
|
|
750
755
|
max: 18,
|
|
751
756
|
hints: ['B1+', 'B2/', 'D3+'],
|
|
@@ -758,8 +763,8 @@ export const puzzles = [
|
|
|
758
763
|
icon: 'ecd',
|
|
759
764
|
level: 3,
|
|
760
765
|
player: 2,
|
|
761
|
-
max:
|
|
762
|
-
|
|
766
|
+
max: 16,
|
|
767
|
+
hints: ['A0+', 'C2\\'],
|
|
763
768
|
},
|
|
764
769
|
{
|
|
765
770
|
id: 'hb4.trx',
|
|
@@ -805,6 +810,127 @@ export const puzzles = [
|
|
|
805
810
|
max: 16,
|
|
806
811
|
hint: 'B1+',
|
|
807
812
|
},
|
|
813
|
+
{
|
|
814
|
+
id: 'book_01.trx',
|
|
815
|
+
src: 'book1',
|
|
816
|
+
game: 'traxloop',
|
|
817
|
+
notation: '@0/ @1+ B0/ C1+ D2/ C3/',
|
|
818
|
+
icon: 'dcba:bdcd:1dc',
|
|
819
|
+
level: 2,
|
|
820
|
+
player: 1,
|
|
821
|
+
max: 13,
|
|
822
|
+
hint: '@2/',
|
|
823
|
+
},
|
|
824
|
+
{
|
|
825
|
+
id: 'book_02.trx',
|
|
826
|
+
src: 'book1',
|
|
827
|
+
game: 'traxloop',
|
|
828
|
+
notation: '@0+ B1+ C1/ B0/ B0/ B0/ B0/ D5/ E5/ E6+ C4/ C3+ B6+',
|
|
829
|
+
icon: '0df:0ca:0df:cd:eecdc:0efae',
|
|
830
|
+
level: 2,
|
|
831
|
+
player: 2,
|
|
832
|
+
max: 18,
|
|
833
|
+
hints: ['D2\\', 'D3/'],
|
|
834
|
+
},
|
|
835
|
+
{
|
|
836
|
+
id: 'book_03.trx',
|
|
837
|
+
src: 'book1',
|
|
838
|
+
game: 'traxloop',
|
|
839
|
+
notation: '@0/ @1/ @1+ @1/ @1+ C0/ D1/ E1+ D0+ E1+ F1\\ E0+',
|
|
840
|
+
icon: '3e:1deef:1cdec:bdecd',
|
|
841
|
+
level: 3,
|
|
842
|
+
player: 1,
|
|
843
|
+
max: 19,
|
|
844
|
+
hint: 'C1\\',
|
|
845
|
+
},
|
|
846
|
+
{
|
|
847
|
+
id: 'book_04.trx',
|
|
848
|
+
src: 'book1',
|
|
849
|
+
game: 'traxloop',
|
|
850
|
+
notation: '@0+ B1/ @1/ A0/ C3/ A4+ @4+ @3/ a5\\',
|
|
851
|
+
icon: '1caf:1dec:dcafd:aeec:fd',
|
|
852
|
+
level: 3,
|
|
853
|
+
player: 2,
|
|
854
|
+
max: 16,
|
|
855
|
+
hint: 'C0/',
|
|
856
|
+
},
|
|
857
|
+
{
|
|
858
|
+
id: 'book_05.trx',
|
|
859
|
+
src: 'book1',
|
|
860
|
+
game: 'traxloop',
|
|
861
|
+
notation: '@0/ @1/ B0+ @2+ A0/ @2+',
|
|
862
|
+
icon: 'cd:ecde:0ecd',
|
|
863
|
+
level: 2,
|
|
864
|
+
player: 1,
|
|
865
|
+
max: 15,
|
|
866
|
+
hints: ['B4\\', 'C4/'],
|
|
867
|
+
},
|
|
868
|
+
{
|
|
869
|
+
id: 'book_07.trx',
|
|
870
|
+
src: 'book1',
|
|
871
|
+
game: 'traxloop',
|
|
872
|
+
notation: '@0/ A2+ A0+ @2\\ B0+ C1+ B0/ B0/ A2+',
|
|
873
|
+
icon: 'cdf:eca:0ee:0e:fd:0b',
|
|
874
|
+
level: 2,
|
|
875
|
+
player: 2,
|
|
876
|
+
max: 14,
|
|
877
|
+
hint: 'C6+',
|
|
878
|
+
},
|
|
879
|
+
{
|
|
880
|
+
id: 'book_08.trx',
|
|
881
|
+
src: 'book1',
|
|
882
|
+
game: 'traxloop',
|
|
883
|
+
notation: '@0/ @1/ A2/ A0/ B1/ @1+ @1/ A0/ @1/',
|
|
884
|
+
icon: 'cdfaf:0cbdc:0facd:0afdc',
|
|
885
|
+
level: 3,
|
|
886
|
+
player: 2,
|
|
887
|
+
max: 18,
|
|
888
|
+
hint: 'A0\\',
|
|
889
|
+
},
|
|
890
|
+
{
|
|
891
|
+
id: 'book_09.trx',
|
|
892
|
+
src: 'book1',
|
|
893
|
+
game: 'traxloop',
|
|
894
|
+
notation: '@0/ @1/ B0/ B0/ A0/ A5/ @3\\ @4/',
|
|
895
|
+
icon: '1d:1cd:dfdc:cacd:0fdc',
|
|
896
|
+
level: 3,
|
|
897
|
+
player: 1,
|
|
898
|
+
max: 15,
|
|
899
|
+
hint: 'A2\\',
|
|
900
|
+
},
|
|
901
|
+
{
|
|
902
|
+
id: 'book_10.trx',
|
|
903
|
+
src: 'book1',
|
|
904
|
+
game: 'traxloop',
|
|
905
|
+
notation: '@0/ A0+ A0/ A4+ A0/ A0/ B4+ C4/ @4+ @4/ @4/',
|
|
906
|
+
icon: '2caf:2dfa:2caf:cdeeec:fafd:2b',
|
|
907
|
+
level: 2,
|
|
908
|
+
player: 2,
|
|
909
|
+
max: 18,
|
|
910
|
+
hint: 'E6+',
|
|
911
|
+
},
|
|
912
|
+
{
|
|
913
|
+
id: 'book_11.trx',
|
|
914
|
+
src: 'book1',
|
|
915
|
+
game: 'traxloop',
|
|
916
|
+
notation: '@0+ B1+ C1/ C0/ B0/ @3+',
|
|
917
|
+
icon: '1d:1cd:eeec',
|
|
918
|
+
level: 1,
|
|
919
|
+
player: 1,
|
|
920
|
+
max: 9,
|
|
921
|
+
hint: 'B1\\',
|
|
922
|
+
},
|
|
923
|
+
{
|
|
924
|
+
id: 'book_12.trx',
|
|
925
|
+
src: 'book1',
|
|
926
|
+
game: 'traxloop',
|
|
927
|
+
notation: '@0+ B1+ C1/ C0/ B0/ D2+',
|
|
928
|
+
icon: '0d:0cde:eecd',
|
|
929
|
+
level: 3,
|
|
930
|
+
player: 1,
|
|
931
|
+
max: 15,
|
|
932
|
+
hint: 'B4\\',
|
|
933
|
+
},
|
|
808
934
|
]
|
|
809
935
|
|
|
810
936
|
puzzles.sort(
|
package/src/threats.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/** Threats database: hand-coded threats
|
|
2
|
-
* @copyright 2022
|
|
2
|
+
* @copyright 2022-2023
|
|
3
3
|
* @author Chad Transtrum <chad@transtrum.net>
|
|
4
4
|
* @license Apache-2.0
|
|
5
5
|
*/
|
|
@@ -89,7 +89,7 @@ export const ThreatsDB = class {
|
|
|
89
89
|
|
|
90
90
|
add(depth, pattern, reforms = false) {
|
|
91
91
|
this.#depth = Math.max(depth, this.#depth)
|
|
92
|
-
if (pattern in this.#db) throw new Error('Duplicate pattern:' + pattern)
|
|
92
|
+
if (pattern in this.#db) throw new Error('Duplicate pattern: ' + pattern)
|
|
93
93
|
this.#db[pattern] = new Threat(depth, pattern, reforms)
|
|
94
94
|
return this.#db[pattern]
|
|
95
95
|
}
|
|
@@ -175,6 +175,8 @@ for (const one of Object.keys(corners)) {
|
|
|
175
175
|
threats.compose(3, one + '!bbrmW') // Same, but slightly different layout
|
|
176
176
|
threats.compose(3, 'ab*lrb¡' + one) // Reverse
|
|
177
177
|
threats.compose(3, 'alrbb¡' + one) // Reverse of different layout
|
|
178
|
+
threats.compose(4, one + '!b*b¡ra?lr') // Partial edge with attack into hollow
|
|
179
|
+
threats.compose(4, 'rm?wr!b*b¡' + one) // Reverse
|
|
178
180
|
|
|
179
181
|
for (const two of Object.keys(corners)) {
|
|
180
182
|
end = two.replace(/[aA]/, 'm').replace('C', 'p')
|
|
@@ -188,9 +190,14 @@ for (const one of Object.keys(corners)) {
|
|
|
188
190
|
if (post) {
|
|
189
191
|
threats.compose(2, one + post + '!' + end) // L
|
|
190
192
|
threats.compose(3, one + post + '!br' + end) // Three-stage
|
|
193
|
+
threats.compose(3, one + post + 'r' + end) // Defense forms an L
|
|
191
194
|
threats.compose(3, one + '!br¡?' + end) // Attack to form an L threat
|
|
192
195
|
}
|
|
193
196
|
|
|
197
|
+
if (pre && post) {
|
|
198
|
+
threats.compose(3, start + 'r' + end) // Defense forms an L
|
|
199
|
+
}
|
|
200
|
+
|
|
194
201
|
threats.compose(3, one + '!b*b¡' + two) // Edge threat
|
|
195
202
|
}
|
|
196
203
|
}
|
|
@@ -200,3 +207,17 @@ threats.add(3, 'Arlbmwr') // Reversed
|
|
|
200
207
|
|
|
201
208
|
threats.add(4, 'rb¡c?w!br') // Connectable pair on an edge
|
|
202
209
|
threats.add(4, 'ArlbmrW') // Wide cave ready for an attack
|
|
210
|
+
|
|
211
|
+
// Threats to solve puzzle iw1.trx
|
|
212
|
+
threats.add(5, 'A?rwarlrar?W') // Back to back white corners next to a corner
|
|
213
|
+
threats.add(4, 'A?rlr!bralrar?W') // ... activated ... becomes:
|
|
214
|
+
threats.add(4, 'A?rlrbrmrwar?W') // Corner surrounded by corners
|
|
215
|
+
threats.add(4, 'A?rlr!bramrar?W') // ... activated ...
|
|
216
|
+
threats.add(3, 'A?rlr!brm?wr!brmrW') // ... defended ...
|
|
217
|
+
|
|
218
|
+
// Threats to solve puzzle ib10.trx
|
|
219
|
+
threats.add(6, 'Arw!b?rarlr') // Sucker's Opening
|
|
220
|
+
threats.add(5, 'Arw!b?brbralr') // ... activated ... becomes:
|
|
221
|
+
threats.add(4, 'Arw!b?brb¡arbW') // Wide corner opposite nearly formed edge
|
|
222
|
+
threats.add(5, 'Arw!b?brb¡rmrW') // ... or hollow corner opposite same edge
|
|
223
|
+
threats.add(4, 'Arw!b?brb¡rmwr') // ... activated ...
|
package/src/types.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
/** Type information.
|
|
2
|
+
* @copyright 2022-2023
|
|
2
3
|
* @author Chad Transtrum <chad@transtrum.net>
|
|
3
4
|
* @license Apache-2.0
|
|
4
5
|
*/
|
|
@@ -44,6 +45,7 @@ type EdgeLocation = {
|
|
|
44
45
|
pw?: boolean
|
|
45
46
|
zb?: string
|
|
46
47
|
zw?: string
|
|
48
|
+
idx?: number
|
|
47
49
|
}
|
|
48
50
|
|
|
49
51
|
type EdgeObject = {
|
|
@@ -53,7 +55,19 @@ type EdgeObject = {
|
|
|
53
55
|
}
|
|
54
56
|
|
|
55
57
|
/** Object representing a concrete threat found in a position. */
|
|
56
|
-
type FoundThreat = {
|
|
58
|
+
type FoundThreat = {
|
|
59
|
+
threat: string
|
|
60
|
+
match: string
|
|
61
|
+
at: number
|
|
62
|
+
value: number
|
|
63
|
+
level: number
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Threats found for each color that cannot actually be activated. */
|
|
67
|
+
type FaultyThreats = {
|
|
68
|
+
b: FoundThreat[]
|
|
69
|
+
w: FoundThreat[]
|
|
70
|
+
}
|
|
57
71
|
|
|
58
72
|
/** An object with keys representing each level, with arrays for each threat of
|
|
59
73
|
* that level. */
|
package/src/version.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// Generated by genversion.
|
|
2
|
-
export const version = '0.
|
|
2
|
+
export const version = '0.18.0'
|