P06-01: Implement approved A06 finite Mana reserve (MP-01-r02 recommended)
Initial 100, capacity 300, world tree +20 per 10 game minutes. Each boundary pays registered upkeep demand before storing surplus; overflow is discarded and recorded, 0 stops the whole tower. Furnace offers are judged after generation and demand payment. New saves carry mana_reserve and Operations rollback includes it; legacy saves keep a 0..300 amount and reject others. Outage tests that relied on Mana 0 persisting now hold the outage with a disabled demand sink, since the world tree recovers at the next boundary. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
c366012446
commit
ae0f998ff4
@@ -0,0 +1,18 @@
|
||||
extends RefCounted
|
||||
|
||||
## Test-only sustained reserve outage under the A06 finite Mana contract.
|
||||
## Zeroing the reserve alone recovers at the next 10-minute boundary because the
|
||||
## world tree keeps generating. A disabled consumer adds upkeep demand that the
|
||||
## reserve must pay but takes no local network allocation, so the tower stays
|
||||
## at 0 until release() without changing any other consumer's supply.
|
||||
const SINK_ID := &"test.mana_outage_sink"
|
||||
const SINK_DEMAND := 1000
|
||||
|
||||
static func hold(runtime: Variant, state: Variant) -> void:
|
||||
if runtime.mana_network_service.get_node(SINK_ID).is_empty():
|
||||
runtime.mana_network_service.register_consumer(SINK_ID, SINK_DEMAND, ManaNetworkService.ConsumerPriority.NORMAL, false)
|
||||
state.set_resource(&"mana", 0)
|
||||
|
||||
static func release(runtime: Variant, state: Variant, amount := 100) -> void:
|
||||
runtime.mana_network_service.unregister_node(SINK_ID)
|
||||
state.set_resource(&"mana", amount)
|
||||
@@ -1,4 +1,5 @@
|
||||
extends SceneTree
|
||||
const ManaOutage = preload("res://tests/helpers/mana_outage.gd")
|
||||
var checks := 0
|
||||
var failures := 0
|
||||
var runtime: Variant
|
||||
@@ -59,7 +60,7 @@ func run() -> void:
|
||||
var mid: Dictionary=runtime.create_snapshot()
|
||||
expect(runtime.restore_snapshot(mid), "real guest and in-progress premium service restore together")
|
||||
var elapsed: int=runtime.facility_service.get_service(service_id).elapsed_minutes
|
||||
state.set_resource(&"mana",0)
|
||||
ManaOutage.hold(runtime, state)
|
||||
clock.advance_minutes(15)
|
||||
var paused: ServiceInstance=runtime.facility_service.get_service(service_id)
|
||||
expect(paused.state==ServiceInstance.State.PAUSED and paused.elapsed_minutes==elapsed, "zero mana pauses actual guest lodging without consuming service time")
|
||||
@@ -70,7 +71,7 @@ func run() -> void:
|
||||
var saved := SaveService.load_snapshot(disk_path)
|
||||
expect(saved[&"ok"] and runtime.restore_snapshot(saved[&"snapshot"]), "restore real guest, paused service and empty mana from disk")
|
||||
SaveService.delete_save(disk_path)
|
||||
state.set_resource(&"mana",100)
|
||||
ManaOutage.release(runtime, state)
|
||||
|
||||
for minute in 370:
|
||||
clock.advance_one_minute()
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
extends SceneTree
|
||||
const Cycle = preload("res://scripts/mana/mana_reserve_cycle.gd")
|
||||
var checks := 0
|
||||
var failures := 0
|
||||
func expect(ok: bool, message: String) -> void:
|
||||
checks += 1
|
||||
if not ok:
|
||||
failures += 1
|
||||
push_error(message)
|
||||
|
||||
func run(before: int, demand: int, minutes: int) -> int:
|
||||
var cycle = Cycle.new()
|
||||
cycle.start_at(360)
|
||||
var amount := before
|
||||
for minute: int in range(361, 360 + minutes + 1):
|
||||
var result: Dictionary = cycle.advance_to(minute, amount, demand)
|
||||
amount = int(result[&"after"])
|
||||
return amount
|
||||
|
||||
func _initialize() -> void:
|
||||
expect(Cycle.INITIAL_MANA == 100 and Cycle.CAPACITY == 300 and Cycle.WORLD_TREE_GENERATION == 20, "approved A06 values")
|
||||
# Proposal comparison table (recommended column), F=0, start 360.
|
||||
expect(run(100, 2, 120) == 300, "low load fills to capacity")
|
||||
expect(run(100, 16, 120) == 148, "normal load 100+12*(20-16)")
|
||||
expect(run(100, 63, 120) == 0, "overload depletes")
|
||||
expect(run(0, 16, 10) == 4, "zero reserve recovers on next boundary")
|
||||
expect(run(300, 2, 10) == 300, "full reserve keeps capacity")
|
||||
var cycle = Cycle.new()
|
||||
expect(not cycle.advance_to(370, 100, 16)[&"ok"], "unstarted cycle rejected")
|
||||
expect(cycle.start_at(365) and cycle.get_minute() == 360, "start floors to boundary")
|
||||
expect(not cycle.advance_to(369, 100, 16)[&"processed"], "non-boundary minute is a no-op")
|
||||
var flow: Dictionary = cycle.advance_to(370, 100, 63)[&"flow"]
|
||||
expect(flow[&"after"] == 57 and flow[&"consumed"] == 63 and flow[&"overflow"] == 0, "demand paid from generation and reserve")
|
||||
var saved: Dictionary = cycle.create_snapshot()
|
||||
expect(not cycle.advance_to(370, 57, 63)[&"processed"] and cycle.create_snapshot() == saved, "same boundary never consumed twice")
|
||||
expect(not cycle.advance_to(360, 57, 63)[&"ok"], "time reversal rejected")
|
||||
expect(not cycle.advance_to(380, 301, 0)[&"ok"] and cycle.create_snapshot() == saved, "over-capacity reserve rejected without change")
|
||||
var skipped: Dictionary = cycle.advance_to(400, 57, 63)
|
||||
expect(skipped[&"boundaries"] == 3 and skipped[&"after"] == 0 and skipped[&"flow"][&"shortage"] == 43, "skipped boundaries settle in order")
|
||||
var restored = Cycle.new()
|
||||
expect(restored.restore_snapshot(cycle.create_snapshot()) and restored.create_snapshot() == cycle.create_snapshot(), "snapshot round trip")
|
||||
var corrupt: Dictionary = cycle.create_snapshot()
|
||||
corrupt[&"last_flow"][&"after"] += 1
|
||||
var untouched = Cycle.new()
|
||||
expect(not untouched.restore_snapshot(corrupt) and untouched.get_minute() == -1, "tampered flow rejected atomically")
|
||||
corrupt = cycle.create_snapshot()
|
||||
corrupt[&"capacity"] = 600
|
||||
expect(not untouched.restore_snapshot(corrupt), "foreign policy capacity rejected")
|
||||
corrupt = cycle.create_snapshot()
|
||||
corrupt[&"minute"] = 405
|
||||
expect(not untouched.restore_snapshot(corrupt), "off-boundary minute rejected")
|
||||
# Furnace is judged after natural generation and demand payment.
|
||||
expect(not Cycle.furnace_offer_accepted(200, 16), "B=200 base_after 204 free 96 rejected")
|
||||
expect(Cycle.furnace_offer_accepted(196, 16), "B=196 base_after 200 free 100 accepted")
|
||||
expect(not Cycle.furnace_offer_accepted(300, 16), "B=300 full rejected")
|
||||
expect(Cycle.plan(196, 16, 100, 360, 370)[&"after"] == 300, "accepted furnace offer fills exactly")
|
||||
expect(Cycle.amount_is_valid(0) and Cycle.amount_is_valid(300) and not Cycle.amount_is_valid(301) and not Cycle.amount_is_valid(-1) and not Cycle.amount_is_valid(1.5), "legacy amount bounds")
|
||||
print("%s: %d checks" % ["PASS" if failures == 0 else "FAIL", checks])
|
||||
quit(failures)
|
||||
@@ -0,0 +1 @@
|
||||
uid://de3w8hfsk83nt
|
||||
@@ -0,0 +1,109 @@
|
||||
extends SceneTree
|
||||
|
||||
const Cycle = preload("res://scripts/mana/mana_reserve_cycle.gd")
|
||||
var _checks := 0
|
||||
var _failures := 0
|
||||
var _clock: Variant
|
||||
var _state: Variant
|
||||
var _runtime: Variant
|
||||
|
||||
|
||||
func _initialize() -> void:
|
||||
call_deferred("_run")
|
||||
|
||||
|
||||
func _run() -> void:
|
||||
print("S2: runtime finite Mana reserve (A06)")
|
||||
_reset_runtime()
|
||||
_test_generation_and_demand()
|
||||
_test_save_round_trip_and_migration()
|
||||
_test_depletion_stops_tower()
|
||||
_cleanup()
|
||||
if _failures == 0:
|
||||
print("PASS: %d checks" % _checks)
|
||||
else:
|
||||
push_error("FAIL: %d of %d checks failed" % [_failures, _checks])
|
||||
quit(_failures)
|
||||
|
||||
|
||||
func _reset_runtime() -> void:
|
||||
_cleanup()
|
||||
_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()
|
||||
var report: Dictionary = _runtime.initialize(_clock, _state, 3303)
|
||||
_clock.set_paused(true)
|
||||
_expect(report[&"valid"], "runtime initializes")
|
||||
_expect(_state.get_resource(&"mana") == Cycle.INITIAL_MANA, "new game starts with approved 100 Mana")
|
||||
_expect(_runtime.mana_reserve.get_minute() == _clock.absolute_minute, "reserve starts on the opening boundary")
|
||||
|
||||
|
||||
func _cleanup() -> void:
|
||||
for node: Variant in [_runtime, _clock, _state]:
|
||||
if node != null:
|
||||
node.free()
|
||||
_runtime = null
|
||||
_clock = null
|
||||
_state = null
|
||||
|
||||
|
||||
func _test_generation_and_demand() -> void:
|
||||
_clock.advance_minutes(10)
|
||||
var demand: int = _runtime.mana_network_service.get_total_consumer_demand()
|
||||
var flow: Dictionary = _runtime.mana_reserve.get_last_flow()
|
||||
_expect(flow[&"generation"] == 20 and flow[&"demand"] == demand, "boundary uses world tree 20 and registered demand")
|
||||
_expect(_state.get_resource(&"mana") == mini(Cycle.CAPACITY, 100 + 20 - demand), "one boundary settles exactly once")
|
||||
_clock.advance_minutes(9)
|
||||
var mid: int = _state.get_resource(&"mana")
|
||||
_expect(mid == int(flow[&"after"]), "no settlement between boundaries")
|
||||
_clock.advance_minutes(200)
|
||||
_expect(_state.get_resource(&"mana") <= Cycle.CAPACITY, "reserve never exceeds capacity")
|
||||
|
||||
|
||||
func _test_save_round_trip_and_migration() -> void:
|
||||
var snapshot: Dictionary = _runtime.create_snapshot()
|
||||
var runtime_data: Dictionary = snapshot[&"systems"][&"runtime"]
|
||||
_expect(runtime_data.has(&"mana_reserve"), "new saves carry the reserve contract")
|
||||
_expect(_runtime.restore_snapshot(snapshot.duplicate(true)), "current save restores")
|
||||
_expect(_runtime.create_snapshot() == snapshot, "restored reserve state is identical")
|
||||
|
||||
var legacy: Dictionary = snapshot.duplicate(true)
|
||||
legacy[&"systems"][&"runtime"].erase(&"mana_reserve")
|
||||
legacy[&"game_state"][&"resources"][&"mana"] = 0
|
||||
_expect(_runtime.restore_snapshot(legacy), "legacy save with explicit 0 migrates")
|
||||
_expect(_state.get_resource(&"mana") == 0 and _runtime.mana_reserve.get_last_flow().is_empty(), "explicit 0 stays 0 with fresh reserve")
|
||||
|
||||
var over: Dictionary = snapshot.duplicate(true)
|
||||
over[&"systems"][&"runtime"].erase(&"mana_reserve")
|
||||
over[&"game_state"][&"resources"][&"mana"] = Cycle.CAPACITY + 1
|
||||
var before: Dictionary = _runtime.create_snapshot()
|
||||
_expect(not _runtime.restore_snapshot(over), "legacy amount over capacity is rejected")
|
||||
_expect(_runtime.create_snapshot() == before, "rejected migration keeps live state")
|
||||
|
||||
var tampered: Dictionary = snapshot.duplicate(true)
|
||||
tampered[&"systems"][&"runtime"][&"mana_reserve"][&"last_flow"][&"after"] += 1
|
||||
_expect(not _runtime.restore_snapshot(tampered), "tampered reserve flow is rejected")
|
||||
_expect(_runtime.restore_snapshot(snapshot.duplicate(true)), "original save still restores")
|
||||
|
||||
|
||||
func _test_depletion_stops_tower() -> void:
|
||||
_state.set_resource(&"mana", 0)
|
||||
var demand: int = _runtime.mana_network_service.get_total_consumer_demand()
|
||||
_runtime.mana_network_service.register_consumer(&"test.mana_sink", 40)
|
||||
_clock.advance_minutes(10 - (_clock.absolute_minute % 10))
|
||||
_expect(_state.get_resource(&"mana") == 0, "demand above generation keeps the reserve at 0")
|
||||
_expect(int(_runtime.mana_reserve.get_last_flow()[&"shortage"]) == demand + 40 - 20, "shortage is recorded")
|
||||
_runtime.mana_network_service.unregister_node(&"test.mana_sink")
|
||||
_clock.advance_minutes(10)
|
||||
_expect(_state.get_resource(&"mana") == maxi(0, 20 - demand), "world tree recovers the reserve at the next boundary")
|
||||
|
||||
|
||||
func _expect(condition: bool, label: String) -> void:
|
||||
_checks += 1
|
||||
if condition:
|
||||
print(" ✓ %s" % label)
|
||||
else:
|
||||
_failures += 1
|
||||
push_error(" ✗ %s" % label)
|
||||
@@ -1,4 +1,5 @@
|
||||
extends SceneTree
|
||||
const ManaOutage = preload("res://tests/helpers/mana_outage.gd")
|
||||
var checks := 0
|
||||
var failures := 0
|
||||
var runtime: Variant
|
||||
@@ -50,7 +51,7 @@ func run() -> void:
|
||||
expect(patient.current_cell == medic.current_cell and patient.health == 0 and runtime.elevators.get_report()[&"trips"].is_empty(), "emergency unloads both at the same floor without healing")
|
||||
expect(runtime.restore_snapshot(runtime.create_snapshot()), "emergency pair release remains saveable")
|
||||
expect(runtime.restore_snapshot(checkpoint), "restore boarded rescue pair for tower-wide depletion")
|
||||
state.set_resource(&"mana", 0)
|
||||
ManaOutage.hold(runtime, state)
|
||||
clock.advance_minutes(1)
|
||||
patient = runtime.staff_service.get_staff(&"patient"); medic = runtime.staff_service.get_staff(&"medic")
|
||||
expect(patient.current_cell == medic.current_cell and patient.health == 0 and runtime.elevators.get_report()[&"trips"].is_empty(), "reserve depletion lands both rescue passengers together without healing")
|
||||
@@ -60,7 +61,7 @@ func run() -> void:
|
||||
expect(runtime.staff_service.get_staff(&"patient").health == 0 and runtime.staff_service.get_staff(&"patient").current_cell == landing, "stopped clinic cannot heal or teleport the landed patient")
|
||||
var outage: Dictionary = runtime.create_snapshot()
|
||||
expect(runtime.staff_service.validate_invariants()[&"ok"] and runtime.restore_snapshot(outage) and runtime.create_snapshot() == outage, "depleted rescue ownership survives exact complete save restoration")
|
||||
state.set_resource(&"mana", 100)
|
||||
ManaOutage.release(runtime, state)
|
||||
clock.advance_minutes(100)
|
||||
expect(runtime.staff_service.get_staff(&"patient").health == 100, "restored power resumes rescue and treatment for the same patient")
|
||||
expect(runtime.staff_service.validate_invariants()[&"ok"] and runtime.restore_snapshot(runtime.create_snapshot()), "recovered rescue has no orphan ownership and remains saveable")
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
extends SceneTree
|
||||
const ManaOutage = preload("res://tests/helpers/mana_outage.gd")
|
||||
var checks := 0
|
||||
var failures := 0
|
||||
var runtime: Variant
|
||||
@@ -40,11 +41,11 @@ func run() -> void:
|
||||
expect(runtime.create_snapshot()==arrived,"staff movement and recovery replay exactly")
|
||||
worker=runtime.staff_service.get_staff(&"dorm_sleeper")
|
||||
var health:=worker.health
|
||||
state.set_resource(&"mana",0);clock.advance_minutes(30)
|
||||
ManaOutage.hold(runtime, state);clock.advance_minutes(30)
|
||||
expect(worker.health==health and not runtime.get_staff_rest_report(worker.staff_id).get(&"resting",false),"unpowered dormitory stops recovery")
|
||||
expect(runtime.restore_snapshot(runtime.create_snapshot()),"stopped dormitory and staff position restore")
|
||||
worker=runtime.staff_service.get_staff(&"dorm_sleeper")
|
||||
state.set_resource(&"mana",100);clock.advance_minutes(40)
|
||||
ManaOutage.release(runtime, state);clock.advance_minutes(40)
|
||||
expect(worker.health>health,"power recovery resumes rest")
|
||||
runtime.facility_service.set_operational_status(room_id,{&"operator_open":false},clock.absolute_minute)
|
||||
health=worker.health;clock.advance_minutes(30)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
extends SceneTree
|
||||
const ManaOutage = preload("res://tests/helpers/mana_outage.gd")
|
||||
|
||||
## Real construction, hired porter and purchased stock; failure injection is
|
||||
## limited to route closures, capacity pressure and expired cargo.
|
||||
@@ -363,12 +364,12 @@ func _finish() -> void:
|
||||
|
||||
func _test_reserve_outage() -> void:
|
||||
_expect(_runtime.restore_snapshot(_loaded), "restore physically loaded cargo for reserve outage")
|
||||
_state.set_resource(&"mana", 0)
|
||||
ManaOutage.hold(_runtime, _state)
|
||||
_clock.advance_minutes(30)
|
||||
_expect(_request().state == LogisticsRequest.State.DELIVERING and _quantity(_cargo()) == 8 and _quantity(_inventory(_destination)) == 0, "outage retains loaded cargo without delivering into stopped facility")
|
||||
_expect(_crosslinks(), "outage preserves Request Task inventory ownership")
|
||||
_expect(_runtime.restore_snapshot(_runtime.create_snapshot()), "cargo waiting through outage restores")
|
||||
_state.set_resource(&"mana", 100)
|
||||
ManaOutage.release(_runtime, _state)
|
||||
_wait_for(LogisticsRequest.State.COMPLETED, 60)
|
||||
_expect(_request().state == LogisticsRequest.State.COMPLETED and _quantity(_cargo()) == 0 and _quantity(_inventory(_destination)) == 8, "recovered power delivers cargo exactly once")
|
||||
_expect(_crosslinks(), "recovered delivery ownership reconciles")
|
||||
@@ -389,7 +390,7 @@ func _test_kitchen_reserve_outage() -> void:
|
||||
if before[&"active_batch"].is_empty():
|
||||
_runtime.restore_snapshot(_loaded)
|
||||
return
|
||||
_state.set_resource(&"mana", 0)
|
||||
ManaOutage.hold(_runtime, _state)
|
||||
_clock.advance_minutes(30)
|
||||
var stopped: Dictionary = _runtime.kitchen_production.get_report(_destination)
|
||||
_expect(stopped[&"state"] == &"NO_POWER" and stopped[&"active_batch"] == before[&"active_batch"], "thirty stopped minutes preserve batch identity reservations and progress")
|
||||
@@ -397,7 +398,7 @@ func _test_kitchen_reserve_outage() -> void:
|
||||
_expect(_runtime.kitchen_production.validate_crosslinks(), "stopped kitchen reservations remain owned")
|
||||
var saved: Dictionary = _runtime.create_snapshot()
|
||||
_expect(_runtime.restore_snapshot(saved) and _runtime.create_snapshot() == saved, "active cooking outage round trips through complete Runtime save")
|
||||
_state.set_resource(&"mana", 100)
|
||||
ManaOutage.release(_runtime, _state)
|
||||
_clock.advance_minutes(30)
|
||||
_expect(_quantity(_inventory(_destination)) == 4 and _runtime.inventory_service.total_quantity(_inventory(_destination), SupplyCatalog.BASIC_MEAL) == 4, "recovery consumes exactly four raw food and produces exactly four meals")
|
||||
_expect(_runtime.kitchen_production.get_report(_destination)[&"state"] == &"TARGET_REACHED" and _runtime.kitchen_production.validate_crosslinks(), "recovered batch releases reservations and stops at target")
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
extends SceneTree
|
||||
const ManaOutage = preload("res://tests/helpers/mana_outage.gd")
|
||||
|
||||
const FixtureCleanup := preload("res://tests/helpers/ui_audio_fixture_cleanup.gd")
|
||||
var _finishing := false
|
||||
@@ -128,7 +129,7 @@ func _verify_mid_ride() -> bool:
|
||||
_expect(_runtime.restore_snapshot(snapshot), "rewind depletion drill to original passenger state")
|
||||
for cell: Vector3i in shaft[&"stops"]:
|
||||
_runtime.path_service.configure_cell_traffic(cell, 4, 0, true)
|
||||
_state.set_resource(&"mana", 0)
|
||||
ManaOutage.hold(_runtime, _state)
|
||||
_clock.advance_minutes(1)
|
||||
for actor_id: StringName in riders:
|
||||
_expect(_runtime.elevators.get_actor_trip(actor_id).get(&"state") == PathService.ELEVATOR_REQUEST_BOARDED, "no safe landing retains boarded passenger instead of teleporting")
|
||||
@@ -140,7 +141,7 @@ func _verify_mid_ride() -> bool:
|
||||
_expect(_runtime.restore_snapshot(held_snapshot) and _runtime.create_snapshot() == held_snapshot, "no-landing outage preserves full Runtime save and passenger ownership")
|
||||
for cell: Vector3i in shaft[&"stops"]:
|
||||
_runtime.path_service.configure_cell_traffic(cell)
|
||||
_state.set_resource(&"mana", 100)
|
||||
ManaOutage.release(_runtime, _state)
|
||||
_clock.advance_minutes(45)
|
||||
for actor_id: StringName in riders:
|
||||
_expect(_runtime.elevators.get_actor_trip(actor_id).is_empty() and _runtime.path_service.get_active_elevator_request(actor_id).is_empty(), "landing repair and power restoration release held trip ownership")
|
||||
|
||||
Reference in New Issue
Block a user