dankgrinder 4.9.9 → 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.
- package/lib/commands/adventure.js +25 -14
- package/lib/commands/beg.js +3 -1
- package/lib/commands/blackjack.js +31 -16
- package/lib/commands/crime.js +37 -13
- package/lib/commands/drops.js +29 -14
- package/lib/commands/fish.js +9 -4
- package/lib/commands/gamble.js +86 -27
- package/lib/commands/generic.js +19 -5
- package/lib/commands/highlow.js +20 -12
- package/lib/commands/index.js +2 -1
- package/lib/commands/inventory.js +54 -21
- package/lib/commands/postmemes.js +6 -4
- package/lib/commands/profile.js +5 -4
- package/lib/commands/search.js +41 -16
- package/lib/commands/shop.js +31 -8
- package/lib/commands/stream.js +1 -1
- package/lib/commands/trivia.js +6 -2
- package/lib/commands/utils.js +165 -81
- package/lib/commands/work.js +17 -8
- package/lib/grinder.js +812 -103
- package/lib/structures.js +725 -0
- package/package.json +3 -2
|
@@ -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 (
|
|
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
|
-
|
|
239
|
-
|
|
240
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
447
|
-
|
|
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(
|
|
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();
|
package/lib/commands/beg.js
CHANGED
|
@@ -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(
|
|
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
|
-
|
|
14
|
-
|
|
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(
|
|
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(
|
|
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
|
-
|
|
30
|
-
|
|
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(
|
|
46
|
+
const totalMatch = field.value.match(RE_BACKTICK_SCORE);
|
|
33
47
|
if (totalMatch) return parseInt(totalMatch[1]);
|
|
34
|
-
const faces = [...field.value.matchAll(
|
|
35
|
-
for (
|
|
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(
|
|
52
|
+
const v = face.replace(RE_BJ_FACE_SUFFIX, '');
|
|
38
53
|
if (v === 'A') return 11;
|
|
39
|
-
if (
|
|
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(
|
|
51
|
-
if (netMatch) return parseInt(netMatch[1].replace(
|
|
52
|
-
const winMatch = text.match(
|
|
53
|
-
if (winMatch) return parseInt(winMatch[1].replace(
|
|
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
|
|
|
@@ -114,7 +129,7 @@ async function runBlackjack({ channel, waitForDankMemer, betAmount = 5000 }) {
|
|
|
114
129
|
if (!targetBtn) break;
|
|
115
130
|
|
|
116
131
|
LOG.info(`[bj] You:${playerTotal} Dealer:${dealerUpcard} → ${targetBtn.label}`);
|
|
117
|
-
await humanDelay(
|
|
132
|
+
await humanDelay(150, 400);
|
|
118
133
|
|
|
119
134
|
try {
|
|
120
135
|
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,17 +13,32 @@ 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
|
-
const SAFE_CRIME_OPTIONS = [
|
|
18
|
+
const SAFE_CRIME_OPTIONS = Object.freeze([
|
|
13
19
|
'tax evasion', 'fraud', 'cybercrime', 'hacking', 'identity theft',
|
|
14
20
|
'money laundering', 'tax fraud', 'insurance fraud', 'scam',
|
|
15
|
-
];
|
|
16
|
-
const SAFE_CRIME_SET = new Set(SAFE_CRIME_OPTIONS);
|
|
21
|
+
]);
|
|
17
22
|
|
|
18
|
-
const RISKY_CRIME_OPTIONS =
|
|
23
|
+
const RISKY_CRIME_OPTIONS = Object.freeze([
|
|
19
24
|
'murder', 'arson', 'assault', 'kidnap', 'terrorism',
|
|
20
25
|
]);
|
|
21
26
|
|
|
27
|
+
// Trie for O(k) safe option matching (replaces O(n) Set iteration)
|
|
28
|
+
const safeCrimeTrie = new Trie();
|
|
29
|
+
for (const opt of SAFE_CRIME_OPTIONS) safeCrimeTrie.insert(opt, opt);
|
|
30
|
+
|
|
31
|
+
const riskyCrimeTrie = new Trie();
|
|
32
|
+
for (const opt of RISKY_CRIME_OPTIONS) {
|
|
33
|
+
riskyCrimeTrie.insert(opt, opt);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Adaptive earnings tracker — learn which crimes pay best
|
|
37
|
+
const crimeEarnings = new LRUCache(32);
|
|
38
|
+
|
|
39
|
+
function isSafeCrime(label) { return safeCrimeTrie.containsInText(label).length > 0; }
|
|
40
|
+
function isRiskyCrime(label) { return riskyCrimeTrie.containsInText(label).length > 0; }
|
|
41
|
+
|
|
22
42
|
function pickSafeButton(buttons, customSafe) {
|
|
23
43
|
if (!buttons || buttons.length === 0) return null;
|
|
24
44
|
const clickable = buttons.filter(b => !b.disabled);
|
|
@@ -32,17 +52,18 @@ function pickSafeButton(buttons, customSafe) {
|
|
|
32
52
|
}
|
|
33
53
|
}
|
|
34
54
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
55
|
+
// Trie-based safe option detection + VoseAlias weighted sampling
|
|
56
|
+
const safeButtons = clickable.filter(b => isSafeCrime((b.label || '').toLowerCase()));
|
|
57
|
+
if (safeButtons.length > 0) {
|
|
58
|
+
const weights = safeButtons.map(b => {
|
|
59
|
+
const past = crimeEarnings.get((b.label || '').toLowerCase());
|
|
60
|
+
return past ? past + 1 : 1;
|
|
61
|
+
});
|
|
62
|
+
const alias = new VoseAlias(weights);
|
|
63
|
+
return safeButtons[alias.sample()];
|
|
38
64
|
}
|
|
39
65
|
|
|
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
|
-
|
|
66
|
+
const safe = clickable.filter(b => !isRiskyCrime((b.label || '').toLowerCase()));
|
|
46
67
|
const pool = safe.length > 0 ? safe : clickable;
|
|
47
68
|
return pool[Math.floor(Math.random() * pool.length)];
|
|
48
69
|
}
|
|
@@ -102,6 +123,9 @@ async function runCrime({ channel, waitForDankMemer, safeAnswers }) {
|
|
|
102
123
|
logMsg(followUp, 'crime-result');
|
|
103
124
|
const text = getFullText(followUp);
|
|
104
125
|
const coins = parseCoins(text);
|
|
126
|
+
const crimeKey = (btn.label || '').toLowerCase();
|
|
127
|
+
const prev = crimeEarnings.get(crimeKey) || 0;
|
|
128
|
+
crimeEarnings.set(crimeKey, prev + coins);
|
|
105
129
|
if (coins > 0) {
|
|
106
130
|
LOG.coin(`[crime] ${btn.label} → ${c.green}+⏣ ${coins.toLocaleString()}${c.reset}`);
|
|
107
131
|
return { result: `${btn.label} → +⏣ ${coins.toLocaleString()}`, coins };
|
package/lib/commands/drops.js
CHANGED
|
@@ -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
|
-
|
|
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(
|
|
28
|
-
for (
|
|
29
|
-
const
|
|
30
|
-
const
|
|
31
|
-
const
|
|
32
|
-
const
|
|
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(
|
|
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(
|
|
51
|
-
const costMatch = line.match(
|
|
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(
|
|
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 (
|
|
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(
|
|
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 (
|
|
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 && (
|
package/lib/commands/fish.js
CHANGED
|
@@ -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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
package/lib/commands/gamble.js
CHANGED
|
@@ -1,43 +1,102 @@
|
|
|
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
|
|
|
90
|
+
const RE_LESS_THAN = /less than\s*\*?\*?[⏣o]?\s*([\d,]+)/i;
|
|
91
|
+
const RE_FIRST_NUM = /(\d[\d,]+)/;
|
|
92
|
+
|
|
30
93
|
function checkMinBet(text) {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
return -1;
|
|
36
|
-
}
|
|
37
|
-
return 0;
|
|
94
|
+
if (!minBetDetector.hasAny(text)) return 0;
|
|
95
|
+
const m = text.match(RE_LESS_THAN) || text.match(RE_FIRST_NUM);
|
|
96
|
+
if (m) return parseInt(m[1].replace(/,/g, ''));
|
|
97
|
+
return -1;
|
|
38
98
|
}
|
|
39
99
|
|
|
40
|
-
// Filter utility buttons (null labels, warnings, refresh)
|
|
41
100
|
function gameButtons(msg) {
|
|
42
101
|
return getAllButtons(msg).filter(b =>
|
|
43
102
|
!b.disabled && b.label && b.label !== 'null' &&
|
|
@@ -89,15 +148,14 @@ async function runCointoss({ channel, waitForDankMemer, betAmount = 10000 }) {
|
|
|
89
148
|
const choice = Math.random() < 0.5 ? 'head' : 'tail';
|
|
90
149
|
const btn = btns.find(b => (b.label || '').toLowerCase().includes(choice)) || btns[0];
|
|
91
150
|
LOG.info(`[cointoss] Picking "${btn.label}"`);
|
|
92
|
-
await humanDelay(
|
|
151
|
+
await humanDelay(80, 200);
|
|
93
152
|
try {
|
|
94
153
|
const followUp = await safeClickButton(response, btn);
|
|
95
154
|
if (followUp) {
|
|
96
155
|
await ensureCV2(followUp);
|
|
97
156
|
return parseResult(getFullText(followUp), 'cointoss');
|
|
98
157
|
}
|
|
99
|
-
|
|
100
|
-
await sleep(2500);
|
|
158
|
+
await sleep(1500);
|
|
101
159
|
response._cv2 = null;
|
|
102
160
|
response._cv2text = null;
|
|
103
161
|
response._cv2buttons = null;
|
|
@@ -110,11 +168,13 @@ async function runCointoss({ channel, waitForDankMemer, betAmount = 10000 }) {
|
|
|
110
168
|
}
|
|
111
169
|
|
|
112
170
|
/**
|
|
113
|
-
* Roulette —
|
|
171
|
+
* Roulette — random color: red ~47.5%, black ~47.5%, green ~5% (rare).
|
|
114
172
|
* Sometimes auto-bets if color is in command, sometimes shows buttons.
|
|
115
173
|
*/
|
|
116
174
|
async function runRoulette({ channel, waitForDankMemer, betAmount = 10000 }) {
|
|
117
|
-
const
|
|
175
|
+
const roll = Math.random();
|
|
176
|
+
const color = roll < 0.475 ? 'red' : roll < 0.95 ? 'black' : 'green';
|
|
177
|
+
const cmd = `pls roulette ${betAmount} ${color}`;
|
|
118
178
|
LOG.cmd(`${c.white}${c.bold}${cmd}${c.reset}`);
|
|
119
179
|
await channel.send(cmd);
|
|
120
180
|
let response = await waitForDankMemer(10000);
|
|
@@ -124,16 +184,15 @@ async function runRoulette({ channel, waitForDankMemer, betAmount = 10000 }) {
|
|
|
124
184
|
|
|
125
185
|
logMsg(response, 'roulette');
|
|
126
186
|
|
|
127
|
-
// Check if buttons appeared (manual pick needed)
|
|
128
187
|
const btns = gameButtons(response).filter(b =>
|
|
129
188
|
!(b.customId || b.custom_id || '').includes('bet')
|
|
130
189
|
);
|
|
131
|
-
const
|
|
132
|
-
if (
|
|
133
|
-
LOG.info(`[roulette] Picking "
|
|
134
|
-
await humanDelay(
|
|
190
|
+
const colorBtn = btns.find(b => (b.label || '').toLowerCase() === color);
|
|
191
|
+
if (colorBtn) {
|
|
192
|
+
LOG.info(`[roulette] Picking "${colorBtn.label}"`);
|
|
193
|
+
await humanDelay(100, 300);
|
|
135
194
|
try {
|
|
136
|
-
const followUp = await safeClickButton(response,
|
|
195
|
+
const followUp = await safeClickButton(response, colorBtn);
|
|
137
196
|
if (followUp) {
|
|
138
197
|
response = followUp;
|
|
139
198
|
logMsg(response, 'roulette-after-pick');
|
|
@@ -141,10 +200,9 @@ async function runRoulette({ channel, waitForDankMemer, betAmount = 10000 }) {
|
|
|
141
200
|
} catch (e) { LOG.error(`[roulette] Click error: ${e.message}`); }
|
|
142
201
|
}
|
|
143
202
|
|
|
144
|
-
// Wait for spin animation to complete
|
|
145
203
|
let text = getFullText(response);
|
|
146
204
|
if (text.toLowerCase().includes('spinning') || text.toLowerCase().includes('rolling') || text.toLowerCase().includes('pick a color')) {
|
|
147
|
-
await sleep(
|
|
205
|
+
await sleep(2000);
|
|
148
206
|
try {
|
|
149
207
|
const final = await channel.messages.fetch(response.id);
|
|
150
208
|
if (final) text = getFullText(final);
|
|
@@ -169,9 +227,8 @@ async function runSlots({ channel, waitForDankMemer, betAmount = 10000 }) {
|
|
|
169
227
|
logMsg(response, 'slots');
|
|
170
228
|
const text = getFullText(response);
|
|
171
229
|
|
|
172
|
-
// Slots auto-spins — if still animating, wait and refetch
|
|
173
230
|
if (text.toLowerCase().includes('spinning')) {
|
|
174
|
-
await sleep(
|
|
231
|
+
await sleep(2000);
|
|
175
232
|
try {
|
|
176
233
|
const final = await channel.messages.fetch(response.id);
|
|
177
234
|
if (final) return parseResult(getFullText(final), 'slots');
|
|
@@ -196,9 +253,8 @@ async function runSnakeeyes({ channel, waitForDankMemer, betAmount = 10000 }) {
|
|
|
196
253
|
logMsg(response, 'snakeeyes');
|
|
197
254
|
const text = getFullText(response);
|
|
198
255
|
|
|
199
|
-
// Snakeeyes auto-rolls — if still animating, wait and refetch
|
|
200
256
|
if (text.toLowerCase().includes('rolling')) {
|
|
201
|
-
await sleep(
|
|
257
|
+
await sleep(2000);
|
|
202
258
|
try {
|
|
203
259
|
const final = await channel.messages.fetch(response.id);
|
|
204
260
|
if (final) return parseResult(getFullText(final), 'snakeeyes');
|
|
@@ -208,4 +264,7 @@ async function runSnakeeyes({ channel, waitForDankMemer, betAmount = 10000 }) {
|
|
|
208
264
|
return parseResult(text, 'snakeeyes');
|
|
209
265
|
}
|
|
210
266
|
|
|
211
|
-
module.exports = {
|
|
267
|
+
module.exports = {
|
|
268
|
+
runCointoss, runRoulette, runSlots, runSnakeeyes,
|
|
269
|
+
kellyFraction, winRateEMA, winWindow, totalWindow,
|
|
270
|
+
};
|