@slugbugblue/trax 0.21.0 → 0.23.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +23 -6
- package/README.md +9 -175
- package/docs/engine.md +1 -1
- package/package.json +12 -23
- package/src/engine.js +128 -52
- package/src/types.d.ts +23 -131
- package/src/version.js +1 -1
- package/.dockerignore +0 -11
- package/Dockerfile +0 -14
- package/docs/analyst.md +0 -236
- package/docs/lru.md +0 -151
- package/docs/point.md +0 -148
- package/src/analyst.js +0 -858
- package/src/cli.js +0 -732
- package/src/cmds/analyze.js +0 -112
- package/src/cmds/delete.js +0 -71
- package/src/cmds/help.js +0 -81
- package/src/cmds/import-export.js +0 -394
- package/src/cmds/list.js +0 -177
- package/src/cmds/new.js +0 -136
- package/src/cmds/notes.js +0 -37
- package/src/cmds/play-try.js +0 -175
- package/src/cmds/puzzles.js +0 -188
- package/src/cmds/select.js +0 -47
- package/src/cmds/suggest.js +0 -55
- package/src/cmds/undo.js +0 -40
- package/src/cmds/view.js +0 -65
- package/src/lru.js +0 -153
- package/src/point.js +0 -173
- package/src/puzzles.js +0 -1236
- package/src/threats.js +0 -223
- package/src/tty.js +0 -317
- package/src/utils.js +0 -22
package/src/threats.js
DELETED
|
@@ -1,223 +0,0 @@
|
|
|
1
|
-
/** Threats database: hand-coded threats
|
|
2
|
-
* @copyright 2022-2023
|
|
3
|
-
* @author Chad Transtrum <chad@transtrum.net>
|
|
4
|
-
* @license Apache-2.0
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
const REGEX = {
|
|
8
|
-
// Some of these are one for one:
|
|
9
|
-
// a: White that pairs with the next white
|
|
10
|
-
A: '[am]', // White maybe in a hollow that pairs with next white
|
|
11
|
-
// c: White that forms a connectable pair with next white
|
|
12
|
-
C: '[cp]', // White maybe in a hollow connectable pair with next white
|
|
13
|
-
l: '[lmp]', // Any White in a hollow
|
|
14
|
-
// m: "a" in a hollow
|
|
15
|
-
// p: "c" in a hollow)
|
|
16
|
-
w: '[acw]', // Any white except in hollows
|
|
17
|
-
W: '[aclmpw]', // Any white at all
|
|
18
|
-
'!': '(?:b[acw])*?', // Ripple black/white
|
|
19
|
-
'¡': '(?:[acw]b)*?', // Ripple white/black
|
|
20
|
-
'*': 'b*?', // Zero or more black pieces
|
|
21
|
-
'?': 'b?', // Zero or one black piece
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export class Threat {
|
|
25
|
-
#depth = 0
|
|
26
|
-
#pattern = ''
|
|
27
|
-
#rx
|
|
28
|
-
#reforms = false
|
|
29
|
-
|
|
30
|
-
constructor(depth, pattern, reforms = false) {
|
|
31
|
-
this.#depth = depth
|
|
32
|
-
this.#pattern = pattern
|
|
33
|
-
this.#reforms = reforms
|
|
34
|
-
|
|
35
|
-
let rx = ''
|
|
36
|
-
for (const char of pattern) {
|
|
37
|
-
rx += REGEX[char] || char
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
// Create the regex with lookahead assertions to allow overlapping matches
|
|
41
|
-
this.#rx = new RegExp('(?=(' + rx + '))', 'g')
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
get depth() {
|
|
45
|
-
return this.#depth
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
get pattern() {
|
|
49
|
-
return this.#pattern
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
get rx() {
|
|
53
|
-
return this.#rx
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
get value() {
|
|
57
|
-
return this.#reforms ? 2 : 1
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
// To make life at the REPL a little easier
|
|
61
|
-
[Symbol.for('nodejs.util.inspect.custom')](_, out) {
|
|
62
|
-
const style = out.stylize
|
|
63
|
-
const display = [
|
|
64
|
-
'[' + style('Threat', 'special') + ']',
|
|
65
|
-
'{',
|
|
66
|
-
style(this.#pattern, 'string') + ':',
|
|
67
|
-
style(this.#depth, 'number'),
|
|
68
|
-
this.#reforms ? '!' : '',
|
|
69
|
-
'}',
|
|
70
|
-
]
|
|
71
|
-
return display.filter((d) => d !== '').join(' ')
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
export class ThreatsDB {
|
|
76
|
-
#db = {}
|
|
77
|
-
#depth = 0
|
|
78
|
-
|
|
79
|
-
constructor(corners) {
|
|
80
|
-
for (const corner of corners) {
|
|
81
|
-
this.add(0, corner)
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
compose(depth, pattern) {
|
|
86
|
-
pattern = pattern.replace(/(?<=.)A/g, 'a').replace(/W(?=.)/g, 'w')
|
|
87
|
-
return this.add(depth, pattern)
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
add(depth, pattern, reforms = false) {
|
|
91
|
-
this.#depth = Math.max(depth, this.#depth)
|
|
92
|
-
if (pattern in this.#db) throw new Error('Duplicate pattern: ' + pattern)
|
|
93
|
-
this.#db[pattern] = new Threat(depth, pattern, reforms)
|
|
94
|
-
return this.#db[pattern]
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
level(depth) {
|
|
98
|
-
return Object.values(this.#db).filter((p) => p.depth === depth)
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
get list() {
|
|
102
|
-
return Object.values(this.#db)
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
get maxDepth() {
|
|
106
|
-
return this.#depth
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
// To make life at the REPL a little easier
|
|
110
|
-
[Symbol.for('nodejs.util.inspect.custom')](_, out) {
|
|
111
|
-
const style = out.stylize
|
|
112
|
-
const inspection = '[' + style('Threats', 'special') + ':'
|
|
113
|
-
let total = 0
|
|
114
|
-
const keys = ['{']
|
|
115
|
-
|
|
116
|
-
for (let x = 0; x <= this.#depth; x++) {
|
|
117
|
-
const count = this.level(x).length
|
|
118
|
-
keys.push(style(x, 'string') + ':' + style(count, 'number'))
|
|
119
|
-
total += count
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
return inspection + style(total, 'number') + '] ' + keys.join(' ') + ' }'
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
const pre = true // This pattern can appear first in an L threat
|
|
127
|
-
const post = true // This pattern can come second in an L threat
|
|
128
|
-
const corners = {
|
|
129
|
-
ArW: { pre, post }, // Simple corner
|
|
130
|
-
AbrW: { post }, // Wide-first corner
|
|
131
|
-
ArbW: { pre }, // Wide-second corner
|
|
132
|
-
AbrbW: {}, // Wide corner
|
|
133
|
-
'ab?bW': { pre, post }, // Flat corner and wide flat corner
|
|
134
|
-
CW: { pre, post }, // Connectable pair
|
|
135
|
-
CbW: {}, // Wide connectable pair
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
export const threats = new ThreatsDB(Object.keys(corners))
|
|
139
|
-
|
|
140
|
-
// Attacks
|
|
141
|
-
const attacks = [
|
|
142
|
-
'a?W', // Simple attack and wide attack
|
|
143
|
-
'm?W', // Simple and wide attack in a corner
|
|
144
|
-
]
|
|
145
|
-
attacks.map((a) => threats.add(1, a))
|
|
146
|
-
|
|
147
|
-
// Re-forming attacks
|
|
148
|
-
const reformAttacks = [
|
|
149
|
-
'rm?lr', // Cave/wide cave attack
|
|
150
|
-
'rbm?lbr', // Deep cave/deep wide cave attack (impossibly rare?)
|
|
151
|
-
'ralmrW', // Attack at top of cave
|
|
152
|
-
'arlmwr', // Reversed
|
|
153
|
-
]
|
|
154
|
-
reformAttacks.map((ra) => threats.add(1, ra, true))
|
|
155
|
-
|
|
156
|
-
const reformThreats = [
|
|
157
|
-
'rp?lr', // Cave/wide cave threat
|
|
158
|
-
'rbp?lbr', // Deep cave/deep wide cave threat (impossibly rare?)
|
|
159
|
-
'rmbb???w', // Black corner with a long white loop potential
|
|
160
|
-
'abb???lr', // Reverse
|
|
161
|
-
]
|
|
162
|
-
reformThreats.map((rt) => threats.add(2, rt))
|
|
163
|
-
|
|
164
|
-
// Create multi-stage threats
|
|
165
|
-
for (const one of Object.keys(corners)) {
|
|
166
|
-
const start = one.replace('W', 'l')
|
|
167
|
-
let end = one.replace(/[aA]/, 'm').replace('C', 'p')
|
|
168
|
-
const pre = corners[one].post ? '?' : ''
|
|
169
|
-
let post = corners[one].pre ? '?' : ''
|
|
170
|
-
threats.compose(3, start + pre + '¡rm?W') // Attacking 3-stage threat
|
|
171
|
-
threats.compose(3, 'a?lr!' + post + end) // Reverse
|
|
172
|
-
threats.compose(3, one + '¡ab*bwr') // Corner by reforming hollow potential
|
|
173
|
-
threats.compose(3, 'rab*bw!' + one) // Reverse
|
|
174
|
-
threats.compose(3, one + '!brm*bW') // Corner by hollow attack, defends to edge
|
|
175
|
-
threats.compose(3, one + '!bbrmW') // Same, but slightly different layout
|
|
176
|
-
threats.compose(3, 'ab*lrb¡' + one) // Reverse
|
|
177
|
-
threats.compose(3, 'alrbb¡' + one) // Reverse of different layout
|
|
178
|
-
threats.compose(4, one + '!b*b¡ra?lr') // Partial edge with attack into hollow
|
|
179
|
-
threats.compose(4, 'rm?wr!b*b¡' + one) // Reverse
|
|
180
|
-
|
|
181
|
-
for (const two of Object.keys(corners)) {
|
|
182
|
-
end = two.replace(/[aA]/, 'm').replace('C', 'p')
|
|
183
|
-
post = corners[two].pre ? '?' : ''
|
|
184
|
-
if (pre) {
|
|
185
|
-
threats.compose(2, start + pre + '¡' + two) // Make an L
|
|
186
|
-
threats.compose(3, start + pre + 'rb¡' + two) // Make a three-stage threat
|
|
187
|
-
threats.compose(3, start + '?!rb¡' + two) // Attack to form an L threat
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
if (post) {
|
|
191
|
-
threats.compose(2, one + post + '!' + end) // L
|
|
192
|
-
threats.compose(3, one + post + '!br' + end) // Three-stage
|
|
193
|
-
threats.compose(3, one + post + 'r' + end) // Defense forms an L
|
|
194
|
-
threats.compose(3, one + '!br¡?' + end) // Attack to form an L threat
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
if (pre && post) {
|
|
198
|
-
threats.compose(3, start + 'r' + end) // Defense forms an L
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
threats.compose(3, one + '!b*b¡' + two) // Edge threat
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
threats.add(3, 'ralbmrW') // Attack into a wide cave
|
|
206
|
-
threats.add(3, 'Arlbmwr') // Reversed
|
|
207
|
-
|
|
208
|
-
threats.add(4, 'rb¡c?w!br') // Connectable pair on an edge
|
|
209
|
-
threats.add(4, 'ArlbmrW') // Wide cave ready for an attack
|
|
210
|
-
|
|
211
|
-
// Threats to solve puzzle iw1.trx
|
|
212
|
-
threats.add(5, 'A?rwarlrar?W') // Back to back white corners next to a corner
|
|
213
|
-
threats.add(4, 'A?rlr!bralrar?W') // ... activated ... becomes:
|
|
214
|
-
threats.add(4, 'A?rlrbrmrwar?W') // Corner surrounded by corners
|
|
215
|
-
threats.add(4, 'A?rlr!bramrar?W') // ... activated ...
|
|
216
|
-
threats.add(3, 'A?rlr!brm?wr!brmrW') // ... defended ...
|
|
217
|
-
|
|
218
|
-
// Threats to solve puzzle ib10.trx
|
|
219
|
-
threats.add(6, 'Arw!b?rarlr') // Sucker's Opening
|
|
220
|
-
threats.add(5, 'Arw!b?brbralr') // ... activated ... becomes:
|
|
221
|
-
threats.add(4, 'Arw!b?brb¡arbW') // Wide corner opposite nearly formed edge
|
|
222
|
-
threats.add(5, 'Arw!b?brb¡rmrW') // ... or hollow corner opposite same edge
|
|
223
|
-
threats.add(4, 'Arw!b?brb¡rmwr') // ... activated ...
|
package/src/tty.js
DELETED
|
@@ -1,317 +0,0 @@
|
|
|
1
|
-
/* Copyright 2021-2022 Chad Transtrum
|
|
2
|
-
*
|
|
3
|
-
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
-
* you may not use this file except in compliance with the License.
|
|
5
|
-
* You may obtain a copy of the License at
|
|
6
|
-
*
|
|
7
|
-
* http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
-
*
|
|
9
|
-
* Unless required by applicable law or agreed to in writing, software
|
|
10
|
-
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
-
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
-
* See the License for the specific language governing permissions and
|
|
13
|
-
* limitations under the License.
|
|
14
|
-
*/
|
|
15
|
-
|
|
16
|
-
// Ever wanted to draw Trax boards on the console? I have.
|
|
17
|
-
|
|
18
|
-
import process from 'node:process'
|
|
19
|
-
|
|
20
|
-
import { Trax } from '@slugbugblue/trax'
|
|
21
|
-
|
|
22
|
-
const esc = '\u001B[' // Start of console color escape code
|
|
23
|
-
const off = esc + 'm' // Color reset code
|
|
24
|
-
const ENV = process.env
|
|
25
|
-
const NERD = ENV.NERDFONT || ENV.NERDFONTS
|
|
26
|
-
const POWER = ENV.POWERLINE || ENV.P9K_TTY || ENV.P9K_SSH
|
|
27
|
-
const leftBubble = NERD ? '' : POWER ? '' : '▐'
|
|
28
|
-
const rightBubble = NERD ? '' : POWER ? '' : '▌'
|
|
29
|
-
|
|
30
|
-
/** Colorize a string.
|
|
31
|
-
* @arg {string} contents
|
|
32
|
-
* @arg {string|number} [fgcolor]
|
|
33
|
-
* @arg {string|number} [bgcolor]
|
|
34
|
-
*/
|
|
35
|
-
export const color = (contents = '', fgcolor = '', bgcolor = '') => {
|
|
36
|
-
const c = []
|
|
37
|
-
if (fgcolor) c.push('38;5;' + fgcolor)
|
|
38
|
-
if (bgcolor) c.push('48;5;' + bgcolor)
|
|
39
|
-
return esc + c.join(';') + 'm' + contents
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
// Display a message with optional color
|
|
43
|
-
export const outty = (message, fgcolor, bgcolor) => {
|
|
44
|
-
console.log(color(message, fgcolor, bgcolor) + off)
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
// A little magic to display the game in progress
|
|
48
|
-
const colorSets = {
|
|
49
|
-
// Colors for terminal sequence: <Esc>[38;5;<color_num>m
|
|
50
|
-
// b, B, w, W: color of line for each player, capital for a winning line
|
|
51
|
-
// e: empty (ie, board) color, t: tile color
|
|
52
|
-
trax: { b: 238, B: 57, e: 19, t: 74, w: 255, W: 184 },
|
|
53
|
-
trax8: { b: 236, B: 21, e: 58, t: 137, w: 254, W: 117 },
|
|
54
|
-
traxloop: { b: 235, B: 20, e: 17, t: 61, w: 253, W: 222 },
|
|
55
|
-
tentative: { b: 235, B: 18, e: 19, t: 244, w: 252, W: 217 },
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
let set = colorSets.trax // Which color set should we draw with?
|
|
59
|
-
|
|
60
|
-
export const name = (rules) => {
|
|
61
|
-
if (Trax.variants.has(rules)) {
|
|
62
|
-
return color(Trax.names[rules], colorSets[rules].t)
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
return rules
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
export const bubble = (clr, text) => {
|
|
69
|
-
const colors = {
|
|
70
|
-
w: { fg: 236, bg: 153 },
|
|
71
|
-
b: { fg: 250, bg: 18 },
|
|
72
|
-
wd: { fg: 236, bg: 248 },
|
|
73
|
-
bd: { fg: 248, bg: 236 },
|
|
74
|
-
wh: { fg: 63, bg: 123 },
|
|
75
|
-
bh: { fg: 220, bg: 20 },
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
const c = colors[clr]
|
|
79
|
-
return (
|
|
80
|
-
color(leftBubble, c.bg) +
|
|
81
|
-
color(text, c.fg, c.bg) +
|
|
82
|
-
off +
|
|
83
|
-
color(rightBubble, c.bg)
|
|
84
|
-
)
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
const ln = 'm\u2584' // Lower half block -- with the leading m to terminate ansi color escape
|
|
88
|
-
const up = 'm\u2588' // Full block
|
|
89
|
-
const lt = 'm\u2599' // Lower half and left half block
|
|
90
|
-
const rt = 'm\u259F' // Lower half and right half block
|
|
91
|
-
const fg = (n) => '38;5;' + String(set[n]) // Foreground color_num
|
|
92
|
-
const bg = (n) => '48;5;' + String(set[n]) // Background color_num
|
|
93
|
-
const sp = () => esc + bg('e') + 'm ' // Lower space
|
|
94
|
-
// tiles are displayed on two lines; the top half of the first line is empty
|
|
95
|
-
// and the second half is composed of three characters, up color surrounded by tile color
|
|
96
|
-
const uptty = (clr) => {
|
|
97
|
-
const left = esc + fg('t') + ';' + bg('e') + ln
|
|
98
|
-
const center = esc + fg(clr) + ln
|
|
99
|
-
const right = esc + fg('t') + ln + ' '
|
|
100
|
-
return left + center + right
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
// The top half of the second line has both horizontal lines with a center transition
|
|
104
|
-
// while the bottom half is the tile background color surrounding the down color
|
|
105
|
-
const horiztty = (clr) => esc + fg('t') + ';' + bg(clr) + ln
|
|
106
|
-
const dntty = (clr, oppclr, char) =>
|
|
107
|
-
esc + fg(clr) + (char === up ? '' : ';' + bg(oppclr)) + char
|
|
108
|
-
// List of icon_char => tile line colors for easy reference in order up, right, down, left
|
|
109
|
-
const tiles = {
|
|
110
|
-
a: 'bbww',
|
|
111
|
-
b: 'bwbw',
|
|
112
|
-
c: 'bwwb',
|
|
113
|
-
d: 'wbbw',
|
|
114
|
-
e: 'wbwb',
|
|
115
|
-
f: 'wwbb',
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
// Return the ANSI codes to display a 3x1.5 block tile, one line at a time
|
|
119
|
-
const tileTty = (tile, top) => {
|
|
120
|
-
// Top: true for first line, false for second
|
|
121
|
-
// quick access to tile color and its opposite in upper case
|
|
122
|
-
const colors = { b: 'b', w: 'w', B: 'w', W: 'b' }
|
|
123
|
-
const save = set
|
|
124
|
-
const tentative = tile !== tile.toLowerCase()
|
|
125
|
-
if (tentative) set = colorSets.tentative
|
|
126
|
-
tile = tile.toLowerCase()
|
|
127
|
-
if (!(tile in tiles)) {
|
|
128
|
-
// Winning paths are represented by adding 6 to the tile char code
|
|
129
|
-
tile = String.fromCodePoint(tile.codePointAt(0) - 6)
|
|
130
|
-
if (tile in tiles) {
|
|
131
|
-
colors.w = 'W'
|
|
132
|
-
colors.B = 'W'
|
|
133
|
-
} else {
|
|
134
|
-
// And another six for a black win
|
|
135
|
-
tile = String.fromCodePoint(tile.codePointAt(0) - 6)
|
|
136
|
-
colors.b = 'B'
|
|
137
|
-
colors.W = 'B'
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
let value = ''
|
|
142
|
-
if (tile in tiles) {
|
|
143
|
-
tile = tiles[tile]
|
|
144
|
-
if (top) {
|
|
145
|
-
value = uptty(colors[tile[0]])
|
|
146
|
-
} else {
|
|
147
|
-
const centerChar =
|
|
148
|
-
tile[1] === tile[2] ? rt : tile[2] === tile[3] ? lt : up
|
|
149
|
-
const centertty = dntty(
|
|
150
|
-
colors[tile[2]],
|
|
151
|
-
colors[tile[2].toUpperCase()],
|
|
152
|
-
centerChar,
|
|
153
|
-
)
|
|
154
|
-
value =
|
|
155
|
-
horiztty(colors[tile[3]]) + centertty + horiztty(colors[tile[1]]) + sp()
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
set = save
|
|
160
|
-
return value || (top ? '??? ' : "''' ")
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
const mergeTentative = () => {
|
|
164
|
-
colorSets.tentative.e = set.e // Copy empty color to tentative set
|
|
165
|
-
colorSets.tentative.W = set.W
|
|
166
|
-
colorSets.tentative.B = set.B
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
const printIcon = (icon, setName) => {
|
|
170
|
-
set = colorSets[setName] || colorSets.trax
|
|
171
|
-
mergeTentative()
|
|
172
|
-
const on = esc + '38;5;' + String(set.t) + ';48;5;' + String(set.e) + 'm'
|
|
173
|
-
let o1 = ' '
|
|
174
|
-
let o2 = '1 '
|
|
175
|
-
let row = 1
|
|
176
|
-
let width = 1
|
|
177
|
-
const info = []
|
|
178
|
-
let lineWidth = 0
|
|
179
|
-
while (icon.length > 0) {
|
|
180
|
-
const c = icon[0]
|
|
181
|
-
icon = icon.slice(1)
|
|
182
|
-
if (c === ':') {
|
|
183
|
-
info.push({ o: o1, w: lineWidth }, { o: o2, w: lineWidth })
|
|
184
|
-
row++
|
|
185
|
-
width = Math.max(width, lineWidth)
|
|
186
|
-
lineWidth = 0
|
|
187
|
-
o1 = ' '
|
|
188
|
-
o2 = String(row).padEnd(3)
|
|
189
|
-
} else if (c >= '0' && c <= '9') {
|
|
190
|
-
const empty = Number(c) + 1
|
|
191
|
-
o1 += ' '.repeat(empty * 4)
|
|
192
|
-
o2 += ' '.repeat(empty * 4)
|
|
193
|
-
lineWidth += empty
|
|
194
|
-
} else {
|
|
195
|
-
o1 += tileTty(c, 'top')
|
|
196
|
-
o2 += tileTty(c)
|
|
197
|
-
lineWidth++
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
if (o1.length > 0) {
|
|
202
|
-
info.push({ o: o1, w: lineWidth }, { o: o2, w: lineWidth })
|
|
203
|
-
width = Math.max(width, lineWidth)
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
const columns = process.stdout.columns || 0
|
|
207
|
-
const rows = process.stdout.rows || 0
|
|
208
|
-
if (width * 4 < columns - 4 && info.length < rows - 2) {
|
|
209
|
-
let colOut = ' '
|
|
210
|
-
for (let col = 1; col <= width; col++) {
|
|
211
|
-
colOut += ' ' + Trax.encodeCol(col).padEnd(3)
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
console.info(on + colOut.padEnd((width + 1) * 4) + off)
|
|
215
|
-
for (const line of info) {
|
|
216
|
-
console.info(on + line.o + ''.padEnd((width - line.w) * 4) + ' ' + off)
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
console.info(on + ''.padEnd((width + 1) * 4) + off)
|
|
220
|
-
} else {
|
|
221
|
-
console.warn('Console is too small to display this game')
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
/** Flexible way to get a Trax game object.
|
|
226
|
-
* @arg {Trax|TraxVariant} [game] - existing Trax instance or a string indicating the variant name
|
|
227
|
-
* @arg {string|string[]} [moves] - optional list or string of moves to be made on the game
|
|
228
|
-
* @returns {Trax} a Trax instance
|
|
229
|
-
*/
|
|
230
|
-
export const traxGame = (game = 'trax', moves = '') => {
|
|
231
|
-
let trax
|
|
232
|
-
if (game instanceof Trax) {
|
|
233
|
-
trax = game
|
|
234
|
-
if (moves) {
|
|
235
|
-
if (typeof moves === 'string') moves = moves.split(' ')
|
|
236
|
-
for (const move of moves) trax.dropTile(move)
|
|
237
|
-
}
|
|
238
|
-
} else {
|
|
239
|
-
trax = new Trax(game, moves, 'cli')
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
return trax
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
const printNotation = (trax, tentative, show = Number.POSITIVE_INFINITY) => {
|
|
246
|
-
let b = false
|
|
247
|
-
let n = 1
|
|
248
|
-
let out = ''
|
|
249
|
-
let length = 0
|
|
250
|
-
const t = tentative ? trax.moves.length - 1 : show
|
|
251
|
-
for (const move of trax.moves) {
|
|
252
|
-
const c = (b ? 'b' : 'w') + (t < n ? 'd' : show === n ? 'h' : '')
|
|
253
|
-
const notation = String(n) + ' ' + move
|
|
254
|
-
if (length + notation.length + 3 > process.stdout.columns) {
|
|
255
|
-
out += '\n'
|
|
256
|
-
length = 0
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
out += bubble(c, notation) + ' '
|
|
260
|
-
length += notation.length + 3
|
|
261
|
-
b = !b
|
|
262
|
-
n++
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
outty(out)
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
export const display = (
|
|
269
|
-
game,
|
|
270
|
-
players,
|
|
271
|
-
tentative = '',
|
|
272
|
-
showMove = Number.POSITIVE_INFINITY,
|
|
273
|
-
) => {
|
|
274
|
-
const trax = traxGame(game)
|
|
275
|
-
const saved = trax.save()
|
|
276
|
-
const colors = ['white', 'black']
|
|
277
|
-
if (!Array.isArray(players)) players = colors
|
|
278
|
-
let { icon } = trax
|
|
279
|
-
if (tentative) {
|
|
280
|
-
const drops = trax.dropTile(tentative)
|
|
281
|
-
if (drops.valid || drops.dropped.length > 0) {
|
|
282
|
-
icon = trax.dropsIcon(drops)
|
|
283
|
-
} else {
|
|
284
|
-
tentative = ''
|
|
285
|
-
}
|
|
286
|
-
} else if (showMove <= trax.moves.length) {
|
|
287
|
-
icon = trax.dropsIcon({
|
|
288
|
-
dropped: Object.values(trax.tiles).filter((t) => t.move > showMove),
|
|
289
|
-
})
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
printNotation(trax, tentative, showMove)
|
|
293
|
-
printIcon(icon, trax.rules)
|
|
294
|
-
const nameColor = { 1: 'w', 2: 'b' }[trax.turn]
|
|
295
|
-
if (tentative) {
|
|
296
|
-
const otherColor = { w: 'bd', b: 'wd' }[nameColor] || 'wd'
|
|
297
|
-
outty('---' + bubble(otherColor, 'tentative position') + off + '---')
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
const name = players[trax.turn - 1] || colors[trax.turn - 1]
|
|
301
|
-
if (trax.over) {
|
|
302
|
-
if (trax.turn) {
|
|
303
|
-
outty(bubble(nameColor + 'h', name) + off + ' wins')
|
|
304
|
-
} else {
|
|
305
|
-
outty('game end' + (tentative ? 's' : 'ed') + ' in a draw')
|
|
306
|
-
}
|
|
307
|
-
} else if (trax.turn) {
|
|
308
|
-
outty(
|
|
309
|
-
(tentative ? 'leaving ' : '') +
|
|
310
|
-
bubble(nameColor, name) +
|
|
311
|
-
off +
|
|
312
|
-
' to play',
|
|
313
|
-
)
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
if (tentative) trax.restore(saved)
|
|
317
|
-
}
|
package/src/utils.js
DELETED
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
/** Shared utilities
|
|
2
|
-
* @copyright 2023
|
|
3
|
-
* @author Chad Transtrum <chad@transtrum.net>
|
|
4
|
-
* @license Apache-2.0
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
/** Convert milliseconds to a human readable string in ms, sec, or min units.
|
|
8
|
-
* @arg {number} ms - the number of milliseconds
|
|
9
|
-
* @returns {string} a short human-readable representation of the time
|
|
10
|
-
*/
|
|
11
|
-
export const timeString = (ms) => {
|
|
12
|
-
if (ms < 1000) {
|
|
13
|
-
return (ms === Math.floor(ms) ? String(ms) : ms.toFixed(2)) + ' ms'
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
ms /= 1000
|
|
17
|
-
if (ms < 60) return ms.toFixed(3) + ' sec'
|
|
18
|
-
|
|
19
|
-
const min = Math.floor(ms / 60)
|
|
20
|
-
ms -= min * 60
|
|
21
|
-
return String(min) + ' min ' + ms.toFixed(3) + ' sec'
|
|
22
|
-
}
|