@slugbugblue/trax 0.21.0 → 0.23.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/src/engine.js CHANGED
@@ -1,37 +1,17 @@
1
- /* Copyright 2019-2022 Chad Transtrum
2
- *
3
- * Licensed under the Apache License, Version 2.0 (the "License");
4
- * you may not use this file except in compliance with the License.
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
+ /** Trax engine.
2
+ * @copyright 2019-2022
3
+ * @author Chad Transtrum <chad@transtrum.net>
4
+ * @license Apache-2.0
14
5
  */
15
6
 
16
- // Trax internals. Woot.
17
-
18
- // Tile type names are determined by listing the line color at each edge,
19
- // starting from the top and going clockwise, and then sorted alphabetically
20
- // and given a single letter name, so 'bbww' becomes 'a', which gives us six
21
- // different tile names: a-f
7
+ import { Point } from '@slugbugblue/point'
22
8
 
23
- import { Point } from '@slugbugblue/trax/point'
24
-
25
- // Type definitions
26
9
  // Fun trax helper constants
27
10
 
28
11
  const zero = new Point(0, 0)
29
12
  const moveNumberRegex = /^(\d+)[.):]?$/
30
13
  const notationRegex = /^([@a-z]+)(\d+)([/\\+])$/i
31
14
 
32
- /** @type ValidTiles[] */
33
- const tileTypes = ['a', 'b', 'c', 'd', 'e', 'f']
34
-
35
15
  /** Tiles are represented by the letters a-f.
36
16
  * a b c d e f
37
17
  * +--#--+ +--#--+ +--#--+ +--o--+ +--o--+ +--o--+
@@ -40,7 +20,14 @@ const tileTypes = ['a', 'b', 'c', 'd', 'e', 'f']
40
20
  * | o | | # | | o | | # | | o | | # |
41
21
  * +--o--+ +--#--+ +--o--+ +--#--+ +--o--+ +--#--+
42
22
  *
43
- * Slashes are represented by / \ or +.
23
+ * @readonly
24
+ * @type ValidTiles[]
25
+ * */
26
+ const tileTypes = ['a', 'b', 'c', 'd', 'e', 'f']
27
+
28
+ /** Slashes indicate the direction the tile is placed. A forward slash or a
29
+ * backslash indicate the tile is a "curve" and the plus sign indicates the
30
+ * tile is a "straight".
44
31
  * @readonly
45
32
  * @type {Record<ValidTiles, Slash>}
46
33
  */
