162 lines
7.1 KiB
GDScript
162 lines
7.1 KiB
GDScript
extends RefCounted
|
|
|
|
## P05-02 F6: the fixture and day script shared by the restart writer
|
|
## (tests/economy/test_p05_funds_restart_1_write.gd) and reader
|
|
## (tests/economy/test_p05_funds_restart_2_read.gd), which run as separate engine
|
|
## processes. Same ledger fixture and accelerated boundary traversal as
|
|
## tests/economy/test_p05_funds_runtime.gd.
|
|
|
|
const HANDOFF_PATH := "res://artifacts/p05-02/restart-handoff.json"
|
|
const HANDOFF_SCHEMA := "p05-02-restart-handoff-v1"
|
|
const WRITER_SOURCE := "res://tests/economy/test_p05_funds_restart_1_write.gd"
|
|
const WRITER_SAVE := "user://p05_restart_write/minute_%d"
|
|
const READER_SAVE := "user://p05_restart_read/minute_%d"
|
|
const READER_TAMPERED_SAVE := "user://p05_restart_read/changed"
|
|
const SEED := 5025
|
|
# Mid-day 2, just before the day 2 00:00 close (which needs a hidden-fund top-up)
|
|
# and just after it.
|
|
const SAVE_MINUTES: Array[int] = [2400, 2850, 2880]
|
|
const LAST_MINUTE := 4320
|
|
|
|
const FRONT := &"PLAYER_CASH"
|
|
const BACK := &"BACK_CASH"
|
|
const LOAN := &"LOAN_LIABILITY:p05.fixture"
|
|
const FRONT_SALES := &"P05_FIXTURE_FRONT_SALES"
|
|
const BACK_SALES := &"P05_FIXTURE_BACK_SALES"
|
|
const PAYER := &"P05_FIXTURE_PAYER"
|
|
|
|
var _expect: Callable
|
|
|
|
|
|
func _init(expect: Callable) -> void:
|
|
_expect = expect
|
|
|
|
|
|
func new_fixture() -> Dictionary:
|
|
var clock: Variant = load("res://autoload/game_clock.gd").new()
|
|
var state: Variant = load("res://autoload/game_state.gd").new()
|
|
var runtime: Variant = load("res://autoload/simulation_runtime.gd").new()
|
|
clock.reset(1, 6, 0)
|
|
state.reset()
|
|
_expect.call(bool(runtime.initialize(clock, state, SEED).get(&"valid", false)), "fixture Runtime initializes")
|
|
clock.set_paused(true)
|
|
runtime.configure_save_policy({&"enabled": false, &"daily_checkpoint_enabled": false})
|
|
_expect.call(bool(runtime.enable_production_operations().get(&"ok", false)), "fixture enables production Operations")
|
|
for account: StringName in [FRONT_SALES, BACK_SALES, PAYER]:
|
|
runtime.economy_service.register_account(account)
|
|
runtime.economy_service.register_account(LOAN, true)
|
|
_expect.call(runtime.economy_service.initialize_account(PAYER, 100000, clock.absolute_minute, &"p05.fixture.payer").is_success(), "fixture payer funded")
|
|
return {&"clock": clock, &"state": state, &"runtime": runtime}
|
|
|
|
|
|
func free_fixture(fixture: Dictionary) -> void:
|
|
for key: StringName in [&"runtime", &"clock", &"state"]:
|
|
if fixture.get(key) != null:
|
|
fixture[key].free()
|
|
|
|
|
|
## Applies the day script's steps after `after_minute` through `through_minute`
|
|
## and calls `on_minute` after each step (the writer saves there).
|
|
## Day 1: borrow 1000, hidden sale 700 (tax 70, interest 10). Day 2: a cost leaves
|
|
## 5 front funds for the 10 interest, so hidden funds top up 5. Day 3: front sale
|
|
## 50 (tax 5, interest 10). Final: front 35, hidden 695.
|
|
func play(fixture: Dictionary, after_minute: int, through_minute: int, on_minute: Callable) -> void:
|
|
var steps: Array = [
|
|
[420, func() -> void: _move(fixture, LOAN, FRONT, 1000, &"LOAN_PRINCIPAL_BORROWED", "borrow")],
|
|
[450, func() -> void: _sale(fixture, BACK_SALES, BACK, 700, "d1.back")],
|
|
[1440, Callable()],
|
|
[2040, func() -> void: _move(fixture, FRONT, &"OPERATING_EXPENSE", 5915, &"OPERATING_EXPENSE", "d2.cost")],
|
|
[2400, Callable()],
|
|
[2850, Callable()],
|
|
[2880, Callable()],
|
|
[3510, func() -> void: _sale(fixture, FRONT_SALES, FRONT, 50, "d3.front")],
|
|
[4320, Callable()],
|
|
]
|
|
for step: Array in steps:
|
|
var minute := int(step[0])
|
|
if minute <= after_minute or minute > through_minute:
|
|
continue
|
|
_advance_to(fixture, minute)
|
|
if (step[1] as Callable).is_valid():
|
|
(step[1] as Callable).call()
|
|
if on_minute.is_valid():
|
|
on_minute.call(minute)
|
|
|
|
|
|
func economy_bytes(runtime: Variant) -> PackedByteArray:
|
|
return var_to_bytes(runtime.create_snapshot()[&"systems"][&"runtime"][&"economy"])
|
|
|
|
|
|
## Operations history in the transport-safe form the Runtime's own restore
|
|
## crosslink compares (StringName ids become String values in saves).
|
|
func days_value(runtime: Variant) -> Variant:
|
|
return runtime.call("_operations_snapshot_comparison_value", runtime.operations_coordinator.get_day_summaries())
|
|
|
|
|
|
func days_equal(runtime: Variant, expected: Variant) -> bool:
|
|
return runtime.call("_strict_variants_equal", expected, days_value(runtime))
|
|
|
|
|
|
func category_count(runtime: Variant, categories: Array) -> int:
|
|
var count := 0
|
|
for entry: LedgerEntry in runtime.economy_service.get_entries():
|
|
if entry.category in categories:
|
|
count += 1
|
|
return count
|
|
|
|
|
|
static func sha256(bytes: PackedByteArray) -> String:
|
|
var context := HashingContext.new()
|
|
context.start(HashingContext.HASH_SHA256)
|
|
if not bytes.is_empty():
|
|
context.update(bytes)
|
|
return context.finish().hex_encode()
|
|
|
|
|
|
# Every Operations boundary is on a 30-minute mark; each step ends on one.
|
|
func _advance_to(fixture: Dictionary, minute: int) -> void:
|
|
var clock: Variant = fixture[&"clock"]
|
|
var runtime: Variant = fixture[&"runtime"]
|
|
while int(clock.absolute_minute) < minute:
|
|
var step := mini(30 - int(clock.absolute_minute) % 30, minute - int(clock.absolute_minute))
|
|
var target: int = clock.absolute_minute + step
|
|
clock.absolute_minute = target
|
|
clock.call("_sync_calendar_fields")
|
|
clock.snapshot_version += step
|
|
runtime.call("_advance_weather", target)
|
|
runtime.call("_advance_facility_upkeep", target)
|
|
runtime.call("_advance_operations_time", target)
|
|
_align_clocks(runtime, target)
|
|
|
|
|
|
# The fixture owns no actors, cars or rides, so only the empty aggregates' clocks
|
|
# follow the minute.
|
|
func _align_clocks(runtime: Variant, minute: int) -> void:
|
|
for service: Variant in [runtime.inventory_service, runtime.facility_service, runtime.kitchen_production,
|
|
runtime.guest_service, runtime.staff_service, runtime.authority_service, runtime.defense_service,
|
|
runtime.construction_service, runtime.excavation_service]:
|
|
service.current_minute = minute
|
|
var transport_snapshot: Dictionary = runtime.elevators.create_snapshot()
|
|
if not (transport_snapshot[&"shafts"] as Array).is_empty() or not (transport_snapshot[&"trips"] as Array).is_empty():
|
|
_expect.call(false, "accelerated fixture cannot skip live elevator movement")
|
|
return
|
|
transport_snapshot[&"current_minute"] = minute
|
|
if not runtime.elevators.restore_snapshot(transport_snapshot):
|
|
_expect.call(false, "empty transport clock aligns through validated snapshot restore")
|
|
|
|
|
|
func _sale(fixture: Dictionary, revenue_account: StringName, fund: StringName, amount: int, key: String) -> void:
|
|
var runtime: Variant = fixture[&"runtime"]
|
|
var minute: int = fixture[&"clock"].absolute_minute
|
|
var entries: Array = [
|
|
LedgerEntry.new().configure(&"", minute, revenue_account, PAYER, amount, &"SERVICE_SALE", &"p05.fixture"),
|
|
LedgerEntry.new().configure(&"", minute, fund, revenue_account, amount, &"SERVICE_CASH_SWEEP", &"p05.fixture"),
|
|
]
|
|
_expect.call(runtime.economy_service.post(entries, StringName("p05.sale." + key)).is_success(), "fixture sale " + key)
|
|
|
|
|
|
func _move(fixture: Dictionary, from: StringName, to: StringName, amount: int, category: StringName, key: String) -> void:
|
|
var runtime: Variant = fixture[&"runtime"]
|
|
_expect.call(runtime.economy_service.transfer(from, to, amount, fixture[&"clock"].absolute_minute, category,
|
|
&"p05.fixture", StringName("p05.move." + key)).is_success(), "fixture move " + key)
|