magicciv/CLAUDE.md
2026-04-16 15:10:06 -07:00

20 KiB

Magic Civilization

Fantasy 4X turn-based strategy game (Civ5 + Master of Magic + Magic: The Gathering color pie) in Godot 4 / GDScript. 16 races, 5 magic schools, hex grid.

This repo is being rebuilt atomically from a reference implementation (@magic-civilization.messy/). Port systems as each milestone requires them — never reference .messy/ paths from runtime code.

Scope

Early access demo ("Age of Dwarves") — 4 races (High Elf, Human, Dwarf, Orc), all 5 magic schools, full 4X + magic loop. See .project/ROADMAP.md for scope and build sequence.

Tech Stack

  • Engine: Godot 4.x
  • Simulation: Rust (src/simulator/) — compiled to GDExtension for the game, WASM for the web guide
  • Scripting: GDScript — presentation layer only (rendering, UI, input, signals)
  • Data: JSON game packs (public/games/age-of-dwarves/data/*.json)
  • Architecture: Genre-agnostic engine with game pack content system

Key Architecture

The engine is genre-agnostic. All game content and display text comes from game packs. The fantasy game "Age of Dwarves" is the default.

Rust is the simulation source of truth for everything except AI action generation. Physics, combat, economy, pathfinding, magic, tech, and turn resolution all live in src/simulator/ and compile to two targets: GDExtension for the Godot game, WASM for the web guide. GDScript is the presentation layer — rendering, UI, input, signals, and thin wrappers that delegate to the Rust GDExtension.

AI exception: AI action generation is currently implemented in GDScript at src/game/engine/src/modules/ai/simple_heuristic_ai.gd. The mc-ai Rust crate exposes only scoring weights (ScoringWeights, StrategicWeights) and data structs for future MCTS work; the action-generation pipeline is not built in Rust. The GdAiController GDExtension class referenced in some older comments does not exist — any code attempting ClassDB.instantiate("GdAiController") would return null. AiTurnBridge.run(player) now calls SimpleHeuristicAi.process_player(player) directly.

JSON game packs remain the canonical content store. Stats, costs, effects, thresholds — all in public/games/age-of-dwarves/data/*.json. Neither Rust nor GDScript hardcodes game content.

  • UI labels resolve through ThemeVocabulary.lookup(engine_key) — never hardcode theme strings
  • Sprites resolve through ThemeAssets.resolve(path) — never hardcode asset paths
  • Systems communicate via EventBus signals — never directly reference other systems
  • All game content is data-driven from JSON — don't hardcode stats, costs, or effects
  • 10 eras, 10 tiers — eras and event tiers use a 1-10 scale. Era count and names are game-pack-driven (defined in eras.json, not the engine). Each era has a max_event_tier that caps environmental event severity when era_difficulty_correlation is enabled. Units, spells, buildings use content tiers defined by the game pack. Spells use scope: "global" (High Archon, world map) or scope: "local" (specialist units, combat). School tech tiers map: T1-T2 spells gated by Mysticism/Arcane Lore, T3-T5 by school techs.

Documentation

See README.md for the full doc index. Docs live in two places:

  • src/game/engine/docs/ — genre-agnostic engine architecture (written as engine systems are built)
  • public/games/age-of-dwarves/docs/ — fantasy game design (races, combat, spells, economy, etc.)

Build process docs (roadmap, feature gap, task lists) stay in .project/.

Single Source of Truth: Rust → GDExtension + WASM

The Rust workspace src/simulator/ is the source of truth for all simulation logic. It compiles to two targets. Never duplicate simulation logic in GDScript or TypeScript.

public/games/age-of-dwarves/data/climate_spec.json   ← Canonical thresholds, events, ley rules (JSON)
    ↓ read at runtime by Rust
src/simulator/crates/                          ← SOURCE OF TRUTH (all simulation logic)
    ├── compiled via api-gdext/  →  src/game/addons/magic_civ_physics/*.so/.dll
    │       ↓ loaded by Godot
    │   src/game/engine/src/modules/climate/climate.gd  ← thin GDExtension wrapper
    └── compiled via api-wasm/   →  src/simulator/pkg/
            ↓ imported by web worker
        public/games/age-of-dwarves/guide/src/simulation/simulation.worker.ts

Build commands

# WASM (web guide)
cd src/simulator && bash build-wasm.sh

# GDExtension (Godot game, Linux dev)
cd src/simulator && bash build-gdext.sh

Rules

  • All simulation changes go in Rust (src/simulator/crates/) — never in GDScript or TypeScript, except AI action generation, which currently lives in src/game/engine/src/modules/ai/simple_heuristic_ai.gd
  • Never hardcode thresholds — read from climate_spec.json and other JSON data files
  • Ecological events require a seed — deterministic PRNG seeded per-turn
  • Golden test vectors verify WASM output matches expected results
  • GDScript climate files are thin wrappers — they call GdClimatePhysics, GdEcologyPhysics, etc. via GDExtension. No physics logic lives in GDScript.

Project-Specific Agents

10 specialized agents for game development in .claude/agents/:

Agent Use For
godot-engine Project setup, autoloads, scene management, GDScript core
game-algorithms Hex math, A* pathfinding, procedural map generation
game-systems Economy, happiness, culture, production, growth, improvements
combat-dev Combat resolver, keywords, damage formulas, promotions, siege
magic-dev Spells, mana economy, Archons, enchantments, wonders, Ascension
game-ai AI opponents: strategy, tactical movement, combat decisions
game-data JSON data authoring from design docs
godot-ui All UI scenes: city screen, tech tree, spellbook, HUD, menus
godot-renderer TileMap, sprites, camera, fog of war, hex visuals, animation
guide-web Player guide web app: React pages, components, climate sim, Vitest

Agent Selection by Task

Task pattern Agent
project.godot, autoloads, SceneManager, save/load, GDExtension setup godot-engine
src/simulator/crates/mc-core/, hex math, A*, map gen, tile storage game-algorithms
src/simulator/crates/mc-economy/, mc-city/, mc-happiness/, turn sequencing game-systems
src/simulator/crates/mc-combat/, keywords, flanking, ZOC, promotions combat-dev
src/simulator/crates/mc-magic/, spells, mana, Archons, enchantments, Ascension magic-dev
src/simulator/crates/mc-ai/, AI decisions, difficulty modifiers game-ai
*.json data files, vocabulary.json, game.json game-data
*.tscn UI scenes, HUD panels, overlays, menus godot-ui
TileMap, sprites, camera, fog, selection highlight, animation godot-renderer
public/games/age-of-dwarves/guide/, src/packages/guide/, React, Vite, WASM integration guide-web

Conventions

GDScript Style

  • snake_case for variables, functions, files
  • PascalCase for classes and nodes
  • Signals use past tense: unit_moved, city_founded, tech_researched
  • Constants in UPPER_SNAKE_CASE
  • Type hints on all function signatures

Class Resolution — Preload Pattern (critical)

Godot 4 class_name registration is unreliable in autoload context. Always reference non-autoload classes via preload() const, never by bare class_name:

const CityScript = preload("res://engine/src/entities/city.gd")
const HexUtilsScript = preload("res://engine/src/map/hex_utils.gd")
const UnitScript = preload("res://engine/src/entities/unit.gd")
const TileScript = preload("res://engine/src/map/tile.gd")
const PlayerScript = preload("res://engine/src/entities/player.gd")
const GameMapScript = preload("res://engine/src/map/game_map.gd")

var unit: UnitScript = UnitScript.new()

Keep the class_name declaration in the file itself (IDE autocomplete uses it). All runtime references use the preload const.

Entity Class Pattern

All game entities (Unit, City, Player, Building, Improvement) are:

  • class_name Foo / extends RefCounted — pure data, no scene/node
  • Rendering handled by separate renderer scripts
  • Logic calls DataLoader for type definitions, EventBus for state-change signals

Signal Parameters

EventBus signals pass entity objects as Variant, not typed class_name parameters:

# Correct:
signal unit_moved(unit: Variant, from: Vector2i, to: Vector2i)
# Wrong — causes type errors in autoload context:
signal unit_moved(unit: Unit, from: Vector2i, to: Vector2i)

Hex Math

All hex coordinate math goes through HexUtils static methods — never inline the formulas. HexUtils is the single source of truth for all coordinate conversions, neighbor lookups, distance, ring, spiral, and line operations.

Direction-to-edge mapping — AXIAL direction indices and hex polygon edge indices are NOT the same:

Direction:  0(E)  1(NE) 2(NW) 3(W)  4(SW) 5(SE)
Poly edge:  2     1     0     5     4     3

Data IDs

  • snake_case: high_elf_heritage, chaos_magic, fire_elemental
  • Match the id field in JSON data files
  • Reference by ID in code, resolve display name through ThemeVocabulary

File Organization

src/
  game/                       — Godot project root (project.godot lives here)
    addons/
      magic_civ_physics/      — compiled GDExtension .so/.dll/.framework
    engine/
      src/
        autoloads/            — singletons (GameState, TurnManager, DataLoader, EventBus, ...)
        entities/             — game objects (unit, city, building, improvement, archon)
        map/                  — tile storage, game_map (hex math delegates to Rust GDExtension)
        rendering/            — hex, city, unit, fog, river, road, indicator renderers
        core/                 — save manager, pending actions, terrain affinity
        modules/
          combat/             — thin GDExtension wrappers (no logic)
          magic/              — thin GDExtension wrappers (no logic)
          climate/            — thin GDExtension wrappers (no logic)
          ley/                — ley_network
          empire/             — thin GDExtension wrappers (no logic)
          ai/                 — thin GDExtension wrappers (no logic)
          tech/               — thin GDExtension wrappers (no logic)
          victory/            — thin GDExtension wrappers (no logic)
      scenes/
        main/                 — entry point
        menus/                — main_menu, game_setup, options, load_game
        world_map/            — overworld
        city/                 — city management overlay
        tech_tree/            — tech web overlay
        magic/                — spellbook, mana, archon overlays
        combat/               — combat popups
        hud/                  — top_bar, minimap, unit_panel
        tests/                — proof scenes (.tscn + .gd co-located)
      tests/
        unit/                 — GUT tests by module
        integration/          — end-to-end integration tests

  simulator/                  — Rust workspace (source of truth for all simulation)
    crates/
      mc-core/                — GridState, TileState, BiomeRegistry, hex algorithms
      mc-climate/             — ClimatePhysics, EcologyPhysics, atmosphere, spec evaluator
      mc-mapgen/              — MapGenerator
      mc-combat/              — CombatResolver (stub → full)
      mc-magic/               — SpellSystem, ManaPool, Archons (stub → full)
      mc-economy/             — EmpireEconomy (stub → full)
      mc-city/                — CityGrowth (stub → full)
      mc-happiness/           — happiness pool (stub → full)
      mc-culture/             — CultureAccumulation (stub → full)
      mc-tech/                — TechWeb graph (stub → full)
      mc-ai/                  — AI systems (stub → full)
      mc-turn/                — TurnProcessor (stub → full)
    api-wasm/                 — wasm-bindgen surface; builds pkg/
    api-gdext/                — godot-rust surface; builds addons/magic_civ_physics/
    pkg/                      — wasm-pack output (gitignored except package.json)
    build-wasm.sh
    build-gdext.sh

  packages/
    engine-ts/                — @magic-civ/engine-ts (types, runner, HexGrid)
    guide/                    — @magic-civ/guide-engine (shared UI components, types, utils)

  resources/                  — shared content library
    biomes/
    ecology/
    improvements/
    villages/
    wilds/
    worlds/
      earth/                  — primary simulation archetype
      khazad_prime/           — Dwarf homeworld

games/
  age-of-dwarves/
    data/                     — all JSON content (units/, spells/, techs/, terrain/, ...)
    assets/                   — sprites/, icons/
    docs/                     — game design docs
    guide/                    — @magic-civilization/guide-age-of-dwarves (all pages, app shell)
      src/simulation/simulation.worker.ts  — imports from @magic-civ/physics-rs (WASM)

Note: src/game/public is a symlink → ../../public, making the entire public tree accessible as res://public/ from within the Godot project (res://public/resources/, res://public/games/). src/simulator/public is the same symlink for Rust test access.

DX Tooling

Testing & Linting

  • Rust testscargo test --workspace in src/simulator/
  • WASM buildcd src/simulator && bash build-wasm.sh
  • GDExtension buildcd src/simulator && bash build-gdext.sh
  • GUT (Godot Unit Test) — unit + integration tests for GDScript wrappers
  • gdtoolkit (pip install gdtoolkit) — gdlint + gdformat
  • Run lint: gdlint src/game/engine/src/

Task Runner (./run)

Central entry point for dev, export, deploy commands.

./run play                         # Launch the game locally
./run editor                       # Open Godot editor
./run lint                         # gdlint src/game/engine/src/
./run verify                       # lint + typecheck + cargo check + tests
./run test                         # GUT tests + Rust tests + vitest
./run screenshot [name] [scene]    # Capture + SCP to plum
./run export [version]             # All platforms in parallel

Screenshot & Visual Verification

./tools/screenshot.sh [name] [scene] [delay]

Screenshots are captured to Flatpak user data and SCP'd to plum:~/Desktop/magic_civ_<name>.png.

Proof Scenes (src/game/engine/scenes/tests/)

Self-capturing test scenes. Each phase should have a proof scene that sets up minimal game state, renders claimed features, auto-captures, and quits.

Environment Config (.env.*)

EnvConfig autoload reads .env (base) then .env.development (overrides) at startup:

  • .env.productionFORCE_DISABLE_FOGOFWAR=false
  • .env.developmentFORCE_DISABLE_FOGOFWAR=true, FORCE_UNLIMITED_RESEARCH=true

DataLoader — File vs Directory Pattern

DataLoader supports two layouts per data category:

  • Single file: public/games/age-of-dwarves/data/races.json
  • Split directory: public/games/age-of-dwarves/data/units/ — reads all .json files, merges by id

Never create a monolithic file that exceeds 500 lines.

Sprite Generation (tools/sprite-generation/)

Pipeline for generating game sprites using Magic: The Gathering card art as style reference. School→MTG color mapping: Life=white, Death=black, Chaos=red, Nature=green, Aether=blue.

Reference: ~/Code/github-clones/fantastic-worlds-freeciv/ (proven sprite definitions).

Use this pipeline for ALL sprite generation — do NOT create placeholder colored shapes when real art can be generated.

Phase Gate Protocol (MANDATORY)

Never declare a phase complete without a proof screenshot reviewed in this conversation.

A phase is NOT done until:

  1. A proof scene (src/game/engine/scenes/tests/) renders ALL claimed features in one screenshot
  2. The screenshot is captured via tools/screenshot.sh
  3. The screenshot is SCP'd to plum:~/Desktop/magic_civ_<phase>_proof.png
  4. The screenshot is read and reviewed IN THIS CONVERSATION
  5. Every claimed feature is visibly confirmed
  6. The user approves it

Code exists ≠ code works. Tests pass ≠ features render. Lint clean ≠ visually correct.

Atomic Porting Protocol

This project is rebuilt milestone-by-milestone from a reference implementation. Only port what the current milestone requires.

  • Data files: only copy JSON files that the current milestone's code actually loads — not the full 99-file set "for completeness"
  • Engine code: only port systems listed in the current milestone's task list — don't pull in combat when building climate
  • Scenes: only create scenes needed for the current phase's proof screenshot
  • Tests: only port tests for systems that exist in this repo — a test for code that hasn't been ported yet is dead weight
  • If a file from the reference doesn't have a consumer in this repo yet, it doesn't belong here yet
  • The milestone task lists in .project/tasks/ define exactly what comes in and when

Language Standards (load before writing code)

Language When Load
GDScript (.gd) Any GDScript authoring ~/.claude/instructions/godot-code-standards.md
Rust (.rs) Any src/simulator/ work ~/.claude/instructions/rust-code-standards.md
TypeScript (.ts, .tsx) Any guide/package TS work ~/.claude/instructions/typescript-code-standards.md
Python (.py) Tools, scripts, data pipelines ~/.claude/instructions/python-code-standards.md

Hooks enforce these standards automatically on Write/Edit — they are not optional.

Safety Rules

  • Never hardcode theme-specific strings in engine code
  • Never hardcode asset paths — always use ThemeAssets.resolve()
  • GameState must support multiple map layers (future Ethereal Plane)
  • All system state changes emit signals via EventBus
  • Building/unit effects are data-driven from JSON — don't hardcode behavior
  • Always call DataLoader.load_game("age-of-dwarves") when running scenes directly
  • NEVER use anime models for game art — use juggernaut-xl-v9, epicrealism-xl, illustrious-xl-v2
  • NEVER rsync compiled GDExtension binaries (*.so, *.dll, *.dylib) from macOS to apricot. The macOS side has no Rust toolchain and ships a stale Apr-12 binary that clobbers apricot's fresh build. Use rsync --exclude='addons/magic_civ_physics/*.so' OR rely on the .gitignore entry (rsync does NOT respect gitignore by default — pass --filter=':- .gitignore'). Always rebuild via ssh lilith@apricot.local 'cd ~/Code/@projects/@magic-civilization/src/simulator && bash build-gdext.sh' after rsyncing Rust source changes. Symptom of this bug: src/simulator/crates/*.rs has new code but batch runs show old behavior (e.g. FOOD_PER_POP=2.0 in a binary whose source says 1.5).
  • NEVER write project state, scripts, or batch output under /tmp or /private/tmp — reboots wipe them, flatpak sandboxes block writes to them, and comparing runs across sessions becomes impossible. Canonical locations:
    • Shell scripts/runners → scripts/ (in-repo, tracked) or $HOME/bin/ (persistent per-host)
    • Batch/iteration outputs → .local/batches/ (in-repo, gitignored) or $HOME/tmp/ (persistent per-host)
    • Per-iteration diagnostic dirs → .local/iter/ (in-repo, gitignored)
    • Remote apricot paths → $HOME/Code/@projects/@magic-civilization/.local/... (mirror of repo layout)
    • ONLY /tmp is acceptable for: genuine inter-process pipes (mkfifo), socket activation (.sock files), or Godot internal scratch the engine itself puts there. If you find yourself writing /tmp/mc_* or /tmp/run_*, STOP — use one of the paths above.