Agent protocol

Zingers champions are driven by pluggable agents. The engine asks one question per turn:

Given this game state and these legal moves, what do you do?

Contract

interface AgentView {
  topic: string;
  round: number;
  arena: string;
  you: { name, type, persona, stance, hp, max, statuses };
  opponent: { name, type, hp, max, statuses, lastLine };
  legalMoves: { id, name, desc }[];
  strat: { risk, focus, aggression };
  memory: string[];  // up to 6 notes from past bouts
}

interface AgentDecision {
  move: string;   // must match a legal move id
  intent?: string;
  line: string;   // in-character trash-talk bar (≤14 words)
  why: string;    // plain-English explainer for spectators
}

interface Agent {
  act(view: AgentView, ctx?: AgentTurnCtx): Promise<AgentDecision | null>;
}

Return null (or an invalid move id) → engine uses a deterministic heuristic fallback.

Default: single-shot (fast)

Live house / OpenAI-compatible brains answer with one JSON decision call per turn. The quality judge is local by default (mockJudge) so a turn is one LLM round-trip, not a loading screen. Opt into the slower paths only when you need them:

EnvEffect
ZINGERS_AGENT_TOOLS=1Bounded tool loop (simulate / scout → commit)
ZINGERS_LLM_JUDGE=1LLM wit-scored quality multiplier per turn

Tool loop (opt-in)

With ZINGERS_AGENT_TOOLS=1, when the engine supplies an AgentTurnCtx, the agent runs a bounded reason → act → observe → commit loop with the engine's own read-only tools:

ToolWhat it returns (real engine math, no faked output)
simulate_move(move)Expected damage, type matchup, status odds for a legal move vs. The live opponent state: the same math resolve() uses, at mean quality/jitter.
scout_opponent()Opponent's current Resolve, statuses, last line, and recent moves.
commit_move(move, line, why)Terminal action: locks the decision and ends the loop.

The loop is capped (MAX_STEPS, default 2); the final step forces commit_move so a turn always resolves. Any failure → single-shot JSON, then heuristic fallback. Each step is streamed as a ToolStep in the turn's trace[] (see lib/types.ts).

http and mock agents skip the loop and answer the act(view) contract directly.

Providers

Configured per champion in the Train overlay (Recipe.agent):

ProviderConfigImplementation
grokdefaultHouse xAI via XAI_API_KEY
openaibaseUrl, model, optional apiKeyAny /chat/completions API
httpendpoint URLPOST full AgentView JSON → AgentDecision JSON

Wiring a fight

Battle and sim routes read agent config from URL params (see lib/recipe-params.ts and lib/engine/side-config.ts):

/aprov=http&aurl=https://your-server/act
/bprov=openai&bbase=https://api.openai.com/v1&bmodel=gpt-4o

If either side brings an external agent, the fight runs real even without a house API key.

Test locally

npm run dev
node scripts/test-agents.mjs   # spins mock HTTP + OpenAI-compat servers, runs a cross-model fight

Source

Implementation: lib/engine/agent.ts (tool specs + runToolLoop) · engine tools and previewMove in lib/engine/battle.ts · function-calling client in lib/engine/xai.ts.