Files
TakeshiandClaude Opus 5.5 0f23d34e9f Path: Apply "!id" access to layer-crossing and no-route diagnostics
A stopped guest warp now reports CONNECTOR_UNAVAILABLE instead of
GOAL_UNREACHABLE (P14-02 review r2 follow-up).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 15:12:47 +09:00

200 lines
11 KiB
GDScript

extends SceneTree
## A07 Mana warp through ordinary construction, natural guests and the reserve.
## "-- writer" (default) runs the mechanics and saves mid-operation; "-- reader"
## restores that save in a new process and must reproduce the writer exactly.
const Warp = preload("res://scripts/transport/warp_service.gd")
const ManaOutage = preload("res://tests/helpers/mana_outage.gd")
const SAVE_PATH := "user://saves/p14_02_warp_new_pid"
const EXPECTED_PATH := "user://saves/p14_02_warp_new_pid_expected.json"
const CONTINUE_MINUTES := 120
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("Warp runtime %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, 8808)
_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("warp.test.%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 _warp_connectors() -> Array[StringName]:
return _runtime.path_service.get_connector_ids_with_prefix(Warp.PREFIX)
func _warp_events() -> Array[Dictionary]:
var rows: Array[Dictionary] = []
for event: SimulationDomainEvent in _runtime.domain_events.recent_events():
if event.event_type == &"WARP_TRAVELED":
rows.append({&"minute": event.absolute_minute, &"mana": int(event.payload[&"mana"])})
return rows
func _write() -> void:
_expect(_cmd(&"change_resources", {&"deltas": {&"gold": 12000, &"materials": 100}}).is_success(), "fund four warps for the fixture")
var gold_before: int = _state.get_resource(&"gold")
if not _build(&"mana_warp", Vector3i(0, 0, 0)) or not _build(&"reception", Vector3i(2, 0, 0)):
return
_expect(gold_before - int(_state.get_resource(&"gold")) >= 3000, "warp construction charges its approved 3000G")
_clock.advance_minutes(359)
_expect(_warp_connectors().is_empty(), "a single warp has no destination")
_clock.advance_minutes(1)
if not _build(&"mana_warp", Vector3i(0, 2, 0)) or not _build(&"standard_room", Vector3i(2, 2, 0)):
return
_clock.advance_minutes(360)
_expect(_runtime.path_service.find_path(Vector3i(0, 0, 0), Vector3i(0, 2, 0)).is_empty(), "new warp waits for its first power evaluation")
_clock.advance_minutes(10 - int(_clock.absolute_minute) % 10)
var ids := _warp_connectors()
_expect(ids.size() == 1, "two completed warps are linked by one connector")
if ids.size() != 1:
return
var connector: Dictionary = _runtime.path_service.get_connector(ids[0])
_expect(connector[&"from"] == Vector3i(0, 0, 0) and connector[&"to"] == Vector3i(0, 2, 0) and connector[&"operational_state"] == PathService.CONNECTOR_OPERATIONAL, "connector joins both warp origins and is operational")
_expect(_runtime.path_service.find_path(Vector3i(0, 0, 0), Vector3i(0, 2, 0)) == [Vector3i(0, 0, 0), Vector3i(0, 2, 0)], "path crosses floors in one instant step")
_expect(_runtime.mana_network_service.get_total_consumer_demand() >= 2, "each warp registers 1 Mana upkeep")
_expect(_runtime.path_service.find_path(Vector3i(0, 0, 0), Vector3i(0, 2, 0), [WorldService.LAYER_FRONT, WorldService.LAYER_BACK], [&"staff"]).is_empty(), "staff routes never use the guest warp (A08)")
_expect(_build(&"service_warp", Vector3i(0, 0, 1)), "build lower BACK service warp")
_clock.advance_minutes(360)
_expect(_build(&"service_warp", Vector3i(0, 2, 1)), "build upper BACK service warp on the completed one")
_clock.advance_minutes(360)
_clock.advance_minutes(10 - int(_clock.absolute_minute) % 10)
_expect(_warp_connectors().size() == 2, "service warps link only to each other, never to guest warps")
var staff_route: Array = _runtime.path_service.find_path(Vector3i(0, 0, 1), Vector3i(0, 2, 1), [WorldService.LAYER_FRONT, WorldService.LAYER_BACK], [&"staff"])
_expect(staff_route == [Vector3i(0, 0, 1), Vector3i(0, 2, 1)], "staff cross floors through the BACK service warp")
_expect(_runtime.path_service.find_path(Vector3i(0, 0, 1), Vector3i(0, 2, 1)).is_empty(), "guests cannot use the service warp")
_expect(_runtime.call(&"_warp_step_connector", Vector3i(0, 0, 1), Vector3i(0, 2, 1), [&"staff"] as Array[StringName]) != &"", "a staff step through the service warp is fare-bearing")
# Outage and an unaffordable fare both remove the warp from routing.
var before_outage: Dictionary = _runtime.create_snapshot()
ManaOutage.hold(_runtime, _state)
_clock.advance_minutes(1)
_expect(_runtime.path_service.get_connector(ids[0])[&"operational_state"] == PathService.CONNECTOR_OUTAGE, "reserve outage stops the warp")
_expect(_runtime.path_service.find_path(Vector3i(0, 0, 0), Vector3i(0, 2, 0)).is_empty(), "stopped warp offers no route")
_expect(_runtime.path_service.find_path_report(Vector3i(0, 0, 0), Vector3i(0, 2, 0))[&"reason"] == PathService.ROUTE_CONNECTOR_UNAVAILABLE, "guest sees the stopped warp as an unavailable connector")
ManaOutage.release(_runtime, _state, Warp.MANA_PER_TRIP - 1)
_clock.advance_minutes(1)
_expect(_runtime.path_service.get_connector(ids[0])[&"operational_state"] == PathService.CONNECTOR_OUTAGE, "reserve below one fare keeps the warp closed")
_expect(_runtime.restore_snapshot(before_outage), "rewind outage drill")
_clock.advance_minutes(1)
var mana_blocked: int = _state.get_resource(&"mana")
_runtime.path_service.set_cell_temporary_block(Vector3i(0, 2, 0), true)
_expect(_runtime.path_service.find_path(Vector3i(0, 0, 0), Vector3i(0, 2, 0)).is_empty()
and _runtime.call(&"_warp_step_connector", Vector3i(0, 0, 0), Vector3i(0, 2, 0), [] as Array[StringName]) == &"", "blocked arrival offers no warp step and no fare")
_expect(_state.get_resource(&"mana") == mana_blocked, "blocked arrival charges nothing")
_expect(_runtime.restore_snapshot(before_outage), "rewind arrival drill")
# Natural guests use the warp; each crossing charges exactly 3 Mana once.
_expect(_cmd(&"hire_staff_candidate", {&"candidate_id": &"staff_akane"}).is_success(), "hire receptionist")
var reception: FacilityInstance = null
for facility: FacilityInstance in _runtime.facility_service.get_instances():
if facility.definition_id == &"reception": reception = facility
_expect(_cmd(&"set_staff_work_assignment", {&"staff_id": &"staff_akane", &"facility_id": reception.instance_id, &"template_id": &"DAY"}).is_success(), "assign receptionist")
var saved := false
var crossings := 0
for minute: int in range(1200):
var mana_before: int = _state.get_resource(&"mana")
var events_before := _warp_events().size()
_clock.advance_minutes(1)
var new_events := _warp_events().size() - events_before
if new_events > 0:
crossings += new_events
var boundary: bool = int(_clock.absolute_minute) % 10 == 0
if not boundary:
_expect(mana_before - int(_state.get_resource(&"mana")) == Warp.MANA_PER_TRIP * new_events, "warp crossing charges 3 Mana per rider")
if not saved:
saved = true
var snapshot: Dictionary = _runtime.create_snapshot()
_expect(SaveService.save_snapshot(SAVE_PATH, snapshot)[&"ok"], "save right after a paid crossing")
_clock.advance_minutes(CONTINUE_MINUTES)
var file := FileAccess.open(EXPECTED_PATH, FileAccess.WRITE)
file.store_string(JSON.stringify({
"writer_pid": OS.get_process_id(),
"saved_events": events_before + new_events,
"mana": _state.get_resource(&"mana"),
"digest": SimulationDigest.compute(_runtime.create_snapshot()),
"events": JSON.stringify(_warp_events()),
}))
file.close()
break
if crossings == 0:
for guest: GuestRuntime in _runtime.guest_service.get_guests():
print("GUEST ", guest.guest_id, " ", guest.state, " ", guest.current_cell)
for facility: FacilityInstance in _runtime.facility_service.get_instances():
print("FAC ", facility.definition_id, " ", facility.state, " ", facility.active_staff_ids)
_expect(crossings > 0, "a natural guest crosses by warp")
_expect(saved, "mid-operation save written")
_expect(_runtime.restore_snapshot(_runtime.create_snapshot()), "warp state round trips")
# Demolishing one end removes the link.
var upper: StringName = &""
for facility: Dictionary in _runtime.construction_service.get_facilities():
if facility[&"definition_id"] == &"mana_warp" and facility[&"origin"] == Vector3i(0, 2, 0):
upper = facility[&"instance_id"]
var occupied := false
for guest: GuestRuntime in _runtime.guest_service.get_guests():
if guest.current_cell.x <= 1 and guest.current_cell.y >= 2: occupied = true
if not occupied:
_expect(_cmd(&"demolish_facility", {&"instance_id": upper}).is_success(), "demolish upper warp")
_expect(_warp_connectors().is_empty(), "demolition removes the warp link")
_expect(_runtime.restore_snapshot(_runtime.create_snapshot()), "post-demolition state restores")
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-operation save")
_expect(_warp_events().size() == int(expected["saved_events"]), "restored crossing history is not replayed")
_clock.advance_minutes(CONTINUE_MINUTES)
_expect(_state.get_resource(&"mana") == int(expected["mana"]), "continued Mana matches: no double fare")
_expect(JSON.stringify(_warp_events()) == String(expected["events"]), "continued crossings match 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)