Files
aetherbound-guild/runtime/tests/fg3/contract2_execution_test.gd
T

249 lines
13 KiB
GDScript

extends SceneTree
const BattleService = preload("res://fg3/contract2_battle_service.gd")
const ExecutionService = preload("res://fg3/contract2_execution_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_twelve_round_execution()
verify_permanent_casualty_and_recovery()
verify_replay_and_operation_atomicity()
verify_route_and_artifact_counterfactuals()
if failures.is_empty():
print("ABG_FG3_CONTRACT2_EXECUTION_OK rounds=12 routes=36 responses=2 state_reversals=2 artifacts=7 nondominant_plans=4 rewards=exact xp=exact history=24 pulse=event_bound casualty=permanent recovery=same_board replay=true duplicate=true save_v10=recovery_roundtrip offline=zero")
quit(0)
else:
for failure in failures:
push_error(failure)
quit(1)
func verify_twelve_round_execution() -> void:
var start := Fixture.new().contract2_market_1_snapshot()
var service := ExecutionService.new(start)
expect(service.valid, "real first-Contract handoff activates Contract 2")
if not service.valid:
return
var expected_coin := int(start.current_run.coin)
var contract2_route_ids: Array = []
for market_index in range(1, 13):
expect(service.state.current_run.phase == "contract2_market", "Market %d is open" % market_index)
expect(service.state.current_run.market_index == market_index, "Market %d index is exact" % market_index)
var route: Dictionary = service.state.current_run.market_board.battle_offers[0]
var board_hash := String(service.state.current_run.market_board.board_hash)
var revision_before := int(service.state.revision)
var operation_id := "fg3-contract2-round-%02d-prepare" % market_index
var prepared := service.commit_route(operation_id, revision_before, String(route.offer_id), board_hash)
expect(prepared.ok and service.state.current_run.phase == "contract2_prepared", "Market %d route prepares" % market_index)
var duplicate_prepare := service.commit_route(operation_id, revision_before, String(route.offer_id), board_hash)
expect(duplicate_prepare.ok and duplicate_prepare.duplicate, "Market %d prepare retry is inert" % market_index)
if not prepared.ok:
return
var plan_id := String(BattleService.PRIMARY_PLAN[market_index])
var battle := BattleService.new(service.state.snapshot()).run_to_terminal(plan_id)
expect(battle.ok and battle.snapshot.outcome == "victory", "Market %d primary response wins" % market_index)
if not battle.ok:
return
expect(event_count(battle.snapshot.event_log, "pulse_linked") == 1, "Market %d emits one disclosed pulse" % market_index)
expect(event_count(battle.snapshot.event_log, "plan_response") == 1, "Market %d emits one bound plan response" % market_index)
var coin_before := int(service.state.current_run.coin)
var settled := service.settle_route(
"fg3-contract2-round-%02d-settle" % market_index,
service.state.revision,
battle.snapshot
)
expect(settled.ok, "Market %d settlement applies" % market_index)
if not settled.ok:
return
expected_coin += int(route.reward_coin)
contract2_route_ids.append(String(route.route_id))
expect(service.state.current_run.coin == coin_before + int(route.reward_coin), "Market %d pays exact reward" % market_index)
expect(settled.receipt.xp_award == int(battle.snapshot.xp_grant), "Market %d pays exact XP" % market_index)
expect(service.state.current_run.cleared_rounds == 12 + market_index, "Market %d increments global clears once" % market_index)
expect(service.state.current_run.phase == "contract2_boss_muster", "Round 12 opens the second Boss Muster")
expect(service.state.current_run.cleared_rounds == 24 and service.state.current_run.route_history.size() == 24, "both Contracts retain 24 route records")
expect(service.state.current_run.route_history.slice(12).map(func(row): return String(row.route_id)) == contract2_route_ids, "second Record input retains the twelve exact routes")
expect(service.state.current_run.coin == expected_coin, "all ordinary rewards sum exactly")
expect(service.state.current_run.boss_muster.boss_id == "BO-R2-02" and service.state.current_run.boss_muster.plan_ids.size() == 2, "Drowned Bell Muster exposes two plans")
func verify_permanent_casualty_and_recovery() -> void:
var start := Fixture.new().contract2_market_1_snapshot()
var service := ExecutionService.new(start)
if not service.valid:
expect(false, "recovery fixture validates")
return
var board_before: Dictionary = service.state.current_run.market_board.duplicate(true)
var company_before: Array = Array(service.state.current_run.company).duplicate()
var coin_before := int(service.state.current_run.coin)
var route: Dictionary = service.state.current_run.market_board.battle_offers[2]
var primary := String(BattleService.PRIMARY_PLAN[1])
var secondary := String(BattleService.SECONDARY_PLAN[1])
var failing_plan := "interrupt_pulse"
if failing_plan in [primary, secondary]:
failing_plan = "reorder_line"
var prepared := service.commit_route(
"fg3-contract2-defeat-prepare",
service.state.revision,
String(route.offer_id),
String(board_before.board_hash)
)
expect(prepared.ok, "risk-two recovery route prepares")
if not prepared.ok:
return
var battle := BattleService.new(service.state.snapshot()).run_to_terminal(failing_plan)
expect(battle.ok and battle.snapshot.outcome == "defeat" and battle.snapshot.fallen_recruit_ids.size() == 1, "unanswered pulse causes one permanent casualty")
if not battle.ok:
return
var fallen_id := String(battle.snapshot.fallen_recruit_ids[0])
var equipment_id := String(start.current_run.equipped_by_recruit.get(fallen_id, ""))
var settled := service.settle_route("fg3-contract2-defeat-settle", service.state.revision, battle.snapshot)
expect(settled.ok and service.state.current_run.phase == "contract2_recovery", "defeat enters explicit recovery")
expect(fallen_id not in service.state.current_run.company and service.state.current_run.company.size() == company_before.size() - 1, "Fallen Recruit is removed permanently")
expect(service.state.current_run.coin == coin_before and service.state.current_run.cleared_rounds == 12, "defeat pays no Coin and no clear")
expect(service.state.current_run.market_board == board_before, "defeat preserves the exact disclosed board")
if not equipment_id.is_empty():
expect(service.state.current_run.inventory[equipment_id].wearer_id == "", "Fallen Recruit equipment returns to inventory")
var recovery_snapshot := service.state.snapshot()
var encoded := SaveV10.new().encode_snapshot(recovery_snapshot, 120)
var decoded := SaveV10.new().decode_text(String(encoded.text), 120)
expect(encoded.ok and decoded.ok and decoded.snapshot == recovery_snapshot, "recovery state roundtrips byte-equivalent")
expect(decoded.offline_delta == SaveV10.OFFLINE_ZERO, "recovery reload invents no offline value")
var recovery_revision := int(service.state.revision)
var recovered := service.recover_route(
"fg3-contract2-defeat-recover",
recovery_revision,
String(battle.snapshot.snapshot_hash),
int(route.recovery_coin)
)
expect(recovered.ok and service.state.current_run.phase == "contract2_market", "recovery reopens the same Market")
expect(service.state.current_run.market_board == board_before, "recovery never rerolls route or pulse 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 - int(route.recovery_coin), "recovery charges the exact disclosed cost")
var after_recovery := service.state.snapshot()
var duplicate_recovery := service.recover_route(
"fg3-contract2-defeat-recover",
recovery_revision,
String(battle.snapshot.snapshot_hash),
int(route.recovery_coin)
)
expect(duplicate_recovery.ok and duplicate_recovery.duplicate and service.state.snapshot() == after_recovery, "recovery retry cannot charge twice")
func verify_replay_and_operation_atomicity() -> void:
var service := ExecutionService.new(Fixture.new().contract2_market_1_snapshot())
if not service.valid:
expect(false, "atomicity fixture validates")
return
var route: Dictionary = service.state.current_run.market_board.battle_offers[0]
var prepared := service.commit_route(
"fg3-contract2-atomic-prepare",
service.state.revision,
String(route.offer_id),
String(service.state.current_run.market_board.board_hash)
)
expect(prepared.ok, "atomicity route prepares")
if not prepared.ok:
return
var battle := BattleService.new(service.state.snapshot()).run_to_terminal(String(BattleService.PRIMARY_PLAN[1]))
var prepared_snapshot := service.state.snapshot()
var tampered: Dictionary = battle.snapshot.duplicate(true)
tampered.score = int(tampered.score) + 1
var rejected := service.settle_route("fg3-contract2-tampered", service.state.revision, tampered)
expect(not rejected.ok and rejected.code == "battle_snapshot_mismatch" and service.state.snapshot() == prepared_snapshot, "tampered replay rejects without mutation")
var settle_revision := int(service.state.revision)
var settled := service.settle_route("fg3-contract2-atomic-settle", settle_revision, battle.snapshot)
expect(settled.ok, "untampered replay settles")
var settled_snapshot := service.state.snapshot()
var duplicate := service.settle_route("fg3-contract2-atomic-settle", settle_revision, battle.snapshot)
expect(duplicate.ok and duplicate.duplicate and service.state.snapshot() == settled_snapshot, "settlement retry cannot duplicate reward or XP")
func verify_route_and_artifact_counterfactuals() -> void:
var support_artifact := {
1: "AR-011", 2: "AR-012", 3: "AR-010", 4: "AR-009",
5: "AR-011", 6: "AR-010", 7: "AR-012", 8: "AR-011",
9: "AR-012", 10: "AR-012", 11: "AR-009", 12: "AR-009",
}
var primary_counts := {}
var secondary_counts := {}
for plan_id in BattleService.PLAN_IDS:
primary_counts[plan_id] = 0
secondary_counts[plan_id] = 0
for market_index in range(1, 13):
var primary := String(BattleService.PRIMARY_PLAN[market_index])
var secondary := String(BattleService.SECONDARY_PLAN[market_index])
var artifact_id := String(support_artifact[market_index])
primary_counts[primary] += 1
secondary_counts[secondary] += 1
var wrong_plan := ""
for plan_id in BattleService.PLAN_IDS:
if plan_id not in [primary, secondary]:
wrong_plan = String(plan_id)
break
for risk in range(3):
var primary_score := BattleService.route_plan_score(
market_index, risk, primary, artifact_id, 3, 1.0
)
var secondary_score := BattleService.route_plan_score(
market_index, risk, secondary, artifact_id, 3, 1.0
)
expect(primary_score >= 15, "Market %d risk %d retains a viable primary response" % [market_index, risk])
expect(secondary_score >= 15, "Market %d risk %d gains a viable secondary response in a matching state" % [market_index, risk])
var wrong_score := BattleService.route_plan_score(
market_index, 0, wrong_plan, artifact_id, 3, 1.0
)
expect(wrong_score < 15, "Market %d does not reduce all four plans to equivalent wins" % market_index)
for plan_id in BattleService.PLAN_IDS:
expect(primary_counts[plan_id] > 0 and secondary_counts[plan_id] > 0, "%s changes rank across Markets" % plan_id)
for artifact_id in BattleService.ARTIFACT_PLAN_BONUS:
var values: Array = []
for plan_id in BattleService.PLAN_IDS:
values.append(int(BattleService.ARTIFACT_PLAN_BONUS[artifact_id].get(plan_id, 0)))
expect(values.max() > values.min(), "%s changes at least one plan comparison" % artifact_id)
var m3_primary := String(BattleService.PRIMARY_PLAN[3])
var m3_secondary := String(BattleService.SECONDARY_PLAN[3])
expect(
BattleService.route_plan_score(3, 1, m3_primary, "AR-011", 0, 1.0)
> BattleService.route_plan_score(3, 1, m3_secondary, "AR-011", 0, 1.0),
"AR-011 favors fortifying the first triple link"
)
expect(
BattleService.route_plan_score(3, 1, m3_primary, "AR-014", 0, 1.0)
< BattleService.route_plan_score(3, 1, m3_secondary, "AR-014", 0, 1.0),
"AR-014 reverses the first triple-link plan toward reordering"
)
var m11_primary := String(BattleService.PRIMARY_PLAN[11])
var m11_secondary := String(BattleService.SECONDARY_PLAN[11])
expect(
BattleService.route_plan_score(11, 1, m11_primary, "AR-011", 0, 1.0)
> BattleService.route_plan_score(11, 1, m11_secondary, "AR-011", 0, 1.0),
"AR-011 favors durable links late in the Contract"
)
expect(
BattleService.route_plan_score(11, 1, m11_primary, "AR-009", 0, 1.0)
< BattleService.route_plan_score(11, 1, m11_secondary, "AR-009", 0, 1.0),
"AR-009 reverses the late link plan toward interruption"
)
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-2 execution: %s" % message)