dankgrinder 6.27.0 → 6.34.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.
Files changed (2) hide show
  1. package/lib/commands/farm.js +127 -47
  2. package/package.json +1 -1
@@ -84,22 +84,8 @@ function parseFarmGrowReadySec(text) {
84
84
  const clean = String(stripAnsi(text || ''));
85
85
  const lower = clean.toLowerCase();
86
86
 
87
- // TEMP DEBUG: log what we're parsing
88
- const hasHarvestReady = /ready to harvest|harvest ready|can be harvested/i.test(lower);
89
- const hasWilt = /\bwilt/i.test(lower);
90
- if (hasHarvestReady || hasWilt) {
91
- // eslint-disable-next-line no-console
92
- console.log('[parseFarmGrowReadySec DEBUG] hasHarvestReady=' + hasHarvestReady + ' hasWilt=' + hasWilt + ' clean=' + clean.slice(0, 300));
93
- }
94
-
95
87
  // If crops are already harvestable, don't queue waiting.
96
- if (/ready to harvest|harvest ready|can be harvested|wilt/i.test(lower)) {
97
- // eslint-disable-next-line no-console
98
- console.log('[parseFarmGrowReadySec] MATCH — returning 0 (lower=' + lower.slice(0, 200) + ')');
99
- return 0;
100
- }
101
- // eslint-disable-next-line no-console
102
- console.log('[parseFarmGrowReadySec] NO MATCH — checking timestamp parse (lower=' + lower.slice(0, 200) + ')');
88
+ if (/ready to harvest|harvest ready|can be harvested|wilts?/i.test(lower)) return 0;
103
89
 
104
90
  // If the farm is empty / shows plantable / needs tilling, no grow queue needed.
105
91
  // This handles the post-harvest state where Dank Memer shows
@@ -164,6 +150,11 @@ const FARM_BUY_ALIASES = Object.freeze({
164
150
 
165
151
  const FARM_TOTAL_SLOTS = 9;
166
152
  const FARM_PHASE_ORDER = ['hoe', 'water', 'plant', 'harvest'];
153
+ // Seed preference: random among potato/beans group, else carrot, else highest-qty fallback
154
+ const SEED_PRIORITY_GROUPS = [
155
+ ['potato', 'beans', 'bean'], // random pick when available
156
+ ['carrot'], // fallback
157
+ ];
167
158
 
168
159
  function parseMissingFarmItem(text) {
169
160
  const lower = String(stripAnsi(text || '')).toLowerCase();
@@ -556,12 +547,22 @@ function chooseSeedOption(menu) {
556
547
  const withQty = options.map(o => {
557
548
  const label = String(o.label || '');
558
549
  const m = label.match(/\((\d{1,5})\)/);
559
- return { option: o, qty: m ? parseInt(m[1], 10) : 0, isDefault: !!o.default };
550
+ return { option: o, qty: m ? parseInt(m[1], 10) : 0, label };
560
551
  });
561
552
 
562
- const defaultOpt = withQty.find(x => x.isDefault && x.qty > 0);
563
- if (defaultOpt) return defaultOpt.option;
553
+ // Try each priority group in order
554
+ for (const group of SEED_PRIORITY_GROUPS) {
555
+ const matches = withQty.filter(x =>
556
+ x.qty > 0 && group.some(s => x.label.toLowerCase().includes(s))
557
+ );
558
+ if (matches.length > 0) {
559
+ const pick = matches[Math.floor(Math.random() * matches.length)];
560
+ LOG.info(`[farm:plant] seed group=[${group.join('/')}] picked="${pick.label}" qty=${pick.qty}`);
561
+ return pick.option;
562
+ }
563
+ }
564
564
 
565
+ // Fallback: highest qty
565
566
  const best = withQty.sort((a, b) => b.qty - a.qty)[0];
566
567
  return best?.option || options[0];
567
568
  }
@@ -1058,7 +1059,7 @@ async function waitForEditedMessage(channel, messageId, baselineText, timeoutMs
1058
1059
  // Determine the next action to take from the manage menu, given what the
1059
1060
  // farm image vision says about the current state.
1060
1061
  // Returns { action, button, reason } where action is null when the cycle is done.
1061
- async function findNextFarmActionFromManage(msg, text, currentAction, imageAnalysis) {
1062
+ async function findNextFarmActionFromManage(msg, text, currentAction, imageAnalysis, forcedNextAction) {
1062
1063
  const btns = getAllButtons(msg).filter(b => !b.disabled && !isNavOrUtilityButton(b));
1063
1064
  const allBtns = getAllButtons(msg); // include disabled for state detection
1064
1065
  if (btns.length === 0) return { action: null, button: null, reason: 'no-buttons' };
@@ -1066,6 +1067,20 @@ async function findNextFarmActionFromManage(msg, text, currentAction, imageAnaly
1066
1067
  const managedActions = getManageActionButtons(msg);
1067
1068
  const lower = String(stripAnsi(text || '')).toLowerCase();
1068
1069
 
1070
+ // If we were explicitly told to try a specific action (e.g., advancing from hoe→water),
1071
+ // skip the empty-farm default. If that action's button is also disabled,
1072
+ // cascade through the rest of the phase order.
1073
+ if (forcedNextAction) {
1074
+ const phaseOrder = FARM_PHASE_ORDER;
1075
+ const forcedIdx = phaseOrder.indexOf(forcedNextAction);
1076
+ // Try forced action first, then cascade through remaining phases
1077
+ for (let i = forcedIdx; i < phaseOrder.length; i++) {
1078
+ const phase = phaseOrder[i];
1079
+ const btn = managedActions[phase] || btns.find(b => hasAny(b, [phase]));
1080
+ if (btn) return { action: phase, button: btn, reason: `forced(${forcedNextAction}→${phase})` };
1081
+ }
1082
+ }
1083
+
1069
1084
  // If farm is empty, always start with hoe.
1070
1085
  if (/seems pretty empty|pretty empty|empty\.{0,3}/i.test(lower)) {
1071
1086
  const btn = managedActions.hoe || btns.find(b => hasAny(b, ['hoe', 'till']));
@@ -1142,7 +1157,7 @@ async function findNextFarmActionFromManage(msg, text, currentAction, imageAnaly
1142
1157
  async function advancePastConfirmation(response, waitForDankMemer) {
1143
1158
  if (!response) return null;
1144
1159
 
1145
- const CONFIRM_WORDS = ['continue', 'confirm', 'confirm all', 'done', 'back to farm', 'close'];
1160
+ const CONFIRM_WORDS = ['continue', 'confirm', 'confirm all', 'done', 'back to farm', 'back', 'close'];
1146
1161
  const MAX_PAGES = 3;
1147
1162
 
1148
1163
  for (let page = 0; page < MAX_PAGES; page++) {
@@ -1212,9 +1227,6 @@ async function runFarm({ channel, waitForDankMemer, client, redis, accountId })
1212
1227
  await channel.send('pls farm view');
1213
1228
  let response = await waitForDankMemer(12000);
1214
1229
 
1215
- // eslint-disable-next-line no-console
1216
- console.log('[farm] runFarm entered — response=' + (response ? 'got-msg' : 'null'));
1217
-
1218
1230
  if (!response) {
1219
1231
  LOG.warn('[farm] No response');
1220
1232
  return { result: 'no response', coins: 0, nextCooldownSec: 90 };
@@ -1261,7 +1273,7 @@ async function runFarm({ channel, waitForDankMemer, client, redis, accountId })
1261
1273
  const redisRecoveryMs = await getHarvestRecoveryMs(redis, accountId);
1262
1274
  const redisRecovered = redisRecoveryMs === 0;
1263
1275
  const growReadySec = parseFarmGrowReadySec(text);
1264
- const textHasHarvestReady = /ready to harvest|harvest ready|can be harvested|wilt/i.test(lower);
1276
+ const textHasHarvestReady = /ready to harvest|harvest ready|can be harvested|wilts?/i.test(lower);
1265
1277
  if (textHasHarvestReady) LOG.info(`[farm] DETECTED harvest-ready in text (growReadySec=${growReadySec})`);
1266
1278
  if (growReadySec > 0) LOG.info(`[farm] GROW-QUEUE DEBUG: lower=${lower.slice(0, 400)}`);
1267
1279
  LOG.info(`[farm] grow-ready parse=${growReadySec == null ? 'none' : `${growReadySec}s`} redis_recovery=${redisRecoveryMs != null ? `${redisRecoveryMs}ms` : 'n/a'}`);
@@ -1275,8 +1287,6 @@ async function runFarm({ channel, waitForDankMemer, client, redis, accountId })
1275
1287
  }
1276
1288
 
1277
1289
  // Check for missing items on the initial farm view — buy and retry once.
1278
- // eslint-disable-next-line no-console
1279
- console.log('[farm] at vision check — text=' + brief(text, 200));
1280
1290
  const farmVision0 = analyzeFarmState({ msg: response, text });
1281
1291
  LOG.info(`[farm:vision] stage=${farmVision0.stage}${farmVision0.missing ? ` missing=${farmVision0.missing}` : ''}`);
1282
1292
  if (farmVision0.missing) {
@@ -1296,8 +1306,6 @@ async function runFarm({ channel, waitForDankMemer, client, redis, accountId })
1296
1306
  }
1297
1307
 
1298
1308
  // ── Open the manage menu ───────────────────────────────────────────────────
1299
- // eslint-disable-next-line no-console
1300
- console.log('[farm] at manage-btn check — text length=' + text.length + ' hasManage=' + (findFarmButton(response, ['manage']) ? 'yes' : 'no'));
1301
1309
  const manageBtn = findFarmButton(response, ['manage', 'farm-farm:manage']);
1302
1310
  if (manageBtn) {
1303
1311
  LOG.info('[farm] Opening Manage menu');
@@ -1330,21 +1338,19 @@ async function runFarm({ channel, waitForDankMemer, client, redis, accountId })
1330
1338
  let lastAction = null;
1331
1339
  let cycleResponse = response;
1332
1340
  let actionsTaken = 0; // Track how many farm actions were actually executed
1341
+ let forcedNextAction = null; // When advancing a phase, force the next action check
1342
+ let lastApplyResp = null; // Track the last apply response for coin/cooldown parsing
1333
1343
 
1334
1344
  while (cycleDepth < 5) {
1335
- // eslint-disable-next-line no-console
1336
- console.log('[farm] cycle iteration ' + cycleDepth + ' — cycleResponse type=' + (cycleResponse && cycleResponse.constructor ? cycleResponse.constructor.name : typeof cycleResponse));
1337
1345
  let actionResult;
1338
1346
  try {
1339
- actionResult = await findNextFarmActionFromManage(cycleResponse, text, lastAction, initialAnalysis);
1347
+ actionResult = await findNextFarmActionFromManage(cycleResponse, text, lastAction, initialAnalysis, forcedNextAction);
1340
1348
  } catch (e) {
1341
- // eslint-disable-next-line no-console
1342
- console.log('[farm] findNextFarmActionFromManage ERROR:', e.message);
1349
+ LOG.warn(`[farm:cycle] findNextFarmActionFromManage error: ${e.message}`);
1343
1350
  break;
1344
1351
  }
1345
1352
  const { action, button, reason } = actionResult;
1346
- // eslint-disable-next-line no-console
1347
- console.log('[farm] findNextFarmActionFromManage result: action=' + action + ' reason=' + reason);
1353
+ forcedNextAction = null;
1348
1354
 
1349
1355
  if (!action || !button) {
1350
1356
  LOG.info(`[farm:cycle:${cycleDepth}] no action found (reason=${reason}) — breaking cycle`);
@@ -1397,37 +1403,93 @@ async function runFarm({ channel, waitForDankMemer, client, redis, accountId })
1397
1403
  ' all', 'all ', 'all)', 'all:', ':all',
1398
1404
  'hoe all', 'water all', 'plant all', 'harvest all', 'confirm all',
1399
1405
  ]);
1406
+
1407
+ // If no enabled "All" button — check if it's disabled (farm is empty for this phase).
1408
+ // If disabled, treat as a no-op and set forcedNextAction for the next iteration.
1400
1409
  if (!allBtn) {
1410
+ const allBtnsInclDisabled = getAllButtons(cycleResponse);
1411
+ const disabledAllBtn = allBtnsInclDisabled.find(b => {
1412
+ const label = String(b.label || '').toLowerCase();
1413
+ return (label.includes('all') || label.includes('hoe all') || label.includes('water all') ||
1414
+ label.includes('plant all') || label.includes('harvest all') || label.includes('confirm')) && b.disabled;
1415
+ });
1416
+ if (disabledAllBtn) {
1417
+ const currentIdx = FARM_PHASE_ORDER.indexOf(action);
1418
+ const nextPhase = FARM_PHASE_ORDER[currentIdx + 1] || null;
1419
+ LOG.info(`[farm:cycle:${cycleDepth}] ${action} all-btn disabled (no-op) — next=${nextPhase || 'done'}`);
1420
+ actionsTaken++;
1421
+ lastAction = action;
1422
+ forcedNextAction = nextPhase;
1423
+ cycleDepth++;
1424
+ if (!nextPhase) break;
1425
+ initialAnalysis = null;
1426
+ await sleep(300);
1427
+ continue;
1428
+ }
1401
1429
  LOG.info(`[farm:cycle:${cycleDepth}] no All button for ${action} — breaking cycle`);
1402
1430
  break;
1403
1431
  }
1404
1432
 
1405
1433
  LOG.info(`[farm:cycle:${cycleDepth}] applying ${action} with "${allBtn.label || '?'}"`);
1406
1434
  await humanDelay(90, 260);
1407
- const applyResp = await clickAndCapture({
1435
+ lastApplyResp = await clickAndCapture({
1408
1436
  channel, waitForDankMemer, response: cycleResponse,
1409
1437
  button: allBtn, tag: `farm-cycle-${action}-apply`,
1410
1438
  });
1411
1439
  if (!applyResp) { LOG.warn('[farm:cycle] All click returned null'); break; }
1412
1440
 
1413
1441
  // Step 5: advance past any confirmation screens back to the manage menu
1414
- // Capture the confirmation screen text so we can detect no-ops.
1415
- const confirmationText = brief(getFullText(applyResp), 200);
1416
1442
  cycleResponse = await advancePastConfirmation(applyResp, waitForDankMemer);
1417
1443
  if (!cycleResponse) { LOG.warn('[farm:cycle] confirmation advance returned null'); break; }
1418
- // Action was successfully applied — count it
1419
1444
  actionsTaken++;
1420
1445
  lastAction = action;
1446
+ text = getFullText(cycleResponse);
1447
+ clean = brief(text, 600);
1448
+ logFarmState('after-confirm', cycleResponse);
1421
1449
 
1422
- // If the confirmation screen text is the same as the post-confirmation text,
1423
- // the action was a no-op (e.g., hoe-ing already-tilled soil). Advance to the next phase.
1424
- const afterConfirmText = brief(getFullText(cycleResponse), 200);
1425
- if (afterConfirmText === confirmationText) {
1450
+ // If confirmation screen is identical to post-confirmation screen,
1451
+ // check whether we're on a planted confirmation (Dank Memer planted crops
1452
+ // and shows "ready at X"). In that case, click "Back" to return to the
1453
+ // farm view, then continue to harvest.
1454
+ const confirmText = brief(getFullText(lastApplyResp), 200);
1455
+ const afterText = brief(getFullText(cycleResponse), 200);
1456
+ const afterLower = afterText.toLowerCase();
1457
+ const isPlantedConfirmation = /potato|seeds ready|planted|crop.*ready|ready.*\d/i.test(afterLower);
1458
+ const afterBtns = getAllButtons(cycleResponse);
1459
+ const backBtn = afterBtns.find(b => !b.disabled && /back|back to farm/i.test(String(b.label || '').toLowerCase()));
1460
+
1461
+ if (afterText === confirmText) {
1462
+ if (isPlantedConfirmation && backBtn) {
1463
+ // Plant was successful — click "Back" to return to farm view, then proceed.
1464
+ LOG.info(`[farm:cycle:${cycleDepth}] plant confirmed (crops planted) — clicking Back`);
1465
+ await humanDelay(80, 200);
1466
+ const backResp = await clickAndCapture({
1467
+ channel, waitForDankMemer, response: cycleResponse,
1468
+ button: backBtn, tag: `farm-cycle-${action}-back`,
1469
+ });
1470
+ if (backResp) {
1471
+ cycleResponse = backResp;
1472
+ text = getFullText(cycleResponse);
1473
+ clean = brief(text, 600);
1474
+ logFarmState('after-plant-back', cycleResponse);
1475
+ }
1476
+ // Plant succeeded — advance to harvest.
1477
+ const currentIdx = FARM_PHASE_ORDER.indexOf(action);
1478
+ const nextPhase = FARM_PHASE_ORDER[currentIdx + 1] || null;
1479
+ LOG.info(`[farm:cycle:${cycleDepth}] plant done, advancing to ${nextPhase || 'done'}`);
1480
+ forcedNextAction = nextPhase;
1481
+ if (!nextPhase) break;
1482
+ cycleDepth++;
1483
+ lastAction = nextPhase;
1484
+ initialAnalysis = null;
1485
+ await sleep(400);
1486
+ continue;
1487
+ }
1488
+ // Truly a no-op (e.g., hoe-ing already-tilled soil).
1426
1489
  const currentIdx = FARM_PHASE_ORDER.indexOf(action);
1427
1490
  const nextPhase = FARM_PHASE_ORDER[currentIdx + 1] || null;
1428
- LOG.info(`[farm:cycle:${cycleDepth}] ${action} was no-op — advancing to ${nextPhase || 'done'}`);
1429
- text = getFullText(cycleResponse);
1430
- clean = brief(text, 600);
1491
+ LOG.info(`[farm:cycle:${cycleDepth}] ${action} was no-op — next=${nextPhase || 'done'}`);
1492
+ forcedNextAction = nextPhase;
1431
1493
  if (!nextPhase) break;
1432
1494
  cycleDepth++;
1433
1495
  lastAction = nextPhase;
@@ -1486,9 +1548,27 @@ async function runFarm({ channel, waitForDankMemer, client, redis, accountId })
1486
1548
  }
1487
1549
 
1488
1550
  // ── Parse final result ─────────────────────────────────────────────────────
1489
- const coins = parseCoins(text);
1551
+ // If the cycle ended with a planted confirmation (harvest→plant→confirmed),
1552
+ // the coins were shown on lastApplyResp (harvest confirmation). Parse from there too.
1553
+ let coins = parseCoins(text);
1554
+ if (coins <= 0 && lastApplyResp) {
1555
+ const applyCoins = parseCoins(getFullText(lastApplyResp));
1556
+ if (applyCoins > 0) coins = applyCoins;
1557
+ }
1490
1558
  let nextCd = parseFarmCooldownSec(text) || 30;
1491
1559
  const growReadyEnd = parseFarmGrowReadySec(text);
1560
+ // If the final screen is a planted confirmation, use its wilt timestamp as the
1561
+ // grow queue timer — the crops are planted and will be ready at that time.
1562
+ const finalLower = brief(text, 300).toLowerCase();
1563
+ const isFinalPlanted = /potato|seeds ready|planted|crop.*ready|ready.*\d/i.test(finalLower);
1564
+ if (isFinalPlanted && growReadyEnd === null) {
1565
+ // Crops were planted. The wilt timestamp in the planted confirmation tells us
1566
+ // when to harvest next. Set cooldown to that time, capped at a reasonable max.
1567
+ const plantedCd = parseFarmCooldownSec(text);
1568
+ if (plantedCd && plantedCd > 0) {
1569
+ nextCd = Math.max(nextCd, Math.min(6 * 3600, plantedCd + 2));
1570
+ }
1571
+ }
1492
1572
  if (Number.isFinite(growReadyEnd) && growReadyEnd > 0) {
1493
1573
  nextCd = Math.max(nextCd, Math.min(6 * 3600, growReadyEnd + 2));
1494
1574
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dankgrinder",
3
- "version": "6.27.0",
3
+ "version": "6.34.0",
4
4
  "description": "Dank Memer automation engine — grind coins while you sleep",
5
5
  "bin": {
6
6
  "dankgrinder": "bin/dankgrinder.js"