@slugbugblue/trax 1.1.0 → 1.2.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,45 +1,122 @@
1
- /** Trax engine.
2
- * @copyright 2019-2026
3
- * @author Chad Transtrum <chad@transtrum.net>
4
- * @license Apache-2.0
5
- */
6
-
7
- /// <reference path="./types.d.ts" />
8
-
1
+ /**
2
+ Trax engine.
3
+ @copyright 2019-2026
4
+ @author Chad Transtrum <chad@transtrum.net>
5
+ @license Apache-2.0
6
+ */
7
+
8
+ /** @import { PointLike } from '@slugbugblue/point' */
9
9
  import { Point } from '@slugbugblue/point'
10
10
  import { version } from './version.js'
11
11
 
12
+ // Type definitions
13
+
14
+ /** Color is a single character to represent white or black. */
15
+ /** @typedef {'w' | 'b'} Color */
16
+
17
+ /**
18
+ One end of a line and the tiles taken to get there.
19
+ @typedef {object} LineEnd
20
+ @property {Point} loc - the ending location
21
+ @property {TileId[]} path - tile ids from start to end
22
+ */
23
+
24
+ /** A notation of a move. */
25
+ /** @typedef {string} Notation */
26
+
27
+ /**
28
+ A tile type and a location determine a raw move.
29
+ @typedef {object} RawMove
30
+ @property {TileType} type - the tile type
31
+ @property {Point} loc - the location
32
+ */
33
+
34
+ /**
35
+ Treat the save state as an opaque object,
36
+ produced by save() and fed into restore().
37
+ @typedef {object} SaveState
38
+ @property {string} id - game id
39
+ @property {number} move - move number
40
+ @property {number} turn - current player (1 or 2)
41
+ @property {boolean} over - whether the game is over
42
+ @property {number} left - leftmost tile column
43
+ @property {number} right - rightmost tile column
44
+ @property {number} top - topmost tile row
45
+ @property {number} bottom - bottommost tile row
46
+ @property {string[]} moves - move notations
47
+ @property {[TileId, Point][]} frontier - frontier locations
48
+ @property {string} tiles - serialized tiles
49
+ @property {string} path - serialized winning path
50
+ @property {boolean} invalid - whether an invalid tile was placed
51
+ */
52
+
53
+ /** Representations of the different ways a tile can curve. */
54
+ /** @typedef {'/' | '\\' | '+'} Slash */
55
+
56
+ /**
57
+ A single tile on the board.
58
+ @typedef {object} Tile
59
+ @property {TileId} id - unique tile identifier
60
+ @property {Point} loc - board location
61
+ @property {TileType} type - tile type letter
62
+ @property {number} move - move number when placed
63
+ @property {number} seq - sequence number among all tiles
64
+ */
65
+
66
+ /** A TileId is just a string. */
67
+ /** @typedef {string} TileId */
68
+
69
+ /**
70
+ When a tile is dropped, this object represents the results.
71
+ @typedef {object} TileDrop
72
+ @property {Tile[]} dropped - all tiles placed, including forced moves
73
+ @property {Notation} notation - the notation of the played move
74
+ @property {boolean} valid - whether the move was legal
75
+ */
76
+
77
+ /** Invalid tiles are represented by 'x'. */
78
+ /** @typedef {ValidTiles | 'x'} TileType */
79
+
80
+ /** All of the variants supported by the engine. */
81
+ /** @typedef {'trax' | 'traxloop' | 'trax8'} TraxVariant */
82
+
83
+ /** Tile type names are single letters a-f encoding edge colors. */
84
+ /** @typedef {'a' | 'b' | 'c' | 'd' | 'e' | 'f'} ValidTiles */
85
+
12
86
  // Fun trax helper constants
13
87
 
14
88
  const zero = new Point(0, 0)
15
- const moveNumberRegex = /^(\d+)[.):]?$/
16
- const notationRegex = /^([@a-z]+)(\d+)([/\\+])$/i
17
-
18
- /** Tiles are represented by the letters a-f.
19
- * a b c d e f
20
- * +--#--+ +--#--+ +--#--+ +--o--+ +--o--+ +--o--+
21
- * | # | | # | | # | | o | | o | | o |
22
- * oo ## ooo#ooo ## oo oo ## ####### ## oo
23
- * | o | | # | | o | | # | | o | | # |
24
- * +--o--+ +--#--+ +--o--+ +--#--+ +--o--+ +--#--+
25
- *
26
- * @readonly
27
- * @type ValidTiles[]
28
- * */
89
+ const moveNumberRegex = /^\d+[\).:]?$/v
90
+ const notationRegex = /^(?<col>[@a-z]+)(?<row>\d+)(?<slash>[+\/\\])$/iv
91
+
92
+ /**
93
+ Tiles are represented by the letters a-f.
94
+ a b c d e f
95
+ +--#--+ +--#--+ +--#--+ +--o--+ +--o--+ +--o--+
96
+ | # | | # | | # | | o | | o | | o |
97
+ oo ## ooo#ooo ## oo oo ## ####### ## oo
98
+ | o | | # | | o | | # | | o | | # |
99
+ +--o--+ +--#--+ +--o--+ +--#--+ +--o--+ +--#--+
100
+
101
+ @readonly
102
+ @type {ValidTiles[]}
103
+ */
29
104
  const tileTypes = ['a', 'b', 'c', 'd', 'e', 'f']
30
105
 
31
- /** Slashes indicate the direction the tile is placed. A forward slash or a
32
- * backslash indicate the tile is a "curve" and the plus sign indicates the
33
- * tile is a "straight".
34
- * @readonly
35
- * @type {Record<ValidTiles, Slash>}
36
- */
106
+ /**
107
+ Slashes indicate the direction the tile is placed. A forward slash or a
108
+ backslash indicate the tile is a "curve" and the plus sign indicates the
109
+ tile is a "straight".
110
+ @readonly
111
+ @type {Record<ValidTiles, Slash>}
112
+ */
37
113
  const slash = { a: '\\', b: '+', c: '/', d: '/', e: '+', f: '\\' }
38
114
 
