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