P06-02: Prove whole-tower stop under natural A06 depletion across PIDs
An ordinary operating tower whose upkeep exceeds the world tree's supply runs dry on its own; for an hour every facility is stopped with no sales, income, production, admissions, warp travel or elevator boarding, and no guest is lost except through ordinary departure. A mid-outage save continues identically in a new process. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
0f23d34e9f
commit
f6f9c9c0d4
@@ -0,0 +1,193 @@
|
||||
extends SceneTree
|
||||
|
||||
## P06-02 whole-tower stop under the A06 finite reserve, reached naturally: the
|
||||
## tower's own upkeep demand exceeds the world tree's 20 Mana per 10 minutes.
|
||||
## "-- writer" (default) builds an ordinary operating tower, waits for natural
|
||||
## depletion, checks every domain during the outage and saves mid-outage;
|
||||
## "-- reader" restores that save in a new process and must match exactly.
|
||||
const SAVE_PATH := "user://saves/p06_02_whole_outage"
|
||||
const EXPECTED_PATH := "user://saves/p06_02_whole_outage_expected.json"
|
||||
const OUTAGE_OBSERVE_MINUTES := 60
|
||||
const CONTINUE_MINUTES := 60
|
||||
var _checks := 0
|
||||
var _failures := 0
|
||||
var _serial := 0
|
||||
var _clock: Variant
|
||||
var _state: Variant
|
||||
var _runtime: Variant
|
||||
|
||||
|
||||
func _initialize() -> void:
|
||||
call_deferred("_run")
|
||||
|
||||
|
||||
func _run() -> void:
|
||||
var mode := "reader" if "reader" in OS.get_cmdline_user_args() else "writer"
|
||||
print("Whole tower outage %s (pid %d)" % [mode, OS.get_process_id()])
|
||||
_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()
|
||||
_runtime.initialize(_clock, _state, 6602)
|
||||
_runtime.configure_save_policy({&"enabled": false, &"daily_checkpoint_enabled": false})
|
||||
_clock.set_paused(true)
|
||||
_expect(bool(_runtime.enable_production_operations().get(&"ok", false)), "natural operations start at Day 1 06:00")
|
||||
if mode == "writer":
|
||||
_write()
|
||||
else:
|
||||
_read()
|
||||
_runtime.free(); _clock.free(); _state.free()
|
||||
print("%s: %d checks" % ["PASS" if _failures == 0 else "FAIL", _checks])
|
||||
quit(_failures)
|
||||
|
||||
|
||||
func _cmd(kind: StringName, payload: Dictionary) -> CommandResult:
|
||||
_serial += 1
|
||||
return _runtime.submit_command(kind, payload, &"player", &"", 0, _clock.absolute_minute, StringName("p0602.%d" % _serial))
|
||||
|
||||
|
||||
func _build(id: StringName, origin: Vector3i) -> bool:
|
||||
var result := _cmd(&"build_facility", {&"definition_id": id, &"origin": origin})
|
||||
_expect(result.is_success(), "build %s at %s %s" % [id, origin, result.message])
|
||||
return result.is_success()
|
||||
|
||||
|
||||
func _facility(id: StringName) -> FacilityInstance:
|
||||
for facility: FacilityInstance in _runtime.facility_service.get_instances():
|
||||
if facility.definition_id == id:
|
||||
return facility
|
||||
return null
|
||||
|
||||
|
||||
## Everything an outage must not create, consume or lose.
|
||||
func _ledger() -> Dictionary:
|
||||
var revenue := 0
|
||||
var wallets := {}
|
||||
for guest: GuestRuntime in _runtime.guest_service.get_guests():
|
||||
wallets[String(guest.guest_id)] = _runtime.economy_service.get_balance(StringName("GUEST_WALLET:%s" % guest.guest_id))
|
||||
for facility: FacilityInstance in _runtime.facility_service.get_instances():
|
||||
revenue += _runtime.economy_service.get_balance(StringName("FACILITY_REVENUE:%s" % facility.instance_id))
|
||||
var meals := 0
|
||||
var stock := 0
|
||||
for facility: FacilityInstance in _runtime.facility_service.get_instances():
|
||||
meals += _runtime.inventory_service.total_quantity(facility.local_inventory_id, SupplyCatalog.BASIC_MEAL)
|
||||
stock += _runtime.inventory_service.total_quantity(facility.local_inventory_id, SupplyCatalog.RAW_FOOD)
|
||||
var guest_ids: Array[String] = []
|
||||
for guest: GuestRuntime in _runtime.guest_service.get_guests():
|
||||
guest_ids.append(String(guest.guest_id))
|
||||
guest_ids.sort()
|
||||
return {"revenue": revenue, "wallets": wallets, "meals": meals, "raw_food": stock,
|
||||
"guests": guest_ids, "cash": _runtime.economy_service.get_balance(&"PLAYER_CASH"), "staff": _runtime.staff_service.get_staff_members().size()}
|
||||
|
||||
|
||||
func _events_since(minute: int, types: Array) -> int:
|
||||
var count := 0
|
||||
for event: SimulationDomainEvent in _runtime.domain_events.recent_events():
|
||||
if event.absolute_minute > minute and event.event_type in types:
|
||||
count += 1
|
||||
return count
|
||||
|
||||
|
||||
func _write() -> void:
|
||||
_expect(_cmd(&"change_resources", {&"deltas": {&"gold": 30000, &"materials": 300}}).is_success(), "fund the mixed tower fixture")
|
||||
# FRONT: warps x-6..-5, massage x-4..-1, elevator stack x0-1, reception/rooms x2-5, cafeteria x6-7.
|
||||
_build(&"guest_elevator", Vector3i(0, 0, 0))
|
||||
_build(&"reception", Vector3i(2, 0, 0))
|
||||
_build(&"cafeteria", Vector3i(6, 0, 0))
|
||||
_build(&"mana_warp", Vector3i(-6, 0, 0))
|
||||
_build(&"massage_room", Vector3i(-4, 0, 0))
|
||||
_build(&"massage_room", Vector3i(-2, 0, 0))
|
||||
_build(&"warehouse", Vector3i(2, 0, 1))
|
||||
_build(&"security_station", Vector3i(-2, 0, 1))
|
||||
_clock.advance_minutes(360)
|
||||
_build(&"guest_elevator", Vector3i(0, 2, 0))
|
||||
_build(&"standard_room", Vector3i(2, 2, 0))
|
||||
_build(&"mana_warp", Vector3i(-6, 2, 0))
|
||||
_clock.advance_minutes(360)
|
||||
var cafe := _facility(&"cafeteria")
|
||||
if cafe != null:
|
||||
var kitchen: Dictionary = load("res://tests/fixtures/required_kitchen_setup.gd").ensure(_runtime, _clock, cafe.instance_id, false)
|
||||
_expect(bool(kitchen.get(&"ok", false)), "kitchen linked to cafeteria")
|
||||
var reception := _facility(&"reception")
|
||||
_expect(reception != null and _cmd(&"hire_staff_candidate", {&"candidate_id": &"staff_akane"}).is_success(), "hire receptionist")
|
||||
if reception != null:
|
||||
_expect(_cmd(&"set_staff_work_assignment", {&"staff_id": &"staff_akane", &"facility_id": reception.instance_id, &"template_id": &"DAY"}).is_success(), "assign receptionist")
|
||||
var demand: int = _runtime.call(&"_mana_reserve_demand")
|
||||
print("FIXTURE demand=%d mana=%d minute=%d" % [demand, _state.get_resource(&"mana"), _clock.absolute_minute])
|
||||
_expect(demand > 20, "ordinary tower upkeep exceeds world tree generation (natural depletion)")
|
||||
|
||||
# Run normally until the reserve reaches 0 through upkeep alone.
|
||||
var depleted_at := -1
|
||||
for minute: int in range(3000):
|
||||
_clock.advance_minutes(1)
|
||||
if _state.get_resource(&"mana") == 0:
|
||||
depleted_at = _clock.absolute_minute
|
||||
break
|
||||
_expect(depleted_at > 0, "reserve reaches 0 naturally")
|
||||
if depleted_at < 0:
|
||||
return
|
||||
var flow: Dictionary = _runtime.mana_reserve.get_last_flow()
|
||||
_expect(int(flow[&"after"]) == 0 and int(flow[&"demand"]) > int(flow[&"generation"]) and int(flow[&"generation"]) == 20, "depletion comes from upkeep exceeding generation, not a test input")
|
||||
_clock.advance_minutes(1)
|
||||
var before := _ledger()
|
||||
var started: int = _clock.absolute_minute
|
||||
_clock.advance_minutes(OUTAGE_OBSERVE_MINUTES)
|
||||
var after := _ledger()
|
||||
_expect(_state.get_resource(&"mana") == 0, "outage persists while upkeep exceeds generation")
|
||||
for facility: FacilityInstance in _runtime.facility_service.get_instances():
|
||||
_expect(not facility.power_available, "%s is stopped" % facility.definition_id)
|
||||
var wallets_kept := true
|
||||
for guest_id: String in after["wallets"]:
|
||||
if before["wallets"].has(guest_id) and before["wallets"][guest_id] != after["wallets"][guest_id]:
|
||||
wallets_kept = false
|
||||
_expect(after["revenue"] == before["revenue"] and wallets_kept, "no unconditional sales during the outage (revenue and every remaining wallet unchanged)")
|
||||
_expect(_runtime.economy_service.get_balance(&"PLAYER_CASH") <= int(before["cash"]), "no income reaches the tower during the outage")
|
||||
_expect(after["meals"] == before["meals"] and after["raw_food"] == before["raw_food"], "no production and no lost stock during the outage")
|
||||
_expect(_events_since(started, [&"GUEST_SPAWNED", &"GUEST_AUTO_ARRIVED"]) == 0, "no admissions during the outage")
|
||||
_expect(_events_since(started, [&"WARP_TRAVELED"]) == 0, "no warp travel during the outage")
|
||||
_expect(_events_since(started, [&"ELEVATOR_BOARDED"]) == 0, "no elevator boarding during the outage")
|
||||
var departed := {}
|
||||
for event: SimulationDomainEvent in _runtime.domain_events.recent_events():
|
||||
if event.absolute_minute > started and event.event_type in [&"GUEST_LEFT", &"GUEST_DESPAWNED"]:
|
||||
departed[String(event.entity_ids[0])] = true
|
||||
for guest_id: String in before["guests"]:
|
||||
_expect(_runtime.guest_service.get_guest(StringName(guest_id)) != null or departed.has(guest_id), "guest %s stays or leaves through an ordinary departure" % guest_id)
|
||||
for trip: Dictionary in _runtime.elevators.get_report()[&"trips"]:
|
||||
_expect(trip[&"state"] == PathService.ELEVATOR_REQUEST_BOARDED, "only held boarded riders remain on a stopped cabin")
|
||||
|
||||
var snapshot: Dictionary = _runtime.create_snapshot()
|
||||
_expect(SaveService.save_snapshot(SAVE_PATH, snapshot)[&"ok"], "save mid-outage")
|
||||
_clock.advance_minutes(CONTINUE_MINUTES)
|
||||
var file := FileAccess.open(EXPECTED_PATH, FileAccess.WRITE)
|
||||
file.store_string(JSON.stringify({
|
||||
"writer_pid": OS.get_process_id(),
|
||||
"ledger": JSON.stringify(_ledger()),
|
||||
"digest": SimulationDigest.compute(_runtime.create_snapshot()),
|
||||
}))
|
||||
file.close()
|
||||
|
||||
|
||||
func _read() -> void:
|
||||
var expected: Variant = JSON.parse_string(FileAccess.get_file_as_string(EXPECTED_PATH))
|
||||
_expect(expected is Dictionary, "reader finds writer record")
|
||||
if not expected is Dictionary:
|
||||
return
|
||||
_expect(int(expected["writer_pid"]) != OS.get_process_id(), "reader runs in a different process")
|
||||
var loaded := SaveService.load_snapshot(SAVE_PATH)
|
||||
_expect(loaded[&"ok"] and _runtime.restore_snapshot(loaded[&"snapshot"]), "reader restores mid-outage save")
|
||||
_expect(_state.get_resource(&"mana") == 0, "restored tower is still stopped")
|
||||
_clock.advance_minutes(CONTINUE_MINUTES)
|
||||
_expect(JSON.stringify(_ledger()) == String(expected["ledger"]), "continued outage ledger matches the writer")
|
||||
_expect(SimulationDigest.compute(_runtime.create_snapshot()) == String(expected["digest"]), "whole continued state digest matches")
|
||||
SaveService.delete_save(SAVE_PATH)
|
||||
DirAccess.remove_absolute(ProjectSettings.globalize_path(EXPECTED_PATH))
|
||||
|
||||
|
||||
func _expect(condition: bool, label: String) -> void:
|
||||
_checks += 1
|
||||
if condition:
|
||||
print(" ✓ %s" % label)
|
||||
else:
|
||||
_failures += 1
|
||||
push_error(" ✗ %s" % label)
|
||||
Reference in New Issue
Block a user