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,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
|
||||
Reference in new issue
Block a user