@slugbugblue/trax 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/CHANGELOG.md +15 -0
- package/README.md +46 -7
- package/docs/analyst.md +44 -2
- package/package.json +6 -7
- package/src/analyst.js +96 -12
- package/src/cli.js +60 -30
- package/src/cmds/analyze.js +2 -2
- package/src/cmds/import-export.js +1 -1
- package/src/cmds/list.js +2 -2
- package/src/cmds/new.js +5 -3
- package/src/cmds/suggest.js +52 -0
- package/src/engine.js +167 -29
- package/src/point.js +54 -12
- package/src/puzzles.js +798 -0
- package/src/threats.js +31 -32
- package/src/tty.js +30 -15
- package/src/types.d.ts +105 -0
- package/src/version.js +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
# @slugbugblue/trax
|
|
2
2
|
|
|
3
|
+
## 0.13.0 - 2022-11-25
|
|
4
|
+
|
|
5
|
+
- Add move suggestions to `analyst.js` for CLI and bot use
|
|
6
|
+
|
|
7
|
+
- use `suggest(trax)` to get a suggestion for the current position
|
|
8
|
+
- added the `suggest` CLI command for quick access to a random suggestion
|
|
9
|
+
|
|
10
|
+
- Use font glyphs for text bubbles in `tty.js` only if one of the following
|
|
11
|
+
environment variables are defined and non-empty:
|
|
12
|
+
|
|
13
|
+
- `NERDFONT`, `POWERLINE`, `P9K_TTY`, `P9K_SSH`
|
|
14
|
+
|
|
15
|
+
- Begin work on documenting the javascript files using jsdoc, with the initial
|
|
16
|
+
focus on types so typescript-powered autocomplete can be more useful
|
|
17
|
+
|
|
3
18
|
## 0.12.0 - 2022-10-28
|
|
4
19
|
|
|
5
20
|
- `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
|
|
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
|
-
|
|
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,45 @@ 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
|
|
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
|
-
-
|
|
123
|
-
-
|
|
156
|
+
- Improve the threats database / analysis engine.
|
|
157
|
+
- Add a puzzle interface to the CLI.
|
|
158
|
+
- Add more puzzles.
|
|
124
159
|
|
|
125
160
|
## Support
|
|
126
161
|
|
|
@@ -174,12 +209,16 @@ David Smith and heirs, and are not to be used without permission.
|
|
|
174
209
|
|
|
175
210
|
[ctrans]: mailto:chad@transtrum.net
|
|
176
211
|
[dgb]: mailto:donald@traxgame.com
|
|
177
|
-
[engine
|
|
212
|
+
[docs-engine]: docs/engine.md
|
|
213
|
+
[docs-analyst]: docs/analyst.md
|
|
178
214
|
[env-path]: https://www.npmjs.com/package/env-paths
|
|
179
215
|
[goldtoken]: https://goldtoken.com/
|
|
180
216
|
[repo]: https://gitlab.com/slugbugblue/trax
|
|
181
217
|
[sbb]: https://slugbugblue.com/
|
|
182
218
|
[traxbook]: http://traxgame.com/shop_book.php
|
|
183
219
|
[traxgame]: http://traxgame.com/
|
|
220
|
+
[traxhistory]: http://www.traxgame.com/about_history.php
|
|
221
|
+
[traxpuzzles]: http://www.traxgame.com/games_puzzles.php
|
|
222
|
+
[traxrules]: http://www.traxgame.com/about_rules.php
|
|
184
223
|
[xdg]:
|
|
185
224
|
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
|
-
- `
|
|
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.
|
|
3
|
+
"version": "0.13.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
|
-
"./
|
|
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
|
-
"./
|
|
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": "^
|
|
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
|
|
18
|
+
import { Point } from '@slugbugblue/trax/point'
|
|
19
19
|
import { Trax } from '@slugbugblue/trax'
|
|
20
20
|
|
|
21
|
-
import { threats } from '@slugbugblue/trax/threats
|
|
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
|
-
//
|
|
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,41 @@ 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]
|
|
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
|
|
170
|
-
const
|
|
171
|
-
|
|
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
|
+
const underAttack = (from) => attackCount(from) + threatCount(from) > 0
|
|
172
204
|
|
|
173
205
|
// Analysis class
|
|
174
206
|
export const Analysis = class {
|
|
207
|
+
/** @type TraxVariant */
|
|
175
208
|
#rules = 'trax'
|
|
176
209
|
#id = 'analysis'
|
|
177
210
|
#moves = []
|
|
@@ -255,17 +288,14 @@ export const Analysis = class {
|
|
|
255
288
|
const inactive = this.threats[Trax.other(game.color)]
|
|
256
289
|
let advantage = Number.POSITIVE_INFINITY
|
|
257
290
|
|
|
258
|
-
// First examine the player
|
|
291
|
+
// First examine the active player
|
|
259
292
|
for (const level of Object.keys(active).map((i) => int(i))) {
|
|
260
293
|
if (level > 0 && level < advantage) advantage = level
|
|
261
294
|
score += attackScore(active, level)
|
|
262
295
|
}
|
|
263
296
|
|
|
264
297
|
const passive = game.color !== color
|
|
265
|
-
const calc =
|
|
266
|
-
!passive ||
|
|
267
|
-
threatCount(this.threats[color]) > 1 ||
|
|
268
|
-
threatCount(this.threats[Trax.other(color)]) > 0
|
|
298
|
+
const calc = !passive || passiveAttack(inactive) || underAttack(active)
|
|
269
299
|
|
|
270
300
|
// Calculate the effects of the passive player first
|
|
271
301
|
for (const level of Object.keys(inactive).map((i) => int(i))) {
|
|
@@ -406,6 +436,7 @@ export const Analysis = class {
|
|
|
406
436
|
threat: threat.pattern,
|
|
407
437
|
match: match[0],
|
|
408
438
|
at: match.index,
|
|
439
|
+
value: threat.value,
|
|
409
440
|
})
|
|
410
441
|
}
|
|
411
442
|
}
|
|
@@ -449,3 +480,56 @@ export const Analysis = class {
|
|
|
449
480
|
}
|
|
450
481
|
|
|
451
482
|
export const analyze = (game) => new Analysis(game)
|
|
483
|
+
|
|
484
|
+
export const Suggestion = class {
|
|
485
|
+
all = []
|
|
486
|
+
options = []
|
|
487
|
+
pick = {}
|
|
488
|
+
ms = 0
|
|
489
|
+
|
|
490
|
+
constructor(analyzed, ms) {
|
|
491
|
+
this.all = analyzed.sort(byScoreDesc)
|
|
492
|
+
const best = analyzed[0] || { move: '', score: 0 }
|
|
493
|
+
this.options = analyzed.filter(withinX(best.score, 5))
|
|
494
|
+
this.pick = randomDraw(this.options) || best
|
|
495
|
+
this.ms = ms
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
get best() {
|
|
499
|
+
return this.all[0]
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
get analyzed() {
|
|
503
|
+
return this.all.length
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
// Make a nice representation for the nodejs REPL
|
|
507
|
+
[Symbol.for('nodejs.util.inspect.custom')](_, options) {
|
|
508
|
+
const style = options.stylize
|
|
509
|
+
let out = style(this.pick.move, 'string')
|
|
510
|
+
out += ' ' + style(this.pick.score, 'number') + ' ['
|
|
511
|
+
out += this.options
|
|
512
|
+
.slice(0, 5)
|
|
513
|
+
.map((s) => style(s.move, 'string') + ' ' + style(s.score, 'number'))
|
|
514
|
+
.join(', ')
|
|
515
|
+
out += '] ' + this.analyzed + ' in ' + this.ms + 'ms'
|
|
516
|
+
return out
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
export const suggest = (game) => {
|
|
521
|
+
const start = Date.now()
|
|
522
|
+
const analyzed = {}
|
|
523
|
+
const { color, moves, rules } = game
|
|
524
|
+
for (const move of game.possibleMoves()) {
|
|
525
|
+
const trax = new Trax(rules, moves)
|
|
526
|
+
trax.play(move)
|
|
527
|
+
const unique = trax.normalized
|
|
528
|
+
if (!analyzed[unique]) {
|
|
529
|
+
const analysis = analyze(trax)
|
|
530
|
+
analyzed[unique] = { move, score: analysis.scores[color], analysis }
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
return new Suggestion(Object.values(analyzed), Date.now() - start)
|
|
535
|
+
}
|
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 '
|
|
25
|
-
import { version } from '
|
|
26
|
-
import * as tty from '
|
|
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'
|
|
@@ -40,6 +40,7 @@ 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
42
|
import { selectCmd } from './cmds/select.js'
|
|
43
|
+
import { suggestCmd } from './cmds/suggest.js'
|
|
43
44
|
import { undoCmd } from './cmds/undo.js'
|
|
44
45
|
import { viewCmd } from './cmds/view.js'
|
|
45
46
|
|
|
@@ -88,6 +89,7 @@ const plugins = [
|
|
|
88
89
|
playCmd,
|
|
89
90
|
quitCmd,
|
|
90
91
|
selectCmd,
|
|
92
|
+
suggestCmd,
|
|
91
93
|
tryCmd,
|
|
92
94
|
undoCmd,
|
|
93
95
|
versionCmd,
|
|
@@ -111,7 +113,10 @@ const newId = (DATA) => {
|
|
|
111
113
|
return String(min - 1)
|
|
112
114
|
}
|
|
113
115
|
|
|
114
|
-
|
|
116
|
+
/** Colorize text according to simplistic and not very complete patterns.
|
|
117
|
+
* @type Colorer
|
|
118
|
+
*/
|
|
119
|
+
// @ts-ignore
|
|
115
120
|
const color = (text, def) => {
|
|
116
121
|
let out = ''
|
|
117
122
|
let quote = false
|
|
@@ -121,7 +126,7 @@ const color = (text, def) => {
|
|
|
121
126
|
const short = (t) => tty.color(t, CLI.COLORS.short) + c()
|
|
122
127
|
for (const char of text) {
|
|
123
128
|
switch (char) {
|
|
124
|
-
case '"':
|
|
129
|
+
case '"': {
|
|
125
130
|
if (quote) {
|
|
126
131
|
colors.pop()
|
|
127
132
|
} else {
|
|
@@ -131,31 +136,45 @@ const color = (text, def) => {
|
|
|
131
136
|
out += c('')
|
|
132
137
|
quote = !quote
|
|
133
138
|
break
|
|
134
|
-
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
case '#': {
|
|
135
142
|
id = true
|
|
136
143
|
colors.push(CLI.COLORS.id)
|
|
137
144
|
out += c('#')
|
|
138
145
|
break
|
|
139
|
-
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
case '|': {
|
|
140
149
|
out += short('|')
|
|
141
150
|
break
|
|
142
|
-
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
case '[': {
|
|
143
154
|
colors.push(CLI.COLORS.optional)
|
|
144
155
|
out += short('[')
|
|
145
156
|
break
|
|
146
|
-
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
case ']': {
|
|
147
160
|
colors.pop()
|
|
148
161
|
out += short(']')
|
|
149
162
|
break
|
|
150
|
-
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
case '<': {
|
|
151
166
|
colors.push(CLI.COLORS.variable)
|
|
152
167
|
out += c('')
|
|
153
168
|
break
|
|
154
|
-
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
case '>': {
|
|
155
172
|
colors.pop()
|
|
156
173
|
out += c('')
|
|
157
174
|
break
|
|
158
|
-
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
default: {
|
|
159
178
|
if (id && !/[\da-z]/.test(char)) {
|
|
160
179
|
id = false
|
|
161
180
|
colors.pop()
|
|
@@ -163,6 +182,7 @@ const color = (text, def) => {
|
|
|
163
182
|
} else {
|
|
164
183
|
out += char
|
|
165
184
|
}
|
|
185
|
+
}
|
|
166
186
|
}
|
|
167
187
|
}
|
|
168
188
|
|
|
@@ -170,7 +190,7 @@ const color = (text, def) => {
|
|
|
170
190
|
}
|
|
171
191
|
|
|
172
192
|
// Internal representations of files
|
|
173
|
-
const CONFIG = { id:
|
|
193
|
+
const CONFIG = { id: '' }
|
|
174
194
|
const DATA = { games: {} }
|
|
175
195
|
|
|
176
196
|
// CLI context object for plugins
|
|
@@ -550,11 +570,34 @@ const completer = (line) => {
|
|
|
550
570
|
return [completions, match]
|
|
551
571
|
}
|
|
552
572
|
|
|
573
|
+
const ambiguousSuggestions = (cmd) => {
|
|
574
|
+
const maybe = new Set()
|
|
575
|
+
for (const alias of cmd) {
|
|
576
|
+
if (alias.startsWith(CLI.CMD)) maybe.add(alias)
|
|
577
|
+
|
|
578
|
+
let alts = commands[alias].alt
|
|
579
|
+
alts = is(alts, 'arr') ? alts : [alts || '']
|
|
580
|
+
|
|
581
|
+
for (const alt of alts) {
|
|
582
|
+
if (alt.startsWith(CLI.CMD)) maybe.add(alt)
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
return maybe
|
|
587
|
+
}
|
|
588
|
+
|
|
553
589
|
const evaluate = async (command) => {
|
|
554
590
|
const [CMD, ...words] = command.split(/\s+/).filter(Boolean)
|
|
555
|
-
const cmdLower = CMD
|
|
591
|
+
const cmdLower = CMD?.toLowerCase() || ''
|
|
556
592
|
CLI.CMD = cmdLower
|
|
557
|
-
|
|
593
|
+
let cmd = abbreviations[cmdLower]
|
|
594
|
+
|
|
595
|
+
// If we are not in a REPL, let's remove hidden commands
|
|
596
|
+
if (!CLI.repl && is(cmd, 'arr')) {
|
|
597
|
+
cmd = cmd.filter((c) => !commands[c].hide)
|
|
598
|
+
if (cmd.length === 1) cmd = cmd[0]
|
|
599
|
+
}
|
|
600
|
+
|
|
558
601
|
if (is(cmd, 'str')) {
|
|
559
602
|
if (words.length > 0 && abbreviations[words[0].toLowerCase()] === 'help') {
|
|
560
603
|
commands.help.fn(CLI, cmd)
|
|
@@ -563,18 +606,7 @@ const evaluate = async (command) => {
|
|
|
563
606
|
}
|
|
564
607
|
} else if (is(cmd, 'arr')) {
|
|
565
608
|
CLI.error('Ambiguous command. Perhaps you meant one of the following:')
|
|
566
|
-
const maybe =
|
|
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
|
-
}
|
|
609
|
+
const maybe = ambiguousSuggestions(cmd)
|
|
578
610
|
|
|
579
611
|
if (maybe.size > 0) {
|
|
580
612
|
for (const alias of [...maybe].sort()) {
|
|
@@ -593,8 +625,6 @@ const evaluate = async (command) => {
|
|
|
593
625
|
CLI.error('Unknown command. Type "help" for a list of valid commands.')
|
|
594
626
|
}
|
|
595
627
|
}
|
|
596
|
-
|
|
597
|
-
return undefined
|
|
598
628
|
}
|
|
599
629
|
|
|
600
630
|
const main = async () => {
|
|
@@ -630,4 +660,4 @@ const main = async () => {
|
|
|
630
660
|
}
|
|
631
661
|
}
|
|
632
662
|
|
|
633
|
-
main()
|
|
663
|
+
await main()
|
package/src/cmds/analyze.js
CHANGED
|
@@ -15,8 +15,8 @@
|
|
|
15
15
|
|
|
16
16
|
// CLI analyze command
|
|
17
17
|
|
|
18
|
-
import { analyze } from '
|
|
19
|
-
import { Trax } from '
|
|
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
|
|
|
@@ -21,7 +21,7 @@ import process from 'node:process'
|
|
|
21
21
|
import envPaths from 'env-paths'
|
|
22
22
|
import makeDir from 'make-dir'
|
|
23
23
|
|
|
24
|
-
import { Trax } from '
|
|
24
|
+
import { Trax } from '@slugbugblue/trax'
|
|
25
25
|
|
|
26
26
|
const paths = envPaths('trax', { suffix: '' })
|
|
27
27
|
const PATHS = {}
|
package/src/cmds/list.js
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
|
|
16
16
|
// CLI list command
|
|
17
17
|
|
|
18
|
-
import { Trax } from '
|
|
18
|
+
import { Trax } from '@slugbugblue/trax'
|
|
19
19
|
|
|
20
20
|
export const listCmd = {
|
|
21
21
|
name: 'list',
|
|
@@ -41,7 +41,7 @@ export const listCmd = {
|
|
|
41
41
|
}
|
|
42
42
|
|
|
43
43
|
const plural = (n, noun, nouns) =>
|
|
44
|
-
n + ' ' + (Math.abs(n) === 1 ? noun : nouns
|
|
44
|
+
n + ' ' + (Math.abs(n) === 1 ? noun : nouns || noun + 's')
|
|
45
45
|
|
|
46
46
|
const alphaSort = (a, b) => {
|
|
47
47
|
a = String(a).toLowerCase()
|
package/src/cmds/new.js
CHANGED
|
@@ -58,13 +58,13 @@ const pairs = [
|
|
|
58
58
|
['Mario', 'Luigi'],
|
|
59
59
|
['Marlin', 'Dory'],
|
|
60
60
|
['Mary-Kate', 'Ashley'],
|
|
61
|
-
['
|
|
61
|
+
['Mickey', 'Minnie'],
|
|
62
62
|
['Miss Piggy', 'Kermit'],
|
|
63
63
|
['Mork', 'Mindy'],
|
|
64
64
|
['Obi-Wan', 'Anakin'],
|
|
65
65
|
['Pan', 'Hook'],
|
|
66
66
|
['Phineas', 'Ferb'],
|
|
67
|
-
['
|
|
67
|
+
['player 1', 'player 2'],
|
|
68
68
|
['p1', 'p2'],
|
|
69
69
|
['R2-D2', 'C-3PO'],
|
|
70
70
|
['Ren', 'Stimpy'],
|
|
@@ -89,7 +89,9 @@ const pairs = [
|
|
|
89
89
|
]
|
|
90
90
|
|
|
91
91
|
const pairUp = () => {
|
|
92
|
-
|
|
92
|
+
const duo = pairs[Math.floor(Math.random() * pairs.length)]
|
|
93
|
+
if (duo[0].toLowerCase() !== duo[0] && Math.random() < 0.5) duo.reverse()
|
|
94
|
+
return duo
|
|
93
95
|
}
|
|
94
96
|
|
|
95
97
|
newCmd.fn = (CLI, variant, ...names) => {
|
|
@@ -0,0 +1,52 @@
|
|
|
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
|
+
// CLI suggest command
|
|
17
|
+
|
|
18
|
+
import { suggest } from '@slugbugblue/trax/analyst'
|
|
19
|
+
|
|
20
|
+
export const suggestCmd = {
|
|
21
|
+
name: 'suggest',
|
|
22
|
+
alt: ['bot'],
|
|
23
|
+
desc: 'suggest a move',
|
|
24
|
+
help: ['Suggest a move in the current game.'],
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
suggestCmd.fn = (CLI, debug) => {
|
|
28
|
+
if (!String(CLI.GAME?.id)) {
|
|
29
|
+
return CLI.error('No active game. Type "new" to start a game.')
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const game = CLI.TRAX
|
|
33
|
+
const suggestion = suggest(game)
|
|
34
|
+
|
|
35
|
+
if (!suggestion.best) return
|
|
36
|
+
|
|
37
|
+
CLI.out(
|
|
38
|
+
CLI.bubble(
|
|
39
|
+
game.turn === '1' ? 'wh' : 'bh',
|
|
40
|
+
String(game.move + 1) + '. ' + suggestion.pick.move,
|
|
41
|
+
),
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
if ('debug'.startsWith(debug)) {
|
|
45
|
+
const m = []
|
|
46
|
+
for (const move of suggestion.options) {
|
|
47
|
+
m.push(move.move + ' ' + move.score)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
CLI.out('[' + m.join(', ') + '] ' + suggestion.ms + 'ms')
|
|
51
|
+
}
|
|
52
|
+
}
|