P14-02: Add buildable Mana warp (A07)
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>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
7e6e540837
commit
0cfaec1626
@@ -0,0 +1,187 @@
|
||||
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)
|
||||
@@ -131,7 +131,8 @@ func _run() -> void:
|
||||
)
|
||||
await _test_defense_notice()
|
||||
var palette := _main.find_child("BuildPalette", true, false) as GridContainer
|
||||
_expect(palette.get_child_count() == 23, "all23 facilities including the cleaning room appear in the build palette")
|
||||
_expect(palette.get_child_count() == 24, "all24 facilities including the cleaning room and A07 warp appear in the build palette")
|
||||
_expect((_main.get("build_buttons") as Dictionary).has(&"mana_warp"), "Mana warp is buildable from the ordinary palette")
|
||||
var iconized_build_buttons := 0
|
||||
for build_button_value: Variant in (_main.get("build_buttons") as Dictionary).values():
|
||||
if (build_button_value as Button).icon != null:
|
||||
|
||||
Reference in New Issue
Block a user