@slugbugblue/trax 0.12.0 → 0.14.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 +25 -0
- package/README.md +45 -7
- package/docs/analyst.md +44 -2
- package/package.json +6 -7
- package/src/analyst.js +97 -12
- package/src/cli.js +114 -31
- package/src/cmds/analyze.js +2 -2
- package/src/cmds/import-export.js +33 -3
- package/src/cmds/list.js +2 -5
- package/src/cmds/new.js +6 -4
- package/src/cmds/play-try.js +54 -7
- package/src/cmds/puzzles.js +152 -0
- package/src/cmds/suggest.js +52 -0
- package/src/cmds/view.js +1 -1
- package/src/engine.js +167 -29
- package/src/point.js +54 -12
- package/src/puzzles.js +808 -0
- package/src/threats.js +39 -35
- package/src/tty.js +30 -15
- package/src/types.d.ts +105 -0
- package/src/version.js +1 -1
package/src/engine.js
CHANGED
|
@@ -20,16 +20,29 @@
|
|
|
20
20
|
// and given a single letter name, so 'bbww' becomes 'a', which gives us six
|
|
21
21
|
// different tile names: a-f
|
|
22
22
|
|
|
23
|
-
import { Point } from '
|
|
23
|
+
import { Point } from '@slugbugblue/trax/point'
|
|
24
24
|
|
|
25
|
+
// Type definitions
|
|
25
26
|
// Fun trax helper constants
|
|
26
27
|
|
|
27
28
|
const zero = new Point(0, 0)
|
|
28
29
|
const moveNumberRegex = /^(\d+)[.):]?$/
|
|
29
30
|
const notationRegex = /^([@a-z]+)(\d+)([/\\+])$/i
|
|
30
31
|
|
|
32
|
+
/** @type ValidTiles[] */
|
|
33
|
+
const tileTypes = ['a', 'b', 'c', 'd', 'e', 'f']
|
|
34
|
+
|
|
35
|
+
/** Tiles are represented by the letters a-f.
|
|
36
|
+
* Slashes are represented by / \ or +.
|
|
37
|
+
* @readonly
|
|
38
|
+
* @type {Record<ValidTiles, Slash>}
|
|
39
|
+
*/
|
|
31
40
|
const slash = { a: '\\', b: '+', c: '/', d: '/', e: '+', f: '\\' }
|
|
32
41
|
|
|
42
|
+
/** Symmetry helper to convert from one tile to another.
|
|
43
|
+
* @readonly
|
|
44
|
+
* @type {Record<string, Record<ValidTiles, ValidTiles>>}
|
|
45
|
+
*/
|
|
33
46
|
const symmetry = {
|
|
34
47
|
// Convert tile to different views
|
|
35
48
|
flip: { a: 'd', b: 'b', c: 'f', d: 'a', e: 'e', f: 'c' },
|
|
@@ -40,6 +53,13 @@ const symmetry = {
|
|
|
40
53
|
|
|
41
54
|
// Fun trax helper functions
|
|
42
55
|
|
|
56
|
+
/** Apply certain combinations of symmetry.
|
|
57
|
+
* @arg {TileType} tile - the tile to apply symmetry to
|
|
58
|
+
* @arg {boolean} [rightToLeft] - mirror the tile
|
|
59
|
+
* @arg {boolean} [bottomToTop] - flip the tile
|
|
60
|
+
* @arg {boolean} [rotate] - rotate the tile counterclockwise
|
|
61
|
+
* @returns {TileType}
|
|
62
|
+
*/
|
|
43
63
|
const applySymmetry = (tile, rightToLeft, bottomToTop, rotate) => {
|
|
44
64
|
if (rightToLeft) tile = symmetry.mirror[tile]
|
|
45
65
|
if (bottomToTop) tile = symmetry.flip[tile]
|
|
@@ -48,15 +68,38 @@ const applySymmetry = (tile, rightToLeft, bottomToTop, rotate) => {
|
|
|
48
68
|
}
|
|
49
69
|
|
|
50
70
|
// Unexpected note: str.includes(t) is true if t is '' but false if t is undefined
|
|
71
|
+
/** The color at the top of the tile.
|
|
72
|
+
* @arg {string} t - the tile to check
|
|
73
|
+
* @returns {Color|false} the color or false if t is not a valid tile
|
|
74
|
+
*/
|
|
51
75
|
const upColor = (t) =>
|
|
52
76
|
'abc'.includes(t) ? 'b' : 'def'.includes(t) ? 'w' : false
|
|
77
|
+
|
|
78
|
+
/** The color at the bottom of the tile.
|
|
79
|
+
* @arg {string} t - the tile to check
|
|
80
|
+
* @returns {Color|false} the color or false if t is not a valid tile
|
|
81
|
+
*/
|
|
53
82
|
const downColor = (t) =>
|
|
54
83
|
'bdf'.includes(t) ? 'b' : 'ace'.includes(t) ? 'w' : false
|
|
84
|
+
|
|
85
|
+
/** The color at the right edge of the tile.
|
|
86
|
+
* @arg {string} t - the tile to check
|
|
87
|
+
* @returns {Color|false} the color or false if t is not a valid tile
|
|
88
|
+
*/
|
|
55
89
|
const rightColor = (t) =>
|
|
56
90
|
'ade'.includes(t) ? 'b' : 'bcf'.includes(t) ? 'w' : false
|
|
91
|
+
|
|
92
|
+
/** The color at the left edge of the tile.
|
|
93
|
+
* @arg {string} t - the tile to check
|
|
94
|
+
* @returns {Color|false} the color or false if t is not a valid tile
|
|
95
|
+
*/
|
|
57
96
|
const leftColor = (t) =>
|
|
58
97
|
'cef'.includes(t) ? 'b' : 'abd'.includes(t) ? 'w' : false
|
|
59
98
|
|
|
99
|
+
/** Add an empty space to the position encoding string.
|
|
100
|
+
* @arg {string} code - The position encoding string
|
|
101
|
+
* @returns {string}
|
|
102
|
+
*/
|
|
60
103
|
const addBlank = (code) => {
|
|
61
104
|
// Encode missing tiles as digits 0-9, for 1-10 missing
|
|
62
105
|
const lastNumber = code.codePointAt(code.length - 1) // This is NaN if code is empty, which is ok
|
|
@@ -67,27 +110,60 @@ const addBlank = (code) => {
|
|
|
67
110
|
return code + '0'
|
|
68
111
|
}
|
|
69
112
|
|
|
70
|
-
// Ugh, this is a dirty little function to get
|
|
113
|
+
// Ugh, this is a dirty little function to get
|
|
114
|
+
/** The length of an encoded position row.
|
|
115
|
+
* @arg {string} row - the encoded row
|
|
116
|
+
* @returns {number} - the actual length of the row
|
|
117
|
+
*/
|
|
71
118
|
const codeRowLength = (row) =>
|
|
72
119
|
row.replace(/\d/g, (n) => '.'.repeat(Number(n) + 1)).length
|
|
73
120
|
|
|
74
121
|
// This is where the magic happens
|
|
122
|
+
/** A digital representation of a Trax game. */
|
|
75
123
|
export const Trax = class {
|
|
76
124
|
// Static class properties
|
|
125
|
+
|
|
126
|
+
/** @readonly @type {Record<TraxVariant, string>} */
|
|
77
127
|
static names = {
|
|
78
128
|
trax: 'Trax',
|
|
79
129
|
traxloop: 'Loop Trax',
|
|
80
130
|
trax8: '8x8 Trax',
|
|
81
131
|
}
|
|
82
132
|
|
|
133
|
+
/** @readonly */
|
|
83
134
|
static variants = new Set(Object.keys(this.names)) // Known variants
|
|
84
135
|
|
|
85
136
|
// Static class methods
|
|
137
|
+
|
|
138
|
+
/** Create an x,y Point object with special functions.
|
|
139
|
+
* @arg {number|PointLike} x - either the x value, or an object with x,y keys
|
|
140
|
+
* @arg {number} [y] - if x is a number, y must be provided as well
|
|
141
|
+
* @returns {Point} a new Point object
|
|
142
|
+
*/
|
|
86
143
|
static point = (x, y) => new Point(x, y)
|
|
144
|
+
|
|
145
|
+
/** Given a player number, get the color.
|
|
146
|
+
* @arg {number} playerNumber - the player number, 1 or 2
|
|
147
|
+
* @returns {Color} the color of that player, w or b
|
|
148
|
+
*/
|
|
87
149
|
static colorOf = (playerNumber) => ({ 1: 'w', 2: 'b' }[playerNumber])
|
|
150
|
+
|
|
151
|
+
/** Given a color, get the player number.
|
|
152
|
+
* @arg {string} color - the color, w or b
|
|
153
|
+
* @returns {number} the player number, 1 or 2
|
|
154
|
+
*/
|
|
88
155
|
static playerNumber = (color) => ({ b: 2, w: 1 }[color])
|
|
156
|
+
|
|
157
|
+
/** Given a color, get the other color.
|
|
158
|
+
* @arg {string} color - the color, w or b
|
|
159
|
+
* @returns {Color} the other color, b or w
|
|
160
|
+
*/
|
|
89
161
|
static other = (color) => ({ b: 'w', w: 'b' }[color])
|
|
90
162
|
|
|
163
|
+
/** Encode a numeric column number into the Trax notation column letter.
|
|
164
|
+
* @arg {number} col - the colum number, with 0 just to the left of the tiles
|
|
165
|
+
* @returns {string} the encoded column letter
|
|
166
|
+
*/
|
|
91
167
|
static encodeCol = (col) => {
|
|
92
168
|
// For notation purposes: 0 -> @, 6 -> F, 28 -> AB
|
|
93
169
|
let n = ''
|
|
@@ -99,6 +175,10 @@ export const Trax = class {
|
|
|
99
175
|
return n || '@'
|
|
100
176
|
}
|
|
101
177
|
|
|
178
|
+
/** Decode a Trax notation column letter back to a number.
|
|
179
|
+
* @arg {string} col - the Trax column letter
|
|
180
|
+
* @returns {number} the column number
|
|
181
|
+
*/
|
|
102
182
|
static decodeCol = (col) => {
|
|
103
183
|
// Turn letters back into numbers
|
|
104
184
|
col = col.toUpperCase()
|
|
@@ -112,6 +192,11 @@ export const Trax = class {
|
|
|
112
192
|
}
|
|
113
193
|
|
|
114
194
|
// Trax class constructor
|
|
195
|
+
/** Create a new Trax game
|
|
196
|
+
* @arg {TraxVariant} [rules='trax'] - the variant to play
|
|
197
|
+
* @arg {string|string[]} [moves=[]] - the initial moves to pre-play
|
|
198
|
+
* @arg {string} [id='trax'] - an id used to differentiate tiles from multiple games
|
|
199
|
+
*/
|
|
115
200
|
constructor(rules = 'trax', moves = [], id = 'trax') {
|
|
116
201
|
if (!Trax.variants.has(rules)) rules = 'trax'
|
|
117
202
|
this.id = String(id)
|
|
@@ -124,7 +209,9 @@ export const Trax = class {
|
|
|
124
209
|
this.top = 1
|
|
125
210
|
this.bottom = 0
|
|
126
211
|
this.notation = ''
|
|
212
|
+
/** @type {Record<TileId, Tile>} */
|
|
127
213
|
this.tiles = {}
|
|
214
|
+
/** @type TileId[] */
|
|
128
215
|
this.path = []
|
|
129
216
|
this.invalid = false
|
|
130
217
|
// Moves might be a notation string
|
|
@@ -133,9 +220,14 @@ export const Trax = class {
|
|
|
133
220
|
}
|
|
134
221
|
}
|
|
135
222
|
|
|
223
|
+
/** Save the current game data to a variable.
|
|
224
|
+
* @returns {SaveState} an opaque save state object
|
|
225
|
+
* @see restore for restoring the state
|
|
226
|
+
*/
|
|
136
227
|
save() {
|
|
137
228
|
// In order to evaluate potential moves, we have to modify the state
|
|
138
229
|
return {
|
|
230
|
+
id: this.id,
|
|
139
231
|
move: this.move,
|
|
140
232
|
turn: this.turn,
|
|
141
233
|
over: this.over,
|
|
@@ -150,8 +242,14 @@ export const Trax = class {
|
|
|
150
242
|
}
|
|
151
243
|
}
|
|
152
244
|
|
|
245
|
+
/** Restore a previously saved position.
|
|
246
|
+
* @arg {SaveState} saved - the previously saved state
|
|
247
|
+
* @see save for saving the state
|
|
248
|
+
* @returns {void}
|
|
249
|
+
*/
|
|
153
250
|
restore(saved) {
|
|
154
251
|
// So we need to be able to restore it when we are done
|
|
252
|
+
this.id = saved.id
|
|
155
253
|
this.move = saved.move
|
|
156
254
|
this.turn = saved.turn
|
|
157
255
|
this.over = saved.over
|
|
@@ -169,37 +267,47 @@ export const Trax = class {
|
|
|
169
267
|
}
|
|
170
268
|
}
|
|
171
269
|
|
|
270
|
+
/** The name of this variant. */
|
|
172
271
|
get name() {
|
|
173
|
-
// Get the name of this variant
|
|
174
272
|
return Trax.names[this.rules]
|
|
175
273
|
}
|
|
176
274
|
|
|
275
|
+
/** The number of tiles currently in play. */
|
|
177
276
|
get count() {
|
|
178
|
-
// Get the number of tiles currently in play
|
|
179
277
|
return Object.keys(this.tiles).length
|
|
180
278
|
}
|
|
181
279
|
|
|
280
|
+
/** The color of the current player, w or b. */
|
|
182
281
|
get color() {
|
|
183
|
-
// Get the color of the current player (w or b)
|
|
184
282
|
return Trax.colorOf(this.turn)
|
|
185
283
|
}
|
|
186
284
|
|
|
285
|
+
/** True if the game is over. */
|
|
187
286
|
get gameOver() {
|
|
188
|
-
// Returns true if the game is over
|
|
189
287
|
return this.over
|
|
190
288
|
}
|
|
191
289
|
|
|
290
|
+
/** 1 or 2 for a win, 0 for a tie, false if the game is still in progress. */
|
|
192
291
|
get winner() {
|
|
193
|
-
|
|
194
|
-
return this.over ? this.turn : false // Or 1 or 2 for a win, or 0 for a tie
|
|
292
|
+
return this.over ? this.turn : false
|
|
195
293
|
}
|
|
196
294
|
|
|
295
|
+
/** Provides the tile ID for the tile at the given location.
|
|
296
|
+
* @arg {PointLike} loc
|
|
297
|
+
* returns {TileId}
|
|
298
|
+
*/
|
|
197
299
|
tileId(loc) {
|
|
198
300
|
return this.id + '-' + String(loc.x) + 'x' + String(loc.y)
|
|
199
301
|
}
|
|
200
302
|
|
|
303
|
+
/** Add a tile to the board. Note that no validity checking is done here,
|
|
304
|
+
* except that invalid tiles are not actually placed on the board, so this
|
|
305
|
+
* should only be called externally.
|
|
306
|
+
* @arg {TileType} type
|
|
307
|
+
* @arg {Point} loc
|
|
308
|
+
* @returns {Tile} the tile placed on the board.
|
|
309
|
+
*/
|
|
201
310
|
addTile(type, loc) {
|
|
202
|
-
// Note: no validity checking is done here
|
|
203
311
|
const id = this.tileId(loc)
|
|
204
312
|
const tile = { type, loc, id, move: this.move, seq: this.count }
|
|
205
313
|
if (type === 'x') {
|
|
@@ -215,10 +323,17 @@ export const Trax = class {
|
|
|
215
323
|
return tile
|
|
216
324
|
}
|
|
217
325
|
|
|
326
|
+
/** The type of tile at the given location.
|
|
327
|
+
* @arg {PointLike} loc - the location the tile is in
|
|
328
|
+
* @returns {TileType|undefined} the type of tile, or undefined if no tile
|
|
329
|
+
*/
|
|
218
330
|
tileAt(loc) {
|
|
219
331
|
return (this.tiles[this.tileId(loc)] || {}).type
|
|
220
332
|
}
|
|
221
333
|
|
|
334
|
+
/** Is this tile valid?
|
|
335
|
+
* @type {(type: string, loc: Point) => type is TileType}
|
|
336
|
+
*/
|
|
222
337
|
validTile(type, loc) {
|
|
223
338
|
if (this.count === 0 && loc.x === 0 && loc.y === 0) {
|
|
224
339
|
return type === 'd' || type === 'e'
|
|
@@ -237,10 +352,15 @@ export const Trax = class {
|
|
|
237
352
|
)
|
|
238
353
|
}
|
|
239
354
|
|
|
355
|
+
/** Get a list of possible tiles that can be played in this location.
|
|
356
|
+
* @arg {Point} loc - the location to check
|
|
357
|
+
* @arg {string | boolean} s=false - if provided, a direction modifier, '/', '\\', or '+'
|
|
358
|
+
* @returns {TileType[]}
|
|
359
|
+
*/
|
|
240
360
|
possibleTiles(loc, s = false) {
|
|
241
|
-
|
|
361
|
+
/** @type TileType[] */
|
|
242
362
|
const possibles = []
|
|
243
|
-
for (const t of
|
|
363
|
+
for (const t of tileTypes) {
|
|
244
364
|
if ((!s || s === slash[t]) && this.validTile(t, loc)) possibles.push(t)
|
|
245
365
|
}
|
|
246
366
|
|
|
@@ -277,20 +397,20 @@ export const Trax = class {
|
|
|
277
397
|
}
|
|
278
398
|
|
|
279
399
|
possibleLocations() {
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
const id = this.tileId(loc)
|
|
284
|
-
locs[id] = locs[id] || loc
|
|
285
|
-
}
|
|
400
|
+
if (this.count === 0) return [zero]
|
|
401
|
+
/** @type Record<TileId, Point> */
|
|
402
|
+
const possibles = {}
|
|
286
403
|
|
|
287
404
|
for (const tile of Object.values(this.tiles)) {
|
|
288
|
-
|
|
289
|
-
|
|
405
|
+
tile.loc.around.map((loc) => {
|
|
406
|
+
const id = this.tileId(loc)
|
|
407
|
+
if (!(id in this.tiles)) possibles[id] = loc
|
|
408
|
+
return loc
|
|
409
|
+
})
|
|
290
410
|
}
|
|
291
411
|
|
|
292
412
|
// Get rid of all the real tiles and return all the maybes
|
|
293
|
-
return Object.values(
|
|
413
|
+
return Object.values(possibles)
|
|
294
414
|
}
|
|
295
415
|
|
|
296
416
|
possibleMoves() {
|
|
@@ -400,6 +520,7 @@ export const Trax = class {
|
|
|
400
520
|
}
|
|
401
521
|
|
|
402
522
|
checkWin(tiles) {
|
|
523
|
+
/** @type {Color|false} */
|
|
403
524
|
let winner = false
|
|
404
525
|
const colors = [this.color, Trax.other(this.color)] // Check win for current player first
|
|
405
526
|
for (const color of colors) {
|
|
@@ -432,13 +553,22 @@ export const Trax = class {
|
|
|
432
553
|
}
|
|
433
554
|
}
|
|
434
555
|
|
|
556
|
+
/** Play a move. Can be called either as:
|
|
557
|
+
* - play(moveNumber, notation) to ensure move safety, or
|
|
558
|
+
* - play(notation) for quicker access.
|
|
559
|
+
* @arg moveNumber {(number|string)} the move number or the notation
|
|
560
|
+
* @arg {string} [notation] - the notation, if a move number was provided
|
|
561
|
+
* @returns {{dropped: Tile[], notation: string, valid: boolean}}
|
|
562
|
+
*/
|
|
435
563
|
play(moveNumber, notation) {
|
|
436
564
|
// Generic API call for playing a move with move number safety
|
|
437
565
|
// or, alternately, pass only a single value, notation, for a quick play
|
|
438
566
|
if (notation) {
|
|
439
|
-
if (moveNumber !== this.move + 1)
|
|
567
|
+
if (moveNumber !== this.move + 1) {
|
|
568
|
+
return { dropped: [], notation: '', valid: false }
|
|
569
|
+
}
|
|
440
570
|
} else {
|
|
441
|
-
notation = moveNumber
|
|
571
|
+
notation = String(moveNumber)
|
|
442
572
|
}
|
|
443
573
|
|
|
444
574
|
return this.dropTile(notation)
|
|
@@ -482,6 +612,7 @@ export const Trax = class {
|
|
|
482
612
|
this.notation += note
|
|
483
613
|
}
|
|
484
614
|
|
|
615
|
+
/** An array of the moves made in the game. */
|
|
485
616
|
get moves() {
|
|
486
617
|
return this.notation
|
|
487
618
|
.replace(/\n/g, ' ')
|
|
@@ -489,8 +620,14 @@ export const Trax = class {
|
|
|
489
620
|
.filter((n) => !n.endsWith('.'))
|
|
490
621
|
}
|
|
491
622
|
|
|
492
|
-
|
|
493
|
-
|
|
623
|
+
/** Drop a tile onto the board. This is a lower level call. Use play() if possible instead.
|
|
624
|
+
* @arg {string|TileType} type - a special move, a tile type, or a notation
|
|
625
|
+
* @arg {Point} [loc] - a location if type is a tile type
|
|
626
|
+
* @arg {string|boolean} [tentative] - if truthy, the move will not be saved
|
|
627
|
+
* @returns {TileDrop} an object representing the results of the drop
|
|
628
|
+
*/
|
|
629
|
+
dropTile(type, loc, tentative) {
|
|
630
|
+
/** @type Tile[] */
|
|
494
631
|
let dropped = []
|
|
495
632
|
let notation = ''
|
|
496
633
|
let valid = false
|
|
@@ -607,11 +744,12 @@ export const Trax = class {
|
|
|
607
744
|
let y = startY
|
|
608
745
|
do {
|
|
609
746
|
const loc = new Point(x, y)
|
|
610
|
-
|
|
747
|
+
const tile = this.tileAt(loc) // Normal tile for normal orientation
|
|
611
748
|
if (tile) {
|
|
612
|
-
|
|
749
|
+
/** @type string */
|
|
750
|
+
let encoded = applySymmetry(tile, rightToLeft, bottomToTop, rotate)
|
|
613
751
|
if (this.path.includes(this.tileId(loc))) {
|
|
614
|
-
|
|
752
|
+
encoded = String.fromCodePoint(encoded.codePointAt(0) + this.turn * 6)
|
|
615
753
|
}
|
|
616
754
|
|
|
617
755
|
if (
|
|
@@ -619,10 +757,10 @@ export const Trax = class {
|
|
|
619
757
|
drops.dropped &&
|
|
620
758
|
drops.dropped.some((t) => loc.eq(t.loc))
|
|
621
759
|
) {
|
|
622
|
-
|
|
760
|
+
encoded = encoded.toUpperCase()
|
|
623
761
|
}
|
|
624
762
|
|
|
625
|
-
line +=
|
|
763
|
+
line += encoded
|
|
626
764
|
} else {
|
|
627
765
|
line = addBlank(line)
|
|
628
766
|
}
|
package/src/point.js
CHANGED
|
@@ -13,77 +13,109 @@
|
|
|
13
13
|
* limitations under the License.
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
|
-
|
|
17
|
-
|
|
16
|
+
/** Because javascript doesn't have a "0 < 3 < 7" syntax.
|
|
17
|
+
* @arg {number} end1 - A number on one end or the other
|
|
18
|
+
* @arg {number} mid - The middle number
|
|
19
|
+
* @arg {number} end2 - The other end
|
|
20
|
+
* @returns {boolean} - If the number is in the middle
|
|
21
|
+
*/
|
|
18
22
|
const between = (end1, mid, end2) => {
|
|
19
23
|
const low = Math.min(end1, end2)
|
|
20
24
|
const high = Math.max(end1, end2)
|
|
21
25
|
return low <= mid && mid <= high
|
|
22
26
|
}
|
|
23
27
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
+
/** Are you tired of doing x,y movements in multiple statements?
|
|
29
|
+
*
|
|
30
|
+
* I know I am.
|
|
31
|
+
*
|
|
32
|
+
* Point: an immutable two-dimensional location (x,y) with
|
|
33
|
+
* functions for moving around quickly to neighboring points.
|
|
34
|
+
*/
|
|
28
35
|
export const Point = class {
|
|
29
36
|
#x = 0
|
|
30
37
|
#y = 0
|
|
31
38
|
|
|
32
39
|
static dirs = ['up', 'down', 'left', 'right']
|
|
33
40
|
|
|
41
|
+
/** You can call this with new Point(point) or new Point({x, y}) or new Point(x, y)
|
|
42
|
+
* @arg {number | PointLike} x - the x value or a point
|
|
43
|
+
* @arg {number} [y] - the y value
|
|
44
|
+
*/
|
|
34
45
|
constructor(x, y) {
|
|
35
|
-
if (
|
|
36
|
-
|
|
46
|
+
if (
|
|
47
|
+
typeof x === 'object' &&
|
|
48
|
+
typeof x.x === 'number' &&
|
|
49
|
+
typeof x.y === 'number'
|
|
50
|
+
) {
|
|
37
51
|
this.#x = x.x
|
|
38
52
|
this.#y = x.y
|
|
39
53
|
} else {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
54
|
+
if (typeof x !== 'number' || Number.isNaN(x)) {
|
|
55
|
+
throw new SyntaxError('x must be a number or a point-like object')
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (typeof y !== 'number' || Number.isNaN(x)) {
|
|
59
|
+
throw new SyntaxError('y must be a number')
|
|
60
|
+
}
|
|
61
|
+
|
|
43
62
|
this.#x = x
|
|
44
63
|
this.#y = y
|
|
45
64
|
}
|
|
46
65
|
}
|
|
47
66
|
|
|
67
|
+
/** The horizontal value of the point. */
|
|
48
68
|
get x() {
|
|
49
69
|
return this.#x
|
|
50
70
|
}
|
|
51
71
|
|
|
72
|
+
/** Throws an error. Points are immutable. */
|
|
52
73
|
set x(_) {
|
|
53
74
|
throw new ReferenceError('Point is immutable')
|
|
54
75
|
}
|
|
55
76
|
|
|
77
|
+
/** The vertical value of the point. */
|
|
56
78
|
get y() {
|
|
57
79
|
return this.#y
|
|
58
80
|
}
|
|
59
81
|
|
|
82
|
+
/** Throws an error. Points are immutable. */
|
|
60
83
|
set y(_) {
|
|
61
84
|
throw new ReferenceError('Point is immutable')
|
|
62
85
|
}
|
|
63
86
|
|
|
87
|
+
/** The point immediately above this point. */
|
|
64
88
|
get up() {
|
|
65
89
|
return new Point(this.#x, this.#y - 1)
|
|
66
90
|
}
|
|
67
91
|
|
|
92
|
+
/** The point immediately below this point. */
|
|
68
93
|
get down() {
|
|
69
94
|
return new Point(this.#x, this.#y + 1)
|
|
70
95
|
}
|
|
71
96
|
|
|
97
|
+
/** The point to the immediate left of this point. */
|
|
72
98
|
get left() {
|
|
73
99
|
return new Point(this.#x - 1, this.#y)
|
|
74
100
|
}
|
|
75
101
|
|
|
102
|
+
/** The point to the immediate right of this point. */
|
|
76
103
|
get right() {
|
|
77
104
|
return new Point(this.#x + 1, this.#y)
|
|
78
105
|
}
|
|
79
106
|
|
|
107
|
+
/** A list of the four points that surround this point: up, down, left, and right. */
|
|
80
108
|
get around() {
|
|
81
109
|
return [this.up, this.down, this.left, this.right]
|
|
82
110
|
}
|
|
83
111
|
|
|
112
|
+
/** Move in a specific direction by passing in a direction string.
|
|
113
|
+
* @arg {string} direction - any string that begins with u, d, l, or r
|
|
114
|
+
* @returns {Point}
|
|
115
|
+
*/
|
|
84
116
|
dir(direction) {
|
|
85
117
|
const error = 'direction must be one of: up, down, left, right, u, d, l, r'
|
|
86
|
-
if (
|
|
118
|
+
if (typeof direction !== 'string') throw new SyntaxError(error)
|
|
87
119
|
direction = direction.toLowerCase()
|
|
88
120
|
if (direction[0] === 'u') return this.up
|
|
89
121
|
if (direction[0] === 'd') return this.down
|
|
@@ -92,22 +124,31 @@ export const Point = class {
|
|
|
92
124
|
throw new SyntaxError(error)
|
|
93
125
|
}
|
|
94
126
|
|
|
127
|
+
/** @arg {PointLike} from - the point to measure x against. */
|
|
95
128
|
distX(from) {
|
|
96
129
|
return Math.abs(this.#x - from.x)
|
|
97
130
|
}
|
|
98
131
|
|
|
132
|
+
/** @arg {PointLike} from - the point to measure y against. */
|
|
99
133
|
distY(from) {
|
|
100
134
|
return Math.abs(this.#y - from.y)
|
|
101
135
|
}
|
|
102
136
|
|
|
137
|
+
/** @arg {PointLike} from - the point to measure against. */
|
|
103
138
|
distance(from) {
|
|
104
139
|
return Math.hypot(this.distX(from), this.distY(from))
|
|
105
140
|
}
|
|
106
141
|
|
|
142
|
+
/** @arg {PointLike} that - the point to test equality against. */
|
|
107
143
|
eq(that) {
|
|
108
144
|
return this.#x === that.x && this.#y === that.y
|
|
109
145
|
}
|
|
110
146
|
|
|
147
|
+
/** Bounding box corners can be passed in any order.
|
|
148
|
+
* @arg {PointLike} a - one corner of the bounding box
|
|
149
|
+
* @arg {PointLike} b - the opposite corner
|
|
150
|
+
* @returns {boolean} - true if the point is on or in the box
|
|
151
|
+
*/
|
|
111
152
|
in(a, b) {
|
|
112
153
|
// Allow for the bounding points to be passed in any order
|
|
113
154
|
// as long as they are opposite corners:
|
|
@@ -120,6 +161,7 @@ export const Point = class {
|
|
|
120
161
|
return 'Point(' + this.#x + ',' + this.#y + ')'
|
|
121
162
|
}
|
|
122
163
|
|
|
164
|
+
/** @returns {PointLike} */
|
|
123
165
|
toJSON() {
|
|
124
166
|
return { x: this.#x, y: this.#y }
|
|
125
167
|
}
|