@slugbugblue/trax 0.14.0 → 0.15.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 +21 -2
- package/README.md +10 -5
- package/package.json +1 -1
- package/src/analyst.js +196 -79
- package/src/cmds/analyze.js +2 -2
- package/src/cmds/list.js +3 -0
- package/src/cmds/new.js +3 -0
- package/src/cmds/puzzles.js +10 -4
- package/src/puzzles.js +19 -15
- package/src/threats.js +68 -35
- package/src/tty.js +1 -1
- package/src/types.d.ts +56 -0
- package/src/version.js +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
# @slugbugblue/trax
|
|
2
2
|
|
|
3
|
+
## 0.15.0 - 2022-12-14
|
|
4
|
+
|
|
5
|
+
- Added shared `puzzles` commands to the CLI:
|
|
6
|
+
|
|
7
|
+
- `trax ls puzzles`: see all the puzzles
|
|
8
|
+
- `trax new puzzle`: start a new puzzle
|
|
9
|
+
|
|
10
|
+
- Puzzle improvements:
|
|
11
|
+
|
|
12
|
+
- Adjust puzzle category levels for harder puzzles
|
|
13
|
+
- Add multiple hints when the puzzle can be solved multiple ways
|
|
14
|
+
|
|
15
|
+
- Analysis engine improvements:
|
|
16
|
+
|
|
17
|
+
- Analysis engine suggestions now "solve" all tough puzzles
|
|
18
|
+
- Reworked threats to be more precise
|
|
19
|
+
- Reworked scoring to add more nuance
|
|
20
|
+
|
|
3
21
|
## 0.14.0 - 2022-12-05
|
|
4
22
|
|
|
5
23
|
- Added `puzzles` to the CLI:
|
|
@@ -7,8 +25,9 @@
|
|
|
7
25
|
- `trax puzzles ls`: see all the puzzles
|
|
8
26
|
- `trax puzzle new`: start a new puzzle
|
|
9
27
|
|
|
10
|
-
-
|
|
11
|
-
|
|
28
|
+
- Analysis engine tests now include correctly suggesting the first move for
|
|
29
|
+
solving all tutorial and easy puzzles. As the threats database improves, we
|
|
30
|
+
will add more puzzles to the tests.
|
|
12
31
|
|
|
13
32
|
## 0.13.0 - 2022-11-25
|
|
14
33
|
|
package/README.md
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
# Trax
|
|
2
2
|
|
|
3
|
-
Trax
|
|
3
|
+
This project aims to provide tools for working with Trax games in Javascript.
|
|
4
|
+
|
|
5
|
+
- Trax game-playing engine
|
|
6
|
+
- Position analysis engine
|
|
7
|
+
- Over 50 puzzles to help hone your Trax skills
|
|
8
|
+
- Command-line interface for ease of use
|
|
4
9
|
|
|
5
10
|
## Description
|
|
6
11
|
|
|
@@ -32,10 +37,10 @@ The command-line interface is provided as `trax`, available in your path if
|
|
|
32
37
|
installed globally, or in the `node_modules/.bin` directory and runnable using
|
|
33
38
|
`npx trax` if installed locally.
|
|
34
39
|
|
|
35
|
-
The CLI can be used interactively by
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
back.
|
|
40
|
+
The CLI can be used interactively by running the command with no parameters or
|
|
41
|
+
by passing the `--interactive` or `-i` flag. When run interactively, you can
|
|
42
|
+
avoid worries about the need to escape certain characters from the shell, and it
|
|
43
|
+
can be easier to run multiple commands back to back.
|
|
39
44
|
|
|
40
45
|
Help is available for the CLI through the use of the `help` command. Type
|
|
41
46
|
`trax help` to see a list of the available commands, and `trax help play` to see
|
package/package.json
CHANGED
package/src/analyst.js
CHANGED
|
@@ -1,26 +1,15 @@
|
|
|
1
|
-
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* You may obtain a copy of the License at
|
|
6
|
-
*
|
|
7
|
-
* http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
-
*
|
|
9
|
-
* Unless required by applicable law or agreed to in writing, software
|
|
10
|
-
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
-
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
-
* See the License for the specific language governing permissions and
|
|
13
|
-
* limitations under the License.
|
|
1
|
+
/** Analyst module: provides analysis functions for Trax games.
|
|
2
|
+
* @copyright 2022
|
|
3
|
+
* @author Chad Transtrum <chad@transtrum.net>
|
|
4
|
+
* @license Apache-2.0
|
|
14
5
|
*/
|
|
15
6
|
|
|
16
|
-
// Analyst module: provides analysis functions for Trax games
|
|
17
|
-
|
|
18
7
|
import { Point } from '@slugbugblue/trax/point'
|
|
19
8
|
import { Trax } from '@slugbugblue/trax'
|
|
20
9
|
|
|
21
10
|
import { threats } from '@slugbugblue/trax/threats'
|
|
22
11
|
|
|
23
|
-
|
|
12
|
+
/** Helper function to generate a unique id for the analysis. */
|
|
24
13
|
const id = () => {
|
|
25
14
|
const chars = 'abcdefghijklmnopqrstuvwxyz0123456789'
|
|
26
15
|
let code = ''
|
|
@@ -31,14 +20,27 @@ const id = () => {
|
|
|
31
20
|
return 'analysis-' + code
|
|
32
21
|
}
|
|
33
22
|
|
|
34
|
-
|
|
23
|
+
/** A sort function for hashes with a numeric score property.
|
|
24
|
+
* @arg {PositionScore} a - The first Position Score to be compared
|
|
25
|
+
* @arg {PositionScore} b - The second Position Score to be compared
|
|
26
|
+
* @returns {number} a.score <=> b.score
|
|
27
|
+
*/
|
|
35
28
|
const byScoreDesc = (a, b) => b.score - a.score
|
|
36
29
|
|
|
37
|
-
|
|
30
|
+
/** A filter function factory to remove scores not within x points of the
|
|
31
|
+
* highest score.
|
|
32
|
+
* @arg highest {number} - The highest score
|
|
33
|
+
* @arg tolerance {number} - How close another score must be to be included
|
|
34
|
+
* @returns {(s:PositionScore)=>boolean} a filter function
|
|
35
|
+
*/
|
|
38
36
|
const withinX = (highest, tolerance) => (s) =>
|
|
39
37
|
s.score === highest || highest - s.score <= tolerance
|
|
40
38
|
|
|
41
|
-
|
|
39
|
+
/** Return a random element of an array.
|
|
40
|
+
* @template T
|
|
41
|
+
* @arg {T[]} array - an array of elements
|
|
42
|
+
* @returns {T} one of the elements at random
|
|
43
|
+
*/
|
|
42
44
|
const randomDraw = (array) => array[Math.floor(Math.random() * array.length)]
|
|
43
45
|
|
|
44
46
|
// Make parseInt a little shorter
|
|
@@ -56,12 +58,16 @@ const edgeColor = (tile, dir) => {
|
|
|
56
58
|
|
|
57
59
|
const encodeNumber = (n) => String.fromCodePoint(48 + n)
|
|
58
60
|
|
|
59
|
-
|
|
61
|
+
/** Explore a raw edge and update cave information.
|
|
62
|
+
* @arg {RawEdge} rawEdge - the raw edge information
|
|
63
|
+
* @returns {RawEdge}
|
|
64
|
+
*/
|
|
60
65
|
const spelunk = (rawEdge) => {
|
|
61
66
|
for (let index = 1; index < rawEdge.length - 1; index++) {
|
|
62
67
|
if (rawEdge[index].t === 'C') {
|
|
63
|
-
const
|
|
64
|
-
const
|
|
68
|
+
const blank = () => ({ x: 0, y: 0, c: '', t: '' })
|
|
69
|
+
const previous = rawEdge[index - 1] || blank()
|
|
70
|
+
const next = rawEdge[index + 1] || blank()
|
|
65
71
|
if (next.t === 'c') next.t = 'C'
|
|
66
72
|
if (previous.t === 'c') {
|
|
67
73
|
previous.t = 'C'
|
|
@@ -78,7 +84,10 @@ const spelunk = (rawEdge) => {
|
|
|
78
84
|
return rawEdge
|
|
79
85
|
}
|
|
80
86
|
|
|
81
|
-
|
|
87
|
+
/** Combine hollows into a single item in the raw edge array.
|
|
88
|
+
* @arg {RawEdge} rawEdge
|
|
89
|
+
* @returns {RawEdge}
|
|
90
|
+
*/
|
|
82
91
|
const hollowOut = (rawEdge) => {
|
|
83
92
|
for (let index = 1; index < rawEdge.length - 1; index++) {
|
|
84
93
|
const space = rawEdge[index]
|
|
@@ -93,8 +102,13 @@ const hollowOut = (rawEdge) => {
|
|
|
93
102
|
return rawEdge
|
|
94
103
|
}
|
|
95
104
|
|
|
96
|
-
|
|
97
|
-
|
|
105
|
+
/** Supposedly deep nesting is bad, so we broke this out of "pairUp" for
|
|
106
|
+
* "better" code, even though this is useless on its own ...
|
|
107
|
+
* @arg {RawEdge} rawEdge
|
|
108
|
+
* @arg {number} index
|
|
109
|
+
* @arg {string} color
|
|
110
|
+
* @pairs {Record<string, number>}
|
|
111
|
+
*/
|
|
98
112
|
const findPair = (rawEdge, index, color, pairs) => {
|
|
99
113
|
const colors = new Set([color, 'l'])
|
|
100
114
|
const space = rawEdge[index]
|
|
@@ -131,8 +145,12 @@ const findPair = (rawEdge, index, color, pairs) => {
|
|
|
131
145
|
}
|
|
132
146
|
}
|
|
133
147
|
|
|
134
|
-
|
|
148
|
+
/** Find connectable pairs in the raw edge.
|
|
149
|
+
* @arg {RawEdge} rawEdge
|
|
150
|
+
* @returns {RawEdge}
|
|
151
|
+
*/
|
|
135
152
|
const pairUp = (rawEdge) => {
|
|
153
|
+
/** @type Record<string, number> */
|
|
136
154
|
const pairs = {}
|
|
137
155
|
for (const [index, space] of rawEdge.entries()) {
|
|
138
156
|
for (const bw of ['w', 'b']) {
|
|
@@ -146,7 +164,10 @@ const pairUp = (rawEdge) => {
|
|
|
146
164
|
return rawEdge
|
|
147
165
|
}
|
|
148
166
|
|
|
149
|
-
|
|
167
|
+
/** Label and finalize the raw edge.
|
|
168
|
+
* @arg {RawEdge} rawEdge
|
|
169
|
+
* @returns {EdgeObject}
|
|
170
|
+
*/
|
|
150
171
|
const label = (rawEdge) => {
|
|
151
172
|
const final = { w: '', b: '', edge: rawEdge }
|
|
152
173
|
for (const space of rawEdge) {
|
|
@@ -170,48 +191,73 @@ const label = (rawEdge) => {
|
|
|
170
191
|
return final
|
|
171
192
|
}
|
|
172
193
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
// for (const threat of threatsArray) count += threat.value
|
|
178
|
-
// return count
|
|
179
|
-
}
|
|
194
|
+
/** Quick way to get the number of items in an array.
|
|
195
|
+
* @returns {number} - length of the array
|
|
196
|
+
*/
|
|
197
|
+
const length = (array) => array?.length || 0
|
|
180
198
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
199
|
+
/** Calculate the points for the number of threats at a particular level.
|
|
200
|
+
* @arg {FoundThreats} from - the threats found for a certain player
|
|
201
|
+
* @arg {number} level - the level to calculate the points for
|
|
202
|
+
* @arg {number} [as] - if included, the level the points should be calculated as
|
|
203
|
+
* @return {number} the number of points for the level
|
|
204
|
+
*/
|
|
205
|
+
const attackScore = (from, level, as) => {
|
|
206
|
+
const count = length(from[level])
|
|
207
|
+
const calculateAs = Math.min(as === undefined ? level : as, threats.maxDepth)
|
|
208
|
+
if (calculateAs < 1) return count
|
|
209
|
+
return count * 10 ** (threats.maxDepth - calculateAs + 1)
|
|
185
210
|
}
|
|
186
211
|
|
|
187
|
-
|
|
188
|
-
|
|
212
|
+
/** Shortcut to get the number of attacks.
|
|
213
|
+
* @arg {FoundThreats} from - the threats found for a certain player
|
|
214
|
+
* @returns {number} the number of attacks (win on next move) present
|
|
215
|
+
*/
|
|
216
|
+
const attackCount = (from) => length(from['1'])
|
|
189
217
|
|
|
190
|
-
|
|
218
|
+
/** Shortcut to get the number of passive threats
|
|
219
|
+
* @arg {FoundThreats} from - the threats found for a player
|
|
220
|
+
* @returns {number} the number of passive threats (win in 2 or more moves)
|
|
221
|
+
*/
|
|
191
222
|
const threatCount = (from) => {
|
|
192
223
|
let count = 0
|
|
193
224
|
for (const [depth, array] of Object.entries(from)) {
|
|
194
|
-
count += int(depth) > 1 ?
|
|
225
|
+
count += int(depth) > 1 ? length(array) : 0
|
|
195
226
|
}
|
|
196
227
|
|
|
197
228
|
return count
|
|
198
229
|
}
|
|
199
230
|
|
|
200
|
-
|
|
231
|
+
/** Shortcut to determine if we have an active attack.
|
|
232
|
+
* @arg {FoundThreats} from - the threats found for a player
|
|
233
|
+
* @returns {boolean}
|
|
234
|
+
*/
|
|
235
|
+
const activeAttack = (from) =>
|
|
201
236
|
attackCount(from) > 1 || (attackCount(from) === 1 && threatCount(from) > 0)
|
|
202
237
|
|
|
203
|
-
|
|
204
|
-
|
|
238
|
+
/** Shortcut to determine if we have a passive attack.
|
|
239
|
+
* @arg {FoundThreats} from - the threats found for a player
|
|
240
|
+
* @returns {boolean}
|
|
241
|
+
*/
|
|
242
|
+
const passiveAttack = (from) => threatCount(from) > 1
|
|
243
|
+
|
|
244
|
+
/** Shortcut to determine if we are under attack.
|
|
245
|
+
* @arg {FoundThreats} from - the threats found for a player
|
|
246
|
+
* @returns {boolean}
|
|
247
|
+
*/
|
|
248
|
+
const underAttack = (from) => attackCount(from) + threatCount(from) > 0
|
|
205
249
|
|
|
206
250
|
// Analysis class
|
|
207
251
|
export const Analysis = class {
|
|
208
|
-
/** @type TraxVariant */
|
|
252
|
+
/** @type {TraxVariant} */
|
|
209
253
|
#rules = 'trax'
|
|
210
254
|
#id = 'analysis'
|
|
255
|
+
/** @type {string[]} - List of moves to arrive at this position */
|
|
211
256
|
#moves = []
|
|
212
257
|
#notation = ''
|
|
213
258
|
#edge = undefined
|
|
214
|
-
|
|
259
|
+
/** @type {Trax} */
|
|
260
|
+
#game
|
|
215
261
|
#scores = {}
|
|
216
262
|
#threats = undefined
|
|
217
263
|
|
|
@@ -242,6 +288,9 @@ export const Analysis = class {
|
|
|
242
288
|
return this.#edge
|
|
243
289
|
}
|
|
244
290
|
|
|
291
|
+
/** All threats present in this position.
|
|
292
|
+
* @returns {FoundThreatsCollection}
|
|
293
|
+
*/
|
|
245
294
|
get threats() {
|
|
246
295
|
if (!this.#threats) {
|
|
247
296
|
this.#threats = { b: this.#applyThreats('b'), w: this.#applyThreats('w') }
|
|
@@ -274,6 +323,69 @@ export const Analysis = class {
|
|
|
274
323
|
return count > 2
|
|
275
324
|
}
|
|
276
325
|
|
|
326
|
+
/** Determine the points of the currently active player.
|
|
327
|
+
* @returns {[number, number]} - the points for this player, and the level
|
|
328
|
+
* of the most dangerous threat
|
|
329
|
+
*/
|
|
330
|
+
#activeScore() {
|
|
331
|
+
let score = 0
|
|
332
|
+
let threatLevel = Number.POSITIVE_INFINITY
|
|
333
|
+
const { game } = this
|
|
334
|
+
const active = this.threats[game.color]
|
|
335
|
+
|
|
336
|
+
// First examine the active player
|
|
337
|
+
for (const level of Object.keys(active).map((i) => int(i))) {
|
|
338
|
+
if (level > 0) threatLevel = Math.min(threatLevel, level)
|
|
339
|
+
score += attackScore(active, level)
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
return [score, threatLevel]
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/** Determine the points of the waiting player.
|
|
346
|
+
* @arg {Color} color - the color for which we are generating the score
|
|
347
|
+
* @arg {number} threatLevel - the lowest level for which the active player
|
|
348
|
+
* has an active threat
|
|
349
|
+
* @returns {[number, number]} - the points for this player and the multiplier
|
|
350
|
+
* to be applied to the other player's points
|
|
351
|
+
*/
|
|
352
|
+
#inactiveScore(color, threatLevel) {
|
|
353
|
+
const { game } = this
|
|
354
|
+
const active = this.threats[game.color]
|
|
355
|
+
const inactive = this.threats[Trax.other(game.color)]
|
|
356
|
+
|
|
357
|
+
// Is the color we are looking at the passive player?
|
|
358
|
+
const passive = game.color !== color
|
|
359
|
+
|
|
360
|
+
// Is the inactive player activating known threats?
|
|
361
|
+
const attacking = activeAttack(inactive)
|
|
362
|
+
|
|
363
|
+
// Was the inactive player unable to clear all threats?
|
|
364
|
+
const defending = !attacking && underAttack(active)
|
|
365
|
+
|
|
366
|
+
// Does the inactive player have multiple threats
|
|
367
|
+
const threatening = !attacking && passiveAttack(inactive)
|
|
368
|
+
|
|
369
|
+
// If calc is false, attacks and threats are treated as corners
|
|
370
|
+
const calc = !passive || attacking || defending || threatening
|
|
371
|
+
|
|
372
|
+
// If the current player does not hold the initiative, reduce the score
|
|
373
|
+
const multiplier = passive && attacking && threatLevel > 1 ? 0.1 : 1
|
|
374
|
+
|
|
375
|
+
let score = 0
|
|
376
|
+
|
|
377
|
+
// Now calculate the effects of the passive player
|
|
378
|
+
for (const level of Object.keys(inactive).map((i) => int(i))) {
|
|
379
|
+
const calcLevel = level > 0 && defending ? level + threatLevel - 1 : level
|
|
380
|
+
let points = attackScore(inactive, level, calc ? calcLevel : 0)
|
|
381
|
+
if (level === threatLevel) points = passive ? Math.floor(points / 2) : 0
|
|
382
|
+
if (threatening) points = Math.floor(points / 2)
|
|
383
|
+
score += points
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
return [score, multiplier]
|
|
387
|
+
}
|
|
388
|
+
|
|
277
389
|
#score(color) {
|
|
278
390
|
if (this.#scores[color] === undefined) {
|
|
279
391
|
const { game } = this
|
|
@@ -283,30 +395,11 @@ export const Analysis = class {
|
|
|
283
395
|
return this.#scores[color]
|
|
284
396
|
}
|
|
285
397
|
|
|
286
|
-
|
|
398
|
+
const [currentPlayer, threatLevel] = this.#activeScore()
|
|
399
|
+
const [nextPlayer, multiplier] = this.#inactiveScore(color, threatLevel)
|
|
287
400
|
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
let advantage = Number.POSITIVE_INFINITY
|
|
291
|
-
|
|
292
|
-
// First examine the active player
|
|
293
|
-
for (const level of Object.keys(active).map((i) => int(i))) {
|
|
294
|
-
if (level > 0 && level < advantage) advantage = level
|
|
295
|
-
score += attackScore(active, level)
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
const passive = game.color !== color
|
|
299
|
-
const calc = !passive || passiveAttack(inactive)
|
|
300
|
-
|
|
301
|
-
// Calculate the effects of the passive player first
|
|
302
|
-
for (const level of Object.keys(inactive).map((i) => int(i))) {
|
|
303
|
-
if (level > advantage) continue
|
|
304
|
-
let points = calc || level === 0 ? attackScore(inactive, level) : 1
|
|
305
|
-
if (level === advantage) points = passive ? Math.floor(points / 2) : 0
|
|
306
|
-
score -= points
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
if (passive) score *= -1
|
|
401
|
+
let score = Math.floor(currentPlayer * multiplier) - nextPlayer
|
|
402
|
+
if (score && game.color !== color) score *= -1
|
|
310
403
|
|
|
311
404
|
this.#scores[color] = score
|
|
312
405
|
}
|
|
@@ -369,6 +462,7 @@ export const Analysis = class {
|
|
|
369
462
|
// Trace the edge of the playing area
|
|
370
463
|
#traceEdge() {
|
|
371
464
|
const { game } = this
|
|
465
|
+
/** @type {RawEdge} */
|
|
372
466
|
const rawEdge = []
|
|
373
467
|
const lines = {}
|
|
374
468
|
const count = { w: 0, b: 0 }
|
|
@@ -423,8 +517,26 @@ export const Analysis = class {
|
|
|
423
517
|
return label(pairUp(hollowOut(spelunk(rawEdge))))
|
|
424
518
|
}
|
|
425
519
|
|
|
426
|
-
|
|
520
|
+
/** Compose the threat into a FoundThreat object.
|
|
521
|
+
* @arg {Threat} threat - the threat that has been found
|
|
522
|
+
* @arg {RegExpMatchArray} match - the matching item
|
|
523
|
+
* @returns {FoundThreat}
|
|
524
|
+
*/
|
|
525
|
+
#foundThreat(threat, match) {
|
|
526
|
+
return {
|
|
527
|
+
threat: threat.pattern,
|
|
528
|
+
match: match[1], // The match is in the capturing group
|
|
529
|
+
at: match.index,
|
|
530
|
+
value: threat.value,
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
/** Apply the threats to a color's regex-ready edge.
|
|
535
|
+
* @arg {Color} color - the color to find threats for
|
|
536
|
+
* @returns {FoundThreats}
|
|
537
|
+
*/
|
|
427
538
|
#applyThreats(color) {
|
|
539
|
+
/** @type {FoundThreats} */
|
|
428
540
|
const matches = {}
|
|
429
541
|
const { length } = this.edge[color]
|
|
430
542
|
const edge = this.edge[color].repeat(2)
|
|
@@ -432,13 +544,11 @@ export const Analysis = class {
|
|
|
432
544
|
const find = matches[threat.depth] || []
|
|
433
545
|
for (const match of edge.matchAll(threat.rx)) {
|
|
434
546
|
if (match.index >= length) break
|
|
435
|
-
if (find.every((f) => f.at !== match.index || f.match !== match[
|
|
436
|
-
find.push(
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
value: threat.value,
|
|
441
|
-
})
|
|
547
|
+
if (find.every((f) => f.at !== match.index || f.match !== match[1])) {
|
|
548
|
+
find.push(this.#foundThreat(threat, match))
|
|
549
|
+
} else {
|
|
550
|
+
const duplicate = matches[99] || []
|
|
551
|
+
duplicate.push(this.#foundThreat(threat, match))
|
|
442
552
|
}
|
|
443
553
|
}
|
|
444
554
|
|
|
@@ -480,12 +590,19 @@ export const Analysis = class {
|
|
|
480
590
|
}
|
|
481
591
|
}
|
|
482
592
|
|
|
593
|
+
/** Analyze a game position.
|
|
594
|
+
* @arg {Trax} game - the position to analyze
|
|
595
|
+
* @returns {Analysis}
|
|
596
|
+
*/
|
|
483
597
|
export const analyze = (game) => new Analysis(game)
|
|
484
598
|
|
|
485
599
|
export const Suggestion = class {
|
|
600
|
+
/** @type {PositionScore[]} */
|
|
486
601
|
all = []
|
|
602
|
+
/** @type {PositionScore[]} */
|
|
487
603
|
options = []
|
|
488
|
-
|
|
604
|
+
/** @type {PositionScore} */
|
|
605
|
+
pick
|
|
489
606
|
ms = 0
|
|
490
607
|
|
|
491
608
|
constructor(analyzed, ms) {
|
package/src/cmds/analyze.js
CHANGED
|
@@ -22,7 +22,7 @@ const notationRx = /^[@a-z]+\d+[bps/\\+]$/i
|
|
|
22
22
|
|
|
23
23
|
export const analyzeCmd = {
|
|
24
24
|
name: 'analyze',
|
|
25
|
-
args: '[
|
|
25
|
+
args: '[move]',
|
|
26
26
|
comp: '<play>',
|
|
27
27
|
desc: 'analyze a position',
|
|
28
28
|
help: [
|
|
@@ -44,7 +44,7 @@ const listThreats = (threats, detailed) => {
|
|
|
44
44
|
const items = []
|
|
45
45
|
if (detailed) {
|
|
46
46
|
for (const item of threats[level]) {
|
|
47
|
-
items.push('[' + item.at + ':' + item.match + ']')
|
|
47
|
+
items.push('[' + item.at + ':' + item.threat + ':' + item.match + ']')
|
|
48
48
|
}
|
|
49
49
|
}
|
|
50
50
|
|
package/src/cmds/list.js
CHANGED
|
@@ -101,6 +101,9 @@ listCmd.fn = (CLI, ...filters) => {
|
|
|
101
101
|
return CLI.error('No games. Type "new" to start a new game.')
|
|
102
102
|
}
|
|
103
103
|
|
|
104
|
+
const puzzle = filters.find((f) => f.length > 1 && 'puzzles'.startsWith(f))
|
|
105
|
+
if (puzzle) return CLI.do('puzzles', ...filters.filter((f) => f !== puzzle))
|
|
106
|
+
|
|
104
107
|
let list = []
|
|
105
108
|
const size = {
|
|
106
109
|
id: 1,
|
package/src/cmds/new.js
CHANGED
|
@@ -98,6 +98,9 @@ newCmd.fn = (CLI, variant, ...names) => {
|
|
|
98
98
|
let rules = ''
|
|
99
99
|
if (variant) {
|
|
100
100
|
variant = variant.toLowerCase()
|
|
101
|
+
if (variant.length > 1 && 'puzzles'.startsWith(variant)) {
|
|
102
|
+
return CLI.do('puzzles', 'new', ...names)
|
|
103
|
+
}
|
|
101
104
|
|
|
102
105
|
if (['8x8', '8', 'trax8x8', '8trax', '8x8trax'].includes(variant)) {
|
|
103
106
|
rules = 'trax8'
|
package/src/cmds/puzzles.js
CHANGED
|
@@ -19,7 +19,7 @@ const completions = [...validLevels, ...puzzleIds].join('|')
|
|
|
19
19
|
|
|
20
20
|
export const puzzlesCmd = {
|
|
21
21
|
name: 'puzzles',
|
|
22
|
-
args: '["list"|"start" [
|
|
22
|
+
args: '["list"|"start"] [level|puzzle]',
|
|
23
23
|
comp: 'list|ls|new|show|start|view ' + completions,
|
|
24
24
|
desc: 'play puzzles',
|
|
25
25
|
help: [
|
|
@@ -28,6 +28,11 @@ export const puzzlesCmd = {
|
|
|
28
28
|
],
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
+
const an = (word = '') => {
|
|
32
|
+
const a = /^[aeiou]/.test(word) ? 'an' : 'a'
|
|
33
|
+
return a + ' ' + word
|
|
34
|
+
}
|
|
35
|
+
|
|
31
36
|
const showPuzzle = (CLI, id) => {
|
|
32
37
|
const puzzle = puzzles.find((p) => p.id === id)
|
|
33
38
|
if (!puzzle) return CLI.error(`Puzzle ${id} not found`)
|
|
@@ -35,7 +40,7 @@ const showPuzzle = (CLI, id) => {
|
|
|
35
40
|
const players = ['white', 'black']
|
|
36
41
|
CLI.display(trax, players)
|
|
37
42
|
let out = CLI.color.short(puzzle.id)
|
|
38
|
-
out += CLI.color(
|
|
43
|
+
out += CLI.color(` is ${an(levels[puzzle.level])} puzzle`) + '\n'
|
|
39
44
|
if (puzzle.title) out += CLI.color(puzzle.title) + '\n'
|
|
40
45
|
if (puzzle.desc) out += CLI.color.help(puzzle.desc) + '\n'
|
|
41
46
|
out += CLI.color(players[puzzle.player - 1] + ' to win by move ' + puzzle.max)
|
|
@@ -143,8 +148,9 @@ puzzlesCmd.fn = (CLI, action, id) => {
|
|
|
143
148
|
}
|
|
144
149
|
|
|
145
150
|
if (action) {
|
|
146
|
-
|
|
147
|
-
if (
|
|
151
|
+
const x = id || action
|
|
152
|
+
if (validLevels.includes(x)) return listLevel(CLI, x)
|
|
153
|
+
if (puzzleIds.includes(x)) return showPuzzle(CLI, x)
|
|
148
154
|
if (id) return CLI.error('Could not find puzzle ' + id)
|
|
149
155
|
}
|
|
150
156
|
|
package/src/puzzles.js
CHANGED
|
@@ -440,7 +440,7 @@ export const puzzles = [
|
|
|
440
440
|
level: 2,
|
|
441
441
|
player: 1,
|
|
442
442
|
max: 11,
|
|
443
|
-
|
|
443
|
+
hints: ['A0/', 'C4\\', 'D5/'],
|
|
444
444
|
},
|
|
445
445
|
{
|
|
446
446
|
id: 'iw3.trx',
|
|
@@ -451,7 +451,7 @@ export const puzzles = [
|
|
|
451
451
|
level: 2,
|
|
452
452
|
player: 1,
|
|
453
453
|
max: 11,
|
|
454
|
-
|
|
454
|
+
hints: ['B0/', 'C0\\'],
|
|
455
455
|
},
|
|
456
456
|
{
|
|
457
457
|
id: 'iw4.trx',
|
|
@@ -462,7 +462,7 @@ export const puzzles = [
|
|
|
462
462
|
level: 2,
|
|
463
463
|
player: 1,
|
|
464
464
|
max: 11,
|
|
465
|
-
|
|
465
|
+
hints: ['@2/', 'C0/'],
|
|
466
466
|
},
|
|
467
467
|
{
|
|
468
468
|
id: 'iw5.trx',
|
|
@@ -495,7 +495,7 @@ export const puzzles = [
|
|
|
495
495
|
level: 2,
|
|
496
496
|
player: 1,
|
|
497
497
|
max: 13,
|
|
498
|
-
|
|
498
|
+
hints: ['A1+', 'D1/'],
|
|
499
499
|
},
|
|
500
500
|
{
|
|
501
501
|
id: 'ib1.trx',
|
|
@@ -517,7 +517,7 @@ export const puzzles = [
|
|
|
517
517
|
level: 2,
|
|
518
518
|
player: 2,
|
|
519
519
|
max: 12,
|
|
520
|
-
|
|
520
|
+
hints: ['A1\\', 'E1/'],
|
|
521
521
|
},
|
|
522
522
|
{
|
|
523
523
|
id: 'ib3.trx',
|
|
@@ -539,7 +539,7 @@ export const puzzles = [
|
|
|
539
539
|
level: 2,
|
|
540
540
|
player: 2,
|
|
541
541
|
max: 12,
|
|
542
|
-
|
|
542
|
+
hints: ['B0/', 'C0+'],
|
|
543
543
|
},
|
|
544
544
|
{
|
|
545
545
|
id: 'ib5.trx',
|
|
@@ -572,7 +572,7 @@ export const puzzles = [
|
|
|
572
572
|
level: 2,
|
|
573
573
|
player: 2,
|
|
574
574
|
max: 10,
|
|
575
|
-
|
|
575
|
+
hints: ['A0/', 'D3/'],
|
|
576
576
|
},
|
|
577
577
|
{
|
|
578
578
|
id: 'ib8.trx',
|
|
@@ -648,8 +648,8 @@ export const puzzles = [
|
|
|
648
648
|
icon: '0ca:deef:0d',
|
|
649
649
|
level: 2,
|
|
650
650
|
player: 1,
|
|
651
|
-
max:
|
|
652
|
-
|
|
651
|
+
max: 15,
|
|
652
|
+
hints: ['B0/', 'C0\\'],
|
|
653
653
|
},
|
|
654
654
|
{
|
|
655
655
|
id: 'hw4.trx',
|
|
@@ -747,8 +747,8 @@ export const puzzles = [
|
|
|
747
747
|
icon: 'd:b:ba:aeecd:0fdec',
|
|
748
748
|
level: 1,
|
|
749
749
|
player: 2,
|
|
750
|
-
max:
|
|
751
|
-
|
|
750
|
+
max: 18,
|
|
751
|
+
hints: ['B1+', 'B2/', 'D3+'],
|
|
752
752
|
},
|
|
753
753
|
{
|
|
754
754
|
id: 'hb3.trx',
|
|
@@ -767,7 +767,7 @@ export const puzzles = [
|
|
|
767
767
|
game: 'traxloop',
|
|
768
768
|
notation: '@0+ A0/ A0/ A4/ A5+ @5/ @5/ @5+ A4/ @5/',
|
|
769
769
|
icon: '3d:3c:3e:0d1d:cbdcb',
|
|
770
|
-
level:
|
|
770
|
+
level: 3,
|
|
771
771
|
player: 1,
|
|
772
772
|
max: 15,
|
|
773
773
|
hint: 'D1\\',
|
|
@@ -778,7 +778,7 @@ export const puzzles = [
|
|
|
778
778
|
game: 'traxloop',
|
|
779
779
|
notation: '@0+ B1/ C1+ C0/ D1\\ E1\\ F1+ E0/ F1/',
|
|
780
780
|
icon: '2cdc:1dfae:ecb',
|
|
781
|
-
level:
|
|
781
|
+
level: 3,
|
|
782
782
|
player: 2,
|
|
783
783
|
max: 16,
|
|
784
784
|
hint: 'F0/',
|
|
@@ -789,7 +789,7 @@ export const puzzles = [
|
|
|
789
789
|
game: 'traxloop',
|
|
790
790
|
notation: '@0/ @1+ A0/ A0/ B2+ @2\\ @2\\ E3\\ E4+',
|
|
791
791
|
icon: '1ca:afde:1bdf:3b',
|
|
792
|
-
level:
|
|
792
|
+
level: 3,
|
|
793
793
|
player: 2,
|
|
794
794
|
max: 16,
|
|
795
795
|
hint: 'A1\\',
|
|
@@ -800,9 +800,13 @@ export const puzzles = [
|
|
|
800
800
|
game: 'traxloop',
|
|
801
801
|
notation: '@0/ B1+ C1/ C0/ C0\\ @3/ C1+ E2/ F2\\',
|
|
802
802
|
icon: '1baf:1cdca:cdecd',
|
|
803
|
-
level:
|
|
803
|
+
level: 3,
|
|
804
804
|
player: 2,
|
|
805
805
|
max: 16,
|
|
806
806
|
hint: 'B1+',
|
|
807
807
|
},
|
|
808
808
|
]
|
|
809
|
+
|
|
810
|
+
puzzles.sort(
|
|
811
|
+
(a, b) => a.level - b.level || a.max - b.max || (a.id < b.id ? -1 : 1),
|
|
812
|
+
)
|
package/src/threats.js
CHANGED
|
@@ -5,20 +5,26 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
const REGEX = {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
8
|
+
// Some of these are one for one:
|
|
9
|
+
// a: White that pairs with the next white
|
|
10
|
+
A: '[am]', // White maybe in a hollow that pairs with next white
|
|
11
|
+
// c: White that forms a connectable pair with next white
|
|
12
|
+
C: '[cp]', // White maybe in a hollow connectable pair with next white
|
|
13
|
+
l: '[lmp]', // Any White in a hollow
|
|
14
|
+
// m: "a" in a hollow
|
|
15
|
+
// p: "c" in a hollow)
|
|
16
|
+
w: '[acw]', // Any white except in hollows
|
|
17
|
+
W: '[aclmpw]', // Any white at all
|
|
12
18
|
'!': '(?:b[acw])*?', // Ripple black/white
|
|
13
19
|
'¡': '(?:[acw]b)*?', // Ripple white/black
|
|
14
20
|
'*': 'b*?', // Zero or more black pieces
|
|
15
21
|
'?': 'b?', // Zero or one black piece
|
|
16
22
|
}
|
|
17
23
|
|
|
18
|
-
|
|
24
|
+
export const Threat = class {
|
|
19
25
|
#depth = 0
|
|
20
26
|
#pattern = ''
|
|
21
|
-
#rx
|
|
27
|
+
#rx
|
|
22
28
|
#reforms = false
|
|
23
29
|
|
|
24
30
|
constructor(depth, pattern, reforms = false) {
|
|
@@ -31,7 +37,8 @@ class Threat {
|
|
|
31
37
|
rx += REGEX[char] || char
|
|
32
38
|
}
|
|
33
39
|
|
|
34
|
-
|
|
40
|
+
// Create the regex with lookahead assertions to allow overlapping matches
|
|
41
|
+
this.#rx = new RegExp('(?=(' + rx + '))', 'g')
|
|
35
42
|
}
|
|
36
43
|
|
|
37
44
|
get depth() {
|
|
@@ -65,7 +72,7 @@ class Threat {
|
|
|
65
72
|
}
|
|
66
73
|
}
|
|
67
74
|
|
|
68
|
-
|
|
75
|
+
export const ThreatsDB = class {
|
|
69
76
|
#db = {}
|
|
70
77
|
#depth = 0
|
|
71
78
|
|
|
@@ -75,6 +82,11 @@ class ThreatsDB {
|
|
|
75
82
|
}
|
|
76
83
|
}
|
|
77
84
|
|
|
85
|
+
compose(depth, pattern) {
|
|
86
|
+
pattern = pattern.replaceAll(/(?<=.)A/g, 'a').replaceAll(/W(?=.)/g, 'w')
|
|
87
|
+
return this.add(depth, pattern)
|
|
88
|
+
}
|
|
89
|
+
|
|
78
90
|
add(depth, pattern, reforms = false) {
|
|
79
91
|
this.#depth = Math.max(depth, this.#depth)
|
|
80
92
|
if (pattern in this.#db) throw new Error('Duplicate pattern:' + pattern)
|
|
@@ -97,37 +109,38 @@ class ThreatsDB {
|
|
|
97
109
|
// To make life at the REPL a little easier
|
|
98
110
|
[Symbol.for('nodejs.util.inspect.custom')](_, out) {
|
|
99
111
|
const style = out.stylize
|
|
100
|
-
const inspection = '[' + style('Threats', 'special') + '
|
|
112
|
+
const inspection = '[' + style('Threats', 'special') + ':'
|
|
101
113
|
let total = 0
|
|
102
|
-
const keys = []
|
|
114
|
+
const keys = ['{']
|
|
103
115
|
|
|
104
116
|
for (let x = 0; x <= this.#depth; x++) {
|
|
105
117
|
const count = this.level(x).length
|
|
106
|
-
keys.push(style(x, 'string') + ':
|
|
118
|
+
keys.push(style(x, 'string') + ':' + style(count, 'number'))
|
|
107
119
|
total += count
|
|
108
120
|
}
|
|
109
121
|
|
|
110
|
-
return inspection +
|
|
122
|
+
return inspection + style(total, 'number') + '] ' + keys.join(' ') + ' }'
|
|
111
123
|
}
|
|
112
124
|
}
|
|
113
125
|
|
|
114
126
|
const pre = true // This pattern can appear first in an L threat
|
|
115
127
|
const post = true // This pattern can come second in an L threat
|
|
116
128
|
const corners = {
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
'ab?
|
|
122
|
-
|
|
123
|
-
|
|
129
|
+
ArW: { pre, post }, // Simple corner
|
|
130
|
+
AbrW: { post }, // Wide-first corner
|
|
131
|
+
ArbW: { pre }, // Wide-second corner
|
|
132
|
+
AbrbW: {}, // Wide corner
|
|
133
|
+
'ab?bW': { pre, post }, // Flat corner and wide flat corner
|
|
134
|
+
CW: { pre, post }, // Connectable pair
|
|
135
|
+
CbW: {}, // Wide connectable pair
|
|
124
136
|
}
|
|
125
137
|
|
|
126
138
|
export const threats = new ThreatsDB(Object.keys(corners))
|
|
127
139
|
|
|
128
140
|
// Attacks
|
|
129
141
|
const attacks = [
|
|
130
|
-
'a?
|
|
142
|
+
'a?W', // Simple attack and wide attack
|
|
143
|
+
'm?W', // Simple and wide attack in a corner
|
|
131
144
|
]
|
|
132
145
|
attacks.map((a) => threats.add(1, a))
|
|
133
146
|
|
|
@@ -135,34 +148,54 @@ attacks.map((a) => threats.add(1, a))
|
|
|
135
148
|
const reformAttacks = [
|
|
136
149
|
'rm?lr', // Cave/wide cave attack
|
|
137
150
|
'rbm?lbr', // Deep cave/deep wide cave attack (impossibly rare?)
|
|
138
|
-
'
|
|
151
|
+
'ralmrW', // Attack at top of cave
|
|
139
152
|
'arlmwr', // Reversed
|
|
140
153
|
]
|
|
141
154
|
reformAttacks.map((ra) => threats.add(1, ra, true))
|
|
142
155
|
|
|
143
156
|
const reformThreats = [
|
|
144
157
|
'rp?lr', // Cave/wide cave threat
|
|
145
|
-
'rbp?lbr', // Deep cave/
|
|
158
|
+
'rbp?lbr', // Deep cave/deep wide cave threat (impossibly rare?)
|
|
159
|
+
'rmbb???w', // Black corner with a long white loop potential
|
|
160
|
+
'abb???lr', // Reverse
|
|
146
161
|
]
|
|
147
162
|
reformThreats.map((rt) => threats.add(2, rt))
|
|
148
163
|
|
|
149
164
|
// Create multi-stage threats
|
|
150
165
|
for (const one of Object.keys(corners)) {
|
|
166
|
+
const start = one.replace('W', 'l')
|
|
167
|
+
let end = one.replace(/[aA]/, 'm').replace('C', 'p')
|
|
168
|
+
const pre = corners[one].post ? '?' : ''
|
|
169
|
+
let post = corners[one].pre ? '?' : ''
|
|
170
|
+
threats.compose(3, start + pre + '¡rm?W') // Attacking 3-stage threat
|
|
171
|
+
threats.compose(3, 'a?lr!' + post + end) // Reverse
|
|
172
|
+
threats.compose(3, one + '¡ab*bwr') // Corner by reforming hollow potential
|
|
173
|
+
threats.compose(3, 'rab*bw!' + one) // Reverse
|
|
174
|
+
threats.compose(3, one + '!brm*bW') // Corner by hollow attack, defends to edge
|
|
175
|
+
threats.compose(3, one + '!bbrmW') // Same, but slightly different layout
|
|
176
|
+
threats.compose(3, 'ab*lrb¡' + one) // Reverse
|
|
177
|
+
threats.compose(3, 'alrbb¡' + one) // Reverse of different layout
|
|
178
|
+
|
|
151
179
|
for (const two of Object.keys(corners)) {
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
if (
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
threats.add(2, start + pre + '¡' + two) // Make an L
|
|
159
|
-
threats.add(3, start + pre + 'rb¡' + two) // Make a three-stage threat
|
|
160
|
-
const end = two.replace('a', 'm').replace('c', 'p')
|
|
161
|
-
threats.add(2, one + post + '!' + end) // L
|
|
162
|
-
threats.add(3, one + post + '!br' + end) // Three-stage
|
|
180
|
+
end = two.replace(/[aA]/, 'm').replace('C', 'p')
|
|
181
|
+
post = corners[two].pre ? '?' : ''
|
|
182
|
+
if (pre) {
|
|
183
|
+
threats.compose(2, start + pre + '¡' + two) // Make an L
|
|
184
|
+
threats.compose(3, start + pre + 'rb¡' + two) // Make a three-stage threat
|
|
185
|
+
threats.compose(3, start + '?!rb¡' + two) // Attack to form an L threat
|
|
163
186
|
}
|
|
164
187
|
|
|
165
|
-
|
|
166
|
-
|
|
188
|
+
if (post) {
|
|
189
|
+
threats.compose(2, one + post + '!' + end) // L
|
|
190
|
+
threats.compose(3, one + post + '!br' + end) // Three-stage
|
|
191
|
+
threats.compose(3, one + '!br¡?' + end) // Attack to form an L threat
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
threats.compose(3, one + '!b*b¡' + two) // Edge threat
|
|
167
195
|
}
|
|
168
196
|
}
|
|
197
|
+
|
|
198
|
+
threats.add(3, 'ralbmrW') // Attack into a wide cave
|
|
199
|
+
threats.add(3, 'Arlbmwr') // Reversed
|
|
200
|
+
|
|
201
|
+
threats.add(4, 'ArlbmrW') // Wide cave ready for an attack
|
package/src/tty.js
CHANGED
|
@@ -22,7 +22,7 @@ import { Trax } from '@slugbugblue/trax'
|
|
|
22
22
|
const esc = '\u001B[' // Start of console color escape code
|
|
23
23
|
const off = esc + 'm' // Color reset code
|
|
24
24
|
const ENV = process.env
|
|
25
|
-
const NERD = ENV.NERDFONT
|
|
25
|
+
const NERD = ENV.NERDFONT || ENV.NERDFONTS
|
|
26
26
|
const POWER = ENV.POWERLINE || ENV.P9K_TTY || ENV.P9K_SSH
|
|
27
27
|
const leftBubble = NERD ? '' : POWER ? '' : '▐'
|
|
28
28
|
const rightBubble = NERD ? '' : POWER ? '' : '▌'
|
package/src/types.d.ts
CHANGED
|
@@ -25,12 +25,56 @@ type Colorer = {
|
|
|
25
25
|
white: Colorize
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
+
/** An object representing a space on the perimeter of a Trax position.
|
|
29
|
+
* x,y: the coordinates of the space
|
|
30
|
+
* c: the color present beside the space (w, b, l: both w and b, r: none)
|
|
31
|
+
* t: the type of possible moves: n-normal, x-none, c-cave, C-restricted cave
|
|
32
|
+
* b,w: if present, the number of the line to match with its other end
|
|
33
|
+
* pb, pw: if present, can pair with the next line in one turn
|
|
34
|
+
* xb, xw: the label of this space for each color
|
|
35
|
+
*/
|
|
36
|
+
type EdgeLocation = {
|
|
37
|
+
x: number
|
|
38
|
+
y: number
|
|
39
|
+
c: string
|
|
40
|
+
t: string
|
|
41
|
+
b?: number
|
|
42
|
+
w?: number
|
|
43
|
+
pb?: boolean
|
|
44
|
+
pw?: boolean
|
|
45
|
+
zb?: string
|
|
46
|
+
zw?: string
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
type EdgeObject = {
|
|
50
|
+
b: string
|
|
51
|
+
w: string
|
|
52
|
+
edge: RawEdge
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Object representing a concrete threat found in a position. */
|
|
56
|
+
type FoundThreat = { threat: string; match: string; at: number; value: number }
|
|
57
|
+
|
|
58
|
+
/** An object with keys representing each level, with arrays for each threat of
|
|
59
|
+
* that level. */
|
|
60
|
+
type FoundThreats = Record<string, FoundThreat[]>
|
|
61
|
+
|
|
62
|
+
/** All the threats found for both the white and black player. */
|
|
63
|
+
type FoundThreatsCollection = { b: ColorThreats; w: ColorThreats }
|
|
64
|
+
|
|
28
65
|
/** A point-like object has numerical x and y properties. */
|
|
29
66
|
type PointLike = {
|
|
30
67
|
x: number
|
|
31
68
|
y: number
|
|
32
69
|
}
|
|
33
70
|
|
|
71
|
+
/** An object used to track an analysis on a position. */
|
|
72
|
+
type PositionScore = {
|
|
73
|
+
move: string
|
|
74
|
+
score: number
|
|
75
|
+
analysis?: Analysis
|
|
76
|
+
}
|
|
77
|
+
|
|
34
78
|
/** A puzzle. */
|
|
35
79
|
type Puzzle = {
|
|
36
80
|
id: string
|
|
@@ -53,6 +97,10 @@ type PuzzleSource = {
|
|
|
53
97
|
url?: string
|
|
54
98
|
}
|
|
55
99
|
|
|
100
|
+
/** Array of objects representing the spaces surrounding the perimeter of a
|
|
101
|
+
* Trax position. */
|
|
102
|
+
type RawEdge = EdgeLocation[]
|
|
103
|
+
|
|
56
104
|
/** Treat the save state as an opaque object,
|
|
57
105
|
* produced by save() and fed into restore().
|
|
58
106
|
*/
|
|
@@ -76,6 +124,14 @@ type SaveState = {
|
|
|
76
124
|
*/
|
|
77
125
|
type Slash = '/' | '\\' | '+'
|
|
78
126
|
|
|
127
|
+
/** Threat definition. */
|
|
128
|
+
type Threat = {
|
|
129
|
+
depth: number
|
|
130
|
+
pattern: string
|
|
131
|
+
rx: RegExp
|
|
132
|
+
value: number
|
|
133
|
+
}
|
|
134
|
+
|
|
79
135
|
/** A single tile on the board. */
|
|
80
136
|
type Tile = {
|
|
81
137
|
id: TileId
|
package/src/version.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// Generated by genversion.
|
|
2
|
-
export const version = '0.
|
|
2
|
+
export const version = '0.15.0'
|