@slugbugblue/trax 0.18.0 → 0.19.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 +4 -0
- package/Dockerfile +5 -1
- package/README.md +5 -4
- package/benchmark/benchmarks.json +18 -0
- package/docs/lru.md +151 -0
- package/package.json +3 -2
- package/src/analyst.js +16 -5
- package/src/engine.js +1 -1
- package/src/lru.js +153 -0
- package/src/point.js +1 -1
- package/src/threats.js +2 -2
- package/src/version.js +1 -1
package/CHANGELOG.md
CHANGED
package/Dockerfile
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
|
-
|
|
1
|
+
# Use the latest nodejs long-term-support release,
|
|
2
|
+
# on the latest Alpine Linux for its small footprint
|
|
3
|
+
FROM node:lts-alpine
|
|
4
|
+
# Install current source code globally
|
|
2
5
|
COPY . /src
|
|
3
6
|
WORKDIR /src
|
|
4
7
|
RUN npm install -g @slugbugblue/trax
|
|
8
|
+
# Use the trax CLI as the starting point
|
|
5
9
|
ENTRYPOINT ["/usr/local/bin/trax"]
|
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@ This project aims to provide tools for working with Trax games in Javascript.
|
|
|
4
4
|
|
|
5
5
|
- Trax game-playing engine
|
|
6
6
|
- Position analysis engine
|
|
7
|
-
- Over
|
|
7
|
+
- Over 60 puzzles to help hone your Trax skills
|
|
8
8
|
- Command-line interface for ease of use
|
|
9
9
|
|
|
10
10
|
## Description
|
|
@@ -153,9 +153,10 @@ threats.
|
|
|
153
153
|
|
|
154
154
|
### Puzzles database
|
|
155
155
|
|
|
156
|
-
The puzzles database is provided in `puzzles.js`, and
|
|
157
|
-
|
|
158
|
-
|
|
156
|
+
The puzzles database is provided in `puzzles.js`, and contains puzzles that I've
|
|
157
|
+
been using to test the analysis engine, publicly available puzzles from [the
|
|
158
|
+
traxgame.com puzzles page][traxpuzzles], and [puzzles curated][puzzlebook] by
|
|
159
|
+
[Martin M. S. Pedersen][traxplayer].
|
|
159
160
|
|
|
160
161
|
## Roadmap
|
|
161
162
|
|
|
@@ -38,5 +38,23 @@
|
|
|
38
38
|
"count": 58,
|
|
39
39
|
"ms": 59031
|
|
40
40
|
}
|
|
41
|
+
},
|
|
42
|
+
"0.19.0": {
|
|
43
|
+
"puzzles.firstmove.tutorial": {
|
|
44
|
+
"count": 16,
|
|
45
|
+
"ms": 9753
|
|
46
|
+
},
|
|
47
|
+
"puzzles.firstmove.beginner": {
|
|
48
|
+
"count": 15,
|
|
49
|
+
"ms": 17376
|
|
50
|
+
},
|
|
51
|
+
"puzzles.firstmove.tough": {
|
|
52
|
+
"count": 25,
|
|
53
|
+
"ms": 26380
|
|
54
|
+
},
|
|
55
|
+
"puzzles.firstmove.total": {
|
|
56
|
+
"count": 56,
|
|
57
|
+
"ms": 55167
|
|
58
|
+
}
|
|
41
59
|
}
|
|
42
60
|
}
|
package/docs/lru.md
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
# lru.js API documentation
|
|
2
|
+
|
|
3
|
+
This module provides a simple Least Recently Used cache.
|
|
4
|
+
|
|
5
|
+
Internally, this uses a Map, which give us insertion-ordering for free. The
|
|
6
|
+
first item in the list is the oldest. The last item is the most recently set or
|
|
7
|
+
accessed. Most of the base functionality of the Map are replicated.
|
|
8
|
+
|
|
9
|
+
Since I don't need it to be super fancy, there isn't a lot of extra
|
|
10
|
+
functionality here.
|
|
11
|
+
|
|
12
|
+
## Example usage
|
|
13
|
+
|
|
14
|
+
```javascript
|
|
15
|
+
import { LRU } from '@slugbugblue/trax/lru.js'
|
|
16
|
+
|
|
17
|
+
const cache = new LRU(100)
|
|
18
|
+
|
|
19
|
+
cache.set('a', 1)
|
|
20
|
+
cache.set('b', 2)
|
|
21
|
+
|
|
22
|
+
// Get an item from the cache
|
|
23
|
+
console.log(cache.get('a')) // 1
|
|
24
|
+
|
|
25
|
+
// Accessing a key makes it the most recent:
|
|
26
|
+
console.log(cache.keys()) // [Map Iterator] { 'b', 'a' }
|
|
27
|
+
console.log(cache.hits) // 1
|
|
28
|
+
|
|
29
|
+
console.log(cache.get('z')) // undefined
|
|
30
|
+
console.log(cache.misses) // 1
|
|
31
|
+
|
|
32
|
+
console.log(cache.size) // 2
|
|
33
|
+
console.log(cache.capacity) // 100
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## API
|
|
37
|
+
|
|
38
|
+
### LRU class
|
|
39
|
+
|
|
40
|
+
An LRU cache is a way to remember a limited set of items, with the oldest
|
|
41
|
+
expiring when the capacity is reached.
|
|
42
|
+
|
|
43
|
+
#### constructor
|
|
44
|
+
|
|
45
|
+
##### `new LRU(capacity)`
|
|
46
|
+
|
|
47
|
+
Create the cache by specifying the maximum number of items it should hold.
|
|
48
|
+
|
|
49
|
+
#### computed properties
|
|
50
|
+
|
|
51
|
+
##### `capacity`
|
|
52
|
+
|
|
53
|
+
The maximum number of items the cache can hold. This cannot be changed after the
|
|
54
|
+
cache creation.
|
|
55
|
+
|
|
56
|
+
##### `size`
|
|
57
|
+
|
|
58
|
+
The number of items currently stored in the cache.
|
|
59
|
+
|
|
60
|
+
##### `hits`
|
|
61
|
+
|
|
62
|
+
The number of times a `get` call has found an item in the cache.
|
|
63
|
+
|
|
64
|
+
##### `misses`
|
|
65
|
+
|
|
66
|
+
The number of times a `get` call has not found an item in the cache.
|
|
67
|
+
|
|
68
|
+
##### `expired`
|
|
69
|
+
|
|
70
|
+
The number of times an item has been removed from the cache.
|
|
71
|
+
|
|
72
|
+
#### methods
|
|
73
|
+
|
|
74
|
+
##### `get(key)`
|
|
75
|
+
|
|
76
|
+
Retrieve an item from the cache. If the item is found in the cache, `hits` will
|
|
77
|
+
be updated, the item will be marked as the most-recently-used item, and the
|
|
78
|
+
value will be returned. If the item is not present in the cache, `misses` will
|
|
79
|
+
be updated, and `undefined` will be returned.
|
|
80
|
+
|
|
81
|
+
Note that if you had stored `undefined` as the value in the cache, the return of
|
|
82
|
+
`undefined` doesn't necessarily represent a cache miss.
|
|
83
|
+
|
|
84
|
+
##### `peek(key)`
|
|
85
|
+
|
|
86
|
+
Retrieve an item from the cache, if present, but without updating any of the
|
|
87
|
+
LRU-specific information about the retrieval attempt. The item's age will not be
|
|
88
|
+
updated. If the item is not present in the cache, `undefined` will be returned.
|
|
89
|
+
|
|
90
|
+
##### `set(key, value)`
|
|
91
|
+
|
|
92
|
+
Store an item in the cache. If the item is already present in the cache, it will
|
|
93
|
+
be replaced and marked as the most-recently-used item.
|
|
94
|
+
|
|
95
|
+
##### `delete(key)`
|
|
96
|
+
|
|
97
|
+
Remove an item from the cache.
|
|
98
|
+
|
|
99
|
+
##### `clear()`
|
|
100
|
+
|
|
101
|
+
Clears all items and statistics from the cache. The maximum capacity will not
|
|
102
|
+
change.
|
|
103
|
+
|
|
104
|
+
##### `entries()`
|
|
105
|
+
|
|
106
|
+
Returns an iterator that operates over all the \[key, value\] pairs stored in
|
|
107
|
+
the cache. The entries will be in order from oldest to newest.
|
|
108
|
+
|
|
109
|
+
##### `has(key)`
|
|
110
|
+
|
|
111
|
+
Returns true if a key is found in the cache, false otherwise.
|
|
112
|
+
|
|
113
|
+
##### `keys()`
|
|
114
|
+
|
|
115
|
+
Returns an iterator that operates over all the keys stored in the cache. The
|
|
116
|
+
keys will be returned oldest first, newest last.
|
|
117
|
+
|
|
118
|
+
##### `values()`
|
|
119
|
+
|
|
120
|
+
Returns an iterator that operates over all the values stored in the cache. The
|
|
121
|
+
values will be returned oldest first, newest last.
|
|
122
|
+
|
|
123
|
+
#### Plays well with others
|
|
124
|
+
|
|
125
|
+
The LRU class also includes functionality to provide useful default results when
|
|
126
|
+
used in other contexts.
|
|
127
|
+
|
|
128
|
+
##### `toString()`
|
|
129
|
+
|
|
130
|
+
Returns a string representation of the cache in the format `LRU(75 of 1000)`.
|
|
131
|
+
This allows simple logging calls with `console.log(cache)`.
|
|
132
|
+
|
|
133
|
+
##### `util.inspect.custom()`
|
|
134
|
+
|
|
135
|
+
When running in the node command line interpreter, an LRU cache instance will be
|
|
136
|
+
pretty-printed using the magic of `util.inspect`.
|
|
137
|
+
|
|
138
|
+
## License
|
|
139
|
+
|
|
140
|
+
Copyright 2023 Chad Transtrum
|
|
141
|
+
|
|
142
|
+
Licensed under the Apache License, Version 2.0 (the "License"); you may not use
|
|
143
|
+
the files in this project except in compliance with the License. You may obtain
|
|
144
|
+
a copy of the License at
|
|
145
|
+
|
|
146
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
147
|
+
|
|
148
|
+
Unless required by applicable law or agreed to in writing, software distributed
|
|
149
|
+
under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
|
|
150
|
+
CONDITIONS OF ANY KIND, either express or implied. See the License for the
|
|
151
|
+
specific language governing permissions and limitations under the License.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@slugbugblue/trax",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.19.0",
|
|
4
4
|
"description": "Trax game engine and friends",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"trax",
|
|
@@ -14,8 +14,9 @@
|
|
|
14
14
|
"types": "./src/types.d.ts",
|
|
15
15
|
"exports": {
|
|
16
16
|
".": "./src/engine.js",
|
|
17
|
-
"./point": "./src/point.js",
|
|
18
17
|
"./analyst": "./src/analyst.js",
|
|
18
|
+
"./lru": "./src/lru.js",
|
|
19
|
+
"./point": "./src/point.js",
|
|
19
20
|
"./puzzles": "./src/puzzles.js",
|
|
20
21
|
"./threats": "./src/threats.js",
|
|
21
22
|
"./tty": "./src/tty.js",
|
package/src/analyst.js
CHANGED
|
@@ -6,9 +6,12 @@
|
|
|
6
6
|
|
|
7
7
|
import { Point } from '@slugbugblue/trax/point'
|
|
8
8
|
import { Trax } from '@slugbugblue/trax'
|
|
9
|
+
import { LRU } from '@slugbugblue/trax/lru'
|
|
9
10
|
|
|
10
11
|
import { threats } from '@slugbugblue/trax/threats'
|
|
11
12
|
|
|
13
|
+
const cache = new LRU(2750)
|
|
14
|
+
|
|
12
15
|
/** Helper function to generate a unique id for the analysis. */
|
|
13
16
|
const id = () => {
|
|
14
17
|
const chars = 'abcdefghijklmnopqrstuvwxyz0123456789'
|
|
@@ -252,7 +255,7 @@ const passiveAttack = (from) => threatCount(from) > 1
|
|
|
252
255
|
const underAttack = (from) => attackCount(from) + threatCount(from) > 0
|
|
253
256
|
|
|
254
257
|
// Analysis class
|
|
255
|
-
export
|
|
258
|
+
export class Analysis {
|
|
256
259
|
/** @type {TraxVariant} */
|
|
257
260
|
#rules = 'trax'
|
|
258
261
|
#id = 'analysis'
|
|
@@ -554,7 +557,7 @@ export const Analysis = class {
|
|
|
554
557
|
/**
|
|
555
558
|
* @arg {FoundThreat} threat - a threat
|
|
556
559
|
* @arg {string} color - the player color
|
|
557
|
-
* @returns {
|
|
560
|
+
* @returns {Point[]} - locations relevant to the threat
|
|
558
561
|
*/
|
|
559
562
|
#threatLocs(threat, color) {
|
|
560
563
|
const { edge } = this.edge
|
|
@@ -658,7 +661,7 @@ export const Analysis = class {
|
|
|
658
661
|
// we just assume that this threat is also valid ...
|
|
659
662
|
if (game.over && game.turn === Trax.playerNumber(color)) return true
|
|
660
663
|
|
|
661
|
-
const { threats } =
|
|
664
|
+
const { threats } = analyze(game, true)
|
|
662
665
|
|
|
663
666
|
// If we give our opponent an attack, this is a faulty move
|
|
664
667
|
const faulty = (threats[Trax.other(color)][1]?.length ?? 0) - other
|
|
@@ -712,11 +715,19 @@ export const Analysis = class {
|
|
|
712
715
|
|
|
713
716
|
/** Analyze a game position.
|
|
714
717
|
* @arg {Trax} game - the position to analyze
|
|
718
|
+
* @arg {boolean} partial - if the position should only be partially analyzed
|
|
715
719
|
* @returns {Analysis}
|
|
716
720
|
*/
|
|
717
|
-
export const analyze = (game) =>
|
|
721
|
+
export const analyze = (game, partial = false) => {
|
|
722
|
+
const id = (partial ? 'y' : 'n') + game.rules + game.normalized
|
|
723
|
+
const cached = cache.get(id)
|
|
724
|
+
if (cached) return cached
|
|
725
|
+
const analysis = new Analysis(game, partial)
|
|
726
|
+
cache.set(id, analysis)
|
|
727
|
+
return analysis
|
|
728
|
+
}
|
|
718
729
|
|
|
719
|
-
export
|
|
730
|
+
export class Suggestion {
|
|
720
731
|
/** @type {PositionScore[]} */
|
|
721
732
|
all = []
|
|
722
733
|
/** @type {PositionScore[]} */
|
package/src/engine.js
CHANGED
|
@@ -127,7 +127,7 @@ const codeRowLength = (row) =>
|
|
|
127
127
|
|
|
128
128
|
// This is where the magic happens
|
|
129
129
|
/** A digital representation of a Trax game. */
|
|
130
|
-
export
|
|
130
|
+
export class Trax {
|
|
131
131
|
// Static class properties
|
|
132
132
|
|
|
133
133
|
/** @readonly @type {Record<TraxVariant, string>} */
|
package/src/lru.js
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/** Least Recently Used cache
|
|
2
|
+
* @copyright 2023
|
|
3
|
+
* @author Chad Transtrum <chad@transtrum.net>
|
|
4
|
+
* @license Apache-2.0
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/** LRU cache: a class for remembering things. It uses a Map internally and
|
|
8
|
+
* exposes most of the same interface.
|
|
9
|
+
*/
|
|
10
|
+
export class LRU {
|
|
11
|
+
/** @type Map */
|
|
12
|
+
#cache
|
|
13
|
+
#capacity = 0
|
|
14
|
+
#hits = 0
|
|
15
|
+
#misses = 0
|
|
16
|
+
#expired = 0
|
|
17
|
+
|
|
18
|
+
/** Instantiate this class with the max number of cached items
|
|
19
|
+
* @arg {number} capacity - the maximum number of items to cache
|
|
20
|
+
*/
|
|
21
|
+
constructor(capacity = 1000) {
|
|
22
|
+
this.#cache = new Map()
|
|
23
|
+
if (typeof capacity === 'number' && capacity >= 1) {
|
|
24
|
+
this.#capacity = Math.floor(capacity)
|
|
25
|
+
} else {
|
|
26
|
+
throw new SyntaxError('capacity must be a positive integer')
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** The maximum capacity. */
|
|
31
|
+
get capacity() {
|
|
32
|
+
return this.#capacity
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** The current size. */
|
|
36
|
+
get size() {
|
|
37
|
+
return this.#cache.size
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Number of times we have had a cache hit. */
|
|
41
|
+
get hits() {
|
|
42
|
+
return this.#hits
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Number of times we have had a cache miss. */
|
|
46
|
+
get misses() {
|
|
47
|
+
return this.#misses
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Number of times we have expired a cache entry. */
|
|
51
|
+
get expired() {
|
|
52
|
+
return this.#expired
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Retrieve an item from the cache.
|
|
56
|
+
* @arg {any} key - the key of the cached item
|
|
57
|
+
* @returns {any} - the cached item or undefined
|
|
58
|
+
*/
|
|
59
|
+
get(key) {
|
|
60
|
+
if (this.#cache.has(key)) {
|
|
61
|
+
// Update the least recently used order
|
|
62
|
+
const value = this.#cache.get(key)
|
|
63
|
+
this.#cache.delete(key)
|
|
64
|
+
this.#cache.set(key, value)
|
|
65
|
+
this.#hits++
|
|
66
|
+
} else {
|
|
67
|
+
this.#misses++
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return this.#cache.get(key)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Retrieve an item from the cache without triggering any LRU handling.
|
|
74
|
+
* @arg {any} key - the key of the cached item to retrieve
|
|
75
|
+
* @returns {any} - the cached item or undefined
|
|
76
|
+
*/
|
|
77
|
+
peek(key) {
|
|
78
|
+
return this.#cache.get(key)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Store an item in the cache.
|
|
82
|
+
* @arg {any} key - the key of the cached item
|
|
83
|
+
* @arg {any} value - the value to store
|
|
84
|
+
*/
|
|
85
|
+
set(key, value) {
|
|
86
|
+
// To update an item, remove and re-add it to track LRU status
|
|
87
|
+
this.#cache.delete(key)
|
|
88
|
+
|
|
89
|
+
// If there are too many items, expire one before adding more
|
|
90
|
+
if (this.#cache.size >= this.#capacity) {
|
|
91
|
+
this.#expired++
|
|
92
|
+
const [expire] = this.#cache[Symbol.iterator]().next().value
|
|
93
|
+
this.#cache.delete(expire)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
this.#cache.set(key, value)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Delete an item from the cache.
|
|
100
|
+
* @arg {any} key - the key of the cached item
|
|
101
|
+
* @returns {boolean} true if the item was in the cache
|
|
102
|
+
*/
|
|
103
|
+
delete(key) {
|
|
104
|
+
return this.#cache.delete(key)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Clear the cache and all its statistics. */
|
|
108
|
+
clear() {
|
|
109
|
+
this.#cache.clear()
|
|
110
|
+
this.#hits = 0
|
|
111
|
+
this.#misses = 0
|
|
112
|
+
this.#expired = 0
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** Get an iterator to return [key, value] pairs from the cache. Does not
|
|
116
|
+
* update any LRU information. */
|
|
117
|
+
entries() {
|
|
118
|
+
return this.#cache.entries()
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** Determine if a key is already in the cache. Does not trigger hit/miss.
|
|
122
|
+
* @arg {any} key - the key to check
|
|
123
|
+
* @returns {boolean} true if the key is in the cache
|
|
124
|
+
*/
|
|
125
|
+
has(key) {
|
|
126
|
+
return this.#cache.has(key)
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** Get an iterator for the keys stored in cache, in order from least to most
|
|
130
|
+
* recent. Does not update any LRU information. */
|
|
131
|
+
keys() {
|
|
132
|
+
return this.#cache.keys()
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** Get an iterator for the values stored in cache, in order from least to
|
|
136
|
+
* most recent. Does not update any LRU information. */
|
|
137
|
+
values() {
|
|
138
|
+
return this.#cache.values()
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Play nicely with the rest of the world
|
|
142
|
+
toString() {
|
|
143
|
+
return `LRU(${this.#cache.size} of ${this.#capacity})`
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
[Symbol.for('nodejs.util.inspect.custom')](_, options) {
|
|
147
|
+
const [y, s, n] = [options.stylize, 'special', 'number']
|
|
148
|
+
return `${y('LRU', s)}(${y(this.#cache.size, n)} of ${y(
|
|
149
|
+
this.#capacity,
|
|
150
|
+
n,
|
|
151
|
+
)})`
|
|
152
|
+
}
|
|
153
|
+
}
|
package/src/point.js
CHANGED
package/src/threats.js
CHANGED
|
@@ -21,7 +21,7 @@ const REGEX = {
|
|
|
21
21
|
'?': 'b?', // Zero or one black piece
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
export
|
|
24
|
+
export class Threat {
|
|
25
25
|
#depth = 0
|
|
26
26
|
#pattern = ''
|
|
27
27
|
#rx
|
|
@@ -72,7 +72,7 @@ export const Threat = class {
|
|
|
72
72
|
}
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
-
export
|
|
75
|
+
export class ThreatsDB {
|
|
76
76
|
#db = {}
|
|
77
77
|
#depth = 0
|
|
78
78
|
|
package/src/version.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// Generated by genversion.
|
|
2
|
-
export const version = '0.
|
|
2
|
+
export const version = '0.19.0'
|