dankgrinder 5.0.0 → 5.0.1

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.
@@ -39,6 +39,9 @@ const {
39
39
  safeClickButton, isHoldTight, logMsg,
40
40
  } = require('./utils');
41
41
 
42
+ const RE_DISCORD_TIMESTAMP = /<t:(\d+)(?::[tTdDfFR])?>/;
43
+ const RE_ADVENTURE_AGAIN_LABEL = /adventure again in (\d+)\s*(minute|min|hour|second)/;
44
+
42
45
  // ── Adventure type rotation (cycle through all types each run) ────
43
46
  let lastAdventureIndex = -1;
44
47
 
@@ -102,8 +105,9 @@ function isAdventureDone(msg) {
102
105
  }
103
106
 
104
107
  // ── Safe choices: prefer non-destructive options ─────────────────
105
- const SAFE_KEYWORDS = ['flee', 'run', 'hide', 'avoid', 'ignore', 'leave', 'walk away', 'back away', 'retreat', 'skip'];
106
- const RISKY_KEYWORDS = ['reach', 'grab', 'fight', 'attack', 'steal', 'open', 'touch', 'eat', 'drink'];
108
+ const SAFE_KEYWORDS = Object.freeze(['flee', 'run', 'hide', 'avoid', 'ignore', 'leave', 'walk away', 'back away', 'retreat', 'skip']);
109
+ const RISKY_KEYWORDS = Object.freeze(['reach', 'grab', 'fight', 'attack', 'steal', 'open', 'touch', 'eat', 'drink']);
110
+ const ADVENTURE_PREFERRED_TYPES = Object.freeze(['space', 'out west']);
107
111
 
