463 lines
28 KiB
GDScript
463 lines
28 KiB
GDScript
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")
|