@slugbugblue/trax 0.4.0 → 0.5.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/engine.js CHANGED
@@ -19,62 +19,93 @@
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
- const variants = ['trax', 'traxloop', 'trax8'] // known variants
23
-
24
22
  // fun trax helper constants
25
23
 
24
+ const notationRegex = /^ *([@a-z]+) *(\d+) *([/\\+]) *$/i
25
+
26
26
  const slash = { a: '\\', b: '+', c: '/', d: '/', e: '+', f: '\\' }
27
27
 
28
- const symmetry = { // convert tile to different views
28
+ const symmetry = {
29
+ // Convert tile to different views
29
30
  flip: { a: 'd', b: 'b', c: 'f', d: 'a', e: 'e', f: 'c' },
30
31
  mirror: { a: 'c', b: 'b', c: 'a', d: 'f', e: 'e', f: 'd' },
31
32
  swap: { a: 'f', b: 'e', c: 'd', d: 'c', e: 'b', f: 'a' },
32
33
  counterclock: { a: 'c', b: 'e', c: 'f', d: 'a', e: 'b', f: 'd' },
33
34
  }
34
35
 
35
- const notation_regex = /^ *([@a-z]+) *(\d+) *([/\\+]) *$/i
36
+ // Fun trax helper functions
37
+
38
+ const applySymmetry = (tile, rightToLeft, bottomToTop, rotate) => {
39
+ if (rightToLeft) tile = symmetry.mirror[tile]
40
+ if (bottomToTop) tile = symmetry.flip[tile]
41
+ if (rotate) tile = symmetry.counterclock[tile]
42
+ return tile
43
+ }
36
44
 
37
- const color_of = { 1: 'w', 2: 'b' }
38
- const player_num = { b: 2, w: 1 }
39
- const other = { b: 'w', w: 'b' }
45
+ // Unexpected note: str.includes(t) is true if t is '' but false if t is undefined
46
+ const upColor = (t) =>
47
+ 'abc'.includes(t) ? 'b' : 'def'.includes(t) ? 'w' : false
48
+ const downColor = (t) =>
49
+ 'bdf'.includes(t) ? 'b' : 'ace'.includes(t) ? 'w' : false
50
+ const rightColor = (t) =>
51
+ 'ade'.includes(t) ? 'b' : 'bcf'.includes(t) ? 'w' : false
52
+ const leftColor = (t) =>
53
+ '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
+
61
+ const addBlank = (code) => {
62
+ // Encode missing tiles as digits 0-9, for 1-10 missing
63
+ const lastNumber = code.codePointAt(code.length - 1) // This is NaN if code is empty, which is ok
64
+ if (lastNumber > 47 && lastNumber < 57)
65
+ return code.slice(0, -1) + String.fromCodePoint(lastNumber + 1)
66
+ return code + '0'
67
+ }
40
68
 
41
- // fun trax helper functions
69
+ // Ugh, this is a dirty little function to get the length of an encoded position row
70
+ const codeRowLength = (row) =>
71
+ row.replace(/\d/g, (n) => '.'.repeat(Number(n) + 1)).length
72
+
73
+ // This is where the magic happens
74
+ export const Trax = class {
75
+ // Static class properties
76
+ static variants = new Set(['trax', 'traxloop', 'trax8']) // Known variants
77
+
78
+ // Static class methods
79
+ static colorOf = (playerNumber) => ({ 1: 'w', 2: 'b' }[playerNumber])
80
+ static playerNumber = (color) => ({ b: 2, w: 1 }[color])
81
+ static other = (color) => ({ b: 'w', w: 'b' }[color])
82
+
83
+ static encodeCol = (col) => {
84
+ // For notation purposes: 0 -> @, 6 -> F, 28 -> AB
85
+ let n = ''
86
+ while (col > 0) {
87
+ n = String.fromCodePoint(65 + ((col - 1) % 26)) + n
88
+ col = Math.floor((col - 1) / 26)
89
+ }
42
90
 
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)
91
+ return n || '@'
54
92
  }
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
93
 
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'
94
+ static decodeCol = (col) => {
95
+ // Turn letters back into numbers
96
+ col = col.toUpperCase()
97
+ let n = 0
98
+ while (col.length > 0) {
99
+ n += (col.codePointAt(0) - 64) * 26 ** (col.length - 1)
100
+ col = col.slice(1)
101
+ }
102
+
103
+ return n
104
+ }
105
+
106
+ // Trax class constructor
107
+ constructor(rules = 'trax', moves = [], id = 'trax') {
108
+ if (!Trax.variants.has(rules)) rules = 'trax'
78
109
  this.id = String(id)
79
110
  this.rules = rules
80
111
  this.move = 0
@@ -88,19 +119,21 @@ const Trax = class {
88
119
  this.tiles = {}
89
120
  this.path = []
90
121
  this.invalid = false
91
- // moves might be a notation string
122
+ // Moves might be a notation string
92
123
  if (moves) {
93
124
  if (typeof moves === 'string') moves = moves.split(' ')
94
- moves.forEach(move => {
125
+ for (let move of moves) {
95
126
  if (typeof move === 'string') {
96
127
  move = { move: this.move, notation: move }
97
128
  }
98
- this.play_move(move.move, move.notation)
99
- })
129
+
130
+ this.playMove(move.move, move.notation)
131
+ }
100
132
  }
101
133
  }
102
134
 
103
- save () { // in order to evaluate potential moves, we have to modify the state
135
+ save() {
136
+ // In order to evaluate potential moves, we have to modify the state
104
137
  return {
105
138
  move: this.move,
106
139
  turn: this.turn,
@@ -116,7 +149,8 @@ const Trax = class {
116
149
  }
117
150
  }
118
151
 
119
- restore (saved) { // so we need to be able to restore it when we are done
152
+ restore(saved) {
153
+ // So we need to be able to restore it when we are done
120
154
  this.move = saved.move
121
155
  this.turn = saved.turn
122
156
  this.over = saved.over
@@ -130,31 +164,36 @@ const Trax = class {
130
164
  this.invalid = saved.invalid
131
165
  }
132
166
 
133
- get count () { // get the number of tiles currently in play
167
+ get count() {
168
+ // Get the number of tiles currently in play
134
169
  return Object.keys(this.tiles).length
135
170
  }
136
171
 
137
- get color () { // get the color of the current player (w or b)
138
- return color_of[this.turn]
172
+ get color() {
173
+ // Get the color of the current player (w or b)
174
+ return Trax.colorOf(this.turn)
139
175
  }
140
176
 
141
- get game_over () { // returns true if the game is over
177
+ get gameOver() {
178
+ // Returns true if the game is over
142
179
  return this.over
143
180
  }
144
181
 
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
182
+ get winner() {
183
+ // Returns false if the game is still in progress
184
+ return this.over ? this.turn : false // Or 1 or 2 for a win, or 0 for a tie
147
185
  }
148
186
 
149
- tile_id (x, y) {
187
+ tileId(x, y) {
150
188
  return this.id + '-' + String(x) + 'x' + String(y)
151
189
  }
152
190
 
153
- add_tile (type, x, y) { // note: no validity checking is done here
154
- const id = this.tile_id(x, y)
191
+ addTile(type, x, y) {
192
+ // Note: no validity checking is done here
193
+ const id = this.tileId(x, y)
155
194
  const tile = { type, x, y, id, move: this.move, seq: this.count }
156
195
  if (type === 'x') {
157
- this.invalid = true // invalid tile is being played
196
+ this.invalid = true // Invalid tile is being played
158
197
  } else {
159
198
  this.tiles[id] = tile
160
199
  this.left = Math.min(this.left, x)
@@ -162,374 +201,435 @@ const Trax = class {
162
201
  this.top = Math.min(this.top, y)
163
202
  this.bottom = Math.max(this.bottom, y)
164
203
  }
204
+
165
205
  return tile
166
206
  }
167
207
 
168
- tile_at (x, y) {
169
- return (this.tiles[this.tile_id(x, y)] || {}).type
208
+ tileAt(x, y) {
209
+ return (this.tiles[this.tileId(x, y)] || {}).type
170
210
  }
171
211
 
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))
212
+ validTile(type, x, y) {
213
+ if (this.count === 0 && x === 0 && y === 0)
214
+ 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))
219
+ return (
220
+ (!up || up === upColor(type)) &&
221
+ (!down || down === downColor(type)) &&
222
+ (!left || left === leftColor(type)) &&
223
+ (!right || right === rightColor(type))
224
+ )
180
225
  }
181
226
 
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
- })
227
+ possibleTiles(x, y, s = false) {
228
+ // S: slash (/ or \ or +)
229
+ const possibles = []
230
+ for (const t of ['a', 'b', 'c', 'd', 'e', 'f']) {
231
+ if ((!s || s === slash[t]) && this.validTile(t, x, y)) possibles.push(t)
232
+ }
233
+
187
234
  return possibles
188
235
  }
189
236
 
190
- get height () {
237
+ get height() {
191
238
  return this.bottom - this.top + 1
192
239
  }
193
240
 
194
- get width () {
241
+ get width() {
195
242
  return this.right - this.left + 1
196
243
  }
197
244
 
198
- valid_location (x, y) {
245
+ validLocation(x, y) {
199
246
  if (this.over) return false
200
- if (this.tile_at(x, y)) return false
247
+ if (this.tileAt(x, y)) return false
201
248
  if (this.count === 0 && x === 0 && y === 0) return true
202
249
  if (this.rules === 'trax8') {
203
250
  if (this.width > 7 && (x < this.left || x > this.right)) return false
204
251
  if (this.height > 7 && (y < this.top || y > this.bottom)) return false
205
252
  }
253
+
206
254
  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)
255
+ this.tileAt(x - 1, y) ||
256
+ this.tileAt(x + 1, y) ||
257
+ this.tileAt(x, y - 1) ||
258
+ this.tileAt(x, y + 1),
209
259
  )
210
260
  }
211
261
 
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
262
+ 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) => {
265
+ // Add a loc if a tile isn't already there
266
+ const loc = x + 'x' + y
216
267
  locs[loc] = locs[loc] || { x, y }
217
268
  }
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
269
+
270
+ 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
223
275
  maybe(tile.x, tile.y + 1)
224
276
  }
225
- // get rid of all the real tiles and return all the maybes
226
- return Object.values(locs).filter(l => l !== 'tile')
277
+
278
+ // Get rid of all the real tiles and return all the maybes
279
+ return Object.values(locs).filter((l) => l !== 'tile')
227
280
  }
228
281
 
229
- possible_moves () { // returns a list of every currently possible move
282
+ possibleMoves() {
283
+ // Returns a list of every currently possible move
230
284
  // 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')
285
+ const possibles = []
286
+ if (this.over) return possibles // Shortcut if the game is over
287
+ 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')
236
290
  if (drop.valid) possibles.push(drop.notation)
237
- })
238
- })
291
+ }
292
+ }
293
+
239
294
  return possibles
240
295
  }
