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:
Takeshi
2026-10-19 17:20:04 +09:00
co-authored by Claude Opus 5.5
parent c366012446
commit ae0f998ff4
18 changed files with 424 additions and 17 deletions
+59
View File
@@ -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)
+1
View File
@@ -0,0 +1 @@
uid://de3w8hfsk83nt
+109
View File
@@ -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)