39
- /** Symmetry helper to convert from one tile to another.
40
- * @readonly
41
- * @type {Record<string, Record<ValidTiles, ValidTiles>>}
42
- */
115
+ /**
116
+ Symmetry helper to convert from one tile to another.
117
+ @readonly
118
+ @type {Record<string, Record<ValidTiles, ValidTiles>>}
119
+ */
43
120
  const symmetry = {
44
121
  // Convert tile to different views
45
122
  flip: { a: 'd', b: 'b', c: 'f', d: 'a', e: 'e', f: 'c' },
@@ -50,13 +127,14 @@ const symmetry = {
50
127
 
51
128
  // Fun trax helper functions
52
129
 
53
- /** Apply certain combinations of symmetry.
54
- * @arg {TileType} tile - the tile to apply symmetry to
55
- * @arg {boolean} [rightToLeft] - mirror the tile
56
- * @arg {boolean} [bottomToTop] - flip the tile
57
- * @arg {boolean} [rotate] - rotate the tile counterclockwise
58
- * @returns {TileType}
59
- */
130
+ /**
131
+ Apply certain combinations of symmetry.
132
+ @param {ValidTiles} tile - the tile to apply symmetry to
133
+ @param {boolean} [rightToLeft] - mirror the tile
134
+ @param {boolean} [bottomToTop] - flip the tile
135
+ @param {boolean} [rotate] - rotate the tile counterclockwise
136
+ @returns {ValidTiles} the transformed tile
137
+ */
60
138
  const applySymmetry = (tile, rightToLeft, bottomToTop, rotate) => {
61
139
  if (rightToLeft) tile = symmetry.mirror[tile]
62
140
  if (bottomToTop) tile = symmetry.flip[tile]
@@ -64,42 +142,48 @@ const applySymmetry = (tile, rightToLeft, bottomToTop, rotate) => {
64
142
  return tile
65
143
  }
66
144
 
67
- // Unexpected note: str.includes(t) is true if t is '' but false if t is undefined
68
- /** The color at the top of the tile.
69
- * @arg {string} t - the tile to check
70
- * @returns {Color|false} the color or false if t is not a valid tile
71
- */
145
+ // Note: String(undefined) === 'undefined', which is never a valid tile letter,
146
+ // so these functions correctly return false when t is undefined.
147
+ /**
148
+ The color at the top of the tile.
149
+ @param {string | undefined} t - the tile to check
150
+ @returns {Color|false} the color or false if t is not a valid tile
151
+ */
72
152
  const upColor = (t) =>
73
- 'abc'.includes(t) ? 'b' : 'def'.includes(t) ? 'w' : false
153
+ 'abc'.includes(String(t)) ? 'b' : 'def'.includes(String(t)) ? 'w' : false
74
154
 
75
- /** The color at the bottom of the tile.
76
- * @arg {string} t - the tile to check
77
- * @returns {Color|false} the color or false if t is not a valid tile
78
- */
155
+ /**
156
+ The color at the bottom of the tile.
157
+ @param {string | undefined} t - the tile to check
158
+ @returns {Color|false} the color or false if t is not a valid tile
159
+ */
79
160
  const downColor = (t) =>
80
- 'bdf'.includes(t) ? 'b' : 'ace'.includes(t) ? 'w' : false
161
+ 'bdf'.includes(String(t)) ? 'b' : 'ace'.includes(String(t)) ? 'w' : false
81
162
 
82
- /** The color at the right edge of the tile.
83
- * @arg {string} t - the tile to check
84
- * @returns {Color|false} the color or false if t is not a valid tile
85
- */
163
+ /**
164
+ The color at the right edge of the tile.
165
+ @param {string | undefined} t - the tile to check
166
+ @returns {Color|false} the color or false if t is not a valid tile
167
+ */
86
168
  const rightColor = (t) =>
87
- 'ade'.includes(t) ? 'b' : 'bcf'.includes(t) ? 'w' : false
169
+ 'ade'.includes(String(t)) ? 'b' : 'bcf'.includes(String(t)) ? 'w' : false
88
170
 
89
- /** The color at the left edge of the tile.
90
- * @arg {string} t - the tile to check
91
- * @returns {Color|false} the color or false if t is not a valid tile
92
- */
171
+ /**
172
+ The color at the left edge of the tile.
173
+ @param {string | undefined} t - the tile to check
174
+ @returns {Color|false} the color or false if t is not a valid tile
175
+ */
93
176
  const leftColor = (t) =>
94
- 'cef'.includes(t) ? 'b' : 'abd'.includes(t) ? 'w' : false
177
+ 'cef'.includes(String(t)) ? 'b' : 'abd'.includes(String(t)) ? 'w' : false
95
178
 
96
- /** Add an empty space to the position encoding string.
97
- * @arg {string} code - The position encoding string
98
- * @returns {string}
99
- */
179
+ /**
180
+ Add an empty space to the position encoding string.
181
+ @param {string} code - the position encoding string
182
+ @returns {string} the updated encoding with a blank appended
183
+ */
100
184
  const addBlank = (code) => {
101
185
  // Encode missing tiles as digits 0-9, for 1-10 missing
102
- const lastNumber = code.codePointAt(code.length - 1) // This is NaN if code is empty, which is ok
186
+ const lastNumber = /** @type {number} */ (code.codePointAt(code.length - 1)) // Undefined if code is empty, which is ok
103
187
  if (lastNumber > 47 && lastNumber < 57) {
104
188
  return code.slice(0, -1) + String.fromCodePoint(lastNumber + 1)
105
189
  }
@@ -108,12 +192,13 @@ const addBlank = (code) => {
108
192
  }
109
193
 
110
194
  // Ugh, this is a dirty little function to get
111
- /** The length of an encoded position row.
112
- * @arg {string} row - the encoded row
113
- * @returns {number} - the actual length of the row
114
- */
195
+ /**
196
+ The length of an encoded position row.
197
+ @param {string} row - the encoded row
198
+ @returns {number} the actual length of the row
199
+ */
115
200
  const codeRowLength = (row) =>
116
- row.replaceAll(/\d/g, (n) => '.'.repeat(Number(n) + 1)).length
201
+ row.replaceAll(/\d/gv, (n) => '.'.repeat(Number(n) + 1)).length
117
202
 
118
203
  // This is where the magic happens
119
204
  /** A digital representation of a Trax game. */
@@ -121,7 +206,10 @@ export class Trax {
121
206
  // Static class properties
122
207
  /** @readonly */
123
208
  static version = version
124
- /** @readonly @type {Record<TraxVariant, string>} */
209
+ /**
210
+ @readonly
211
+ @type {Record<TraxVariant, string>}
212
+ */
125
213
  static names = {
126
214
  trax: 'Trax',
127
215
  traxloop: 'Loop Trax',
@@ -130,31 +218,48 @@ export class Trax {
130
218
  /** @readonly */
131
219
  static variants = new Set(Object.keys(this.names)) // Known variants
132
220
  // Static class methods
133
- /** Create an x,y Point object with special functions.
134
- * @arg {number|PointLike} x - either the x value, or an object with x,y keys
135
- * @arg {number} [y] - if x is a number, y must be provided as well
136
- * @returns {Point} a new Point object
137
- */
221
+ /**
222
+ Create an x,y Point object with special functions.
223
+ @param {number|PointLike} x - either the x value, or an object with x,y keys
224
+ @param {number} [y] - if x is a number, y must be provided as well
225
+ @returns {Point} a new Point object
226
+ */
138
227
  static point = (x, y) => new Point(x, y)
139
- /** Given a player number, get the color.
140
- * @arg {number} playerNumber - the player number, 1 or 2
141
- * @returns {Color} the color of that player, w or b
142
- */
143
- static colorOf = (playerNumber) => ({ 1: 'w', 2: 'b' })[playerNumber]
144
- /** Given a color, get the player number.
145
- * @arg {string} color - the color, w or b
146
- * @returns {number} the player number, 1 or 2
147
- */
148
- static playerNumber = (color) => ({ b: 2, w: 1 })[color]
149
- /** Given a color, get the other color.
150
- * @arg {string} color - the color, w or b
151
- * @returns {Color} the other color, b or w
152
- */
153
- static other = (color) => ({ b: 'w', w: 'b' })[color]
154
- /** Encode a numeric column number into the Trax notation column letter.
155
- * @arg {number} col - the colum number, with 0 just to the left of the tiles
156
- * @returns {string} the encoded column letter
157
- */
228
+ /**
229
+ Given a player number, get the color.
230
+ @param {number} playerNumber - the player number, 1 or 2
231
+ @returns {Color} the color of that player, w or b
232
+ */
233
+ static colorOf = (playerNumber) => {
234
+ /** @type {Record<number, Color>} */
235
+ const map = { 1: 'w', 2: 'b' }
236
+ return map[playerNumber]
237
+ }
238
+ /**
239
+ Given a color, get the player number.
240
+ @param {string} color - the color, w or b
241
+ @returns {number} the player number, 1 or 2
242
+ */
243
+ static playerNumber = (color) => {
244
+ /** @type {Record<string, number>} */
245
+ const map = { b: 2, w: 1 }
246
+ return map[color]
247
+ }
248
+ /**
249
+ Given a color, get the other color.
250
+ @param {string} color - the color, w or b
251
+ @returns {Color} the other color, b or w
252
+ */
253
+ static other = (color) => {
254
+ /** @type {Record<string, Color>} */
255
+ const map = { b: 'w', w: 'b' }
256
+ return map[color]
257
+ }
258
+ /**
259
+ Encode a numeric column number into the Trax notation column letter.
260
+ @param {number} col - the column number, with 0 just to the left of the tiles
261
+ @returns {string} the encoded column letter
262
+ */
158
263
  static encodeCol = (col) => {
159
264
  // For notation purposes: 0 -> @, 6 -> F, 28 -> AB
160
265
  let n = ''
@@ -165,16 +270,18 @@ export class Trax {
165
270
 
166
271
  return n || '@'
167
272
  }
168
- /** Decode a Trax notation column letter back to a number.
169
- * @arg {string} col - the Trax column letter
170
- * @returns {number} the column number
171
- */
273
+ /**
274
+ Decode a Trax notation column letter back to a number.
275
+ @param {string} col - the Trax column letter
276
+ @returns {number} the column number
277
+ */
172
278
  static decodeCol = (col) => {
173
279
  // Turn letters back into numbers
174
280
  col = col.toUpperCase()
175
281
  let n = 0
176
282
  while (col.length > 0) {
177
- n += (col.codePointAt(0) - 64) * 26 ** (col.length - 1)
283
+ const cp = /** @type {number} */ (col.codePointAt(0))
284
+ n += (cp - 64) * 26 ** (col.length - 1)
178
285
  col = col.slice(1)
179
286
  }
180
287
 
@@ -182,26 +289,30 @@ export class Trax {
182
289
  }
183
290
 
184
291
  // Trax class constructor
185
- /** Create a new Trax game
186
- * @arg {TraxVariant} [rules='trax'] - the variant to play
187
- * @arg {string|string[]} [moves=''] - the initial moves to pre-play
188
- * @arg {string} [id='trax'] - an id used to differentiate tiles from multiple games
189
- */
292
+ /**
293
+ Create a new Trax game
294
+ @param {TraxVariant} [rules='trax'] - the variant to play
295
+ @param {string|string[]} [moves=''] - the initial moves to pre-play
296
+ @param {string} [id='trax'] - an id used to differentiate tiles from multiple games
297
+ */
190
298
  constructor(rules = 'trax', moves = '', id = 'trax') {
191
299
  if (!Trax.variants.has(rules)) rules = 'trax'
192
300
  this.id = String(id)
193
301
  this.rules = rules
194
- this.move = 0
195
302
  this.turn = 1
196
303
  this.over = false
197
304
  this.left = 1
198
305
  this.right = 0
199
306
  this.top = 1
200
307
  this.bottom = 0
201
- this.notation = ''
308
+ this.count = 0
309
+ /** @type {string[]} */
310
+ this.moves = []
311
+ /** @type {Map<TileId, Point>} */
312
+ this.frontier = new Map()
202
313
  /** @type {Record<TileId, Tile>} */
203
314
  this.tiles = {}
204
- /** @type TileId[] */
315
+ /** @type {TileId[]} */
205
316
  this.path = []
206
317
  this.invalid = false
207
318
  // Moves might be a notation string
@@ -210,10 +321,11 @@ export class Trax {
210
321
  }
211
322
  }
212
323
 
213
- /** Save the current game data to a variable.
214
- * @returns {SaveState} an opaque save state object
215
- * @see restore for restoring the state
216
- */
324
+ /**
325
+ Save the current game data to a variable.
326
+ @returns {SaveState} an opaque save state object
327
+ @see restore for restoring the state
328
+ */
217
329
  save() {
218
330
  // In order to evaluate potential moves, we have to modify the state
219
331
  return {
@@ -225,28 +337,30 @@ export class Trax {
225
337
  right: this.right,
226
338
  top: this.top,
227
339
  bottom: this.bottom,
228
- notation: this.notation,
340
+ moves: [...this.moves],
341
+ frontier: this.frontier.entries().toArray(),
229
342
  tiles: JSON.stringify(this.tiles),
230
343
  path: JSON.stringify(this.path),
231
344
  invalid: this.invalid,
232
345
  }
233
346
  }
234
347
 
235
- /** Restore a previously saved position.
236
- * @arg {SaveState} saved - the previously saved state
237
- * @see save for saving the state
238
- */
348
+ /**
349
+ Restore a previously saved position.
350
+ @param {SaveState} saved - the previously saved state
351
+ @see save for saving the state
352
+ */
239
353
  restore(saved) {
240
354
  // So we need to be able to restore it when we are done
241
355
  this.id = saved.id
242
- this.move = saved.move
243
356
  this.turn = saved.turn
244
357
  this.over = saved.over
245
358
  this.left = saved.left
246
359
  this.right = saved.right
247
360
  this.top = saved.top
248
361
  this.bottom = saved.bottom
249
- this.notation = saved.notation
362
+ this.moves = [...saved.moves]
363
+ this.frontier = new Map(saved.frontier)
250
364
  this.tiles = JSON.parse(saved.tiles)
251
365
  this.path = JSON.parse(saved.path)
252
366
  this.invalid = saved.invalid
@@ -254,6 +368,69 @@ export class Trax {
254
368
  for (const tile of Object.values(this.tiles)) {
255
369
  tile.loc = new Point(tile.loc)
256
370
  }
371
+
372
+ this.count = Object.keys(this.tiles).length
373
+ }
374
+
375
+ /**
376
+ Lightweight checkpoint for internal rollback. Valid only for ancestor restores.
377
+ Callers must be in a non-over, valid game state — rewind() restores those
378
+ fields to those fixed values rather than saving and restoring them.
379
+ @returns {Checkpoint} a lightweight snapshot of mutable board dimensions
380
+ @typedef {{ move: number, count: number, turn: number, left: number, right: number, top: number, bottom: number }} Checkpoint
381
+ */
382
+ checkpoint() {
383
+ return {
384
+ move: this.move,
385
+ count: this.count,
386
+ turn: this.turn,
387
+ left: this.left,
388
+ right: this.right,
389
+ top: this.top,
390
+ bottom: this.bottom,
391
+ }
392
+ }
393
+
394
+ /**
395
+ Rewind to a checkpoint. Only valid for ancestor restores.
396
+ @param {Checkpoint} cp - the checkpoint to rewind to
397
+ */
398
+ rewind(cp) {
399
+ this.count = cp.count
400
+ this.moves.length = cp.move
401
+ this.turn = cp.turn
402
+ this.left = cp.left
403
+ this.right = cp.right
404
+ this.top = cp.top
405
+ this.bottom = cp.bottom
406
+ this.over = false
407
+ this.invalid = false
408
+ this.path = []
409
+ /** @type {Tile[]} */
410
+ const deleted = []
411
+ for (const [key, tile] of Object.entries(this.tiles)) {
412
+ if (tile.move > cp.move) {
413
+ deleted.push(tile)
414
+ delete this.tiles[key]
415
+ }
416
+ }
417
+
418
+ for (const tile of deleted) {
419
+ if (tile.loc.around.some((adjacent) => this.tileAt(adjacent))) {
420
+ this.frontier.set(tile.id, tile.loc)
421
+ }
422
+
423
+ for (const neighbor of tile.loc.around) {
424
+ const neighborId = this.tileId(neighbor)
425
+ if (
426
+ !this.tileAt(neighbor) &&
427
+ this.frontier.has(neighborId) &&
428
+ neighbor.around.every((adjacent) => !this.tileAt(adjacent))
429
+ ) {
430
+ this.frontier.delete(neighborId)
431
+ }
432
+ }
433
+ }
257
434
  }
258
435
 
259
436
  /** The name of this variant. */
@@ -261,9 +438,9 @@ export class Trax {
261
438
  return Trax.names[this.rules]
262
439
  }
263
440
 
264
- /** The number of tiles currently in play. */
265
- get count() {
266
- return Object.keys(this.tiles).length
441
+ /** The move number of the last-played move. */
442
+ get move() {
443
+ return this.moves.length
267
444
  }
268
445
 
269
446
  /** The color of the current player, w or b. */
@@ -281,21 +458,23 @@ export class Trax {
281
458
  return this.over ? this.turn : false
282
459
  }
283
460
 
284
- /** Provides the tile ID for the tile at the given location.
285
- * @arg {PointLike} loc
286
- * returns {TileId}
287
- */
461
+ /**
462
+ Provides the tile ID for the tile at the given location.
463
+ @param {PointLike} loc - the location
464
+ @returns {TileId} a string key encoding this game's id and the location
465
+ */
288
466
  tileId(loc) {
289
467
  return this.id + '-' + String(loc.x) + 'x' + String(loc.y)
290
468
  }
291
469
 
292
- /** Add a tile to the board. Note that no validity checking is done here,
293
- * except that invalid tiles are not actually placed on the board, so this
294
- * should only be called externally.
295
- * @arg {TileType} type
296
- * @arg {Point} loc
297
- * @returns {Tile} the tile placed on the board.
298
- */
470
+ /**
471
+ Add a tile to the board. Note that no validity checking is done here,
472
+ except that invalid tiles are not actually placed on the board, so this
473
+ should only be called externally.
474
+ @param {TileType} type - letter 'a'-'f' for valid tiles, 'x' for invalid
475
+ @param {Point} loc - the location
476
+ @returns {Tile} the tile placed on the board
477
+ */
299
478
  addTile(type, loc) {
300
479
  const id = this.tileId(loc)
301
480
  /** @type {Tile} */
@@ -304,6 +483,15 @@ export class Trax {
304
483
  this.invalid = true // Invalid tile is being played
305
484
  } else {
306
485
  this.tiles[id] = tile
486
+ this.count++
487
+ this.frontier.delete(id)
488
+ for (const neighbor of loc.around) {
489
+ const neighborId = this.tileId(neighbor)
490
+ if (!Object.hasOwn(this.tiles, neighborId)) {
491
+ this.frontier.set(neighborId, neighbor)
492
+ }
493
+ }
494
+
307
495
  this.left = Math.min(this.left, loc.x)
308
496
  this.right = Math.max(this.right, loc.x)
309
497
  this.top = Math.min(this.top, loc.y)
@@ -313,17 +501,21 @@ export class Trax {
313
501
  return tile
314
502
  }
315
503
 
316
- /** The type of tile at the given location.
317
- * @arg {PointLike} loc - the location the tile is in
318
- * @returns {TileType|undefined} the type of tile, or undefined if no tile
319
- */
504
+ /**
505
+ The type of tile at the given location.
506
+ @param {PointLike} loc - the location the tile is in
507
+ @returns {ValidTiles|undefined} the type of tile, or undefined if no tile
508
+ */
320
509
  tileAt(loc) {
321
- return (this.tiles[this.tileId(loc)] || {}).type
510
+ return /** @type {ValidTiles | undefined} */ (
511
+ (this.tiles[this.tileId(loc)] || {}).type
512
+ )
322
513
  }
323
514
 
324
- /** Is this tile valid?
325
- * @type {(type: string, loc: Point) => type is ValidTiles}
326
- */
515
+ /**
516
+ Is this tile valid?
517
+ @type {(type: string, loc: Point) => type is ValidTiles}
518
+ */
327
519
  validTile(type, loc) {
328
520
  if (this.count === 0 && loc.x === 0 && loc.y === 0) {
329
521
  return type === 'd' || type === 'e'
@@ -342,13 +534,14 @@ export class Trax {
342
534
  )
343
535
  }
344
536
 
345
- /** Get a list of possible tiles that can be played in this location.
346
- * @arg {Point} loc - the location to check
347
- * @arg {string | boolean} s=false - if provided, a direction modifier, '/', '\\', or '+'
348
- * @returns {TileType[]}
349
- */
537
+ /**
538
+ Get a list of possible tiles that can be played in this location.
539
+ @param {Point} loc - the location to check
540
+ @param {string | boolean} [s=false] - if provided, a direction modifier, '/', '\\', or '+'
541
+ @returns {TileType[]} the list of valid tile types for this location
542
+ */
350
543
  possibleTiles(loc, s = false) {
351
- /** @type TileType[] */
544
+ /** @type {TileType[]} */
352
545
  const possibles = []
353
546
  for (const t of tileTypes) {
354
547
  if ((!s || s === slash[t]) && this.validTile(t, loc)) possibles.push(t)
@@ -365,10 +558,11 @@ export class Trax {
365
558
  return this.right - this.left + 1
366
559
  }
367
560
 
368
- /** Determine if a location is valid to play in.
369
- * @arg {Point} loc - the location to check
370
- * @returns {boolean}
371
- */
561
+ /**
562
+ Determine if a location is valid to play in.
563
+ @param {Point} loc - the location to check
564
+ @returns {boolean} true if a tile can be placed here
565
+ */
372
566
  validLocation(loc) {
373
567
  if (this.over) return false
374
568
  if (this.tileAt(loc)) return false
@@ -390,29 +584,23 @@ export class Trax {
390
584
  return false
391
585
  }
392
586
 
393
- /** Find all possible locations to play in. Note that these are not
394
- * necessarily valid locations, just empty ones that border existing tiles.
395
- */
587
+ /**
588
+ Find all possible locations to play in. Note that these are not
589
+ necessarily valid locations, just empty ones that border existing tiles.
590
+ @returns {Point[]} all frontier locations
591
+ */
396
592
  possibleLocations() {
397
593
  if (this.count === 0) return [zero]
398
- /** @type Record<TileId, Point> */
399
- const possibles = {}
400
-
401
- for (const tile of Object.values(this.tiles)) {
402
- for (const loc of tile.loc.around) {
403
- const id = this.tileId(loc)
404
- if (!(id in this.tiles)) possibles[id] = loc
405
- }
406
- }
407
-
408
- return Object.values(possibles)
594
+ return this.frontier.values().toArray()
409
595
  }
410
596
 
411
- /** Find all possible moves as a list of notations,
412
- * ie: ['@1+', '@1/', '@1\\', ...]
413
- *
414
- * Note that moves are not guaranteed to be valid.
415
- */
597
+ /**
598
+ Find all possible moves as a list of notations,
599
+ ie: ['@1+', '@1/', '@1\\', ...]
600
+
601
+ Note that moves are not guaranteed to be valid.
602
+ @returns {string[]} all valid move notations from the current position
603
+ */
416
604
  possibleMoves() {
417
605
  /** @type {string[]} */
418
606
  const possibles = []
@@ -427,10 +615,11 @@ export class Trax {
427
615
  return possibles
428
616
  }
429
617
 
430
- /** Called after a move was just played, to determine the forced moves.
431
- * @arg {Point} loc - the location just played at
432
- * @returns {Tile[]} a list of tiles that should be added as part of the move
433
- */
618
+ /**
619
+ Called after a move was just played, to determine the forced moves.
620
+ @param {Point} loc - the location just played at
621
+ @returns {Tile[]} a list of tiles that should be added as part of the move
622
+ */
434
623
  forcedMoves(loc) {
435
624
  /** @type {Tile[]} */
436
625
  const forced = []
@@ -438,7 +627,7 @@ export class Trax {
438
627
  const invalids = {}
439
628
  let check = loc.around
440
629
  while (check.length > 0) {
441
- const pos = check.shift()
630
+ const pos = /** @type {Point} */ (check.shift())
442
631
  if (this.validLocation(pos)) {
443
632
  const possibles = this.possibleTiles(pos)
444
633
  if (possibles.length === 1) {
@@ -448,7 +637,7 @@ export class Trax {
448
637
  } else if (possibles.length === 0) {
449
638
  // No possible moves at all
450
639
  const invalid = this.addTile('x', pos)
451
- if (!invalids[invalid.id]) forced.push(invalid)
640
+ if (!Object.hasOwn(invalids, invalid.id)) forced.push(invalid)
452
641
  invalids[invalid.id] = true
453
642
  }
454
643
  }
@@ -457,12 +646,13 @@ export class Trax {
457
646
  return forced
458
647
  }
459
648
 
460
- /** Determine the notation for a move. Note that this must be determined
461
- * BEFORE the move is placed on the board.
462
- * @arg {ValidTiles} type - the tile placed on the board
463
- * @arg {Point} loc - the location the tile is placed
464
- * @returns {string} the notation of the move
465
- */
649
+ /**
650
+ Determine the notation for a move. Note that this must be determined
651
+ BEFORE the move is placed on the board.
652
+ @param {ValidTiles} type - the tile placed on the board
653
+ @param {Point} loc - the location the tile is placed
654
+ @returns {string} the notation of the move
655
+ */
466
656
  notate(type, loc) {
467
657
  let notation = Trax.encodeCol(loc.x - this.left + 1)
468
658
  notation += String(loc.y - this.top + 1)
@@ -470,34 +660,37 @@ export class Trax {
470
660
  return notation
471
661
  }
472
662
 
473
- /** Turn a move notation into a tile type and location.
474
- * @arg {string} notation - a notation for a single move to be played at
475
- * the current board position
476
- * @returns {RawMove} the tile type and location of the move
477
- */
663
+ /**
664
+ Turn a move notation into a tile type and location.
665
+ @param {string} notation - a notation for a single move to be played at
666
+ the current board position
667
+ @returns {RawMove} the tile type and location of the move
668
+ */
478
669
  decodeNotation(notation) {
479
670
  /** @type {RawMove} */
480
671
  const bad = { type: 'x', loc: zero }
481
672
  const match = notation.match(notationRegex)
482
673
  if (match === null) return bad
483
674
  const loc = new Point(
484
- this.left - 1 + Trax.decodeCol(match[1]),
485
- this.top - 1 + Number(match[2]),
675
+ this.left - 1 + Trax.decodeCol(match.groups.col),
676
+ this.top - 1 + Number(match.groups.row),
486
677
  )
487
- const possible = this.possibleTiles(loc, match[3])
678
+ const possible = this.possibleTiles(loc, match.groups.slash)
488
679
  if (possible.length > 0) return { type: possible[0], loc }
489
680
  return bad
490
681
  }
491
682
 
492
- /** Follow a color from one location through one or more tiles to the other
493
- * end of the color line.
494
- * @arg {Color} color - the color to follow
495
- * @arg {Point} loc - the location to start
496
- * @arg {string} from - the edge of the tile to start from
497
- * @returns {LineEnd} the ending location and a list of the tile ids the path
498
- * takes to get there.
499
- */
683
+ /**
684
+ Follow a color from one location through one or more tiles to the other
685
+ end of the color line.
686
+ @param {Color} color - the color to follow
687
+ @param {Point} loc - the location to start
688
+ @param {string} from - the edge of the tile to start from
689
+ @returns {LineEnd} the ending location and a list of the tile ids the path
690
+ takes to get there
691
+ */
500
692
  follow(color, loc, from) {
693
+ /** @type {TileId[]} */
501
694
  const path = []
502
695
  let id = this.tileId(loc)
503
696
  let tile = this.tileAt(loc)
@@ -524,11 +717,12 @@ export class Trax {
524
717
  return { loc, path }
525
718
  }
526
719
 
527
- /** Given a tile and a color, follow the line for that color to each end.
528
- * @arg {Color} color - the color of ends of interest
529
- * @arg { Point} loc - the location of the tile of interest
530
- * @returns {LineEnd[]} a list of two items, each one end of the line
531
- */
720
+ /**
721
+ Given a tile and a color, follow the line for that color to each end.
722
+ @param {Color} color - the color of ends of interest
723
+ @param {Point} loc - the location of the tile of interest
724
+ @returns {LineEnd[]} a list of two items, each one end of the line
725
+ */
532
726
  findEnds(color, loc) {
533
727
  /** @type {LineEnd[]} */
534
728
  const ends = []
@@ -540,11 +734,12 @@ export class Trax {
540
734
  return ends
541
735
  }
542
736
 
543
- /** Determine if a given line ends the game.
544
- * @arg {Point} locA - one end of the line
545
- * @arg {Point} locB - the other end of the line
546
- * @returns {boolean} - true if this line wins the game
547
- */
737
+ /**
738
+ Determine if a given line ends the game.
739
+ @param {Point} locA - one end of the line
740
+ @param {Point} locB - the other end of the line
741
+ @returns {boolean} true if this line wins the game
742
+ */
548
743
  lineWin(locA, locB) {
549
744
  if (this.rules === 'traxloop') return false
550
745
  if (this.width > 7 && locA.distX(locB) > this.width) return true
@@ -552,26 +747,32 @@ export class Trax {
552
747
  return false
553
748
  }
554
749
 
555
- /** Determine if the game has ended.
556
- * @arg {Tile[]} tiles - a list of the tiles placed during the last move.
557
- */
750
+ /**
751
+ Determine if the game has ended.
752
+ @param {Tile[]} tiles - a list of the tiles placed during the last move
753
+ */
558
754
  checkWin(tiles) {
559
755
  /** @type {Color|false} */
560
756
  let winner = false
561
757
  const colors = [this.color, Trax.other(this.color)] // Check win for current player first
562
758
  for (const color of colors) {
563
759
  if (winner) continue // Skip the second player if the first has won
564
- for (const tile of tiles) {
565
- if (winner) continue // Skip the remaining tiles once we find a win
760
+ tiles.some((tile) => {
566
761
  const [end1, end2] = this.findEnds(color, tile.loc)
567
762
  if (end1.loc.eq(end2.loc)) {
568
763
  winner = color // Loop win
569
- this.path = end1.path // Both paths are essentially the same
570
- } else if (this.lineWin(end1.loc, end2.loc)) {
764
+ this.path = end1.path
765
+ return true
766
+ }
767
+
768
+ if (this.lineWin(end1.loc, end2.loc)) {
571
769
  winner = color // Line win, paths need to be combined
572
- this.path = [...end1.path.reverse(), ...end2.path.slice(1)]
770
+ this.path = [...end1.path.toReversed(), ...end2.path.slice(1)]
771
+ return true
573
772
  }
574
- }
773
+
774
+ return false
775
+ })
575
776
  }
576
777
 
577
778
  if (winner) {
@@ -589,13 +790,14 @@ export class Trax {
589
790
  }
590
791
  }
591
792
 
592
- /** Play a move. Can be called either as:
593
- * - play(moveNumber, notation) to ensure move safety, or
594
- * - play(notation) for quicker access.
595
- * @arg moveNumber {(number|string)} the move number or the notation
596
- * @arg {string} [notation] - the notation, if a move number was provided
597
- * @returns {{dropped: Tile[], notation: string, valid: boolean}}
598
- */
793
+ /**
794
+ Play a move. Can be called either as:
795
+ - play(moveNumber, notation) to ensure move safety, or
796
+ - play(notation) for quicker access.
797
+ @param {number|string} moveNumber - the move number or the notation
798
+ @param {string} [notation] - the notation, if a move number was provided
799
+ @returns {{dropped: Tile[], notation: string, valid: boolean}} the result of the move
800
+ */
599
801
  play(moveNumber, notation) {
600
802
  // Generic API call for playing a move with move number safety
601
803
  // or, alternately, pass only a single value, notation, for a quick play
@@ -610,11 +812,12 @@ export class Trax {
610
812
  return this.dropTile(notation)
611
813
  }
612
814
 
613
- /** Play one or more moves.
614
- * @arg {string|string[]} moves - the list of moves, provided either as a
615
- * space-separated string of notations, or as a list of notations. Move
616
- * numbers are optional, but if provided will be checked for accuracy.
617
- */
815
+ /**
816
+ Play one or more moves.
817
+ @param {string|string[]} moves - the list of moves, provided either as a
818
+ space-separated string of notations, or as a list of notations. Move
819
+ numbers are optional, but if provided will be checked for accuracy.
820
+ */
618
821
  playMoves(moves) {
619
822
  // Play one or more moves, from a string or a list of moves
620
823
  // If move numbers are included, ensure they are accurate
@@ -622,12 +825,12 @@ export class Trax {
622
825
  moves = moves.join(' ')
623
826
  }
624
827
 
625
- moves = moves.replaceAll('\n', ' ').split(/\s+/)
828
+ moves = moves.replaceAll('\n', ' ').split(/\s+/v)
626
829
 
627
830
  let moveNumber = 0
628
831
  for (const move of moves) {
629
832
  if (moveNumberRegex.test(move)) {
630
- moveNumber = Number(move.replaceAll(/\D/g, ''))
833
+ moveNumber = Number(move.replaceAll(/\D/gv, ''))
631
834
  }
632
835
 
633
836
  if (notationRegex.test(move)) {
@@ -642,36 +845,38 @@ export class Trax {
642
845
  }
643
846
  }
644
847
 
645
- /** Add the current move notation to the notation string
646
- * @arg {string} notation - the notation of the current move
647
- */
648
- updateNotation(notation) {
649
- this.move++
650
- let note = this.move + '. ' + notation
651
- const line = this.notation.split('\n').pop()
652
- if (line) {
653
- note = (line.length + note.length > 79 ? '\n' : ' ') + note
848
+ /** The formatted notation string for the current game, wrapped at 80 columns. */
849
+ get notation() {
850
+ let result = ''
851
+ let lineLength = 0
852
+ for (const [i, move] of this.moves.entries()) {
853
+ const note = `${i + 1}. ${move}`
854
+ if (result) {
855
+ if (lineLength + 1 + note.length > 79) {
856
+ result += `\n${note}`
857
+ lineLength = note.length
858
+ } else {
859
+ result += ` ${note}`
860
+ lineLength += 1 + note.length
861
+ }
862
+ } else {
863
+ result = note
864
+ lineLength = note.length
865
+ }
654
866
  }
655
867
 
656
- this.notation += note
868
+ return result
657
869
  }
658
870
 
659
- /** An array of the moves made in the game. */
660
- get moves() {
661
- return this.notation
662
- .replaceAll('\n', ' ')
663
- .split(' ')
664
- .filter((n) => Boolean(n) && !n.endsWith('.'))
665
- }
666
-
667
- /** Drop a tile onto the board. This is a lower level call. Use play() if possible instead.
668
- * @arg {string|ValidTiles} type - a special move, a tile type, or a notation
669
- * @arg {Point} [loc] - a location if type is a tile type
670
- * @arg {string|boolean} [tentative] - if truthy, the move will not be saved
671
- * @returns {TileDrop} an object representing the results of the drop
672
- */
871
+ /**
872
+ Drop a tile onto the board. This is a lower level call. Use play() if possible instead.
873
+ @param {string|ValidTiles} type - a special move, a tile type, or a notation
874
+ @param {Point} [loc] - a location if type is a tile type
875
+ @param {string|boolean} [tentative] - if truthy, the move will not be saved
876
+ @returns {TileDrop} an object representing the results of the drop
877
+ */
673
878
  dropTile(type, loc, tentative) {
674
- /** @type Tile[] */
879
+ /** @type {Tile[]} */
675
880
  let dropped = []
676
881
  let notation = ''
677
882
  let valid = false
@@ -684,7 +889,7 @@ export class Trax {
684
889
  if (type === 'draw') this.turn = 0 // Draw means we both win
685
890
  }
686
891
 
687
- this.updateNotation(type)
892
+ this.moves.push(type)
688
893
  notation = type
689
894
  valid = true
690
895
  } else {
@@ -697,14 +902,14 @@ export class Trax {
697
902
 
698
903
  if (loc && this.validLocation(loc) && this.validTile(type, loc)) {
699
904
  valid = true
700
- const saved = this.save() // Just in case we need to roll back
905
+ const cp = this.checkpoint() // Just in case we need to roll back
701
906
  notation = this.notate(type, loc) // Determine notation BEFORE we play the move
702
- this.updateNotation(notation)
907
+ this.moves.push(notation)
703
908
  dropped.push(this.addTile(type, loc)) // Play the tile
704
909
  dropped = [...dropped, ...this.forcedMoves(loc)] // Play all forced moves
705
910
  if (this.invalid || tentative) {
706
911
  if (this.invalid) valid = false
707
- this.restore(saved) // Abort! abort!
912
+ this.rewind(cp) // Abort! abort!
708
913
  } else {
709
914
  this.checkWin(dropped)
710
915
  if (!this.over) this.turn = this.turn === 1 ? 2 : 1
@@ -715,42 +920,44 @@ export class Trax {
715
920
  return { dropped, notation, valid }
716
921
  }
717
922
 
718
- /** Symmetry helper. Rotates a move around the board in case we are trying to
719
- * play a symmetrical rather than an exact move.
720
- * @arg {string} move - the notation of the move to be rotated
721
- * @returns {string[]} the four rotations of this move
722
- */
923
+ /**
924
+ Symmetry helper. Rotates a move around the board in case we are trying to
925
+ play a symmetrical rather than an exact move.
926
+ @param {string} move - the notation of the move to be rotated
927
+ @returns {string[]} the four rotations of this move
928
+ */
723
929
  moveRotations(move) {
724
930
  // Symmetry helper, rotate a move around the board
725
931
  const match = move.match(notationRegex)
726
932
  if (match === null) return []
727
- const x = Trax.decodeCol(match[1])
728
- const y = Number(match[2])
933
+ const x = Trax.decodeCol(match.groups.col)
934
+ const y = Number(match.groups.row)
729
935
  const X = this.height - x + 1
730
936
  const Y = this.width - y + 1
731
- const s = match[3]
937
+ const s = match.groups.slash
732
938
  const slashes = s === '+' ? [s] : ['/', '\\']
733
939
  const moves = new Set()
734
940
  // This isn't super precise, but it limits the search space ... maybe #TODO?
735
- for (const slash of slashes) {
736
- moves.add(Trax.encodeCol(x) + String(y) + slash)
737
- moves.add(Trax.encodeCol(y) + String(x) + slash)
738
- moves.add(Trax.encodeCol(X) + String(Y) + slash)
739
- moves.add(Trax.encodeCol(Y) + String(X) + slash)
941
+ for (const slashChar of slashes) {
942
+ moves.add(Trax.encodeCol(x) + String(y) + slashChar)
943
+ moves.add(Trax.encodeCol(y) + String(x) + slashChar)
944
+ moves.add(Trax.encodeCol(X) + String(Y) + slashChar)
945
+ moves.add(Trax.encodeCol(Y) + String(X) + slashChar)
740
946
  }
741
947
 
742
948
  return [...moves]
743
949
  }
744
950
 
745
- /** Play a provisional move if it is valid.
746
- * @arg {string} from - the normalized encoding of the starting position
747
- * @arg {string} to - the normalized encoding of the ending position
748
- * @arg {string} via - the move to be used to transition
749
- * @returns {false|string} if the provisional move is invalid: false; if the
750
- * provisional move will never be valid for any future moves:
751
- * 'delete-provisional'; if the provisional move is valid, the correct
752
- * notation, which may be symmetrically adjusted as needed
753
- */
951
+ /**
952
+ Play a provisional move if it is valid.
953
+ @param {string} from - the normalized encoding of the starting position
954
+ @param {string} to - the normalized encoding of the ending position
955
+ @param {string} via - the move to be used to transition
956
+ @returns {false|string} if the provisional move is invalid: false; if the
957
+ provisional move will never be valid for any future moves:
958
+ 'delete-provisional'; if the provisional move is valid, the correct
959
+ notation, which may be symmetrically adjusted as needed
960
+ */
754
961
  provisionalMove(from, to, via) {
755
962
  // Test a provisional move against the current state
756
963
  const lines = from.slice(1).split(':')
@@ -768,13 +975,13 @@ export class Trax {
768
975
  }
769
976
 
770
977
  if (from === this.normalized) {
771
- const saved = this.save() // Save this position
978
+ const cp = this.checkpoint() // Save this position
772
979
  for (const move of this.moveRotations(via)) {
773
980
  // Try all symmetrical moves
774
981
  const drop = this.dropTile(move)
775
982
  if (drop.valid) {
776
983
  if (to === this.normalized) return move // This move matched!
777
- this.restore(saved) // Put the board back how it was
984
+ this.rewind(cp) // Put the board back how it was
778
985
  }
779
986
  }
780
987
  }
@@ -782,31 +989,34 @@ export class Trax {
782
989
  return false
783
990
  }
784
991
 
785
- /** Get an encoded representation of the current position, useful for drawing
786
- * the board without having to do much analysis.
787
- * @returns {string} the current position code
788
- */
992
+ /**
993
+ Get an encoded representation of the current position, useful for drawing
994
+ the board without having to do much analysis.
995
+ @returns {string} the current position code
996
+ */
789
997
  get icon() {
790
998
  return this.positionCode()
791
999
  }
792
1000
 
793
- /** Get an encoded representation of the current position, with a set of
794
- * tiles highlighted differently, useful for showing the effects of a move.
795
- * @arg {TileDrop} drops - the drops of the most recent play
796
- * @returns {string} the current position code, with drops highlighted
797
- */
1001
+ /**
1002
+ Get an encoded representation of the current position, with a set of
1003
+ tiles highlighted differently, useful for showing the effects of a move.
1004
+ @param {TileDrop} drops - the drops of the most recent play
1005
+ @returns {string} the current position code, with drops highlighted
1006
+ */
798
1007
  dropsIcon(drops) {
799
1008
  return this.positionCode(undefined, undefined, undefined, drops)
800
1009
  }
801
1010
 
802
- /** Symmetry helper, draw the board from different angles.
803
- * @arg {boolean} [rightToLeft] - reverse order horizontally
804
- * @arg {boolean} [bottomToTop] - reverse order vertically
805
- * @arg {boolean} [rotate] - rotate the tiles by 90 degrees
806
- * @arg {TileDrop} [drops] - the drops of the most recent play, if you want
807
- * them highlighted
808
- * @returns {string} an encoding of the position
809
- */
1011
+ /**
1012
+ Symmetry helper, draw the board from different angles.
1013
+ @param {boolean} [rightToLeft] - reverse order horizontally
1014
+ @param {boolean} [bottomToTop] - reverse order vertically
1015
+ @param {boolean} [rotate] - rotate the tiles by 90 degrees
1016
+ @param {TileDrop} [drops] - the drops of the most recent play, if you want
1017
+ them highlighted
1018
+ @returns {string} an encoding of the position
1019
+ */
810
1020
  positionCode(rightToLeft, bottomToTop, rotate, drops) {
811
1021
  const startX = rightToLeft ? this.right : this.left
812
1022
  const startY = bottomToTop ? this.bottom : this.top
@@ -820,10 +1030,12 @@ export class Trax {
820
1030
  const loc = new Point(x, y)
821
1031
  const tile = this.tileAt(loc) // Normal tile for normal orientation
822
1032
  if (tile) {
823
- /** @type string */
1033
+ /** @type {string} */
824
1034
  let encoded = applySymmetry(tile, rightToLeft, bottomToTop, rotate)
825
1035
  if (this.path.includes(this.tileId(loc))) {
826
- encoded = String.fromCodePoint(encoded.codePointAt(0) + this.turn * 6)
1036
+ encoded = String.fromCodePoint(
1037
+ /** @type {number} */ (encoded.codePointAt(0)) + this.turn * 6,
1038
+ )
827
1039
  }
828
1040
 
829
1041
  if (
@@ -846,7 +1058,7 @@ export class Trax {
846
1058
  y = startY
847
1059
  x += incX
848
1060
  // Remove final number and add to code
849
- code.push(line.replace(/\d+$/, ''))
1061
+ code.push(line.replace(/\d$/v, ''))
850
1062
  line = ''
851
1063
  }
852
1064
  } else {
@@ -856,7 +1068,7 @@ export class Trax {
856
1068
  x = startX
857
1069
  y += incY
858
1070
  // Remove final number and add to code
859
- code.push(line.replace(/\d+$/, ''))
1071
+ code.push(line.replace(/\d$/v, ''))
860
1072
  line = ''
861
1073
  }
862
1074
  }
@@ -871,10 +1083,11 @@ export class Trax {
871
1083
  return code.join(':')
872
1084
  }
873
1085
 
874
- /** Trax has the potential for symmetry, so this gives us the ability to
875
- * examine horizontal, vertical, and rotational symmetry for a color.
876
- * @returns {string} a position code that matches all symmetrical positions
877
- */
1086
+ /**
1087
+ Trax has the potential for symmetry, so this gives us the ability to
1088
+ examine horizontal, vertical, and rotational symmetry for a color.
1089
+ @returns {string} a position code that matches all symmetrical positions
1090
+ */
878
1091
  normalize() {
879
1092
  let norm = 'z'
880
1093
  const bools = [true, false]
@@ -890,9 +1103,11 @@ export class Trax {
890
1103
  return (this.color || 't').toUpperCase() + norm
891
1104
  }
892
1105
 
893
- /** Get the normalized code for this position. All symmetrical positions will
894
- * result in the same normalized code.
895
- */
1106
+ /**
1107
+ Get the normalized code for this position. All symmetrical positions will
1108
+ result in the same normalized code.
1109
+ @returns {string} the normalized position code
1110
+ */
896
1111
  get normalized() {
897
1112
  return this.normalize()
898
1113
  }