194 lines
11 KiB
GDScript
194 lines
11 KiB
GDScript
extends "res://tests/fixtures/p08-03/placement.gd"
|
|
## Real Runtime commands; inherited formal codec/save-operation helpers unchanged.
|
|
const Stream := preload("res://tests/fixtures/p23-03/diagnostic_stream.gd")
|
|
const Comparison := preload("res://tests/fixtures/p23-03/layout_comparison.gd")
|
|
const PurchaseQuoteV2 := preload("res://scripts/supply/supply_purchase_quote_v2.gd")
|
|
var stream: RefCounted
|
|
var layout := "A"
|
|
var facility_ids: Dictionary = {}
|
|
|
|
func setup() -> void:
|
|
clock.reset(1, 6, 0)
|
|
var state: Variant = tree.root.get_node("GameState")
|
|
state.reset()
|
|
expect(runtime.initialize(clock, state, 806042).get(&"valid", false), "initialize observation")
|
|
clock.set_paused(true)
|
|
tree.root.size = Vector2i(1440, 810)
|
|
layout = OS.get_environment("P23_LAYOUT")
|
|
expect(layout in ["A", "B"], "declared layout A or B")
|
|
main = load("res://scenes/main/main.tscn").instantiate()
|
|
main.manual_save_path = "user://saves/manual_ui"
|
|
if role == "reader":
|
|
import_package(directory.path_join("formal"), String(main.manual_save_path))
|
|
tree.root.add_child(main)
|
|
runtime.configure_save_policy({&"enabled": false, &"daily_checkpoint_enabled": false})
|
|
stream = Stream.new()
|
|
expect(stream.open(directory.path_join(role + "-stream")), "fresh observation stream")
|
|
stream.watch(main.world_preview)
|
|
await frames()
|
|
|
|
func command(id: StringName, payload: Dictionary) -> bool:
|
|
var result: CommandResult = runtime.submit_command(id, payload)
|
|
expect(result.is_success(), String(id) + " " + str(payload) + ": " + str(result.code))
|
|
expect(stream.record("command", {&"command": id, &"payload": payload,
|
|
&"result": result.to_dictionary(), &"minute": clock.absolute_minute}), "record public command")
|
|
return result.is_success()
|
|
|
|
func advance(minutes: int) -> void:
|
|
for unused: int in range(minutes):
|
|
clock.advance_minutes(1)
|
|
await frames()
|
|
await observe_progress()
|
|
if clock.absolute_minute % 30 == 0:
|
|
expect(await stream.await_draws(tree, main.world_preview), "real draw history before capture")
|
|
expect(stream.capture("minute-%d" % clock.absolute_minute, runtime, main.world_preview), "sparse minute observation")
|
|
|
|
func observe_progress() -> void:
|
|
pass
|
|
|
|
func prepare() -> void:
|
|
# Both layouts receive the same runtime-derived budget for all setup spends.
|
|
# Cafeteria/kitchen move together to x=6, leaving x=4..5 for the declared B EV.
|
|
var builds: Array = [[&"reception", Vector3i(-8, 0, 0)],
|
|
[&"cafeteria", Vector3i(6, 0, 0)], [&"warehouse", Vector3i(-8, 0, 1)],
|
|
[&"kitchen", Vector3i(6, 0, 1)], [&"staff_door", Vector3i(0, 0, 1)],
|
|
[&"standard_room", Vector3i(-8, 2, 0)], [&"guest_stairs", Vector3i(-4, 1, 0)],
|
|
[&"guest_stairs", Vector3i(-4, 2, 0)], [&"guest_corridor", Vector3i(-5, 2, 0)],
|
|
[&"guest_corridor", Vector3i(-6, 2, 0)], [&"guest_elevator", Vector3i.ZERO],
|
|
[&"guest_corridor", Vector3i(2, 1, 0)], [&"guest_corridor", Vector3i(3, 1, 0)],
|
|
[&"staff_waiting_room", Vector3i(-4, 0, 1)]]
|
|
var improvement: Dictionary = Comparison.new().declaration()[&"improvement"]
|
|
var budget_builds := builds.duplicate(true)
|
|
budget_builds.append([improvement[&"definition_id"], improvement[&"origin"]])
|
|
var assignments: Array = [[&"staff_akane", &"reception"], [&"staff_gina", &"warehouse"],
|
|
[&"staff_chika", &"kitchen"], [&"staff_iris", &"cafeteria"], [&"staff_juno", &"cafeteria"]]
|
|
var purchase_quantity := 40
|
|
var needed := {&"gold": 0, &"materials": 0}
|
|
var costs: Array = []
|
|
for row: Array in budget_builds:
|
|
var definition: FacilityDefinition = runtime.definition_registry.get_facility(row[0])
|
|
costs.append({&"definition": row[0], &"cost": definition.construction_cost.duplicate(true)})
|
|
for key: StringName in needed:
|
|
needed[key] += int(definition.construction_cost.get(key, 0))
|
|
var construction_total := needed.duplicate(true)
|
|
var signing_costs: Array = []
|
|
var signing_total := 0
|
|
for assignment: Array in assignments:
|
|
var candidate: Dictionary = runtime.mvp_content_catalog.get_recruitment_entry(assignment[0])
|
|
expect(candidate.has(&"signing_cost"), "registered signing cost " + String(assignment[0]))
|
|
var signing_cost := int(candidate[&"signing_cost"])
|
|
signing_costs.append({&"candidate_id": assignment[0], &"signing_cost": signing_cost,
|
|
&"source": "MvpContentCatalog.get_recruitment_entry.signing_cost"})
|
|
signing_total += signing_cost
|
|
needed[&"gold"] += signing_total
|
|
# Before warehouse construction, use the purchase service's catalog/quote
|
|
# branch. Check this total against its live preview immediately before buying.
|
|
var item := SupplyCatalog.get_item(SupplyCatalog.RAW_FOOD)
|
|
var purchase_quote := PurchaseQuoteV2.make(SupplyCatalog.RAW_FOOD, purchase_quantity, int(item[&"unit_price"]))
|
|
expect(not purchase_quote.is_empty(), "registered raw-food purchase quote")
|
|
var purchase_total := int(purchase_quote[&"effective_total"] if PurchaseQuoteV2.eligible(runtime) else purchase_quote[&"base_total"])
|
|
needed[&"gold"] += purchase_total
|
|
var purchase_cost := {&"item_id": SupplyCatalog.RAW_FOOD, &"quantity": purchase_quantity,
|
|
&"unit_price": item[&"unit_price"], &"quote": purchase_quote, &"total_cost": purchase_total,
|
|
&"source": "SupplyCatalog.unit_price / PurchaseQuoteV2.make; preview_purchase eligibility"}
|
|
var state: Variant = tree.root.get_node("GameState")
|
|
var deltas := {}
|
|
for key: StringName in needed:
|
|
if needed[key] > state.get_resource(key):
|
|
deltas[key] = needed[key] - state.get_resource(key)
|
|
budget = {"setup": "diagnostic", "costs": costs, "needed": needed,
|
|
"initial": state.resources.duplicate(true), "deltas": deltas,
|
|
"construction_total": construction_total, "signing_costs": signing_costs,
|
|
"signing_total": signing_total, "purchase_cost": purchase_cost}
|
|
if not deltas.is_empty(): command(&"change_resources", {&"deltas": deltas})
|
|
expect(stream.record("budget", budget), "common diagnostic budget")
|
|
if layout == "B": builds.append([improvement[&"definition_id"], improvement[&"origin"]])
|
|
# Preserve the supply scenario's staged support/construction sequence.
|
|
for index: int in range(builds.size()):
|
|
if index == 5: await advance(180)
|
|
if index == 7: await advance(60)
|
|
if index == 8: await advance(10)
|
|
command(&"build_facility", {&"definition_id": builds[index][0], &"origin": builds[index][1]})
|
|
var transport_minutes := 0
|
|
for id: StringName in [&"guest_elevator", &"guest_corridor"]:
|
|
transport_minutes = maxi(transport_minutes, runtime.definition_registry.get_facility(id).build_minutes)
|
|
await advance(maxi(10, transport_minutes))
|
|
for facility: FacilityInstance in runtime.facility_service.get_instances():
|
|
facility_ids[facility.definition_id] = facility.instance_id
|
|
command(&"set_facility_conditions", {&"facility_id": facility_ids[&"cafeteria"],
|
|
&"conditions": {&"related_facility_id": facility_ids[&"kitchen"]}})
|
|
for assignment: Array in assignments:
|
|
command(&"hire_staff_candidate", {&"candidate_id": assignment[0]})
|
|
command(&"set_staff_work_assignment", {&"staff_id": assignment[0],
|
|
&"facility_id": facility_ids[assignment[1]], &"template_id": &"DAY"})
|
|
var actual_quote: Dictionary = runtime.preview_supply_purchase(facility_ids[&"warehouse"], SupplyCatalog.RAW_FOOD, purchase_quantity)
|
|
expect(int(actual_quote[&"total_cost"]) == purchase_total, "budget matches actual purchase quote")
|
|
expect(stream.record("setup-purchase-quote", actual_quote), "record actual setup purchase quote")
|
|
command(&"purchase_supplies", {&"warehouse_id": facility_ids[&"warehouse"],
|
|
&"item_id": SupplyCatalog.RAW_FOOD, &"quantity": purchase_quantity})
|
|
command(&"set_kitchen_production", {&"facility_id": facility_ids[&"kitchen"],
|
|
&"enabled": true, &"target_stock": 8})
|
|
# The OS observation uses the already declared transport cohort, plus
|
|
# ordinary public arrivals whose service choice remains Runtime-owned.
|
|
# These commands are recorded and identical across A/B; no actor is moved,
|
|
# despawned, or inserted into a room/queue by the fixture.
|
|
var demand: Dictionary = Comparison.new().declaration()
|
|
expect(stream.record("observation-demand", {"cohort": demand[&"arrivals"],
|
|
"schedule": "all observation actors at the recorded current minute; offsets apply only to native layout comparison",
|
|
"minute": clock.absolute_minute}), "declared observation demand")
|
|
for arrival: Dictionary in demand[&"arrivals"]:
|
|
command(&"spawn_guest", {&"guest_id": StringName("os-" + String(arrival[&"id"])),
|
|
&"guest_type_id": &"traveler", &"gold": 100,
|
|
&"options": {&"current_cell": Comparison.START, &"state": GuestRuntime.State.MOVING,
|
|
&"state_entered_at": clock.absolute_minute - 1, &"planned_exit_at": clock.absolute_minute + 2000,
|
|
&"path_handle": {&"target_cell": Comparison.GOAL}}})
|
|
command(&"spawn_guest", {&"guest_id": StringName("service-" + String(arrival[&"id"])),
|
|
&"guest_type_id": &"traveler", &"gold": 100})
|
|
await advance(120)
|
|
|
|
func layer_observations(prefix: String) -> void:
|
|
await button(main.front_button)
|
|
expect(await stream.await_draws(tree, main.world_preview), "real draw history before capture")
|
|
expect(stream.capture(prefix + "-FRONT", runtime, main.world_preview), "FRONT state/report")
|
|
await button(main.back_button)
|
|
expect(await stream.await_draws(tree, main.world_preview), "real draw history before capture")
|
|
expect(stream.capture(prefix + "-BACK", runtime, main.world_preview), "BACK state/report")
|
|
|
|
func writer() -> void:
|
|
await prepare()
|
|
await layer_observations("writer")
|
|
capture("presave")
|
|
await button(main.find_child("ManualSaveButton", true, false))
|
|
expect(main.build_status_label.text == "手動セーブを保存しました。", "normal save")
|
|
capture("checkpoint")
|
|
export_package(String(main.manual_save_path), "formal")
|
|
var formal := inspect_formal()
|
|
var presave := FileAccess.get_file_as_bytes(directory.path_join("writer-presave.var"))
|
|
expect(presave == var_to_bytes(formal), "whole presave formal model")
|
|
expect(not operation_results.is_empty(), "save result recorded")
|
|
if not operation_results.is_empty():
|
|
verify_operation(formal, operation_results.back(), &"SAVE_CHECKPOINT_SUCCEEDED")
|
|
var members := {}
|
|
for member: String in MEMBERS:
|
|
members[member] = FileAccess.get_sha256(directory.path_join("formal/" + member))
|
|
json_file("save.json", {"pid": OS.get_process_id(), "members": members})
|
|
|
|
func reader(distinct: bool) -> void:
|
|
var saved: Dictionary = JSON.parse_string(FileAccess.get_file_as_string(directory.path_join("save.json")))
|
|
expect(distinct and saved.pid != OS.get_process_id(), "distinct actual reader PID")
|
|
await load_menu(main.find_child("SaveLoadMenu", true, false))
|
|
await frames()
|
|
expect(main.build_status_label.text == "手動セーブを読み込みました。", "normal resume")
|
|
capture("checkpoint")
|
|
var formal := inspect_formal()
|
|
expect(FileAccess.get_file_as_bytes(directory.path_join("writer-formal-model.var")) == var_to_bytes(formal), "whole formal resume model")
|
|
for member: String in MEMBERS:
|
|
expect(FileAccess.get_sha256(directory.path_join("formal/" + member)) == saved.members[member], "formal member " + member)
|
|
expect(not operation_results.is_empty(), "load result recorded")
|
|
if not operation_results.is_empty():
|
|
verify_operation(formal, operation_results.back(), &"SAVE_RECOVERY_LOADED")
|
|
await layer_observations("reader")
|
|
|
|
func finish_observations() -> void:
|
|
stream.close()
|