@slugbugblue/trax 0.13.0 → 0.15.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/CHANGELOG.md +29 -0
- package/README.md +10 -6
- package/package.json +1 -1
- package/src/analyst.js +195 -77
- package/src/cli.js +55 -2
- package/src/cmds/analyze.js +2 -2
- package/src/cmds/import-export.js +32 -2
- package/src/cmds/list.js +4 -4
- package/src/cmds/new.js +4 -1
- package/src/cmds/play-try.js +54 -7
- package/src/cmds/puzzles.js +158 -0
- package/src/cmds/suggest.js +1 -1
- package/src/cmds/view.js +1 -1
- package/src/puzzles.js +53 -39
- package/src/threats.js +78 -40
- package/src/tty.js +1 -1
- package/src/types.d.ts +56 -0
- package/src/version.js +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,34 @@
|
|
|
1
1
|
# @slugbugblue/trax
|
|
2
2
|
|
|
3
|
+
## 0.15.0 - 2022-12-14
|
|
4
|
+
|
|
5
|
+
- Added shared `puzzles` commands to the CLI:
|
|
6
|
+
|
|
7
|
+
- `trax ls puzzles`: see all the puzzles
|
|
8
|
+
- `trax new puzzle`: start a new puzzle
|
|
9
|
+
|
|
10
|
+
- Puzzle improvements:
|
|
11
|
+
|
|
12
|
+
- Adjust puzzle category levels for harder puzzles
|
|
13
|
+
- Add multiple hints when the puzzle can be solved multiple ways
|
|
14
|
+
|
|
15
|
+
- Analysis engine improvements:
|
|
16
|
+
|
|
17
|
+
- Analysis engine suggestions now "solve" all tough puzzles
|
|
18
|
+
- Reworked threats to be more precise
|
|
19
|
+
- Reworked scoring to add more nuance
|
|
20
|
+
|
|
21
|
+
## 0.14.0 - 2022-12-05
|
|
22
|
+
|
|
23
|
+
- Added `puzzles` to the CLI:
|
|
24
|
+
|
|
25
|
+
- `trax puzzles ls`: see all the puzzles
|
|
26
|
+
- `trax puzzle new`: start a new puzzle
|
|
27
|
+
|
|
28
|
+
- Analysis engine tests now include correctly suggesting the first move for
|
|
29
|
+
solving all tutorial and easy puzzles. As the threats database improves, we
|
|
30
|
+
will add more puzzles to the tests.
|
|
31
|
+
|
|
3
32
|
## 0.13.0 - 2022-11-25
|
|
4
33
|
|
|
5
34
|
- Add move suggestions to `analyst.js` for CLI and bot use
|
package/README.md
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
# Trax
|
|
2
2
|
|
|
3
|
-
Trax
|
|
3
|
+
This project aims to provide tools for working with Trax games in Javascript.
|
|
4
|
+
|
|
5
|
+
- Trax game-playing engine
|
|
6
|
+
- Position analysis engine
|
|
7
|
+
- Over 50 puzzles to help hone your Trax skills
|
|
8
|
+
- Command-line interface for ease of use
|
|
4
9
|
|
|
5
10
|
## Description
|
|
6
11
|
|
|
@@ -32,10 +37,10 @@ The command-line interface is provided as `trax`, available in your path if
|
|
|
32
37
|
installed globally, or in the `node_modules/.bin` directory and runnable using
|
|
33
38
|
`npx trax` if installed locally.
|
|
34
39
|
|
|
35
|
-
The CLI can be used interactively by
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
back.
|
|
40
|
+
The CLI can be used interactively by running the command with no parameters or
|
|
41
|
+
by passing the `--interactive` or `-i` flag. When run interactively, you can
|
|
42
|
+
avoid worries about the need to escape certain characters from the shell, and it
|
|
43
|
+
can be easier to run multiple commands back to back.
|
|
39
44
|
|
|
40
45
|
Help is available for the CLI through the use of the `help` command. Type
|
|
41
46
|
`trax help` to see a list of the available commands, and `trax help play` to see
|
|
@@ -154,7 +159,6 @@ publicly available puzzles at [the traxgame.com puzzles page][traxpuzzles].
|
|
|
154
159
|
## Roadmap
|
|
155
160
|
|
|
156
161
|
- Improve the threats database / analysis engine.
|
|
157
|
-
- Add a puzzle interface to the CLI.
|
|
158
162
|
- Add more puzzles.
|
|
159
163
|
|
|
160
164
|
## Support
|
package/package.json
CHANGED
package/src/analyst.js
CHANGED
|
@@ -1,26 +1,15 @@
|
|
|
1
|
-
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* You may obtain a copy of the License at
|
|
6
|
-
*
|
|
7
|
-
* http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
-
*
|
|
9
|
-
* Unless required by applicable law or agreed to in writing, software
|
|
10
|
-
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
-
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
-
* See the License for the specific language governing permissions and
|
|
13
|
-
* limitations under the License.
|
|
1
|
+
/** Analyst module: provides analysis functions for Trax games.
|
|
2
|
+
* @copyright 2022
|
|
3
|
+
* @author Chad Transtrum <chad@transtrum.net>
|
|
4
|
+
* @license Apache-2.0
|
|
14
5
|
*/
|
|
15
6
|
|
|
16
|
-
// Analyst module: provides analysis functions for Trax games
|
|
17
|
-
|
|
18
7
|
import { Point } from '@slugbugblue/trax/point'
|
|
19
8
|
import { Trax } from '@slugbugblue/trax'
|
|
20
9
|
|
|
21
10
|
import { threats } from '@slugbugblue/trax/threats'
|
|
22
11
|
|
|
23
|
-
|
|
12
|
+
/** Helper function to generate a unique id for the analysis. */
|
|
24
13
|
const id = () => {
|
|
25
14
|
const chars = 'abcdefghijklmnopqrstuvwxyz0123456789'
|
|
26
15
|
let code = ''
|
|
@@ -31,14 +20,27 @@ const id = () => {
|
|
|
31
20
|
return 'analysis-' + code
|
|
32
21
|
}
|
|
33
22
|
|
|
34
|
-
|
|
23
|
+
/** A sort function for hashes with a numeric score property.
|
|
24
|
+
* @arg {PositionScore} a - The first Position Score to be compared
|
|
25
|
+
* @arg {PositionScore} b - The second Position Score to be compared
|
|
26
|
+
* @returns {number} a.score <=> b.score
|
|
27
|
+
*/
|
|
35
28
|
const byScoreDesc = (a, b) => b.score - a.score
|
|
36
29
|
|
|
37
|
-
|
|
30
|
+
/** A filter function factory to remove scores not within x points of the
|
|
31
|
+
* highest score.
|
|
32
|
+
* @arg highest {number} - The highest score
|
|
33
|
+
* @arg tolerance {number} - How close another score must be to be included
|
|
34
|
+
* @returns {(s:PositionScore)=>boolean} a filter function
|
|
35
|
+
*/
|
|
38
36
|
const withinX = (highest, tolerance) => (s) =>
|
|
39
37
|
s.score === highest || highest - s.score <= tolerance
|
|
40
38
|
|
|
41
|
-
|
|
39
|
+
/** Return a random element of an array.
|
|
40
|
+
* @template T
|
|
41
|
+
* @arg {T[]} array - an array of elements
|
|
42
|
+
* @returns {T} one of the elements at random
|
|
43
|
+
*/
|
|
42
44
|
const randomDraw = (array) => array[Math.floor(Math.random() * array.length)]
|
|
43
45
|
|
|
44
46
|
// Make parseInt a little shorter
|
|
@@ -56,12 +58,16 @@ const edgeColor = (tile, dir) => {
|
|
|
56
58
|
|
|
57
59
|
const encodeNumber = (n) => String.fromCodePoint(48 + n)
|
|
58
60
|
|
|
59
|
-
|
|
61
|
+
/** Explore a raw edge and update cave information.
|
|
62
|
+
* @arg {RawEdge} rawEdge - the raw edge information
|
|
63
|
+
* @returns {RawEdge}
|
|
64
|
+
*/
|
|
60
65
|
const spelunk = (rawEdge) => {
|
|
61
66
|
for (let index = 1; index < rawEdge.length - 1; index++) {
|
|
62
67
|
if (rawEdge[index].t === 'C') {
|
|
63
|
-
const
|
|
64
|
-
const
|
|
68
|
+
const blank = () => ({ x: 0, y: 0, c: '', t: '' })
|
|
69
|
+
const previous = rawEdge[index - 1] || blank()
|
|
70
|
+
const next = rawEdge[index + 1] || blank()
|
|
65
71
|
if (next.t === 'c') next.t = 'C'
|
|
66
72
|
if (previous.t === 'c') {
|
|
67
73
|
previous.t = 'C'
|
|
@@ -78,7 +84,10 @@ const spelunk = (rawEdge) => {
|
|
|
78
84
|
return rawEdge
|
|
79
85
|
}
|
|
80
86
|
|
|
81
|
-
|
|
87
|
+
/** Combine hollows into a single item in the raw edge array.
|
|
88
|
+
* @arg {RawEdge} rawEdge
|
|
89
|
+
* @returns {RawEdge}
|
|
90
|
+
*/
|
|
82
91
|
const hollowOut = (rawEdge) => {
|
|
83
92
|
for (let index = 1; index < rawEdge.length - 1; index++) {
|
|
84
93
|
const space = rawEdge[index]
|
|
@@ -93,8 +102,13 @@ const hollowOut = (rawEdge) => {
|
|
|
93
102
|
return rawEdge
|
|
94
103
|
}
|
|
95
104
|
|
|
96
|
-
|
|
97
|
-
|
|
105
|
+
/** Supposedly deep nesting is bad, so we broke this out of "pairUp" for
|
|
106
|
+
* "better" code, even though this is useless on its own ...
|
|
107
|
+
* @arg {RawEdge} rawEdge
|
|
108
|
+
* @arg {number} index
|
|
109
|
+
* @arg {string} color
|
|
110
|
+
* @pairs {Record<string, number>}
|
|
111
|
+
*/
|
|
98
112
|
const findPair = (rawEdge, index, color, pairs) => {
|
|
99
113
|
const colors = new Set([color, 'l'])
|
|
100
114
|
const space = rawEdge[index]
|
|
@@ -131,8 +145,12 @@ const findPair = (rawEdge, index, color, pairs) => {
|
|
|
131
145
|
}
|
|
132
146
|
}
|
|
133
147
|
|
|
134
|
-
|
|
148
|
+
/** Find connectable pairs in the raw edge.
|
|
149
|
+
* @arg {RawEdge} rawEdge
|
|
150
|
+
* @returns {RawEdge}
|
|
151
|
+
*/
|
|
135
152
|
const pairUp = (rawEdge) => {
|
|
153
|
+
/** @type Record<string, number> */
|
|
136
154
|
const pairs = {}
|
|
137
155
|
for (const [index, space] of rawEdge.entries()) {
|
|
138
156
|
for (const bw of ['w', 'b']) {
|
|
@@ -146,7 +164,10 @@ const pairUp = (rawEdge) => {
|
|
|
146
164
|
return rawEdge
|
|
147
165
|
}
|
|
148
166
|
|
|
149
|
-
|
|
167
|
+
/** Label and finalize the raw edge.
|
|
168
|
+
* @arg {RawEdge} rawEdge
|
|
169
|
+
* @returns {EdgeObject}
|
|
170
|
+
*/
|
|
150
171
|
const label = (rawEdge) => {
|
|
151
172
|
const final = { w: '', b: '', edge: rawEdge }
|
|
152
173
|
for (const space of rawEdge) {
|
|
@@ -170,47 +191,73 @@ const label = (rawEdge) => {
|
|
|
170
191
|
return final
|
|
171
192
|
}
|
|
172
193
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
// for (const threat of threatsArray) count += threat.value
|
|
178
|
-
// return count
|
|
179
|
-
}
|
|
194
|
+
/** Quick way to get the number of items in an array.
|
|
195
|
+
* @returns {number} - length of the array
|
|
196
|
+
*/
|
|
197
|
+
const length = (array) => array?.length || 0
|
|
180
198
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
199
|
+
/** Calculate the points for the number of threats at a particular level.
|
|
200
|
+
* @arg {FoundThreats} from - the threats found for a certain player
|
|
201
|
+
* @arg {number} level - the level to calculate the points for
|
|
202
|
+
* @arg {number} [as] - if included, the level the points should be calculated as
|
|
203
|
+
* @return {number} the number of points for the level
|
|
204
|
+
*/
|
|
205
|
+
const attackScore = (from, level, as) => {
|
|
206
|
+
const count = length(from[level])
|
|
207
|
+
const calculateAs = Math.min(as === undefined ? level : as, threats.maxDepth)
|
|
208
|
+
if (calculateAs < 1) return count
|
|
209
|
+
return count * 10 ** (threats.maxDepth - calculateAs + 1)
|
|
185
210
|
}
|
|
186
211
|
|
|
187
|
-
|
|
188
|
-
|
|
212
|
+
/** Shortcut to get the number of attacks.
|
|
213
|
+
* @arg {FoundThreats} from - the threats found for a certain player
|
|
214
|
+
* @returns {number} the number of attacks (win on next move) present
|
|
215
|
+
*/
|
|
216
|
+
const attackCount = (from) => length(from['1'])
|
|
189
217
|
|
|
190
|
-
|
|
218
|
+
/** Shortcut to get the number of passive threats
|
|
219
|
+
* @arg {FoundThreats} from - the threats found for a player
|
|
220
|
+
* @returns {number} the number of passive threats (win in 2 or more moves)
|
|
221
|
+
*/
|
|
191
222
|
const threatCount = (from) => {
|
|
192
223
|
let count = 0
|
|
193
224
|
for (const [depth, array] of Object.entries(from)) {
|
|
194
|
-
count += int(depth) > 1 ?
|
|
225
|
+
count += int(depth) > 1 ? length(array) : 0
|
|
195
226
|
}
|
|
196
227
|
|
|
197
228
|
return count
|
|
198
229
|
}
|
|
199
230
|
|
|
200
|
-
|
|
231
|
+
/** Shortcut to determine if we have an active attack.
|
|
232
|
+
* @arg {FoundThreats} from - the threats found for a player
|
|
233
|
+
* @returns {boolean}
|
|
234
|
+
*/
|
|
235
|
+
const activeAttack = (from) =>
|
|
201
236
|
attackCount(from) > 1 || (attackCount(from) === 1 && threatCount(from) > 0)
|
|
202
237
|
|
|
238
|
+
/** Shortcut to determine if we have a passive attack.
|
|
239
|
+
* @arg {FoundThreats} from - the threats found for a player
|
|
240
|
+
* @returns {boolean}
|
|
241
|
+
*/
|
|
242
|
+
const passiveAttack = (from) => threatCount(from) > 1
|
|
243
|
+
|
|
244
|
+
/** Shortcut to determine if we are under attack.
|
|
245
|
+
* @arg {FoundThreats} from - the threats found for a player
|
|
246
|
+
* @returns {boolean}
|
|
247
|
+
*/
|
|
203
248
|
const underAttack = (from) => attackCount(from) + threatCount(from) > 0
|
|
204
249
|
|
|
205
250
|
// Analysis class
|
|
206
251
|
export const Analysis = class {
|
|
207
|
-
/** @type TraxVariant */
|
|
252
|
+
/** @type {TraxVariant} */
|
|
208
253
|
#rules = 'trax'
|
|
209
254
|
#id = 'analysis'
|
|
255
|
+
/** @type {string[]} - List of moves to arrive at this position */
|
|
210
256
|
#moves = []
|
|
211
257
|
#notation = ''
|
|
212
258
|
#edge = undefined
|
|
213
|
-
|
|
259
|
+
/** @type {Trax} */
|
|
260
|
+
#game
|
|
214
261
|
#scores = {}
|
|
215
262
|
#threats = undefined
|
|
216
263
|
|
|
@@ -241,6 +288,9 @@ export const Analysis = class {
|
|
|
241
288
|
return this.#edge
|
|
242
289
|
}
|
|
243
290
|
|
|
291
|
+
/** All threats present in this position.
|
|
292
|
+
* @returns {FoundThreatsCollection}
|
|
293
|
+
*/
|
|
244
294
|
get threats() {
|
|
245
295
|
if (!this.#threats) {
|
|
246
296
|
this.#threats = { b: this.#applyThreats('b'), w: this.#applyThreats('w') }
|
|
@@ -273,6 +323,69 @@ export const Analysis = class {
|
|
|
273
323
|
return count > 2
|
|
274
324
|
}
|
|
275
325
|
|
|
326
|
+
/** Determine the points of the currently active player.
|
|
327
|
+
* @returns {[number, number]} - the points for this player, and the level
|
|
328
|
+
* of the most dangerous threat
|
|
329
|
+
*/
|
|
330
|
+
#activeScore() {
|
|
331
|
+
let score = 0
|
|
332
|
+
let threatLevel = Number.POSITIVE_INFINITY
|
|
333
|
+
const { game } = this
|
|
334
|
+
const active = this.threats[game.color]
|
|
335
|
+
|
|
336
|
+
// First examine the active player
|
|
337
|
+
for (const level of Object.keys(active).map((i) => int(i))) {
|
|
338
|
+
if (level > 0) threatLevel = Math.min(threatLevel, level)
|
|
339
|
+
score += attackScore(active, level)
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
return [score, threatLevel]
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/** Determine the points of the waiting player.
|
|
346
|
+
* @arg {Color} color - the color for which we are generating the score
|
|
347
|
+
* @arg {number} threatLevel - the lowest level for which the active player
|
|
348
|
+
* has an active threat
|
|
349
|
+
* @returns {[number, number]} - the points for this player and the multiplier
|
|
350
|
+
* to be applied to the other player's points
|
|
351
|
+
*/
|
|
352
|
+
#inactiveScore(color, threatLevel) {
|
|
353
|
+
const { game } = this
|
|
354
|
+
const active = this.threats[game.color]
|
|
355
|
+
const inactive = this.threats[Trax.other(game.color)]
|
|
356
|
+
|
|
357
|
+
// Is the color we are looking at the passive player?
|
|
358
|
+
const passive = game.color !== color
|
|
359
|
+
|
|
360
|
+
// Is the inactive player activating known threats?
|
|
361
|
+
const attacking = activeAttack(inactive)
|
|
362
|
+
|
|
363
|
+
// Was the inactive player unable to clear all threats?
|
|
364
|
+
const defending = !attacking && underAttack(active)
|
|
365
|
+
|
|
366
|
+
// Does the inactive player have multiple threats
|
|
367
|
+
const threatening = !attacking && passiveAttack(inactive)
|
|
368
|
+
|
|
369
|
+
// If calc is false, attacks and threats are treated as corners
|
|
370
|
+
const calc = !passive || attacking || defending || threatening
|
|
371
|
+
|
|
372
|
+
// If the current player does not hold the initiative, reduce the score
|
|
373
|
+
const multiplier = passive && attacking && threatLevel > 1 ? 0.1 : 1
|
|
374
|
+
|
|
375
|
+
let score = 0
|
|
376
|
+
|
|
377
|
+
// Now calculate the effects of the passive player
|
|
378
|
+
for (const level of Object.keys(inactive).map((i) => int(i))) {
|
|
379
|
+
const calcLevel = level > 0 && defending ? level + threatLevel - 1 : level
|
|
380
|
+
let points = attackScore(inactive, level, calc ? calcLevel : 0)
|
|
381
|
+
if (level === threatLevel) points = passive ? Math.floor(points / 2) : 0
|
|
382
|
+
if (threatening) points = Math.floor(points / 2)
|
|
383
|
+
score += points
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
return [score, multiplier]
|
|
387
|
+
}
|
|
388
|
+
|
|
276
389
|
#score(color) {
|
|
277
390
|
if (this.#scores[color] === undefined) {
|
|
278
391
|
const { game } = this
|
|
@@ -282,30 +395,11 @@ export const Analysis = class {
|
|
|
282
395
|
return this.#scores[color]
|
|
283
396
|
}
|
|
284
397
|
|
|
285
|
-
|
|
398
|
+
const [currentPlayer, threatLevel] = this.#activeScore()
|
|
399
|
+
const [nextPlayer, multiplier] = this.#inactiveScore(color, threatLevel)
|
|
286
400
|
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
let advantage = Number.POSITIVE_INFINITY
|
|
290
|
-
|
|
291
|
-
// First examine the active player
|
|
292
|
-
for (const level of Object.keys(active).map((i) => int(i))) {
|
|
293
|
-
if (level > 0 && level < advantage) advantage = level
|
|
294
|
-
score += attackScore(active, level)
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
const passive = game.color !== color
|
|
298
|
-
const calc = !passive || passiveAttack(inactive) || underAttack(active)
|
|
299
|
-
|
|
300
|
-
// Calculate the effects of the passive player first
|
|
301
|
-
for (const level of Object.keys(inactive).map((i) => int(i))) {
|
|
302
|
-
if (level > advantage) continue
|
|
303
|
-
let points = calc || level === 0 ? attackScore(inactive, level) : 1
|
|
304
|
-
if (level === advantage) points = passive ? Math.floor(points / 2) : 0
|
|
305
|
-
score -= points
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
if (passive) score *= -1
|
|
401
|
+
let score = Math.floor(currentPlayer * multiplier) - nextPlayer
|
|
402
|
+
if (score && game.color !== color) score *= -1
|
|
309
403
|
|
|
310
404
|
this.#scores[color] = score
|
|
311
405
|
}
|
|
@@ -368,6 +462,7 @@ export const Analysis = class {
|
|
|
368
462
|
// Trace the edge of the playing area
|
|
369
463
|
#traceEdge() {
|
|
370
464
|
const { game } = this
|
|
465
|
+
/** @type {RawEdge} */
|
|
371
466
|
const rawEdge = []
|
|
372
467
|
const lines = {}
|
|
373
468
|
const count = { w: 0, b: 0 }
|
|
@@ -422,8 +517,26 @@ export const Analysis = class {
|
|
|
422
517
|
return label(pairUp(hollowOut(spelunk(rawEdge))))
|
|
423
518
|
}
|
|
424
519
|
|
|
425
|
-
|
|
520
|
+
/** Compose the threat into a FoundThreat object.
|
|
521
|
+
* @arg {Threat} threat - the threat that has been found
|
|
522
|
+
* @arg {RegExpMatchArray} match - the matching item
|
|
523
|
+
* @returns {FoundThreat}
|
|
524
|
+
*/
|
|
525
|
+
#foundThreat(threat, match) {
|
|
526
|
+
return {
|
|
527
|
+
threat: threat.pattern,
|
|
528
|
+
match: match[1], // The match is in the capturing group
|
|
529
|
+
at: match.index,
|
|
530
|
+
value: threat.value,
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
/** Apply the threats to a color's regex-ready edge.
|
|
535
|
+
* @arg {Color} color - the color to find threats for
|
|
536
|
+
* @returns {FoundThreats}
|
|
537
|
+
*/
|
|
426
538
|
#applyThreats(color) {
|
|
539
|
+
/** @type {FoundThreats} */
|
|
427
540
|
const matches = {}
|
|
428
541
|
const { length } = this.edge[color]
|
|
429
542
|
const edge = this.edge[color].repeat(2)
|
|
@@ -431,13 +544,11 @@ export const Analysis = class {
|
|
|
431
544
|
const find = matches[threat.depth] || []
|
|
432
545
|
for (const match of edge.matchAll(threat.rx)) {
|
|
433
546
|
if (match.index >= length) break
|
|
434
|
-
if (find.every((f) => f.at !== match.index || f.match !== match[
|
|
435
|
-
find.push(
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
value: threat.value,
|
|
440
|
-
})
|
|
547
|
+
if (find.every((f) => f.at !== match.index || f.match !== match[1])) {
|
|
548
|
+
find.push(this.#foundThreat(threat, match))
|
|
549
|
+
} else {
|
|
550
|
+
const duplicate = matches[99] || []
|
|
551
|
+
duplicate.push(this.#foundThreat(threat, match))
|
|
441
552
|
}
|
|
442
553
|
}
|
|
443
554
|
|
|
@@ -479,12 +590,19 @@ export const Analysis = class {
|
|
|
479
590
|
}
|
|
480
591
|
}
|
|
481
592
|
|
|
593
|
+
/** Analyze a game position.
|
|
594
|
+
* @arg {Trax} game - the position to analyze
|
|
595
|
+
* @returns {Analysis}
|
|
596
|
+
*/
|
|
482
597
|
export const analyze = (game) => new Analysis(game)
|
|
483
598
|
|
|
484
599
|
export const Suggestion = class {
|
|
600
|
+
/** @type {PositionScore[]} */
|
|
485
601
|
all = []
|
|
602
|
+
/** @type {PositionScore[]} */
|
|
486
603
|
options = []
|
|
487
|
-
|
|
604
|
+
/** @type {PositionScore} */
|
|
605
|
+
pick
|
|
488
606
|
ms = 0
|
|
489
607
|
|
|
490
608
|
constructor(analyzed, ms) {
|
package/src/cli.js
CHANGED
|
@@ -39,6 +39,7 @@ import {
|
|
|
39
39
|
import { listCmd } from './cmds/list.js'
|
|
40
40
|
import { newCmd } from './cmds/new.js'
|
|
41
41
|
import { playCmd, tryCmd } from './cmds/play-try.js'
|
|
42
|
+
import { puzzlesCmd } from './cmds/puzzles.js'
|
|
42
43
|
import { selectCmd } from './cmds/select.js'
|
|
43
44
|
import { suggestCmd } from './cmds/suggest.js'
|
|
44
45
|
import { undoCmd } from './cmds/undo.js'
|
|
@@ -87,6 +88,7 @@ const plugins = [
|
|
|
87
88
|
listCmd,
|
|
88
89
|
newCmd,
|
|
89
90
|
playCmd,
|
|
91
|
+
puzzlesCmd,
|
|
90
92
|
quitCmd,
|
|
91
93
|
selectCmd,
|
|
92
94
|
suggestCmd,
|
|
@@ -190,8 +192,8 @@ const color = (text, def) => {
|
|
|
190
192
|
}
|
|
191
193
|
|
|
192
194
|
// Internal representations of files
|
|
193
|
-
const CONFIG = { id: '' }
|
|
194
|
-
const DATA = { games: {} }
|
|
195
|
+
const CONFIG = { id: '', puzzleLevel: 0 }
|
|
196
|
+
const DATA = { games: {}, puzzles: {} }
|
|
195
197
|
|
|
196
198
|
// CLI context object for plugins
|
|
197
199
|
const CLI = {
|
|
@@ -215,6 +217,10 @@ const CLI = {
|
|
|
215
217
|
get ID() {
|
|
216
218
|
return CONFIG.id
|
|
217
219
|
},
|
|
220
|
+
// The current puzzle level
|
|
221
|
+
get puzzleLevel() {
|
|
222
|
+
return CONFIG.puzzleLevel
|
|
223
|
+
},
|
|
218
224
|
|
|
219
225
|
// Provide access to the current Trax object
|
|
220
226
|
TRAX: new Trax(),
|
|
@@ -231,6 +237,7 @@ const CLI = {
|
|
|
231
237
|
variable: 123,
|
|
232
238
|
optional: 110,
|
|
233
239
|
short: 153,
|
|
240
|
+
success: 35,
|
|
234
241
|
white: 250,
|
|
235
242
|
},
|
|
236
243
|
|
|
@@ -298,11 +305,57 @@ const CLI = {
|
|
|
298
305
|
return id
|
|
299
306
|
},
|
|
300
307
|
|
|
308
|
+
newPuzzle(puzzle) {
|
|
309
|
+
// Starts a new puzzle
|
|
310
|
+
const id = newId(DATA)
|
|
311
|
+
const trax = new Trax(puzzle.game, puzzle.notation, 'cli')
|
|
312
|
+
const players = ['challenger', 'puzzlebot']
|
|
313
|
+
DATA.games[id] = {
|
|
314
|
+
id,
|
|
315
|
+
puzzle: puzzle.id,
|
|
316
|
+
rules: trax.rules,
|
|
317
|
+
name: trax.name,
|
|
318
|
+
turn: trax.turn,
|
|
319
|
+
max: puzzle.max,
|
|
320
|
+
players: trax.turn === 1 ? players : players.reverse(),
|
|
321
|
+
moves: trax.moves.join(' '),
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
if (DATA.puzzles[puzzle.id]) {
|
|
325
|
+
DATA.puzzles[puzzle.id].attempts += 1
|
|
326
|
+
} else {
|
|
327
|
+
DATA.puzzles[puzzle.id] = { attempts: 1, solved: 0 }
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
CONFIG.id = id
|
|
331
|
+
CONFIG.puzzleLevel = puzzle.level
|
|
332
|
+
CLI.TRAX = trax
|
|
333
|
+
|
|
334
|
+
saveConfig()
|
|
335
|
+
saveData()
|
|
336
|
+
|
|
337
|
+
return id
|
|
338
|
+
},
|
|
339
|
+
|
|
301
340
|
out(text) {
|
|
302
341
|
// Prints to stdout
|
|
303
342
|
console.info(text + tty.color(''))
|
|
304
343
|
},
|
|
305
344
|
|
|
345
|
+
plural(n, noun, nouns) {
|
|
346
|
+
return String(n) + ' ' + (Math.abs(n) === 1 ? noun : nouns || noun + 's')
|
|
347
|
+
},
|
|
348
|
+
|
|
349
|
+
puzzle(id) {
|
|
350
|
+
return DATA.puzzles?.[id] || { attempts: 0, solved: 0 }
|
|
351
|
+
},
|
|
352
|
+
|
|
353
|
+
puzzleSolved(id) {
|
|
354
|
+
DATA.puzzles = DATA.puzzles || {}
|
|
355
|
+
DATA.puzzles[id] = DATA.puzzles[id] || { attempts: 1, solved: 0 }
|
|
356
|
+
DATA.puzzles[id].solved += 1
|
|
357
|
+
},
|
|
358
|
+
|
|
306
359
|
resolveCommand(word) {
|
|
307
360
|
// Determine if an entered word is a valid command.
|
|
308
361
|
// Returns the command if a single match is found, an array if multiple
|
package/src/cmds/analyze.js
CHANGED
|
@@ -22,7 +22,7 @@ const notationRx = /^[@a-z]+\d+[bps/\\+]$/i
|
|
|
22
22
|
|
|
23
23
|
export const analyzeCmd = {
|
|
24
24
|
name: 'analyze',
|
|
25
|
-
args: '[
|
|
25
|
+
args: '[move]',
|
|
26
26
|
comp: '<play>',
|
|
27
27
|
desc: 'analyze a position',
|
|
28
28
|
help: [
|
|
@@ -44,7 +44,7 @@ const listThreats = (threats, detailed) => {
|
|
|
44
44
|
const items = []
|
|
45
45
|
if (detailed) {
|
|
46
46
|
for (const item of threats[level]) {
|
|
47
|
-
items.push('[' + item.at + ':' + item.match + ']')
|
|
47
|
+
items.push('[' + item.at + ':' + item.threat + ':' + item.match + ']')
|
|
48
48
|
}
|
|
49
49
|
}
|
|
50
50
|
|
|
@@ -22,6 +22,7 @@ import envPaths from 'env-paths'
|
|
|
22
22
|
import makeDir from 'make-dir'
|
|
23
23
|
|
|
24
24
|
import { Trax } from '@slugbugblue/trax'
|
|
25
|
+
import { puzzles, sources } from '@slugbugblue/trax/puzzles'
|
|
25
26
|
|
|
26
27
|
const paths = envPaths('trax', { suffix: '' })
|
|
27
28
|
const PATHS = {}
|
|
@@ -30,7 +31,12 @@ const PATHS = {}
|
|
|
30
31
|
let cli = {}
|
|
31
32
|
|
|
32
33
|
const fsOuch = (fsError) => {
|
|
33
|
-
cli.out
|
|
34
|
+
if (cli.out) {
|
|
35
|
+
cli.out(fsError, cli.COLORS.fatal)
|
|
36
|
+
} else {
|
|
37
|
+
console.log(fsError)
|
|
38
|
+
}
|
|
39
|
+
|
|
34
40
|
throw fsError
|
|
35
41
|
}
|
|
36
42
|
|
|
@@ -72,7 +78,12 @@ export const getFile = async (filetype, filename, variable, decode) => {
|
|
|
72
78
|
}
|
|
73
79
|
} catch (fsError) {
|
|
74
80
|
if (fsError instanceof TypeError || fsError instanceof SyntaxError) {
|
|
75
|
-
|
|
81
|
+
const error = `Unable to read ${filetype} file ${path}/${filename}`
|
|
82
|
+
if (cli.error) {
|
|
83
|
+
cli.error(error)
|
|
84
|
+
} else {
|
|
85
|
+
console.log(error)
|
|
86
|
+
}
|
|
76
87
|
} else if (fsError.code !== 'ENOENT') {
|
|
77
88
|
fsOuch(fsError)
|
|
78
89
|
}
|
|
@@ -200,6 +211,25 @@ exportCmd.fn = (CLI, id, filename) => {
|
|
|
200
211
|
content += ' vs '
|
|
201
212
|
content += game.players?.[1] || 'black'
|
|
202
213
|
content += '\n; @slugbugblue/trax cli.js v' + CLI.version + '\n'
|
|
214
|
+
if (game.puzzle) {
|
|
215
|
+
const puzzle = puzzles.find((p) => p.id === game.puzzle)
|
|
216
|
+
if (puzzle) {
|
|
217
|
+
const source = sources[puzzle.src]
|
|
218
|
+
content += '; Puzzle ' + game.puzzle
|
|
219
|
+
if (source) {
|
|
220
|
+
content += ' provided courtesy of ' + source.name
|
|
221
|
+
if (source.url) content += ' (' + source.url + ')'
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
if (puzzle.title) content += '\n; ' + puzzle.title
|
|
225
|
+
if (puzzle.desc) content += '\n; ' + puzzle.desc
|
|
226
|
+
content += '\n; Starting position ' + puzzle.notation + '\n; '
|
|
227
|
+
const players = [0, 'white', 'black']
|
|
228
|
+
content += players[puzzle.player] + ' to win by move '
|
|
229
|
+
content += String(puzzle.max) + '\n'
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
203
233
|
content += trax.notation + '\n'
|
|
204
234
|
if (filename) filename = filename.replace(/[^ a-z\d.~/]/g, '')
|
|
205
235
|
if (filename) {
|