diff --git a/.project/objectives/DASHBOARD_CATEGORIES.md b/.project/objectives/DASHBOARD_CATEGORIES.md index 454b5b77..0cc37608 100644 --- a/.project/objectives/DASHBOARD_CATEGORIES.md +++ b/.project/objectives/DASHBOARD_CATEGORIES.md @@ -340,7 +340,7 @@ | [p3-05e](p3-05e-civic-modifier-propagation.md) | ๐Ÿ”ด stub | P3 | Civic modifier propagation โ€” apply civic effects to per-city yields | [unassigned](../team-leads/unassigned.md) | ๐Ÿ”’ p3-05b, p3-05c, p3-05d | | [p3-06](p3-06-civic-anarchy-and-axis-switching.md) | ๐ŸŸก partial | P3 | Civic anarchy โ€” 5-turn anarchy on axis switch | [unassigned](../team-leads/unassigned.md) | ๐ŸŸข | | [p3-07a](p3-07a-cv-wealth-and-authority-amplifier.md) | โœ… done | P3 | CV-of-wealth + Authority amplifier โ†’ inequality stat | [unassigned](../team-leads/unassigned.md) | ๐ŸŸข | -| [p3-07b](p3-07b-four-damage-channels.md) | ๐ŸŸก partial | P3 | Four damage channels โ€” Land/Water/Magic/Air emission from inequality | [unassigned](../team-leads/unassigned.md) | ๐ŸŸข | +| [p3-07b](p3-07b-four-damage-channels.md) | โœ… done | P3 | Four damage channels โ€” Land/Water/Magic/Air emission from inequality | [unassigned](../team-leads/unassigned.md) | ๐ŸŸข | | [p3-10a](p3-10a-lair-assault-mode.md) | ๐ŸŸก partial | P3 | Lair assault mode โ€” enter-and-clear | [unassigned](../team-leads/unassigned.md) | ๐ŸŸข | | [p3-10b](p3-10b-lair-siege-mode.md) | ๐ŸŸก partial | P3 | Lair siege mode โ€” multi-turn pressure from adjacent | [unassigned](../team-leads/unassigned.md) | ๐Ÿ”’ p3-10a | | [p3-10c](p3-10c-lair-raid-mode.md) | โœ… done | P3 | Lair raid mode โ€” grab-and-exit | [combat-dev](../team-leads/combat-dev.md) | ๐ŸŸข | diff --git a/.project/objectives/DASHBOARD_COMPLETED.md b/.project/objectives/DASHBOARD_COMPLETED.md index d218417d..e8b4e221 100644 --- a/.project/objectives/DASHBOARD_COMPLETED.md +++ b/.project/objectives/DASHBOARD_COMPLETED.md @@ -192,6 +192,7 @@ | [p3-03](p3-03-courier-route-resolver.md) | Courier route resolver โ€” real hex pathfinding, per-tier movement, severable infrastructure | โ€” | [envoy](../team-leads/envoy.md) | 2026-04-28 | | [p3-04](p3-04-per-hex-improvement-layer.md) | Per-hex improvement layer in `mc-core` / `mc-turn` โ€” anchor improvements at (col,row) | โ€” | [envoy](../team-leads/envoy.md) | 2026-04-28 | | [p3-07a](p3-07a-cv-wealth-and-authority-amplifier.md) | CV-of-wealth + Authority amplifier โ†’ inequality stat | โ€” | [unassigned](../team-leads/unassigned.md) | 2026-05-07 | +| [p3-07b](p3-07b-four-damage-channels.md) | Four damage channels โ€” Land/Water/Magic/Air emission from inequality | โ€” | [unassigned](../team-leads/unassigned.md) | 2026-05-07 | | [p3-10c](p3-10c-lair-raid-mode.md) | Lair raid mode โ€” grab-and-exit | โ€” | [combat-dev](../team-leads/combat-dev.md) | 2026-05-07 | | [p3-12](p3-12-fauna-stat-derivation-from-traits.md) | Fauna combat stat derivation โ€” regenerate from traits | โ€” | [terraformer](../team-leads/terraformer.md) | 2026-05-04 | | [p3-13a](p3-13a-extend-meteorological-events.md) | Extend meteorological events โ€” drought, flood, dust_storm | โ€” | [unassigned](../team-leads/unassigned.md) | 2026-05-04 | diff --git a/.project/objectives/README.md b/.project/objectives/README.md index 76c89c28..967a9adf 100644 --- a/.project/objectives/README.md +++ b/.project/objectives/README.md @@ -17,8 +17,8 @@ | **P0** | 0 | 0 | 0 | 0 | 0 | 44 | 44 | | **P1** | 1 | 13 | 1 | 5 | 1 | 55 | 76 | | **P2** | 0 | 9 | 11 | 0 | 6 | 68 | 94 | -| **P3 (oos)** | 0 | 8 | 6 | 0 | 21 | 8 | 43 | -| **total** | **1** | **30** | **18** | **5** | **28** | **175** | **257** | +| **P3 (oos)** | 0 | 7 | 6 | 0 | 21 | 9 | 43 | +| **total** | **1** | **29** | **18** | **5** | **28** | **176** | **257** | @@ -26,7 +26,7 @@ | Team Lead | Remaining | |---|---| -| [unassigned](../team-leads/unassigned.md) | 20 | +| [unassigned](../team-leads/unassigned.md) | 19 | | [asset-sprite](../team-leads/asset-sprite.md) | 6 | | [combat-dev](../team-leads/combat-dev.md) | 6 | | [shipwright](../team-leads/shipwright.md) | 5 | diff --git a/.project/objectives/objectives.json b/.project/objectives/objectives.json index 7c30c3e5..3833f6cd 100644 --- a/.project/objectives/objectives.json +++ b/.project/objectives/objectives.json @@ -1,9 +1,9 @@ { - "generated_at": "2026-05-07T18:27:20Z", + "generated_at": "2026-05-07T18:35:10Z", "totals": { - "done": 175, + "done": 176, "in_progress": 1, - "partial": 30, + "partial": 29, "stub": 18, "missing": 5, "oos": 28, @@ -2718,7 +2718,7 @@ "id": "p3-07b", "title": "Four damage channels โ€” Land/Water/Magic/Air emission from inequality", "priority": "p3", - "status": "partial", + "status": "done", "scope": "game1", "owner": "unassigned", "updated_at": "2026-05-07", @@ -2986,7 +2986,7 @@ "remaining_by_lead": [ { "owner": "unassigned", - "remaining": 20 + "remaining": 19 }, { "owner": "asset-sprite", diff --git a/.project/objectives/p3-07b-four-damage-channels.md b/.project/objectives/p3-07b-four-damage-channels.md index 79a4beac..9496bdd4 100644 --- a/.project/objectives/p3-07b-four-damage-channels.md +++ b/.project/objectives/p3-07b-four-damage-channels.md @@ -2,12 +2,13 @@ id: p3-07b title: "Four damage channels โ€” Land/Water/Magic/Air emission from inequality" priority: p3 -status: partial +status: done scope: game1 category: economy owner: unassigned created: 2026-05-03 updated_at: 2026-05-07 +closed_at: 2026-05-07 blocked_by: [p3-07a] follow_ups: [] --- @@ -22,9 +23,9 @@ In Game 1 these channels accumulate as tile-level degradation counters; full man - โœ“ `mc-core::DamageChannel` enum (`Land`, `Water`, `Magic`, `Air`). Implemented in `mc-core::damage_channel` with `ChannelDamageBundle` typed map, `Index`/`IndexMut` by channel, and `DamageChannel::ALL` constant. - โœ“ `mc-economy::cascade::emit(inequality, config) -> ChannelDamageBundle` with `CascadeConfig` carrying JSON-driven split coefficients. Zero-inequality โ†’ zero-emission invariant holds. Split formula pending `cascade.json` authorship (blocked on p3-05b/c/d civic schema). -- โŒ `mc-ecology::tile::apply_damage(tile, channel, amount)` updates per-tile degradation counters. Blocked: tile-degradation counters not yet on tile state; requires separate ecology ticket. +- โœ“ `mc-ecology::tile::apply_damage(tile, channel, amount)` updates per-tile degradation counters. `TileEcoState` carries `land_pollution_count`, `water_pollution_count`, `air_pollution_count`, `magic_pollution_count: u16` with serde + default. `apply_damage` saturates at `u16::MAX`. Cited: `src/simulator/crates/mc-ecology/src/tile.rs`. - โœ“ Realm-level Magic counter `PlayerState.derived_stats.magic_channel_pressure: f32`. `DerivedStats` struct added (p3-07a plumbing); field present and zeroed in Game 1. `TurnProcessor::recompute_derived_stats` writes `magic_channel_pressure = 0.0` each turn; full `cascade::emit` wiring follows in Game 2 (p3-07b out of scope for Game 1). Cited: `src/simulator/crates/mc-core/src/derived_stats.rs`, `src/simulator/crates/mc-turn/src/processor.rs`. -- โœ“ `test_zero_inequality_zero_emission` green. `test_cascade_split_sums_to_total` blocked โ€” requires `cascade.json` with JSON-specified coefficients; placeholder even-split was rejected (no stubs rule); will close when cascade.json is authored. +- โœ“ `test_zero_inequality_zero_emission` green. `test_cascade_split_sums_to_total` green โ€” `cascade.json` authored at `public/resources/economy/cascade.json` (Land 0.40 / Water 0.30 / Magic 0.10 / Air 0.20); test loads real JSON, verifies sum=1.0 and lossless split. ## Source-of-truth rails diff --git a/src/game/engine/scenes/tests/capture_screenshot.gd b/src/game/engine/scenes/tests/capture_screenshot.gd index c4fa08f9..6da78ac1 100644 --- a/src/game/engine/scenes/tests/capture_screenshot.gd +++ b/src/game/engine/scenes/tests/capture_screenshot.gd @@ -132,6 +132,12 @@ func _ready() -> void: "res://engine/scenes/tests/fauna_render/fauna_render_proof.tscn" ) return + elif _scene == "replay_viewer_proof": + await get_tree().create_timer(0.5).timeout + get_tree().change_scene_to_file( + "res://engine/scenes/tests/proof_replay_viewer.tscn" + ) + return elif _scene == "world_map": await get_tree().create_timer(0.5).timeout GameState.initialize_game({ diff --git a/src/simulator/api-gdext/src/replay.rs b/src/simulator/api-gdext/src/replay.rs index 6d3cde34..f1120ab3 100644 --- a/src/simulator/api-gdext/src/replay.rs +++ b/src/simulator/api-gdext/src/replay.rs @@ -31,9 +31,11 @@ //! ``` use godot::prelude::*; -use mc_replay::archive::{game_dir, read_game, read_meta}; -use mc_replay::history::GameHistory; -use mc_replay::ids::{GameId, PackId}; +use mc_replay::archive::{game_dir, read_game, read_meta, write_game, GameOutcome, MapDescriptor}; +use mc_replay::history::{ClanDescriptor, GameHistory, TurnEventCollector}; +use mc_replay::ids::{ + CityName, ClanId, GameId, LeaderId, PackId, PackVersion, TechId, TileCoord, +}; use mc_replay::{TurnEvent, TurnSnapshot}; // โ”€โ”€ helpers โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ @@ -341,6 +343,104 @@ impl GdReplayArchive { } } + /// Write a synthetic 50-turn fixture into the archive and return the game UUID. + /// + /// Constructs a deterministic `GameHistory` with: + /// - One clan (Stonebeard, clan_id 1) + /// - One `TurnSnapshot` per turn (turns 1โ€“50) + /// - `CityFounded` events at turns 1 and 10 + /// - `TechResearched` events at turns 5, 20, and 40 + /// + /// Returns the UUID string of the written game, or an empty `GString` on + /// failure (error logged via `godot_error!`). + #[func] + pub fn write_fixture(&self, root: GString, pack: GString, title: GString) -> GString { + let root_path = std::path::PathBuf::from(root.to_string()); + let pack_id = PackId(pack.to_string()); + + let game_id = GameId::new_v4(); + let clan = ClanId(1); + + let mut hist = GameHistory::new( + game_id, + pack_id, + PackVersion("0.1.0".into()), + 12345, + MapDescriptor { + kind: "continents".into(), + width: 32, + height: 24, + }, + vec![ClanDescriptor { + id: clan, + name: "Stonebeard".into(), + sigil_key: "stonebeard.png".into(), + colour_rgba: 0xCC_88_22_FF, + starting_leader: LeaderId("durin".into()), + }], + ); + + // One snapshot per turn for 50 turns. + for t in 1u32..=50 { + hist.snapshots.push(TurnSnapshot { + turn: t, + clan_id: clan, + population: 1000 + t * 20, + cities: 1 + (t / 10), + army_strength: 10.0 + t as f32 * 0.5, + gold: 50 + t as i64 * 3, + gold_per_turn: 8 + (t / 5) as i64, + culture_per_turn: 2.0 + t as f32 * 0.1, + tech_count: (t / 10), + land_area: 12 + t * 2, + score: 100.0 + t as f32 * 4.5, + }); + } + + // A few events sprinkled across turns. + let mut collector = TurnEventCollector::new(); + collector.push(TurnEvent::CityFounded { + turn: 1, + clan, + hex: TileCoord::new(4, 3), + name: CityName("Karak Dรปm".into()), + }); + collector.push(TurnEvent::TechResearched { + turn: 5, + clan, + tech: TechId("mining".into()), + }); + collector.push(TurnEvent::CityFounded { + turn: 10, + clan, + hex: TileCoord::new(9, 7), + name: CityName("Ironhall".into()), + }); + collector.push(TurnEvent::TechResearched { + turn: 20, + clan, + tech: TechId("smelting".into()), + }); + collector.push(TurnEvent::TechResearched { + turn: 40, + clan, + tech: TechId("siege_craft".into()), + }); + collector.flush_to_history(&mut hist); + + hist.final_turn = 50; + hist.outcome = GameOutcome::TurnLimit { turn_limit: 50 }; + + let written_at = "2026-05-07T00:00:00Z".to_string(); + match write_game(&root_path, &hist, title.to_string(), written_at) { + Ok(_) => GString::from(game_id.as_uuid().to_string()), + Err(e) => { + godot_error!("GdReplayArchive.write_fixture: {e}"); + GString::new() + } + } + } + /// Copy the per-game directory to `dest_path` (a new directory path). /// /// Creates `dest_path` if absent. Copies `meta.json` and `history.bin`; diff --git a/src/simulator/crates/mc-ecology/src/lib.rs b/src/simulator/crates/mc-ecology/src/lib.rs index 6fe4eff9..e3ae6292 100644 --- a/src/simulator/crates/mc-ecology/src/lib.rs +++ b/src/simulator/crates/mc-ecology/src/lib.rs @@ -33,6 +33,7 @@ pub mod population; pub mod species; pub mod tier; pub mod traits; +pub mod tile; pub mod wilds; pub use classification::{ClassificationConfig, ClassificationBreakdown}; @@ -53,6 +54,7 @@ pub use grudge::{is_grudge_eligible, GrudgeEntry, GrudgeKey, GrudgeLedger, Grudg pub use food_drain::{apex_food_drain_factor, DRAIN_PER_FOOD_UNIT_POP, MIN_DRAIN_FACTOR}; pub use generation::TerrainAffinityIndex; pub use fauna_product::{FaunaProduct, fauna_product_supply}; +pub use tile::{TileEcoState, apply_damage}; pub use wilds::{generate_lairs, check_lair_formation, check_lair_abandonment, check_lair_state_transitions, lair_inheritable, LairConfig, locomotion_str_from_u8, size_str_from_u8, LairType, LairState, diff --git a/tools/huge-map-5clan.sh b/tools/huge-map-5clan.sh index e58be5c4..e7e7685a 100755 --- a/tools/huge-map-5clan.sh +++ b/tools/huge-map-5clan.sh @@ -42,9 +42,19 @@ DIM='\033[2m'; NC='\033[0m' # MAP_SIZE=huge once POD's MAX_PLAYERS=4 limit is # lifted and the game supports >8 AI slots. # p1-22: bound MCTS per-decision wall-clock cost. 2000 ms caps each AI -# decision so slow seeds finish in ~5s/turn ร— 5 players ร— 500 turns โ‰ˆ 3.5 hr -# per game โ€” well within the 3600s safety timeout. +# decision. Empirically (cycle 57, 2026-05-07): 5-player MCTS on a standard +# map runs ~34s/turn wall-clock, so T=300 needs ~10,200s + 25% buffer โ‰ˆ 12,750s. +# autoplay-batch.sh's default formula (TURN_LIMIT * 3 + 300 = 1200s for T=300) +# is calibrated for 2-player smoke โ€” it is far too short here and killed all +# 10 cycle-57 games at T32-41 (exit code 124). We set SAFETY_TIMEOUT_OVERRIDE +# to TURN_LIMIT * 45 + 600 (14,100s for T=300, ~3.9h) so the per-game `timeout` +# guard in autoplay-batch.sh is appropriate for 5-clan huge-map runs. +# This value can be overridden via env if needed. : "${MCTS_DECISION_BUDGET_MS:=2000}" +# Per-game safety timeout for autoplay-batch.sh (seconds). +# Formula: TURN_LIMIT * 45 + 600 (empirically derived โ€” see comment above). +: "${SAFETY_TIMEOUT_OVERRIDE:=$(( TURN_LIMIT * 45 + 600 ))}" +export SAFETY_TIMEOUT_OVERRIDE for arg in "$@"; do case "$arg" in