@slugbugblue/trax-cli 0.12.0 → 0.13.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/.claude/settings.local.json +8 -0
- package/CHANGELOG.md +11 -0
- package/LICENSE +1 -1
- package/README.md +1 -1
- package/eslint.config.js +24 -0
- package/package.json +7 -18
- package/src/cli.js +246 -106
- package/src/cmds/analyze.js +72 -66
- package/src/cmds/delete.js +35 -43
- package/src/cmds/help.js +54 -57
- package/src/cmds/import-export.js +149 -122
- package/src/cmds/index.js +40 -0
- package/src/cmds/list.js +92 -87
- package/src/cmds/new.js +50 -55
- package/src/cmds/notes.js +20 -18
- package/src/cmds/play-try.js +147 -106
- package/src/cmds/puzzles.js +53 -39
- package/src/cmds/select.js +22 -31
- package/src/cmds/suggest.js +28 -37
- package/src/cmds/undo.js +31 -26
- package/src/cmds/view.js +42 -51
- package/src/utils.js +1 -1
- package/src/version.js +1 -1
- package/xo.config.js +10 -0
- package/src/types.d.ts +0 -33
package/src/cmds/analyze.js
CHANGED
|
@@ -1,24 +1,22 @@
|
|
|
1
|
-
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
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.
|
|
1
|
+
/** CLI analyze command
|
|
2
|
+
* @copyright 2022-2026
|
|
3
|
+
* @author Chad Transtrum <chad@transtrum.net>
|
|
4
|
+
* @license Apache-2.0
|
|
14
5
|
*/
|
|
15
6
|
|
|
16
|
-
|
|
7
|
+
/** @typedef {import('../cli.js').CLIContext} CLIContext */
|
|
17
8
|
|
|
18
9
|
import { analyze } from '@slugbugblue/trax-analyst'
|
|
19
10
|
import { Trax } from '@slugbugblue/trax'
|
|
20
11
|
|
|
21
|
-
const notationRx = /^[@a-z]+\d+[bps
|
|
12
|
+
const notationRx = /^[@a-z]+\d+[bps\/\\+]$/iv
|
|
13
|
+
|
|
14
|
+
/** @type {Record<string, string>} */
|
|
15
|
+
const threatNames = {
|
|
16
|
+
0: 'corners',
|
|
17
|
+
1: 'attacks',
|
|
18
|
+
2: 'Ls',
|
|
19
|
+
}
|
|
22
20
|
|
|
23
21
|
export const analyzeCmd = {
|
|
24
22
|
name: 'analyze',
|
|
@@ -29,14 +27,64 @@ export const analyzeCmd = {
|
|
|
29
27
|
'Analyze the current position of the currently selected game, or pass in a',
|
|
30
28
|
'move to analyze the result of that move.',
|
|
31
29
|
],
|
|
32
|
-
}
|
|
30
|
+
/** @param {CLIContext} CLI @param {string} [move] */
|
|
31
|
+
fn(CLI, move) {
|
|
32
|
+
if (!String(CLI.GAME?.id)) {
|
|
33
|
+
return CLI.error('No active game. Type "new" to start a game.')
|
|
34
|
+
}
|
|
33
35
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
36
|
+
let trax = CLI.TRAX
|
|
37
|
+
|
|
38
|
+
const detailed =
|
|
39
|
+
typeof move === 'string' &&
|
|
40
|
+
('detail'.startsWith(move) || 'debug'.startsWith(move))
|
|
41
|
+
|
|
42
|
+
if (move && notationRx.test(move)) {
|
|
43
|
+
move = CLI.fixNotation(move)
|
|
44
|
+
CLI.do('try', move)
|
|
45
|
+
trax = new Trax(trax.rules, trax.moves, 'cli')
|
|
46
|
+
const play = trax.dropTile(move)
|
|
47
|
+
if (!play.valid || trax.over) return
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (trax.over) {
|
|
51
|
+
return CLI.error('Game has ended.')
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const { edge, threats, scores, faulty } = analyze(trax)
|
|
55
|
+
|
|
56
|
+
if (detailed) CLI.out(CLI.color.white(edge.w))
|
|
57
|
+
CLI.out(
|
|
58
|
+
CLI.bubble('w', scores.w) +
|
|
59
|
+
CLI.color.white(' ') +
|
|
60
|
+
listThreats(threats.w, detailed),
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
if (detailed) {
|
|
64
|
+
if (faulty.w.length > 0) {
|
|
65
|
+
CLI.out(CLI.color.white('') + listFaulty(faulty.w))
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
CLI.out(CLI.color.black(edge.b))
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
CLI.out(
|
|
72
|
+
CLI.bubble('b', scores.b) +
|
|
73
|
+
CLI.color.black(' ') +
|
|
74
|
+
listThreats(threats.b, detailed),
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
if (detailed && faulty.b.length > 0) {
|
|
78
|
+
CLI.out(CLI.color.black('') + listFaulty(faulty.b))
|
|
79
|
+
}
|
|
80
|
+
},
|
|
38
81
|
}
|
|
39
82
|
|
|
83
|
+
/**
|
|
84
|
+
* @param {Record<string, any[]>} threats
|
|
85
|
+
* @param {boolean} detailed
|
|
86
|
+
* @returns {string}
|
|
87
|
+
*/
|
|
40
88
|
const listThreats = (threats, detailed) => {
|
|
41
89
|
const summary = []
|
|
42
90
|
for (const level of Object.keys(threats || {})) {
|
|
@@ -54,6 +102,10 @@ const listThreats = (threats, detailed) => {
|
|
|
54
102
|
return summary.join(' ')
|
|
55
103
|
}
|
|
56
104
|
|
|
105
|
+
/**
|
|
106
|
+
* @param {any[]} faulty
|
|
107
|
+
* @returns {string}
|
|
108
|
+
*/
|
|
57
109
|
const listFaulty = (faulty) => {
|
|
58
110
|
const summary = []
|
|
59
111
|
for (const threat of faulty) {
|
|
@@ -64,49 +116,3 @@ const listFaulty = (faulty) => {
|
|
|
64
116
|
|
|
65
117
|
return 'Faulty: ' + summary.join(' ')
|
|
66
118
|
}
|
|
67
|
-
|
|
68
|
-
analyzeCmd.fn = (CLI, move) => {
|
|
69
|
-
if (!String(CLI.GAME?.id)) {
|
|
70
|
-
return CLI.error('No active game. Type "new" to start a game.')
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
let trax = CLI.TRAX
|
|
74
|
-
|
|
75
|
-
const detailed = 'detail'.startsWith(move) || 'debug'.startsWith(move)
|
|
76
|
-
|
|
77
|
-
if (notationRx.test(move)) {
|
|
78
|
-
move = CLI.fixNotation(move)
|
|
79
|
-
CLI.do('try', move)
|
|
80
|
-
trax = new Trax(trax.rules, trax.moves, 'cli')
|
|
81
|
-
const play = trax.dropTile(move)
|
|
82
|
-
if (!play.valid || trax.over) return
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
if (trax.over) {
|
|
86
|
-
return CLI.error('Game has ended.')
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
const { edge, threats, scores, faulty } = analyze(trax)
|
|
90
|
-
|
|
91
|
-
if (detailed) CLI.out(CLI.color.white(edge.w))
|
|
92
|
-
CLI.out(
|
|
93
|
-
CLI.bubble('w', scores.w) +
|
|
94
|
-
CLI.color.white(' ') +
|
|
95
|
-
listThreats(threats.w, detailed),
|
|
96
|
-
)
|
|
97
|
-
|
|
98
|
-
if (detailed) {
|
|
99
|
-
if (faulty.w.length > 0) CLI.out(CLI.color.white('') + listFaulty(faulty.w))
|
|
100
|
-
CLI.out(CLI.color.black(edge.b))
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
CLI.out(
|
|
104
|
-
CLI.bubble('b', scores.b) +
|
|
105
|
-
CLI.color.black(' ') +
|
|
106
|
-
listThreats(threats.b, detailed),
|
|
107
|
-
)
|
|
108
|
-
|
|
109
|
-
if (detailed && faulty.b.length > 0) {
|
|
110
|
-
CLI.out(CLI.color.black('') + listFaulty(faulty.b))
|
|
111
|
-
}
|
|
112
|
-
}
|
package/src/cmds/delete.js
CHANGED
|
@@ -1,19 +1,10 @@
|
|
|
1
|
-
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
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.
|
|
1
|
+
/** CLI delete command
|
|
2
|
+
* @copyright 2022-2026
|
|
3
|
+
* @author Chad Transtrum <chad@transtrum.net>
|
|
4
|
+
* @license Apache-2.0
|
|
14
5
|
*/
|
|
15
6
|
|
|
16
|
-
|
|
7
|
+
/** @typedef {import('../cli.js').CLIContext} CLIContext */
|
|
17
8
|
|
|
18
9
|
export const deleteCmd = {
|
|
19
10
|
name: 'delete',
|
|
@@ -25,11 +16,40 @@ export const deleteCmd = {
|
|
|
25
16
|
'Delete the current game. Or specify a game number to delete that game.',
|
|
26
17
|
'If the game is in progress, you must type "force" to delete it.',
|
|
27
18
|
],
|
|
19
|
+
/** @param {CLIContext} CLI @param {string} [id] @param {string} [force] */
|
|
20
|
+
fn(CLI, id, force = 'x') {
|
|
21
|
+
const current = String(CLI.GAME.id)
|
|
22
|
+
id ||= current
|
|
23
|
+
if ('force'.startsWith(id)) {
|
|
24
|
+
id = current
|
|
25
|
+
force = 'force'
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (id.startsWith('#')) id = id.slice(1)
|
|
29
|
+
|
|
30
|
+
const game = CLI.GAMES[id]
|
|
31
|
+
|
|
32
|
+
if (!game || !String(game.id)) {
|
|
33
|
+
return CLI.error('Invalid id. Type "list" to see available games.')
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (game.over || game.moves === '' || 'force'.startsWith(force)) {
|
|
37
|
+
CLI.delete(id)
|
|
38
|
+
CLI.out(CLI.color('Deleted game #' + id + '.'))
|
|
39
|
+
|
|
40
|
+
if (id === String(current)) {
|
|
41
|
+
selectAnyGame(CLI)
|
|
42
|
+
}
|
|
43
|
+
} else {
|
|
44
|
+
CLI.error('Game #' + id + ' is still active. Use "force" to delete it.')
|
|
45
|
+
}
|
|
46
|
+
},
|
|
28
47
|
}
|
|
29
48
|
|
|
49
|
+
/** @param {CLIContext} CLI */
|
|
30
50
|
const selectAnyGame = (CLI) => {
|
|
31
51
|
if (CLI.GAMES) {
|
|
32
|
-
const games = Object.values(CLI.GAMES).
|
|
52
|
+
const games = Object.values(CLI.GAMES).toSorted((a, b) => {
|
|
33
53
|
// Active games first
|
|
34
54
|
if (a.over && !b.over) return 1
|
|
35
55
|
if (b.over && !a.over) return -1
|
|
@@ -41,31 +61,3 @@ const selectAnyGame = (CLI) => {
|
|
|
41
61
|
}
|
|
42
62
|
}
|
|
43
63
|
}
|
|
44
|
-
|
|
45
|
-
deleteCmd.fn = (CLI, id, force = 'x') => {
|
|
46
|
-
const current = String(CLI.GAME.id)
|
|
47
|
-
id ||= current
|
|
48
|
-
if ('force'.startsWith(id)) {
|
|
49
|
-
id = current
|
|
50
|
-
force = 'force'
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
if (id.startsWith('#')) id = id.slice(1)
|
|
54
|
-
|
|
55
|
-
const game = CLI.GAMES[id]
|
|
56
|
-
|
|
57
|
-
if (!game || !String(game.id)) {
|
|
58
|
-
return CLI.error('Invalid id. Type "list" to see available games.')
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
if (game.over || game.moves === '' || 'force'.startsWith(force)) {
|
|
62
|
-
CLI.delete(id)
|
|
63
|
-
CLI.out(CLI.color('Deleted game #' + id + '.'))
|
|
64
|
-
|
|
65
|
-
if (id === String(current)) {
|
|
66
|
-
selectAnyGame(CLI)
|
|
67
|
-
}
|
|
68
|
-
} else {
|
|
69
|
-
CLI.error('Game #' + id + ' is still active. Use "force" to delete it.')
|
|
70
|
-
}
|
|
71
|
-
}
|
package/src/cmds/help.js
CHANGED
|
@@ -1,19 +1,10 @@
|
|
|
1
|
-
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
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.
|
|
1
|
+
/** CLI help command
|
|
2
|
+
* @copyright 2022-2026
|
|
3
|
+
* @author Chad Transtrum <chad@transtrum.net>
|
|
4
|
+
* @license Apache-2.0
|
|
14
5
|
*/
|
|
15
6
|
|
|
16
|
-
|
|
7
|
+
/** @typedef {import('../cli.js').CLIContext} CLIContext */
|
|
17
8
|
|
|
18
9
|
export const helpCmd = {
|
|
19
10
|
name: 'help',
|
|
@@ -26,56 +17,62 @@ export const helpCmd = {
|
|
|
26
17
|
'Type "help <command>" or "<command> help" to see additional information',
|
|
27
18
|
'for a specific command.',
|
|
28
19
|
],
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
//
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
cmds = CLI.commands.filter(
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
20
|
+
/** @param {CLIContext} CLI @param {string} [word] */
|
|
21
|
+
fn(CLI, word) {
|
|
22
|
+
// If we are looking for help on a particular commands, get that command
|
|
23
|
+
const resolved = CLI.resolveCommand(word)
|
|
24
|
+
const resolvedWord =
|
|
25
|
+
typeof resolved === 'string'
|
|
26
|
+
? resolved
|
|
27
|
+
: word
|
|
28
|
+
? String(word).toLowerCase()
|
|
29
|
+
: ''
|
|
30
|
+
let cmds = CLI.commands.filter(
|
|
31
|
+
(c) => !resolvedWord || c.startsWith(resolvedWord),
|
|
32
|
+
)
|
|
33
|
+
if (cmds.length === 0) {
|
|
34
|
+
cmds = CLI.commands.filter((c) => c.includes(resolvedWord))
|
|
35
|
+
}
|
|
44
36
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
let max = 1
|
|
48
|
-
const commands = {}
|
|
49
|
-
for (const key of cmds) {
|
|
50
|
-
commands[key] = CLI.cmd(key)
|
|
51
|
-
max = Math.max(max, key.length + length(commands[key].args))
|
|
52
|
-
}
|
|
37
|
+
// If we don't have a few commands, select all commands
|
|
38
|
+
if (cmds.length === 0) cmds = CLI.commands
|
|
53
39
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
const
|
|
59
|
-
|
|
60
|
-
CLI.
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
}
|
|
40
|
+
// Load all the commands we need to print into a local variable
|
|
41
|
+
// and also use the loop to get the length of the longest args
|
|
42
|
+
let max = 1
|
|
43
|
+
/** @type {Record<string, ReturnType<CLIContext['cmd']>>} */
|
|
44
|
+
const commands = {}
|
|
45
|
+
for (const key of cmds) {
|
|
46
|
+
commands[key] = CLI.cmd(key)
|
|
47
|
+
max = Math.max(max, key.length + printLength(commands[key].args))
|
|
48
|
+
}
|
|
64
49
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
50
|
+
// Print a summary of all matching commands
|
|
51
|
+
for (const key of cmds.toSorted()) {
|
|
52
|
+
const topic = commands[key]
|
|
53
|
+
if (topic.hide && !CLI.repl) continue // Hidden commands only in repl
|
|
54
|
+
const space = ' '.repeat(5 + max - printLength(topic.args) - key.length)
|
|
55
|
+
CLI.out(
|
|
56
|
+
CLI.color.command(` ${key} ${topic.args}`) +
|
|
57
|
+
CLI.color.short(space + topic.desc),
|
|
58
|
+
)
|
|
70
59
|
}
|
|
71
60
|
|
|
72
|
-
|
|
73
|
-
if (
|
|
74
|
-
|
|
75
|
-
|
|
61
|
+
// And if we have just one command, print details on that command
|
|
62
|
+
if (cmds.length === 1) {
|
|
63
|
+
const topic = commands[cmds[0]]
|
|
64
|
+
for (const alias of topic.alt) {
|
|
65
|
+
CLI.out(CLI.color.command(` ${alias}`))
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
CLI.out('')
|
|
76
69
|
for (const line of topic.help) {
|
|
77
70
|
CLI.out(' ' + CLI.color.help(line))
|
|
78
71
|
}
|
|
79
72
|
}
|
|
80
|
-
}
|
|
73
|
+
},
|
|
81
74
|
}
|
|
75
|
+
|
|
76
|
+
// Find the length of the printable characters of the string
|
|
77
|
+
/** @param {string} text @returns {number} */
|
|
78
|
+
const printLength = (text) => text.replaceAll(/[<">]/gv, '').length
|