Files
aetherbound-guild/runtime/tests/p10_2/mid_contract_resolution_test.gd
T

176 lines
12 KiB
GDScript

extends SceneTree
const BattleService = preload("res://p10_2/mid_contract_battle_service.gd")
const BoardService = preload("res://p10_2/market_board_service.gd")
const MarketService = preload("res://p10_2/market_service.gd")
const ResolutionService = preload("res://p10_2/mid_contract_resolution_service.gd")
const SaveV8 = preload("res://p10_2/save_v8_service.gd")
const Fixture = preload("res://tests/p10_2/mid_contract_fixture.gd")
var failures: Array[String] = []
func _initialize() -> void:
call_deferred("run_test")
func run_test() -> void:
verify_three_round_progression()
verify_lock_carries_to_next_market()
verify_defeat_casualty_and_recovery()
verify_atomic_boundaries()
if failures.is_empty():
print("ABG_P10_2_MID_CONTRACT_RESOLUTION_OK rounds=2,3,4 rewards=24,33,42 xp=14,20,26 capacity=4,5,5,6 markets=2,3,4,5 route_history=4 locks=carry casualty=permanent credit=true recovery=bounded ferry=restored save_v8=roundtrip duplicate=true")
quit(0)
else:
for failure in failures:
push_error(failure)
quit(1)
func verify_three_round_progression() -> void:
var service = Fixture.new().market_service()
expect(service != null, "three-Round fixture starts at Market 2")
if service == null:
return
var starting_coin := int(service.state.current_run.coin)
var starting_company: Array = Array(service.state.current_run.company).duplicate()
var starting_survivals := {}
for recruit_id in starting_company:
starting_survivals[recruit_id] = int(service.state.current_run.recruit_records[recruit_id].survived_battles)
var total_growth_rows := 0
var expected := {
2: {"risk": 0, "reward": 24, "xp": 14, "next_market": 3, "capacity": 5},
3: {"risk": 1, "reward": 33, "xp": 20, "next_market": 4, "capacity": 5},
4: {"risk": 2, "reward": 42, "xp": 26, "next_market": 5, "capacity": 6},
}
for market_index in [2, 3, 4]:
var row: Dictionary = expected[market_index]
var route: Dictionary = service.state.current_run.market_board.battle_offers[int(row.risk)]
var prepared: Dictionary = service.commit_route(
"progress-route-%d" % market_index,
service.state.revision,
String(route.offer_id),
String(service.state.current_run.market_board.board_hash)
)
expect(prepared.ok, "Round %d route prepares" % market_index)
var plan_id := String(BattleService.PREFERRED_PLAN[String(route.route_id)])
var battle: Dictionary = BattleService.new(service.state.snapshot()).run_to_terminal(plan_id)
expect(battle.ok and battle.snapshot.outcome == "victory", "Round %d preferred response wins" % market_index)
var resolution = ResolutionService.new(service.state.snapshot())
var revision_before: int = int(resolution.state.revision)
var settled: Dictionary = resolution.settle(
"progress-settle-%d" % market_index,
revision_before,
battle.snapshot
)
expect(settled.ok and resolution.state.revision == revision_before + 1, "Round %d settlement applies once" % market_index)
expect(settled.receipt.reward_coin == row.reward and settled.receipt.xp_award == row.xp, "Round %d reward and XP are exact" % market_index)
expect(resolution.state.current_run.market_index == row.next_market and resolution.state.current_run.line_capacity == row.capacity, "Round %d opens Market %d at capacity %d" % [market_index, row.next_market, row.capacity])
expect(resolution.state.current_run.cleared_rounds == market_index and resolution.state.current_run.route_history.size() == market_index, "Round %d clear and route history advance once" % market_index)
total_growth_rows += settled.receipt.growth.size()
var after: Dictionary = resolution.state.snapshot()
var duplicate: Dictionary = resolution.settle(
"progress-settle-%d" % market_index,
revision_before,
battle.snapshot
)
expect(duplicate.ok and duplicate.duplicate and resolution.state.snapshot() == after, "Round %d settlement retry is idempotent" % market_index)
service = MarketService.new(resolution.state.snapshot())
expect(service.state.current_run.phase == "market_5" and service.state.current_run.reed_ferry_restored, "Round 4 restores Ferry and reaches Market 5")
expect(service.state.current_run.company == starting_company and service.state.current_run.coin == starting_coin + 99, "three victories preserve Company and grant exact total Coin")
expect(total_growth_rows == starting_company.size() * 3, "every living deployed Recruit receives three growth rows")
for recruit_id in starting_company:
expect(service.state.current_run.recruit_records[recruit_id].survived_battles == int(starting_survivals[recruit_id]) + 3, "%s preserves migrated history and records exactly three survived Rounds" % recruit_id)
expect(service.state.current_run.market_board.handoff_kind == "p10.2.market_5_continuation" and service.state.current_run.market_board.carried_lock_ids.is_empty(), "Market 5 is an explicit continuation handoff")
var encoded: Dictionary = SaveV8.new().encode_snapshot(service.state.snapshot(), 20)
var decoded: Dictionary = SaveV8.new().decode_text(String(encoded.text), 20)
expect(encoded.ok and decoded.ok and decoded.snapshot == service.state.snapshot(), "Market 5 roundtrips through Save v8")
func verify_lock_carries_to_next_market() -> void:
var service = Fixture.new().market_service()
var offer_id := String(service.state.current_run.available_equipment_offer_ids[0])
var locked_row := find_offer(service.state.current_run.market_board.equipment_offers, "offer_id", offer_id)
expect(service.lock_offer("carry-lock", service.state.revision, offer_id, 2).ok, "equipment lock applies before Round 2")
var route: Dictionary = service.state.current_run.market_board.battle_offers[0]
expect(service.commit_route("carry-route", service.state.revision, String(route.offer_id), String(service.state.current_run.market_board.board_hash)).ok, "locked-board route prepares")
var battle: Dictionary = BattleService.new(service.state.snapshot()).run_to_terminal("hold_line")
var resolution = ResolutionService.new(service.state.snapshot())
var settled: Dictionary = resolution.settle("carry-settle", resolution.state.revision, battle.snapshot)
expect(settled.ok and resolution.state.current_run.market_index == 3, "locked-board victory opens Market 3")
expect(offer_id in resolution.state.current_run.locked_offer_ids, "lock ID persists into next Market")
expect(find_offer(resolution.state.current_run.market_board.equipment_offers, "offer_id", offer_id) == locked_row, "locked equipment row persists byte-equivalent")
func verify_defeat_casualty_and_recovery() -> void:
var service = Fixture.new().prepared_service(4, 2)
expect(service != null, "defeat fixture reaches Market 4 elite route")
if service == null:
return
var before: Dictionary = service.state.snapshot()
var before_company: Array = Array(before.current_run.company).duplicate()
var before_coin := int(before.current_run.coin)
var before_history := Array(before.current_run.route_history).duplicate(true)
var battle: Dictionary = BattleService.new(before).run_to_terminal("hold_line")
expect(battle.ok and battle.snapshot.outcome == "defeat" and battle.snapshot.fallen_recruit_ids.size() == 1, "wrong Sinking Nursery response defeats with one casualty")
var fallen_id := String(battle.snapshot.fallen_recruit_ids[0])
var survivor_id := String(before_company[0]) if String(before_company[0]) != fallen_id else String(before_company[1])
var survivor_xp_before := int(before.current_run.recruit_records[survivor_id].xp)
var resolution = ResolutionService.new(before)
var settled: Dictionary = resolution.settle("defeat-settle", resolution.state.revision, battle.snapshot)
expect(settled.ok and resolution.state.current_run.phase == "recovery", "defeat enters explicit recovery")
expect(fallen_id not in resolution.state.current_run.company and fallen_id not in resolution.state.current_run.recruit_records, "Fallen Recruit is permanently removed")
expect(resolution.state.current_run.inventory[settled.receipt.casualties[0].returned_equipment_instance_id].wearer_id == "", "Fallen equipment returns to inventory")
expect(resolution.state.current_run.replacement_credit > 0 and resolution.state.current_run.recruits_fallen_total == 1, "casualty grants bounded Credit and increments loss count")
expect(resolution.state.current_run.coin == before_coin and resolution.state.current_run.route_history == before_history and resolution.state.current_run.cleared_rounds == 3, "defeat grants no Coin or clear")
expect(settled.receipt.xp_award == 13 and int(resolution.state.current_run.recruit_records[survivor_id].xp) != survivor_xp_before, "survivor receives exact half defeat XP")
expect(resolution.state.current_run.market_board.board_hash == before.current_run.market_board.board_hash, "defeat restores exact pre-battle Market board")
var recovery_save: Dictionary = SaveV8.new().encode_snapshot(resolution.state.snapshot(), 30)
expect(recovery_save.ok and SaveV8.new().decode_text(String(recovery_save.text), 30).ok, "recovery state roundtrips through Save v8")
var company_after_loss: Array = Array(resolution.state.current_run.company).duplicate()
var recovered: Dictionary = resolution.recover(
"defeat-recover",
resolution.state.revision,
String(battle.snapshot.snapshot_hash),
12
)
expect(recovered.ok and resolution.state.current_run.phase == "market", "bounded recovery returns to changed Market 4")
expect(resolution.state.current_run.company == company_after_loss and fallen_id not in resolution.state.current_run.company, "recovery cannot resurrect Fallen Recruit")
expect(resolution.state.current_run.coin == before_coin - 12 and resolution.state.battle_checkpoints.is_empty(), "recovery charges exact cost and clears checkpoint")
for recruit_id in resolution.state.current_run.company:
var record: Dictionary = resolution.state.current_run.recruit_records[recruit_id]
expect(float(record.current_hp) / float(record.max_hp) >= 0.4, "recovery restores %s to at least 40 percent" % recruit_id)
func verify_atomic_boundaries() -> void:
var service = Fixture.new().prepared_service(2, 0)
var battle: Dictionary = BattleService.new(service.state.snapshot()).run_to_terminal("hold_line")
var resolution = ResolutionService.new(service.state.snapshot())
var before: Dictionary = resolution.state.snapshot()
var stale: Dictionary = resolution.settle("stale-settle", resolution.state.revision + 1, battle.snapshot)
expect(not stale.ok and stale.code == "stale_revision" and resolution.state.snapshot() == before, "stale settlement rejects without mutation")
var tampered: Dictionary = battle.snapshot.duplicate(true)
tampered.reward_coin = 999
var hash_rejected: Dictionary = resolution.settle("tampered-hash", resolution.state.revision, tampered)
expect(not hash_rejected.ok and hash_rejected.code == "battle_hash_mismatch" and resolution.state.snapshot() == before, "tampered battle hash rejects without mutation")
var forged: Dictionary = battle.snapshot.duplicate(true)
forged.reward_coin = 999
forged.erase("snapshot_hash")
forged["snapshot_hash"] = BoardService.hash_value(forged)
var reward_rejected: Dictionary = resolution.settle("tampered-reward", resolution.state.revision, forged)
expect(not reward_rejected.ok and reward_rejected.code == "battle_reward_mismatch" and resolution.state.snapshot() == before, "self-consistent wrong reward rejects against encounter authority")
func find_offer(offers: Array, field: String, offer_id: String) -> Dictionary:
for offer in offers:
if String(offer.get(field, "")) == offer_id:
return Dictionary(offer).duplicate(true)
return {}
func expect(condition: bool, message: String) -> void:
if not condition:
failures.append("P10.2 mid-Contract resolution: %s" % message)