dankgrinder 4.9.9 → 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.
@@ -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(400, 900);
117
+ await humanDelay(150, 400);
118
118
 
119
119
  try {
120
120
  const followUp = await safeClickButton(current, targetBtn);
@@ -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
- const RISKY_CRIME_OPTIONS = new Set([
19
- 'murder', 'arson', 'assault', 'kidnap', 'terrorism',
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
- for (const btn of clickable) {
36
- const label = (btn.label || '').toLowerCase();
37
- for (const s of SAFE_CRIME_SET) { if (label.includes(s)) return btn; }
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 };
@@ -1,43 +1,99 @@
1
1
  /**
2
2
  * Gambling command handlers.
3
3
  * Covers: cointoss (CV2), roulette, slots, snakeeyes
4
- * Each handler is tailored to the actual Dank Memer response format.
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
- const lower = text.toLowerCase();
32
- if (lower.includes("can't bet less than") || lower.includes('cannot bet less than') || lower.includes('minimum bet')) {
33
- const m = text.match(/less than\s*\*?\*?[⏣o]?\s*([\d,]+)/i) || text.match(/(\d[\d,]+)/);
34
- if (m) return parseInt(m[1].replace(/,/g, ''));
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(200, 500);
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
- // CV2 click returns null — wait for message update, clear cache, refetch
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 — embed format. Always pick Red (best even-money odds ~48.6%).
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 cmd = `pls roulette ${betAmount} red`;
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 redBtn = btns.find(b => (b.label || '').toLowerCase() === 'red');
132
- if (redBtn) {
133
- LOG.info(`[roulette] Picking "Red"`);
134
- await humanDelay(200, 500);
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, redBtn);
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(3500);
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(3500);
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(3500);
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 = { runCointoss, runRoulette, runSlots, runSnakeeyes };
264
+ module.exports = {
265
+ runCointoss, runRoulette, runSlots, runSnakeeyes,
266
+ kellyFraction, winRateEMA, winWindow, totalWindow,
267
+ };
@@ -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
- async function waitForEditedMessage(channel, messageId, baselineText, timeoutMs = 12000) {
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(700);
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(3000);
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
- const dismissLabels = ['ok', 'dismiss', 'accept', 'got it', 'continue'];
212
- const btn = buttons.find(b => !b.disabled && dismissLabels.some(s => (b.label || '').toLowerCase().includes(s)));
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();
@@ -72,7 +72,7 @@ async function playHighLow(response, depth = 0) {
72
72
  return { result: 'buttons disabled', coins };
73
73
  }
74
74
 
75
- await humanDelay(400, 900);
75
+ await humanDelay(150, 400);
76
76
 
77
77
  try {
78
78
  const followUp = await safeClickButton(response, targetBtn);
@@ -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
- let _itemValuesCache = null;
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; // 1 hour
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 (_itemValuesCache && Date.now() - _itemValuesFetchedAt < ITEM_CACHE_TTL) {
25
- return _itemValuesCache;
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
- map[item.name.toLowerCase()] = {
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 from gwapes.com`);
61
+ LOG.info(`[inv] Fetched ${Object.keys(map).length} item values Trie + LRU`);
48
62
  resolve(map);
49
63
  } else {
50
- resolve(_itemValuesCache || {});
64
+ resolve({});
51
65
  }
52
66
  } catch (e) {
53
67
  LOG.error(`[inv] gwapes API parse error: ${e.message}`);
54
- resolve(_itemValuesCache || {});
68
+ resolve({});
55
69
  }
56
70
  });
57
71
  }).on('error', (e) => {
58
72
  LOG.error(`[inv] gwapes API error: ${e.message}`);
59
- resolve(_itemValuesCache || {});
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
- const values = await fetchItemValues();
169
+ await fetchItemValues();
142
170
  let totalValue = 0;
143
171
  let totalMarket = 0;
144
172
  for (const item of items) {
145
- const info = values[item.name.toLowerCase()];
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') || emoji.includes('doubleright') || emoji === '▶';
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(300, 600);
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
- // CV2 button click returns null — wait for Discord to update the message
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(500);
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(600);
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(600);
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(600);
137
+ await sleep(300);
138
138
  const final = await refetchMsg(channel, msgId);
139
139
  if (final) {
140
140
  logMsg(final, 'pm-result');
@@ -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
- const DANGEROUS_LOCATIONS = new Set([
21
- 'police', 'area 51', 'jail', 'hospital', 'sewer',
22
- 'gun', 'warehouse', 'casino', 'bank', 'airport',
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
- for (const btn of clickable) {
39
- const label = (btn.label || '').toLowerCase();
40
- for (const s of SAFE_SET) { if (label.includes(s)) return btn; }
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
- const safe = clickable.filter(b => {
44
- const label = (b.label || '').toLowerCase();
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 };