diff --git a/src/game/engine/scenes/tests/courier_era10_instant_sync_proof.gd b/src/game/engine/scenes/tests/courier_era10_instant_sync_proof.gd new file mode 100644 index 00000000..2a7d8c09 --- /dev/null +++ b/src/game/engine/scenes/tests/courier_era10_instant_sync_proof.gd @@ -0,0 +1,181 @@ +extends Node +## p3-01 bullet 11: era_10 / instant sync proof. +## +## Tests two sub-cases: +## 1. Era_2 courier over 9 hexes does NOT deliver on step 1 (no Adamantine Echo). +## Confirms the baseline slow-courier behavior. +## 2. A courier whose position already equals the receiver capital delivers on +## the very next step (simulates the end-state of instant delivery; the +## step function checks position == dest and emits SharedMapDelivered). +## +## NOTE: Full Adamantine Echo path (both players hold the wonder → instant +## delivery from any distance regardless of position) is covered by Rust unit +## test `adamantine_echo_delivers_instantly` in mc-turn/src/courier_resolver.rs, +## since injecting wonders into GdGameState is not yet exposed to GDScript. + +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_era2_baseline_not_instant() + _test_courier_at_destination_delivers_immediately() + + +func _test_era2_baseline_not_instant() -> void: + # Era_2 over 9 hexes (1 hex/turn) must NOT deliver on step 1. + var state: RefCounted = ClassDB.instantiate("GdGameState") as RefCounted + if not _assert("baseline: GdGameState instantiates", state != null): + return + state.call("create_grid", 15, 10) + state.call("add_player_militarist", 0, 0) + state.call("add_player_militarist", 9, 0) + + var map_view_inst: RefCounted = ClassDB.instantiate("GdCourierMapView") as RefCounted + if not _assert("baseline: 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("baseline: from_game_state non-null", gd_map_view != null): + return + + var gd_trade: RefCounted = ClassDB.instantiate("GdTrade") as RefCounted + if not _assert("baseline: GdTrade instantiates", gd_trade != null): + return + + 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": 8, + "share_turns_remaining": 0, + "payment_gold": 200, + "payment_luxury": null, + "courier_route": { + "sender": 0, + "receiver": 1, + "courier_era_tier": 2, + "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("baseline: ledger loads", ledger != null): + return + + var events: Array = gd_trade.call("step_shared_map_agreements", ledger, gd_map_view, 1) as Array + var delivered_step1: bool = false + for ev: Dictionary in events: + if ev.get("type") == "shared_map_delivered": + delivered_step1 = true + break + _assert("baseline: era_2 does NOT deliver on step 1 over 9-hex gap", not delivered_step1) + + +func _test_courier_at_destination_delivers_immediately() -> void: + # Courier already at position == receiver capital → delivers on next step. + var state: RefCounted = ClassDB.instantiate("GdGameState") as RefCounted + if not _assert("instant: GdGameState instantiates", state != null): + return + state.call("create_grid", 15, 10) + state.call("add_player_militarist", 0, 0) + state.call("add_player_militarist", 9, 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("instant: GdCourierMapView created", gd_map_view != null): + return + + var gd_trade: RefCounted = ClassDB.instantiate("GdTrade") as RefCounted + if not _assert("instant: GdTrade instantiates", gd_trade != null): + return + + # Courier position (9,0) == receiver capital; not yet delivered. + 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": 8, + "share_turns_remaining": 0, + "payment_gold": 200, + "payment_luxury": null, + "courier_route": { + "sender": 0, + "receiver": 1, + "courier_era_tier": 2, + "dispatched_turn": 1, + "position": [9, 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("instant: ledger loads", ledger != null): + return + + var events: Array = gd_trade.call("step_shared_map_agreements", ledger, gd_map_view, 1) as Array + var delivered_found: bool = false + var turns_remaining_from_event: int = -1 + for ev: Dictionary in events: + if ev.get("type") == "shared_map_delivered" and ev.get("agreement_id") as int == 0: + delivered_found = true + turns_remaining_from_event = ev.get("turns_remaining") as int + break + + _assert("instant: SharedMapDelivered when courier starts at destination", delivered_found) + _assert("instant: turns_remaining in event equals duration (8)", turns_remaining_from_event == 8) + + var sm_agreements: Array = ledger.call("iter_shared_map") as Array + _assert("instant: ledger retains agreement in share-window phase", sm_agreements.size() == 1) + if sm_agreements.size() > 0: + var sm_out: RefCounted = sm_agreements[0] as RefCounted + _assert("instant: agreement is_delivered after step", sm_out.call("is_delivered") as bool) + var share_remaining: int = sm_out.call("get_share_turns_remaining") as int + _assert("instant: share window = 8 turns", share_remaining == 8) + + +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_era10_instant_sync_proof: %d passed, %d failed" % [_pass_count, _fail_count]) diff --git a/src/game/engine/scenes/tests/courier_era10_instant_sync_proof.tscn b/src/game/engine/scenes/tests/courier_era10_instant_sync_proof.tscn new file mode 100644 index 00000000..91b98465 --- /dev/null +++ b/src/game/engine/scenes/tests/courier_era10_instant_sync_proof.tscn @@ -0,0 +1,6 @@ +[gd_scene load_steps=2 format=3 uid="uid://courier_era10_instant_sync"] + +[ext_resource type="Script" path="res://engine/scenes/tests/courier_era10_instant_sync_proof.gd" id="1"] + +[node name="CourierEra10InstantSyncProof" type="Node"] +script = ExtResource("1")