@slugbugblue/trax 0.12.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 CHANGED
@@ -1,5 +1,30 @@
1
1
  # @slugbugblue/trax
2
2
 
3
+ ## 0.14.0 - 2022-12-05
4
+
5
+ - Added `puzzles` to the CLI:
6
+
7
+ - `trax puzzles ls`: see all the puzzles
8
+ - `trax puzzle new`: start a new puzzle
9
+
10
+ - Bot tests now include solving all tutorial and easy puzzles. As the bot
11
+ improves, we will add more puzzles to the tests.
12
+
13
+ ## 0.13.0 - 2022-11-25
14
+
15
+ - Add move suggestions to `analyst.js` for CLI and bot use
16
+
17
+ - use `suggest(trax)` to get a suggestion for the current position
18
+ - added the `suggest` CLI command for quick access to a random suggestion
19
+
20
+ - Use font glyphs for text bubbles in `tty.js` only if one of the following
21
+ environment variables are defined and non-empty:
22
+
23
+ - `NERDFONT`, `POWERLINE`, `P9K_TTY`, `P9K_SSH`
24
+
25
+ - Begin work on documenting the javascript files using jsdoc, with the initial
26
+ focus on types so typescript-powered autocomplete can be more useful
27
+
3
28
  ## 0.12.0 - 2022-10-28
4
29
 
5
30
  - `point.in()` now accepts any two opposing corners to define the bounding box
package/README.md CHANGED
@@ -5,8 +5,8 @@ Trax game-playing engine and etc.
5
5
  ## Description
6
6
 
7
7
  Trax is a two-player boardless board game invented by David Smith. For more
8
- information about Trax, including its rules and history, see the official
9
- website at [traxgame.com][traxgame].
8
+ information about Trax, including its [rules][traxrules] and
9
+ [history][traxhistory], see the official website at [traxgame.com][traxgame].
10
10
 
11
11
  This goal of this javascript project is to allow Trax to be played
12
12
  programmatically. It can be used as an engine in a web browser, for example at
@@ -95,7 +95,9 @@ Examples:
95
95
 
96
96
  ## API Usage
97
97
 
98
- The main Trax engine is provided as `engine.js`, which can be used as follows:
98
+ ### Trax engine
99
+
100
+ The main Trax engine is provided in `engine.js`, which can be used as follows:
99
101
 
100
102
  ```javascript
101
103
  import { Trax } from '@slugbugblue/trax'
@@ -115,12 +117,44 @@ let puzzle = new Trax('trax', '@0/ a0\\ @2/')
115
117
  puzzle.play('@2\\')
116
118
  ```
117
119
 
118
- For specifics, see the [engine.js documentation][engine-docs].
120
+ For specifics, see the [engine.js documentation][docs-engine].
121
+
122
+ ### Analysis
123
+
124
+ The analysis engine is provided in `analyst.js`, and it can be used as follows:
125
+
126
+ ```javascript
127
+ import { Trax } from '@slugbugblue/trax'
128
+ import { analyze } from '@slugbugblue/trax/analyst'
129
+
130
+ let trax = new Trax('trax', '@0/ @1\\')
131
+
132
+ let analysis = analyze(trax)
133
+
134
+ console.log('Edge analysis:', analysis.edge)
135
+ console.log('Threat analysis:', analysis.threats)
136
+ console.log('Score analysis:', analysis.scores)
137
+ ```
138
+
139
+ For additional information, refer to the [analyst.js
140
+ documentation][docs-analyst].
141
+
142
+ ### Threats database
143
+
144
+ The threats database is currently hand-coded in `threats.js`, so refer to that
145
+ file if you want to try to update the analysis engine's understanding of
146
+ threats.
147
+
148
+ ### Puzzles database
149
+
150
+ The puzzles database is provided in `puzzles.js`, and so far just contains
151
+ puzzles that I've been using to test the analysis engine, along with the list of
152
+ publicly available puzzles at [the traxgame.com puzzles page][traxpuzzles].
119
153
 
120
154
  ## Roadmap
121
155
 
122
- - Game position analysis
123
- - Puzzles and Puzzlebot
156
+ - Improve the threats database / analysis engine.
157
+ - Add more puzzles.
124
158
 
125
159
  ## Support
126
160
 
@@ -174,12 +208,16 @@ David Smith and heirs, and are not to be used without permission.
174
208
 
175
209
  [ctrans]: mailto:chad@transtrum.net
