extends RefCounted ## P07-01 F7: the fixture and minute script shared by the restart writer ## (tests/environment/test_p07_weather_restart_1_write.gd) and reader ## (tests/environment/test_p07_weather_restart_2_read.gd), which run as separate ## engine processes. Seed 1106 opens day 0 with rain 06:00-12:00 (A13), a guest ## due at 06:02 is held to 10:02, production arrivals run, and day 1 draws at 06:00. ## With no facility in this world the held guest starts leaving at 06:03 (still ## present with its hold) and is gone at 06:04 (its hold leaves with it). ## The fog scene (WX7): seed 1592 opens day 0 with mana fog 06:00-10:00, two ## adult-verified guests arrive at 06:58 and 09:58 (with no facility in this ## world a guest leaves after a few minutes, so each one is present at its ## saves), and the saves bracket the 07:00 mana boundary and the fog end. const HANDOFF_PATH := "res://artifacts/p07-01/restart-handoff.json" const HANDOFF_SCHEMA := "p07-01-restart-handoff-v2" const WRITER_SOURCE := "res://tests/environment/test_p07_weather_restart_1_write.gd" const WRITER_SAVE := "user://p07_restart_write/minute_%d" const READER_SAVE := "user://p07_restart_read/minute_%d" const SEED := 1106 const FIRST_MINUTE := 360 # The held guest present with its hold, the minute it left, mid-rain, just before and # just after the rain end, just before and just after the day 1 draw. const SAVE_MINUTES: Array[int] = [363, 364, 500, 719, 720, 1799, 1800] const HOLD := {"original_exit": 362, "extended_to": 602} const LAST_MINUTE := 1830 const DRAW_MINUTES: Array[int] = [1800] const HELD_GUEST := &"p07_restart_guest" const FOG_SEED := 1592 const FOG_WRITER_SAVE := "user://p07_restart_write/fog_minute_%d" const FOG_READER_SAVE := "user://p07_restart_read/fog_minute_%d" # Just before, at and after the 07:00 mana boundary in fog; just before, at and after the fog end. const FOG_SAVE_MINUTES: Array[int] = [419, 420, 421, 599, 600, 601] const FOG_LAST_MINUTE := 630 const FOG_ADULTS := {418: &"p07_restart_fog_adult_a", 598: &"p07_restart_fog_adult_b"} var _expect: Callable func _init(expect: Callable) -> void: _expect = expect func new_fixture(with_guest := true, seed := SEED) -> 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") var fixture := {&"clock": clock, &"state": state, &"runtime": runtime, &"draws": {&"count": 0}, &"seed": seed} if with_guest and seed == SEED: var result: CommandResult = runtime.submit_command(&"spawn_guest", { &"guest_id": HELD_GUEST, &"guest_type_id": &"traveler", &"gold": 100, &"options": { &"current_cell": Vector3i(0, 0, WorldService.LAYER_FRONT), &"needs": {&"hunger": 10, &"sleep": 10, &"hygiene": 10, &"medical": 0, &"leisure": 10, &"intimacy": 0}, &"planned_exit_at": 362, }, }, &"world", HELD_GUEST, 10, -1, &"p07.restart.guest") _expect.call(result.is_success(), "fixture spawns the guest due at 06:02") watch_draws(fixture) return fixture func watch_draws(fixture: Dictionary) -> void: var draws: Dictionary = fixture[&"draws"] fixture[&"runtime"].domain_events.subscribe(&"WEATHER_FORECAST_DRAWN", &"p07_restart", func(_event: SimulationDomainEvent) -> void: draws[&"count"] += 1) func free_fixture(fixture: Dictionary) -> void: for key: StringName in [&"runtime", &"clock", &"state"]: if fixture.get(key) != null and is_instance_valid(fixture[key]): fixture[key].free() ## Advances one minute at a time after `after_minute` through `through_minute` ## and calls `on_minute` after each minute (the writer saves there). In the fog ## scene the adult-verified guests arrive at their minutes in every run. func play(fixture: Dictionary, after_minute: int, through_minute: int, on_minute: Callable) -> void: var clock: Variant = fixture[&"clock"] _expect.call(clock.absolute_minute == after_minute, "play starts at minute %d" % after_minute) for minute: int in range(after_minute + 1, through_minute + 1): clock.advance_minutes(1) if fixture[&"seed"] == FOG_SEED and FOG_ADULTS.has(minute): _spawn_adult(fixture[&"runtime"], FOG_ADULTS[minute]) if on_minute.is_valid(): on_minute.call(minute) func _spawn_adult(runtime: Variant, guest_id: StringName) -> void: var result: CommandResult = runtime.submit_command(&"spawn_guest", { &"guest_id": guest_id, &"guest_type_id": &"adventurer", &"gold": 100, &"options": { &"current_cell": Vector3i(0, 0, WorldService.LAYER_FRONT), &"needs": {&"hunger": 10, &"sleep": 10, &"hygiene": 10, &"medical": 0, &"leisure": 10, &"intimacy": 0}, &"planned_exit_at": 2000, }, }, &"world", guest_id, 10, -1, StringName("p07.restart." + String(guest_id))) _expect.call(result.is_success(), "the fog scene spawns the adult-verified guest %s" % guest_id) ## The weather-facing state compared across processes: weather and its stream, ## guest plans and needs (the fog intimacy), the whole reserve with its last ## flow (the fog yield) and the arrival totals. Needs are listed in text order of ## their names, because a loaded guest may keep its needs in another key order ## (StringName ordering is not text ordering and differs between processes). func state_value(runtime: Variant) -> Dictionary: var plans := {} var needs := {} for guest: GuestRuntime in runtime.guest_service.get_guests(): plans[String(guest.guest_id)] = guest.planned_exit_at var names: Array[String] = [] for name: Variant in guest.needs: names.append(String(name)) names.sort() var row := {} for name: String in names: row[name] = guest.needs[name] needs[String(guest.guest_id)] = row var arrivals := 0 for report: Dictionary in runtime.operations_coordinator.get_arrival_reports(): arrivals += int(report[&"total_count"]) return { "minute": runtime.weather.get_minute(), "weather": runtime.weather.create_snapshot(), "stream": runtime.get_rng_stream(&"weather").snapshot(), "plans": plans, "needs": needs, "reserve": runtime.mana_reserve.create_snapshot(), "reserve_flow": runtime.mana_reserve.get_last_flow(), "arrivals": arrivals, } func state_bytes(runtime: Variant) -> PackedByteArray: return var_to_bytes(state_value(runtime)) static func sha256(bytes: PackedByteArray) -> String: var context := HashingContext.new() context.start(HashingContext.HASH_SHA256) context.update(bytes) return context.finish().hex_encode()