dankgrinder 4.5.1 → 4.8.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.
@@ -34,11 +34,10 @@
34
34
  */
35
35
 
36
36
  const {
37
- LOG, c, sleep, humanDelay, getFullText, parseCoins, parseBalance,
38
- getAllButtons, getAllSelectMenus, findButton, findSelectMenuOption,
39
- safeClickButton, isHoldTight, logMsg, dumpMessage, needsItem,
37
+ LOG, c, sleep, humanDelay, getFullText, parseCoins,
38
+ getAllButtons, getAllSelectMenus, findButton,
39
+ safeClickButton, isHoldTight, logMsg,
40
40
  } = require('./utils');
41
- const { buyItem } = require('./shop');
42
41
 
43
42
  // ── Adventure type rotation (cycle through all types each run) ────
44
43
  let lastAdventureIndex = -1;
@@ -63,8 +62,7 @@ async function clickAndRefetch(channel, msg, btn) {
63
62
  LOG.error(`[adventure] Click error: ${e.message}`);
64
63
  return null;
65
64
  }
66
- // Dank Memer edits the message after a button click
67
- await sleep(500);
65
+ await sleep(250);
68
66
  return await refetchMsg(channel, msg.id);
69
67
  }
70
68
 
@@ -172,7 +170,7 @@ async function playAdventureRounds(channel, msg) {
172
170
  const choice = pickSafeChoice(choices);
173
171
  if (choice) {
174
172
  LOG.info(`[adventure] → Choosing: "${choice.label}"`);
175
- await sleep(200);
173
+ await sleep(100);
176
174
  const afterChoice = await clickAndRefetch(channel, current, choice);
177
175
  if (afterChoice) {
178
176
  current = afterChoice;
@@ -194,7 +192,7 @@ async function playAdventureRounds(channel, msg) {
194
192
  const nextBtnNow = getNextButton(current);
195
193
  if (nextBtnNow && !nextBtnNow.disabled) {
196
194
  LOG.debug(`[adventure] Clicking Next arrow...`);
197
- await sleep(200);
195
+ await sleep(100);
198
196
  const afterNext = await clickAndRefetch(channel, current, nextBtnNow);
199
197
  if (afterNext) {
200
198
  current = afterNext;
@@ -206,7 +204,7 @@ async function playAdventureRounds(channel, msg) {
206
204
  } else if (nextBtnNow && nextBtnNow.disabled) {
207
205
  // Next is disabled but no choices found — might be loading
208
206
  LOG.debug(`[adventure] Next disabled, no choices — waiting...`);
209
- await sleep(600);
207
+ await sleep(300);
210
208
  const refreshed = await refetchMsg(channel, current.id);
211
209
  if (refreshed) {
212
210
  current = refreshed;
@@ -219,8 +217,7 @@ async function playAdventureRounds(channel, msg) {
219
217
  // No next button at all
220
218
  LOG.debug(`[adventure] No Next button — checking if done`);
221
219
  if (isAdventureDone(current)) break;
222
- // Wait and re-fetch as Dank Memer might still be editing
223
- await sleep(1500);
220
+ await sleep(500);
224
221
  const refreshed = await refetchMsg(channel, current.id);
225
222
  if (refreshed) {
226
223
  current = refreshed;
@@ -314,42 +311,10 @@ async function runAdventure({ channel, waitForDankMemer, client }) {
314
311
  return { result: 'cooldown', coins: 0, nextCooldownSec: cooldownSec + 3 };
315
312
  }
316
313
 
317
- // ── 4) If we need a ticket, try to buy one ─────────────────
314
+ // ── 4) If we need a ticket, skip (too expensive to auto-buy) ──
318
315
  if (needsTicket) {
319
- LOG.warn(`[adventure] Need adventure ticket! Attempting to buy...`);
320
-
321
- // Ticket costs 250,000 coins — check balance first to avoid wasting time in shop
322
- const TICKET_COST = 250000;
323
- let currentBalance = 0;
324
- await channel.send('pls bal');
325
- const balMsg = await waitForDankMemer(8000);
326
- if (balMsg) {
327
- currentBalance = parseBalance(balMsg);
328
- LOG.info(`[adventure] Balance: ${c.yellow}⏣ ${currentBalance.toLocaleString()}${c.reset} (ticket costs ⏣ ${TICKET_COST.toLocaleString()})`);
329
- }
330
-
331
- if (currentBalance < TICKET_COST) {
332
- LOG.warn(`[adventure] Not enough coins for ticket (⏣ ${currentBalance.toLocaleString()} < ⏣ ${TICKET_COST.toLocaleString()}). Grind more first.`);
333
- return { result: `need ticket (⏣ ${currentBalance.toLocaleString()}/${TICKET_COST.toLocaleString()})`, coins: 0, nextCooldownSec: 120 };
334
- }
335
-
336
- const bought = await buyItem({
337
- channel, waitForDankMemer, client,
338
- itemName: 'Adventure Ticket', quantity: 1,
339
- });
340
-
341
- if (!bought) {
342
- LOG.error('[adventure] Could not buy adventure ticket from shop.');
343
- return { result: 'need ticket (buy failed)', coins: 0, nextCooldownSec: 120 };
344
- }
345
-
346
- LOG.success('[adventure] Tickets purchased! Re-running adventure...');
347
- await sleep(1500);
348
-
349
- await channel.send('pls adventure');
350
- response = await waitForDankMemer(12000);
351
- if (!response) return { result: 'no response after ticket buy', coins: 0, nextCooldownSec: null };
352
- logMsg(response, 'adventure-after-buy');
316
+ LOG.warn(`[adventure] No ticket skipping (too expensive to auto-buy)`);
317
+ return { result: 'no ticket', coins: 0, nextCooldownSec: 3600, skipReason: 'no_ticket' };
353
318
  }
354
319
 
355
320
  // ── Check if we're already mid-adventure (no select menu) ──
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Blackjack command handler.
3
- * Send "pls bj <bet>", play hit/stand based on card total.
3
+ * Smart hit/stand: follows basic strategy based on player total and dealer upcard.
4
4
  */
5
5
 
6
6
  const {
@@ -8,14 +8,50 @@ const {
8
8
  logMsg, isHoldTight, getHoldTightReason, sleep, humanDelay,
9
9
  } = require('./utils');
10
10
 
11
+ function parsePlayerTotal(text) {
12
+ // Match the player section: "username (Player)\n ♦ J ♦ 2 12"
13
+ // The number after the cards on the player line is the total
14
+ const playerSection = text.match(/\(Player\)[\s\S]*?(\d+)/i);
15
+ if (playerSection) return parseInt(playerSection[1]);
16
+ const totalMatch = text.match(/total[:\s]*\**(\d+)\**/i) || text.match(/value[:\s]*\**(\d+)\**/i);
17
+ return totalMatch ? parseInt(totalMatch[1]) : 0;
18
+ }
19
+
20
+ function parseDealerUpcard(text) {
21
+ // Match dealer section: "Dank Memer (Dealer)\n ♦ 10 ♣ 8 18"
22
+ const dealerSection = text.match(/\(Dealer\)[\s\S]*?(\d+)/i);
23
+ if (dealerSection) return parseInt(dealerSection[1]);
24
+ return 0;
25
+ }
26
+
27
+ function parseNetCoins(text) {
28
+ // Parse "Net: ⏣ -5,000" or "Net: ⏣ 5,000"
29
+ const netMatch = text.match(/Net:\s*[⏣o]\s*(-?[\d,]+)/i);
30
+ if (netMatch) {
31
+ return parseInt(netMatch[1].replace(/,/g, ''));
32
+ }
33
+ // Parse "Winnings: ⏣ 5,000"
34
+ const winMatch = text.match(/Winnings:\s*[⏣o]\s*([\d,]+)/i);
35
+ if (winMatch) return parseInt(winMatch[1].replace(/,/g, ''));
36
+ return 0;
37
+ }
38
+
11
39
  /**
12
- * @param {object} opts
13
- * @param {object} opts.channel
14
- * @param {function} opts.waitForDankMemer
15
- * @param {number} [opts.betAmount=1000]
16
- * @returns {Promise<{result: string, coins: number}>}
40
+ * Basic strategy decision.
41
+ * - Always hit on ≤ 11
42
+ * - Stand on ≥ 17
43
+ * - 12-16: hit if dealer shows 7+ (strong upcard), else stand
17
44
  */
18
- async function runBlackjack({ channel, waitForDankMemer, betAmount = 1000 }) {
45
+ function shouldHit(playerTotal, dealerTotal) {
46
+ if (playerTotal <= 11) return true;
47
+ if (playerTotal >= 17) return false;
48
+ // 12-16: dealer has strong hand (7+), we need to risk hitting
49
+ if (dealerTotal >= 7) return true;
50
+ // Dealer has weak hand (2-6), likely to bust — stand and wait
51
+ return false;
52
+ }
53
+
54
+ async function runBlackjack({ channel, waitForDankMemer, betAmount = 5000 }) {
19
55
  const cmd = `pls bj ${betAmount}`;
20
56
  LOG.cmd(`${c.white}${c.bold}${cmd}${c.reset}`);
21
57
 
@@ -42,21 +78,22 @@ async function runBlackjack({ channel, waitForDankMemer, betAmount = 1000 }) {
42
78
  const buttons = getAllButtons(current);
43
79
  if (buttons.length === 0 || buttons.every(b => b.disabled)) break;
44
80
 
45
- // Parse player total
46
- const totalMatch = text.match(/total[:\s]*\**(\d+)\**/i) || text.match(/value[:\s]*\**(\d+)\**/i);
47
- const playerTotal = totalMatch ? parseInt(totalMatch[1]) : 0;
81
+ const playerTotal = parsePlayerTotal(text);
82
+ const dealerTotal = parseDealerUpcard(text);
48
83
 
49
84
  let targetBtn;
50
- if (playerTotal >= 17 || playerTotal === 0) {
85
+ if (playerTotal === 0) {
51
86
  targetBtn = buttons.find(b => (b.label || '').toLowerCase().includes('stand')) || buttons[1];
52
- } else {
87
+ } else if (shouldHit(playerTotal, dealerTotal)) {
53
88
  targetBtn = buttons.find(b => (b.label || '').toLowerCase().includes('hit')) || buttons[0];
89
+ } else {
90
+ targetBtn = buttons.find(b => (b.label || '').toLowerCase().includes('stand')) || buttons[1];
54
91
  }
55
92
 
56
93
  if (!targetBtn || targetBtn.disabled) break;
57
94
 
58
- LOG.info(`[bj] Total: ${playerTotal} → ${targetBtn.label}`);
59
- await humanDelay(500, 1200);
95
+ LOG.info(`[bj] You:${playerTotal} Dealer:${dealerTotal} → ${targetBtn.label}`);
96
+ await humanDelay(400, 900);
60
97
 
61
98
  try {
62
99
  const followUp = await safeClickButton(current, targetBtn);
@@ -70,15 +107,28 @@ async function runBlackjack({ channel, waitForDankMemer, betAmount = 1000 }) {
70
107
  }
71
108
 
72
109
  const finalText = getFullText(current);
73
- const coins = parseCoins(finalText);
110
+ const net = parseNetCoins(finalText);
74
111
  const lower = finalText.toLowerCase();
75
112
 
76
- if (coins > 0) {
77
- LOG.coin(`[bj] ${c.green}+⏣ ${coins.toLocaleString()}${c.reset}`);
78
- return { result: `bj → +⏣ ${coins.toLocaleString()}`, coins };
113
+ if (net > 0) {
114
+ LOG.coin(`[bj] ${c.green}+⏣ ${net.toLocaleString()}${c.reset}`);
115
+ return { result: `bj → +⏣ ${net.toLocaleString()}`, coins: net };
116
+ }
117
+ if (net < 0) {
118
+ LOG.warn(`[bj] ${c.red}-⏣ ${Math.abs(net).toLocaleString()}${c.reset}`);
119
+ return { result: `bj → -⏣ ${Math.abs(net).toLocaleString()}`, coins: 0, lost: Math.abs(net) };
79
120
  }
80
- if (lower.includes('won')) return { result: `bj → ${c.green}won${c.reset}`, coins: 0 };
81
- if (lower.includes('lost') || lower.includes('bust')) return { result: `bj → ${c.red}lost${c.reset}`, coins: 0 };
121
+ if (lower.includes('won') || lower.includes('beat')) {
122
+ const coins = parseCoins(finalText);
123
+ return { result: `bj → ${c.green}won${c.reset}`, coins };
124
+ }
125
+ if (lower.includes('lost') || lower.includes('bust') || lower.includes('lower score')) {
126
+ return { result: `bj → lost`, coins: 0, lost: betAmount };
127
+ }
128
+ if (lower.includes('tied') || lower.includes('push')) {
129
+ return { result: 'bj → push', coins: 0 };
130
+ }
131
+
82
132
  return { result: 'bj done', coins: 0 };
83
133
  }
84
134
 
@@ -45,7 +45,7 @@ async function runDig({ channel, waitForDankMemer, client }) {
45
45
  const bought = await buyItem({
46
46
  channel, waitForDankMemer, client,
47
47
  itemName: 'Shovel',
48
- quantity: 3,
48
+ quantity: 1,
49
49
  });
50
50
 
51
51
  if (bought) {
@@ -1,23 +1,36 @@
1
1
  /**
2
- * Simple gambling command handlers.
3
- * Covers: coinflip, roulette, slots, snakeeyes
2
+ * Gambling command handlers.
3
+ * Covers: cointoss, roulette, slots, snakeeyes
4
4
  */
5
5
 
6
6
  const {
7
- LOG, c, getFullText, parseCoins, getAllButtons, safeClickButton,
7
+ LOG, c, getFullText, parseCoins, parseNetCoins, getAllButtons, safeClickButton,
8
8
  logMsg, isHoldTight, getHoldTightReason, sleep, humanDelay,
9
9
  } = require('./utils');
10
10
 
11
- /**
12
- * Generic gamble handler — works for coinflip, roulette, slots, snakeeyes.
13
- *
14
- * @param {object} opts
15
- * @param {object} opts.channel
16
- * @param {function} opts.waitForDankMemer
17
- * @param {string} opts.cmdName - e.g. 'coinflip', 'roulette', 'slots', 'snakeeyes'
18
- * @param {string} opts.cmdString - Full command string e.g. 'pls coinflip 1000 heads'
19
- * @returns {Promise<{result: string, coins: number}>}
20
- */
11
+ function parseGambleResult(text, cmdName) {
12
+ const net = parseNetCoins(text);
13
+ const lower = text.toLowerCase();
14
+
15
+ if (net > 0) {
16
+ LOG.coin(`[${cmdName}] ${c.green}+⏣ ${net.toLocaleString()}${c.reset}`);
17
+ return { result: `${cmdName} +⏣ ${net.toLocaleString()}`, coins: net };
18
+ }
19
+ if (net < 0) {
20
+ LOG.warn(`[${cmdName}] ${c.red}-⏣ ${Math.abs(net).toLocaleString()}${c.reset}`);
21
+ return { result: `${cmdName} → -⏣ ${Math.abs(net).toLocaleString()}`, coins: 0, lost: Math.abs(net) };
22
+ }
23
+ const coins = parseCoins(text);
24
+ if (coins > 0) {
25
+ LOG.coin(`[${cmdName}] ${c.green}+⏣ ${coins.toLocaleString()}${c.reset}`);
26
+ return { result: `${cmdName} → +⏣ ${coins.toLocaleString()}`, coins };
27
+ }
28
+ if (lower.includes('won') || lower.includes('beat')) return { result: `${cmdName} → won`, coins: 0 };
29
+ if (lower.includes('lost') || lower.includes('bust')) return { result: `${cmdName} → lost`, coins: 0, lost: coins };
30
+
31
+ return { result: `${cmdName} done`, coins: 0 };
32
+ }
33
+
21
34
  async function runGamble({ channel, waitForDankMemer, cmdName, cmdString }) {
22
35
  LOG.cmd(`${c.white}${c.bold}${cmdString}${c.reset}`);
23
36
 
@@ -36,52 +49,48 @@ async function runGamble({ channel, waitForDankMemer, cmdName, cmdString }) {
36
49
  return { result: `hold tight (${reason || 'unknown'})`, coins: 0, holdTightReason: reason };
37
50
  }
38
51
 
52
+ // Check for min bet error
53
+ const initText = getFullText(response);
54
+ const initLower = initText.toLowerCase();
55
+ if (initLower.includes("can't bet less than") || initLower.includes('cannot bet less than') || initLower.includes('minimum bet')) {
56
+ const betMatch = initText.match(/less than\s*\*?\*?[⏣o]?\s*([\d,]+)/i) || initText.match(/(\d[\d,]+)/);
57
+ if (betMatch) {
58
+ const minBet = parseInt(betMatch[1].replace(/,/g, ''));
59
+ if (minBet > 0) return { result: `min bet ${minBet}`, coins: 0, newMinBet: minBet };
60
+ }
61
+ return { result: 'min bet error', coins: 0 };
62
+ }
63
+
39
64
  logMsg(response, cmdName);
40
65
  const text = getFullText(response);
41
- const coins = parseCoins(text);
42
66
 
43
- // Some gambles have buttons (e.g. pick heads/tails)
67
+ // For cointoss/gambles with buttons: click a random non-disabled button
44
68
  const buttons = getAllButtons(response);
45
69
  if (buttons.length > 0) {
46
- const btn = buttons.find(b => !b.disabled);
47
- if (btn) {
48
- LOG.info(`[${cmdName}] Clicking "${btn.label}"`);
49
- await humanDelay();
70
+ const clickable = buttons.filter(b => !b.disabled);
71
+ if (clickable.length > 0) {
72
+ const btn = clickable[Math.floor(Math.random() * clickable.length)];
73
+ LOG.info(`[${cmdName}] Clicking "${btn.label || '?'}"`);
74
+ await humanDelay(50, 200);
50
75
  try {
51
76
  const followUp = await safeClickButton(response, btn);
52
77
  if (followUp) {
53
78
  logMsg(followUp, `${cmdName}-result`);
54
79
  const fText = getFullText(followUp);
55
- const fCoins = parseCoins(fText);
56
- if (fCoins > 0) {
57
- LOG.coin(`[${cmdName}] ${c.green}+⏣ ${fCoins.toLocaleString()}${c.reset}`);
58
- return { result: `${cmdName} → +⏣ ${fCoins.toLocaleString()}`, coins: fCoins };
59
- }
60
- if (fText.toLowerCase().includes('won')) return { result: `${cmdName} → ${c.green}won${c.reset}`, coins: 0 };
61
- if (fText.toLowerCase().includes('lost')) return { result: `${cmdName} → ${c.red}lost${c.reset}`, coins: 0 };
80
+ return parseGambleResult(fText, cmdName);
62
81
  }
63
82
  } catch (e) { LOG.error(`[${cmdName}] Click error: ${e.message}`); }
64
83
  }
65
84
  }
66
85
 
67
- if (coins > 0) {
68
- LOG.coin(`[${cmdName}] ${c.green}+⏣ ${coins.toLocaleString()}${c.reset}`);
69
- return { result: `${cmdName} → +⏣ ${coins.toLocaleString()}`, coins };
70
- }
71
-
72
- const lower = text.toLowerCase();
73
- if (lower.includes('won')) return { result: `${cmdName} → won`, coins };
74
- if (lower.includes('lost')) {
75
- const lostCoins = parseCoins(text);
76
- return { result: `${cmdName} → lost`, coins: 0, lost: lostCoins };
77
- }
78
-
79
- return { result: `${cmdName} done`, coins: 0 };
86
+ return parseGambleResult(text, cmdName);
80
87
  }
81
88
 
82
- // Convenience wrappers
83
- async function runCoinflip({ channel, waitForDankMemer, betAmount = 5000 }) {
84
- return runGamble({ channel, waitForDankMemer, cmdName: 'coinflip', cmdString: `pls coinflip ${betAmount} heads` });
89
+ /**
90
+ * Cointoss: send "pls cointoss <bet>", then click Heads or Tails randomly.
91
+ */
92
+ async function runCointoss({ channel, waitForDankMemer, betAmount = 10000 }) {
93
+ return runGamble({ channel, waitForDankMemer, cmdName: 'cointoss', cmdString: `pls cointoss ${betAmount}` });
85
94
  }
86
95
 
87
96
  async function runRoulette({ channel, waitForDankMemer, betAmount = 5000 }) {
@@ -96,4 +105,4 @@ async function runSnakeeyes({ channel, waitForDankMemer, betAmount = 5000 }) {
96
105
  return runGamble({ channel, waitForDankMemer, cmdName: 'snakeeyes', cmdString: `pls snakeeyes ${betAmount}` });
97
106
  }
98
107
 
99
- module.exports = { runGamble, runCoinflip, runRoulette, runSlots, runSnakeeyes };
108
+ module.exports = { runGamble, runCointoss, runRoulette, runSlots, runSnakeeyes };
@@ -45,7 +45,7 @@ async function runGeneric({ channel, waitForDankMemer, cmdString, cmdName, clien
45
45
  const missing = needsItem(text);
46
46
  if (missing) {
47
47
  LOG.warn(`[${cmdName}] Missing ${c.bold}${missing}${c.reset} — auto-buying...`);
48
- const bought = await buyItem({ channel, waitForDankMemer, client, itemName: missing, quantity: 3 });
48
+ const bought = await buyItem({ channel, waitForDankMemer, client, itemName: missing, quantity: 1 });
49
49
  if (bought) {
50
50
  LOG.success(`[${cmdName}] Bought ${missing}, retrying command...`);
51
51
  await sleep(3000);
@@ -65,11 +65,12 @@ async function runGeneric({ channel, waitForDankMemer, cmdString, cmdName, clien
65
65
  return { result: `need ${missing} (buy failed)`, coins: 0 };
66
66
  }
67
67
 
68
- // Handle buttons if present
68
+ // Handle buttons if present — pick a random non-disabled button
69
69
  const buttons = getAllButtons(response);
70
70
  if (buttons.length > 0) {
71
- const btn = buttons.find(b => !b.disabled) || buttons[0];
72
- if (btn && !btn.disabled) {
71
+ const clickable = buttons.filter(b => !b.disabled);
72
+ const btn = clickable.length > 0 ? clickable[Math.floor(Math.random() * clickable.length)] : null;
73
+ if (btn) {
73
74
  LOG.info(`[${cmdName}] Clicking "${btn.label || '?'}"`);
74
75
  await humanDelay();
75
76
  try {
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * HighLow command handler.
3
- * Send "pls hl", parse the hint number, pick higher/lower/jackpot.
3
+ * Strategy: if hint > 50 → lower, if hint < 50 → higher, if exactly 50 → jackpot.
4
4
  */
5
5
 
6
6
  const {
@@ -8,38 +8,63 @@ const {
8
8
  logMsg, isHoldTight, getHoldTightReason, sleep, humanDelay,
9
9
  } = require('./utils');
10
10
 
11
- /**
12
- * Recursively handle highlow rounds (multi-round game).
13
- */
11
+ 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);
17
+ 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);
20
+ if (nums) {
21
+ for (const n of nums) {
22
+ const v = parseInt(n);
23
+ if (v >= 1 && v <= 100) return v;
24
+ }
25
+ }
26
+ return null;
27
+ }
28
+
29
+ function parseNetCoins(text) {
30
+ const netMatch = text.match(/Net:\s*[⏣o]\s*(-?[\d,]+)/i);
31
+ if (netMatch) return parseInt(netMatch[1].replace(/,/g, ''));
32
+ const winMatch = text.match(/Winnings:\s*[⏣o]\s*([\d,]+)/i);
33
+ if (winMatch) return parseInt(winMatch[1].replace(/,/g, ''));
34
+ return 0;
35
+ }
36
+
14
37
  async function playHighLow(response, depth = 0) {
15
38
  if (!response || depth > 5) return { result: 'done', coins: 0 };
16
39
 
17
40
  const text = getFullText(response);
18
41
  const buttons = getAllButtons(response);
19
42
 
20
- if (buttons.length === 0) {
21
- const coins = parseCoins(text);
22
- return { result: coins > 0 ? `+⏣ ${coins.toLocaleString()}` : 'done', coins };
43
+ if (buttons.length === 0 || buttons.every(b => b.disabled)) {
44
+ const net = parseNetCoins(text);
45
+ const coins = net > 0 ? net : parseCoins(text);
46
+ const lost = net < 0 ? Math.abs(net) : 0;
47
+ return { result: coins > 0 ? `+⏣ ${coins.toLocaleString()}` : 'done', coins, lost };
23
48
  }
24
49
 
25
- // Parse the hint number
26
- const match = text.match(/number.*?(\d+)/i) || text.match(/(\d+)/);
50
+ const hint = parseHintNumber(text);
27
51
  let targetBtn;
28
52
 
29
- if (match && buttons.length >= 2) {
30
- const num = parseInt(match[1]);
31
- if (num > 50) {
32
- targetBtn = buttons.find(b => (b.label || '').toLowerCase().includes('lower')) || buttons[1];
33
- } else if (num < 50) {
34
- targetBtn = buttons.find(b => (b.label || '').toLowerCase().includes('higher')) || buttons[0];
53
+ if (hint !== null && buttons.length >= 2) {
54
+ if (hint > 50) {
55
+ targetBtn = buttons.find(b => (b.label || '').toLowerCase().includes('lower'));
56
+ } else if (hint < 50) {
57
+ targetBtn = buttons.find(b => (b.label || '').toLowerCase().includes('higher'));
35
58
  } else {
36
- const jackpot = buttons.find(b => (b.label || '').toLowerCase().includes('jackpot'));
37
- targetBtn = jackpot || buttons[Math.floor(Math.random() * 2)];
59
+ // Exactly 50 — try jackpot, fallback to higher (slightly better odds)
60
+ targetBtn = buttons.find(b => (b.label || '').toLowerCase().includes('jackpot'))
61
+ || buttons.find(b => (b.label || '').toLowerCase().includes('higher'));
38
62
  }
39
- LOG.info(`[hl] Number: ${num} picking "${targetBtn.label}"`);
63
+ if (!targetBtn) targetBtn = buttons.find(b => !b.disabled);
64
+ LOG.info(`[hl] Hint: ${hint} → ${targetBtn?.label || '?'}`);
40
65
  } else {
41
66
  targetBtn = buttons.find(b => !b.disabled) || buttons[0];
42
- LOG.info(`[hl] No hint, picking "${targetBtn.label}"`);
67
+ LOG.info(`[hl] No hint parsed ${targetBtn?.label || '?'}`);
43
68
  }
44
69
 
45
70
  if (!targetBtn || targetBtn.disabled) {
@@ -47,26 +72,24 @@ async function playHighLow(response, depth = 0) {
47
72
  return { result: 'buttons disabled', coins };
48
73
  }
49
74
 
50
- await humanDelay(500, 1200);
75
+ await humanDelay(400, 900);
51
76
 
52
77
  try {
53
78
  const followUp = await safeClickButton(response, targetBtn);
54
79
  if (followUp) {
55
80
  logMsg(followUp, `hl-round-${depth}`);
56
81
  const fText = getFullText(followUp);
57
- const fCoins = parseCoins(fText);
58
82
 
59
- // Check for more rounds
60
83
  const moreButtons = getAllButtons(followUp);
61
84
  if (moreButtons.length >= 2 && !moreButtons.every(b => b.disabled)) {
62
- const deeper = await playHighLow(followUp, depth + 1);
63
- return { result: deeper.result, coins: fCoins + deeper.coins };
85
+ return playHighLow(followUp, depth + 1);
64
86
  }
65
87
 
66
- if (fCoins > 0) {
67
- return { result: `${targetBtn.label} → +⏣ ${fCoins.toLocaleString()}`, coins: fCoins };
68
- }
69
- return { result: `${targetBtn.label}`, coins: 0 };
88
+ // Final round parse net earnings
89
+ const net = parseNetCoins(fText);
90
+ const coins = net > 0 ? net : parseCoins(fText);
91
+ const lost = net < 0 ? Math.abs(net) : 0;
92
+ return { result: `${targetBtn.label} → ${coins > 0 ? '+⏣ ' + coins.toLocaleString() : 'done'}`, coins, lost };
70
93
  }
71
94
  } catch (e) {
72
95
  LOG.error(`[hl] Click error: ${e.message}`);
@@ -75,12 +98,6 @@ async function playHighLow(response, depth = 0) {
75
98
  return { result: 'done', coins: 0 };
76
99
  }
77
100
 
78
- /**
79
- * @param {object} opts
80
- * @param {object} opts.channel
81
- * @param {function} opts.waitForDankMemer
82
- * @returns {Promise<{result: string, coins: number}>}
83
- */
84
101
  async function runHighLow({ channel, waitForDankMemer }) {
85
102
  LOG.cmd(`${c.white}${c.bold}pls hl${c.reset}`);
86
103
 
@@ -100,13 +117,13 @@ async function runHighLow({ channel, waitForDankMemer }) {
100
117
  }
101
118
 
102
119
  logMsg(response, 'hl');
103
- const { result, coins } = await playHighLow(response);
120
+ const { result, coins, lost } = await playHighLow(response);
104
121
 
105
122
  if (coins > 0) {
106
123
  LOG.coin(`[hl] ${c.green}+⏣ ${coins.toLocaleString()}${c.reset}`);
107
124
  }
108
125
 
109
- return { result: `hl → ${result}`, coins };
126
+ return { result: `hl → ${result}`, coins, lost: lost || 0 };
110
127
  }
111
128
 
112
129
  module.exports = { runHighLow };
@@ -45,7 +45,7 @@ async function runHunt({ channel, waitForDankMemer, client }) {
45
45
  const bought = await buyItem({
46
46
  channel, waitForDankMemer, client,
47
47
  itemName: 'Hunting Rifle',
48
- quantity: 3,
48
+ quantity: 1,
49
49
  });
50
50
 
51
51
  if (bought) {
@@ -16,7 +16,7 @@ const { runScratch } = require('./scratch');
16
16
  const { runBlackjack } = require('./blackjack');
17
17
  const { runTrivia, triviaDB } = require('./trivia');
18
18
  const { runWorkShift } = require('./work');
19
- const { runCoinflip, runRoulette, runSlots, runSnakeeyes, runGamble } = require('./gamble');
19
+ const { runCointoss, runRoulette, runSlots, runSnakeeyes, runGamble } = require('./gamble');
20
20
  const { runDeposit } = require('./deposit');
21
21
  const { runGeneric, runAlert } = require('./generic');
22
22
  const { runStream } = require('./stream');
@@ -39,7 +39,7 @@ module.exports = {
39
39
  runBlackjack,
40
40
  runTrivia,
41
41
  runWorkShift,
42
- runCoinflip,
42
+ runCointoss,
43
43
  runRoulette,
44
44
  runSlots,
45
45
  runSnakeeyes,
@@ -54,15 +54,26 @@ async function runPostMemes({ channel, waitForDankMemer }) {
54
54
 
55
55
  // Check for cooldown or direct text response (no select menus)
56
56
  let selects = getAllSelectMenus(response);
57
+ const initText = getFullText(response);
58
+ const initLower = initText.toLowerCase();
59
+
60
+ // Detect "dead meme" / cooldown message → return with nextCooldownSec
61
+ if (initLower.includes('cannot post another meme') || initLower.includes('dead meme') ||
62
+ initLower.includes('another meme for another')) {
63
+ const minMatch = initText.match(/(\d+)\s*minute/i);
64
+ const cdSec = minMatch ? parseInt(minMatch[1]) * 60 : 120;
65
+ LOG.warn(`[pm] Cooldown: ${cdSec}s`);
66
+ return { result: `pm cooldown ${cdSec}s`, coins: 0, nextCooldownSec: cdSec };
67
+ }
68
+
57
69
  if (selects.length === 0) {
58
- const text = getFullText(response);
59
- const coins = parseCoins(text);
70
+ const coins = parseCoins(initText);
60
71
  if (coins > 0) {
61
72
  LOG.coin(`[pm] ${c.green}+⏣ ${coins.toLocaleString()}${c.reset}`);
62
73
  return { result: `pm → +⏣ ${coins.toLocaleString()}`, coins };
63
74
  }
64
- LOG.info(`[pm] No menus: ${text.substring(0, 100).replace(/\n/g, ' ')}`);
65
- return { result: text.substring(0, 60), coins: 0 };
75
+ LOG.info(`[pm] No menus: ${initText.substring(0, 100).replace(/\n/g, ' ')}`);
76
+ return { result: initText.substring(0, 60), coins: 0 };
66
77
  }
67
78
 
68
79
  const msgId = response.id;
@@ -32,8 +32,12 @@ const ITEM_COSTS = {
32
32
  * @returns {Promise<boolean>} true if purchase succeeded
33
33
  */
34
34
  async function buyItem({ channel, waitForDankMemer, itemName, quantity = 1, client }) {
35
- const MAX_RETRIES = 3;
36
- const searchName = itemName.toLowerCase().replace('hunting ', '').replace('fishing ', '').replace('adventure ', '');
35
+ const MAX_RETRIES = 1;
36
+ const searchNames = [
37
+ itemName.toLowerCase(),
38
+ itemName.toLowerCase().replace('hunting ', '').replace('fishing ', '').replace('adventure ', ''),
39
+ itemName.toLowerCase().split(' ').pop(),
40
+ ];
37
41
 
38
42
  for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
39
43
  LOG.buy(`Opening shop to buy ${c.bold}${quantity}x ${itemName}${c.reset} (attempt ${attempt}/${MAX_RETRIES})`);
@@ -120,30 +124,17 @@ async function buyItem({ channel, waitForDankMemer, itemName, quantity = 1, clie
120
124
  logMsg(response, 'shop-after-nav');
121
125
 
122
126
  // Step 3: Find the Buy button for our item
123
- let buyBtn = getAllButtons(response).find(b => b.label && b.label.toLowerCase().includes(searchName));
127
+ let buyBtn = getAllButtons(response).find(b => {
128
+ if (!b.label) return false;
129
+ const label = b.label.toLowerCase();
130
+ return searchNames.some(s => label.includes(s) || s.includes(label));
131
+ });
124
132
 
125
133
  if (!buyBtn) {
126
- LOG.warn(`No Buy button found for "${itemName}" (searched: "${searchName}")`);
127
- // Log all available buttons for debugging
134
+ LOG.warn(`No Buy button found for "${itemName}"`);
128
135
  const allBtns = getAllButtons(response);
129
136
  if (allBtns.length > 0) {
130
- LOG.debug(`Available buttons: ${allBtns.map(b => `"${b.label}"`).join(', ')}`);
131
- }
132
- // Maybe we need to scroll/paginate?
133
- const nextBtn = findButton(response, 'next') || findButton(response, '▶') || findButton(response, '→');
134
- if (nextBtn && !nextBtn.disabled) {
135
- LOG.buy('Clicking next page to find item...');
136
- try {
137
- const nextPage = await safeClickButton(response, nextBtn);
138
- if (nextPage) {
139
- response = nextPage;
140
- logMsg(response, 'shop-page2');
141
- // Try finding button again
142
- buyBtn = getAllButtons(response).find(b => b.label && b.label.toLowerCase().includes(searchName));
143
- }
144
- } catch (e) {
145
- LOG.error(`Page nav failed: ${e.message}`);
146
- }
137
+ LOG.debug(`Available: ${allBtns.map(b => `"${b.label}"`).join(', ')}`);
147
138
  }
148
139
 
149
140
  if (!buyBtn) {
@@ -65,7 +65,22 @@ function getFullText(msg) {
65
65
 
66
66
  function parseCoins(text) {
67
67
  if (!text) return 0;
68
- // Match prefixed coins
68
+ // Prefer "Net:" if present (accurate earned/lost from Dank Memer)
69
+ const netMatch = text.match(/Net:\s*[⏣o]\s*(-?[\d,]+)/i);
70
+ if (netMatch) {
71
+ const net = parseInt(netMatch[1].replace(/,/g, ''));
72
+ return net > 0 ? net : 0;
73
+ }
74
+ // Prefer "Winnings:" over random coin values
75
+ const winMatch = text.match(/Winnings:\s*[⏣o]\s*([\d,]+)/i);
76
+ if (winMatch) {
77
+ const w = parseInt(winMatch[1].replace(/,/g, ''));
78
+ if (w > 0) return w;
79
+ }
80
+ // Prefer "placed in your wallet" pattern (daily, beg, etc.)
81
+ const walletMatch = text.match(/⏣\s*([\d,]+)\s*was placed/i);
82
+ if (walletMatch) return parseInt(walletMatch[1].replace(/,/g, ''));
83
+ // Fallback: max ⏣ number
69
84
  const matches = text.match(/⏣\s*([\d,]+)/g);
70
85
  if (!matches) return 0;
71
86
  let best = 0;
@@ -79,6 +94,13 @@ function parseCoins(text) {
79
94
  return best;
80
95
  }
81
96
 
97
+ function parseNetCoins(text) {
98
+ if (!text) return 0;
99
+ const netMatch = text.match(/Net:\s*[⏣o]\s*(-?[\d,]+)/i);
100
+ if (netMatch) return parseInt(netMatch[1].replace(/,/g, ''));
101
+ return 0;
102
+ }
103
+
82
104
  /**
83
105
  * Parse wallet balance from a balance response message.
84
106
  * Handles both embed-based and CV2 TEXT_DISPLAY component formats.
@@ -288,6 +310,7 @@ module.exports = {
288
310
  humanDelay,
289
311
  getFullText,
290
312
  parseCoins,
313
+ parseNetCoins,
291
314
  parseBalance,
292
315
  getAllButtons,
293
316
  getAllSelectMenus,
package/lib/grinder.js CHANGED
@@ -154,54 +154,75 @@ function renderDashboard() {
154
154
  lastRenderTime = Date.now();
155
155
 
156
156
  totalBalance = 0; totalCoins = 0; totalCommands = 0;
157
+ let totalErrors = 0;
157
158
  for (const w of workers) {
158
159
  totalBalance += w.stats.balance || 0;
159
160
  totalCoins += w.stats.coins || 0;
160
161
  totalCommands += w.stats.commands || 0;
162
+ totalErrors += w.stats.errors || 0;
161
163
  }
164
+ const successRate = totalCommands > 0 ? Math.round(((totalCommands - totalErrors) / totalCommands) * 100) : 100;
162
165
 
163
166
  const lines = [];
164
- const tw = Math.min(process.stdout.columns || 80, 76);
167
+ const tw = Math.min(process.stdout.columns || 80, 78);
165
168
  const thinBar = c.dim + '─'.repeat(tw) + c.reset;
166
- const thickTop = rgb(139, 92, 246) + c.bold + ''.repeat(tw) + c.reset;
167
- const thickBot = rgb(34, 211, 238) + c.bold + ''.repeat(tw) + c.reset;
168
-
169
- lines.push(thickTop);
169
+ const topBar = rgb(139, 92, 246) + c.bold + ''.repeat(tw) + c.reset;
170
+ const botBar = rgb(34, 211, 238) + c.bold + ''.repeat(tw) + c.reset;
171
+
172
+ // Status bar
173
+ lines.push(topBar);
174
+ const liveIcon = rgb(52, 211, 153) + '◉' + c.reset;
175
+ const balStr = `${rgb(192, 132, 252)}${c.bold}⏣ ${formatCoins(totalBalance)}${c.reset}`;
176
+ const earnStr = `${rgb(52, 211, 153)}↑ ${formatCoins(totalCoins)}${c.reset}`;
177
+ const cmdStr = `${rgb(96, 165, 250)}${totalCommands}${c.reset}${c.dim} cmds${c.reset}`;
178
+ const rateStr = successRate >= 95
179
+ ? `${rgb(52, 211, 153)}${successRate}%${c.reset}`
180
+ : successRate >= 80 ? `${rgb(251, 191, 36)}${successRate}%${c.reset}`
181
+ : `${rgb(239, 68, 68)}${successRate}%${c.reset}`;
182
+ const upStr = `${rgb(251, 191, 36)}⏱ ${formatUptime()}${c.reset}`;
170
183
  lines.push(
171
- ` ${rgb(192, 132, 252)}${c.bold} ${formatCoins(totalBalance)}${c.reset}` +
172
- ` ${c.dim}│${c.reset}` +
173
- ` ${rgb(52, 211, 153)}+${formatCoins(totalCoins)}${c.reset}` +
174
- ` ${c.dim}│${c.reset}` +
175
- ` ${c.white}${totalCommands}${c.reset}${c.dim} cmds${c.reset}` +
176
- ` ${c.dim}│${c.reset}` +
177
- ` ${rgb(251, 191, 36)}${formatUptime()}${c.reset}` +
178
- ` ${c.dim}│${c.reset}` +
179
- ` ${rgb(52, 211, 153)}● LIVE${c.reset}`
184
+ ` ${liveIcon} ${balStr} ${c.dim}│${c.reset} ${earnStr} ${c.dim}│${c.reset} ${cmdStr} ${c.dim}(${c.reset}${rateStr}${c.dim})${c.reset} ${c.dim}│${c.reset} ${upStr}`
180
185
  );
181
186
  lines.push(thinBar);
182
187
 
183
- const nameWidth = Math.min(14, tw > 60 ? 14 : 10);
184
- const statusWidth = Math.max(12, tw - nameWidth - 30);
188
+ // Worker rows
189
+ const nameWidth = Math.min(16, tw > 65 ? 16 : 10);
190
+ const statusWidth = Math.max(16, tw - nameWidth - 34);
185
191
 
186
192
  for (const wk of workers) {
187
193
  const rawStatus = (wk.lastStatus || 'idle').replace(/\x1b\[[0-9;]*m/g, '');
188
194
  const last = rawStatus.substring(0, statusWidth);
189
- const dot = wk.running
190
- ? (wk.paused ? `${rgb(239, 68, 68)}⏸${c.reset}`
191
- : wk.dashboardPaused ? `${rgb(251, 191, 36)}⏸${c.reset}`
192
- : wk.busy ? `${rgb(251, 191, 36)}◉${c.reset}`
193
- : `${rgb(52, 211, 153)}●${c.reset}`)
194
- : `${rgb(239, 68, 68)}○${c.reset}`;
195
- const name = `${wk.color}${c.bold}${(wk.username || '?').substring(0, nameWidth).padEnd(nameWidth)}${c.reset}`;
195
+
196
+ let dot, stateLabel;
197
+ if (!wk.running) {
198
+ dot = `${rgb(239, 68, 68)}○${c.reset}`;
199
+ stateLabel = `${c.dim}offline${c.reset}`;
200
+ } else if (wk.paused) {
201
+ dot = `${rgb(239, 68, 68)}⏸${c.reset}`;
202
+ stateLabel = `${rgb(239, 68, 68)}PAUSED${c.reset}`;
203
+ } else if (wk.dashboardPaused) {
204
+ dot = `${rgb(251, 191, 36)}⏸${c.reset}`;
205
+ stateLabel = `${rgb(251, 191, 36)}paused${c.reset}`;
206
+ } else if (wk.busy) {
207
+ dot = `${rgb(251, 191, 36)}◉${c.reset}`;
208
+ stateLabel = `${c.dim}${last}${c.reset}`;
209
+ } else {
210
+ dot = `${rgb(52, 211, 153)}●${c.reset}`;
211
+ stateLabel = `${c.dim}${last}${c.reset}`;
212
+ }
213
+
214
+ const name = `${wk.color}${c.bold}${(wk.username || '?').substring(0, nameWidth)}${c.reset}`;
196
215
  const bal = wk.stats.balance > 0
197
- ? `${rgb(251, 191, 36)}⏣${c.reset}${c.white}${formatCoins(wk.stats.balance).padStart(6)}${c.reset}`
198
- : `${c.dim}⏣ -${c.reset}`;
216
+ ? `${rgb(251, 191, 36)}⏣${c.reset} ${c.white}${formatCoins(wk.stats.balance).padStart(7)}${c.reset}`
217
+ : `${c.dim}⏣ -${c.reset}`;
199
218
  const earned = wk.stats.coins > 0
200
- ? `${rgb(52, 211, 153)}+${formatCoins(wk.stats.coins)}${c.reset}`
201
- : `${c.dim}+0${c.reset}`;
202
- lines.push(` ${dot} ${name} ${bal} ${earned} ${c.dim}${last}${c.reset}`);
219
+ ? `${rgb(52, 211, 153)}+${formatCoins(wk.stats.coins).padStart(6)}${c.reset}`
220
+ : `${c.dim} +0${c.reset}`;
221
+
222
+ lines.push(` ${dot} ${name.padEnd(nameWidth + wk.color.length + c.bold.length + c.reset.length)} ${bal} ${earned} ${stateLabel}`);
203
223
  }
204
224
 
225
+ // Log section
205
226
  if (recentLogs.length > 0) {
206
227
  lines.push(thinBar);
207
228
  for (const entry of recentLogs) {
@@ -209,7 +230,7 @@ function renderDashboard() {
209
230
  }
210
231
  }
211
232
 
212
- lines.push(thickBot);
233
+ lines.push(botBar);
213
234
 
214
235
  if (dashboardLines > 0) {
215
236
  process.stdout.write(c.cursorUp(dashboardLines));
@@ -291,19 +312,21 @@ function safeParseJSON(str, fallback = []) {
291
312
  try { return JSON.parse(str || '[]'); } catch { return fallback; }
292
313
  }
293
314
 
294
- // ── Coin Parser (conservative onlyamounts) ─────────────
315
+ // ── Coin Parser — prefers Net:/Winnings: fields, falls back to max ──
295
316
  function parseCoins(text) {
296
317
  if (!text) return 0;
297
- // Only match coins that are clearly prefixed
318
+ const netMatch = text.match(/Net:\s*[o]\s*(-?[\d,]+)/i);
319
+ if (netMatch) { const n = parseInt(netMatch[1].replace(/,/g, '')); return n > 0 ? n : 0; }
320
+ const winMatch = text.match(/Winnings:\s*[⏣o]\s*([\d,]+)/i);
321
+ if (winMatch) { const w = parseInt(winMatch[1].replace(/,/g, '')); if (w > 0) return w; }
322
+ const walletMatch = text.match(/⏣\s*([\d,]+)\s*was placed/i);
323
+ if (walletMatch) return parseInt(walletMatch[1].replace(/,/g, ''));
298
324
  const matches = text.match(/⏣\s*([\d,]+)/g);
299
325
  if (!matches) return 0;
300
326
  let best = 0;
301
327
  for (const m of matches) {
302
328
  const numStr = m.replace(/[^\d]/g, '');
303
- if (numStr) {
304
- const val = parseInt(numStr);
305
- if (val > 0 && val < 10000000) best = Math.max(best, val); // Cap at 10M sanity
306
- }
329
+ if (numStr) { const val = parseInt(numStr); if (val > 0 && val < 10000000) best = Math.max(best, val); }
307
330
  }
308
331
  return best;
309
332
  }
@@ -486,6 +509,7 @@ class AccountWorker {
486
509
  this.globalCooldownUntil = 0;
487
510
  this.commandQueue = null;
488
511
  this.lastHealthCheck = Date.now();
512
+ this.doneToday = new Map(); // in-memory dedup: cmd → expiry timestamp
489
513
  }
490
514
 
491
515
  get tag() { return `${this.color}${c.bold}${this.username}${c.reset}`; }
@@ -593,8 +617,8 @@ class AccountWorker {
593
617
  return null;
594
618
  }
595
619
 
596
- async buyItem(itemName, quantity = 10) {
597
- const MAX_RETRIES = 3;
620
+ async buyItem(itemName, quantity = 1) {
621
+ const MAX_RETRIES = 1;
598
622
  for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
599
623
  this.log('buy', `Opening shop to buy ${c.bold}${quantity}x ${itemName}${c.reset}... (attempt ${attempt}/${MAX_RETRIES})`);
600
624
  if (this.account.use_slash) {
@@ -648,12 +672,18 @@ class AccountWorker {
648
672
  }
649
673
  await humanDelay(1000, 2000);
650
674
 
651
- // Find Buy button
675
+ // Find Buy button — match by full name or partial name
652
676
  let buyBtn = null;
653
- const searchName = itemName.toLowerCase().replace('hunting ', '').replace('fishing ', '');
677
+ const searchNames = [
678
+ itemName.toLowerCase(),
679
+ itemName.toLowerCase().replace('hunting ', '').replace('fishing ', ''),
680
+ itemName.toLowerCase().split(' ')[0],
681
+ ];
654
682
  for (const row of response.components || []) {
655
683
  for (const comp of row.components || []) {
656
- if (comp.type === 2 && comp.label && comp.label.toLowerCase().includes(searchName)) {
684
+ if (comp.type !== 2 || !comp.label) continue;
685
+ const label = comp.label.toLowerCase();
686
+ if (searchNames.some(s => label.includes(s) || s.includes(label))) {
657
687
  buyBtn = comp; break;
658
688
  }
659
689
  }
@@ -765,7 +795,7 @@ class AccountWorker {
765
795
  case 'dep max': cmdString = `${prefix} dep max`; break;
766
796
  case 'with max': cmdString = `${prefix} with max`; break;
767
797
  case 'blackjack': cmdString = `${prefix} bj ${betAmount}`; break;
768
- case 'coinflip': cmdString = `${prefix} coinflip ${betAmount} heads`; break;
798
+ case 'cointoss': cmdString = `${prefix} cointoss ${betAmount}`; break;
769
799
  case 'roulette': cmdString = `${prefix} roulette ${betAmount} red`; break;
770
800
  case 'slots': cmdString = `${prefix} slots ${betAmount}`; break;
771
801
  case 'snakeeyes': cmdString = `${prefix} snakeeyes ${betAmount}`; break;
@@ -803,7 +833,7 @@ class AccountWorker {
803
833
  case 'blackjack': cmdResult = await commands.runBlackjack(cmdOpts); break;
804
834
  case 'trivia': cmdResult = await commands.runTrivia(cmdOpts); break;
805
835
  case 'work shift': cmdResult = await commands.runWorkShift(cmdOpts); break;
806
- case 'coinflip': cmdResult = await commands.runCoinflip(cmdOpts); break;
836
+ case 'cointoss': cmdResult = await commands.runCointoss(cmdOpts); break;
807
837
  case 'roulette': cmdResult = await commands.runRoulette(cmdOpts); break;
808
838
  case 'slots': cmdResult = await commands.runSlots(cmdOpts); break;
809
839
  case 'snakeeyes': cmdResult = await commands.runSnakeeyes(cmdOpts); break;
@@ -825,6 +855,15 @@ class AccountWorker {
825
855
  return;
826
856
  }
827
857
 
858
+ // PostMemes / command-specific cooldown from response
859
+ if (resultLower.includes('cannot post another meme') || resultLower.includes('dead meme')) {
860
+ const minMatch = result.match(/(\d+)\s*minute/i);
861
+ const cdSec = minMatch ? parseInt(minMatch[1]) * 60 : 120;
862
+ this.log('warn', `${cmdName} on cooldown: ${cdSec}s`);
863
+ await this.setCooldown(cmdName, cdSec);
864
+ return;
865
+ }
866
+
828
867
  // Captcha/verification detection — deactivate account and stop
829
868
  if (resultLower.includes('captcha') || resultLower.includes('verification required') ||
830
869
  resultLower.includes('verify your account') || resultLower.includes('pass verification') ||
@@ -846,18 +885,24 @@ class AccountWorker {
846
885
  return;
847
886
  }
848
887
 
849
- // Min bet detection — raise bet amount
850
- if (resultLower.includes('cannot bet less than') || resultLower.includes('minimum bet')) {
851
- const betMatch = result.match(/⏣\s*([\d,]+)/);
888
+ // Min bet detection — "You can't bet less than 10,000" or "cannot bet less than ⏣ 5,000"
889
+ if (resultLower.includes("can't bet less than") || resultLower.includes('cannot bet less than') || resultLower.includes('minimum bet')) {
890
+ const betMatch = result.match(/less than\s*\*?\*?\s*[⏣o]?\s*([\d,]+)/i) || result.match(/(\d[\d,]+)/);
852
891
  if (betMatch) {
853
892
  const minBet = parseInt(betMatch[1].replace(/,/g, ''));
854
893
  if (minBet > 0) {
855
894
  this.account.bet_amount = minBet;
856
- this.log('info', `${cmdName} min bet: ⏣ ${minBet.toLocaleString()}`);
895
+ this.log('info', `${cmdName} min bet raised → ⏣ ${minBet.toLocaleString()}`);
857
896
  }
858
897
  }
859
898
  return;
860
899
  }
900
+ // Also handle newMinBet from gamble handler
901
+ if (cmdResult.newMinBet && cmdResult.newMinBet > (this.account.bet_amount || 0)) {
902
+ this.account.bet_amount = cmdResult.newMinBet;
903
+ this.log('info', `${cmdName} min bet raised → ⏣ ${cmdResult.newMinBet.toLocaleString()}`);
904
+ return;
905
+ }
861
906
 
862
907
  // Premium-only command detection — disable for 24h
863
908
  if (resultLower.includes('only available on premium') || resultLower.includes('premium') ||
@@ -867,32 +912,52 @@ class AccountWorker {
867
912
  return;
868
913
  }
869
914
 
870
- // Already claimed today (daily/weekly) — set long cooldown
915
+ // Already claimed today (daily/weekly) — set long cooldown + mark done
871
916
  if (resultLower.includes('already got your daily') || resultLower.includes('try again <t:')) {
872
917
  this.log('info', `${cmdName} already claimed — waiting`);
873
918
  const timeMatch = result.match(/<t:(\d+):R>/);
919
+ let waitSec;
874
920
  if (timeMatch) {
875
921
  const nextAvail = parseInt(timeMatch[1]) * 1000;
876
- const waitSec = Math.max(60, Math.ceil((nextAvail - Date.now()) / 1000));
877
- await this.setCooldown(cmdName, waitSec);
922
+ waitSec = Math.max(60, Math.ceil((nextAvail - Date.now()) / 1000));
878
923
  } else {
879
- await this.setCooldown(cmdName, cmdName === 'daily' ? 86400 : 604800);
924
+ waitSec = cmdName === 'daily' ? 86400 : 604800;
880
925
  }
926
+ await this.setCooldown(cmdName, waitSec);
927
+ this.doneToday.set(cmdName, Date.now() + waitSec * 1000);
928
+ if (redis) try { await redis.set(`dkg:done:${this.account.id}:${cmdName}`, '1', 'EX', waitSec); } catch {}
881
929
  return;
882
930
  }
883
931
 
884
- // Scratch level-gate: if level too low, disable for 24h
932
+ // Skip reasons: level too low, no ticket, etc.
885
933
  if (cmdResult.skipReason === 'level') {
886
934
  this.log('warn', `${cmdName} level too low — retry in 24h`);
887
935
  await this.setCooldown(cmdName, 86400);
888
936
  return;
889
937
  }
938
+ if (cmdResult.skipReason === 'no_ticket') {
939
+ this.log('warn', `${cmdName} no ticket — retry in 1h`);
940
+ await this.setCooldown(cmdName, 3600);
941
+ return;
942
+ }
890
943
 
891
944
  const earned = Math.max(0, cmdResult.coins || 0);
892
945
  const spent = Math.max(0, cmdResult.lost || 0);
893
946
  if (earned > 0) this.stats.coins += earned;
894
947
  if (cmdResult.nextCooldownSec) await this.setCooldown(cmdName, cmdResult.nextCooldownSec);
895
948
 
949
+ // Mark daily/drops as done so we don't re-run this session
950
+ if (cmdName === 'daily' && earned > 0) {
951
+ const expiry = Date.now() + 86400 * 1000;
952
+ this.doneToday.set('daily', expiry);
953
+ if (redis) try { await redis.set(`dkg:done:${this.account.id}:daily`, '1', 'EX', 86400); } catch {}
954
+ }
955
+ if (cmdName === 'drops') {
956
+ const expiry = Date.now() + 86400 * 1000;
957
+ this.doneToday.set('drops', expiry);
958
+ if (redis) try { await redis.set(`dkg:done:${this.account.id}:drops`, '1', 'EX', 86400); } catch {}
959
+ }
960
+
896
961
  if (cmdResult.holdTightReason) {
897
962
  const reason = cmdResult.holdTightReason;
898
963
  this.log('warn', `Hold Tight: /${reason} — 35s cooldown`);
@@ -948,7 +1013,7 @@ class AccountWorker {
948
1013
  { key: 'cmd_trivia', cmd: 'trivia', cdKey: 'cd_trivia', defaultCd: 10, priority: 2 },
949
1014
  // Gambling (fast cycle)
950
1015
  { key: 'cmd_blackjack', cmd: 'blackjack', cdKey: 'cd_blackjack', defaultCd: 3, priority: 3 },
951
- { key: 'cmd_cointoss', cmd: 'coinflip', cdKey: 'cd_cointoss', defaultCd: 2, priority: 3 },
1016
+ { key: 'cmd_cointoss', cmd: 'cointoss', cdKey: 'cd_cointoss', defaultCd: 2, priority: 3 },
952
1017
  { key: 'cmd_roulette', cmd: 'roulette', cdKey: 'cd_roulette', defaultCd: 3, priority: 3 },
953
1018
  { key: 'cmd_slots', cmd: 'slots', cdKey: 'cd_slots', defaultCd: 3, priority: 3 },
954
1019
  { key: 'cmd_snakeeyes', cmd: 'snakeeyes', cdKey: 'cd_snakeeyes', defaultCd: 3, priority: 3 },
@@ -1104,6 +1169,30 @@ class AccountWorker {
1104
1169
  return;
1105
1170
  }
1106
1171
 
1172
+ // Skip daily/drops if already done today (in-memory + Redis)
1173
+ if (item.cmd === 'daily' || item.cmd === 'drops') {
1174
+ const memExpiry = this.doneToday.get(item.cmd);
1175
+ if (memExpiry && Date.now() < memExpiry) {
1176
+ item.nextRunAt = memExpiry;
1177
+ if (this.commandQueue) this.commandQueue.push(item);
1178
+ this.tickTimeout = setTimeout(() => this.tick(), 100);
1179
+ return;
1180
+ }
1181
+ if (redis) {
1182
+ try {
1183
+ const done = await redis.get(`dkg:done:${this.account.id}:${item.cmd}`);
1184
+ if (done) {
1185
+ const expiry = now + 86400 * 1000;
1186
+ this.doneToday.set(item.cmd, expiry);
1187
+ item.nextRunAt = expiry;
1188
+ if (this.commandQueue) this.commandQueue.push(item);
1189
+ this.tickTimeout = setTimeout(() => this.tick(), 100);
1190
+ return;
1191
+ }
1192
+ } catch {}
1193
+ }
1194
+ }
1195
+
1107
1196
  this.busy = true;
1108
1197
  const cd = (this.account[item.info.cdKey] || item.info.defaultCd);
1109
1198
  const jitter = 1 + Math.random() * 3;
@@ -1111,10 +1200,11 @@ class AccountWorker {
1111
1200
 
1112
1201
  await this.setCooldown(item.cmd, totalWait);
1113
1202
 
1203
+ // Inter-command delay: 1-3s random (human-like spacing)
1114
1204
  const timeSinceLastCmd = now - (this.lastCommandRun || 0);
1115
- const globalJitter = 500 + Math.random() * 1000;
1116
- if (timeSinceLastCmd < globalJitter) {
1117
- await new Promise(r => setTimeout(r, globalJitter - timeSinceLastCmd));
1205
+ const minGap = 1000 + Math.random() * 2000; // 1-3s
1206
+ if (timeSinceLastCmd < minGap) {
1207
+ await new Promise(r => setTimeout(r, minGap - timeSinceLastCmd));
1118
1208
  }
1119
1209
 
1120
1210
  const prefix = this.account.use_slash ? '/' : 'pls';
@@ -1270,7 +1360,7 @@ class AccountWorker {
1270
1360
  { key: 'cmd_stream', l: 'stream' }, { key: 'cmd_scratch', l: 'scratch' },
1271
1361
  { key: 'cmd_adventure', l: 'adv' }, { key: 'cmd_farm', l: 'farm' },
1272
1362
  { key: 'cmd_tidy', l: 'tidy' }, { key: 'cmd_blackjack', l: 'bj' },
1273
- { key: 'cmd_cointoss', l: 'flip' }, { key: 'cmd_roulette', l: 'roul' },
1363
+ { key: 'cmd_cointoss', l: 'toss' }, { key: 'cmd_roulette', l: 'roul' },
1274
1364
  { key: 'cmd_slots', l: 'slots' }, { key: 'cmd_snakeeyes', l: 'snake' },
1275
1365
  { key: 'cmd_trivia', l: 'trivia' }, { key: 'cmd_use', l: 'use' },
1276
1366
  { key: 'cmd_deposit', l: 'dep' }, { key: 'cmd_drops', l: 'drops' },
@@ -1321,7 +1411,7 @@ async function start(apiKey, apiUrl) {
1321
1411
 
1322
1412
  console.log(colorBanner());
1323
1413
  console.log(
1324
- ` ${rgb(139, 92, 246)}v4.5${c.reset}` +
1414
+ ` ${rgb(139, 92, 246)}v4.8${c.reset}` +
1325
1415
  ` ${c.dim}·${c.reset} ${c.white}30 Commands${c.reset}` +
1326
1416
  ` ${c.dim}·${c.reset} ${rgb(34, 211, 238)}Priority Queue${c.reset}` +
1327
1417
  ` ${c.dim}·${c.reset} ${rgb(52, 211, 153)}Redis Cooldowns${c.reset}` +
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dankgrinder",
3
- "version": "4.5.1",
3
+ "version": "4.8.0",
4
4
  "description": "Dank Memer automation engine — grind coins while you sleep",
5
5
  "bin": {
6
6
  "dankgrinder": "bin/dankgrinder.js"