176
210
  [dgb]: mailto:donald@traxgame.com
177
- [engine-docs]: docs/engine.md
211
+ [docs-engine]: docs/engine.md
212
+ [docs-analyst]: docs/analyst.md
178
213
  [env-path]: https://www.npmjs.com/package/env-paths
179
214
  [goldtoken]: https://goldtoken.com/
180
215
  [repo]: https://gitlab.com/slugbugblue/trax
181
216
  [sbb]: https://slugbugblue.com/
182
217
  [traxbook]: http://traxgame.com/shop_book.php
183
218
  [traxgame]: http://traxgame.com/
219
+ [traxhistory]: http://www.traxgame.com/about_history.php
220
+ [traxpuzzles]: http://www.traxgame.com/games_puzzles.php
221
+ [traxrules]: http://www.traxgame.com/about_rules.php
184
222
  [xdg]:
185
223
  https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html
package/docs/analyst.md CHANGED
@@ -7,7 +7,7 @@ CLI and for bots.
7
7
 
8
8
  ```javascript
9
9
  import { Trax } from '@slugbugblue/trax'
10
- import { analyze } from '@slugbugblue/trax/analyst'
10
+ import { analyze, suggest } from '@slugbugblue/trax/analyst'
11
11
 
12
12
  const trax = new Trax('trax', '@0/ @1/ B0\\')
13
13
 
@@ -16,6 +16,12 @@ const analysis = analyze(trax)
16
16
  console.log(JSON.stringify(analysis.edge, null, 2))
17
17
  console.log(JSON.stringify(analysis.threats, null, 2))
18
18
  console.log(JSON.stringify(analysis.scores, null, 2))
19
+
20
+ const suggestion = suggest(trax)
21
+
22
+ console.log(JSON.stringify(suggestion.options, null, 2))
23
+
24
+ trax.play(suggestion.pick.move)
19
25
  ```
20
26
 
21
27
  ## API
@@ -107,7 +113,7 @@ simple count of the threats at each level can give a good first guess at the
107
113
  "score" of the current position. Each element of the array is an object with the
108
114
  following keys:
109
115
 
110
- - `pattern`: The threat pattern as it was fed into the threats database. This is
116
+ - `threat`: The threat pattern as it was fed into the threats database. This is
111
117
  a simple edge string.
112
118
  - `match`: The portion of the actual edge string that matched the pattern, which
113
119
  may or may not be exactly the same as the pattern.
@@ -144,6 +150,42 @@ For any given location, determine the type of playable space. Returns one of:
144
150
  - `C`: This location is in a cave, and certain plays at this position result in
145
151
  illegal moves.
146
152
 
153
+ ### Suggestion class
154
+
155
+ Accessible from `suggest(game)`, this class is returned with several useful
156
+ pieces of information.
157
+
158
+ #### properties
159
+
160
+ ##### `all`
161
+
162
+ A list of all the moves analyzed, ordered by highest scoring move first.
163
+
164
+ Each move is an object with the following properties:
165
+
166
+ - `move`: the notation of the move
167
+ - `score`: the score of the move
168
+ - `analysis`: the analysis object of the position for this move
169
+
170
+ ##### `options`
171
+
172
+ A smaller subset of the `all` list, with only the moves that are worth
173
+ considering.
174
+
175
+ ##### `pick`
176
+
177
+ A random selection of one of the `options`.
178
+
179
+ ##### `ms`
180
+
181
+ The number of milliseconds spent performing the analyses required to create the
182
+ suggestion.
183
+
184
+ #### computed properties
185
+
186
+ - `best`: quick access to the top-scoring move
187
+ - `analyzed`: quick access to a count of the number of moves analyzed
188
+
147
189
  ## License
148
190
 
149
191
  Copyright 2019-2022 Chad Transtrum
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@slugbugblue/trax",
3
- "version": "0.12.0",
3
+ "version": "0.14.0",
4
4
  "description": "Trax game engine and friends",
