@slugbugblue/trax 0.5.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +64 -24
- package/README.md +121 -70
- package/docs/engine.md +357 -7
- package/docs/point.md +134 -0
- package/package.json +22 -7
- package/src/cli.js +623 -0
- package/src/cmds/delete.js +71 -0
- package/src/cmds/help.js +81 -0
- package/src/cmds/import-export.js +250 -0
- package/src/cmds/list.js +155 -0
- package/src/cmds/new.js +131 -0
- package/src/cmds/play-try.js +129 -0
- package/src/cmds/select.js +47 -0
- package/src/cmds/undo.js +40 -0
- package/src/cmds/view.js +53 -0
- package/{engine.js → src/engine.js} +147 -125
- package/src/point.js +116 -0
- package/{tty.js → src/tty.js} +101 -32
- package/src/version.js +2 -0
- package/.eslintrc.backup.yaml +0 -68
- package/jsconfig.json +0 -8
|
@@ -15,13 +15,16 @@
|
|
|
15
15
|
|
|
16
16
|
// Trax internals. Woot.
|
|
17
17
|
|
|
18
|
-
//
|
|
18
|
+
// Tile type names are determined by listing the color of line at each edge, clockwise
|
|
19
19
|
// and then sorted alphabetically and given a single letter name, so 'bbww' becomes 'a'
|
|
20
20
|
// which gives us six different tile names: a-f
|
|
21
21
|
|
|
22
|
-
|
|
22
|
+
import { Point } from './point.js'
|
|
23
23
|
|
|
24
|
-
|
|
24
|
+
// Fun trax helper constants
|
|
25
|
+
|
|
26
|
+
const zero = new Point(0, 0)
|
|
27
|
+
const notationRegex = /^([@a-z]+)(\d+)([/\\+])$/i
|
|
25
28
|
|
|
26
29
|
const slash = { a: '\\', b: '+', c: '/', d: '/', e: '+', f: '\\' }
|
|
27
30
|
|
|
@@ -51,18 +54,14 @@ const rightColor = (t) =>
|
|
|
51
54
|
'ade'.includes(t) ? 'b' : 'bcf'.includes(t) ? 'w' : false
|
|
52
55
|
const leftColor = (t) =>
|
|
53
56
|
'cef'.includes(t) ? 'b' : 'abd'.includes(t) ? 'w' : false
|
|
54
|
-
const squaresAround = (x, y) => [
|
|
55
|
-
{ x: x - 1, y },
|
|
56
|
-
{ x: x + 1, y },
|
|
57
|
-
{ x, y: y - 1 },
|
|
58
|
-
{ x, y: y + 1 },
|
|
59
|
-
]
|
|
60
57
|
|
|
61
58
|
const addBlank = (code) => {
|
|
62
59
|
// Encode missing tiles as digits 0-9, for 1-10 missing
|
|
63
60
|
const lastNumber = code.codePointAt(code.length - 1) // This is NaN if code is empty, which is ok
|
|
64
|
-
if (lastNumber > 47 && lastNumber < 57)
|
|
61
|
+
if (lastNumber > 47 && lastNumber < 57) {
|
|
65
62
|
return code.slice(0, -1) + String.fromCodePoint(lastNumber + 1)
|
|
63
|
+
}
|
|
64
|
+
|
|
66
65
|
return code + '0'
|
|
67
66
|
}
|
|
68
67
|
|
|
@@ -73,9 +72,16 @@ const codeRowLength = (row) =>
|
|
|
73
72
|
// This is where the magic happens
|
|
74
73
|
export const Trax = class {
|
|
75
74
|
// Static class properties
|
|
76
|
-
static
|
|
75
|
+
static names = {
|
|
76
|
+
trax: 'Trax',
|
|
77
|
+
traxloop: 'Loop Trax',
|
|
78
|
+
trax8: '8x8 Trax',
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
static variants = new Set(Object.keys(this.names)) // Known variants
|
|
77
82
|
|
|
78
83
|
// Static class methods
|
|
84
|
+
static point = (x, y) => new Point(x, y)
|
|
79
85
|
static colorOf = (playerNumber) => ({ 1: 'w', 2: 'b' }[playerNumber])
|
|
80
86
|
static playerNumber = (color) => ({ b: 2, w: 1 }[color])
|
|
81
87
|
static other = (color) => ({ b: 'w', w: 'b' }[color])
|
|
@@ -121,13 +127,11 @@ export const Trax = class {
|
|
|
121
127
|
this.invalid = false
|
|
122
128
|
// Moves might be a notation string
|
|
123
129
|
if (moves) {
|
|
124
|
-
if (typeof moves === 'string') moves = moves.split(
|
|
125
|
-
for (
|
|
126
|
-
if (
|
|
127
|
-
|
|
130
|
+
if (typeof moves === 'string') moves = moves.split(/\s+/)
|
|
131
|
+
for (const move of moves) {
|
|
132
|
+
if (notationRegex.test(move)) {
|
|
133
|
+
this.dropTile(move)
|
|
128
134
|
}
|
|
129
|
-
|
|
130
|
-
this.playMove(move.move, move.notation)
|
|
131
135
|
}
|
|
132
136
|
}
|
|
133
137
|
}
|
|
@@ -162,6 +166,15 @@ export const Trax = class {
|
|
|
162
166
|
this.tiles = JSON.parse(saved.tiles)
|
|
163
167
|
this.path = JSON.parse(saved.path)
|
|
164
168
|
this.invalid = saved.invalid
|
|
169
|
+
// Reconstitute the locations
|
|
170
|
+
for (const tile of Object.values(this.tiles)) {
|
|
171
|
+
tile.loc = new Point(tile.loc)
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
get name() {
|
|
176
|
+
// Get the name of this variant
|
|
177
|
+
return Trax.names[this.rules]
|
|
165
178
|
}
|
|
166
179
|
|
|
167
180
|
get count() {
|
|
@@ -184,39 +197,42 @@ export const Trax = class {
|
|
|
184
197
|
return this.over ? this.turn : false // Or 1 or 2 for a win, or 0 for a tie
|
|
185
198
|
}
|
|
186
199
|
|
|
187
|
-
tileId(
|
|
188
|
-
return this.id + '-' + String(x) + 'x' + String(y)
|
|
200
|
+
tileId(loc) {
|
|
201
|
+
return this.id + '-' + String(loc.x) + 'x' + String(loc.y)
|
|
189
202
|
}
|
|
190
203
|
|
|
191
|
-
addTile(type,
|
|
204
|
+
addTile(type, loc) {
|
|
192
205
|
// Note: no validity checking is done here
|
|
193
|
-
const id = this.tileId(
|
|
194
|
-
const tile = { type,
|
|
206
|
+
const id = this.tileId(loc)
|
|
207
|
+
const tile = { type, loc, id, move: this.move, seq: this.count }
|
|
195
208
|
if (type === 'x') {
|
|
196
209
|
this.invalid = true // Invalid tile is being played
|
|
197
210
|
} else {
|
|
198
211
|
this.tiles[id] = tile
|
|
199
|
-
this.left = Math.min(this.left, x)
|
|
200
|
-
this.right = Math.max(this.right, x)
|
|
201
|
-
this.top = Math.min(this.top, y)
|
|
202
|
-
this.bottom = Math.max(this.bottom, y)
|
|
212
|
+
this.left = Math.min(this.left, loc.x)
|
|
213
|
+
this.right = Math.max(this.right, loc.x)
|
|
214
|
+
this.top = Math.min(this.top, loc.y)
|
|
215
|
+
this.bottom = Math.max(this.bottom, loc.y)
|
|
203
216
|
}
|
|
204
217
|
|
|
205
218
|
return tile
|
|
206
219
|
}
|
|
207
220
|
|
|
208
|
-
tileAt(
|
|
209
|
-
return (this.tiles[this.tileId(
|
|
221
|
+
tileAt(loc) {
|
|
222
|
+
return (this.tiles[this.tileId(loc)] || {}).type
|
|
210
223
|
}
|
|
211
224
|
|
|
212
|
-
validTile(type,
|
|
213
|
-
if (this.count === 0 && x === 0 && y === 0)
|
|
225
|
+
validTile(type, loc) {
|
|
226
|
+
if (this.count === 0 && loc.x === 0 && loc.y === 0) {
|
|
214
227
|
return type === 'd' || type === 'e'
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
const
|
|
218
|
-
const
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const up = downColor(this.tileAt(loc.up))
|
|
231
|
+
const down = upColor(this.tileAt(loc.down))
|
|
232
|
+
const left = rightColor(this.tileAt(loc.left))
|
|
233
|
+
const right = leftColor(this.tileAt(loc.right))
|
|
219
234
|
return (
|
|
235
|
+
(up || down || left || right) &&
|
|
220
236
|
(!up || up === upColor(type)) &&
|
|
221
237
|
(!down || down === downColor(type)) &&
|
|
222
238
|
(!left || left === leftColor(type)) &&
|
|
@@ -224,11 +240,11 @@ export const Trax = class {
|
|
|
224
240
|
)
|
|
225
241
|
}
|
|
226
242
|
|
|
227
|
-
possibleTiles(
|
|
243
|
+
possibleTiles(loc, s = false) {
|
|
228
244
|
// S: slash (/ or \ or +)
|
|
229
245
|
const possibles = []
|
|
230
246
|
for (const t of ['a', 'b', 'c', 'd', 'e', 'f']) {
|
|
231
|
-
if ((!s || s === slash[t]) && this.validTile(t,
|
|
247
|
+
if ((!s || s === slash[t]) && this.validTile(t, loc)) possibles.push(t)
|
|
232
248
|
}
|
|
233
249
|
|
|
234
250
|
return possibles
|
|
@@ -242,41 +258,42 @@ export const Trax = class {
|
|
|
242
258
|
return this.right - this.left + 1
|
|
243
259
|
}
|
|
244
260
|
|
|
245
|
-
validLocation(
|
|
261
|
+
validLocation(loc) {
|
|
246
262
|
if (this.over) return false
|
|
247
|
-
if (this.tileAt(
|
|
248
|
-
if (this.count === 0 &&
|
|
263
|
+
if (this.tileAt(loc)) return false
|
|
264
|
+
if (this.count === 0 && loc.eq(zero)) return true
|
|
249
265
|
if (this.rules === 'trax8') {
|
|
250
|
-
if (this.width > 7 && (x < this.left || x > this.right))
|
|
251
|
-
|
|
266
|
+
if (this.width > 7 && (loc.x < this.left || loc.x > this.right)) {
|
|
267
|
+
return false
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
if (this.height > 7 && (loc.y < this.top || loc.y > this.bottom)) {
|
|
271
|
+
return false
|
|
272
|
+
}
|
|
252
273
|
}
|
|
253
274
|
|
|
254
|
-
|
|
255
|
-
this.tileAt(
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
)
|
|
275
|
+
for (const location of loc.around) {
|
|
276
|
+
if (this.tileAt(location)) return true
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
return false
|
|
260
280
|
}
|
|
261
281
|
|
|
262
282
|
possibleLocations() {
|
|
263
|
-
const locs = {
|
|
264
|
-
const maybe = (
|
|
283
|
+
const locs = { [this.tileId(zero)]: zero } // If no tiles are placed, we can play a starting tile
|
|
284
|
+
const maybe = (loc) => {
|
|
265
285
|
// Add a loc if a tile isn't already there
|
|
266
|
-
const
|
|
267
|
-
locs[
|
|
286
|
+
const id = this.tileId(loc)
|
|
287
|
+
locs[id] = locs[id] || loc
|
|
268
288
|
}
|
|
269
289
|
|
|
270
290
|
for (const tile of Object.values(this.tiles)) {
|
|
271
|
-
locs[tile.
|
|
272
|
-
|
|
273
|
-
maybe(tile.x + 1, tile.y) // Which will either be replaced by real tiles
|
|
274
|
-
maybe(tile.x, tile.y - 1) // Or will be potentially valid locations
|
|
275
|
-
maybe(tile.x, tile.y + 1)
|
|
291
|
+
locs[tile.id] = 'tile' // Add an existing tile
|
|
292
|
+
tile.loc.around.map((loc) => maybe(loc))
|
|
276
293
|
}
|
|
277
294
|
|
|
278
295
|
// Get rid of all the real tiles and return all the maybes
|
|
279
|
-
return Object.values(locs).filter((
|
|
296
|
+
return Object.values(locs).filter((loc) => loc !== 'tile')
|
|
280
297
|
}
|
|
281
298
|
|
|
282
299
|
possibleMoves() {
|
|
@@ -285,8 +302,8 @@ export const Trax = class {
|
|
|
285
302
|
const possibles = []
|
|
286
303
|
if (this.over) return possibles // Shortcut if the game is over
|
|
287
304
|
for (const loc of this.possibleLocations()) {
|
|
288
|
-
for (const tile of this.possibleTiles(loc
|
|
289
|
-
const drop = this.dropTile(tile, loc
|
|
305
|
+
for (const tile of this.possibleTiles(loc)) {
|
|
306
|
+
const drop = this.dropTile(tile, loc, 'tentative')
|
|
290
307
|
if (drop.valid) possibles.push(drop.notation)
|
|
291
308
|
}
|
|
292
309
|
}
|
|
@@ -294,22 +311,22 @@ export const Trax = class {
|
|
|
294
311
|
return possibles
|
|
295
312
|
}
|
|
296
313
|
|
|
297
|
-
forcedMoves(
|
|
298
|
-
// We just played at
|
|
314
|
+
forcedMoves(loc) {
|
|
315
|
+
// We just played at loc, so return all forced moves
|
|
299
316
|
const forced = []
|
|
300
317
|
const invalids = {}
|
|
301
|
-
let check =
|
|
318
|
+
let check = loc.around
|
|
302
319
|
while (check.length > 0) {
|
|
303
320
|
const pos = check.shift()
|
|
304
|
-
if (this.validLocation(pos
|
|
305
|
-
const possibles = this.possibleTiles(pos
|
|
321
|
+
if (this.validLocation(pos)) {
|
|
322
|
+
const possibles = this.possibleTiles(pos)
|
|
306
323
|
if (possibles.length === 1) {
|
|
307
324
|
// Only 1 possible move
|
|
308
|
-
forced.push(this.addTile(possibles[0], pos
|
|
309
|
-
check = [...check, ...
|
|
325
|
+
forced.push(this.addTile(possibles[0], pos))
|
|
326
|
+
check = [...check, ...pos.around]
|
|
310
327
|
} else if (possibles.length === 0) {
|
|
311
328
|
// No possible moves at all
|
|
312
|
-
const invalid = this.addTile('x', pos
|
|
329
|
+
const invalid = this.addTile('x', pos)
|
|
313
330
|
if (!invalids[invalid.id]) forced.push(invalid)
|
|
314
331
|
invalids[invalid.id] = true
|
|
315
332
|
}
|
|
@@ -319,62 +336,72 @@ export const Trax = class {
|
|
|
319
336
|
return forced
|
|
320
337
|
}
|
|
321
338
|
|
|
322
|
-
notate(type,
|
|
339
|
+
notate(type, loc) {
|
|
323
340
|
// Notation depends on the position of the board BEFORE the move
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
341
|
+
let notation = Trax.encodeCol(loc.x - this.left + 1)
|
|
342
|
+
notation += String(loc.y - this.top + 1)
|
|
343
|
+
notation += slash[type]
|
|
344
|
+
return notation
|
|
327
345
|
}
|
|
328
346
|
|
|
329
347
|
decodeNotation(notation) {
|
|
330
|
-
// Return {type,
|
|
331
|
-
const
|
|
348
|
+
// Return {type, loc} from a notation
|
|
349
|
+
const bad = { type: 'x', loc: zero }
|
|
332
350
|
const match = notation.match(notationRegex)
|
|
333
|
-
if (match === null) return
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
351
|
+
if (match === null) return bad
|
|
352
|
+
const loc = new Point(
|
|
353
|
+
this.left - 1 + Trax.decodeCol(match[1]),
|
|
354
|
+
this.top - 1 + Number(match[2]),
|
|
355
|
+
)
|
|
356
|
+
const possible = this.possibleTiles(loc, match[3])
|
|
357
|
+
if (possible.length > 0) return { type: possible[0], loc }
|
|
358
|
+
return bad
|
|
339
359
|
}
|
|
340
360
|
|
|
341
|
-
follow(color,
|
|
361
|
+
follow(color, loc, from) {
|
|
342
362
|
const path = []
|
|
343
|
-
let id = this.tileId(
|
|
344
|
-
let tile = this.tileAt(
|
|
363
|
+
let id = this.tileId(loc)
|
|
364
|
+
let tile = this.tileAt(loc)
|
|
345
365
|
while (tile && path[0] !== id) {
|
|
346
366
|
if (from !== 'top' && upColor(tile) === color) {
|
|
347
367
|
from = 'bottom'
|
|
348
|
-
|
|
368
|
+
loc = loc.up
|
|
349
369
|
} else if (from !== 'bottom' && downColor(tile) === color) {
|
|
350
370
|
from = 'top'
|
|
351
|
-
|
|
371
|
+
loc = loc.down
|
|
352
372
|
} else if (from !== 'left' && leftColor(tile) === color) {
|
|
353
373
|
from = 'right'
|
|
354
|
-
|
|
374
|
+
loc = loc.left
|
|
355
375
|
} else if (from !== 'right' && rightColor(tile) === color) {
|
|
356
376
|
from = 'left'
|
|
357
|
-
|
|
377
|
+
loc = loc.right
|
|
358
378
|
}
|
|
359
379
|
|
|
360
380
|
path.push(id)
|
|
361
|
-
id = this.tileId(
|
|
362
|
-
tile = this.tileAt(
|
|
381
|
+
id = this.tileId(loc)
|
|
382
|
+
tile = this.tileAt(loc)
|
|
363
383
|
}
|
|
364
384
|
|
|
365
|
-
return {
|
|
385
|
+
return { loc, path }
|
|
366
386
|
}
|
|
367
387
|
|
|
368
|
-
findEnds(color,
|
|
388
|
+
findEnds(color, loc) {
|
|
369
389
|
const ends = []
|
|
370
|
-
const tile = this.tileAt(
|
|
371
|
-
if (upColor(tile) === color) ends.push(this.follow(color,
|
|
372
|
-
if (downColor(tile) === color) ends.push(this.follow(color,
|
|
373
|
-
if (leftColor(tile) === color) ends.push(this.follow(color,
|
|
374
|
-
if (rightColor(tile) === color) ends.push(this.follow(color,
|
|
390
|
+
const tile = this.tileAt(loc)
|
|
391
|
+
if (upColor(tile) === color) ends.push(this.follow(color, loc, 'top'))
|
|
392
|
+
if (downColor(tile) === color) ends.push(this.follow(color, loc, 'bottom'))
|
|
393
|
+
if (leftColor(tile) === color) ends.push(this.follow(color, loc, 'left'))
|
|
394
|
+
if (rightColor(tile) === color) ends.push(this.follow(color, loc, 'right'))
|
|
375
395
|
return ends
|
|
376
396
|
}
|
|
377
397
|
|
|
398
|
+
lineWin(locA, locB) {
|
|
399
|
+
if (this.rules === 'traxloop') return false
|
|
400
|
+
if (this.width > 7 && locA.distX(locB) > this.width) return true
|
|
401
|
+
if (this.height > 7 && locA.distY(locB) > this.height) return true
|
|
402
|
+
return false
|
|
403
|
+
}
|
|
404
|
+
|
|
378
405
|
checkWin(tiles) {
|
|
379
406
|
let winner = false
|
|
380
407
|
const colors = [this.color, Trax.other(this.color)] // Check win for current player first
|
|
@@ -382,15 +409,11 @@ export const Trax = class {
|
|
|
382
409
|
if (winner) continue // Skip the second player if the first has won
|
|
383
410
|
for (const tile of tiles) {
|
|
384
411
|
if (winner) continue // Skip the remaining tiles once we find a win
|
|
385
|
-
const ends = this.findEnds(color, tile.
|
|
386
|
-
if (ends[0].
|
|
412
|
+
const ends = this.findEnds(color, tile.loc)
|
|
413
|
+
if (ends[0].loc.eq(ends[1].loc)) {
|
|
387
414
|
winner = color // Loop win
|
|
388
415
|
this.path = ends[0].path // Both paths are essentially the same
|
|
389
|
-
} else if (
|
|
390
|
-
this.rules !== 'traxloop' && // Check for line win
|
|
391
|
-
((this.width > 7 && Math.abs(ends[0].x - ends[1].x) > this.width) ||
|
|
392
|
-
(this.height > 7 && Math.abs(ends[0].y - ends[1].y) > this.height))
|
|
393
|
-
) {
|
|
416
|
+
} else if (this.lineWin(ends[0].loc, ends[1].loc)) {
|
|
394
417
|
winner = color // Line win, paths need to be combined
|
|
395
418
|
this.path = [...ends[0].path.reverse(), ...ends[1].path.slice(1)]
|
|
396
419
|
}
|
|
@@ -418,8 +441,8 @@ export const Trax = class {
|
|
|
418
441
|
}
|
|
419
442
|
|
|
420
443
|
playMove(moveNumber, notation) {
|
|
421
|
-
// Generic API call for
|
|
422
|
-
if (moveNumber === this.move) return this.dropTile(notation)
|
|
444
|
+
// Generic API call for playing a move with move number safety
|
|
445
|
+
if (moveNumber === this.move + 1) return this.dropTile(notation)
|
|
423
446
|
return { valid: false }
|
|
424
447
|
}
|
|
425
448
|
|
|
@@ -441,7 +464,7 @@ export const Trax = class {
|
|
|
441
464
|
.filter((n) => !n.endsWith('.'))
|
|
442
465
|
}
|
|
443
466
|
|
|
444
|
-
dropTile(type,
|
|
467
|
+
dropTile(type, loc, tentative = false) {
|
|
445
468
|
// Pass in tentative to not save the move
|
|
446
469
|
let dropped = []
|
|
447
470
|
let notation = ''
|
|
@@ -461,16 +484,18 @@ export const Trax = class {
|
|
|
461
484
|
} else {
|
|
462
485
|
if (type.search(notationRegex) === 0) {
|
|
463
486
|
// We were passed a notation
|
|
464
|
-
|
|
487
|
+
const decoded = this.decodeNotation(type)
|
|
488
|
+
type = decoded.type
|
|
489
|
+
loc = decoded.loc
|
|
465
490
|
}
|
|
466
491
|
|
|
467
|
-
if (this.validLocation(
|
|
492
|
+
if (loc && this.validLocation(loc) && this.validTile(type, loc)) {
|
|
468
493
|
valid = true
|
|
469
494
|
const saved = this.save() // Just in case we need to roll back
|
|
470
|
-
notation = this.notate(type,
|
|
495
|
+
notation = this.notate(type, loc) // Determine notation BEFORE we play the move
|
|
471
496
|
this.updateNotation(notation)
|
|
472
|
-
dropped.push(this.addTile(type,
|
|
473
|
-
dropped = [...dropped, ...this.forcedMoves(
|
|
497
|
+
dropped.push(this.addTile(type, loc)) // Play the tile
|
|
498
|
+
dropped = [...dropped, ...this.forcedMoves(loc)] // Play all forced moves
|
|
474
499
|
if (this.invalid || tentative) {
|
|
475
500
|
if (this.invalid) valid = false
|
|
476
501
|
this.restore(saved) // Abort! abort!
|
|
@@ -490,6 +515,8 @@ export const Trax = class {
|
|
|
490
515
|
if (match === null) return []
|
|
491
516
|
const x = Trax.decodeCol(match[1])
|
|
492
517
|
const y = Number(match[2])
|
|
518
|
+
const X = this.height - x + 1
|
|
519
|
+
const Y = this.width - y + 1
|
|
493
520
|
const s = match[3]
|
|
494
521
|
const slashes = s === '+' ? [s] : ['/', '\\']
|
|
495
522
|
const moves = new Set()
|
|
@@ -497,16 +524,8 @@ export const Trax = class {
|
|
|
497
524
|
for (const slash of slashes) {
|
|
498
525
|
moves.add(Trax.encodeCol(x) + String(y) + slash)
|
|
499
526
|
moves.add(Trax.encodeCol(y) + String(x) + slash)
|
|
500
|
-
moves.add(
|
|
501
|
-
|
|
502
|
-
String(this.width - y + 1) +
|
|
503
|
-
slash,
|
|
504
|
-
)
|
|
505
|
-
moves.add(
|
|
506
|
-
Trax.encodeCol(this.width - y + 1) +
|
|
507
|
-
String(this.height - x + 1) +
|
|
508
|
-
slash,
|
|
509
|
-
)
|
|
527
|
+
moves.add(Trax.encodeCol(X) + String(Y) + slash)
|
|
528
|
+
moves.add(Trax.encodeCol(Y) + String(X) + slash)
|
|
510
529
|
}
|
|
511
530
|
|
|
512
531
|
return [...moves]
|
|
@@ -547,7 +566,7 @@ export const Trax = class {
|
|
|
547
566
|
return this.positionCode()
|
|
548
567
|
}
|
|
549
568
|
|
|
550
|
-
|
|
569
|
+
dropsIcon(drops) {
|
|
551
570
|
return this.positionCode(undefined, undefined, undefined, drops)
|
|
552
571
|
}
|
|
553
572
|
|
|
@@ -562,19 +581,22 @@ export const Trax = class {
|
|
|
562
581
|
let x = startX
|
|
563
582
|
let y = startY
|
|
564
583
|
do {
|
|
565
|
-
|
|
584
|
+
const loc = new Point(x, y)
|
|
585
|
+
let tile = this.tileAt(loc) // Normal tile for normal orientation
|
|
566
586
|
if (tile) {
|
|
567
587
|
tile = applySymmetry(tile, rightToLeft, bottomToTop, rotate)
|
|
568
|
-
if (this.path.includes(this.tileId(
|
|
588
|
+
if (this.path.includes(this.tileId(loc))) {
|
|
569
589
|
tile = String.fromCodePoint(tile.codePointAt(0) + this.turn * 6)
|
|
570
590
|
}
|
|
571
591
|
|
|
572
592
|
if (
|
|
573
593
|
drops &&
|
|
574
594
|
drops.dropped &&
|
|
575
|
-
drops.dropped.some((t) =>
|
|
576
|
-
)
|
|
595
|
+
drops.dropped.some((t) => loc.eq(t.loc))
|
|
596
|
+
) {
|
|
577
597
|
tile = tile.toUpperCase()
|
|
598
|
+
}
|
|
599
|
+
|
|
578
600
|
line += tile
|
|
579
601
|
} else {
|
|
580
602
|
line = addBlank(line)
|
package/src/point.js
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/* Copyright 2022 Chad Transtrum
|
|
2
|
+
*
|
|
3
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
* you may not use this file except in compliance with the License.
|
|
5
|
+
* You may obtain a copy of the License at
|
|
6
|
+
*
|
|
7
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
*
|
|
9
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
* See the License for the specific language governing permissions and
|
|
13
|
+
* limitations under the License.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const is = (v, s) => (typeof v).slice(0, 3) === s // Sigh
|
|
17
|
+
|
|
18
|
+
// Are you tired of doing x,y movements in multiple statements? I am.
|
|
19
|
+
|
|
20
|
+
// Point: an immutable two-dimensional location (x,y)
|
|
21
|
+
// with functions for moving around quickly to neighboring points
|
|
22
|
+
export const Point = class {
|
|
23
|
+
#x = 0
|
|
24
|
+
#y = 0
|
|
25
|
+
|
|
26
|
+
constructor(x, y) {
|
|
27
|
+
if (is(x, 'obj') && is(x.x, 'num') && is(x.y, 'num')) {
|
|
28
|
+
// You can call this with new Point(point) or new Point({x,y})
|
|
29
|
+
this.#x = x.x
|
|
30
|
+
this.#y = x.y
|
|
31
|
+
} else {
|
|
32
|
+
// Or with new Point(x, y)
|
|
33
|
+
if (!is(x, 'num')) throw new SyntaxError('x must be a number')
|
|
34
|
+
if (!is(y, 'num')) throw new SyntaxError('y must be a number')
|
|
35
|
+
this.#x = x
|
|
36
|
+
this.#y = y
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
get x() {
|
|
41
|
+
return this.#x
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
set x(_) {
|
|
45
|
+
throw new ReferenceError('Point is immutable')
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
get y() {
|
|
49
|
+
return this.#y
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
set y(_) {
|
|
53
|
+
throw new ReferenceError('Point is immutable')
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
get up() {
|
|
57
|
+
return new Point(this.#x, this.#y - 1)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
get down() {
|
|
61
|
+
return new Point(this.#x, this.#y + 1)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
get left() {
|
|
65
|
+
return new Point(this.#x - 1, this.#y)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
get right() {
|
|
69
|
+
return new Point(this.#x + 1, this.#y)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
get around() {
|
|
73
|
+
return [this.up, this.down, this.left, this.right]
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
dir(direction) {
|
|
77
|
+
const error = 'direction must be one of: up, down, left, right, u, d, l, r'
|
|
78
|
+
if (!is(direction, 'str')) throw new SyntaxError(error)
|
|
79
|
+
if (direction[0] === 'u') return this.up
|
|
80
|
+
if (direction[0] === 'd') return this.down
|
|
81
|
+
if (direction[0] === 'l') return this.left
|
|
82
|
+
if (direction[0] === 'r') return this.right
|
|
83
|
+
throw new SyntaxError(error)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
distX(from) {
|
|
87
|
+
return Math.abs(this.#x - from.x)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
distY(from) {
|
|
91
|
+
return Math.abs(this.#y - from.y)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
distance(from) {
|
|
95
|
+
return Math.hypot(this.distX(from), this.distY(from))
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
eq(that) {
|
|
99
|
+
return this.#x === that.x && this.#y === that.y
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Play nicely with the rest of the world
|
|
103
|
+
|
|
104
|
+
toString() {
|
|
105
|
+
return 'Point(' + this.#x + ',' + this.#y + ')'
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
toJSON() {
|
|
109
|
+
return { x: this.#x, y: this.#y }
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
[Symbol.for('nodejs.util.inspect.custom')](_, options) {
|
|
113
|
+
const [y, s, n] = [options.stylize, 'special', 'number']
|
|
114
|
+
return `${y('Point', s)}(${y(this.#x, n)},${y(this.#y, n)})`
|
|
115
|
+
}
|
|
116
|
+
}
|