Files
security-game/src/test/tests/test_scenarios.gd
T
Gig Line Dev 98c31fe44c
Full build / Import + headless tests (push) Successful in 40s
Full build / Full desktop build + optional release (push) Skipped
Audit and fix NPC, door-staff and UI dialogue
- Route the Reason verb through authored reason contexts and give every shared archetype an in-character answer.
- Add Hollywood wristband and TV Studio ticket/credential door-staff lines.
- Clarify radio cancellation, Ask terminology, start hints and campaign-reset copy.
- Record the dialogue audit and length findings in docs/ui-copy.md; add regression tests.
2026-11-02 14:44:01 +01:00

159 lines
7.3 KiB
GDScript

extends RefCounted
## Headless tests for venue scenario data (Plan: Venues and Scenarios).
const Rules := preload("res://src/core/rules.gd")
const VenueScenario := preload("res://src/core/venue_scenario.gd")
const ScenarioFactory := preload("res://src/content/scenario_factory.gd")
const ShiftCoordinator := preload("res://src/core/shift_coordinator.gd")
func test_hollywood_building_loads() -> bool:
var scenario: VenueScenario = ScenarioFactory.hollywood_building()
var a := TestUtils.check_eq(scenario.id, "hollywood_building", "scenario id")
var b := TestUtils.check(scenario.npc_profiles.size() > 0, "has profiles")
var c := TestUtils.check(scenario.access_policy.requires(Rules.AccessKind.WRISTBAND), "requires wristband")
return a and b and c
func test_joke_shop_rules() -> bool:
var scenario: VenueScenario = ScenarioFactory.joke_shop()
var a := TestUtils.check_eq(scenario.access_policy.minimum_age, 21, "21+ club")
var b := TestUtils.check(scenario.access_policy.require_id, "ID required")
var c := TestUtils.check(scenario.access_policy.wand_screening_required, "wand screening required")
var d := TestUtils.check(scenario.equipment.has("wand"), "wand in equipment")
return a and b and c and d
func test_tv_studio_has_all_npc_types() -> bool:
var scenario: VenueScenario = ScenarioFactory.tv_studio()
var all_types := [Rules.NpcType.TOURIST, Rules.NpcType.PERFORMER, Rules.NpcType.INFLUENCER,
Rules.NpcType.CONFUSED_VISITOR, Rules.NpcType.INTOXICATED, Rules.NpcType.TRESPASSER,
Rules.NpcType.MENTAL_CRISIS, Rules.NpcType.VIOLENT_THREAT,
Rules.NpcType.AUTHORIZED_GUEST, Rules.NpcType.AUTHORIZED_STAFF, Rules.NpcType.DELIVERY]
var profile_types: Array = []
for p in scenario.npc_profiles:
profile_types.append(p.npc_type)
for npc_type in all_types:
if not profile_types.has(npc_type):
return TestUtils.fail("TV studio missing npc type %s" % Rules.NPC_TYPE_NAMES[npc_type])
return true
var _spawn_count := 0
func _on_spawn(_profile: NpcProfile, _intent: String, _id: String, _event: Dictionary = {}) -> void:
_spawn_count += 1
func test_late_phase_reaches_violent_event() -> bool:
## Bug fix: the LATE phase never fired because no shift_phases entry existed.
var scenario: VenueScenario = ScenarioFactory.hollywood_building()
var coordinator := ShiftCoordinator.new()
coordinator.setup(scenario, 900.0, 1)
coordinator.start()
for i in 760:
coordinator.tick(1.0)
var a := TestUtils.check_eq(coordinator.current_phase, Rules.Phase.LATE, "LATE phase reached by 760s")
var eligible := scenario.events_in_phase(Rules.Phase.LATE)
var found := false
for e in eligible:
if e.get("id", "") == "violent_emergency_event":
found = true
var b := TestUtils.check(found, "violent event is eligible in LATE phase")
# All venues declare the LATE phase honestly.
var c := true
for venue in [ScenarioFactory.hollywood_building(), ScenarioFactory.joke_shop(), ScenarioFactory.tv_studio()]:
if not venue.shift_phases.has(Rules.Phase.LATE):
c = false
return a and b and TestUtils.check(c, "all venues declare a LATE phase")
func test_door_staff_lines_available() -> bool:
var scenario: VenueScenario = ScenarioFactory.joke_shop()
var rng := RandomNumberGenerator.new()
rng.seed = 7
var ok := true
for context in ["id_check", "ticket_check", "wand_check", "refusal", "prohibited_item"]:
var line: String = scenario.door_staff_line(context, rng)
if line == "":
ok = false
var a := TestUtils.check(ok, "all joke-shop contexts have a door-staff line")
var b := TestUtils.check_eq(scenario.door_staff_line("unknown_context", rng), "", "unknown context returns empty")
var b_country := ScenarioFactory.hollywood_building()
var c := TestUtils.check_eq(b_country.door_staff_line("refusal", rng), "", "venue without that context returns empty")
var e := TestUtils.check(b_country.door_staff_line("wristband_check", rng) != "",
"Hollywood has a wristband door line")
var studio := ScenarioFactory.tv_studio()
var f := TestUtils.check(studio.door_staff_line("ticket_check", rng) != "",
"Studio has a ticket/credential door line")
# Same seed picks the same line.
var r0 := RandomNumberGenerator.new()
r0.seed = 11
var r1 := RandomNumberGenerator.new()
r1.seed = 11
var d := TestUtils.check_eq(
scenario.door_staff_line("wand_check", r0),
scenario.door_staff_line("wand_check", r1),
"seeded door-staff pick is deterministic")
return a and b and c and d and e and f
func test_protester_archetype_present() -> bool:
var profiles := ScenarioFactory.shared_profiles()
var protester_found := false
for p in profiles:
if p.id == "protester":
protester_found = true
var a := TestUtils.check(protester_found, "protester profile defined")
var in_pool := false
for venue in [ScenarioFactory.hollywood_building(), ScenarioFactory.tv_studio()]:
for e in venue.event_pool:
if e.get("npc_type", -1) == Rules.NpcType.PROTESTER:
in_pool = true
return a and TestUtils.check(in_pool, "protester event referenced in public venues")
func test_shift_coordinator_runs_a_shift() -> bool:
var scenario: VenueScenario = ScenarioFactory.hollywood_building()
var coordinator := ShiftCoordinator.new()
coordinator.setup(scenario, 60.0, 1234)
_spawn_count = 0
coordinator.npc_spawn_requested.connect(_on_spawn)
coordinator.start()
for i in 60:
coordinator.tick(1.0)
if not coordinator.running:
break
var a := TestUtils.check(coordinator.elapsed_s > 0.0, "time advanced")
var b := TestUtils.check(_spawn_count > 0, "NPCs spawned during shift")
var review := coordinator.build_review()
var c := TestUtils.check_dict_has(review, "reputation", "review has reputation")
return a and b and c
func test_emergency_event_requires_emergency() -> bool:
var scenario: VenueScenario = ScenarioFactory.hollywood_building()
var found := false
for event in scenario.event_pool:
if event.get("requires_emergency", false):
found = true
return TestUtils.check(found, "scenario contains a scripted emergency event")
func test_waypoints_map_intent_to_location() -> bool:
var scenario: VenueScenario = ScenarioFactory.hollywood_building()
var entrance: Vector3 = scenario.locations["entrance"]
var sidewalk: Vector3 = scenario.locations["sidewalk"]
var steps_end: Vector3 = scenario.locations["steps_end"]
var a := TestUtils.check_eq(scenario.waypoint_for_intent("enter"), entrance, "enter -> entrance")
var b := TestUtils.check_eq(scenario.waypoint_for_intent("trespass"), entrance, "trespass -> entrance")
var c := TestUtils.check_eq(scenario.waypoint_for_intent("photo"), sidewalk, "photo -> sidewalk")
var d := TestUtils.check_eq(scenario.waypoint_for_intent("sit"), steps_end, "sit -> steps_end")
var e := TestUtils.check_eq(scenario.waypoint_for_intent("unknown_intent"), sidewalk, "unknown -> sidewalk fallback")
return a and b and c and d and e
func test_spawn_rate_is_time_based() -> bool:
# Regression: tick() runs at physics rate. Rolling the event scheduler every
# frame spawned >1000 NPCs in seconds. Ten simulated seconds at 60 Hz must
# stay in the normal single-digit-per-second range.
var scenario: VenueScenario = ScenarioFactory.hollywood_building()
var coordinator := ShiftCoordinator.new()
coordinator.setup(scenario, 60.0, 99)
_spawn_count = 0
coordinator.npc_spawn_requested.connect(_on_spawn)
coordinator.start()
for i in 600:
coordinator.tick(1.0 / 60.0)
return TestUtils.check(_spawn_count <= 60, "spawns stay time-based at physics rate (was %d)" % _spawn_count)