diff --git a/src/game/engine/scenes/tests/courier_era7_severance_proof.gd b/src/game/engine/scenes/tests/courier_era7_severance_proof.gd new file mode 100644 index 00000000..57c8e8f1 --- /dev/null +++ b/src/game/engine/scenes/tests/courier_era7_severance_proof.gd @@ -0,0 +1,165 @@ +extends Node +## p3-01 bullet 11: era_7 courier severance proof. +## +## Tests two sub-cases: +## 1. Era_7 courier (8 hex/turn) over a 4-hex gap with intact route delivers +## in a single step. +## 2. A courier already marked intercepted=true produces no further events +## (terminal state per spec: intercepted is non-refundable, no re-processing). +## +## NOTE: The deterministic tile-severance path (pillaging steam_track mid-route) +## requires `stamp_tile_improvement` on GdGameState, not yet exposed to GDScript. +## That path is covered by the Rust unit test +## `steam_track_pillage_intercepts_on_next_step` in mc-turn/src/courier_resolver.rs. + +var _pass_count: int = 0 +var _fail_count: int = 0 + + +func _ready() -> void: + if not ClassDB.class_exists("GdTradeLedger"): + print("[SKIP] GdTradeLedger not registered — GDExtension not loaded") + get_tree().quit(0) + return + _run_proof() + _print_results() + get_tree().quit(0 if _fail_count == 0 else 1) + + +func _run_proof() -> void: + _test_era7_delivers_intact_route() + _test_intercepted_route_is_terminal() + + +func _test_era7_delivers_intact_route() -> void: + var state: RefCounted = ClassDB.instantiate("GdGameState") as RefCounted + if not _assert("era7_intact: GdGameState instantiates", state != null): + return + state.call("create_grid", 10, 10) + state.call("add_player_militarist", 0, 0) + state.call("add_player_militarist", 4, 0) + + var map_view_inst: RefCounted = ClassDB.instantiate("GdCourierMapView") as RefCounted + if not _assert("era7_intact: GdCourierMapView instantiates", map_view_inst != null): + return + var gd_map_view: RefCounted = map_view_inst.call("from_game_state", state) as RefCounted + if not _assert("era7_intact: from_game_state returns non-null", gd_map_view != null): + return + + var gd_trade: RefCounted = ClassDB.instantiate("GdTrade") as RefCounted + if not _assert("era7_intact: GdTrade instantiates", gd_trade != null): + return + + # Era_7 moves 8 hexes/turn via straight-line; 4-hex gap → delivers in 1 step. + var ledger_inst: RefCounted = ClassDB.instantiate("GdTradeLedger") as RefCounted + var agreement_json: String = JSON.stringify({ + "agreements": [ + { + "SharedMap": { + "agreement_id": 0, + "partners": [0, 1], + "turn_started": 1, + "duration": 5, + "share_turns_remaining": 0, + "payment_gold": 100, + "payment_luxury": null, + "courier_route": { + "sender": 0, + "receiver": 1, + "courier_era_tier": 7, + "dispatched_turn": 1, + "position": [0, 0], + "eta_turn": null, + "delivered": false, + "intercepted": false, + "planned_path": [], + "path_step": 0 + } + } + } + ], + "next_agreement_id": 1 + }) + var ledger: RefCounted = ledger_inst.call("from_json", agreement_json) as RefCounted + if not _assert("era7_intact: ledger loads", ledger != null): + return + + var events: Array = gd_trade.call("step_shared_map_agreements", ledger, gd_map_view, 2) as Array + var delivered: bool = false + for ev: Dictionary in events: + if ev.get("type") == "shared_map_delivered": + delivered = true + break + _assert("era7_intact: era_7 delivers in one step over 4-hex gap", delivered) + + +func _test_intercepted_route_is_terminal() -> void: + var state: RefCounted = ClassDB.instantiate("GdGameState") as RefCounted + if not _assert("era7_terminal: GdGameState instantiates", state != null): + return + state.call("create_grid", 10, 10) + state.call("add_player_militarist", 0, 0) + state.call("add_player_militarist", 8, 0) + + var map_view_inst: RefCounted = ClassDB.instantiate("GdCourierMapView") as RefCounted + var gd_map_view: RefCounted = map_view_inst.call("from_game_state", state) as RefCounted + if not _assert("era7_terminal: GdCourierMapView created", gd_map_view != null): + return + + var gd_trade: RefCounted = ClassDB.instantiate("GdTrade") as RefCounted + if not _assert("era7_terminal: GdTrade instantiates", gd_trade != null): + return + + # Courier already in terminal intercepted=true state. + var ledger_inst: RefCounted = ClassDB.instantiate("GdTradeLedger") as RefCounted + var agreement_json: String = JSON.stringify({ + "agreements": [ + { + "SharedMap": { + "agreement_id": 0, + "partners": [0, 1], + "turn_started": 1, + "duration": 10, + "share_turns_remaining": 0, + "payment_gold": 100, + "payment_luxury": null, + "courier_route": { + "sender": 0, + "receiver": 1, + "courier_era_tier": 7, + "dispatched_turn": 1, + "position": [3, 0], + "eta_turn": null, + "delivered": false, + "intercepted": true, + "planned_path": [], + "path_step": 0 + } + } + } + ], + "next_agreement_id": 1 + }) + var ledger: RefCounted = ledger_inst.call("from_json", agreement_json) as RefCounted + if not _assert("era7_terminal: ledger loads", ledger != null): + return + + var events: Array = gd_trade.call("step_shared_map_agreements", ledger, gd_map_view, 2) as Array + _assert("era7_terminal: no events emitted for already-intercepted courier", events.is_empty()) + + var sm_agreements: Array = ledger.call("iter_shared_map") as Array + _assert("era7_terminal: intercepted agreement stays in ledger", sm_agreements.size() == 1) + + +func _assert(label: String, condition: bool) -> bool: + if condition: + _pass_count += 1 + print("[PASS] %s" % label) + else: + _fail_count += 1 + push_error("[FAIL] %s" % label) + return condition + + +func _print_results() -> void: + print("courier_era7_severance_proof: %d passed, %d failed" % [_pass_count, _fail_count]) diff --git a/src/game/engine/scenes/tests/courier_era7_severance_proof.tscn b/src/game/engine/scenes/tests/courier_era7_severance_proof.tscn new file mode 100644 index 00000000..55c56968 --- /dev/null +++ b/src/game/engine/scenes/tests/courier_era7_severance_proof.tscn @@ -0,0 +1,6 @@ +[gd_scene load_steps=2 format=3 uid="uid://courier_era7_severance"] + +[ext_resource type="Script" path="res://engine/scenes/tests/courier_era7_severance_proof.gd" id="1"] + +[node name="CourierEra7SeveranceProof" type="Node"] +script = ExtResource("1")