145 lines
7.2 KiB
GDScript
145 lines
7.2 KiB
GDScript
extends SceneTree
|
|
|
|
const Battle = preload("res://fg4/contract2_battle_service.gd")
|
|
const Contract = preload("res://fg4/contract2_service.gd")
|
|
const Fixture = preload("res://tests/fg4/contract1_fixture.gd")
|
|
|
|
var failures: Array[String] = []
|
|
var successful_routes := 0
|
|
var counterfactual_routes := 0
|
|
|
|
|
|
func _initialize() -> void:
|
|
call_deferred("run_test")
|
|
|
|
|
|
func run_test() -> void:
|
|
verify_two_viable_plans_per_route()
|
|
verify_complete_twelve_round_flow()
|
|
verify_permanent_loss_and_same_board_recovery()
|
|
verify_tamper_stale_and_duplicate_boundaries()
|
|
if failures.is_empty():
|
|
print("ABG_FG4_CONTRACT2_ROUNDS_OK routes=%d counterfactual_routes=%d rounds=12 history=48 plans=4 repeat_events=true rewards=24..60 exact_once=true permanent_loss=true same_board_recovery=true resurrection=0" % [successful_routes, counterfactual_routes])
|
|
quit(0)
|
|
else:
|
|
for failure in failures:
|
|
push_error(failure)
|
|
quit(1)
|
|
|
|
|
|
func verify_two_viable_plans_per_route() -> void:
|
|
for market_index in range(1, 13):
|
|
for risk in range(3):
|
|
var primary := String(Battle.PRIMARY_PLAN[market_index])
|
|
var secondary := String(Battle.SECONDARY_PLAN[market_index])
|
|
var primary_score := Battle.route_plan_score(market_index, risk, primary, "", 0, 1.0)
|
|
var secondary_score := Battle.route_plan_score(market_index, risk, secondary, "", 0, 1.0)
|
|
expect(primary_score >= 15 and secondary_score >= 15, "Market %d risk %d has two viable plans" % [market_index, risk])
|
|
if primary_score >= 15 and secondary_score >= 15:
|
|
counterfactual_routes += 1
|
|
|
|
|
|
func verify_complete_twelve_round_flow() -> void:
|
|
var service := Contract.new(Fixture.new().contract2_market_1_snapshot())
|
|
expect(service.valid, "complete Contract-2 fixture is valid")
|
|
if not service.valid:
|
|
return
|
|
for market_index in range(1, 13):
|
|
var run: Dictionary = service.state.current_run
|
|
var route: Dictionary = run.market_board.battle_offers[0]
|
|
var coin_before := int(run.coin)
|
|
var prepared := service.commit_contract2_route(
|
|
"fg4-contract2-round-%02d-prepare" % market_index,
|
|
service.state.revision,
|
|
String(route.offer_id),
|
|
String(run.market_board.board_hash)
|
|
)
|
|
expect(prepared.ok, "Market %d route prepares" % market_index)
|
|
if not prepared.ok:
|
|
return
|
|
var plan_id := String(Battle.PRIMARY_PLAN[market_index])
|
|
var battle := Battle.new(service.state.snapshot()).run_to_terminal(plan_id)
|
|
expect(battle.ok and battle.snapshot.outcome == "victory", "Market %d primary response wins" % market_index)
|
|
expect(has_event(battle.snapshot.event_log, "repeat_counter_disclosed"), "Market %d emits repeat disclosure" % market_index)
|
|
var settled := service.settle_contract2_route(
|
|
"fg4-contract2-round-%02d-settle" % market_index,
|
|
service.state.revision,
|
|
battle.snapshot
|
|
)
|
|
expect(settled.ok and settled.receipt.reward_coin == route.reward_coin, "Market %d settles exact reward" % market_index)
|
|
expect(service.state.current_run.coin == coin_before + route.reward_coin, "Market %d Coin changes once" % market_index)
|
|
if settled.ok:
|
|
successful_routes += 3
|
|
expect(service.state.current_run.phase == "contract2_boss_muster" and service.state.current_run.cleared_rounds == 48, "twelve victories open Foreman Muster")
|
|
expect(service.state.current_run.route_history.size() == 48, "history contains prior 36 plus new 12 routes")
|
|
|
|
|
|
func verify_permanent_loss_and_same_board_recovery() -> void:
|
|
var service := Contract.new(Fixture.new().contract2_market_1_snapshot())
|
|
if not service.valid:
|
|
expect(false, "recovery fixture is valid")
|
|
return
|
|
var board: Dictionary = service.state.current_run.market_board.duplicate(true)
|
|
var route: Dictionary = board.battle_offers[2]
|
|
var company_before: Array = Array(service.state.current_run.company).duplicate()
|
|
var history_before: int = service.state.current_run.route_history.size()
|
|
var prepared := service.commit_contract2_route(
|
|
"fg4-contract2-loss-prepare",
|
|
service.state.revision,
|
|
String(route.offer_id),
|
|
String(board.board_hash)
|
|
)
|
|
var battle := Battle.new(service.state.snapshot()).run_to_terminal(first_wrong_plan(1))
|
|
expect(prepared.ok and battle.ok and battle.snapshot.outcome == "defeat", "wrong response causes defeat")
|
|
expect(battle.snapshot.fallen_recruit_ids.size() == 1, "defeat discloses one permanent casualty")
|
|
var fallen_id := String(battle.snapshot.fallen_recruit_ids[0])
|
|
var settled := service.settle_contract2_route("fg4-contract2-loss-settle", service.state.revision, battle.snapshot)
|
|
expect(settled.ok and service.state.current_run.phase == "contract2_recovery", "defeat opens Contract-2 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 permanently removed")
|
|
expect(service.state.current_run.route_history.size() == history_before and settled.receipt.reward_coin == 0, "defeat pays no Coin and clears no route")
|
|
var battle_hash := String(service.state.current_run.last_battle_report.battle_hash)
|
|
var recovery_cost := int(service.state.current_run.recovery_cost)
|
|
var recovered := service.recover_contract2_route(
|
|
"fg4-contract2-loss-recover",
|
|
service.state.revision,
|
|
battle_hash,
|
|
recovery_cost
|
|
)
|
|
expect(recovered.ok and service.state.current_run.phase == "contract2_market", "recovery returns to Contract-2 Market")
|
|
expect(service.state.current_run.market_board == board, "recovery preserves the exact board")
|
|
expect(fallen_id not in service.state.current_run.company, "recovery never resurrects")
|
|
|
|
|
|
func verify_tamper_stale_and_duplicate_boundaries() -> void:
|
|
var service := Contract.new(Fixture.new().contract2_market_1_snapshot())
|
|
if not service.valid:
|
|
return
|
|
var route: Dictionary = service.state.current_run.market_board.battle_offers[0]
|
|
var revision_before := service.state.revision
|
|
var board_hash := String(service.state.current_run.market_board.board_hash)
|
|
expect(not service.commit_contract2_route("fg4-contract2-stale", revision_before - 1, String(route.offer_id), board_hash).ok, "stale prepare rejects")
|
|
var prepared := service.commit_contract2_route("fg4-contract2-boundary-prepare", revision_before, String(route.offer_id), board_hash)
|
|
var duplicate := service.commit_contract2_route("fg4-contract2-boundary-prepare", revision_before, String(route.offer_id), board_hash)
|
|
expect(prepared.ok and duplicate.ok and duplicate.duplicate, "duplicate prepare is inert")
|
|
var battle := Battle.new(service.state.snapshot()).run_to_terminal(String(Battle.PRIMARY_PLAN[1]))
|
|
var tampered: Dictionary = battle.snapshot.duplicate(true)
|
|
tampered.reward_coin += 1
|
|
expect(not service.settle_contract2_route("fg4-contract2-tampered-settle", service.state.revision, tampered).ok, "tampered battle snapshot rejects")
|
|
expect(service.state.current_run.phase == "contract2_prepared", "rejected settlement preserves prepared state")
|
|
|
|
|
|
func first_wrong_plan(market_index: int) -> String:
|
|
for plan_id in Battle.PLAN_IDS:
|
|
if plan_id not in [Battle.PRIMARY_PLAN[market_index], Battle.SECONDARY_PLAN[market_index]]:
|
|
return String(plan_id)
|
|
return ""
|
|
|
|
|
|
func has_event(events: Array, kind: String) -> bool:
|
|
return events.any(func(event): return String(event.get("kind", "")) == kind)
|
|
|
|
|
|
func expect(condition: bool, message: String) -> void:
|
|
if not condition:
|
|
failures.append("FG-4 Contract-2 rounds: %s" % message)
|