@slugbugblue/trax 0.16.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 +35 -9
- package/Dockerfile +5 -0
- package/README.md +17 -8
- 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 +89 -40
- package/src/cmds/play-try.js +1 -1
- 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,12 +1,38 @@
|
|
|
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
|
+
|
|
25
|
+
## 0.17.0 - 2022-12-29
|
|
26
|
+
|
|
27
|
+
- The `import` CLI command now loads comments from a .trx file
|
|
28
|
+
|
|
3
29
|
## 0.16.0 - 2022-12-27
|
|
4
30
|
|
|
5
31
|
- Added a `notes` command to the CLI to add comments to a game
|
|
6
32
|
|
|
7
|
-
-
|
|
8
|
-
-
|
|
9
|
-
-
|
|
33
|
+
- all comments are included in the output generated by the `export` command
|
|
34
|
+
- the latest comment can be used as a text filter by the `list` command
|
|
35
|
+
- the latest comment is displayed by the `view` and `list` commands
|
|
10
36
|
|
|
11
37
|
- Minor analysis improvements
|
|
12
38
|
|
|
@@ -22,9 +48,9 @@
|
|
|
22
48
|
- Adjust puzzle category levels for harder puzzles
|
|
23
49
|
- Add multiple hints when the puzzle can be solved multiple ways
|
|
24
50
|
|
|
25
|
-
- Analysis
|
|
51
|
+
- Analysis improvements:
|
|
26
52
|
|
|
27
|
-
- Analysis
|
|
53
|
+
- Analysis suggestions now "solve" all tough puzzles
|
|
28
54
|
- Reworked threats to be more precise
|
|
29
55
|
- Reworked scoring to add more nuance
|
|
30
56
|
|
|
@@ -35,9 +61,9 @@
|
|
|
35
61
|
- `trax puzzles ls`: see all the puzzles
|
|
36
62
|
- `trax puzzle new`: start a new puzzle
|
|
37
63
|
|
|
38
|
-
- Analysis
|
|
39
|
-
|
|
40
|
-
|
|
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.
|
|
41
67
|
|
|
42
68
|
## 0.13.0 - 2022-11-25
|
|
43
69
|
|
|
@@ -130,7 +156,7 @@ variables from snake_case to camelCase.
|
|
|
130
156
|
- `to`: the normalized position code of the board after the move
|
|
131
157
|
- `via`: the move notation to go from `from` to `to`
|
|
132
158
|
|
|
133
|
-
|
|
159
|
+
## 0.3.1 - 2022-01-07
|
|
134
160
|
|
|
135
161
|
- Make a faster implementation of `trax.possible_locations()`
|
|
136
162
|
- Display updates for tentative moves
|
package/Dockerfile
ADDED
package/README.md
CHANGED
|
@@ -13,7 +13,7 @@ Trax is a two-player boardless board game invented by David Smith. For more
|
|
|
13
13
|
information about Trax, including its [rules][traxrules] and
|
|
14
14
|
[history][traxhistory], see the official website at [traxgame.com][traxgame].
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
The goal of this javascript project is to allow Trax to be played
|
|
17
17
|
programmatically. It can be used as an engine in a web browser, for example at
|
|
18
18
|
[slugbugblue.com][sbb]; or in nodejs, for example by using the `trax` CLI
|
|
19
19
|
provided as part of this project.
|
|
@@ -27,7 +27,7 @@ changes to the engine should meet the same standard.
|
|
|
27
27
|
# Install into a javascript project
|
|
28
28
|
npm install @slugbugblue/trax
|
|
29
29
|
|
|
30
|
-
# Or install globally to use the trax CLI
|
|
30
|
+
# Or install globally to use the trax CLI more easily
|
|
31
31
|
npm install -g @slugbugblue/trax
|
|
32
32
|
```
|
|
33
33
|
|
|
@@ -43,11 +43,12 @@ avoid worries about the need to escape certain characters from the shell, and it
|
|
|
43
43
|
can be easier to run multiple commands back to back.
|
|
44
44
|
|
|
45
45
|
Help is available for the CLI through the use of the `help` command. Type
|
|
46
|
-
`trax help`
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
46
|
+
`trax help` (or `npx trax help` with a local installation, or simply `help` if
|
|
47
|
+
run interactively) to see a list of the available commands, and `trax help play`
|
|
48
|
+
to see information about the `play` command. Aliases and abbreviations are
|
|
49
|
+
provided to minimize typing and to try to avoid the requirement of memorizing
|
|
50
|
+
the commands. For example, the `list` command has an alias of `ls`, and the
|
|
51
|
+
`select` command can be activated using `#`.
|
|
51
52
|
|
|
52
53
|
When you first launch the CLI, you can create a new game using the `new`
|
|
53
54
|
command, and then enter moves for that game using `play`. Since `play` is likely
|
|
@@ -186,9 +187,14 @@ always ready to share his insights on the mechanics of the game, as well as
|
|
|
186
187
|
various approaches to encoding its complexities. His undeserved kindness and
|
|
187
188
|
infinite patience in indulging my many questions can never be repaid.
|
|
188
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
|
+
|
|
189
195
|
## License
|
|
190
196
|
|
|
191
|
-
Copyright 2019-
|
|
197
|
+
Copyright 2019-2023 Chad Transtrum
|
|
192
198
|
|
|
193
199
|
Licensed under the Apache License, Version 2.0 (the "License"); you may not use
|
|
194
200
|
the files in this project except in compliance with the License. You may obtain
|
|
@@ -216,12 +222,15 @@ David Smith and heirs, and are not to be used without permission.
|
|
|
216
222
|
[docs-engine]: docs/engine.md
|
|
217
223
|
[docs-analyst]: docs/analyst.md
|
|
218
224
|
[env-path]: https://www.npmjs.com/package/env-paths
|
|
225
|
+
[gnutrax]: https://gnutrax.com
|
|
219
226
|
[goldtoken]: https://goldtoken.com/
|
|
227
|
+
[puzzlebook]: https://gnutrax.com/book
|
|
220
228
|
[repo]: https://gitlab.com/slugbugblue/trax
|
|
221
229
|
[sbb]: https://slugbugblue.com/
|
|
222
230
|
[traxbook]: http://traxgame.com/shop_book.php
|
|
223
231
|
[traxgame]: http://traxgame.com/
|
|
224
232
|
[traxhistory]: http://www.traxgame.com/about_history.php
|
|
233
|
+
[traxplayer]: mailto:traxplayer@gmail.com
|
|
225
234
|
[traxpuzzles]: http://www.traxgame.com/games_puzzles.php
|
|
226
235
|
[traxrules]: http://www.traxgame.com/about_rules.php
|
|
227
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.
|
|
@@ -137,6 +137,79 @@ export const exportCmd = {
|
|
|
137
137
|
],
|
|
138
138
|
}
|
|
139
139
|
|
|
140
|
+
/** Interpret the rules string of a .trx file.
|
|
141
|
+
* @arg {string} r - rules string in lower case
|
|
142
|
+
* @returns {TraxVariant}
|
|
143
|
+
*/
|
|
144
|
+
const getRules = (r) =>
|
|
145
|
+
r.includes('loop') ? 'traxloop' : r.includes('8x8') ? 'trax8' : 'trax'
|
|
146
|
+
|
|
147
|
+
/** Decode a .trx formatted string into a game, preserving players and comments.
|
|
148
|
+
* @arg {any} CLI - the CLI object
|
|
149
|
+
* @arg {string} content - the contents of the .trx file
|
|
150
|
+
* @returns {boolean} - true if a game was created
|
|
151
|
+
*/
|
|
152
|
+
const interpretFile = (CLI, content) => {
|
|
153
|
+
let rules
|
|
154
|
+
let trax
|
|
155
|
+
let players = []
|
|
156
|
+
const moves = []
|
|
157
|
+
const comments = []
|
|
158
|
+
for (const line of content.split('\n')) {
|
|
159
|
+
if (!line) continue
|
|
160
|
+
const [ln, ...comment] = line.split(/[#;]/)
|
|
161
|
+
if (ln) {
|
|
162
|
+
const lower = ln.toLowerCase()
|
|
163
|
+
if (!rules && lower.includes('trax')) {
|
|
164
|
+
rules = getRules(lower)
|
|
165
|
+
} else if (players.length === 0 && lower.includes(' vs ')) {
|
|
166
|
+
players = ln.split(' ').filter(Boolean)
|
|
167
|
+
} else {
|
|
168
|
+
moves.push(...ln.split(' '))
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (!trax && rules && (players.length > 0 || moves.length > 0)) {
|
|
173
|
+
trax = new Trax(rules)
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (moves.length > 0) {
|
|
177
|
+
if (!trax) return false
|
|
178
|
+
if (trax.gameOver) {
|
|
179
|
+
comments.push({
|
|
180
|
+
note: moves.map((c) => c.trim()).join(' '),
|
|
181
|
+
move: trax.move,
|
|
182
|
+
})
|
|
183
|
+
} else {
|
|
184
|
+
trax.playMoves(moves.filter(Boolean))
|
|
185
|
+
moves.splice(0, moves.length)
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (comment.length > 0) {
|
|
190
|
+
const note = comment.map((c) => c.trim()).join(' ')
|
|
191
|
+
if (!/^@slugbugblue\/trax cli\.js v/.test(note)) {
|
|
192
|
+
comments.push({
|
|
193
|
+
note,
|
|
194
|
+
move: trax?.move || 0,
|
|
195
|
+
})
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (!trax) return false
|
|
201
|
+
|
|
202
|
+
CLI.do('new', rules, ...players)
|
|
203
|
+
CLI.GAMES[String(CLI.GAME.id)].notes = comments
|
|
204
|
+
if (trax.moves.length > 0) {
|
|
205
|
+
CLI.do('play', ...trax.moves)
|
|
206
|
+
} else {
|
|
207
|
+
CLI.save()
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return true
|
|
211
|
+
}
|
|
212
|
+
|
|
140
213
|
importCmd.fn = async (CLI, filename) => {
|
|
141
214
|
cli = CLI
|
|
142
215
|
if (!filename) {
|
|
@@ -156,35 +229,11 @@ importCmd.fn = async (CLI, filename) => {
|
|
|
156
229
|
}
|
|
157
230
|
}
|
|
158
231
|
|
|
159
|
-
if (!content) CLI.error(fsError.toString())
|
|
232
|
+
if (!content) return CLI.error(fsError.toString())
|
|
160
233
|
}
|
|
161
234
|
|
|
162
|
-
if (content) {
|
|
163
|
-
|
|
164
|
-
let players = []
|
|
165
|
-
const moves = []
|
|
166
|
-
for (const line of content.split('\n')) {
|
|
167
|
-
if (!line) continue
|
|
168
|
-
const ln = line.split(/[#;]/)[0]
|
|
169
|
-
if (!ln) continue
|
|
170
|
-
const lower = ln.toLowerCase()
|
|
171
|
-
if (!game && lower.includes('trax')) {
|
|
172
|
-
game = 'trax'
|
|
173
|
-
if (lower.includes('loop')) game = 'traxloop'
|
|
174
|
-
if (lower.includes('8x8')) game = 'trax8'
|
|
175
|
-
} else if (players.length === 0 && lower.includes(' vs ')) {
|
|
176
|
-
players = ln.split(' ').filter(Boolean)
|
|
177
|
-
} else {
|
|
178
|
-
moves.push(...ln.split(' '))
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
if (game) {
|
|
183
|
-
CLI.do('new', game, ...players)
|
|
184
|
-
CLI.do('play', ...moves.filter(Boolean))
|
|
185
|
-
} else {
|
|
186
|
-
CLI.error(filename + ' does not appear to be formatted correctly.')
|
|
187
|
-
}
|
|
235
|
+
if (!interpretFile(CLI, content)) {
|
|
236
|
+
CLI.error(filename + ' does not appear to be formatted correctly.')
|
|
188
237
|
}
|
|
189
238
|
}
|
|
190
239
|
|
|
@@ -204,6 +253,15 @@ const gameNotes = (move, notes) => {
|
|
|
204
253
|
)
|
|
205
254
|
}
|
|
206
255
|
|
|
256
|
+
/** Join two strings together with a character with the appropriate joining
|
|
257
|
+
* character depending on the length of the strings.
|
|
258
|
+
* @arg {string} a - the first string
|
|
259
|
+
* @arg {string} b - the second string
|
|
260
|
+
* @returns {string} - both strings correctly joined
|
|
261
|
+
*/
|
|
262
|
+
const join = (a, b) =>
|
|
263
|
+
a.length === 0 ? b : a.length + b.length > 78 ? a + '\n' + b : a + ' ' + b
|
|
264
|
+
|
|
207
265
|
/** Interleave the game moves with any notes, formatted for the export file.
|
|
208
266
|
* @arg {string[]} moves - an array of move notations
|
|
209
267
|
* @arg {GameNotes} notes? - The notes for a game
|
|
@@ -216,9 +274,7 @@ const interleaveNotation = (moves, notes) => {
|
|
|
216
274
|
for (const move of moves) {
|
|
217
275
|
const comments = gameNotes(moveNumber, notes)
|
|
218
276
|
if (comments) {
|
|
219
|
-
|
|
220
|
-
const space = lengthA > 0 ? (lengthA + lengthC < 78 ? ' ' : '\n') : ''
|
|
221
|
-
content += space + annotation + comments
|
|
277
|
+
content += join(annotation, comments)
|
|
222
278
|
annotation = ''
|
|
223
279
|
}
|
|
224
280
|
|
|
@@ -234,9 +290,7 @@ const interleaveNotation = (moves, notes) => {
|
|
|
234
290
|
|
|
235
291
|
const comments = gameNotes(moveNumber, notes)
|
|
236
292
|
if (comments) {
|
|
237
|
-
|
|
238
|
-
const space = lengthA > 0 ? (lengthA + lengthC < 78 ? ' ' : '\n') : ''
|
|
239
|
-
content += space + annotation + comments
|
|
293
|
+
content += join(annotation, comments)
|
|
240
294
|
annotation = ''
|
|
241
295
|
}
|
|
242
296
|
|
|
@@ -278,12 +332,7 @@ exportCmd.fn = (CLI, id, filename) => {
|
|
|
278
332
|
if (source.url) content += '\n; ' + source.url
|
|
279
333
|
}
|
|
280
334
|
|
|
281
|
-
|
|
282
|
-
if (puzzle.desc) content += '\n; ' + puzzle.desc
|
|
283
|
-
content += '\n; Starting position ' + puzzle.notation + '\n; '
|
|
284
|
-
const players = [0, 'white', 'black']
|
|
285
|
-
content += players[puzzle.player] + ' to win by move '
|
|
286
|
-
content += String(puzzle.max) + '\n'
|
|
335
|
+
content += '\n'
|
|
287
336
|
}
|
|
288
337
|
}
|
|
289
338
|
|
package/src/cmds/play-try.js
CHANGED
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'
|