dankgrinder 4.5.0 → 4.6.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.
@@ -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
 
@@ -4,20 +4,10 @@
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
- */
21
11
  async function runGamble({ channel, waitForDankMemer, cmdName, cmdString }) {
22
12
  LOG.cmd(`${c.white}${c.bold}${cmdString}${c.reset}`);
23
13
 
@@ -38,9 +28,8 @@ async function runGamble({ channel, waitForDankMemer, cmdName, cmdString }) {
38
28
 
39
29
  logMsg(response, cmdName);
40
30
  const text = getFullText(response);
41
- const coins = parseCoins(text);
42
31
 
43
- // Some gambles have buttons (e.g. pick heads/tails)
32
+ // Some gambles have buttons (coinflip: pick heads/tails)
44
33
  const buttons = getAllButtons(response);
45
34
  if (buttons.length > 0) {
46
35
  const btn = buttons.find(b => !b.disabled);
@@ -52,34 +41,39 @@ async function runGamble({ channel, waitForDankMemer, cmdName, cmdString }) {
52
41
  if (followUp) {
53
42
  logMsg(followUp, `${cmdName}-result`);
54
43
  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 };
44
+ return parseGambleResult(fText, cmdName);
62
45
  }
63
46
  } catch (e) { LOG.error(`[${cmdName}] Click error: ${e.message}`); }
64
47
  }
65
48
  }
66
49
 
50
+ return parseGambleResult(text, cmdName);
51
+ }
52
+
53
+ function parseGambleResult(text, cmdName) {
54
+ const net = parseNetCoins(text);
55
+ const lower = text.toLowerCase();
56
+
57
+ if (net > 0) {
58
+ LOG.coin(`[${cmdName}] ${c.green}+⏣ ${net.toLocaleString()}${c.reset}`);
59
+ return { result: `${cmdName} → +⏣ ${net.toLocaleString()}`, coins: net };
60
+ }
61
+ if (net < 0) {
62
+ LOG.warn(`[${cmdName}] ${c.red}-⏣ ${Math.abs(net).toLocaleString()}${c.reset}`);
63
+ return { result: `${cmdName} → -⏣ ${Math.abs(net).toLocaleString()}`, coins: 0, lost: Math.abs(net) };
64
+ }
65
+ // Fallback to parseCoins
66
+ const coins = parseCoins(text);
67
67
  if (coins > 0) {
68
68
  LOG.coin(`[${cmdName}] ${c.green}+⏣ ${coins.toLocaleString()}${c.reset}`);
69
69
  return { result: `${cmdName} → +⏣ ${coins.toLocaleString()}`, coins };
70
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
- }
71
+ if (lower.includes('won') || lower.includes('beat')) return { result: `${cmdName} → won`, coins: 0 };
72
+ if (lower.includes('lost') || lower.includes('bust')) return { result: `${cmdName} → lost`, coins: 0, lost: coins };
78
73
 
79
74
  return { result: `${cmdName} done`, coins: 0 };
80
75
  }
81
76
 
82
- // Convenience wrappers
83
77
  async function runCoinflip({ channel, waitForDankMemer, betAmount = 5000 }) {
84
78
  return runGamble({ channel, waitForDankMemer, cmdName: 'coinflip', cmdString: `pls coinflip ${betAmount} heads` });
85
79
  }
@@ -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 };
@@ -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;
@@ -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
@@ -1,7 +1,7 @@
1
1
  const { Client } = require('discord.js-selfbot-v13');
2
2
  const Redis = require('ioredis');
3
3
  const commands = require('./commands');
4
- const { setDashboardActive, getFullText } = require('./commands/utils');
4
+ const { setDashboardActive } = require('./commands/utils');
5
5
 
6
6
  // ── Terminal Colors & ANSI ───────────────────────────────────
7
7
  const c = {
@@ -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
  }
@@ -825,6 +848,15 @@ class AccountWorker {
825
848
  return;
826
849
  }
827
850
 
851
+ // PostMemes / command-specific cooldown from response
852
+ if (resultLower.includes('cannot post another meme') || resultLower.includes('dead meme')) {
853
+ const minMatch = result.match(/(\d+)\s*minute/i);
854
+ const cdSec = minMatch ? parseInt(minMatch[1]) * 60 : 120;
855
+ this.log('warn', `${cmdName} on cooldown: ${cdSec}s`);
856
+ await this.setCooldown(cmdName, cdSec);
857
+ return;
858
+ }
859
+
828
860
  // Captcha/verification detection — deactivate account and stop
