182 lines
9.4 KiB
GDScript
182 lines
9.4 KiB
GDScript
extends SceneTree
|
|
|
|
const BattleService = preload("res://fg3/contract1_battle_service.gd")
|
|
const ContractService = preload("res://fg3/contract1_service.gd")
|
|
const Fixture = preload("res://tests/fg3/contract1_fixture.gd")
|
|
const SaveV10 = preload("res://fg3/save_v10_service.gd")
|
|
|
|
var failures: Array[String] = []
|
|
|
|
|
|
func _initialize() -> void:
|
|
call_deferred("run_test")
|
|
|
|
|
|
func run_test() -> void:
|
|
verify_rounds_8_through_12()
|
|
verify_defeat_casualty_and_recovery()
|
|
verify_atomic_boundaries()
|
|
if failures.is_empty():
|
|
print("ABG_FG3_CONTRACT1_ROUNDS_OK rounds=8,9,10,11,12 rewards=36,45,54,42,51 xp=26,32,38,32,38 capacities=8,8,9,9,10 markets=9,10,11,12,boss_muster casualty=permanent recovery=same_board duplicate=true replay=true save_v10=roundtrip")
|
|
quit(0)
|
|
else:
|
|
for failure in failures:
|
|
push_error(failure)
|
|
quit(1)
|
|
|
|
|
|
func verify_rounds_8_through_12() -> void:
|
|
var service := ContractService.new(Fixture.new().fg3_market_8_snapshot())
|
|
expect(service.valid, "real migrated Market 8 initializes")
|
|
if not service.valid:
|
|
return
|
|
var starting_coin := int(service.state.current_run.coin)
|
|
var starting_company: Array = Array(service.state.current_run.company).duplicate()
|
|
var risk_by_round := {8: 0, 9: 1, 10: 2, 11: 0, 12: 1}
|
|
var reward_by_round := {8: 36, 9: 45, 10: 54, 11: 42, 12: 51}
|
|
var xp_by_round := {8: 26, 9: 32, 10: 38, 11: 32, 12: 38}
|
|
var next_market := {8: 9, 9: 10, 10: 11, 11: 12, 12: 13}
|
|
var next_capacity := {8: 8, 9: 8, 10: 9, 11: 9, 12: 10}
|
|
for round_index in [8, 9, 10, 11, 12]:
|
|
var run: Dictionary = service.state.current_run
|
|
var route: Dictionary = run.market_board.battle_offers[int(risk_by_round[round_index])]
|
|
var revision_before := int(service.state.revision)
|
|
var prepared := service.commit_route(
|
|
"fg3-round-%d-prepare" % round_index,
|
|
revision_before,
|
|
String(route.offer_id),
|
|
String(run.market_board.board_hash)
|
|
)
|
|
expect(prepared.ok and service.state.revision == revision_before + 1, "Round %d prepares once" % round_index)
|
|
if not prepared.ok:
|
|
return
|
|
var duplicate_prepare := service.commit_route(
|
|
"fg3-round-%d-prepare" % round_index,
|
|
revision_before,
|
|
String(route.offer_id),
|
|
String(run.market_board.board_hash)
|
|
)
|
|
expect(duplicate_prepare.ok and duplicate_prepare.duplicate, "Round %d prepare retry is idempotent" % round_index)
|
|
var plan_id := String(BattleService.PREFERRED_PLAN[String(route.route_id)])
|
|
var battle := BattleService.new(service.state.snapshot()).run_to_terminal(plan_id)
|
|
expect(battle.ok and battle.snapshot.outcome == "victory", "Round %d disclosed response wins" % round_index)
|
|
if not battle.ok:
|
|
return
|
|
var settle_revision := int(service.state.revision)
|
|
var settled := service.settle_route(
|
|
"fg3-round-%d-settle" % round_index,
|
|
settle_revision,
|
|
battle.snapshot
|
|
)
|
|
expect(settled.ok and service.state.revision == settle_revision + 1, "Round %d settlement applies once" % round_index)
|
|
if not settled.ok:
|
|
return
|
|
expect(settled.receipt.reward_coin == reward_by_round[round_index] and settled.receipt.xp_award == xp_by_round[round_index], "Round %d reward and XP are exact" % round_index)
|
|
expect(service.state.current_run.market_index == next_market[round_index] and service.state.current_run.line_capacity == next_capacity[round_index], "Round %d opens exact next capacity" % round_index)
|
|
var after := service.state.snapshot()
|
|
var duplicate_settle := service.settle_route(
|
|
"fg3-round-%d-settle" % round_index,
|
|
settle_revision,
|
|
battle.snapshot
|
|
)
|
|
expect(duplicate_settle.ok and duplicate_settle.duplicate and service.state.snapshot() == after, "Round %d settlement retry is inert" % round_index)
|
|
|
|
var final_run: Dictionary = service.state.current_run
|
|
expect(final_run.phase == "boss_muster" and final_run.cleared_rounds == 12, "five victories open Boss Muster after twelve total clears")
|
|
expect(final_run.company == starting_company and final_run.recruits_fallen_total == 0, "victory path preserves the Company")
|
|
expect(final_run.coin == starting_coin + 228, "five victories grant exact 228 Coin")
|
|
expect(final_run.route_history.size() == 12, "route history contains twelve exact victories")
|
|
expect(final_run.boss_muster.boss_id == "BO-R2-01" and final_run.boss_muster.plan_ids == ContractService.BOSS_PLAN_IDS, "Muster exposes exactly two legal Boss plans")
|
|
expect(String(final_run.boss_muster.muster_hash).length() == 64 and final_run.available_battle_offer_ids.is_empty(), "fixed Muster replaces ordinary offers")
|
|
|
|
|
|
func verify_defeat_casualty_and_recovery() -> void:
|
|
var service := ContractService.new(Fixture.new().fg3_market_8_snapshot())
|
|
var before := service.state.snapshot()
|
|
var board_before: Dictionary = before.current_run.market_board.duplicate(true)
|
|
var company_before: Array = Array(before.current_run.company).duplicate()
|
|
var coin_before := int(before.current_run.coin)
|
|
var route: Dictionary = service.state.current_run.market_board.battle_offers[2]
|
|
var prepared := service.commit_route(
|
|
"fg3-defeat-prepare",
|
|
service.state.revision,
|
|
String(route.offer_id),
|
|
String(service.state.current_run.market_board.board_hash)
|
|
)
|
|
expect(prepared.ok, "elite Market-8 route prepares")
|
|
var battle := BattleService.new(service.state.snapshot()).run_to_terminal("hold_line")
|
|
expect(battle.ok and battle.snapshot.outcome == "defeat" and battle.snapshot.fallen_recruit_ids.size() == 1, "wrong response defeats with one permanent casualty")
|
|
if not battle.ok:
|
|
return
|
|
var fallen_id := String(battle.snapshot.fallen_recruit_ids[0])
|
|
var returned_item := String(before.current_run.equipped_by_recruit.get(fallen_id, ""))
|
|
var settled := service.settle_route(
|
|
"fg3-defeat-settle",
|
|
service.state.revision,
|
|
battle.snapshot
|
|
)
|
|
expect(settled.ok and service.state.current_run.phase == "recovery", "defeat enters explicit recovery")
|
|
expect(fallen_id not in service.state.current_run.company and fallen_id not in service.state.current_run.recruit_records, "Fallen Recruit is permanently removed")
|
|
expect(service.state.current_run.company.size() == company_before.size() - 1 and service.state.current_run.recruits_fallen_total == int(before.current_run.recruits_fallen_total) + 1, "casualty count and Company shrink exactly")
|
|
expect(service.state.current_run.replacement_credit > int(before.current_run.replacement_credit), "casualty grants bounded Credit")
|
|
expect(service.state.current_run.coin == coin_before and service.state.current_run.cleared_rounds == 7, "defeat grants no Coin or clear")
|
|
expect(service.state.current_run.market_board == board_before, "defeat preserves the exact Market board")
|
|
if not returned_item.is_empty():
|
|
expect(String(service.state.current_run.inventory[returned_item].wearer_id).is_empty(), "Fallen equipment returns to inventory")
|
|
var recovery_save := SaveV10.new().encode_snapshot(service.state.snapshot(), 80)
|
|
var recovery_loaded := SaveV10.new().decode_text(String(recovery_save.text), 80)
|
|
expect(recovery_save.ok and recovery_loaded.ok and recovery_loaded.snapshot == service.state.snapshot(), "recovery state roundtrips")
|
|
var recovery_revision := int(service.state.revision)
|
|
var recovered := service.recover_route(
|
|
"fg3-defeat-recover",
|
|
recovery_revision,
|
|
String(battle.snapshot.snapshot_hash),
|
|
18
|
|
)
|
|
expect(recovered.ok and service.state.current_run.phase == "market", "bounded recovery returns to the same Market")
|
|
expect(service.state.current_run.market_board == board_before and service.state.current_run.company.size() == company_before.size() - 1, "recovery neither rerolls nor resurrects")
|
|
expect(service.state.current_run.coin == coin_before - 18 and service.state.battle_checkpoints.is_empty(), "recovery charges exact cost and clears checkpoint")
|
|
var after := service.state.snapshot()
|
|
var duplicate := service.recover_route(
|
|
"fg3-defeat-recover",
|
|
recovery_revision,
|
|
String(battle.snapshot.snapshot_hash),
|
|
18
|
|
)
|
|
expect(duplicate.ok and duplicate.duplicate and service.state.snapshot() == after, "recovery retry is idempotent")
|
|
|
|
|
|
func verify_atomic_boundaries() -> void:
|
|
var service := ContractService.new(Fixture.new().fg3_market_8_snapshot())
|
|
var run: Dictionary = service.state.current_run
|
|
var unavailable := service.commit_route(
|
|
"fg3-unavailable",
|
|
service.state.revision,
|
|
"missing-offer",
|
|
String(run.market_board.board_hash)
|
|
)
|
|
expect(not unavailable.ok and unavailable.code == "battle_offer_unavailable", "unknown route rejects")
|
|
var route: Dictionary = run.market_board.battle_offers[0]
|
|
var prepared := service.commit_route(
|
|
"fg3-atomic-prepare",
|
|
service.state.revision,
|
|
String(route.offer_id),
|
|
String(run.market_board.board_hash)
|
|
)
|
|
expect(prepared.ok, "atomic fixture prepares")
|
|
var battle := BattleService.new(service.state.snapshot()).run_to_terminal(
|
|
String(BattleService.PREFERRED_PLAN[String(route.route_id)])
|
|
)
|
|
var before := service.state.snapshot()
|
|
var stale := service.settle_route("fg3-stale-settle", service.state.revision + 1, battle.snapshot)
|
|
expect(not stale.ok and stale.code == "stale_revision" and service.state.snapshot() == before, "stale settlement rejects without mutation")
|
|
var tampered: Dictionary = battle.snapshot.duplicate(true)
|
|
tampered.reward_coin = 999
|
|
var rejected := service.settle_route("fg3-tampered-settle", service.state.revision, tampered)
|
|
expect(not rejected.ok and rejected.code == "battle_snapshot_mismatch" and service.state.snapshot() == before, "tampered battle rejects against deterministic replay")
|
|
|
|
|
|
func expect(condition: bool, message: String) -> void:
|
|
if not condition:
|
|
failures.append("FG-3 Contract-1 rounds: %s" % message)
|