The FRONT Mana warp is guest-only: its connectors carry the "!staff" access, which path access checks treat as open to everyone except staff. A new 2x2 BACK service warp links only to other service warps for staff, with A07 values. Registry and palette now hold 25 definitions. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
199 lines
11 KiB
GDScript
199 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")
|
|
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)
|