241
296
 
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)
297
+ forcedMoves(x, y) {
298
+ // We just played at x, y, so return all forced moves
299
+ const forced = []
300
+ const invalids = {}
301
+ let check = squaresAround(x, y)
302
+ while (check.length > 0) {
303
+ const pos = check.shift()
304
+ if (this.validLocation(pos.x, pos.y)) {
305
+ const possibles = this.possibleTiles(pos.x, pos.y)
306
+ if (possibles.length === 1) {
307
+ // Only 1 possible move
308
+ forced.push(this.addTile(possibles[0], pos.x, pos.y))
309
+ check = [...check, ...squaresAround(pos.x, pos.y)]
310
+ } else if (possibles.length === 0) {
311
+ // No possible moves at all
312
+ const invalid = this.addTile('x', pos.x, pos.y)
255
313
  if (!invalids[invalid.id]) forced.push(invalid)
256
314
  invalids[invalid.id] = true
257
315
  }
258
316
  }
259
317
  }
318
+
260
319
  return forced
261
320
  }
262
321
 
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]
322
+ notate(type, x, y) {
323
+ // 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
+ )
265
327
  }
266
328
 
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)
329
+ decodeNotation(notation) {
330
+ // Return {type, x, y} from a notation
331
+ const r = { type: 'x', x: 0, y: 0 } // Default to an invalid move
332
+ const match = notation.match(notationRegex)
270
333
  if (match === null) return r
271
- r.x = this.left - 1 + decode_col(match[1])
334
+ r.x = this.left - 1 + Trax.decodeCol(match[1])
272
335
  r.y = this.top - 1 + Number(match[2])
273
- let possible = this.possible_tiles(r.x, r.y, match[3])
336
+ const possible = this.possibleTiles(r.x, r.y, match[3])
274
337
  if (possible) r.type = possible[0]
275
338
  return r
276
339
  }
