P07-01-WX: 仕上がった天候の機能の受入:自然天候・雨・嵐・世界樹と成人客のマナ霧・上部の表示と予報・保存と移行(マナ炉の産出 120 を除く。P07-01 の補助の作業。原単位への加算 0)
This commit is contained in:
@@ -0,0 +1,86 @@
|
||||
extends SceneTree
|
||||
|
||||
## P07-01 F7, writer phase. Plays the minute script, writes ordinary save files
|
||||
## mid-rain, just before and after the rain end and just before and after the
|
||||
## day 1 draw, and hands the exact save packages to the reader suite
|
||||
## (test_p07_weather_restart_2_read.gd), which runs as a separate engine process.
|
||||
## The handoff is the declared generated report res://artifacts/p07-01/restart-handoff.json
|
||||
## (tools/regression/suite-policy.json fixed_generated_report_routes).
|
||||
|
||||
const Fixture := preload("res://tests/helpers/p07_weather_restart_fixture.gd")
|
||||
|
||||
var _checks := 0
|
||||
var _failures := 0
|
||||
|
||||
|
||||
func _initialize() -> void:
|
||||
call_deferred("_run")
|
||||
|
||||
|
||||
func _run() -> void:
|
||||
print("P07-01: restart writer")
|
||||
var fixtures := Fixture.new(_expect)
|
||||
var straight := fixtures.new_fixture()
|
||||
var straight_runtime: Variant = straight[&"runtime"]
|
||||
fixtures.play(straight, Fixture.FIRST_MINUTE, Fixture.LAST_MINUTE, Callable())
|
||||
_expect(straight_runtime.weather.get_rain_hold(Fixture.HELD_GUEST) == {"original_exit": 362, "extended_to": 602} or straight_runtime.guest_service.get_guest(Fixture.HELD_GUEST) == null,
|
||||
"the guest due at 06:02 was held to 10:02 (or has left since)")
|
||||
_expect(straight[&"draws"][&"count"] == Fixture.DRAW_MINUTES.size() and straight_runtime.weather.get_last_draw_day() == 1, "the uninterrupted script draws day 1 once")
|
||||
var saving := fixtures.new_fixture()
|
||||
var saving_runtime: Variant = saving[&"runtime"]
|
||||
var saved_states := {}
|
||||
var saved_paths := {}
|
||||
fixtures.play(saving, Fixture.FIRST_MINUTE, Fixture.LAST_MINUTE, func(minute: int) -> void:
|
||||
if minute in Fixture.SAVE_MINUTES:
|
||||
if minute == 500:
|
||||
var held_present: bool = saving_runtime.guest_service.get_guest(Fixture.HELD_GUEST) != null
|
||||
_expect(saving_runtime.weather.get_kind() == &"rain" and (not held_present or not saving_runtime.weather.get_rain_hold(Fixture.HELD_GUEST).is_empty()), "minute 500 saves mid-rain with the hold of a present guest")
|
||||
var path := Fixture.WRITER_SAVE % minute
|
||||
saved_states[minute] = fixtures.state_bytes(saving_runtime)
|
||||
_expect(bool(saving_runtime.save_to_path(path).get(&"ok", false)), "save file at minute %d" % minute)
|
||||
saved_paths[minute] = path)
|
||||
_expect(fixtures.state_bytes(saving_runtime) == fixtures.state_bytes(straight_runtime), "writing the save files leaves the weather state as without saving")
|
||||
var saves: Array = []
|
||||
for minute: int in Fixture.SAVE_MINUTES:
|
||||
var directory := ProjectSettings.globalize_path(str(saved_paths.get(minute, Fixture.WRITER_SAVE % minute)))
|
||||
_expect(DirAccess.dir_exists_absolute(directory) and DirAccess.get_directories_at(directory).is_empty(), "save package at minute %d holds only files" % minute)
|
||||
var files := {}
|
||||
for name: String in DirAccess.get_files_at(directory):
|
||||
var bytes := FileAccess.get_file_as_bytes(directory.path_join(name))
|
||||
files[name] = {"sha256": Fixture.sha256(bytes), "bytes": bytes.size(), "base64": Marshalls.raw_to_base64(bytes)}
|
||||
_expect(files.has("manifest.json") and files.has("world.bin"), "save package at minute %d has its manifest and world" % minute)
|
||||
var state: PackedByteArray = saved_states.get(minute, PackedByteArray())
|
||||
saves.append({"minute": minute, "files": files, "state_sha256": Fixture.sha256(state), "state_base64": Marshalls.raw_to_base64(state)})
|
||||
var final_state := fixtures.state_bytes(straight_runtime)
|
||||
var handoff := {
|
||||
"schema": Fixture.HANDOFF_SCHEMA,
|
||||
"writer_pid": OS.get_process_id(),
|
||||
"writer_source_sha256": FileAccess.get_sha256(Fixture.WRITER_SOURCE),
|
||||
"seed": Fixture.SEED,
|
||||
"last_minute": Fixture.LAST_MINUTE,
|
||||
"straight_final_state_sha256": Fixture.sha256(final_state),
|
||||
"straight_final_state_base64": Marshalls.raw_to_base64(final_state),
|
||||
"saves": saves,
|
||||
}
|
||||
DirAccess.make_dir_recursive_absolute(Fixture.HANDOFF_PATH.get_base_dir())
|
||||
var file := FileAccess.open(Fixture.HANDOFF_PATH, FileAccess.WRITE)
|
||||
_expect(file != null, "handoff file opens for writing")
|
||||
if file != null:
|
||||
file.store_string(JSON.stringify(handoff, "\t") + "\n")
|
||||
file.close()
|
||||
print("P07_RESTART_WRITER pid=%d handoff_sha256=%s" % [OS.get_process_id(), FileAccess.get_sha256(Fixture.HANDOFF_PATH)])
|
||||
fixtures.free_fixture(straight)
|
||||
fixtures.free_fixture(saving)
|
||||
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)
|
||||
@@ -0,0 +1 @@
|
||||
uid://bz9x67zdqdrc6
|
||||
@@ -0,0 +1,92 @@
|
||||
extends SceneTree
|
||||
|
||||
## P07-01 F7, reader phase (a separate engine process after the writer). Each
|
||||
## handed-off save package is loaded into a fresh Runtime, must equal the writer's
|
||||
## state at that minute, and must continue to the writer's uninterrupted final
|
||||
## state, with every remaining 06:00 draw happening exactly once.
|
||||
|
||||
const Fixture := preload("res://tests/helpers/p07_weather_restart_fixture.gd")
|
||||
|
||||
var _checks := 0
|
||||
var _failures := 0
|
||||
|
||||
|
||||
func _initialize() -> void:
|
||||
call_deferred("_run")
|
||||
|
||||
|
||||
func _run() -> void:
|
||||
print("P07-01: restart reader")
|
||||
var handoff := _read_handoff()
|
||||
if not handoff.is_empty():
|
||||
_check_restarts(handoff)
|
||||
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 _read_handoff() -> Dictionary:
|
||||
_expect(FileAccess.file_exists(Fixture.HANDOFF_PATH), "the writer phase left its handoff (run test_p07_weather_restart_1_write.gd first)")
|
||||
if not FileAccess.file_exists(Fixture.HANDOFF_PATH):
|
||||
return {}
|
||||
var parsed: Variant = JSON.parse_string(FileAccess.get_file_as_string(Fixture.HANDOFF_PATH))
|
||||
_expect(parsed is Dictionary and str((parsed as Dictionary).get("schema", "")) == Fixture.HANDOFF_SCHEMA, "handoff schema")
|
||||
if not parsed is Dictionary:
|
||||
return {}
|
||||
var handoff: Dictionary = parsed
|
||||
var writer_pid := int(handoff.get("writer_pid", 0))
|
||||
_expect(writer_pid > 0 and writer_pid != OS.get_process_id(), "the writer ran in another engine process")
|
||||
_expect(str(handoff.get("writer_source_sha256", "")) == FileAccess.get_sha256(Fixture.WRITER_SOURCE), "the handoff comes from the current writer suite")
|
||||
_expect(int(handoff.get("seed", -1)) == Fixture.SEED and int(handoff.get("last_minute", -1)) == Fixture.LAST_MINUTE, "same fixture and minute script")
|
||||
print("P07_RESTART_READER pid=%d writer_pid=%d handoff_sha256=%s" % [OS.get_process_id(), writer_pid, FileAccess.get_sha256(Fixture.HANDOFF_PATH)])
|
||||
return handoff
|
||||
|
||||
|
||||
func _check_restarts(handoff: Dictionary) -> void:
|
||||
var fixtures := Fixture.new(_expect)
|
||||
var final_state := Marshalls.base64_to_raw(str(handoff.get("straight_final_state_base64", "")))
|
||||
_expect(Fixture.sha256(final_state) == str(handoff.get("straight_final_state_sha256", "")), "the writer's final state arrived intact")
|
||||
var saves: Array = handoff.get("saves", [])
|
||||
_expect(saves.size() == Fixture.SAVE_MINUTES.size(), "every save package was handed off")
|
||||
for save: Variant in saves:
|
||||
var minute := int((save as Dictionary).get("minute", -1))
|
||||
var path := Fixture.READER_SAVE % minute
|
||||
var directory := ProjectSettings.globalize_path(path)
|
||||
DirAccess.make_dir_recursive_absolute(directory)
|
||||
var files: Dictionary = (save as Dictionary).get("files", {})
|
||||
var intact := not files.is_empty()
|
||||
for name: String in files:
|
||||
var bytes := Marshalls.base64_to_raw(str(files[name].get("base64", "")))
|
||||
intact = intact and Fixture.sha256(bytes) == str(files[name].get("sha256", "")) and bytes.size() == int(files[name].get("bytes", -1))
|
||||
var file := FileAccess.open(directory.path_join(name), FileAccess.WRITE)
|
||||
if file != null:
|
||||
file.store_buffer(bytes)
|
||||
file.close()
|
||||
_expect(intact, "save package at minute %d arrived byte-exact" % minute)
|
||||
var expected_state := Marshalls.base64_to_raw(str((save as Dictionary).get("state_base64", "")))
|
||||
var fixture := fixtures.new_fixture(false)
|
||||
var runtime: Variant = fixture[&"runtime"]
|
||||
_expect(bool(runtime.load_from_path(path).get(&"ok", false)), "the reader loads the minute %d save" % minute)
|
||||
_expect(fixtures.state_bytes(runtime) == expected_state and fixture[&"clock"].absolute_minute == minute, "the minute %d save resumes the writer's weather, holds, stream and plans" % minute)
|
||||
# Loading replaces the event stream, so the draw counter subscribes again.
|
||||
fixture[&"draws"][&"count"] = 0
|
||||
fixtures.watch_draws(fixture)
|
||||
fixtures.play(fixture, minute, Fixture.LAST_MINUTE, Callable())
|
||||
var expected_draws := 0
|
||||
for draw_minute: int in Fixture.DRAW_MINUTES:
|
||||
if draw_minute > minute:
|
||||
expected_draws += 1
|
||||
_expect(fixture[&"draws"][&"count"] == expected_draws, "after the minute %d save the remaining 06:00 draws happen once (%d)" % [minute, expected_draws])
|
||||
_expect(fixtures.state_bytes(runtime) == final_state, "continuing from minute %d reaches the uninterrupted final state" % minute)
|
||||
fixtures.free_fixture(fixture)
|
||||
SaveService.delete_save(path)
|
||||
|
||||
|
||||
func _expect(condition: bool, message: String) -> void:
|
||||
_checks += 1
|
||||
if not condition:
|
||||
_failures += 1
|
||||
push_error(message)
|
||||
@@ -0,0 +1 @@
|
||||
uid://bni9w8axk6f97
|
||||
@@ -0,0 +1,351 @@
|
||||
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_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")
|
||||
|
||||
|
||||
## 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")
|
||||
|
||||
|
||||
## 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 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)
|
||||
@@ -0,0 +1 @@
|
||||
uid://bsaoksmz5s26w
|
||||
@@ -20,10 +20,15 @@ func run() -> void:
|
||||
legacy[&"systems"][&"runtime"].erase(&"weather")
|
||||
legacy[&"systems"][&"runtime"].erase(&"facility_cleanliness")
|
||||
expect(runtime.restore_snapshot(legacy) and runtime.weather.get_kind() == &"clear" and runtime.weather.get_minute() == 360, "v10 migrates to neutral weather at saved minute")
|
||||
expect(runtime.create_snapshot() == original, "old neutral save migration preserves other state")
|
||||
# P07-01 (A13): the original carries the opening day's natural draw, which a
|
||||
# pre-weather save never had; every other system must stay byte-equal.
|
||||
var migrated: Dictionary = runtime.create_snapshot()
|
||||
var migrated_rest: Dictionary = migrated.duplicate(true); migrated_rest[&"systems"][&"runtime"].erase(&"weather")
|
||||
var original_rest: Dictionary = original.duplicate(true); original_rest[&"systems"][&"runtime"].erase(&"weather")
|
||||
expect(migrated_rest == original_rest and runtime.weather.get_last_draw_day() == -1 and runtime.weather.get_today_draw().is_empty(), "old neutral save migration preserves other state")
|
||||
var malformed := original.duplicate(true)
|
||||
malformed[&"systems"][&"runtime"][&"weather"][&"minute"] = 361
|
||||
expect(not runtime.restore_snapshot(malformed) and runtime.create_snapshot() == original, "future weather clock rejects atomically")
|
||||
expect(not runtime.restore_snapshot(malformed) and runtime.create_snapshot() == migrated, "future weather clock rejects atomically")
|
||||
malformed = original.duplicate(true)
|
||||
malformed[&"systems"][&"runtime"].erase(&"weather")
|
||||
expect(not runtime.restore_snapshot(malformed), "new schema cannot silently lose weather")
|
||||
|
||||
@@ -32,7 +32,11 @@ func run() -> void:
|
||||
var completed: Dictionary = runtime.create_snapshot()
|
||||
expect(runtime.restore_snapshot(completed) and runtime.create_snapshot() == completed, "completed retry restores exactly")
|
||||
clock.advance_minutes(30)
|
||||
expect(runtime.weather.get_kind() == &"clear" and runtime.weather.create_snapshot()[&"pending"].is_empty(), "disk-restored recovery occurs once at scheduled minute")
|
||||
# P07-01 (A13): only the opening day's natural draw may remain scheduled.
|
||||
var today: Dictionary = runtime.weather.get_today_draw()
|
||||
var natural_pending: Array = [] if String(today.get("kind", "clear")) == "clear" else [
|
||||
{&"kind": StringName(today["kind"]), &"minute": int(today["start"])}, {&"kind": &"clear", &"minute": int(today["end"])}]
|
||||
expect(runtime.weather.get_kind() == &"clear" and runtime.weather.create_snapshot()[&"pending"] == natural_pending, "disk-restored recovery occurs once at scheduled minute")
|
||||
expect(not runtime.get_operations_report()[&"runtime_failure_latched"], "recovery resumes ordinary operations")
|
||||
expect(SaveService.delete_save(path), "delete isolated test save")
|
||||
runtime.free(); clock.free(); state.free()
|
||||
|
||||
@@ -32,7 +32,7 @@ func _initialize() -> void:
|
||||
for field: String in ["version", "minute", "kind", "since", "pending"]:
|
||||
var bad := baseline.duplicate(true); bad.erase(field)
|
||||
expect(not weather.restore_snapshot(bad) and weather.create_snapshot() == baseline, "missing field rejects atomically: " + field)
|
||||
for change: Dictionary in [{"version": 2}, {"minute": 380.0}, {"kind": "snow"}, {"since": 381}, {"pending": [{"kind":"clear","minute":380}]}, {"pending": [{"kind":"clear","minute":400},{"kind":"rain","minute":390}]}, {"extra":true}]:
|
||||
for change: Dictionary in [{"version": 3}, {"minute": 380.0}, {"kind": "snow"}, {"since": 381}, {"pending": [{"kind":"clear","minute":380}]}, {"pending": [{"kind":"clear","minute":400},{"kind":"rain","minute":390}]}, {"extra":true}]:
|
||||
var bad := baseline.duplicate(true)
|
||||
bad.merge(change, true)
|
||||
expect(not weather.restore_snapshot(bad) and weather.create_snapshot() == baseline, "malformed or contradictory state rejects atomically")
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
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.
|
||||
const HANDOFF_PATH := "res://artifacts/p07-01/restart-handoff.json"
|
||||
const HANDOFF_SCHEMA := "p07-01-restart-handoff-v1"
|
||||
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
|
||||
# Mid-rain, just before and just after the rain end, just before and just after the day 1 draw.
|
||||
const SAVE_MINUTES: Array[int] = [500, 719, 720, 1799, 1800]
|
||||
const LAST_MINUTE := 1830
|
||||
const DRAW_MINUTES: Array[int] = [1800]
|
||||
const HELD_GUEST := &"p07_restart_guest"
|
||||
|
||||
var _expect: Callable
|
||||
|
||||
|
||||
func _init(expect: Callable) -> void:
|
||||
_expect = expect
|
||||
|
||||
|
||||
func new_fixture(with_guest := true) -> 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}}
|
||||
if with_guest:
|
||||
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).
|
||||
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 on_minute.is_valid():
|
||||
on_minute.call(minute)
|
||||
|
||||
|
||||
## The weather-facing state compared across processes: weather and its stream,
|
||||
## guest plans, the reserve flow and the arrival totals.
|
||||
func state_value(runtime: Variant) -> Dictionary:
|
||||
var plans := {}
|
||||
for guest: GuestRuntime in runtime.guest_service.get_guests():
|
||||
plans[String(guest.guest_id)] = guest.planned_exit_at
|
||||
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,
|
||||
"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()
|
||||
@@ -0,0 +1 @@
|
||||
uid://bczrqi1qiv5zk
|
||||
Reference in New Issue
Block a user