Files
aetherbound-guild/runtime/tests/dev5/battle_foundation_test.gd
T

194 lines
9.5 KiB
GDScript

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)