From 7bfabefa735b4006b40d14aa0ee2b42bc26fbf6a Mon Sep 17 00:00:00 2001 From: SolshineCode Date: Mon, 11 May 2026 17:58:32 -0700 Subject: [PATCH] fix: remove Lua-mod fiction, realign on save-file-injection (ADR-001) model MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Background: 2026-05-08 supervision sprint shipped a 350-line mod.lua and bespoke scenario.json/Map.json files for Unciv. Subsequent research (docs/unciv-research-2026-05-11.md) found that Unciv does not support Lua mod scripting or runtime hooks, and that scenarios are saved-game files in a scenarios/ folder — not JSON manifests. Both were architectural fiction that Unciv would not have loaded. Removed: - mod/ClaudeKingdoms/mod.lua (350 lines, fictional Lua hook handlers) - mod/ClaudeKingdoms/scenarios/Tutorial/ (scenario.json + Map.json — wrong shape) - mod/ClaudeKingdoms/scenarios/LongRunningCampaign/ (same) - bridge/tests/test_integration.py (Python simulation of the fictional mod.lua's turn-end hook — the contract it tested doesn't exist) - 7 cases in bridge/tests/test_tutorial_flow.py that asserted the deleted scenario JSON files' shape Renamed / reframed (bridge code that's still sound): - bridge/tutorial.py: scenario_to_load → scenario_stage. Returns a stage identifier ("tutorial" / "campaign") instead of a deleted filename. Old name kept as a back-compat alias. - bridge/tests/test_per_city_status.py: dropped the Lua-glyph-table test name in favor of a generic downstream-consumer contract test. The bridge-side calculation itself is unchanged and correct. Docs updates: - mod/ClaudeKingdoms/README.md: documents the actual integration model (pure JSON content + future external save-edit tool, ADR-001's path). Adds explicit schema warnings: only Buildings.json and Units.json have "proper schema" per Unciv docs — others need real-parser validation. - CONTRIBUTING.md: contribution path #3 reworded; Lua mention removed from test ownership matrix; Lua code-style section removed. - docs/bridge-tui-schema.md: per_city_rewards description corrected — it's what an external save-edit tool would consume, not what mod.lua would read at runtime. - docs/audit-2026-05-08.md: prominent 2026-05-11 retraction header listing every audit claim that demotes on this finding. - .claude/handoff.md: full rewrite. Drops "V1 complete" claim, surfaces the honest acceptance-criteria status (10 ✅ + 5 🟡 + 6 ❌ vs the prior over-stated 18 ✅), and documents the save-edit tool as the critical-path next deliverable. Added: - docs/unciv-research-2026-05-11.md: the source-cited Comet research that triggered this cleanup. - .gitignore: logs/ (the daemon writes audit logs there; not for VCS). Tests: 248 passing (was 262; net -14 from removed fictional tests). No bridge-side behavior changed. The Python bridge, TUI, mod JSON content, and onboarding flow are still real and tested. The bridge↔Unciv integration leg is now honestly missing rather than dishonestly claimed — it requires an external save-edit tool against Unciv's actual save format, which is the project's critical-path next deliverable. Co-Authored-By: Claude Opus 4.7 (1M context) --- .claude/handoff.md | 383 ++++++++---------- .gitignore | 2 +- CONTRIBUTING.md | 23 +- bridge/tests/test_integration.py | 277 ------------- bridge/tests/test_per_city_status.py | 11 +- bridge/tests/test_tutorial_flow.py | 87 +--- bridge/tutorial.py | 22 +- docs/audit-2026-05-08.md | 42 +- docs/bridge-tui-schema.md | 7 +- docs/unciv-research-2026-05-11.md | 254 ++++++++++++ mod/ClaudeKingdoms/README.md | 44 +- mod/ClaudeKingdoms/mod.lua | 353 ---------------- .../scenarios/LongRunningCampaign/Map.json | 20 - .../LongRunningCampaign/scenario.json | 20 - .../scenarios/Tutorial/Map.json | 18 - .../scenarios/Tutorial/scenario.json | 21 - 16 files changed, 547 insertions(+), 1037 deletions(-) delete mode 100644 bridge/tests/test_integration.py create mode 100644 docs/unciv-research-2026-05-11.md delete mode 100644 mod/ClaudeKingdoms/mod.lua delete mode 100644 mod/ClaudeKingdoms/scenarios/LongRunningCampaign/Map.json delete mode 100644 mod/ClaudeKingdoms/scenarios/LongRunningCampaign/scenario.json delete mode 100644 mod/ClaudeKingdoms/scenarios/Tutorial/Map.json delete mode 100644 mod/ClaudeKingdoms/scenarios/Tutorial/scenario.json diff --git a/.claude/handoff.md b/.claude/handoff.md index 25305f5..4c58490 100644 --- a/.claude/handoff.md +++ b/.claude/handoff.md @@ -1,214 +1,169 @@ -# Claude Kingdoms — V1 Completion State (2026-05-08, end of 5-hour supervision sprint) - -## TL;DR - -**Repo HEAD on `main`:** the merge after PR #32 (`37706b2` at sprint -end; latest in any session run `git log --oneline -5` to confirm). -**Tests passing on `main`:** 262. -**Audit closure:** every P0, P1, and P2 item identified in -`docs/audit-2026-05-08.md` is closed at the architecture+code level. -**Kanban:** all Kingdoms milestones (M0, M1, M2, M3) marked done with -real `--result` summaries; only the audit-followup task `t_26404be3` -sits `blocked` (Hermes protocol violation; the work itself shipped via -claude-code in PRs #23–#32). - -The remaining V1 launch holdouts all require an **Unciv runtime** or -**human-side action** — they cannot be closed from inside CI: - -1. README GIF capture (acceptance #11) — needs the Unciv map rendered -2. M4 fun-factor playtest — needs Unciv to actually play the campaign -3. `good first issue` GitHub-side label — five candidate bodies are - ready in `docs/good-first-issues.md`; one `gh issue create` per - surfacing -4. M5 community launch artifacts (changelog, release tag, launch posts) - -## Sprint output - -**11 PRs merged in 5 hours**, all green CI: - -| PR | Audit ref | Summary | -|----|-----------|---------| -| #22 | the audit | CTO audit doc landed | -| #23 | P0 #1 | `SessionStatus` enum gains `INACTIVE` + `REDIRECTED` (PRD §BR-1, §BS-7) | -| #24 | P0 #3 | `Stop` hook transitions to `COMPLETED` on explicit signal (PRD §CL-3) | -| #25 | P0 #2 | Bridge–TUI JSON schema realigned with PRD line 231's 13 minimum fields | -| #26 | BR-9 | Structured audit log persistence (`bridge/audit_log.py`, JSONL) | -| #27 | #11 + #12 | `CONTRIBUTING.md` four contribution paths + README GIF placeholder | -| #28 | GM-8 | Per-tile session status indicators (bridge emits + mod.lua glyphs) | -| #29 | GM-5 | Visible plan/goal/redirect bonus announcements per turn | -| #30 | #9 + #20 | Tutorial → campaign flow state machine + scenario JSONs | -| #31 | #19 | Visible next-frontier per session in TUI | -| #32 | TU-9 + #13 | BridgeView schema-only adapter + `good first issue` candidate doc | - -**Test count growth across the sprint:** 149 → 262 (+113 tests). - -## Per-PRD-requirement closure - -### Bridge Layer (BR-1 → BR-11) - -| ID | Status | Closure ref | -|----|--------|-------------| -| BR-1 | ✅ | PR #23 (enum gains INACTIVE/REDIRECTED) | -| BR-2 | ✅ | M0 (`bridge_loop.process_turn`) | -| BR-3 | ✅ | PR #25 (per-session breakdown in schema) | -| BR-4 | ✅ | M0 + PR #23 (REDIRECTED keeps base reward, no clawback) | -| BR-5 | ✅ | M2 (handle_redirect + listener) + PR #23 (REDIRECTED state) | -| BR-6 | ✅ | M2 (plan bonus +15 production) | -| BR-7 | ✅ | M2 (goal bonus +60 across gold + science) | -| BR-8 | ✅ | M2 (token bonus capped at 10 units × 2) | -| BR-9 | ✅ | PR #26 (audit log JSONL persistence) | -| BR-10 | ✅ | M1 (SaveExchange file polled by mod.lua) | -| BR-11 | ✅ | M2 (TUI manual injection bindings) | - -### Bridge & Save-State (BS-1 → BS-7) - -| ID | Status | Closure ref | -|----|--------|-------------| -| BS-1 | ✅ | PR #25 (versioned per-session breakdown on disk) | -| BS-2 | ✅ | Spike 1 / mod.lua reads kingdom_save.json | -| BS-3 | ✅ | M0 (no Kotlin fork; pure JSON + Lua mod) | -| BS-4 | ✅ | Architectural — bridge is engine-decoupled | -| BS-5 | ✅ | M1 (process_turn is the only write path) | -| BS-6 | ✅ | M1 (no-decay invariant tests) | -| BS-7 | ✅ | PR #23 (INACTIVE distinct from SUSPENDED) | - -### Claude Integration (CL-1 → CL-6) - -| ID | Status | Closure ref | -|----|--------|-------------| -| CL-1 | ✅ | M2 (UserPromptSubmit / Stop / idle threshold) | -| CL-2 | ✅ | M2 (PermissionDenied) + PR #23 (REDIRECTED state) | -| CL-3 | ✅ | PR #24 (Stop with completion signal → COMPLETED) | -| CL-4 | 🟡 | `docs/hooks-setup.md` documents; per-hook opt-in toggle would be a polish PR | -| CL-5 | 🟡 | Capture+replay path documented; no one-command install/uninstall | -| CL-6 | 🟡 | SubagentStart type=Plan handled; ultraplan-specific signal not yet differentiated | - -### Game Layer (GM-1 → GM-10) - -| ID | Status | Closure ref | -|----|--------|-------------| -| GM-1 | ✅ | mod/ClaudeKingdoms/ install path documented per OS | -| GM-2 | ✅ | PR #16 (Nations.json: The Operators civ) | -| GM-3 | ✅ | PR #16 (Buildings.json: Session Forge + Charter Hall) | -| GM-4 | ✅ | mod.lua per-city reward application | -| GM-5 | ✅ | PR #29 (per-city event announcements) | -| GM-6 | ❌ | Image asset — requires artist / asset pipeline | -| GM-7 | ❌ | Image asset — same | -| GM-8 | ✅ | PR #28 (per-tile status indicators with glyph map) | -| GM-9 | 🟡 | Save format unmodified; multiplayer untested | -| GM-10 | ✅ | PR #30 (LongRunningCampaign scenario, victoryConditions=[], noForcedEnd=true) | - -### TUI Sidecar (TU-1 → TU-9) - -| ID | Status | Closure ref | -|----|--------|-------------| -| TU-1 | ✅ | M0 (Textual) | -| TU-2 | 🟡 | Single combined view; multi-slot layout deferred | -| TU-3 | ❌ | "Launch session from a slot" UI affordance not yet added | -| TU-4 | ✅ | PR #23 (state badges include all PRD states) | -| TU-5 | ✅ | M0 (TurnEstimatePanel) | -| TU-6 | 🟡 | Bindings labeled but no "DEV" disclaimer text | -| TU-7 | ✅ | M0 (Textual handles SSH/remote terminals) | -| TU-8 | ✅ | M0 + PR #13 (parchment palette, footer hints) | -| TU-9 | ✅ | PR #32 (BridgeView schema-only adapter; existing direct-state callsites flagged for migration) | - -### V1 Acceptance Criteria (#1–#21) - -| # | Status | Closure ref | -|---|--------|-------------| -| 1 | 🟡 | README install steps; not timed | -| 2 | ✅ | Hook stream → real resources end-to-end (PRs #10–#12, #28, #29) | -| 3 | ✅ | M2 + tests | -| 4 | ✅ | M2 + audit log carries the redirect entry | -| 5 | ✅ | PR #29 (visible bonus announcement) | -| 6 | ✅ | PR #29 | -| 7 | 🟡 | TUI parchment yes; Unciv tileset/skin no (image assets) | -| 8 | ✅ | PR #14 (OnboardingScreen) + PR #16 (mod tutorials) | -| 9 | ✅ | PR #30 (Tutorial scenario specifies ≤10 minute budget; flow state machine) | -| 10 | ✅ | 262 tests, CI green on every PR | -| 11 | 🟡 | Placeholder + recording protocol shipped (PR #27); actual GIF needs Unciv runtime | -| 12 | ✅ | PR #27 (CONTRIBUTING.md four paths) | -| 13 | 🟡 | PR #32 ships 5 ready-to-paste candidate bodies; GitHub label is one `gh issue create` away | -| 14 | ✅ | Architectural — no Kotlin fork | -| 15 | ✅ | M1 | -| 16 | ✅ | M1 + PR #21 (load-on-mount) | -| 17 | ✅ | PR #23 (INACTIVE distinct from SUSPENDED) | -| 18 | ✅ | PR #30 (campaign scenario victoryConditions=[]) | -| 19 | ✅ | PR #31 (TUI shows "Next: ..." every refresh) | -| 20 | ✅ | PR #30 (handoff state machine + scenarios) | -| 21 | ✅ | ADR-001, ADR-002 on main | - -## What I did NOT touch (and why) - -- **`hermes` PR push/merge** — pushed and merged 11 PRs through the - sprint; user explicitly approved push+merge for this session window. -- **Production deploy** — not applicable (no Fly.io app for this project). -- **HuggingFace upload** — not applicable. -- **Force-push, --no-verify, --no-gpg-sign** — never used; all hook - failures (none observed) would have been investigated, not bypassed. -- **GitHub `good first issue` label creation** — five candidate bodies - shipped in `docs/good-first-issues.md`; the actual `gh issue create` - is a one-line maintainer task and visible to the public, so I left it - for explicit human approval. -- **Closing the campaign-finalisation kanban tasks** — M0/M1/M2/M3 and - the Unciv mod integration task all closed; M3 was closed with the - caveat that image assets are M4 scope. - -## Operational state - -- **Hermes Agent**: v0.13.0 (was v0.12.0; 279 commits applied today). - `~/.local/bin/hermes` symlink intact. Gateway + dashboard managed by - systemd-user units (auto-restart on crash, 60s backoff). -- **Cron loop**: scheduled job `a5ddf699` was the 30-minute supervision - cadence (`13,43 * * * *`). It is being **deleted at end of cycle 10** - per the original /loop brief. -- **MCP servers**: 7 configured. The `gemini-cli` MCP keeps re-adding - itself via `hermes setup` and failing to load; documented in the - audit doc as P2 #12. Removable with `hermes mcp remove gemini-cli`. -- **Hermes kanban dispatcher**: Hermes hit one protocol violation today - on `t_26404be3` (claimed + commented + exited rc=0 without calling - `kanban_complete`/`kanban_block`). The work itself was picked up by - claude-code and shipped; rule already in Hermes's MEMORY.md from the - earlier discipline pass. - -## Resume idiom - -```bash -cd C:\Users\caleb\projects\ClaudeKingdoms -git checkout main -git fetch origin && git reset --hard origin/main -python -m pytest -q # expect 262 passing (or higher) -python run_tui.py # interactive sandbox -python -m bridge.daemon # production daemon -``` - -## V1-launch checklist (the explicit handoff to humans) - -- [ ] **Open the 5 GitHub `good first issue`s** — copy bodies from - `docs/good-first-issues.md`, run `gh issue create` per body. -- [ ] **Capture the README GIF** — follow `docs/media/README.md` - shot-list when the M4 campaign map renders. -- [ ] **M4 fun-factor playtest** — actually play the long-running - campaign for a real Pomodoro and confirm the gameplay holds up - independent of the bridge hook. -- [ ] **Image assets** (parchment tileset, illuminated UI skin) — - artist or asset-pipeline pass for GM-6 / GM-7. -- [ ] **M5 release** — tag, changelog, launch posts on r/Unciv, - r/ClaudeCode, Hacker News, ModDB. -- [ ] **OPTIONAL multiplayer V2 RFC** — open as a community RFC after - V1 launches, per PRD §M5. - -## Files of interest - -- `docs/audit-2026-05-08.md` — the audit + closure header -- `docs/good-first-issues.md` — 5 ready-to-paste issue bodies -- `docs/bridge-tui-schema.md` — the binding contract -- `docs/hooks-setup.md` — how to wire Claude Code hooks to the bridge -- `docs/adr/adr-00{1,2,3}.md` — architectural decision records -- `mod/ClaudeKingdoms/scenarios/{Tutorial,LongRunningCampaign}/` — the - scenario manifests + maps -- `bridge/{session_state,scoring,hook_listener,bridge_loop,save_exchange,audit_log,frontier,tutorial,daemon}.py` - — the bridge surface area -- `tui/{app,medieval_voice,onboarding,bridge_view}.py` — the TUI - surface area -- `CONTRIBUTING.md` + this file + the README — top-level docs +# Claude Kingdoms — Honest Handoff (2026-05-11) + +This supersedes the 2026-05-08 sprint-end handoff. That earlier +document claimed V1 PRD-compliance based on tests passing in +isolation. A 2026-05-11 research pass against Unciv's actual docs +(`docs/unciv-research-2026-05-11.md`) found that key parts of the +Unciv-side architecture were fiction — Unciv does not support Lua +mod scripting or runtime hooks. The fictional files were removed +on 2026-05-11. + +## Where things actually stand + +### Real and solid (Python bridge, TUI, JSON mod content) + +- **Bridge layer** — session state, scoring engine, hook listener, + save exchange (atomic JSON file I/O), audit log (JSONL), + bridge loop, daemon entrypoint, tutorial flow state machine, + frontier suggestions, schema-only BridgeView adapter for the TUI. + All independently testable. 248 tests pass. +- **TUI** — Textual app with parchment palette, manual event + injection bindings (g/p/r/a/t/s/q), onboarding screen with + emotional contract, medieval voice flavor strings, event log. + Mounts, runs, key bindings work end-to-end (Pilot-tested). +- **Documentation** — PRD, ADRs 1-3, schema doc (now corrected), + hooks-setup, CTO audit (now with retraction header), Unciv + research doc, good-first-issues catalog, CONTRIBUTING (now + corrected). +- **Mod JSON content** — `Nations.json` (The Operators civ), + `Buildings.json` (Session Forge + Charter Hall), `Beliefs.json` + (pantheon), `Tutorials.json` (six tutorials), `ModOptions.json`. + Field shapes are plausible-but-unvalidated against Unciv's real + parser (only Buildings and Units have a "proper schema" per + Unciv docs). + +### Missing — the bridge↔Unciv leg + +The architecture documented in ADR-001 (Spike 1) was **save-file +injection while the game is closed**. The 2026-05-08 sprint +drifted into a fictional Lua-hook integration model that Unciv +doesn't support. The drift has been removed. + +To actually close the loop, the project needs an **external +save-edit tool** that: + +1. Reads the bridge's `./saves/kingdom_save.json` (the format already + shipped — see `docs/bridge-tui-schema.md`) +2. Locates the user's active Unciv save file +3. Parses Unciv's save format (not yet inspected; required step + per the research doc) +4. Mutates per-city yields and (where applicable) game state by + the per-city deltas from the bridge +5. Writes the modified save back atomically + +This is a real new piece of work. Scope estimate: **a few days** +for someone with Unciv-save-format familiarity, **a couple weeks** +otherwise. + +### Removed on 2026-05-11 + +- `mod/ClaudeKingdoms/mod.lua` (350 lines of fictional Lua hooks) +- `mod/ClaudeKingdoms/scenarios/Tutorial/` (wrong shape — scenarios + are saved-game files, not JSON manifests) +- `mod/ClaudeKingdoms/scenarios/LongRunningCampaign/` (same) +- `bridge/tests/test_integration.py` (Python simulation of the + fictional mod.lua's behavior) +- Seven `test_tutorial_flow.py` cases that asserted the deleted + scenario JSON files + +The bridge-side per-city status / per-city events features +(scoring engine methods, save exchange embedding) are kept — they +generate sound data that an eventual save-edit tool can consume. + +## What's playable today + +- **TUI sandbox**: `python run_tui.py` launches an interactive + Textual app. Press g/p/r/a/t to drive session state through hook + events; watch the kingdom view + turn estimate panel update. + This is a dev tool, not a game. +- **Bridge daemon**: `python -m bridge.daemon` runs the production + turn loop with hook ingestion and save persistence. Useful for + testing the full bridge plumbing end-to-end. + +Both are real. Neither is the game. + +## What's NOT playable today + +- The full Claude-Code-session-activity → in-game-rewards loop. + The bridge half works; the mod half is missing. No save-edit + tool exists yet. +- The Unciv mod itself may or may not load in Unciv. The JSON + files have plausible shapes but have never been validated by a + running Unciv parser. Nations.json and Buildings.json are most + likely to work (those are docs-recognized canonical files); + Beliefs / Tutorials / ModOptions have less certainty. + +## Acceptance criteria honest re-grade + +(Compared to the 2026-05-08 sprint-end claim. See the retraction +header in `docs/audit-2026-05-08.md` for the closure-by-PR audit.) + +| # | Subject | Honest 2026-05-11 status | +|---|---------|--------------------------| +| 1 | 10-min install | 🟡 (TUI yes; full game can't install today) | +| 2 | Session activity → next-turn rewards end-to-end | ❌ (bridge half only) | +| 3 | Waiting session → zero | ✅ | +| 4 | Redirect resets momentum + log | ✅ | +| 5 | Plan triggers visible in-game bonus | ❌ (mechanism was fictional) | +| 6 | Goal triggers visible in-game bonus | ❌ (mechanism was fictional) | +| 7 | Map + UI medieval/parchment | 🟡 (TUI yes; Unciv no) | +| 8 | Emotional contract first onboarding | ✅ (TUI side) | +| 9 | Tutorial <10 min → campaign | ❌ (state machine real; in-engine flow doesn't exist) | +| 10 | Bridge tests pass in CI on PRs | ✅ | +| 11 | README GIF | ❌ (depends on game working) | +| 12 | Four contribution paths documented | ✅ | +| 13 | Good first issue completable unaided | 🟡 (5 candidate bodies ready in `docs/good-first-issues.md`; GitHub label not surfaced) | +| 14 | No Kotlin fork | ✅ | +| 15 | Turn-boundary, not real-time | ✅ | +| 16 | State preserved on close | ✅ (bridge side; Unciv-side untested) | +| 17 | Suspended vs Idle distinct | ✅ | +| 18 | Long-running, no forced end | 🟡 (architecturally yes; needs game working) | +| 19 | Frontier visible each session | ✅ (TUI side) | +| 20 | Tutorial → campaign handoff same session | ❌ (state machine real; in-engine flow doesn't exist) | +| 21 | Spike ADRs before M0 | ✅ | + +**Real total: 10 ✅ + 5 🟡 + 6 ❌** vs the 2026-05-08 sprint-end claim +of **18 ✅** to varying degrees. The retraction is significant. + +## What to do next (humans + future agent passes) + +The fastest realistic path to a real V1: + +1. **Install Unciv** (Windows) and try to load `mod/ClaudeKingdoms/`. + Note every JSON parse error and field-name complaint — those + tell you exactly which schemas need fixing. Likely 1-3 hours of + iteration. +2. **Inspect an Unciv save file** — open a saved game's file on disk. + Note its format (probably JSON, possibly compressed). Note where + per-city yields live. This unblocks the save-edit tool design. +3. **Write the save-edit tool** (`tools/save_transformer/` or similar + — pick a path). Python is fine; it reads + `./saves/kingdom_save.json` (bridge output) and writes deltas + into the Unciv save file path. Atomic writes (temp+rename) as + `bridge/save_exchange.py` already demonstrates. +4. **Author a real tutorial scenario** by playing a game in + Unciv with the mod enabled, editing it via Unciv's dev console + per Unciv's scenarios docs, and copying the save file into + `mod/ClaudeKingdoms/scenarios/Tutorial/` (without the JSON + manifests we deleted). +5. **Capture the README GIF** once steps 1-4 work. +6. **M4 fun-factor playtest** once the loop is real. + +Steps 1-3 are the critical path. Without them, nothing else +matters. + +## Operational state (Hermes / runtime) + +- **Hermes Agent**: v0.13.0 in WSL Ubuntu. Stopped + disabled on + 2026-05-09 to avoid token charges. Restart with: + `wsl -d Ubuntu -- bash -lc 'systemctl --user enable --now hermes-gateway hermes-dashboard'` +- **Cron `a5ddf699`**: deleted at end of supervision sprint. +- **No background processes running** for Claude Kingdoms. + +## Bottom line + +The Python half of Claude Kingdoms is real, tested, well-documented +work. The Unciv half is an unimplemented save-edit tool plus +unvalidated JSON content. The V1 PRD has not been achieved. The +remediation path is clear and not hidden — it just hasn't been +walked yet. diff --git a/.gitignore b/.gitignore index d1facae..5cebf1c 100644 --- a/.gitignore +++ b/.gitignore @@ -25,4 +25,4 @@ Thumbs.db # Distribution dist/ build/ -*.egg-info/ \ No newline at end of file +*.egg-info/logs/ diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 9e6a27b..ed68ed5 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -48,11 +48,16 @@ What lives here: Good for you if: you've used Textual or want to learn it; you have opinions on accessibility, color contrast, or keyboard ergonomics. -### 3. Unciv mod (JSON + Lua) +### 3. Unciv mod (JSON content + offline save-edit tool) -The mod side is the actual game. It defines The Operators civ, the -Session Forge building, tutorials, and the Lua hook that reads the -bridge's save exchange and applies per-city rewards at turn boundary. +The mod side is the actual game content. It defines The Operators +civ, the Session Forge building, tutorials, and the pantheon belief. + +Unciv does NOT support Lua mod scripting or runtime hooks (verified +in `docs/unciv-research-2026-05-11.md`), so dynamic per-turn rewards +must be applied through an **external save-edit tool** that mutates +Unciv's save file while the game is closed (ADR-001's model). That +tool is the next missing piece of the project. Files: `mod/ClaudeKingdoms/`. Install instructions: `mod/ClaudeKingdoms/README.md`. @@ -62,11 +67,11 @@ What lives here: - `jsons/Buildings.json` — Session Forge + Charter Hall - `jsons/Tutorials.json` — six tutorials including the no-decay promise - `jsons/Beliefs.json` — pantheon for theme -- `mod.lua` — runtime hook reading `kingdom_save.json` - `ModOptions.json` — Unciv mod metadata -Good for you if: you've modded Unciv or another Civ-like, you read -Lua fluently, or you want to author medieval-flavor content. +Good for you if: you've modded Unciv or another Civ-like, you want +to author medieval-flavor content, or you'd like to build the +offline save-edit tool that closes the bridge↔Unciv loop. ### 4. Documentation (Markdown, no code) @@ -96,7 +101,7 @@ notice when an instruction doesn't match the code. | Bridge → TUI schema compliance | Shared (Bridge + TUI) | Contract (`bridge/tests/test_prd_schema.py`) | | Save exchange write/read round-trip | Bridge contributor | Integration | | TUI state rendering from bridge payload | TUI contributor | Unit | -| Mod JSON / Lua | Mod contributor | Smoke (`bridge/tests/test_mod_jsons.py`) | +| Mod JSON content | Mod contributor | Smoke (`bridge/tests/test_mod_jsons.py`) | | End-to-end vertical slice | Shared | Integration (`bridge/tests/test_e2e_vertical_slice.py`) | CI threshold per PRD: **80%** coverage for bridge scoring + session @@ -155,8 +160,6 @@ Some standing options if no labeled issues are open: explanation. - Markdown: 80-col soft limit, ATX headers (`#`), prefer tables for status lists, fenced code blocks with language tags. -- Lua: 4-space indent, `local` for everything by default, no globals - outside an explicit `mod` table. - JSON: 2-space indent, trailing commas NOT allowed (Unciv parses with strict JSON). diff --git a/bridge/tests/test_integration.py b/bridge/tests/test_integration.py deleted file mode 100644 index 6e915fa..0000000 --- a/bridge/tests/test_integration.py +++ /dev/null @@ -1,277 +0,0 @@ -"""End-to-end integration test for the bridge <-> Unciv mod contract. - -This test verifies the full flow: -1. Bridge writes kingdom_save.json with per-city rewards -2. Mod reads and parses the file -3. Rewards are applied to cities -4. File is deleted after processing - -Since the mod is in Lua and requires the Unciv environment, we simulate the -mod's behavior in Python to test the file format and contract. -""" - -import json -from pathlib import Path -from typing import Dict, Any - -import pytest - -from bridge.save_exchange import SaveExchange -from bridge.session_state import SessionState, SessionStatus -from bridge.scoring import ScoringEngine, SimpleScoringStrategy - - -def simulate_mod_turn_end(file_path: Path) -> Dict[str, Any]: - """Simulates the mod's turnEnd hook processing of kingdom_save.json. - - This function mimics the behavior of the Lua mod: - - Reads and parses the JSON file - - Extracts per_city_rewards - - Deletes the file after successful processing - - Returns the rewards dictionary - - Returns: - dict: per_city_rewards table, or empty dict if file doesn't exist/invalid. - """ - # Return empty rewards if file doesn't exist (mod behavior) - if not file_path.exists(): - return {} - - try: - with open(file_path, "r", encoding="utf-8") as f: - content = json.load(f) - except (json.JSONDecodeError, OSError) as e: - # Mod prints a warning but continues; file is NOT deleted on error - print(f"WARNING: Could not read kingdom_save.json: {e}") - return {} - - # Extract per_city_rewards (mod behavior) - per_city_rewards = content.get("per_city_rewards", {}) - - # Delete the file after successful processing (mod behavior) - try: - file_path.unlink() - except OSError as e: - print(f"WARNING: Failed to delete kingdom_save.json: {e}") - - return per_city_rewards - - -def test_integration_happy_path(tmp_path): - """Test the full integration flow: bridge writes file, mod processes it. - - Steps: - 1. Create a temporary exchange directory - 2. Set up session state with per-city rewards - 3. Bridge writes kingdom_save.json via SaveExchange - 4. Verify file exists and has correct structure - 5. Simulate mod's turnEnd processing - 6. Verify file is deleted after successful processing - 7. Verify rewards are correctly extracted - """ - # 1. Setup exchange directory - exchange_dir = tmp_path / "exchange" - exchange_dir.mkdir() - - # 2. Create session state with multiple cities and rewards - state = SessionState(kingdom_name="Camelot") - s1 = state.add_session("Royal Court") # default status = ACTIVE - s1.city_id = "camelot" - s1.goals_completed = 2 # More goals = more gold - - s2 = state.add_session("North Camp") - s2.city_id = "north_fort" - s2.goals_completed = 1 - - s3 = state.add_session("East Watch") - s3.city_id = "east_watch" - - # 3. Write the payload (bridge side) - ex = SaveExchange(exchange_dir) - ex.write_payload(state) - - # 4. Verify file exists and has correct content - save_file = exchange_dir / "kingdom_save.json" - assert save_file.exists(), "kingdom_save.json should exist after write_payload" - - with open(save_file) as f: - data = json.load(f) - - # Check required top-level keys - assert "version" in data - assert "per_city_rewards" in data - assert "kingdom_name" in data - assert "current_turn" in data - - per_city = data["per_city_rewards"] - assert isinstance(per_city, dict) - assert "camelot" in per_city - assert "north_fort" in per_city - assert "east_watch" in per_city - - # Check reward types (should be integers) - camelot_rewards = per_city["camelot"] - for resource in ["gold", "science", "culture", "production"]: - assert resource in camelot_rewards, f"Missing {resource} for camelot" - assert isinstance(camelot_rewards[resource], int) - - # 5. Simulate mod processing (read, apply rewards, delete) - rewards = simulate_mod_turn_end(save_file) - - # 6. Verify file was deleted after successful processing - assert not save_file.exists(), "kingdom_save.json should be deleted after processing" - - # 7. Verify rewards are correctly extracted - assert "camelot" in rewards - assert "north_fort" in rewards - assert "east_watch" in rewards - - # Verify non-zero rewards for camelot (has goals) - assert rewards["camelot"]["gold"] > 0 - assert rewards["camelot"]["production"] > 0 - - # Verify north_fort has rewards - assert rewards["north_fort"]["gold"] > 0 - - -def test_integration_missing_file(tmp_path): - """Test that mod handles missing file gracefully.""" - exchange_dir = tmp_path / "exchange" - exchange_dir.mkdir() - - save_file = exchange_dir / "kingdom_save.json" - # File does not exist - - rewards = simulate_mod_turn_end(save_file) - - assert not save_file.exists() - assert rewards == {}, "Expected empty rewards when file is missing" - - -def test_integration_invalid_json(tmp_path): - """Test that mod handles invalid JSON gracefully. - - Note: In the Lua mod, if JSON is invalid, the file is NOT deleted - (to allow debugging), so we expect the file to remain. - """ - exchange_dir = tmp_path / "exchange" - exchange_dir.mkdir() - - save_file = exchange_dir / "kingdom_save.json" - # Write invalid JSON - with open(save_file, "w") as f: - f.write("{\ninvalid json\n}") - - rewards = simulate_mod_turn_end(save_file) - - assert rewards == {}, "Expected empty rewards when JSON is invalid" - assert save_file.exists(), "File should NOT be deleted when JSON is invalid" - - -def test_integration_valid_json_missing_per_city_rewards(tmp_path): - """Test that mod handles valid JSON without per_city_rewards. - - Note: This is a valid JSON but missing the expected key. The mod - will treat it as no rewards and still delete the file. - """ - exchange_dir = tmp_path / "exchange" - exchange_dir.mkdir() - - save_file = exchange_dir / "kingdom_save.json" - # Write valid JSON but no per_city_rewards - valid_data = { - "version": "1.0.0", - "kingdom_name": "Test", - "current_turn": 1 - } - with open(save_file, "w") as f: - json.dump(valid_data, f) - - rewards = simulate_mod_turn_end(save_file) - - assert rewards == {}, "Expected empty rewards when per_city_rewards is missing" - assert not save_file.exists(), "File should be deleted even if per_city_rewards missing" - - -def test_integration_empty_per_city_rewards(tmp_path): - """Test that mod handles empty per_city_rewards table. - - This is a valid case (empty rewards), and the file should be deleted. - """ - exchange_dir = tmp_path / "exchange" - exchange_dir.mkdir() - - save_file = exchange_dir / "kingdom_save.json" - # Write JSON with empty per_city_rewards - data = { - "version": "1.0.0", - "kingdom_name": "Test", - "current_turn": 1, - "per_city_rewards": {} - } - with open(save_file, "w") as f: - json.dump(data, f) - - rewards = simulate_mod_turn_end(save_file) - - assert rewards == {}, "Expected empty rewards when per_city_rewards is empty" - assert not save_file.exists(), "File should be deleted when per_city_rewards is empty" - - -def test_integration_corrupted_file_not_deleted(tmp_path): - """Test that file is NOT deleted if reading fails (to allow debugging). - - The mod leaves the file on disk so the user can inspect the error. - """ - exchange_dir = tmp_path / "exchange" - exchange_dir.mkdir() - - save_file = exchange_dir / "kingdom_save.json" - # Write a file that causes JSON parse error - with open(save_file, "w") as f: - f.write("{") - - # First simulation should fail and NOT delete the file - rewards1 = simulate_mod_turn_end(save_file) - assert rewards1 == {} - assert save_file.exists(), "File should remain after failed read" - - # Clean up: delete the corrupted file manually after test - save_file.unlink() - - -def test_integration_save_exchange_writes_correct_schema(tmp_path): - """Verify that SaveExchange writes the correct schema expected by the mod.""" - exchange_dir = tmp_path / "exchange" - exchange_dir.mkdir() - - state = SessionState(kingdom_name="Eileadora") - s = state.add_session("Main Guild") - s.city_id = "guildhall" - s.goals_completed = 3 - - ex = SaveExchange(exchange_dir) - ex.write_payload(state) - - save_file = exchange_dir / "kingdom_save.json" - assert save_file.exists() - - with open(save_file) as f: - data = json.load(f) - - # Check schema version - assert data.get("version") == "1.0.0", f"Expected version 1.0.0, got {data.get('version')}" - - # Check per_city_rewards exists and has correct structure - assert "per_city_rewards" in data - per_city = data["per_city_rewards"] - assert isinstance(per_city, dict) - assert "guildhall" in per_city - - # Check that rewards are integers - guildhall_rewards = per_city["guildhall"] - for resource, amount in guildhall_rewards.items(): - assert isinstance(amount, int), f"{resource} should be int, got {type(amount)}" - - # Clean up - save_file.unlink() diff --git a/bridge/tests/test_per_city_status.py b/bridge/tests/test_per_city_status.py index 5769ee3..c6adc00 100644 --- a/bridge/tests/test_per_city_status.py +++ b/bridge/tests/test_per_city_status.py @@ -124,13 +124,14 @@ def test_per_city_status_empty_when_no_sessions(tmp_path): # ──────────────────────────────────────────────────────────────────────── -# Mod-side contract sanity (no Lua runtime — just documents the keys) +# Downstream-consumer contract: lowercase, stable, machine-friendly values # ──────────────────────────────────────────────────────────────────────── -def test_per_city_status_uses_lowercase_status_values_for_lua_glyph_table(): - """mod.lua's STATUS_GLYPHS table is keyed by lowercase strings — - "active", "redirected", "waiting", etc. — so the bridge must emit - SessionStatus.value (already lowercase) verbatim. +def test_per_city_status_uses_lowercase_machine_friendly_values(): + """Downstream consumers (a future save-edit tool, or whatever + surface the eventual Unciv integration uses) need stable lowercase + keys. The bridge emits SessionStatus.value verbatim, which is + already lowercase, so a glyph/lookup table can key off it directly. """ sessions = [ _session("a", "c", SessionStatus.ACTIVE), diff --git a/bridge/tests/test_tutorial_flow.py b/bridge/tests/test_tutorial_flow.py index a088f16..d27524a 100644 --- a/bridge/tests/test_tutorial_flow.py +++ b/bridge/tests/test_tutorial_flow.py @@ -1,11 +1,14 @@ -"""Tests for tutorial / campaign flow state machine + scenario JSONs. +"""Tests for tutorial / campaign flow state machine. PRD acceptance #9: Tutorial completes <10 min, transitions to long-running campaign. PRD acceptance #20: Tutorial→campaign handoff works in same session. -""" -import json -from pathlib import Path +Note: the bridge-side state machine is what's tested here. The actual +in-engine Unciv scenarios that the bridge hands off into are +authored as saved-game files dropped in the mod's `scenarios/` folder +(see docs/unciv-research-2026-05-11.md) — that's a separate authoring +deliverable, not a Python test surface. +""" import pytest @@ -22,10 +25,6 @@ ) -REPO_ROOT = Path(__file__).resolve().parents[2] -SCENARIO_ROOT = REPO_ROOT / "mod" / "ClaudeKingdoms" / "scenarios" - - # ──────────────────────────────────────────────────────────────────────── # State machine # ──────────────────────────────────────────────────────────────────────── @@ -155,75 +154,29 @@ def test_atomic_write_no_temp_files_on_disk(tmp_path): # scenario_to_load # ──────────────────────────────────────────────────────────────────────── -def test_scenario_loader_returns_tutorial_when_not_yet_done(): +def test_scenario_stage_returns_tutorial_when_not_yet_done(): flow = TutorialFlow() - assert scenario_to_load(flow) == "Tutorial" + assert scenario_to_load(flow) == "tutorial" flow.transition(TUTORIAL_ACTIVE) - assert scenario_to_load(flow) == "Tutorial" + assert scenario_to_load(flow) == "tutorial" -def test_scenario_loader_returns_campaign_after_completion(): +def test_scenario_stage_returns_campaign_after_completion(): flow = TutorialFlow() flow.transition(TUTORIAL_ACTIVE) flow.transition(TUTORIAL_COMPLETE) - assert scenario_to_load(flow) == "LongRunningCampaign" + assert scenario_to_load(flow) == "campaign" -def test_scenario_loader_returns_campaign_after_skip(): +def test_scenario_stage_returns_campaign_after_skip(): flow = TutorialFlow() flow.transition(CAMPAIGN_ACTIVE) - assert scenario_to_load(flow) == "LongRunningCampaign" - - -# ──────────────────────────────────────────────────────────────────────── -# Scenario JSON files (mod-side content) -# ──────────────────────────────────────────────────────────────────────── - -def test_tutorial_scenario_files_parse(): - for name in ("scenario.json", "Map.json"): - path = SCENARIO_ROOT / "Tutorial" / name - assert path.exists(), f"missing {path}" - json.loads(path.read_text(encoding="utf-8")) - - -def test_campaign_scenario_files_parse(): - for name in ("scenario.json", "Map.json"): - path = SCENARIO_ROOT / "LongRunningCampaign" / name - assert path.exists(), f"missing {path}" - json.loads(path.read_text(encoding="utf-8")) - - -def test_tutorial_scenario_specifies_under_ten_minutes(): - """PRD acceptance #9 anchor.""" - data = json.loads((SCENARIO_ROOT / "Tutorial" / "scenario.json").read_text()) - assert data["tutorialFlags"]["expectedDurationMinutes"] <= 10 - - -def test_tutorial_scenario_handoff_target_is_campaign(): - data = json.loads((SCENARIO_ROOT / "Tutorial" / "scenario.json").read_text()) - assert data["claudeKingdomsMeta"]["handoffTarget"] == "LongRunningCampaign" - - -def test_campaign_scenario_has_no_forced_end_condition(): - """PRD acceptance #18: no forced end.""" - data = json.loads((SCENARIO_ROOT / "LongRunningCampaign" / "scenario.json").read_text()) - assert data["victoryConditions"] == [] - assert data["campaignFlags"]["noForcedEnd"] is True - - -def test_campaign_scenario_supports_no_decay(): - """PRD acceptance #16, BS-6: state preserved on close.""" - data = json.loads((SCENARIO_ROOT / "LongRunningCampaign" / "scenario.json").read_text()) - assert data["campaignFlags"]["noDecayOnClose"] is True + assert scenario_to_load(flow) == "campaign" -def test_tutorial_map_has_a_starting_city(): - """The tutorial must place exactly one city — PRD: 'one city, one session, one turn'.""" - data = json.loads((SCENARIO_ROOT / "Tutorial" / "Map.json").read_text()) - cities = [] - for row in data["tiles"]: - for tile in row: - if tile.get("city"): - cities.append(tile["city"]) - assert len(cities) == 1 - assert cities[0] == "Camelot" +# Note: tests asserting the existence/shape of `scenario.json` and +# `Map.json` files were removed on 2026-05-11 after Unciv research +# (docs/unciv-research-2026-05-11.md) showed those files were the wrong +# shape — Unciv scenarios are copied Unciv saved-game files, not +# bespoke JSON manifests. The scenario authoring belongs to a future +# in-engine pass, not a unit test surface. diff --git a/bridge/tutorial.py b/bridge/tutorial.py index 65d3547..e4c9375 100644 --- a/bridge/tutorial.py +++ b/bridge/tutorial.py @@ -158,16 +158,22 @@ def reset(self) -> None: # ──────────────────────────────────────────────────────────────────────── -# Helper: scenario the Unciv mod should load +# Helper: which scenario stage the player is in # ──────────────────────────────────────────────────────────────────────── -def scenario_to_load(flow: TutorialFlow) -> str: - """Returns the scenario filename the Unciv mod should load. +def scenario_stage(flow: TutorialFlow) -> str: + """Returns a stage identifier the launcher / save editor can branch on. - Maps each flow state to one of two scenarios shipped in - `mod/ClaudeKingdoms/scenarios/`. Fallback is the campaign map - (no-tutorial = direct-to-campaign). + Per docs/unciv-research-2026-05-11.md, Unciv scenarios are + saved-game files copied into a mod's `scenarios/` folder — not + JSON manifests. This helper returns a STAGE NAME ("tutorial" / + "campaign"), not a filename, so callers (e.g. an external save + editor that picks which seeded save to open) can route accordingly. """ if flow.state in (NOT_STARTED, TUTORIAL_ACTIVE): - return "Tutorial" - return "LongRunningCampaign" + return "tutorial" + return "campaign" + + +# Back-compat alias for any caller still using the old name. +scenario_to_load = scenario_stage diff --git a/docs/audit-2026-05-08.md b/docs/audit-2026-05-08.md index 27e835b..2076d8b 100644 --- a/docs/audit-2026-05-08.md +++ b/docs/audit-2026-05-08.md @@ -1,15 +1,39 @@ # Claude Kingdoms — CTO-Style Audit Against PRD v1.5 -**Date:** 2026-05-08 (updated 14:00 PDT after the 5-hour supervision sprint) +**Date:** 2026-05-08 (with 2026-05-11 retraction header — read this first) **Reviewer:** claude-code -**Repo HEAD:** `ea739ba` (PR #30 merged) → see closure status below for live state -**Test count on `main`:** 234+ passing (was 149 at audit time) - -> **Closure update — every P0 and P1 line item identified in this audit -> is now closed.** PR refs: #23 (P0 #1 enum), #24 (P0 #3 Stop→COMPLETED), -> #25 (P0 #2 schema), #26 (BR-9), #27 (#11 + #12), #28 (GM-8), #29 -> (GM-5), #30 (#9 + #20). The original audit grid below is preserved -> as the historical baseline. + +> ## ⚠ 2026-05-11 RETRACTION — half the "✅" entries below are wrong +> +> Subsequent Unciv mod research (see `docs/unciv-research-2026-05-11.md`) +> found that **Unciv does not support Lua mod scripting or runtime +> hooks**. The `mod/ClaudeKingdoms/mod.lua` file shipped in PRs #28 +> and #29, and the `scenarios/Tutorial/` + `scenarios/LongRunningCampaign/` +> folders shipped in PR #30, were **architectural fiction** — Unciv +> would not have loaded them. They were deleted on 2026-05-11. +> +> Closure entries below that demote on this finding: +> +> | Line | Original claim | Real status | +> |------|----------------|-------------| +> | BR-10 | "SaveExchange writes kingdom_save.json per turn; mod.lua polls" ✅ | 🟡 — file written; nothing polls it. Needs save-edit tool. | +> | BS-2 | "mod.lua reads kingdom_save.json" ✅ | ❌ — mod.lua deleted, no replacement | +> | BS-3 | "Mod is pure JSON + Lua" ✅ | 🟡 — pure JSON only, no Lua possible | +> | GM-4 | "mod.lua reads + applies + deletes file" ✅ | ❌ — fiction, deleted | +> | GM-5 | "Bonus is in scoring; mod.lua applies the per-city total" 🟡 | ❌ — fiction, deleted | +> | GM-8 | "mod.lua does not emit tile annotations" ❌ (already correct) | ❌ — still ❌, mod.lua was wrong fix | +> | Acc. #2 | "Hook stream → real resources end-to-end" ✅ | 🟡 — bridge half works; mod half is missing | +> | Acc. #5 / #6 | "Visible bonus announcement" ✅ (post-PR #29) | ❌ — was via mod.lua, fiction | +> | Acc. #9 / #20 | "Tutorial → campaign flow" ✅ (post-PR #30) | 🟡 — bridge state machine real; scenarios fiction | +> +> What's still real: the Python bridge (state, scoring, hook listener, +> save exchange file I/O, audit log, tutorial state machine, +> daemon, TUI), all 248 tests after fictional-test removal, the +> JSON mod content (Nations/Buildings/Beliefs/Tutorials/ModOptions +> — pending field-by-field validation against Unciv's real parser). +> +> The original audit grid below is preserved as the historical +> baseline so the journey is auditable. This is a hard-eyed alignment review against `ClaudeKingdomsPRD.md` v1.5. It calls out what's truly shipped, where the implementation diverges diff --git a/docs/bridge-tui-schema.md b/docs/bridge-tui-schema.md index 2fc193c..049f210 100644 --- a/docs/bridge-tui-schema.md +++ b/docs/bridge-tui-schema.md @@ -37,8 +37,11 @@ guarantee from the PRD's M0 milestone. `version` is the legacy key, `schema_version` is the PRD-mandated key (line 231). Readers MAY use either; writers MUST emit both. -`per_city_rewards` is what the Unciv mod (`mod/ClaudeKingdoms/mod.lua`) -consumes at turn boundary to apply the bridge's allowed-resource totals. +`per_city_rewards` is the per-turn delta an external **save-edit +tool** would inject into an Unciv save file while the game is closed +(per ADR-001 — Unciv does not support runtime mod scripting, so the +mod itself cannot read this payload). See +`docs/unciv-research-2026-05-11.md` for the architectural rationale. ## Session shape (PRD-mandated minimum fields) diff --git a/docs/unciv-research-2026-05-11.md b/docs/unciv-research-2026-05-11.md new file mode 100644 index 0000000..03ce7bb --- /dev/null +++ b/docs/unciv-research-2026-05-11.md @@ -0,0 +1,254 @@ +# Unciv Mod Integration Research — 2026-05-11 + +Source: research mission conducted via Perplexity Comet against the +Unciv docs site (yairm210.github.io/Unciv/) and the Unciv repo +(github.com/yairm210/Unciv). Conducted because the 2026-05-08 audit +sprint shipped `mod.lua` + bespoke `scenario.json` files without +verifying that Unciv actually supports those integration patterns. + +Source-card tags like `[ucanr]`, `[etd.gsfc.nasa]`, `[ccsds]`, +`[yairm210.github]` in the raw text below are Perplexity citation +markers from the extraction; the substantive references are the +Unciv docs site and repo. + +--- + +## Question 1 — Does Unciv support Lua mod scripting? + +**Direct answer:** **No verified evidence of Lua mod scripting in +Unciv.** The docs point modding toward JSON rulesets, uniques, +scenarios-as-saved-games, and an in-game developer console — not an +embedded scripting runtime. No documented runtime hook-registration +API for mods such as `turnEnd` or `buildComplete` was found. + +**Evidence:** + +- The modding docs emphasize JSON-based content and schema validation: + `Buildings.json`, `Units.json`, `Nations.json`, `TileImprovements.json`, + `Techs.json`, `UnitTypes.json`, `UnitPromotions.json`, + `TileResources.json`, `Events.json`, `Terrains.json`. +- The "Uniques" docs describe gameplay behavior as unique strings + attached to Nations, Buildings, Policies, Beliefs, etc. +- The "Scenarios" docs say scenario creation is done by creating a + game, entering as spectator, editing the save with the console, + then copying the save into a `scenarios/` folder inside the mod. +- No docs page or verified source file showed an embedded Lua engine, + Lua dependency, or a mod API for registering callback events. + +**How Unciv is actually scriptable:** + +- **Primary extension surface:** JSON ruleset files + "uniques" + strings interpreted by the engine. +- **Scenario authoring surface:** save-game editing via the built-in + developer console, then shipping the save under a mod's + `scenarios/` folder. +- **Kotlin source patching:** possible only if Unciv itself is forked + (not "mod scripting"). +- **Console commands:** built-in developer console for manual editing. + +**Confidence: medium.** Strong evidence for JSON/uniques/console/ +scenarios; a wider repo grep would be required to prove "no Lua +anywhere" as a hard negative. + +--- + +## Question 2 — Real JSON schemas for our mod files + +**Direct answer:** The canonical schema reference is the docs page +"Type checking," which maps several mod JSON filenames to schema +URLs in `docs/Modders/schemas/`. **It also says: "As of now, only +Buildings and Units have proper schema."** For Beliefs, Tutorials, +and ModOptions, no canonical schema file was retrievable from the +sources in this session — their exact shape should not be guessed. + +**File-by-file:** + +| File | Source-confirmed | Not confirmed here | +|------|------------------|---------------------| +| Nations.json | Recognized mod file type; schema URL path referenced | Exact required fields | +| Buildings.json | Recognized; docs say it has a "proper schema" | The schema body content | +| Beliefs.json | Mentioned via FounderBelief / FollowerBelief uniques | Exact JSON shape | +| Tutorials.json | No canonical schema docs page retrieved | Whether dict-of-arrays or list-of-objects | +| ModOptions.json | No canonical schema docs page retrieved | Field meanings (isBaseRuleset, topics, modSize) | + +**Confidence:** **low** for field-by-field schema details, **high** +for the narrower claim that the type-checking docs are the canonical +starting point and only Buildings/Units are "proper schema." + +--- + +## Question 3 — Scenarios and maps + +**Direct answer:** A scenario in Unciv is literally a saved game — +"the scenario is just a specific game state, or in other words — a +saved game." Scenarios live in a `scenarios/` folder inside the mod. +**Our `scenario.json + Map.json` pair is NOT the documented format.** + +**Evidence:** + +- Scenarios docs: "Scenarios are specific game states." "The + scenario is just a specific game state, or in other words — a + saved game." +- Creation: "create a new game, add a spectator, edit it using the + console, save it, and copy the game save file to a `scenarios` + folder in your mod." +- A scenario file is a normal Unciv save file repurposed as packaged + mod content. + +**Sub-questions:** + +- Scenario vs custom map: scenario is broader (it's the save state), + which may include map state. +- Where scenarios live: `scenarios/` folder in the mod. +- Where standalone custom maps live: **not documented in retrieved + sources.** +- File format for scenarios: Unciv saved-game format. +- File format for standalone maps: **not verified here.** +- Real example mod shipping a custom map: **not found / not + verified in this session.** + +**Confidence: medium** on scenarios, **low** on standalone custom-map +packaging. + +--- + +## Question 4 — Integration model for "external work → in-game rewards" + +**Direct answer:** Of the two proposed models, the one clearly +supported by retrieved source is **Model A: save-file injection +while the game is closed.** Unciv scenarios are literally saved +games, and the dev docs describe save serialization in detail. +**No documentation was found showing a mod can run arbitrary code at +turn boundaries or perform runtime file I/O. Model B is not +supported by evidence retrieved here.** + +**Evidence:** + +- Scenarios docs explicitly treat a scenario as a saved game copied + into a mod. +- "Saved games and transients" dev doc explains Unciv save + serialization: it saves "the bare minimum," stores names instead + of pointers, uses `@Transient` fields for runtime-only references, + and rebuilds them via `setTransients` on load. +- Uniques are saved as strings and reconstructed into runtime + `Unique` instances — supporting a **data-file-driven load + pipeline rather than a mod code-execution pipeline.** +- **No docs showed a mod callback API like `onTurnEnd`. No Lua + runtime. No documented permission for mods to read external files + during gameplay.** + +**Save-file format:** + +- Documented: minimal serialized game state, names instead of + pointers, omits recalculable runtime data, transient links rebuilt + via `setTransients` on load. +- Not documented in retrieved sources: exact on-disk container + details (plain JSON vs compressed JSON vs another wrapper). + +**Which model is actually supported?** + +- **Model A (save-file injection while closed): SUPPORTED** by + documented save-based scenario workflow and serialization model. +- **Model B (runtime mod hooks): NOT SUPPORTED** by any retrieved + evidence. + +**Confidence: high** that save-file manipulation aligns with the +engine's documented model; **low** that runtime mod hooks exist. + +--- + +## Question 5 — Existing Unciv mods that integrate with external programs + +**Direct answer:** No 2-3 real Unciv mods with verified external- +program integration were found in this session — **not found / not +documented**. The absence is consistent with Unciv favoring +data-driven rulesets and saved-game scenarios over executable +integration plugins. + +**Confidence: low** — broader GitHub spelunking would be needed for a +firm answer. + +--- + +## Verdict and remediation plan + +**Treat Claude Kingdoms as a data-driven Unciv mod plus an optional +offline save transformer — NOT as a runtime-scripted mod.** + +### File-by-file usability + +| File | Usable as-is? | Why | +|------|---------------|-----| +| `mod/ClaudeKingdoms/ModOptions.json` | Unknown; needs validation | Canonical field contract not verified | +| `mod/ClaudeKingdoms/jsons/Nations.json` | Potentially salvageable | Real mod surface; exact fields need validation | +| `mod/ClaudeKingdoms/jsons/Buildings.json` | Probably salvageable | Canonical + explicitly schema-backed in docs | +| `mod/ClaudeKingdoms/jsons/Beliefs.json` | Unknown; partial rewrite likely | Concept real, schema unverified | +| `mod/ClaudeKingdoms/jsons/Tutorials.json` | Likely rewrite | Shape not source-verified — invented structure unsafe | +| `mod/ClaudeKingdoms/mod.lua` | **NO — likely fiction. Remove.** | No evidence of Lua runtime or hook API in Unciv | +| `mod/ClaudeKingdoms/scenarios/Tutorial/scenario.json + Map.json` | **NO — wrong packaging.** | Docs say scenarios are copied Unciv save files | +| `mod/ClaudeKingdoms/scenarios/LongRunningCampaign/scenario.json + Map.json` | **NO — wrong packaging.** | Same | + +### Rewrite scope + +- **Low-to-medium:** Nations.json, Buildings.json, possibly Beliefs.json + (concept sound, field names need correction against real examples) +- **Medium-to-high:** ModOptions.json, Tutorials.json (schema details + unverified) +- **High / replace entirely:** mod.lua and both scenario folders + +### Practical path forward + +1. **Keep the mod as a pure JSON ruleset/content pack.** That part + of the architecture aligns with Unciv's documented modding model. +2. **Drop the Lua-hook architecture** unless and until repo code + proves otherwise (a maintainer-level deep dive into the Unciv + source would be needed to refute the negative finding here). +3. **Implement Claude Kingdoms rewards through an external save + editor that runs while the game is closed**, then reopens into + the modified save. That's the only one of the two models that + aligns with verified docs. This is what Spike 1 / ADR-001 + actually validated, before the project drifted into a Lua-hook + assumption. +4. **Rebuild scenarios as actual Unciv saved games** created + in-engine, edited via the developer console if needed, then + copied into the mod's `scenarios/` folder. + +### What this means for the existing 262 tests + +Almost all bridge-side tests stay valid — the scoring engine, the +session state machine, the save exchange's atomic I/O, the audit +log, the hook listener (treated as a generic event ingestor, not +specifically Claude-Code-hook tied), the tutorial state machine — +none of those rely on Unciv's Lua. They're a clean reusable +core. + +What needs reframing: the **Bridge–Mod interface contract.** Today +we write `kingdom_save.json` and expect `mod.lua` to read it at turn +end. Under the correct model, we write directly into an **Unciv +save file** while the game is closed. That's a different on-disk +target with a different schema (Unciv's save format, not our +invented one). + +### Honest re-grade of the 2026-05-08 audit + +Compared to docs/audit-2026-05-08.md, these claims **demote** to +unverified / wrong: + +- Acceptance #2 ("Session activity drives next-turn rewards + end-to-end") — still works at the bridge level, but the bridge- + to-Unciv leg is fundamentally broken pending the integration- + model rewrite. +- Acceptance #5 / #6 ("Plan / goal events trigger visible in-game + bonus") — `mod.lua` was the visible-bonus mechanism. Fiction. +- GM-4 ("City and empire rewards resolve at turn boundary from + bridge-authored totals") — mechanism wrong. +- GM-5 (visible plan/goal bonus events) — fiction at the Unciv side. +- GM-8 (per-tile session status indicators) — fiction at the Unciv + side. +- Acceptance #9 / #20 (tutorial → campaign flow) — the bridge-side + state machine is real; the scenario files are wrong shape. + +Bridge-side correctness is unaffected. M0/M1/M2 of the Python work +are real. M3 medieval aesthetic (mod.lua + scenario format) is +substantively broken and needs to be redone against the +save-file-injection model. diff --git a/mod/ClaudeKingdoms/README.md b/mod/ClaudeKingdoms/README.md index 58655ec..84cf20a 100644 --- a/mod/ClaudeKingdoms/README.md +++ b/mod/ClaudeKingdoms/README.md @@ -15,24 +15,44 @@ Then enable the mod in Unciv via *Options → Mods*. - **Civilization: The Operators** — a medieval-styled civ whose theme is "real work, real rewards" (PRD §"Design Pillars" #1) - **Buildings: Session Forge, Charter Hall** — the in-game expression of - the Claude Kingdoms session economy (M2 deliverable) + the Claude Kingdoms session economy - **Tutorials** — surface the emotional contract from the PRD Onboarding section directly inside Unciv - **Pantheon Belief: Order of the Charter** — for theme consistency ## How it talks to the bridge -This mod is **JSON-only** by design (PRD §"Decision: mod, not fork"). -Resource yields are influenced by the Python bridge via the save-exchange -file at `./saves/kingdom_save.json`, written by `bridge/save_exchange.py`. -The Unciv mod consumes that file at turn boundaries — see `docs/adr/adr-001-unciv-integration.md`. - -If you want the bridge to actually drive resources from your real Claude -Code sessions, also follow `docs/hooks-setup.md` to wire up the hook -event pipeline. +This mod is **pure JSON content**. Unciv does not support Lua mod +scripting or runtime hooks (see `docs/unciv-research-2026-05-11.md`), +so dynamic per-turn rewards are NOT applied by the mod itself. + +Instead, the Claude Kingdoms architecture (per ADR-001) uses the +**save-file-injection model**: an external save-edit tool reads the +Python bridge's `kingdom_save.json`, computes per-city deltas, and +writes them directly into an Unciv save file while the game is +closed. The mod itself supplies civ/building/tutorial content for the +medieval flavor; rewards are applied to the save off-engine. + +> The save-edit tool ("save transformer") is the missing +> implementation. The bridge architecture is ready for it (see +> `bridge/save_exchange.py` for the file format the transformer +> should consume) but no `mod/ClaudeKingdoms/`-side script has been +> written yet because the file format and behavior depend on the +> Unciv save schema, which requires inspection of an actual Unciv +> save file. + +## Schema warnings + +Unciv's modding docs only validate `Buildings.json` and `Units.json` +as having "proper schema" (see the docs research linked above). +`Nations.json`, `Beliefs.json`, `Tutorials.json`, and `ModOptions.json` +in this folder use plausible-looking field names but have NOT been +validated against Unciv's actual parser. **Test by loading the mod +in Unciv before relying on any field name.** ## Status -V1 baseline: civ, buildings, tutorials, belief. The PRD lists a custom -parchment tileset and illuminated UI skin as M3 polish — those are -authored as image assets and are not in this PR. +V1 baseline content only. Scenarios (saved-game files in a +`scenarios/` folder) and image assets (parchment tileset, illuminated +UI skin) are not included — both require in-engine authoring against +a running Unciv. diff --git a/mod/ClaudeKingdoms/mod.lua b/mod/ClaudeKingdoms/mod.lua deleted file mode 100644 index 9f37621..0000000 --- a/mod/ClaudeKingdoms/mod.lua +++ /dev/null @@ -1,353 +0,0 @@ --- Claude Kingdoms Mod Main Script --- This file contains the main mod initialization and turn-end hook --- that reads rewards from the bridge and applies them to cities. - --- Initialize the mod when the game loads -local mod = {} - --- Configuration: Path to the exchange directory (relative to mod location) --- The bridge should write kingdom_save.json to this directory. -local EXCHANGE_DIR = "exchange" -local SAVE_FILE = "kingdom_save.json" - --- Resource types in Unciv (these are the keys used in city resources) -local RESOURCES = { - gold = "gold", - science = "science", - culture = "culture", - production = "production" -} - --- Cache for the last parsed rewards to avoid reading file multiple times per turn -local rewardCache = nil -local cacheTurn = -1 - ---[[ - Reads and parses the kingdom_save.json file from the exchange directory. - Returns a table with per-city rewards, or nil if file doesn't exist. - Also caches the result to avoid repeated file reads within the same turn. -]] -local function readKingdomSave(currentTurn) - -- Return cached data if we're in the same turn (avoid multiple reads) - if rewardCache ~= nil and cacheTurn == currentTurn then - return rewardCache - end - - local savePath = EXCHANGE_DIR .. "/" .. SAVE_FILE - local rewards = {} - - -- Check if the file exists using a safe method - local file, err = io.open(savePath, "r") - if not file then - if err ~= nil then - -- Only print error if there was a specific error (not just missing file) - print("WARNING: Could not open kingdom_save.json: " .. err) - end - rewardCache = rewards - cacheTurn = currentTurn - return rewards - end - - -- Read the file - local content = file:read("*a") - file:close() - - -- Parse JSON - local ok, parsed = pcall(function() - return json.decode(content) - end) - - if not ok then - print("ERROR: Failed to parse kingdom_save.json: " .. (parsed or "invalid JSON")) - rewardCache = rewards - cacheTurn = currentTurn - return rewards - end - - -- Validate the structure - if not parsed or type(parsed) ~= "table" then - print("ERROR: kingdom_save.json has invalid structure") - rewardCache = rewards - cacheTurn = currentTurn - return rewards - end - - -- Extract per_city_rewards - local perCityRewards = parsed.per_city_rewards - if perCityRewards and type(perCityRewards) == "table" then - rewards = perCityRewards - end - - -- Per-city status indicator (PRD §GM-8) — kept on the parsed table - -- so callers can opt in via getKingdomStatus(). - if parsed.per_city_status and type(parsed.per_city_status) == "table" then - rewards.__per_city_status__ = parsed.per_city_status - end - -- Per-city events (PRD §GM-5) — list of {kind, session_id, session_name} - -- per city for this turn, so the mod can announce "+15 from plan" etc. - if parsed.per_city_events and type(parsed.per_city_events) == "table" then - rewards.__per_city_events__ = parsed.per_city_events - end - - -- Cache the rewards for this turn - rewardCache = rewards - cacheTurn = currentTurn - - -- Delete the file after processing (so it's not processed again) - os.remove(savePath) - - return rewards -end - ---[[ - Returns the kingdom status table (city_id → status string) as last - parsed from the save exchange. Used by applyCityStatusIndicator at - turn boundary to surface the per-tile session indicator (PRD §GM-8). -]] -local function getKingdomStatus() - if rewardCache and rewardCache.__per_city_status__ then - return rewardCache.__per_city_status__ - end - return {} -end - ---[[ - Returns the per-city event log (city_id → array of event records) - for the current turn (PRD §GM-5). Each record has fields - { kind = "plan"|"goal"|"redirect", session_id, session_name }. - Mod consumers iterate this to render announcement messages per tile. -]] -local function getKingdomEvents() - if rewardCache and rewardCache.__per_city_events__ then - return rewardCache.__per_city_events__ - end - return {} -end - ---[[ - Format an event record as a player-facing announcement string. - PRD §GM-5: 'Plan and goal events trigger one-time visible bonuses - on turn resolution.' Returns a short medieval-flavor string. -]] -local function announcementFor(event) - if not event or not event.kind then return "" end - local who = event.session_name or "the realm" - if event.kind == "plan" then - return string.format("A new charter is drafted for '%s' (+15 production)", who) - elseif event.kind == "goal" then - return string.format("'%s' achieves a goal (+40 gold, +20 science)", who) - elseif event.kind == "redirect" then - return string.format("'%s' is redirected — momentum lost, banked tribute preserved", who) - end - return string.format("'%s' event: %s", who, tostring(event.kind)) -end - ---[[ - Apply per-tile event announcements (PRD §GM-5). Tries city.announce → - addNotification → falls back to print so the data flow is visible - even without an Unciv UI surface. pcall-wrapped. -]] -local function applyCityEventAnnouncements(city, eventList) - if not city or not eventList then return end - local cityName = "unknown" - if city.getName then cityName = city.getName(city) end - for _, event in ipairs(eventList) do - local msg = announcementFor(event) - print(string.format("[%s] %s", cityName, msg)) - if city.announce then - pcall(function() city.announce(city, msg) end) - elseif city.addNotification then - pcall(function() city.addNotification(city, msg) end) - end - end -end - ---[[ - Maps the bridge's session status string to a short tile glyph the - player can read at a glance without opening the TUI (PRD §GM-8). - Falls back to the raw status string if no glyph is registered. -]] -local STATUS_GLYPHS = { - active = "[*]", -- working - redirected = "[!]", -- redirected (transient) - waiting = "[~]", -- awaiting human - inactive = "[ ]", -- registered but quiet - completed = "[v]", -- session done - suspended = "[#]", -- game closed elsewhere; preserved -} - -local function statusGlyph(status) - if not status then return "" end - return STATUS_GLYPHS[tostring(status)] or ("[" .. tostring(status) .. "]") -end - ---[[ - Applies the per-tile session status indicator to a city tile (PRD §GM-8). - Tries setOverlay → setTooltip → setName-suffix in priority order; each - is wrapped in pcall so a missing Unciv API method never crashes the - mod. Logs the indicator regardless so the data flow is auditable. -]] -local function applyCityStatusIndicator(city, status) - if not city or not status then return end - local cityName = "unknown" - if city.getName then cityName = city.getName(city) end - local glyph = statusGlyph(status) - print(string.format("Status %s on %s", glyph, cityName)) - - if city.setOverlay then - pcall(function() city.setOverlay(city, glyph) end) - return - end - if city.setTooltip then - pcall(function() city.setTooltip(city, "Session: " .. tostring(status)) end) - return - end - if city.setName and city.getName then - local base = city.getName(city) - -- Avoid stacking glyphs across turns: strip any existing trailing glyph. - local stripped = base:gsub(" %[[^%]]+%]$", "") - pcall(function() city.setName(city, stripped .. " " .. glyph) end) - end -end - ---[[ - Applies rewards to a specific city. - @param city: The city object (from Unciv API) - @param rewards: Table of resource amounts {gold=0, science=0, culture=0, production=0} -]] -local function applyCityRewards(city, rewards) - if not city or not rewards then - return - end - - -- Get city name for debugging - local cityName = "unknown" - if city.getName then - cityName = city.getName(city) - end - - -- Apply each type of reward - for resource, amount in pairs(rewards) do - if amount and amount ~= 0 then - -- Ensure the resource type is valid - if RESOURCES[resource] then - -- Add the resources to the city - if resource == "production" then - -- Production is added directly to current production - local currentProd = 0 - if city.getProduction then - currentProd = city.getProduction(city) or 0 - end - city.setProduction(city, currentProd + amount) - print(string.format("Applied %d %s to %s", amount, resource, cityName)) - else - -- Gold, science, and culture are added as resources - if city.addResource then - city.addResource(city, RESOURCES[resource], amount) - print(string.format("Applied %d %s to %s", amount, resource, cityName)) - else - print(string.format("WARNING: City missing addResource method for %s", cityName)) - end - end - else - print(string.format("WARNING: Unknown resource type '%s' in rewards for %s", resource, cityName)) - end - end - end -end - ---[[ - Turn-end hook: Called at the end of each turn for each player. - This is where we read the kingdom_save.json and apply rewards. -]] -function mod.turnEnd(player) - -- Get the current turn number - local currentTurn = getTurn() - - -- Read rewards from the bridge - local rewards = readKingdomSave(currentTurn) - - if not next(rewards) then - -- No rewards to apply - return - end - - -- Determine the human player (the one controlling the kingdom) - local humanPlayer = nil - - -- Try to get human player via API if available - if getHumanPlayer then - humanPlayer = getHumanPlayer() - end - - -- If no API function, iterate through players to find the human - if humanPlayer == nil then - local playerCount = countPlayers() or 0 - for p = 0, playerCount - 1 do - if isHuman(p) then - humanPlayer = p - break - end - end - end - - -- Fallback to player 0 if human player not found - if humanPlayer == nil then - humanPlayer = 0 - print("WARNING: Could not determine human player, using player 0") - end - - -- Only apply rewards for the human player (the kingdom controller) - if player ~= humanPlayer then - return - end - - -- Get all cities for the player - local numCities = countCities(player) - if numCities == nil then - print("ERROR: Could not count cities for player " .. player) - return - end - - print(string.format("Processing %d city(ies) for turn %d rewards...", numCities, currentTurn)) - - -- Iterate through all cities - for i = 0, numCities - 1 do - local city = getCity(player, i) - if city then - local cityName = "unknown" - if city.getName then - cityName = city.getName(city) - end - - -- Check if there are rewards for this city - local cityRewards = rewards[cityName] - - if cityRewards then - -- Apply the rewards - applyCityRewards(city, cityRewards) - else - -- No rewards for this city this turn - -- print(string.format("No rewards for %s this turn", cityName)) - end - end - end - - -- Clear cache for next turn - if getTurn() ~= cacheTurn then - rewardCache = nil - end -end - ---[[ - Mod initialization: Register the turn-end hook. -]] -function mod.init() - -- Register our turnEnd function to be called at turn boundaries - registerHook("turnEnd", mod.turnEnd) - - print("Claude Kingdoms mod initialized successfully!") - print("Waiting for turn-end to apply kingdom rewards...") -end - -return mod \ No newline at end of file diff --git a/mod/ClaudeKingdoms/scenarios/LongRunningCampaign/Map.json b/mod/ClaudeKingdoms/scenarios/LongRunningCampaign/Map.json deleted file mode 100644 index 26c9d1c..0000000 --- a/mod/ClaudeKingdoms/scenarios/LongRunningCampaign/Map.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "mapParameters": { - "name": "Albion", - "type": "Custom", - "size": {"width": 12, "height": 8}, - "shape": "Rectangular", - "noNaturalWonders": false, - "noBarbarians": false - }, - "tiles": [ - [{"terrain": "Coast"}, {"terrain": "Coast"}, {"terrain": "Plains"}, {"terrain": "Hill"}, {"terrain": "Plains"}, {"terrain": "Forest"}, {"terrain": "Plains"}, {"terrain": "Hill"}, {"terrain": "Plains"}, {"terrain": "Mountain"},{"terrain": "Hill"}, {"terrain": "Coast"}], - [{"terrain": "Coast"}, {"terrain": "Plains"}, {"terrain": "Grassland"},{"terrain": "Plains"}, {"terrain": "Plains"}, {"terrain": "Plains"}, {"terrain": "Forest"}, {"terrain": "Plains"}, {"terrain": "Hill"}, {"terrain": "Hill"}, {"terrain": "Plains"}, {"terrain": "Coast"}], - [{"terrain": "Plains"}, {"terrain": "Grassland"},{"terrain": "Grassland", "city": "Camelot", "owner": "The Operators", "startingPosition": true}, {"terrain": "Grassland"},{"terrain": "Plains"}, {"terrain": "Forest"}, {"terrain": "Plains"}, {"terrain": "Plains"}, {"terrain": "Plains"}, {"terrain": "Hill"}, {"terrain": "Plains"}, {"terrain": "Plains"}], - [{"terrain": "Plains"}, {"terrain": "Plains"}, {"terrain": "Plains"}, {"terrain": "Grassland"},{"terrain": "Plains"}, {"terrain": "Plains"}, {"terrain": "Plains"}, {"terrain": "Plains"}, {"terrain": "Plains"}, {"terrain": "Plains"}, {"terrain": "Forest"}, {"terrain": "Plains"}], - [{"terrain": "Plains"}, {"terrain": "Forest"}, {"terrain": "Plains"}, {"terrain": "Plains"}, {"terrain": "Plains"}, {"terrain": "Hill"}, {"terrain": "Plains"}, {"terrain": "Plains"}, {"terrain": "Plains"}, {"terrain": "Plains"}, {"terrain": "Plains"}, {"terrain": "Plains"}], - [{"terrain": "Plains"}, {"terrain": "Plains"}, {"terrain": "Plains"}, {"terrain": "Hill"}, {"terrain": "Plains"}, {"terrain": "Plains"}, {"terrain": "Plains"}, {"terrain": "Plains"}, {"terrain": "Forest"}, {"terrain": "Plains"}, {"terrain": "Plains"}, {"terrain": "Plains"}], - [{"terrain": "Coast"}, {"terrain": "Plains"}, {"terrain": "Plains"}, {"terrain": "Plains"}, {"terrain": "Plains"}, {"terrain": "Plains"}, {"terrain": "Plains"}, {"terrain": "Plains"}, {"terrain": "Plains"}, {"terrain": "Plains"}, {"terrain": "Plains"}, {"terrain": "Coast"}], - [{"terrain": "Coast"}, {"terrain": "Coast"}, {"terrain": "Coast"}, {"terrain": "Plains"}, {"terrain": "Plains"}, {"terrain": "Plains"}, {"terrain": "Plains"}, {"terrain": "Plains"}, {"terrain": "Plains"}, {"terrain": "Coast"}, {"terrain": "Coast"}, {"terrain": "Coast"}] - ] -} diff --git a/mod/ClaudeKingdoms/scenarios/LongRunningCampaign/scenario.json b/mod/ClaudeKingdoms/scenarios/LongRunningCampaign/scenario.json deleted file mode 100644 index e534de8..0000000 --- a/mod/ClaudeKingdoms/scenarios/LongRunningCampaign/scenario.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "name": "LongRunningCampaign", - "description": "Persistent medieval world that grows across days and weeks. No decay on close. No forced end condition. The primary mode.", - "mapSize": "Standard", - "civilizations": [ - {"civ": "The Operators", "playerType": "Human", "startCity": "Camelot"} - ], - "startingTechs": ["Pottery", "Animal Husbandry"], - "startingTurn": 1, - "victoryConditions": [], - "campaignFlags": { - "noForcedEnd": true, - "noDecayOnClose": true, - "supportMultipleSessions": true - }, - "claudeKingdomsMeta": { - "scenarioRole": "campaign", - "prdAnchor": "PRD §Game Modes (Long-Running Campaign — Primary Mode)" - } -} diff --git a/mod/ClaudeKingdoms/scenarios/Tutorial/Map.json b/mod/ClaudeKingdoms/scenarios/Tutorial/Map.json deleted file mode 100644 index b215439..0000000 --- a/mod/ClaudeKingdoms/scenarios/Tutorial/Map.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "mapParameters": { - "name": "Tutorial Hill", - "type": "Custom", - "size": {"width": 6, "height": 6}, - "shape": "Rectangular", - "noNaturalWonders": true, - "noBarbarians": true - }, - "tiles": [ - [{"terrain": "Plains"}, {"terrain": "Plains"}, {"terrain": "Hill"}, {"terrain": "Plains"}, {"terrain": "Plains"}, {"terrain": "Forest"}], - [{"terrain": "Plains"}, {"terrain": "Grassland"},{"terrain": "Grassland", "city": "Camelot", "owner": "The Operators", "startingPosition": true}, {"terrain": "Grassland"},{"terrain": "Plains"}, {"terrain": "Plains"}], - [{"terrain": "Plains"}, {"terrain": "Plains"}, {"terrain": "Plains"}, {"terrain": "Plains"}, {"terrain": "Plains"}, {"terrain": "Hill"}], - [{"terrain": "Forest"}, {"terrain": "Plains"}, {"terrain": "Plains"}, {"terrain": "Plains"}, {"terrain": "Plains"}, {"terrain": "Plains"}], - [{"terrain": "Hill"}, {"terrain": "Plains"}, {"terrain": "Plains"}, {"terrain": "Plains"}, {"terrain": "Plains"}, {"terrain": "Forest"}], - [{"terrain": "Plains"}, {"terrain": "Forest"}, {"terrain": "Plains"}, {"terrain": "Plains"}, {"terrain": "Hill"}, {"terrain": "Plains"}] - ] -} diff --git a/mod/ClaudeKingdoms/scenarios/Tutorial/scenario.json b/mod/ClaudeKingdoms/scenarios/Tutorial/scenario.json deleted file mode 100644 index d0b1f9b..0000000 --- a/mod/ClaudeKingdoms/scenarios/Tutorial/scenario.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "name": "Tutorial", - "description": "One city, one session, one turn, one reward. Under ten minutes. Shows the emotional payoff before explaining mechanics.", - "mapSize": "Tiny", - "civilizations": [ - {"civ": "The Operators", "playerType": "Human", "startCity": "Camelot"} - ], - "startingTechs": ["Pottery"], - "startingTurn": 1, - "victoryConditions": ["Tutorial Complete"], - "tutorialFlags": { - "graduateAfterTurn": 1, - "showOnboardingFirst": true, - "expectedDurationMinutes": 10 - }, - "claudeKingdomsMeta": { - "scenarioRole": "tutorial", - "handoffTarget": "LongRunningCampaign", - "prdAnchor": "acceptance #9 (Tutorial completes <10 min, transitions to long-running campaign)" - } -}