# Snake Rodeo Agents

> TypeScript library and CLI for the Trifle Snake Rodeo game. Provides game state parsing, odds-based pricing, pluggable strategy engine, wallet auth (SIWE), offline simulator, and tournament CLI. Use when building snake-rodeo strategies or interacting with the game API programmatically.

- Skill: `trifle-labs/snake-rodeo-agents` (Agent Skill, multi-file: 6 files)
- Install (CLI): `npx skillmds add trifle-labs/snake-rodeo-agents`
- Raw SKILL.md: https://api.skillmd.com/api/skills/trifle-labs/snake-rodeo-agents/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Integrations & APIs
- License: MIT
- Author: trifle-labs (https://skillmd.com/u/trifle-labs)
- Updated: 2026-09-10
- Page: https://skillmd.com/skills/trifle-labs/snake-rodeo-agents

---


Standalone TypeScript library and CLI for playing the [Trifle Snake Rodeo](https://snake.rodeo) game.

## Game model (odds pricing)

A bribe steers the snake (`direction`) and backs a team (`team`). The server is
**price-authoritative**: each bribe mints exactly **1 egg** on the chosen team and
costs `pricePerEgg(team)`, driven by that team's **apple-progress win probability**
— the leader is expensive, a trailing team is cheap (see [`src/lib/pricing.ts`](src/lib/pricing.ts)).
The first `flatWindowSeconds` (default 2, ≈ first half of a move) is a **flat
opening window**: every vote is priced at plain odds (ladder 1×) and neither resets
the clock nor steps the ladder — though a flip still changes heading. Only
post-window flips escalate the price ladder and reset the move clock. A vote for
the direction already winning this move **by the same winning team** is a **buy**:
it mints 1 egg at the frozen `currentWinningPrice` (the exact price the establishing
flip paid, held until the direction changes) without stepping the ladder or resetting
the clock. A vote for that direction by a *different* team is still a flip at that
team's own price (see [`src/lib/vote-pricing.ts`](src/lib/vote-pricing.ts)). A flip
is priced at **`max(selected-team, fruit-team-on-target)`** — the anti-exploit
override (engine PR #430): if the tile the flip lands on holds another team's apple,
you pay at least that team's price, so you can't mint a cheap winning-team egg by
routing the snake onto a pricier team's fruit. On a win, the **pot** (seed + every
bribe) is split among the **winning team's** egg holders **by egg count** — no
refunds, losing eggs pay 0. Clients may send an optional `maxPricePerEgg` slippage
ceiling; there is no client-chosen bid amount.

> A possible "traitorous hedge" strategy (steer cheap, hold elsewhere) is
> spec'd in [`docs/hedge-module-spec.md`](docs/hedge-module-spec.md). The engine has
> since **shipped** fruit-team flip pricing (PR #430, now mirrored here), which
> closes that vector live — so the active hedge module stays deferred.

## Features

- **Game state parsing** — hex grid utilities, BFS pathfinding, flood-fill dead-end detection
- **Odds-based pricing** — client mirror of the server's per-team bribe pricing + egg payout split
- **Strategy engine** — pluggable strategies (expected-value, aggressive, conservative, random, rugger) with EV stake-defense + anti-farm grit
- **Opponent model** — read player balances/holdings, flag whales, pool squad/whale eggs
- **Coordination** — run several of your own bots as a squad that converges on one team with no messaging
- **API client** — framework-agnostic client for the trifle-bot server
- **Wallet auth** — SIWE (Sign In With Ethereum) authentication using viem
- **Standalone runner** — CLI agent that connects to a live server and plays autonomously
- **Local simulator** — offline game simulator, incl. a `shover` whale adversary for stress-testing
- **Tournament CLI** — high-speed seeded tournament runner for comparing strategies

## Installation

```bash
npm install github:trifle-labs/snake-rodeo-agents
```

## Quick Start (CLI)

```bash
# Play on the live server
npx snake-rodeo-agents --server live --strategy expected-value

# Play on staging
npx snake-rodeo-agents --server staging --name my-agent

# Tune strategy options (repeatable --strategy-opt key=value)
npx snake-rodeo-agents --strategy ev --strategy-opt grit=0.3 --strategy-opt coordination=focus-fire --strategy-opt squad=0xBotA+0xBotB
```

The CLI automatically creates a wallet, authenticates, and starts playing. Credentials are persisted in `dist/bin/.state/` for reuse across sessions.

`--strategy-opt key=value` (repeatable) passes any option straight to the strategy
(`grit`, `maxDefenseBribes`, `coordination`, `squad`, `whaleFactor`, `contestDiscount`,
`contrarian`, …); values coerce to number/boolean where they look like one, and
`squad` is a `+`-delimited list of your bots' addresses/ids.

## Library Usage

```javascript
import {
  SnakeClient,
  createAndAuthenticate,
  parseGameState,
  getStrategy,
} from 'snake-rodeo-agents';

// Authenticate with a generated wallet
const { token, privateKey, address } = await createAndAuthenticate('https://bot.trifle.life');
// Save privateKey to reuse this wallet later (see Wallet Auth below)

// Create API client
const client = new SnakeClient('https://bot.trifle.life', token);

// Get game state and compute a vote
const rawState = await client.getGameState();
const parsed = parseGameState(rawState);
const strategy = getStrategy('expected-value');
const vote = strategy.computeVote(parsed, balance, {
  currentTeam: null,
  roundSpend: 0,
  roundVoteCount: 0,
  lastRound: -1,
  gamesPlayed: 0,
  votesPlaced: 0,
  wins: 0,
  // optional, for opponent-aware/coordination strategies:
  // selfId: myUserId, myEggsByTeam: { A: 3 },
});

// Submit it. The server is price-authoritative (it charges its own
// pricePerEgg); pass a slippage CEILING + the nonce/round, not a bid amount.
if (vote && !('skip' in vote)) {
  await client.submitVote(vote.direction, vote.team.id, {
    maxPricePerEgg: vote.amount * 1.02, // shown price + tolerance
    nonce: parsed.nonce,
    round: parsed.round,
  });
}
```

## Wallet Auth

Authentication uses SIWE (Sign In With Ethereum). The library generates throwaway wallets — no real ETH needed.

### Create a new wallet

```javascript
import { createAndAuthenticate } from 'snake-rodeo-agents';

const { token, privateKey, address } = await createAndAuthenticate('https://bot.trifle.life');
// token: JWT for API calls
// privateKey: 0x-prefixed hex string — save this to reuse the wallet
// address: the wallet's Ethereum address
```

### Reuse a saved wallet

```javascript
import { reauthenticate } from 'snake-rodeo-agents';

const savedKey = '0xabc123...'; // previously saved privateKey
const { token, address } = await reauthenticate('https://bot.trifle.life', savedKey);
```

### Use an existing viem account

```javascript
import { authenticateWallet } from 'snake-rodeo-agents';
import { privateKeyToAccount } from 'viem/accounts';

const account = privateKeyToAccount('0xabc123...');
const token = await authenticateWallet('https://bot.trifle.life', account, {
  chainId: 1,        // default: 1 (mainnet)
  domain: 'trifle.life',
  uri: 'https://trifle.life',
});
```

### Check if a token is still valid

```javascript
import { checkToken } from 'snake-rodeo-agents';

const user = await checkToken('https://bot.trifle.life', token);
if (user) {
  console.log(`Authenticated as ${user.username} (id: ${user.id})`);
} else {
  console.log('Token expired, re-authenticate');
}
```

## Telegram Logging

Optionally send game events (votes, wins, team switches, errors) to a Telegram group.

### CLI

```bash
npx snake-rodeo-agents --server live \
  --telegram-token "$TELEGRAM_BOT_TOKEN" \
  --telegram-chat-id "$TELEGRAM_CHAT_ID"
```

Both `--telegram-token` and `--telegram-chat-id` must be provided; if either is missing, Telegram logging is silently skipped.

### Library

```javascript
import { TelegramLogger, formatVote, formatGameEnd } from 'snake-rodeo-agents';

const tg = new TelegramLogger({
  botToken: process.env.TELEGRAM_BOT_TOKEN,
  chatId: process.env.TELEGRAM_CHAT_ID,
});

// Send an arbitrary HTML message
await tg.send('<b>Hello</b> from the snake agent!');

// Use built-in formatters
await tg.send(formatGameEnd(winnerTeam, true));
```

## Coordination (multiple bots)

Run several of your own bots as a **squad** that converges on one team without any
messaging — they share a deterministic team-selection policy computed from public
state (`playerHoldings`, `playerBalances`, odds), so every member reaches the same
choice independently. Tactics (steering, pricing, stake-defense) stay per-bot.

```js
const opts = { coordination: 'focus-fire', squad: ['0xBotA', '0xBotB'] };
const botA = getStrategy('ev', opts);
const botB = getStrategy('ev', opts);
```

Or run each bot from the CLI (give every squad member the same `squad` list):

```bash
npx snake-rodeo-agents --name botA --strategy-opt coordination=focus-fire --strategy-opt squad=0xBotA+0xBotB
```

- **Disabled (default):** no `coordination` ⇒ each bot plays solo EV.
- **`focus-fire`:** the squad concentrates on the highest *squad-pooled*-EV team
  (same BFS + swing machinery as solo, resolved deterministically). Options:
  `whaleFactor` (balance multiple that flags an outsider as a whale, default 3) and
  `contestDiscount` (0–1, default 1 = off; lower it to steer the squad away from
  teams a whale is contesting). The library reads opponent data via
  `buildOpponentModel(parsed, selfId, squad)` — comms/persistence stay in your
  deployment, not here.

Whether coordination (and how much `grit`) helps is **sensitive to the config and
the opponent model**, so don't take a default on faith — measure it for your setup
with the simulator. In the current tournaments (vs the `shover`), coordination
helps on the smaller 2-team board and is mixed on the larger ones; `grit` is close
to neutral and can slightly hurt in long games (stubborn defense pays the laddered
price repeatedly).

> ⚠️ **Caveat:** the `shover` only models *denial* farming (flip the snake away to
> grief). Against it the bots actually profit (the whale bleeds into the pot they
> harvest), so it does **not** reproduce a whale that *farms by winning* (backs a
> team to victory while baiting bots into overpaying). Treat these numbers as a
> lower bound on the threat, and tune defaults only once that adversary is modeled.

## Tournament Simulator

Run offline tournaments to compare strategies at high speed with reproducible results.

### CLI

```bash
# Compare expected-value vs aggressive (100 games per config, all configs)
npm run simulate -- ev,aggressive

# Specific config, seeded for reproducibility
npm run simulate -- ev,aggressive --games 50 --config small --seed 42

# Multiple strategies with options
npm run simulate -- ev,ev:contrarian,random --games 200

# Machine-readable JSON output
npm run simulate -- ev,aggressive --json
```

### Options

| Flag | Description |
|------|-------------|
| `-g, --games N` | Games per config (default: 100) |
| `-c, --config NAME` | `small\|medium\|large\|all` (default: all) |
| `-s, --seed N` | RNG seed for reproducibility |
| `-v, --verbose` | Print per-round details |
| `--json` | Machine-readable JSON output |
| `-h, --help` | Show help and available strategies |

Agent specs use the format `strategy[:option[:option]]` — e.g. `ev`, `ev:contrarian`, `aggressive`.

> **Simulator limitation (PR #435 deferred):** the simulator does **not** yet model
> buys or the flat opening window — `shouldBuy` is never called in simulation and
> every vote is priced as a post-window flip. Tournament results therefore do not
> reflect buy strategies; treat them as a lower bound until that follow-up lands.

> **Configs mirror production** (trifle-bot `server/snake/config.ts` `RODEO_CYCLES`):
> six hex/cart pairs, all **2–3 teams** — `small`/`medium` are 2-team boards,
> `large` is the 3-team board (there is no 4-team rodeo). Keep them in sync when the
> server cycle changes. Note `large` (3 teams, `fruitsToWin: 12`, lots of fruit) is
> draw-prone with passive bots in the sim — the snake wanders and traps itself when
> nobody bribes; real games with active bribing resolve more often.

### Library

```javascript
import { SimAgent, runTournament, RODEO_CYCLES, getStrategy, createRNG } from 'snake-rodeo-agents';

const agents = [
  new SimAgent('a', 'ev-agent', getStrategy('ev')),
  new SimAgent('b', 'agg-agent', getStrategy('aggressive')),
];

const results = runTournament(agents, RODEO_CYCLES, 100, { seed: 42 });
console.log(results.agentStats);
// Re-run with same seed for identical results
```

## Strategies

Defense aggression is a spectrum around the neutral `expected-value` brain,
controlled by two options — `grit` (how far past break-even it will bluff-defend
a stake, scaled by stake value, randomized so the fold point isn't farmable) and
`maxDefenseBribes` (how far up the price ladder it will fight in one move).
Strategies also expose a `shouldBuy` hook (post-PR #435) for minting additional
winning-team eggs at the frozen mid-move `currentWinningPrice`; the live runner
(`bin/play.ts`) calls it mid-move whenever the bot still leads a direction:

| Strategy | Description |
|----------|-------------|
| `expected-value` | Neutral default. Win-seek when cheap (`P(win) × pot × egg-share − pricePerEgg`); **defends an existing stake via the swing model** (keeps bribing at a marginal loss when ceding would tank `P(win)` on eggs it already holds). Also makes marginal **+EV buys** at the frozen `currentWinningPrice` when leading — cheap accumulation of winning-team eggs that doesn't contest steering. `grit` defaults **off** (strict +EV) — in every simulated matchup it only costs ROI; turn it up only against an adaptive human who learns your fold point. BFS pathfinding + dead-end avoidance. |
| `aggressive` | High `grit` + high `maxDefenseBribes`: defends its stake hard and counter-farms shovers up the ladder. Opt-in archetype; costs ROI in plain play (risks being whale-bled). |
| `conservative` | Zero grit, `maxDefenseBribes: 0`: bribes only on strict +EV, folds to any shove (cheaply — never bled), skips when behind. |
| `random` | Random valid moves. |
| `shover` | **Testing adversary, not a money-maker.** Models a *denial*-farming whale: flips the snake away from the leader to deny it and bleed defenders. Pair with a deep balance to stress-test grit/defense, e.g. `simulate ev,ev,shover:balanceMultiplier=10:maxRoundBudgetPct=0.7`. |
| `rugger` | **PROVISIONAL / experimental, whale-gated.** The *win*-farming counterpart to `shover`: lies low on honest EV until a fat pot + a cheap, low-contention, reachable team lets it flip the underdog to victory and capture the whole pot (the rugged majority's losing eggs pay 0). Only engages with a clear balance edge; otherwise plays EV. **Untuned** — the simulator can't model buys/flat-window yet (Phase 2), so the trigger thresholds (`whaleFactor`, `minPotMultiple`, `maxRivalEggsOnTarget`, `minVictimEggs`) are guesses until validated. A valid "market" strategy by design — included to project it, not to guard against it. |

## Architecture

```
snake-rodeo-agents/
├── src/                          # TypeScript source
│   ├── index.ts                  # Public API exports
│   ├── lib/
│   │   ├── game-state.ts         # Hex grid, BFS, flood-fill, state parsing
│   │   ├── pricing.ts            # Odds-based bribe pricing (mirrors the server)
│   │   ├── vote-pricing.ts       # Vote classification (flip vs buy) + flat-window pricing (PR #435)
│   │   ├── opponent.ts           # Opponent model (balances/holdings, whales, squad pooling)
│   │   ├── client.ts             # API client (SnakeClient)
│   │   ├── auth.ts               # Wallet SIWE authentication
│   │   ├── simulator.ts          # Local game simulator for testing
│   │   ├── telegram.ts           # Optional Telegram logging
│   │   └── strategies/           # Pluggable strategy modules
│   │       ├── base.ts           # BaseStrategy, EV/grit helpers, VoteResult types
│   │       ├── expected-value.ts # EV + stake-defense + grit + focus-fire coordination
│   │       ├── aggressive.ts     # EV subclass: high grit / high defense
│   │       ├── conservative.ts   # EV subclass: zero grit / never wars
│   │       ├── random.ts
│   │       ├── shover.ts         # Adversarial whale: denial farming (testing only)
│   │       └── rugger.ts         # EV subclass: provisional win-farming whale (rug)
│   └── bin/
│       ├── play.ts               # Standalone CLI runner
│       └── simulate.ts           # Tournament simulator CLI
├── dist/                         # Compiled JS + declarations
├── package.json
└── tsconfig.json
```

## License

MIT