108
112
  function pickSafeChoice(choices) {
109
113
  if (choices.length === 0) return null;
@@ -114,7 +118,8 @@ function pickSafeChoice(choices) {
114
118
 
115
119
  // Prefer safe keywords
116
120
  for (let i = 0; i < labels.length; i++) {
117
- for (const kw of SAFE_KEYWORDS) {
121
+ for (let k = 0; k < SAFE_KEYWORDS.length; k++) {
122
+ const kw = SAFE_KEYWORDS[k];
118
123
  if (labels[i].includes(kw)) {
119
124
  LOG.info(`[adventure] Picking safe option: "${choices[i].label}" (matched: ${kw})`);
120
125
  return choices[i];
@@ -235,9 +240,11 @@ async function playAdventureRounds(channel, msg) {
235
240
 
236
241
  // Parse rewards from Adventure Progress embed
237
242
  let rewards = '-';
238
- for (const e of current.embeds || []) {
239
- for (const f of e.fields || []) {
240
- if (f.name === 'Rewards') rewards = f.value;
243
+ const embeds = current.embeds || [];
244
+ for (let ei = 0; ei < embeds.length; ei++) {
245
+ const fields = embeds[ei].fields || [];
246
+ for (let fi = 0; fi < fields.length; fi++) {
247
+ if (fields[fi].name === 'Rewards') rewards = fields[fi].value;
241
248
  }
242
249
  }
243
250
  LOG.info(`[adventure] Rewards: ${rewards}`);
@@ -282,7 +289,7 @@ async function runAdventure({ channel, waitForDankMemer, client }) {
282
289
  // ── 1) Check cooldown via Unix timestamp <t:UNIX:t> ────────
283
290
  // Format: "You can start another adventure at <t:1774415487:t> (<t:1774415487:R>)"
284
291
  let cooldownSec = 0;
285
- const tsMatch = text.match(/<t:(\d+)(?::[tTdDfFR])?>/);
292
+ const tsMatch = text.match(RE_DISCORD_TIMESTAMP);
286
293
  if (tsMatch) {
287
294
  const unixTarget = parseInt(tsMatch[1]);
288
295
  const nowUnix = Math.floor(Date.now() / 1000);
@@ -341,7 +348,9 @@ async function runAdventure({ channel, waitForDankMemer, client }) {
341
348
  let menuRowIdx = -1;
342
349
  for (let i = 0; i < (response.components || []).length; i++) {
343
350
  const row = response.components[i];
344
- for (const comp of row.components || []) {
351
+ const rowComps = row.components || [];
352
+ for (let j = 0; j < rowComps.length; j++) {
353
+ const comp = rowComps[j];
345
354
  if (comp.type === 'STRING_SELECT' || comp.type === 3) { menuRowIdx = i; break; }
346
355
  }
347
356
  if (menuRowIdx >= 0) break;
@@ -349,9 +358,8 @@ async function runAdventure({ channel, waitForDankMemer, client }) {
349
358
 
350
359
  const menu = response.components[menuRowIdx]?.components[0];
351
360
  const options = menu?.options || [];
352
- const PREFERRED = ['space', 'out west'];
353
361
  const preferred = options.filter(o =>
354
- PREFERRED.some(kw => (o.label || '').toLowerCase().includes(kw) || (o.value || '').toLowerCase().includes(kw))
362
+ ADVENTURE_PREFERRED_TYPES.some(kw => (o.label || '').toLowerCase().includes(kw) || (o.value || '').toLowerCase().includes(kw))
355
363
  );
356
364
  const pool = preferred.length > 0 ? preferred : options;
357
365
 
@@ -433,7 +441,7 @@ function buildResult(finalText, coins, interactions, rewards, msg) {
433
441
 
434
442
  // 1) Best: Unix timestamp <t:UNIX:t> in final text or embed
435
443
  const allText = msg ? getFullText(msg) : finalText;
436
- const tsMatch = allText.match(/<t:(\d+)(?::[tTdDfFR])?>/);
444
+ const tsMatch = allText.match(RE_DISCORD_TIMESTAMP);
437
445
  if (tsMatch) {
438
446
  const unixTarget = parseInt(tsMatch[1]);
439
447
  const nowUnix = Math.floor(Date.now() / 1000);
@@ -443,10 +451,13 @@ function buildResult(finalText, coins, interactions, rewards, msg) {
443
451
 
444
452
  // 2) Fallback: "Adventure again in X minutes" button label
445
453
  if (!nextCooldownSec && msg) {
446
- for (const row of msg.components || []) {
447
- for (const comp of row.components || []) {
454
+ const rows = msg.components || [];
455
+ for (let ri = 0; ri < rows.length; ri++) {
456
+ const comps = rows[ri].components || [];
457
+ for (let ci = 0; ci < comps.length; ci++) {
458
+ const comp = comps[ci];
448
459
  const label = (comp.label || '').toLowerCase();
449
- const btnMatch = label.match(/adventure again in (\d+)\s*(minute|min|hour|second)/);
460
+ const btnMatch = label.match(RE_ADVENTURE_AGAIN_LABEL);
450
461
  if (btnMatch) {
451
462
  nextCooldownSec = parseInt(btnMatch[1]);
452
463
  const unit = btnMatch[2].toLowerCase();
@@ -5,6 +5,8 @@
5
5
 
6
6
  const { LOG, c, getFullText, parseCoins, logMsg, isHoldTight, getHoldTightReason, sleep } = require('./utils');
7
7
 
8
+ const RE_NEWLINE = /\n/g;
9
+
8
10
  /**
9
11
  * @param {object} opts
10
12
  * @param {object} opts.channel
@@ -38,7 +40,7 @@ async function runBeg({ channel, waitForDankMemer }) {
38
40
  return { result: `beg → ${c.green}+⏣ ${coins.toLocaleString()}${c.reset}`, coins };
39
41
  }
40
42
 
41
- LOG.info(`[beg] ${text.substring(0, 80).replace(/\n/g, ' ')}`);
43
+ LOG.info(`[beg] ${text.substring(0, 80).replace(RE_NEWLINE, ' ')}`);
42
44
  return { result: text.substring(0, 60) || 'done', coins: 0 };
43
45
  }
44
46
 
@@ -8,35 +8,50 @@ const {
8
8
  logMsg, isHoldTight, getHoldTightReason, sleep, humanDelay, ensureCV2,
9
9
  } = require('./utils');
10
10
 
11
+ const RE_BACKTICK_SCORE = /`\s*(\d+)\s*`/;
12
+ const RE_BJ_FACE_GLOBAL = /bjFace(\w+?):/g;
13
+ const RE_BJ_FACE_SUFFIX = /[RB]$/;
14
+ const RE_NET_LINE = /Net:\s*\*{0,2}\s*[⏣]\s*\*{0,2}([+-]?[\d,]+)/i;
15
+ const RE_WINNINGS_LINE = /Winnings:\s*\*{0,2}\s*[⏣]\s*\*{0,2}([\d,]+)/i;
16
+ const RE_COMMA = /,/g;
17
+ const COURT_RANKS = Object.freeze(['K', 'Q', 'J']);
18
+
11
19
  function parsePlayerTotal(msg) {
12
20
  // Score is in embed field: ` 18 ` (backtick-wrapped number in Player field)
13
- for (const embed of msg?.embeds || []) {
14
- for (const field of embed.fields || []) {
21
+ const embeds = msg?.embeds || [];
22
+ for (let ei = 0; ei < embeds.length; ei++) {
23
+ const fields = embeds[ei].fields || [];
24
+ for (let fi = 0; fi < fields.length; fi++) {
25
+ const field = fields[fi];
15
26
  if (field.name?.toLowerCase().includes('player')) {
16
- const m = field.value.match(/`\s*(\d+)\s*`/);
27
+ const m = field.value.match(RE_BACKTICK_SCORE);
17
28
  if (m) return parseInt(m[1]);
18
29
  }
19
30
  }
20
31
  }
21
32
  const text = typeof msg === 'string' ? msg : getFullText(msg);
22
- const m = text.match(/`\s*(\d+)\s*`/);
33
+ const m = text.match(RE_BACKTICK_SCORE);
23
34
  return m ? parseInt(m[1]) : 0;
24
35
  }
25
36
 
26
37
  function parseDealerUpcard(msg) {
27
38
  // Dealer field: revealed total ` 18 ` or hidden ` ? `
28
39
  // Upcard from emoji: bjFaceKB=K(10), bjFaceAR=A(11), bjFace7R=7
29
- for (const embed of msg?.embeds || []) {
30
- for (const field of embed.fields || []) {
40
+ const embeds = msg?.embeds || [];
41
+ for (let ei = 0; ei < embeds.length; ei++) {
42
+ const fields = embeds[ei].fields || [];
43
+ for (let fi = 0; fi < fields.length; fi++) {
44
+ const field = fields[fi];
31
45
  if (field.name?.toLowerCase().includes('dealer')) {
32
- const totalMatch = field.value.match(/`\s*(\d+)\s*`/);
46
+ const totalMatch = field.value.match(RE_BACKTICK_SCORE);
33
47
  if (totalMatch) return parseInt(totalMatch[1]);
34
- const faces = [...field.value.matchAll(/bjFace(\w+?):/g)];
35
- for (const [, face] of faces) {
48
+ const faces = [...field.value.matchAll(RE_BJ_FACE_GLOBAL)];
49
+ for (let fii = 0; fii < faces.length; fii++) {
50
+ const face = faces[fii][1];
36
51
  if (face === 'Unknown') continue;
37
- const v = face.replace(/[RB]$/, '');
52
+ const v = face.replace(RE_BJ_FACE_SUFFIX, '');
38
53
  if (v === 'A') return 11;
39
- if (['K', 'Q', 'J'].includes(v)) return 10;
54
+ if (COURT_RANKS.includes(v)) return 10;
40
55
  const n = parseInt(v);
41
56
  if (!isNaN(n)) return n;
42
57
  }
@@ -47,10 +62,10 @@ function parseDealerUpcard(msg) {
47
62
  }
48
63
 
49
64
  function parseNetCoins(text) {
50
- const netMatch = text.match(/Net:\s*\*{0,2}\s*[⏣]\s*\*{0,2}([+-]?[\d,]+)/i);
51
- if (netMatch) return parseInt(netMatch[1].replace(/,/g, ''));
52
- const winMatch = text.match(/Winnings:\s*\*{0,2}\s*[⏣]\s*\*{0,2}([\d,]+)/i);
53
- if (winMatch) return parseInt(winMatch[1].replace(/,/g, ''));
65
+ const netMatch = text.match(RE_NET_LINE);
66
+ if (netMatch) return parseInt(netMatch[1].replace(RE_COMMA, ''));
67
+ const winMatch = text.match(RE_WINNINGS_LINE);
68
+ if (winMatch) return parseInt(winMatch[1].replace(RE_COMMA, ''));
54
69
  return 0;
55
70
  }
56
71
 
@@ -15,17 +15,21 @@ const {
15
15
  } = require('./utils');
16
16
  const { Trie, VoseAlias, LRUCache } = require('../structures');
17
17
 
18
- const SAFE_CRIME_OPTIONS = [
18
+ const SAFE_CRIME_OPTIONS = Object.freeze([
19
19
  'tax evasion', 'fraud', 'cybercrime', 'hacking', 'identity theft',
20
20
  'money laundering', 'tax fraud', 'insurance fraud', 'scam',
21
- ];
21
+ ]);
22
+
23
+ const RISKY_CRIME_OPTIONS = Object.freeze([
24
+ 'murder', 'arson', 'assault', 'kidnap', 'terrorism',
25
+ ]);
22
26
 
23
27
  // Trie for O(k) safe option matching (replaces O(n) Set iteration)
24
28
  const safeCrimeTrie = new Trie();
25
29
  for (const opt of SAFE_CRIME_OPTIONS) safeCrimeTrie.insert(opt, opt);
26
30
 
27
31
  const riskyCrimeTrie = new Trie();
28
- for (const opt of ['murder', 'arson', 'assault', 'kidnap', 'terrorism']) {
32
+ for (const opt of RISKY_CRIME_OPTIONS) {
29
33
  riskyCrimeTrie.insert(opt, opt);
30
34
  }
31
35
 
@@ -3,6 +3,16 @@ const {
3
3
  safeClickButton, logMsg, isHoldTight, getHoldTightReason, sleep, humanDelay,
4
4
  } = require('./utils');
5
5
 
6
+ const RE_BLOCK_SPLIT = /\n\n+/;
7
+ const RE_DROP_NAME_BOLD = /\*\*(.+?)\*\*/;
8
+ const RE_COST_EMBED = /Cost:\s*[⏣💰]?\s*([\d,]+)/i;
9
+ const RE_STOCK = /Stock:\s*(\d+|Infinite)/i;
10
+ const RE_DROP_TIMESTAMP = /<t:(\d+):[tTdDfFR]>/;
11
+ const RE_COMMA = /,/g;
12
+ const RE_DROP_NAME_FALLBACK = /(\w[\w\s']+)/;
13
+ const RE_COST_LINE = /Cost:\s*[⏣💰o]?\s*([\d,]+)/i;
14
+ const RE_WHITESPACE_UNDERSCORE = /\s+/g;
15
+
6
16
  async function runDrops({ channel, waitForDankMemer, redis, accountId }) {
7
17
  LOG.cmd(`${c.white}${c.bold}pls drops${c.reset}`);
8
18
 
@@ -22,18 +32,21 @@ async function runDrops({ channel, waitForDankMemer, redis, accountId }) {
22
32
 
23
33
  const drops = [];
24
34
 
25
- for (const embed of response.embeds || []) {
35
+ const embedList = response.embeds || [];
36
+ for (let ei = 0; ei < embedList.length; ei++) {
37
+ const embed = embedList[ei];
26
38
  const desc = embed.description || '';
27
- const items = desc.split(/\n\n+/);
28
- for (const block of items) {
29
- const nameMatch = block.match(/\*\*(.+?)\*\*/);
30
- const costMatch = block.match(/Cost:\s*[⏣💰]?\s*([\d,]+)/i);
31
- const stockMatch = block.match(/Stock:\s*(\d+|Infinite)/i);
32
- const dateMatch = block.match(/<t:(\d+):[tTdDfFR]>/);
39
+ const items = desc.split(RE_BLOCK_SPLIT);
40
+ for (let bi = 0; bi < items.length; bi++) {
41
+ const block = items[bi];
42
+ const nameMatch = block.match(RE_DROP_NAME_BOLD);
43
+ const costMatch = block.match(RE_COST_EMBED);
44
+ const stockMatch = block.match(RE_STOCK);
45
+ const dateMatch = block.match(RE_DROP_TIMESTAMP);
33
46
  if (nameMatch) {
34
47
  drops.push({
35
48
  name: nameMatch[1],
36
- cost: costMatch ? parseInt(costMatch[1].replace(/,/g, '')) : 0,
49
+ cost: costMatch ? parseInt(costMatch[1].replace(RE_COMMA, '')) : 0,
37
50
  stock: stockMatch ? (stockMatch[1] === 'Infinite' ? Infinity : parseInt(stockMatch[1])) : 0,
38
51
  dropTimestamp: dateMatch ? parseInt(dateMatch[1]) : 0,
39
52
  });
@@ -47,10 +60,10 @@ async function runDrops({ channel, waitForDankMemer, redis, accountId }) {
47
60
  const line = textBlocks[i];
48
61
  if (line.includes('Cost:') && i > 0) {
49
62
  const prevLine = textBlocks[i - 1];
50
- const nameMatch = prevLine.match(/\*\*(.+?)\*\*/) || prevLine.match(/(\w[\w\s']+)/);
51
- const costMatch = line.match(/Cost:\s*[⏣💰o]?\s*([\d,]+)/i);
63
+ const nameMatch = prevLine.match(RE_DROP_NAME_BOLD) || prevLine.match(RE_DROP_NAME_FALLBACK);
64
+ const costMatch = line.match(RE_COST_LINE);
52
65
  if (nameMatch && costMatch) {
53
- const cost = parseInt(costMatch[1].replace(/,/g, ''));
66
+ const cost = parseInt(costMatch[1].replace(RE_COMMA, ''));
54
67
  const existing = drops.find(d => d.name === nameMatch[1]);
55
68
  if (!existing && cost > 0) {
56
69
  drops.push({ name: nameMatch[1], cost, stock: 0, dropTimestamp: 0 });
@@ -64,9 +77,10 @@ async function runDrops({ channel, waitForDankMemer, redis, accountId }) {
64
77
  try {
65
78
  await redis.set('dkg:drops:latest', JSON.stringify(drops), 'EX', 172800);
66
79
  // Schedule individual drops with their timestamps
67
- for (const drop of drops) {
80
+ for (let di = 0; di < drops.length; di++) {
81
+ const drop = drops[di];
68
82
  if (drop.dropTimestamp > 0) {
69
- const key = `dkg:drops:schedule:${drop.name.replace(/\s+/g, '_').toLowerCase()}`;
83
+ const key = `dkg:drops:schedule:${drop.name.replace(RE_WHITESPACE_UNDERSCORE, '_').toLowerCase()}`;
70
84
  await redis.set(key, JSON.stringify(drop), 'EX', 172800);
71
85
  }
72
86
  }
@@ -77,7 +91,8 @@ async function runDrops({ channel, waitForDankMemer, redis, accountId }) {
77
91
  const now = Math.floor(Date.now() / 1000);
78
92
  const buttons = getAllButtons(response);
79
93
  let boughtCount = 0;
80
- for (const drop of drops) {
94
+ for (let di = 0; di < drops.length; di++) {
95
+ const drop = drops[di];
81
96
  if (drop.stock > 0 && (drop.dropTimestamp === 0 || drop.dropTimestamp <= now)) {
82
97
  const buyBtn = buttons.find(b =>
83
98
  !b.disabled && b.label && (
@@ -22,6 +22,11 @@ const { downloadImage, extractImageUrl, findSafeCells } = require('./fishVision'
22
22
 
23
23
  const { DANK_MEMER_ID } = require('./utils');
24
24
 
25
+ const RE_FISH_COOLDOWN_TS = /<t:(\d+):R>/;
26
+ const RE_FISH_HEADER = /###\s*(.+?)(?:\s*-#|$)/;
27
+ const RE_CAUGHT_PHRASE = /caught\s+(?:a\s+)?(.+?)(?:\s*[!.]|$)/i;
28
+ const RE_CATCH_CUSTOM_ID_COORDS = /:(\d+):(\d+)$/;
29
+
25
30
  async function refetchMsg(channel, msgId) {
26
31
  try { return await channel.messages.fetch(msgId); } catch { return null; }
27
32
  }
@@ -127,7 +132,7 @@ async function sellAllFish({ channel, waitForDankMemer, sellFor = 'tokens' }) {
127
132
  * Parse cooldown timestamp from text.
128
133
  */
129
134
  function parseFishCooldown(text) {
130
- const m = text.match(/<t:(\d+):R>/);
135
+ const m = text.match(RE_FISH_COOLDOWN_TS);
131
136
  if (m) {
132
137
  const diff = parseInt(m[1]) - Math.floor(Date.now() / 1000);
133
138
  return diff > 0 ? diff : null;
@@ -194,7 +199,7 @@ async function waitForResult({ channel, msgId, waitForDankMemer }) {
194
199
  function parseFishResult(text) {
195
200
  const tl = text.toLowerCase();
196
201
  const cd = parseFishCooldown(text);
197
- const headerMatch = text.match(/###\s*(.+?)(?:\s*-#|$)/);
202
+ const headerMatch = text.match(RE_FISH_HEADER);
198
203
  const header = headerMatch ? headerMatch[1].trim() : '';
199
204
 
200
205
  if (tl.includes('nothing to catch') || tl.includes('no fish')) {
@@ -207,7 +212,7 @@ function parseFishResult(text) {
207
212
  return { outcome: 'got_away', header, cd };
208
213
  }
209
214
  if (tl.includes('caught')) {
210
- const fishMatch = text.match(/caught\s+(?:a\s+)?(.+?)(?:\s*[!.]|$)/i);
215
+ const fishMatch = text.match(RE_CAUGHT_PHRASE);
211
216
  return { outcome: 'caught', fish: fishMatch ? fishMatch[1].substring(0, 40) : 'a fish', header, cd };
212
217
  }
213
218
  return { outcome: 'unknown', header: header || text.substring(0, 60), cd };
@@ -263,7 +268,7 @@ async function playFishRound({ gameMsg, channel, msgId, waitForDankMemer }) {
263
268
 
264
269
  const catchBtns = getAllButtons(gameMsg).filter(b => (b.label || '').toLowerCase() === 'catch' && !b.disabled);
265
270
  const btn = catchBtns.find(b => {
266
- const m = (b.customId || '').match(/:(\d+):(\d+)$/);
271
+ const m = (b.customId || '').match(RE_CATCH_CUSTOM_ID_COORDS);
267
272
  return m && parseInt(m[1]) === target.col && parseInt(m[2]) === target.row;
268
273
  });
269
274
 
@@ -87,9 +87,12 @@ function parseResult(text, cmdName) {
87
87
  return { result: `${cmdName} done`, coins: 0 };
88
88
  }
89
89
 
90
+ const RE_LESS_THAN = /less than\s*\*?\*?[⏣o]?\s*([\d,]+)/i;
91
+ const RE_FIRST_NUM = /(\d[\d,]+)/;
92
+
90
93
  function checkMinBet(text) {
91
94
  if (!minBetDetector.hasAny(text)) return 0;
92
- const m = text.match(/less than\s*\*?\*?[⏣o]?\s*([\d,]+)/i) || text.match(/(\d[\d,]+)/);
95
+ const m = text.match(RE_LESS_THAN) || text.match(RE_FIRST_NUM);
93
96
  if (m) return parseInt(m[1].replace(/,/g, ''));
94
97
  return -1;
95
98
  }
@@ -8,18 +8,26 @@ const {
8
8
  logMsg, isHoldTight, getHoldTightReason, sleep, humanDelay,
9
9
  } = require('./utils');
10
10
 
11
+ // Pre-compiled regex — avoid V8 recompilation on every call
12
+ const RE_HINT_BOLD = /hint.*?\*\*(\d+)\*\*/i;
13
+ const RE_NUMBER_BOLD = /number.*?\*\*(\d+)\*\*/i;
14
+ const RE_HINT_PLAIN = /hint.*?(\d+)/i;
15
+ const RE_NUMBER_PLAIN = /number.*?(\d+)/i;
16
+ const RE_STANDALONE_NUM = /\b(\d{1,3})\b/g;
17
+ const RE_HL_NET = /Net:\s*\*{0,2}\s*[⏣]\s*\*{0,2}([+-]?[\d,]+)/i;
18
+ const RE_HL_WIN = /Winnings:\s*\*{0,2}\s*[⏣]\s*\*{0,2}([\d,]+)/i;
19
+
11
20
  function parseHintNumber(text) {
12
- // Dank Memer shows something like "your hint is **69**" or "The number is 42"
13
- const hintMatch = text.match(/hint.*?\*\*(\d+)\*\*/i)
14
- || text.match(/number.*?\*\*(\d+)\*\*/i)
15
- || text.match(/hint.*?(\d+)/i)
16
- || text.match(/number.*?(\d+)/i);
21
+ const hintMatch = text.match(RE_HINT_BOLD)
22
+ || text.match(RE_NUMBER_BOLD)
23
+ || text.match(RE_HINT_PLAIN)
24
+ || text.match(RE_NUMBER_PLAIN);
17
25
  if (hintMatch) return parseInt(hintMatch[1]);
18
- // Fallback: find any standalone number in the text (1-100 range)
19
- const nums = text.match(/\b(\d{1,3})\b/g);
26
+ RE_STANDALONE_NUM.lastIndex = 0;
27
+ const nums = text.match(RE_STANDALONE_NUM);
20
28
  if (nums) {
21
- for (const n of nums) {
22
- const v = parseInt(n);
29
+ for (let i = 0; i < nums.length; i++) {
30
+ const v = parseInt(nums[i]);
23
31
  if (v >= 1 && v <= 100) return v;
24
32
  }
25
33
  }
@@ -27,9 +35,9 @@ function parseHintNumber(text) {
27
35
  }
28
36
 
29
37
  function parseNetCoins(text) {
30
- const netMatch = text.match(/Net:\s*\*{0,2}\s*[⏣]\s*\*{0,2}([+-]?[\d,]+)/i);
38
+ const netMatch = text.match(RE_HL_NET);
31
39
  if (netMatch) return parseInt(netMatch[1].replace(/,/g, ''));
32
- const winMatch = text.match(/Winnings:\s*\*{0,2}\s*[⏣]\s*\*{0,2}([\d,]+)/i);
40
+ const winMatch = text.match(RE_HL_WIN);
33
41
  if (winMatch) return parseInt(winMatch[1].replace(/,/g, ''));
34
42
  return 0;
35
43
  }
@@ -18,6 +18,8 @@ const {
18
18
  sleep, humanDelay,
19
19
  } = require('./utils');
20
20
 
21
+ const RE_COOLDOWN_MIN = /(\d+)\s*minute/i;
22
+
21
23
  /**
22
24
  * Re-fetch a message to get updated state after an interaction.
23
25
  */
@@ -60,7 +62,7 @@ async function runPostMemes({ channel, waitForDankMemer }) {
60
62
  // Detect "dead meme" / cooldown message → return with nextCooldownSec
61
63
  if (initLower.includes('cannot post another meme') || initLower.includes('dead meme') ||
62
64
  initLower.includes('another meme for another')) {
63
- const minMatch = initText.match(/(\d+)\s*minute/i);
65
+ const minMatch = initText.match(RE_COOLDOWN_MIN);
64
66
  const cdSec = minMatch ? parseInt(minMatch[1]) * 60 : 120;
65
67
  LOG.warn(`[pm] Cooldown: ${cdSec}s`);
66
68
  return { result: `pm cooldown ${cdSec}s`, coins: 0, nextCooldownSec: cdSec };
@@ -12,16 +12,17 @@ const {
12
12
  const levelCache = {};
13
13
  const CACHE_TTL = 10 * 60 * 1000; // 10 minutes
14
14
 
15
+ const RE_PROFILE_LEVEL = /(?:level|lvl)\s*:?\s*(\d+)/i;
16
+ const RE_PROFILE_PRESTIGE_LEVEL = /prestige\s+\d+\s+level\s+(\d+)/i;
17
+
15
18
  /**
16
19
  * Parse level from profile response text.
17
20
  * Looks for patterns like "Level 25", "Lvl 25", "level: 25"
18
21
  */
19
22
  function parseLevelFromText(text) {
20
- // Pattern: "Level XX" or "Lvl XX" or "level XX"
21
- const match = text.match(/(?:level|lvl)\s*:?\s*(\d+)/i);
23
+ const match = text.match(RE_PROFILE_LEVEL);
22
24
  if (match) return parseInt(match[1]);
23
- // Fallback: "Prestige X Level Y" pattern
24
- const presMatch = text.match(/prestige\s+\d+\s+level\s+(\d+)/i);
25
+ const presMatch = text.match(RE_PROFILE_PRESTIGE_LEVEL);
25
26
  if (presMatch) return parseInt(presMatch[1]);
26
27
  return null;
27
28
  }
@@ -15,12 +15,12 @@ const {
15
15
  } = require('./utils');
16
16
  const { VoseAlias, Trie, EMA, LRUCache } = require('../structures');
17
17
 
18
- const SAFE_SEARCH_LOCATIONS = [
18
+ const SAFE_SEARCH_LOCATIONS = Object.freeze([
19
19
  'sofa', 'mailbox', 'dog', 'car', 'dresser', 'laundromat', 'bed',
20
20
  'couch', 'pantry', 'fridge', 'kitchen', 'bathroom', 'attic',
21
21
  'closet', 'shoe', 'vacuum', 'toilet', 'sink', 'shower',
22
22
  'tree', 'grass', 'bushes', 'garden', 'park', 'backyard',
23
- ];
23
+ ]);
24
24
 
25
25
  // Trie: O(k) lookup for safe locations — replaces O(n) Set iteration per button
26
26
  const safeTrie = new Trie();
@@ -4,7 +4,7 @@ const {
4
4
  } = require('./utils');
5
5
  const { buyItem } = require('./shop');
6
6
 
7
- const STREAM_ITEMS = ['keyboard', 'mouse'];
7
+ const STREAM_ITEMS = Object.freeze(['keyboard', 'mouse']);
8
8
 
9
9
  async function runStream({ channel, waitForDankMemer, client }) {
10
10
  LOG.cmd(`${c.white}${c.bold}pls stream${c.reset}`);
@@ -12,6 +12,10 @@ const {
12
12
  logMsg, isHoldTight, getHoldTightReason, sleep, humanDelay,
13
13
  } = require('./utils');
14
14
 
15
+ const RE_TRIVIA_MD_BOLD = /\*\*/g;
16
+ const RE_TRIVIA_MD_ITALIC = /\*.*?\*/g;
17
+ const RE_TRIVIA_CORRECT_WAS = /correct answer was \*\*(.+?)\*\*/i;
18
+
15
19
  const triviaDB = {};
16
20
 
17
21
  function makeQuestionKey(question) {
@@ -101,7 +105,7 @@ async function runTrivia({ channel, waitForDankMemer, redis }) {
101
105
  const clickable = buttons.filter(b => !b.disabled);
102
106
  if (clickable.length === 0) return { result: 'no clickable buttons', coins: 0 };
103
107
 
104
- const question = (response.embeds?.[0]?.description || '').replace(/\*\*/g, '').replace(/\*.*?\*/g, '').trim();
108
+ const question = (response.embeds?.[0]?.description || '').replace(RE_TRIVIA_MD_BOLD, '').replace(RE_TRIVIA_MD_ITALIC, '').trim();
105
109
 
106
110
  let btn = await lookupAnswer(question, clickable, redis || null);
107
111
  const fromDB = !!btn;
@@ -119,7 +123,7 @@ async function runTrivia({ channel, waitForDankMemer, redis }) {
119
123
  const coins = parseCoins(text);
120
124
 
121
125
  const lower = text.toLowerCase();
122
- const correctMatch = text.match(/correct answer was \*\*(.+?)\*\*/i);
126
+ const correctMatch = text.match(RE_TRIVIA_CORRECT_WAS);
123
127
  if (correctMatch) {
124
128
  await learnFromResult(question, correctMatch[1], redis || null);
125
129
  } else if (lower.includes('correct') || lower.includes('nice') || lower.includes('right')) {