@slugbugblue/trax 0.21.0 → 0.22.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/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
- }