829
861
  if (resultLower.includes('captcha') || resultLower.includes('verification required') ||
830
862
  resultLower.includes('verify your account') || resultLower.includes('pass verification') ||
@@ -867,7 +899,7 @@ class AccountWorker {
867
899
  return;
868
900
  }
869
901
 
870
- // Already claimed today (daily/weekly) — set long cooldown
902
+ // Already claimed today (daily/weekly) — set long cooldown + mark in Redis
871
903
  if (resultLower.includes('already got your daily') || resultLower.includes('try again <t:')) {
872
904
  this.log('info', `${cmdName} already claimed — waiting`);
873
905
  const timeMatch = result.match(/<t:(\d+):R>/);
@@ -875,8 +907,11 @@ class AccountWorker {
875
907
  const nextAvail = parseInt(timeMatch[1]) * 1000;
876
908
  const waitSec = Math.max(60, Math.ceil((nextAvail - Date.now()) / 1000));
877
909
  await this.setCooldown(cmdName, waitSec);
910
+ if (redis) try { await redis.set(`dkg:done:${this.account.id}:${cmdName}`, '1', 'EX', waitSec); } catch {}
878
911
  } else {
879
- await this.setCooldown(cmdName, cmdName === 'daily' ? 86400 : 604800);
912
+ const fallbackSec = cmdName === 'daily' ? 86400 : 604800;
913
+ await this.setCooldown(cmdName, fallbackSec);
914
+ if (redis) try { await redis.set(`dkg:done:${this.account.id}:${cmdName}`, '1', 'EX', fallbackSec); } catch {}
880
915
  }
881
916
  return;
882
917
  }
@@ -893,6 +928,14 @@ class AccountWorker {
893
928
  if (earned > 0) this.stats.coins += earned;
894
929
  if (cmdResult.nextCooldownSec) await this.setCooldown(cmdName, cmdResult.nextCooldownSec);
895
930
 
931
+ // Mark daily/drops as done in Redis so we don't re-run this session
932
+ if (redis && earned > 0 && cmdName === 'daily') {
933
+ try { await redis.set(`dkg:done:${this.account.id}:daily`, '1', 'EX', 86400); } catch {}
934
+ }
935
+ if (redis && cmdName === 'drops') {
936
+ try { await redis.set(`dkg:done:${this.account.id}:drops`, '1', 'EX', 86400); } catch {}
937
+ }
938
+
896
939
  if (cmdResult.holdTightReason) {
897
940
  const reason = cmdResult.holdTightReason;
898
941
  this.log('warn', `Hold Tight: /${reason} — 35s cooldown`);
@@ -1104,6 +1147,19 @@ class AccountWorker {
1104
1147
  return;
1105
1148
  }
1106
1149
 
1150
+ // Skip daily/drops if already done today (Redis marker)
1151
+ if (redis && (item.cmd === 'daily' || item.cmd === 'drops')) {
1152
+ try {
1153
+ const done = await redis.get(`dkg:done:${this.account.id}:${item.cmd}`);
1154
+ if (done) {
1155
+ item.nextRunAt = now + 86400 * 1000;
1156
+ if (this.commandQueue) this.commandQueue.push(item);
1157
+ this.tickTimeout = setTimeout(() => this.tick(), 100);
1158
+ return;
1159
+ }
1160
+ } catch {}
1161
+ }
1162
+
1107
1163
  this.busy = true;
1108
1164
  const cd = (this.account[item.info.cdKey] || item.info.defaultCd);
1109
1165
  const jitter = 1 + Math.random() * 3;
@@ -1321,7 +1377,7 @@ async function start(apiKey, apiUrl) {
1321
1377
 
1322
1378
  console.log(colorBanner());
1323
1379
  console.log(
1324
- ` ${rgb(139, 92, 246)}v4.5${c.reset}` +
1380
+ ` ${rgb(139, 92, 246)}v4.6${c.reset}` +
1325
1381
  ` ${c.dim}·${c.reset} ${c.white}30 Commands${c.reset}` +
1326
1382
  ` ${c.dim}·${c.reset} ${rgb(34, 211, 238)}Priority Queue${c.reset}` +
1327
1383
  ` ${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.0",
3
+ "version": "4.6.0",
4
4
  "description": "Dank Memer automation engine — grind coins while you sleep",
5
5
  "bin": {
6
6
  "dankgrinder": "bin/dankgrinder.js"