@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 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
- # Or install globally to use the trax CLI more easily
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
- # Or create a docker image
34
- docker build -t trax .
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 #1 active, enter `select 1` or `select #1` or
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
- The CLI uses standard Windows and Linux [XDG][xdg] file locations as provided by
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. Note that the threats are
108
- not checked for faultiness at this time. In other words, there may be a faulty L
109
- listed as a threat even if it is not currently possible to activate the L
110
- successfully.
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 fauly.
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@slugbugblue/trax",
3
- "version": "0.20.0",
3
+ "version": "0.21.0",
4
4
  "description": "Trax game engine and friends",
5
5
  "keywords": [
6
6
  "trax",
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
- // Make parseInt a little shorter
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
- // Analysis class
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
- #scores = {}
300
+ /** @type Scores */
301
+ #scores = undefined
302
+ /** @type FoundThreatsCollection */
282
303
  #threats = undefined
283
- #faulty = { b: [], w: [] }
284
- #partial = false
304
+ #validated = false
285
305
 
286
- constructor(game, partial = false) {
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
- this.#partial = Boolean(partial)
318
+ /** @type FaultyThreats */
319
+ this.faulty = { b: [], w: [] }
320
+ this.#validated = false
298
321
  if (this.#moves.length === 0 || game.over) {
299
- this.#edge = { b: '', w: '', rawEdge: [] }
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
- if (this.#scores[color] === undefined) {
430
- const { game } = this
431
- if (game.over) {
432
- this.#scores[game.color] = Number.POSITIVE_INFINITY
433
- this.#scores[Trax.other(game.color)] = Number.NEGATIVE_INFINITY
434
- return this.#scores[color]
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
- const [currentPlayer, threatLevel] = this.#activeScore()
438
- const [nextPlayer, multiplier] = this.#inactiveScore(color, threatLevel)
459
+ const [currentPlayer, threatLevel] = this.#activeScore()
460
+ const [nextPlayer, multiplier] = this.#inactiveScore(color, threatLevel)
439
461
 
440
- let score = Math.floor(currentPlayer * multiplier) - nextPlayer
441
- if (score && game.color !== color) score *= -1
462
+ let score = Math.floor(currentPlayer * multiplier) - nextPlayer
463
+ if (score && game.color !== color) score *= -1
442
464
 
443
- this.#scores[color] = score
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
- return {
451
- w: this.#score('w'),
452
- b: this.#score('b'),
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
- // Space type
461
- //
462
- // determine which type of space we have:
463
- // c = in a cave but has no restrictions (converted to n in edge)
464
- // C = a cave where some moves are not allowed (converted to c in edge)
465
- // n = normal space
466
- // x = no tile can be played here
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
- // Trace the edge of the playing area
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
- #validateThreats() {
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.#threats[bw]
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.#faulty[bw].push(threat)
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.#threats[color])
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.#threats[other])
673
- const higher = threatCount(this.#threats[color], level - 1)
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, true)
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 += 'w' in this.#scores ? sub(this.#scores) : tbd
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, partial = false) => {
745
- const id = (partial ? 'y' : 'n') + game.rules + game.normalized
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, partial)
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
- const best = analyzed[0] || { move: '', score: 0 }
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, partial = false) => {
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, partial)
802
- analyzed[unique] = { move, score: analysis.scores[color], analysis }
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
  }
@@ -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 ('start'.startsWith(action) || 'new'.startsWith(action)) {
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 ('list'.startsWith(x) || 'ls'.startsWith(x)) {
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.20.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
package/.husky/pre-commit DELETED
@@ -1,5 +0,0 @@
1
- #!/usr/bin/env sh
2
- . "$(dirname -- "$0")/_/husky.sh"
3
-
4
- npm outdated
5
- npm test