@slugbugblue/trax 1.1.1 → 1.3.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/RELEASE.v1.3.0.md +12 -0
- package/benchmark/benchmarks.json +138 -0
- package/benchmark/engine.bench.js +228 -0
- package/gl-sast-report.json +1 -1
- package/package.json +6 -5
- package/src/engine.d.ts +363 -179
- package/src/engine.js +635 -419
- package/src/version.js +1 -1
- package/RELEASE.v1.1.1.md +0 -5
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
## 1.3.0 - 2026-08-16
|
|
2
|
+
|
|
3
|
+
- Add `canonical`: a position code that collapses color-swapped duplicates in
|
|
4
|
+
addition to the rotation/mirror symmetry `normalize()` already handled
|
|
5
|
+
- Deprecate `normalize()`/`normalized` in favor of `canonical`, to be removed in
|
|
6
|
+
version 2
|
|
7
|
+
- `provisionalMove()` now accepts either a `canonical` or a (deprecated)
|
|
8
|
+
`normalized` code for `from`/`to`, so provisional moves saved before migrating
|
|
9
|
+
to `canonical` keep resolving correctly
|
|
10
|
+
- Update dependencies
|
|
11
|
+
- Resolve all new xo rule findings
|
|
12
|
+
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
{
|
|
2
|
+
"1.1.1": {
|
|
3
|
+
"tutorial.01": {
|
|
4
|
+
"construction": {
|
|
5
|
+
"count": 5000,
|
|
6
|
+
"ms": 0.0843
|
|
7
|
+
},
|
|
8
|
+
"save+restore": {
|
|
9
|
+
"count": 1000,
|
|
10
|
+
"ms": 0.0058
|
|
11
|
+
},
|
|
12
|
+
"possibleMoves": {
|
|
13
|
+
"count": 1000,
|
|
14
|
+
"ms": 0.7045
|
|
15
|
+
},
|
|
16
|
+
"play": {
|
|
17
|
+
"count": 1000,
|
|
18
|
+
"ms": 0.1158
|
|
19
|
+
},
|
|
20
|
+
"normalized": {
|
|
21
|
+
"count": 5000,
|
|
22
|
+
"ms": 0.0092
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
"book.20": {
|
|
26
|
+
"construction": {
|
|
27
|
+
"count": 5000,
|
|
28
|
+
"ms": 0.2955
|
|
29
|
+
},
|
|
30
|
+
"save+restore": {
|
|
31
|
+
"count": 1000,
|
|
32
|
+
"ms": 0.0131
|
|
33
|
+
},
|
|
34
|
+
"possibleMoves": {
|
|
35
|
+
"count": 1000,
|
|
36
|
+
"ms": 2.3677
|
|
37
|
+
},
|
|
38
|
+
"play": {
|
|
39
|
+
"count": 1000,
|
|
40
|
+
"ms": 0.2953
|
|
41
|
+
},
|
|
42
|
+
"normalized": {
|
|
43
|
+
"count": 5000,
|
|
44
|
+
"ms": 0.0433
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
"book.30": {
|
|
48
|
+
"construction": {
|
|
49
|
+
"count": 5000,
|
|
50
|
+
"ms": 1.3156
|
|
51
|
+
},
|
|
52
|
+
"save+restore": {
|
|
53
|
+
"count": 1000,
|
|
54
|
+
"ms": 0.0486
|
|
55
|
+
},
|
|
56
|
+
"possibleMoves": {
|
|
57
|
+
"count": 1000,
|
|
58
|
+
"ms": 6.785
|
|
59
|
+
},
|
|
60
|
+
"play": {
|
|
61
|
+
"count": 1000,
|
|
62
|
+
"ms": 1.3788
|
|
63
|
+
},
|
|
64
|
+
"normalized": {
|
|
65
|
+
"count": 5000,
|
|
66
|
+
"ms": 0.1611
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
"1.2.0": {
|
|
71
|
+
"tutorial.01": {
|
|
72
|
+
"construction": {
|
|
73
|
+
"count": 5000,
|
|
74
|
+
"ms": 0.0898032286
|
|
75
|
+
},
|
|
76
|
+
"save+restore": {
|
|
77
|
+
"count": 1000,
|
|
78
|
+
"ms": 0.008606527000000029
|
|
79
|
+
},
|
|
80
|
+
"possibleMoves": {
|
|
81
|
+
"count": 1000,
|
|
82
|
+
"ms": 0.897647975
|
|
83
|
+
},
|
|
84
|
+
"play": {
|
|
85
|
+
"count": 1000,
|
|
86
|
+
"ms": 0.12720280200000003
|
|
87
|
+
},
|
|
88
|
+
"normalized": {
|
|
89
|
+
"count": 5000,
|
|
90
|
+
"ms": 0.010315181600000006
|
|
91
|
+
}
|
|
92
|
+
},
|
|
93
|
+
"book.20": {
|
|
94
|
+
"construction": {
|
|
95
|
+
"count": 5000,
|
|
96
|
+
"ms": 0.3137343523999999
|
|
97
|
+
},
|
|
98
|
+
"save+restore": {
|
|
99
|
+
"count": 1000,
|
|
100
|
+
"ms": 0.016668513000000076
|
|
101
|
+
},
|
|
102
|
+
"possibleMoves": {
|
|
103
|
+
"count": 1000,
|
|
104
|
+
"ms": 2.691874038
|
|
105
|
+
},
|
|
106
|
+
"play": {
|
|
107
|
+
"count": 1000,
|
|
108
|
+
"ms": 0.3145682210000004
|
|
109
|
+
},
|
|
110
|
+
"normalized": {
|
|
111
|
+
"count": 5000,
|
|
112
|
+
"ms": 0.05063697260000008
|
|
113
|
+
}
|
|
114
|
+
},
|
|
115
|
+
"book.30": {
|
|
116
|
+
"construction": {
|
|
117
|
+
"count": 5000,
|
|
118
|
+
"ms": 1.125999694
|
|
119
|
+
},
|
|
120
|
+
"save+restore": {
|
|
121
|
+
"count": 1000,
|
|
122
|
+
"ms": 0.055884163000000625
|
|
123
|
+
},
|
|
124
|
+
"possibleMoves": {
|
|
125
|
+
"count": 1000,
|
|
126
|
+
"ms": 4.9472956880000005
|
|
127
|
+
},
|
|
128
|
+
"play": {
|
|
129
|
+
"count": 1000,
|
|
130
|
+
"ms": 1.125647703999999
|
|
131
|
+
},
|
|
132
|
+
"normalized": {
|
|
133
|
+
"count": 5000,
|
|
134
|
+
"ms": 0.17698205720000043
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync } from 'node:fs'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import { fileURLToPath } from 'node:url'
|
|
4
|
+
import { version } from '../src/version.js'
|
|
5
|
+
import { Trax } from '../src/engine.js'
|
|
6
|
+
|
|
7
|
+
/** @import { TraxVariant } from '../src/engine.js' */
|
|
8
|
+
|
|
9
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
|
10
|
+
const RESULTS_PATH = path.join(__dirname, 'benchmarks.json')
|
|
11
|
+
|
|
12
|
+
/** @type {{ id: string, game: TraxVariant, notation: string, gatesRegressions: boolean }[]} */
|
|
13
|
+
const POSITIONS = [
|
|
14
|
+
{
|
|
15
|
+
id: 'tutorial.01',
|
|
16
|
+
game: 'trax',
|
|
17
|
+
notation: '@0/ @1\\',
|
|
18
|
+
gatesRegressions: false,
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
id: 'book.20',
|
|
22
|
+
game: 'traxloop',
|
|
23
|
+
notation: '@0/ @1/ B0+ B0/ C3+ D3+ E3\\',
|
|
24
|
+
gatesRegressions: false,
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
id: 'book.30',
|
|
28
|
+
game: 'traxloop',
|
|
29
|
+
notation:
|
|
30
|
+
'@0/ @1/ @1/ D1+ A2+ E1+ E2+ E0+ F1\\ G1+ G2\\ F3+ E4\\ G3+ H2\\ C5\\ D5+ D6\\ D7\\',
|
|
31
|
+
gatesRegressions: true,
|
|
32
|
+
},
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
const ESC = String.fromCodePoint(27)
|
|
36
|
+
const GREEN = `${ESC}[38;5;35m`
|
|
37
|
+
const BLUE = `${ESC}[38;5;39m`
|
|
38
|
+
const ORANGE = `${ESC}[38;5;208m`
|
|
39
|
+
const RED = `${ESC}[38;5;196m`
|
|
40
|
+
const GRAY = `${ESC}[38;5;240m`
|
|
41
|
+
const RESET = `${ESC}[0m`
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
Wrap a string with an ANSI color escape sequence.
|
|
45
|
+
@param {string} string_ - the string to colorize
|
|
46
|
+
@param {string} ansi - the ANSI escape code to apply
|
|
47
|
+
@returns {string} the colorized string
|
|
48
|
+
*/
|
|
49
|
+
const colorize = (string_, ansi) => `${ansi}${string_}${RESET}`
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
Pick an ANSI color for a benchmark regression percentage.
|
|
53
|
+
@param {number} pct - the percentage change, positive means slower
|
|
54
|
+
@returns {string} an ANSI escape code for the appropriate color
|
|
55
|
+
*/
|
|
56
|
+
function diffColor(pct) {
|
|
57
|
+
if (pct > 20) return RED
|
|
58
|
+
if (pct > 5) return ORANGE
|
|
59
|
+
if (pct < -20) return GREEN
|
|
60
|
+
if (pct < -5) return BLUE
|
|
61
|
+
return GRAY
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
Run a function repeatedly and return timing results.
|
|
66
|
+
@param {() => unknown} fn - the function to benchmark
|
|
67
|
+
@param {number} iterations - how many times to run fn
|
|
68
|
+
@returns {{ count: number, ms: number }} average time per iteration
|
|
69
|
+
*/
|
|
70
|
+
function bench(fn, iterations) {
|
|
71
|
+
const start = performance.now()
|
|
72
|
+
for (let i = 0; i < iterations; i++) fn()
|
|
73
|
+
const ms = (performance.now() - start) / iterations
|
|
74
|
+
return { count: iterations, ms }
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** @typedef {{ name: string, current: string, prior: string, diff: string, percent: string, color: string, isRegression: boolean }} MetricRow */
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
Benchmark a single game position across all metrics.
|
|
81
|
+
@param {TraxVariant} game - the game variant to use
|
|
82
|
+
@param {string} notation - the move sequence to set up the position
|
|
83
|
+
@param {Record<string, { count: number, ms: number }> | undefined} baseline - prior results to compare against
|
|
84
|
+
@returns {{ results: Record<string, { count: number, ms: number }>, rows: MetricRow[] }} timing results and formatted comparison rows
|
|
85
|
+
*/
|
|
86
|
+
function benchPosition(game, notation, baseline) {
|
|
87
|
+
const instance = new Trax(game, notation)
|
|
88
|
+
const { moves } = instance
|
|
89
|
+
const lastMove = moves.at(-1)
|
|
90
|
+
|
|
91
|
+
const construction = bench(() => new Trax(game, notation), 5000)
|
|
92
|
+
const saveRestore = bench(() => {
|
|
93
|
+
const saved = instance.save()
|
|
94
|
+
instance.restore(saved)
|
|
95
|
+
}, 1000)
|
|
96
|
+
const possibleMovesResult = bench(() => instance.possibleMoves(), 1000)
|
|
97
|
+
const play = bench(() => {
|
|
98
|
+
const g = new Trax(game, notation)
|
|
99
|
+
g.play(/** @type {string} */ (lastMove))
|
|
100
|
+
}, 1000)
|
|
101
|
+
const canonical = bench(() => instance.canonical, 5000)
|
|
102
|
+
|
|
103
|
+
/** @type {Record<string, { count: number, ms: number }>} */
|
|
104
|
+
const results = {
|
|
105
|
+
construction,
|
|
106
|
+
'save+restore': saveRestore,
|
|
107
|
+
possibleMoves: possibleMovesResult,
|
|
108
|
+
play,
|
|
109
|
+
canonical,
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** @type {MetricRow[]} */
|
|
113
|
+
const rows = []
|
|
114
|
+
for (const [name, { ms }] of Object.entries(results)) {
|
|
115
|
+
const prior = baseline?.[name]
|
|
116
|
+
const current = `${ms.toFixed(4)} ms`
|
|
117
|
+
if (prior) {
|
|
118
|
+
const delta = ms - prior.ms
|
|
119
|
+
const pct = (delta / prior.ms) * 100
|
|
120
|
+
const sign = delta >= 0 ? '+' : ''
|
|
121
|
+
rows.push({
|
|
122
|
+
name,
|
|
123
|
+
current,
|
|
124
|
+
prior: `${prior.ms.toFixed(4)} ms`,
|
|
125
|
+
diff: `${sign}${delta.toFixed(4)} ms`,
|
|
126
|
+
percent: `${sign}${pct.toFixed(1)}%`,
|
|
127
|
+
color: diffColor(pct),
|
|
128
|
+
isRegression: pct > 20,
|
|
129
|
+
})
|
|
130
|
+
} else {
|
|
131
|
+
rows.push({
|
|
132
|
+
name,
|
|
133
|
+
current,
|
|
134
|
+
prior: '',
|
|
135
|
+
diff: '',
|
|
136
|
+
percent: '',
|
|
137
|
+
color: GRAY,
|
|
138
|
+
isRegression: false,
|
|
139
|
+
})
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return { results, rows }
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
Load prior benchmark results from disk.
|
|
148
|
+
@returns {Record<string, Record<string, Record<string, { count: number, ms: number }>>>} all saved results keyed by version
|
|
149
|
+
*/
|
|
150
|
+
function loadResults() {
|
|
151
|
+
try {
|
|
152
|
+
return JSON.parse(readFileSync(RESULTS_PATH, 'utf8'))
|
|
153
|
+
} catch {
|
|
154
|
+
return {}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
Print benchmark results to the console.
|
|
160
|
+
@param {MetricRow[][]} allRows - one array of rows per position
|
|
161
|
+
*/
|
|
162
|
+
function printResults(allRows) {
|
|
163
|
+
const flat = allRows.flat()
|
|
164
|
+
const hasComparison = flat.some((r) => r.prior !== '')
|
|
165
|
+
const nameWidth = Math.max(...flat.map((r) => r.name.length))
|
|
166
|
+
const currentWidth = Math.max(...flat.map((r) => r.current.length))
|
|
167
|
+
const priorWidth = Math.max(...flat.map((r) => r.prior.length))
|
|
168
|
+
const diffWidth = Math.max(...flat.map((r) => r.diff.length))
|
|
169
|
+
const pctWidth = Math.max(...flat.map((r) => r.percent.length))
|
|
170
|
+
|
|
171
|
+
for (const [index, position] of POSITIONS.entries()) {
|
|
172
|
+
console.log(`\n ${position.id}`)
|
|
173
|
+
for (const row of allRows[index]) {
|
|
174
|
+
const name = row.name.padEnd(nameWidth)
|
|
175
|
+
const current = row.current.padStart(currentWidth)
|
|
176
|
+
if (hasComparison && row.prior) {
|
|
177
|
+
const prior = `was ${row.prior.padStart(priorWidth)}`
|
|
178
|
+
const diff = colorize(row.diff.padStart(diffWidth), row.color)
|
|
179
|
+
const pct = colorize(row.percent.padStart(pctWidth), row.color)
|
|
180
|
+
console.log(` ${name} ${current} (${prior}, ${diff}, ${pct})`)
|
|
181
|
+
} else {
|
|
182
|
+
console.log(` ${name} ${current}`)
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
console.log(`\n@slugbugblue/trax benchmark v${version}`)
|
|
189
|
+
console.log('='.repeat(50))
|
|
190
|
+
|
|
191
|
+
const allResults = loadResults()
|
|
192
|
+
const baseline = allResults[version]
|
|
193
|
+
/** @type {Record<string, Record<string, { count: number, ms: number }>>} */
|
|
194
|
+
const versionResults = {}
|
|
195
|
+
/** @type {MetricRow[][]} */
|
|
196
|
+
const allRows = []
|
|
197
|
+
const allRegressions = []
|
|
198
|
+
|
|
199
|
+
for (const { id, game, notation, gatesRegressions } of POSITIONS) {
|
|
200
|
+
const { results, rows } = benchPosition(game, notation, baseline?.[id])
|
|
201
|
+
versionResults[id] = results
|
|
202
|
+
allRows.push(rows)
|
|
203
|
+
if (gatesRegressions) {
|
|
204
|
+
allRegressions.push(
|
|
205
|
+
...rows
|
|
206
|
+
.filter((r) => r.isRegression)
|
|
207
|
+
.map(
|
|
208
|
+
(r) =>
|
|
209
|
+
` ${id}/${r.name}: ${r.prior} → ${r.current} (${r.percent})`,
|
|
210
|
+
),
|
|
211
|
+
)
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
printResults(allRows)
|
|
216
|
+
console.log('')
|
|
217
|
+
|
|
218
|
+
if (!baseline) {
|
|
219
|
+
allResults[version] = versionResults
|
|
220
|
+
writeFileSync(RESULTS_PATH, JSON.stringify(allResults, null, 2) + '\n')
|
|
221
|
+
console.log('Results written to benchmark/benchmarks.json')
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
if (allRegressions.length > 0) {
|
|
225
|
+
console.log('\nRegressions detected:')
|
|
226
|
+
for (const message of allRegressions) console.log(message)
|
|
227
|
+
throw new Error('Benchmark regressions detected')
|
|
228
|
+
}
|
package/gl-sast-report.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":"15.2.
|
|
1
|
+
{"version":"15.2.4","vulnerabilities":[],"scan":{"analyzer":{"id":"semgrep","name":"Semgrep","url":"https://gitlab.com/gitlab-org/security-products/analyzers/semgrep","vendor":{"name":"GitLab"},"version":"6.19.1"},"scanner":{"id":"semgrep","name":"Semgrep","url":"https://github.com/returntocorp/semgrep","vendor":{"name":"GitLab"},"version":"1.168.0"},"type":"sast","start_time":"2026-08-16T23:13:50","end_time":"2026-08-16T23:13:59","status":"success","observability":{"events":[{"event":"collect_sast_scan_metrics_from_pipeline","property":"b7450097-a7e5-4ab8-88e8-d42c900bb7ec","label":"semgrep","value":0,"version":"6.19.1","exit_code":0,"override_count":0,"passthrough_count":0,"custom_exclude_path_count":0,"time_s":8,"file_count":8}]}}}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@slugbugblue/trax",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"description": "Trax game engine",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"trax",
|
|
@@ -26,8 +26,9 @@
|
|
|
26
26
|
"url": "git+https://gitlab.com/slugbugblue/trax.git"
|
|
27
27
|
},
|
|
28
28
|
"scripts": {
|
|
29
|
+
"benchmark": "node benchmark/engine.bench.js",
|
|
29
30
|
"genversion": "genversion --esm src/version.js",
|
|
30
|
-
"git-add": "git add src/version.js src/engine.d.ts",
|
|
31
|
+
"git-add": "git add src/version.js src/engine.d.ts benchmark/benchmarks.json",
|
|
31
32
|
"ts-build": "tsc -p tsconfig.build.json && rm -f src/version.d.ts",
|
|
32
33
|
"test": "xo && c8 ava",
|
|
33
34
|
"prepare": "[ -n \"$CI\" ] || husky",
|
|
@@ -40,12 +41,12 @@
|
|
|
40
41
|
"@slugbugblue/point": "^1.0.0"
|
|
41
42
|
},
|
|
42
43
|
"devDependencies": {
|
|
43
|
-
"ava": "^
|
|
44
|
-
"c8": "^
|
|
44
|
+
"ava": "^8.0.1",
|
|
45
|
+
"c8": "^12.0.0",
|
|
45
46
|
"genversion": "^3.0.2",
|
|
46
47
|
"husky": "^9.1.7",
|
|
47
48
|
"prettier": "^3.6.2",
|
|
48
|
-
"xo": "^
|
|
49
|
+
"xo": "^4.0.0"
|
|
49
50
|
},
|
|
50
51
|
"type": "module",
|
|
51
52
|
"engines": {
|