Implement prototype: venue-driven simulation core, scenarios, tests, and playable entrance scene
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:
zwitschi committed 2026-10-31 11:37:54 +01:00
1 parent e23f284ad4
commit 439e0a4281
45 files changed
+4368 -7

No files matched your search

+83
View File
@@ -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