@slugbugblue/trax 0.10.2 → 0.11.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 +11 -1
- package/README.md +4 -4
- package/docs/analyst.md +160 -0
- package/docs/engine.md +26 -10
- package/docs/point.md +8 -2
- package/package.json +11 -3
- package/src/analyst.js +395 -0
- package/src/cli.js +10 -0
- package/src/cmds/analyze.js +92 -0
- package/src/cmds/play-try.js +3 -10
- package/src/engine.js +40 -15
- package/src/threats.js +164 -0
- package/src/version.js +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,12 +1,22 @@
|
|
|
1
1
|
# @slugbugblue/trax
|
|
2
2
|
|
|
3
|
+
## 0.11.0 - 2022-09-22
|
|
4
|
+
|
|
5
|
+
- Breaking change: Refactor `play()` and `playMove()` into a single function
|
|
6
|
+
`play()`. `playMove()` is no longer available.
|
|
7
|
+
- Add a new function `playMoves()` to submit multiple moves at once
|
|
8
|
+
- Initial work on `analyst.js`, with documentation and a start on tests
|
|
9
|
+
|
|
10
|
+
- use `analyze(trax)` to perform an analysis
|
|
11
|
+
- added the `analyze` CLI command to see an analysis
|
|
12
|
+
|
|
3
13
|
## 0.10.1 - 0.10.2 - 2022-06-12
|
|
4
14
|
|
|
5
15
|
- Added an entry point for the `Point` class at `@slugbugblue/trax/point`
|
|
6
16
|
|
|
7
17
|
## 0.10.0 - 2022-06-12
|
|
8
18
|
|
|
9
|
-
- Added `point.in(
|
|
19
|
+
- Added `point.in(topLeft, bottomRight)` to see if a point is inside a
|
|
10
20
|
rectangular bounded area
|
|
11
21
|
- Minor coding style changes due to updated `xo` rules
|
|
12
22
|
|
package/README.md
CHANGED
|
@@ -117,9 +117,10 @@ puzzle.play('@2\\')
|
|
|
117
117
|
|
|
118
118
|
For specifics, see the [engine.js documentation][engine-docs].
|
|
119
119
|
|
|
120
|
-
##
|
|
120
|
+
## Roadmap
|
|
121
121
|
|
|
122
|
-
-
|
|
122
|
+
- Game position analysis
|
|
123
|
+
- Puzzles and Puzzlebot
|
|
123
124
|
|
|
124
125
|
## Support
|
|
125
126
|
|
|
@@ -139,7 +140,7 @@ his generosity in allowing me to add the game to [GoldToken.com][goldtoken].
|
|
|
139
140
|
Thanks to his widow Colleen Foley-Smith for extending that courtesy to allow me
|
|
140
141
|
to include Trax on [slugbugblue.com][sbb] as well.
|
|
141
142
|
|
|
142
|
-
I would also like to
|
|
143
|
+
I would also like to express my huge heartfelt appreciation to the magnanimous
|
|
143
144
|
[Donald G. Bailey][dgb], who, through his excellent book [Trax Strategy for
|
|
144
145
|
Beginners][traxbook], taught me the basics (and more!) of Trax. I found him
|
|
145
146
|
always ready to share his insights on the mechanics of the game, as well as
|
|
@@ -179,7 +180,6 @@ David Smith and heirs, and are not to be used without permission.
|
|
|
179
180
|
[repo]: https://gitlab.com/slugbugblue/trax
|
|
180
181
|
[sbb]: https://slugbugblue.com/
|
|
181
182
|
[traxbook]: http://traxgame.com/shop_book.php
|
|
182
|
-
[traxbot]: https://gitlab.com/slugbugblue/traxbot
|
|
183
183
|
[traxgame]: http://traxgame.com/
|
|
184
184
|
[xdg]:
|
|
185
185
|
https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html
|
package/docs/analyst.md
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
# analyst.js API documentation
|
|
2
|
+
|
|
3
|
+
This module provides additional analysis of Trax games, which can be used in the
|
|
4
|
+
CLI and for bots.
|
|
5
|
+
|
|
6
|
+
## Example usage
|
|
7
|
+
|
|
8
|
+
```javascript
|
|
9
|
+
import { Trax } from '@slugbugblue/trax'
|
|
10
|
+
import { analyze } from '@slugbugblue/trax/analyst'
|
|
11
|
+
|
|
12
|
+
const trax = new Trax('trax', '@0/ @1/ B0\\')
|
|
13
|
+
|
|
14
|
+
const analysis = analyze(trax)
|
|
15
|
+
|
|
16
|
+
console.log(JSON.stringify(analysis.edge, null, 2))
|
|
17
|
+
console.log(JSON.stringify(analysis.threats, null, 2))
|
|
18
|
+
console.log(JSON.stringify(analysis.scores, null, 2))
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## API
|
|
22
|
+
|
|
23
|
+
### Analysis class
|
|
24
|
+
|
|
25
|
+
Available either as `Analysis` to be used with `new`, or it can also be returned
|
|
26
|
+
using the `analyze` shortcut function.
|
|
27
|
+
|
|
28
|
+
Provides important properties and functions to understand the current state of a
|
|
29
|
+
Trax game.
|
|
30
|
+
|
|
31
|
+
#### constructor
|
|
32
|
+
|
|
33
|
+
##### `new Analysis(game)`
|
|
34
|
+
|
|
35
|
+
##### `analyze(game)`
|
|
36
|
+
|
|
37
|
+
Provide a Trax `game`, ideally with at least one move and not yet completed.
|
|
38
|
+
|
|
39
|
+
#### computed properties
|
|
40
|
+
|
|
41
|
+
##### `game`
|
|
42
|
+
|
|
43
|
+
The Trax object that represents the position originally analyzed.
|
|
44
|
+
|
|
45
|
+
##### `edge`
|
|
46
|
+
|
|
47
|
+
An object with two properties, `b` and `w`, which are string representations of
|
|
48
|
+
the traced edge, with one character per space, along with an array named `edge`
|
|
49
|
+
which contains the full internal details of each edge space. This object is
|
|
50
|
+
tightly tied to the internal representation of the edge tracing algorithm and as
|
|
51
|
+
such is subject to change in future iterations.
|
|
52
|
+
|
|
53
|
+
Edge tracing begins in the top left of the tiles (ie, space A0 or the nearest
|
|
54
|
+
playable space to it) and walks clockwise around the perimeter of the played
|
|
55
|
+
tiles, recording information about the lines that lead to each space. Note that
|
|
56
|
+
the edge spaces are just outside the existing tiles, not on the tiles
|
|
57
|
+
themselves. The following keys are present in each edge space representation:
|
|
58
|
+
|
|
59
|
+
- `x` and `y`: The location of the edge space, which can be directly used to
|
|
60
|
+
create a `Point` for use in any function that takes a location.
|
|
61
|
+
- `c`: The color of the line that leads into this space. This can be `b` or `w`
|
|
62
|
+
or `l` or `r`, the latter two representing both black and white lines ("left
|
|
63
|
+
turn") or neither ("right turn").
|
|
64
|
+
- `t`: The type of space, which is `n` for a normal space, `c` for a space
|
|
65
|
+
inside a cave that has limits on which tiles can be legally played in it, or
|
|
66
|
+
`x` if there are no legal moves available for this space, which can happen
|
|
67
|
+
both in caves or in 8x8 Trax when the game size limits have been reached.
|
|
68
|
+
- `w` and `b`: True if the next line of the same color is the other end of this
|
|
69
|
+
line.
|
|
70
|
+
- `nw` and `nb`: The number of the white and/or black lines coming to this
|
|
71
|
+
space, which can then be used to find the other end of the line.
|
|
72
|
+
- `pb` and `pw`: True if the next line of the same color is part of a
|
|
73
|
+
connectable pair of lines with this color.
|
|
74
|
+
- `zb` and `zw`: The character used in the black or white string representation
|
|
75
|
+
of this space.
|
|
76
|
+
|
|
77
|
+
For the edge string, the following characters are used as a hopefully useful
|
|
78
|
+
shorthand summary of the tiles surrounding the space. To make regex pattern
|
|
79
|
+
matching simpler, the black string is constructed by first swapping the colors,
|
|
80
|
+
so that it appears as though all black tiles are white tiles.
|
|
81
|
+
|
|
82
|
+
- `b`: a black line enters this space.
|
|
83
|
+
- `w`: a white line enters this space.
|
|
84
|
+
- `l`: both a black and a white line enter this space.
|
|
85
|
+
- `a`: a white line enters this space, and the next white line in the edge is
|
|
86
|
+
the other end of this line.
|
|
87
|
+
- 'c': a white line enters this space, and the next white line in the edge forms
|
|
88
|
+
a connectable pair with this line.
|
|
89
|
+
- `m`: both a black and a white line enter this space, and the next white line
|
|
90
|
+
in the edge is the other end of the white line.
|
|
91
|
+
- `p`: both a black and a white line enter this space, and the next while line
|
|
92
|
+
in the edge forms a connectable pair with the white line.
|
|
93
|
+
- `r`: no lines enter this space, but it represents a right-corner in the edge.
|
|
94
|
+
|
|
95
|
+
##### `threats`
|
|
96
|
+
|
|
97
|
+
Analyzes the existing threats for the current game position. Returns an object
|
|
98
|
+
with both `b` and `w` keys, where each is an object with zero or more "depth"
|
|
99
|
+
keys. Threats of depth 0 are corners or connectable-pairs. Threats of depth 1
|
|
100
|
+
are immediate attacks. Threats of depth 2 are Ls. etc. Note that the threats are
|
|
101
|
+
not checked for faultiness at this time. In other words, there may be a faulty L
|
|
102
|
+
listed as a threat even if it is not currently possible to activate the L
|
|
103
|
+
successfully.
|
|
104
|
+
|
|
105
|
+
The "depth" keys are arrays of the threats at that level, which means that a
|
|
106
|
+
simple count of the threats at each level can give a good first guess at the
|
|
107
|
+
"score" of the current position. Each element of the array is an object with the
|
|
108
|
+
following keys:
|
|
109
|
+
|
|
110
|
+
- `pattern`: The threat pattern as it was fed into the threats database. This is
|
|
111
|
+
a simple edge string.
|
|
112
|
+
- `match`: The portion of the actual edge string that matched the pattern, which
|
|
113
|
+
may or may not be exactly the same as the pattern.
|
|
114
|
+
- `at`: The index of the edge string at which the pattern match began.
|
|
115
|
+
|
|
116
|
+
##### `score`
|
|
117
|
+
|
|
118
|
+
Computes a rough "score" of the current position using the list of threats. A
|
|
119
|
+
positive score means that the player whose turn it is has a potential advantage,
|
|
120
|
+
with the larger the number of higher the advantage. A negative score indicates a
|
|
121
|
+
disadvantage.
|
|
122
|
+
|
|
123
|
+
##### `scores`
|
|
124
|
+
|
|
125
|
+
Returns the same score as above, but packaged into an object with both a `b` and
|
|
126
|
+
`w` key, to more easily grab the score of a particular player without regard to
|
|
127
|
+
whose turn it is. Note that `b` will not necessarily be equal to `w` times -1
|
|
128
|
+
because the scores can change depending on who has the initiative.
|
|
129
|
+
|
|
130
|
+
#### methods
|
|
131
|
+
|
|
132
|
+
##### `inCave(loc)`
|
|
133
|
+
|
|
134
|
+
For any given location, returns true if that location is found within a cave.
|
|
135
|
+
|
|
136
|
+
##### `spaceType(loc)`
|
|
137
|
+
|
|
138
|
+
For any given location, determine the type of playable space. Returns one of:
|
|
139
|
+
|
|
140
|
+
- `n`: This location can be played in normally.
|
|
141
|
+
- `x`: A move is not valid in this location.
|
|
142
|
+
- `c`: This location is in a cave, but otherwise has no restrictions on its
|
|
143
|
+
play.
|
|
144
|
+
- `C`: This location is in a cave, and certain plays at this position result in
|
|
145
|
+
illegal moves.
|
|
146
|
+
|
|
147
|
+
## License
|
|
148
|
+
|
|
149
|
+
Copyright 2019-2022 Chad Transtrum
|
|
150
|
+
|
|
151
|
+
Licensed under the Apache License, Version 2.0 (the "License"); you may not use
|
|
152
|
+
the files in this project except in compliance with the License. You may obtain
|
|
153
|
+
a copy of the License at
|
|
154
|
+
|
|
155
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
156
|
+
|
|
157
|
+
Unless required by applicable law or agreed to in writing, software distributed
|
|
158
|
+
under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
|
|
159
|
+
CONDITIONS OF ANY KIND, either express or implied. See the License for the
|
|
160
|
+
specific language governing permissions and limitations under the License.
|
package/docs/engine.md
CHANGED
|
@@ -167,8 +167,16 @@ each move that can be played will be legal.
|
|
|
167
167
|
|
|
168
168
|
##### `play(notation)`
|
|
169
169
|
|
|
170
|
-
|
|
171
|
-
|
|
170
|
+
##### `play(moveNumber, notation)`
|
|
171
|
+
|
|
172
|
+
Attempts to play the given `notation`. If `moveNumber` is provided, add a
|
|
173
|
+
safeguard around the move to ensure that the move is being submitted against the
|
|
174
|
+
expected position, which can be useful in the context of asynchronous
|
|
175
|
+
communication and retries, such as when this engine is being used as the back
|
|
176
|
+
end of a web interface. For example, if no moves have been made yet in a game,
|
|
177
|
+
this can be called either as `play(1, '@0/')` or `play(1, '@0+')`.
|
|
178
|
+
|
|
179
|
+
Updates the game instance if the notation is valid for the current position.
|
|
172
180
|
|
|
173
181
|
Returns an object with at minimum the following key/value pair:
|
|
174
182
|
|
|
@@ -179,16 +187,24 @@ If the move is valid, the following will also be part of the returned object:
|
|
|
179
187
|
- `dropped`: a list of the tiles that were played by this move
|
|
180
188
|
- `notation`: the resulting notation of the move
|
|
181
189
|
|
|
182
|
-
##### `
|
|
190
|
+
##### `playMoves(moves)`
|
|
183
191
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
192
|
+
Can be used to enter multiple `moves` at once. Pass in either a string of
|
|
193
|
+
space-separated notations or an array of notations. In both cases move numbers
|
|
194
|
+
can be optionally included, in which case the moves are not made if the move
|
|
195
|
+
numbers do not match the board position. This function tries to be as forgiving
|
|
196
|
+
as possible of format.
|
|
197
|
+
|
|
198
|
+
For example, if a new trax object with zero moves were just created, each of the
|
|
199
|
+
following statements would put the game in the same position:
|
|
190
200
|
|
|
191
|
-
|
|
201
|
+
```javascript
|
|
202
|
+
trax.playMoves('@0/ A0+ B1/')
|
|
203
|
+
trax.playMoves(['@0/', 'a0+', 'B1/'])
|
|
204
|
+
trax.playMoves('1 @0/ 2. a0+ 3 b1/')
|
|
205
|
+
trax.playMoves([1, '@0/', '2', 'A0+', '3.', 'B1/')
|
|
206
|
+
trax.playMoves(['1. @0/', 'a0+', 3, 'b1/')
|
|
207
|
+
```
|
|
192
208
|
|
|
193
209
|
##### `provisionalMove(from, to, via)`
|
|
194
210
|
|
package/docs/point.md
CHANGED
|
@@ -49,6 +49,8 @@ an `x` and a `y` key with numeric values), or from individual `x` and `y`
|
|
|
49
49
|
numbers. Once a point is created, it cannot be changed; however, you can use its
|
|
50
50
|
functions to return other points relative to it.
|
|
51
51
|
|
|
52
|
+
#### properties
|
|
53
|
+
|
|
52
54
|
##### `x` and `y` immutable properties
|
|
53
55
|
|
|
54
56
|
You can return the individual `x` or `y` property of a point by accessing it
|
|
@@ -64,6 +66,8 @@ points.
|
|
|
64
66
|
|
|
65
67
|
Or, get an array of all four cardinal points around the existing point.
|
|
66
68
|
|
|
69
|
+
#### methods
|
|
70
|
+
|
|
67
71
|
##### `dir(direction)`
|
|
68
72
|
|
|
69
73
|
Pass in a string named direction (ie, one of `up`, `down`, `left`, or `right`)
|
|
@@ -111,11 +115,13 @@ default. For example:
|
|
|
111
115
|
|
|
112
116
|
```javascript
|
|
113
117
|
const zero = new Point(0, 0)
|
|
114
|
-
const json = JSON.stringify(zero)
|
|
115
|
-
const jsonzero = JSON.parse(json)
|
|
118
|
+
const json = JSON.stringify(zero) // '{"x":0,"y":0}'
|
|
119
|
+
const jsonzero = JSON.parse(json) // { x: 0, y: 0 }
|
|
116
120
|
|
|
117
121
|
console.log(zero.eq(jsonzero)) // true
|
|
118
122
|
jsonzero.eq(zero) // TypeError: jsonzero.eq is not a function
|
|
123
|
+
|
|
124
|
+
// However, the object can easily be turned back into a Point:
|
|
119
125
|
const reconstituted = new Point(jsonzero)
|
|
120
126
|
console.log(reconstituted.eq(zero)) // true
|
|
121
127
|
```
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@slugbugblue/trax",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0",
|
|
4
4
|
"description": "Trax game engine and friends",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"trax",
|
|
@@ -13,7 +13,14 @@
|
|
|
13
13
|
"author": "Chad Transtrum <chad@transtrum.net>",
|
|
14
14
|
"exports": {
|
|
15
15
|
".": "./src/engine.js",
|
|
16
|
-
"./
|
|
16
|
+
"./analyst": "./src/analyst.js",
|
|
17
|
+
"./analyst.js": "./src/analyst.js",
|
|
18
|
+
"./point": "./src/point.js",
|
|
19
|
+
"./point.js": "./src/point.js",
|
|
20
|
+
"./threats": "./src/threats.js",
|
|
21
|
+
"./threats.js": "./src/threats.js",
|
|
22
|
+
"./tty": "./src/tty.js",
|
|
23
|
+
"./tty.js": "./src/tty.js"
|
|
17
24
|
},
|
|
18
25
|
"repository": "gitlab:slugbugblue/trax",
|
|
19
26
|
"bin": {
|
|
@@ -22,7 +29,8 @@
|
|
|
22
29
|
"scripts": {
|
|
23
30
|
"test": "xo && c8 ava",
|
|
24
31
|
"preversion": "npm test",
|
|
25
|
-
"
|
|
32
|
+
"version": "genversion --es6 src/version.js && git add src/version.js",
|
|
33
|
+
"postversion": "git push && git push --tags && npm publish"
|
|
26
34
|
},
|
|
27
35
|
"dependencies": {
|
|
28
36
|
"env-paths": "^3.0.0",
|
package/src/analyst.js
ADDED
|
@@ -0,0 +1,395 @@
|
|
|
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
|
+
// Analyst module: provides analysis functions for Trax games
|
|
17
|
+
|
|
18
|
+
import { Point } from '@slugbugblue/trax/point.js'
|
|
19
|
+
import { Trax } from '@slugbugblue/trax'
|
|
20
|
+
|
|
21
|
+
import { threats } from '@slugbugblue/trax/threats.js'
|
|
22
|
+
|
|
23
|
+
// Helpers
|
|
24
|
+
|
|
25
|
+
// Make parseInt Array.map()-friendly
|
|
26
|
+
const int = (s) => Number.parseInt(s, 10)
|
|
27
|
+
|
|
28
|
+
const leftTurn = { d: 'r', l: 'd', r: 'u', u: 'l' }
|
|
29
|
+
const rightTurn = { d: 'l', l: 'u', r: 'd', u: 'r' }
|
|
30
|
+
const follow = { d: 'right', l: 'bottom', r: 'top', u: 'left' }
|
|
31
|
+
|
|
32
|
+
const edgeColor = (tile, dir) => {
|
|
33
|
+
return { d: 'ace', l: 'abd', r: 'bcf', u: 'def' }[dir].includes(tile)
|
|
34
|
+
? 'w'
|
|
35
|
+
: 'b'
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const encodeNumber = (n) => String.fromCodePoint(48 + n)
|
|
39
|
+
|
|
40
|
+
// Explore a raw edge and update cave information
|
|
41
|
+
const spelunk = (rawEdge) => {
|
|
42
|
+
for (let index = 1; index < rawEdge.length - 1; index++) {
|
|
43
|
+
if (rawEdge[index].t === 'C') {
|
|
44
|
+
const previous = rawEdge[index - 1] || {}
|
|
45
|
+
const next = rawEdge[index + 1] || {}
|
|
46
|
+
if (next.t === 'c') next.t = 'C'
|
|
47
|
+
if (previous.t === 'c') {
|
|
48
|
+
previous.t = 'C'
|
|
49
|
+
if (index > 1) index -= 2
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
for (const space of rawEdge) {
|
|
55
|
+
if (space.t === 'c') space.t = 'n'
|
|
56
|
+
if (space.t === 'C') space.t = 'c'
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return rawEdge
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Combine hollows into a single item in the raw edge array
|
|
63
|
+
const hollowOut = (rawEdge) => {
|
|
64
|
+
for (let index = 1; index < rawEdge.length - 1; index++) {
|
|
65
|
+
const space = rawEdge[index]
|
|
66
|
+
if (space.c === 'l') {
|
|
67
|
+
Object.assign(space, rawEdge[index - 1], rawEdge[index + 1])
|
|
68
|
+
space.c = 'l'
|
|
69
|
+
rawEdge.splice(index + 1, 1)
|
|
70
|
+
rawEdge.splice(index - 1, 1)
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return rawEdge
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Supposedly deep nesting is bad, so we broke this out of "pairUp" for
|
|
78
|
+
// "better" code, even though this is useless on its own ...
|
|
79
|
+
const findPair = (rawEdge, index, color, pairs) => {
|
|
80
|
+
const colors = new Set([color, 'l'])
|
|
81
|
+
const space = rawEdge[index]
|
|
82
|
+
const nColor = 'n' + color
|
|
83
|
+
for (const x of [1, 2]) {
|
|
84
|
+
let j = x + index
|
|
85
|
+
if (j >= rawEdge.length) j -= rawEdge.length
|
|
86
|
+
const next = rawEdge[j]
|
|
87
|
+
if (colors.has(next.c) && space[nColor] !== next[nColor]) {
|
|
88
|
+
const one = color + space[nColor]
|
|
89
|
+
const two = color + next[nColor]
|
|
90
|
+
pairs[one + '-' + two] = index
|
|
91
|
+
if (two + '-' + one in pairs) {
|
|
92
|
+
for (const y of [index, pairs[two + '-' + one]]) {
|
|
93
|
+
rawEdge[y]['p' + color] = true
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Only process one connection
|
|
98
|
+
break
|
|
99
|
+
} else if (next.c === 'r') {
|
|
100
|
+
// Cannot connect past an r space
|
|
101
|
+
break
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
for (let x = 1; x < rawEdge.length; x++) {
|
|
106
|
+
let j = index + x
|
|
107
|
+
if (j >= rawEdge.length) j -= rawEdge.length
|
|
108
|
+
if (colors.has(rawEdge[j].c)) {
|
|
109
|
+
space[color] = rawEdge[j][nColor] === space[nColor]
|
|
110
|
+
break
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Find connectable pairs in the raw edge
|
|
116
|
+
const pairUp = (rawEdge) => {
|
|
117
|
+
const pairs = {}
|
|
118
|
+
for (const [index, space] of rawEdge.entries()) {
|
|
119
|
+
for (const bw of ['w', 'b']) {
|
|
120
|
+
const colors = [bw, 'l']
|
|
121
|
+
if (colors.includes(space.c)) {
|
|
122
|
+
findPair(rawEdge, index, bw, pairs)
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return rawEdge
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Label and finalize the raw edge
|
|
131
|
+
const label = (rawEdge) => {
|
|
132
|
+
const final = { w: '', b: '', edge: rawEdge }
|
|
133
|
+
for (const space of rawEdge) {
|
|
134
|
+
for (const bw of ['b', 'w']) {
|
|
135
|
+
const colors = new Set([bw, 'l'])
|
|
136
|
+
let z = 'w'
|
|
137
|
+
if (colors.has(space.c)) {
|
|
138
|
+
if (space['p' + bw]) z = 'c'
|
|
139
|
+
if (space[bw]) z = 'a'
|
|
140
|
+
if (space.c === 'l') z = { w: 'l', a: 'm', c: 'p' }[z]
|
|
141
|
+
} else {
|
|
142
|
+
z = space.c === 'r' ? 'r' : 'b'
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (space.t === 'x') z = 'x'
|
|
146
|
+
space['z' + bw] = z
|
|
147
|
+
final[bw] += z
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return final
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const attackScore = (from, level) => {
|
|
155
|
+
const count = from[level]?.length ?? 0
|
|
156
|
+
if (level < 1) return count
|
|
157
|
+
return count * 10 ** (threats.maxDepth - level + 1)
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Shortcut to get the number of active threats
|
|
161
|
+
const threatCount = (from) =>
|
|
162
|
+
Object.entries(from).filter(([d, l]) => d > 0 && l.length > 0).length
|
|
163
|
+
|
|
164
|
+
// Analysis class
|
|
165
|
+
export const Analysis = class {
|
|
166
|
+
#edge = undefined
|
|
167
|
+
#game = {}
|
|
168
|
+
#move = 0
|
|
169
|
+
#save = {}
|
|
170
|
+
#scores = {}
|
|
171
|
+
#threats = undefined
|
|
172
|
+
|
|
173
|
+
constructor(game) {
|
|
174
|
+
// Make a copy of the game
|
|
175
|
+
this.#game = game
|
|
176
|
+
this.#save = game.save()
|
|
177
|
+
this.#move = game.move
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
get game() {
|
|
181
|
+
// Return a fresh copy of the game as presented on init
|
|
182
|
+
if (this.#game.move !== this.#move) {
|
|
183
|
+
this.#game = new Trax().restore(this.#save)
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return this.#game
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
get edge() {
|
|
190
|
+
if (!this.#edge) {
|
|
191
|
+
// Cache the edge
|
|
192
|
+
this.#edge = this.#traceEdge()
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return this.#edge
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
get threats() {
|
|
199
|
+
if (!this.#threats) {
|
|
200
|
+
this.#threats = { b: this.#applyThreats('b'), w: this.#applyThreats('w') }
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return this.#threats
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Is a given location in a cave?
|
|
207
|
+
inCave(loc) {
|
|
208
|
+
const { game } = this
|
|
209
|
+
|
|
210
|
+
if (game.tileAt(loc)) {
|
|
211
|
+
return false
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const topLeft = new Point(game.left, game.top)
|
|
215
|
+
const bottomRight = new Point(game.right, game.bottom)
|
|
216
|
+
let count = 0
|
|
217
|
+
|
|
218
|
+
for (const dir of Point.dirs) {
|
|
219
|
+
let look = loc.dir(dir)
|
|
220
|
+
while (look.in(topLeft, bottomRight) && !game.tileAt(loc)) {
|
|
221
|
+
look = look.dir(dir)
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
if (game.tileAt(look)) count++
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
return count > 2
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
#score(color) {
|
|
231
|
+
if (this.#scores[color] === undefined) {
|
|
232
|
+
const { game } = this
|
|
233
|
+
if (game.over) {
|
|
234
|
+
this.#scores[game.color] = Number.POSITIVE_INFINITY
|
|
235
|
+
this.#scores[Trax.other(game.color)] = Number.NEGATIVE_INFINITY
|
|
236
|
+
return this.#scores[color]
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
let score = 0
|
|
240
|
+
|
|
241
|
+
const active = this.threats[game.color]
|
|
242
|
+
const inactive = this.threats[Trax.other(game.color)]
|
|
243
|
+
let advantage = Number.POSITIVE_INFINITY
|
|
244
|
+
|
|
245
|
+
// First examine the player who has the initiative
|
|
246
|
+
for (const level of Object.keys(active).map((i) => int(i))) {
|
|
247
|
+
if (level > 0 && level < advantage) advantage = level
|
|
248
|
+
score += attackScore(active, level)
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const passive = game.color !== color
|
|
252
|
+
const calc =
|
|
253
|
+
!passive ||
|
|
254
|
+
threatCount(this.threats[color]) > 1 ||
|
|
255
|
+
threatCount(this.threats[Trax.other(color)]) > 0
|
|
256
|
+
|
|
257
|
+
// Calculate the effects of the passive player first
|
|
258
|
+
for (const level of Object.keys(inactive).map((i) => int(i))) {
|
|
259
|
+
if (level > advantage) continue
|
|
260
|
+
let points = calc || level === 0 ? attackScore(inactive, level) : 1
|
|
261
|
+
if (level === advantage) points = passive ? Math.floor(points / 2) : 0
|
|
262
|
+
score -= points
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
if (passive) score *= -1
|
|
266
|
+
|
|
267
|
+
this.#scores[color] = score
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
return this.#scores[color]
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
get scores() {
|
|
274
|
+
return {
|
|
275
|
+
w: this.#score('w'),
|
|
276
|
+
b: this.#score('b'),
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
get score() {
|
|
281
|
+
return this.scores[this.game.color]
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// Space type
|
|
285
|
+
//
|
|
286
|
+
// determine which type of space we have:
|
|
287
|
+
// c = in a cave but has no restrictions (converted to n in edge)
|
|
288
|
+
// C = a cave where some moves are not allowed (converted to c in edge)
|
|
289
|
+
// n = normal space
|
|
290
|
+
// x = we cannot play here
|
|
291
|
+
spaceType(loc) {
|
|
292
|
+
const { game } = this
|
|
293
|
+
|
|
294
|
+
if (!game.validLocation(loc)) {
|
|
295
|
+
return 'x'
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
if (!this.inCave(loc)) {
|
|
299
|
+
return 'n'
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// Check to see if we have an invalid move
|
|
303
|
+
for (const tile of game.possibleTiles(loc)) {
|
|
304
|
+
const drop = game.dropTile(tile, loc, 'tentative')
|
|
305
|
+
if (!drop.valid) {
|
|
306
|
+
return 'C'
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
return 'c'
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// Trace the edge of the playing area
|
|
314
|
+
#traceEdge() {
|
|
315
|
+
const { game } = this
|
|
316
|
+
const rawEdge = []
|
|
317
|
+
const lines = {}
|
|
318
|
+
const count = { w: 0, b: 0 }
|
|
319
|
+
let start = new Point(game.left, game.top)
|
|
320
|
+
let dir = 'r'
|
|
321
|
+
|
|
322
|
+
while (!game.tileAt(start)) {
|
|
323
|
+
start = start.right
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
let loc = start
|
|
327
|
+
|
|
328
|
+
while (game.move > 0 && !game.over) {
|
|
329
|
+
let space = loc.dir(leftTurn[dir])
|
|
330
|
+
if (rawEdge.length > 0 && space.eq(rawEdge[0])) break
|
|
331
|
+
|
|
332
|
+
const color = edgeColor(game.tileAt(loc), leftTurn[dir])
|
|
333
|
+
const end = game.follow(color, loc, follow[dir]).loc
|
|
334
|
+
const type = this.spaceType(space)
|
|
335
|
+
const tile = { x: space.x, y: space.y, c: color, t: type }
|
|
336
|
+
const id = color + end.x + ':' + end.y
|
|
337
|
+
const nColor = 'n' + color
|
|
338
|
+
|
|
339
|
+
if (id in lines) {
|
|
340
|
+
tile[nColor] = lines[id][nColor]
|
|
341
|
+
} else {
|
|
342
|
+
tile[nColor] = encodeNumber(count[color])
|
|
343
|
+
count[color]++
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
lines[color + space.x + ':' + space.y] = tile
|
|
347
|
+
rawEdge.push(tile)
|
|
348
|
+
|
|
349
|
+
const next = loc.dir(dir)
|
|
350
|
+
|
|
351
|
+
if (game.tileAt(next)) {
|
|
352
|
+
loc = next.dir(leftTurn[dir])
|
|
353
|
+
if (game.tileAt(loc)) {
|
|
354
|
+
rawEdge.push({ x: space.x, y: space.y, c: 'l', t: type })
|
|
355
|
+
dir = leftTurn[dir]
|
|
356
|
+
} else {
|
|
357
|
+
loc = next
|
|
358
|
+
}
|
|
359
|
+
} else {
|
|
360
|
+
space = next.dir(leftTurn[dir])
|
|
361
|
+
const t = this.inCave(space) ? 'c' : 'n'
|
|
362
|
+
rawEdge.push({ x: space.x, y: space.y, c: 'r', t })
|
|
363
|
+
dir = rightTurn[dir]
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
return label(pairUp(hollowOut(spelunk(rawEdge))))
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// Apply the threats to a color's regex-ready edge
|
|
371
|
+
#applyThreats(color) {
|
|
372
|
+
const matches = {}
|
|
373
|
+
const { length } = this.edge[color]
|
|
374
|
+
const edge = this.edge[color].repeat(2)
|
|
375
|
+
for (const threat of threats.list) {
|
|
376
|
+
const find = matches[threat.depth] || []
|
|
377
|
+
for (const match of edge.matchAll(threat.rx)) {
|
|
378
|
+
if (match.index >= length) break
|
|
379
|
+
if (find.every((f) => f.at !== match.index || f.match !== match[0])) {
|
|
380
|
+
find.push({
|
|
381
|
+
threat: threat.pattern,
|
|
382
|
+
match: match[0],
|
|
383
|
+
at: match.index,
|
|
384
|
+
})
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
if (find.length > 0) matches[threat.depth] = find
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
return matches
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
export const analyze = (game) => new Analysis(game)
|
package/src/cli.js
CHANGED
|
@@ -26,6 +26,7 @@ import { version } from './version.js'
|
|
|
26
26
|
import * as tty from './tty.js'
|
|
27
27
|
|
|
28
28
|
// Load all the commands as individual plugins
|
|
29
|
+
import { analyzeCmd } from './cmds/analyze.js'
|
|
29
30
|
import { deleteCmd } from './cmds/delete.js'
|
|
30
31
|
import { helpCmd } from './cmds/help.js'
|
|
31
32
|
import {
|
|
@@ -77,6 +78,7 @@ const quitCmd = {
|
|
|
77
78
|
}
|
|
78
79
|
|
|
79
80
|
const plugins = [
|
|
81
|
+
analyzeCmd,
|
|
80
82
|
deleteCmd,
|
|
81
83
|
exportCmd,
|
|
82
84
|
helpCmd,
|
|
@@ -173,6 +175,14 @@ const DATA = { games: {} }
|
|
|
173
175
|
|
|
174
176
|
// CLI context object for plugins
|
|
175
177
|
const CLI = {
|
|
178
|
+
// Notation manipulation
|
|
179
|
+
fixNotation(move) {
|
|
180
|
+
if (move.endsWith('b')) return move.slice(0, -1) + '\\'
|
|
181
|
+
if (move.endsWith('s')) return move.slice(0, -1) + '/'
|
|
182
|
+
if (move.endsWith('p')) return move.slice(0, -1) + '+'
|
|
183
|
+
return move
|
|
184
|
+
},
|
|
185
|
+
|
|
176
186
|
// Quick access to all stored games
|
|
177
187
|
get GAMES() {
|
|
178
188
|
return DATA.games || {}
|
|
@@ -0,0 +1,92 @@
|
|
|
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 analyze command
|
|
17
|
+
|
|
18
|
+
import { analyze } from '../analyst.js'
|
|
19
|
+
import { Trax } from '../engine.js'
|
|
20
|
+
|
|
21
|
+
const notationRx = /^[@a-z]+\d+[bps/\\+]$/i
|
|
22
|
+
|
|
23
|
+
export const analyzeCmd = {
|
|
24
|
+
name: 'analyze',
|
|
25
|
+
args: '[<move>]',
|
|
26
|
+
comp: '<play>',
|
|
27
|
+
desc: 'analyze a position',
|
|
28
|
+
help: [
|
|
29
|
+
'Analyze the current position of the currently selected game, or pass in a',
|
|
30
|
+
'move to analyze the result of that move.',
|
|
31
|
+
],
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const threatNames = {
|
|
35
|
+
0: 'corners',
|
|
36
|
+
1: 'attacks',
|
|
37
|
+
2: 'Ls',
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const listThreats = (threats, detailed) => {
|
|
41
|
+
const summary = []
|
|
42
|
+
for (const level of Object.keys(threats || {})) {
|
|
43
|
+
const name = (threatNames[level] || `stage-${level} threats`) + ': '
|
|
44
|
+
const items = []
|
|
45
|
+
if (detailed) {
|
|
46
|
+
for (const item of threats[level]) {
|
|
47
|
+
items.push('[' + item.at + ':' + item.match + ']')
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
summary.push(name + threats[level].length + ' ' + items.join(' '))
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return summary.join(' ')
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
analyzeCmd.fn = (CLI, move) => {
|
|
58
|
+
if (!String(CLI.GAME?.id)) {
|
|
59
|
+
return CLI.error('No active game. Type "new" to start a game.')
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
let trax = CLI.TRAX
|
|
63
|
+
|
|
64
|
+
const detailed = 'detail'.startsWith(move) || 'debug'.startsWith(move)
|
|
65
|
+
|
|
66
|
+
if (notationRx.test(move)) {
|
|
67
|
+
move = CLI.fixNotation(move)
|
|
68
|
+
CLI.do('try', move)
|
|
69
|
+
trax = new Trax(trax.rules, trax.moves, 'cli')
|
|
70
|
+
const play = trax.dropTile(move)
|
|
71
|
+
if (!play.valid || trax.over) return
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (trax.over) {
|
|
75
|
+
return CLI.error('Game has ended.')
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const { edge, threats, scores } = analyze(trax)
|
|
79
|
+
|
|
80
|
+
if (detailed) CLI.out(edge.w)
|
|
81
|
+
CLI.out(
|
|
82
|
+
CLI.bubble('w', scores.w) +
|
|
83
|
+
CLI.color.white(' ') +
|
|
84
|
+
listThreats(threats.w, detailed),
|
|
85
|
+
)
|
|
86
|
+
if (detailed) CLI.out(edge.b)
|
|
87
|
+
CLI.out(
|
|
88
|
+
CLI.bubble('b', scores.b) +
|
|
89
|
+
CLI.color.black(' ') +
|
|
90
|
+
listThreats(threats.b, detailed),
|
|
91
|
+
)
|
|
92
|
+
}
|
package/src/cmds/play-try.js
CHANGED
|
@@ -46,13 +46,6 @@ export const tryCmd = {
|
|
|
46
46
|
],
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
-
const fixNotation = (move) => {
|
|
50
|
-
if (move.endsWith('b')) return move.slice(0, -1) + '\\'
|
|
51
|
-
if (move.endsWith('s')) return move.slice(0, -1) + '/'
|
|
52
|
-
if (move.endsWith('p')) return move.slice(0, -1) + '+'
|
|
53
|
-
return move
|
|
54
|
-
}
|
|
55
|
-
|
|
56
49
|
playCmd.fn = (CLI, ...moves) => {
|
|
57
50
|
if (moves.length === 0 || !moves[0]) {
|
|
58
51
|
CLI.error('You must provide a move.')
|
|
@@ -73,9 +66,9 @@ playCmd.fn = (CLI, ...moves) => {
|
|
|
73
66
|
for (let move of moves) {
|
|
74
67
|
if (notationRx.test(move)) {
|
|
75
68
|
const moveNumber = CLI.TRAX.move
|
|
76
|
-
move = fixNotation(move)
|
|
69
|
+
move = CLI.fixNotation(move)
|
|
77
70
|
if (checkMove) {
|
|
78
|
-
CLI.TRAX.
|
|
71
|
+
CLI.TRAX.play(checkMove, move)
|
|
79
72
|
} else {
|
|
80
73
|
CLI.TRAX.play(move)
|
|
81
74
|
}
|
|
@@ -116,7 +109,7 @@ tryCmd.fn = (CLI, move) => {
|
|
|
116
109
|
}
|
|
117
110
|
|
|
118
111
|
if (notationRx.test(move)) {
|
|
119
|
-
move = fixNotation(move)
|
|
112
|
+
move = CLI.fixNotation(move)
|
|
120
113
|
const play = CLI.TRAX.dropTile(move, undefined, 'tentative')
|
|
121
114
|
if (play.valid || play.dropped.length > 0) {
|
|
122
115
|
CLI.display(CLI.TRAX, CLI.GAME.players, move)
|
package/src/engine.js
CHANGED
|
@@ -15,15 +15,17 @@
|
|
|
15
15
|
|
|
16
16
|
// Trax internals. Woot.
|
|
17
17
|
|
|
18
|
-
// Tile type names are determined by listing the color
|
|
19
|
-
//
|
|
20
|
-
//
|
|
18
|
+
// Tile type names are determined by listing the line color at each edge,
|
|
19
|
+
// starting from the top and going clockwise, and then sorted alphabetically
|
|
20
|
+
// and given a single letter name, so 'bbww' becomes 'a', which gives us six
|
|
21
|
+
// different tile names: a-f
|
|
21
22
|
|
|
22
23
|
import { Point } from './point.js'
|
|
23
24
|
|
|
24
25
|
// Fun trax helper constants
|
|
25
26
|
|
|
26
27
|
const zero = new Point(0, 0)
|
|
28
|
+
const moveNumberRegex = /^(\d+)[.):]?$/
|
|
27
29
|
const notationRegex = /^([@a-z]+)(\d+)([/\\+])$/i
|
|
28
30
|
|
|
29
31
|
const slash = { a: '\\', b: '+', c: '/', d: '/', e: '+', f: '\\' }
|
|
@@ -127,12 +129,7 @@ export const Trax = class {
|
|
|
127
129
|
this.invalid = false
|
|
128
130
|
// Moves might be a notation string
|
|
129
131
|
if (moves) {
|
|
130
|
-
|
|
131
|
-
for (const move of moves) {
|
|
132
|
-
if (notationRegex.test(move)) {
|
|
133
|
-
this.dropTile(move)
|
|
134
|
-
}
|
|
135
|
-
}
|
|
132
|
+
this.playMoves(moves)
|
|
136
133
|
}
|
|
137
134
|
}
|
|
138
135
|
|
|
@@ -435,15 +432,43 @@ export const Trax = class {
|
|
|
435
432
|
}
|
|
436
433
|
}
|
|
437
434
|
|
|
438
|
-
play(notation) {
|
|
439
|
-
//
|
|
435
|
+
play(moveNumber, notation) {
|
|
436
|
+
// Generic API call for playing a move with move number safety
|
|
437
|
+
// or, alternately, pass only a single value, notation, for a quick play
|
|
438
|
+
if (notation) {
|
|
439
|
+
if (moveNumber !== this.move + 1) return { valid: false }
|
|
440
|
+
} else {
|
|
441
|
+
notation = moveNumber
|
|
442
|
+
}
|
|
443
|
+
|
|
440
444
|
return this.dropTile(notation)
|
|
441
445
|
}
|
|
442
446
|
|
|
443
|
-
|
|
444
|
-
//
|
|
445
|
-
|
|
446
|
-
|
|
447
|
+
playMoves(moves) {
|
|
448
|
+
// Play one or more moves, from a string or a list of moves
|
|
449
|
+
// If move numbers are included, ensure they are accurate
|
|
450
|
+
if (Array.isArray(moves)) {
|
|
451
|
+
moves = moves.join(' ')
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
moves = moves.replace(/\n/g, ' ').split(/\s+/)
|
|
455
|
+
|
|
456
|
+
let moveNumber = 0
|
|
457
|
+
for (const move of moves) {
|
|
458
|
+
if (moveNumberRegex.test(move)) {
|
|
459
|
+
moveNumber = Number(move.replace(/\D/g, ''))
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
if (notationRegex.test(move)) {
|
|
463
|
+
if (moveNumber) {
|
|
464
|
+
this.play(moveNumber, move)
|
|
465
|
+
} else {
|
|
466
|
+
this.play(move)
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
moveNumber = 0
|
|
470
|
+
}
|
|
471
|
+
}
|
|
447
472
|
}
|
|
448
473
|
|
|
449
474
|
updateNotation(notation) {
|
package/src/threats.js
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
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
|
+
// Threats database: hand-coded threats
|
|
17
|
+
|
|
18
|
+
const REGEX = {
|
|
19
|
+
a: '[am]', // White that pairs with the next white
|
|
20
|
+
c: '[cp]', // White that forms a connectable pair with next white
|
|
21
|
+
l: '[lmp]', // White in a hollow
|
|
22
|
+
w: '[aclmpw]', // Any white
|
|
23
|
+
'!': '(?:b[acw])*?', // Ripple black/white
|
|
24
|
+
'¡': '(?:[acw]b)*?', // Ripple white/black
|
|
25
|
+
'*': 'b*?', // Zero or more black pieces
|
|
26
|
+
'?': 'b?', // Zero or one black piece
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
class CustomInspectableThreat {
|
|
30
|
+
#depth = 0
|
|
31
|
+
#pattern = ''
|
|
32
|
+
#rx = null
|
|
33
|
+
|
|
34
|
+
constructor(depth, pattern) {
|
|
35
|
+
this.#depth = depth
|
|
36
|
+
this.#pattern = pattern
|
|
37
|
+
|
|
38
|
+
let rx = ''
|
|
39
|
+
for (const char of pattern) {
|
|
40
|
+
rx += REGEX[char] || char
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
this.#rx = new RegExp(rx, 'g')
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
get depth() {
|
|
47
|
+
return this.#depth
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
get pattern() {
|
|
51
|
+
return this.#pattern
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
get rx() {
|
|
55
|
+
return this.#rx
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// To make life at the REPL a little easier
|
|
59
|
+
[Symbol.for('nodejs.util.inspect.custom')](_, out) {
|
|
60
|
+
const style = out.stylize
|
|
61
|
+
return [
|
|
62
|
+
'[' + style('Threat', 'special') + ']',
|
|
63
|
+
'{',
|
|
64
|
+
style(this.#pattern, 'string') + ':',
|
|
65
|
+
style(this.#depth, 'number'),
|
|
66
|
+
'}',
|
|
67
|
+
].join(' ')
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
class CustomInspectableThreatsDB {
|
|
72
|
+
#db = {}
|
|
73
|
+
#depth = 0
|
|
74
|
+
|
|
75
|
+
constructor(corners) {
|
|
76
|
+
for (const corner of corners) {
|
|
77
|
+
this.add(0, corner)
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
add(depth, pattern) {
|
|
82
|
+
this.#depth = Math.max(depth, this.#depth)
|
|
83
|
+
this.#db[pattern] = new CustomInspectableThreat(depth, pattern)
|
|
84
|
+
return this.#db[pattern]
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
level(depth) {
|
|
88
|
+
return Object.values(this.#db).filter((p) => p.depth === depth)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
get list() {
|
|
92
|
+
return Object.values(this.#db)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
get maxDepth() {
|
|
96
|
+
return this.#depth
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// To make life at the REPL a little easier
|
|
100
|
+
[Symbol.for('nodejs.util.inspect.custom')](_, out) {
|
|
101
|
+
const style = out.stylize
|
|
102
|
+
const inspection = '[' + style('Threats', 'special') + '] { '
|
|
103
|
+
let total = 0
|
|
104
|
+
const keys = []
|
|
105
|
+
|
|
106
|
+
for (let x = 0; x <= this.#depth; x++) {
|
|
107
|
+
const count = this.level(x).length
|
|
108
|
+
keys.push(style(x, 'string') + ': ' + style(count, 'number'))
|
|
109
|
+
total += count
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return inspection + keys.join(', ') + ' } total: ' + total
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const pre = true
|
|
117
|
+
const post = true
|
|
118
|
+
const corners = {
|
|
119
|
+
arw: { pre, post }, // Simple corner
|
|
120
|
+
abrw: { post }, // Wide-first corner
|
|
121
|
+
arbw: { pre }, // Wide-second corner
|
|
122
|
+
abrbw: {}, // Wide corner
|
|
123
|
+
'ab?bw': { pre, post }, // Flat corner and wide flat corner
|
|
124
|
+
cw: { pre, post }, // Connectable pair
|
|
125
|
+
cbw: {}, // Wide connectable pair
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export const threats = new CustomInspectableThreatsDB(Object.keys(corners))
|
|
129
|
+
|
|
130
|
+
// Attacks
|
|
131
|
+
const attacks = [
|
|
132
|
+
'a?w', // Simple attack and wide attack
|
|
133
|
+
]
|
|
134
|
+
attacks.map((a) => threats.add(1, a))
|
|
135
|
+
|
|
136
|
+
// Re-forming attacks
|
|
137
|
+
const reform = [
|
|
138
|
+
'rm?lr', // Cave/wide cave attack
|
|
139
|
+
'rp?lr',
|
|
140
|
+
'rbm?lbr', // Deep cave/deep wide cave attack (impossibly rare?)
|
|
141
|
+
'rbp?lbr',
|
|
142
|
+
'ralmrw', // Attack at top of cave
|
|
143
|
+
'arlmwr', // Reversed
|
|
144
|
+
]
|
|
145
|
+
reform.map((r) => threats.add(2, r))
|
|
146
|
+
|
|
147
|
+
// Create multi-stage threats
|
|
148
|
+
for (const one of Object.keys(corners)) {
|
|
149
|
+
for (const two of Object.keys(corners)) {
|
|
150
|
+
// L threats
|
|
151
|
+
const [before, after] = [corners[one].post, corners[two].pre]
|
|
152
|
+
if (before || after) {
|
|
153
|
+
const start = one.replace('w', 'l')
|
|
154
|
+
threats.add(2, start + (before ? '?¡' : '¡') + two)
|
|
155
|
+
const end = two.replace('a', 'm').replace('c', 'p')
|
|
156
|
+
threats.add(2, one + (after ? '?!' : '!') + end)
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Edge threats
|
|
160
|
+
threats.add(3, one + '!b*b¡' + two)
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Create edge threats
|
package/src/version.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// Generated by genversion.
|
|
2
|
-
export const version = '0.
|
|
2
|
+
export const version = '0.11.0'
|