@slugbugblue/trax 0.11.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 +20 -0
- package/README.md +46 -7
- package/docs/analyst.md +44 -2
- package/docs/point.md +2 -1
- package/package.json +6 -7
- package/src/analyst.js +163 -23
- package/src/cli.js +60 -30
- package/src/cmds/analyze.js +3 -3
- 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 +63 -18
- 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,25 @@
|
|
|
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
|
+
|
|
18
|
+
## 0.12.0 - 2022-10-28
|
|
19
|
+
|
|
20
|
+
- `point.in()` now accepts any two opposing corners to define the bounding box
|
|
21
|
+
- Fleshing out testing and debugging of `analyst.js`
|
|
22
|
+
|
|
3
23
|
## 0.11.0 - 2022-09-22
|
|
4
24
|
|
|
5
25
|
- Breaking change: Refactor `play()` and `playMove()` into a single function
|
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/docs/point.md
CHANGED
|
@@ -89,9 +89,10 @@ Returns the distance between two points. This will never be a negative number.
|
|
|
89
89
|
Returns `true` if the given point or point-like object is at the same location
|
|
90
90
|
as the existing point.
|
|
91
91
|
|
|
92
|
-
##### `in(
|
|
92
|
+
##### `in(a, b)`
|
|
93
93
|
|
|
94
94
|
Pass in two points (or point-like objects) to define a rectangular bounding box.
|
|
95
|
+
Each point represents opposite corners of the box.
|
|
95
96
|
|
|
96
97
|
Returns `true` if the calling point is located inside the box or along any of
|
|
97
98
|
its edges (ie, both bounding points are considered "inside").
|
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,14 +15,33 @@
|
|
|
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
|
+
const id = () => {
|
|
25
|
+
const chars = 'abcdefghijklmnopqrstuvwxyz0123456789'
|
|
26
|
+
let code = ''
|
|
27
|
+
while (code.length < 6) {
|
|
28
|
+
code += chars[Math.floor(Math.random() * chars.length)]
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return 'analysis-' + code
|
|
32
|
+
}
|
|
33
|
+
|
|
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
|
|
24
40
|
|
|
25
|
-
//
|
|
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
|
|
26
45
|
const int = (s) => Number.parseInt(s, 10)
|
|
27
46
|
|
|
28
47
|
const leftTurn = { d: 'r', l: 'd', r: 'u', u: 'l' }
|
|
@@ -151,36 +170,63 @@ const label = (rawEdge) => {
|
|
|
151
170
|
return final
|
|
152
171
|
}
|
|
153
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
|
+
|
|
154
181
|
const attackScore = (from, level) => {
|
|
155
|
-
const count = from[level]
|
|
182
|
+
const count = threatValues(from[level] || [])
|
|
156
183
|
if (level < 1) return count
|
|
157
184
|
return count * 10 ** (threats.maxDepth - level + 1)
|
|
158
185
|
}
|
|
159
186
|
|
|
160
|
-
// Shortcut to get the number of
|
|
161
|
-
const
|
|
162
|
-
|
|
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
|
|
163
204
|
|
|
164
205
|
// Analysis class
|
|
165
206
|
export const Analysis = class {
|
|
207
|
+
/** @type TraxVariant */
|
|
208
|
+
#rules = 'trax'
|
|
209
|
+
#id = 'analysis'
|
|
210
|
+
#moves = []
|
|
211
|
+
#notation = ''
|
|
166
212
|
#edge = undefined
|
|
167
213
|
#game = {}
|
|
168
|
-
#move = 0
|
|
169
|
-
#save = {}
|
|
170
214
|
#scores = {}
|
|
171
215
|
#threats = undefined
|
|
172
216
|
|
|
173
217
|
constructor(game) {
|
|
174
218
|
// Make a copy of the game
|
|
175
|
-
this.#
|
|
176
|
-
this.#
|
|
177
|
-
this.#
|
|
219
|
+
this.#rules = game.rules
|
|
220
|
+
this.#moves = game.moves
|
|
221
|
+
this.#id = id()
|
|
222
|
+
this.#notation = game.notation
|
|
223
|
+
this.#game = new Trax(this.#rules, this.#moves, this.#id)
|
|
178
224
|
}
|
|
179
225
|
|
|
180
226
|
get game() {
|
|
181
227
|
// Return a fresh copy of the game as presented on init
|
|
182
|
-
if (this.#game.
|
|
183
|
-
this.#game = new Trax(
|
|
228
|
+
if (this.#game.notation !== this.#notation) {
|
|
229
|
+
this.#game = new Trax(this.#rules, this.#moves, this.#id)
|
|
184
230
|
}
|
|
185
231
|
|
|
186
232
|
return this.#game
|
|
@@ -217,7 +263,7 @@ export const Analysis = class {
|
|
|
217
263
|
|
|
218
264
|
for (const dir of Point.dirs) {
|
|
219
265
|
let look = loc.dir(dir)
|
|
220
|
-
while (look.in(topLeft, bottomRight) && !game.tileAt(
|
|
266
|
+
while (look.in(topLeft, bottomRight) && !game.tileAt(look)) {
|
|
221
267
|
look = look.dir(dir)
|
|
222
268
|
}
|
|
223
269
|
|
|
@@ -242,17 +288,14 @@ export const Analysis = class {
|
|
|
242
288
|
const inactive = this.threats[Trax.other(game.color)]
|
|
243
289
|
let advantage = Number.POSITIVE_INFINITY
|
|
244
290
|
|
|
245
|
-
// First examine the player
|
|
291
|
+
// First examine the active player
|
|
246
292
|
for (const level of Object.keys(active).map((i) => int(i))) {
|
|
247
293
|
if (level > 0 && level < advantage) advantage = level
|
|
248
294
|
score += attackScore(active, level)
|
|
249
295
|
}
|
|
250
296
|
|
|
251
297
|
const passive = game.color !== color
|
|
252
|
-
const calc =
|
|
253
|
-
!passive ||
|
|
254
|
-
threatCount(this.threats[color]) > 1 ||
|
|
255
|
-
threatCount(this.threats[Trax.other(color)]) > 0
|
|
298
|
+
const calc = !passive || passiveAttack(inactive) || underAttack(active)
|
|
256
299
|
|
|
257
300
|
// Calculate the effects of the passive player first
|
|
258
301
|
for (const level of Object.keys(inactive).map((i) => int(i))) {
|
|
@@ -287,7 +330,7 @@ export const Analysis = class {
|
|
|
287
330
|
// c = in a cave but has no restrictions (converted to n in edge)
|
|
288
331
|
// C = a cave where some moves are not allowed (converted to c in edge)
|
|
289
332
|
// n = normal space
|
|
290
|
-
// x =
|
|
333
|
+
// x = no tile can be played here
|
|
291
334
|
spaceType(loc) {
|
|
292
335
|
const { game } = this
|
|
293
336
|
|
|
@@ -300,13 +343,25 @@ export const Analysis = class {
|
|
|
300
343
|
}
|
|
301
344
|
|
|
302
345
|
// Check to see if we have an invalid move
|
|
346
|
+
let someValid = false
|
|
347
|
+
let someInvalid = false
|
|
303
348
|
for (const tile of game.possibleTiles(loc)) {
|
|
304
349
|
const drop = game.dropTile(tile, loc, 'tentative')
|
|
305
|
-
if (
|
|
306
|
-
|
|
350
|
+
if (drop.valid) {
|
|
351
|
+
someValid = true
|
|
352
|
+
} else {
|
|
353
|
+
someInvalid = true
|
|
307
354
|
}
|
|
308
355
|
}
|
|
309
356
|
|
|
357
|
+
if (!someValid) {
|
|
358
|
+
return 'x'
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
if (someInvalid) {
|
|
362
|
+
return 'C'
|
|
363
|
+
}
|
|
364
|
+
|
|
310
365
|
return 'c'
|
|
311
366
|
}
|
|
312
367
|
|
|
@@ -381,6 +436,7 @@ export const Analysis = class {
|
|
|
381
436
|
threat: threat.pattern,
|
|
382
437
|
match: match[0],
|
|
383
438
|
at: match.index,
|
|
439
|
+
value: threat.value,
|
|
384
440
|
})
|
|
385
441
|
}
|
|
386
442
|
}
|
|
@@ -390,6 +446,90 @@ export const Analysis = class {
|
|
|
390
446
|
|
|
391
447
|
return matches
|
|
392
448
|
}
|
|
449
|
+
|
|
450
|
+
// Make a reasonable inspect representation of the analysis
|
|
451
|
+
[Symbol.for('nodejs.util.inspect.custom')](depth, options, inspect) {
|
|
452
|
+
const sub = (object) => inspect(object, options).replace(/\n/g, '\n ')
|
|
453
|
+
const style = options.stylize
|
|
454
|
+
if (depth < 0) return style('<Analysis>', 'special')
|
|
455
|
+
if (options.depth !== null) options.depth -= 1
|
|
456
|
+
const tbd = style('tbd', 'undefined')
|
|
457
|
+
let custom = style('Analysis', 'special') + ' {\n'
|
|
458
|
+
custom += ' game: ' + style(this.#rules, 'special')
|
|
459
|
+
custom += '(' + style(this.#moves, 'string') + '),\n'
|
|
460
|
+
custom += ' edge: '
|
|
461
|
+
if (this.#edge) {
|
|
462
|
+
custom += '{\n w: ' + style(this.#edge.w, 'string')
|
|
463
|
+
custom += ',\n b: ' + style(this.#edge.b, 'string')
|
|
464
|
+
custom += ',\n edge: ' + style('[rawEdge]', 'special')
|
|
465
|
+
custom += '\n }'
|
|
466
|
+
} else {
|
|
467
|
+
custom += tbd
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
custom += ',\n threats: '
|
|
471
|
+
custom += this.#threats ? sub(this.#threats) : tbd
|
|
472
|
+
custom += ',\n scores: '
|
|
473
|
+
custom += 'w' in this.#scores ? sub(this.#scores) : tbd
|
|
474
|
+
custom += ',\n score: '
|
|
475
|
+
const score = 'w' in this.#scores ? this.score : null
|
|
476
|
+
custom += score === null ? tbd : style(score, 'number')
|
|
477
|
+
|
|
478
|
+
return custom + '\n}'
|
|
479
|
+
}
|
|
393
480
|
}
|
|
394
481
|
|
|
395
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
|
+
}
|