@slugbugblue/trax 0.1.0 → 0.4.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/.eslintrc.yaml +1 -1
- package/CHANGELOG.md +34 -0
- package/README.md +24 -11
- package/engine.js +97 -58
- package/jsconfig.json +1 -1
- package/package.json +10 -8
- package/tty.js +71 -45
package/.eslintrc.yaml
CHANGED
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# @slugbugblue/trax
|
|
2
|
+
|
|
3
|
+
## 0.3.0
|
|
4
|
+
|
|
5
|
+
Display a tentative move on the screen
|
|
6
|
+
|
|
7
|
+
* Add `trax.tentative_icon()` function
|
|
8
|
+
* `tty.display()` now accepts a tentative move
|
|
9
|
+
|
|
10
|
+
## 0.2.0
|
|
11
|
+
|
|
12
|
+
Breaking changes:
|
|
13
|
+
|
|
14
|
+
* Converted from CommonJS to ESM
|
|
15
|
+
* `trax.normalized` now prepends turn information to the string
|
|
16
|
+
|
|
17
|
+
More convenience:
|
|
18
|
+
|
|
19
|
+
* `trax.color`: the color of the player whose turn it is
|
|
20
|
+
* `Trax.color_of[num]`: go from player number to color
|
|
21
|
+
* `Trax.player_num[color]`: or from color to player number
|
|
22
|
+
* `Trax.other[color]`: easily switch from 'w' to 'b' and back
|
|
23
|
+
|
|
24
|
+
## 0.1.0
|
|
25
|
+
|
|
26
|
+
Initial release:
|
|
27
|
+
|
|
28
|
+
* engine.js
|
|
29
|
+
* create a new Trax game with `trax = new Trax()`
|
|
30
|
+
* play one move at a time with `trax.play('@0/')`
|
|
31
|
+
* ... etc
|
|
32
|
+
|
|
33
|
+
* tty.js
|
|
34
|
+
* display a game with `tty.display(trax)`
|
package/README.md
CHANGED
|
@@ -26,11 +26,21 @@ The main Trax engine is provided as `engine.js`,
|
|
|
26
26
|
which can be used as follows:
|
|
27
27
|
|
|
28
28
|
```javascript
|
|
29
|
-
|
|
29
|
+
import Trax from '@slugbugblue/trax'
|
|
30
30
|
|
|
31
|
+
// default constructor gives you a Trax game
|
|
31
32
|
let trax = new Trax()
|
|
32
|
-
|
|
33
33
|
trax.play('@0/')
|
|
34
|
+
|
|
35
|
+
// but you can also select Loop Trax
|
|
36
|
+
let loops = new Trax('traxloop')
|
|
37
|
+
|
|
38
|
+
// or 8x8 Trax
|
|
39
|
+
let tiny = new Trax('trax8')
|
|
40
|
+
|
|
41
|
+
// and the constructor lets you start with an initial set of moves
|
|
42
|
+
let puzzle = new Trax('trax', '@0/ a0\\ @2/')
|
|
43
|
+
puzzle.play('@2\\')
|
|
34
44
|
```
|
|
35
45
|
|
|
36
46
|
Explore the file `engine.js` for more information
|
|
@@ -48,7 +58,7 @@ until I get around to documenting the functionality.
|
|
|
48
58
|
|
|
49
59
|
This project is built by [Chad Transtrum][ctrans]
|
|
50
60
|
for [slugbugblue.com][sbb].
|
|
51
|
-
Issues can be opened on the
|
|
61
|
+
Issues can be opened on the
|
|
52
62
|
[gitlab project page][repo].
|
|
53
63
|
|
|
54
64
|
## Contributing
|
|
@@ -66,18 +76,21 @@ and for his generosity in allowing me to
|
|
|
66
76
|
add the game to [GoldToken.com][goldtoken].
|
|
67
77
|
|
|
68
78
|
Thanks to his widow Colleen Foley-Smith
|
|
69
|
-
for extending that
|
|
70
|
-
to allow me to include Trax on [slugbugblue.com][sbb]
|
|
79
|
+
for extending that courtesy
|
|
80
|
+
to allow me to include Trax on [slugbugblue.com][sbb]
|
|
81
|
+
as well.
|
|
71
82
|
|
|
72
83
|
I would also like to extend a huge heartfelt appreciation
|
|
73
84
|
to the magnanimous [Donald G. Bailey][dgb],
|
|
74
85
|
who, through his excellent [book][traxbook],
|
|
75
|
-
taught me the basics (and more!) of Trax
|
|
76
|
-
|
|
86
|
+
taught me the basics (and more!) of Trax.
|
|
87
|
+
I found him always ready to share his insights
|
|
77
88
|
on the mechanics of the game,
|
|
78
|
-
various approaches to encoding its complexities
|
|
79
|
-
|
|
80
|
-
|
|
89
|
+
as well as various approaches to encoding its complexities.
|
|
90
|
+
His undeserved kindness
|
|
91
|
+
and infinite patience
|
|
92
|
+
in indulging my many questions
|
|
93
|
+
can never be repaid.
|
|
81
94
|
|
|
82
95
|
## License
|
|
83
96
|
|
|
@@ -98,7 +111,7 @@ limitations under the License.
|
|
|
98
111
|
|
|
99
112
|
## Trax rules copyright
|
|
100
113
|
|
|
101
|
-
In the United States, game mechanics may not be copyrighted
|
|
114
|
+
In the United States, game mechanics may not be copyrighted;
|
|
102
115
|
however, the specific wording of game rules does fall under copyright law.
|
|
103
116
|
|
|
104
117
|
The official rules for Trax are found at http://traxgame.com/about_rules.php.
|
package/engine.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
/* Copyright
|
|
2
|
-
*
|
|
1
|
+
/* Copyright 2019-2022 Chad Transtrum
|
|
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.
|
|
5
5
|
* You may obtain a copy of the License at
|
|
6
|
-
*
|
|
6
|
+
*
|
|
7
7
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
-
*
|
|
8
|
+
*
|
|
9
9
|
* Unless required by applicable law or agreed to in writing, software
|
|
10
10
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
11
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
@@ -19,6 +19,7 @@
|
|
|
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
23
|
|
|
23
24
|
// fun trax helper constants
|
|
24
25
|
|
|
@@ -33,7 +34,9 @@ const symmetry = { // convert tile to different views
|
|
|
33
34
|
|
|
34
35
|
const notation_regex = /^ *([@a-z]+) *(\d+) *([/\\+]) *$/i
|
|
35
36
|
|
|
36
|
-
const
|
|
37
|
+
const color_of = { 1: 'w', 2: 'b' }
|
|
38
|
+
const player_num = { b: 2, w: 1 }
|
|
39
|
+
const other = { b: 'w', w: 'b' }
|
|
37
40
|
|
|
38
41
|
// fun trax helper functions
|
|
39
42
|
|
|
@@ -84,16 +87,17 @@ const Trax = class {
|
|
|
84
87
|
this.notation = ''
|
|
85
88
|
this.tiles = {}
|
|
86
89
|
this.path = []
|
|
87
|
-
this.count = 0
|
|
88
90
|
this.invalid = false
|
|
89
91
|
// moves might be a notation string
|
|
90
|
-
if (
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
92
|
+
if (moves) {
|
|
93
|
+
if (typeof moves === 'string') moves = moves.split(' ')
|
|
94
|
+
moves.forEach(move => {
|
|
95
|
+
if (typeof move === 'string') {
|
|
96
|
+
move = { move: this.move, notation: move }
|
|
97
|
+
}
|
|
98
|
+
this.play_move(move.move, move.notation)
|
|
99
|
+
})
|
|
100
|
+
}
|
|
97
101
|
}
|
|
98
102
|
|
|
99
103
|
save () { // in order to evaluate potential moves, we have to modify the state
|
|
@@ -108,7 +112,6 @@ const Trax = class {
|
|
|
108
112
|
notation: this.notation,
|
|
109
113
|
tiles: JSON.stringify(this.tiles),
|
|
110
114
|
path: JSON.stringify(this.path),
|
|
111
|
-
count: this.count,
|
|
112
115
|
invalid: this.invalid,
|
|
113
116
|
}
|
|
114
117
|
}
|
|
@@ -124,10 +127,17 @@ const Trax = class {
|
|
|
124
127
|
this.notation = saved.notation
|
|
125
128
|
this.tiles = JSON.parse(saved.tiles)
|
|
126
129
|
this.path = JSON.parse(saved.path)
|
|
127
|
-
this.count = saved.count
|
|
128
130
|
this.invalid = saved.invalid
|
|
129
131
|
}
|
|
130
132
|
|
|
133
|
+
get count () { // get the number of tiles currently in play
|
|
134
|
+
return Object.keys(this.tiles).length
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
get color () { // get the color of the current player (w or b)
|
|
138
|
+
return color_of[this.turn]
|
|
139
|
+
}
|
|
140
|
+
|
|
131
141
|
get game_over () { // returns true if the game is over
|
|
132
142
|
return this.over
|
|
133
143
|
}
|
|
@@ -147,7 +157,6 @@ const Trax = class {
|
|
|
147
157
|
this.invalid = true // invalid tile is being played
|
|
148
158
|
} else {
|
|
149
159
|
this.tiles[id] = tile
|
|
150
|
-
this.count++
|
|
151
160
|
this.left = Math.min(this.left, x)
|
|
152
161
|
this.right = Math.max(this.right, x)
|
|
153
162
|
this.top = Math.min(this.top, y)
|
|
@@ -194,17 +203,27 @@ const Trax = class {
|
|
|
194
203
|
if (this.width > 7 && (x < this.left || x > this.right)) return false
|
|
195
204
|
if (this.height > 7 && (y < this.top || y > this.bottom)) return false
|
|
196
205
|
}
|
|
197
|
-
return
|
|
206
|
+
return Boolean(
|
|
207
|
+
this.tile_at(x-1, y) || this.tile_at(x+1, y) ||
|
|
208
|
+
this.tile_at(x, y-1) || this.tile_at(x, y+1)
|
|
209
|
+
)
|
|
198
210
|
}
|
|
199
211
|
|
|
200
212
|
possible_locations () {
|
|
201
|
-
let
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
}
|
|
213
|
+
let locs = { '0x0': { x:0, y:0 } } // if no tiles are placed, we can play a starting tile
|
|
214
|
+
const maybe = (x, y) => { // add a loc if a tile isn't already there
|
|
215
|
+
let loc = x + 'x' + y
|
|
216
|
+
locs[loc] = locs[loc] || { x, y }
|
|
206
217
|
}
|
|
207
|
-
|
|
218
|
+
for (let tile of Object.values(this.tiles)) {
|
|
219
|
+
locs[tile.x + 'x' + tile.y] = 'tile' // add an existing tile
|
|
220
|
+
maybe(tile.x - 1, tile.y) // and four maybe tiles around it
|
|
221
|
+
maybe(tile.x + 1, tile.y) // which will either be replaced by real tiles
|
|
222
|
+
maybe(tile.x, tile.y - 1) // or will be potentially valid locations
|
|
223
|
+
maybe(tile.x, tile.y + 1)
|
|
224
|
+
}
|
|
225
|
+
// get rid of all the real tiles and return all the maybes
|
|
226
|
+
return Object.values(locs).filter(l => l !== 'tile')
|
|
208
227
|
}
|
|
209
228
|
|
|
210
229
|
possible_moves () { // returns a list of every currently possible move
|
|
@@ -293,13 +312,12 @@ const Trax = class {
|
|
|
293
312
|
|
|
294
313
|
check_win (tiles) {
|
|
295
314
|
let winner = false
|
|
296
|
-
let colors = this.
|
|
315
|
+
let colors = [ this.color, other[this.color] ] // check win for current player first
|
|
297
316
|
colors.forEach(color => {
|
|
298
317
|
if (winner) return // skip the second player if the first has won
|
|
299
318
|
tiles.forEach(tile => {
|
|
300
319
|
if (winner) return // skip the remaining tiles once we find a win
|
|
301
320
|
let ends = this.find_ends(color, tile.x, tile.y)
|
|
302
|
-
if (ends.length !== 2) return // this should never happen, but if it does, don't die
|
|
303
321
|
if (ends[0].x === ends[1].x && ends[0].y === ends[1].y) {
|
|
304
322
|
winner = color // loop win
|
|
305
323
|
this.path = ends[0].path // both paths are essentially the same
|
|
@@ -314,12 +332,14 @@ const Trax = class {
|
|
|
314
332
|
})
|
|
315
333
|
if (winner) {
|
|
316
334
|
this.over = true
|
|
317
|
-
this.turn = winner
|
|
335
|
+
this.turn = player_num[winner]
|
|
318
336
|
} else {
|
|
319
337
|
// 8x8 Trax can end in a tie once there are no valid moves left
|
|
320
|
-
if (this.rules === 'trax8' && this.
|
|
321
|
-
this.
|
|
322
|
-
|
|
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
|
+
}
|
|
323
343
|
}
|
|
324
344
|
}
|
|
325
345
|
}
|
|
@@ -371,56 +391,66 @@ const Trax = class {
|
|
|
371
391
|
this.update_notation(notation)
|
|
372
392
|
dropped.push(this.add_tile(type, x, y)) // play the tile
|
|
373
393
|
dropped = dropped.concat(this.forced_moves(x, y)) // play all forced moves
|
|
374
|
-
if (!tentative) this.check_win(dropped, tentative)
|
|
375
394
|
if (this.invalid || tentative) {
|
|
376
|
-
valid = false
|
|
395
|
+
if (this.invalid) valid = false
|
|
377
396
|
this.restore(saved) // abort! abort!
|
|
378
|
-
} else
|
|
379
|
-
this.
|
|
397
|
+
} else {
|
|
398
|
+
this.check_win(dropped)
|
|
399
|
+
if (!this.over) this.turn = this.turn === 1 ? 2 : 1
|
|
380
400
|
}
|
|
381
401
|
}
|
|
382
402
|
}
|
|
383
403
|
return { dropped, notation, valid }
|
|
384
404
|
}
|
|
385
405
|
|
|
386
|
-
|
|
406
|
+
move_rotations (move) { // symmetry helper, rotate a move around the board
|
|
387
407
|
let match = move.match(notation_regex)
|
|
388
|
-
if (match === null) return
|
|
389
|
-
let x =
|
|
390
|
-
let y =
|
|
391
|
-
let s =
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
408
|
+
if (match === null) return []
|
|
409
|
+
let x = decode_col(match[1])
|
|
410
|
+
let y = Number(match[2])
|
|
411
|
+
let s = match[3]
|
|
412
|
+
let slashes = s === '+' ? [s] : ['/', '\\']
|
|
413
|
+
let moves = new Set()
|
|
414
|
+
// this isn't super precise, but it limits the search space ... maybe #TODO?
|
|
415
|
+
for (let slash of slashes) {
|
|
416
|
+
moves.add(encode_col(x) + String(y) + slash)
|
|
417
|
+
moves.add(encode_col(y) + String(x) + slash)
|
|
418
|
+
moves.add(encode_col(this.height - x + 1) + String(this.width - y + 1) + slash)
|
|
419
|
+
moves.add(encode_col(this.width - y + 1) + String(this.height -x + 1) + slash)
|
|
420
|
+
}
|
|
421
|
+
return [...moves]
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
provisional_move (from, to, via) { // test a provisional move against the current state
|
|
425
|
+
let rows = from.slice(1).split(':')
|
|
400
426
|
let cols = rows.reduce((acc, cur) => Math.max(acc, code_row_len(cur)), 0)
|
|
401
427
|
rows = rows.length
|
|
402
428
|
if (this.width > rows && this.height > cols || this.width > cols && this.height > rows) {
|
|
403
429
|
return 'delete-provisional' // current board is bigger than the provisional move
|
|
404
430
|
}
|
|
405
|
-
if (
|
|
406
|
-
let move = prov[2]
|
|
431
|
+
if (from === this.normalized) {
|
|
407
432
|
let saved = this.save() // save this position
|
|
408
|
-
for (let
|
|
409
|
-
this.drop_tile(move)
|
|
410
|
-
if (
|
|
411
|
-
|
|
412
|
-
|
|
433
|
+
for (let move of this.move_rotations(via)) { // try all symmetrical moves
|
|
434
|
+
let drop = this.drop_tile(move)
|
|
435
|
+
if (drop.valid) {
|
|
436
|
+
if (to === this.normalized) return move // this move matched!
|
|
437
|
+
this.restore(saved) // put the board back how it was
|
|
438
|
+
}
|
|
413
439
|
}
|
|
414
440
|
}
|
|
415
|
-
return
|
|
441
|
+
return false
|
|
416
442
|
}
|
|
417
443
|
|
|
418
444
|
get icon () {
|
|
419
445
|
return this.position_code()
|
|
420
446
|
}
|
|
421
447
|
|
|
448
|
+
tentative_icon (drops) {
|
|
449
|
+
return this.position_code(undefined, undefined, undefined, undefined, drops)
|
|
450
|
+
}
|
|
451
|
+
|
|
422
452
|
// symmetry helper
|
|
423
|
-
position_code (right_to_left, bottom_to_top, rotate, swap_colors) {
|
|
453
|
+
position_code (right_to_left, bottom_to_top, rotate, swap_colors, drops) {
|
|
424
454
|
const start_x = right_to_left ? this.right : this.left
|
|
425
455
|
const start_y = bottom_to_top ? this.bottom : this.top
|
|
426
456
|
const inc_x = right_to_left ? -1 : 1
|
|
@@ -439,6 +469,9 @@ const Trax = class {
|
|
|
439
469
|
if (this.path.includes(this.tile_id(x, y))) {
|
|
440
470
|
tile = String.fromCharCode(tile.charCodeAt(0) + this.turn * 6)
|
|
441
471
|
}
|
|
472
|
+
if (drops && drops.dropped) {
|
|
473
|
+
if (drops.dropped.filter(t => t.x === x && t.y === y).length) tile = tile.toUpperCase()
|
|
474
|
+
}
|
|
442
475
|
line += tile
|
|
443
476
|
} else {
|
|
444
477
|
line = add_blank(line)
|
|
@@ -481,7 +514,7 @@ const Trax = class {
|
|
|
481
514
|
})
|
|
482
515
|
})
|
|
483
516
|
})
|
|
484
|
-
return norm
|
|
517
|
+
return (this.color || 't').toUpperCase() + norm
|
|
485
518
|
}
|
|
486
519
|
|
|
487
520
|
get normalized () {
|
|
@@ -490,7 +523,13 @@ const Trax = class {
|
|
|
490
523
|
}
|
|
491
524
|
}
|
|
492
525
|
|
|
493
|
-
|
|
526
|
+
// add class methods
|
|
527
|
+
Trax.column = encode_col
|
|
528
|
+
|
|
529
|
+
// and class constants
|
|
494
530
|
Trax.variants = variants
|
|
531
|
+
Trax.color_of = color_of
|
|
532
|
+
Trax.player_num = player_num
|
|
533
|
+
Trax.other = other
|
|
495
534
|
|
|
496
|
-
|
|
535
|
+
export default Trax
|
package/jsconfig.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@slugbugblue/trax",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Trax game engine and friends",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"trax",
|
|
@@ -11,18 +11,20 @@
|
|
|
11
11
|
"bugs": "https://gitlab.com/slugbugblue/trax/issues",
|
|
12
12
|
"license": "Apache-2.0",
|
|
13
13
|
"author": "Chad Transtrum <chad@transtrum.net>",
|
|
14
|
-
"
|
|
14
|
+
"exports": "./engine.js",
|
|
15
15
|
"repository": "gitlab:slugbugblue/trax",
|
|
16
16
|
"scripts": {
|
|
17
|
-
"test": "
|
|
17
|
+
"test": "c8 tap --no-coverage",
|
|
18
|
+
"preversion": "npm test",
|
|
19
|
+
"postversion": "git push && git push --tags"
|
|
18
20
|
},
|
|
19
21
|
"devDependencies": {
|
|
20
|
-
"
|
|
21
|
-
"
|
|
22
|
-
"
|
|
23
|
-
"typescript-language-server": "^0.8.1"
|
|
22
|
+
"c8": "^7.11.0",
|
|
23
|
+
"eslint": "^8.7.0",
|
|
24
|
+
"tap": "^15.1.6"
|
|
24
25
|
},
|
|
26
|
+
"type": "module",
|
|
25
27
|
"engines": {
|
|
26
|
-
"node": ">=16.0.0"
|
|
28
|
+
"node": "^14.14.0 || >=16.0.0"
|
|
27
29
|
}
|
|
28
30
|
}
|
package/tty.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
/* Copyright 2021 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.
|
|
5
5
|
* You may obtain a copy of the License at
|
|
6
|
-
*
|
|
6
|
+
*
|
|
7
7
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
-
*
|
|
8
|
+
*
|
|
9
9
|
* Unless required by applicable law or agreed to in writing, software
|
|
10
10
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
11
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
@@ -13,17 +13,12 @@
|
|
|
13
13
|
* limitations under the License.
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
|
-
//
|
|
16
|
+
// Ever wanted to draw Trax boards on the console? I have.
|
|
17
17
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
const Trax = require('./engine')
|
|
21
|
-
|
|
22
|
-
// fix Javascript's broken typeof
|
|
23
|
-
const TypeOf = o => ({}).toString.call(o).slice(8,-1)
|
|
18
|
+
import Trax from './engine.js'
|
|
24
19
|
|
|
25
20
|
const esc = '\x1b[' // start of console color escape code
|
|
26
|
-
const off = esc + 'm'
|
|
21
|
+
const off = esc + 'm' // color reset code
|
|
27
22
|
|
|
28
23
|
// display a message with optional color
|
|
29
24
|
const out = (message, fgcolor, bgcolor) => {
|
|
@@ -34,34 +29,44 @@ const out = (message, fgcolor, bgcolor) => {
|
|
|
34
29
|
console.log(ansi + message + off)
|
|
35
30
|
}
|
|
36
31
|
|
|
32
|
+
let set = 'trax' // which color set should we draw with?
|
|
37
33
|
|
|
38
34
|
// a little magic to display the game in progress
|
|
39
35
|
const color_sets = { // Colors for terminal sequence: <Esc>[38;5;<color_num>m
|
|
36
|
+
// b, B, w, W: color of line for each player, capital for a winning line
|
|
37
|
+
// e: empty (ie, board) color, t: tile color
|
|
40
38
|
trax: { b: 238, B: 57, e: 19, t: 74, w: 255, W: 184 },
|
|
41
39
|
trax8: { b: 236, B: 21, e: 58, t: 137, w: 254, W: 117 },
|
|
42
40
|
traxloop: { b: 235, B: 20, e: 17, t: 61, w: 253, W: 222 },
|
|
41
|
+
tentative: { b: 235, B: 18, e: 19, t: 244, w: 252, W: 217 },
|
|
43
42
|
}
|
|
44
43
|
|
|
44
|
+
const ln = 'm\u2584' // lower half block -- with the leading m to terminate ansi color escape
|
|
45
|
+
const up = 'm\u2588' // full block
|
|
46
|
+
const lt = 'm\u2599' // lower half and left half block
|
|
47
|
+
const rt = 'm\u259f' // lower half and right half block
|
|
48
|
+
const fg = n => '38;5;' + String(set[n]) // foreground color_num
|
|
49
|
+
const bg = n => '48;5;' + String(set[n]) // background color_num
|
|
50
|
+
const sp = () => esc + bg('e') + 'm ' // lower space
|
|
51
|
+
// tiles are displayed on two lines; the top half of the first line is empty
|
|
52
|
+
// and the second half is composed of three characters, up color surrounded by tile color
|
|
53
|
+
const uptty = clr => esc + fg('t') + ';' + bg('e') + ln
|
|
54
|
+
+ esc + fg(clr) + ln + esc + fg('t') + ln + ' '
|
|
55
|
+
// the top half of the second line has both horizontal lines with a center transition
|
|
56
|
+
// while the bottom half is the tile background color surrounding the down color
|
|
57
|
+
const horiztty = clr => esc + fg('t') + ';' + bg(clr) + ln
|
|
58
|
+
const dntty = (clr, oppclr, char) => esc + fg(clr) + (char === up ? '' : ';' + bg(oppclr)) + char
|
|
59
|
+
// list of icon_char => tile line colors for easy reference in order up, right, down, left
|
|
60
|
+
const tiles = { a: 'bbww', b: 'bwbw', c: 'bwwb', d: 'wbbw', e: 'wbwb', f: 'wwbb' }
|
|
61
|
+
|
|
45
62
|
// return the ANSI codes to display a 3x1.5 block tile, one line at a time
|
|
46
|
-
const tile_tty = (
|
|
47
|
-
const ln = 'm\u2584' // lower half block -- with the leading m to terminate a terminal color escape sequence
|
|
48
|
-
const up = 'm\u2588' // full block
|
|
49
|
-
const lt = 'm\u2599' // lower half and left half block
|
|
50
|
-
const rt = 'm\u259f' // lower half and right half block
|
|
51
|
-
const fg = n => '38;5;' + String(set[n]) // foreground color_num
|
|
52
|
-
const bg = n => '48;5;' + String(set[n]) // background color_num
|
|
53
|
-
const sp = esc + bg('e') + 'm ' // lower space
|
|
54
|
-
// tiles are displayed on two lines; the top half of the first line is empty
|
|
55
|
-
// and the second half is composed of three characters, up color surrounded by tile color
|
|
56
|
-
const uptty = clr => esc + fg('t') + ';' + bg('e') + ln + esc + fg(clr) + ln + esc + fg('t') + ln + ' '
|
|
57
|
-
// the top half of the second line has both horizontal lines with a center transition
|
|
58
|
-
// while the bottom half is the tile background color surrounding the down color
|
|
59
|
-
const horiztty = clr => esc + fg('t') + ';' + bg(clr) + ln
|
|
60
|
-
const dntty = (clr, oppclr, char) => esc + fg(clr) + (char === up ? '' : ';' + bg(oppclr)) + char
|
|
61
|
-
// list of icon_char => tile line colors for easy reference in order up, right, down, left
|
|
62
|
-
const tiles = { a: 'bbww', b: 'bwbw', c: 'bwwb', d: 'wbbw', e: 'wbwb', f: 'wwbb' }
|
|
63
|
+
const tile_tty = (tile, top) => { // top: true for first line, false for second
|
|
63
64
|
// quick access to tile color and its opposite in upper case
|
|
64
|
-
|
|
65
|
+
let colors = { b: 'b', w: 'w', B: 'w', W: 'b' }
|
|
66
|
+
let save = set
|
|
67
|
+
let tentative = tile !== tile.toLowerCase()
|
|
68
|
+
if (tentative) set = color_sets.tentative
|
|
69
|
+
tile = tile.toLowerCase()
|
|
65
70
|
if (!(tile in tiles)) { // winning paths are represented by adding 6 to the tile char code
|
|
66
71
|
tile = String.fromCharCode(tile.charCodeAt(0) - 6)
|
|
67
72
|
if (tile in tiles) {
|
|
@@ -73,19 +78,30 @@ const tile_tty = (set, tile, top) => { // top: true for first line, false for se
|
|
|
73
78
|
colors.W = 'B'
|
|
74
79
|
}
|
|
75
80
|
}
|
|
81
|
+
let val = false
|
|
76
82
|
if (tile in tiles) {
|
|
77
83
|
tile = tiles[tile]
|
|
78
|
-
if (top)
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
84
|
+
if (top) {
|
|
85
|
+
val = uptty(colors[tile[0]])
|
|
86
|
+
} else {
|
|
87
|
+
let center_char = tile[1] === tile[2] ? rt : tile[2] === tile[3] ? lt : up
|
|
88
|
+
let centertty = dntty(colors[tile[2]], colors[tile[2].toUpperCase()], center_char)
|
|
89
|
+
val = horiztty(colors[tile[3]]) + centertty + horiztty(colors[tile[1]]) + sp()
|
|
90
|
+
}
|
|
82
91
|
}
|
|
83
|
-
|
|
84
|
-
return '\'\'\' '
|
|
92
|
+
set = save
|
|
93
|
+
return val || (top ? '??? ' : '\'\'\' ')
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const merge_tentative = () => {
|
|
97
|
+
color_sets.tentative.e = set.e // copy empty color to tentative set
|
|
98
|
+
color_sets.tentative.W = set.W
|
|
99
|
+
color_sets.tentative.B = set.B
|
|
85
100
|
}
|
|
86
101
|
|
|
87
102
|
const print_icon = (icon, set_name) => {
|
|
88
|
-
|
|
103
|
+
set = color_sets[set_name] || color_sets.trax
|
|
104
|
+
merge_tentative(set)
|
|
89
105
|
const on = esc + '38;5;' + String(set.t) + ';48;5;' + String(set.e) + 'm'
|
|
90
106
|
let o1 = ' '
|
|
91
107
|
let o2 = '1 '
|
|
@@ -110,8 +126,8 @@ const print_icon = (icon, set_name) => {
|
|
|
110
126
|
o2 += ' '.repeat(empty * 4)
|
|
111
127
|
line_width += empty
|
|
112
128
|
} else {
|
|
113
|
-
o1 += tile_tty(
|
|
114
|
-
o2 += tile_tty(
|
|
129
|
+
o1 += tile_tty(c, 'top')
|
|
130
|
+
o2 += tile_tty(c)
|
|
115
131
|
line_width++
|
|
116
132
|
}
|
|
117
133
|
}
|
|
@@ -144,7 +160,7 @@ const trax_game = (game='trax', moves) => {
|
|
|
144
160
|
if (game instanceof Trax) {
|
|
145
161
|
trax = game
|
|
146
162
|
if (moves) {
|
|
147
|
-
if (
|
|
163
|
+
if (typeof moves === 'string') moves = moves.split(' ')
|
|
148
164
|
moves.forEach(move => trax.drop_tile(move))
|
|
149
165
|
}
|
|
150
166
|
} else {
|
|
@@ -153,23 +169,33 @@ const trax_game = (game='trax', moves) => {
|
|
|
153
169
|
return trax
|
|
154
170
|
}
|
|
155
171
|
|
|
156
|
-
const display_game = (game='trax', moves='') => {
|
|
172
|
+
const display_game = (game='trax', moves='', tentative) => {
|
|
157
173
|
let trax = trax_game(game, moves)
|
|
174
|
+
let icon = trax.icon
|
|
175
|
+
if (tentative) {
|
|
176
|
+
let drops = trax.drop_tile(tentative)
|
|
177
|
+
if (drops.valid) {
|
|
178
|
+
icon = trax.tentative_icon(drops)
|
|
179
|
+
} else {
|
|
180
|
+
tentative = false
|
|
181
|
+
}
|
|
182
|
+
}
|
|
158
183
|
console.info(trax.notation)
|
|
159
|
-
print_icon(
|
|
160
|
-
|
|
184
|
+
print_icon(icon, trax.rules)
|
|
185
|
+
if (tentative) console.info('--- tentative position ---')
|
|
186
|
+
let name = {1: 'white', 2: 'black'}[trax.turn]
|
|
161
187
|
if (trax.over) {
|
|
162
188
|
if (trax.turn) {
|
|
163
189
|
console.info(name + ' wins')
|
|
164
190
|
} else {
|
|
165
|
-
console.info('game
|
|
191
|
+
console.info('game end' + (tentative ? 's' : 'ed') + ' in a draw')
|
|
166
192
|
}
|
|
167
193
|
} else if (trax.turn) {
|
|
168
|
-
console.info(name +' to play')
|
|
194
|
+
console.info((tentative ? 'leaving ' : '') + name +' to play')
|
|
169
195
|
}
|
|
170
196
|
}
|
|
171
197
|
|
|
172
|
-
|
|
198
|
+
export default {
|
|
173
199
|
display: display_game,
|
|
174
200
|
game: trax_game,
|
|
175
201
|
out,
|