277
340
 
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)
341
+ follow(color, x, y, from) {
342
+ const path = []
343
+ let id = this.tileId(x, y)
344
+ let tile = this.tileAt(x, y)
282
345
  while (tile && path[0] !== id) {
283
- if (from !== 'top' && up_color(tile) === color) {
346
+ if (from !== 'top' && upColor(tile) === color) {
284
347
  from = 'bottom'
285
348
  y--
286
- } else if (from !== 'bottom' && down_color(tile) === color) {
349
+ } else if (from !== 'bottom' && downColor(tile) === color) {
287
350
  from = 'top'
288
351
  y++
289
- } else if (from !== 'left' && left_color(tile) === color) {
352
+ } else if (from !== 'left' && leftColor(tile) === color) {
290
353
  from = 'right'
291
354
  x--
292
- } else if (from !== 'right' && right_color(tile) === color) {
355
+ } else if (from !== 'right' && rightColor(tile) === color) {
293
356
  from = 'left'
294
357
  x++
295
358
  }
359
+
296
360
  path.push(id)
297
- id = this.tile_id(x, y)
298
- tile = this.tile_at(x, y)
361
+ id = this.tileId(x, y)
362
+ tile = this.tileAt(x, y)
299
363
  }
364
+
300
365
  return { x, y, path }
301
366
  }
302
367
 
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'))
368
+ findEnds(color, x, y) {
369
+ 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'))
310
375
  return ends
311
376
  }
312
377
 
313
- check_win (tiles) {
378
+ checkWin(tiles) {
314
379
  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)
380
+ const colors = [this.color, Trax.other(this.color)] // Check win for current player first
381
+ for (const color of colors) {
382
+ if (winner) continue // Skip the second player if the first has won
383
+ for (const tile of tiles) {
384
+ if (winner) continue // Skip the remaining tiles once we find a win
385
+ const ends = this.findEnds(color, tile.x, tile.y)
321
386
  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
- }
387
+ winner = color // Loop win
388
+ 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
+ ) {
394
+ winner = color // Line win, paths need to be combined
395
+ this.path = [...ends[0].path.reverse(), ...ends[1].path.slice(1)]
330
396
  }
331
- })
332
- })
397
+ }
398
+ }
399
+
333
400
  if (winner) {
334
401
  this.over = true
335
- this.turn = player_num[winner]
336
- } else {
402
+ this.turn = Trax.playerNumber(winner)
403
+ } else if (
337
404
  // 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
- }
405
+ this.rules === 'trax8' &&
406
+ this.width > 7 &&
407
+ this.height > 7 &&
408
+ this.possibleMoves().length === 0
409
+ ) {
410
+ this.over = true
411
+ this.turn = 0 // Tie
344
412
  }
