@slugbugblue/trax 0.22.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/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # @slugbugblue/trax
2
2
 
3
+ ## 0.23.0 - 2023-03-28
4
+
5
+ Release candidate
6
+
7
+ Finalize work with jsdoc/typescript
8
+
3
9
  ## 0.22.0 - 2023-03-25
4
10
 
5
11
  - Pull point.js into its own package: `@slugbugblue/point`
@@ -28,7 +34,7 @@
28
34
  - Drop support for node 14; node v16.15 is our minimum supported version
29
35
  - More puzzles from Martin M. S. Pedersen -- bringing the total to 100+
30
36
  - Enhance the docker configuration and document it in the README
31
- - Refine faulty threat detection and add test for once case it was failing
37
+ - Refine faulty threat detection and add test for one case it was failing
32
38
 
33
39
  ## 0.19.0 - 2023-02-06
34
40
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@slugbugblue/trax",
3
- "version": "0.22.0",
3
+ "version": "0.23.0",
4
4
  "description": "Trax game engine",
5
5
  "keywords": [
6
6
  "trax",
package/src/engine.js CHANGED
@@ -1,20 +1,9 @@
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
7
  import { Point } from '@slugbugblue/point'
19
8
 
20
9
  // Fun trax helper constants
@@ -36,7 +25,9 @@ const notationRegex = /^([@a-z]+)(\d+)([/\\+])$/i
36
25
  * */
37
26
  const tileTypes = ['a', 'b', 'c', 'd', 'e', 'f']
38
27
 
39
- /** Slashes are represented by / \ or +.
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".
40
31
  * @readonly
41
32
  * @type {Record<ValidTiles, Slash>}
42
33
  */
@@ -197,10 +188,10 @@ export class Trax {
197
188
  // Trax class constructor
198
189
  /** Create a new Trax game
199
190
  * @arg {TraxVariant} [rules='trax'] - the variant to play
200
- * @arg {string|string[]} [moves=[]] - the initial moves to pre-play
191
+ * @arg {string|string[]} [moves=''] - the initial moves to pre-play
201
192
  * @arg {string} [id='trax'] - an id used to differentiate tiles from multiple games
202
193
  */
203
- constructor(rules = 'trax', moves = [], id = 'trax') {
194
+ constructor(rules = 'trax', moves = '', id = 'trax') {
204
195
  if (!Trax.variants.has(rules)) rules = 'trax'
205
196
  this.id = String(id)
206
197
  this.rules = rules
@@ -311,6 +302,7 @@ export class Trax {
311
302
  */
312
303
  addTile(type, loc) {
313
304
  const id = this.tileId(loc)
305
+ /** @type {Tile} */
314
306
  const tile = { type, loc, id, move: this.move, seq: this.count }
315
307
  if (type === 'x') {
316
308
  this.invalid = true // Invalid tile is being played
@@ -334,7 +326,7 @@ export class Trax {
334
326
  }
335
327
 
336
328
  /** Is this tile valid?
337
- * @type {(type: string, loc: Point) => type is TileType}
329
+ * @type {(type: string, loc: Point) => type is ValidTiles}
338
330
  */
339
331
  validTile(type, loc) {
340
332
  if (this.count === 0 && loc.x === 0 && loc.y === 0) {
@@ -377,6 +369,10 @@ export class Trax {
377
369
  return this.right - this.left + 1
378
370
  }
379
371
 
372
+ /** Determine if a location is valid to play in.
373
+ * @arg {Point} loc - the location to check
374
+ * @returns {boolean}
375
+ */
380
376
  validLocation(loc) {
381
377
  if (this.over) return false
382
378
  if (this.tileAt(loc)) return false
@@ -398,26 +394,31 @@ export class Trax {
398
394
  return false
399
395
  }
400
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
+ */
401
400
  possibleLocations() {
402
401
  if (this.count === 0) return [zero]
403
402
  /** @type Record<TileId, Point> */
404
403
  const possibles = {}
405
404
 
406
405
  for (const tile of Object.values(this.tiles)) {
407
- tile.loc.around.map((loc) => {
406
+ for (const loc of tile.loc.around) {
408
407
  const id = this.tileId(loc)
409
408
  if (!(id in this.tiles)) possibles[id] = loc
410
- return loc
411
- })
409
+ }
412
410
  }
413
411
 
414
- // Get rid of all the real tiles and return all the maybes
415
412
  return Object.values(possibles)
416
413
  }
417
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
+ */
418
420
  possibleMoves() {
419
- // Returns a list of every currently possible move
420
- // as notations, ie ['@1+', '@1/', '@1\\', ...]
421
+ /** @type {string[]} */
421
422
  const possibles = []
422
423
  if (this.over) return possibles // Shortcut if the game is over
423
424
  for (const loc of this.possibleLocations()) {
@@ -430,9 +431,14 @@ export class Trax {
430
431
  return possibles
431
432
  }
432
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
+ */
433
438
  forcedMoves(loc) {
434
- // We just played at loc, so return all forced moves
439
+ /** @type {Tile[]} */
435
440
  const forced = []
441
+ /** @type {Record<string, boolean>} */
436
442
  const invalids = {}
437
443
  let check = loc.around
438
444
  while (check.length > 0) {
@@ -455,16 +461,26 @@ export class Trax {
455
461
  return forced
456
462
  }
457
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
+ */
458
470
  notate(type, loc) {
459
- // Notation depends on the position of the board BEFORE the move
460
471
  let notation = Trax.encodeCol(loc.x - this.left + 1)
461
472
  notation += String(loc.y - this.top + 1)
462
473
  notation += slash[type]
463
474
  return notation
464
475
  }
465
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
+ */
466
482
  decodeNotation(notation) {
467
- // Return {type, loc} from a notation
483
+ /** @type {RawMove} */
468
484
  const bad = { type: 'x', loc: zero }
469
485
  const match = notation.match(notationRegex)
470
486
  if (match === null) return bad
@@ -477,6 +493,14 @@ export class Trax {
477
493
  return bad
478
494
  }
479
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
+ */
480
504
  follow(color, loc, from) {
481
505
  const path = []
482
506
  let id = this.tileId(loc)
@@ -504,7 +528,13 @@ export class Trax {
504
528
  return { loc, path }
505
529
  }
506
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
+ */
507
536
  findEnds(color, loc) {
537
+ /** @type {LineEnd[]} */
508
538
  const ends = []
509
539
  const tile = this.tileAt(loc)
510
540
  if (upColor(tile) === color) ends.push(this.follow(color, loc, 'top'))
@@ -514,6 +544,11 @@ export class Trax {
514
544
  return ends
515
545
  }
516
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
+ */
517
552
  lineWin(locA, locB) {
518
553
  if (this.rules === 'traxloop') return false
519
554
  if (this.width > 7 && locA.distX(locB) > this.width) return true
@@ -521,6 +556,9 @@ export class Trax {
521
556
  return false
522
557
  }
523
558
 
559
+ /** Determine if the game has ended.
560
+ * @arg {Tile[]} tiles - a list of the tiles placed during the last move.
561
+ */
524
562
  checkWin(tiles) {
525
563
  /** @type {Color|false} */
526
564
  let winner = false
@@ -529,13 +567,13 @@ export class Trax {
529
567
  if (winner) continue // Skip the second player if the first has won
530
568
  for (const tile of tiles) {
531
569
  if (winner) continue // Skip the remaining tiles once we find a win
532
- const ends = this.findEnds(color, tile.loc)
533
- if (ends[0].loc.eq(ends[1].loc)) {
570
+ const [end1, end2] = this.findEnds(color, tile.loc)
571
+ if (end1.loc.eq(end2.loc)) {
534
572
  winner = color // Loop win
535
- this.path = ends[0].path // Both paths are essentially the same
536
- } 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)) {
537
575
  winner = color // Line win, paths need to be combined
538
- this.path = [...ends[0].path.reverse(), ...ends[1].path.slice(1)]
576
+ this.path = [...end1.path.reverse(), ...end2.path.slice(1)]
539
577
  }
540
578
  }
541
579
  }
@@ -576,6 +614,11 @@ export class Trax {
576
614
  return this.dropTile(notation)
577
615
  }
578
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
+ */
579
622
  playMoves(moves) {
580
623
  // Play one or more moves, from a string or a list of moves
581
624
  // If move numbers are included, ensure they are accurate
@@ -603,6 +646,9 @@ export class Trax {
603
646
  }
604
647
  }
605
648
 
649
+ /** Add the current move notation to the notation string
650
+ * @arg {string} notation - the notation of the current move
651
+ */
606
652
  updateNotation(notation) {
607
653
  this.move++
608
654
  let note = this.move + '. ' + notation
@@ -623,7 +669,7 @@ export class Trax {
623
669
  }
624
670
 
625
671
  /** Drop a tile onto the board. This is a lower level call. Use play() if possible instead.
626
- * @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
627
673
  * @arg {Point} [loc] - a location if type is a tile type
628
674
  * @arg {string|boolean} [tentative] - if truthy, the move will not be saved
629
675
  * @returns {TileDrop} an object representing the results of the drop
@@ -673,6 +719,11 @@ export class Trax {
673
719
  return { dropped, notation, valid }
674
720
  }
675
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
+ */
676
727
  moveRotations(move) {
677
728
  // Symmetry helper, rotate a move around the board
678
729
  const match = move.match(notationRegex)
@@ -695,15 +746,24 @@ export class Trax {
695
746
  return [...moves]
696
747
  }
697
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
+ */
698
758
  provisionalMove(from, to, via) {
699
759
  // Test a provisional move against the current state
700
- let rows = from.slice(1).split(':')
760
+ const lines = from.slice(1).split(':')
701
761
  let cols = 0
702
- for (const row of rows) {
703
- cols = Math.max(cols, codeRowLength(row))
762
+ for (const line of lines) {
763
+ cols = Math.max(cols, codeRowLength(line))
704
764
  }
705
765
 
706
- rows = rows.length
766
+ const rows = lines.length
707
767
  if (
708
768
  (this.width > rows && this.height > cols) ||
709
769
  (this.width > cols && this.height > rows)
@@ -726,15 +786,31 @@ export class Trax {
726
786
  return false
727
787
  }
728
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
+ */
729
793
  get icon() {
730
794
  return this.positionCode()
731
795
  }
732
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
+ */
733
802
  dropsIcon(drops) {
734
803
  return this.positionCode(undefined, undefined, undefined, drops)
735
804
  }
736
805
 
737
- // 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
+ */
738
814
  positionCode(rightToLeft, bottomToTop, rotate, drops) {
739
815
  const startX = rightToLeft ? this.right : this.left
740
816
  const startY = bottomToTop ? this.bottom : this.top
@@ -799,9 +875,11 @@ export class Trax {
799
875
  return code.join(':')
800
876
  }
801
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
+ */
802
882
  normalize() {
803
- // Trax has the potential for symmetry, so this gives us the ability to
804
- // examine horizontal, vertical, and rotational symmetry for a color
805
883
  let norm = 'z'
806
884
  const bools = [true, false]
807
885
  for (const rightToLeft of bools) {
@@ -816,6 +894,9 @@ export class Trax {
816
894
  return (this.color || 't').toUpperCase() + norm
817
895
  }
818
896
 
897
+ /** Get the normalized code for this position. All symmetrical positions will
898
+ * result in the same normalized code.
899
+ */
819
900
  get normalized() {
820
901
  return this.normalize()
821
902
  }
package/src/types.d.ts CHANGED
@@ -7,6 +7,21 @@
7
7
  /** Color is a single character to represent white or black. */
8
8
  type Color = 'w' | 'b'
9
9
 
10
+ /** One end of a line and the tiles taken to get there. */
11
+ type LineEnd = {
12
+ loc: Point
13
+ path: TileId[]
14
+ }
15
+
16
+ /** A notation of a move. */
17
+ type Notation = string
18
+
19
+ /** A tile type and a location determine a raw move. */
20
+ type RawMove = {
21
+ type: TileType
22
+ loc: Point
23
+ }
24
+
10
25
  /** Treat the save state as an opaque object,
11
26
  * produced by save() and fed into restore().
12
27
  */
@@ -19,7 +34,7 @@ type SaveState = {
19
34
  right: number
20
35
  top: number
21
36
  bottom: number
22
- notation: string
37
+ notation: Notation
23
38
  tiles: string
24
39
  path: string
25
40
  invalid: boolean
@@ -42,7 +57,7 @@ type Tile = {
42
57
  /** When a tile is dropped, this object represents the results. */
43
58
  type TileDrop = {
44
59
  dropped: Tile[]
45
- notation: string
60
+ notation: Notation
46
61
  valid: boolean
47
62
  }
48
63
 
package/src/version.js CHANGED
@@ -1,2 +1,2 @@
1
1
  // Generated by genversion.
2
- export const version = '0.22.0'
2
+ export const version = '0.23.0'