@slugbugblue/trax 0.2.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
@@ -1,4 +1,4 @@
1
- /* Copyright 2021 Chad Transtrum
1
+ /* Copyright 2019-2022 Chad Transtrum
2
2
  *
3
3
  * Licensed under the Apache License, Version 2.0 (the "License");
4
4
  * you may not use this file except in compliance with the License.
@@ -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
36
37
 
37
- const color_of = { 1: 'w', 2: 'b' }
38
- const player_num = { b: 2, w: 1 }
39
- const other = { b: 'w', w: 'b' }
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
+ }
40
44
 
41
- // fun trax helper functions
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
+ }
42
68
 
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)
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
+ }
90
+
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
@@ -87,19 +118,22 @@ const Trax = class {
87
118
  this.notation = ''
88
119
  this.tiles = {}
89
120
  this.path = []
90
- this.count = 0
91
121
  this.invalid = false
92
- // moves might be a notation string
93
- if (typeof moves === 'string') moves = moves.split(' ')
94
- moves.forEach(move => {
95
- if (typeof move === 'string') {
96
- move = { move: this.move, notation: move }
122
+ // Moves might be a notation string
123
+ if (moves) {
124
+ if (typeof moves === 'string') moves = moves.split(' ')
125
+ for (let move of moves) {
126
+ if (typeof move === 'string') {
127
+ move = { move: this.move, notation: move }
128
+ }
129
+
130
+ this.playMove(move.move, move.notation)
97
131
  }
98
- this.play_move(move.move, move.notation)
99
- })
132
+ }
100
133
  }
101
134
 
102
- 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
103
137
  return {
104
138
  move: this.move,
105
139
  turn: this.turn,
@@ -111,12 +145,12 @@ const Trax = class {
111
145
  notation: this.notation,
112
146
  tiles: JSON.stringify(this.tiles),
113
147
  path: JSON.stringify(this.path),
114
- count: this.count,
115
148
  invalid: this.invalid,
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
@@ -127,386 +161,475 @@ const Trax = class {
127
161
  this.notation = saved.notation
128
162
  this.tiles = JSON.parse(saved.tiles)
129
163
  this.path = JSON.parse(saved.path)
130
- this.count = saved.count
131
164
  this.invalid = saved.invalid
132
165
  }
133
166
 
134
- get color () { // get the color of the current player (w or b)
135
- return color_of[this.turn]
167
+ get count() {
168
+ // Get the number of tiles currently in play
169
+ return Object.keys(this.tiles).length
136
170
  }
137
171
 
138
- get game_over () { // returns true if the game is over
172
+ get color() {
173
+ // Get the color of the current player (w or b)
174
+ return Trax.colorOf(this.turn)
175
+ }
176
+
177
+ get gameOver() {
178
+ // Returns true if the game is over
139
179
  return this.over
140
180
  }
141
181
 
142
- get winner () { // returns false if the game is still in progress
143
- 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
144
185
  }
145
186
 
146
- tile_id (x, y) {
187
+ tileId(x, y) {
147
188
  return this.id + '-' + String(x) + 'x' + String(y)
148
189
  }
149
190
 
150
- add_tile (type, x, y) { // note: no validity checking is done here
151
- 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)
152
194
  const tile = { type, x, y, id, move: this.move, seq: this.count }
153
195
  if (type === 'x') {
154
- this.invalid = true // invalid tile is being played
196
+ this.invalid = true // Invalid tile is being played
155
197
  } else {
156
198
  this.tiles[id] = tile
157
- this.count++
158
199
  this.left = Math.min(this.left, x)
159
200
  this.right = Math.max(this.right, x)
160
201
  this.top = Math.min(this.top, y)
161
202
  this.bottom = Math.max(this.bottom, y)
162
203
  }
204
+
163
205
  return tile
164
206
  }
165
207
 
166
- tile_at (x, y) {
167
- return (this.tiles[this.tile_id(x, y)] || {}).type
208
+ tileAt(x, y) {
209
+ return (this.tiles[this.tileId(x, y)] || {}).type
168
210
  }
169
211
 
170
- valid_tile (type, x, y) {
171
- if (this.count === 0 && x === 0 && y === 0) return type === 'd' || type === 'e'
172
- let up = down_color(this.tile_at(x, y - 1))
173
- let down = up_color(this.tile_at(x, y + 1))
174
- let left = right_color(this.tile_at(x - 1, y))
175
- let right = left_color(this.tile_at(x + 1, y))
176
- return (!up || up === up_color(type)) && (!down || down === down_color(type)) &&
177
- (!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
+ )
178
225
  }
179
226
 
180
- possible_tiles (x, y, s=false) { // s: slash (/ or \ or +)
181
- let possibles = []
182
- ;['a', 'b', 'c', 'd', 'e', 'f'].forEach(t => {
183
- if ((!s || s === slash[t]) && this.valid_tile(t, x, y)) possibles.push(t)
184
- })
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
+
185
234
  return possibles
186
235
  }
187
236
 
188
- get height () {
237
+ get height() {
189
238
  return this.bottom - this.top + 1
190
239
  }
191
240
 
192
- get width () {
241
+ get width() {
193
242
  return this.right - this.left + 1
194
243
  }
195
244
 
196
- valid_location (x, y) {
245
+ validLocation(x, y) {
197
246
  if (this.over) return false
198
- if (this.tile_at(x, y)) return false
247
+ if (this.tileAt(x, y)) return false
199
248
  if (this.count === 0 && x === 0 && y === 0) return true
200
249
  if (this.rules === 'trax8') {
201
250
  if (this.width > 7 && (x < this.left || x > this.right)) return false
202
251
  if (this.height > 7 && (y < this.top || y > this.bottom)) return false
203
252
  }
253
+
204
254
  return Boolean(
205
- this.tile_at(x-1, y) || this.tile_at(x+1, y) ||
206
- 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),
207
259
  )
208
260
  }
209
261
 
210
- possible_locations () {
211
- let possibles = []
212
- for (let x = this.left - 1 ; x <= this.right + 1 ; x++) {
213
- for (let y = this.top -1 ; y <= this.bottom + 1 ; y ++) {
214
- if (this.valid_location(x, y)) possibles.push({x, y})
215
- }
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
267
+ locs[loc] = locs[loc] || { x, y }
216
268
  }
217
- return possibles
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
275
+ maybe(tile.x, tile.y + 1)
276
+ }
277
+
278
+ // Get rid of all the real tiles and return all the maybes
279
+ return Object.values(locs).filter((l) => l !== 'tile')
218
280
  }
219
281
 
220
- possible_moves () { // returns a list of every currently possible move
282
+ possibleMoves() {
283
+ // Returns a list of every currently possible move
221
284
  // as notations, ie ['@1+', '@1/', '@1\\', ...]
222
- let possibles = []
223
- if (this.over) return possibles // shortcut if the game is over
224
- this.possible_locations().forEach(loc => {
225
- this.possible_tiles(loc.x, loc.y).forEach(tile => {
226
- 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')
227
290
  if (drop.valid) possibles.push(drop.notation)
228
- })
229
- })
291
+ }
292
+ }
293
+
230
294
  return possibles
231
295
  }
232
296
 
233
- forced_moves (x, y) { // we just played at x, y, so return all forced moves
234
- let forced = []
235
- let invalids = {}
236
- let check = squares_around(x, y)
237
- while (check.length) {
238
- let pos = check.shift()
239
- if (this.valid_location(pos.x, pos.y)) {
240
- let possibles = this.possible_tiles(pos.x, pos.y)
241
- if (possibles.length === 1) { // only 1 possible move
242
- forced.push(this.add_tile(possibles[0], pos.x, pos.y))
243
- check = check.concat(squares_around(pos.x, pos.y))
244
- } else if (possibles.length === 0) { // no possible moves at all
245
- 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)
246
313
  if (!invalids[invalid.id]) forced.push(invalid)
247
314
  invalids[invalid.id] = true
248
315
  }
249
316
  }
250
317
  }
318
+
251
319
  return forced
252
320
  }
253
321
 
254
- notate (type, x, y) { // notation depends on the position of the board BEFORE the move
255
- 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
+ )
256
327
  }
257
328
 
258
- decode_notation (notation) { // return {type, x, y} from a notation
259
- let r = { type: 'x', x: 0, y: 0 } // default to an invalid move
260
- 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)
261
333
  if (match === null) return r
262
- r.x = this.left - 1 + decode_col(match[1])
334
+ r.x = this.left - 1 + Trax.decodeCol(match[1])
263
335
  r.y = this.top - 1 + Number(match[2])
264
- let possible = this.possible_tiles(r.x, r.y, match[3])
336
+ const possible = this.possibleTiles(r.x, r.y, match[3])
265
337
  if (possible) r.type = possible[0]
266
338
  return r
267
339
  }
268
340
 
269
- follow (color, x, y, from) {
270
- let path = []
271
- let id = this.tile_id(x, y)
272
- 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)
273
345
  while (tile && path[0] !== id) {
274
- if (from !== 'top' && up_color(tile) === color) {
346
+ if (from !== 'top' && upColor(tile) === color) {
275
347
  from = 'bottom'
276
348
  y--
277
- } else if (from !== 'bottom' && down_color(tile) === color) {
349
+ } else if (from !== 'bottom' && downColor(tile) === color) {
278
350
  from = 'top'
279
351
  y++
280
- } else if (from !== 'left' && left_color(tile) === color) {
352
+ } else if (from !== 'left' && leftColor(tile) === color) {
281
353
  from = 'right'
282
354
  x--
283
- } else if (from !== 'right' && right_color(tile) === color) {
355
+ } else if (from !== 'right' && rightColor(tile) === color) {
284
356
  from = 'left'
285
357
  x++
286
358
  }
359
+
287
360
  path.push(id)
288
- id = this.tile_id(x, y)
289
- tile = this.tile_at(x, y)
361
+ id = this.tileId(x, y)
362
+ tile = this.tileAt(x, y)
290
363
  }
364
+
291
365
  return { x, y, path }
292
366
  }
293
367
 
294
- find_ends (color, x, y) {
295
- let ends = []
296
- let tile = this.tile_at(x, y)
297
- if (up_color(tile) === color) ends.push(this.follow(color, x, y, 'top'))
298
- if (down_color(tile) === color) ends.push(this.follow(color, x, y, 'bottom'))
299
- if (left_color(tile) === color) ends.push(this.follow(color, x, y, 'left'))
300
- 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'))
301
375
  return ends
302
376
  }
303
377
 
304
- check_win (tiles) {
378
+ checkWin(tiles) {
305
379
  let winner = false
306
- let colors = [ this.color, other[this.color] ] // check win for current player first
307
- colors.forEach(color => {
308
- if (winner) return // skip the second player if the first has won
309
- tiles.forEach(tile => {
310
- if (winner) return // skip the remaining tiles once we find a win
311
- let ends = this.find_ends(color, tile.x, tile.y)
312
- if (ends.length !== 2) return // this should never happen, but if it does, don't die
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)
313
386
  if (ends[0].x === ends[1].x && ends[0].y === ends[1].y) {
314
- winner = color // loop win
315
- this.path = ends[0].path // both paths are essentially the same
316
- } else if (this.rules !== 'traxloop') { // check for line win
317
- if (this.width > 7 && Math.abs(ends[0].x - ends[1].x) > this.width ||
318
- this.height > 7 && Math.abs(ends[0].y - ends[1].y) > this.height) {
319
- winner = color // line win, paths need to be combined
320
- this.path = ends[0].path.reverse().concat(ends[1].path.slice(1))
321
- }
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)]
322
396
  }
323
- })
324
- })
397
+ }
398
+ }
399
+
325
400
  if (winner) {
326
401
  this.over = true
327
- this.turn = player_num[winner]
328
- } else {
402
+ this.turn = Trax.playerNumber(winner)
403
+ } else if (
329
404
  // 8x8 Trax can end in a tie once there are no valid moves left
330
- if (this.rules === 'trax8' && this.width > 7 && this.height > 7) {
331
- if (this.possible_moves().length < 1) {
332
- this.over = true
333
- this.turn = 0 // tie
334
- }
335
- }
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
336
412
  }
337
413
  }
338
414
 
339
- play (notation) { // fast and easy way to play a move
340
- return this.drop_tile(notation)
415
+ play(notation) {
416
+ // Fast and easy way to play a move
417
+ return this.dropTile(notation)
341
418
  }
342
419
 
343
- play_move (move_num, notation) { // generic API call for drop_tile
344
- 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)
345
423
  return { valid: false }
346
424
  }
347
425
 
348
- update_notation (notation) {
426
+ updateNotation(notation) {
349
427
  this.move++
350
- let note = this.move +'. ' + notation
351
- let line = this.notation.split('\n').pop()
428
+ let note = this.move + '. ' + notation
429
+ const line = this.notation.split('\n').pop()
352
430
  if (line) {
353
431
  note = (line.length + note.length > 79 ? '\n' : ' ') + note
354
432
  }
433
+
355
434
  this.notation += note
356
435
  }
357
436
 
358
- get moves () {
359
- 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('.'))
360
442
  }
361
443
 
362
- 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
363
446
  let dropped = []
364
447
  let notation = ''
365
448
  let valid = false
366
- if (['timeout', 'resign', 'draw', 'puzzled'].includes(type)) { // handle special events
449
+ if (['timeout', 'resign', 'draw', 'puzzled'].includes(type)) {
450
+ // Handle special events
367
451
  this.over = true
368
- if (type !== 'puzzled') { // puzzled: puzzle was not completed; this player wins
369
- this.turn = this.turn === 1 ? 2 : 1 // timeout, resign means the other player wins
370
- 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
371
456
  }
372
- this.update_notation(type)
457
+
458
+ this.updateNotation(type)
373
459
  notation = type
374
460
  valid = true
375
461
  } else {
376
- if (type.search(notation_regex) === 0) { // we were passed a notation
377
- ({ 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))
378
465
  }
379
- if (this.valid_location(x, y) && this.valid_tile(type, x, y)) {
466
+
467
+ if (this.validLocation(x, y) && this.validTile(type, x, y)) {
380
468
  valid = true
381
- let saved = this.save() // just in case we need to roll back
382
- notation = this.notate(type, x, y) // determine notation BEFORE we play the move
383
- this.update_notation(notation)
384
- dropped.push(this.add_tile(type, x, y)) // play the tile
385
- 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
386
474
  if (this.invalid || tentative) {
387
- valid = false
388
- this.restore(saved) // abort! abort!
475
+ if (this.invalid) valid = false
476
+ this.restore(saved) // Abort! abort!
389
477
  } else {
390
- this.check_win(dropped)
478
+ this.checkWin(dropped)
391
479
  if (!this.over) this.turn = this.turn === 1 ? 2 : 1
392
480
  }
393
481
  }
394
482
  }
483
+
395
484
  return { dropped, notation, valid }
396
485
  }
397
486
 
398
- rotate_move (move) { // symmetry helper, rotate a move around the board
399
- let match = move.match(notation_regex)
400
- if (match === null) return move
401
- let x = this.width + 1 - Number(match[2])
402
- let y = decode_col(match[1])
403
- let s = { '/': '\\', '\\': '/', '+': '+'}[match[3]]
404
- return encode_col(x) + String(y) + s
487
+ moveRotations(move) {
488
+ // Symmetry helper, rotate a move around the board
489
+ const match = move.match(notationRegex)
490
+ if (match === null) return []
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
+ )
510
+ }
511
+
512
+ return [...moves]
405
513
  }
406
514
 
407
- provisional_notation (provisional) { // test a provisional move against the current state
408
- let prov = provisional.split(';') // return false if the provisional move does not match
409
- if (prov.length !== 4) return 'delete-provisional' // <-- if the provisional move is bad
410
- let notation = false // otherwise, return the notation required to make the move
411
- let rows = prov[1].split(':')
412
- let cols = rows.reduce((acc, cur) => Math.max(acc, code_row_len(cur)), 0)
515
+ provisionalMove(from, to, via) {
516
+ // Test a provisional move against the current state
517
+ let rows = from.slice(1).split(':')
518
+ let cols = 0
519
+ for (const row of rows) {
520
+ cols = Math.max(cols, codeRowLength(row))
521
+ }
522
+
413
523
  rows = rows.length
414
- if (this.width > rows && this.height > cols || this.width > cols && this.height > rows) {
415
- 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
416
529
  }
417
- if (prov[1] === this.normalized) {
418
- let move = prov[2]
419
- let saved = this.save() // save this position
420
- for (let i = 0 ; i < 4 && !notation; i++) { // try all four orientations because symmetry
421
- this.drop_tile(move)
422
- if (prov[3] === this.normalized) notation = move // this move matched!
423
- this.restore(saved) // put the board back how it was
424
- move = this.rotate_move(move) // rotate to the next orientation
530
+
531
+ if (from === this.normalized) {
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)
536
+ if (drop.valid) {
537
+ if (to === this.normalized) return move // This move matched!
538
+ this.restore(saved) // Put the board back how it was
539
+ }
425
540
  }
426
541
  }
427
- return notation
542
+
543
+ return false
544
+ }
545
+
546
+ get icon() {
547
+ return this.positionCode()
428
548
  }
429
549
 
430
- get icon () {
431
- return this.position_code()
550
+ tentativeIcon(drops) {
551
+ return this.positionCode(undefined, undefined, undefined, drops)
432
552
  }
433
553
 
434
- // symmetry helper
435
- position_code (right_to_left, bottom_to_top, rotate, swap_colors) {
436
- const start_x = right_to_left ? this.right : this.left
437
- const start_y = bottom_to_top ? this.bottom : this.top
438
- const inc_x = right_to_left ? -1 : 1
439
- const inc_y = bottom_to_top ? -1 : 1
440
- 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 = []
441
561
  let line = ''
442
- let x = start_x
443
- let y = start_y
562
+ let x = startX
563
+ let y = startY
444
564
  do {
445
- let tile = this.tile_at(x, y) // normal tile for normal orientation
565
+ let tile = this.tileAt(x, y) // Normal tile for normal orientation
446
566
  if (tile) {
447
- if (right_to_left) tile = symmetry.mirror[tile]
448
- if (bottom_to_top) tile = symmetry.flip[tile]
449
- if (rotate) tile = symmetry.counterclock[tile]
450
- if (swap_colors) tile = symmetry.swap[tile]
451
- if (this.path.includes(this.tile_id(x, y))) {
452
- tile = String.fromCharCode(tile.charCodeAt(0) + this.turn * 6)
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)
453
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()
454
578
  line += tile
455
579
  } else {
456
- line = add_blank(line)
580
+ line = addBlank(line)
457
581
  }
458
- if (rotate) { // increment y more often than x
459
- y += inc_y
582
+
583
+ if (rotate) {
584
+ // Increment y more often than x
585
+ y += incY
460
586
  if (y < this.top || y > this.bottom) {
461
- y = start_y
462
- x += inc_x
463
- // remove final number and add to code
464
- 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+$/, ''))
465
591
  line = ''
466
592
  }
467
- } else { // this is the standard order
468
- x += inc_x
593
+ } else {
594
+ // This is the standard order
595
+ x += incX
469
596
  if (x < this.left || x > this.right) {
470
- x = start_x
471
- y += inc_y
472
- // remove final number and add to code
473
- 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+$/, ''))
474
601
  line = ''
475
602
  }
476
603
  }
477
- } 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
+
478
611
  if (rotate) code.reverse()
479
612
  return code.join(':')
480
613
  }
481
614
 
482
- normalize (swap_colors=false) {
483
- // 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
484
617
  // examine horizontal, vertical, and rotational symmetry for a color
485
- // --- pass in a truthy value to get a position for the opposite color
486
618
  let norm = 'z'
487
- let bools = [true, false]
488
- bools.forEach(r_to_l => {
489
- bools.forEach(b_to_t => {
490
- bools.forEach(rot => {
491
- 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)
492
624
  if (code < norm) norm = code
493
- })
494
- })
495
- })
625
+ }
626
+ }
627
+ }
628
+
496
629
  return (this.color || 't').toUpperCase() + norm
497
630
  }
498
631
 
499
- get normalized () {
500
- // the default call does not check for color symmetry, because you rarely need that
632
+ get normalized() {
501
633
  return this.normalize()
502
634
  }
503
635
  }
504
-
505
- // add class methods
506
- Trax.column = encode_col
507
- Trax.variants = variants
508
- Trax.color_of = color_of
509
- Trax.player_num = player_num
510
- Trax.other = other
511
-
512
- export default Trax