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