feat(@projects/@magic-civilization): ✨ add weather/climate event tracking
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
parent
d245ea469b
commit
2607491a1b
4 changed files with 135 additions and 2 deletions
|
|
@ -300,6 +300,27 @@ func _on_lair_cleared_aggregate(_tile: Vector2i, _reward: Dictionary) -> void:
|
|||
_lair_cleared_count += 1
|
||||
|
||||
|
||||
func _on_weather_event_applied(kind: String, tile: Vector2i, severity: float) -> void:
|
||||
_weather_events_this_turn += 1
|
||||
_total_weather_events += 1
|
||||
_append_event({
|
||||
"type": "weather_event",
|
||||
"kind": kind,
|
||||
"tile_x": tile.x,
|
||||
"tile_y": tile.y,
|
||||
"severity": severity,
|
||||
})
|
||||
|
||||
|
||||
func _on_climate_effect_applied(unit_id: int, cause: String, hp_loss: int) -> void:
|
||||
_append_event({
|
||||
"type": "climate_effect",
|
||||
"unit_id": unit_id,
|
||||
"cause": cause,
|
||||
"hp_loss": hp_loss,
|
||||
})
|
||||
|
||||
|
||||
func _maybe_queue_siege_replacement(unit: Variant, idx: int) -> void:
|
||||
# Siege sustain: if a military unit belonging to the currently-attacking
|
||||
# player dies mid-siege and the stack is at or below 3, prepend a warrior
|
||||
|
|
@ -2245,7 +2266,10 @@ func _append_turn_stats(outcome: String) -> void:
|
|||
"turn_first_city_captured": _turn_first_city_captured,
|
||||
"strategic_gate_rejected": _strategic_gate_rejected_count,
|
||||
"lair_cleared": _lair_cleared_count,
|
||||
"weather_events_count": _weather_events_this_turn,
|
||||
"total_weather_events": _total_weather_events,
|
||||
}
|
||||
var ecology_block: Dictionary = _snapshot_ecology()
|
||||
var winner_personality: String = ""
|
||||
if _victory_winner >= 0:
|
||||
for p: Variant in GameState.players:
|
||||
|
|
@ -2260,6 +2284,7 @@ func _append_turn_stats(outcome: String) -> void:
|
|||
"victory_type": _victory_type,
|
||||
"wall_clock_sec": wall_clock,
|
||||
"aggregate": aggregate,
|
||||
"ecology": ecology_block,
|
||||
"player_stats": _build_player_stats(),
|
||||
"invariant_violations": _violations,
|
||||
}
|
||||
|
|
@ -2285,11 +2310,27 @@ func _save_turn_snapshot() -> void:
|
|||
push_error("AutoPlay: save failed (%s): %s" % [error_string(err), save_path])
|
||||
|
||||
|
||||
func _snapshot_ecology() -> Dictionary:
|
||||
## Ecology block for the current turn_stats line (p0-35). Sources canopy
|
||||
## mean + delta from Climate.get_canopy_summary, which wraps the Rust
|
||||
## GdEcologyPhysics bridge. Returns zeros before TurnManager has produced
|
||||
## a climate tick so the first seeded line still schema-validates.
|
||||
var climate: RefCounted = TurnManager.climate as RefCounted
|
||||
if climate == null or not climate.has_method("get_canopy_summary"):
|
||||
return {"flora_canopy_mean": 0.0, "flora_canopy_delta": 0.0}
|
||||
var summary: Dictionary = climate.get_canopy_summary() as Dictionary
|
||||
return {
|
||||
"flora_canopy_mean": float(summary.get("mean", 0.0)),
|
||||
"flora_canopy_delta": float(summary.get("delta_since_last_turn", 0.0)),
|
||||
}
|
||||
|
||||
|
||||
func _flush_turn_artifacts() -> void:
|
||||
## End-of-turn persistence: events, turn_stats line, save snapshot.
|
||||
## Cheap to skip for unseeded runs (each callee short-circuits on _seed_set).
|
||||
_flush_events()
|
||||
_append_turn_stats(_outcome)
|
||||
_weather_events_this_turn = 0
|
||||
_save_turn_snapshot()
|
||||
if _save_at_turn > 0 and _turn_count == _save_at_turn:
|
||||
var save_path: String = _output_dir.path_join("mid_run.save")
|
||||
|
|
|
|||
|
|
@ -44,6 +44,7 @@ META_SCHEMA_NAME = "meta"
|
|||
EVENT_TYPES = [
|
||||
"city_founded", "city_captured", "city_grew", "city_starved",
|
||||
"tech_researched", "unit_created", "unit_destroyed", "combat_resolved", "victory",
|
||||
"weather_event", "climate_effect",
|
||||
]
|
||||
|
||||
|
||||
|
|
@ -248,6 +249,13 @@ def extract_row(
|
|||
# Out-of-band fields for the per-clan aggregator. Not emitted to CSV.
|
||||
row["_player_clans"] = dict(player_clans) if player_clans else {}
|
||||
row["_snapshots"] = snapshots or {}
|
||||
# Canopy telemetry (p0-35). Absent on pre-p0-35 batches — surfaced as
|
||||
# None so the summary pipeline can skip those rows cleanly.
|
||||
ecology = data.get("ecology") or {}
|
||||
row["_canopy_mean_final"] = ecology.get("flora_canopy_mean")
|
||||
row["_canopy_delta_final"] = ecology.get("flora_canopy_delta")
|
||||
agg = data.get("aggregate", {})
|
||||
row["_total_weather_events"] = agg.get("total_weather_events")
|
||||
return row
|
||||
|
||||
|
||||
|
|
@ -622,6 +630,55 @@ def print_quality_metrics(
|
|||
return q
|
||||
|
||||
|
||||
def print_canopy_summary(
|
||||
rows: list[dict[str, Any]], out: Any = sys.stderr
|
||||
) -> None:
|
||||
"""Report canopy trend across the batch (p0-35).
|
||||
|
||||
Sources each game's final `ecology.flora_canopy_mean` / `flora_canopy_delta`
|
||||
from the last turn_stats line. Historical batches pre-p0-35 have None for
|
||||
these fields — we skip those rows rather than mixing 0.0 into the median.
|
||||
"""
|
||||
means = [r["_canopy_mean_final"] for r in rows
|
||||
if isinstance(r.get("_canopy_mean_final"), (int, float))]
|
||||
deltas = [r["_canopy_delta_final"] for r in rows
|
||||
if isinstance(r.get("_canopy_delta_final"), (int, float))]
|
||||
print("flora canopy (p0-35):", file=out)
|
||||
if not means:
|
||||
print(" (no data — batch pre-dates p0-35 instrumentation)", file=out)
|
||||
return
|
||||
print(f" median final canopy mean: {_fmt(statistics.median(means))}", file=out)
|
||||
nonzero_deltas = [d for d in deltas if d != 0.0]
|
||||
print(
|
||||
f" games with evolving canopy: {len(nonzero_deltas)} / {len(rows)} "
|
||||
f"(non-zero final delta)", file=out
|
||||
)
|
||||
if deltas:
|
||||
print(f" median final |delta|: {_fmt(statistics.median([abs(d) for d in deltas]))}", file=out)
|
||||
|
||||
|
||||
def print_weather_summary(
|
||||
rows: list[dict[str, Any]], out: Any = sys.stderr
|
||||
) -> None:
|
||||
"""Report weather-event coverage across the batch (p0-36)."""
|
||||
totals = [r["_total_weather_events"] for r in rows
|
||||
if isinstance(r.get("_total_weather_events"), (int, float))]
|
||||
evt_counts = [r.get("evt_weather_event", 0) for r in rows]
|
||||
print("weather events (p0-36):", file=out)
|
||||
if not totals:
|
||||
print(" (no data — batch pre-dates p0-36 instrumentation)", file=out)
|
||||
return
|
||||
games_with_any = sum(1 for t in totals if t > 0)
|
||||
print(
|
||||
f" games with >=1 weather_event: {games_with_any} / {len(rows)}",
|
||||
file=out
|
||||
)
|
||||
if totals:
|
||||
print(f" median total_weather_events: {_fmt(statistics.median(totals))}", file=out)
|
||||
total_evts = sum(evt_counts)
|
||||
print(f" total weather_event records (events.jsonl): {total_evts}", file=out)
|
||||
|
||||
|
||||
def print_summary(rows: list[dict[str, Any]], out: Any = sys.stderr) -> None:
|
||||
print("=== autoplay batch report ===", file=out)
|
||||
print(f"games: {len(rows)}", file=out)
|
||||
|
|
@ -660,6 +717,8 @@ def print_summary(rows: list[dict[str, Any]], out: Any = sys.stderr) -> None:
|
|||
total_v = sum(r["invariant_violations"] for r in rows)
|
||||
print(f"invariant violations (total): {total_v}", file=out)
|
||||
print_quality_metrics(rows, out=out)
|
||||
print_canopy_summary(rows, out=out)
|
||||
print_weather_summary(rows, out=out)
|
||||
print_personality_summary(rows, out=out)
|
||||
render_per_clan_table(rows, out=out)
|
||||
|
||||
|
|
|
|||
|
|
@ -19,7 +19,15 @@
|
|||
"unit_created",
|
||||
"unit_destroyed",
|
||||
"combat_resolved",
|
||||
"victory"
|
||||
"victory",
|
||||
"weather_event",
|
||||
"climate_effect",
|
||||
"improvement_started",
|
||||
"improvement_built",
|
||||
"city_building_completed",
|
||||
"loot_dropped",
|
||||
"wild_spawned",
|
||||
"strategic_gate_rejected"
|
||||
],
|
||||
"description": "Machine-readable event identifier"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -54,7 +54,32 @@
|
|||
},
|
||||
"turn_first_city_captured": { "type": "integer", "minimum": -1 },
|
||||
"lair_cleared": { "type": "integer", "minimum": 0 },
|
||||
"strategic_gate_rejected": { "type": "integer", "minimum": 0 }
|
||||
"strategic_gate_rejected": { "type": "integer", "minimum": 0 },
|
||||
"weather_events_count": {
|
||||
"type": "integer",
|
||||
"minimum": 0,
|
||||
"description": "Weather events emitted during this turn (p0-36)."
|
||||
},
|
||||
"total_weather_events": {
|
||||
"type": "integer",
|
||||
"minimum": 0,
|
||||
"description": "Cumulative weather events since run start (p0-36)."
|
||||
}
|
||||
}
|
||||
},
|
||||
"ecology": {
|
||||
"type": "object",
|
||||
"description": "Per-turn flora ecology summary (p0-35). Sourced from GdEcologyPhysics.canopy_summary. Absent on pre-p0-35 batches.",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"flora_canopy_mean": {
|
||||
"type": "number",
|
||||
"description": "Mean canopy_cover across all non-water tiles this turn."
|
||||
},
|
||||
"flora_canopy_delta": {
|
||||
"type": "number",
|
||||
"description": "Change in mean canopy vs. the previous turn (0.0 on turn 1)."
|
||||
}
|
||||
}
|
||||
},
|
||||
"player_stats": {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue