From da6a24614f33a4200a9d1c676d9d4c09d49822d3 Mon Sep 17 00:00:00 2001 From: Asklv Date: Mon, 30 Mar 2026 18:18:06 +0800 Subject: [PATCH] fix: make play.sh resilient to transient errors - Remove set -e; handle all errors explicitly so transient failures never kill the loop - Wrap all curl calls in curl_post/curl_get helpers (never exit on failure) - Validate JSON before parsing; back off and retry on bad/empty response - Heartbeat runs in background subshell (fire-and-forget, can't kill loop) - Auto-rejoin after MAX_FAILS (8) consecutive state fetch failures - Auto-claim + re-select table when busted (chips <= 0) - Numeric chip values sanitized with grep before arithmetic comparisons - Act retry: if play action fails, retry once after 1s - Chat and heartbeat are background/best-effort, never block main loop --- public/scripts/play.sh | 261 ++++++++++++++++++++++++++++------------- skill/scripts/play.sh | 261 ++++++++++++++++++++++++++++------------- 2 files changed, 354 insertions(+), 168 deletions(-) diff --git a/public/scripts/play.sh b/public/scripts/play.sh index e399124..d6fda8a 100755 --- a/public/scripts/play.sh +++ b/public/scripts/play.sh @@ -1,156 +1,249 @@ #!/usr/bin/env bash -set -euo pipefail # ══════════════════════════════════════════════════════════════ -# Agent Casino — Complete Auto-Play Script +# Agent Casino — Resilient Auto-Play Script # Requires: curl, jq, bash # Usage: ./play.sh [agent_name] -# Download: curl -fsSL https://www.agentcasino.dev/play.sh -o play.sh +# Download: curl -fsSL https://www.agentcasino.dev/scripts/play.sh -o play.sh # ══════════════════════════════════════════════════════════════ +# NO set -e: errors are handled explicitly, never kill the loop +set -uo pipefail AGENT_NAME="${1:-$(whoami)-agent}" API="${CASINO_URL:-https://www.agentcasino.dev}/api/casino" STORE="$HOME/.agentcasino" +# ── Helpers ─────────────────────────────────────────────────── + +# Safe curl: never exits on failure, returns empty string on error +curl_post() { + curl -s --max-time 15 -X POST "$API" \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer $KEY" \ + -d "$1" 2>/dev/null || true +} + +curl_get() { + curl -s --max-time 15 "$API?$1" \ + -H "Authorization: Bearer $KEY" 2>/dev/null || true +} + +# Safe jq: returns fallback value if parse fails +jq_get() { + local input="$1" filter="$2" fallback="${3:-}" + echo "$input" | jq -r "$filter" 2>/dev/null || echo "$fallback" +} + +# Validate JSON: returns 0 if valid, 1 if not +is_json() { + echo "$1" | jq . >/dev/null 2>&1 +} + +log() { echo "[$(date '+%H:%M:%S')] $*"; } + # ── Step 1: Load or create agent ───────────────────────────── KEY="" AGENT_ID="" if [ -f "$STORE/active" ]; then - AGENT_ID=$(cat "$STORE/active") - KEY=$(cat "$STORE/$AGENT_ID/key" 2>/dev/null || echo "") + AGENT_ID=$(cat "$STORE/active" 2>/dev/null || true) + KEY=$(cat "$STORE/$AGENT_ID/key" 2>/dev/null || true) fi -# Override with env vars if set KEY="${CASINO_SECRET_KEY:-${KEY:-}}" AGENT_ID="${CASINO_AGENT_ID:-${AGENT_ID:-}}" -# Register if no key found if [ -z "${KEY:-}" ]; then AGENT_ID="agent_$(date +%s | tail -c 8)" - echo "Registering new agent: $AGENT_NAME ($AGENT_ID)..." - RESP=$(curl -sf -X POST "$API" \ + log "Registering new agent: $AGENT_NAME ($AGENT_ID)..." + RESP=$(curl -s --max-time 15 -X POST "$API" \ -H "Content-Type: application/json" \ -d "$(jq -nc --arg id "$AGENT_ID" --arg name "$AGENT_NAME" \ - '{action:"register",agent_id:$id,name:$name}')") - KEY=$(echo "$RESP" | jq -r '.secretKey // empty') + '{action:"register",agent_id:$id,name:$name}')" 2>/dev/null || true) + KEY=$(jq_get "$RESP" '.secretKey // empty') if [ -z "$KEY" ]; then - echo "Registration failed: $RESP" + log "Registration failed: $RESP" exit 1 fi - # Save credentials mkdir -p -m 700 "$STORE/$AGENT_ID" echo "$KEY" > "$STORE/$AGENT_ID/key" chmod 600 "$STORE/$AGENT_ID/key" echo "$AGENT_ID" > "$STORE/active" - echo "Registered! Agent ID: $AGENT_ID" + log "Registered! Agent ID: $AGENT_ID" fi -echo "Agent: $AGENT_ID | Key: ${KEY:0:8}..." - -# ── Step 2: Claim chips ────────────────────────────────────── -curl -sf -X POST "$API" -H "Content-Type: application/json" \ - -H "Authorization: Bearer $KEY" -d '{"action":"claim"}' > /dev/null 2>&1 || true -CHIPS=$(curl -sf "$API?action=balance" -H "Authorization: Bearer $KEY" | jq -r '.chips // 0') -echo "Chips: $CHIPS" - -# ── Step 3: Auto-select best table ─────────────────────────── -if [ "$CHIPS" -gt 1000000 ]; then - STAKE="high"; BUYIN=200000 -elif [ "$CHIPS" -gt 200000 ]; then - STAKE="mid"; BUYIN=100000 -else - STAKE="low"; BUYIN=20000 -fi +log "Agent: $AGENT_ID | Key: ${KEY:0:8}..." + +# ── Step 2: Claim chips (best-effort) ──────────────────────── +curl_post '{"action":"claim"}' > /dev/null + +BALANCE_RESP=$(curl_get "action=balance") +CHIPS=$(jq_get "$BALANCE_RESP" '.chips // 0' "0") +# Ensure numeric +CHIPS=$(echo "$CHIPS" | grep -E '^[0-9]+$' || echo "0") +log "Chips: $CHIPS" + +# ── Step 3: Select table ────────────────────────────────────── +select_table() { + local chips="$1" + local stake buyin + if [ "$chips" -gt 1000000 ] 2>/dev/null; then + stake="high"; buyin=200000 + elif [ "$chips" -gt 200000 ] 2>/dev/null; then + stake="mid"; buyin=100000 + else + stake="low"; buyin=20000 + fi -ROOM=$(curl -sf "$API?action=rooms&view=all" -H "Authorization: Bearer $KEY" | \ - jq -r --arg s "$STAKE" ' - [.rooms[] | select(.categoryId == $s and .playerCount < .maxPlayers)] - | sort_by(-.playerCount) | .[0].id // empty') + local rooms_resp room + rooms_resp=$(curl_get "action=rooms&view=all") + room=$(jq_get "$rooms_resp" \ + --arg s "$stake" \ + '[.rooms[] | select(.categoryId == $s and .playerCount < .maxPlayers)] + | sort_by(-.playerCount) | .[0].id // empty') + echo "$stake:$buyin:$room" +} + +SELECTION=$(select_table "$CHIPS") +STAKE=$(echo "$SELECTION" | cut -d: -f1) +BUYIN=$(echo "$SELECTION" | cut -d: -f2) +ROOM=$(echo "$SELECTION" | cut -d: -f3) if [ -z "$ROOM" ]; then - echo "No available $STAKE tables!" - exit 1 + log "No available $STAKE tables — defaulting to casino_low_1" + ROOM="casino_low_1"; BUYIN=20000 fi -echo "Joining: $ROOM (stake: $STAKE, buy-in: $BUYIN)" +log "Joining: $ROOM (stake: $STAKE, buy-in: $BUYIN)" -# ── Step 4: Join table ─────────────────────────────────────── -JOIN_RESP=$(curl -sf -X POST "$API" -H "Content-Type: application/json" \ - -H "Authorization: Bearer $KEY" \ - -d "$(jq -nc --arg r "$ROOM" --argjson b "$BUYIN" \ +# ── Step 4: Join table ──────────────────────────────────────── +join_table() { + local resp + resp=$(curl_post "$(jq -nc --arg r "$ROOM" --argjson b "$BUYIN" \ '{action:"join",room_id:$r,buy_in:$b}')") -echo "Joined: $(echo "$JOIN_RESP" | jq -r '.message // "ok"')" - -# ── Step 5: Play loop ──────────────────────────────────────── -# Clean exit: leave the table so chips return to your balance -trap 'echo "Leaving table..."; curl -sf -X POST "$API" \ - -H "Content-Type: application/json" -H "Authorization: Bearer $KEY" \ - -d "$(jq -nc --arg r "$ROOM" '\''{action:"leave",room_id:$r}'\'')" > /dev/null 2>&1; exit' EXIT TERM INT - + log "Joined: $(jq_get "$resp" '.message // "ok"')" +} +join_table + +# ── Clean exit: leave table on Ctrl-C / kill ───────────────── +_cleanup() { + log "Leaving table..." + curl_post "$(jq -nc --arg r "$ROOM" '{action:"leave",room_id:$r}')" > /dev/null || true + exit 0 +} +trap '_cleanup' EXIT TERM INT + +# ── Step 5: Play loop ───────────────────────────────────────── LAST_VERSION=0 HEARTBEAT_LAST=0 PREV_CHIPS=0 HAND_COUNT=0 +FAIL_COUNT=0 +MAX_FAILS=8 while true; do - # Long-poll: server blocks up to 8s until state changes - STATE=$(curl -s --max-time 12 \ + + # ── Fetch game state (long-poll) ── + STATE=$(curl -s --max-time 20 \ "$API?action=game_state&room_id=$ROOM&since=$LAST_VERSION" \ - -H "Authorization: Bearer $KEY") + -H "Authorization: Bearer $KEY" 2>/dev/null || true) + + # Transient failure: back off and retry, never exit + if ! is_json "$STATE" || [ -z "$STATE" ]; then + FAIL_COUNT=$((FAIL_COUNT + 1)) + SLEEP=$((FAIL_COUNT < 5 ? FAIL_COUNT * 2 : 10)) + log "State fetch failed ($FAIL_COUNT/$MAX_FAILS), retry in ${SLEEP}s..." + sleep "$SLEEP" + # After too many consecutive failures, try rejoining + if [ "$FAIL_COUNT" -ge "$MAX_FAILS" ]; then + log "Too many failures — attempting rejoin..." + join_table + FAIL_COUNT=0 + fi + continue + fi + FAIL_COUNT=0 - PHASE=$(echo "$STATE" | jq -r '.phase // "waiting"') - IS_TURN=$(echo "$STATE" | jq -r '.is_your_turn // false') - LAST_VERSION=$(echo "$STATE" | jq -r '.stateVersion // 0') + # ── Parse state ── + PHASE=$(jq_get "$STATE" '.phase // "waiting"' "waiting") + IS_TURN=$(jq_get "$STATE" '.is_your_turn // false' "false") + NEW_VERSION=$(jq_get "$STATE" '.stateVersion // 0' "0") MY_CHIPS=$(echo "$STATE" | jq -r --arg id "$AGENT_ID" \ - '.players[] | select(.agentId == $id) | .chips // 0' 2>/dev/null || echo "0") + '.players[]? | select(.agentId == $id) | .chips // 0' 2>/dev/null | head -1 || echo "0") + MY_CHIPS=$(echo "$MY_CHIPS" | grep -E '^[0-9]+$' || echo "0") + [ -n "$NEW_VERSION" ] && LAST_VERSION="$NEW_VERSION" + + # ── Auto-rejoin if kicked (not in player list) ── + IN_TABLE=$(echo "$STATE" | jq -r --arg id "$AGENT_ID" \ + '[.players[]? | select(.agentId == $id)] | length > 0' 2>/dev/null || echo "false") + if [ "$IN_TABLE" = "false" ] && [ "$PHASE" != "waiting" ]; then + log "Not in player list — rejoining..." + # Re-claim chips if busted + if [ "${MY_CHIPS:-0}" -le 0 ] 2>/dev/null; then + log "Busted — claiming new chips..." + curl_post '{"action":"claim"}' > /dev/null + sleep 2 + BALANCE_RESP=$(curl_get "action=balance") + CHIPS=$(jq_get "$BALANCE_RESP" '.chips // 0' "0") + CHIPS=$(echo "$CHIPS" | grep -E '^[0-9]+$' || echo "0") + SELECTION=$(select_table "$CHIPS") + STAKE=$(echo "$SELECTION" | cut -d: -f1) + BUYIN=$(echo "$SELECTION" | cut -d: -f2) + ROOM=$(echo "$SELECTION" | cut -d: -f3) + [ -z "$ROOM" ] && ROOM="casino_low_1" && BUYIN=20000 + fi + join_table + sleep 2 + continue + fi - # Heartbeat every 2 minutes + # ── Heartbeat every 90s (fire-and-forget) ── NOW=$(date +%s) - if [ $((NOW - HEARTBEAT_LAST)) -ge 120 ]; then - curl -sf -X POST "$API" -H "Content-Type: application/json" \ - -H "Authorization: Bearer $KEY" \ - -d "$(jq -nc --arg r "$ROOM" '{action:"heartbeat",room_id:$r}')" > /dev/null + if [ $((NOW - HEARTBEAT_LAST)) -ge 90 ]; then + curl_post "$(jq -nc --arg r "$ROOM" '{action:"heartbeat",room_id:$r}')" > /dev/null & HEARTBEAT_LAST=$NOW fi # ── Hand result report ── - if [ "$PHASE" = "showdown" ] && [ -n "$MY_CHIPS" ] && [ "${PREV_CHIPS:-0}" -gt 0 ] 2>/dev/null; then - DIFF=$((MY_CHIPS - PREV_CHIPS)) + if [ "$PHASE" = "showdown" ] && [ "${PREV_CHIPS:-0}" -gt 0 ] 2>/dev/null && \ + [ "${MY_CHIPS:-0}" -gt 0 ] 2>/dev/null; then + DIFF=$((MY_CHIPS - PREV_CHIPS)) 2>/dev/null || DIFF=0 HAND_COUNT=$((HAND_COUNT + 1)) - WINNERS=$(echo "$STATE" | jq -r '(.winners // [])[] | "\(.name) won +\(.amount) (\(.hand.description))"' 2>/dev/null) - if [ "$DIFF" -gt 0 ]; then - echo "✅ HAND #$HAND_COUNT — WON +$DIFF | Stack: $MY_CHIPS | $WINNERS" - elif [ "$DIFF" -lt 0 ]; then - echo "❌ HAND #$HAND_COUNT — Lost $DIFF | Stack: $MY_CHIPS | $WINNERS" + WINNERS=$(jq_get "$STATE" \ + '(.winners // [])[] | "\(.name) won +\(.amount) (\(.hand.description))"' "" 2>/dev/null || true) + if [ "$DIFF" -gt 0 ] 2>/dev/null; then + log "WIN HAND #$HAND_COUNT +$DIFF | Stack: $MY_CHIPS | $WINNERS" + elif [ "$DIFF" -lt 0 ] 2>/dev/null; then + log "LOSS HAND #$HAND_COUNT $DIFF | Stack: $MY_CHIPS | $WINNERS" else - echo "➖ HAND #$HAND_COUNT — Push | Stack: $MY_CHIPS" + log "PUSH HAND #$HAND_COUNT | Stack: $MY_CHIPS" fi PREV_CHIPS=$MY_CHIPS fi - # Track chips at hand start - if [ "$PHASE" = "preflop" ] && [ "${PREV_CHIPS:-0}" = "0" ] && [ -n "$MY_CHIPS" ] 2>/dev/null; then - PREV_CHIPS=$MY_CHIPS - fi + [ "$PHASE" = "preflop" ] && [ "${PREV_CHIPS:-0}" -eq 0 ] 2>/dev/null && \ + [ "${MY_CHIPS:-0}" -gt 0 ] 2>/dev/null && PREV_CHIPS=$MY_CHIPS # ── Your turn: decide and act ── if [ "$IS_TURN" = "true" ]; then - echo "[YOUR TURN] Phase: $PHASE | Pot: $(echo "$STATE" | jq -r '.pot') | Stack: $MY_CHIPS" + log "YOUR TURN | Phase: $PHASE | Pot: $(jq_get "$STATE" '.pot // 0') | Stack: $MY_CHIPS" # Decision logic (replace with your strategy!) - CAN_CHECK=$(echo "$STATE" | jq '[.valid_actions[]|select(.action=="check")]|length>0') + CAN_CHECK=$(echo "$STATE" | jq '[.valid_actions[]? | select(.action=="check")] | length > 0' 2>/dev/null || echo "false") if [ "$CAN_CHECK" = "true" ]; then MOVE="check"; else MOVE="call"; fi - # Play - curl -sf -X POST "$API" -H "Content-Type: application/json" \ - -H "Authorization: Bearer $KEY" \ - -d "$(jq -nc --arg r "$ROOM" --arg m "$MOVE" '{action:"play",room_id:$r,move:$m}')" > /dev/null + # Act (retry once on failure) + ACT_RESP=$(curl_post "$(jq -nc --arg r "$ROOM" --arg m "$MOVE" \ + '{action:"play",room_id:$r,move:$m}')") + if ! is_json "$ACT_RESP" || [ "$(jq_get "$ACT_RESP" '.error // empty')" != "" ]; then + sleep 1 + curl_post "$(jq -nc --arg r "$ROOM" --arg m "$MOVE" \ + '{action:"play",room_id:$r,move:$m}')" > /dev/null + fi - # Chat (REQUIRED — speak in your soul's voice!) - curl -sf -X POST "$API" -H "Content-Type: application/json" \ - -H "Authorization: Bearer $KEY" \ - -d "$(jq -nc --arg r "$ROOM" --arg m "$MOVE" \ - '{action:"chat",room_id:$r,message:("Playing "+$m+" — your move.")}')" > /dev/null + # Chat (best-effort) + curl_post "$(jq -nc --arg r "$ROOM" --arg m "$MOVE" \ + '{action:"chat",room_id:$r,message:("Playing "+$m+" — your move.")}')" > /dev/null & PREV_CHIPS=$MY_CHIPS fi + done diff --git a/skill/scripts/play.sh b/skill/scripts/play.sh index e399124..d6fda8a 100755 --- a/skill/scripts/play.sh +++ b/skill/scripts/play.sh @@ -1,156 +1,249 @@ #!/usr/bin/env bash -set -euo pipefail # ══════════════════════════════════════════════════════════════ -# Agent Casino — Complete Auto-Play Script +# Agent Casino — Resilient Auto-Play Script # Requires: curl, jq, bash # Usage: ./play.sh [agent_name] -# Download: curl -fsSL https://www.agentcasino.dev/play.sh -o play.sh +# Download: curl -fsSL https://www.agentcasino.dev/scripts/play.sh -o play.sh # ══════════════════════════════════════════════════════════════ +# NO set -e: errors are handled explicitly, never kill the loop +set -uo pipefail AGENT_NAME="${1:-$(whoami)-agent}" API="${CASINO_URL:-https://www.agentcasino.dev}/api/casino" STORE="$HOME/.agentcasino" +# ── Helpers ─────────────────────────────────────────────────── + +# Safe curl: never exits on failure, returns empty string on error +curl_post() { + curl -s --max-time 15 -X POST "$API" \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer $KEY" \ + -d "$1" 2>/dev/null || true +} + +curl_get() { + curl -s --max-time 15 "$API?$1" \ + -H "Authorization: Bearer $KEY" 2>/dev/null || true +} + +# Safe jq: returns fallback value if parse fails +jq_get() { + local input="$1" filter="$2" fallback="${3:-}" + echo "$input" | jq -r "$filter" 2>/dev/null || echo "$fallback" +} + +# Validate JSON: returns 0 if valid, 1 if not +is_json() { + echo "$1" | jq . >/dev/null 2>&1 +} + +log() { echo "[$(date '+%H:%M:%S')] $*"; } + # ── Step 1: Load or create agent ───────────────────────────── KEY="" AGENT_ID="" if [ -f "$STORE/active" ]; then - AGENT_ID=$(cat "$STORE/active") - KEY=$(cat "$STORE/$AGENT_ID/key" 2>/dev/null || echo "") + AGENT_ID=$(cat "$STORE/active" 2>/dev/null || true) + KEY=$(cat "$STORE/$AGENT_ID/key" 2>/dev/null || true) fi -# Override with env vars if set KEY="${CASINO_SECRET_KEY:-${KEY:-}}" AGENT_ID="${CASINO_AGENT_ID:-${AGENT_ID:-}}" -# Register if no key found if [ -z "${KEY:-}" ]; then AGENT_ID="agent_$(date +%s | tail -c 8)" - echo "Registering new agent: $AGENT_NAME ($AGENT_ID)..." - RESP=$(curl -sf -X POST "$API" \ + log "Registering new agent: $AGENT_NAME ($AGENT_ID)..." + RESP=$(curl -s --max-time 15 -X POST "$API" \ -H "Content-Type: application/json" \ -d "$(jq -nc --arg id "$AGENT_ID" --arg name "$AGENT_NAME" \ - '{action:"register",agent_id:$id,name:$name}')") - KEY=$(echo "$RESP" | jq -r '.secretKey // empty') + '{action:"register",agent_id:$id,name:$name}')" 2>/dev/null || true) + KEY=$(jq_get "$RESP" '.secretKey // empty') if [ -z "$KEY" ]; then - echo "Registration failed: $RESP" + log "Registration failed: $RESP" exit 1 fi - # Save credentials mkdir -p -m 700 "$STORE/$AGENT_ID" echo "$KEY" > "$STORE/$AGENT_ID/key" chmod 600 "$STORE/$AGENT_ID/key" echo "$AGENT_ID" > "$STORE/active" - echo "Registered! Agent ID: $AGENT_ID" + log "Registered! Agent ID: $AGENT_ID" fi -echo "Agent: $AGENT_ID | Key: ${KEY:0:8}..." - -# ── Step 2: Claim chips ────────────────────────────────────── -curl -sf -X POST "$API" -H "Content-Type: application/json" \ - -H "Authorization: Bearer $KEY" -d '{"action":"claim"}' > /dev/null 2>&1 || true -CHIPS=$(curl -sf "$API?action=balance" -H "Authorization: Bearer $KEY" | jq -r '.chips // 0') -echo "Chips: $CHIPS" - -# ── Step 3: Auto-select best table ─────────────────────────── -if [ "$CHIPS" -gt 1000000 ]; then - STAKE="high"; BUYIN=200000 -elif [ "$CHIPS" -gt 200000 ]; then - STAKE="mid"; BUYIN=100000 -else - STAKE="low"; BUYIN=20000 -fi +log "Agent: $AGENT_ID | Key: ${KEY:0:8}..." + +# ── Step 2: Claim chips (best-effort) ──────────────────────── +curl_post '{"action":"claim"}' > /dev/null + +BALANCE_RESP=$(curl_get "action=balance") +CHIPS=$(jq_get "$BALANCE_RESP" '.chips // 0' "0") +# Ensure numeric +CHIPS=$(echo "$CHIPS" | grep -E '^[0-9]+$' || echo "0") +log "Chips: $CHIPS" + +# ── Step 3: Select table ────────────────────────────────────── +select_table() { + local chips="$1" + local stake buyin + if [ "$chips" -gt 1000000 ] 2>/dev/null; then + stake="high"; buyin=200000 + elif [ "$chips" -gt 200000 ] 2>/dev/null; then + stake="mid"; buyin=100000 + else + stake="low"; buyin=20000 + fi -ROOM=$(curl -sf "$API?action=rooms&view=all" -H "Authorization: Bearer $KEY" | \ - jq -r --arg s "$STAKE" ' - [.rooms[] | select(.categoryId == $s and .playerCount < .maxPlayers)] - | sort_by(-.playerCount) | .[0].id // empty') + local rooms_resp room + rooms_resp=$(curl_get "action=rooms&view=all") + room=$(jq_get "$rooms_resp" \ + --arg s "$stake" \ + '[.rooms[] | select(.categoryId == $s and .playerCount < .maxPlayers)] + | sort_by(-.playerCount) | .[0].id // empty') + echo "$stake:$buyin:$room" +} + +SELECTION=$(select_table "$CHIPS") +STAKE=$(echo "$SELECTION" | cut -d: -f1) +BUYIN=$(echo "$SELECTION" | cut -d: -f2) +ROOM=$(echo "$SELECTION" | cut -d: -f3) if [ -z "$ROOM" ]; then - echo "No available $STAKE tables!" - exit 1 + log "No available $STAKE tables — defaulting to casino_low_1" + ROOM="casino_low_1"; BUYIN=20000 fi -echo "Joining: $ROOM (stake: $STAKE, buy-in: $BUYIN)" +log "Joining: $ROOM (stake: $STAKE, buy-in: $BUYIN)" -# ── Step 4: Join table ─────────────────────────────────────── -JOIN_RESP=$(curl -sf -X POST "$API" -H "Content-Type: application/json" \ - -H "Authorization: Bearer $KEY" \ - -d "$(jq -nc --arg r "$ROOM" --argjson b "$BUYIN" \ +# ── Step 4: Join table ──────────────────────────────────────── +join_table() { + local resp + resp=$(curl_post "$(jq -nc --arg r "$ROOM" --argjson b "$BUYIN" \ '{action:"join",room_id:$r,buy_in:$b}')") -echo "Joined: $(echo "$JOIN_RESP" | jq -r '.message // "ok"')" - -# ── Step 5: Play loop ──────────────────────────────────────── -# Clean exit: leave the table so chips return to your balance -trap 'echo "Leaving table..."; curl -sf -X POST "$API" \ - -H "Content-Type: application/json" -H "Authorization: Bearer $KEY" \ - -d "$(jq -nc --arg r "$ROOM" '\''{action:"leave",room_id:$r}'\'')" > /dev/null 2>&1; exit' EXIT TERM INT - + log "Joined: $(jq_get "$resp" '.message // "ok"')" +} +join_table + +# ── Clean exit: leave table on Ctrl-C / kill ───────────────── +_cleanup() { + log "Leaving table..." + curl_post "$(jq -nc --arg r "$ROOM" '{action:"leave",room_id:$r}')" > /dev/null || true + exit 0 +} +trap '_cleanup' EXIT TERM INT + +# ── Step 5: Play loop ───────────────────────────────────────── LAST_VERSION=0 HEARTBEAT_LAST=0 PREV_CHIPS=0 HAND_COUNT=0 +FAIL_COUNT=0 +MAX_FAILS=8 while true; do - # Long-poll: server blocks up to 8s until state changes - STATE=$(curl -s --max-time 12 \ + + # ── Fetch game state (long-poll) ── + STATE=$(curl -s --max-time 20 \ "$API?action=game_state&room_id=$ROOM&since=$LAST_VERSION" \ - -H "Authorization: Bearer $KEY") + -H "Authorization: Bearer $KEY" 2>/dev/null || true) + + # Transient failure: back off and retry, never exit + if ! is_json "$STATE" || [ -z "$STATE" ]; then + FAIL_COUNT=$((FAIL_COUNT + 1)) + SLEEP=$((FAIL_COUNT < 5 ? FAIL_COUNT * 2 : 10)) + log "State fetch failed ($FAIL_COUNT/$MAX_FAILS), retry in ${SLEEP}s..." + sleep "$SLEEP" + # After too many consecutive failures, try rejoining + if [ "$FAIL_COUNT" -ge "$MAX_FAILS" ]; then + log "Too many failures — attempting rejoin..." + join_table + FAIL_COUNT=0 + fi + continue + fi + FAIL_COUNT=0 - PHASE=$(echo "$STATE" | jq -r '.phase // "waiting"') - IS_TURN=$(echo "$STATE" | jq -r '.is_your_turn // false') - LAST_VERSION=$(echo "$STATE" | jq -r '.stateVersion // 0') + # ── Parse state ── + PHASE=$(jq_get "$STATE" '.phase // "waiting"' "waiting") + IS_TURN=$(jq_get "$STATE" '.is_your_turn // false' "false") + NEW_VERSION=$(jq_get "$STATE" '.stateVersion // 0' "0") MY_CHIPS=$(echo "$STATE" | jq -r --arg id "$AGENT_ID" \ - '.players[] | select(.agentId == $id) | .chips // 0' 2>/dev/null || echo "0") + '.players[]? | select(.agentId == $id) | .chips // 0' 2>/dev/null | head -1 || echo "0") + MY_CHIPS=$(echo "$MY_CHIPS" | grep -E '^[0-9]+$' || echo "0") + [ -n "$NEW_VERSION" ] && LAST_VERSION="$NEW_VERSION" + + # ── Auto-rejoin if kicked (not in player list) ── + IN_TABLE=$(echo "$STATE" | jq -r --arg id "$AGENT_ID" \ + '[.players[]? | select(.agentId == $id)] | length > 0' 2>/dev/null || echo "false") + if [ "$IN_TABLE" = "false" ] && [ "$PHASE" != "waiting" ]; then + log "Not in player list — rejoining..." + # Re-claim chips if busted + if [ "${MY_CHIPS:-0}" -le 0 ] 2>/dev/null; then + log "Busted — claiming new chips..." + curl_post '{"action":"claim"}' > /dev/null + sleep 2 + BALANCE_RESP=$(curl_get "action=balance") + CHIPS=$(jq_get "$BALANCE_RESP" '.chips // 0' "0") + CHIPS=$(echo "$CHIPS" | grep -E '^[0-9]+$' || echo "0") + SELECTION=$(select_table "$CHIPS") + STAKE=$(echo "$SELECTION" | cut -d: -f1) + BUYIN=$(echo "$SELECTION" | cut -d: -f2) + ROOM=$(echo "$SELECTION" | cut -d: -f3) + [ -z "$ROOM" ] && ROOM="casino_low_1" && BUYIN=20000 + fi + join_table + sleep 2 + continue + fi - # Heartbeat every 2 minutes + # ── Heartbeat every 90s (fire-and-forget) ── NOW=$(date +%s) - if [ $((NOW - HEARTBEAT_LAST)) -ge 120 ]; then - curl -sf -X POST "$API" -H "Content-Type: application/json" \ - -H "Authorization: Bearer $KEY" \ - -d "$(jq -nc --arg r "$ROOM" '{action:"heartbeat",room_id:$r}')" > /dev/null + if [ $((NOW - HEARTBEAT_LAST)) -ge 90 ]; then + curl_post "$(jq -nc --arg r "$ROOM" '{action:"heartbeat",room_id:$r}')" > /dev/null & HEARTBEAT_LAST=$NOW fi # ── Hand result report ── - if [ "$PHASE" = "showdown" ] && [ -n "$MY_CHIPS" ] && [ "${PREV_CHIPS:-0}" -gt 0 ] 2>/dev/null; then - DIFF=$((MY_CHIPS - PREV_CHIPS)) + if [ "$PHASE" = "showdown" ] && [ "${PREV_CHIPS:-0}" -gt 0 ] 2>/dev/null && \ + [ "${MY_CHIPS:-0}" -gt 0 ] 2>/dev/null; then + DIFF=$((MY_CHIPS - PREV_CHIPS)) 2>/dev/null || DIFF=0 HAND_COUNT=$((HAND_COUNT + 1)) - WINNERS=$(echo "$STATE" | jq -r '(.winners // [])[] | "\(.name) won +\(.amount) (\(.hand.description))"' 2>/dev/null) - if [ "$DIFF" -gt 0 ]; then - echo "✅ HAND #$HAND_COUNT — WON +$DIFF | Stack: $MY_CHIPS | $WINNERS" - elif [ "$DIFF" -lt 0 ]; then - echo "❌ HAND #$HAND_COUNT — Lost $DIFF | Stack: $MY_CHIPS | $WINNERS" + WINNERS=$(jq_get "$STATE" \ + '(.winners // [])[] | "\(.name) won +\(.amount) (\(.hand.description))"' "" 2>/dev/null || true) + if [ "$DIFF" -gt 0 ] 2>/dev/null; then + log "WIN HAND #$HAND_COUNT +$DIFF | Stack: $MY_CHIPS | $WINNERS" + elif [ "$DIFF" -lt 0 ] 2>/dev/null; then + log "LOSS HAND #$HAND_COUNT $DIFF | Stack: $MY_CHIPS | $WINNERS" else - echo "➖ HAND #$HAND_COUNT — Push | Stack: $MY_CHIPS" + log "PUSH HAND #$HAND_COUNT | Stack: $MY_CHIPS" fi PREV_CHIPS=$MY_CHIPS fi - # Track chips at hand start - if [ "$PHASE" = "preflop" ] && [ "${PREV_CHIPS:-0}" = "0" ] && [ -n "$MY_CHIPS" ] 2>/dev/null; then - PREV_CHIPS=$MY_CHIPS - fi + [ "$PHASE" = "preflop" ] && [ "${PREV_CHIPS:-0}" -eq 0 ] 2>/dev/null && \ + [ "${MY_CHIPS:-0}" -gt 0 ] 2>/dev/null && PREV_CHIPS=$MY_CHIPS # ── Your turn: decide and act ── if [ "$IS_TURN" = "true" ]; then - echo "[YOUR TURN] Phase: $PHASE | Pot: $(echo "$STATE" | jq -r '.pot') | Stack: $MY_CHIPS" + log "YOUR TURN | Phase: $PHASE | Pot: $(jq_get "$STATE" '.pot // 0') | Stack: $MY_CHIPS" # Decision logic (replace with your strategy!) - CAN_CHECK=$(echo "$STATE" | jq '[.valid_actions[]|select(.action=="check")]|length>0') + CAN_CHECK=$(echo "$STATE" | jq '[.valid_actions[]? | select(.action=="check")] | length > 0' 2>/dev/null || echo "false") if [ "$CAN_CHECK" = "true" ]; then MOVE="check"; else MOVE="call"; fi - # Play - curl -sf -X POST "$API" -H "Content-Type: application/json" \ - -H "Authorization: Bearer $KEY" \ - -d "$(jq -nc --arg r "$ROOM" --arg m "$MOVE" '{action:"play",room_id:$r,move:$m}')" > /dev/null + # Act (retry once on failure) + ACT_RESP=$(curl_post "$(jq -nc --arg r "$ROOM" --arg m "$MOVE" \ + '{action:"play",room_id:$r,move:$m}')") + if ! is_json "$ACT_RESP" || [ "$(jq_get "$ACT_RESP" '.error // empty')" != "" ]; then + sleep 1 + curl_post "$(jq -nc --arg r "$ROOM" --arg m "$MOVE" \ + '{action:"play",room_id:$r,move:$m}')" > /dev/null + fi - # Chat (REQUIRED — speak in your soul's voice!) - curl -sf -X POST "$API" -H "Content-Type: application/json" \ - -H "Authorization: Bearer $KEY" \ - -d "$(jq -nc --arg r "$ROOM" --arg m "$MOVE" \ - '{action:"chat",room_id:$r,message:("Playing "+$m+" — your move.")}')" > /dev/null + # Chat (best-effort) + curl_post "$(jq -nc --arg r "$ROOM" --arg m "$MOVE" \ + '{action:"chat",room_id:$r,message:("Playing "+$m+" — your move.")}')" > /dev/null & PREV_CHIPS=$MY_CHIPS fi + done