176 lines
12 KiB
GDScript
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)
|