5
5
  "keywords": [
6
6
  "trax",
@@ -11,16 +11,15 @@
11
11
  "bugs": "https://gitlab.com/slugbugblue/trax/issues",
12
12
  "license": "Apache-2.0",
13
13
  "author": "Chad Transtrum <chad@transtrum.net>",
14
+ "types": "./src/types.d.ts",
14
15
  "exports": {
15
16
  ".": "./src/engine.js",
16
- "./analyst": "./src/analyst.js",
17
- "./analyst.js": "./src/analyst.js",
18
17
  "./point": "./src/point.js",
19
- "./point.js": "./src/point.js",
18
+ "./analyst": "./src/analyst.js",
19
+ "./puzzles": "./src/puzzles.js",
20
20
  "./threats": "./src/threats.js",
21
- "./threats.js": "./src/threats.js",
22
21
  "./tty": "./src/tty.js",
23
- "./tty.js": "./src/tty.js"
22
+ "./version": "./src/version.js"
24
23
  },
25
24
  "repository": "gitlab:slugbugblue/trax",
26
25
  "bin": {
@@ -38,7 +37,7 @@
38
37
  "yaml": "^2.0.0-11"
39
38
  },
40
39
  "devDependencies": {
41
- "ava": "^4.1.0",
40
+ "ava": "^5.1.0",
42
41
  "c8": "^7.11.0",
43
42
  "genversion": "^3.0.2",
44
43
  "prettier": "^2.6.1",
package/src/analyst.js CHANGED
@@ -15,10 +15,10 @@
15
15
 
16
16
  // Analyst module: provides analysis functions for Trax games
17
17
 
18
- import { Point } from '@slugbugblue/trax/point.js'
18
+ import { Point } from '@slugbugblue/trax/point'
19
19
  import { Trax } from '@slugbugblue/trax'
20
20
 
21
- import { threats } from '@slugbugblue/trax/threats.js'
21
+ import { threats } from '@slugbugblue/trax/threats'
22
22
 
23
23
  // Helpers
24
24
  const id = () => {
@@ -31,7 +31,17 @@ const id = () => {
31
31
  return 'analysis-' + code
32
32
  }
33
33
 
34
- // Make parseInt Array.map()-friendly
34
+ // A sort function for hashes with a numeric score property
35
+ const byScoreDesc = (a, b) => b.score - a.score
36
+
37
+ // A filter function factory to remove scores not within x points of the highest score
38
+ const withinX = (highest, tolerance) => (s) =>
39
+ s.score === highest || highest - s.score <= tolerance
40
+
41
+ // Return a random element of an array
42
+ const randomDraw = (array) => array[Math.floor(Math.random() * array.length)]
43
+
44
+ // Make parseInt a little shorter
35
45
  const int = (s) => Number.parseInt(s, 10)
36
46
 
37
47
  const leftTurn = { d: 'r', l: 'd', r: 'u', u: 'l' }
@@ -160,18 +170,42 @@ const label = (rawEdge) => {
160
170
  return final
161
171
  }
162
172
 
173
+ const threatValues = (threatsArray) => {
174
+ return threatsArray.length
175
+ // Hm. Maybe we don't need this ...
176
+ // let count = 0
177
+ // for (const threat of threatsArray) count += threat.value
178
+ // return count
179
+ }
180
+
163
181
  const attackScore = (from, level) => {
164
- const count = from[level]?.length ?? 0
182
+ const count = threatValues(from[level] || [])
165
183
  if (level < 1) return count
166
184
  return count * 10 ** (threats.maxDepth - level + 1)
167
185
  }
168
186
 
169
- // Shortcut to get the number of active threats
170
- const threatCount = (from) =>
171
- Object.entries(from).filter(([d, l]) => d > 0 && l.length > 0).length
187
+ // Shortcut to get the number of attacks
188
+ const attackCount = (from) => threatValues(from['1'] || [])
189
+
190
+ // Shortcut to get the number of passive threats
191
+ const threatCount = (from) => {
192
+ let count = 0
193
+ for (const [depth, array] of Object.entries(from)) {
194
+ count += int(depth) > 1 ? threatValues(array) : 0
195
+ }
196
+
197
+ return count
198
+ }
199
+
200
+ const passiveAttack = (from) =>
201
+ attackCount(from) > 1 || (attackCount(from) === 1 && threatCount(from) > 0)
202
+
203
+ // Maybe we don't need this one
204
+ // const underAttack = (from) => attackCount(from) + threatCount(from) > 0
172
205
 
173
206
  // Analysis class
174
207
  export const Analysis = class {
208
+ /** @type TraxVariant */
175
209
  #rules = 'trax'
176
210
  #id = 'analysis'
177
211
  #moves = []
@@ -255,17 +289,14 @@ export const Analysis = class {
255
289
  const inactive = this.threats[Trax.other(game.color)]
256
290
  let advantage = Number.POSITIVE_INFINITY
257
291
 
258
- // First examine the player who has the initiative
292
+ // First examine the active player
259
293
  for (const level of Object.keys(active).map((i) => int(i))) {
260
294
  if (level > 0 && level < advantage) advantage = level
261
295
  score += attackScore(active, level)
262
296
  }
263
297
 
264
298
  const passive = game.color !== color
265
- const calc =
266
- !passive ||
267
- threatCount(this.threats[color]) > 1 ||
268
- threatCount(this.threats[Trax.other(color)]) > 0
299
+ const calc = !passive || passiveAttack(inactive)
269
300
 
270
301
  // Calculate the effects of the passive player first
271
302
  for (const level of Object.keys(inactive).map((i) => int(i))) {
@@ -406,6 +437,7 @@ export const Analysis = class {
406
437
  threat: threat.pattern,
407
438
  match: match[0],
408
439
  at: match.index,
440
+ value: threat.value,
409
441
  })
410
442
  }
411
443
  }
@@ -449,3 +481,56 @@ export const Analysis = class {
449
481
  }
450
482
 
451
483
  export const analyze = (game) => new Analysis(game)
484
+
485
+ export const Suggestion = class {
486
+ all = []
487
+ options = []
488
+ pick = {}
489
+ ms = 0
490
+
491
+ constructor(analyzed, ms) {
492
+ this.all = analyzed.sort(byScoreDesc)
493
+ const best = analyzed[0] || { move: '', score: 0 }
494
+ this.options = analyzed.filter(withinX(best.score, 5))
495
+ this.pick = randomDraw(this.options) || best
496
+ this.ms = ms
497
+ }
498
+
499
+ get best() {
500
+ return this.all[0]
501
+ }
502
+
503
+ get analyzed() {
504
+ return this.all.length
505
+ }
506
+
507
+ // Make a nice representation for the nodejs REPL
508
+ [Symbol.for('nodejs.util.inspect.custom')](_, options) {
509
+ const style = options.stylize
510
+ let out = style(this.pick.move, 'string')
511
+ out += ' ' + style(this.pick.score, 'number') + ' ['
512
+ out += this.options
513
+ .slice(0, 5)
514
+ .map((s) => style(s.move, 'string') + ' ' + style(s.score, 'number'))
515
+ .join(', ')
516
+ out += '] ' + this.analyzed + ' in ' + this.ms + 'ms'
517
+ return out
518
+ }
519
+ }
520
+
521
+ export const suggest = (game) => {
522
+ const start = Date.now()
523
+ const analyzed = {}
524
+ const { color, moves, rules } = game
525
+ for (const move of game.possibleMoves()) {
526
+ const trax = new Trax(rules, moves)
527
+ trax.play(move)
528
+ const unique = trax.normalized
529
+ if (!analyzed[unique]) {
530
+ const analysis = analyze(trax)
531
+ analyzed[unique] = { move, score: analysis.scores[color], analysis }
532
+ }
533
+ }
534
+
535
+ return new Suggestion(Object.values(analyzed), Date.now() - start)
536
+ }
package/src/cli.js CHANGED
@@ -21,9 +21,9 @@ import repl from 'node:repl'
21
21
 
22
22
  import YAML from 'yaml'
23
23
 
24
- import { Trax } from './engine.js'
25
- import { version } from './version.js'
26
- import * as tty from './tty.js'
24
+ import { Trax } from '@slugbugblue/trax'
25
+ import { version } from '@slugbugblue/trax/version'
26
+ import * as tty from '@slugbugblue/trax/tty'
27
27
 
28
28
  // Load all the commands as individual plugins
29
29
  import { analyzeCmd } from './cmds/analyze.js'
@@ -39,7 +39,9 @@ import {
39
39
  import { listCmd } from './cmds/list.js'
40
40
  import { newCmd } from './cmds/new.js'
41
41
  import { playCmd, tryCmd } from './cmds/play-try.js'
42
+ import { puzzlesCmd } from './cmds/puzzles.js'
42
43
  import { selectCmd } from './cmds/select.js'
44
+ import { suggestCmd } from './cmds/suggest.js'
43
45
  import { undoCmd } from './cmds/undo.js'
44
46
  import { viewCmd } from './cmds/view.js'
45
47
 
@@ -86,8 +88,10 @@ const plugins = [
86
88
  listCmd,
87
89
  newCmd,
88
90
  playCmd,
91
+ puzzlesCmd,
89
92
  quitCmd,
90
93
  selectCmd,
94
+ suggestCmd,
91
95
  tryCmd,
92
96
  undoCmd,
93
97
  versionCmd,
@@ -111,7 +115,10 @@ const newId = (DATA) => {
111
115
  return String(min - 1)
112
116
  }
113
117
 
114
- // Colorize text according to simplistic and not very complete patterns
118
+ /** Colorize text according to simplistic and not very complete patterns.
119
+ * @type Colorer
120
+ */
121
+ // @ts-ignore
115
122
  const color = (text, def) => {
116
123
  let out = ''
117
124
  let quote = false
@@ -121,7 +128,7 @@ const color = (text, def) => {
121
128
  const short = (t) => tty.color(t, CLI.COLORS.short) + c()
122
129
  for (const char of text) {
123
130
  switch (char) {
124
- case '"':
131
+ case '"': {
125
132
  if (quote) {
126
133
  colors.pop()
127
134
  } else {
@@ -131,31 +138,45 @@ const color = (text, def) => {
131
138
  out += c('')
132
139
  quote = !quote
133
140
  break
134
- case '#':
141
+ }
142
+
143
+ case '#': {
135
144
  id = true
136
145
  colors.push(CLI.COLORS.id)
137
146
  out += c('#')
138
147
  break
139
- case '|':
148
+ }
149
+
150
+ case '|': {
140
151
  out += short('|')
141
152
  break
142
- case '[':
153
+ }
154
+
155
+ case '[': {
143
156
  colors.push(CLI.COLORS.optional)
144
157
  out += short('[')
145
158
  break
146
- case ']':
159
+ }
160
+
161
+ case ']': {
147
162
  colors.pop()
148
163
  out += short(']')
149
164
  break
150
- case '<':
165
+ }
166
+
167
+ case '<': {
151
168
  colors.push(CLI.COLORS.variable)
152
169
  out += c('')
153
170
  break
154
- case '>':
171
+ }
172
+
173
+ case '>': {
155
174
  colors.pop()
156
175
  out += c('')
157
176
  break
158
- default:
177
+ }
178
+
179
+ default: {
159
180
  if (id && !/[\da-z]/.test(char)) {
160
181
  id = false
161
182
  colors.pop()
@@ -163,6 +184,7 @@ const color = (text, def) => {
163
184
  } else {
164
185
  out += char
165
186
  }
187
+ }
166
188
  }
167
189
  }
168
190
 
@@ -170,8 +192,8 @@ const color = (text, def) => {
170
192
  }
171
193
 
172
194
  // Internal representations of files
173
- const CONFIG = { id: 0 }
174
- const DATA = { games: {} }
195
+ const CONFIG = { id: '', puzzleLevel: 0 }
196
+ const DATA = { games: {}, puzzles: {} }
175
197
 
176
198
  // CLI context object for plugins
177
199
  const CLI = {
@@ -195,6 +217,10 @@ const CLI = {
195
217
  get ID() {
196
218
  return CONFIG.id
197
219
  },
220
+ // The current puzzle level
221
+ get puzzleLevel() {
222
+ return CONFIG.puzzleLevel
223
+ },
198
224
 
199
225
  // Provide access to the current Trax object
200
226
  TRAX: new Trax(),
@@ -211,6 +237,7 @@ const CLI = {
211
237
  variable: 123,
212
238
  optional: 110,
213
239
  short: 153,
240
+ success: 35,
214
241
  white: 250,
215
242
  },
216
243
 
@@ -278,11 +305,57 @@ const CLI = {
278
305
  return id
279
306
  },
280
307
 
308
+ newPuzzle(puzzle) {
309
+ // Starts a new puzzle
310
+ const id = newId(DATA)
311
+ const trax = new Trax(puzzle.game, puzzle.notation, 'cli')
312
+ const players = ['challenger', 'puzzlebot']
313
+ DATA.games[id] = {
314
+ id,
315
+ puzzle: puzzle.id,
316
+ rules: trax.rules,
317
+ name: trax.name,
318
+ turn: trax.turn,
319
+ max: puzzle.max,
320
+ players: trax.turn === 1 ? players : players.reverse(),
321
+ moves: trax.moves.join(' '),
322
+ }
323
+
324
+ if (DATA.puzzles[puzzle.id]) {
325
+ DATA.puzzles[puzzle.id].attempts += 1
326
+ } else {
327
+ DATA.puzzles[puzzle.id] = { attempts: 1, solved: 0 }
328
+ }
329
+
330
+ CONFIG.id = id
331
+ CONFIG.puzzleLevel = puzzle.level
332
+ CLI.TRAX = trax
333
+
334
+ saveConfig()
335
+ saveData()
336
+
337
+ return id
338
+ },
339
+
281
340
  out(text) {
282
341
  // Prints to stdout
283
342
  console.info(text + tty.color(''))
284
343
  },
285
344
 
345
+ plural(n, noun, nouns) {
346
+ return String(n) + ' ' + (Math.abs(n) === 1 ? noun : nouns || noun + 's')
347
+ },
348
+
349
+ puzzle(id) {
350
+ return DATA.puzzles?.[id] || { attempts: 0, solved: 0 }
351
+ },
352
+
353
+ puzzleSolved(id) {
354
+ DATA.puzzles = DATA.puzzles || {}
355
+ DATA.puzzles[id] = DATA.puzzles[id] || { attempts: 1, solved: 0 }
356
+ DATA.puzzles[id].solved += 1
357
+ },
358
+
286
359
  resolveCommand(word) {
287
360
  // Determine if an entered word is a valid command.
288
361
  // Returns the command if a single match is found, an array if multiple
@@ -550,11 +623,34 @@ const completer = (line) => {
550
623
  return [completions, match]
551
624
  }
552
625
 
626
+ const ambiguousSuggestions = (cmd) => {
627
+ const maybe = new Set()
628
+ for (const alias of cmd) {
629
+ if (alias.startsWith(CLI.CMD)) maybe.add(alias)
630
+
631
+ let alts = commands[alias].alt
632
+ alts = is(alts, 'arr') ? alts : [alts || '']
633
+
634
+ for (const alt of alts) {
635
+ if (alt.startsWith(CLI.CMD)) maybe.add(alt)
636
+ }
637
+ }
638
+
639
+ return maybe
640
+ }
641
+
553
642
  const evaluate = async (command) => {
554
643
  const [CMD, ...words] = command.split(/\s+/).filter(Boolean)
555
- const cmdLower = CMD.toLowerCase()
644
+ const cmdLower = CMD?.toLowerCase() || ''
556
645
  CLI.CMD = cmdLower
557
- const cmd = abbreviations[cmdLower]
646
+ let cmd = abbreviations[cmdLower]
647
+
648
+ // If we are not in a REPL, let's remove hidden commands
649
+ if (!CLI.repl && is(cmd, 'arr')) {
650
+ cmd = cmd.filter((c) => !commands[c].hide)
651
+ if (cmd.length === 1) cmd = cmd[0]
652
+ }
653
+
558
654
  if (is(cmd, 'str')) {
559
655
  if (words.length > 0 && abbreviations[words[0].toLowerCase()] === 'help') {
560
656
  commands.help.fn(CLI, cmd)
@@ -563,18 +659,7 @@ const evaluate = async (command) => {
563
659
  }
564
660
  } else if (is(cmd, 'arr')) {
565
661
  CLI.error('Ambiguous command. Perhaps you meant one of the following:')
566
- const maybe = new Set()
567
-
568
- for (const alias of cmd) {
569
- if (alias.startsWith(cmdLower)) maybe.add(alias)
570
-
571
- let alts = commands[alias].alt
572
- alts = is(alts, 'arr') ? alts : [alts || '']
573
-
574
- for (const alt of alts) {
575
- if (alt.startsWith(cmdLower)) maybe.add(alt)
576
- }
577
- }
662
+ const maybe = ambiguousSuggestions(cmd)
578
663
 
579
664
  if (maybe.size > 0) {
580
665
  for (const alias of [...maybe].sort()) {
@@ -593,8 +678,6 @@ const evaluate = async (command) => {
593
678
  CLI.error('Unknown command. Type "help" for a list of valid commands.')
594
679
  }
595
680
  }
596
-
597
- return undefined
598
681
  }
599
682
 
600
683
  const main = async () => {
@@ -630,4 +713,4 @@ const main = async () => {
630
713
  }
631
714
  }
632
715
 
633
- main()
716
+ await main()
@@ -15,8 +15,8 @@
15
15
 
16
16
  // CLI analyze command
17
17
 
18
- import { analyze } from '../analyst.js'
19
- import { Trax } from '../engine.js'
18
+ import { analyze } from '@slugbugblue/trax/analyst'
19
+ import { Trax } from '@slugbugblue/trax'
20
20
 
21
21
  const notationRx = /^[@a-z]+\d+[bps/\\+]$/i
22
22