@slugbugblue/trax 0.17.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/.husky/pre-commit +4 -0
- package/CHANGELOG.md +32 -6
- package/Dockerfile +9 -0
- package/README.md +14 -5
- package/benchmark/benchmarks.json +60 -0
- package/benchmark/puzzles.bench.js +190 -0
- package/docs/analyst.md +16 -2
- package/docs/engine.md +2 -2
- package/docs/lru.md +151 -0
- package/package.json +7 -3
- package/src/analyst.js +139 -8
- package/src/cli.js +10 -1
- package/src/cmds/analyze.js +24 -4
- package/src/cmds/import-export.js +2 -7
- package/src/cmds/view.js +5 -2
- package/src/engine.js +8 -1
- package/src/lru.js +153 -0
- package/src/point.js +1 -1
- package/src/puzzles.js +135 -9
- package/src/threats.js +25 -4
- package/src/types.d.ts +15 -1
- package/src/version.js +1 -1
package/src/analyst.js
CHANGED
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
/** Analyst module: provides analysis functions for Trax games.
|
|
2
|
-
* @copyright 2022
|
|
2
|
+
* @copyright 2022-2023
|
|
3
3
|
* @author Chad Transtrum <chad@transtrum.net>
|
|
4
4
|
* @license Apache-2.0
|
|
5
5
|
*/
|
|
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'
|
|
@@ -170,6 +173,7 @@ const pairUp = (rawEdge) => {
|
|
|
170
173
|
*/
|
|
171
174
|
const label = (rawEdge) => {
|
|
172
175
|
const final = { w: '', b: '', edge: rawEdge }
|
|
176
|
+
let index = 0
|
|
173
177
|
for (const space of rawEdge) {
|
|
174
178
|
for (const bw of ['b', 'w']) {
|
|
175
179
|
const colors = new Set([bw, 'l'])
|
|
@@ -186,6 +190,9 @@ const label = (rawEdge) => {
|
|
|
186
190
|
space['z' + bw] = z
|
|
187
191
|
final[bw] += z
|
|
188
192
|
}
|
|
193
|
+
|
|
194
|
+
space.idx = index
|
|
195
|
+
index += 1
|
|
189
196
|
}
|
|
190
197
|
|
|
191
198
|
return final
|
|
@@ -248,7 +255,7 @@ const passiveAttack = (from) => threatCount(from) > 1
|
|
|
248
255
|
const underAttack = (from) => attackCount(from) + threatCount(from) > 0
|
|
249
256
|
|
|
250
257
|
// Analysis class
|
|
251
|
-
export
|
|
258
|
+
export class Analysis {
|
|
252
259
|
/** @type {TraxVariant} */
|
|
253
260
|
#rules = 'trax'
|
|
254
261
|
#id = 'analysis'
|
|
@@ -260,14 +267,21 @@ export const Analysis = class {
|
|
|
260
267
|
#game
|
|
261
268
|
#scores = {}
|
|
262
269
|
#threats = undefined
|
|
270
|
+
#faulty = { b: [], w: [] }
|
|
271
|
+
#partial = false
|
|
263
272
|
|
|
264
|
-
constructor(game) {
|
|
273
|
+
constructor(game, partial = false) {
|
|
265
274
|
// Make a copy of the game
|
|
266
275
|
this.#rules = game.rules
|
|
267
276
|
this.#moves = game.moves
|
|
268
277
|
this.#id = id()
|
|
269
278
|
this.#notation = game.notation
|
|
270
279
|
this.#game = new Trax(this.#rules, this.#moves, this.#id)
|
|
280
|
+
this.count = {
|
|
281
|
+
w: (level) => this.threats.w[level]?.length || 0,
|
|
282
|
+
b: (level) => this.threats.b[level]?.length || 0,
|
|
283
|
+
}
|
|
284
|
+
this.#partial = Boolean(partial)
|
|
271
285
|
}
|
|
272
286
|
|
|
273
287
|
get game() {
|
|
@@ -288,12 +302,20 @@ export const Analysis = class {
|
|
|
288
302
|
return this.#edge
|
|
289
303
|
}
|
|
290
304
|
|
|
305
|
+
/** Any faulty threats
|
|
306
|
+
* @returns {FaultyThreats}
|
|
307
|
+
*/
|
|
308
|
+
get faulty() {
|
|
309
|
+
return this.#faulty
|
|
310
|
+
}
|
|
311
|
+
|
|
291
312
|
/** All threats present in this position.
|
|
292
313
|
* @returns {FoundThreatsCollection}
|
|
293
314
|
*/
|
|
294
315
|
get threats() {
|
|
295
316
|
if (!this.#threats) {
|
|
296
317
|
this.#threats = { b: this.#applyThreats('b'), w: this.#applyThreats('w') }
|
|
318
|
+
if (!this.#partial) this.#validateThreats()
|
|
297
319
|
}
|
|
298
320
|
|
|
299
321
|
return this.#threats
|
|
@@ -528,9 +550,43 @@ export const Analysis = class {
|
|
|
528
550
|
match: match[1], // The match is in the capturing group
|
|
529
551
|
at: match.index,
|
|
530
552
|
value: threat.value,
|
|
553
|
+
level: threat.depth,
|
|
531
554
|
}
|
|
532
555
|
}
|
|
533
556
|
|
|
557
|
+
/**
|
|
558
|
+
* @arg {FoundThreat} threat - a threat
|
|
559
|
+
* @arg {string} color - the player color
|
|
560
|
+
* @returns {Point[]} - locations relevant to the threat
|
|
561
|
+
*/
|
|
562
|
+
#threatLocs(threat, color) {
|
|
563
|
+
const { edge } = this.edge
|
|
564
|
+
const max = edge.length
|
|
565
|
+
let index = threat.at
|
|
566
|
+
const locs = new Set([index])
|
|
567
|
+
while (threat.match.length > locs.size) {
|
|
568
|
+
index += 1
|
|
569
|
+
if (index >= max) index -= max
|
|
570
|
+
locs.add(index)
|
|
571
|
+
// Check to see if we have a connectable pair
|
|
572
|
+
if (edge[index]['p' + color]) {
|
|
573
|
+
const line = 'n' + color
|
|
574
|
+
const end = edge.find(
|
|
575
|
+
(space) =>
|
|
576
|
+
space.idx !== edge[index].idx && space[line] === edge[index][line],
|
|
577
|
+
)
|
|
578
|
+
if (end) {
|
|
579
|
+
// Add the opposite end and the space right before it, just in case
|
|
580
|
+
locs.add(end.idx)
|
|
581
|
+
locs.add(end.idx - 1)
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
const value = [...locs].map((i) => new Point(edge[i]))
|
|
587
|
+
return value
|
|
588
|
+
}
|
|
589
|
+
|
|
534
590
|
/** Apply the threats to a color's regex-ready edge.
|
|
535
591
|
* @arg {Color} color - the color to find threats for
|
|
536
592
|
* @returns {FoundThreats}
|
|
@@ -544,11 +600,10 @@ export const Analysis = class {
|
|
|
544
600
|
const find = matches[threat.depth] || []
|
|
545
601
|
for (const match of edge.matchAll(threat.rx)) {
|
|
546
602
|
if (match.index >= length) break
|
|
603
|
+
|
|
604
|
+
// Avoid saving duplicates
|
|
547
605
|
if (find.every((f) => f.at !== match.index || f.match !== match[1])) {
|
|
548
606
|
find.push(this.#foundThreat(threat, match))
|
|
549
|
-
} else {
|
|
550
|
-
const duplicate = matches[99] || []
|
|
551
|
-
duplicate.push(this.#foundThreat(threat, match))
|
|
552
607
|
}
|
|
553
608
|
}
|
|
554
609
|
|
|
@@ -558,6 +613,74 @@ export const Analysis = class {
|
|
|
558
613
|
return matches
|
|
559
614
|
}
|
|
560
615
|
|
|
616
|
+
/** Clean up the list of matches, removing any false positives. */
|
|
617
|
+
#validateThreats() {
|
|
618
|
+
for (const bw of ['w', 'b']) {
|
|
619
|
+
/** @type FoundThreats */
|
|
620
|
+
const matches = this.#threats[bw]
|
|
621
|
+
for (const key of Object.keys(matches)) {
|
|
622
|
+
const level = Number(key)
|
|
623
|
+
if (level < 2) continue
|
|
624
|
+
/** @type {FoundThreat[]} */
|
|
625
|
+
const verified = []
|
|
626
|
+
const threats = matches[key]
|
|
627
|
+
for (const threat of threats) {
|
|
628
|
+
if (this.#canActivateThreat(threat, bw, level)) {
|
|
629
|
+
verified.push(threat)
|
|
630
|
+
} else {
|
|
631
|
+
this.#faulty[bw].push(threat)
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
if (verified.length > 0) {
|
|
636
|
+
matches[key] = verified
|
|
637
|
+
} else {
|
|
638
|
+
delete matches[key]
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
/** Determine if the given threat can be activated for this color
|
|
645
|
+
* @param {FoundThreat} threat - The threat to try to activate
|
|
646
|
+
* @param {string} color - The color this match belongs to
|
|
647
|
+
* @param {number} level - The depth level of this threat
|
|
648
|
+
* @returns {boolean}
|
|
649
|
+
*/
|
|
650
|
+
#canActivateThreat(threat, color, level) {
|
|
651
|
+
const attacks = this.#threats[color][1]?.length ?? 0
|
|
652
|
+
const other = this.#threats[Trax.other(color)][1]?.length ?? 0
|
|
653
|
+
const higher = this.#threats[color][level - 1]?.length ?? 0
|
|
654
|
+
for (const loc of this.#threatLocs(threat, color)) {
|
|
655
|
+
for (const type of this.game.possibleTiles(loc)) {
|
|
656
|
+
const game = new Trax(this.game.rules, this.game.moves)
|
|
657
|
+
const result = game.dropTile(type, loc)
|
|
658
|
+
if (!result.valid) continue
|
|
659
|
+
|
|
660
|
+
// This might be a bad shortcut, but if we already have an attack here
|
|
661
|
+
// we just assume that this threat is also valid ...
|
|
662
|
+
if (game.over && game.turn === Trax.playerNumber(color)) return true
|
|
663
|
+
|
|
664
|
+
const { threats } = analyze(game, true)
|
|
665
|
+
|
|
666
|
+
// If we give our opponent an attack, this is a faulty move
|
|
667
|
+
const faulty = (threats[Trax.other(color)][1]?.length ?? 0) - other
|
|
668
|
+
if (faulty > 0) continue
|
|
669
|
+
|
|
670
|
+
// Activating the threat means we get an attack and a threat
|
|
671
|
+
const newAttacks = (threats[color][1]?.length ?? 0) - attacks
|
|
672
|
+
if (level === 2) {
|
|
673
|
+
if (newAttacks > 1) return true
|
|
674
|
+
} else {
|
|
675
|
+
const newThreats = (threats[color][level - 1]?.length ?? 0) - higher
|
|
676
|
+
if (newAttacks > 0 && newThreats > 0) return true
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
return false
|
|
682
|
+
}
|
|
683
|
+
|
|
561
684
|
// Make a reasonable inspect representation of the analysis
|
|
562
685
|
[Symbol.for('nodejs.util.inspect.custom')](depth, options, inspect) {
|
|
563
686
|
const sub = (object) => inspect(object, options).replace(/\n/g, '\n ')
|
|
@@ -592,11 +715,19 @@ export const Analysis = class {
|
|
|
592
715
|
|
|
593
716
|
/** Analyze a game position.
|
|
594
717
|
* @arg {Trax} game - the position to analyze
|
|
718
|
+
* @arg {boolean} partial - if the position should only be partially analyzed
|
|
595
719
|
* @returns {Analysis}
|
|
596
720
|
*/
|
|
597
|
-
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
|
+
}
|
|
598
729
|
|
|
599
|
-
export
|
|
730
|
+
export class Suggestion {
|
|
600
731
|
/** @type {PositionScore[]} */
|
|
601
732
|
all = []
|
|
602
733
|
/** @type {PositionScore[]} */
|
package/src/cli.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
/* Copyright 2022 Chad Transtrum
|
|
2
|
+
/* Copyright 2022-2023 Chad Transtrum
|
|
3
3
|
*
|
|
4
4
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
5
|
* you may not use this file except in compliance with the License.
|
|
@@ -326,6 +326,14 @@ const CLI = {
|
|
|
326
326
|
const id = newId(DATA)
|
|
327
327
|
const trax = new Trax(puzzle.game, puzzle.notation, 'cli')
|
|
328
328
|
const players = ['challenger', 'puzzlebot']
|
|
329
|
+
const notes = []
|
|
330
|
+
if (puzzle.title) notes.push({ move: 0, note: puzzle.title })
|
|
331
|
+
if (puzzle.desc) notes.push({ move: 0, note: puzzle.desc })
|
|
332
|
+
const player = [0, 'white', 'black'][puzzle.player]
|
|
333
|
+
notes.push(
|
|
334
|
+
{ move: 0, note: 'Starting position ' + puzzle.notation },
|
|
335
|
+
{ move: 0, note: `${puzzle.id}: ${player} to win by move ${puzzle.max}` },
|
|
336
|
+
)
|
|
329
337
|
DATA.games[id] = {
|
|
330
338
|
id,
|
|
331
339
|
puzzle: puzzle.id,
|
|
@@ -335,6 +343,7 @@ const CLI = {
|
|
|
335
343
|
max: puzzle.max,
|
|
336
344
|
players: trax.turn === 1 ? players : players.reverse(),
|
|
337
345
|
moves: trax.moves.join(' '),
|
|
346
|
+
notes,
|
|
338
347
|
}
|
|
339
348
|
|
|
340
349
|
if (DATA.puzzles[puzzle.id]) {
|
package/src/cmds/analyze.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/* Copyright 2022 Chad Transtrum
|
|
1
|
+
/* Copyright 2022-2023 Chad Transtrum
|
|
2
2
|
*
|
|
3
3
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
4
|
* you may not use this file except in compliance with the License.
|
|
@@ -54,6 +54,17 @@ const listThreats = (threats, detailed) => {
|
|
|
54
54
|
return summary.join(' ')
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
+
const listFaulty = (faulty) => {
|
|
58
|
+
const summary = []
|
|
59
|
+
for (const threat of faulty) {
|
|
60
|
+
summary.push(
|
|
61
|
+
`[L${threat.level} ${threat.at}:${threat.threat}:${threat.match}]`,
|
|
62
|
+
)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return 'Faulty: ' + summary.join(' ')
|
|
66
|
+
}
|
|
67
|
+
|
|
57
68
|
analyzeCmd.fn = (CLI, move) => {
|
|
58
69
|
if (!String(CLI.GAME?.id)) {
|
|
59
70
|
return CLI.error('No active game. Type "new" to start a game.')
|
|
@@ -75,18 +86,27 @@ analyzeCmd.fn = (CLI, move) => {
|
|
|
75
86
|
return CLI.error('Game has ended.')
|
|
76
87
|
}
|
|
77
88
|
|
|
78
|
-
const { edge, threats, scores } = analyze(trax)
|
|
89
|
+
const { edge, threats, scores, faulty } = analyze(trax)
|
|
79
90
|
|
|
80
|
-
if (detailed) CLI.out(edge.w)
|
|
91
|
+
if (detailed) CLI.out(CLI.color.white(edge.w))
|
|
81
92
|
CLI.out(
|
|
82
93
|
CLI.bubble('w', scores.w) +
|
|
83
94
|
CLI.color.white(' ') +
|
|
84
95
|
listThreats(threats.w, detailed),
|
|
85
96
|
)
|
|
86
|
-
|
|
97
|
+
|
|
98
|
+
if (detailed) {
|
|
99
|
+
if (faulty.w.length > 0) CLI.out(CLI.color.white('') + listFaulty(faulty.w))
|
|
100
|
+
CLI.out(CLI.color.black(edge.b))
|
|
101
|
+
}
|
|
102
|
+
|
|
87
103
|
CLI.out(
|
|
88
104
|
CLI.bubble('b', scores.b) +
|
|
89
105
|
CLI.color.black(' ') +
|
|
90
106
|
listThreats(threats.b, detailed),
|
|
91
107
|
)
|
|
108
|
+
|
|
109
|
+
if (detailed && faulty.b.length > 0) {
|
|
110
|
+
CLI.out(CLI.color.black('') + listFaulty(faulty.b))
|
|
111
|
+
}
|
|
92
112
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/* Copyright 2022 Chad Transtrum
|
|
1
|
+
/* Copyright 2022-2023 Chad Transtrum
|
|
2
2
|
*
|
|
3
3
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
4
|
* you may not use this file except in compliance with the License.
|
|
@@ -332,12 +332,7 @@ exportCmd.fn = (CLI, id, filename) => {
|
|
|
332
332
|
if (source.url) content += '\n; ' + source.url
|
|
333
333
|
}
|
|
334
334
|
|
|
335
|
-
|
|
336
|
-
if (puzzle.desc) content += '\n; ' + puzzle.desc
|
|
337
|
-
content += '\n; Starting position ' + puzzle.notation + '\n; '
|
|
338
|
-
const players = [0, 'white', 'black']
|
|
339
|
-
content += players[puzzle.player] + ' to win by move '
|
|
340
|
-
content += String(puzzle.max) + '\n'
|
|
335
|
+
content += '\n'
|
|
341
336
|
}
|
|
342
337
|
}
|
|
343
338
|
|
package/src/cmds/view.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/* Copyright 2022 Chad Transtrum
|
|
1
|
+
/* Copyright 2022-2023 Chad Transtrum
|
|
2
2
|
*
|
|
3
3
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
4
|
* you may not use this file except in compliance with the License.
|
|
@@ -53,6 +53,9 @@ viewCmd.fn = (CLI, move) => {
|
|
|
53
53
|
CLI.bubble('b' + (trax.turn === 2 ? 'h' : ''), players[1]),
|
|
54
54
|
)
|
|
55
55
|
CLI.display(trax, players, undefined, show)
|
|
56
|
-
if (puzzle
|
|
56
|
+
if (puzzle && (!note || !note.includes(puzzle))) {
|
|
57
|
+
CLI.out(CLI.color('Puzzle ' + puzzle))
|
|
58
|
+
}
|
|
59
|
+
|
|
57
60
|
if (note) CLI.out(CLI.color.help(note))
|
|
58
61
|
}
|
package/src/engine.js
CHANGED
|
@@ -33,6 +33,13 @@ const notationRegex = /^([@a-z]+)(\d+)([/\\+])$/i
|
|
|
33
33
|
const tileTypes = ['a', 'b', 'c', 'd', 'e', 'f']
|
|
34
34
|
|
|
35
35
|
/** Tiles are represented by the letters a-f.
|
|
36
|
+
* a b c d e f
|
|
37
|
+
* +--#--+ +--#--+ +--#--+ +--o--+ +--o--+ +--o--+
|
|
38
|
+
* | # | | # | | # | | o | | o | | o |
|
|
39
|
+
* oo ## ooo#ooo ## oo oo ## ####### ## oo
|
|
40
|
+
* | o | | # | | o | | # | | o | | # |
|
|
41
|
+
* +--o--+ +--#--+ +--o--+ +--#--+ +--o--+ +--#--+
|
|
42
|
+
*
|
|
36
43
|
* Slashes are represented by / \ or +.
|
|
37
44
|
* @readonly
|
|
38
45
|
* @type {Record<ValidTiles, Slash>}
|
|
@@ -120,7 +127,7 @@ const codeRowLength = (row) =>
|
|
|
120
127
|
|
|
121
128
|
// This is where the magic happens
|
|
122
129
|
/** A digital representation of a Trax game. */
|
|
123
|
-
export
|
|
130
|
+
export class Trax {
|
|
124
131
|
// Static class properties
|
|
125
132
|
|
|
126
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