241 lines
10 KiB
GDScript
241 lines
10 KiB
GDScript
extends SceneTree
|
|
|
|
var _checks := 0
|
|
var _failures := 0
|
|
var _clock: Variant
|
|
var _state: Variant
|
|
var _runtime: Variant
|
|
var _facility_id: StringName
|
|
var _facility_inventory_id: StringName
|
|
|
|
|
|
func _initialize() -> void:
|
|
call_deferred("_run")
|
|
|
|
|
|
func _run() -> void:
|
|
print("S3: integrated Guest and Staff runtime cycle")
|
|
_reset_runtime()
|
|
_build_cafeteria()
|
|
_register_staff_and_run_task()
|
|
_run_autonomous_guest_service_with_replay()
|
|
_run_long_horizon_replay()
|
|
_cleanup()
|
|
if _failures == 0:
|
|
print("PASS: %d checks" % _checks)
|
|
else:
|
|
push_error("FAIL: %d of %d checks failed" % [_failures, _checks])
|
|
quit(_failures)
|
|
|
|
|
|
func _reset_runtime() -> void:
|
|
_cleanup()
|
|
_clock = load("res://autoload/game_clock.gd").new()
|
|
_state = load("res://autoload/game_state.gd").new()
|
|
_runtime = load("res://autoload/simulation_runtime.gd").new()
|
|
_clock.reset(1, 6, 0)
|
|
_state.reset()
|
|
var report: Dictionary = _runtime.initialize(_clock, _state, 3303)
|
|
_clock.set_paused(true)
|
|
_expect(report[&"valid"], "runtime initializes S3 actor services")
|
|
_expect(_runtime.guest_service.get_definitions().size() == 35, "runtime preserves three legacy archetypes and registers 32 MVP catalog archetypes")
|
|
_expect(_runtime.guest_service.current_minute == _clock.absolute_minute, "Guest cadence starts aligned to the kernel")
|
|
_expect(_runtime.staff_service.current_minute == _clock.absolute_minute, "Staff cadence starts aligned to the kernel")
|
|
|
|
|
|
func _cleanup() -> void:
|
|
if _runtime != null:
|
|
_runtime.free()
|
|
if _clock != null:
|
|
_clock.free()
|
|
if _state != null:
|
|
_state.free()
|
|
_runtime = null
|
|
_clock = null
|
|
_state = null
|
|
|
|
|
|
func _build_cafeteria() -> void:
|
|
var build = _runtime.submit_command(
|
|
&"build_facility",
|
|
{&"definition_id": &"cafeteria", &"origin": Vector3i(-5, 0, WorldService.LAYER_FRONT)},
|
|
&"player", &"", 50, -1, &"s3.runtime.build"
|
|
)
|
|
_expect(build.is_success(), "cafeteria construction command succeeds")
|
|
_clock.advance_minutes(180)
|
|
var facilities: Array[Dictionary] = _runtime.construction_service.get_facilities()
|
|
_expect(facilities.size() == 1, "construction completes before actor integration")
|
|
_facility_id = StringName(facilities[0][&"instance_id"])
|
|
var facility = _runtime.facility_service.get_instance(_facility_id)
|
|
_facility_inventory_id = facility.local_inventory_id
|
|
_expect(facility.state == FacilityInstance.State.NO_STAFF, "unassigned S3 facility starts in NO_STAFF")
|
|
|
|
|
|
func _register_staff_and_run_task() -> void:
|
|
var common_options := {
|
|
&"current_cell": Vector3i(0, 0, WorldService.LAYER_FRONT),
|
|
&"state": StaffRuntime.State.OFF_DUTY,
|
|
&"permitted_task_tags": [&"service", &"work"],
|
|
&"equipment_tags": [&"uniform"],
|
|
}
|
|
var cook_options: Dictionary = common_options.duplicate(true)
|
|
cook_options[&"role_tags"] = [&"cook"]
|
|
cook_options[&"skills"] = {&"cooking": 80, &"operation": 30}
|
|
var server_options: Dictionary = common_options.duplicate(true)
|
|
server_options[&"role_tags"] = [&"server"]
|
|
server_options[&"skills"] = {&"service": 75, &"operation": 20}
|
|
var cook = _runtime.submit_command(
|
|
&"register_staff",
|
|
{&"staff_id": &"cook_aya", &"role_id": &"cook", &"options": cook_options},
|
|
&"manager", &"cook_aya", 20, -1, &"s3.runtime.staff.cook"
|
|
)
|
|
var server = _runtime.submit_command(
|
|
&"register_staff",
|
|
{&"staff_id": &"server_ren", &"role_id": &"server", &"options": server_options},
|
|
&"manager", &"server_ren", 20, -1, &"s3.runtime.staff.server"
|
|
)
|
|
_expect(cook.is_success() and server.is_success(), "two StaffRuntime records register through commands")
|
|
var start_minute := posmod(_clock.absolute_minute, 1_440)
|
|
for staff_id: StringName in [&"cook_aya", &"server_ren"]:
|
|
var shift = _runtime.submit_command(
|
|
&"create_staff_shift",
|
|
{
|
|
&"staff_id": staff_id,
|
|
&"template_id": &"FLEX",
|
|
&"start_day": _clock.absolute_minute / 1_440,
|
|
&"start_minute_of_day": start_minute,
|
|
&"end_minute_of_day": start_minute + 600,
|
|
&"facility_id": _facility_id,
|
|
},
|
|
&"manager", staff_id, 20, -1, StringName("s3.runtime.shift.%s" % staff_id)
|
|
)
|
|
_expect(shift.is_success(), "Flex Shift registers for %s" % staff_id)
|
|
_clock.advance_one_minute()
|
|
var facility = _runtime.facility_service.get_instance(_facility_id)
|
|
_expect(facility.active_staff_ids == [&"cook_aya", &"server_ren"], "on-duty Staff project into FacilityService in stable ID order")
|
|
_expect(facility.state == FacilityInstance.State.OPEN, "required S3 staffing opens the cafeteria")
|
|
|
|
var tool = _runtime.submit_command(
|
|
&"add_inventory_stack",
|
|
{
|
|
&"owner_inventory_id": _facility_inventory_id,
|
|
&"item_id": &"tool/cleaning_kit",
|
|
&"quantity": 1,
|
|
},
|
|
&"manager", _facility_id, 10, -1, &"s3.runtime.tool"
|
|
)
|
|
_expect(tool.is_success(), "facility receives a reservable Staff task tool")
|
|
var task_result = _runtime.submit_command(
|
|
&"create_staff_task",
|
|
{
|
|
&"task_id": &"clean_cafeteria",
|
|
&"task_type": &"clean",
|
|
&"priority": 70,
|
|
&"options": {
|
|
&"target_entity_id": _facility_id,
|
|
&"target_cell": Vector3i(-1, 0, WorldService.LAYER_FRONT),
|
|
&"required_role_tags": [&"server"],
|
|
&"required_skill_min": {&"service": 10},
|
|
&"required_items": {&"tool/cleaning_kit": 1},
|
|
&"task_tags": [&"service"],
|
|
&"estimated_minutes": 2,
|
|
},
|
|
},
|
|
&"manager", _facility_id, 10, -1, &"s3.runtime.task"
|
|
)
|
|
_expect(task_result.is_success(), "Staff task enters the runtime dispatcher")
|
|
_clock.advance_minutes(8)
|
|
var task = _runtime.staff_service.get_task(&"clean_cafeteria")
|
|
_expect(task.state == StaffTask.State.COMPLETED, "runtime automatically dispatches, begins and completes the Staff task")
|
|
_expect(_runtime.inventory_service.available_quantity(_facility_inventory_id, &"tool/cleaning_kit") == 1, "completed task releases its atomic tool Reservation")
|
|
_expect(_runtime.staff_service.validate_invariants()[&"ok"], "integrated Staff task graph preserves bidirectional invariants")
|
|
|
|
|
|
func _run_autonomous_guest_service_with_replay() -> void:
|
|
var ingredient = _runtime.submit_command(
|
|
&"add_inventory_stack",
|
|
{
|
|
&"owner_inventory_id": _facility_inventory_id,
|
|
&"item_id": &"ingredient/basic_meal",
|
|
&"quantity": 1,
|
|
&"quality": 80,
|
|
},
|
|
&"manager", _facility_id, 10, -1, &"s3.runtime.ingredient"
|
|
)
|
|
_expect(ingredient.is_success(), "one service recipe is stocked")
|
|
var spawn = _runtime.submit_command(
|
|
&"spawn_guest",
|
|
{
|
|
&"guest_id": &"guest_mio",
|
|
&"guest_type_id": &"traveler",
|
|
&"gold": 150,
|
|
&"options": {
|
|
&"current_cell": Vector3i(0, 0, WorldService.LAYER_FRONT),
|
|
&"needs": {&"hunger": 90, &"sleep": 20, &"hygiene": 20, &"medical": 0, &"leisure": 20, &"intimacy": 0},
|
|
&"planned_exit_at": _clock.absolute_minute + 240,
|
|
},
|
|
},
|
|
&"world", &"guest_mio", 10, -1, &"s3.runtime.guest"
|
|
)
|
|
_expect(spawn.is_success(), "spawn_guest atomically creates GuestRuntime and ledger wallet")
|
|
_expect(_runtime.economy_service.get_balance(&"GUEST_WALLET:guest_mio") == 150, "Guest wallet is authoritative in EconomyService")
|
|
|
|
var active_service: ServiceInstance = null
|
|
for _minute in range(10):
|
|
_clock.advance_one_minute()
|
|
var active: Array = _runtime.facility_service.get_active_services(_facility_id)
|
|
if not active.is_empty():
|
|
active_service = active[0]
|
|
break
|
|
_expect(active_service != null, "autonomous Guest reaches the facility queue and starts service")
|
|
if active_service == null:
|
|
return
|
|
var guest = _runtime.guest_service.get_guest(&"guest_mio")
|
|
_expect(guest.state == GuestRuntime.State.RECEIVING_SERVICE, "Facility service start atomically updates Guest state")
|
|
_expect(guest.current_service_id == active_service.service_id, "Guest and Facility reference the same active service")
|
|
var mid_snapshot: Dictionary = _runtime.create_snapshot()
|
|
var mid_digest := SimulationDigest.compute(mid_snapshot)
|
|
var duration := active_service.duration_minutes
|
|
_clock.advance_minutes(duration)
|
|
var completed = _runtime.facility_service.get_service(active_service.service_id)
|
|
guest = _runtime.guest_service.get_guest(&"guest_mio")
|
|
_expect(completed != null and completed.state == ServiceInstance.State.COMPLETED, "service completes after its immutable duration")
|
|
_expect(guest != null and guest.satisfaction_samples == 1, "completion records exactly one Guest satisfaction sample")
|
|
_expect(guest != null and int(guest.needs[&"hunger"]) < 40, "cafeteria completion applies its Need relief")
|
|
_expect(_runtime.economy_service.get_balance(&"GUEST_WALLET:guest_mio") == 115, "Guest pays the quoted service price once")
|
|
_expect(_runtime.economy_service.get_balance(&"PLAYER_CASH") == 4235, "service revenue reaches authoritative player cash")
|
|
_expect(_runtime.inventory_service.total_quantity(_facility_inventory_id, &"ingredient/basic_meal") == 0, "service consumes exactly one reserved recipe input")
|
|
var final_digest: String = _runtime.latest_digest
|
|
|
|
_expect(_runtime.restore_snapshot(mid_snapshot), "mid-service S3 snapshot restores")
|
|
_expect(_runtime.latest_digest == mid_digest, "S3 restore reproduces the exact mid-service digest")
|
|
_clock.advance_minutes(duration)
|
|
_expect(_runtime.latest_digest == final_digest, "replayed Guest, Staff, ledger and event transitions reproduce exactly")
|
|
_expect(_runtime.economy_service.get_entries().filter(
|
|
func(entry: LedgerEntry) -> bool: return entry.category == &"SERVICE_SALE"
|
|
).size() == 1, "replay records the service sale exactly once")
|
|
|
|
|
|
func _run_long_horizon_replay() -> void:
|
|
var horizon_snapshot: Dictionary = _runtime.create_snapshot()
|
|
_clock.advance_minutes(1_440)
|
|
var first_digest: String = _runtime.latest_digest
|
|
var first_snapshot: Dictionary = _runtime.create_snapshot()
|
|
_expect(_runtime.guest_service.validate_invariants()[&"valid"], "24-hour Guest graph has no orphan or illegal state")
|
|
_expect(_runtime.staff_service.validate_invariants()[&"ok"], "24-hour Staff graph has no orphan reservation")
|
|
_expect(_runtime.inventory_service.validate_invariants()[&"ok"], "24-hour actor simulation preserves Inventory invariants")
|
|
_expect(_runtime.economy_service.validate_ledger()[&"ok"], "24-hour actor simulation keeps the ledger balanced")
|
|
_expect(_runtime.restore_snapshot(horizon_snapshot), "long-horizon starting point restores")
|
|
_clock.advance_minutes(1_440)
|
|
_expect(_runtime.latest_digest == first_digest, "24-hour actor simulation is deterministic after restore")
|
|
_expect(_runtime.create_snapshot() == first_snapshot, "24-hour replay reproduces the complete authoritative snapshot")
|
|
|
|
|
|
func _expect(condition: bool, label: String) -> void:
|
|
_checks += 1
|
|
if condition:
|
|
print(" ✓ %s" % label)
|
|
return
|
|
_failures += 1
|
|
push_error(" ✗ %s" % label)
|