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

+123
View File
@@ -0,0 +1,123 @@
class_name AccessVerifier
extends RefCounted
const Rules := preload("res://src/core/rules.gd")
const AccessPolicy := preload("res://src/core/access_policy.gd")
const AccessArtifact := preload("res://src/core/access_artifact.gd")
## Evaluates access artifacts against a venue AccessPolicy.
## Pure logic: no UI, no Node dependency, fully headless-testable.
## Result dictionary contract:
## { granted: bool, reasons: Array[String], issues: Array[String],
## decisive_artifacts: Array[String], checked_artifacts: Array[String] }
func _init() -> void:
pass
## Verify a single person's set of artifacts (person_artifacts: Array[AccessArtifact]).
func verify(person_artifacts: Array, policy: AccessPolicy) -> Dictionary:
var reasons: Array[String] = []
var issues: Array[String] = []
var checked: Array[String] = []
for artifact in person_artifacts:
if not (artifact is AccessArtifact):
continue
checked.append(artifact.summary())
if artifact.kind == Rules.AccessKind.RECOGNITION and artifact.status == Rules.ArtifactStatus.VALID:
reasons.append("Recognized by guard")
elif artifact.status == Rules.ArtifactStatus.VALID:
reasons.append("Valid %s" % Rules.ACCESS_KIND_NAMES.get(artifact.kind, "artifact"))
elif artifact.status == Rules.ArtifactStatus.EXPIRED:
issues.append("Expired %s" % Rules.ACCESS_KIND_NAMES.get(artifact.kind, "artifact"))
elif artifact.status == Rules.ArtifactStatus.COUNTERFEIT:
issues.append("Suspected counterfeit %s" % Rules.ACCESS_KIND_NAMES.get(artifact.kind, "artifact"))
elif artifact.status == Rules.ArtifactStatus.MISMATCHED:
issues.append("%s does not match person" % Rules.ACCESS_KIND_NAMES.get(artifact.kind, "artifact"))
elif artifact.status == Rules.ArtifactStatus.AMBIGUOUS:
issues.append("Ambiguous %s" % Rules.ACCESS_KIND_NAMES.get(artifact.kind, "artifact"))
elif artifact.status == Rules.ArtifactStatus.MISSING:
issues.append("Missing %s" % Rules.ACCESS_KIND_NAMES.get(artifact.kind, "artifact"))
# Age check.
if policy.minimum_age > 0 and require_age_info(person_artifacts, policy):
var age := person_age(person_artifacts)
if age < policy.minimum_age:
issues.append("Underage (%d < %d)" % [age, policy.minimum_age])
return _result(false, reasons, issues, checked)
# Wand screening is processed by the interaction layer (false alarms), but a
# policy can hard-require it; a refused screen blocks entry.
if policy.wand_screening_required and not person_screened(person_artifacts):
issues.append("Screening required")
return _result(false, reasons, issues, checked)
# Recognition short-circuits if present and valid.
var recognized := false
for artifact in person_artifacts:
if artifact is AccessArtifact:
if artifact.kind == Rules.AccessKind.RECOGNITION and artifact.status == Rules.ArtifactStatus.VALID:
recognized = true
if recognized:
return _result(true, reasons, issues, checked)
# Every required artifact kind must have a valid match (AND admission),
# unless the policy grants on any single valid artifact (OR admission).
var any_satisfied := false
for kind in policy.required_artifacts.keys():
var satisfied := false
for artifact in person_artifacts:
if artifact is AccessArtifact and artifact.kind == kind:
if artifact.status == Rules.ArtifactStatus.VALID:
satisfied = true
any_satisfied = any_satisfied or satisfied
if not satisfied:
issues.append("Missing valid %s" % Rules.ACCESS_KIND_NAMES.get(kind, "credential"))
if not policy.grant_if_any_required:
return _result(false, reasons, issues, checked)
if policy.grant_if_any_required and not any_satisfied:
return _result(false, reasons, issues, checked)
if policy.require_id:
var id_ok := false
for artifact in person_artifacts:
if artifact is AccessArtifact and artifact.kind == Rules.AccessKind.ID and artifact.status == Rules.ArtifactStatus.VALID:
id_ok = true
if not id_ok:
issues.append("Valid ID required")
return _result(false, reasons, issues, checked)
return _result(true, reasons, issues, checked)
func _result(granted: bool, reasons: Array, issues: Array, checked: Array) -> Dictionary:
return {
"granted": granted,
"reasons": reasons,
"issues": issues,
"decisive_artifacts": reasons.duplicate(),
"checked_artifacts": checked,
}
func require_age_info(person_artifacts: Array, policy: AccessPolicy) -> bool:
if policy.minimum_age <= 0:
return false
var id_present := false
for artifact in person_artifacts:
if artifact is AccessArtifact and artifact.kind == Rules.AccessKind.ID:
id_present = true
return id_present
func person_age(person_artifacts: Array) -> int:
for artifact in person_artifacts:
if artifact is AccessArtifact and artifact.kind == Rules.AccessKind.ID:
var age: Variant = artifact.attributes.get("age", -1)
if age is int and age > 0:
return age
return -1
func person_screened(person_artifacts: Array) -> bool:
for artifact in person_artifacts:
if artifact is AccessArtifact:
if artifact.kind == Rules.AccessKind.CREDENTIAL and artifact.attributes.get("screened", false) == true:
return true
return false