@slugbugblue/trax 0.4.0 → 0.8.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/point.js ADDED
@@ -0,0 +1,116 @@
1
+ /* Copyright 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
+ const is = (v, s) => (typeof v).slice(0, 3) === s // Sigh
17
+
18
+ // Are you tired of doing x,y movements in multiple statements? I am.
19
+
20
+ // Point: an immutable two-dimensional location (x,y)
21
+ // with functions for moving around quickly to neighboring points
22
+ export const Point = class {
23
+ #x = 0
24
+ #y = 0
25
+
26
+ constructor(x, y) {
27
+ if (is(x, 'obj') && is(x.x, 'num') && is(x.y, 'num')) {
28
+ // You can call this with new Point(point) or new Point({x,y})
29
+ this.#x = x.x
30
+ this.#y = x.y
31
+ } else {
32
+ // Or with new Point(x, y)
33
+ if (!is(x, 'num')) throw new SyntaxError('x must be a number')
34
+ if (!is(y, 'num')) throw new SyntaxError('y must be a number')
35
+ this.#x = x
36
+ this.#y = y
37
+ }
38
+ }
39
+
40
+ get x() {
41
+ return this.#x
42
+ }
43
+
44
+ set x(_) {
45
+ throw new ReferenceError('Point is immutable')
46
+ }
47
+
48
+ get y() {
49
+ return this.#y
50
+ }
51
+
52
+ set y(_) {
53
+ throw new ReferenceError('Point is immutable')
54
+ }
55
+
56
+ get up() {
57
+ return new Point(this.#x, this.#y - 1)
58
+ }
59
+
60
+ get down() {
61
+ return new Point(this.#x, this.#y + 1)
62
+ }
63
+
64
+ get left() {
65
+ return new Point(this.#x - 1, this.#y)
66
+ }
67
+
68
+ get right() {
69
+ return new Point(this.#x + 1, this.#y)
70
+ }
71
+
72
+ get around() {
73
+ return [this.up, this.down, this.left, this.right]
74
+ }
75
+
76
+ dir(direction) {
77
+ const error = 'direction must be one of: up, down, left, right, u, d, l, r'
78
+ if (!is(direction, 'str')) throw new SyntaxError(error)
79
+ if (direction[0] === 'u') return this.up
80
+ if (direction[0] === 'd') return this.down
81
+ if (direction[0] === 'l') return this.left
82
+ if (direction[0] === 'r') return this.right
83
+ throw new SyntaxError(error)
84
+ }
85
+
86
+ distX(from) {
87
+ return Math.abs(this.#x - from.x)
88
+ }
89
+
90
+ distY(from) {
91
+ return Math.abs(this.#y - from.y)
92
+ }
93
+
94
+ distance(from) {
95
+ return Math.hypot(this.distX(from), this.distY(from))
96
+ }
97
+
98
+ eq(that) {
99
+ return this.#x === that.x && this.#y === that.y
100
+ }
101
+
102
+ // Play nicely with the rest of the world
103
+
104
+ toString() {
105
+ return 'Point(' + this.#x + ',' + this.#y + ')'
106
+ }
107
+
108
+ toJSON() {
109
+ return { x: this.#x, y: this.#y }
110
+ }
111
+
112
+ [Symbol.for('nodejs.util.inspect.custom')](_, options) {
113
+ const [y, s, n] = [options.stylize, 'special', 'number']
114
+ return `${y('Point', s)}(${y(this.#x, n)},${y(this.#y, n)})`
115
+ }
116
+ }
package/src/tty.js ADDED
@@ -0,0 +1,295 @@
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 './engine.js'
21
+
22
+ const esc = '\u001B[' // Start of console color escape code
23
+ const off = esc + 'm' // Color reset code
24
+
25
+ // Colorize a string
26
+ export const color = (contents = '', fgcolor = '', bgcolor = '') => {
27
+ let ansi = esc
28
+ const c = []
29
+ if (fgcolor) c.push('38;5;' + fgcolor)
30
+ if (bgcolor) c.push('48;5;' + bgcolor)
31
+ ansi += c.join(';') + 'm'
32
+ return ansi + contents
33
+ }
34
+
35
+ // Display a message with optional color
36
+ export const outty = (message, fgcolor, bgcolor) => {
37
+ console.log(color(message, fgcolor, bgcolor) + off)
38
+ }
39
+
40
+ let set = 'trax' // Which color set should we draw with?
41
+
42
+ // a little magic to display the game in progress
43
+ const colorSets = {
44
+ // Colors for terminal sequence: <Esc>[38;5;<color_num>m
45
+ // b, B, w, W: color of line for each player, capital for a winning line
46
+ // e: empty (ie, board) color, t: tile color
47
+ trax: { b: 238, B: 57, e: 19, t: 74, w: 255, W: 184 },
48
+ trax8: { b: 236, B: 21, e: 58, t: 137, w: 254, W: 117 },
49
+ traxloop: { b: 235, B: 20, e: 17, t: 61, w: 253, W: 222 },
50
+ tentative: { b: 235, B: 18, e: 19, t: 244, w: 252, W: 217 },
51
+ }
52
+
53
+ export const name = (rules) => {
54
+ if (Trax.variants.has(rules)) {
55
+ return color(Trax.names[rules], colorSets[rules].t)
56
+ }
57
+
58
+ return rules
59
+ }
60
+
61
+ export const bubble = (clr, text) => {
62
+ const colors = {
63
+ w: { fg: 236, bg: 153 },
64
+ b: { fg: 250, bg: 18 },
65
+ wd: { fg: 236, bg: 248 },
66
+ bd: { fg: 248, bg: 236 },
67
+ wh: { fg: 63, bg: 123 },
68
+ bh: { fg: 220, bg: 20 },
69
+ }
70
+
71
+ const c = colors[clr]
72
+ return color('', c.bg) + color(text, c.fg, c.bg) + off + color('', c.bg)
73
+ }
74
+
75
+ const ln = 'm\u2584' // Lower half block -- with the leading m to terminate ansi color escape
76
+ const up = 'm\u2588' // Full block
77
+ const lt = 'm\u2599' // Lower half and left half block
78
+ const rt = 'm\u259F' // Lower half and right half block
79
+ const fg = (n) => '38;5;' + String(set[n]) // Foreground color_num
80
+ const bg = (n) => '48;5;' + String(set[n]) // Background color_num
81
+ const sp = () => esc + bg('e') + 'm ' // Lower space
82
+ // tiles are displayed on two lines; the top half of the first line is empty
83
+ // and the second half is composed of three characters, up color surrounded by tile color
84
+ const uptty = (clr) => {
85
+ const left = esc + fg('t') + ';' + bg('e') + ln
86
+ const center = esc + fg(clr) + ln
87
+ const right = esc + fg('t') + ln + ' '
88
+ return left + center + right
89
+ }
90
+
91
+ // The top half of the second line has both horizontal lines with a center transition
92
+ // while the bottom half is the tile background color surrounding the down color
93
+ const horiztty = (clr) => esc + fg('t') + ';' + bg(clr) + ln
94
+ const dntty = (clr, oppclr, char) =>
95
+ esc + fg(clr) + (char === up ? '' : ';' + bg(oppclr)) + char
96
+ // List of icon_char => tile line colors for easy reference in order up, right, down, left
97
+ const tiles = {
98
+ a: 'bbww',
99
+ b: 'bwbw',
100
+ c: 'bwwb',
101
+ d: 'wbbw',
102
+ e: 'wbwb',
103
+ f: 'wwbb',
104
+ }
105
+
106
+ // Return the ANSI codes to display a 3x1.5 block tile, one line at a time
107
+ const tileTty = (tile, top) => {
108
+ // Top: true for first line, false for second
109
+ // quick access to tile color and its opposite in upper case
110
+ const colors = { b: 'b', w: 'w', B: 'w', W: 'b' }
111
+ const save = set
112
+ const tentative = tile !== tile.toLowerCase()
113
+ if (tentative) set = colorSets.tentative
114
+ tile = tile.toLowerCase()
115
+ if (!(tile in tiles)) {
116
+ // Winning paths are represented by adding 6 to the tile char code
117
+ tile = String.fromCodePoint(tile.codePointAt(0) - 6)
118
+ if (tile in tiles) {
119
+ colors.w = 'W'
120
+ colors.B = 'W'
121
+ } else {
122
+ // And another six for a black win
123
+ tile = String.fromCodePoint(tile.codePointAt(0) - 6)
124
+ colors.b = 'B'
125
+ colors.W = 'B'
126
+ }
127
+ }
128
+
129
+ let value = false
130
+ if (tile in tiles) {
131
+ tile = tiles[tile]
132
+ if (top) {
133
+ value = uptty(colors[tile[0]])
134
+ } else {
135
+ const centerChar =
136
+ tile[1] === tile[2] ? rt : tile[2] === tile[3] ? lt : up
137
+ const centertty = dntty(
138
+ colors[tile[2]],
139
+ colors[tile[2].toUpperCase()],
140
+ centerChar,
141
+ )
142
+ value =
143
+ horiztty(colors[tile[3]]) + centertty + horiztty(colors[tile[1]]) + sp()
144
+ }
145
+ }
146
+
147
+ set = save
148
+ return value || (top ? '??? ' : "''' ")
149
+ }
150
+
151
+ const mergeTentative = () => {
152
+ colorSets.tentative.e = set.e // Copy empty color to tentative set
153
+ colorSets.tentative.W = set.W
154
+ colorSets.tentative.B = set.B
155
+ }
156
+
157
+ const printIcon = (icon, setName) => {
158
+ set = colorSets[setName] || colorSets.trax
159
+ mergeTentative(set)
160
+ const on = esc + '38;5;' + String(set.t) + ';48;5;' + String(set.e) + 'm'
161
+ let o1 = ' '
162
+ let o2 = '1 '
163
+ let row = 1
164
+ let width = 1
165
+ const info = []
166
+ let lineWidth = 0
167
+ while (icon.length > 0) {
168
+ const c = icon[0]
169
+ icon = icon.slice(1)
170
+ if (c === ':') {
171
+ info.push({ o: o1, w: lineWidth }, { o: o2, w: lineWidth })
172
+ row++
173
+ width = Math.max(width, lineWidth)
174
+ lineWidth = 0
175
+ o1 = ' '
176
+ o2 = String(row).padEnd(3)
177
+ } else if (c >= '0' && c <= '9') {
178
+ const empty = Number(c) + 1
179
+ o1 += ' '.repeat(empty * 4)
180
+ o2 += ' '.repeat(empty * 4)
181
+ lineWidth += empty
182
+ } else {
183
+ o1 += tileTty(c, 'top')
184
+ o2 += tileTty(c)
185
+ lineWidth++
186
+ }
187
+ }
188
+
189
+ if (o1.length > 0) {
190
+ info.push({ o: o1, w: lineWidth }, { o: o2, w: lineWidth })
191
+ width = Math.max(width, lineWidth)
192
+ }
193
+
194
+ const columns = process.stdout.columns || 0
195
+ const rows = process.stdout.rows || 0
196
+ if (width * 4 < columns - 4 && info.length < rows - 2) {
197
+ let colOut = ' '
198
+ for (let col = 1; col <= width; col++) {
199
+ colOut += ' ' + Trax.encodeCol(col).padEnd(3)
200
+ }
201
+
202
+ console.info(on + colOut.padEnd((width + 1) * 4) + off)
203
+ for (const line of info) {
204
+ console.info(on + line.o + ''.padEnd((width - line.w) * 4) + ' ' + off)
205
+ }
206
+
207
+ console.info(on + ''.padEnd((width + 1) * 4) + off)
208
+ } else {
209
+ console.warn('Console is too small to display this game')
210
+ }
211
+ }
212
+
213
+ // Flexible way to get a Trax game object
214
+ // game = existing Trax instance, or a string indicating the variant name
215
+ // moves = optional list or string of moves to be made on the game
216
+ // returns a Trax instance
217
+ export const traxGame = (game = 'trax', moves = '') => {
218
+ let trax
219
+ if (game instanceof Trax) {
220
+ trax = game
221
+ if (moves) {
222
+ if (typeof moves === 'string') moves = moves.split(' ')
223
+ for (const move of moves) trax.dropTile(move)
224
+ }
225
+ } else {
226
+ trax = new Trax(game, moves, 'cli')
227
+ }
228
+
229
+ return trax
230
+ }
231
+
232
+ const printNotation = (trax, tentative) => {
233
+ let b = false
234
+ let n = 1
235
+ let out = ''
236
+ let length = 0
237
+ const t = tentative ? trax.moves.length : 0
238
+ for (const move of trax.moves) {
239
+ const c = (b ? 'b' : 'w') + (t === n ? 'd' : '')
240
+ const notation = String(n) + ' ' + move
241
+ if (length + notation.length + 3 > process.stdout.columns) {
242
+ out += '\n'
243
+ length = 0
244
+ }
245
+
246
+ out += bubble(c, notation) + ' '
247
+ length += notation.length + 3
248
+ b = !b
249
+ n++
250
+ }
251
+
252
+ outty(out)
253
+ }
254
+
255
+ export const display = (game, players, tentative = false) => {
256
+ const trax = traxGame(game)
257
+ const saved = trax.save()
258
+ const colors = ['white', 'black']
259
+ if (!Array.isArray(players)) players = colors
260
+ let { icon } = trax
261
+ if (tentative) {
262
+ const drops = trax.dropTile(tentative)
263
+ if (drops.valid || drops.dropped.length > 0) {
264
+ icon = trax.dropsIcon(drops)
265
+ } else {
266
+ tentative = false
267
+ }
268
+ }
269
+
270
+ printNotation(trax, tentative)
271
+ printIcon(icon, trax.rules)
272
+ const nameColor = { 1: 'w', 2: 'b' }[trax.turn]
273
+ if (tentative) {
274
+ const otherColor = { w: 'bd', b: 'wd' }[nameColor] || 'wd'
275
+ outty('---' + bubble(otherColor, 'tentative position') + off + '---')
276
+ }
277
+
278
+ const name = players[trax.turn - 1] || colors[trax.turn - 1]
279
+ if (trax.over) {
280
+ if (trax.turn) {
281
+ outty(bubble(nameColor + 'h', name) + off + ' wins')
282
+ } else {
283
+ outty('game end' + (tentative ? 's' : 'ed') + ' in a draw')
284
+ }
285
+ } else if (trax.turn) {
286
+ outty(
287
+ (tentative ? 'leaving ' : '') +
288
+ bubble(nameColor, name) +
289
+ off +
290
+ ' to play',
291
+ )
292
+ }
293
+
294
+ if (tentative) trax.restore(saved)
295
+ }
package/src/version.js ADDED
@@ -0,0 +1,2 @@
1
+ // Generated by genversion.
2
+ export const version = '0.8.0'
package/.eslintrc.yaml DELETED
@@ -1,68 +0,0 @@
1
- root: true
2
-
3
- extends:
4
- - eslint:recommended
5
-
6
- parserOptions:
7
- sourceType: module
8
-
9
- env:
10
- node: true
11
- es2021: true
12
-
13
- globals:
14
- crypto: readonly
15
-
16
- rules:
17
- curly:
18
- - error
19
- - multi-line
20
- - consistent
21
- dot-location:
22
- - error
23
- - property
24
- dot-notation: warn
25
- eqeqeq:
26
- - warn
27
- - smart
28
- guard-for-in: warn
29
- indent:
30
- - warn
31
- - 2
32
- - MemberExpression: 0
33
- SwitchCase: 1
34
- linebreak-style:
35
- - error
36
- - unix
37
- max-len:
38
- - warn
39
- - code: 99
40
- tabWidth: 2
41
- no-alert: error
42
- no-caller: error
43
- no-div-regex: warn
44
- no-else-return:
45
- - error
46
- - allowElseIf: false
47
- no-eval: error
48
- no-extra-parens: warn
49
- no-invalid-this: error
50
- no-new: error
51
- no-unused-vars:
52
- - warn
53
- - vars: all
54
- args: all
55
- caughtErrors: all
56
- no-with: error
57
- quotes:
58
- - warn
59
- - single
60
- - avoidEscape: true
61
- allowTemplateLiterals: true
62
- semi:
63
- - error
64
- - never
65
- - beforeStatementContinuationChars: always
66
- wrap-iife:
67
- - error
68
- - inside