@@ -201,10 +188,10 @@ export class Trax {
201
188
  // Trax class constructor
202
189
  /** Create a new Trax game
203
190
  * @arg {TraxVariant} [rules='trax'] - the variant to play
204
- * @arg {string|string[]} [moves=[]] - the initial moves to pre-play
191
+ * @arg {string|string[]} [moves=''] - the initial moves to pre-play
205
192
  * @arg {string} [id='trax'] - an id used to differentiate tiles from multiple games
206
193
  */
207
- constructor(rules = 'trax', moves = [], id = 'trax') {
194
+ constructor(rules = 'trax', moves = '', id = 'trax') {
208
195
  if (!Trax.variants.has(rules)) rules = 'trax'
209
196
  this.id = String(id)
210
197
  this.rules = rules
@@ -252,7 +239,6 @@ export class Trax {
252
239
  /** Restore a previously saved position.
253
240
  * @arg {SaveState} saved - the previously saved state
254
241
  * @see save for saving the state
255
- * @returns {void}
256
242
  */
257
243
  restore(saved) {
258
244
  // So we need to be able to restore it when we are done
@@ -316,6 +302,7 @@ export class Trax {
316
302
  */
317
303
  addTile(type, loc) {
318
304
  const id = this.tileId(loc)
305
+ /** @type {Tile} */
319
306
  const tile = { type, loc, id, move: this.move, seq: this.count }
320
307
  if (type === 'x') {
321
308
  this.invalid = true // Invalid tile is being played
@@ -339,7 +326,7 @@ export class Trax {
339
326
  }
340
327
 
341
328
  /** Is this tile valid?
342
- * @type {(type: string, loc: Point) => type is TileType}
329
+ * @type {(type: string, loc: Point) => type is ValidTiles}
343
330
  */
344
331
  validTile(type, loc) {
345
332
  if (this.count === 0 && loc.x === 0 && loc.y === 0) {
@@ -382,6 +369,10 @@ export class Trax {
382
369
  return this.right - this.left + 1
383
370
  }
384
371
 
372
+ /** Determine if a location is valid to play in.
373
+ * @arg {Point} loc - the location to check
374
+ * @returns {boolean}
375
+ */
385
376
  validLocation(loc) {
386
377
  if (this.over) return false
387
378
  if (this.tileAt(loc)) return false
@@ -403,26 +394,31 @@ export class Trax {
403
394
  return false
404
395
  }
405
396
 
397
+ /** Find all possible locations to play in. Note that these are not
398
+ * necessarily valid locations, just empty ones that border existing tiles.
399
+ */
406
400
  possibleLocations() {
407
401
  if (this.count === 0) return [zero]
408
402
  /** @type Record<TileId, Point> */
409
403
  const possibles = {}
410
404
 
411
405
  for (const tile of Object.values(this.tiles)) {
412
- tile.loc.around.map((loc) => {
406
+ for (const loc of tile.loc.around) {
413
407
  const id = this.tileId(loc)
414
408
  if (!(id in this.tiles)) possibles[id] = loc
415
- return loc
416
- })
409
+ }
417
410
  }
418
411
 
419
- // Get rid of all the real tiles and return all the maybes
420
412
  return Object.values(possibles)
421
413
  }
422
414
 
415
+ /** Find all possible moves as a list of notations,
416
+ * ie: ['@1+', '@1/', '@1\\', ...]
417
+ *
418
+ * Note that moves are not guaranteed to be valid.
419
+ */
423
420
  possibleMoves() {
424
- // Returns a list of every currently possible move
425
- // as notations, ie ['@1+', '@1/', '@1\\', ...]
421
+ /** @type {string[]} */
426
422
  const possibles = []
427
423
  if (this.over) return possibles // Shortcut if the game is over
428
424
  for (const loc of this.possibleLocations()) {
@@ -435,9 +431,14 @@ export class Trax {
435
431
  return possibles
436
432
  }
437
433
 
434
+ /** Called after a move was just played, to determine the forced moves.
435
+ * @arg {Point} loc - the location just played at
436
+ * @returns {Tile[]} a list of tiles that should be added as part of the move
437
+ */
438
438
  forcedMoves(loc) {
439
- // We just played at loc, so return all forced moves
439
+ /** @type {Tile[]} */
440
440
  const forced = []
441
+ /** @type {Record<string, boolean>} */
441
442
  const invalids = {}
442
443
  let check = loc.around
443
444
  while (check.length > 0) {
@@ -460,16 +461,26 @@ export class Trax {
460
461
  return forced
461
462
  }
462
463
 
464
+ /** Determine the notation for a move. Note that this must be determined
465
+ * BEFORE the move is placed on the board.
466
+ * @arg {ValidTiles} type - the tile placed on the board
467
+ * @arg {Point} loc - the location the tile is placed
468
+ * @returns {string} the notation of the move
469
+ */
463
470
  notate(type, loc) {
464
- // Notation depends on the position of the board BEFORE the move
465
471
  let notation = Trax.encodeCol(loc.x - this.left + 1)
466
472
  notation += String(loc.y - this.top + 1)
467
473
  notation += slash[type]
468
474
  return notation
469
475
  }
470
476
 
477
+ /** Turn a move notation into a tile type and location.
478
+ * @arg {string} notation - a notation for a single move to be played at
479
+ * the current board position
480
+ * @returns {RawMove} the tile type and location of the move
481
+ */
471
482
  decodeNotation(notation) {
472
- // Return {type, loc} from a notation
483
+ /** @type {RawMove} */
473
484
  const bad = { type: 'x', loc: zero }
474
485
  const match = notation.match(notationRegex)
475
486
  if (match === null) return bad
@@ -482,6 +493,14 @@ export class Trax {
482
493
  return bad
483
494
  }
484
495
 
496
+ /** Follow a color from one location through one or more tiles to the other
497
+ * end of the color line.
498
+ * @arg {Color} color - the color to follow
499
+ * @arg {Point} loc - the location to start
500
+ * @arg {string} from - the edge of the tile to start from
501
+ * @returns {LineEnd} the ending location and a list of the tile ids the path
502
+ * takes to get there.
503
+ */
485
504
  follow(color, loc, from) {
486
505
  const path = []
487
506
  let id = this.tileId(loc)
@@ -509,7 +528,13 @@ export class Trax {
509
528
  return { loc, path }
510
529
  }
511
530
 
531
+ /** Given a tile and a color, follow the line for that color to each end.
532
+ * @arg {Color} color - the color of ends of interest
533
+ * @arg { Point} loc - the location of the tile of interest
534
+ * @returns {LineEnd[]} a list of two items, each one end of the line
535
+ */
512
536
  findEnds(color, loc) {
537
+ /** @type {LineEnd[]} */
513
538
  const ends = []
514
539
  const tile = this.tileAt(loc)
515
540
  if (upColor(tile) === color) ends.push(this.follow(color, loc, 'top'))
@@ -519,6 +544,11 @@ export class Trax {
519
544
  return ends
520
545
  }
521
546
 
547
+ /** Determine if a given line ends the game.
548
+ * @arg {Point} locA - one end of the line
549
+ * @arg {Point} locB - the other end of the line
550
+ * @returns {boolean} - true if this line wins the game
551
+ */
522
552
  lineWin(locA, locB) {
523
553
  if (this.rules === 'traxloop') return false
524
554
  if (this.width > 7 && locA.distX(locB) > this.width) return true
@@ -526,6 +556,9 @@ export class Trax {
526
556
  return false
527
557
  }
528
558
 
559
+ /** Determine if the game has ended.
560
+ * @arg {Tile[]} tiles - a list of the tiles placed during the last move.
561
+ */
529
562
  checkWin(tiles) {
530
563
  /** @type {Color|false} */
531
564
  let winner = false
@@ -534,13 +567,13 @@ export class Trax {
534
567
  if (winner) continue // Skip the second player if the first has won
535
568
  for (const tile of tiles) {
536
569
  if (winner) continue // Skip the remaining tiles once we find a win
537
- const ends = this.findEnds(color, tile.loc)
538
- if (ends[0].loc.eq(ends[1].loc)) {
570
+ const [end1, end2] = this.findEnds(color, tile.loc)
571
+ if (end1.loc.eq(end2.loc)) {
539
572
  winner = color // Loop win
540
- this.path = ends[0].path // Both paths are essentially the same
541
- } else if (this.lineWin(ends[0].loc, ends[1].loc)) {
573
+ this.path = end1.path // Both paths are essentially the same
574
+ } else if (this.lineWin(end1.loc, end2.loc)) {
542
575
  winner = color // Line win, paths need to be combined
543
- this.path = [...ends[0].path.reverse(), ...ends[1].path.slice(1)]
576
+ this.path = [...end1.path.reverse(), ...end2.path.slice(1)]
544
577
  }
545
578
  }
546
579
  }
@@ -581,6 +614,11 @@ export class Trax {
581
614
  return this.dropTile(notation)
582
615
  }
583
616
 
617
+ /** Play one or more moves.
618
+ * @arg {string|string[]} moves - the list of moves, provided either as a
619
+ * space-separated string of notations, or as a list of notations. Move
620
+ * numbers are optional, but if provided will be checked for accuracy.
621
+ */
584
622
  playMoves(moves) {
585
623
  // Play one or more moves, from a string or a list of moves
586
624
  // If move numbers are included, ensure they are accurate
@@ -608,6 +646,9 @@ export class Trax {
608
646
  }
609
647
  }
610
648
 
649
+ /** Add the current move notation to the notation string
650
+ * @arg {string} notation - the notation of the current move
651
+ */
611
652
  updateNotation(notation) {
612
653
  this.move++
613
654
  let note = this.move + '. ' + notation
@@ -628,7 +669,7 @@ export class Trax {
628
669
  }
629
670
 
630
671
  /** Drop a tile onto the board. This is a lower level call. Use play() if possible instead.
631
- * @arg {string|TileType} type - a special move, a tile type, or a notation
672
+ * @arg {string|ValidTiles} type - a special move, a tile type, or a notation
632
673
  * @arg {Point} [loc] - a location if type is a tile type
633
674
  * @arg {string|boolean} [tentative] - if truthy, the move will not be saved
634
675
  * @returns {TileDrop} an object representing the results of the drop
@@ -678,6 +719,11 @@ export class Trax {
678
719
  return { dropped, notation, valid }
679
720
  }
680
721
 
722
+ /** Symmetry helper. Rotates a move around the board in case we are trying to
723
+ * play a symmetrical rather than an exact move.
724
+ * @arg {string} move - the notation of the move to be rotated
725
+ * @returns {string[]} the four rotations of this move
726
+ */
681
727
  moveRotations(move) {
682
728
  // Symmetry helper, rotate a move around the board
683
729
  const match = move.match(notationRegex)
@@ -700,15 +746,24 @@ export class Trax {
700
746
  return [...moves]
701
747
  }
702
748
 
749
+ /** Play a provisional move if it is valid.
750
+ * @arg {string} from - the normalized encoding of the starting position
751
+ * @arg {string} to - the normalized encoding of the ending position
752
+ * @arg {string} via - the move to be used to transition
753
+ * @returns {false|string} if the provisional move is invalid: false; if the
754
+ * provisional move will never be valid for any future moves:
755
+ * 'delete-provisional'; if the provisional move is valid, the correct
756
+ * notation, which may be symmetrically adjusted as needed
757
+ */
703
758
  provisionalMove(from, to, via) {
704
759
  // Test a provisional move against the current state
705
- let rows = from.slice(1).split(':')
760
+ const lines = from.slice(1).split(':')
706
761
  let cols = 0
707
- for (const row of rows) {
708
- cols = Math.max(cols, codeRowLength(row))
762
+ for (const line of lines) {
763
+ cols = Math.max(cols, codeRowLength(line))
709
764
  }
710
765
 
711
- rows = rows.length
766
+ const rows = lines.length
712
767
  if (
713
768
  (this.width > rows && this.height > cols) ||
714
769
  (this.width > cols && this.height > rows)
@@ -731,15 +786,31 @@ export class Trax {
731
786
  return false
732
787
  }
733
788
 
789
+ /** Get an encoded representation of the current position, useful for drawing
790
+ * the board without having to do much analysis.
791
+ * @returns {string} the current position code
792
+ */
734
793
  get icon() {
735
794
  return this.positionCode()
736
795
  }
737
796
 
797
+ /** Get an encoded representation of the current position, with a set of
798
+ * tiles highlighted differently, useful for showing the effects of a move.
799
+ * @arg {TileDrop} drops - the drops of the most recent play
800
+ * @returns {string} the current position code, with drops highlighted
801
+ */
738
802
  dropsIcon(drops) {
739
803
  return this.positionCode(undefined, undefined, undefined, drops)
740
804
  }
741
805
 
742
- // Symmetry helper, draw the board from different angles
806
+ /** Symmetry helper, draw the board from different angles.
807
+ * @arg {boolean} [rightToLeft] - reverse order horizontally
808
+ * @arg {boolean} [bottomToTop] - reverse order vertically
809
+ * @arg {boolean} [rotate] - rotate the tiles by 90 degrees
810
+ * @arg {TileDrop} [drops] - the drops of the most recent play, if you want
811
+ * them highlighted
812
+ * @returns {string} an encoding of the position
813
+ */
743
814
  positionCode(rightToLeft, bottomToTop, rotate, drops) {
744
815
  const startX = rightToLeft ? this.right : this.left
745
816
  const startY = bottomToTop ? this.bottom : this.top
@@ -804,9 +875,11 @@ export class Trax {
804
875
  return code.join(':')
805
876
  }
806
877
 
878
+ /** Trax has the potential for symmetry, so this gives us the ability to
879
+ * examine horizontal, vertical, and rotational symmetry for a color.
880
+ * @returns {string} a position code that matches all symmetrical positions
881
+ */
807
882
  normalize() {
808
- // Trax has the potential for symmetry, so this gives us the ability to
809
- // examine horizontal, vertical, and rotational symmetry for a color
810
883
  let norm = 'z'
811
884
  const bools = [true, false]
812
885
  for (const rightToLeft of bools) {
@@ -821,6 +894,9 @@ export class Trax {
821
894
  return (this.color || 't').toUpperCase() + norm
822
895
  }
823
896
 
897
+ /** Get the normalized code for this position. All symmetrical positions will
898
+ * result in the same normalized code.
899
+ */
824
900
  get normalized() {
825
901
  return this.normalize()
826
902
  }
package/src/types.d.ts CHANGED
@@ -7,131 +7,21 @@
7
7
  /** Color is a single character to represent white or black. */
8
8
  type Color = 'w' | 'b'
9
9
 
10
- /** Colorize is a fancy function for colorizing text. */
11
- type Colorize = (text: string, def?: string | number) => string
12
-
13
- /** Colorer is a little too fancy. Hence the gnarly typescript. */
14
- type Colorer = {
15
- (text: string, def?: string | number): string
16
- black: Colorize
17
- command: Colorize
18
- default: Colorize
19
- error: Colorize
20
- fatal: Colorize
21
- help: Colorize
22
- id: Colorize
23
- variable: Colorize
24
- optional: Colorize
25
- short: Colorize
26
- white: Colorize
27
- }
28
-
29
- /** An object representing a space on the perimeter of a Trax position.
30
- * x,y: the coordinates of the space
31
- * c: the color present beside the space (w, b, l: both w and b, r: none)
32
- * t: the type of possible moves: n-normal, x-none, c-cave, C-restricted cave
33
- * b,w: if present, the number of the line to match with its other end
34
- * pb, pw: if present, can pair with the next line in one turn
35
- * xb, xw: the label of this space for each color
36
- */
37
- type EdgeLocation = {
38
- x: number
39
- y: number
40
- c: string
41
- t: string
42
- b?: number
43
- w?: number
44
- pb?: boolean
45
- pw?: boolean
46
- zb?: string
47
- zw?: string
48
- idx?: number
49
- }
50
-
51
- type EdgeObject = {
52
- b: string
53
- w: string
54
- edge: RawEdge
55
- }
56
-
57
- /** Object representing a concrete threat found in a position. */
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
- }
71
-
72
- /** An object with keys representing each level, with arrays for each threat of
73
- * that level. */
74
- type ColorThreats = Record<string, FoundThreat[]>
75
-
76
- /** All the threats found for both the white and black player. */
77
- type FoundThreatsCollection = { b: ColorThreats; w: ColorThreats }
78
-
79
- /** Scores for each player. */
80
- type Scores = {
81
- b: number
82
- w: number
83
- }
84
-
85
- /** A note with a move number */
86
- type GameNote = {
87
- move: number
88
- note: string
89
- }
90
-
91
- /** Notes saved in the game object in the CLI. */
92
- type GameNotes = GameNote[]
93
-
94
- /** A point-like object has numerical x and y properties. */
95
- type PointLike = {
96
- x: number
97
- y: number
10
+ /** One end of a line and the tiles taken to get there. */
11
+ type LineEnd = {
12
+ loc: Point
13
+ path: TileId[]
98
14
  }
99
15
 
100
- /** An object used to track an analysis on a position. */
101
- type PositionScore = {
102
- move: string
103
- score: number
104
- analysis?: Analysis
105
- }
16
+ /** A notation of a move. */
17
+ type Notation = string
106
18
 
107
- /** A puzzle. */
108
- type Puzzle = {
109
- id: string
110
- src: string
111
- game: TraxVariant
112
- notation: string
113
- icon: string
114
- level: number
115
- max: number
116
- player: number
117
- title?: string
118
- desc?: string
119
- hint?: string
120
- hints?: string[]
121
- }
122
-
123
- type PuzzleSource = {
124
- name: string
125
- url?: string
126
- copyright?: string
127
- license?: string
128
- licenseUrl?: string
19
+ /** A tile type and a location determine a raw move. */
20
+ type RawMove = {
21
+ type: TileType
22
+ loc: Point
129
23
  }
130
24
 
131
- /** Array of objects representing the spaces surrounding the perimeter of a
132
- * Trax position. */
133
- type RawEdge = EdgeLocation[]
134
-
135
25
  /** Treat the save state as an opaque object,
136
26
  * produced by save() and fed into restore().
137
27
  */
@@ -144,7 +34,7 @@ type SaveState = {
144
34
  right: number
145
35
  top: number
146
36
  bottom: number
147
- notation: string
37
+ notation: Notation
148
38
  tiles: string
149
39
  path: string
150
40
  invalid: boolean
@@ -155,14 +45,6 @@ type SaveState = {
155
45
  */
156
46
  type Slash = '/' | '\\' | '+'
157
47
 
158
- /** Threat definition. */
159
- type Threat = {
160
- depth: number
161
- pattern: string
162
- rx: RegExp
163
- value: number
164
- }
165
-
166
48
  /** A single tile on the board. */
167
49
  type Tile = {
168
50
  id: TileId
@@ -175,7 +57,7 @@ type Tile = {
175
57
  /** When a tile is dropped, this object represents the results. */
176
58
  type TileDrop = {
177
59
  dropped: Tile[]
178
- notation: string
60
+ notation: Notation
179
61
  valid: boolean
180
62
  }
181
63
 
@@ -188,5 +70,15 @@ type TileType = ValidTiles | 'x'
188
70
  /** All of the variants supported by the engine. */
189
71
  type TraxVariant = 'trax' | 'traxloop' | 'trax8'
190
72
 
191
- /** Tile types are represented by one of the following single characters. */
73
+ /** Tile type names are determined by listing the line color at each edge,
74
+ * starting from the top and going clockwise, and then sorted alphabetically
75
+ * and given a single letter name, so 'bbww' becomes 'a', which gives us six
76
+ * different tile names: a-f, as follows:
77
+ * a b c d e f
78
+ * +--#--+ +--#--+ +--#--+ +--o--+ +--o--+ +--o--+
79
+ * | # | | # | | # | | o | | o | | o |
80
+ * oo ## ooo#ooo ## oo oo ## ####### ## oo
81
+ * | o | | # | | o | | # | | o | | # |
82
+ * +--o--+ +--#--+ +--o--+ +--#--+ +--o--+ +--#--+
83
+ */
192
84
  type ValidTiles = 'a' | 'b' | 'c' | 'd' | 'e' | 'f'
package/src/version.js CHANGED
@@ -1,2 +1,2 @@
1
1
  // Generated by genversion.
2
- export const version = '0.21.0'
2
+ export const version = '0.23.0'
package/.dockerignore DELETED
@@ -1,11 +0,0 @@
1
- .dockerignore
2
- .git*
3
- .husky
4
- .npmignore
5
- benchmark
6
- coverage
7
- Dockerfile
8
- docs
9
- jsconfig.json
10
- node_modules
11
- test
package/Dockerfile DELETED
@@ -1,14 +0,0 @@
1
- # Use the latest nodejs long-term-support release,
2
- # on the latest Alpine Linux for its small footprint
3
- FROM node:lts-alpine
4
- # Install current source code
5
- WORKDIR /trax
6
- COPY . .
7
- RUN npm install
8
- # Ensure the data files can be persisted
9
- WORKDIR /mnt/data
10
- ENV XDG_CONFIG_HOME=/mnt/data
11
- ENV XDG_DATA_HOME=/mnt/data
12
- VOLUME /mnt/data
13
- # Use the trax CLI as the starting point
14
- ENTRYPOINT ["/trax/src/cli.js"]