@slugbugblue/trax 0.4.0 → 0.8.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 +68 -17
- package/README.md +123 -69
- package/docs/engine.md +414 -0
- package/docs/point.md +134 -0
- package/package.json +33 -6
- package/src/cli.js +994 -0
- package/src/engine.js +657 -0
- package/src/point.js +116 -0
- package/src/tty.js +295 -0
- package/src/version.js +2 -0
- package/.eslintrc.yaml +0 -68
- package/engine.js +0 -535
- package/jsconfig.json +0 -8
- package/tty.js +0 -202
package/engine.js
DELETED
|
@@ -1,535 +0,0 @@
|
|
|
1
|
-
/* Copyright 2019-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
|
-
// Trax internals. Woot.
|
|
17
|
-
|
|
18
|
-
// tile type names are determined by listing the color of line at each edge, clockwise
|
|
19
|
-
// and then sorted alphabetically and given a single letter name, so 'bbww' becomes 'a'
|
|
20
|
-
// which gives us six different tile names: a-f
|
|
21
|
-
|
|
22
|
-
const variants = ['trax', 'traxloop', 'trax8'] // known variants
|
|
23
|
-
|
|
24
|
-
// fun trax helper constants
|
|
25
|
-
|
|
26
|
-
const slash = { a: '\\', b: '+', c: '/', d: '/', e: '+', f: '\\' }
|
|
27
|
-
|
|
28
|
-
const symmetry = { // convert tile to different views
|
|
29
|
-
flip: { a: 'd', b: 'b', c: 'f', d: 'a', e: 'e', f: 'c' },
|
|
30
|
-
mirror: { a: 'c', b: 'b', c: 'a', d: 'f', e: 'e', f: 'd' },
|
|
31
|
-
swap: { a: 'f', b: 'e', c: 'd', d: 'c', e: 'b', f: 'a' },
|
|
32
|
-
counterclock: { a: 'c', b: 'e', c: 'f', d: 'a', e: 'b', f: 'd' },
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
const notation_regex = /^ *([@a-z]+) *(\d+) *([/\\+]) *$/i
|
|
36
|
-
|
|
37
|
-
const color_of = { 1: 'w', 2: 'b' }
|
|
38
|
-
const player_num = { b: 2, w: 1 }
|
|
39
|
-
const other = { b: 'w', w: 'b' }
|
|
40
|
-
|
|
41
|
-
// fun trax helper functions
|
|
42
|
-
|
|
43
|
-
// unexpected note: str.includes(t) is true if t is '' but false if t is undefined
|
|
44
|
-
const up_color = t => 'abc'.includes(t) ? 'b' : 'def'.includes(t) ? 'w' : false
|
|
45
|
-
const down_color = t => 'bdf'.includes(t) ? 'b' : 'ace'.includes(t) ? 'w' : false
|
|
46
|
-
const right_color = t => 'ade'.includes(t) ? 'b' : 'bcf'.includes(t) ? 'w' : false
|
|
47
|
-
const left_color = t => 'cef'.includes(t) ? 'b' : 'abd'.includes(t) ? 'w' : false
|
|
48
|
-
const squares_around = (x, y) => [{x:x-1,y},{x:x+1,y},{x,y:y-1},{x,y:y+1}]
|
|
49
|
-
const encode_col = col => { // for notation purposes: 0 -> @, 6 -> F, 28 -> AB
|
|
50
|
-
let n = ''
|
|
51
|
-
while (col > 0) {
|
|
52
|
-
n = String.fromCharCode(65 + (col - 1) % 26) + n
|
|
53
|
-
col = Math.floor((col - 1) / 26)
|
|
54
|
-
}
|
|
55
|
-
return n || '@'
|
|
56
|
-
}
|
|
57
|
-
const decode_col = col => { // turn letters back into numbers
|
|
58
|
-
col = col.toUpperCase()
|
|
59
|
-
let n = 0
|
|
60
|
-
while (col.length) {
|
|
61
|
-
n = n + (col.charCodeAt(0) - 64) * Math.pow(26, col.length - 1)
|
|
62
|
-
col = col.slice(1)
|
|
63
|
-
}
|
|
64
|
-
return n
|
|
65
|
-
}
|
|
66
|
-
const add_blank = code => { // encode missing tiles as digits 0-9, for 1-10 missing
|
|
67
|
-
let num = code.charCodeAt(code.length - 1) // this is NaN if code is empty, which is ok
|
|
68
|
-
if (num > 47 && num < 57) return code.slice(0, code.length -1) + String.fromCharCode(num + 1)
|
|
69
|
-
return code += '0'
|
|
70
|
-
}
|
|
71
|
-
// ugh, this is a dirty little function to get the length of an encoded position row
|
|
72
|
-
const code_row_len = row => row.replace(/[0-9]/g, n => '.'.repeat(Number(n)+1)).length
|
|
73
|
-
|
|
74
|
-
// this is where the magic happens
|
|
75
|
-
const Trax = class {
|
|
76
|
-
constructor (rules='trax', moves=[], id='trax') {
|
|
77
|
-
if (!variants.includes(rules)) rules = 'trax'
|
|
78
|
-
this.id = String(id)
|
|
79
|
-
this.rules = rules
|
|
80
|
-
this.move = 0
|
|
81
|
-
this.turn = 1
|
|
82
|
-
this.over = false
|
|
83
|
-
this.left = 1
|
|
84
|
-
this.right = 0
|
|
85
|
-
this.top = 1
|
|
86
|
-
this.bottom = 0
|
|
87
|
-
this.notation = ''
|
|
88
|
-
this.tiles = {}
|
|
89
|
-
this.path = []
|
|
90
|
-
this.invalid = false
|
|
91
|
-
// moves might be a notation string
|
|
92
|
-
if (moves) {
|
|
93
|
-
if (typeof moves === 'string') moves = moves.split(' ')
|
|
94
|
-
moves.forEach(move => {
|
|
95
|
-
if (typeof move === 'string') {
|
|
96
|
-
move = { move: this.move, notation: move }
|
|
97
|
-
}
|
|
98
|
-
this.play_move(move.move, move.notation)
|
|
99
|
-
})
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
save () { // in order to evaluate potential moves, we have to modify the state
|
|
104
|
-
return {
|
|
105
|
-
move: this.move,
|
|
106
|
-
turn: this.turn,
|
|
107
|
-
over: this.over,
|
|
108
|
-
left: this.left,
|
|
109
|
-
right: this.right,
|
|
110
|
-
top: this.top,
|
|
111
|
-
bottom: this.bottom,
|
|
112
|
-
notation: this.notation,
|
|
113
|
-
tiles: JSON.stringify(this.tiles),
|
|
114
|
-
path: JSON.stringify(this.path),
|
|
115
|
-
invalid: this.invalid,
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
restore (saved) { // so we need to be able to restore it when we are done
|
|
120
|
-
this.move = saved.move
|
|
121
|
-
this.turn = saved.turn
|
|
122
|
-
this.over = saved.over
|
|
123
|
-
this.left = saved.left
|
|
124
|
-
this.right = saved.right
|
|
125
|
-
this.top = saved.top
|
|
126
|
-
this.bottom = saved.bottom
|
|
127
|
-
this.notation = saved.notation
|
|
128
|
-
this.tiles = JSON.parse(saved.tiles)
|
|
129
|
-
this.path = JSON.parse(saved.path)
|
|
130
|
-
this.invalid = saved.invalid
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
get count () { // get the number of tiles currently in play
|
|
134
|
-
return Object.keys(this.tiles).length
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
get color () { // get the color of the current player (w or b)
|
|
138
|
-
return color_of[this.turn]
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
get game_over () { // returns true if the game is over
|
|
142
|
-
return this.over
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
get winner () { // returns false if the game is still in progress
|
|
146
|
-
return this.over ? this.turn : false // or 1 or 2 for a win, or 0 for a tie
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
tile_id (x, y) {
|
|
150
|
-
return this.id + '-' + String(x) + 'x' + String(y)
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
add_tile (type, x, y) { // note: no validity checking is done here
|
|
154
|
-
const id = this.tile_id(x, y)
|
|
155
|
-
const tile = { type, x, y, id, move: this.move, seq: this.count }
|
|
156
|
-
if (type === 'x') {
|
|
157
|
-
this.invalid = true // invalid tile is being played
|
|
158
|
-
} else {
|
|
159
|
-
this.tiles[id] = tile
|
|
160
|
-
this.left = Math.min(this.left, x)
|
|
161
|
-
this.right = Math.max(this.right, x)
|
|
162
|
-
this.top = Math.min(this.top, y)
|
|
163
|
-
this.bottom = Math.max(this.bottom, y)
|
|
164
|
-
}
|
|
165
|
-
return tile
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
tile_at (x, y) {
|
|
169
|
-
return (this.tiles[this.tile_id(x, y)] || {}).type
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
valid_tile (type, x, y) {
|
|
173
|
-
if (this.count === 0 && x === 0 && y === 0) return type === 'd' || type === 'e'
|
|
174
|
-
let up = down_color(this.tile_at(x, y - 1))
|
|
175
|
-
let down = up_color(this.tile_at(x, y + 1))
|
|
176
|
-
let left = right_color(this.tile_at(x - 1, y))
|
|
177
|
-
let right = left_color(this.tile_at(x + 1, y))
|
|
178
|
-
return (!up || up === up_color(type)) && (!down || down === down_color(type)) &&
|
|
179
|
-
(!left || left === left_color(type)) && (!right || right === right_color(type))
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
possible_tiles (x, y, s=false) { // s: slash (/ or \ or +)
|
|
183
|
-
let possibles = []
|
|
184
|
-
;['a', 'b', 'c', 'd', 'e', 'f'].forEach(t => {
|
|
185
|
-
if ((!s || s === slash[t]) && this.valid_tile(t, x, y)) possibles.push(t)
|
|
186
|
-
})
|
|
187
|
-
return possibles
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
get height () {
|
|
191
|
-
return this.bottom - this.top + 1
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
get width () {
|
|
195
|
-
return this.right - this.left + 1
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
valid_location (x, y) {
|
|
199
|
-
if (this.over) return false
|
|
200
|
-
if (this.tile_at(x, y)) return false
|
|
201
|
-
if (this.count === 0 && x === 0 && y === 0) return true
|
|
202
|
-
if (this.rules === 'trax8') {
|
|
203
|
-
if (this.width > 7 && (x < this.left || x > this.right)) return false
|
|
204
|
-
if (this.height > 7 && (y < this.top || y > this.bottom)) return false
|
|
205
|
-
}
|
|
206
|
-
return Boolean(
|
|
207
|
-
this.tile_at(x-1, y) || this.tile_at(x+1, y) ||
|
|
208
|
-
this.tile_at(x, y-1) || this.tile_at(x, y+1)
|
|
209
|
-
)
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
possible_locations () {
|
|
213
|
-
let locs = { '0x0': { x:0, y:0 } } // if no tiles are placed, we can play a starting tile
|
|
214
|
-
const maybe = (x, y) => { // add a loc if a tile isn't already there
|
|
215
|
-
let loc = x + 'x' + y
|
|
216
|
-
locs[loc] = locs[loc] || { x, y }
|
|
217
|
-
}
|
|
218
|
-
for (let tile of Object.values(this.tiles)) {
|
|
219
|
-
locs[tile.x + 'x' + tile.y] = 'tile' // add an existing tile
|
|
220
|
-
maybe(tile.x - 1, tile.y) // and four maybe tiles around it
|
|
221
|
-
maybe(tile.x + 1, tile.y) // which will either be replaced by real tiles
|
|
222
|
-
maybe(tile.x, tile.y - 1) // or will be potentially valid locations
|
|
223
|
-
maybe(tile.x, tile.y + 1)
|
|
224
|
-
}
|
|
225
|
-
// get rid of all the real tiles and return all the maybes
|
|
226
|
-
return Object.values(locs).filter(l => l !== 'tile')
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
possible_moves () { // returns a list of every currently possible move
|
|
230
|
-
// as notations, ie ['@1+', '@1/', '@1\\', ...]
|
|
231
|
-
let possibles = []
|
|
232
|
-
if (this.over) return possibles // shortcut if the game is over
|
|
233
|
-
this.possible_locations().forEach(loc => {
|
|
234
|
-
this.possible_tiles(loc.x, loc.y).forEach(tile => {
|
|
235
|
-
let drop = this.drop_tile(tile, loc.x, loc.y, 'tentative')
|
|
236
|
-
if (drop.valid) possibles.push(drop.notation)
|
|
237
|
-
})
|
|
238
|
-
})
|
|
239
|
-
return possibles
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
forced_moves (x, y) { // we just played at x, y, so return all forced moves
|
|
243
|
-
let forced = []
|
|
244
|
-
let invalids = {}
|
|
245
|
-
let check = squares_around(x, y)
|
|
246
|
-
while (check.length) {
|
|
247
|
-
let pos = check.shift()
|
|
248
|
-
if (this.valid_location(pos.x, pos.y)) {
|
|
249
|
-
let possibles = this.possible_tiles(pos.x, pos.y)
|
|
250
|
-
if (possibles.length === 1) { // only 1 possible move
|
|
251
|
-
forced.push(this.add_tile(possibles[0], pos.x, pos.y))
|
|
252
|
-
check = check.concat(squares_around(pos.x, pos.y))
|
|
253
|
-
} else if (possibles.length === 0) { // no possible moves at all
|
|
254
|
-
let invalid = this.add_tile('x', pos.x, pos.y)
|
|
255
|
-
if (!invalids[invalid.id]) forced.push(invalid)
|
|
256
|
-
invalids[invalid.id] = true
|
|
257
|
-
}
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
return forced
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
notate (type, x, y) { // notation depends on the position of the board BEFORE the move
|
|
264
|
-
return encode_col(x - this.left + 1) + String(y - this.top + 1) + slash[type]
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
decode_notation (notation) { // return {type, x, y} from a notation
|
|
268
|
-
let r = { type: 'x', x: 0, y: 0 } // default to an invalid move
|
|
269
|
-
let match = notation.match(notation_regex)
|
|
270
|
-
if (match === null) return r
|
|
271
|
-
r.x = this.left - 1 + decode_col(match[1])
|
|
272
|
-
r.y = this.top - 1 + Number(match[2])
|
|
273
|
-
let possible = this.possible_tiles(r.x, r.y, match[3])
|
|
274
|
-
if (possible) r.type = possible[0]
|
|
275
|
-
return r
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
follow (color, x, y, from) {
|
|
279
|
-
let path = []
|
|
280
|
-
let id = this.tile_id(x, y)
|
|
281
|
-
let tile = this.tile_at(x, y)
|
|
282
|
-
while (tile && path[0] !== id) {
|
|
283
|
-
if (from !== 'top' && up_color(tile) === color) {
|
|
284
|
-
from = 'bottom'
|
|
285
|
-
y--
|
|
286
|
-
} else if (from !== 'bottom' && down_color(tile) === color) {
|
|
287
|
-
from = 'top'
|
|
288
|
-
y++
|
|
289
|
-
} else if (from !== 'left' && left_color(tile) === color) {
|
|
290
|
-
from = 'right'
|
|
291
|
-
x--
|
|
292
|
-
} else if (from !== 'right' && right_color(tile) === color) {
|
|
293
|
-
from = 'left'
|
|
294
|
-
x++
|
|
295
|
-
}
|
|
296
|
-
path.push(id)
|
|
297
|
-
id = this.tile_id(x, y)
|
|
298
|
-
tile = this.tile_at(x, y)
|
|
299
|
-
}
|
|
300
|
-
return { x, y, path }
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
find_ends (color, x, y) {
|
|
304
|
-
let ends = []
|
|
305
|
-
let tile = this.tile_at(x, y)
|
|
306
|
-
if (up_color(tile) === color) ends.push(this.follow(color, x, y, 'top'))
|
|
307
|
-
if (down_color(tile) === color) ends.push(this.follow(color, x, y, 'bottom'))
|
|
308
|
-
if (left_color(tile) === color) ends.push(this.follow(color, x, y, 'left'))
|
|
309
|
-
if (right_color(tile) === color) ends.push(this.follow(color, x, y, 'right'))
|
|
310
|
-
return ends
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
check_win (tiles) {
|
|
314
|
-
let winner = false
|
|
315
|
-
let colors = [ this.color, other[this.color] ] // check win for current player first
|
|
316
|
-
colors.forEach(color => {
|
|
317
|
-
if (winner) return // skip the second player if the first has won
|
|
318
|
-
tiles.forEach(tile => {
|
|
319
|
-
if (winner) return // skip the remaining tiles once we find a win
|
|
320
|
-
let ends = this.find_ends(color, tile.x, tile.y)
|
|
321
|
-
if (ends[0].x === ends[1].x && ends[0].y === ends[1].y) {
|
|
322
|
-
winner = color // loop win
|
|
323
|
-
this.path = ends[0].path // both paths are essentially the same
|
|
324
|
-
} else if (this.rules !== 'traxloop') { // check for line win
|
|
325
|
-
if (this.width > 7 && Math.abs(ends[0].x - ends[1].x) > this.width ||
|
|
326
|
-
this.height > 7 && Math.abs(ends[0].y - ends[1].y) > this.height) {
|
|
327
|
-
winner = color // line win, paths need to be combined
|
|
328
|
-
this.path = ends[0].path.reverse().concat(ends[1].path.slice(1))
|
|
329
|
-
}
|
|
330
|
-
}
|
|
331
|
-
})
|
|
332
|
-
})
|
|
333
|
-
if (winner) {
|
|
334
|
-
this.over = true
|
|
335
|
-
this.turn = player_num[winner]
|
|
336
|
-
} else {
|
|
337
|
-
// 8x8 Trax can end in a tie once there are no valid moves left
|
|
338
|
-
if (this.rules === 'trax8' && this.width > 7 && this.height > 7) {
|
|
339
|
-
if (this.possible_moves().length < 1) {
|
|
340
|
-
this.over = true
|
|
341
|
-
this.turn = 0 // tie
|
|
342
|
-
}
|
|
343
|
-
}
|
|
344
|
-
}
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
play (notation) { // fast and easy way to play a move
|
|
348
|
-
return this.drop_tile(notation)
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
play_move (move_num, notation) { // generic API call for drop_tile
|
|
352
|
-
if (move_num === this.move) return this.drop_tile(notation)
|
|
353
|
-
return { valid: false }
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
update_notation (notation) {
|
|
357
|
-
this.move++
|
|
358
|
-
let note = this.move +'. ' + notation
|
|
359
|
-
let line = this.notation.split('\n').pop()
|
|
360
|
-
if (line) {
|
|
361
|
-
note = (line.length + note.length > 79 ? '\n' : ' ') + note
|
|
362
|
-
}
|
|
363
|
-
this.notation += note
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
get moves () {
|
|
367
|
-
return this.notation.replace(/\n/g, ' ').split(' ').filter(n => !n.endsWith('.'))
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
drop_tile (type, x, y, tentative=false) { // pass in tentative to not save the move
|
|
371
|
-
let dropped = []
|
|
372
|
-
let notation = ''
|
|
373
|
-
let valid = false
|
|
374
|
-
if (['timeout', 'resign', 'draw', 'puzzled'].includes(type)) { // handle special events
|
|
375
|
-
this.over = true
|
|
376
|
-
if (type !== 'puzzled') { // puzzled: puzzle was not completed; this player wins
|
|
377
|
-
this.turn = this.turn === 1 ? 2 : 1 // timeout, resign means the other player wins
|
|
378
|
-
if (type === 'draw') this.turn = 0 // draw means we both win
|
|
379
|
-
}
|
|
380
|
-
this.update_notation(type)
|
|
381
|
-
notation = type
|
|
382
|
-
valid = true
|
|
383
|
-
} else {
|
|
384
|
-
if (type.search(notation_regex) === 0) { // we were passed a notation
|
|
385
|
-
({ type, x, y } = this.decode_notation(type))
|
|
386
|
-
}
|
|
387
|
-
if (this.valid_location(x, y) && this.valid_tile(type, x, y)) {
|
|
388
|
-
valid = true
|
|
389
|
-
let saved = this.save() // just in case we need to roll back
|
|
390
|
-
notation = this.notate(type, x, y) // determine notation BEFORE we play the move
|
|
391
|
-
this.update_notation(notation)
|
|
392
|
-
dropped.push(this.add_tile(type, x, y)) // play the tile
|
|
393
|
-
dropped = dropped.concat(this.forced_moves(x, y)) // play all forced moves
|
|
394
|
-
if (this.invalid || tentative) {
|
|
395
|
-
if (this.invalid) valid = false
|
|
396
|
-
this.restore(saved) // abort! abort!
|
|
397
|
-
} else {
|
|
398
|
-
this.check_win(dropped)
|
|
399
|
-
if (!this.over) this.turn = this.turn === 1 ? 2 : 1
|
|
400
|
-
}
|
|
401
|
-
}
|
|
402
|
-
}
|
|
403
|
-
return { dropped, notation, valid }
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
move_rotations (move) { // symmetry helper, rotate a move around the board
|
|
407
|
-
let match = move.match(notation_regex)
|
|
408
|
-
if (match === null) return []
|
|
409
|
-
let x = decode_col(match[1])
|
|
410
|
-
let y = Number(match[2])
|
|
411
|
-
let s = match[3]
|
|
412
|
-
let slashes = s === '+' ? [s] : ['/', '\\']
|
|
413
|
-
let moves = new Set()
|
|
414
|
-
// this isn't super precise, but it limits the search space ... maybe #TODO?
|
|
415
|
-
for (let slash of slashes) {
|
|
416
|
-
moves.add(encode_col(x) + String(y) + slash)
|
|
417
|
-
moves.add(encode_col(y) + String(x) + slash)
|
|
418
|
-
moves.add(encode_col(this.height - x + 1) + String(this.width - y + 1) + slash)
|
|
419
|
-
moves.add(encode_col(this.width - y + 1) + String(this.height -x + 1) + slash)
|
|
420
|
-
}
|
|
421
|
-
return [...moves]
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
provisional_move (from, to, via) { // test a provisional move against the current state
|
|
425
|
-
let rows = from.slice(1).split(':')
|
|
426
|
-
let cols = rows.reduce((acc, cur) => Math.max(acc, code_row_len(cur)), 0)
|
|
427
|
-
rows = rows.length
|
|
428
|
-
if (this.width > rows && this.height > cols || this.width > cols && this.height > rows) {
|
|
429
|
-
return 'delete-provisional' // current board is bigger than the provisional move
|
|
430
|
-
}
|
|
431
|
-
if (from === this.normalized) {
|
|
432
|
-
let saved = this.save() // save this position
|
|
433
|
-
for (let move of this.move_rotations(via)) { // try all symmetrical moves
|
|
434
|
-
let drop = this.drop_tile(move)
|
|
435
|
-
if (drop.valid) {
|
|
436
|
-
if (to === this.normalized) return move // this move matched!
|
|
437
|
-
this.restore(saved) // put the board back how it was
|
|
438
|
-
}
|
|
439
|
-
}
|
|
440
|
-
}
|
|
441
|
-
return false
|
|
442
|
-
}
|
|
443
|
-
|
|
444
|
-
get icon () {
|
|
445
|
-
return this.position_code()
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
tentative_icon (drops) {
|
|
449
|
-
return this.position_code(undefined, undefined, undefined, undefined, drops)
|
|
450
|
-
}
|
|
451
|
-
|
|
452
|
-
// symmetry helper
|
|
453
|
-
position_code (right_to_left, bottom_to_top, rotate, swap_colors, drops) {
|
|
454
|
-
const start_x = right_to_left ? this.right : this.left
|
|
455
|
-
const start_y = bottom_to_top ? this.bottom : this.top
|
|
456
|
-
const inc_x = right_to_left ? -1 : 1
|
|
457
|
-
const inc_y = bottom_to_top ? -1 : 1
|
|
458
|
-
let code = []
|
|
459
|
-
let line = ''
|
|
460
|
-
let x = start_x
|
|
461
|
-
let y = start_y
|
|
462
|
-
do {
|
|
463
|
-
let tile = this.tile_at(x, y) // normal tile for normal orientation
|
|
464
|
-
if (tile) {
|
|
465
|
-
if (right_to_left) tile = symmetry.mirror[tile]
|
|
466
|
-
if (bottom_to_top) tile = symmetry.flip[tile]
|
|
467
|
-
if (rotate) tile = symmetry.counterclock[tile]
|
|
468
|
-
if (swap_colors) tile = symmetry.swap[tile]
|
|
469
|
-
if (this.path.includes(this.tile_id(x, y))) {
|
|
470
|
-
tile = String.fromCharCode(tile.charCodeAt(0) + this.turn * 6)
|
|
471
|
-
}
|
|
472
|
-
if (drops && drops.dropped) {
|
|
473
|
-
if (drops.dropped.filter(t => t.x === x && t.y === y).length) tile = tile.toUpperCase()
|
|
474
|
-
}
|
|
475
|
-
line += tile
|
|
476
|
-
} else {
|
|
477
|
-
line = add_blank(line)
|
|
478
|
-
}
|
|
479
|
-
if (rotate) { // increment y more often than x
|
|
480
|
-
y += inc_y
|
|
481
|
-
if (y < this.top || y > this.bottom) {
|
|
482
|
-
y = start_y
|
|
483
|
-
x += inc_x
|
|
484
|
-
// remove final number and add to code
|
|
485
|
-
code.push(line.replace(/[0-9]+$/, ''))
|
|
486
|
-
line = ''
|
|
487
|
-
}
|
|
488
|
-
} else { // this is the standard order
|
|
489
|
-
x += inc_x
|
|
490
|
-
if (x < this.left || x > this.right) {
|
|
491
|
-
x = start_x
|
|
492
|
-
y += inc_y
|
|
493
|
-
// remove final number and add to code
|
|
494
|
-
code.push(line.replace(/[0-9]+$/, ''))
|
|
495
|
-
line = ''
|
|
496
|
-
}
|
|
497
|
-
}
|
|
498
|
-
} while (x >= this.left && x <= this.right && y >= this.top && y <= this.bottom)
|
|
499
|
-
if (rotate) code.reverse()
|
|
500
|
-
return code.join(':')
|
|
501
|
-
}
|
|
502
|
-
|
|
503
|
-
normalize (swap_colors=false) {
|
|
504
|
-
// trax has the potential for symmetry, so this gives us the ability to
|
|
505
|
-
// examine horizontal, vertical, and rotational symmetry for a color
|
|
506
|
-
// --- pass in a truthy value to get a position for the opposite color
|
|
507
|
-
let norm = 'z'
|
|
508
|
-
let bools = [true, false]
|
|
509
|
-
bools.forEach(r_to_l => {
|
|
510
|
-
bools.forEach(b_to_t => {
|
|
511
|
-
bools.forEach(rot => {
|
|
512
|
-
let code = this.position_code(r_to_l, b_to_t, rot, swap_colors)
|
|
513
|
-
if (code < norm) norm = code
|
|
514
|
-
})
|
|
515
|
-
})
|
|
516
|
-
})
|
|
517
|
-
return (this.color || 't').toUpperCase() + norm
|
|
518
|
-
}
|
|
519
|
-
|
|
520
|
-
get normalized () {
|
|
521
|
-
// the default call does not check for color symmetry, because you rarely need that
|
|
522
|
-
return this.normalize()
|
|
523
|
-
}
|
|
524
|
-
}
|
|
525
|
-
|
|
526
|
-
// add class methods
|
|
527
|
-
Trax.column = encode_col
|
|
528
|
-
|
|
529
|
-
// and class constants
|
|
530
|
-
Trax.variants = variants
|
|
531
|
-
Trax.color_of = color_of
|
|
532
|
-
Trax.player_num = player_num
|
|
533
|
-
Trax.other = other
|
|
534
|
-
|
|
535
|
-
export default Trax
|