289 lines
14 KiB
GDScript
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]]
|