Files
aetherbound-guild/runtime/tests/p9_1/full_first_session_test.gd
T

289 lines
14 KiB
GDScript

extends SceneTree
const Contracts = preload("res://domain/p9_1/contracts.gd")
const FirstSessionServiceScript = preload("res://domain/p9_1/session_service.gd")
const FirstSessionStateScript = preload("res://domain/p9_1/session_state.gd")
const PAIRS: Array[Array] = [
["Hearthguard", "Wayrunner"],
["Hearthguard", "Farstring"],
["Hearthguard", "Kindhand"],
["Wayrunner", "Farstring"],
["Wayrunner", "Kindhand"],
["Farstring", "Kindhand"],
]
var failures: Array[String] = []
var completed_paths := 0
var health_bounds_verified := false
func _initialize() -> void:
call_deferred("run_test")
func run_test() -> void:
_assert_member_health_bounds()
for pair in PAIRS:
for adaptation in Contracts.ADAPTATION_IDS:
for guild_response in Contracts.GUILD_RESPONSE_IDS:
for route_id in Contracts.ROUTE_IDS:
for growth_id in expected_members(pair, adaptation, guild_response):
_run_path(pair, adaptation, guild_response, route_id, growth_id)
if completed_paths != 180:
_fail("expected 180 complete paths, got %d" % completed_paths)
if not health_bounds_verified:
_fail("member health bounds were not verified")
if failures.is_empty():
print("ABG_P9_1_FULL_FIRST_SESSION_OK paths=%d pairs=6 adaptations=2 guild=2 routes=3 growth=all exact_hp=true exact_coin=true formation=true level=true" % completed_paths)
quit(0)
else:
for failure in failures:
push_error(failure)
quit(1)
func _assert_member_health_bounds() -> void:
var state = FirstSessionStateScript.new()
state.front_choice = "Hearthguard"
state.rear_choice = "Farstring"
state.field_order.assign(["Hearthguard", "Farstring"])
state.reset_member_health(140)
_expect(state.member_health("Hearthguard") == 100 and state.member_health("Farstring") == 100, "health reset clamps to 100")
state.set_member_health("Hearthguard", -20)
state.set_member_health("Farstring", 125)
_expect(state.member_health("Hearthguard") == 0 and state.member_health("Farstring") == 100, "health mutation clamps to 0..100")
_expect(state.invariants_hold(), "clamped member health preserves invariants")
health_bounds_verified = true
func _run_path(pair: Array, adaptation: String, guild_response: String, route_id: String, growth_id: String) -> void:
var service = FirstSessionServiceScript.new(FirstSessionStateScript.new())
_assert_ok("open_new_session", service.open_new_session())
service.state.set_screen(Contracts.SCREEN_RECRUIT_SELECTION)
_assert_ok("select first %s" % pair[0], service.toggle_recruit(pair[0]))
_assert_ok("select second %s" % pair[1], service.toggle_recruit(pair[1]))
_assert_ok("review formation", service.review_formation())
_assert_ok("commit formation", service.commit_formation())
_expect(service.state.starting_pair == pair, "%s keeps the normalized starting pair" % pair_key(pair))
_expect(service.state.field_order == pair, "%s starts in automatic order" % pair_key(pair))
var battle_1_events := _resolve_battle(service, Contracts.ENCOUNTER_1, 6)
_assert_screen("battle 1 result", service, Contracts.SCREEN_RESULT_1)
_assert_action_sources("battle 1", battle_1_events, [pair[1]])
_expect(is_equal_approx(service.state.result_times.get(Contracts.ENCOUNTER_1, 0.0), 7.25), "battle 1 time remains 7.25")
_expect(service.state.member_hp.get(pair[0], -1) == expected_battle_1_lead_hp(pair), "%s battle 1 lead HP" % pair_key(pair))
_assert_ok("open adaptation", service.open_adaptation())
_assert_ok("choose adaptation %s" % adaptation, service.choose_adaptation(adaptation))
_assert_ok("commit adaptation", service.commit_adaptation())
var battle_2_events := _resolve_battle(service, Contracts.ENCOUNTER_2, 6)
_assert_screen("battle 2 result", service, Contracts.SCREEN_RESULT_2)
_assert_action_sources("battle 2", battle_2_events, [pair[1]])
_expect(is_equal_approx(service.state.result_times.get(Contracts.ENCOUNTER_2, 0.0), 7.05 if adaptation == "EQ-002" else 6.00), "battle 2 time matches adaptation")
_expect(service.state.member_hp.get(pair[0], -1) == expected_battle_2_lead_hp(pair, adaptation), "%s/%s battle 2 lead HP" % [pair_key(pair), adaptation])
_assert_ok("open guild response", service.open_guild_response())
_assert_ok("choose guild %s" % guild_response, service.choose_guild_response(guild_response))
_assert_ok("commit guild response", service.commit_guild_response())
var members := expected_members(pair, adaptation, guild_response)
_expect(service.state.field_order == members, "%s/%s/%s reforms to %s" % [pair_key(pair), adaptation, guild_response, members])
_expect(service.snapshot().party.lead_id == members[0], "snapshot exposes the actual lead after Guild response")
var battle_3_events := _resolve_battle(service, Contracts.ENCOUNTER_3, 7)
_assert_screen("battle 3 result", service, Contracts.SCREEN_RESULT_3)
_assert_action_sources("battle 3", battle_3_events, members.slice(1))
var battle_3_time := 7.00 if guild_response == "add_recruit" else (7.60 if adaptation == "EQ-002" else 6.60)
_expect(is_equal_approx(service.state.result_times.get(Contracts.ENCOUNTER_3, 0.0), battle_3_time), "battle 3 time matches Guild build")
_expect(service.state.member_hp.get(members[0], -1) == expected_battle_3_lead_hp(members, adaptation, guild_response), "battle 3 lead HP matches actual reformed line")
_assert_ok("open route board", service.open_route_board())
_assert_ok("choose route %s" % route_id, service.choose_route(route_id))
_assert_ok("review route", service.review_route())
_assert_ok("commit route", service.commit_route())
var battle_4_events := _resolve_battle(service, Contracts.ENCOUNTER_4, 7)
_assert_screen("battle 4 result", service, Contracts.SCREEN_RESULT_4)
_assert_action_sources("battle 4", battle_4_events, members.slice(1))
var battle_4 := expected_battle_4(members, pair[1], adaptation, guild_response, route_id)
_expect(service.state.member_hp.get(members[0], -1) == battle_4.lead_hp, "battle 4 lead HP matches route/build")
_expect(service.state.member_hp.get(pair[1], -1) == battle_4.marked_hp, "battle 4 marked HP follows original second member")
_expect(is_equal_approx(service.state.result_times.get(Contracts.ENCOUNTER_4, 0.0), battle_4.time), "battle 4 time matches route/build")
_assert_ok("open growth", service.open_growth())
_expect(service.present_member_ids() == members, "growth offers every current member in field order")
_assert_ok("choose growth %s" % growth_id, service.choose_growth(growth_id))
_assert_ok("review growth", service.review_growth())
_assert_ok("commit growth", service.commit_growth())
var battle_5_events := _resolve_battle(service, Contracts.ENCOUNTER_5, 7)
_assert_screen("session result", service, Contracts.SCREEN_SESSION_RESULT)
var battle_5_actors: Array = members.slice(1)
if growth_id == "Farstring" and growth_id not in battle_5_actors:
battle_5_actors.append(growth_id)
_assert_action_sources("battle 5", battle_5_events, battle_5_actors)
var battle_5 := expected_battle_5(members, pair[1], adaptation, guild_response, growth_id)
_expect(service.state.enemy_hp == 0, "enemy is defeated")
_expect(service.state.fifth_reward_committed, "fifth reward is committed")
_expect(service.state.member_hp.get(members[0], -1) == battle_5.lead_hp, "battle 5 lead HP matches trained member")
_expect(service.state.member_hp.get(pair[1], -1) == battle_5.marked_hp, "battle 5 marked HP matches trained member")
_expect(is_equal_approx(service.state.result_times.get(Contracts.ENCOUNTER_5, 0.0), battle_5.time), "battle 5 time matches trained profession")
_expect(service.state.coin == expected_final_coin(guild_response, route_id), "final Coin matches Guild and route")
_expect(service.state.training_committed and service.state.trained_member_id == growth_id, "one selected member is trained")
for member_id in members:
_expect(service.state.member_levels.get(member_id, 0) == (2 if member_id == growth_id else 1), "exact member level for %s" % member_id)
completed_paths += 1
func _resolve_battle(service, encounter_id: StringName, phase_count: int) -> Array[Dictionary]:
var events: Array[Dictionary] = []
for phase in range(phase_count):
var impact: Dictionary = service.resolve_impact(encounter_id, phase, {
"profile": "guarded",
"trained_slot": "",
"trained_profession": "",
})
if not impact.ok and impact.code != Contracts.INVALID_ID:
failures.append("%s phase %d impact rejected with %s" % [encounter_id, phase, impact.code])
elif impact.ok:
for event in impact.events:
events.append(event)
var result: Dictionary = service.complete_phase(encounter_id, phase)
if not result.ok:
failures.append("%s phase %d completion rejected with %s" % [encounter_id, phase, result.code])
return events
func _assert_action_sources(label: String, events: Array[Dictionary], expected_sources: Array) -> void:
var counts := {}
for event in events:
var source_id := String(event.get("source_member_id", ""))
if not source_id.is_empty():
counts[source_id] = int(counts.get(source_id, 0)) + 1
_expect(int(event.get("delta", 0)) != 0, "%s %s action changes state" % [label, source_id])
for source_id in expected_sources:
_expect(int(counts.get(source_id, 0)) == 1, "%s %s acts exactly once" % [label, source_id])
var source_event := _event_from_source(events, String(source_id))
_expect(_profession_action_matches(String(source_id), source_event), "%s %s uses its actual profession action" % [label, source_id])
_expect(counts.size() == expected_sources.size(), "%s exposes no foreign action source" % label)
func _event_from_source(events: Array[Dictionary], source_id: String) -> Dictionary:
for event in events:
if String(event.get("source_member_id", "")) == source_id:
return event
return {}
func _profession_action_matches(source_id: String, event: Dictionary) -> bool:
var qualifier := String(event.get("qualifier_en", ""))
match source_id:
"Wayrunner": return "counter" in qualifier
"Farstring": return "hit" in qualifier or "shot" in qualifier
"Kindhand": return "heal" in qualifier or "recovery" in qualifier or "support" in qualifier
return false
func _assert_ok(label: String, result: Dictionary) -> void:
if not result.ok:
_fail("%s rejected with %s" % [label, result.code])
func _assert_screen(label: String, service, expected: StringName) -> void:
if service.state.screen != expected:
_fail("%s expected %s, got %s" % [label, expected, service.state.screen])
func _expect(condition: bool, label: String) -> void:
if not condition:
_fail(label)
func _fail(message: String) -> void:
if failures.size() < 80:
failures.append(message)
func expected_members(pair: Array, adaptation: String, guild_response: String) -> Array[String]:
var members: Array[String] = [String(pair[0]), String(pair[1])]
if guild_response == "add_recruit":
members.append(expected_added(pair, adaptation))
var ordered: Array[String] = []
for member_id in Contracts.AUTO_FORMATION_ORDER:
if member_id in members:
ordered.append(member_id)
return ordered
func expected_added(pair: Array, adaptation: String) -> String:
return {
"Hearthguard:Wayrunner:EQ-002": "Farstring",
"Hearthguard:Wayrunner:EQ-061": "Kindhand",
"Hearthguard:Farstring:EQ-002": "Kindhand",
"Hearthguard:Farstring:EQ-061": "Kindhand",
"Hearthguard:Kindhand:EQ-002": "Farstring",
"Hearthguard:Kindhand:EQ-061": "Farstring",
"Wayrunner:Farstring:EQ-002": "Kindhand",
"Wayrunner:Farstring:EQ-061": "Kindhand",
"Wayrunner:Kindhand:EQ-002": "Farstring",
"Wayrunner:Kindhand:EQ-061": "Farstring",
"Farstring:Kindhand:EQ-002": "Hearthguard",
"Farstring:Kindhand:EQ-061": "Hearthguard",
}.get("%s:%s" % [pair_key(pair), adaptation], "")
func expected_battle_1_lead_hp(pair: Array) -> int:
var hp: int = {"Hearthguard": 82, "Wayrunner": 88, "Farstring": 58}.get(pair[0], 100)
return mini(100, hp + 12) if "Kindhand" in pair else hp
func expected_battle_2_lead_hp(pair: Array, adaptation: String) -> int:
var hp := 90 if adaptation == "EQ-002" else 70
return mini(100, hp + 12) if "Kindhand" in pair else hp
func expected_battle_3_lead_hp(members: Array, adaptation: String, guild_response: String) -> int:
var hp := 70
if guild_response == "retain_loan" and adaptation == "EQ-002":
hp = 90
if "Kindhand" in members:
hp = mini(100, hp + 12)
return hp
func expected_battle_4(members: Array, marked_id: String, adaptation: String, guild_response: String, route_id: String) -> Dictionary:
var profile := "full_line" if guild_response == "add_recruit" else ("guarded" if adaptation == "EQ-002" else "tempo")
var lead_hp := 100
var marked_hp := 100
var time: float = {
"R6-01:guarded": 7.40, "R6-01:tempo": 6.40, "R6-01:full_line": 7.00,
"R6-02:guarded": 8.00, "R6-02:tempo": 6.00, "R6-02:full_line": 6.80,
"R6-03:guarded": 7.80, "R6-03:tempo": 6.70, "R6-03:full_line": 7.00,
}["%s:%s" % [route_id, profile]]
if route_id == "R6-01":
lead_hp = 90 if profile == "guarded" else (60 if profile == "tempo" else 70)
if "Kindhand" in members:
lead_hp = mini(100, lead_hp + 12)
elif route_id == "R6-02":
lead_hp = 70
if "Kindhand" in members:
lead_hp = 82
else:
marked_hp = 65
if "Kindhand" in members:
marked_hp = 88 if members.size() == 3 and "Farstring" in members else 77
return {"lead_hp": lead_hp, "marked_hp": marked_hp, "marked_id": marked_id, "time": time}
func expected_battle_5(members: Array, marked_id: String, adaptation: String, guild_response: String, growth_id: String) -> Dictionary:
var retained_item := adaptation if guild_response == "retain_loan" else ""
var lead_id: String = members[0]
var lead_hp := 70 if retained_item == "EQ-002" else 55
if growth_id == lead_id and growth_id in ["Hearthguard", "Wayrunner"]:
lead_hp += 20
var marked_hp := 80 if growth_id == marked_id and growth_id == "Wayrunner" else 60
if "Kindhand" in members:
marked_hp = mini(100, marked_hp + (25 if growth_id == "Kindhand" else 12))
return {"lead_hp": lead_hp, "marked_hp": marked_hp, "time": 6.40 if growth_id == "Farstring" else 7.40}
func expected_final_coin(guild_response: String, route_id: String) -> int:
var after_guild := 8 if guild_response == "retain_loan" else 6
return after_guild + {"R6-01": 4, "R6-02": 7, "R6-03": 10}[route_id] + 8
func pair_key(pair: Array) -> String:
return "%s:%s" % [pair[0], pair[1]]