@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/RELEASE.v1.2.0.md +10 -0
- package/benchmark/benchmarks.json +25 -0
- package/benchmark/engine.bench.js +228 -0
- package/gl-sast-report.json +1 -1
- package/package.json +5 -4
- package/src/engine.d.ts +321 -178
- package/src/engine.js +460 -336
- package/src/version.js +1 -1
- package/RELEASE.v1.1.1.md +0 -5
package/src/engine.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
/**
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
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
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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 =
|
|
88
|
-
const notationRegex = /^([@a-z]+)(
|
|
89
|
-
|
|
90
|
-
/**
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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
|
-
/**
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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
|
-
/**
|
|
112
|
-
|
|
113
|
-
|
|
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
|
-
/**
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
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
|
-
/**
|
|
142
|
-
|
|
143
|
-
|
|
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
|
-
/**
|
|
149
|
-
|
|
150
|
-
|
|
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
|
-
/**
|
|
156
|
-
|
|
157
|
-
|
|
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
|
-
/**
|
|
163
|
-
|
|
164
|
-
|
|
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
|
-
/**
|
|
170
|
-
|
|
171
|
-
|
|
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
|
-
/**
|
|
185
|
-
|
|
186
|
-
|
|
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/
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
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
|
-
/**
|
|
213
|
-
|
|
214
|
-
|
|
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
|
-
/**
|
|
222
|
-
|
|
223
|
-
|
|
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
|
-
/**
|
|
231
|
-
|
|
232
|
-
|
|
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
|
-
/**
|
|
240
|
-
|
|
241
|
-
|
|
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
|
-
/**
|
|
254
|
-
|
|
255
|
-
|
|
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
|
-
/**
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
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.
|
|
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
|
-
/**
|
|
300
|
-
|
|
301
|
-
|
|
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
|
-
|
|
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
|
-
/**
|
|
322
|
-
|
|
323
|
-
|
|
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.
|
|
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
|
|
351
|
-
get
|
|
352
|
-
return
|
|
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
|
-
/**
|
|
371
|
-
|
|
372
|
-
|
|
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
|
-
/**
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
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
|
-
/**
|
|
403
|
-
|
|
404
|
-
|
|
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
|
-
/**
|
|
413
|
-
|
|
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
|
-
/**
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
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
|
-
/**
|
|
457
|
-
|
|
458
|
-
|
|
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
|
-
/**
|
|
482
|
-
|
|
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
|
-
|
|
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
|
-
/**
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
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
|
-
/**
|
|
519
|
-
|
|
520
|
-
|
|
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
|
|
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
|
-
/**
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
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
|
-
/**
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
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
|
|
573
|
-
this.top - 1 + Number(match
|
|
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
|
|
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
|
-
/**
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
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
|
-
/**
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
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
|
-
/**
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
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
|
-
/**
|
|
645
|
-
|
|
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
|
-
|
|
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
|
|
659
|
-
|
|
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.
|
|
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
|
-
/**
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
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
|
-
/**
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
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/
|
|
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
|
-
/**
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
this.
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
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
|
-
|
|
868
|
+
return result
|
|
746
869
|
}
|
|
747
870
|
|
|
748
|
-
/**
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
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.
|
|
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
|
|
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.
|
|
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.
|
|
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
|
-
/**
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
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
|
|
817
|
-
const y = Number(match
|
|
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
|
|
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
|
|
825
|
-
moves.add(Trax.encodeCol(x) + String(y) +
|
|
826
|
-
moves.add(Trax.encodeCol(y) + String(x) +
|
|
827
|
-
moves.add(Trax.encodeCol(X) + String(Y) +
|
|
828
|
-
moves.add(Trax.encodeCol(Y) + String(X) +
|
|
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
|
-
/**
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
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
|
|
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.
|
|
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
|
-
/**
|
|
875
|
-
|
|
876
|
-
|
|
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
|
-
/**
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
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
|
-
/**
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
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
|
-
/**
|
|
966
|
-
|
|
967
|
-
|
|
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
|
-
/**
|
|
985
|
-
|
|
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
|
}
|