@slugbugblue/trax 0.5.0 → 0.7.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 +43 -24
- package/README.md +44 -69
- package/docs/engine.md +342 -7
- package/docs/point.md +134 -0
- package/engine.js +134 -119
- package/package.json +16 -5
- package/point.js +116 -0
- package/tty.js +13 -17
- package/.eslintrc.backup.yaml +0 -68
- package/jsconfig.json +0 -8
package/docs/point.md
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
# point.js API documentation
|
|
2
|
+
|
|
3
|
+
This module provides a class structure to work with `x`, `y` locations as a
|
|
4
|
+
single point.
|
|
5
|
+
|
|
6
|
+
Since I don't need it to be super fancy, there isn't a lot of extra
|
|
7
|
+
functionality here.
|
|
8
|
+
|
|
9
|
+
## Example usage
|
|
10
|
+
|
|
11
|
+
```javascript
|
|
12
|
+
import { Point } from '@slugbugblue/trax/point.js'
|
|
13
|
+
|
|
14
|
+
const zero = new Point(0, 0)
|
|
15
|
+
|
|
16
|
+
const up = zero.up
|
|
17
|
+
console.log('up:', up.x, up.y) // up: 0 -1
|
|
18
|
+
|
|
19
|
+
const rt = zero.right
|
|
20
|
+
console.log('rt:', rt) // rt: Point(1,0)
|
|
21
|
+
|
|
22
|
+
const up2 = zero.dir('up')
|
|
23
|
+
console.log('up === up2 ?', up.eq(up2)) // up === up2 ? true
|
|
24
|
+
|
|
25
|
+
// distances
|
|
26
|
+
console.log(zero.distX(up), zero.distX(rt)) // 0 1
|
|
27
|
+
console.log(up.distX(rt), up.distY(rt)) // 0 0
|
|
28
|
+
console.log(zero.distance(up), zero.distance(rt)) // 1 1
|
|
29
|
+
console.log(up.distance(rt)) // 1.4142135623730951
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## API
|
|
33
|
+
|
|
34
|
+
### Point class
|
|
35
|
+
|
|
36
|
+
A Point instance is a representation of an immutable point in two dimensional
|
|
37
|
+
space, represented by an `x` and a `y` component. Since this implementation was
|
|
38
|
+
created for game boards, we assume that all points are integers, though that is
|
|
39
|
+
not a requirement, and the math will work equally well on non-integers.
|
|
40
|
+
|
|
41
|
+
#### constructor
|
|
42
|
+
|
|
43
|
+
##### `new Point(fromPoint)`
|
|
44
|
+
|
|
45
|
+
##### `new Point(x, y)`
|
|
46
|
+
|
|
47
|
+
You can create a point from either a point-like object (ie, any object with both
|
|
48
|
+
an `x` and a `y` key with numeric values), or from individual `x` and `y`
|
|
49
|
+
numbers. Once a point is created, it cannot be changed; however, you can use its
|
|
50
|
+
functions to return other points relative to it.
|
|
51
|
+
|
|
52
|
+
##### `x` and `y` immutable properties
|
|
53
|
+
|
|
54
|
+
You can return the individual `x` or `y` property of a point by accessing it
|
|
55
|
+
directly. Attempts to change these properties will fail.
|
|
56
|
+
|
|
57
|
+
##### `up`, `down`, `left`, `right` calculated properties
|
|
58
|
+
|
|
59
|
+
Since points are immutable, to move a point, you need to create a new point.
|
|
60
|
+
These calculated properties provide quick access to cardinal integer neighboring
|
|
61
|
+
points.
|
|
62
|
+
|
|
63
|
+
##### `around` calculated property
|
|
64
|
+
|
|
65
|
+
Or, get an array of all four cardinal points around the existing point.
|
|
66
|
+
|
|
67
|
+
##### `dir(direction)`
|
|
68
|
+
|
|
69
|
+
Pass in a string named direction (ie, one of `up`, `down`, `left`, or `right`)
|
|
70
|
+
to get a point one integer away from the existing point in the direction
|
|
71
|
+
indicated. This does the exact same thing as the calculated properties with
|
|
72
|
+
these names, but it can be done using a variable instead.
|
|
73
|
+
|
|
74
|
+
##### `distX(from)`, `distY(from)`
|
|
75
|
+
|
|
76
|
+
Returns the difference between the `x` or `y` properties of two points. This
|
|
77
|
+
will always be represented by zero or a positive number.
|
|
78
|
+
|
|
79
|
+
##### `distance(from)`
|
|
80
|
+
|
|
81
|
+
Returns the distance between two points. This will never be a negative number.
|
|
82
|
+
|
|
83
|
+
##### `eq(to)`
|
|
84
|
+
|
|
85
|
+
Returns `true` if the given point or point-like object is at the same location
|
|
86
|
+
as the existing point.
|
|
87
|
+
|
|
88
|
+
#### Plays well with others
|
|
89
|
+
|
|
90
|
+
The Point class also includes functionality to provide useful default results
|
|
91
|
+
when used in other contexts.
|
|
92
|
+
|
|
93
|
+
##### `toString()`
|
|
94
|
+
|
|
95
|
+
Returns a string representation of the point in the format `Point(x,y)`. This
|
|
96
|
+
allows simple logging calls with `console.log(point)`.
|
|
97
|
+
|
|
98
|
+
##### `toJSON()`
|
|
99
|
+
|
|
100
|
+
JSON cannot encode special classes, but this class can be stored as a bare
|
|
101
|
+
object with `x` and `y` keys when converted to JSON. Note, though, that when
|
|
102
|
+
parsing the resultant JSON string, all Point functionality is not restored by
|
|
103
|
+
default. For example:
|
|
104
|
+
|
|
105
|
+
```javascript
|
|
106
|
+
const zero = new Point(0, 0)
|
|
107
|
+
const json = JSON.stringify(zero)
|
|
108
|
+
const jsonzero = JSON.parse(json)
|
|
109
|
+
|
|
110
|
+
console.log(zero.eq(jsonzero)) // true
|
|
111
|
+
jsonzero.eq(zero) // TypeError: jsonzero.eq is not a function
|
|
112
|
+
const reconstituted = new Point(jsonzero)
|
|
113
|
+
console.log(reconstituted.eq(zero)) // true
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
##### `util.inspect.custom()`
|
|
117
|
+
|
|
118
|
+
When running in the node command line interpreter, a point will be
|
|
119
|
+
pretty-printed using the magic of `util.inspect`.
|
|
120
|
+
|
|
121
|
+
## License
|
|
122
|
+
|
|
123
|
+
Copyright 2019-2022 Chad Transtrum
|
|
124
|
+
|
|
125
|
+
Licensed under the Apache License, Version 2.0 (the "License"); you may not use
|
|
126
|
+
the files in this project except in compliance with the License. You may obtain
|
|
127
|
+
a copy of the License at
|
|
128
|
+
|
|
129
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
130
|
+
|
|
131
|
+
Unless required by applicable law or agreed to in writing, software distributed
|
|
132
|
+
under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
|
|
133
|
+
CONDITIONS OF ANY KIND, either express or implied. See the License for the
|
|
134
|
+
specific language governing permissions and limitations under the License.
|
package/engine.js
CHANGED
|
@@ -15,12 +15,15 @@
|
|
|
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
|
+
// Fun trax helper constants
|
|
25
|
+
|
|
26
|
+
const zero = new Point(0, 0)
|
|
24
27
|
const notationRegex = /^ *([@a-z]+) *(\d+) *([/\\+]) *$/i
|
|
25
28
|
|
|
26
29
|
const slash = { a: '\\', b: '+', c: '/', d: '/', e: '+', f: '\\' }
|
|
@@ -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
|
|
|
@@ -76,6 +75,7 @@ export const Trax = class {
|
|
|
76
75
|
static variants = new Set(['trax', 'traxloop', 'trax8']) // Known variants
|
|
77
76
|
|
|
78
77
|
// Static class methods
|
|
78
|
+
static point = (x, y) => new Point(x, y)
|
|
79
79
|
static colorOf = (playerNumber) => ({ 1: 'w', 2: 'b' }[playerNumber])
|
|
80
80
|
static playerNumber = (color) => ({ b: 2, w: 1 }[color])
|
|
81
81
|
static other = (color) => ({ b: 'w', w: 'b' }[color])
|
|
@@ -121,13 +121,15 @@ export const Trax = class {
|
|
|
121
121
|
this.invalid = false
|
|
122
122
|
// Moves might be a notation string
|
|
123
123
|
if (moves) {
|
|
124
|
-
if (typeof moves === 'string') moves = moves.split(
|
|
124
|
+
if (typeof moves === 'string') moves = moves.split(/\s+/)
|
|
125
125
|
for (let move of moves) {
|
|
126
126
|
if (typeof move === 'string') {
|
|
127
127
|
move = { move: this.move, notation: move }
|
|
128
128
|
}
|
|
129
129
|
|
|
130
|
-
|
|
130
|
+
if (notationRegex.test(move.notation)) {
|
|
131
|
+
this.playMove(move.move, move.notation)
|
|
132
|
+
}
|
|
131
133
|
}
|
|
132
134
|
}
|
|
133
135
|
}
|
|
@@ -162,6 +164,10 @@ export const Trax = class {
|
|
|
162
164
|
this.tiles = JSON.parse(saved.tiles)
|
|
163
165
|
this.path = JSON.parse(saved.path)
|
|
164
166
|
this.invalid = saved.invalid
|
|
167
|
+
// Reconstitute the locations
|
|
168
|
+
for (const tile of Object.values(this.tiles)) {
|
|
169
|
+
tile.loc = new Point(tile.loc)
|
|
170
|
+
}
|
|
165
171
|
}
|
|
166
172
|
|
|
167
173
|
get count() {
|
|
@@ -184,39 +190,42 @@ export const Trax = class {
|
|
|
184
190
|
return this.over ? this.turn : false // Or 1 or 2 for a win, or 0 for a tie
|
|
185
191
|
}
|
|
186
192
|
|
|
187
|
-
tileId(
|
|
188
|
-
return this.id + '-' + String(x) + 'x' + String(y)
|
|
193
|
+
tileId(loc) {
|
|
194
|
+
return this.id + '-' + String(loc.x) + 'x' + String(loc.y)
|
|
189
195
|
}
|
|
190
196
|
|
|
191
|
-
addTile(type,
|
|
197
|
+
addTile(type, loc) {
|
|
192
198
|
// Note: no validity checking is done here
|
|
193
|
-
const id = this.tileId(
|
|
194
|
-
const tile = { type,
|
|
199
|
+
const id = this.tileId(loc)
|
|
200
|
+
const tile = { type, loc, id, move: this.move, seq: this.count }
|
|
195
201
|
if (type === 'x') {
|
|
196
202
|
this.invalid = true // Invalid tile is being played
|
|
197
203
|
} else {
|
|
198
204
|
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)
|
|
205
|
+
this.left = Math.min(this.left, loc.x)
|
|
206
|
+
this.right = Math.max(this.right, loc.x)
|
|
207
|
+
this.top = Math.min(this.top, loc.y)
|
|
208
|
+
this.bottom = Math.max(this.bottom, loc.y)
|
|
203
209
|
}
|
|
204
210
|
|
|
205
211
|
return tile
|
|
206
212
|
}
|
|
207
213
|
|
|
208
|
-
tileAt(
|
|
209
|
-
return (this.tiles[this.tileId(
|
|
214
|
+
tileAt(loc) {
|
|
215
|
+
return (this.tiles[this.tileId(loc)] || {}).type
|
|
210
216
|
}
|
|
211
217
|
|
|
212
|
-
validTile(type,
|
|
213
|
-
if (this.count === 0 && x === 0 && y === 0)
|
|
218
|
+
validTile(type, loc) {
|
|
219
|
+
if (this.count === 0 && loc.x === 0 && loc.y === 0) {
|
|
214
220
|
return type === 'd' || type === 'e'
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
const
|
|
218
|
-
const
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const up = downColor(this.tileAt(loc.up))
|
|
224
|
+
const down = upColor(this.tileAt(loc.down))
|
|
225
|
+
const left = rightColor(this.tileAt(loc.left))
|
|
226
|
+
const right = leftColor(this.tileAt(loc.right))
|
|
219
227
|
return (
|
|
228
|
+
(up || down || left || right) &&
|
|
220
229
|
(!up || up === upColor(type)) &&
|
|
221
230
|
(!down || down === downColor(type)) &&
|
|
222
231
|
(!left || left === leftColor(type)) &&
|
|
@@ -224,11 +233,11 @@ export const Trax = class {
|
|
|
224
233
|
)
|
|
225
234
|
}
|
|
226
235
|
|
|
227
|
-
possibleTiles(
|
|
236
|
+
possibleTiles(loc, s = false) {
|
|
228
237
|
// S: slash (/ or \ or +)
|
|
229
238
|
const possibles = []
|
|
230
239
|
for (const t of ['a', 'b', 'c', 'd', 'e', 'f']) {
|
|
231
|
-
if ((!s || s === slash[t]) && this.validTile(t,
|
|
240
|
+
if ((!s || s === slash[t]) && this.validTile(t, loc)) possibles.push(t)
|
|
232
241
|
}
|
|
233
242
|
|
|
234
243
|
return possibles
|
|
@@ -242,41 +251,42 @@ export const Trax = class {
|
|
|
242
251
|
return this.right - this.left + 1
|
|
243
252
|
}
|
|
244
253
|
|
|
245
|
-
validLocation(
|
|
254
|
+
validLocation(loc) {
|
|
246
255
|
if (this.over) return false
|
|
247
|
-
if (this.tileAt(
|
|
248
|
-
if (this.count === 0 &&
|
|
256
|
+
if (this.tileAt(loc)) return false
|
|
257
|
+
if (this.count === 0 && loc.eq(zero)) return true
|
|
249
258
|
if (this.rules === 'trax8') {
|
|
250
|
-
if (this.width > 7 && (x < this.left || x > this.right))
|
|
251
|
-
|
|
259
|
+
if (this.width > 7 && (loc.x < this.left || loc.x > this.right)) {
|
|
260
|
+
return false
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
if (this.height > 7 && (loc.y < this.top || loc.y > this.bottom)) {
|
|
264
|
+
return false
|
|
265
|
+
}
|
|
252
266
|
}
|
|
253
267
|
|
|
254
|
-
|
|
255
|
-
this.tileAt(
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
)
|
|
268
|
+
for (const location of loc.around) {
|
|
269
|
+
if (this.tileAt(location)) return true
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
return false
|
|
260
273
|
}
|
|
261
274
|
|
|
262
275
|
possibleLocations() {
|
|
263
|
-
const locs = {
|
|
264
|
-
const maybe = (
|
|
276
|
+
const locs = { [this.tileId(zero)]: zero } // If no tiles are placed, we can play a starting tile
|
|
277
|
+
const maybe = (loc) => {
|
|
265
278
|
// Add a loc if a tile isn't already there
|
|
266
|
-
const
|
|
267
|
-
locs[
|
|
279
|
+
const id = this.tileId(loc)
|
|
280
|
+
locs[id] = locs[id] || loc
|
|
268
281
|
}
|
|
269
282
|
|
|
270
283
|
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)
|
|
284
|
+
locs[tile.id] = 'tile' // Add an existing tile
|
|
285
|
+
tile.loc.around.map((loc) => maybe(loc))
|
|
276
286
|
}
|
|
277
287
|
|
|
278
288
|
// Get rid of all the real tiles and return all the maybes
|
|
279
|
-
return Object.values(locs).filter((
|
|
289
|
+
return Object.values(locs).filter((loc) => loc !== 'tile')
|
|
280
290
|
}
|
|
281
291
|
|
|
282
292
|
possibleMoves() {
|
|
@@ -285,8 +295,8 @@ export const Trax = class {
|
|
|
285
295
|
const possibles = []
|
|
286
296
|
if (this.over) return possibles // Shortcut if the game is over
|
|
287
297
|
for (const loc of this.possibleLocations()) {
|
|
288
|
-
for (const tile of this.possibleTiles(loc
|
|
289
|
-
const drop = this.dropTile(tile, loc
|
|
298
|
+
for (const tile of this.possibleTiles(loc)) {
|
|
299
|
+
const drop = this.dropTile(tile, loc, 'tentative')
|
|
290
300
|
if (drop.valid) possibles.push(drop.notation)
|
|
291
301
|
}
|
|
292
302
|
}
|
|
@@ -294,22 +304,22 @@ export const Trax = class {
|
|
|
294
304
|
return possibles
|
|
295
305
|
}
|
|
296
306
|
|
|
297
|
-
forcedMoves(
|
|
298
|
-
// We just played at
|
|
307
|
+
forcedMoves(loc) {
|
|
308
|
+
// We just played at loc, so return all forced moves
|
|
299
309
|
const forced = []
|
|
300
310
|
const invalids = {}
|
|
301
|
-
let check =
|
|
311
|
+
let check = loc.around
|
|
302
312
|
while (check.length > 0) {
|
|
303
313
|
const pos = check.shift()
|
|
304
|
-
if (this.validLocation(pos
|
|
305
|
-
const possibles = this.possibleTiles(pos
|
|
314
|
+
if (this.validLocation(pos)) {
|
|
315
|
+
const possibles = this.possibleTiles(pos)
|
|
306
316
|
if (possibles.length === 1) {
|
|
307
317
|
// Only 1 possible move
|
|
308
|
-
forced.push(this.addTile(possibles[0], pos
|
|
309
|
-
check = [...check, ...
|
|
318
|
+
forced.push(this.addTile(possibles[0], pos))
|
|
319
|
+
check = [...check, ...pos.around]
|
|
310
320
|
} else if (possibles.length === 0) {
|
|
311
321
|
// No possible moves at all
|
|
312
|
-
const invalid = this.addTile('x', pos
|
|
322
|
+
const invalid = this.addTile('x', pos)
|
|
313
323
|
if (!invalids[invalid.id]) forced.push(invalid)
|
|
314
324
|
invalids[invalid.id] = true
|
|
315
325
|
}
|
|
@@ -319,62 +329,72 @@ export const Trax = class {
|
|
|
319
329
|
return forced
|
|
320
330
|
}
|
|
321
331
|
|
|
322
|
-
notate(type,
|
|
332
|
+
notate(type, loc) {
|
|
323
333
|
// Notation depends on the position of the board BEFORE the move
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
334
|
+
let notation = Trax.encodeCol(loc.x - this.left + 1)
|
|
335
|
+
notation += String(loc.y - this.top + 1)
|
|
336
|
+
notation += slash[type]
|
|
337
|
+
return notation
|
|
327
338
|
}
|
|
328
339
|
|
|
329
340
|
decodeNotation(notation) {
|
|
330
|
-
// Return {type,
|
|
331
|
-
const
|
|
341
|
+
// Return {type, loc} from a notation
|
|
342
|
+
const bad = { type: 'x', loc: zero }
|
|
332
343
|
const match = notation.match(notationRegex)
|
|
333
|
-
if (match === null) return
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
344
|
+
if (match === null) return bad
|
|
345
|
+
const loc = new Point(
|
|
346
|
+
this.left - 1 + Trax.decodeCol(match[1]),
|
|
347
|
+
this.top - 1 + Number(match[2]),
|
|
348
|
+
)
|
|
349
|
+
const possible = this.possibleTiles(loc, match[3])
|
|
350
|
+
if (possible.length > 0) return { type: possible[0], loc }
|
|
351
|
+
return bad
|
|
339
352
|
}
|
|
340
353
|
|
|
341
|
-
follow(color,
|
|
354
|
+
follow(color, loc, from) {
|
|
342
355
|
const path = []
|
|
343
|
-
let id = this.tileId(
|
|
344
|
-
let tile = this.tileAt(
|
|
356
|
+
let id = this.tileId(loc)
|
|
357
|
+
let tile = this.tileAt(loc)
|
|
345
358
|
while (tile && path[0] !== id) {
|
|
346
359
|
if (from !== 'top' && upColor(tile) === color) {
|
|
347
360
|
from = 'bottom'
|
|
348
|
-
|
|
361
|
+
loc = loc.up
|
|
349
362
|
} else if (from !== 'bottom' && downColor(tile) === color) {
|
|
350
363
|
from = 'top'
|
|
351
|
-
|
|
364
|
+
loc = loc.down
|
|
352
365
|
} else if (from !== 'left' && leftColor(tile) === color) {
|
|
353
366
|
from = 'right'
|
|
354
|
-
|
|
367
|
+
loc = loc.left
|
|
355
368
|
} else if (from !== 'right' && rightColor(tile) === color) {
|
|
356
369
|
from = 'left'
|
|
357
|
-
|
|
370
|
+
loc = loc.right
|
|
358
371
|
}
|
|
359
372
|
|
|
360
373
|
path.push(id)
|
|
361
|
-
id = this.tileId(
|
|
362
|
-
tile = this.tileAt(
|
|
374
|
+
id = this.tileId(loc)
|
|
375
|
+
tile = this.tileAt(loc)
|
|
363
376
|
}
|
|
364
377
|
|
|
365
|
-
return {
|
|
378
|
+
return { loc, path }
|
|
366
379
|
}
|
|
367
380
|
|
|
368
|
-
findEnds(color,
|
|
381
|
+
findEnds(color, loc) {
|
|
369
382
|
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,
|
|
383
|
+
const tile = this.tileAt(loc)
|
|
384
|
+
if (upColor(tile) === color) ends.push(this.follow(color, loc, 'top'))
|
|
385
|
+
if (downColor(tile) === color) ends.push(this.follow(color, loc, 'bottom'))
|
|
386
|
+
if (leftColor(tile) === color) ends.push(this.follow(color, loc, 'left'))
|
|
387
|
+
if (rightColor(tile) === color) ends.push(this.follow(color, loc, 'right'))
|
|
375
388
|
return ends
|
|
376
389
|
}
|
|
377
390
|
|
|
391
|
+
lineWin(locA, locB) {
|
|
392
|
+
if (this.rules === 'traxloop') return false
|
|
393
|
+
if (this.width > 7 && locA.distX(locB) > this.width) return true
|
|
394
|
+
if (this.height > 7 && locA.distY(locB) > this.height) return true
|
|
395
|
+
return false
|
|
396
|
+
}
|
|
397
|
+
|
|
378
398
|
checkWin(tiles) {
|
|
379
399
|
let winner = false
|
|
380
400
|
const colors = [this.color, Trax.other(this.color)] // Check win for current player first
|
|
@@ -382,15 +402,11 @@ export const Trax = class {
|
|
|
382
402
|
if (winner) continue // Skip the second player if the first has won
|
|
383
403
|
for (const tile of tiles) {
|
|
384
404
|
if (winner) continue // Skip the remaining tiles once we find a win
|
|
385
|
-
const ends = this.findEnds(color, tile.
|
|
386
|
-
if (ends[0].
|
|
405
|
+
const ends = this.findEnds(color, tile.loc)
|
|
406
|
+
if (ends[0].loc.eq(ends[1].loc)) {
|
|
387
407
|
winner = color // Loop win
|
|
388
408
|
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
|
-
) {
|
|
409
|
+
} else if (this.lineWin(ends[0].loc, ends[1].loc)) {
|
|
394
410
|
winner = color // Line win, paths need to be combined
|
|
395
411
|
this.path = [...ends[0].path.reverse(), ...ends[1].path.slice(1)]
|
|
396
412
|
}
|
|
@@ -417,9 +433,9 @@ export const Trax = class {
|
|
|
417
433
|
return this.dropTile(notation)
|
|
418
434
|
}
|
|
419
435
|
|
|
420
|
-
playMove(
|
|
436
|
+
playMove(currentMoveNumber, nextMoveNotation) {
|
|
421
437
|
// Generic API call for dropTile
|
|
422
|
-
if (
|
|
438
|
+
if (currentMoveNumber === this.move) return this.dropTile(nextMoveNotation)
|
|
423
439
|
return { valid: false }
|
|
424
440
|
}
|
|
425
441
|
|
|
@@ -441,7 +457,7 @@ export const Trax = class {
|
|
|
441
457
|
.filter((n) => !n.endsWith('.'))
|
|
442
458
|
}
|
|
443
459
|
|
|
444
|
-
dropTile(type,
|
|
460
|
+
dropTile(type, loc, tentative = false) {
|
|
445
461
|
// Pass in tentative to not save the move
|
|
446
462
|
let dropped = []
|
|
447
463
|
let notation = ''
|
|
@@ -461,16 +477,18 @@ export const Trax = class {
|
|
|
461
477
|
} else {
|
|
462
478
|
if (type.search(notationRegex) === 0) {
|
|
463
479
|
// We were passed a notation
|
|
464
|
-
|
|
480
|
+
const decoded = this.decodeNotation(type)
|
|
481
|
+
type = decoded.type
|
|
482
|
+
loc = decoded.loc
|
|
465
483
|
}
|
|
466
484
|
|
|
467
|
-
if (this.validLocation(
|
|
485
|
+
if (this.validLocation(loc) && this.validTile(type, loc)) {
|
|
468
486
|
valid = true
|
|
469
487
|
const saved = this.save() // Just in case we need to roll back
|
|
470
|
-
notation = this.notate(type,
|
|
488
|
+
notation = this.notate(type, loc) // Determine notation BEFORE we play the move
|
|
471
489
|
this.updateNotation(notation)
|
|
472
|
-
dropped.push(this.addTile(type,
|
|
473
|
-
dropped = [...dropped, ...this.forcedMoves(
|
|
490
|
+
dropped.push(this.addTile(type, loc)) // Play the tile
|
|
491
|
+
dropped = [...dropped, ...this.forcedMoves(loc)] // Play all forced moves
|
|
474
492
|
if (this.invalid || tentative) {
|
|
475
493
|
if (this.invalid) valid = false
|
|
476
494
|
this.restore(saved) // Abort! abort!
|
|
@@ -490,6 +508,8 @@ export const Trax = class {
|
|
|
490
508
|
if (match === null) return []
|
|
491
509
|
const x = Trax.decodeCol(match[1])
|
|
492
510
|
const y = Number(match[2])
|
|
511
|
+
const X = this.height - x + 1
|
|
512
|
+
const Y = this.width - y + 1
|
|
493
513
|
const s = match[3]
|
|
494
514
|
const slashes = s === '+' ? [s] : ['/', '\\']
|
|
495
515
|
const moves = new Set()
|
|
@@ -497,16 +517,8 @@ export const Trax = class {
|
|
|
497
517
|
for (const slash of slashes) {
|
|
498
518
|
moves.add(Trax.encodeCol(x) + String(y) + slash)
|
|
499
519
|
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
|
-
)
|
|
520
|
+
moves.add(Trax.encodeCol(X) + String(Y) + slash)
|
|
521
|
+
moves.add(Trax.encodeCol(Y) + String(X) + slash)
|
|
510
522
|
}
|
|
511
523
|
|
|
512
524
|
return [...moves]
|
|
@@ -547,7 +559,7 @@ export const Trax = class {
|
|
|
547
559
|
return this.positionCode()
|
|
548
560
|
}
|
|
549
561
|
|
|
550
|
-
|
|
562
|
+
dropsIcon(drops) {
|
|
551
563
|
return this.positionCode(undefined, undefined, undefined, drops)
|
|
552
564
|
}
|
|
553
565
|
|
|
@@ -562,19 +574,22 @@ export const Trax = class {
|
|
|
562
574
|
let x = startX
|
|
563
575
|
let y = startY
|
|
564
576
|
do {
|
|
565
|
-
|
|
577
|
+
const loc = new Point(x, y)
|
|
578
|
+
let tile = this.tileAt(loc) // Normal tile for normal orientation
|
|
566
579
|
if (tile) {
|
|
567
580
|
tile = applySymmetry(tile, rightToLeft, bottomToTop, rotate)
|
|
568
|
-
if (this.path.includes(this.tileId(
|
|
581
|
+
if (this.path.includes(this.tileId(loc))) {
|
|
569
582
|
tile = String.fromCodePoint(tile.codePointAt(0) + this.turn * 6)
|
|
570
583
|
}
|
|
571
584
|
|
|
572
585
|
if (
|
|
573
586
|
drops &&
|
|
574
587
|
drops.dropped &&
|
|
575
|
-
drops.dropped.some((t) =>
|
|
576
|
-
)
|
|
588
|
+
drops.dropped.some((t) => loc.eq(t.loc))
|
|
589
|
+
) {
|
|
577
590
|
tile = tile.toUpperCase()
|
|
591
|
+
}
|
|
592
|
+
|
|
578
593
|
line += tile
|
|
579
594
|
} else {
|
|
580
595
|
line = addBlank(line)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@slugbugblue/trax",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "Trax game engine and friends",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"trax",
|
|
@@ -18,11 +18,16 @@
|
|
|
18
18
|
"preversion": "npm test",
|
|
19
19
|
"postversion": "git push && git push --tags"
|
|
20
20
|
},
|
|
21
|
+
"dependencies": {
|
|
22
|
+
"env-paths": "^3.0.0",
|
|
23
|
+
"make-dir": "^3.1.0",
|
|
24
|
+
"yaml": "^2.0.0-11"
|
|
25
|
+
},
|
|
21
26
|
"devDependencies": {
|
|
22
|
-
"ava": "^4.0
|
|
27
|
+
"ava": "^4.1.0",
|
|
23
28
|
"c8": "^7.11.0",
|
|
24
|
-
"prettier": "^2.
|
|
25
|
-
"xo": "^0.
|
|
29
|
+
"prettier": "^2.6.1",
|
|
30
|
+
"xo": "^0.48.0"
|
|
26
31
|
},
|
|
27
32
|
"type": "module",
|
|
28
33
|
"engines": {
|
|
@@ -37,6 +42,12 @@
|
|
|
37
42
|
"useTabs": false
|
|
38
43
|
},
|
|
39
44
|
"xo": {
|
|
40
|
-
"prettier": true
|
|
45
|
+
"prettier": true,
|
|
46
|
+
"rules": {
|
|
47
|
+
"curly": [
|
|
48
|
+
"error",
|
|
49
|
+
"multi-line"
|
|
50
|
+
]
|
|
51
|
+
}
|
|
41
52
|
}
|
|
42
53
|
}
|