feat(@projects/@magic-civilization): ✨ rename claude-player to player-api refactor
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
parent
62d6a13ac9
commit
91ef4bc21f
16 changed files with 219 additions and 43 deletions
|
|
@ -1,7 +1,7 @@
|
|||
#!/usr/bin/env bash
|
||||
# p2-67 Phase 5 — Real-apricot Claude-vs-AI demo driver, 25 EndTurns.
|
||||
#
|
||||
# Spawns claude-player-server.sh, sends CP_TURNS act:end_turn requests
|
||||
# Spawns player-api-server.sh, sends CP_TURNS act:end_turn requests
|
||||
# (default 25) followed by shutdown. Streams the FULL JSON-Lines
|
||||
# response stream verbatim to stdout — caller redirects to transcript.jsonl.
|
||||
#
|
||||
|
|
@ -47,7 +47,7 @@ trap "rm -rf '$TMP'" EXIT
|
|||
# Drive the harness. timeout is a hard ceiling; CP_TIMEOUT_SEC drives the
|
||||
# in-process Godot watchdog inside the harness.
|
||||
HARNESS_RC=0
|
||||
timeout "$((CP_TIMEOUT_SEC + 60))" "$SCRIPT_DIR/claude-player-server.sh" \
|
||||
timeout "$((CP_TIMEOUT_SEC + 60))" "$SCRIPT_DIR/player-api-server.sh" \
|
||||
< "$TMP/in.jsonl" > "$TMP/out.jsonl" 2>"$TMP/err.log" \
|
||||
|| HARNESS_RC=$?
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
#!/usr/bin/env bash
|
||||
# p2-71 — 5-EndTurn smoke driver for the Claude Player API harness.
|
||||
#
|
||||
# Spawns claude-player-server.sh, sends 5 act:end_turn requests over stdin,
|
||||
# Spawns player-api-server.sh, sends 5 act:end_turn requests over stdin,
|
||||
# parses JSON-Lines responses, and prints a one-line verdict:
|
||||
#
|
||||
# {"turns": 5, "ai_turn_completed_events": N,
|
||||
|
|
@ -37,7 +37,7 @@ trap "rm -rf '$TMP'" EXIT
|
|||
} > "$TMP/in.jsonl"
|
||||
|
||||
# Run harness with timeout safety. 60s should be plenty for 5 turns on duel.
|
||||
timeout 90 "$SCRIPT_DIR/claude-player-server.sh" < "$TMP/in.jsonl" > "$TMP/out.jsonl" 2>"$TMP/err.log" || true
|
||||
timeout 90 "$SCRIPT_DIR/player-api-server.sh" < "$TMP/in.jsonl" > "$TMP/out.jsonl" 2>"$TMP/err.log" || true
|
||||
|
||||
# Parse — for each turn-response, count `ai_turn_completed` events and
|
||||
# sum actions_applied across slots. Output one verdict line.
|
||||
|
|
|
|||
147
scripts/player-api-example.py
Executable file
147
scripts/player-api-example.py
Executable file
|
|
@ -0,0 +1,147 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Example: drive the Magic Civilization Player API from a plain Python client.
|
||||
|
||||
This script exists as documentation-by-example: it shows that
|
||||
`scripts/player-api-server.sh` is not Claude-specific. Any client that
|
||||
speaks JSON-Lines over stdin/stdout can take a player slot — Claude Code
|
||||
via `tooling/claude-player-mcp/`, an OpenSpiel adapter pumping the same
|
||||
pipe, or this 80-line random/greedy bot.
|
||||
|
||||
Wire-protocol contract: `src/game/engine/docs/PLAYER_API.md`.
|
||||
|
||||
Usage:
|
||||
python3 scripts/player-api-example.py [TURNS]
|
||||
|
||||
Spawns the harness, plays `TURNS` turns (default 10) with a trivial
|
||||
greedy policy, prints a one-line summary per turn, then shuts the
|
||||
harness down cleanly.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parent.parent
|
||||
HARNESS = REPO_ROOT / "scripts" / "player-api-server.sh"
|
||||
|
||||
|
||||
class PlayerApiClient:
|
||||
"""Thin JSON-Lines client over a child process. Stateless except for
|
||||
the request-id counter — the harness holds the simulator state."""
|
||||
|
||||
def __init__(self, env_overrides: dict[str, str] | None = None) -> None:
|
||||
env = {**os.environ, **(env_overrides or {})}
|
||||
self._proc = subprocess.Popen(
|
||||
["bash", str(HARNESS)],
|
||||
stdin=subprocess.PIPE,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.DEVNULL,
|
||||
cwd=str(REPO_ROOT),
|
||||
text=True,
|
||||
bufsize=1,
|
||||
env=env,
|
||||
)
|
||||
self._next_id = 1
|
||||
|
||||
def _send(self, msg: dict[str, Any]) -> dict[str, Any] | None:
|
||||
msg["id"] = self._next_id
|
||||
self._next_id += 1
|
||||
assert self._proc.stdin is not None and self._proc.stdout is not None
|
||||
self._proc.stdin.write(json.dumps(msg) + "\n")
|
||||
self._proc.stdin.flush()
|
||||
# Read until correlated response (skip async notifications).
|
||||
for _ in range(5000):
|
||||
line = self._proc.stdout.readline()
|
||||
if not line:
|
||||
return None
|
||||
try:
|
||||
obj = json.loads(line)
|
||||
except json.JSONDecodeError:
|
||||
continue
|
||||
if obj.get("id") == msg["id"]:
|
||||
return obj
|
||||
return None
|
||||
|
||||
def view(self) -> dict[str, Any] | None:
|
||||
r = self._send({"type": "view"})
|
||||
return r["view"] if r and r.get("ok") else None
|
||||
|
||||
def act(self, action: dict[str, Any]) -> bool:
|
||||
r = self._send({"type": "act", "action": action})
|
||||
return bool(r and r.get("ok"))
|
||||
|
||||
def end_turn(self) -> bool:
|
||||
return self.act({"type": "end_turn"})
|
||||
|
||||
def shutdown(self) -> None:
|
||||
self._send({"type": "shutdown"})
|
||||
try:
|
||||
self._proc.wait(timeout=5)
|
||||
except subprocess.TimeoutExpired:
|
||||
self._proc.kill()
|
||||
|
||||
|
||||
def greedy_policy(view: dict[str, Any]) -> list[dict[str, Any]]:
|
||||
"""Pick a small bundle of actions for this turn — found if possible,
|
||||
fortify warriors, queue a worker in any city with an empty queue."""
|
||||
actions: list[dict[str, Any]] = []
|
||||
for u in view.get("units", []):
|
||||
if u.get("owner") != view.get("player"):
|
||||
continue
|
||||
legal = [a["action"] for a in u.get("legal_actions", [])]
|
||||
# 1. Found city if available (founder)
|
||||
for a in legal:
|
||||
if a["type"] == "found_city":
|
||||
actions.append(a)
|
||||
break
|
||||
else:
|
||||
# 2. Fortify warriors
|
||||
for a in legal:
|
||||
if a["type"] == "fortify" and not u.get("fortified"):
|
||||
actions.append(a)
|
||||
break
|
||||
for c in view.get("cities", []):
|
||||
if c.get("production_queue"):
|
||||
continue
|
||||
for a in (a["action"] for a in c.get("legal_actions", [])):
|
||||
if a.get("type") == "queue_production" and a.get("item") == "worker":
|
||||
actions.append(a)
|
||||
break
|
||||
return actions
|
||||
|
||||
|
||||
def main() -> int:
|
||||
turns = int(sys.argv[1]) if len(sys.argv) > 1 else 10
|
||||
client = PlayerApiClient(env_overrides={"CP_SEED": "42", "CP_PLAYERS": "2"})
|
||||
try:
|
||||
for t in range(1, turns + 1):
|
||||
view = client.view()
|
||||
if view is None:
|
||||
print(f"[t{t}] view failed; aborting")
|
||||
return 1
|
||||
score = view.get("score", {})
|
||||
res = view.get("resources", {})
|
||||
print(
|
||||
f"[t{t:3d}] cities={int(score.get('city_count', 0))} "
|
||||
f"units={int(score.get('unit_count', 0))} "
|
||||
f"gold={int(res.get('gold', 0))} "
|
||||
f"score={int(score.get('score_estimate', 0))}"
|
||||
)
|
||||
for action in greedy_policy(view):
|
||||
if not client.act(action):
|
||||
print(f" action failed: {action}")
|
||||
break
|
||||
if not client.end_turn():
|
||||
print(f"[t{t}] end_turn failed; aborting")
|
||||
return 1
|
||||
finally:
|
||||
client.shutdown()
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
|
|
@ -1,14 +1,22 @@
|
|||
#!/usr/bin/env bash
|
||||
# p2-67 Phase 3 — Launch wrapper for the Claude Player headless harness.
|
||||
# Generic external-player JSON-Lines harness for Magic Civilization.
|
||||
#
|
||||
# Spawns flatpak Godot headless with the claude_player_main scene and
|
||||
# stdin/stdout piped through. Designed to be exec'd by the Claude SDK
|
||||
# adapter as a child process — `child = spawn("scripts/claude-player-server.sh")`.
|
||||
# Spawns Godot headless with the `player_api_main` scene and pipes
|
||||
# stdin/stdout. Any client that speaks the JSON-Lines wire protocol
|
||||
# documented in `src/game/engine/docs/PLAYER_API.md` can drive a game
|
||||
# slot through this harness — Claude Code via the
|
||||
# `tooling/claude-player-mcp/` adapter, an OpenSpiel/RL trainer via a
|
||||
# Python `subprocess.Popen`, a smoke-test shell script, etc.
|
||||
#
|
||||
# Env vars are forwarded into the sandbox (see CLAUDE_PLAYER_API.md):
|
||||
# Env vars (see PLAYER_API.md for the full schema):
|
||||
# CP_SEED, CP_PLAYERS, CP_CLAUDE_SLOT, CP_MAP_SIZE, CP_MAP_TYPE,
|
||||
# CP_OMNISCIENT, CP_TIMEOUT_SEC, CP_LOG_FILE.
|
||||
#
|
||||
# `CP_CLAUDE_SLOT` is the env-var name the harness has used since p2-67;
|
||||
# it identifies the externally-controlled player slot — kept as-is for
|
||||
# backward compatibility with existing clients. Despite the name it is
|
||||
# not Claude-specific.
|
||||
#
|
||||
# Exit code 0 on clean shutdown (shutdown request received or stdin EOF).
|
||||
# Non-zero on protocol error or harness crash.
|
||||
|
||||
|
|
@ -47,7 +55,7 @@ case "$(uname -s)" in
|
|||
--path "$PROJECT_DIR/src/game" \
|
||||
--headless \
|
||||
--rendering-method gl_compatibility \
|
||||
res://engine/scenes/headless/claude_player_main.tscn
|
||||
res://engine/scenes/headless/player_api_main.tscn
|
||||
;;
|
||||
*)
|
||||
exec flatpak run --user \
|
||||
|
|
@ -63,6 +71,6 @@ case "$(uname -s)" in
|
|||
--path "$PROJECT_DIR/src/game" \
|
||||
--headless \
|
||||
--rendering-method gl_compatibility \
|
||||
res://engine/scenes/headless/claude_player_main.tscn
|
||||
res://engine/scenes/headless/player_api_main.tscn
|
||||
;;
|
||||
esac
|
||||
|
|
@ -353,7 +353,7 @@ Three MCP tools become available after reload:
|
|||
| `magic_civ_act` | `{action: PlayerAction}` | `{events, view}` |
|
||||
| `magic_civ_end_turn` | none | Sugar for `act({type:"end_turn"})` |
|
||||
|
||||
The MCP server spawns `scripts/claude-player-server.sh` as a child on
|
||||
The MCP server spawns `scripts/player-api-server.sh` as a child on
|
||||
first tool invocation and reuses the harness across the session. Any
|
||||
MCP-stdio client works the same way — `claude-player-mcp` is just a
|
||||
thin pump.
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
[gd_scene load_steps=2 format=3]
|
||||
|
||||
[ext_resource type="Script" path="res://engine/scenes/headless/claude_player_main.gd" id="1"]
|
||||
|
||||
[node name="ClaudePlayerMain" type="Node"]
|
||||
script = ExtResource("1")
|
||||
|
|
@ -1,11 +1,17 @@
|
|||
extends Node
|
||||
## p2-67 Phase 3 — Headless harness for the Claude Player API.
|
||||
## Headless harness for the generic external-player JSON-Lines API.
|
||||
##
|
||||
## Boots a seeded GameState, instantiates a `GdPlayerApi`, then enters
|
||||
## a JSON-Lines pump on stdin/stdout. Each line in is one `Request`
|
||||
## (`view` / `act` / `shutdown`); each line out is one `Response` or
|
||||
## one `Notification`. The protocol contract lives in
|
||||
## `src/game/engine/docs/CLAUDE_PLAYER_API.md`.
|
||||
## `src/game/engine/docs/PLAYER_API.md`.
|
||||
##
|
||||
## Originally introduced in p2-67 as `claude_player_main.gd`. Renamed
|
||||
## 2026-05-17 to drop the Claude-flavored naming — the wire protocol is
|
||||
## client-agnostic; Claude Code is one consumer via
|
||||
## `tooling/claude-player-mcp/`, an OpenSpiel/RL trainer can plug in via
|
||||
## `subprocess.Popen`, and shell smoke tests use raw JSON-Lines.
|
||||
##
|
||||
## Env vars consumed:
|
||||
## - `CP_SEED` — RNG seed (default 42)
|
||||
|
|
@ -272,7 +278,7 @@ func _apply_combat_balance(gs: RefCounted) -> void:
|
|||
## JSON-serialise the Array and hand it to the new Rust setters
|
||||
## (`set_units_catalog_json` / `set_buildings_catalog_json`).
|
||||
func _apply_ai_catalogs() -> void:
|
||||
# Project root is mounted at `src/game/` (see claude-player-server.sh
|
||||
# Project root is mounted at `src/game/` (see player-api-server.sh
|
||||
# `--path` arg), so the bridge module's `res://` form drops the
|
||||
# `src/game/` prefix.
|
||||
var AiTurnBridgeState: Script = load("res://engine/src/modules/ai/ai_turn_bridge_state.gd")
|
||||
6
src/game/engine/scenes/headless/player_api_main.tscn
Normal file
6
src/game/engine/scenes/headless/player_api_main.tscn
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
[gd_scene load_steps=2 format=3]
|
||||
|
||||
[ext_resource type="Script" path="res://engine/scenes/headless/player_api_main.gd" id="1"]
|
||||
|
||||
[node name="PlayerApiMain" type="Node"]
|
||||
script = ExtResource("1")
|
||||
|
|
@ -3393,7 +3393,7 @@ impl GdGameState {
|
|||
/// `JSON.stringify`s the result before handing it to this setter — same
|
||||
/// disk-IO discipline as `_apply_ai_personalities`.
|
||||
///
|
||||
/// **Order contract (claude_player_main.gd)**: this setter MUST run
|
||||
/// **Order contract (player_api_main.gd)**: this setter MUST run
|
||||
/// BEFORE `add_player_militarist`. Without a populated catalog,
|
||||
/// `MapUnit::new` reads `base_moves = 0` (the `UnitsCatalog::get` miss
|
||||
/// path), every AI `MoveUnit` is rejected at
|
||||
|
|
@ -3435,7 +3435,7 @@ impl GdGameState {
|
|||
/// `public/games/age-of-dwarves/data/combat_balance.json`.
|
||||
///
|
||||
/// Caller (GDScript boot, autoload `GameState.initialize_game` or the
|
||||
/// headless `claude_player_main`) reads the file bytes and forwards the
|
||||
/// headless `player_api_main`) reads the file bytes and forwards the
|
||||
/// raw string. The Rust parser
|
||||
/// (`mc_turn::combat_balance::load_combat_balance`) is the single source
|
||||
/// of truth for the wire shape; unknown keys are ignored and missing
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@
|
|||
//! Error path: on any failure (parse, illegal action, internal) the
|
||||
//! returned JSON is an error-shaped envelope with `ok: false` and a
|
||||
//! typed `error` payload — adapters branch on the `ok` field exactly
|
||||
//! as documented in `CLAUDE_PLAYER_API.md`.
|
||||
//! as documented in `PLAYER_API.md`.
|
||||
|
||||
use godot::prelude::*;
|
||||
use mc_ai::tactical::state::{TacticalBuildingSpec, TacticalUnitSpec};
|
||||
|
|
@ -71,7 +71,7 @@ impl GdPlayerApi {
|
|||
/// choices MUST follow up with `set_units_catalog_json` /
|
||||
/// `set_buildings_catalog_json` / `set_difficulty_threshold_mult`
|
||||
/// after every `load_state_json`. The harness
|
||||
/// (`claude_player_main.gd::_apply_ai_catalogs`) does this once at
|
||||
/// (`player_api_main.gd::_apply_ai_catalogs`) does this once at
|
||||
/// boot — if a future flow re-loads state mid-game, repeat the
|
||||
/// catalog setters.
|
||||
#[func]
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
//!
|
||||
//! Drives a 3-player `GameState` (Claude=slot 0, AI=slots 1+2) for up to
|
||||
//! 25 turns and emits the canonical JSON-Lines wire transcript that
|
||||
//! `claude_player_main.gd` would produce if it ran headlessly against the
|
||||
//! `player_api_main.gd` would produce if it ran headlessly against the
|
||||
//! same construction. Output lands under
|
||||
//! `.local/demo-runs/2026-05-12-claude-vs-ai-mock/`:
|
||||
//!
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
//!
|
||||
//! Exercises `mc_player_api::apply_action(PlayerAction::EndTurn)` through 5
|
||||
//! consecutive end-turn cycles against a hand-built `GameState` that mirrors
|
||||
//! the GDScript headless harness (`claude_player_main.gd`) one-for-one.
|
||||
//! the GDScript headless harness (`player_api_main.gd`) one-for-one.
|
||||
//! Construction lives in `tests/common/mod.rs` so it can be reused by the
|
||||
//! 25-turn `full_game_transcript` test (and any future scripted-game test).
|
||||
//!
|
||||
|
|
|
|||
|
|
@ -1,12 +1,23 @@
|
|||
# @magic-civ/claude-player-mcp
|
||||
|
||||
MCP server exposing the Magic Civilization **Claude Player API** as tools
|
||||
that Claude Code (or any MCP-compatible client) can call. Lets Claude Code
|
||||
play the game directly — no API key, no second SDK layer.
|
||||
Claude-Code-specific MCP transport for the generic **Magic Civilization
|
||||
Player API**. Lets Claude Code play one slot of the game directly — no
|
||||
API key, no second SDK layer.
|
||||
|
||||
This package is *one* client of the player API; the underlying Godot
|
||||
harness (`scripts/player-api-server.sh` + `src/game/engine/scenes/headless/player_api_main.tscn`)
|
||||
is client-agnostic and speaks JSON-Lines. Other consumers:
|
||||
|
||||
- **Plain Python / RL trainer / OpenSpiel adapter** — `subprocess.Popen`
|
||||
the harness directly; example in `scripts/player-api-example.py`.
|
||||
- **Shell smoke / regression tests** — `scripts/claude-smoke-5endturn.sh`,
|
||||
`scripts/claude-demo-25turn.sh` write JSON-Lines to stdin.
|
||||
|
||||
Wire-protocol contract: `src/game/engine/docs/PLAYER_API.md`.
|
||||
|
||||
## What it does
|
||||
|
||||
Stdio-transport MCP server. Spawns `scripts/claude-player-server.sh` as a
|
||||
Stdio-transport MCP server. Spawns `scripts/player-api-server.sh` as a
|
||||
child process on first tool invocation; reuses the harness across calls.
|
||||
Translates each MCP tool call into one JSON-Lines request to the harness
|
||||
and returns the response as MCP `text` content.
|
||||
|
|
@ -20,7 +31,7 @@ and returns the response as MCP `text` content.
|
|||
| `magic_civ_end_turn` | none | Sugar for `magic_civ_act({type:"end_turn"})` |
|
||||
|
||||
Full `PlayerAction` taxonomy and view shape live in
|
||||
`src/game/engine/docs/CLAUDE_PLAYER_API.md`.
|
||||
`src/game/engine/docs/PLAYER_API.md`.
|
||||
|
||||
## Install + build
|
||||
|
||||
|
|
@ -72,11 +83,11 @@ Claude Code (stdio MCP client)
|
|||
@magic-civ/claude-player-mcp (this package)
|
||||
│
|
||||
▼ spawn() + JSON-Lines pipe
|
||||
scripts/claude-player-server.sh
|
||||
scripts/player-api-server.sh
|
||||
│
|
||||
▼ flatpak Godot --headless
|
||||
GdPlayerApi (api-gdext) → mc-player-api → mc-turn handlers
|
||||
```
|
||||
|
||||
Owning objective: `.project/objectives/p2-67-claude-player-api.md`.
|
||||
Wire-protocol contract: `src/game/engine/docs/CLAUDE_PLAYER_API.md`.
|
||||
Wire-protocol contract: `src/game/engine/docs/PLAYER_API.md`.
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
// JSON-Lines pump over a child process running `scripts/claude-player-server.sh`.
|
||||
// JSON-Lines pump over a child process running `scripts/player-api-server.sh`.
|
||||
//
|
||||
// Each request/response/notification is one JSON value per line. Responses are
|
||||
// correlated by an optional `id` field; the MCP server assigns monotonically
|
||||
|
|
@ -17,7 +17,7 @@ import type {
|
|||
} from "./types.js";
|
||||
|
||||
export interface HarnessOptions {
|
||||
/** Absolute path to claude-player-server.sh. */
|
||||
/** Absolute path to player-api-server.sh. */
|
||||
serverScript: string;
|
||||
/** Env-var overrides for the harness (CP_SEED, CP_PLAYERS, ...). */
|
||||
env?: Record<string, string>;
|
||||
|
|
|
|||
|
|
@ -1,11 +1,15 @@
|
|||
#!/usr/bin/env node
|
||||
// MCP server entry — exposes the Magic Civilization Claude Player API as
|
||||
// MCP server entry — exposes the Magic Civilization external-player API as
|
||||
// stdio-transport MCP tools. Claude Code (or any MCP client) lists three
|
||||
// tools and calls them: `magic_civ_view`, `magic_civ_act`, `magic_civ_end_turn`.
|
||||
//
|
||||
// Spawns `scripts/claude-player-server.sh` as a child process on first
|
||||
// tool invocation and reuses it across calls in the same session. The
|
||||
// child speaks JSON-Lines per `CLAUDE_PLAYER_API.md`.
|
||||
// This package is the Claude-Code-specific transport for the *generic*
|
||||
// player API; the underlying Godot harness is not Claude-flavored — it
|
||||
// speaks JSON-Lines per `src/game/engine/docs/PLAYER_API.md` and is
|
||||
// drivable by any client (OpenSpiel adapter, RL trainer, shell smoke).
|
||||
//
|
||||
// Spawns `scripts/player-api-server.sh` as a child process on first
|
||||
// tool invocation and reuses it across calls in the same session.
|
||||
|
||||
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
||||
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
||||
|
|
@ -24,7 +28,7 @@ const PROJECT_ROOT = resolvePath(HERE, "..", "..", "..");
|
|||
const DEFAULT_SERVER_SCRIPT = resolvePath(
|
||||
PROJECT_ROOT,
|
||||
"scripts",
|
||||
"claude-player-server.sh",
|
||||
"player-api-server.sh",
|
||||
);
|
||||
|
||||
function emitStderr(line: string): void {
|
||||
|
|
@ -73,7 +77,7 @@ const ACT_TOOL = {
|
|||
"Take one player action in Magic Civilization. The `action` argument is a " +
|
||||
"PlayerAction JSON object (e.g. {type:'move',unit_id:'42',to:[5,7]} or " +
|
||||
"{type:'found_city',unit_id:'42'} or {type:'end_turn'}). The full action " +
|
||||
"taxonomy is documented in src/game/engine/docs/CLAUDE_PLAYER_API.md. " +
|
||||
"taxonomy is documented in src/game/engine/docs/PLAYER_API.md. " +
|
||||
"Returns the events that fired plus the post-action view.",
|
||||
inputSchema: {
|
||||
type: "object" as const,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
// Wire-protocol types — mirror the Rust enums in `mc-player-api`.
|
||||
// Source of truth: src/game/engine/docs/CLAUDE_PLAYER_API.md.
|
||||
// Source of truth: src/game/engine/docs/PLAYER_API.md.
|
||||
|
||||
export type WireHex = [number, number];
|
||||
export type PlayerId = number;
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue