P05-02: 全費目と資金区分:表金・裏金、維持費未払い、仕入れ、人件費、元本、利息、税の帳簿への接続(原 P05-02 の完了の条件)

This commit is contained in:
Agent Control Runner
2026-10-23 16:02:54 +09:00
parent 613b9886da
commit 7808ca76db
20 changed files with 1906 additions and 17 deletions
+462
View File
@@ -0,0 +1,462 @@
extends SceneTree
const FixtureCleanup := preload("res://tests/helpers/ui_audio_fixture_cleanup.gd")
## P05-02: front (表金) and hidden (裏金) funds, the A09 payment rule, the A10 rank
## tax, the A11 loan interest, the three business day reconciliation and saves.
## Sales, debt and drains are explicit ledger fixtures; the daily closes are the
## production Operations boundaries. The last check opens the ledger from the
## game's own Main (F7), so the runner gives this suite the ordinary Main
## audio cleanup proof (tools/regression/suite-policy.json).
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"
const SINK := &"P05_FIXTURE_SINK"
var _checks := 0
var _failures := 0
func _initialize() -> void:
call_deferred("_run")
func _run() -> void:
print("P05-02: front and hidden funds")
_test_policy_values()
_test_sale_fund_routing()
_test_three_business_days()
_test_resume_matches_uninterrupted()
_test_old_save_migration()
_test_repayment_top_up()
_test_purchase_top_up()
_test_upkeep_top_up()
await _test_ledger_opened_from_main()
await process_frame
if _failures == 0:
print("PASS: %d checks" % _checks)
else:
push_error("FAIL: %d of %d checks failed" % [_failures, _checks])
quit(_failures)
func _expect(condition: bool, message: String) -> void:
_checks += 1
if not condition:
_failures += 1
push_error(message)
func _new_fixture(seed: int) -> 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(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(bool(runtime.enable_production_operations().get(&"ok", false)), "fixture enables production Operations")
for account: StringName in [FRONT_SALES, BACK_SALES, PAYER, SINK]:
runtime.economy_service.register_account(account)
runtime.economy_service.register_account(LOAN, true)
# Guests' Gold enters the ledger only through explicit initialization.
_expect(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()
# Same accelerated boundary traversal as tests/mvp/test_runtime_operations_acceptance.gd:
# the fixtures hold no Facilities, Staff or logistics, only ledger postings, which
# are made on 30-minute marks.
func _advance(fixture: Dictionary, amount: int) -> void:
var clock: Variant = fixture[&"clock"]
var runtime: Variant = fixture[&"runtime"]
var advanced := 0
while advanced < amount:
# Every Operations boundary is on a 30-minute mark; each step ends on one.
var step := mini(30 - int(clock.absolute_minute) % 30, amount - advanced)
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)
advanced += step
# Same as tests/mvp/test_runtime_operations_acceptance.gd: 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(false, "accelerated fixture cannot skip live elevator movement")
return
transport_snapshot[&"current_minute"] = minute
if not runtime.elevators.restore_snapshot(transport_snapshot):
_expect(false, "empty transport clock aligns through validated snapshot restore")
func _advance_to(fixture: Dictionary, minute: int) -> void:
_advance(fixture, minute - int(fixture[&"clock"].absolute_minute))
func _sale(fixture: Dictionary, revenue_account: StringName, fund: StringName, amount: int, key: String) -> bool:
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"),
]
return runtime.economy_service.post(entries, StringName("p05.sale." + key)).is_success()
func _move(fixture: Dictionary, from: StringName, to: StringName, amount: int, category: StringName, key: String) -> bool:
var runtime: Variant = fixture[&"runtime"]
return runtime.economy_service.transfer(from, to, amount, fixture[&"clock"].absolute_minute, category,
&"p05.fixture", StringName("p05.move." + key)).is_success()
func _fund_delta(runtime: Variant, account: StringName, after_minute: int, through_minute: int) -> int:
var delta := 0
for entry: LedgerEntry in runtime.economy_service.get_entries():
if entry.absolute_minute <= after_minute or entry.absolute_minute > through_minute:
continue
if entry.debit_account == account:
delta += entry.amount
if entry.credit_account == account:
delta -= entry.amount
return delta
func _test_policy_values() -> void:
_expect(FinancePolicy.rank_tax(800) == 80 and FinancePolicy.rank_tax(109) == 10 and FinancePolicy.rank_tax(9) == 0, "A10 rank tax is 10% rounded down")
_expect(FinancePolicy.rank_tax(-5) == 0, "no tax on a negative day")
_expect(FinancePolicy.loan_interest(1000) == 10 and FinancePolicy.loan_interest(199) == 1 and FinancePolicy.loan_interest(99) == 0, "A11 interest is 1% per day rounded down")
func _test_sale_fund_routing() -> void:
var fixture := _new_fixture(5021)
var runtime: Variant = fixture[&"runtime"]
var back_ids: Array[StringName] = []
var front_ids: Array[StringName] = []
# Expansion definitions are read from their files; they are not registered by default.
for path: String in ["res://data/facilities/adult_private_room.tres", "res://data/facilities/expansion/black_market.tres",
"res://data/facilities/expansion/vip_private_salon.tres", "res://data/facilities/cafeteria.tres"]:
var definition: Variant = load(path)
if definition == null:
continue
if FinancePolicy.is_back_fund_service(definition.tags):
back_ids.append(definition.id)
else:
front_ids.append(definition.id)
_expect(back_ids.has(&"adult_private_room") and back_ids.has(&"black_market") and back_ids.has(&"vip_private_salon"), "adult, trade and private salon services earn hidden funds")
_expect(front_ids.has(&"cafeteria"), "dining earns front funds")
_expect(runtime.economy_service.has_account(BACK) and not runtime.economy_service.account_allows_negative(BACK), "the hidden fund is its own non-negative account")
_expect(runtime.economy_service.get_balance(BACK) == 0 and runtime.economy_service.get_balance(FRONT) == 5000, "a new game starts with every Gold in front funds")
_free_fixture(fixture)
func _test_three_business_days() -> void:
var fixture := _new_fixture(5022)
var runtime: Variant = fixture[&"runtime"]
var state: Variant = fixture[&"state"]
var coordinator: Variant = runtime.operations_coordinator
# Day 1: borrow 1000, front sale 300, hidden sale 500.
_advance_to(fixture, 420)
_expect(_move(fixture, LOAN, FRONT, 1000, &"LOAN_PRINCIPAL_BORROWED", "borrow"), "fixture principal borrowed")
_expect(_sale(fixture, FRONT_SALES, FRONT, 300, "d1.front") and _sale(fixture, BACK_SALES, BACK, 500, "d1.back"), "day 1 sales")
_advance_to(fixture, 1440)
var day1: Dictionary = coordinator.get_day_summary(1)
_expect(not day1.is_empty(), "day 1 closes at 00:00")
var totals1: Dictionary = day1.get(&"category_totals", {})
_expect(int(totals1.get(&"revenue", -1)) == 800, "day 1 sales are front + hidden")
_expect(int(totals1.get(&"taxes_and_interest", -1)) == 90, "day 1 rank tax 80 and interest 10")
_expect(int(totals1.get(&"financing_principal_inflows", -1)) == 1000, "principal stays financing, outside sales")
var funds1: Dictionary = day1.get(&"settlement", {}).get(&"funds", {})
_expect(int(funds1.get(&"revenue_front", -1)) == 300 and int(funds1.get(&"revenue_back", -1)) == 500, "day 1 fund report splits sales")
_expect(int(funds1.get(&"rank_tax", -1)) == 80 and int(funds1.get(&"rank_tax_paid", -1)) == 80, "day 1 rank tax paid in full")
_expect(int(funds1.get(&"loan_interest", -1)) == 10 and int(funds1.get(&"loan_interest_paid", -1)) == 10, "day 1 interest paid in full")
_expect(int(funds1.get(&"back_top_up", -1)) == 0, "front funds alone paid day 1")
_expect(runtime.economy_service.get_balance(FRONT) == 6210 and runtime.economy_service.get_balance(BACK) == 500, "day 1 balances")
_expect(state.get_resource(&"gold") == runtime.economy_service.get_balance(FRONT), "Gold mirrors front funds")
# The starting Gold is posted at 06:00 (minute 360); day 1 changes follow it.
_expect(_fund_delta(runtime, FRONT, 360, 1440) == 1210 and _fund_delta(runtime, BACK, 360, 1440) == 500, "day 1 fund changes equal the ledger")
# Day 2: front funds fall to 5, so 5 of the 10 interest comes from hidden funds.
_advance_to(fixture, 1440 + 600)
_expect(_move(fixture, FRONT, &"OPERATING_EXPENSE", 6205, &"OPERATING_EXPENSE", "d2.cost"), "day 2 purchase-like cost")
_advance_to(fixture, 2880)
var funds2: Dictionary = coordinator.get_day_summary(2).get(&"settlement", {}).get(&"funds", {})
_expect(int(funds2.get(&"back_top_up", -1)) == 5, "A09: hidden funds cover the 5 front funds cannot pay")
_expect(int(funds2.get(&"loan_interest_paid", -1)) == 10 and int(funds2.get(&"rank_tax", -1)) == 0, "day 2 interest paid, no sales no tax")
_expect(runtime.economy_service.get_balance(FRONT) == 0 and runtime.economy_service.get_balance(BACK) == 495, "day 2 balances")
_expect(int(coordinator.get_day_summary(2)[&"category_totals"][&"revenue"]) == 0, "the top-up is not sales")
_expect(_fund_delta(runtime, FRONT, 1440, 2880) == -6210 and _fund_delta(runtime, BACK, 1440, 2880) == -5, "day 2 fund changes equal the ledger")
# Day 3: hidden funds are gone too, so the interest stays unpaid.
_advance_to(fixture, 2880 + 600)
_expect(_move(fixture, BACK, SINK, 495, &"TRANSFER", "d3.drain"), "day 3 hidden funds drained")
_advance_to(fixture, 4320)
var day3: Dictionary = coordinator.get_day_summary(3)
var funds3: Dictionary = day3.get(&"settlement", {}).get(&"funds", {})
_expect(int(funds3.get(&"loan_interest", -1)) == 10 and int(funds3.get(&"loan_interest_paid", -1)) == 0 and int(funds3.get(&"loan_interest_arrears", -1)) == 10, "A09: unpaid interest carries over")
_expect(int(day3[&"settlement"][&"arrears_total"]) == 10, "day 3 arrears include the interest")
_expect(int(runtime.get_operations_report().get(&"arrears_total", -1)) == 10, "Operations report sees the interest arrears")
_expect(int(day3[&"category_totals"][&"taxes_and_interest"]) == 10, "the unpaid interest is still the day's expense")
# Day 4: a front sale pays the older interest first.
_advance_to(fixture, 4320 + 600)
_expect(_sale(fixture, FRONT_SALES, FRONT, 100, "d4.front"), "day 4 sale")
_advance_to(fixture, 5760)
var funds4: Dictionary = coordinator.get_day_summary(4).get(&"settlement", {}).get(&"funds", {})
_expect(int(funds4.get(&"rank_tax", -1)) == 10 and int(funds4.get(&"rank_tax_paid", -1)) == 10, "day 4 tax paid")
_expect(int(funds4.get(&"loan_interest_paid", -1)) == 20 and int(funds4.get(&"loan_interest_arrears", -1)) == 0, "day 4 pays the older interest and the new one")
_expect(runtime.economy_service.get_balance(FRONT) == 70, "day 4 front balance")
_expect(runtime.economy_service.validate_ledger()[&"ok"], "the ledger reconciles after four closes")
_check_ledger_panel(runtime, fixture[&"clock"])
var saved: Dictionary = runtime.create_snapshot()
_expect(runtime.restore_snapshot(saved) and var_to_bytes(saved) == var_to_bytes(runtime.create_snapshot()), "four closed days restore exactly")
_free_fixture(fixture)
func _scripted_days(fixture: Dictionary, restore_at: Array[int], through_files := false) -> Dictionary:
var runtime: Variant = fixture[&"runtime"]
# Postings and save/restore points in time order; the last minute closes day 3.
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", 5900, &"OPERATING_EXPENSE", "d2.cost")],
[2400, 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])
_advance_to(fixture, minute)
if (step[1] as Callable).is_valid():
(step[1] as Callable).call()
if minute in restore_at and through_files:
# The ordinary save file path: write the save, then load it back.
var path := "user://p05_resume_%d.save" % minute
_expect(bool(runtime.save_to_path(path).get(&"ok", false)), "save file at minute %d" % minute)
_expect(bool(runtime.load_from_path(path).get(&"ok", false)), "load the save file at minute %d" % minute)
elif minute in restore_at:
var saved: Dictionary = runtime.create_snapshot()
_expect(runtime.restore_snapshot(saved), "restore at minute %d" % minute)
return runtime.create_snapshot()
func _test_resume_matches_uninterrupted() -> void:
var straight := _new_fixture(5023)
var resumed := _new_fixture(5023)
var a := _scripted_days(straight, [])
var b := _scripted_days(resumed, [1440, 2400, 2880])
_expect(var_to_bytes(a) == var_to_bytes(b), "saving mid-day and at 00:00 changes nothing: no double tax, interest or top-up")
var runtime: Variant = resumed[&"runtime"]
var keys := 0
for entry: LedgerEntry in runtime.economy_service.get_entries():
if entry.category in [FinancePolicy.RANK_TAX_CATEGORY, FinancePolicy.LOAN_INTEREST_CATEGORY]:
keys += 1
# Day 1: tax 70 and interest 10; day 2: interest only (no sales); day 3: tax 5 and interest 10.
_expect(keys == 5, "each tax and interest accrual is posted once (no duplicates)")
# Through save files the save and load events are new, so compare the funds side only.
var through_files := _new_fixture(5023)
var c := _scripted_days(through_files, [1440, 2400, 2880], true)
_expect(var_to_bytes(c[&"systems"][&"runtime"][&"economy"]) == var_to_bytes(a[&"systems"][&"runtime"][&"economy"]), "saving to files and loading them back leaves the ledger exactly as without saving")
var straight_days: Array = straight[&"runtime"].operations_coordinator.get_day_summaries()
var file_days: Array = through_files[&"runtime"].operations_coordinator.get_day_summaries()
# Operations history is persisted in a transport-safe form (StringName ids become
# String values), so compare as the Runtime's own restore crosslink does.
var runtime_a: Variant = straight[&"runtime"]
_expect(runtime_a.call("_strict_variants_equal", runtime_a.call("_operations_snapshot_comparison_value", straight_days),
runtime_a.call("_operations_snapshot_comparison_value", file_days)), "the daily closes, fund reports and arrears are the same through save files")
_free_fixture(straight)
_free_fixture(resumed)
_free_fixture(through_files)
func _test_old_save_migration() -> void:
var fixture := _new_fixture(5024)
var runtime: Variant = fixture[&"runtime"]
_advance_to(fixture, 420)
var current: Dictionary = runtime.create_snapshot()
var old := current.duplicate(true)
var economy: Dictionary = old[&"systems"][&"runtime"][&"economy"]
var kept: Array = []
for account: Dictionary in economy[&"accounts"]:
if String(account[&"account_id"]) not in ["BACK_CASH", "RANK_TAX_EXPENSE", "RANK_TAX_PAYABLE", "LOAN_INTEREST_EXPENSE", "LOAN_INTEREST_PAYABLE"]:
kept.append(account)
economy[&"accounts"] = kept
_expect(runtime.restore_snapshot(old), "A12: a save from before the split loads")
_expect(runtime.economy_service.get_balance(FRONT) == 5000 and runtime.economy_service.get_balance(BACK) == 0, "A12: every Gold is front funds, hidden funds start at 0")
_expect(var_to_bytes(runtime.create_snapshot()) == var_to_bytes(current), "A12: the migrated save equals a current save of the same game")
var partial := old.duplicate(true)
var partial_accounts: Array = partial[&"systems"][&"runtime"][&"economy"][&"accounts"]
partial_accounts.append({&"account_id": "RANK_TAX_PAYABLE", &"allow_negative": true, &"system": false})
var before: Dictionary = runtime.create_snapshot()
_expect(not runtime.restore_snapshot(partial) and var_to_bytes(before) == var_to_bytes(runtime.create_snapshot()), "a partial set of fund accounts is rejected and changes nothing")
_free_fixture(fixture)
func _test_repayment_top_up() -> void:
var fixture := _new_fixture(5025)
var runtime: Variant = fixture[&"runtime"]
_advance_to(fixture, 420)
_expect(_move(fixture, LOAN, FRONT, 1000, &"LOAN_PRINCIPAL_BORROWED", "borrow"), "fixture principal")
_expect(_sale(fixture, BACK_SALES, BACK, 300, "back") and _move(fixture, FRONT, &"OPERATING_EXPENSE", 5980, &"OPERATING_EXPENSE", "cost"), "front 20, hidden 300")
var receipts: Array = []
runtime.economy_service.transaction_posted.connect(func(id: StringName, _entries: Array) -> void: receipts.append(id))
var before: Dictionary = runtime.create_snapshot()
_expect(not runtime.repay_loan_principal(&"p05.fixture", 400, &"p05.repay.too_much").is_success() and var_to_bytes(before) == var_to_bytes(runtime.create_snapshot()), "front + hidden short: repayment rejected, nothing changes")
var paid: CommandResult = runtime.repay_loan_principal(&"p05.fixture", 200, &"p05.repay")
_expect(paid.is_success() and int(paid.details.get(&"outstanding_principal", -1)) == 800, "repayment uses hidden funds for the rest")
_expect(runtime.economy_service.get_balance(FRONT) == 0 and runtime.economy_service.get_balance(BACK) == 120, "front 20 first, then 180 from hidden funds")
_expect(receipts.size() == 1, "payment and top-up are one transaction")
var entries: Array = runtime.economy_service.get_transaction_entries(StringName(paid.details[&"transaction_id"]))
_expect(entries.size() == 2 and entries[0].category == FinancePolicy.TOP_UP_CATEGORY and entries[1].category == &"LOAN_PRINCIPAL_REPAID", "top-up then principal")
_expect(fixture[&"state"].get_resource(&"gold") == 0, "Gold mirrors front funds after repayment")
_free_fixture(fixture)
func _test_purchase_top_up() -> void:
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(bool(runtime.initialize(clock, state, 5026).get(&"valid", false)), "purchase fixture Runtime initializes")
clock.set_paused(true)
runtime.configure_save_policy({&"enabled": false, &"daily_checkpoint_enabled": false})
for row: Array in [[&"warehouse", Vector3i(-8, 0, 1)], [&"staff_door", Vector3i(0, 0, 1)]]:
_expect(runtime.submit_command(&"build_facility", {&"definition_id": row[0], &"origin": row[1]}).is_success(), "build %s" % row[0])
clock.advance_minutes(180)
var warehouse := &""
for facility: FacilityInstance in runtime.facility_service.get_instances():
if facility.definition_id == &"warehouse":
warehouse = facility.instance_id
_expect(not warehouse.is_empty(), "warehouse completes")
var fixture := {&"clock": clock, &"state": state, &"runtime": runtime}
for account: StringName in [BACK_SALES, PAYER]:
runtime.economy_service.register_account(account)
_expect(runtime.economy_service.initialize_account(PAYER, 1000, clock.absolute_minute, &"p05.purchase.payer").is_success(), "fixture payer")
var front: int = runtime.economy_service.get_balance(FRONT)
_expect(_move(fixture, FRONT, &"OPERATING_EXPENSE", front - 2, &"OPERATING_EXPENSE", "purchase.drain") and _sale(fixture, BACK_SALES, BACK, 100, "purchase.back"), "front 2, hidden 100")
var preview: Dictionary = runtime.preview_supply_purchase(warehouse, SupplyCatalog.RAW_FOOD, 5)
_expect(bool(preview.get(&"ok", false)) and int(preview[&"total_cost"]) == 15 and int(preview[&"cash_after"]) == 87, "A09: the quote counts front + hidden funds")
var paid: CommandResult = runtime.submit_command(&"purchase_supplies", {&"warehouse_id": warehouse, &"item_id": SupplyCatalog.RAW_FOOD, &"quantity": 5})
_expect(paid.is_success(), "the purchase uses hidden funds for what front funds cannot pay")
_expect(runtime.economy_service.get_balance(FRONT) == 0 and runtime.economy_service.get_balance(BACK) == 87, "front 2 first, then 13 from hidden funds")
var purchase_entries: Array = runtime.economy_service.get_transaction_entries(StringName(paid.details.get(&"transaction_id", &"")))
_expect(purchase_entries.size() == 1 and purchase_entries[0].credit_account == FRONT and purchase_entries[0].amount == 15, "the purchase receipt keeps its single front-fund entry")
var top_ups := 0
for entry: LedgerEntry in runtime.economy_service.get_entries():
if entry.category == FinancePolicy.TOP_UP_CATEGORY:
top_ups += entry.amount
_expect(top_ups == 13, "one 13 Gold top-up from hidden funds")
_expect(state.get_resource(&"gold") == 0, "Gold mirrors front funds after the purchase")
# A refused command is still recorded as processed; the funds and stock must not move.
var economy_before: Dictionary = runtime.economy_service.create_snapshot()
var inventory_before: Dictionary = runtime.inventory_service.create_snapshot()
var refused: CommandResult = runtime.submit_command(&"purchase_supplies", {&"warehouse_id": warehouse, &"item_id": SupplyCatalog.RAW_FOOD, &"quantity": 40})
_expect(not refused.is_success() and StringName(refused.details.get(&"reason", &"")) == &"INSUFFICIENT_FUNDS", "front + hidden short: purchase refused for funds")
_expect(var_to_bytes(economy_before) == var_to_bytes(runtime.economy_service.create_snapshot()) \
and var_to_bytes(inventory_before) == var_to_bytes(runtime.inventory_service.create_snapshot()), "the refused purchase moves no Gold and no stock")
var saved: Dictionary = runtime.create_snapshot()
_expect(runtime.restore_snapshot(saved) and var_to_bytes(saved) == var_to_bytes(runtime.create_snapshot()), "purchase with a top-up restores exactly")
runtime.free()
clock.free()
state.free()
func _test_upkeep_top_up() -> void:
var fixture := _new_fixture(5027)
var runtime: Variant = fixture[&"runtime"]
var clock: Variant = fixture[&"clock"]
_expect(runtime.submit_command(&"build_facility", {&"definition_id": &"warehouse", &"origin": Vector3i(-8, 0, 1)}).is_success(), "build a warehouse")
clock.advance_minutes(180)
_advance_to(fixture, 600)
var front: int = runtime.economy_service.get_balance(FRONT)
_expect(_move(fixture, FRONT, &"OPERATING_EXPENSE", front, &"OPERATING_EXPENSE", "upkeep.drain") and _sale(fixture, BACK_SALES, BACK, 1000, "upkeep.back"), "front 0, hidden 1000")
_advance_to(fixture, 1440)
var summary: Dictionary = runtime.operations_coordinator.get_day_summary(1)
var maintenance := int(summary.get(&"category_totals", {}).get(&"maintenance", -1))
var funds: Dictionary = summary.get(&"settlement", {}).get(&"funds", {})
_expect(maintenance > 0, "the warehouse owes upkeep for day 1")
_expect(int(funds.get(&"back_top_up", -1)) == maintenance + int(funds.get(&"rank_tax", 0)), "A09: hidden funds cover the upkeep (and tax) front funds cannot pay")
_expect(runtime.upkeep_settlement.get_arrears_total() == 0 and int(summary[&"settlement"][&"arrears_total"]) == 0, "no upkeep arrears")
_expect(runtime.economy_service.get_balance(FRONT) == 0 and runtime.economy_service.get_balance(BACK) == 1000 - int(funds[&"back_top_up"]), "front 0, hidden paid the rest")
_free_fixture(fixture)
# F7: the ordinary ledger panel reads both funds, the rank tax, the interest, the
# top-ups and their arrears from the same ledger and daily closes.
func _check_ledger_panel(runtime: Variant, clock: Variant) -> void:
var panel := OperatingLedgerPanel.new()
root.add_child(panel)
panel.setup(runtime, clock)
var report: Dictionary = panel.get_report()
_expect(int(report.get(&"cash", -1)) == runtime.economy_service.get_balance(FRONT)
and int(report.get(&"back_cash", -1)) == runtime.economy_service.get_balance(BACK), "the panel reports both funds")
_expect(int(report.get(&"finance_arrears", -1)) == 0, "the panel reports no tax or interest arrears after day 4")
var cash_text: String = panel._cash_label.text
_expect(cash_text.contains("裏金 %dG" % runtime.economy_service.get_balance(BACK)) and cash_text.contains("本日の税") and cash_text.contains("利息"), "the cash section shows the hidden fund, tax and interest")
panel.select_latest_close()
var close_text: String = panel._closed_label.text
_expect(close_text.contains("うち表の売上 100G / 裏の売上 0G") and close_text.contains("世界樹ランク税 10G(支払 10G)") \
and close_text.contains("利息 10G(支払 20G)") and close_text.contains("未払い 税 0G・利息 0G"), "the day 4 close shows sales by fund, tax, interest and arrears")
panel.queue_free()
# F7 through ordinary operation: after a real 00:00 close on the game's own
# Runtime and clock, Main's 帳簿 navigation button opens the ledger.
func _test_ledger_opened_from_main() -> void:
var runtime: Variant = root.get_node("SimulationRuntime")
var clock: Variant = root.get_node("GameClock")
var state: Variant = root.get_node("GameState")
clock.reset(1, 6, 0)
state.reset()
runtime.initialize(clock, state, 5028)
clock.set_paused(true)
root.size = Vector2i(1440, 900)
var main: Control = load("res://scenes/main/main.tscn").instantiate()
root.add_child(main)
await process_frame
runtime.configure_save_policy({&"enabled": false, &"daily_checkpoint_enabled": false})
for account: StringName in [FRONT_SALES, BACK_SALES, PAYER]:
runtime.economy_service.register_account(account)
runtime.economy_service.register_account(LOAN, true)
_expect(runtime.economy_service.initialize_account(PAYER, 100000, clock.absolute_minute, &"p05.fixture.payer").is_success(), "game Runtime fixture payer funded")
clock.advance_minutes(60)
var fixture := {&"clock": clock, &"state": state, &"runtime": runtime}
_expect(_move(fixture, LOAN, FRONT, 1000, &"LOAN_PRINCIPAL_BORROWED", "ui.borrow") and _sale(fixture, FRONT_SALES, FRONT, 300, "ui.front")
and _sale(fixture, BACK_SALES, BACK, 500, "ui.back"), "day 1 loan and sales on the game Runtime")
clock.advance_minutes(1440 - int(clock.absolute_minute))
_expect(not runtime.operations_coordinator.get_day_summary(1).is_empty(), "the normal clock closes day 1 at 00:00")
var ledger_button: Button = (main.get("navigation_buttons") as Dictionary)[&"ledger"]
ledger_button.pressed.emit()
var panel: OperatingLedgerPanel = main.get("operating_ledger_panel")
_expect(panel.visible and ledger_button.button_pressed, "the 帳簿 button opens the ledger")
var report: Dictionary = panel.get_report()
_expect(int(report.get(&"cash", -1)) == 6210 and int(report.get(&"back_cash", -1)) == 500, "the opened ledger reports front 6210 and hidden 500")
# The day 1 tax and interest are paid at the 00:00 that opens day 2.
var cash_text: String = panel._cash_label.text
_expect(cash_text.contains("裏金 500G(本日の入金 +0G / 表金へ補填 −0G)") and cash_text.contains("本日の税 80G / 利息 10G(未払い 0G)"), "the opened ledger shows the hidden fund, today's tax and interest")
var close_text: String = panel._closed_label.text
_expect(close_text.contains("うち表の売上 300G / 裏の売上 500G") and close_text.contains("世界樹ランク税 80G(支払 80G)/ 利息 10G(支払 10G)")
and close_text.contains("裏金から表金へ補填 0G / 未払い 税 0G・利息 0G"), "the opened ledger shows the day 1 close by fund with tax and interest")
var cleanup: Dictionary = await FixtureCleanup.free_owner_and_wait(self, main)
_expect(bool(cleanup.get(&"passed", false)), "ordinary Main release with audio cleanup")