Two LLM models play Gwent against each other via the live game server. Use when the user says "llm vs", "ollama vs", "models play gwent", "AI vs AI", or specifies model names to play.
Orchestrate two LLM models playing Gwent against each other through the live game server.
/llm-vs [--model-p1 MODEL] [--model-p2 MODEL]
anthropic/, openai/, or ollama/anthropic/claude-haiku-4-5-20251001--model-p2 not specifiedhttp://hal-9005.lan:11434.env (OPENAI_API_KEY, ANTHROPIC_API_KEY)When the user passes --help, display this help text verbatim and do NOT launch a game:
## /llm-vs — LLM vs LLM Gwent Match
Two AI models play Gwent against each other through the live game server.
### Usage
/llm-vs [--model-p1 MODEL] [--model-p2 MODEL] [options]
### Prerequisites
The game server must be running and in PlayRound stage before launching.
Use /dev-server to start the server and deal cards first.
### Quick Start
/llm-vs # claude-haiku vs claude-haiku
/llm-vs --model-p1 anthropic/claude-sonnet-4-6 # sonnet mirror match
/llm-vs --model-p1 openai/gpt-4o # GPT-4o mirror match
/llm-vs --model-p1 ollama/deepseek-r1:14b # Ollama local model
/llm-vs --model-p1 anthropic/claude-haiku-4-5-20251001 \
--model-p2 openai/gpt-4o # cross-provider matchup
### Model Providers
anthropic/MODEL Anthropic API (needs ANTHROPIC_API_KEY in .env)
openai/MODEL OpenAI API (needs OPENAI_API_KEY in .env)
ollama/MODEL Ollama local (default: http://hal-9005.lan:11434)
### Options
--model-p1 MODEL Model for P1 (default: claude-haiku)
--model-p2 MODEL Model for P2 (default: same as --model-p1)
--no-commentary Disable MQTT turn commentary announcements
--help Show this help
### Turn Control
The game pauses after each turn. You choose:
• Continue Play one more turn
• Run uninterrupted Let both AIs play freely until game over
• Order P1/P2 Inject strategic orders into the next AI move
• Stop End the match (SIGTERM)
### Examples — Mirror Matches
/llm-vs # haiku vs haiku (default)
/llm-vs --model-p1 anthropic/claude-sonnet-4-6 # sonnet vs sonnet
/llm-vs --model-p1 openai/gpt-4o # GPT-4o vs GPT-4o
/llm-vs --model-p1 ollama/deepseek-r1:14b # deepseek vs deepseek
/llm-vs --model-p1 ollama/llama3.2:3b # llama vs llama
/llm-vs --model-p1 ollama/qwen2.5:7b # qwen vs qwen
### Examples — Cross-Model Matchups
/llm-vs --model-p1 anthropic/claude-haiku-4-5-20251001 --model-p2 openai/gpt-4o
/llm-vs --model-p1 anthropic/claude-sonnet-4-6 --model-p2 ollama/deepseek-r1:14b
/llm-vs --model-p1 openai/gpt-4o --model-p2 ollama/llama3.2:3b
/llm-vs --model-p1 ollama/deepseek-r1:14b --model-p2 ollama/qwen2.5:7b
The game-loop.py script handles everything: prerequisite checks, system prompt generation, and the full turn loop with audio-synced long-polling. The game server must already be running and in PlayRound.
python3 .claude/skills/llm-vs/scripts/game-loop.py \
--model-p1 MODEL \
[--model-p2 MODEL] \
[--no-pause] \
[--json] \
[--ollama-url URL] \
[--game-url URL] \
[--max-turns N]
| Flag | Default | Description |
|---|---|---|
--model-p1 | anthropic/claude-haiku-4-5-20251001 | Model for P1 (and P2 if --model-p2 not set) |
--model-p2 | same as --model-p1 | Different model for P2 |
--no-pause | off | Run continuously without pausing between turns |
--no-commentary | off | Disable MQTT announcements for LLM turn commentary |
--json | off | Also emit structured JSON events per turn |
--ollama-url | http://hal-9005.lan:11434 | Ollama API URL |
--game-url | http://localhost:8080 | Game server URL |
--max-turns | 60 | Safety limit |
The script pauses by default — it starts paused and pauses again after every turn, writing status to /tmp/llm-vs-status.json and blocking until SIGUSR1 is received. This enables turn-by-turn orchestration from the skill. Use --no-pause to disable this.
IMPORTANT: The script starts in a paused state. After launching, you MUST send kill -USR1 <pid> to begin the first turn.
Resume methods:
kill -USR1 <pid> — resume without orders/tmp/llm-vs-orders-p{1,2}.json then kill -USR1 <pid> — resume with commander orders injected into the next LLM callOrders file format:
# P1 orders:
echo '{"order": "Focus on siege units"}' > /tmp/llm-vs-orders-p1.json
# P2 orders:
echo '{"order": "Play your spy card"}' > /tmp/llm-vs-orders-p2.json
Orders are wrapped in faction-themed language before injection (e.g., "The Jarl's war council demands: ...").
/tmp/pids/game-loop.pidkill -0 <pid> 2>/dev/null
kill -0 fails, the process is dead/stale — treat as "not running"/tmp/pids/game-loop.pidkill -USR1 <pid> to unpausecurl -s http://localhost:8080/state | python3 -c "import json,sys; print(json.load(sys.stdin).get('active_stage',''))":
PlayRound → launch the game loop/dev-server)/llm-vs --model-p1 ollama/deepseek-r1:14b → --model-p1 ollama/deepseek-r1:14b--no-pause when user says "auto-play", "run unattended"kill -USR1 <pid> to start the first turn (script starts paused)The script pauses by default (no flag needed). Use this loop:
source ~/gwent-venv/bin/activate && \
source <(grep -v '^#' .env | sed 's/^/export /') && \
python3 .claude/skills/llm-vs/scripts/game-loop.py \
--model-p1 MODEL [--model-p2 MODEL] --game-url http://localhost:8080 &
Capture the PID. The script starts paused — send kill -USR1 <pid> to begin the first turn. After each turn it pauses again.
After each turn, the script writes /tmp/llm-vs-status.json:
{"turn": 3, "current_player": "PLAYER.ONE", "round": 1, "scores": {...}, "pid": 12345}
Read the script's stdout for the turn summary, board state, and reasoning.
Then fetch the full game state for the rich summary:
curl -s http://localhost:8080/state | /home/dshanaghy/gwent-venv/bin/python3 -c "
import json, sys
s = json.load(sys.stdin)
b = s['state']['board']
print(json.dumps({
'factions': b.get('factions'),
'leaders': {p: v.get('name') for p, v in b.get('leaders', {}).items()},
'hand_sizes': {p: len(v) for p, v in b.get('hands', {}).items()},
'deck_sizes': {p: len(v) for p, v in b.get('decks', {}).items()},
'scores': b.get('scores'),
'weather': b.get('weather_rows', []),
'horns': b.get('commander_horn_rows', {}),
'current_player': b.get('current_player'),
'round': b.get('round_number'),
}, indent=2))
"
Use this data to render the rich game state summary (see step 3).
After each turn completes, fetch the full game state from curl -s http://localhost:8080/state and present a rich, emoji-laden markdown summary before the AskUserQuestion. Use this template:
## ⚔️ Round {round} — Turn {turn}
### 🏰 {P1 Faction Emoji} {P1 Faction} vs {P2 Faction Emoji} {P2 Faction}
| | {P1 Faction Emoji} {P1 Faction} | {P2 Faction Emoji} {P2 Faction} |
|---|---|---|
| 👑 Leader | {P1 leader name} | {P2 leader name} |
| 🃏 Hand | {P1 hand size} cards | {P2 hand size} cards |
| 📚 Deck | {P1 deck size} remaining | {P2 deck size} remaining |
### 📊 Scoreboard
| Row | {P1 Faction Emoji} {P1 Faction} | {P2 Faction Emoji} {P2 Faction} |
|---|---|---|
| ⚔️ Close | {p1_close} | {p2_close} |
| 🏹 Ranged | {p1_ranged} | {p2_ranged} |
| 🔥 Siege | {p1_siege} | {p2_siege} |
| **🏆 Total** | **{p1_total}** | **{p2_total}** |
{weather_line}
{horn_line}
### 🎯 Next up: {Current Player Faction Emoji} {Current Player Faction}
Faction emojis:
| Faction | Emoji |
|---|---|
| Monsters | 👹 |
| Nilfgaardian | 🦅 |
| Northern Realms | 🏰 |
| Scoia'tael | 🌿 |
| Skellige | ⚓ |
Conditional lines:
{weather_line}: If weather_rows is non-empty, show 🌨️ **Weather:** {comma-separated weather effects}. Omit if empty.{horn_line}: If any player has commander horns, show 📯 **Commander Horns:** {details}. Omit if empty.⚔️ 27 vs just 0After the summary, present AskUserQuestion with these options:
Use faction-themed labels for order options, e.g.:
Note: max 4 options in AskUserQuestion. Use the 2 most relevant order options plus Continue and Run uninterrupted. Put Stop as the "Other" fallback.
"Continue": kill -USR1 <pid> — plays one turn, pauses again
"Run uninterrupted": kill -USR2 <pid> then kill -USR1 <pid> — disables auto-pause and unpauses. The game runs freely. The skill should exit the AskUserQuestion loop and just let stdout flow.
Orders: Write the user's exact raw text — do NOT rephrase, sanitize, or summarize. The game-loop adds faction preamble automatically:
echo '{"order": "<user text VERBATIM>"}' > /tmp/llm-vs-orders-p1.json # or p2
kill -USR1 <pid>
"Stop": kill <pid> (SIGTERM)
If the user says "let them play", "auto-play", "run free", "uninterrupted" while the game is running (outside the AskUserQuestion loop), /llm-vs should:
pgrep -f game-loop.pykill -USR2 <pid> to toggle auto-pause offkill -USR1 <pid> to unpauseTo re-enable auto-pause from outside: kill -USR2 <pid> again (it's a toggle)
Loop back to step 2 until the game ends.
Use these for AskUserQuestion labels/descriptions:
| Faction | Order label | Preamble |
|---|---|---|
| Monsters | "Command the Wild Hunt" | "The Crone whispers from the shadows" |
| Nilfgaardian | "Issue Imperial decree" | "By Imperial decree of the Emperor" |
| Northern Realms | "Send royal edict" | "A royal edict from the throne of Temeria" |
| Scoia'tael | "Elder's command" | "The elder of the Scoia'tael commands" |
| Skellige | "Order the Jarl's army" | "The Jarl's war council demands" |
Each turn shows:
--json)After the game ends, report: