dankgrinder 6.8.2 → 6.16.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,7 @@
1
1
  /**
2
2
  * Blackjack command handler.
3
- * Smart hit/stand: follows basic strategy based on player total and dealer upcard.
3
+ * Full basic strategy: hit, stand, double down, surrender.
4
+ * Fast button clicks with retry on failure.
4
5
  */
5
6
 
6
7
  const {
@@ -12,19 +13,16 @@ const RE_BACKTICK_SCORE = /`\s*(\d+)\s*`/;
12
13
  const RE_BJ_FACE_GLOBAL = /bjFace(\w+?):/g;
13
14
  const RE_BJ_FACE_SUFFIX = /[RB]$/;
14
15
  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_WINNINGS_LINE = /Winnings:\s*\*{0,2}\s*[⏣]\s*\*{0,2}([+-]?[\d,]+)/i;
16
17
  const RE_COMMA = /,/g;
17
18
  const COURT_RANKS = Object.freeze(['K', 'Q', 'J']);
18
19
 
19
20
  function parsePlayerTotal(msg) {
20
- // Score is in embed field: ` 18 ` (backtick-wrapped number in Player field)
21
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];
26
- if (field.name?.toLowerCase().includes('player')) {
27
- const m = field.value.match(RE_BACKTICK_SCORE);
22
+ for (const e of embeds) {
23
+ for (const f of (e.fields || [])) {
24
+ if (f.name?.toLowerCase().includes('player')) {
25
+ const m = f.value.match(RE_BACKTICK_SCORE);
28
26
  if (m) return parseInt(m[1]);
29
27
  }
30
28
  }
@@ -35,21 +33,16 @@ function parsePlayerTotal(msg) {
35
33
  }
36
34
 
37
35
  function parseDealerUpcard(msg) {
38
- // Dealer field: revealed total ` 18 ` or hidden ` ? `
39
- // Upcard from emoji: bjFaceKB=K(10), bjFaceAR=A(11), bjFace7R=7
40
36
  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];
45
- if (field.name?.toLowerCase().includes('dealer')) {
46
- const totalMatch = field.value.match(RE_BACKTICK_SCORE);
37
+ for (const e of embeds) {
38
+ for (const f of (e.fields || [])) {
39
+ if (f.name?.toLowerCase().includes('dealer')) {
40
+ const totalMatch = f.value.match(RE_BACKTICK_SCORE);
47
41
  if (totalMatch) return parseInt(totalMatch[1]);
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];
51
- if (face === 'Unknown') continue;
52
- const v = face.replace(RE_BJ_FACE_SUFFIX, '');
42
+ const faces = [...f.value.matchAll(RE_BJ_FACE_GLOBAL)];
43
+ for (const face of faces) {
44
+ const v = face[1].replace(RE_BJ_FACE_SUFFIX, '');
45
+ if (v === 'Unknown') continue;
53
46
  if (v === 'A') return 11;
54
47
  if (COURT_RANKS.includes(v)) return 10;
55
48
  const n = parseInt(v);
@@ -70,18 +63,74 @@ function parseNetCoins(text) {
70
63
  }
71
64
 
72
65
  /**
73
- * Basic strategy decision.
74
- * - Always hit on 11
75
- * - Stand on ≥ 17
76
- * - 12-16: hit if dealer shows 7+ (strong upcard), else stand
66
+ * Full basic strategy decision.
67
+ * Returns: 'hit' | 'stand' | 'double' | 'surrender'
68
+ *
69
+ * Double Down (2x bet, get exactly 1 more card):
70
+ * - Always double on 11
71
+ * - Double on 10 if dealer shows 2-9
72
+ * - Double on 9 if dealer shows 3-6
73
+ *
74
+ * Surrender:
75
+ * - 16 vs dealer 9, 10, A
76
+ * - 15 vs dealer 10
77
+ *
78
+ * Standard:
79
+ * - Always hit ≤ 8
80
+ * - Always stand ≥ 17
81
+ * - 12: hit vs dealer 2-3 or 7+, stand vs 4-6
82
+ * - 13-16: hit vs dealer 7+, stand vs 2-6
77
83
  */
78
- function shouldHit(playerTotal, dealerTotal) {
79
- if (playerTotal <= 11) return true;
80
- if (playerTotal >= 17) return false;
81
- // 12-16: dealer has strong hand (7+), we need to risk hitting
82
- if (dealerTotal >= 7) return true;
83
- // Dealer has weak hand (2-6), likely to bust stand and wait
84
- return false;
84
+ function decide(playerTotal, dealerUpcard, canDouble, canSurrender) {
85
+ // Double down opportunities (best +EV plays in blackjack)
86
+ if (canDouble) {
87
+ if (playerTotal === 11) return 'double';
88
+ if (playerTotal === 10 && dealerUpcard >= 2 && dealerUpcard <= 9) return 'double';
89
+ if (playerTotal === 9 && dealerUpcard >= 3 && dealerUpcard <= 6) return 'double';
90
+ }
91
+
92
+ // Surrender (cut losses on worst hands)
93
+ if (canSurrender) {
94
+ if (playerTotal === 16 && (dealerUpcard >= 9 || dealerUpcard === 11)) return 'surrender';
95
+ if (playerTotal === 15 && dealerUpcard === 10) return 'surrender';
96
+ }
97
+
98
+ // Always hit soft hands (≤ 8 can't bust)
99
+ if (playerTotal <= 8) return 'hit';
100
+
101
+ // Always stand on 17+
102
+ if (playerTotal >= 17) return 'stand';
103
+
104
+ // 12: tricky — hit vs 2, 3, 7+ ; stand vs 4-6
105
+ if (playerTotal === 12) {
106
+ return (dealerUpcard <= 3 || dealerUpcard >= 7) ? 'hit' : 'stand';
107
+ }
108
+
109
+ // 13-16: hit vs dealer 7+, stand vs 2-6
110
+ if (playerTotal >= 13 && playerTotal <= 16) {
111
+ return dealerUpcard >= 7 ? 'hit' : 'stand';
112
+ }
113
+
114
+ // 9-11 without double: hit
115
+ return 'hit';
116
+ }
117
+
118
+ // Fast button click with retry
119
+ async function clickFast(msg, btn, retries = 2) {
120
+ for (let attempt = 0; attempt <= retries; attempt++) {
121
+ try {
122
+ await sleep(50 + Math.random() * 100); // 50-150ms (fast)
123
+ const result = await safeClickButton(msg, btn);
124
+ if (result) return result;
125
+ } catch (e) {
126
+ if (attempt === retries) {
127
+ LOG.error(`[bj] Click failed after ${retries + 1} attempts: ${e.message}`);
128
+ return null;
129
+ }
130
+ await sleep(200);
131
+ }
132
+ }
133
+ return null;
85
134
  }
86
135
 
87
136
  async function runBlackjack({ channel, waitForDankMemer, betAmount = 5000 }) {
@@ -89,7 +138,7 @@ async function runBlackjack({ channel, waitForDankMemer, betAmount = 5000 }) {
89
138
  LOG.cmd(`${c.white}${c.bold}${cmd}${c.reset}`);
90
139
 
91
140
  await channel.send(cmd);
92
- let current = await waitForDankMemer(10000);
141
+ let current = await waitForDankMemer(12000);
93
142
 
94
143
  if (!current) {
95
144
  LOG.warn('[bj] No response');
@@ -100,7 +149,7 @@ async function runBlackjack({ channel, waitForDankMemer, betAmount = 5000 }) {
100
149
 
101
150
  if (isHoldTight(current)) {
102
151
  const reason = getHoldTightReason(current);
103
- LOG.warn(`[bj] Hold Tight${reason ? ` (reason: /${reason})` : ''} — waiting 30s`);
152
+ LOG.warn(`[bj] Hold Tight${reason ? ` (/${reason})` : ''} — waiting 30s`);
104
153
  await sleep(30000);
105
154
  return { result: `hold tight (${reason || 'unknown'})`, coins: 0, holdTightReason: reason };
106
155
  }
@@ -110,34 +159,44 @@ async function runBlackjack({ channel, waitForDankMemer, betAmount = 5000 }) {
110
159
  const MAX_ROUNDS = 10;
111
160
  for (let i = 0; i < MAX_ROUNDS; i++) {
112
161
  const buttons = getAllButtons(current).filter(b => b.label && b.label !== 'null');
162
+
113
163
  const hitBtn = buttons.find(b => !b.disabled && (b.label || '').toLowerCase().includes('hit'));
114
164
  const standBtn = buttons.find(b => !b.disabled && (b.label || '').toLowerCase().includes('stand'));
165
+ const doubleBtn = buttons.find(b => !b.disabled && (b.label || '').toLowerCase().includes('double'));
166
+ const surrenderBtn = buttons.find(b => !b.disabled && (b.label || '').toLowerCase().includes('surrender'));
167
+
168
+ // No action buttons = game over
115
169
  if (!hitBtn && !standBtn) break;
116
170
 
117
171
  const playerTotal = parsePlayerTotal(current);
118
172
  const dealerUpcard = parseDealerUpcard(current);
173
+ const canDouble = !!doubleBtn;
174
+ const canSurrender = !!surrenderBtn;
175
+
176
+ const action = playerTotal === 0
177
+ ? 'stand' // can't parse score, play safe
178
+ : decide(playerTotal, dealerUpcard, canDouble, canSurrender);
119
179
 
120
180
  let targetBtn;
121
- if (playerTotal === 0) {
122
- targetBtn = standBtn || hitBtn;
123
- } else if (shouldHit(playerTotal, dealerUpcard)) {
124
- targetBtn = hitBtn || standBtn;
125
- } else {
126
- targetBtn = standBtn || hitBtn;
181
+ switch (action) {
182
+ case 'double': targetBtn = doubleBtn || hitBtn; break;
183
+ case 'surrender': targetBtn = surrenderBtn || standBtn; break;
184
+ case 'hit': targetBtn = hitBtn || standBtn; break;
185
+ case 'stand': targetBtn = standBtn || hitBtn; break;
186
+ default: targetBtn = standBtn || hitBtn;
127
187
  }
128
188
 
129
189
  if (!targetBtn) break;
130
190
 
131
- LOG.info(`[bj] You:${playerTotal} Dealer:${dealerUpcard} → ${targetBtn.label}`);
132
- await humanDelay(150, 400);
191
+ LOG.info(`[bj] You:${playerTotal} Dealer:${dealerUpcard} → ${action.toUpperCase()} (${targetBtn.label})`);
133
192
 
134
- try {
135
- const followUp = await safeClickButton(current, targetBtn);
136
- if (followUp) {
137
- current = followUp;
138
- logMsg(current, `bj-round-${i}`);
139
- } else break;
140
- } catch { break; }
193
+ const followUp = await clickFast(current, targetBtn);
194
+ if (followUp) {
195
+ current = followUp;
196
+ logMsg(current, `bj-round-${i}`);
197
+ } else {
198
+ break;
199
+ }
141
200
  }
142
201
 
143
202
  const finalText = getFullText(current);
@@ -154,7 +213,7 @@ async function runBlackjack({ channel, waitForDankMemer, betAmount = 5000 }) {
154
213
  }
155
214
  if (lower.includes('won') || lower.includes('beat')) {
156
215
  const coins = parseCoins(finalText);
157
- return { result: `bj → ${c.green}won${c.reset}`, coins };
216
+ return { result: `bj → won`, coins };
158
217
  }
159
218
  if (lower.includes('lost') || lower.includes('bust') || lower.includes('lower score')) {
160
219
  return { result: `bj → lost`, coins: 0, lost: betAmount };
@@ -126,6 +126,14 @@ async function runCrime({ channel, waitForDankMemer, safeAnswers }) {
126
126
  const crimeKey = (btn.label || '').toLowerCase();
127
127
  const prev = crimeEarnings.get(crimeKey) || 0;
128
128
  crimeEarnings.set(crimeKey, prev + coins);
129
+
130
+ // Check for death
131
+ const textLower = text.toLowerCase();
132
+ if (textLower.includes('you died') || textLower.includes('lifesaver protected')) {
133
+ LOG.error(`[crime] DEATH DETECTED! Lifesaver may have been used.`);
134
+ return { result: `you died during crime`, coins: 0, died: true };
135
+ }
136
+
129
137
  if (coins > 0) {
130
138
  LOG.coin(`[crime] ${btn.label} → ${c.green}+⏣ ${coins.toLocaleString()}${c.reset}`);
131
139
  return { result: `${btn.label} → +⏣ ${coins.toLocaleString()}`, coins };
@@ -17,6 +17,7 @@
17
17
  const {
18
18
  LOG, c, getFullText, parseCoins, getAllButtons, safeClickButton,
19
19
  logMsg, isHoldTight, getHoldTightReason, sleep, humanDelay,
20
+ isCV2, ensureCV2,
20
21
  } = require('./utils');
21
22
  const { downloadImage, extractImageUrl, findSafeCells } = require('./fishVision');
22
23
 
@@ -328,6 +329,9 @@ async function runFish({ channel, waitForDankMemer }) {
328
329
  return { result: `fish cooldown (${cd}s)`, coins: 0, nextCooldownSec: cd };
329
330
  }
330
331
 
332
+ // Ensure CV2 buttons are hydrated from raw gateway
333
+ if (isCV2(response)) await ensureCV2(response);
334
+
331
335
  // Re-fetch for hydrated CV2 components
332
336
  const msgId = response.id;
333
337
  const fresh = await refetchMsg(channel, msgId);
@@ -396,6 +400,9 @@ async function runFishLoop({ channel, waitForDankMemer, maxRounds = 50, onRound,
396
400
  return { totalRounds: 0, totalCaught: 0, totalMines: 0, results: [] };
397
401
  }
398
402
 
403
+ // Ensure CV2 buttons are hydrated
404
+ if (isCV2(response)) await ensureCV2(response);
405
+
399
406
  // Refetch for hydrated components
400
407
  let msgId = response.id;
401
408
  const fresh = await refetchMsg(channel, msgId);
@@ -2,11 +2,11 @@
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
+ * All commands use:
6
+ * - Fast button clicks (50-150ms) with retry (2 attempts)
7
+ * - Kelly Criterion bet sizing
8
+ * - EMA win-rate tracking
9
+ * - Raw gateway data via rawLogger for CV2 buttons
10
10
  */
11
11
 
12
12
  const {
@@ -15,8 +15,7 @@ const {
15
15
  } = require('./utils');
16
16
  const { EMA, AhoCorasick, SlidingWindowCounter } = require('../structures');
17
17
 
18
- // ── Per-game EMA win-rate tracker ────────────────────────────
19
- // α=0.2 gives recent results ~5x more weight than old ones
18
+ // ── Per-game EMA win-rate tracker ──
20
19
  const winRateEMA = {
21
20
  cointoss: new EMA(0.2),
22
21
  roulette: new EMA(0.2),
@@ -24,7 +23,6 @@ const winRateEMA = {
24
23
  snakeeyes: new EMA(0.2),
25
24
  };
26
25
 
27
- // ── Sliding window: wins in last 5 minutes per game ──────────
28
26
  const winWindow = {
29
27
  cointoss: new SlidingWindowCounter(300000),
30
28
  roulette: new SlidingWindowCounter(300000),
@@ -38,12 +36,6 @@ const totalWindow = {
38
36
  snakeeyes: new SlidingWindowCounter(300000),
39
37
  };
40
38
 
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
39
  function kellyFraction(winProb, odds = 1.0) {
48
40
  if (winProb <= 0 || winProb >= 1) return 0.1;
49
41
  const q = 1 - winProb;
@@ -51,8 +43,7 @@ function kellyFraction(winProb, odds = 1.0) {
51
43
  return Math.max(0, Math.min(f, 0.25));
52
44
  }
53
45
 
54
- // ── Aho-Corasick for min-bet detection ───────────────────────
55
- // Single O(n) pass instead of 3 separate .includes() calls
46
+ // ── Min-bet detection ──
56
47
  const minBetDetector = new AhoCorasick();
57
48
  minBetDetector.addPattern("can't bet less than", 'minbet');
58
49
  minBetDetector.addPattern('cannot bet less than', 'minbet');
@@ -126,14 +117,32 @@ async function commonChecks(response, cmdName) {
126
117
  return { skip: false };
127
118
  }
128
119
 
120
+ // ── Fast click with retry ──
121
+ async function clickFast(msg, btn, retries = 2) {
122
+ for (let attempt = 0; attempt <= retries; attempt++) {
123
+ try {
124
+ await sleep(50 + Math.random() * 100); // 50-150ms
125
+ const result = await safeClickButton(msg, btn);
126
+ if (result) return result;
127
+ } catch (e) {
128
+ if (attempt === retries) {
129
+ LOG.error(`Click failed after ${retries + 1} tries: ${e.message}`);
130
+ return null;
131
+ }
132
+ await sleep(200);
133
+ }
134
+ }
135
+ return null;
136
+ }
137
+
129
138
  /**
130
- * Cointoss — CV2 format. Fetch raw components, pick Heads/Tails randomly.
139
+ * Cointoss — CV2 format. Pick Heads/Tails randomly.
131
140
  */
132
141
  async function runCointoss({ channel, waitForDankMemer, betAmount = 10000 }) {
133
142
  const cmd = `pls cointoss ${betAmount}`;
134
143
  LOG.cmd(`${c.white}${c.bold}${cmd}${c.reset}`);
135
144
  await channel.send(cmd);
136
- let response = await waitForDankMemer(10000);
145
+ let response = await waitForDankMemer(12000);
137
146
 
138
147
  await ensureCV2(response);
139
148
  const chk = await commonChecks(response, 'cointoss');
@@ -148,28 +157,27 @@ async function runCointoss({ channel, waitForDankMemer, betAmount = 10000 }) {
148
157
  const choice = Math.random() < 0.5 ? 'head' : 'tail';
149
158
  const btn = btns.find(b => (b.label || '').toLowerCase().includes(choice)) || btns[0];
150
159
  LOG.info(`[cointoss] Picking "${btn.label}"`);
151
- await humanDelay(80, 200);
152
- try {
153
- const followUp = await safeClickButton(response, btn);
154
- if (followUp) {
155
- await ensureCV2(followUp);
156
- return parseResult(getFullText(followUp), 'cointoss');
157
- }
158
- await sleep(1500);
159
- response._cv2 = null;
160
- response._cv2text = null;
161
- response._cv2buttons = null;
162
- await ensureCV2(response);
163
- return parseResult(getFullText(response), 'cointoss');
164
- } catch (e) { LOG.error(`[cointoss] Click error: ${e.message}`); }
160
+
161
+ const followUp = await clickFast(response, btn);
162
+ if (followUp) {
163
+ await ensureCV2(followUp);
164
+ return parseResult(getFullText(followUp), 'cointoss');
165
+ }
166
+ // Retry: re-fetch CV2 data from raw gateway
167
+ await sleep(1500);
168
+ response._cv2 = null;
169
+ response._cv2text = null;
170
+ response._cv2buttons = null;
171
+ await ensureCV2(response, true);
172
+ return parseResult(getFullText(response), 'cointoss');
165
173
  }
166
174
 
167
175
  return parseResult(getFullText(response), 'cointoss');
168
176
  }
169
177
 
170
178
  /**
171
- * Roulette — random color: red ~47.5%, black ~47.5%, green ~5% (rare).
172
- * Sometimes auto-bets if color is in command, sometimes shows buttons.
179
+ * Roulette — pick color, click button, wait for result.
180
+ * Strategy: ~47.5% red, ~47.5% black, ~5% green.
173
181
  */
174
182
  async function runRoulette({ channel, waitForDankMemer, betAmount = 10000 }) {
175
183
  const roll = Math.random();
@@ -177,32 +185,31 @@ async function runRoulette({ channel, waitForDankMemer, betAmount = 10000 }) {
177
185
  const cmd = `pls roulette ${betAmount} ${color}`;
178
186
  LOG.cmd(`${c.white}${c.bold}${cmd}${c.reset}`);
179
187
  await channel.send(cmd);
180
- let response = await waitForDankMemer(10000);
188
+ let response = await waitForDankMemer(12000);
181
189
 
182
190
  const chk = await commonChecks(response, 'roulette');
183
191
  if (chk.skip) return chk.ret;
184
192
 
185
193
  logMsg(response, 'roulette');
186
194
 
195
+ // Click the color button
187
196
  const btns = gameButtons(response).filter(b =>
188
197
  !(b.customId || b.custom_id || '').includes('bet')
189
198
  );
190
199
  const colorBtn = btns.find(b => (b.label || '').toLowerCase() === color);
191
200
  if (colorBtn) {
192
201
  LOG.info(`[roulette] Picking "${colorBtn.label}"`);
193
- await humanDelay(100, 300);
194
- try {
195
- const followUp = await safeClickButton(response, colorBtn);
196
- if (followUp) {
197
- response = followUp;
198
- logMsg(response, 'roulette-after-pick');
199
- }
200
- } catch (e) { LOG.error(`[roulette] Click error: ${e.message}`); }
202
+ const followUp = await clickFast(response, colorBtn);
203
+ if (followUp) {
204
+ response = followUp;
205
+ logMsg(response, 'roulette-after-pick');
206
+ }
201
207
  }
202
208
 
209
+ // Wait for result (roulette animates)
203
210
  let text = getFullText(response);
204
211
  if (text.toLowerCase().includes('spinning') || text.toLowerCase().includes('rolling') || text.toLowerCase().includes('pick a color')) {
205
- await sleep(2000);
212
+ await sleep(2500);
206
213
  try {
207
214
  const final = await channel.messages.fetch(response.id);
208
215
  if (final) text = getFullText(final);
@@ -213,25 +220,30 @@ async function runRoulette({ channel, waitForDankMemer, betAmount = 10000 }) {
213
220
  }
214
221
 
215
222
  /**
216
- * Slots — embed format. Auto-spins, no button needed. Wait for animation.
223
+ * Slots — auto-spin, wait for animation to finish, parse result.
217
224
  */
218
225
  async function runSlots({ channel, waitForDankMemer, betAmount = 10000 }) {
219
226
  const cmd = `pls slots ${betAmount}`;
220
227
  LOG.cmd(`${c.white}${c.bold}${cmd}${c.reset}`);
221
228
  await channel.send(cmd);
222
- let response = await waitForDankMemer(10000);
229
+ let response = await waitForDankMemer(12000);
223
230
 
224
231
  const chk = await commonChecks(response, 'slots');
225
232
  if (chk.skip) return chk.ret;
226
233
 
227
234
  logMsg(response, 'slots');
228
- const text = getFullText(response);
235
+ let text = getFullText(response);
229
236
 
237
+ // Slots animate — wait for the UPDATE with final result
230
238
  if (text.toLowerCase().includes('spinning')) {
231
- await sleep(2000);
239
+ // Wait for message update event (animation takes ~2s)
240
+ await sleep(2500);
232
241
  try {
233
242
  const final = await channel.messages.fetch(response.id);
234
- if (final) return parseResult(getFullText(final), 'slots');
243
+ if (final) {
244
+ text = getFullText(final);
245
+ logMsg(final, 'slots-final');
246
+ }
235
247
  } catch {}
236
248
  }
237
249
 
@@ -239,25 +251,29 @@ async function runSlots({ channel, waitForDankMemer, betAmount = 10000 }) {
239
251
  }
240
252
 
241
253
  /**
242
- * Snakeeyes — embed format. Auto-rolls, no button needed. Wait for animation.
254
+ * Snakeeyes — auto-roll, wait for animation to finish, parse result.
243
255
  */
244
256
  async function runSnakeeyes({ channel, waitForDankMemer, betAmount = 10000 }) {
245
257
  const cmd = `pls snakeeyes ${betAmount}`;
246
258
  LOG.cmd(`${c.white}${c.bold}${cmd}${c.reset}`);
247
259
  await channel.send(cmd);
248
- let response = await waitForDankMemer(10000);
260
+ let response = await waitForDankMemer(12000);
249
261
 
250
262
  const chk = await commonChecks(response, 'snakeeyes');
251
263
  if (chk.skip) return chk.ret;
252
264
 
253
265
  logMsg(response, 'snakeeyes');
254
- const text = getFullText(response);
266
+ let text = getFullText(response);
255
267
 
268
+ // Snakeeyes animate — wait for final
256
269
  if (text.toLowerCase().includes('rolling')) {
257
- await sleep(2000);
270
+ await sleep(2500);
258
271
  try {
259
272
  const final = await channel.messages.fetch(response.id);
260
- if (final) return parseResult(getFullText(final), 'snakeeyes');
273
+ if (final) {
274
+ text = getFullText(final);
275
+ logMsg(final, 'snakeeyes-final');
276
+ }
261
277
  } catch {}
262
278
  }
263
279