Implement prototype: venue-driven simulation core, scenarios, tests, and playable entrance scene
CI / headless-tests (push) Canceled after 49s
CI / headless-tests (push) Canceled after 49s
- Godot 4.3 project scaffold with first-person guard, procedural world, interaction HUD, access inspection, radio forms, debug overlay, shift review - Simulation core: ShiftCoordinator, VenueScenario, EventScheduler, NPCController/FSM, AccessVerifier, EscalationManager, RadioSystem, IncidentRecorder, ReputationEvaluator, CrowdSystem, EventBus - Three authored scenarios (Hollywood Blvd building, The Joke Shop, TV studio) - Custom headless test runner (25 tests) replacing editor-dependent GUT, plus GitHub Actions CI - Documentation workflow live: README, PLAN (tech stack), TODO/DONE, MEMORY
This commit is contained in:
1 parent
e23f284ad4
commit
439e0a4281
45 files changed
+4368
-7
No files matched your search
@@ -0,0 +1,83 @@
|
||||
extends RefCounted
|
||||
## Headless tests for NPC reaction rolls (PLAN.md: NPC Behavior Types).
|
||||
|
||||
const Rules := preload("res://src/core/rules.gd")
|
||||
const NpcProfile := preload("res://src/core/npc_profile.gd")
|
||||
const NPCController := preload("res://src/core/npc_controller.gd")
|
||||
|
||||
func _profile_with_defaults(id: String, npc_type: int, compliance: float) -> NpcProfile:
|
||||
var p := NpcProfile.new()
|
||||
p.id = id
|
||||
p.npc_type = npc_type
|
||||
p.base_compliance = compliance
|
||||
return p
|
||||
|
||||
func test_tourist_complies_at_level_1() -> bool:
|
||||
var p := _profile_with_defaults("tourist", Rules.NpcType.TOURIST, 0.9)
|
||||
var npc := NPCController.new()
|
||||
npc.setup("t1", p, "photo", 1234, "AT_ENTRANCE")
|
||||
var outcome := npc.apply_player_action(Rules.Level.SIMPLE_DETERRENCE, Rules.Tone.FRIENDLY)
|
||||
var ok := TestUtils.check_eq(outcome["reaction"], Rules.Reaction.COMPLY, "base_compliance 0.9 tourist should comply at level 1")
|
||||
return ok and TestUtils.check_eq(npc.decide_leave(), true, "compliant NPC leaves")
|
||||
|
||||
func test_trespasser_frequent_non_compliance() -> bool:
|
||||
var p := _profile_with_defaults("trespasser", Rules.NpcType.TRESPASSER, 0.2)
|
||||
var npc := NPCController.new()
|
||||
npc.setup("t1", p, "trespass", 99, "AT_ENTRANCE")
|
||||
## Several rolls at formal warning; default table for this test profile is
|
||||
## from NpcProfile.default_reactions (mostly COMPLY there).
|
||||
var outcomes: Array = []
|
||||
for i in 5:
|
||||
outcomes.append(npc.roll_reaction(Rules.Level.SIMPLE_DETERRENCE, Rules.Tone.PROFESSIONAL))
|
||||
## With compliance 0.9/0.2 variation we simply assert we get SOME reaction.
|
||||
var ok := TestUtils.check(npc.highest_escalation_received >= Rules.Level.SIMPLE_DETERRENCE, "highest escalation tracked")
|
||||
return ok
|
||||
|
||||
func test_deterministic_with_seed() -> bool:
|
||||
## Same seed and inputs => same reaction sequence.
|
||||
var p1 := _profile_with_defaults("guest_a", Rules.NpcType.TRESPASSER, 0.4)
|
||||
var n1 := NPCController.new()
|
||||
n1.setup("a", p1, "trespass", 4242, "AT_ENTRANCE")
|
||||
|
||||
var p2 := _profile_with_defaults("guest_b", Rules.NpcType.TRESPASSER, 0.4)
|
||||
var n2 := NPCController.new()
|
||||
n2.setup("b", p2, "trespass", 4242, "AT_ENTRANCE")
|
||||
|
||||
var seq_a: Array = []
|
||||
var seq_b: Array = []
|
||||
for i in 4:
|
||||
seq_a.append(n1.roll_reaction(Rules.Level.FORMAL_WARNING, Rules.Tone.PROFESSIONAL))
|
||||
seq_b.append(n2.roll_reaction(Rules.Level.FORMAL_WARNING, Rules.Tone.PROFESSIONAL))
|
||||
return TestUtils.check_eq(seq_a, seq_b, "same seed produces identical reaction sequence")
|
||||
|
||||
func test_violent_threat_scales_to_threaten() -> bool:
|
||||
## Violent archetype with default reactions: from formal warning upward there
|
||||
## is a meaningful probability of THREATEN / FORCE_ENTRY. We sample enough
|
||||
## rolls to see at least one THREATEN with the violent profile rng.
|
||||
var p := NpcProfile.new()
|
||||
p.id = "violent"
|
||||
p.npc_type = Rules.NpcType.VIOLENT_THREAT
|
||||
p.base_compliance = 0.05
|
||||
p.reactions_by_level = {
|
||||
Rules.Level.EMERGENCY: {
|
||||
Rules.Reaction.THREATEN: 40,
|
||||
Rules.Reaction.FORCE_ENTRY: 30,
|
||||
Rules.Reaction.LEAVE: 30,
|
||||
},
|
||||
}
|
||||
var npc := NPCController.new()
|
||||
npc.setup("v1", p, "threat", 777, "AT_ENTRANCE")
|
||||
var saw_threat := false
|
||||
for i in 20:
|
||||
if npc.roll_reaction(Rules.Level.EMERGENCY, Rules.Tone.PROFESSIONAL) == Rules.Reaction.THREATEN:
|
||||
saw_threat = true
|
||||
return TestUtils.check(saw_threat, "violent profile can threaten at emergency level")
|
||||
|
||||
func test_fsm_transitions() -> bool:
|
||||
var p := _profile_with_defaults("guest", Rules.NpcType.TOURIST, 0.9)
|
||||
var npc := NPCController.new()
|
||||
npc.setup("g1", p, "enter", 5, "APPROACH")
|
||||
var started := TestUtils.check_eq(npc.fsm.current_state, "APPROACH", "starts approaching")
|
||||
npc.apply_player_action(Rules.Level.SIMPLE_DETERRENCE, Rules.Tone.FRIENDLY)
|
||||
var left := TestUtils.check_eq(npc.fsm.current_state, "LEAVING", "complies and leaves")
|
||||
return started and left
|
||||
Reference in new issue
Block a user