extends SceneTree ## P07-01 (rain, mana fog and natural weather) F1-F7 on the real Runtime with the ## approved initial values A13 (SPEC sections 4 and 5). F6 reads the ordinary Main ## HUD. The seeds open day 0 with clear (1000), rain (1106), mana fog (1592) and ## storm (2501) starting at 06:00; the draws are asserted, not assumed. const NaturalWeather := preload("res://scripts/environment/natural_weather_policy.gd") const WeatherTimeline := preload("res://scripts/environment/weather_timeline.gd") const ManaReserveCycle := preload("res://scripts/mana/mana_reserve_cycle.gd") const AudioTeardown := preload("res://tests/helpers/ui_audio_teardown.gd") const SEED_CLEAR := 1000 const SEED_RAIN := 1106 const SEED_FOG := 1592 const SEED_STORM := 2501 var _checks := 0 var _failures := 0 var _fixtures: Array[Dictionary] = [] func _initialize() -> void: call_deferred("_run") func _run() -> void: print("P07-01: natural weather runtime") _test_policy_draws() _test_daily_natural_draw() _test_rain_arrivals() _test_rain_stay_and_saves() _test_rain_hold_exit() _test_storm() _test_mana_fog() _test_migration_and_rejection() await _test_hud_forecast() _release_fixtures() if _failures == 0: print("PASS: %d checks" % _checks) else: push_error("FAIL: %d of %d checks failed" % [_failures, _checks]) quit(_failures) func _new_runtime(seed: int, hour := 6, minute := 0) -> Dictionary: var runtime: Variant = load("res://autoload/simulation_runtime.gd").new() var clock: Variant = load("res://autoload/game_clock.gd").new() var state: Variant = load("res://autoload/game_state.gd").new() clock.reset(1, hour, minute) state.reset() _expect(runtime.initialize(clock, state, seed)[&"valid"], "initialize Runtime with seed %d" % seed) clock.set_paused(true) var fixture := {&"runtime": runtime, &"clock": clock, &"state": state} _fixtures.append(fixture) return fixture func _release_fixtures() -> void: for fixture: Dictionary in _fixtures: for key: StringName in [&"runtime", &"clock", &"state"]: if is_instance_valid(fixture[key]): fixture[key].free() _fixtures.clear() func _drawn_events(runtime: Variant) -> Array: var found: Array = [] for event: SimulationDomainEvent in runtime.domain_events.recent_events(): if event.event_type == &"WEATHER_FORECAST_DRAWN": found.append(event.payload.duplicate(true)) return found func _spawn(runtime: Variant, guest_id: StringName, guest_type: StringName, planned_exit_at: int) -> Variant: var result: CommandResult = runtime.submit_command(&"spawn_guest", { &"guest_id": guest_id, &"guest_type_id": guest_type, &"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": planned_exit_at, }, }, &"world", guest_id, 10, -1, StringName("p07." + String(guest_id))) _expect(result.is_success(), "spawn %s" % guest_id) return runtime.guest_service.get_guest(guest_id) ## F1: the A13 draw rule, determinism and the once-per-day timeline rule. func _test_policy_draws() -> void: var rng := DeterministicRng.from_root_seed(77, &"weather") as DeterministicRng var counts := {&"clear": 0, &"rain": 0, &"mana_fog": 0, &"storm": 0} var rule_ok := true for day in range(2000): var draw: Dictionary = NaturalWeather.draw(rng, day) var kind: StringName = draw[&"kind"] counts[kind] += 1 var roll: int = draw[&"roll"] var expected_kind: StringName = &"clear" if roll < 60 else (&"rain" if roll < 85 else (&"mana_fog" if roll < 95 else &"storm")) rule_ok = rule_ok and kind == expected_kind and roll >= 0 and roll <= 99 if kind == &"clear": rule_ok = rule_ok and draw[&"slot"] == -1 and draw[&"start"] == -1 and draw[&"end"] == -1 else: var offset: int = int(draw[&"start"]) - day * 1440 - 360 rule_ok = rule_ok and offset >= 0 and offset <= 14 * 60 and offset % 30 == 0 and offset / 30 == int(draw[&"slot"]) \ and int(draw[&"end"]) - int(draw[&"start"]) == int(NaturalWeather.DURATION_MINUTES[kind]) _expect(rule_ok, "every draw maps rolls 0-59/60-84/85-94/95-99 to clear/rain/fog/storm, starts 06:00-20:00 in 30-minute steps and lasts 6/4/2 hours") _expect(counts[&"clear"] > 1100 and counts[&"clear"] < 1300 and counts[&"rain"] > 400 and counts[&"rain"] < 600 and counts[&"mana_fog"] > 140 and counts[&"mana_fog"] < 260 and counts[&"storm"] > 60 and counts[&"storm"] < 140, "2000 draws follow 60/25/10/5 percent (%s)" % str(counts)) var left := DeterministicRng.from_root_seed(5, &"weather") as DeterministicRng var right := DeterministicRng.from_root_seed(5, &"weather") as DeterministicRng var same := true for day in range(60): same = same and NaturalWeather.draw(left, day) == NaturalWeather.draw(right, day) _expect(same, "the same seed draws the same weather list") var timeline := WeatherTimeline.new() _expect(timeline.initialize(&"clear", 360) and timeline.get_last_draw_day() == -1, "a timeline opened at 06:00 still owes that day's draw") var rain := {&"day": 0, &"kind": &"rain", &"roll": 70, &"slot": 2, &"start": 420, &"end": 780} _expect(timeline.apply_draw(rain)[&"ok"] and timeline.get_last_draw_day() == 0 and timeline.next_change() == {&"kind": &"rain", &"minute": 420}, "a drawn rain is scheduled with its return to clear") _expect(timeline.apply_draw(rain)[&"code"] == &"DRAW_NOT_DUE", "a second draw for the same day is refused") var later := WeatherTimeline.new() later.initialize(&"clear", 361) _expect(later.apply_draw(rain)[&"code"] == &"DRAW_NOT_DUE", "a draw outside its 06:00 is refused") ## F1: one draw at 06:00 on the saved stream, recorded as WEATHER_FORECAST_DRAWN. func _test_daily_natural_draw() -> void: var opening := _new_runtime(SEED_RAIN) var runtime: Variant = opening[&"runtime"] var today: Dictionary = runtime.weather.get_today_draw() _expect(String(today.get("kind", "")) == "rain" and int(today.get("start", -1)) == 360 and int(today.get("end", -1)) == 720, "seed 1106 opens day 0 with rain 06:00-12:00 (%s)" % str(today)) _expect(runtime.weather.get_kind() == &"rain" and runtime.weather.get_last_draw_day() == 0, "a start at the draw minute switches at once") var events := _drawn_events(runtime) _expect(events.size() == 1 and int(events[0][&"day"]) == 0 and String(events[0][&"kind"]) == "rain" and bool(events[0][&"applied"]), "the opening draw is recorded once") runtime.call("_draw_natural_weather_if_due", 360) _expect(_drawn_events(runtime).size() == 1, "the same minute does not draw again") var twin: Variant = _new_runtime(SEED_RAIN)[&"runtime"] _expect(twin.weather.create_snapshot() == runtime.weather.create_snapshot() and twin.get_rng_stream(&"weather").snapshot() == runtime.get_rng_stream(&"weather").snapshot(), "the same seed gives the same weather and stream") var early := _new_runtime(SEED_RAIN, 5, 50) var early_runtime: Variant = early[&"runtime"] _expect(early_runtime.weather.get_today_draw().is_empty() and _drawn_events(early_runtime).is_empty(), "a game opened before 06:00 waits for the boundary") early[&"clock"].advance_minutes(10) _expect(early_runtime.weather.get_today_draw() == today and early_runtime.weather.get_kind() == &"rain", "the 06:00 boundary draws the same day from the same stream") var daily := _new_runtime(SEED_CLEAR) var daily_runtime: Variant = daily[&"runtime"] _expect(String(daily_runtime.weather.get_today_draw().get("kind", "")) == "clear" and daily_runtime.weather.get_kind() == &"clear", "seed 1000 opens day 0 clear") var seen := {&"count": 0} daily_runtime.domain_events.subscribe(&"WEATHER_FORECAST_DRAWN", &"p07_test", func(_event: SimulationDomainEvent) -> void: seen[&"count"] += 1) daily[&"clock"].advance_minutes(1439) _expect(seen[&"count"] == 0 and daily_runtime.weather.get_last_draw_day() == 0, "no draw before the next 06:00") daily[&"clock"].advance_minutes(1) _expect(seen[&"count"] == 1 and daily_runtime.weather.get_last_draw_day() == 1 and int(daily_runtime.weather.get_today_draw().get("day", -1)) == 1, "the next 06:00 draws day 1 once") ## F2: rain halves the planned new arrivals of a boundary; the dropped ones are not carried. func _test_rain_arrivals() -> void: var clear_side := _new_runtime(SEED_CLEAR) var rain_side := _new_runtime(SEED_CLEAR) var clear_runtime: Variant = clear_side[&"runtime"] var rain_runtime: Variant = rain_side[&"runtime"] _expect(clear_runtime.enable_production_operations()[&"ok"] and rain_runtime.enable_production_operations()[&"ok"], "enable production arrivals on both sides") _expect(rain_runtime.weather.schedule(&"rain", 361)[&"ok"] and rain_runtime.weather.schedule(&"clear", 1200)[&"ok"], "rain from 06:01 on one side only") var compared := false for _minute in range(300): clear_side[&"clock"].advance_minutes(1) rain_side[&"clock"].advance_minutes(1) var clear_reports: Array = clear_runtime.operations_coordinator.get_arrival_reports() var rain_reports: Array = rain_runtime.operations_coordinator.get_arrival_reports() # Only boundaries inside the rain (from 06:01) are compared. if clear_reports.is_empty() or int(clear_reports[-1][&"minute"]) <= 360 or int(clear_reports[-1][&"total_count"]) < 2: continue var minute: int = clear_reports[-1][&"minute"] var clear_total: int = clear_reports[-1][&"total_count"] _expect(not rain_reports.is_empty() and int(rain_reports[-1][&"minute"]) == minute and int(rain_reports[-1][&"total_count"]) == clear_total / 2, "rain plans floor(%d / 2) arrivals at minute %d" % [clear_total, minute]) var carry_same := true for profile: Dictionary in clear_runtime.operations_coordinator.get_demand_profiles(): carry_same = carry_same and clear_runtime.operations_coordinator.get_arrival_carry(profile[&"facility_tag"], profile[&"guest_class"], profile[&"time_band"]) \ == rain_runtime.operations_coordinator.get_arrival_carry(profile[&"facility_tag"], profile[&"guest_class"], profile[&"time_band"]) _expect(carry_same, "arrivals lost to rain are not carried to the next boundary") compared = true break _expect(compared, "an arrival boundary with at least two arrivals was compared") var rain_capture: Dictionary = rain_runtime.call("_operations_arrival_boundary", {}, OperationsCoordinator.PHASE_CAPTURE, null) var clear_capture: Dictionary = clear_runtime.call("_operations_arrival_boundary", {}, OperationsCoordinator.PHASE_CAPTURE, null) _expect(int(rain_capture[&"arrival_percent"]) == 50 and int(clear_capture[&"arrival_percent"]) == 100, "the arrival boundary reports the rain share") ## F3 and F7 (same process): a guest due to leave in rain stays once, up to the rain end or 240 minutes. func _test_rain_stay_and_saves() -> void: var rainy := _new_runtime(SEED_RAIN) var runtime: Variant = rainy[&"runtime"] var guest: Variant = _spawn(runtime, &"p07_rain_guest", &"traveler", 362) # Spawned 06:00, entering 06:01, deciding 06:02, first exit check at 06:03. rainy[&"clock"].advance_minutes(3) var hold: Dictionary = runtime.weather.get_rain_hold(&"p07_rain_guest") _expect(hold == {"original_exit": 362, "extended_to": 602} and guest.planned_exit_at == 602, "a guest due at 06:02 in rain stays to 10:02 (+240, before the rain end 12:00) (%s)" % str(hold)) _expect(guest.exit_reason != &"PLANNED_EXIT", "the held guest did not leave at its first plan") _expect(not runtime.call("_hold_guest_for_rain", guest, 400), "a guest is held only once") var late: Variant = _spawn(runtime, &"p07_late_guest", &"traveler", 650) _expect(runtime.call("_hold_guest_for_rain", late, 650) and late.planned_exit_at == 720, "a guest due at 10:50 stays to the rain end 12:00") var saved: Dictionary = runtime.create_snapshot() _expect(runtime.restore_snapshot(saved) and runtime.create_snapshot() == saved, "rain, holds and the stream restore exactly in the same process") var path := "user://tests/p07_weather_%d" % OS.get_process_id() var weather_before: Dictionary = runtime.weather.create_snapshot() var stream_before: Dictionary = runtime.get_rng_stream(&"weather").snapshot() _expect(bool(runtime.save_to_path(path).get(&"ok", false)) and bool(runtime.load_from_path(path).get(&"ok", false)) and runtime.weather.create_snapshot() == weather_before and runtime.get_rng_stream(&"weather").snapshot() == stream_before and runtime.guest_service.get_guest(&"p07_late_guest").planned_exit_at == 720, "an ordinary save file restores rain, holds and the weather stream") SaveService.delete_save(path) # The ordinary save operation records its own lifecycle; later checks compare with the loaded state. saved = runtime.create_snapshot() for bad_hold: Dictionary in [{"original_exit": 362, "extended_to": 603}, {"original_exit": 362, "extended_to": 362}]: var malformed := saved.duplicate(true) malformed[&"systems"][&"runtime"][&"weather"][&"rain_holds"]["p07_rain_guest"] = bad_hold _expect(not runtime.restore_snapshot(malformed) and runtime.create_snapshot() == saved, "a hold beyond 240 minutes or not extending is refused atomically (%s)" % str(bad_hold)) var ghost := saved.duplicate(true) ghost[&"systems"][&"runtime"][&"weather"][&"rain_holds"]["nobody"] = {"original_exit": 400, "extended_to": 500} _expect(not runtime.restore_snapshot(ghost) and runtime.create_snapshot() == saved, "a hold of a missing guest is refused atomically") var unmatched := saved.duplicate(true) unmatched[&"systems"][&"runtime"][&"weather"][&"rain_holds"]["p07_late_guest"] = {"original_exit": 650, "extended_to": 700} _expect(not runtime.restore_snapshot(unmatched) and runtime.create_snapshot() == saved, "a hold that differs from the guest plan is refused atomically") var clear_runtime: Variant = _new_runtime(SEED_CLEAR)[&"runtime"] var dry: Variant = _spawn(clear_runtime, &"p07_dry_guest", &"traveler", 361) _expect(not clear_runtime.call("_hold_guest_for_rain", dry, 361) and dry.planned_exit_at == 361, "no hold without rain") _expect(clear_runtime.weather.schedule(&"storm", 361)[&"ok"], "schedule a storm") clear_runtime.weather.advance_to(361) _expect(not clear_runtime.call("_hold_guest_for_rain", dry, 361), "no hold in a storm") var fog_runtime: Variant = _new_runtime(SEED_FOG)[&"runtime"] var misty: Variant = _spawn(fog_runtime, &"p07_fog_due_guest", &"traveler", 361) _expect(fog_runtime.weather.get_kind() == &"mana_fog" and not fog_runtime.call("_hold_guest_for_rain", misty, 361) and misty.planned_exit_at == 361, "no hold in mana fog") ## F3: after its hold the guest leaves by the current rule. A guest without a ## suitable facility leaves at its next decision, so the ordinary decision step ## itself runs at the extended time: it is not held again and begins leaving ## with the ordinary PLANNED_EXIT. func _test_rain_hold_exit() -> void: var natural := _new_runtime(SEED_RAIN) var natural_runtime: Variant = natural[&"runtime"] var departing: Variant = _spawn(natural_runtime, &"p07_departing_guest", &"traveler", 362) natural[&"clock"].advance_minutes(3) _expect(natural_runtime.weather.get_rain_hold(&"p07_departing_guest") == {"original_exit": 362, "extended_to": 602} and departing.state == GuestRuntime.State.LEAVING and departing.exit_reason == &"NO_SUITABLE_FACILITY", "in ordinary minutes the held guest still leaves by the current rules") natural[&"clock"].advance_minutes(1) var departed: Dictionary = natural_runtime.create_snapshot() _expect(natural_runtime.guest_service.get_guest(&"p07_departing_guest") == null and natural_runtime.weather.get_rain_holds().is_empty() and natural_runtime.restore_snapshot(departed) and natural_runtime.create_snapshot() == departed, "the hold leaves with the guest, so the minute it left restores") var rainy := _new_runtime(SEED_RAIN) var runtime: Variant = rainy[&"runtime"] var guest: Variant = _spawn(runtime, &"p07_exit_guest", &"traveler", 362) rainy[&"clock"].advance_minutes(2) _expect(guest.state == GuestRuntime.State.DECIDING and runtime.weather.get_kind() == &"rain", "the guest decides at 06:02 in rain") _expect(runtime.call("_hold_guest_for_rain", guest, 362) and guest.planned_exit_at == 602 and runtime.weather.get_rain_hold(&"p07_exit_guest") == {"original_exit": 362, "extended_to": 602}, "the rain holds the guest due at 06:02 to 10:02") runtime.call("_advance_guest_ai_decisions", 602) _expect(guest.exit_reason == &"PLANNED_EXIT" and guest.state == GuestRuntime.State.LEAVING and guest.planned_exit_at == 602, "at 10:02 the held guest's decision begins leaving with PLANNED_EXIT (%s)" % String(guest.exit_reason)) ## F4: natural storm keeps zero new arrivals. func _test_storm() -> void: var stormy := _new_runtime(SEED_STORM) var runtime: Variant = stormy[&"runtime"] _expect(runtime.weather.get_kind() == &"storm" and String(runtime.weather.get_today_draw().get("kind", "")) == "storm" and int(runtime.weather.get_today_draw().get("end", -1)) == 480, "seed 2501 opens day 0 with a natural storm 06:00-08:00") _expect(runtime.enable_production_operations()[&"ok"], "enable production arrivals in the storm") var capture: Dictionary = runtime.call("_operations_arrival_boundary", {}, OperationsCoordinator.PHASE_CAPTURE, null) _expect(int(capture[&"capacity_remaining"]) == 0, "a natural storm exposes zero admission capacity") var spawned := {&"count": 0} runtime.guest_service.guest_spawned.connect(func(_guest: GuestRuntime) -> void: spawned[&"count"] += 1) stormy[&"clock"].advance_minutes(119) _expect(spawned[&"count"] == 0 and runtime.weather.get_kind() == &"storm", "no new guest enters during the natural storm") stormy[&"clock"].advance_minutes(1) _expect(runtime.weather.get_kind() == &"clear", "the storm returns to clear at 08:00") ## F5: mana fog x1.2 world tree and furnace yield (floor), adult intimacy increase x1.5 (floor). func _test_mana_fog() -> void: var foggy := _new_runtime(SEED_FOG) var fog_runtime: Variant = foggy[&"runtime"] var clear_side := _new_runtime(SEED_CLEAR) var clear_runtime: Variant = clear_side[&"runtime"] _expect(fog_runtime.weather.get_kind() == &"mana_fog" and int(fog_runtime.weather.get_today_draw().get("end", -1)) == 600, "seed 1592 opens day 0 with mana fog 06:00-10:00") _expect(int(fog_runtime.get_mana_reserve_report()[&"generation"]) == 24 and int(clear_runtime.get_mana_reserve_report()[&"generation"]) == 20, "the reserve report shows yield 24 in fog and 20 in clear") foggy[&"clock"].advance_minutes(10) clear_side[&"clock"].advance_minutes(10) _expect(int(fog_runtime.mana_reserve.get_last_flow().get(&"generation", -1)) == 24 and int(clear_runtime.mana_reserve.get_last_flow().get(&"generation", -1)) == 20, "the 06:10 boundary adds 24 in fog and 20 in clear") var fog_saved: Dictionary = fog_runtime.create_snapshot() _expect(fog_runtime.restore_snapshot(fog_saved) and fog_runtime.create_snapshot() == fog_saved, "a fog boundary flow restores") var reserve := ManaReserveCycle.new() var bad_reserve: Dictionary = fog_saved[&"systems"][&"runtime"][&"mana_reserve"].duplicate(true) bad_reserve[&"last_flow"][&"generation"] = 23 _expect(not reserve.restore_snapshot(bad_reserve), "a yield that is neither 20 nor 24 is refused") _expect(ManaReserveCycle.furnace_offer_accepted(160, 0) and not ManaReserveCycle.furnace_offer_accepted(160, 0, ManaReserveCycle.FOG_WORLD_TREE_GENERATION, ManaReserveCycle.FOG_FURNACE_YIELD), "a fog boundary judges the furnace with yield 24 and 120") _expect(int(ManaReserveCycle.plan(100, 0, ManaReserveCycle.FOG_FURNACE_YIELD, 0, 10, ManaReserveCycle.FOG_WORLD_TREE_GENERATION)[&"after"]) == 244 and int(ManaReserveCycle.plan(200, 0, 120, 0, 10, 24)[&"after"]) == 300, "a fog furnace adds 120 and capacity 300 stays") foggy[&"clock"].advance_minutes(230) _expect(fog_runtime.weather.get_kind() == &"clear" and int(fog_runtime.mana_reserve.get_last_flow().get(&"generation", -1)) == 20, "after the fog the yield is 20 again") var adult: Variant = _spawn(fog_runtime, &"p07_adult", &"adventurer", 2000) var minor: Variant = _spawn(fog_runtime, &"p07_other", &"traveler", 2000) var fog_report: Dictionary = fog_runtime.guest_service.update_needs(&"p07_adult", {&"intimacy_day": 3, &"mana_fog": true}) var plain_report: Dictionary = fog_runtime.guest_service.update_needs(&"p07_adult", {&"intimacy_day": 3, &"mana_fog": false}) var other_report: Dictionary = fog_runtime.guest_service.update_needs(&"p07_other", {&"intimacy_day": 3, &"mana_fog": true}) var down_report: Dictionary = fog_runtime.guest_service.update_needs(&"p07_adult", {&"need_effects": {&"intimacy": -4}, &"mana_fog": true}) _expect(int(fog_report[&"deltas"]["intimacy"]) == 4 and int(plain_report[&"deltas"]["intimacy"]) == 3, "fog raises an adult-verified increase of 3 to floor(4.5) = 4") _expect(int(other_report[&"deltas"]["intimacy"]) == 0 and int(down_report[&"deltas"]["intimacy"]) == -4, "fog leaves guests without adult verification and decreases unchanged") _expect(not fog_runtime.guest_service.update_needs(&"p07_adult", {&"mana_fog": 1})[&"ok"], "a non-boolean fog context is refused") var vital_fog := _new_runtime(SEED_FOG) var vital_clear := _new_runtime(SEED_CLEAR) var fog_guest: Variant = _spawn(vital_fog[&"runtime"], &"p07_fog_adult", &"adventurer", 2000) var clear_guest: Variant = _spawn(vital_clear[&"runtime"], &"p07_clear_adult", &"adventurer", 2000) vital_fog[&"clock"].advance_minutes(1) vital_clear[&"clock"].advance_minutes(1) _expect(int(fog_guest.needs[&"intimacy"]) == 4 and int(clear_guest.needs[&"intimacy"]) == 3, "the ordinary minute raises intimacy by 4 in fog and 3 in clear (day)") _expect(adult != null and minor != null, "fog guests exist") ## F1 and F7: a pre-natural save migrates without a past draw; malformed natural state is refused. func _test_migration_and_rejection() -> void: var opened := _new_runtime(SEED_RAIN) var runtime: Variant = opened[&"runtime"] opened[&"clock"].advance_minutes(5) var saved: Dictionary = runtime.create_snapshot() var legacy := saved.duplicate(true) var legacy_weather: Dictionary = legacy[&"systems"][&"runtime"][&"weather"] legacy_weather.erase(&"natural") legacy_weather.erase(&"rain_holds") legacy_weather[&"version"] = 1 legacy[&"systems"][&"runtime"][&"rng_streams"].erase("weather") _expect(runtime.restore_snapshot(legacy), "a version 1 weather save without the weather stream loads") _expect(runtime.weather.get_kind() == &"rain" and runtime.weather.get_last_draw_day() == 0 and runtime.weather.get_today_draw().is_empty() and runtime.weather.get_rain_holds().is_empty(), "the migration keeps the current weather and does not draw for the past") _expect(runtime.get_rng_stream(&"weather").snapshot() == DeterministicRng.from_root_seed(SEED_RAIN, &"weather").snapshot(), "the migrated weather stream starts from the world seed") var migrated: Dictionary = runtime.create_snapshot() var missing_stream := saved.duplicate(true) missing_stream[&"systems"][&"runtime"][&"rng_streams"].erase("weather") _expect(not runtime.restore_snapshot(missing_stream) and runtime.create_snapshot() == migrated, "a natural-weather save without its stream is refused atomically") var wrong_day := saved.duplicate(true) wrong_day[&"systems"][&"runtime"][&"weather"][&"natural"][&"last_draw_day"] = 1 _expect(not runtime.restore_snapshot(wrong_day) and runtime.create_snapshot() == migrated, "a draw day after the saved minute is refused atomically") var unknown_kind := saved.duplicate(true) unknown_kind[&"systems"][&"runtime"][&"weather"][&"natural"][&"today"]["kind"] = "snow" _expect(not runtime.restore_snapshot(unknown_kind) and runtime.create_snapshot() == migrated, "an unknown drawn kind is refused atomically") var overlap := saved.duplicate(true) overlap[&"systems"][&"runtime"][&"weather"][&"pending"] = [{"kind": "storm", "minute": 700}, {"kind": "clear", "minute": 700}] _expect(not runtime.restore_snapshot(overlap) and runtime.create_snapshot() == migrated, "overlapping weather changes are refused atomically") # The saved rain draw of seed 1106 (roll 60-84, slot 0, 06:00-12:00) changed in one field # each: a roll of a clear day, a slot that does not match the start, a wrong duration. var today: Dictionary = saved[&"systems"][&"runtime"][&"weather"][&"natural"][&"today"] _expect(NaturalWeather.is_consistent_draw(today) and String(today["kind"]) == "rain" and int(today["slot"]) == 0, "the saved opening draw follows the draw rule") for change: Dictionary in [{"roll": 10}, {"slot": 1}, {"end": 700}]: var inconsistent := saved.duplicate(true) var drawn: Dictionary = inconsistent[&"systems"][&"runtime"][&"weather"][&"natural"][&"today"] for key: String in change: drawn[key] = change[key] _expect(not runtime.restore_snapshot(inconsistent) and runtime.create_snapshot() == migrated, "an internally inconsistent saved draw is refused atomically (%s)" % str(change)) ## F6: the ordinary Main HUD shows the weather, the forecast and the effect guidance. func _test_hud_forecast() -> void: var clock: Variant = root.get_node("GameClock") var state: Variant = root.get_node("GameState") var runtime: Variant = root.get_node("SimulationRuntime") clock.reset(1, 6, 0) state.reset() _expect(runtime.initialize(clock, state, SEED_RAIN)[&"valid"], "initialize the ordinary Runtime with seed 1106") clock.set_paused(true) var main := load("res://scenes/main/main.tscn").instantiate() as Control main.set("manual_save_path", "user://tests/p07_weather_main_%d/manual_ui" % OS.get_process_id()) root.add_child(main) await process_frame await process_frame main.call("_refresh_dashboard") var weather_label := main.get("weather_label") as Label var forecast_label := main.get("weather_forecast_label") as Label var report: Dictionary = runtime.get_weather_report() _expect(weather_label.text == "雨" and report[&"kind"] == &"rain", "HUD shows the Runtime rain") _expect(forecast_label != null and forecast_label.text == "次: 晴れ 12:00" and report[&"next_change"] == {&"kind": &"clear", &"minute": 720}, "HUD forecast shows the next change from the Runtime (%s)" % (forecast_label.text if forecast_label != null else "none")) _expect(String(report[&"effect_text"]) in weather_label.tooltip_text and "新規来客が半分" in weather_label.tooltip_text, "HUD guidance explains the rain effect") clock.advance_minutes(360) main.call("_refresh_dashboard") _expect(weather_label.text == "晴れ" and forecast_label.text == "次: 予定なし" and "影響はありません" in weather_label.tooltip_text, "HUD follows the end of the rain") var now: int = clock.absolute_minute _expect(runtime.weather.schedule(&"storm", now + 1)[&"ok"] and runtime.weather.schedule(&"mana_fog", now + 2)[&"ok"], "schedule a storm and then mana fog on the ordinary Runtime") clock.advance_minutes(1) main.call("_refresh_dashboard") report = runtime.get_weather_report() var fog_minute := posmod(now + 2, 1440) _expect(weather_label.text == "嵐" and report[&"kind"] == &"storm" and String(report[&"effect_text"]) in weather_label.tooltip_text and "新規来客はありません" in weather_label.tooltip_text, "HUD shows the storm and its guidance") _expect(forecast_label.text == "次: マナ霧 %02d:%02d" % [fog_minute / 60, fog_minute % 60], "HUD forecast shows the next change to mana fog (%s)" % forecast_label.text) clock.advance_minutes(1) main.call("_refresh_dashboard") report = runtime.get_weather_report() _expect(weather_label.text == "マナ霧" and report[&"kind"] == &"mana_fog" and String(report[&"effect_text"]) in weather_label.tooltip_text and "1.2倍" in weather_label.tooltip_text and "1.5倍" in weather_label.tooltip_text and not "炉" in weather_label.tooltip_text, "HUD shows the mana fog and its guidance without a furnace claim") var teardown: Dictionary = await AudioTeardown.free_owner_and_wait(self, main) _expect(bool(teardown.get(&"passed", false)) and not bool(teardown.get(&"deadline_reached", true)), "owned audio references release before bounded teardown deadline with whole Runtime unchanged") func _expect(condition: bool, message: String) -> void: _checks += 1 if condition: return _failures += 1 push_error(message)