@ape-church/skill 1.0.3 → 1.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/cli.js CHANGED
@@ -414,7 +414,7 @@ function getStrategyConfig(strategy) {
414
414
  );
415
415
  const configs = {
416
416
  conservative: {
417
- minBetApe: 10,
417
+ minBetApe: 1,
418
418
  targetBetPct: 0.05,
419
419
  maxBetPct: 0.1,
420
420
  baseCooldownMs: 60 * 1000, // 60 seconds
@@ -423,7 +423,7 @@ function getStrategyConfig(strategy) {
423
423
  gameWeights: defaultWeights,
424
424
  },
425
425
  balanced: {
426
- minBetApe: 10,
426
+ minBetApe: 1,
427
427
  targetBetPct: 0.08,
428
428
  maxBetPct: 0.15,
429
429
  baseCooldownMs: 30 * 1000, // 30 seconds
@@ -432,7 +432,7 @@ function getStrategyConfig(strategy) {
432
432
  gameWeights: defaultWeights,
433
433
  },
434
434
  aggressive: {
435
- minBetApe: 10,
435
+ minBetApe: 1,
436
436
  targetBetPct: 0.12,
437
437
  maxBetPct: 0.25,
438
438
  baseCooldownMs: 15 * 1000, // 15 seconds
@@ -441,7 +441,7 @@ function getStrategyConfig(strategy) {
441
441
  gameWeights: defaultWeights,
442
442
  },
443
443
  degen: {
444
- minBetApe: 10,
444
+ minBetApe: 1,
445
445
  targetBetPct: 0.2,
446
446
  maxBetPct: 0.35,
447
447
  baseCooldownMs: 10 * 1000, // 10 seconds
@@ -856,26 +856,75 @@ program
856
856
  console.log('You can retry later with: apechurch register --username <NAME>');
857
857
  }
858
858
 
859
- // 4. The Handshake (Replacing the Claim Link)
860
- console.log('\nSETUP COMPLETE');
861
- console.log('---------------------------------------');
862
- console.log(`AGENT ADDRESS: ${address}`);
863
- console.log(`USERNAME: ${username}`);
864
- if (!usernameWasProvided) {
865
- console.log(' (Change anytime: apechurch register --username <YOUR_NAME>)');
866
- }
867
- console.log(`PERSONA: ${persona}`);
859
+ // 4. The Handshake
860
+ console.log('\n========================================');
861
+ console.log('šŸŽ° APE CHURCH - SETUP COMPLETE');
862
+ console.log('========================================');
868
863
  console.log('');
869
- console.log('ACTION REQUIRED: Send APE (ApeChain) to this address.');
870
- console.log('FUNDING GUIDE:');
871
- console.log(
872
- '1) Open https://relay.link/bridge/apechain?toCurrency=0x0000000000000000000000000000000000000000'
873
- );
874
- console.log('2) Connect your wallet.');
875
- console.log('3) Paste the agent address in ApeChain buy area:');
876
- console.log(' Select wallet -> Paste wallet address');
877
- console.log('The Agent will wake up automatically once funded.');
878
- console.log('---------------------------------------');
864
+ console.log('Ape Church is a FULLY on-chain, 100% decentralized casino on ApeChain.');
865
+ console.log('Every bet is placed and settled on-chain. Zero servers.');
866
+ console.log('');
867
+ console.log('Player vs Player: The "house" is a public staking pool where anyone');
868
+ console.log('can deposit or withdraw APE anytime. You\'re playing against other');
869
+ console.log('players, not the house. Truly PvP.');
870
+ console.log('');
871
+ console.log('YOUR AGENT:');
872
+ console.log(` Address: ${address}`);
873
+ console.log(` Username: ${username}`);
874
+ console.log(` Strategy: ${persona}`);
875
+ console.log('');
876
+ console.log('========================================');
877
+ console.log('STEP 1: FUND YOUR AGENT');
878
+ console.log('========================================');
879
+ console.log('Send APE (on ApeChain) to the address above.');
880
+ console.log('Bridge: https://relay.link/bridge/apechain');
881
+ console.log('');
882
+ console.log('========================================');
883
+ console.log('STEP 2: START PLAYING');
884
+ console.log('========================================');
885
+ console.log('Easiest way - just run:');
886
+ console.log('');
887
+ console.log(' apechurch play --loop');
888
+ console.log('');
889
+ console.log('Or tell your AI agent (copy this message):');
890
+ console.log('');
891
+ console.log('---START MESSAGE---');
892
+ console.log(`I've installed the Ape Church skill for you. Read the skill file at`);
893
+ console.log(`~/.openclaw/skills/ape-church/SKILL.md to understand how to play.`);
894
+ console.log(`Then run: apechurch play --loop`);
895
+ console.log(`This will play casino games continuously on ApeChain.`);
896
+ console.log('---END MESSAGE---');
897
+ console.log('');
898
+ console.log('========================================');
899
+ console.log('QUICK REFERENCE');
900
+ console.log('========================================');
901
+ console.log('PLAY:');
902
+ console.log(' apechurch play Play one game');
903
+ console.log(' apechurch play --loop Play continuously (2s between games)');
904
+ console.log('');
905
+ console.log('GAMES (auto-selected, or use --game):');
906
+ console.log(' jungle-plinko Plinko (--mode 0-4, --balls 1-100)');
907
+ console.log(' dino-dough Slots (--spins 1-15)');
908
+ console.log(' bubblegum-heist Slots (--spins 1-15)');
909
+ console.log('');
910
+ console.log('STRATEGIES (controls bet sizing):');
911
+ console.log(' conservative 5% of balance per bet');
912
+ console.log(' balanced 8% of balance per bet (default)');
913
+ console.log(' aggressive 12% of balance per bet');
914
+ console.log(' degen 20% of balance per bet');
915
+ console.log('');
916
+ console.log('CONTROL:');
917
+ console.log(' apechurch status Check balance');
918
+ console.log(' apechurch pause Pause play');
919
+ console.log(' apechurch resume Resume play');
920
+ console.log(' apechurch games List all games');
921
+ console.log(' apechurch commands Full command reference');
922
+ console.log(' apechurch help Show help');
923
+ console.log('');
924
+ console.log('CHANGE SETTINGS:');
925
+ console.log(' apechurch register --username <NAME>');
926
+ console.log(' apechurch profile set --persona <STRATEGY>');
927
+ console.log('========================================');
879
928
  });
880
929
 
881
930
  // --- COMMAND: STATUS (The Agent Experience) ---
@@ -1083,163 +1132,584 @@ program
1083
1132
  // --- COMMAND: HEARTBEAT (Autonomous Loop) ---
1084
1133
  program
1085
1134
  .command('heartbeat')
1086
- .option('--strategy <name>', 'conservative | balanced | aggressive')
1135
+ .option('--strategy <name>', 'conservative | balanced | aggressive | degen')
1087
1136
  .option('--cooldown <ms>', 'Minimum ms between plays (0 = use strategy cooldown)', '0')
1088
1137
  .option('--timeout <ms>', 'Max ms to wait for GameEnded event. Use 0 to wait indefinitely.', '0')
1138
+ .option('--loop', 'Run continuously until paused or stopped (Ctrl+C)')
1089
1139
  .option('--json', 'Output JSON only')
1090
1140
  .action(async (opts) => {
1091
1141
  const account = getWallet();
1092
- const state = loadState();
1093
- const profile = loadProfile();
1094
- const now = Date.now();
1142
+ const requestedCooldown = parseNonNegativeInt(opts.cooldown, 'cooldown');
1143
+ const timeoutMs = parseNonNegativeInt(opts.timeout, 'timeout');
1144
+ const loopMode = Boolean(opts.loop);
1145
+
1146
+ if (loopMode && !opts.json) {
1147
+ console.log('šŸŽ° Starting continuous play mode (Ctrl+C to stop)...\n');
1148
+ }
1149
+
1150
+ // Main play function - returns cooldown to wait (0 = no wait needed)
1151
+ async function runHeartbeat() {
1152
+ const state = loadState();
1153
+ const profile = loadProfile();
1154
+ const now = Date.now();
1155
+
1156
+ // Check if paused
1157
+ if (profile.paused) {
1158
+ state.lastHeartbeat = now;
1159
+ saveState(state);
1160
+ const response = {
1161
+ action: 'heartbeat',
1162
+ status: 'skipped',
1163
+ reason: 'paused',
1164
+ message: 'Autonomous play is paused. Run `apechurch resume` to continue.',
1165
+ address: account.address,
1166
+ paused: true,
1167
+ };
1168
+ if (opts.json) console.log(JSON.stringify(response));
1169
+ else console.log(JSON.stringify(response, null, 2));
1170
+ return { shouldStop: true, waitMs: 0 };
1171
+ }
1095
1172
 
1096
- // Check if paused - skip gracefully without fetching balance
1097
- if (profile.paused) {
1098
1173
  state.lastHeartbeat = now;
1099
- saveState(state);
1100
- const response = {
1174
+ if (opts.strategy) state.strategy = normalizeStrategy(opts.strategy);
1175
+ else if (profile.persona) state.strategy = normalizeStrategy(profile.persona);
1176
+ if (requestedCooldown > 0) state.cooldownMs = requestedCooldown;
1177
+
1178
+ const { publicClient } = createClients();
1179
+ let balance;
1180
+ try {
1181
+ balance = await publicClient.getBalance({ address: account.address });
1182
+ } catch (error) {
1183
+ console.error(JSON.stringify({ error: `Failed to fetch balance: ${sanitizeError(error)}` }));
1184
+ return { shouldStop: true, waitMs: 0 };
1185
+ }
1186
+
1187
+ const balanceApe = parseFloat(formatEther(balance));
1188
+ const availableApe = Math.max(balanceApe - GAS_RESERVE_APE, 0);
1189
+ const strategy = normalizeStrategy(state.strategy);
1190
+ const strategyConfig = applyProfileOverrides(
1191
+ getStrategyConfig(strategy),
1192
+ profile.overrides
1193
+ );
1194
+ const dynamicCooldownMs = computeCooldownMs(strategyConfig, state);
1195
+ const cooldownMs = requestedCooldown > 0 ? requestedCooldown : dynamicCooldownMs;
1196
+
1197
+ const baseResponse = {
1101
1198
  action: 'heartbeat',
1102
- status: 'skipped',
1103
- reason: 'paused',
1104
- message: 'Autonomous play is paused. Run `apechurch resume` to continue.',
1199
+ strategy,
1105
1200
  address: account.address,
1106
- paused: true,
1201
+ balance_ape: balanceApe.toFixed(6),
1202
+ available_ape: availableApe.toFixed(6),
1203
+ gas_reserve_ape: GAS_RESERVE_APE.toFixed(6),
1204
+ paused: false,
1205
+ last_play: state.lastPlay,
1206
+ cooldown_ms: cooldownMs,
1207
+ consecutive_wins: state.consecutiveWins,
1208
+ consecutive_losses: state.consecutiveLosses,
1107
1209
  };
1108
- if (opts.json) console.log(JSON.stringify(response));
1109
- else console.log(JSON.stringify(response, null, 2));
1110
- return;
1111
- }
1112
1210
 
1113
- state.lastHeartbeat = now;
1114
- if (opts.strategy) state.strategy = normalizeStrategy(opts.strategy);
1115
- else if (profile.persona) state.strategy = normalizeStrategy(profile.persona);
1116
- const requestedCooldown = parseNonNegativeInt(opts.cooldown, 'cooldown');
1117
- if (requestedCooldown > 0) state.cooldownMs = requestedCooldown;
1118
- const timeoutMs = parseNonNegativeInt(opts.timeout, 'timeout');
1211
+ if (availableApe <= 0 || availableApe < strategyConfig.minBetApe) {
1212
+ saveState(state);
1213
+ const response = {
1214
+ ...baseResponse,
1215
+ status: 'skipped',
1216
+ reason: 'insufficient_available_ape',
1217
+ };
1218
+ if (opts.json) console.log(JSON.stringify(response));
1219
+ else console.log(JSON.stringify(response, null, 2));
1220
+ return { shouldStop: true, waitMs: 0 };
1221
+ }
1119
1222
 
1120
- const { publicClient } = createClients();
1121
- let balance;
1122
- try {
1123
- balance = await publicClient.getBalance({ address: account.address });
1124
- } catch (error) {
1125
- console.error(JSON.stringify({ error: `Failed to fetch balance: ${sanitizeError(error)}` }));
1126
- process.exit(1);
1223
+ if (state.lastPlay && cooldownMs > 0 && now - state.lastPlay < cooldownMs) {
1224
+ const waitMs = Math.max(cooldownMs - (now - state.lastPlay), 0);
1225
+ saveState(state);
1226
+ const response = {
1227
+ ...baseResponse,
1228
+ status: 'skipped',
1229
+ reason: 'cooldown',
1230
+ next_play_after_ms: waitMs,
1231
+ };
1232
+ if (opts.json) console.log(JSON.stringify(response));
1233
+ else console.log(JSON.stringify(response, null, 2));
1234
+ return { shouldStop: false, waitMs };
1235
+ }
1236
+
1237
+ const wagerApe = calculateWager(availableApe, strategyConfig);
1238
+ if (wagerApe < strategyConfig.minBetApe) {
1239
+ saveState(state);
1240
+ const response = {
1241
+ ...baseResponse,
1242
+ status: 'skipped',
1243
+ reason: 'wager_below_minimum',
1244
+ wager_ape: formatApeAmount(wagerApe),
1245
+ };
1246
+ if (opts.json) console.log(JSON.stringify(response));
1247
+ else console.log(JSON.stringify(response, null, 2));
1248
+ return { shouldStop: true, waitMs: 0 };
1249
+ }
1250
+
1251
+ const selection = selectGameAndConfig(strategyConfig);
1252
+ const wagerApeString = formatApeAmount(wagerApe);
1253
+
1254
+ try {
1255
+ const playResponse = await playGame({
1256
+ account,
1257
+ game: selection.game,
1258
+ amountApe: wagerApeString,
1259
+ mode: selection.mode,
1260
+ balls: selection.balls,
1261
+ spins: selection.spins,
1262
+ timeoutMs,
1263
+ });
1264
+
1265
+ state.lastPlay = Date.now();
1266
+ if (playResponse?.result) {
1267
+ const pnlWei = (BigInt(playResponse.result.payout_wei) -
1268
+ BigInt(playResponse.result.buy_in_wei)).toString();
1269
+ state.totalPnLWei = addBigIntStrings(state.totalPnLWei, pnlWei);
1270
+ if (BigInt(pnlWei) >= 0n) {
1271
+ state.sessionWins += 1;
1272
+ state.consecutiveWins += 1;
1273
+ state.consecutiveLosses = 0;
1274
+ } else {
1275
+ state.sessionLosses += 1;
1276
+ state.consecutiveLosses += 1;
1277
+ state.consecutiveWins = 0;
1278
+ }
1279
+ }
1280
+
1281
+ saveState(state);
1282
+
1283
+ // Recalculate cooldown after state update (may change due to win/loss streaks)
1284
+ const newCooldownMs = requestedCooldown > 0
1285
+ ? requestedCooldown
1286
+ : computeCooldownMs(strategyConfig, state);
1287
+
1288
+ const response = {
1289
+ ...baseResponse,
1290
+ cooldown_ms: newCooldownMs,
1291
+ consecutive_wins: state.consecutiveWins,
1292
+ consecutive_losses: state.consecutiveLosses,
1293
+ status: playResponse.status,
1294
+ wager_ape: wagerApeString,
1295
+ game: playResponse.game,
1296
+ config: playResponse.config,
1297
+ tx: playResponse.tx,
1298
+ gameId: playResponse.gameId,
1299
+ game_url: playResponse.game_url,
1300
+ result: playResponse.result,
1301
+ };
1302
+
1303
+ if (opts.json) console.log(JSON.stringify(response));
1304
+ else console.log(JSON.stringify(response, null, 2));
1305
+
1306
+ return { shouldStop: false, waitMs: newCooldownMs };
1307
+ } catch (error) {
1308
+ saveState(state);
1309
+ console.error(JSON.stringify({ error: error.message }));
1310
+ return { shouldStop: true, waitMs: 0 };
1311
+ }
1127
1312
  }
1128
1313
 
1129
- const balanceApe = parseFloat(formatEther(balance));
1130
- const availableApe = Math.max(balanceApe - GAS_RESERVE_APE, 0);
1131
- const strategy = normalizeStrategy(state.strategy);
1132
- const strategyConfig = applyProfileOverrides(
1133
- getStrategyConfig(strategy),
1134
- profile.overrides
1135
- );
1136
- const dynamicCooldownMs = computeCooldownMs(strategyConfig, state);
1137
- const cooldownMs =
1138
- requestedCooldown > 0 ? requestedCooldown : dynamicCooldownMs;
1314
+ // Run once or loop
1315
+ if (!loopMode) {
1316
+ const result = await runHeartbeat();
1317
+ if (result.shouldStop && result.waitMs === 0) {
1318
+ // Error or fatal skip - exit with appropriate code
1319
+ const state = loadState();
1320
+ const profile = loadProfile();
1321
+ if (profile.paused) process.exit(0);
1322
+ }
1323
+ } else {
1324
+ // Loop mode
1325
+ let running = true;
1326
+ process.on('SIGINT', () => {
1327
+ if (!opts.json) console.log('\nšŸ‘‹ Stopping continuous play...');
1328
+ running = false;
1329
+ });
1330
+ process.on('SIGTERM', () => {
1331
+ running = false;
1332
+ });
1139
1333
 
1140
- const baseResponse = {
1141
- action: 'heartbeat',
1142
- strategy,
1143
- address: account.address,
1144
- balance_ape: balanceApe.toFixed(6),
1145
- available_ape: availableApe.toFixed(6),
1146
- gas_reserve_ape: GAS_RESERVE_APE.toFixed(6),
1147
- paused: false,
1148
- last_play: state.lastPlay,
1149
- cooldown_ms: cooldownMs,
1150
- consecutive_wins: state.consecutiveWins,
1151
- consecutive_losses: state.consecutiveLosses,
1152
- };
1334
+ while (running) {
1335
+ const result = await runHeartbeat();
1336
+
1337
+ if (!running) break;
1338
+
1339
+ if (result.shouldStop) {
1340
+ if (!opts.json) console.log('\nā¹ļø Stopped: cannot continue playing.');
1341
+ break;
1342
+ }
1153
1343
 
1154
- if (availableApe <= 0 || availableApe < strategyConfig.minBetApe) {
1155
- saveState(state);
1156
- const response = {
1157
- ...baseResponse,
1158
- status: 'skipped',
1159
- reason: 'insufficient_available_ape',
1160
- };
1161
- if (opts.json) console.log(JSON.stringify(response));
1162
- else console.log(JSON.stringify(response, null, 2));
1163
- return;
1344
+ if (result.waitMs > 0) {
1345
+ if (!opts.json) {
1346
+ console.log(`\nā³ Waiting ${(result.waitMs / 1000).toFixed(0)}s until next bet...\n`);
1347
+ }
1348
+ await new Promise((resolve) => {
1349
+ const timeout = setTimeout(resolve, result.waitMs);
1350
+ const checkStop = setInterval(() => {
1351
+ if (!running) {
1352
+ clearTimeout(timeout);
1353
+ clearInterval(checkStop);
1354
+ resolve();
1355
+ }
1356
+ }, 500);
1357
+ });
1358
+ }
1359
+ }
1164
1360
  }
1361
+ });
1165
1362
 
1166
- if (state.lastPlay && cooldownMs > 0 && now - state.lastPlay < cooldownMs) {
1167
- saveState(state);
1363
+ // --- COMMAND: PLAY (Simple play command - no cooldowns) ---
1364
+ program
1365
+ .command('play')
1366
+ .description('Play a game immediately (no cooldowns)')
1367
+ .option('--strategy <name>', 'conservative | balanced | aggressive | degen', 'balanced')
1368
+ .option('--loop', 'Play continuously with 2s between games')
1369
+ .option('--delay <seconds>', 'Seconds between games in loop mode', '2')
1370
+ .option('--json', 'Output JSON only')
1371
+ .action(async (opts) => {
1372
+ const account = getWallet();
1373
+ const loopMode = Boolean(opts.loop);
1374
+ const delaySeconds = Math.max(parseFloat(opts.delay) || 2, 1);
1375
+ const delayMs = delaySeconds * 1000;
1376
+
1377
+ // Check if paused
1378
+ const profile = loadProfile();
1379
+ if (profile.paused) {
1168
1380
  const response = {
1169
- ...baseResponse,
1381
+ action: 'play',
1170
1382
  status: 'skipped',
1171
- reason: 'cooldown',
1172
- next_play_after_ms: Math.max(cooldownMs - (now - state.lastPlay), 0),
1383
+ reason: 'paused',
1384
+ message: 'Play is paused. Run `apechurch resume` to continue.',
1173
1385
  };
1174
1386
  if (opts.json) console.log(JSON.stringify(response));
1175
1387
  else console.log(JSON.stringify(response, null, 2));
1176
1388
  return;
1177
1389
  }
1178
1390
 
1179
- const wagerApe = calculateWager(availableApe, strategyConfig);
1180
- if (wagerApe < strategyConfig.minBetApe) {
1181
- saveState(state);
1182
- const response = {
1183
- ...baseResponse,
1184
- status: 'skipped',
1185
- reason: 'wager_below_minimum',
1186
- wager_ape: formatApeAmount(wagerApe),
1187
- };
1188
- if (opts.json) console.log(JSON.stringify(response));
1189
- else console.log(JSON.stringify(response, null, 2));
1190
- return;
1391
+ if (loopMode && !opts.json) {
1392
+ console.log(`šŸŽ° Starting continuous play (${delaySeconds}s between games, Ctrl+C to stop)...\n`);
1191
1393
  }
1192
1394
 
1193
- const selection = selectGameAndConfig(strategyConfig);
1194
- const wagerApeString = formatApeAmount(wagerApe);
1395
+ async function playOnce() {
1396
+ const state = loadState();
1397
+ const freshProfile = loadProfile();
1398
+
1399
+ if (freshProfile.paused) {
1400
+ return { shouldStop: true, reason: 'paused' };
1401
+ }
1195
1402
 
1196
- try {
1197
- const playResponse = await playGame({
1198
- account,
1199
- game: selection.game,
1200
- amountApe: wagerApeString,
1201
- mode: selection.mode,
1202
- balls: selection.balls,
1203
- spins: selection.spins,
1204
- timeoutMs,
1403
+ const strategy = normalizeStrategy(opts.strategy);
1404
+ const strategyConfig = applyProfileOverrides(
1405
+ getStrategyConfig(strategy),
1406
+ freshProfile.overrides
1407
+ );
1408
+
1409
+ const { publicClient } = createClients();
1410
+ let balance;
1411
+ try {
1412
+ balance = await publicClient.getBalance({ address: account.address });
1413
+ } catch (error) {
1414
+ console.error(JSON.stringify({ error: `Failed to fetch balance: ${sanitizeError(error)}` }));
1415
+ return { shouldStop: true, reason: 'balance_error' };
1416
+ }
1417
+
1418
+ const balanceApe = parseFloat(formatEther(balance));
1419
+ const availableApe = Math.max(balanceApe - GAS_RESERVE_APE, 0);
1420
+
1421
+ if (availableApe <= 0 || availableApe < strategyConfig.minBetApe) {
1422
+ const response = {
1423
+ action: 'play',
1424
+ status: 'skipped',
1425
+ reason: 'insufficient_balance',
1426
+ balance_ape: balanceApe.toFixed(6),
1427
+ available_ape: availableApe.toFixed(6),
1428
+ };
1429
+ if (opts.json) console.log(JSON.stringify(response));
1430
+ else console.log(JSON.stringify(response, null, 2));
1431
+ return { shouldStop: true, reason: 'insufficient_balance' };
1432
+ }
1433
+
1434
+ const wagerApe = calculateWager(availableApe, strategyConfig);
1435
+ const selection = selectGameAndConfig(strategyConfig);
1436
+ const wagerApeString = formatApeAmount(wagerApe);
1437
+
1438
+ try {
1439
+ const playResponse = await playGame({
1440
+ account,
1441
+ game: selection.game,
1442
+ amountApe: wagerApeString,
1443
+ mode: selection.mode,
1444
+ balls: selection.balls,
1445
+ spins: selection.spins,
1446
+ timeoutMs: 0,
1447
+ });
1448
+
1449
+ // Update state
1450
+ if (playResponse?.result) {
1451
+ const pnlWei = (BigInt(playResponse.result.payout_wei) -
1452
+ BigInt(playResponse.result.buy_in_wei)).toString();
1453
+ state.totalPnLWei = addBigIntStrings(state.totalPnLWei, pnlWei);
1454
+ if (BigInt(pnlWei) >= 0n) {
1455
+ state.sessionWins += 1;
1456
+ state.consecutiveWins += 1;
1457
+ state.consecutiveLosses = 0;
1458
+ } else {
1459
+ state.sessionLosses += 1;
1460
+ state.consecutiveLosses += 1;
1461
+ state.consecutiveWins = 0;
1462
+ }
1463
+ }
1464
+ state.lastPlay = Date.now();
1465
+ saveState(state);
1466
+
1467
+ const response = {
1468
+ action: 'play',
1469
+ status: playResponse.status,
1470
+ strategy,
1471
+ balance_ape: balanceApe.toFixed(6),
1472
+ wager_ape: wagerApeString,
1473
+ game: playResponse.game,
1474
+ config: playResponse.config,
1475
+ tx: playResponse.tx,
1476
+ gameId: playResponse.gameId,
1477
+ game_url: playResponse.game_url,
1478
+ result: playResponse.result,
1479
+ session: {
1480
+ wins: state.sessionWins,
1481
+ losses: state.sessionLosses,
1482
+ total_pnl_ape: formatEther(BigInt(state.totalPnLWei)),
1483
+ },
1484
+ };
1485
+
1486
+ if (opts.json) console.log(JSON.stringify(response));
1487
+ else console.log(JSON.stringify(response, null, 2));
1488
+
1489
+ return { shouldStop: false };
1490
+ } catch (error) {
1491
+ console.error(JSON.stringify({ error: error.message }));
1492
+ return { shouldStop: true, reason: 'play_error' };
1493
+ }
1494
+ }
1495
+
1496
+ if (!loopMode) {
1497
+ await playOnce();
1498
+ } else {
1499
+ let running = true;
1500
+ process.on('SIGINT', () => {
1501
+ if (!opts.json) console.log('\nšŸ‘‹ Stopping...');
1502
+ running = false;
1205
1503
  });
1504
+ process.on('SIGTERM', () => {
1505
+ running = false;
1506
+ });
1507
+
1508
+ while (running) {
1509
+ const result = await playOnce();
1510
+
1511
+ if (!running) break;
1512
+ if (result.shouldStop) {
1513
+ if (!opts.json) console.log(`\nā¹ļø Stopped: ${result.reason}`);
1514
+ break;
1515
+ }
1206
1516
 
1207
- state.lastPlay = now;
1208
- if (playResponse?.result) {
1209
- const pnlWei = (BigInt(playResponse.result.payout_wei) -
1210
- BigInt(playResponse.result.buy_in_wei)).toString();
1211
- state.totalPnLWei = addBigIntStrings(state.totalPnLWei, pnlWei);
1212
- if (BigInt(pnlWei) >= 0n) {
1213
- state.sessionWins += 1;
1214
- state.consecutiveWins += 1;
1215
- state.consecutiveLosses = 0;
1216
- } else {
1217
- state.sessionLosses += 1;
1218
- state.consecutiveLosses += 1;
1219
- state.consecutiveWins = 0;
1517
+ if (!opts.json) {
1518
+ console.log(`\nā³ Next game in ${delaySeconds}s...\n`);
1220
1519
  }
1520
+ await new Promise((resolve) => {
1521
+ const timeout = setTimeout(resolve, delayMs);
1522
+ const checkStop = setInterval(() => {
1523
+ if (!running) {
1524
+ clearTimeout(timeout);
1525
+ clearInterval(checkStop);
1526
+ resolve();
1527
+ }
1528
+ }, 200);
1529
+ });
1221
1530
  }
1531
+ }
1532
+ });
1222
1533
 
1223
- saveState(state);
1224
- const response = {
1225
- ...baseResponse,
1226
- status: playResponse.status,
1227
- wager_ape: wagerApeString,
1228
- game: playResponse.game,
1229
- config: playResponse.config,
1230
- tx: playResponse.tx,
1231
- gameId: playResponse.gameId,
1232
- game_url: playResponse.game_url,
1233
- result: playResponse.result,
1534
+ // --- COMMAND: GAMES (Show available games and parameters) ---
1535
+ program
1536
+ .command('games')
1537
+ .description('List all available games and their parameters')
1538
+ .option('--json', 'Output JSON only')
1539
+ .action((opts) => {
1540
+ const games = GAME_REGISTRY.map((game) => {
1541
+ const params = [];
1542
+ if (game.type === 'plinko') {
1543
+ params.push({
1544
+ name: 'mode',
1545
+ type: 'integer',
1546
+ min: game.config.mode.min,
1547
+ max: game.config.mode.max,
1548
+ default: game.config.mode.default,
1549
+ description: 'Risk level (higher = riskier, bigger payouts)',
1550
+ });
1551
+ params.push({
1552
+ name: 'balls',
1553
+ type: 'integer',
1554
+ min: game.config.balls.min,
1555
+ max: game.config.balls.max,
1556
+ default: game.config.balls.default,
1557
+ description: 'Number of balls to drop',
1558
+ });
1559
+ } else if (game.type === 'slots') {
1560
+ params.push({
1561
+ name: 'spins',
1562
+ type: 'integer',
1563
+ min: game.config.spins.min,
1564
+ max: game.config.spins.max,
1565
+ default: game.config.spins.default,
1566
+ description: 'Number of spins per bet',
1567
+ });
1568
+ }
1569
+ return {
1570
+ key: game.key,
1571
+ name: game.name,
1572
+ type: game.type,
1573
+ aliases: game.aliases || [],
1574
+ contract: game.contract,
1575
+ parameters: params,
1234
1576
  };
1577
+ });
1235
1578
 
1236
- if (opts.json) console.log(JSON.stringify(response));
1237
- else console.log(JSON.stringify(response, null, 2));
1238
- } catch (error) {
1239
- saveState(state);
1240
- console.error(JSON.stringify({ error: error.message }));
1241
- process.exit(1);
1579
+ if (opts.json) {
1580
+ console.log(JSON.stringify({ games }, null, 2));
1581
+ } else {
1582
+ console.log('\nšŸŽ° AVAILABLE GAMES\n');
1583
+ for (const game of games) {
1584
+ console.log(`${game.name} (${game.key})`);
1585
+ console.log(` Type: ${game.type}`);
1586
+ console.log(` Aliases: ${game.aliases.join(', ') || 'none'}`);
1587
+ console.log(' Parameters:');
1588
+ for (const param of game.parameters) {
1589
+ console.log(` --${param.name} <${param.min}-${param.max}> ${param.description} (default: ${param.default})`);
1590
+ }
1591
+ console.log('');
1592
+ }
1593
+ console.log('EXAMPLE BETS:');
1594
+ console.log(' apechurch bet --game jungle-plinko --amount 5 --mode 2 --balls 50');
1595
+ console.log(' apechurch bet --game dino-dough --amount 10 --spins 8');
1596
+ console.log(' apechurch bet --game bubblegum-heist --amount 5 --spins 12');
1597
+ console.log('');
1242
1598
  }
1243
1599
  });
1244
1600
 
1601
+ // --- COMMAND: COMMANDS (Show all commands overview) ---
1602
+ program
1603
+ .command('commands')
1604
+ .alias('help')
1605
+ .description('Show all available commands with examples')
1606
+ .action(() => {
1607
+ console.log(`
1608
+ ========================================
1609
+ šŸŽ° APE CHURCH CLI - FULL REFERENCE
1610
+ ========================================
1611
+
1612
+ ABOUT
1613
+ Ape Church is a FULLY on-chain, 100% decentralized casino on ApeChain.
1614
+ Every bet is placed and settled on-chain. Zero servers.
1615
+
1616
+ Player vs Player: The "house" is a public staking pool - anyone can
1617
+ deposit or withdraw APE anytime. You're playing against other players,
1618
+ not the house. Truly PvP. Games use VRF for provably fair randomness.
1619
+
1620
+ ========================================
1621
+ GETTING STARTED
1622
+ ========================================
1623
+
1624
+ apechurch install [--username NAME] [--persona TYPE]
1625
+ Set up your agent wallet and register on Ape Church.
1626
+
1627
+ apechurch status [--json]
1628
+ Check your wallet balance and current status.
1629
+
1630
+ ========================================
1631
+ PLAYING GAMES
1632
+ ========================================
1633
+
1634
+ apechurch play [--strategy TYPE] [--loop] [--json]
1635
+ Play games automatically. Picks a random game, bets based on strategy.
1636
+ --loop Play continuously (2s between games)
1637
+ --delay <sec> Custom delay between games (default: 2)
1638
+ --strategy conservative | balanced | aggressive | degen
1639
+
1640
+ apechurch bet --game <NAME> --amount <APE> [options]
1641
+ Place a manual bet on a specific game.
1642
+ --game jungle-plinko | dino-dough | bubblegum-heist
1643
+ --amount APE to wager
1644
+ --mode <0-4> Plinko risk level (higher = riskier)
1645
+ --balls <1-100> Plinko balls to drop
1646
+ --spins <1-15> Slots spins per bet
1647
+
1648
+ apechurch games [--json]
1649
+ List all available games with parameters.
1650
+
1651
+ ========================================
1652
+ STRATEGIES (controls how much your agent bets)
1653
+ ========================================
1654
+
1655
+ conservative 5% of balance per bet, low-risk game configs
1656
+ balanced 8% of balance per bet (default)
1657
+ aggressive 12% of balance per bet, high-risk game configs
1658
+ degen 20% of balance per bet, max risk
1659
+
1660
+ Change your strategy anytime:
1661
+ apechurch profile set --persona aggressive
1662
+
1663
+ Or specify per-session:
1664
+ apechurch play --loop --strategy degen
1665
+
1666
+ ========================================
1667
+ CONTROL
1668
+ ========================================
1669
+
1670
+ apechurch pause Stop playing (loop will exit)
1671
+ apechurch resume Resume playing
1672
+ Ctrl+C Stop the current loop
1673
+
1674
+ ========================================
1675
+ PROFILE & SETTINGS
1676
+ ========================================
1677
+
1678
+ apechurch profile show [--json]
1679
+ View your current profile.
1680
+
1681
+ apechurch profile set [--persona TYPE] [--username NAME]
1682
+ Update your strategy or username.
1683
+
1684
+ apechurch register --username <NAME>
1685
+ Change your on-chain username.
1686
+
1687
+ ========================================
1688
+ EXAMPLES
1689
+ ========================================
1690
+
1691
+ # Quick start - play continuously
1692
+ apechurch play --loop
1693
+
1694
+ # Play aggressively
1695
+ apechurch play --loop --strategy aggressive
1696
+
1697
+ # Single manual bet
1698
+ apechurch bet --game jungle-plinko --amount 10 --mode 3 --balls 50
1699
+
1700
+ # Check status
1701
+ apechurch status
1702
+
1703
+ ========================================
1704
+ FOR AI AGENTS
1705
+ ========================================
1706
+
1707
+ To activate this skill, tell your agent:
1708
+
1709
+ "Read ~/.openclaw/skills/ape-church/SKILL.md and run: apechurch play --loop"
1710
+
1711
+ All commands support --json for machine-readable output.
1712
+ `);
1713
+ });
1714
+
1245
1715
  program.parse(process.argv);