fix(@projects/@magic-civilization): 🐛 mark p3-07b as completed

Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
Natalie 2026-05-07 11:37:59 -07:00
parent 800071cdb3
commit faf497c8c9
9 changed files with 137 additions and 17 deletions

View file

@ -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) | 🟢 |

View file

@ -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 |

View file

@ -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** |
</td><td valign='top' style='padding-left:2em'>
@ -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 |

View file

@ -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",

View file

@ -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

View file

@ -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({

View file

@ -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 150)
/// - `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`;

View file

@ -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,

View file

@ -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