@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 +7 -1
- package/package.json +1 -1
- package/src/engine.js +122 -41
- package/src/types.d.ts +17 -2
- package/src/version.js +1 -1
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
|
|
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
package/src/engine.js
CHANGED
|
@@ -1,20 +1,9 @@
|
|
|
1
|
-
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* You may obtain a copy of the License at
|
|
6
|
-
*
|
|
7
|
-
* http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
-
*
|
|
9
|
-
* Unless required by applicable law or agreed to in writing, software
|
|
10
|
-
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
-
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
-
* See the License for the specific language governing permissions and
|
|
13
|
-
* limitations under the License.
|
|
1
|
+
/** 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
|
|
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=
|
|
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 =
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
533
|
-
if (
|
|
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 =
|
|
536
|
-
} else if (this.lineWin(
|
|
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 = [...
|
|
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|
|
|
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
|
-
|
|
760
|
+
const lines = from.slice(1).split(':')
|
|
701
761
|
let cols = 0
|
|
702
|
-
for (const
|
|
703
|
-
cols = Math.max(cols, codeRowLength(
|
|
762
|
+
for (const line of lines) {
|
|
763
|
+
cols = Math.max(cols, codeRowLength(line))
|
|
704
764
|
}
|
|
705
765
|
|
|
706
|
-
rows =
|
|
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
|
-
|
|
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:
|
|
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:
|
|
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.
|
|
2
|
+
export const version = '0.23.0'
|