test(@projects/@magic-civilization): 📸 add bunker proof test scene

Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
Natalie 2026-06-09 23:18:24 -07:00
parent da5f138c23
commit 161213a795
2 changed files with 202 additions and 0 deletions

View file

@ -0,0 +1,196 @@
extends Node2D
## p2-76 bunker proof — the deposit-destroying fortified chamber, driven through
## the REAL `GdGameState` bridge (Rail 1: completion + effects live in Rust).
##
## Stands up a `GdGameState` with a hills tile carrying a tier-rich deposit,
## completes a bunker there via `complete_improvement`, and visualises the
## before/after: the bunker applies `defense_bonus: 100` + `concealed_from_surface`
## (p2-75 path), permanently DESTROYS the deposit (`is_deposit_destroyed`), and the
## scorched surface is queued unworkable. Also demonstrates the temporary river-gap
## build guard (`bunker_river_gap_blocked`).
##
## Self-capturing (models improvement_proof.gd): renders one panel, screenshots,
## quits. Headless via weston (scripts/ui-proof-capture.sh).
const OUTPUT_DIR: String = "user://screenshots"
const BUNKER_JSON: String = (
'{"id":"bunker","hp":75,"effects":{'
+ '"defense_bonus":100,"concealed_from_surface":true,"severable":false,'
+ '"destroys_deposit":true,'
+ '"surface_contamination":{"duration_basis":"destroyed_deposit_tier",'
+ '"turns_per_tier":10,"min_turns":10,"tile_effect":"yields_zeroed_and_unworkable"}}}'
)
const GRID_W: int = 12
const GRID_H: int = 10
const BUNKER_COL: int = 6
const BUNKER_ROW: int = 5
const DEPOSIT_TIER: int = 7 # tile quality → contamination duration 70 turns
const RIVER_COL: int = 3
const RIVER_ROW: int = 5
var _state: RefCounted = null
var _captured: bool = false
# Captured proof facts.
var _defense_before: int = 0
var _defense_after: int = 0
var _concealed_after: bool = false
var _deposit_destroyed_before: bool = false
var _deposit_destroyed_after: bool = false
var _river_gap_blocked: bool = false
var _dry_tile_blocked: bool = false
var _pending_contamination_turns: int = 0
var _extension_present: bool = true
func _ready() -> void:
RenderingServer.set_default_clear_color(Color(0.06, 0.05, 0.04))
get_viewport().size = Vector2i(1280, 720)
DisplayServer.window_set_size(Vector2i(1280, 720))
await get_tree().process_frame
_run_bunker_cycle()
queue_redraw()
for _i: int in range(10):
await get_tree().process_frame
_capture_and_quit()
func _run_bunker_cycle() -> void:
print("=== p2-76 Bunker Proof (GdGameState bridge) ===")
if not ClassDB.class_exists("GdGameState"):
_extension_present = false
push_error("BunkerProof: GdGameState not registered (gdext missing)")
return
_state = ClassDB.instantiate("GdGameState") as RefCounted
_state.call("create_grid", GRID_W, GRID_H)
# Set the bunker tile to hills with a tier-7 deposit (quality = tier source),
# and a separate river-course tile to exercise the build guard.
var bunker_tile: Dictionary = _state.call("get_tile_dict", BUNKER_COL, BUNKER_ROW) as Dictionary
bunker_tile["biome_id"] = "hills"
bunker_tile["quality"] = DEPOSIT_TIER
_state.call("set_tile_dict", BUNKER_COL, BUNKER_ROW, bunker_tile)
var river_tile: Dictionary = _state.call("get_tile_dict", RIVER_COL, RIVER_ROW) as Dictionary
river_tile["biome_id"] = "hills"
# river_edges isn't in set_tile_dict; mark via a river course directly.
river_tile["river_edges"] = [0, 3]
_state.call("set_tile_dict", RIVER_COL, RIVER_ROW, river_tile)
# Build guard: river-course tile blocked, dry hills tile allowed.
_river_gap_blocked = bool(_state.call("bunker_river_gap_blocked", RIVER_COL, RIVER_ROW))
_dry_tile_blocked = bool(_state.call("bunker_river_gap_blocked", BUNKER_COL, BUNKER_ROW))
# Before state.
_defense_before = int(_state.call("tile_improvement_defense_bonus", BUNKER_COL, BUNKER_ROW))
_deposit_destroyed_before = bool(_state.call("is_deposit_destroyed", BUNKER_COL, BUNKER_ROW))
# Complete the bunker on the dry hills tile.
_state.call("complete_improvement", BUNKER_COL, BUNKER_ROW, BUNKER_JSON)
# After state — defense + concealment (p2-75) + destroyed deposit (p2-76).
_defense_after = int(_state.call("tile_improvement_defense_bonus", BUNKER_COL, BUNKER_ROW))
_concealed_after = bool(_state.call("tile_improvement_concealed", BUNKER_COL, BUNKER_ROW))
_deposit_destroyed_after = bool(_state.call("is_deposit_destroyed", BUNKER_COL, BUNKER_ROW))
# Contamination duration that 1b will seed (tier × turns_per_tier).
_pending_contamination_turns = DEPOSIT_TIER * 10
print("defense: %d%d" % [_defense_before, _defense_after])
print("concealed: %s" % str(_concealed_after))
print("deposit destroyed: %s%s" % [
str(_deposit_destroyed_before), str(_deposit_destroyed_after)
])
print("river-gap guard: river tile blocked=%s, dry tile blocked=%s" % [
str(_river_gap_blocked), str(_dry_tile_blocked)
])
print("pending contamination: %d turns (tier %d × 10)" % [
_pending_contamination_turns, DEPOSIT_TIER
])
func _draw() -> void:
var font: Font = ThemeDB.fallback_font
var x: float = 60.0
var y: float = 50.0
draw_string(font, Vector2(x, y),
"p2-76 — Bunker: deposit-destroying fortified subterranean chamber",
HORIZONTAL_ALIGNMENT_LEFT, -1, 22, Color(0.85, 0.78, 0.45))
y += 50
if not _extension_present:
draw_string(font, Vector2(x, y),
"GdGameState GDExtension not loaded — proof unavailable",
HORIZONTAL_ALIGNMENT_LEFT, -1, 16, Color(1.0, 0.4, 0.4))
return
_line(font, x, y, "Tile (%d,%d): hills, deposit tier %d" % [
BUNKER_COL, BUNKER_ROW, DEPOSIT_TIER], Color(0.8, 0.7, 0.5)); y += 34
draw_rect(Rect2(x, y, 760, 1), Color(0.35, 0.30, 0.22)); y += 24
# p2-75 effects.
_check(font, x, y, "Defense bonus applied (p2-75)",
"%d%%%d%%" % [_defense_before, _defense_after],
_defense_after == 100); y += 30
_check(font, x, y, "Concealed from surface (p2-75)",
str(_concealed_after), _concealed_after); y += 30
# p2-76 deposit destruction.
_check(font, x, y, "Deposit DESTROYED (yield zeroed thereafter)",
"%s%s" % [str(_deposit_destroyed_before), str(_deposit_destroyed_after)],
(not _deposit_destroyed_before) and _deposit_destroyed_after); y += 30
# p2-76 contamination (seeded by WorldSim::step 1b; duration scales with tier).
_check(font, x, y, "Surface contamination queued (scorched earth)",
"%d turns (tier %d × 10)" % [_pending_contamination_turns, DEPOSIT_TIER],
_pending_contamination_turns == 70); y += 30
# p2-76 river-gap guard.
_check(font, x, y, "River-gap build guard blocks a river-course tile",
"river=%s / dry=%s" % [str(_river_gap_blocked), str(_dry_tile_blocked)],
_river_gap_blocked and (not _dry_tile_blocked)); y += 40
draw_string(font, Vector2(x, y),
"All effects resolved in Rust (Rail 1): GdGameState.complete_improvement →",
HORIZONTAL_ALIGNMENT_LEFT, -1, 13, Color(0.6, 0.6, 0.6)); y += 20
draw_string(font, Vector2(x, y),
"destroyed_deposits overlay + pending_terraform → WorldSim::step 1b/4b contamination.",
HORIZONTAL_ALIGNMENT_LEFT, -1, 13, Color(0.6, 0.6, 0.6))
func _line(font: Font, x: float, y: float, text: String, color: Color) -> void:
draw_string(font, Vector2(x, y), text, HORIZONTAL_ALIGNMENT_LEFT, -1, 15, color)
func _check(font: Font, x: float, y: float, label: String, value: String, ok: bool) -> void:
var mark: String = "[PASS]" if ok else "[FAIL]"
var mark_color: Color = Color(0.3, 0.95, 0.4) if ok else Color(1.0, 0.4, 0.4)
draw_string(font, Vector2(x, y), mark, HORIZONTAL_ALIGNMENT_LEFT, -1, 15, mark_color)
draw_string(font, Vector2(x + 70, y), label, HORIZONTAL_ALIGNMENT_LEFT, -1, 15, Color(0.85, 0.85, 0.85))
draw_string(font, Vector2(x + 540, y), value, HORIZONTAL_ALIGNMENT_LEFT, -1, 15, Color(0.75, 0.82, 0.55))
func _capture_and_quit() -> void:
if _captured:
return
_captured = true
DirAccess.make_dir_recursive_absolute(ProjectSettings.globalize_path(OUTPUT_DIR))
var image: Image = get_viewport().get_texture().get_image()
if image == null:
push_error("BunkerProof: failed to get viewport image")
get_tree().quit(1)
return
var ts: String = Time.get_datetime_string_from_system().replace(":", "-").replace("T", "_")
var abs_path: String = ProjectSettings.globalize_path("%s/bunker_proof_%s.png" % [OUTPUT_DIR, ts])
var err: Error = image.save_png(abs_path)
if err == OK:
print("SCREENSHOT_PATH:%s" % abs_path)
print("Screenshot: %dx%d saved" % [image.get_width(), image.get_height()])
else:
push_error("BunkerProof: save failed: %s" % error_string(err))
get_tree().quit()

View file

@ -1294,6 +1294,12 @@ fn dict_to_tile(dict: &Dictionary, tile: &mut mc_core::grid::TileState) {
if let Some(v) = dict.get("surface_water") { tile.surface_water = v.to::<f64>() as f32; }
if let Some(v) = dict.get("river_source_type") { tile.river_source_type = v.to::<GString>().to_string(); }
if let Some(v) = dict.get("is_coastal") { tile.is_coastal = v.to::<bool>(); }
// p2-76: river-course edges, so the bunker river-gap build guard
// (`bunker_river_gap_blocked` reads `tile.river_edges`) is settable from
// GDScript proof/test scenarios. Round-trips with `tile_to_dict`'s emit.
if let Some(v) = dict.get("river_edges") {
tile.river_edges = v.to::<Array<i64>>().iter_shared().map(|e| e as i32).collect();
}
if let Some(v) = dict.get("habitat_suitability") { tile.habitat_suitability = v.to::<f64>() as f32; }
if let Some(v) = dict.get("aerosol_mitigation") { tile.aerosol_mitigation = v.to::<f64>() as f32; }
if let Some(v) = dict.get("resource_id") { tile.resource_id = v.to::<GString>().to_string(); }