@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 +25 -0
- package/README.md +45 -7
- package/docs/analyst.md +44 -2
- package/package.json +6 -7
- package/src/analyst.js +97 -12
- package/src/cli.js +114 -31
- package/src/cmds/analyze.js +2 -2
- package/src/cmds/import-export.js +33 -3
- package/src/cmds/list.js +2 -5
- package/src/cmds/new.js +6 -4
- package/src/cmds/play-try.js +54 -7
- package/src/cmds/puzzles.js +152 -0
- package/src/cmds/suggest.js +52 -0
- package/src/cmds/view.js +1 -1
- package/src/engine.js +167 -29
- package/src/point.js +54 -12
- package/src/puzzles.js +808 -0
- package/src/threats.js +39 -35
- 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,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
|
|
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,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
|
|
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 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
|
|
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
|
-
- `
|
|
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.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
|
-
"./
|
|
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,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]
|
|
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
|
+
// 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
|
|
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 '
|
|
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'
|
|
@@ -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
|
-
|
|
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
|
-
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
case '#': {
|
|
135
144
|
id = true
|
|
136
145
|
colors.push(CLI.COLORS.id)
|
|
137
146
|
out += c('#')
|
|
138
147
|
break
|
|
139
|
-
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
case '|': {
|
|
140
151
|
out += short('|')
|
|
141
152
|
break
|
|
142
|
-
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
case '[': {
|
|
143
156
|
colors.push(CLI.COLORS.optional)
|
|
144
157
|
out += short('[')
|
|
145
158
|
break
|
|
146
|
-
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
case ']': {
|
|
147
162
|
colors.pop()
|
|
148
163
|
out += short(']')
|
|
149
164
|
break
|
|
150
|
-
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
case '<': {
|
|
151
168
|
colors.push(CLI.COLORS.variable)
|
|
152
169
|
out += c('')
|
|
153
170
|
break
|
|
154
|
-
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
case '>': {
|
|
155
174
|
colors.pop()
|
|
156
175
|
out += c('')
|
|
157
176
|
break
|
|
158
|
-
|
|
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
|
|
644
|
+
const cmdLower = CMD?.toLowerCase() || ''
|
|
556
645
|
CLI.CMD = cmdLower
|
|
557
|
-
|
|
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 =
|
|
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()
|
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
|
|