extends SceneTree const DraftService = preload("res://entry/initial_company_draft_service.gd") const ContractService = preload("res://entry/guild_contract_service.gd") const FormationService = preload("res://entry/formation_risk_service.gd") const BattleService = preload("res://entry/battle_service.gd") const DRAFT_PATH := "user://dev5_battle_draft.json" const COMPANY_PATH := "user://dev5_battle_company.json" const SEED := "0123456789abcdeffedcba9876543210" const CONTRACT_OP := "abcdef0123456789abcdef0123456789" const HIRE_OP_1 := "11111111222222223333333344444444" const HIRE_OP_2 := "55555555666666667777777788888888" const ORDER_OP := "99999999aaaaaaaabbbbbbbbcccccccc" const READY_OP := "ddddddddeeeeeeeeffffffff00000000" const BATTLE_OP := "1234567890abcdef1234567890abcdef" const OTHER_BATTLE_OP := "fedcba0987654321fedcba0987654321" const RECOVERY_OP := "0f0f0f0ff0f0f0f00f0f0f0fff0f0f0f" var failures: Array[String] = [] func _initialize() -> void: call_deferred("run_test") func run_test() -> void: _cleanup() var prepared := _create_prepared_company() if not prepared.is_empty(): verify_begin_and_timeline(prepared) verify_recovery_and_restart() _cleanup() if failures.is_empty(): print("ABG_DEV5_BATTLE_OK begin=atomic,idempotent conflict=true snapshot=true phases=opening,telegraph,guard,ranged,magic,healing,damage,knockout,result outcome=victory reward=27 coin=24,51 history=true knockout=one same_identity=true recovery=next_stage,candidate duplicate_reward=false battle_receipts=1") quit(0) else: for failure in failures: push_error(failure) quit(1) func verify_begin_and_timeline(prepared: Dictionary) -> void: var service := _battle_service() var before: Dictionary = _draft_service().load_company() var contract: Dictionary = before.company.active_contract var option_id := String(contract.formation.risk_options[1].risk_option_id) var started: Dictionary = service.begin_battle(BATTLE_OP, option_id) expect(started.ok and started.code == "battle_started", "accepted party/risk creates one battle receipt") if not started.ok: return expect(started.company.active_contract.status == "battle" and started.battle.phase_index == 0 and started.battle.total_phases == 9, "battle starts at deterministic opening phase") expect(started.battle.front_to_rear_ids == contract.formation.accepted_party_snapshot.front_to_rear_ids and started.battle.snapshot_hash == contract.formation.accepted_party_snapshot.snapshot_hash, "battle receipt freezes the exact accepted Party snapshot") var duplicate: Dictionary = service.begin_battle(BATTLE_OP, option_id) expect(duplicate.ok and duplicate.code == "battle_already_started" and String(duplicate.battle.battle_hash) == String(started.battle.battle_hash), "duplicate begin returns the original battle receipt") var conflict: Dictionary = service.begin_battle(OTHER_BATTLE_OP, option_id) expect(not conflict.ok and conflict.code == "BATTLE_ALREADY_ACTIVE", "a second operation cannot create a second active battle") var kinds: Array[String] = [] var last: Dictionary = started for _phase in range(9): last = service.advance_battle(BATTLE_OP) expect(last.ok, "battle phase advances without mutation error") if not last.ok: return if last.has("event"): kinds.append(String(last.event.kind)) expect(kinds == ["opening", "telegraph", "guard", "ranged", "magic", "healing", "damage", "knockout", "result"], "event timeline contains the exact authored event order") var result_company: Dictionary = last.company var result_battle: Dictionary = last.battle expect(result_battle.status == "result" and result_battle.result.outcome == "victory" and int(result_battle.result.reward_coin) == 27, "result settles a truthful victory and pressed reward") expect(int(result_company.active_contract.coin) == 51 and result_company.active_contract.battle_settlement_receipts.size() == 1, "result awards 27 Coin once from the 24 Coin opening balance") var knocked: Array = result_battle.result.knocked_out_ids expect(knocked.size() == 1, "pressed risk creates exactly one battle-local knockout") if knocked.size() == 1: var knocked_id := String(knocked[0]) var recruit := _recruit_by_id(result_company, knocked_id) expect(not recruit.is_empty() and int(recruit.battle_history.battles_entered) == 1 and int(recruit.battle_history.victories) == 1 and int(recruit.battle_history.knockouts) == 1, "knocked Recruit keeps stable history instead of being removed") var settled_retry: Dictionary = service.advance_battle(BATTLE_OP) expect(settled_retry.ok and settled_retry.code == "battle_already_settled" and int(settled_retry.company.active_contract.coin) == 51, "settlement retry cannot duplicate reward") func verify_recovery_and_restart() -> void: var settled: Dictionary = _draft_service().load_company() expect(settled.ok and settled.company.active_contract.battle_state.status == "result", "result state survives process restart") if not settled.ok: return var before_ids := _recruit_ids(settled.company) var recovered: Dictionary = _battle_service().recover_next_stage(RECOVERY_OP) expect(recovered.ok and recovered.code == "recovered", "next-stage recovery creates one receipt") if not recovered.ok: return expect(recovered.company.active_contract.status == "market" and not recovered.company.active_contract.has("formation"), "recovery returns to next-stage preparation without stale frozen order") expect(_recruit_ids(recovered.company) == before_ids and recovered.company.active_contract.recovery_receipts.size() == 1, "recovery retains every Recruit identity and one recovery receipt") expect(recovered.receipt.return_rule == "returns_next_stage_same_identity" and recovered.receipt.next_stage_ready, "recovery names the same-identity next-stage rule") var duplicate: Dictionary = _battle_service().recover_next_stage(RECOVERY_OP) expect(duplicate.ok and duplicate.code == "recovery_already_applied" and duplicate.company.active_contract.recovery_receipts.size() == 1, "duplicate recovery does not create another receipt") var saved: Dictionary = _draft_service().load_company() expect(saved.ok and _draft_service().validate_company(saved.company).ok, "recovered Company remains strictly valid") var expected_hash := String(saved.company.company_hash) _write(COMPANY_PATH, "{broken primary") var backup: Dictionary = _battle_service().load_company() expect(backup.ok and backup.code == "recovered_backup" and String(backup.company.company_hash) == expected_hash, "backup recovery preserves result and next-stage state") func _create_prepared_company() -> Dictionary: var draft: Dictionary = _draft_service().begin_or_load(SEED) if not draft.ok: expect(false, "fixture creates opening draft") return {} var selected: Dictionary = _draft_service().set_selection([String(draft.draft.offers[0].offer_id), String(draft.draft.offers[3].offer_id)]) var committed: Dictionary = _draft_service().commit_company() if not selected.ok or not committed.ok: expect(false, "fixture commits opening Company") return {} var created: Dictionary = _contract_service().create_contract(CONTRACT_OP, "CTR-R1-01", "standard") if not created.ok: expect(false, "fixture creates first Contract") return {} var offers: Array = created.company.active_contract.market_board.recruit_offers _contract_service().hire_recruit(HIRE_OP_1, String(offers[0].source_offer_id), 14) var hired: Dictionary = _contract_service().hire_recruit(HIRE_OP_2, String(offers[1].source_offer_id), 14) if not hired.ok: expect(false, "fixture completes both opening hires") return {} var begun: Dictionary = _formation_service().begin_formation() if not begun.ok: expect(false, "fixture creates formation") return {} var saved: Dictionary = _formation_service().save_order(ORDER_OP, begun.formation.base_front_to_rear_ids) if not saved.ok: expect(false, "fixture saves formation") return {} var ready: Dictionary = _formation_service().accept_readiness(READY_OP, String(saved.receipt.order_hash)) return ready.company if ready.ok else {} func _draft_service() -> RefCounted: var service := DraftService.new() service.path = DRAFT_PATH service.company_path = COMPANY_PATH return service func _contract_service() -> RefCounted: var service := ContractService.new() service.company_path = COMPANY_PATH return service func _formation_service() -> RefCounted: var service := FormationService.new() service.company_path = COMPANY_PATH return service func _battle_service() -> RefCounted: var service := BattleService.new() service.company_path = COMPANY_PATH return service func _recruit_by_id(company: Dictionary, recruit_id: String) -> Dictionary: for recruit in company.owned_recruits: if String(recruit.recruit_id) == recruit_id: return Dictionary(recruit) return {} func _recruit_ids(company: Dictionary) -> Array[String]: var ids: Array[String] = [] for recruit in company.owned_recruits: ids.append(String(recruit.recruit_id)) return ids func _write(path: String, text_value: String) -> void: var file := FileAccess.open(path, FileAccess.WRITE) if file == null: expect(false, "fixture writes %s" % path) return file.store_string(text_value) file.close() func _cleanup() -> void: for path in [DRAFT_PATH, "%s.bak" % DRAFT_PATH, "%s.candidate" % DRAFT_PATH, "%s.bak.candidate" % DRAFT_PATH, COMPANY_PATH, "%s.bak" % COMPANY_PATH, "%s.candidate" % COMPANY_PATH, "%s.bak.candidate" % COMPANY_PATH, "%s.corrupt" % COMPANY_PATH]: if FileAccess.file_exists(path): DirAccess.remove_absolute(path) func expect(condition: bool, message: String) -> void: if not condition: failures.append("DEV-5 battle foundation: %s" % message)