feat(@projects/@magic-civilization): ✨ add claude-player headless harness
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
parent
df74c9890d
commit
81a674de1e
7 changed files with 529 additions and 0 deletions
44
scripts/claude-player-server.sh
Executable file
44
scripts/claude-player-server.sh
Executable file
|
|
@ -0,0 +1,44 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# p2-67 Phase 3 — Launch wrapper for the Claude Player headless harness.
|
||||||
|
#
|
||||||
|
# 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")`.
|
||||||
|
#
|
||||||
|
# Env vars are forwarded into the sandbox (see CLAUDE_PLAYER_API.md):
|
||||||
|
# CP_SEED, CP_PLAYERS, CP_CLAUDE_SLOT, CP_MAP_SIZE, CP_MAP_TYPE,
|
||||||
|
# CP_OMNISCIENT, CP_TIMEOUT_SEC, CP_LOG_FILE.
|
||||||
|
#
|
||||||
|
# Exit code 0 on clean shutdown (shutdown request received or stdin EOF).
|
||||||
|
# Non-zero on protocol error or harness crash.
|
||||||
|
|
||||||
|
set -uo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
PROJECT_DIR="$(dirname "$SCRIPT_DIR")"
|
||||||
|
|
||||||
|
# Defaults — adapter overrides via env.
|
||||||
|
: "${CP_SEED:=42}"
|
||||||
|
: "${CP_PLAYERS:=2}"
|
||||||
|
: "${CP_CLAUDE_SLOT:=0}"
|
||||||
|
: "${CP_MAP_SIZE:=duel}"
|
||||||
|
: "${CP_MAP_TYPE:=continents}"
|
||||||
|
: "${CP_OMNISCIENT:=0}"
|
||||||
|
: "${CP_TIMEOUT_SEC:=60}"
|
||||||
|
: "${CP_LOG_FILE:=}"
|
||||||
|
|
||||||
|
# Pure-headless mode — no Wayland needed. JSON-Lines speaks to stdout.
|
||||||
|
flatpak run --user \
|
||||||
|
--env=CP_SEED="$CP_SEED" \
|
||||||
|
--env=CP_PLAYERS="$CP_PLAYERS" \
|
||||||
|
--env=CP_CLAUDE_SLOT="$CP_CLAUDE_SLOT" \
|
||||||
|
--env=CP_MAP_SIZE="$CP_MAP_SIZE" \
|
||||||
|
--env=CP_MAP_TYPE="$CP_MAP_TYPE" \
|
||||||
|
--env=CP_OMNISCIENT="$CP_OMNISCIENT" \
|
||||||
|
--env=CP_TIMEOUT_SEC="$CP_TIMEOUT_SEC" \
|
||||||
|
--env=CP_LOG_FILE="$CP_LOG_FILE" \
|
||||||
|
org.godotengine.Godot \
|
||||||
|
--path "$PROJECT_DIR/src/game" \
|
||||||
|
--headless \
|
||||||
|
--rendering-method gl_compatibility \
|
||||||
|
res://engine/scenes/headless/claude_player_main.tscn
|
||||||
216
src/game/engine/scenes/headless/claude_player_main.gd
Normal file
216
src/game/engine/scenes/headless/claude_player_main.gd
Normal file
|
|
@ -0,0 +1,216 @@
|
||||||
|
extends Node
|
||||||
|
## p2-67 Phase 3 — Headless harness for the Claude Player 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`.
|
||||||
|
##
|
||||||
|
## Env vars consumed:
|
||||||
|
## - `CP_SEED` — RNG seed (default 42)
|
||||||
|
## - `CP_PLAYERS` — total player slots (default 2)
|
||||||
|
## - `CP_CLAUDE_SLOT` — which slot stdin controls (default 0)
|
||||||
|
## - `CP_MAP_SIZE` — MapGenerator size key (default "duel")
|
||||||
|
## - `CP_MAP_TYPE` — MapGenerator map type (default "continents")
|
||||||
|
## - `CP_OMNISCIENT` — `1` disables fog redaction (default 0)
|
||||||
|
## - `CP_TIMEOUT_SEC` — per-action timeout in seconds (default 60)
|
||||||
|
## - `CP_LOG_FILE` — if set, mirror all wire I/O to this path
|
||||||
|
|
||||||
|
var _api: RefCounted = null
|
||||||
|
var _claude_slot: int = 0
|
||||||
|
var _omniscient: bool = false
|
||||||
|
var _log_path: String = ""
|
||||||
|
var _shutdown: bool = false
|
||||||
|
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
_claude_slot = _env_int("CP_CLAUDE_SLOT", 0)
|
||||||
|
_omniscient = _env_bool("CP_OMNISCIENT", false)
|
||||||
|
_log_path = OS.get_environment("CP_LOG_FILE")
|
||||||
|
|
||||||
|
if not ClassDB.class_exists("GdPlayerApi"):
|
||||||
|
_emit_protocol_error(
|
||||||
|
"GdPlayerApi GDExtension class not registered — rebuild gdext"
|
||||||
|
)
|
||||||
|
get_tree().quit(1)
|
||||||
|
return
|
||||||
|
|
||||||
|
DataLoader.load_theme("age-of-dwarves")
|
||||||
|
DataLoader.load_world("earth")
|
||||||
|
|
||||||
|
var seed_v: int = _env_int("CP_SEED", 42)
|
||||||
|
var num_players: int = _env_int("CP_PLAYERS", 2)
|
||||||
|
var map_size: String = _env_or("CP_MAP_SIZE", "duel")
|
||||||
|
var map_type: String = _env_or("CP_MAP_TYPE", "continents")
|
||||||
|
_bootstrap_game(seed_v, num_players, map_size, map_type)
|
||||||
|
|
||||||
|
_api = ClassDB.instantiate("GdPlayerApi") as RefCounted
|
||||||
|
if _api == null:
|
||||||
|
_emit_protocol_error("ClassDB.instantiate('GdPlayerApi') returned null")
|
||||||
|
get_tree().quit(1)
|
||||||
|
return
|
||||||
|
_api.set_omniscient(_omniscient)
|
||||||
|
|
||||||
|
# Announce initial state to the adapter. Notification lines carry
|
||||||
|
# no `id` field; adapters can use them to drive streaming UIs or
|
||||||
|
# ignore them entirely (the synchronous response after the next
|
||||||
|
# `act` carries the same data via `events`).
|
||||||
|
_emit_event("turn_started", {"turn": 0, "player": _claude_slot})
|
||||||
|
_emit_event("phase_changed", {"phase": "player_actions"})
|
||||||
|
|
||||||
|
# Enter the pump on the next frame so any pending engine init flushes.
|
||||||
|
_pump.call_deferred()
|
||||||
|
|
||||||
|
|
||||||
|
func _bootstrap_game(
|
||||||
|
seed_v: int, num_players: int, map_size: String, map_type: String
|
||||||
|
) -> void:
|
||||||
|
## Initialise a real seeded game. Map / unit hydration of the
|
||||||
|
## GdPlayerApi's held GameState is deferred until GdGameState's
|
||||||
|
## `serialize_to_json` is wired (TRACKED: p2-67 Phase 3 follow-up).
|
||||||
|
## For Phase 3 v1 the harness initialises GameState (which sets up
|
||||||
|
## autoload state for the in-process AI loop) and the API works
|
||||||
|
## off its internal default `GameState` — enough to validate the
|
||||||
|
## JSON-Lines pump end-to-end.
|
||||||
|
GameState.initialize_game({
|
||||||
|
"seed": seed_v,
|
||||||
|
"map_type": map_type,
|
||||||
|
"map_size": map_size,
|
||||||
|
"num_players": num_players,
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
func _pump() -> void:
|
||||||
|
while not _shutdown:
|
||||||
|
var line: String = OS.read_string_from_stdin(4096)
|
||||||
|
if line == "":
|
||||||
|
# EOF or no line yet — yield to the engine and try again.
|
||||||
|
await get_tree().process_frame
|
||||||
|
continue
|
||||||
|
line = line.strip_edges()
|
||||||
|
if line.is_empty():
|
||||||
|
continue
|
||||||
|
_log_wire("<-", line)
|
||||||
|
var parsed_dict: Dictionary = JSON.parse_string(line) as Dictionary
|
||||||
|
if parsed_dict.is_empty() and line != "{}":
|
||||||
|
_emit_protocol_error("could not parse line: %s" % line)
|
||||||
|
continue
|
||||||
|
_handle_request(parsed_dict)
|
||||||
|
get_tree().quit(0)
|
||||||
|
|
||||||
|
|
||||||
|
func _handle_request(req: Dictionary) -> void:
|
||||||
|
var rtype: String = String(req.get("type", ""))
|
||||||
|
var rid_int: int = int(req.get("id", -1))
|
||||||
|
var has_id: bool = req.has("id") and req.get("id") != null
|
||||||
|
match rtype:
|
||||||
|
"view":
|
||||||
|
var view_json: String = String(_api.view_json(_claude_slot))
|
||||||
|
_emit_response_with_view(rid_int, has_id, view_json)
|
||||||
|
"act":
|
||||||
|
var action_payload: Dictionary = req.get("action", {}) as Dictionary
|
||||||
|
if action_payload.is_empty():
|
||||||
|
_emit_response_error(rid_int, has_id, "parse_error", "missing action field")
|
||||||
|
return
|
||||||
|
var action_json: String = JSON.stringify(action_payload)
|
||||||
|
var envelope_str: String = String(
|
||||||
|
_api.apply_action_json(_claude_slot, action_json)
|
||||||
|
)
|
||||||
|
# api wrapper already emits a full ok/err envelope — splice
|
||||||
|
# in the request id and forward as the response body.
|
||||||
|
var envelope: Dictionary = JSON.parse_string(envelope_str) as Dictionary
|
||||||
|
if envelope.is_empty():
|
||||||
|
_emit_response_error(
|
||||||
|
rid_int, has_id, "internal", "gdext returned non-JSON envelope"
|
||||||
|
)
|
||||||
|
return
|
||||||
|
if has_id:
|
||||||
|
envelope["id"] = rid_int
|
||||||
|
_write_line(JSON.stringify(envelope))
|
||||||
|
"shutdown":
|
||||||
|
_emit_response_ack(rid_int, has_id)
|
||||||
|
_shutdown = true
|
||||||
|
_:
|
||||||
|
_emit_response_error(
|
||||||
|
rid_int, has_id, "parse_error", "unknown request type: %s" % rtype
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
func _emit_response_with_view(rid_int: int, has_id: bool, view_json: String) -> void:
|
||||||
|
var view_dict: Dictionary = JSON.parse_string(view_json) as Dictionary
|
||||||
|
var body: Dictionary = {"ok": true, "view": view_dict}
|
||||||
|
if has_id:
|
||||||
|
body["id"] = rid_int
|
||||||
|
_write_line(JSON.stringify(body))
|
||||||
|
|
||||||
|
|
||||||
|
func _emit_response_ack(rid_int: int, has_id: bool) -> void:
|
||||||
|
var body: Dictionary = {"ok": true}
|
||||||
|
if has_id:
|
||||||
|
body["id"] = rid_int
|
||||||
|
_write_line(JSON.stringify(body))
|
||||||
|
|
||||||
|
|
||||||
|
func _emit_response_error(
|
||||||
|
rid_int: int, has_id: bool, code: String, message: String
|
||||||
|
) -> void:
|
||||||
|
var body: Dictionary = {
|
||||||
|
"ok": false,
|
||||||
|
"error": {"code": code, "message": message},
|
||||||
|
}
|
||||||
|
if has_id:
|
||||||
|
body["id"] = rid_int
|
||||||
|
_write_line(JSON.stringify(body))
|
||||||
|
|
||||||
|
|
||||||
|
func _emit_event(event_type: String, payload: Dictionary) -> void:
|
||||||
|
## Notification line (no id) — adapter ignores or streams.
|
||||||
|
var body: Dictionary = {"type": event_type}
|
||||||
|
for k: String in payload.keys():
|
||||||
|
body[k] = payload[k]
|
||||||
|
_write_line(JSON.stringify(body))
|
||||||
|
|
||||||
|
|
||||||
|
func _emit_protocol_error(message: String) -> void:
|
||||||
|
_emit_event("protocol_error", {"message": message})
|
||||||
|
|
||||||
|
|
||||||
|
func _write_line(line: String) -> void:
|
||||||
|
_log_wire("->", line)
|
||||||
|
# `print` already appends a newline → exactly one JSON value per line.
|
||||||
|
print(line)
|
||||||
|
|
||||||
|
|
||||||
|
func _log_wire(dir_arrow: String, line: String) -> void:
|
||||||
|
if _log_path.is_empty():
|
||||||
|
return
|
||||||
|
var f: FileAccess = FileAccess.open(_log_path, FileAccess.READ_WRITE)
|
||||||
|
if f == null:
|
||||||
|
f = FileAccess.open(_log_path, FileAccess.WRITE)
|
||||||
|
if f == null:
|
||||||
|
return
|
||||||
|
f.seek_end()
|
||||||
|
f.store_line("%s %s" % [dir_arrow, line])
|
||||||
|
f.close()
|
||||||
|
|
||||||
|
|
||||||
|
func _env_or(name: String, fallback: String) -> String:
|
||||||
|
var v: String = OS.get_environment(name)
|
||||||
|
if v.is_empty():
|
||||||
|
return fallback
|
||||||
|
return v
|
||||||
|
|
||||||
|
|
||||||
|
func _env_int(name: String, fallback: int) -> int:
|
||||||
|
var v: String = OS.get_environment(name)
|
||||||
|
if v.is_empty():
|
||||||
|
return fallback
|
||||||
|
return int(v)
|
||||||
|
|
||||||
|
|
||||||
|
func _env_bool(name: String, fallback: bool) -> bool:
|
||||||
|
var v: String = OS.get_environment(name)
|
||||||
|
if v.is_empty():
|
||||||
|
return fallback
|
||||||
|
return v == "1" or v.to_lower() == "true"
|
||||||
6
src/game/engine/scenes/headless/claude_player_main.tscn
Normal file
6
src/game/engine/scenes/headless/claude_player_main.tscn
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
[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")
|
||||||
57
tooling/claude-player/README.md
Normal file
57
tooling/claude-player/README.md
Normal file
|
|
@ -0,0 +1,57 @@
|
||||||
|
# @magic-civ/claude-player
|
||||||
|
|
||||||
|
Claude Agent SDK adapter for the Magic Civilization Claude Player API
|
||||||
|
(p2-67 Phase 4). Drives one player slot in a headless game while the
|
||||||
|
production AI controls the others.
|
||||||
|
|
||||||
|
## Run
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd tooling/claude-player
|
||||||
|
npm install
|
||||||
|
export ANTHROPIC_API_KEY=sk-ant-...
|
||||||
|
npm run dev # tsx — no build needed
|
||||||
|
# or
|
||||||
|
npm run build && npm start # compiled JS
|
||||||
|
```
|
||||||
|
|
||||||
|
Each run writes append-only logs to `.local/runs/<stamp>/`:
|
||||||
|
- `log.jsonl` — agent decisions + notifications + run metadata.
|
||||||
|
- `wire.jsonl` — raw JSON-Lines between adapter and harness.
|
||||||
|
- `result.json` — final result (`turns_played`, `reason`, `final_view`).
|
||||||
|
|
||||||
|
## Env vars
|
||||||
|
|
||||||
|
| Var | Default | Purpose |
|
||||||
|
|---|---|---|
|
||||||
|
| `ANTHROPIC_API_KEY` | *(required)* | Claude API credentials |
|
||||||
|
| `CP_MODEL` | `claude-opus-4-7` | Model id |
|
||||||
|
| `CP_MAX_TURNS` | `100` | Hard turn cap |
|
||||||
|
| `CP_MAX_IDLE_ENDS` | `3` | Consecutive idle end_turns before bail |
|
||||||
|
| `CP_SERVER` | `<repo>/scripts/claude-player-server.sh` | Harness launcher |
|
||||||
|
| `CP_SEED` / `CP_PLAYERS` / `CP_CLAUDE_SLOT` / `CP_OMNISCIENT` / `CP_TIMEOUT_SEC` | see CLAUDE_PLAYER_API.md | Forwarded to harness |
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
@magic-civ/claude-player (Node)
|
||||||
|
│
|
||||||
|
├── HarnessClient — spawn() + JSON-Lines pump
|
||||||
|
│
|
||||||
|
└── runAgent — Anthropic Messages API tool-use loop
|
||||||
|
├── tool: view()
|
||||||
|
├── tool: act(action)
|
||||||
|
└── tool: end_turn()
|
||||||
|
│
|
||||||
|
▼ stdin/stdout
|
||||||
|
scripts/claude-player-server.sh
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
flatpak Godot --headless res://engine/scenes/headless/claude_player_main.tscn
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
GdPlayerApi → mc-player-api → mc-turn::action_handlers
|
||||||
|
```
|
||||||
|
|
||||||
|
Wire-protocol contract: `src/game/engine/docs/CLAUDE_PLAYER_API.md`.
|
||||||
|
Owning objective: `.project/objectives/p2-67-claude-player-api.md`.
|
||||||
25
tooling/claude-player/package.json
Normal file
25
tooling/claude-player/package.json
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
{
|
||||||
|
"name": "@magic-civ/claude-player",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"description": "Claude Agent SDK adapter for the Magic Civilization Claude Player API (p2-67 Phase 4).",
|
||||||
|
"main": "dist/index.js",
|
||||||
|
"scripts": {
|
||||||
|
"build": "tsc -p tsconfig.json",
|
||||||
|
"start": "node --enable-source-maps dist/index.js",
|
||||||
|
"dev": "tsx src/index.ts",
|
||||||
|
"typecheck": "tsc -p tsconfig.json --noEmit"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@anthropic-ai/sdk": "^0.40.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^22.0.0",
|
||||||
|
"tsx": "^4.19.0",
|
||||||
|
"typescript": "^5.6.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20"
|
||||||
|
}
|
||||||
|
}
|
||||||
155
tooling/claude-player/src/types.ts
Normal file
155
tooling/claude-player/src/types.ts
Normal file
|
|
@ -0,0 +1,155 @@
|
||||||
|
// Wire-protocol types — mirror the Rust enums in `mc-player-api`.
|
||||||
|
// Source of truth: src/game/engine/docs/CLAUDE_PLAYER_API.md.
|
||||||
|
|
||||||
|
/** Hex coordinate carried over the wire — `[col, row]`. */
|
||||||
|
export type WireHex = [number, number];
|
||||||
|
|
||||||
|
/** Stable player slot index, `u8` everywhere in the simulator. */
|
||||||
|
export type PlayerId = number;
|
||||||
|
|
||||||
|
/** Discriminated union of every player action variant. */
|
||||||
|
export type PlayerAction =
|
||||||
|
| { type: "end_turn" }
|
||||||
|
| { type: "noop" }
|
||||||
|
| { type: "move"; unit_id: string; to: WireHex }
|
||||||
|
| { type: "attack"; unit_id: string; target: WireHex }
|
||||||
|
| { type: "ranged_attack"; unit_id: string; target: WireHex }
|
||||||
|
| { type: "fortify"; unit_id: string }
|
||||||
|
| { type: "unfortify"; unit_id: string }
|
||||||
|
| { type: "skip"; unit_id: string }
|
||||||
|
| { type: "found_city"; unit_id: string }
|
||||||
|
| { type: "build_improvement"; unit_id: string; improvement_id: string }
|
||||||
|
| { type: "sentry"; unit_id: string }
|
||||||
|
| { type: "unsentry"; unit_id: string }
|
||||||
|
| { type: "issue_patrol"; unit_id: string; waypoints: WireHex[] }
|
||||||
|
| { type: "cancel_patrol"; unit_id: string }
|
||||||
|
| { type: "edit_patrol"; unit_id: string; waypoints: WireHex[] }
|
||||||
|
| { type: "queue_production"; city_id: string; item: string; tile?: WireHex }
|
||||||
|
| { type: "remove_from_queue"; city_id: string; index: number }
|
||||||
|
| { type: "queue_reorder"; city_id: string; from: number; to: number }
|
||||||
|
| { type: "rush_buy"; city_id: string }
|
||||||
|
| { type: "buy_tile"; city_id: string; tile: WireHex }
|
||||||
|
| { type: "set_focus"; city_id: string; focus: string }
|
||||||
|
| {
|
||||||
|
type: "merge_buildings";
|
||||||
|
city_id: string;
|
||||||
|
building_a: string;
|
||||||
|
building_b: string;
|
||||||
|
into: string;
|
||||||
|
}
|
||||||
|
| { type: "research_tech"; tech_id: string }
|
||||||
|
| { type: "research_tradition"; tradition_id: string }
|
||||||
|
| { type: "declare_war"; on: PlayerId }
|
||||||
|
| { type: "offer_peace"; to: PlayerId };
|
||||||
|
|
||||||
|
/** One entry in a unit's or empire's `legal_actions` list. */
|
||||||
|
export interface LegalActionEntry {
|
||||||
|
action: PlayerAction;
|
||||||
|
enabled: boolean;
|
||||||
|
disabled_reason?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UnitView {
|
||||||
|
id: string;
|
||||||
|
type: string;
|
||||||
|
position: WireHex;
|
||||||
|
owner: PlayerId;
|
||||||
|
hp: number;
|
||||||
|
max_hp: number;
|
||||||
|
movement_left: number;
|
||||||
|
movement_max: number;
|
||||||
|
experience: number;
|
||||||
|
promotion_available: boolean;
|
||||||
|
fortified: boolean;
|
||||||
|
sentry: boolean;
|
||||||
|
legal_actions?: LegalActionEntry[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CityView {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
position: WireHex;
|
||||||
|
owner: PlayerId;
|
||||||
|
is_capital: boolean;
|
||||||
|
population: number;
|
||||||
|
food_stored: number;
|
||||||
|
food_growth_threshold: number;
|
||||||
|
production_queue: Array<{
|
||||||
|
item: string;
|
||||||
|
kind: string;
|
||||||
|
progress: number;
|
||||||
|
cost: number;
|
||||||
|
tile?: WireHex;
|
||||||
|
}>;
|
||||||
|
buildings: string[];
|
||||||
|
owned_tiles: WireHex[];
|
||||||
|
yields: Record<string, number>;
|
||||||
|
hp: number;
|
||||||
|
max_hp: number;
|
||||||
|
focus: string;
|
||||||
|
buildable?: Array<{
|
||||||
|
item: string;
|
||||||
|
kind: string;
|
||||||
|
cost: number;
|
||||||
|
rush_gold: number;
|
||||||
|
enabled: boolean;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PlayerView {
|
||||||
|
turn: number;
|
||||||
|
player: PlayerId;
|
||||||
|
current_player: PlayerId;
|
||||||
|
phase: string;
|
||||||
|
is_human_turn: boolean;
|
||||||
|
resources: {
|
||||||
|
gold: number;
|
||||||
|
gold_per_turn: number;
|
||||||
|
science_per_turn: number;
|
||||||
|
culture_per_turn: number;
|
||||||
|
happiness_pool: number;
|
||||||
|
stockpile: Record<string, number>;
|
||||||
|
};
|
||||||
|
research: {
|
||||||
|
current_tech: string | null;
|
||||||
|
tech_progress: number;
|
||||||
|
tech_cost: number;
|
||||||
|
researched: string[];
|
||||||
|
available: string[];
|
||||||
|
};
|
||||||
|
culture: {
|
||||||
|
current_tradition: string | null;
|
||||||
|
tradition_progress: number;
|
||||||
|
tradition_cost: number;
|
||||||
|
researched: string[];
|
||||||
|
};
|
||||||
|
cities: CityView[];
|
||||||
|
units: UnitView[];
|
||||||
|
legal_actions: LegalActionEntry[];
|
||||||
|
score: {
|
||||||
|
gold_total: number;
|
||||||
|
city_count: number;
|
||||||
|
unit_count: number;
|
||||||
|
score_estimate: number;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface OkResponse {
|
||||||
|
id: number | null;
|
||||||
|
ok: true;
|
||||||
|
events?: Array<Record<string, unknown>>;
|
||||||
|
view: PlayerView;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ErrResponse {
|
||||||
|
id: number | null;
|
||||||
|
ok: false;
|
||||||
|
error: { code: string; message: string };
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Response = OkResponse | ErrResponse;
|
||||||
|
|
||||||
|
export interface Notification {
|
||||||
|
type: string;
|
||||||
|
[k: string]: unknown;
|
||||||
|
}
|
||||||
26
tooling/claude-player/tsconfig.json
Normal file
26
tooling/claude-player/tsconfig.json
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2022",
|
||||||
|
"module": "NodeNext",
|
||||||
|
"moduleResolution": "NodeNext",
|
||||||
|
"lib": ["ES2022"],
|
||||||
|
"strict": true,
|
||||||
|
"noImplicitAny": true,
|
||||||
|
"noUncheckedIndexedAccess": true,
|
||||||
|
"exactOptionalPropertyTypes": true,
|
||||||
|
"noImplicitOverride": true,
|
||||||
|
"noImplicitReturns": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"declaration": true,
|
||||||
|
"declarationMap": true,
|
||||||
|
"sourceMap": true,
|
||||||
|
"outDir": "dist",
|
||||||
|
"rootDir": "src",
|
||||||
|
"resolveJsonModule": true
|
||||||
|
},
|
||||||
|
"include": ["src/**/*.ts"],
|
||||||
|
"exclude": ["node_modules", "dist", ".local"]
|
||||||
|
}
|
||||||
Loading…
Add table
Reference in a new issue