@slugbugblue/trax 0.20.0 → 0.21.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 +9 -0
- package/README.md +31 -10
- package/docs/analyst.md +14 -5
- package/package.json +1 -1
- package/src/analyst.js +114 -63
- package/src/cmds/puzzles.js +17 -6
- package/src/cmds/view.js +5 -1
- package/src/puzzles.js +29 -0
- package/src/types.d.ts +6 -0
- package/src/version.js +1 -1
- package/.gitlab-ci.yml +0 -90
- package/.husky/pre-commit +0 -5
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
# @slugbugblue/trax
|
|
2
2
|
|
|
3
|
+
## 0.21.0 -2023-03-12
|
|
4
|
+
|
|
5
|
+
- Update the analysis engine to be more lazy
|
|
6
|
+
|
|
7
|
+
- Call `validateThreats()` to make it work harder
|
|
8
|
+
- This also increases LRU cache hits
|
|
9
|
+
- This makes the suggestions much faster because it skips validations for
|
|
10
|
+
low-scoring moves
|
|
11
|
+
|
|
3
12
|
## 0.20.0 - 2023-02-11
|
|
4
13
|
|
|
5
14
|
- Provide timing feedback on the puzzlebot's actions
|
package/README.md
CHANGED
|
@@ -23,15 +23,26 @@ changes to the engine should meet the same standard.
|
|
|
23
23
|
|
|
24
24
|
## Installation
|
|
25
25
|
|
|
26
|
+
Installation can be done in one of three ways, each for a slightly different
|
|
27
|
+
purpose.
|
|
28
|
+
|
|
29
|
+
If you want to use this code in your own javascript project:
|
|
30
|
+
|
|
26
31
|
```bash
|
|
27
|
-
# Install into a javascript project
|
|
28
32
|
npm install @slugbugblue/trax
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
If you want to use the trax CLI on your computer:
|
|
29
36
|
|
|
30
|
-
|
|
37
|
+
```bash
|
|
31
38
|
npm install -g @slugbugblue/trax
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
If you don't have node installed and want to use the CLI as a docker container:
|
|
32
42
|
|
|
33
|
-
|
|
34
|
-
|
|
43
|
+
```bash
|
|
44
|
+
git clone https://gitlab.com/slugbugblue/trax.git ~/trax
|
|
45
|
+
docker build -t trax ~/trax
|
|
35
46
|
```
|
|
36
47
|
|
|
37
48
|
## CLI Usage
|
|
@@ -75,7 +86,7 @@ entered `play 1. @0/`.
|
|
|
75
86
|
Multiple games can be started at once, and the CLI will remember the currently
|
|
76
87
|
active game and apply future actions to that game. Each game will retain its
|
|
77
88
|
`#id` number until it is deleted, and you can use that number to switch between
|
|
78
|
-
games. For example, to make game
|
|
89
|
+
games. For example, to make game \#1 active, enter `select 1` or `select #1` or
|
|
79
90
|
simply `#1` at the interactive prompt.
|
|
80
91
|
|
|
81
92
|
For ease of working at the command line, the symbols used as the last character
|
|
@@ -84,11 +95,7 @@ symbol (`/`), `b` for backslash (`\`), and `p` for plus (`+`). While there is no
|
|
|
84
95
|
need to escape at the interactive prompt, these substitutions will be avilable
|
|
85
96
|
there as well.
|
|
86
97
|
|
|
87
|
-
|
|
88
|
-
the [env-paths library][env-path], with current uses being the `config` and
|
|
89
|
-
`data` folders.
|
|
90
|
-
|
|
91
|
-
Examples:
|
|
98
|
+
### Examples
|
|
92
99
|
|
|
93
100
|
```text
|
|
94
101
|
# Start a new Trax game with Chad playing as white
|
|
@@ -115,6 +122,18 @@ Examples:
|
|
|
115
122
|
> trax ls
|
|
116
123
|
```
|
|
117
124
|
|
|
125
|
+
### Environment variables
|
|
126
|
+
|
|
127
|
+
The CLI uses standard Windows and Linux [XDG][xdg] file locations as provided by
|
|
128
|
+
the [env-paths library][env-path], with current uses being user-specific
|
|
129
|
+
`config` (`XDG_CONFIG_HOME`) and `data` (`XDG_DATA_HOME`) folders.
|
|
130
|
+
|
|
131
|
+
In addition, the CLI recognizes several environment variables to determine the
|
|
132
|
+
glyphs to use in the user interface. Setting either `NERDFONT` or `NERDFONTS` to
|
|
133
|
+
any non-empty string will use characters from [Nerdfonts][nerfont]. Otherwise,
|
|
134
|
+
any of `POWERLINE`, `P9K_TTY`, or `P9K_SSH` will use characters from [Powerline
|
|
135
|
+
fonts][powerline].
|
|
136
|
+
|
|
118
137
|
## API Usage
|
|
119
138
|
|
|
120
139
|
### Trax engine
|
|
@@ -243,6 +262,8 @@ David Smith and heirs, and are not to be used without permission.
|
|
|
243
262
|
[env-path]: https://www.npmjs.com/package/env-paths
|
|
244
263
|
[gnutrax]: https://gnutrax.com
|
|
245
264
|
[goldtoken]: https://goldtoken.com/
|
|
265
|
+
[nerdfont]: https://nerdfonts.com/
|
|
266
|
+
[powerline]: https://github.com/powerline/fonts
|
|
246
267
|
[puzzlebook]: https://gnutrax.com/book
|
|
247
268
|
[repo]: https://gitlab.com/slugbugblue/trax
|
|
248
269
|
[sbb]: https://slugbugblue.com/
|
package/docs/analyst.md
CHANGED
|
@@ -104,10 +104,12 @@ so that it appears as though all black tiles are white tiles.
|
|
|
104
104
|
Analyzes the existing threats for the current game position. Returns an object
|
|
105
105
|
with both `b` and `w` keys, where each is an object with zero or more "depth"
|
|
106
106
|
keys. Threats of depth 0 are corners or connectable-pairs. Threats of depth 1
|
|
107
|
-
are immediate attacks. Threats of depth 2 are Ls. etc.
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
107
|
+
are immediate attacks. Threats of depth 2 are Ls. etc.
|
|
108
|
+
|
|
109
|
+
Note that the threats are not checked for faultiness at this time. In other
|
|
110
|
+
words, there may be a faulty L listed as a threat even if it is not currently
|
|
111
|
+
possible to activate the L successfully. To ensure that the listed threats are
|
|
112
|
+
valid, use the `validateThreats()` method call.
|
|
111
113
|
|
|
112
114
|
The "depth" keys are arrays of the threats at that level, which means that a
|
|
113
115
|
simple count of the threats at each level can give a good first guess at the
|
|
@@ -125,7 +127,8 @@ following keys:
|
|
|
125
127
|
##### `faulty`
|
|
126
128
|
|
|
127
129
|
An object with two keys, `w` and `b`, each of which is a list of threats that
|
|
128
|
-
were found to be
|
|
130
|
+
were found to be faulty. This object will be empty if `validateThreats()` has
|
|
131
|
+
not yet been called.
|
|
129
132
|
|
|
130
133
|
Example data:
|
|
131
134
|
|
|
@@ -175,6 +178,12 @@ For any given location, determine the type of playable space. Returns one of:
|
|
|
175
178
|
- `C`: This location is in a cave, and certain plays at this position result in
|
|
176
179
|
illegal moves.
|
|
177
180
|
|
|
181
|
+
##### `validateThreats()`
|
|
182
|
+
|
|
183
|
+
Ensure that all of the threats that are present for each color are actually
|
|
184
|
+
valid. If it finds any faulty threats, it will update the `score` and populate
|
|
185
|
+
the `faulty` property with each threat that was found to be invalid.
|
|
186
|
+
|
|
178
187
|
### Suggestion class
|
|
179
188
|
|
|
180
189
|
Accessible from `suggest(game)`, this class is returned with several useful
|
package/package.json
CHANGED
package/src/analyst.js
CHANGED
|
@@ -12,7 +12,9 @@ import { threats } from '@slugbugblue/trax/threats'
|
|
|
12
12
|
|
|
13
13
|
const cache = new LRU(2750)
|
|
14
14
|
|
|
15
|
-
/** Helper function to generate a unique id for the analysis.
|
|
15
|
+
/** Helper function to generate a unique id for the analysis.
|
|
16
|
+
* @returns {string} a 6 character random string
|
|
17
|
+
*/
|
|
16
18
|
const id = () => {
|
|
17
19
|
const chars = 'abcdefghijklmnopqrstuvwxyz0123456789'
|
|
18
20
|
let code = ''
|
|
@@ -46,19 +48,35 @@ const withinX = (highest, tolerance) => (s) =>
|
|
|
46
48
|
*/
|
|
47
49
|
const randomDraw = (array) => array[Math.floor(Math.random() * array.length)]
|
|
48
50
|
|
|
49
|
-
|
|
51
|
+
/** Make parseInt a little shorter with decent defaults.
|
|
52
|
+
* @arg s {string} - A string representation of a decimal number
|
|
53
|
+
* @returns {number} the parsed number
|
|
54
|
+
*/
|
|
50
55
|
const int = (s) => Number.parseInt(s, 10)
|
|
51
56
|
|
|
57
|
+
/** The direction after a left turn. */
|
|
52
58
|
const leftTurn = { d: 'r', l: 'd', r: 'u', u: 'l' }
|
|
59
|
+
/** The direction after a right turn. */
|
|
53
60
|
const rightTurn = { d: 'l', l: 'u', r: 'd', u: 'r' }
|
|
61
|
+
/** Which way to follow. */
|
|
54
62
|
const follow = { d: 'right', l: 'bottom', r: 'top', u: 'left' }
|
|
55
63
|
|
|
64
|
+
/** Given a tile and a direction, return the color of the line going that
|
|
65
|
+
* direction.
|
|
66
|
+
* @arg tile {TileType} - the tile in question
|
|
67
|
+
* @arg dir {string} - direction u, d, l, or r
|
|
68
|
+
* @returns {Color}
|
|
69
|
+
*/
|
|
56
70
|
const edgeColor = (tile, dir) => {
|
|
57
71
|
return { d: 'ace', l: 'abd', r: 'bcf', u: 'def' }[dir].includes(tile)
|
|
58
72
|
? 'w'
|
|
59
73
|
: 'b'
|
|
60
74
|
}
|
|
61
75
|
|
|
76
|
+
/** Encode a number as a character.
|
|
77
|
+
* @arg n {number} - the number to encode
|
|
78
|
+
* @returns {string} the encoded number
|
|
79
|
+
*/
|
|
62
80
|
const encodeNumber = (n) => String.fromCodePoint(48 + n)
|
|
63
81
|
|
|
64
82
|
/** Explore a raw edge and update cave information.
|
|
@@ -267,7 +285,7 @@ const passiveAttack = (from) => passiveCount(from) > 1
|
|
|
267
285
|
*/
|
|
268
286
|
const underThreat = (from) => attackCount(from) + passiveCount(from) > 0
|
|
269
287
|
|
|
270
|
-
|
|
288
|
+
/** Trax position analysis. */
|
|
271
289
|
export class Analysis {
|
|
272
290
|
/** @type {TraxVariant} */
|
|
273
291
|
#rules = 'trax'
|
|
@@ -275,15 +293,18 @@ export class Analysis {
|
|
|
275
293
|
/** @type {string[]} - List of moves to arrive at this position */
|
|
276
294
|
#moves = []
|
|
277
295
|
#notation = ''
|
|
296
|
+
/** @type EdgeObject */
|
|
278
297
|
#edge = undefined
|
|
279
298
|
/** @type {Trax} */
|
|
280
299
|
#game
|
|
281
|
-
|
|
300
|
+
/** @type Scores */
|
|
301
|
+
#scores = undefined
|
|
302
|
+
/** @type FoundThreatsCollection */
|
|
282
303
|
#threats = undefined
|
|
283
|
-
#
|
|
284
|
-
#partial = false
|
|
304
|
+
#validated = false
|
|
285
305
|
|
|
286
|
-
|
|
306
|
+
/** @arg game {Trax} - an in-progress trax game */
|
|
307
|
+
constructor(game) {
|
|
287
308
|
// Make a copy of the game
|
|
288
309
|
this.#rules = game.rules
|
|
289
310
|
this.#moves = game.moves
|
|
@@ -294,15 +315,18 @@ export class Analysis {
|
|
|
294
315
|
w: (level) => this.threats.w[level]?.length || 0,
|
|
295
316
|
b: (level) => this.threats.b[level]?.length || 0,
|
|
296
317
|
}
|
|
297
|
-
|
|
318
|
+
/** @type FaultyThreats */
|
|
319
|
+
this.faulty = { b: [], w: [] }
|
|
320
|
+
this.#validated = false
|
|
298
321
|
if (this.#moves.length === 0 || game.over) {
|
|
299
|
-
|
|
322
|
+
/** @type EdgeObject */
|
|
323
|
+
this.#edge = { b: '', w: '', edge: [] }
|
|
300
324
|
this.#threats = { b: {}, w: {} }
|
|
301
325
|
}
|
|
302
326
|
}
|
|
303
327
|
|
|
328
|
+
/** A fresh copy of the game as presented on init. */
|
|
304
329
|
get game() {
|
|
305
|
-
// Return a fresh copy of the game as presented on init
|
|
306
330
|
if (this.#game.notation !== this.#notation) {
|
|
307
331
|
this.#game = new Trax(this.#rules, this.#moves, this.#id)
|
|
308
332
|
}
|
|
@@ -310,6 +334,7 @@ export class Analysis {
|
|
|
310
334
|
return this.#game
|
|
311
335
|
}
|
|
312
336
|
|
|
337
|
+
/** The edge of the playing area. */
|
|
313
338
|
get edge() {
|
|
314
339
|
if (!this.#edge) {
|
|
315
340
|
// Cache the edge
|
|
@@ -319,20 +344,12 @@ export class Analysis {
|
|
|
319
344
|
return this.#edge
|
|
320
345
|
}
|
|
321
346
|
|
|
322
|
-
/** Any faulty threats
|
|
323
|
-
* @returns {FaultyThreats}
|
|
324
|
-
*/
|
|
325
|
-
get faulty() {
|
|
326
|
-
return this.#faulty
|
|
327
|
-
}
|
|
328
|
-
|
|
329
347
|
/** All threats present in this position.
|
|
330
348
|
* @returns {FoundThreatsCollection}
|
|
331
349
|
*/
|
|
332
350
|
get threats() {
|
|
333
351
|
if (!this.#threats) {
|
|
334
352
|
this.#threats = { b: this.#applyThreats('b'), w: this.#applyThreats('w') }
|
|
335
|
-
if (!this.#partial) this.#validateThreats()
|
|
336
353
|
}
|
|
337
354
|
|
|
338
355
|
return this.#threats
|
|
@@ -425,45 +442,56 @@ export class Analysis {
|
|
|
425
442
|
return [score, multiplier]
|
|
426
443
|
}
|
|
427
444
|
|
|
445
|
+
/** Calculate the score for a player.
|
|
446
|
+
* @arg {Color} color - Color of the player
|
|
447
|
+
* @returns number
|
|
448
|
+
*/
|
|
428
449
|
#score(color) {
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
450
|
+
const { game } = this
|
|
451
|
+
if (game.over) {
|
|
452
|
+
return game.color === color
|
|
453
|
+
? Number.POSITIVE_INFINITY
|
|
454
|
+
: game.color === color
|
|
455
|
+
? Number.NEGATIVE_INFINITY
|
|
456
|
+
: -100_000
|
|
457
|
+
}
|
|
436
458
|
|
|
437
|
-
|
|
438
|
-
|
|
459
|
+
const [currentPlayer, threatLevel] = this.#activeScore()
|
|
460
|
+
const [nextPlayer, multiplier] = this.#inactiveScore(color, threatLevel)
|
|
439
461
|
|
|
440
|
-
|
|
441
|
-
|
|
462
|
+
let score = Math.floor(currentPlayer * multiplier) - nextPlayer
|
|
463
|
+
if (score && game.color !== color) score *= -1
|
|
442
464
|
|
|
443
|
-
|
|
444
|
-
}
|
|
445
|
-
|
|
446
|
-
return this.#scores[color]
|
|
465
|
+
return score
|
|
447
466
|
}
|
|
448
467
|
|
|
468
|
+
/** Get the scores for each player. */
|
|
449
469
|
get scores() {
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
470
|
+
if (this.#scores === undefined) {
|
|
471
|
+
this.#scores = {
|
|
472
|
+
w: this.#score('w'),
|
|
473
|
+
b: this.#score('b'),
|
|
474
|
+
}
|
|
453
475
|
}
|
|
476
|
+
|
|
477
|
+
return this.#scores
|
|
454
478
|
}
|
|
455
479
|
|
|
480
|
+
/** Get the score for the active player. */
|
|
456
481
|
get score() {
|
|
457
482
|
return this.scores[this.game.color]
|
|
458
483
|
}
|
|
459
484
|
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
485
|
+
/** Determine which type of space we have:
|
|
486
|
+
*
|
|
487
|
+
* determine which type of space we have:
|
|
488
|
+
* c = in a cave but has no restrictions (converted to n in edge)
|
|
489
|
+
* C = a cave where some moves are not allowed (converted to c in edge)
|
|
490
|
+
* n = normal space
|
|
491
|
+
* x = no tile can be played here
|
|
492
|
+
* @arg {Point} loc - The location of the space
|
|
493
|
+
* @returns string
|
|
494
|
+
*/
|
|
467
495
|
spaceType(loc) {
|
|
468
496
|
const { game } = this
|
|
469
497
|
|
|
@@ -498,7 +526,7 @@ export class Analysis {
|
|
|
498
526
|
return 'c'
|
|
499
527
|
}
|
|
500
528
|
|
|
501
|
-
|
|
529
|
+
/** Trace the edge of the playing area. */
|
|
502
530
|
#traceEdge() {
|
|
503
531
|
const { game } = this
|
|
504
532
|
/** @type {RawEdge} */
|
|
@@ -632,10 +660,12 @@ export class Analysis {
|
|
|
632
660
|
}
|
|
633
661
|
|
|
634
662
|
/** Clean up the list of matches, removing any false positives. */
|
|
635
|
-
|
|
663
|
+
validateThreats() {
|
|
664
|
+
if (this.#validated) return
|
|
665
|
+
let someFaulty = false
|
|
636
666
|
for (const bw of ['w', 'b']) {
|
|
637
667
|
/** @type ColorThreats */
|
|
638
|
-
const matches = this
|
|
668
|
+
const matches = this.threats[bw]
|
|
639
669
|
for (const key of Object.keys(matches)) {
|
|
640
670
|
const level = Number(key)
|
|
641
671
|
if (level < 2) continue
|
|
@@ -646,7 +676,8 @@ export class Analysis {
|
|
|
646
676
|
if (this.#canActivateThreat(threat, bw, level)) {
|
|
647
677
|
verified.push(threat)
|
|
648
678
|
} else {
|
|
649
|
-
this
|
|
679
|
+
this.faulty[bw].push(threat)
|
|
680
|
+
someFaulty = true
|
|
650
681
|
}
|
|
651
682
|
}
|
|
652
683
|
|
|
@@ -657,6 +688,9 @@ export class Analysis {
|
|
|
657
688
|
}
|
|
658
689
|
}
|
|
659
690
|
}
|
|
691
|
+
|
|
692
|
+
if (someFaulty) this.#scores = undefined
|
|
693
|
+
this.#validated = true
|
|
660
694
|
}
|
|
661
695
|
|
|
662
696
|
/** Determine if the given threat can be activated for this color
|
|
@@ -666,11 +700,11 @@ export class Analysis {
|
|
|
666
700
|
* @returns {boolean}
|
|
667
701
|
*/
|
|
668
702
|
#canActivateThreat(threat, color, level) {
|
|
669
|
-
const attacks = attackCount(this
|
|
703
|
+
const attacks = attackCount(this.threats[color])
|
|
670
704
|
const player = Trax.playerNumber(color)
|
|
671
705
|
const other = Trax.other(color)
|
|
672
|
-
const otherAttacks = attackCount(this
|
|
673
|
-
const higher = threatCount(this
|
|
706
|
+
const otherAttacks = attackCount(this.threats[other])
|
|
707
|
+
const higher = threatCount(this.threats[color], level - 1)
|
|
674
708
|
for (const loc of this.threatLocs(threat, color)) {
|
|
675
709
|
for (const type of this.game.possibleTiles(loc)) {
|
|
676
710
|
const game = new Trax(this.game.rules, this.game.moves)
|
|
@@ -684,7 +718,7 @@ export class Analysis {
|
|
|
684
718
|
// Otherwise, we cannot evaluate an ended game
|
|
685
719
|
if (game.over) continue
|
|
686
720
|
|
|
687
|
-
const { threats } = analyze(game
|
|
721
|
+
const { threats } = analyze(game)
|
|
688
722
|
|
|
689
723
|
// If we give our opponent an attack, this is a faulty move
|
|
690
724
|
const faulty = attackCount(threats[other]) - otherAttacks
|
|
@@ -724,12 +758,12 @@ export class Analysis {
|
|
|
724
758
|
custom += tbd
|
|
725
759
|
}
|
|
726
760
|
|
|
761
|
+
const score = this.#scores ? this.score : null
|
|
727
762
|
custom += ',\n threats: '
|
|
728
763
|
custom += this.#threats ? sub(this.#threats) : tbd
|
|
729
764
|
custom += ',\n scores: '
|
|
730
|
-
custom +=
|
|
765
|
+
custom += this.#scores ? sub(this.#scores) : tbd
|
|
731
766
|
custom += ',\n score: '
|
|
732
|
-
const score = 'w' in this.#scores ? this.score : null
|
|
733
767
|
custom += score === null ? tbd : style(score, 'number')
|
|
734
768
|
|
|
735
769
|
return custom + '\n}'
|
|
@@ -738,14 +772,13 @@ export class Analysis {
|
|
|
738
772
|
|
|
739
773
|
/** Analyze a game position.
|
|
740
774
|
* @arg {Trax} game - the position to analyze
|
|
741
|
-
* @arg {boolean} partial - if the position should only be partially analyzed
|
|
742
775
|
* @returns {Analysis}
|
|
743
776
|
*/
|
|
744
|
-
export const analyze = (game
|
|
745
|
-
const id =
|
|
777
|
+
export const analyze = (game) => {
|
|
778
|
+
const id = game.rules + game.normalized
|
|
746
779
|
const cached = cache.get(id)
|
|
747
780
|
if (cached) return cached
|
|
748
|
-
const analysis = new Analysis(game
|
|
781
|
+
const analysis = new Analysis(game)
|
|
749
782
|
cache.set(id, analysis)
|
|
750
783
|
return analysis
|
|
751
784
|
}
|
|
@@ -759,9 +792,23 @@ export class Suggestion {
|
|
|
759
792
|
pick
|
|
760
793
|
ms = 0
|
|
761
794
|
|
|
762
|
-
constructor(analyzed, ms) {
|
|
795
|
+
constructor(color, analyzed, ms) {
|
|
763
796
|
this.all = analyzed.sort(byScoreDesc)
|
|
764
|
-
|
|
797
|
+
let best = analyzed[0] || { move: '', score: 0 }
|
|
798
|
+
let score = Number.NEGATIVE_INFINITY
|
|
799
|
+
|
|
800
|
+
while (score !== best.score) {
|
|
801
|
+
score = best.score
|
|
802
|
+
const options = analyzed.filter(withinX(best.score, 5))
|
|
803
|
+
for (const option of options) {
|
|
804
|
+
option.analysis.validateThreats()
|
|
805
|
+
option.score = option.analysis.scores[color]
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
analyzed.sort(byScoreDesc)
|
|
809
|
+
best = analyzed[0] || { move: '', score: 0 }
|
|
810
|
+
}
|
|
811
|
+
|
|
765
812
|
this.options = analyzed.filter(withinX(best.score, 5))
|
|
766
813
|
this.pick = randomDraw(this.options) || best
|
|
767
814
|
this.ms = ms
|
|
@@ -789,7 +836,7 @@ export class Suggestion {
|
|
|
789
836
|
}
|
|
790
837
|
}
|
|
791
838
|
|
|
792
|
-
export const suggest = (game
|
|
839
|
+
export const suggest = (game) => {
|
|
793
840
|
const start = Date.now()
|
|
794
841
|
const analyzed = {}
|
|
795
842
|
const { color, moves, rules } = game
|
|
@@ -798,10 +845,14 @@ export const suggest = (game, partial = false) => {
|
|
|
798
845
|
trax.play(move)
|
|
799
846
|
const unique = trax.normalized
|
|
800
847
|
if (!analyzed[unique]) {
|
|
801
|
-
const analysis = analyze(trax
|
|
802
|
-
analyzed[unique] = {
|
|
848
|
+
const analysis = analyze(trax)
|
|
849
|
+
analyzed[unique] = {
|
|
850
|
+
move,
|
|
851
|
+
analysis,
|
|
852
|
+
score: analysis.scores[color],
|
|
853
|
+
}
|
|
803
854
|
}
|
|
804
855
|
}
|
|
805
856
|
|
|
806
|
-
return new Suggestion(Object.values(analyzed), Date.now() - start)
|
|
857
|
+
return new Suggestion(color, Object.values(analyzed), Date.now() - start)
|
|
807
858
|
}
|
package/src/cmds/puzzles.js
CHANGED
|
@@ -17,11 +17,14 @@ for (const puzzle of puzzles) {
|
|
|
17
17
|
const validLevels = levels.slice(0, maxLevel + 1)
|
|
18
18
|
const completions = [...validLevels, ...puzzleIds].join('|')
|
|
19
19
|
|
|
20
|
+
const listAliases = ['display', 'list', 'ls', 'show', 'view']
|
|
21
|
+
const startAliases = ['new', 'start']
|
|
22
|
+
|
|
20
23
|
export const puzzlesCmd = {
|
|
21
24
|
name: 'puzzles',
|
|
22
25
|
args: '["list"|"start"] [level|puzzle]',
|
|
23
|
-
comp: 'list|ls|new|show|start|view ' + completions,
|
|
24
|
-
desc: 'play puzzles',
|
|
26
|
+
comp: 'display|list|ls|new|show|start|view ' + completions,
|
|
27
|
+
desc: 'view and play puzzles',
|
|
25
28
|
help: [
|
|
26
29
|
'Practice your skills with puzzles. Use "list" to see the puzzle',
|
|
27
30
|
'categories, or "start" to play a puzzle.',
|
|
@@ -33,7 +36,7 @@ const an = (word = '') => {
|
|
|
33
36
|
return a + ' ' + word
|
|
34
37
|
}
|
|
35
38
|
|
|
36
|
-
const showPuzzle = (CLI, id) => {
|
|
39
|
+
const showPuzzle = (CLI, id, started = false) => {
|
|
37
40
|
let puzzle = puzzles.find((p) => p.id === id)
|
|
38
41
|
if (!puzzle) {
|
|
39
42
|
const possibles = puzzles.filter((p) => p.id.includes(id))
|
|
@@ -67,6 +70,14 @@ const showPuzzle = (CLI, id) => {
|
|
|
67
70
|
}
|
|
68
71
|
|
|
69
72
|
CLI.out(out)
|
|
73
|
+
|
|
74
|
+
if (started) return
|
|
75
|
+
|
|
76
|
+
CLI.out(
|
|
77
|
+
CLI.color.short(
|
|
78
|
+
`Want to try this puzzle? Enter: "puzzles start ${puzzle.id}"`,
|
|
79
|
+
),
|
|
80
|
+
)
|
|
70
81
|
}
|
|
71
82
|
|
|
72
83
|
const listPuzzles = (CLI) => {
|
|
@@ -126,7 +137,7 @@ const listLevel = (CLI, levelName) => {
|
|
|
126
137
|
const startPuzzle = (CLI, puzzle) => {
|
|
127
138
|
const id = CLI.newPuzzle(puzzle)
|
|
128
139
|
CLI.out(CLI.color(`Started puzzle ${puzzle.id} as game #${id}`))
|
|
129
|
-
showPuzzle(CLI, puzzle.id)
|
|
140
|
+
showPuzzle(CLI, puzzle.id, true)
|
|
130
141
|
}
|
|
131
142
|
|
|
132
143
|
const startNextPuzzle = (CLI, level) => {
|
|
@@ -143,7 +154,7 @@ const startNextPuzzle = (CLI, level) => {
|
|
|
143
154
|
|
|
144
155
|
puzzlesCmd.fn = (CLI, action, id) => {
|
|
145
156
|
action = action?.toLowerCase() ?? 'list'
|
|
146
|
-
if (
|
|
157
|
+
if (startAliases.some((alias) => alias.startsWith(action))) {
|
|
147
158
|
if (!id) return startNextPuzzle(CLI)
|
|
148
159
|
|
|
149
160
|
const puzzle = puzzles.find((p) => p.id === id)
|
|
@@ -166,7 +177,7 @@ puzzlesCmd.fn = (CLI, action, id) => {
|
|
|
166
177
|
if (action) {
|
|
167
178
|
const x = id || action
|
|
168
179
|
if (validLevels.includes(x)) return listLevel(CLI, x)
|
|
169
|
-
if (
|
|
180
|
+
if (listAliases.some((alias) => alias.startsWith(x))) {
|
|
170
181
|
return listPuzzles(CLI)
|
|
171
182
|
}
|
|
172
183
|
|
package/src/cmds/view.js
CHANGED
|
@@ -27,11 +27,15 @@ export const viewCmd = {
|
|
|
27
27
|
],
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
-
viewCmd.fn = (CLI, move) => {
|
|
30
|
+
viewCmd.fn = (CLI, move, ...extra) => {
|
|
31
31
|
if (!CLI.GAME?.id) {
|
|
32
32
|
return CLI.error('No active game. Type "new" to start a game.')
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
+
if (move?.length > 1 && 'puzzles'.startsWith(move)) {
|
|
36
|
+
return CLI.do('puzzles', 'list', ...extra)
|
|
37
|
+
}
|
|
38
|
+
|
|
35
39
|
const trax = CLI.TRAX
|
|
36
40
|
|
|
37
41
|
let show = Number.POSITIVE_INFINITY
|
package/src/puzzles.js
CHANGED
|
@@ -30,6 +30,12 @@ export const sources = {
|
|
|
30
30
|
license: 'CC BY 4.0',
|
|
31
31
|
licenseUrl: 'https://creativecommons.org/licenses/by/4.0/',
|
|
32
32
|
},
|
|
33
|
+
book2: {
|
|
34
|
+
name: 'Martin M. S. Pedersen',
|
|
35
|
+
copyright: '2023',
|
|
36
|
+
license: 'CC BY 4.0',
|
|
37
|
+
licenseUrl: 'https://creativecommons.org/licenses/by/4.0/',
|
|
38
|
+
},
|
|
33
39
|
}
|
|
34
40
|
|
|
35
41
|
/** @type {Puzzle[]} */
|
|
@@ -1200,6 +1206,29 @@ export const puzzles = [
|
|
|
1200
1206
|
max: 16,
|
|
1201
1207
|
hint: 'D1+',
|
|
1202
1208
|
},
|
|
1209
|
+
{
|
|
1210
|
+
id: 'book2.132',
|
|
1211
|
+
src: 'book2',
|
|
1212
|
+
game: 'traxloop',
|
|
1213
|
+
notation: '@0+ B1+ C1/ @1\\ D2\\ E2+ D3\\ D4\\',
|
|
1214
|
+
icon: 'aeec:2fb:2ac:2fd',
|
|
1215
|
+
level: 4,
|
|
1216
|
+
player: 1,
|
|
1217
|
+
max: 15,
|
|
1218
|
+
hint: 'C3\\',
|
|
1219
|
+
},
|
|
1220
|
+
{
|
|
1221
|
+
id: 'book2.148',
|
|
1222
|
+
src: 'book2',
|
|
1223
|
+
game: 'traxloop',
|
|
1224
|
+
notation:
|
|
1225
|
+
'@0/ B1\\ A2/ A0/ C1\\ @2/ @2+ A0\\ @2/ @2/ @2/ B4+ A5+ A1+ E0+ E0/ E0/',
|
|
1226
|
+
icon: '3ca:3df:0cdcba:efafaf:dcdcdcaf:afaecdf:fbd1c:b',
|
|
1227
|
+
level: 4,
|
|
1228
|
+
player: 2,
|
|
1229
|
+
max: 24,
|
|
1230
|
+
hint: 'B2\\',
|
|
1231
|
+
},
|
|
1203
1232
|
]
|
|
1204
1233
|
|
|
1205
1234
|
puzzles.sort(
|
package/src/types.d.ts
CHANGED
|
@@ -76,6 +76,12 @@ type ColorThreats = Record<string, FoundThreat[]>
|
|
|
76
76
|
/** All the threats found for both the white and black player. */
|
|
77
77
|
type FoundThreatsCollection = { b: ColorThreats; w: ColorThreats }
|
|
78
78
|
|
|
79
|
+
/** Scores for each player. */
|
|
80
|
+
type Scores = {
|
|
81
|
+
b: number
|
|
82
|
+
w: number
|
|
83
|
+
}
|
|
84
|
+
|
|
79
85
|
/** A note with a move number */
|
|
80
86
|
type GameNote = {
|
|
81
87
|
move: number
|
package/src/version.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// Generated by genversion.
|
|
2
|
-
export const version = '0.
|
|
2
|
+
export const version = '0.21.0'
|
package/.gitlab-ci.yml
DELETED
|
@@ -1,90 +0,0 @@
|
|
|
1
|
-
include:
|
|
2
|
-
- template: Security/SAST.gitlab-ci.yml
|
|
3
|
-
- template: Security/Secret-Detection.gitlab-ci.yml
|
|
4
|
-
|
|
5
|
-
stages:
|
|
6
|
-
- test
|
|
7
|
-
- sast
|
|
8
|
-
- benchmark
|
|
9
|
-
- release
|
|
10
|
-
|
|
11
|
-
sast:
|
|
12
|
-
stage: sast
|
|
13
|
-
|
|
14
|
-
nodejs-scan-sast:
|
|
15
|
-
allow_failure: false
|
|
16
|
-
rules:
|
|
17
|
-
- if: "$SAST_DISABLED"
|
|
18
|
-
when: never
|
|
19
|
-
- if: "$SAST_EXCLUDED_ANALYZERS =~ /nodejs-scan/"
|
|
20
|
-
when: never
|
|
21
|
-
- if: "$CI_COMMIT_BRANCH == 'main'"
|
|
22
|
-
exists:
|
|
23
|
-
- "**/package.json"
|
|
24
|
-
|
|
25
|
-
semgrep-sast:
|
|
26
|
-
allow_failure: false
|
|
27
|
-
rules:
|
|
28
|
-
- if: "$SAST_DISABLED"
|
|
29
|
-
when: never
|
|
30
|
-
- if: "$SAST_EXCLUDED_ANALYZERS =~ /semgrep/"
|
|
31
|
-
when: never
|
|
32
|
-
- if: "$CI_COMMIT_BRANCH == 'main'"
|
|
33
|
-
exists:
|
|
34
|
-
- "**/*.js"
|
|
35
|
-
|
|
36
|
-
secret_detection:
|
|
37
|
-
allow_failure: false
|
|
38
|
-
|
|
39
|
-
code-coverage:
|
|
40
|
-
stage: test
|
|
41
|
-
image: node:lts-alpine
|
|
42
|
-
cache:
|
|
43
|
-
key: lts
|
|
44
|
-
paths:
|
|
45
|
-
- node_modules/
|
|
46
|
-
script:
|
|
47
|
-
- node --version
|
|
48
|
-
- npm install
|
|
49
|
-
- npm test
|
|
50
|
-
coverage: /^All files\s*\|\s*([\d.]+)/
|
|
51
|
-
|
|
52
|
-
node16:
|
|
53
|
-
stage: test
|
|
54
|
-
image: node:16.15.0-alpine
|
|
55
|
-
cache:
|
|
56
|
-
key: node16
|
|
57
|
-
paths:
|
|
58
|
-
- node_modules/
|
|
59
|
-
script:
|
|
60
|
-
- node --version
|
|
61
|
-
- npm install
|
|
62
|
-
- npm test
|
|
63
|
-
|
|
64
|
-
benchmark:
|
|
65
|
-
stage: benchmark
|
|
66
|
-
image: node:lts-alpine
|
|
67
|
-
cache:
|
|
68
|
-
key: lts
|
|
69
|
-
paths:
|
|
70
|
-
- node_modules/
|
|
71
|
-
rules:
|
|
72
|
-
- if: $CI_COMMIT_TAG =~ /^v\d\+\.\d+\.\d+$/
|
|
73
|
-
script:
|
|
74
|
-
- npm run benchmark
|
|
75
|
-
- awk '/^## /{p++}p==1;p==2{exit}' CHANGELOG.md > RELEASE.$CI_COMMIT_TAG.md
|
|
76
|
-
artifacts:
|
|
77
|
-
paths:
|
|
78
|
-
- RELEASE.$CI_COMMIT_TAG.md
|
|
79
|
-
expire_in: 1 day
|
|
80
|
-
|
|
81
|
-
release:
|
|
82
|
-
stage: release
|
|
83
|
-
image: registry.gitlab.com/gitlab-org/release-cli:latest
|
|
84
|
-
rules:
|
|
85
|
-
- if: $CI_COMMIT_TAG =~ /^v\d\+\.\d+\.\d+$/
|
|
86
|
-
script:
|
|
87
|
-
- echo "Releasing $CI_COMMIT_TAG"
|
|
88
|
-
release:
|
|
89
|
-
tag_name: $CI_COMMIT_TAG
|
|
90
|
-
description: ./RELEASE.$CI_COMMIT_TAG.md
|