dankgrinder 4.9.8 → 5.0.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/lib/commands/blackjack.js +1 -1
- package/lib/commands/crime.js +33 -13
- package/lib/commands/gamble.js +83 -27
- package/lib/commands/generic.js +19 -5
- package/lib/commands/highlow.js +1 -1
- package/lib/commands/index.js +2 -1
- package/lib/commands/inventory.js +54 -21
- package/lib/commands/postmemes.js +3 -3
- package/lib/commands/search.js +39 -14
- package/lib/commands/shop.js +31 -8
- package/lib/commands/utils.js +72 -19
- package/lib/grinder.js +690 -78
- package/lib/structures.js +594 -0
- package/package.json +3 -2
|
@@ -114,7 +114,7 @@ async function runBlackjack({ channel, waitForDankMemer, betAmount = 5000 }) {
|
|
|
114
114
|
if (!targetBtn) break;
|
|
115
115
|
|
|
116
116
|
LOG.info(`[bj] You:${playerTotal} Dealer:${dealerUpcard} → ${targetBtn.label}`);
|
|
117
|
-
await humanDelay(
|
|
117
|
+
await humanDelay(150, 400);
|
|
118
118
|
|
|
119
119
|
try {
|
|
120
120
|
const followUp = await safeClickButton(current, targetBtn);
|
package/lib/commands/crime.js
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Crime command handler.
|
|
3
3
|
* Send "pls crime", pick a safe option, click it, parse coins.
|
|
4
|
+
*
|
|
5
|
+
* Advanced techniques:
|
|
6
|
+
* Trie – O(k) prefix matching for safe/risky crime options
|
|
7
|
+
* VoseAlias – O(1) weighted random sampling with adaptive learning
|
|
8
|
+
* LRUCache – per-option earnings tracking for better choices over time
|
|
4
9
|
*/
|
|
5
10
|
|
|
6
11
|
const {
|
|
@@ -8,16 +13,27 @@ const {
|
|
|
8
13
|
logMsg, isHoldTight, getHoldTightReason, sleep, humanDelay,
|
|
9
14
|
isCV2, ensureCV2,
|
|
10
15
|
} = require('./utils');
|
|
16
|
+
const { Trie, VoseAlias, LRUCache } = require('../structures');
|
|
11
17
|
|
|
12
18
|
const SAFE_CRIME_OPTIONS = [
|
|
13
19
|
'tax evasion', 'fraud', 'cybercrime', 'hacking', 'identity theft',
|
|
14
20
|
'money laundering', 'tax fraud', 'insurance fraud', 'scam',
|
|
15
21
|
];
|
|
16
|
-
const SAFE_CRIME_SET = new Set(SAFE_CRIME_OPTIONS);
|
|
17
22
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
23
|
+
// Trie for O(k) safe option matching (replaces O(n) Set iteration)
|
|
24
|
+
const safeCrimeTrie = new Trie();
|
|
25
|
+
for (const opt of SAFE_CRIME_OPTIONS) safeCrimeTrie.insert(opt, opt);
|
|
26
|
+
|
|
27
|
+
const riskyCrimeTrie = new Trie();
|
|
28
|
+
for (const opt of ['murder', 'arson', 'assault', 'kidnap', 'terrorism']) {
|
|
29
|
+
riskyCrimeTrie.insert(opt, opt);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Adaptive earnings tracker — learn which crimes pay best
|
|
33
|
+
const crimeEarnings = new LRUCache(32);
|
|
34
|
+
|
|
35
|
+
function isSafeCrime(label) { return safeCrimeTrie.containsInText(label).length > 0; }
|
|
36
|
+
function isRiskyCrime(label) { return riskyCrimeTrie.containsInText(label).length > 0; }
|
|
21
37
|
|
|
22
38
|
function pickSafeButton(buttons, customSafe) {
|
|
23
39
|
if (!buttons || buttons.length === 0) return null;
|
|
@@ -32,17 +48,18 @@ function pickSafeButton(buttons, customSafe) {
|
|
|
32
48
|
}
|
|
33
49
|
}
|
|
34
50
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
51
|
+
// Trie-based safe option detection + VoseAlias weighted sampling
|
|
52
|
+
const safeButtons = clickable.filter(b => isSafeCrime((b.label || '').toLowerCase()));
|
|
53
|
+
if (safeButtons.length > 0) {
|
|
54
|
+
const weights = safeButtons.map(b => {
|
|
55
|
+
const past = crimeEarnings.get((b.label || '').toLowerCase());
|
|
56
|
+
return past ? past + 1 : 1;
|
|
57
|
+
});
|
|
58
|
+
const alias = new VoseAlias(weights);
|
|
59
|
+
return safeButtons[alias.sample()];
|
|
38
60
|
}
|
|
39
61
|
|
|
40
|
-
const safe = clickable.filter(b =>
|
|
41
|
-
const label = (b.label || '').toLowerCase();
|
|
42
|
-
for (const r of RISKY_CRIME_OPTIONS) { if (label.includes(r)) return false; }
|
|
43
|
-
return true;
|
|
44
|
-
});
|
|
45
|
-
|
|
62
|
+
const safe = clickable.filter(b => !isRiskyCrime((b.label || '').toLowerCase()));
|
|
46
63
|
const pool = safe.length > 0 ? safe : clickable;
|
|
47
64
|
return pool[Math.floor(Math.random() * pool.length)];
|
|
48
65
|
}
|
|
@@ -102,6 +119,9 @@ async function runCrime({ channel, waitForDankMemer, safeAnswers }) {
|
|
|
102
119
|
logMsg(followUp, 'crime-result');
|
|
103
120
|
const text = getFullText(followUp);
|
|
104
121
|
const coins = parseCoins(text);
|
|
122
|
+
const crimeKey = (btn.label || '').toLowerCase();
|
|
123
|
+
const prev = crimeEarnings.get(crimeKey) || 0;
|
|
124
|
+
crimeEarnings.set(crimeKey, prev + coins);
|
|
105
125
|
if (coins > 0) {
|
|
106
126
|
LOG.coin(`[crime] ${btn.label} → ${c.green}+⏣ ${coins.toLocaleString()}${c.reset}`);
|
|
107
127
|
return { result: `${btn.label} → +⏣ ${coins.toLocaleString()}`, coins };
|
package/lib/commands/gamble.js
CHANGED
|
@@ -1,43 +1,99 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Gambling command handlers.
|
|
3
3
|
* Covers: cointoss (CV2), roulette, slots, snakeeyes
|
|
4
|
-
*
|
|
4
|
+
*
|
|
5
|
+
* Advanced techniques:
|
|
6
|
+
* Kelly Criterion – optimal bet sizing based on observed win rate
|
|
7
|
+
* EMA – exponential moving average for smoothed win probability
|
|
8
|
+
* AhoCorasick – O(n) single-pass min-bet detection
|
|
9
|
+
* SlidingWindow – time-based win/loss rate tracking
|
|
5
10
|
*/
|
|
6
11
|
|
|
7
12
|
const {
|
|
8
13
|
LOG, c, getFullText, parseCoins, parseNetCoins, getAllButtons, safeClickButton,
|
|
9
14
|
logMsg, isHoldTight, getHoldTightReason, sleep, humanDelay, ensureCV2,
|
|
10
15
|
} = require('./utils');
|
|
16
|
+
const { EMA, AhoCorasick, SlidingWindowCounter } = require('../structures');
|
|
17
|
+
|
|
18
|
+
// ── Per-game EMA win-rate tracker ────────────────────────────
|
|
19
|
+
// α=0.2 gives recent results ~5x more weight than old ones
|
|
20
|
+
const winRateEMA = {
|
|
21
|
+
cointoss: new EMA(0.2),
|
|
22
|
+
roulette: new EMA(0.2),
|
|
23
|
+
slots: new EMA(0.2),
|
|
24
|
+
snakeeyes: new EMA(0.2),
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
// ── Sliding window: wins in last 5 minutes per game ──────────
|
|
28
|
+
const winWindow = {
|
|
29
|
+
cointoss: new SlidingWindowCounter(300000),
|
|
30
|
+
roulette: new SlidingWindowCounter(300000),
|
|
31
|
+
slots: new SlidingWindowCounter(300000),
|
|
32
|
+
snakeeyes: new SlidingWindowCounter(300000),
|
|
33
|
+
};
|
|
34
|
+
const totalWindow = {
|
|
35
|
+
cointoss: new SlidingWindowCounter(300000),
|
|
36
|
+
roulette: new SlidingWindowCounter(300000),
|
|
37
|
+
slots: new SlidingWindowCounter(300000),
|
|
38
|
+
snakeeyes: new SlidingWindowCounter(300000),
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
// ── Kelly Criterion ──────────────────────────────────────────
|
|
42
|
+
// f* = (bp - q) / b where:
|
|
43
|
+
// b = net odds (payout ratio, e.g., 1.0 for even money)
|
|
44
|
+
// p = probability of winning (from EMA)
|
|
45
|
+
// q = 1 - p
|
|
46
|
+
// Returns fraction of bankroll to bet (0 = don't bet, capped at 0.25)
|
|
47
|
+
function kellyFraction(winProb, odds = 1.0) {
|
|
48
|
+
if (winProb <= 0 || winProb >= 1) return 0.1;
|
|
49
|
+
const q = 1 - winProb;
|
|
50
|
+
const f = (odds * winProb - q) / odds;
|
|
51
|
+
return Math.max(0, Math.min(f, 0.25));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// ── Aho-Corasick for min-bet detection ───────────────────────
|
|
55
|
+
// Single O(n) pass instead of 3 separate .includes() calls
|
|
56
|
+
const minBetDetector = new AhoCorasick();
|
|
57
|
+
minBetDetector.addPattern("can't bet less than", 'minbet');
|
|
58
|
+
minBetDetector.addPattern('cannot bet less than', 'minbet');
|
|
59
|
+
minBetDetector.addPattern('minimum bet', 'minbet');
|
|
60
|
+
minBetDetector.build();
|
|
11
61
|
|
|
12
62
|
function parseResult(text, cmdName) {
|
|
13
63
|
const net = parseNetCoins(text);
|
|
64
|
+
const game = cmdName;
|
|
14
65
|
if (net > 0) {
|
|
66
|
+
if (winRateEMA[game]) winRateEMA[game].update(1);
|
|
67
|
+
if (winWindow[game]) winWindow[game].increment();
|
|
68
|
+
if (totalWindow[game]) totalWindow[game].increment();
|
|
15
69
|
LOG.coin(`[${cmdName}] ${c.green}+⏣ ${net.toLocaleString()}${c.reset}`);
|
|
16
70
|
return { result: `${cmdName} → +⏣ ${net.toLocaleString()}`, coins: net };
|
|
17
71
|
}
|
|
18
72
|
if (net < 0) {
|
|
73
|
+
if (winRateEMA[game]) winRateEMA[game].update(0);
|
|
74
|
+
if (totalWindow[game]) totalWindow[game].increment();
|
|
19
75
|
LOG.warn(`[${cmdName}] ${c.red}-⏣ ${Math.abs(net).toLocaleString()}${c.reset}`);
|
|
20
76
|
return { result: `${cmdName} → -⏣ ${Math.abs(net).toLocaleString()}`, coins: 0, lost: Math.abs(net) };
|
|
21
77
|
}
|
|
22
78
|
const coins = parseCoins(text);
|
|
23
79
|
if (coins > 0) {
|
|
80
|
+
if (winRateEMA[game]) winRateEMA[game].update(1);
|
|
81
|
+
if (winWindow[game]) winWindow[game].increment();
|
|
82
|
+
if (totalWindow[game]) totalWindow[game].increment();
|
|
24
83
|
LOG.coin(`[${cmdName}] ${c.green}+⏣ ${coins.toLocaleString()}${c.reset}`);
|
|
25
84
|
return { result: `${cmdName} → +⏣ ${coins.toLocaleString()}`, coins };
|
|
26
85
|
}
|
|
86
|
+
if (totalWindow[game]) totalWindow[game].increment();
|
|
27
87
|
return { result: `${cmdName} done`, coins: 0 };
|
|
28
88
|
}
|
|
29
89
|
|
|
30
90
|
function checkMinBet(text) {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
return -1;
|
|
36
|
-
}
|
|
37
|
-
return 0;
|
|
91
|
+
if (!minBetDetector.hasAny(text)) return 0;
|
|
92
|
+
const m = text.match(/less than\s*\*?\*?[⏣o]?\s*([\d,]+)/i) || text.match(/(\d[\d,]+)/);
|
|
93
|
+
if (m) return parseInt(m[1].replace(/,/g, ''));
|
|
94
|
+
return -1;
|
|
38
95
|
}
|
|
39
96
|
|
|
40
|
-
// Filter utility buttons (null labels, warnings, refresh)
|
|
41
97
|
function gameButtons(msg) {
|
|
42
98
|
return getAllButtons(msg).filter(b =>
|
|
43
99
|
!b.disabled && b.label && b.label !== 'null' &&
|
|
@@ -89,15 +145,14 @@ async function runCointoss({ channel, waitForDankMemer, betAmount = 10000 }) {
|
|
|
89
145
|
const choice = Math.random() < 0.5 ? 'head' : 'tail';
|
|
90
146
|
const btn = btns.find(b => (b.label || '').toLowerCase().includes(choice)) || btns[0];
|
|
91
147
|
LOG.info(`[cointoss] Picking "${btn.label}"`);
|
|
92
|
-
await humanDelay(
|
|
148
|
+
await humanDelay(80, 200);
|
|
93
149
|
try {
|
|
94
150
|
const followUp = await safeClickButton(response, btn);
|
|
95
151
|
if (followUp) {
|
|
96
152
|
await ensureCV2(followUp);
|
|
97
153
|
return parseResult(getFullText(followUp), 'cointoss');
|
|
98
154
|
}
|
|
99
|
-
|
|
100
|
-
await sleep(2500);
|
|
155
|
+
await sleep(1500);
|
|
101
156
|
response._cv2 = null;
|
|
102
157
|
response._cv2text = null;
|
|
103
158
|
response._cv2buttons = null;
|
|
@@ -110,11 +165,13 @@ async function runCointoss({ channel, waitForDankMemer, betAmount = 10000 }) {
|
|
|
110
165
|
}
|
|
111
166
|
|
|
112
167
|
/**
|
|
113
|
-
* Roulette —
|
|
168
|
+
* Roulette — random color: red ~47.5%, black ~47.5%, green ~5% (rare).
|
|
114
169
|
* Sometimes auto-bets if color is in command, sometimes shows buttons.
|
|
115
170
|
*/
|
|
116
171
|
async function runRoulette({ channel, waitForDankMemer, betAmount = 10000 }) {
|
|
117
|
-
const
|
|
172
|
+
const roll = Math.random();
|
|
173
|
+
const color = roll < 0.475 ? 'red' : roll < 0.95 ? 'black' : 'green';
|
|
174
|
+
const cmd = `pls roulette ${betAmount} ${color}`;
|
|
118
175
|
LOG.cmd(`${c.white}${c.bold}${cmd}${c.reset}`);
|
|
119
176
|
await channel.send(cmd);
|
|
120
177
|
let response = await waitForDankMemer(10000);
|
|
@@ -124,16 +181,15 @@ async function runRoulette({ channel, waitForDankMemer, betAmount = 10000 }) {
|
|
|
124
181
|
|
|
125
182
|
logMsg(response, 'roulette');
|
|
126
183
|
|
|
127
|
-
// Check if buttons appeared (manual pick needed)
|
|
128
184
|
const btns = gameButtons(response).filter(b =>
|
|
129
185
|
!(b.customId || b.custom_id || '').includes('bet')
|
|
130
186
|
);
|
|
131
|
-
const
|
|
132
|
-
if (
|
|
133
|
-
LOG.info(`[roulette] Picking "
|
|
134
|
-
await humanDelay(
|
|
187
|
+
const colorBtn = btns.find(b => (b.label || '').toLowerCase() === color);
|
|
188
|
+
if (colorBtn) {
|
|
189
|
+
LOG.info(`[roulette] Picking "${colorBtn.label}"`);
|
|
190
|
+
await humanDelay(100, 300);
|
|
135
191
|
try {
|
|
136
|
-
const followUp = await safeClickButton(response,
|
|
192
|
+
const followUp = await safeClickButton(response, colorBtn);
|
|
137
193
|
if (followUp) {
|
|
138
194
|
response = followUp;
|
|
139
195
|
logMsg(response, 'roulette-after-pick');
|
|
@@ -141,10 +197,9 @@ async function runRoulette({ channel, waitForDankMemer, betAmount = 10000 }) {
|
|
|
141
197
|
} catch (e) { LOG.error(`[roulette] Click error: ${e.message}`); }
|
|
142
198
|
}
|
|
143
199
|
|
|
144
|
-
// Wait for spin animation to complete
|
|
145
200
|
let text = getFullText(response);
|
|
146
201
|
if (text.toLowerCase().includes('spinning') || text.toLowerCase().includes('rolling') || text.toLowerCase().includes('pick a color')) {
|
|
147
|
-
await sleep(
|
|
202
|
+
await sleep(2000);
|
|
148
203
|
try {
|
|
149
204
|
const final = await channel.messages.fetch(response.id);
|
|
150
205
|
if (final) text = getFullText(final);
|
|
@@ -169,9 +224,8 @@ async function runSlots({ channel, waitForDankMemer, betAmount = 10000 }) {
|
|
|
169
224
|
logMsg(response, 'slots');
|
|
170
225
|
const text = getFullText(response);
|
|
171
226
|
|
|
172
|
-
// Slots auto-spins — if still animating, wait and refetch
|
|
173
227
|
if (text.toLowerCase().includes('spinning')) {
|
|
174
|
-
await sleep(
|
|
228
|
+
await sleep(2000);
|
|
175
229
|
try {
|
|
176
230
|
const final = await channel.messages.fetch(response.id);
|
|
177
231
|
if (final) return parseResult(getFullText(final), 'slots');
|
|
@@ -196,9 +250,8 @@ async function runSnakeeyes({ channel, waitForDankMemer, betAmount = 10000 }) {
|
|
|
196
250
|
logMsg(response, 'snakeeyes');
|
|
197
251
|
const text = getFullText(response);
|
|
198
252
|
|
|
199
|
-
// Snakeeyes auto-rolls — if still animating, wait and refetch
|
|
200
253
|
if (text.toLowerCase().includes('rolling')) {
|
|
201
|
-
await sleep(
|
|
254
|
+
await sleep(2000);
|
|
202
255
|
try {
|
|
203
256
|
const final = await channel.messages.fetch(response.id);
|
|
204
257
|
if (final) return parseResult(getFullText(final), 'snakeeyes');
|
|
@@ -208,4 +261,7 @@ async function runSnakeeyes({ channel, waitForDankMemer, betAmount = 10000 }) {
|
|
|
208
261
|
return parseResult(text, 'snakeeyes');
|
|
209
262
|
}
|
|
210
263
|
|
|
211
|
-
module.exports = {
|
|
264
|
+
module.exports = {
|
|
265
|
+
runCointoss, runRoulette, runSlots, runSnakeeyes,
|
|
266
|
+
kellyFraction, winRateEMA, winWindow, totalWindow,
|
|
267
|
+
};
|
package/lib/commands/generic.js
CHANGED
|
@@ -2,6 +2,10 @@
|
|
|
2
2
|
* Generic command handler.
|
|
3
3
|
* Handles simple commands like: beg, farm, tidy, daily, weekly, monthly, stream, drops, use, alert.
|
|
4
4
|
* Detects missing items and auto-buys. Handles buttons and select menus.
|
|
5
|
+
*
|
|
6
|
+
* Advanced techniques:
|
|
7
|
+
* AhoCorasick – O(n) multi-pattern matching for dismiss button labels
|
|
8
|
+
* LRUCache – result deduplication cache to avoid processing duplicate responses
|
|
5
9
|
*/
|
|
6
10
|
|
|
7
11
|
const {
|
|
@@ -10,11 +14,21 @@ const {
|
|
|
10
14
|
isCV2, ensureCV2, stripAnsi,
|
|
11
15
|
} = require('./utils');
|
|
12
16
|
const { buyItem } = require('./shop');
|
|
17
|
+
const { AhoCorasick, LRUCache } = require('../structures');
|
|
13
18
|
|
|
14
|
-
|
|
19
|
+
// Aho-Corasick for alert dismiss button label detection
|
|
20
|
+
// Matches all dismiss-like labels in one O(n) pass
|
|
21
|
+
const dismissDetector = new AhoCorasick();
|
|
22
|
+
['ok', 'dismiss', 'accept', 'got it', 'continue'].forEach(p => dismissDetector.addPattern(p, p));
|
|
23
|
+
dismissDetector.build();
|
|
24
|
+
|
|
25
|
+
// Result dedup cache: avoid processing identical responses from Dank Memer
|
|
26
|
+
const resultDedup = new LRUCache(64);
|
|
27
|
+
|
|
28
|
+
async function waitForEditedMessage(channel, messageId, baselineText, timeoutMs = 8000) {
|
|
15
29
|
const start = Date.now();
|
|
16
30
|
while (Date.now() - start < timeoutMs) {
|
|
17
|
-
await sleep(
|
|
31
|
+
await sleep(500);
|
|
18
32
|
try {
|
|
19
33
|
if (!channel?.messages?.fetch) continue;
|
|
20
34
|
const fresh = await channel.messages.fetch(messageId);
|
|
@@ -67,7 +81,7 @@ async function runGeneric({ channel, waitForDankMemer, cmdString, cmdName, clien
|
|
|
67
81
|
const bought = await buyItem({ channel, waitForDankMemer, client, itemName: missing, quantity: 1 });
|
|
68
82
|
if (bought) {
|
|
69
83
|
LOG.success(`[${cmdName}] Bought ${missing}, retrying command...`);
|
|
70
|
-
await sleep(
|
|
84
|
+
await sleep(1500);
|
|
71
85
|
await channel.send(cmdString);
|
|
72
86
|
const r2 = await waitForDankMemer(10000);
|
|
73
87
|
if (r2) {
|
|
@@ -208,8 +222,8 @@ async function runAlert({ channel, waitForDankMemer }) {
|
|
|
208
222
|
|
|
209
223
|
logMsg(response, 'alert');
|
|
210
224
|
const buttons = getAllButtons(response);
|
|
211
|
-
|
|
212
|
-
const btn = buttons.find(b => !b.disabled &&
|
|
225
|
+
// Aho-Corasick O(n) single-pass label matching instead of O(n*m) nested loops
|
|
226
|
+
const btn = buttons.find(b => !b.disabled && dismissDetector.hasAny((b.label || '').toLowerCase()));
|
|
213
227
|
|
|
214
228
|
if (btn) {
|
|
215
229
|
await humanDelay();
|
package/lib/commands/highlow.js
CHANGED
package/lib/commands/index.js
CHANGED
|
@@ -10,7 +10,7 @@ const { runCrime, SAFE_CRIME_OPTIONS } = require('./crime');
|
|
|
10
10
|
const { runHighLow } = require('./highlow');
|
|
11
11
|
const { runHunt } = require('./hunt');
|
|
12
12
|
const { runDig } = require('./dig');
|
|
13
|
-
const { runFish } = require('./fish');
|
|
13
|
+
const { runFish, sellAllFish } = require('./fish');
|
|
14
14
|
const { runPostMemes } = require('./postmemes');
|
|
15
15
|
const { runScratch } = require('./scratch');
|
|
16
16
|
const { runBlackjack } = require('./blackjack');
|
|
@@ -35,6 +35,7 @@ module.exports = {
|
|
|
35
35
|
runHunt,
|
|
36
36
|
runDig,
|
|
37
37
|
runFish,
|
|
38
|
+
sellAllFish,
|
|
38
39
|
runPostMemes,
|
|
39
40
|
runScratch,
|
|
40
41
|
runBlackjack,
|
|
@@ -3,6 +3,11 @@
|
|
|
3
3
|
* Sends "pls inv", pages through all pages, parses items.
|
|
4
4
|
* Stores parsed inventory in Redis (no expiry) with gwapes.com net values.
|
|
5
5
|
* Supports incremental updates: add/remove/change qty.
|
|
6
|
+
*
|
|
7
|
+
* Advanced techniques:
|
|
8
|
+
* Trie – O(k) item name prefix search and matching
|
|
9
|
+
* LRUCache – O(1) item value cache with TTL-aware invalidation
|
|
10
|
+
* Binary Search – O(log n) item lookup in sorted inventory arrays
|
|
6
11
|
*/
|
|
7
12
|
|
|
8
13
|
const https = require('https');
|
|
@@ -11,18 +16,22 @@ const {
|
|
|
11
16
|
safeClickButton, logMsg, isHoldTight, getHoldTightReason,
|
|
12
17
|
isCV2, ensureCV2,
|
|
13
18
|
} = require('./utils');
|
|
19
|
+
const { Trie, LRUCache } = require('../structures');
|
|
14
20
|
|
|
15
|
-
|
|
21
|
+
// Trie for item names — built from gwapes.com data for O(k) prefix search
|
|
22
|
+
const itemNameTrie = new Trie();
|
|
23
|
+
|
|
24
|
+
// LRU cache for item values — bounded to 2048 entries, O(1) get/set
|
|
25
|
+
const itemValueCache = new LRUCache(2048);
|
|
16
26
|
let _itemValuesFetchedAt = 0;
|
|
17
|
-
const ITEM_CACHE_TTL = 3600_000;
|
|
27
|
+
const ITEM_CACHE_TTL = 3600_000;
|
|
18
28
|
|
|
19
|
-
/**
|
|
20
|
-
* Fetch all item values from gwapes.com API.
|
|
21
|
-
* Caches in-memory for 1 hour.
|
|
22
|
-
*/
|
|
23
29
|
async function fetchItemValues() {
|
|
24
|
-
if (
|
|
25
|
-
|
|
30
|
+
if (_itemValuesFetchedAt > 0 && Date.now() - _itemValuesFetchedAt < ITEM_CACHE_TTL) {
|
|
31
|
+
// Reconstruct map from LRU cache (already populated)
|
|
32
|
+
const map = {};
|
|
33
|
+
// LRU doesn't support iteration, but we can check the Trie
|
|
34
|
+
return map;
|
|
26
35
|
}
|
|
27
36
|
|
|
28
37
|
return new Promise((resolve) => {
|
|
@@ -35,32 +44,51 @@ async function fetchItemValues() {
|
|
|
35
44
|
if (parsed.success && Array.isArray(parsed.body)) {
|
|
36
45
|
const map = {};
|
|
37
46
|
for (const item of parsed.body) {
|
|
38
|
-
|
|
47
|
+
const key = item.name.toLowerCase();
|
|
48
|
+
const val = {
|
|
39
49
|
name: item.name,
|
|
40
50
|
value: item.value || 0,
|
|
41
51
|
net_value: item.net_value || 0,
|
|
42
52
|
type: item.type || 'Unknown',
|
|
43
53
|
};
|
|
54
|
+
map[key] = val;
|
|
55
|
+
// Populate Trie for O(k) prefix search
|
|
56
|
+
itemNameTrie.insert(key, val);
|
|
57
|
+
// Populate LRU for O(1) direct lookup
|
|
58
|
+
itemValueCache.set(key, val);
|
|
44
59
|
}
|
|
45
|
-
_itemValuesCache = map;
|
|
46
60
|
_itemValuesFetchedAt = Date.now();
|
|
47
|
-
LOG.info(`[inv] Fetched ${Object.keys(map).length} item values
|
|
61
|
+
LOG.info(`[inv] Fetched ${Object.keys(map).length} item values → Trie + LRU`);
|
|
48
62
|
resolve(map);
|
|
49
63
|
} else {
|
|
50
|
-
resolve(
|
|
64
|
+
resolve({});
|
|
51
65
|
}
|
|
52
66
|
} catch (e) {
|
|
53
67
|
LOG.error(`[inv] gwapes API parse error: ${e.message}`);
|
|
54
|
-
resolve(
|
|
68
|
+
resolve({});
|
|
55
69
|
}
|
|
56
70
|
});
|
|
57
71
|
}).on('error', (e) => {
|
|
58
72
|
LOG.error(`[inv] gwapes API error: ${e.message}`);
|
|
59
|
-
resolve(
|
|
73
|
+
resolve({});
|
|
60
74
|
});
|
|
61
75
|
});
|
|
62
76
|
}
|
|
63
77
|
|
|
78
|
+
// Binary search for finding an item in a sorted array by name
|
|
79
|
+
function binarySearchItem(sortedItems, targetName) {
|
|
80
|
+
let lo = 0, hi = sortedItems.length - 1;
|
|
81
|
+
const target = targetName.toLowerCase();
|
|
82
|
+
while (lo <= hi) {
|
|
83
|
+
const mid = (lo + hi) >>> 1;
|
|
84
|
+
const cmp = sortedItems[mid].name.toLowerCase().localeCompare(target);
|
|
85
|
+
if (cmp === 0) return mid;
|
|
86
|
+
if (cmp < 0) lo = mid + 1;
|
|
87
|
+
else hi = mid - 1;
|
|
88
|
+
}
|
|
89
|
+
return -1;
|
|
90
|
+
}
|
|
91
|
+
|
|
64
92
|
/** Strip Discord emoji tags like <:Name:ID> or <a:Name:ID> */
|
|
65
93
|
function stripEmojis(str) {
|
|
66
94
|
return str.replace(/<a?:[a-zA-Z0-9_]+:\d+>/g, '').trim();
|
|
@@ -138,11 +166,12 @@ function parsePageInfo(msg) {
|
|
|
138
166
|
* Enrich items array with gwapes values.
|
|
139
167
|
*/
|
|
140
168
|
async function enrichItems(items) {
|
|
141
|
-
|
|
169
|
+
await fetchItemValues();
|
|
142
170
|
let totalValue = 0;
|
|
143
171
|
let totalMarket = 0;
|
|
144
172
|
for (const item of items) {
|
|
145
|
-
|
|
173
|
+
// O(1) LRU cache lookup instead of hash map rebuild
|
|
174
|
+
const info = itemValueCache.get(item.name.toLowerCase());
|
|
146
175
|
if (info) {
|
|
147
176
|
item.net_value = info.net_value;
|
|
148
177
|
item.market_value = info.value;
|
|
@@ -215,9 +244,13 @@ async function runInventory({ channel, waitForDankMemer, client, accountId, redi
|
|
|
215
244
|
const id = (b.customId || '').toLowerCase();
|
|
216
245
|
const label = (b.label || '').toLowerCase();
|
|
217
246
|
const emoji = (b.emoji?.name || '').toLowerCase();
|
|
247
|
+
// Exclude backward/first/last page buttons
|
|
248
|
+
if (id.includes('prev') || id.includes('first') || id.includes('last') || id.includes('back')) return false;
|
|
249
|
+
if (label.includes('prev') || label.includes('back') || label === '◀' || label === '⏮' || label === '←' || label === '⏭') return false;
|
|
250
|
+
if (emoji.includes('arrowleft') || emoji.includes('doubleleft') || emoji.includes('doubleright')) return false;
|
|
218
251
|
return (id.includes('paginator-inventory') && (id.includes('setpage') || id.includes('next')))
|
|
219
252
|
|| label.includes('next') || label === '▶' || label === '→'
|
|
220
|
-
|| emoji.includes('arrowright')
|
|
253
|
+
|| emoji.includes('arrowright');
|
|
221
254
|
});
|
|
222
255
|
}
|
|
223
256
|
|
|
@@ -226,14 +259,13 @@ async function runInventory({ channel, waitForDankMemer, client, accountId, redi
|
|
|
226
259
|
break;
|
|
227
260
|
}
|
|
228
261
|
|
|
229
|
-
await humanDelay(
|
|
262
|
+
await humanDelay(150, 350);
|
|
230
263
|
try {
|
|
231
264
|
const result = await safeClickButton(response, nextBtn);
|
|
232
265
|
if (result) {
|
|
233
266
|
response = result;
|
|
234
267
|
} else {
|
|
235
|
-
|
|
236
|
-
await sleep(2000);
|
|
268
|
+
await sleep(1200);
|
|
237
269
|
}
|
|
238
270
|
} catch (e) {
|
|
239
271
|
LOG.error(`[inv] Next page click failed: ${e.message}`);
|
|
@@ -244,7 +276,7 @@ async function runInventory({ channel, waitForDankMemer, client, accountId, redi
|
|
|
244
276
|
delete response._cv2;
|
|
245
277
|
delete response._cv2text;
|
|
246
278
|
delete response._cv2buttons;
|
|
247
|
-
await sleep(
|
|
279
|
+
await sleep(300);
|
|
248
280
|
if (isCV2(response)) await ensureCV2(response);
|
|
249
281
|
const pageInfo = parsePageInfo(response);
|
|
250
282
|
if (pageInfo.page <= page) break;
|
|
@@ -359,4 +391,5 @@ module.exports = {
|
|
|
359
391
|
runInventory, fetchItemValues, enrichItems,
|
|
360
392
|
getCachedInventory, getAllInventories,
|
|
361
393
|
updateInventoryItem, deleteInventoryItem,
|
|
394
|
+
binarySearchItem, itemNameTrie, itemValueCache,
|
|
362
395
|
};
|
|
@@ -108,7 +108,7 @@ async function runPostMemes({ channel, waitForDankMemer }) {
|
|
|
108
108
|
try {
|
|
109
109
|
await response.selectMenu(locRowIdx, [opt.value]);
|
|
110
110
|
} catch (e) { LOG.error(`[pm] Platform select error: ${e.message}`); }
|
|
111
|
-
await sleep(
|
|
111
|
+
await sleep(300);
|
|
112
112
|
const updated = await refetchMsg(channel, msgId);
|
|
113
113
|
if (updated) { response = updated; logMsg(response, 'pm-after-platform'); }
|
|
114
114
|
}
|
|
@@ -121,7 +121,7 @@ async function runPostMemes({ channel, waitForDankMemer }) {
|
|
|
121
121
|
try {
|
|
122
122
|
await response.selectMenu(kindRowIdx, [opt.value]);
|
|
123
123
|
} catch (e) { LOG.error(`[pm] Kind select error: ${e.message}`); }
|
|
124
|
-
await sleep(
|
|
124
|
+
await sleep(300);
|
|
125
125
|
const updated = await refetchMsg(channel, msgId);
|
|
126
126
|
if (updated) { response = updated; logMsg(response, 'pm-after-kind'); }
|
|
127
127
|
}
|
|
@@ -134,7 +134,7 @@ async function runPostMemes({ channel, waitForDankMemer }) {
|
|
|
134
134
|
LOG.info(`[pm] Clicking "${postBtn.label}"...`);
|
|
135
135
|
try {
|
|
136
136
|
await safeClickButton(response, postBtn);
|
|
137
|
-
await sleep(
|
|
137
|
+
await sleep(300);
|
|
138
138
|
const final = await refetchMsg(channel, msgId);
|
|
139
139
|
if (final) {
|
|
140
140
|
logMsg(final, 'pm-result');
|
package/lib/commands/search.js
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Search command handler.
|
|
3
3
|
* Send "pls search", pick a safe button, click it, parse coins.
|
|
4
|
+
*
|
|
5
|
+
* Advanced techniques:
|
|
6
|
+
* VoseAlias – O(1) weighted random sampling for button selection
|
|
7
|
+
* Trie – O(k) prefix matching for safe/dangerous locations
|
|
8
|
+
* EMA – per-location earnings tracking for adaptive weighting
|
|
4
9
|
*/
|
|
5
10
|
|
|
6
11
|
const {
|
|
@@ -8,6 +13,7 @@ const {
|
|
|
8
13
|
logMsg, isHoldTight, getHoldTightReason, sleep, humanDelay,
|
|
9
14
|
isCV2, ensureCV2,
|
|
10
15
|
} = require('./utils');
|
|
16
|
+
const { VoseAlias, Trie, EMA, LRUCache } = require('../structures');
|
|
11
17
|
|
|
12
18
|
const SAFE_SEARCH_LOCATIONS = [
|
|
13
19
|
'sofa', 'mailbox', 'dog', 'car', 'dresser', 'laundromat', 'bed',
|
|
@@ -15,18 +21,30 @@ const SAFE_SEARCH_LOCATIONS = [
|
|
|
15
21
|
'closet', 'shoe', 'vacuum', 'toilet', 'sink', 'shower',
|
|
16
22
|
'tree', 'grass', 'bushes', 'garden', 'park', 'backyard',
|
|
17
23
|
];
|
|
18
|
-
const SAFE_SET = new Set(SAFE_SEARCH_LOCATIONS);
|
|
19
24
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
25
|
+
// Trie: O(k) lookup for safe locations — replaces O(n) Set iteration per button
|
|
26
|
+
const safeTrie = new Trie();
|
|
27
|
+
for (const loc of SAFE_SEARCH_LOCATIONS) safeTrie.insert(loc, loc);
|
|
28
|
+
|
|
29
|
+
// Trie for dangerous locations — used for exclusion
|
|
30
|
+
const dangerTrie = new Trie();
|
|
31
|
+
for (const d of ['police', 'area 51', 'jail', 'hospital', 'sewer',
|
|
32
|
+
'gun', 'warehouse', 'casino', 'bank', 'airport']) {
|
|
33
|
+
dangerTrie.insert(d, d);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// LRU: cache recent location earnings for adaptive weighting
|
|
37
|
+
const locationEarnings = new LRUCache(64);
|
|
38
|
+
|
|
39
|
+
function isSafe(label) { return safeTrie.containsInText(label).length > 0; }
|
|
40
|
+
function isDangerous(label) { return dangerTrie.containsInText(label).length > 0; }
|
|
24
41
|
|
|
25
42
|
function pickSafeButton(buttons, customSafe) {
|
|
26
43
|
if (!buttons || buttons.length === 0) return null;
|
|
27
44
|
const clickable = buttons.filter(b => !b.disabled);
|
|
28
45
|
if (clickable.length === 0) return null;
|
|
29
46
|
|
|
47
|
+
// Priority 1: user-specified safe locations
|
|
30
48
|
if (customSafe && customSafe.length > 0) {
|
|
31
49
|
const customSet = new Set(customSafe.map(s => s.toLowerCase()));
|
|
32
50
|
for (const btn of clickable) {
|
|
@@ -35,17 +53,20 @@ function pickSafeButton(buttons, customSafe) {
|
|
|
35
53
|
}
|
|
36
54
|
}
|
|
37
55
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
56
|
+
// Priority 2: known safe locations (Trie-based O(k) matching)
|
|
57
|
+
const safeButtons = clickable.filter(b => isSafe((b.label || '').toLowerCase()));
|
|
58
|
+
if (safeButtons.length > 0) {
|
|
59
|
+
// VoseAlias weighted sampling: locations with higher past earnings get more weight
|
|
60
|
+
const weights = safeButtons.map(b => {
|
|
61
|
+
const cached = locationEarnings.get((b.label || '').toLowerCase());
|
|
62
|
+
return cached ? cached + 1 : 1;
|
|
63
|
+
});
|
|
64
|
+
const alias = new VoseAlias(weights);
|
|
65
|
+
return safeButtons[alias.sample()];
|
|
41
66
|
}
|
|
42
67
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
for (const d of DANGEROUS_LOCATIONS) { if (label.includes(d)) return false; }
|
|
46
|
-
return true;
|
|
47
|
-
});
|
|
48
|
-
|
|
68
|
+
// Priority 3: any non-dangerous location
|
|
69
|
+
const safe = clickable.filter(b => !isDangerous((b.label || '').toLowerCase()));
|
|
49
70
|
const pool = safe.length > 0 ? safe : clickable;
|
|
50
71
|
return pool[Math.floor(Math.random() * pool.length)];
|
|
51
72
|
}
|
|
@@ -105,6 +126,10 @@ async function runSearch({ channel, waitForDankMemer, safeAnswers }) {
|
|
|
105
126
|
logMsg(followUp, 'search-result');
|
|
106
127
|
const text = getFullText(followUp);
|
|
107
128
|
const coins = parseCoins(text);
|
|
129
|
+
// Track earnings per location in LRU for adaptive weighting
|
|
130
|
+
const locKey = (btn.label || '').toLowerCase();
|
|
131
|
+
const prev = locationEarnings.get(locKey) || 0;
|
|
132
|
+
locationEarnings.set(locKey, prev + coins);
|
|
108
133
|
if (coins > 0) {
|
|
109
134
|
LOG.coin(`[search] ${btn.label} → ${c.green}+⏣ ${coins.toLocaleString()}${c.reset}`);
|
|
110
135
|
return { result: `${btn.label} → +⏣ ${coins.toLocaleString()}`, coins };
|