183 lines
9.3 KiB
GDScript
183 lines
9.3 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_boss_victory_record_and_handoff()
|
|
verify_boss_defeat_and_recovery()
|
|
verify_boss_atomic_boundaries()
|
|
if failures.is_empty():
|
|
print("ABG_FG3_CONTRACT1_BOSS_OK boss=BO-R2-01 plans=2 victory=true defeat=true casualty=permanent recovery=same_muster boss_coin=0 clear=CLEAR-RG02-C01 renown=16 company_record=1 contract2_handoff=true duplicate=true replay=true save_v10=roundtrip")
|
|
quit(0)
|
|
else:
|
|
for failure in failures:
|
|
push_error(failure)
|
|
quit(1)
|
|
|
|
|
|
func verify_boss_victory_record_and_handoff() -> void:
|
|
var boss_snapshot := Fixture.new().boss_muster_snapshot()
|
|
var service := ContractService.new(boss_snapshot)
|
|
expect(service.valid and service.state.current_run.phase == "boss_muster", "real twelve-Round state opens fixed Muster")
|
|
if not service.valid:
|
|
return
|
|
var coin_before := int(service.state.current_run.coin)
|
|
var renown_before := int(service.state.guild_profile.persistent_renown)
|
|
var records_before: int = service.state.guild_profile.company_records.size()
|
|
var muster_hash := String(service.state.current_run.boss_muster.muster_hash)
|
|
var prepare_revision := int(service.state.revision)
|
|
var prepared := service.commit_boss_plan(
|
|
"fg3-boss-victory-prepare",
|
|
prepare_revision,
|
|
muster_hash,
|
|
"follow_index_clear_eggs"
|
|
)
|
|
expect(prepared.ok and service.state.current_run.phase == "boss_prepared", "follow-index plan prepares")
|
|
var duplicate_prepare := service.commit_boss_plan(
|
|
"fg3-boss-victory-prepare",
|
|
prepare_revision,
|
|
muster_hash,
|
|
"follow_index_clear_eggs"
|
|
)
|
|
expect(duplicate_prepare.ok and duplicate_prepare.duplicate, "Boss prepare retry is idempotent")
|
|
var battle := BattleService.new(service.state.snapshot()).run_to_terminal("follow_index_clear_eggs")
|
|
expect(battle.ok and battle.snapshot.outcome == "victory", "disclosed follow-index response defeats Sluiceback Mother")
|
|
expect(event_count(battle.snapshot.event_log, "safe_index_forecast") == 1 and event_count(battle.snapshot.event_log, "egg_positions_disclosed") == 1, "battle exposes safe index and eggs")
|
|
expect(event_count(battle.snapshot.event_log, "safe_index_reversed") == 1 and event_count(battle.snapshot.event_log, "eggs_cleared_on_index") == 1, "battle executes phase change and selected response")
|
|
if not battle.ok:
|
|
return
|
|
var settle_revision := int(service.state.revision)
|
|
var settled := service.settle_boss(
|
|
"fg3-boss-victory-settle",
|
|
settle_revision,
|
|
battle.snapshot
|
|
)
|
|
expect(settled.ok and service.state.current_run.phase == "contract_result", "Boss victory stages one Contract result")
|
|
expect(service.state.current_run.coin == coin_before and settled.receipt.reward_coin == 0, "Boss grants no spendable Coin")
|
|
expect(String(service.state.current_run.pending_boss_result.close_id).length() == 32, "victory creates one exact close identity")
|
|
var after_settle := service.state.snapshot()
|
|
var duplicate_settle := service.settle_boss(
|
|
"fg3-boss-victory-settle",
|
|
settle_revision,
|
|
battle.snapshot
|
|
)
|
|
expect(duplicate_settle.ok and duplicate_settle.duplicate and service.state.snapshot() == after_settle, "Boss settlement retry is inert")
|
|
var close_id := String(service.state.current_run.pending_boss_result.close_id)
|
|
var close_revision := int(service.state.revision)
|
|
var closed := service.close_contract(
|
|
"fg3-contract1-close",
|
|
close_revision,
|
|
close_id
|
|
)
|
|
expect(closed.ok and service.state.current_run.phase == "contract2_handoff", "first Contract closes into same-Run Contract 2 handoff")
|
|
expect(service.state.guild_profile.persistent_renown == renown_before + 16 and "CLEAR-RG02-C01" in service.state.guild_profile.contract_clear_ids, "first clear grants exact 16 Renown once")
|
|
expect(service.state.guild_profile.company_records.size() == records_before + 1, "close creates exactly one Company Record")
|
|
var record: Dictionary = service.state.guild_profile.company_records[-1]
|
|
expect(record.contract_id == "CTR-R2-01" and record.boss_id == "BO-R2-01" and record.route_ids.size() == 12, "Company Record binds Contract, Boss and twelve routes")
|
|
expect(service.state.current_run.next_contract_id == "CTR-R2-02" and service.state.current_run.market_board.next_contract_id == "CTR-R2-02", "handoff names exact next Contract")
|
|
expect(service.state.current_run.company == boss_snapshot.current_run.company, "surviving Company continues without reset")
|
|
var after_close := service.state.snapshot()
|
|
var duplicate_close := service.close_contract(
|
|
"fg3-contract1-close",
|
|
close_revision,
|
|
close_id
|
|
)
|
|
expect(duplicate_close.ok and duplicate_close.duplicate and service.state.snapshot() == after_close, "Contract close retry cannot duplicate Renown or Record")
|
|
var encoded := SaveV10.new().encode_snapshot(after_close, 90)
|
|
var decoded := SaveV10.new().decode_text(String(encoded.text), 90)
|
|
expect(encoded.ok and decoded.ok and decoded.snapshot == after_close, "Contract-2 handoff roundtrips through Save-v10")
|
|
|
|
|
|
func verify_boss_defeat_and_recovery() -> void:
|
|
var boss_snapshot := Fixture.new().boss_muster_snapshot()
|
|
var company_before: Array = Array(boss_snapshot.current_run.company).duplicate()
|
|
var deployed_count := mini(4, company_before.size())
|
|
boss_snapshot.current_run.deployed_recruit_ids = company_before.slice(0, deployed_count)
|
|
boss_snapshot.current_run.reserve_recruit_ids = company_before.slice(deployed_count)
|
|
boss_snapshot.current_run.formation_proposal = company_before.slice(0, deployed_count)
|
|
var service := ContractService.new(boss_snapshot)
|
|
expect(service.valid, "bounded four-member Boss recovery fixture is valid")
|
|
if not service.valid:
|
|
return
|
|
var muster_before: Dictionary = service.state.current_run.boss_muster.duplicate(true)
|
|
var coin_before := int(service.state.current_run.coin)
|
|
var prepared := service.commit_boss_plan(
|
|
"fg3-boss-defeat-prepare",
|
|
service.state.revision,
|
|
String(muster_before.muster_hash),
|
|
"shift_hatch_area"
|
|
)
|
|
expect(prepared.ok, "legal area-shift plan prepares with a weak line")
|
|
var battle := BattleService.new(service.state.snapshot()).run_to_terminal("shift_hatch_area")
|
|
expect(battle.ok and battle.snapshot.outcome == "defeat" and battle.snapshot.fallen_recruit_ids.size() == 1, "state-dependent legal plan can fail with one casualty")
|
|
if not battle.ok:
|
|
return
|
|
var fallen_id := String(battle.snapshot.fallen_recruit_ids[0])
|
|
var settled := service.settle_boss(
|
|
"fg3-boss-defeat-settle",
|
|
service.state.revision,
|
|
battle.snapshot
|
|
)
|
|
expect(settled.ok and service.state.current_run.phase == "boss_recovery", "Boss defeat enters explicit recovery")
|
|
expect(fallen_id not in service.state.current_run.company and service.state.current_run.coin == coin_before, "Boss defeat permanently removes casualty and grants no Coin")
|
|
expect(service.state.current_run.boss_muster == muster_before, "defeat preserves exact disclosed Muster")
|
|
expect(service.state.current_run.replacement_credit > int(boss_snapshot.current_run.replacement_credit), "Boss casualty grants bounded Credit")
|
|
var recovered := service.recover_boss(
|
|
"fg3-boss-defeat-recover",
|
|
service.state.revision,
|
|
String(battle.snapshot.snapshot_hash),
|
|
22
|
|
)
|
|
expect(recovered.ok and service.state.current_run.phase == "boss_muster", "Boss recovery reopens plan selection")
|
|
expect(service.state.current_run.boss_muster == muster_before, "recovery never rerolls Boss facts")
|
|
expect(fallen_id not in service.state.current_run.company and service.state.current_run.company.size() == company_before.size() - 1, "recovery never resurrects or substitutes")
|
|
expect(service.state.current_run.coin == coin_before - 22 and service.state.battle_checkpoints.is_empty(), "Boss recovery charges exact cost and clears checkpoint")
|
|
|
|
|
|
func verify_boss_atomic_boundaries() -> void:
|
|
var service := ContractService.new(Fixture.new().boss_muster_snapshot())
|
|
var before := service.state.snapshot()
|
|
var invalid := service.commit_boss_plan(
|
|
"fg3-boss-invalid-plan",
|
|
service.state.revision,
|
|
String(service.state.current_run.boss_muster.muster_hash),
|
|
"rush_shell"
|
|
)
|
|
expect(not invalid.ok and invalid.code == "boss_plan_invalid" and service.state.snapshot() == before, "undeclared Boss plan rejects without mutation")
|
|
var prepared := service.commit_boss_plan(
|
|
"fg3-boss-atomic-prepare",
|
|
service.state.revision,
|
|
String(service.state.current_run.boss_muster.muster_hash),
|
|
"follow_index_clear_eggs"
|
|
)
|
|
expect(prepared.ok, "atomic Boss fixture prepares")
|
|
var battle := BattleService.new(service.state.snapshot()).run_to_terminal("follow_index_clear_eggs")
|
|
var prepared_snapshot := service.state.snapshot()
|
|
var tampered: Dictionary = battle.snapshot.duplicate(true)
|
|
tampered.cause = "forged"
|
|
var rejected := service.settle_boss("fg3-boss-tampered", service.state.revision, tampered)
|
|
expect(not rejected.ok and rejected.code == "battle_snapshot_mismatch" and service.state.snapshot() == prepared_snapshot, "tampered Boss result rejects against replay")
|
|
|
|
|
|
func event_count(events: Array, kind: String) -> int:
|
|
var count := 0
|
|
for event in events:
|
|
if String(event.get("kind", "")) == kind:
|
|
count += 1
|
|
return count
|
|
|
|
|
|
func expect(condition: bool, message: String) -> void:
|
|
if not condition:
|
|
failures.append("FG-3 Contract-1 Boss: %s" % message)
|