@slugbugblue/trax-cli 0.13.0 → 0.14.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 +6 -0
- package/README.md +1 -1
- package/package.json +2 -2
- package/src/cmds/import-export.js +76 -48
- package/src/cmds/list.js +33 -25
- package/src/cmds/new.js +6 -0
- package/src/cmds/play-try.js +19 -9
- package/src/version.js +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# @slugbugblue/trax
|
|
2
2
|
|
|
3
|
+
## 0.14.0 - 2026-06-05
|
|
4
|
+
|
|
5
|
+
- Update to latest puzzles version with new defensive move format
|
|
6
|
+
- Change trax game export attribution
|
|
7
|
+
- Break out inner logic to new functions in a few complex functions
|
|
8
|
+
|
|
3
9
|
## 0.13.0 - 2026-05-20
|
|
4
10
|
|
|
5
11
|
- Update to latest puzzles version and add defensive moves
|
package/README.md
CHANGED
|
@@ -77,7 +77,7 @@ simply `#1` at the interactive prompt.
|
|
|
77
77
|
For ease of working at the command line, the symbols used as the last character
|
|
78
78
|
of the Trax notation can optionally be replaced with letters. Use `s` for slash
|
|
79
79
|
symbol (`/`), `b` for backslash (`\`), and `p` for plus (`+`). While there is no
|
|
80
|
-
need to escape at the interactive prompt, these substitutions will be
|
|
80
|
+
need to escape at the interactive prompt, these substitutions will be available
|
|
81
81
|
there as well.
|
|
82
82
|
|
|
83
83
|
### Examples
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@slugbugblue/trax-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.0",
|
|
4
4
|
"description": "Trax command line interface",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"trax",
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
"dependencies": {
|
|
33
33
|
"@slugbugblue/trax": "^1.0.0",
|
|
34
34
|
"@slugbugblue/trax-analyst": "^0.14.0",
|
|
35
|
-
"@slugbugblue/trax-puzzles": "^0.
|
|
35
|
+
"@slugbugblue/trax-puzzles": "^0.9.0",
|
|
36
36
|
"@slugbugblue/trax-tty": "^1.0.0",
|
|
37
37
|
"env-paths": "^4.0.0",
|
|
38
38
|
"yaml": "^2.0.0-11"
|
|
@@ -169,6 +169,31 @@ export const importCmd = {
|
|
|
169
169
|
},
|
|
170
170
|
}
|
|
171
171
|
|
|
172
|
+
/** Generate puzzle attribution comment lines for export.
|
|
173
|
+
* @param {CLIContext['GAME']} game - the current game
|
|
174
|
+
* @returns {string} - formatted attribution string, or empty if no puzzle
|
|
175
|
+
*/
|
|
176
|
+
const puzzleAttribution = (game) => {
|
|
177
|
+
if (!game.puzzle) return ''
|
|
178
|
+
const puzzle = puzzles.find((p) => p.id === game.puzzle)
|
|
179
|
+
if (!puzzle) return ''
|
|
180
|
+
const source = sources[puzzle.src]
|
|
181
|
+
let attr = '; Puzzle ' + game.puzzle
|
|
182
|
+
if (source) {
|
|
183
|
+
attr += source.copyright
|
|
184
|
+
? ' ©' + source.copyright + ' by '
|
|
185
|
+
: ' provided courtesy of '
|
|
186
|
+
attr += source.name
|
|
187
|
+
if (source.url) attr += '\n; ' + source.url
|
|
188
|
+
if (source.license) {
|
|
189
|
+
attr += '\n; Licensed under ' + source.license
|
|
190
|
+
attr += source.licenseUrl ? ' ' + source.licenseUrl : ''
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return attr + '\n'
|
|
195
|
+
}
|
|
196
|
+
|
|
172
197
|
export const exportCmd = {
|
|
173
198
|
name: 'export',
|
|
174
199
|
alt: ['save', 'keep', 'share'],
|
|
@@ -202,28 +227,8 @@ export const exportCmd = {
|
|
|
202
227
|
content += game.players?.[0] || 'white'
|
|
203
228
|
content += ' vs '
|
|
204
229
|
content += game.players?.[1] || 'black'
|
|
205
|
-
content += '\n; @slugbugblue/trax
|
|
206
|
-
|
|
207
|
-
const puzzle = puzzles.find((p) => p.id === game.puzzle)
|
|
208
|
-
if (puzzle) {
|
|
209
|
-
const source = sources[puzzle.src]
|
|
210
|
-
content += '; Puzzle ' + game.puzzle
|
|
211
|
-
if (source) {
|
|
212
|
-
content += source.copyright
|
|
213
|
-
? ' ©' + source.copyright + ' by '
|
|
214
|
-
: ' provided courtesy of '
|
|
215
|
-
content += source.name
|
|
216
|
-
if (source.url) content += '\n; ' + source.url
|
|
217
|
-
if (source.license) {
|
|
218
|
-
content += '\n; Licensed under ' + source.license
|
|
219
|
-
content += source.licenseUrl ? ' ' + source.licenseUrl : ''
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
content += '\n'
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
|
|
230
|
+
content += '\n; @slugbugblue/trax-cli v' + CLI.version + '\n'
|
|
231
|
+
content += puzzleAttribution(game)
|
|
227
232
|
content += interleaveNotation(trax.moves, game.notes)
|
|
228
233
|
|
|
229
234
|
filename &&= filename.replaceAll(/[^ a-z\d.~\/]/gv, '')
|
|
@@ -250,21 +255,48 @@ export const exportCmd = {
|
|
|
250
255
|
const getRules = (r) =>
|
|
251
256
|
r.includes('loop') ? 'traxloop' : r.includes('8x8') ? 'trax8' : 'trax'
|
|
252
257
|
|
|
253
|
-
/**
|
|
254
|
-
* @param {
|
|
258
|
+
/** Apply pending moves to the game, or record them as notes if the game is over.
|
|
259
|
+
* @param {string[]} moves - pending move tokens (mutated in place)
|
|
260
|
+
* @param {Trax} trax - the current game instance
|
|
261
|
+
* @param {GameNotes} notes - notes accumulator
|
|
262
|
+
*/
|
|
263
|
+
const applyMoves = (moves, trax, notes) => {
|
|
264
|
+
if (trax.gameOver) {
|
|
265
|
+
notes.push({ note: moves.map((c) => c.trim()).join(' '), move: trax.move })
|
|
266
|
+
} else {
|
|
267
|
+
trax.playMoves(moves.filter(Boolean))
|
|
268
|
+
moves.splice(0)
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/** Record a comment segment as a note, unless it is the generator attribution.
|
|
273
|
+
* @param {string[]} comment - comment tokens from after the # or ; delimiter
|
|
274
|
+
* @param {Trax | undefined} trax - the current game instance, if initialized
|
|
275
|
+
* @param {GameNotes} notes - notes accumulator
|
|
276
|
+
*/
|
|
277
|
+
const collectComment = (comment, trax, notes) => {
|
|
278
|
+
if (comment.length === 0) return
|
|
279
|
+
const note = comment.map((c) => c.trim()).join(' ')
|
|
280
|
+
if (!/^@slugbugblue\/trax-cli v/v.test(note)) {
|
|
281
|
+
notes.push({ note, move: trax?.move || 0 })
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/** Parse a .trx formatted string into structured game data.
|
|
255
286
|
* @param {string} content - the contents of the .trx file
|
|
256
|
-
* @returns {
|
|
287
|
+
* @returns {{ rules: TraxVariant, players: string[], trax: Trax, notes: GameNotes } | null} - parsed game data, or null if invalid
|
|
257
288
|
*/
|
|
258
|
-
const
|
|
289
|
+
const parseFile = (content) => {
|
|
259
290
|
/** @type {TraxVariant | undefined} */
|
|
260
291
|
let rules
|
|
292
|
+
/** @type {Trax | undefined} */
|
|
261
293
|
let trax
|
|
262
294
|
/** @type {string[]} */
|
|
263
295
|
let players = []
|
|
264
296
|
/** @type {string[]} */
|
|
265
297
|
const moves = []
|
|
266
298
|
/** @type {GameNotes} */
|
|
267
|
-
const
|
|
299
|
+
const notes = []
|
|
268
300
|
for (const line of content.split('\n')) {
|
|
269
301
|
if (!line) continue
|
|
270
302
|
const [ln, ...comment] = line.split(/[#;]/v)
|
|
@@ -284,33 +316,29 @@ const interpretFile = (CLI, content) => {
|
|
|
284
316
|
}
|
|
285
317
|
|
|
286
318
|
if (moves.length > 0) {
|
|
287
|
-
if (!trax) return
|
|
288
|
-
|
|
289
|
-
comments.push({
|
|
290
|
-
note: moves.map((c) => c.trim()).join(' '),
|
|
291
|
-
move: trax.move,
|
|
292
|
-
})
|
|
293
|
-
} else {
|
|
294
|
-
trax.playMoves(moves.filter(Boolean))
|
|
295
|
-
moves.splice(0)
|
|
296
|
-
}
|
|
319
|
+
if (!trax) return null
|
|
320
|
+
applyMoves(moves, trax, notes)
|
|
297
321
|
}
|
|
298
322
|
|
|
299
|
-
|
|
300
|
-
const note = comment.map((c) => c.trim()).join(' ')
|
|
301
|
-
if (!/^@slugbugblue\/trax cli\.js v/v.test(note)) {
|
|
302
|
-
comments.push({
|
|
303
|
-
note,
|
|
304
|
-
move: trax?.move || 0,
|
|
305
|
-
})
|
|
306
|
-
}
|
|
307
|
-
}
|
|
323
|
+
collectComment(comment, trax, notes)
|
|
308
324
|
}
|
|
309
325
|
|
|
310
|
-
if (!trax) return
|
|
326
|
+
if (!trax || !rules) return null
|
|
327
|
+
return { rules, players, trax, notes }
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/** Decode a .trx formatted string into a game, preserving players and comments.
|
|
331
|
+
* @param {CLIContext} CLI - the CLI object
|
|
332
|
+
* @param {string} content - the contents of the .trx file
|
|
333
|
+
* @returns {boolean} - true if a game was created
|
|
334
|
+
*/
|
|
335
|
+
const interpretFile = (CLI, content) => {
|
|
336
|
+
const parsed = parseFile(content)
|
|
337
|
+
if (!parsed) return false
|
|
311
338
|
|
|
339
|
+
const { rules, players, trax, notes } = parsed
|
|
312
340
|
CLI.do('new', rules, ...players)
|
|
313
|
-
CLI.GAMES[String(CLI.GAME.id)].notes =
|
|
341
|
+
CLI.GAMES[String(CLI.GAME.id)].notes = notes
|
|
314
342
|
if (trax.moves.length > 0) {
|
|
315
343
|
CLI.do('play', ...trax.moves)
|
|
316
344
|
} else {
|
package/src/cmds/list.js
CHANGED
|
@@ -49,7 +49,6 @@ export const listCmd = {
|
|
|
49
49
|
p1: 1,
|
|
50
50
|
p2: 1,
|
|
51
51
|
}
|
|
52
|
-
let noteSize = Number.POSITIVE_INFINITY
|
|
53
52
|
|
|
54
53
|
for (const game of Object.values(CLI.GAMES)) {
|
|
55
54
|
const moves = game.moves.length === 0 ? 0 : game.moves.split(' ').length
|
|
@@ -70,32 +69,41 @@ export const listCmd = {
|
|
|
70
69
|
}
|
|
71
70
|
|
|
72
71
|
list = listSort(list, filters)
|
|
72
|
+
renderList(CLI, list, size)
|
|
73
|
+
},
|
|
74
|
+
}
|
|
73
75
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
76
|
+
/**
|
|
77
|
+
* @param {CLIContext} CLI - the CLI object
|
|
78
|
+
* @param {Array<Record<string, string | number | boolean | undefined>>} list - sorted game list
|
|
79
|
+
* @param {Record<string, number>} size - initial column width minimums
|
|
80
|
+
*/
|
|
81
|
+
const renderList = (CLI, list, size) => {
|
|
82
|
+
let noteSize = Number.POSITIVE_INFINITY
|
|
83
|
+
for (const game of list) {
|
|
84
|
+
size.id = Math.max(size.id, String(game.id).length)
|
|
85
|
+
size.name = Math.max(size.name, Number(game.name))
|
|
86
|
+
size.moves = Math.max(size.moves, String(game.moves).length)
|
|
87
|
+
size.p1 = Math.max(size.p1, String(game.p1).length)
|
|
88
|
+
size.p2 = Math.max(size.p2, String(game.p2).length)
|
|
89
|
+
noteSize = Math.min(noteSize, leftover(size))
|
|
90
|
+
}
|
|
82
91
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
},
|
|
92
|
+
for (const game of list) {
|
|
93
|
+
CLI.out(
|
|
94
|
+
CLI.color(String(game.id).padStart(size.id) + ' ') +
|
|
95
|
+
Trax.names[
|
|
96
|
+
/** @type {import('../cli.js').TraxVariant} */ (String(game.rules))
|
|
97
|
+
] +
|
|
98
|
+
' '.repeat(size.name - Number(game.name) + 1) +
|
|
99
|
+
CLI.bubble(game.turn === 1 ? 'wh' : 'w', String(game.p1)) +
|
|
100
|
+
CLI.color('vs '.padStart(size.p1 - String(game.p1).length + 4)) +
|
|
101
|
+
CLI.bubble(game.turn === 2 ? 'bh' : 'b', String(game.p2)) +
|
|
102
|
+
CLI.color(' '.padStart(size.p2 - String(game.p2).length + 1)) +
|
|
103
|
+
CLI.color(String(game.moves).padEnd(size.moves)) +
|
|
104
|
+
CLI.color.help(' ' + String(game.note).slice(0, noteSize)),
|
|
105
|
+
)
|
|
106
|
+
}
|
|
99
107
|
}
|
|
100
108
|
|
|
101
109
|
/** @param {string} a @param {string} b @returns {number} */
|
package/src/cmds/new.js
CHANGED
|
@@ -68,6 +68,7 @@ const pairs = [
|
|
|
68
68
|
['Aang', 'Katara'],
|
|
69
69
|
['Abbott', 'Costello'],
|
|
70
70
|
['Adam', 'Eve'],
|
|
71
|
+
['Ali', 'Frazier'],
|
|
71
72
|
['Anne Boleyn', 'Henry VIII'],
|
|
72
73
|
['Ash', 'Pikachu'],
|
|
73
74
|
['Barbie', 'Ken'],
|
|
@@ -80,12 +81,15 @@ const pairs = [
|
|
|
80
81
|
['Chip', 'Dale'],
|
|
81
82
|
['Donald', 'Daisy'],
|
|
82
83
|
['Depp', 'Heard'],
|
|
84
|
+
['Dorothy', 'Toto'],
|
|
83
85
|
['Fred', 'Barney'],
|
|
84
86
|
['Fred', 'George'],
|
|
87
|
+
['Frida', 'Diego'],
|
|
85
88
|
['Frodo', 'Samwise'],
|
|
86
89
|
['Hamilton', 'Burr'],
|
|
87
90
|
['Han', 'Chewie'],
|
|
88
91
|
['Harry', 'Ron'],
|
|
92
|
+
['Hatfield', 'McCoy'],
|
|
89
93
|
['Holmes', 'Moriarty'],
|
|
90
94
|
['Holmes', 'Watson'],
|
|
91
95
|
['Iñigo', 'Fezzik'],
|
|
@@ -109,6 +113,7 @@ const pairs = [
|
|
|
109
113
|
['Pan', 'Hook'],
|
|
110
114
|
['Phineas', 'Ferb'],
|
|
111
115
|
['player 1', 'player 2'],
|
|
116
|
+
['p1', 'p2'],
|
|
112
117
|
['Potter', 'Voldemort'],
|
|
113
118
|
['R2-D2', 'C-3PO'],
|
|
114
119
|
['Ren', 'Stimpy'],
|
|
@@ -121,6 +126,7 @@ const pairs = [
|
|
|
121
126
|
['Snoopy', 'Woodstock'],
|
|
122
127
|
['Sonny', 'Cher'],
|
|
123
128
|
['Spongebob', 'Patrick'],
|
|
129
|
+
['Sully', 'Neytiri'],
|
|
124
130
|
['Tarzan', 'Jane'],
|
|
125
131
|
['Thelma', 'Louise'],
|
|
126
132
|
['Thing 1', 'Thing 2'],
|
package/src/cmds/play-try.js
CHANGED
|
@@ -149,18 +149,24 @@ const matchesToken = (move, token) => {
|
|
|
149
149
|
|
|
150
150
|
/**
|
|
151
151
|
* Find the defense move for the current puzzle state, if one exists.
|
|
152
|
+
* First checks position-code keys (exact board-state match, O(1)), then
|
|
153
|
+
* falls back to wildcard notation keys for paths containing '*'.
|
|
152
154
|
* @param {import('@slugbugblue/trax-puzzles').Defense} defense
|
|
155
|
+
* @param {string} positionCode
|
|
153
156
|
* @param {string[]} moves
|
|
154
157
|
* @returns {string | undefined}
|
|
155
158
|
*/
|
|
156
|
-
const getDefenseMove = (defense, moves) => {
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
tokens
|
|
162
|
-
|
|
163
|
-
|
|
159
|
+
const getDefenseMove = (defense, positionCode, moves) => {
|
|
160
|
+
if (Object.hasOwn(defense, positionCode)) return defense[positionCode]
|
|
161
|
+
const key = Object.keys(defense)
|
|
162
|
+
.filter((k) => k.includes('*'))
|
|
163
|
+
.find((k) => {
|
|
164
|
+
const tokens = k.split(' ')
|
|
165
|
+
return (
|
|
166
|
+
tokens.length === moves.length &&
|
|
167
|
+
tokens.every((token, i) => matchesToken(moves[i], token))
|
|
168
|
+
)
|
|
169
|
+
})
|
|
164
170
|
return key ? defense[key] : undefined
|
|
165
171
|
}
|
|
166
172
|
|
|
@@ -194,7 +200,11 @@ const bot = (CLI) => {
|
|
|
194
200
|
const puzzle = puzzles.find((p) => p.id === game.puzzle)
|
|
195
201
|
if (puzzle?.defense) {
|
|
196
202
|
const offset = puzzle.notation.split(' ').length
|
|
197
|
-
move = getDefenseMove(
|
|
203
|
+
move = getDefenseMove(
|
|
204
|
+
puzzle.defense,
|
|
205
|
+
trax.positionCode(),
|
|
206
|
+
trax.moves.slice(offset),
|
|
207
|
+
)
|
|
198
208
|
}
|
|
199
209
|
}
|
|
200
210
|
|
package/src/version.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// Generated by genversion.
|
|
2
|
-
export const version = '0.
|
|
2
|
+
export const version = '0.14.0'
|