@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.
@@ -15,13 +15,16 @@
15
15
 
16
16
  // Trax internals. Woot.
17
17
 
18
- // tile type names are determined by listing the color of line at each edge, clockwise
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
- // fun trax helper constants
22
+ import { Point } from './point.js'
23
23
 
24
- const notationRegex = /^ *([@a-z]+) *(\d+) *([/\\+]) *$/i
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 variants = new Set(['trax', 'traxloop', 'trax8']) // Known variants
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 (let move of moves) {
126
- if (typeof move === 'string') {
127
- move = { move: this.move, notation: move }
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(x, y) {
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, x, y) {
204
+ addTile(type, loc) {
192
205
  // Note: no validity checking is done here
193
- const id = this.tileId(x, y)
194
- const tile = { type, x, y, id, move: this.move, seq: this.count }
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(x, y) {
209
- return (this.tiles[this.tileId(x, y)] || {}).type
221
+ tileAt(loc) {
222
+ return (this.tiles[this.tileId(loc)] || {}).type
210
223
  }
211
224
 
212
- validTile(type, x, y) {
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
- const up = downColor(this.tileAt(x, y - 1))
216
- const down = upColor(this.tileAt(x, y + 1))
217
- const left = rightColor(this.tileAt(x - 1, y))
218
- const right = leftColor(this.tileAt(x + 1, y))
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(x, y, s = false) {
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, x, y)) possibles.push(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(x, y) {
261
+ validLocation(loc) {
246
262
  if (this.over) return false
247
- if (this.tileAt(x, y)) return false
248
- if (this.count === 0 && x === 0 && y === 0) return true
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)) return false
251
- if (this.height > 7 && (y < this.top || y > this.bottom)) return false
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
- return Boolean(
255
- this.tileAt(x - 1, y) ||
256
- this.tileAt(x + 1, y) ||
257
- this.tileAt(x, y - 1) ||
258
- this.tileAt(x, y + 1),
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 = { '0x0': { x: 0, y: 0 } } // If no tiles are placed, we can play a starting tile
264
- const maybe = (x, y) => {
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 loc = x + 'x' + y
267
- locs[loc] = locs[loc] || { x, y }
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.x + 'x' + tile.y] = 'tile' // Add an existing tile
272
- maybe(tile.x - 1, tile.y) // And four maybe tiles around it
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((l) => l !== 'tile')
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.x, loc.y)) {
289
- const drop = this.dropTile(tile, loc.x, loc.y, 'tentative')
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(x, y) {
298
- // We just played at x, y, so return all forced moves
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 = squaresAround(x, y)
318
+ let check = loc.around
302
319
  while (check.length > 0) {
303
320
  const pos = check.shift()
304
- if (this.validLocation(pos.x, pos.y)) {
305
- const possibles = this.possibleTiles(pos.x, pos.y)
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.x, pos.y))
309
- check = [...check, ...squaresAround(pos.x, pos.y)]
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.x, pos.y)
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, x, y) {
339
+ notate(type, loc) {
323
340
  // Notation depends on the position of the board BEFORE the move
324
- return (
325
- Trax.encodeCol(x - this.left + 1) + String(y - this.top + 1) + slash[type]
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, x, y} from a notation
331
- const r = { type: 'x', x: 0, y: 0 } // Default to an invalid move
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 r
334
- r.x = this.left - 1 + Trax.decodeCol(match[1])
335
- r.y = this.top - 1 + Number(match[2])
336
- const possible = this.possibleTiles(r.x, r.y, match[3])
337
- if (possible) r.type = possible[0]
338
- return r
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, x, y, from) {
361
+ follow(color, loc, from) {
342
362
  const path = []
343
- let id = this.tileId(x, y)
344
- let tile = this.tileAt(x, y)
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
- y--
368
+ loc = loc.up
349
369
  } else if (from !== 'bottom' && downColor(tile) === color) {
350
370
  from = 'top'
351
- y++
371
+ loc = loc.down
352
372
  } else if (from !== 'left' && leftColor(tile) === color) {
353
373
  from = 'right'
354
- x--
374
+ loc = loc.left
355
375
  } else if (from !== 'right' && rightColor(tile) === color) {
356
376
  from = 'left'
357
- x++
377
+ loc = loc.right
358
378
  }
359
379
 
360
380
  path.push(id)
361
- id = this.tileId(x, y)
362
- tile = this.tileAt(x, y)
381
+ id = this.tileId(loc)
382
+ tile = this.tileAt(loc)
363
383
  }
364
384
 
365
- return { x, y, path }
385
+ return { loc, path }
366
386
  }
367
387
 
368
- findEnds(color, x, y) {
388
+ findEnds(color, loc) {
369
389
  const ends = []
370
- const tile = this.tileAt(x, y)
371
- if (upColor(tile) === color) ends.push(this.follow(color, x, y, 'top'))
372
- if (downColor(tile) === color) ends.push(this.follow(color, x, y, 'bottom'))
373
- if (leftColor(tile) === color) ends.push(this.follow(color, x, y, 'left'))
374
- if (rightColor(tile) === color) ends.push(this.follow(color, x, y, 'right'))
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.x, tile.y)
386
- if (ends[0].x === ends[1].x && ends[0].y === ends[1].y) {
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 dropTile
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, x, y, tentative = false) {
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
- ;({ type, x, y } = this.decodeNotation(type))
487
+ const decoded = this.decodeNotation(type)
488
+ type = decoded.type
489
+ loc = decoded.loc
465
490
  }
466
491
 
467
- if (this.validLocation(x, y) && this.validTile(type, x, y)) {
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, x, y) // Determine notation BEFORE we play the move
495
+ notation = this.notate(type, loc) // Determine notation BEFORE we play the move
471
496
  this.updateNotation(notation)
472
- dropped.push(this.addTile(type, x, y)) // Play the tile
473
- dropped = [...dropped, ...this.forcedMoves(x, y)] // Play all forced moves
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
- Trax.encodeCol(this.height - x + 1) +
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
- tentativeIcon(drops) {
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
- let tile = this.tileAt(x, y) // Normal tile for normal orientation
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(x, y))) {
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) => t.x === x && t.y === y)
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
+ }