345
413
  }
346
414
 
347
- play (notation) { // fast and easy way to play a move
348
- return this.drop_tile(notation)
415
+ play(notation) {
416
+ // Fast and easy way to play a move
417
+ return this.dropTile(notation)
349
418
  }
350
419
 
351
- play_move (move_num, notation) { // generic API call for drop_tile
352
- if (move_num === this.move) return this.drop_tile(notation)
420
+ playMove(moveNumber, notation) {
421
+ // Generic API call for dropTile
422
+ if (moveNumber === this.move) return this.dropTile(notation)
353
423
  return { valid: false }
354
424
  }
355
425
 
356
- update_notation (notation) {
426
+ updateNotation(notation) {
357
427
  this.move++
358
- let note = this.move +'. ' + notation
359
- let line = this.notation.split('\n').pop()
428
+ let note = this.move + '. ' + notation
429
+ const line = this.notation.split('\n').pop()
360
430
  if (line) {
361
431
  note = (line.length + note.length > 79 ? '\n' : ' ') + note
362
432
  }
433
+
363
434
  this.notation += note
364
435
  }
365
436
 
366
- get moves () {
367
- return this.notation.replace(/\n/g, ' ').split(' ').filter(n => !n.endsWith('.'))
437
+ get moves() {
438
+ return this.notation
439
+ .replace(/\n/g, ' ')
440
+ .split(' ')
441
+ .filter((n) => !n.endsWith('.'))
368
442
  }
369
443
 
370
- drop_tile (type, x, y, tentative=false) { // pass in tentative to not save the move
444
+ dropTile(type, x, y, tentative = false) {
445
+ // Pass in tentative to not save the move
371
446
  let dropped = []
372
447
  let notation = ''
373
448
  let valid = false
374
- if (['timeout', 'resign', 'draw', 'puzzled'].includes(type)) { // handle special events
449
+ if (['timeout', 'resign', 'draw', 'puzzled'].includes(type)) {
450
+ // Handle special events
375
451
  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
452
+ if (type !== 'puzzled') {
453
+ // Puzzled: puzzle was not completed; this player wins
454
+ this.turn = this.turn === 1 ? 2 : 1 // Timeout, resign means the other player wins
455
+ if (type === 'draw') this.turn = 0 // Draw means we both win
379
456
  }
380
- this.update_notation(type)
457
+
458
+ this.updateNotation(type)
381
459
  notation = type
382
460
  valid = true
383
461
  } else {
384
- if (type.search(notation_regex) === 0) { // we were passed a notation
385
- ({ type, x, y } = this.decode_notation(type))
462
+ if (type.search(notationRegex) === 0) {
463
+ // We were passed a notation
464
+ ;({ type, x, y } = this.decodeNotation(type))
386
465
  }
387
- if (this.valid_location(x, y) && this.valid_tile(type, x, y)) {
466
+
467
+ if (this.validLocation(x, y) && this.validTile(type, x, y)) {
388
468
  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
469
+ 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
471
+ 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
394
474
  if (this.invalid || tentative) {
395
475
  if (this.invalid) valid = false
396
- this.restore(saved) // abort! abort!
476
+ this.restore(saved) // Abort! abort!
397
477
  } else {
398
- this.check_win(dropped)
478
+ this.checkWin(dropped)
399
479
  if (!this.over) this.turn = this.turn === 1 ? 2 : 1
400
480
  }
401
481
  }
402
482
  }
483
+
403
484
  return { dropped, notation, valid }
404
485
  }
405
486
 
406
- move_rotations (move) { // symmetry helper, rotate a move around the board
407
- let match = move.match(notation_regex)
487
+ moveRotations(move) {
488
+ // Symmetry helper, rotate a move around the board
489
+ const match = move.match(notationRegex)
408
490
  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)
491
+ const x = Trax.decodeCol(match[1])
492
+ const y = Number(match[2])
493
+ const s = match[3]
494
+ const slashes = s === '+' ? [s] : ['/', '\\']
495
+ const moves = new Set()
496
+ // This isn't super precise, but it limits the search space ... maybe #TODO?
497
+ for (const slash of slashes) {
498
+ moves.add(Trax.encodeCol(x) + String(y) + slash)
499
+ 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
+ )
420
510
  }
511
+
421
512
  return [...moves]
422
513
  }
423
514
 
424
- provisional_move (from, to, via) { // test a provisional move against the current state
515
+ provisionalMove(from, to, via) {
516
+ // Test a provisional move against the current state
425
517
  let rows = from.slice(1).split(':')
426
- let cols = rows.reduce((acc, cur) => Math.max(acc, code_row_len(cur)), 0)
518
+ let cols = 0
519
+ for (const row of rows) {
520
+ cols = Math.max(cols, codeRowLength(row))
521
+ }
522
+
427
523
  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
524
+ if (
525
+ (this.width > rows && this.height > cols) ||
526
+ (this.width > cols && this.height > rows)
527
+ ) {
528
+ return 'delete-provisional' // Current board is bigger than the provisional move
430
529
  }
530
+
431
531
  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)
532
+ const saved = this.save() // Save this position
533
+ for (const move of this.moveRotations(via)) {
534
+ // Try all symmetrical moves
535
+ const drop = this.dropTile(move)
435
536
  if (drop.valid) {
436
- if (to === this.normalized) return move // this move matched!
437
- this.restore(saved) // put the board back how it was
537
+ if (to === this.normalized) return move // This move matched!
538
+ this.restore(saved) // Put the board back how it was
438
539
  }
439
540
  }
440
541
  }
542
+
441
543
  return false
442
544
  }
443
545
 
444
- get icon () {
445
- return this.position_code()
546
+ get icon() {
547
+ return this.positionCode()
446
548
  }
447
549
 
448
- tentative_icon (drops) {
449
- return this.position_code(undefined, undefined, undefined, undefined, drops)
550
+ tentativeIcon(drops) {
551
+ return this.positionCode(undefined, undefined, undefined, drops)
450
552
  }
451
553
 
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 = []
554
+ // Symmetry helper, draw the board from different angles
555
+ positionCode(rightToLeft, bottomToTop, rotate, drops) {
556
+ const startX = rightToLeft ? this.right : this.left
557
+ const startY = bottomToTop ? this.bottom : this.top
558
+ const incX = rightToLeft ? -1 : 1
559
+ const incY = bottomToTop ? -1 : 1
560
+ const code = []
459
561
  let line = ''
460
- let x = start_x
461
- let y = start_y
562
+ let x = startX
563
+ let y = startY
462
564
  do {
463
- let tile = this.tile_at(x, y) // normal tile for normal orientation
565
+ let tile = this.tileAt(x, y) // Normal tile for normal orientation
464
566
  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()
567
+ tile = applySymmetry(tile, rightToLeft, bottomToTop, rotate)
568
+ if (this.path.includes(this.tileId(x, y))) {
569
+ tile = String.fromCodePoint(tile.codePointAt(0) + this.turn * 6)
474
570
  }
571
+
572
+ if (
573
+ drops &&
574
+ drops.dropped &&
575
+ drops.dropped.some((t) => t.x === x && t.y === y)
576
+ )
577
+ tile = tile.toUpperCase()
475
578
  line += tile
476
579
  } else {
477
- line = add_blank(line)
580
+ line = addBlank(line)
478
581
  }
479
- if (rotate) { // increment y more often than x
480
- y += inc_y
582
+
583
+ if (rotate) {
584
+ // Increment y more often than x
585
+ y += incY
481
586
  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]+$/, ''))
587
+ y = startY
588
+ x += incX
589
+ // Remove final number and add to code
590
+ code.push(line.replace(/\d+$/, ''))
486
591
  line = ''
487
592
  }
488
- } else { // this is the standard order
489
- x += inc_x
593
+ } else {
594
+ // This is the standard order
595
+ x += incX
490
596
  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]+$/, ''))
597
+ x = startX
598
+ y += incY
599
+ // Remove final number and add to code
600
+ code.push(line.replace(/\d+$/, ''))
495
601
  line = ''
496
602
  }
497
603
  }
498
- } while (x >= this.left && x <= this.right && y >= this.top && y <= this.bottom)
604
+ } while (
605
+ x >= this.left &&
606
+ x <= this.right &&
607
+ y >= this.top &&
608
+ y <= this.bottom
609
+ )
610
+
499
611
  if (rotate) code.reverse()
500
612
  return code.join(':')
501
613
  }
502
614
 
503
- normalize (swap_colors=false) {
504
- // trax has the potential for symmetry, so this gives us the ability to
615
+ normalize() {
616
+ // Trax has the potential for symmetry, so this gives us the ability to
505
617
  // examine horizontal, vertical, and rotational symmetry for a color
506
- // --- pass in a truthy value to get a position for the opposite color
507
618
  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)
619
+ const bools = [true, false]
620
+ for (const rightToLeft of bools) {
621
+ for (const bottomToTop of bools) {
622
+ for (const rotate of bools) {
623
+ const code = this.positionCode(rightToLeft, bottomToTop, rotate)
513
624
  if (code < norm) norm = code
514
- })
515
- })
516
- })
625
+ }
626
+ }
627
+ }
628
+
517
629
  return (this.color || 't').toUpperCase() + norm
518
630
  }
519
631
 
520
- get normalized () {
521
- // the default call does not check for color symmetry, because you rarely need that
632
+ get normalized() {
522
633
  return this.normalize()
523
634
  }
524
635
  }
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