2x2 FRONT warp at 3000G/30 materials/360 min, 15G/day and 1 Mana upkeep. Every completed warp pair is linked by one path connector, so crossing is a single step; a 3-Mana fare per rider is charged once in the step's tick. Warps close while either end is unpowered, the reserve is empty or below one fare, and blocked arrivals offer no step. Saves validate the derived links. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
188 lines
9.4 KiB
GDScript
188 lines
9.4 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": 6000, &"materials": 40}}).is_success(), "fund two 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")
|
|
|
|
# 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)
|