P07-01-WX: 仕上がった天候の機能の受入:自然天候・雨・嵐・世界樹と成人客のマナ霧・上部の表示と予報・保存と移行(マナ炉の産出 120 を除く。P07-01 の補助の作業。原単位への加算 0)
This commit is contained in:
@@ -276,8 +276,9 @@ func _legacy_controls(writer: Dictionary, reader: Dictionary) -> void:
|
||||
|
||||
## Approved later save migrations applied when an older save is loaded, each
|
||||
## checked against its own contract and then mapped into `expected` in place:
|
||||
## A06 starts a fresh finite Mana reserve at the restored boundary, and P14-03
|
||||
## raises the elevator snapshot from v1 to v2 with every other field unchanged.
|
||||
## A06 starts a fresh finite Mana reserve at the restored boundary, P14-03
|
||||
## raises the elevator snapshot from v1 to v2 with every other field unchanged,
|
||||
## and P07-01 A13 migrates the weather snapshot and adds the weather stream.
|
||||
func _approved_runtime_migrations(expected: Dictionary, reader: Dictionary) -> Dictionary:
|
||||
var old: Dictionary = expected[&"systems"][&"runtime"]
|
||||
var now: Dictionary = reader[&"systems"][&"runtime"]
|
||||
@@ -322,6 +323,35 @@ func _approved_runtime_migrations(expected: Dictionary, reader: Dictionary) -> D
|
||||
old[&"economy"] = economy
|
||||
fields.append("economy_A12_empty_fund_accounts")
|
||||
proof["economy_A12"] = {"added_accounts": added.keys(), "only_accounts_added": only_accounts_added}
|
||||
# P07-01 A13 (SPEC section 5.2): a save from before natural weather keeps its
|
||||
# weather and pending schedule, counts every day whose 06:00 has passed as
|
||||
# drawn (no past draw), holds no rain extension, and starts the weather
|
||||
# stream from the world seed; every other field stays byte-identical.
|
||||
var old_weather: Variant = old.get(&"weather")
|
||||
var now_weather: Variant = now.get(&"weather")
|
||||
if old_weather is Dictionary and int(old_weather.get(&"version", 0)) == 1 and now_weather is Dictionary and int(now_weather.get(&"version", 0)) == 2:
|
||||
var weather_minute := int(old_weather[&"minute"])
|
||||
var last_draw_day := floori(weather_minute / 1440.0) - (0 if posmod(weather_minute, 1440) > 360 else 1)
|
||||
var migrated: Dictionary = (old_weather as Dictionary).duplicate(true)
|
||||
migrated[&"version"] = 2
|
||||
migrated[&"natural"] = {&"last_draw_day": last_draw_day, &"today": {}}
|
||||
migrated[&"rain_holds"] = {}
|
||||
var only_natural_state_added: bool = migrated == now_weather
|
||||
ok = ok and only_natural_state_added
|
||||
old[&"weather"] = (now_weather as Dictionary).duplicate(true)
|
||||
fields.append("weather_v1_to_v2_A13")
|
||||
proof["weather_A13"] = {"last_draw_day": last_draw_day, "only_natural_state_added": only_natural_state_added}
|
||||
var old_streams: Variant = old.get(&"rng_streams")
|
||||
var now_streams: Variant = now.get(&"rng_streams")
|
||||
if old_streams is Dictionary and now_streams is Dictionary and not (old_streams as Dictionary).has("weather") and (now_streams as Dictionary).has("weather"):
|
||||
var seeded: Dictionary = DeterministicRng.from_root_seed(int(old[&"world_seed"]), &"weather").snapshot()
|
||||
var others: Dictionary = (now_streams as Dictionary).duplicate(true)
|
||||
others.erase("weather")
|
||||
var only_weather_stream_added: bool = now_streams["weather"] == seeded and var_to_bytes(others) == var_to_bytes(old_streams)
|
||||
ok = ok and only_weather_stream_added
|
||||
old[&"rng_streams"] = (now_streams as Dictionary).duplicate(true)
|
||||
fields.append("rng_streams_weather_from_world_seed_A13")
|
||||
proof["rng_streams_A13"] = {"only_weather_stream_added": only_weather_stream_added}
|
||||
if not fields.is_empty():
|
||||
# Saves keep the Runtime's own key order, so the mapped tree follows it.
|
||||
ok = ok and old.size() == now.size()
|
||||
|
||||
@@ -4,6 +4,9 @@ extends SceneTree
|
||||
## 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 fog scene (WX7) writes saves around the 07:00 fog mana boundary and the
|
||||
## fog end, reloads each one in this process and continues it, and hands those
|
||||
## packages to the reader too.
|
||||
## The handoff is the declared generated report res://artifacts/p07-01/restart-handoff.json
|
||||
## (tools/regression/suite-policy.json fixed_generated_report_routes).
|
||||
|
||||
@@ -22,9 +25,17 @@ func _run() -> void:
|
||||
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)")
|
||||
var held_guest: Variant = straight_runtime.guest_service.get_guest(Fixture.HELD_GUEST)
|
||||
var held := {"hold": {}, "reason": &""}
|
||||
fixtures.play(straight, Fixture.FIRST_MINUTE, Fixture.LAST_MINUTE, func(_minute: int) -> void:
|
||||
if (held["hold"] as Dictionary).is_empty():
|
||||
held["hold"] = straight_runtime.weather.get_rain_hold(Fixture.HELD_GUEST)
|
||||
if StringName(held["reason"]).is_empty():
|
||||
held["reason"] = held_guest.exit_reason)
|
||||
# The hold only moves the planned exit; every other current rule still applies,
|
||||
# so in this world without facilities the guest leaves for lack of one.
|
||||
_expect(held["hold"] == {"original_exit": 362, "extended_to": 602} and held_guest.planned_exit_at == 602 and held["reason"] == &"NO_SUITABLE_FACILITY",
|
||||
"the guest due at 06:02 was held to 10:02 and still left by the current rules (%s)" % String(held["reason"]))
|
||||
_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"]
|
||||
@@ -40,18 +51,9 @@ func _run() -> void:
|
||||
_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 saves := _packages(Fixture.SAVE_MINUTES, saved_paths, saved_states, Fixture.WRITER_SAVE)
|
||||
var final_state := fixtures.state_bytes(straight_runtime)
|
||||
var fog := _fog_scene(fixtures)
|
||||
var handoff := {
|
||||
"schema": Fixture.HANDOFF_SCHEMA,
|
||||
"writer_pid": OS.get_process_id(),
|
||||
@@ -61,6 +63,7 @@ func _run() -> void:
|
||||
"straight_final_state_sha256": Fixture.sha256(final_state),
|
||||
"straight_final_state_base64": Marshalls.raw_to_base64(final_state),
|
||||
"saves": saves,
|
||||
"fog": fog,
|
||||
}
|
||||
DirAccess.make_dir_recursive_absolute(Fixture.HANDOFF_PATH.get_base_dir())
|
||||
var file := FileAccess.open(Fixture.HANDOFF_PATH, FileAccess.WRITE)
|
||||
@@ -79,6 +82,95 @@ func _run() -> void:
|
||||
quit(_failures)
|
||||
|
||||
|
||||
## WX7 fog scene: the uninterrupted run, the run that writes the save files, and
|
||||
## a same-process reload of every save file continued to the end.
|
||||
func _fog_scene(fixtures: Variant) -> Dictionary:
|
||||
var straight: Dictionary = fixtures.new_fixture(false, Fixture.FOG_SEED)
|
||||
var runtime: Variant = straight[&"runtime"]
|
||||
_expect(runtime.weather.get_kind() == &"mana_fog" and int(runtime.weather.get_today_draw().get("end", -1)) == 600, "seed 1592 opens the fog scene with mana fog 06:00-10:00")
|
||||
var seen := {"intimacy": {}, "rises": 0, "fog_rises": 0, "rises_ok": true, "flows": 0, "fog_flows": 0, "flows_ok": true, "after": {}}
|
||||
fixtures.play(straight, Fixture.FIRST_MINUTE, Fixture.FOG_LAST_MINUTE, func(minute: int) -> void:
|
||||
var fog_now: bool = runtime.weather.get_kind() == &"mana_fog"
|
||||
var last: Dictionary = seen["intimacy"]
|
||||
for guest_id: StringName in Fixture.FOG_ADULTS.values():
|
||||
var guest: Variant = runtime.guest_service.get_guest(guest_id)
|
||||
if guest == null:
|
||||
continue
|
||||
var now := int(guest.needs[&"intimacy"])
|
||||
if last.has(guest_id) and now < 100:
|
||||
seen["rises"] += 1
|
||||
seen["rises_ok"] = seen["rises_ok"] and now - int(last[guest_id]) == (4 if fog_now else 3)
|
||||
if fog_now:
|
||||
seen["fog_rises"] += 1
|
||||
last[guest_id] = now
|
||||
if minute % 10 == 0:
|
||||
seen["flows"] += 1
|
||||
seen["flows_ok"] = seen["flows_ok"] and int(runtime.mana_reserve.get_last_flow().get(&"generation", -1)) == (24 if fog_now else 20)
|
||||
if fog_now:
|
||||
seen["fog_flows"] += 1
|
||||
if minute - 1 in Fixture.FOG_SAVE_MINUTES:
|
||||
seen["after"][minute] = fixtures.state_bytes(runtime))
|
||||
_expect(seen["rises_ok"] and int(seen["fog_rises"]) >= 3 and int(seen["rises"]) - int(seen["fog_rises"]) >= 2,
|
||||
"adult-verified intimacy rises by 4 a minute in fog and 3 after it (%d rises, %d in fog)" % [int(seen["rises"]), int(seen["fog_rises"])])
|
||||
_expect(seen["flows_ok"] and int(seen["fog_flows"]) >= 20 and int(seen["flows"]) > int(seen["fog_flows"]),
|
||||
"each 10-minute mana boundary adds 24 in fog and 20 after it (%d boundaries, %d in fog)" % [int(seen["flows"]), int(seen["fog_flows"])])
|
||||
var saving: Dictionary = fixtures.new_fixture(false, Fixture.FOG_SEED)
|
||||
var saving_runtime: Variant = saving[&"runtime"]
|
||||
var states := {}
|
||||
var paths := {}
|
||||
fixtures.play(saving, Fixture.FIRST_MINUTE, Fixture.FOG_LAST_MINUTE, func(minute: int) -> void:
|
||||
if minute in Fixture.FOG_SAVE_MINUTES:
|
||||
var path := Fixture.FOG_WRITER_SAVE % minute
|
||||
states[minute] = fixtures.state_bytes(saving_runtime)
|
||||
_expect(bool(saving_runtime.save_to_path(path).get(&"ok", false)), "fog save file at minute %d" % minute)
|
||||
paths[minute] = path)
|
||||
var final_state: PackedByteArray = fixtures.state_bytes(runtime)
|
||||
_expect(fixtures.state_bytes(saving_runtime) == final_state, "writing the fog save files leaves the state as without saving")
|
||||
for minute: int in Fixture.FOG_SAVE_MINUTES:
|
||||
var reload: Dictionary = fixtures.new_fixture(false, Fixture.FOG_SEED)
|
||||
var reload_runtime: Variant = reload[&"runtime"]
|
||||
_expect(bool(reload_runtime.load_from_path(str(paths.get(minute, Fixture.FOG_WRITER_SAVE % minute))).get(&"ok", false))
|
||||
and fixtures.state_bytes(reload_runtime) == states.get(minute, PackedByteArray()) and reload[&"clock"].absolute_minute == minute,
|
||||
"this process reloads the fog save file of minute %d exactly" % minute)
|
||||
fixtures.play(reload, minute, minute + 1, Callable())
|
||||
_expect(fixtures.state_bytes(reload_runtime) == seen["after"].get(minute + 1, PackedByteArray()),
|
||||
"one minute after the minute %d fog reload equals the uninterrupted run (the fog yield and intimacy apply once)" % minute)
|
||||
fixtures.play(reload, minute + 1, Fixture.FOG_LAST_MINUTE, Callable())
|
||||
_expect(fixtures.state_bytes(reload_runtime) == final_state, "the minute %d fog reload continues to the uninterrupted final state" % minute)
|
||||
fixtures.free_fixture(reload)
|
||||
var after := {}
|
||||
for minute: int in seen["after"]:
|
||||
var bytes: PackedByteArray = seen["after"][minute]
|
||||
after[str(minute)] = {"sha256": Fixture.sha256(bytes), "base64": Marshalls.raw_to_base64(bytes)}
|
||||
var saves := _packages(Fixture.FOG_SAVE_MINUTES, paths, states, Fixture.FOG_WRITER_SAVE)
|
||||
fixtures.free_fixture(straight)
|
||||
fixtures.free_fixture(saving)
|
||||
return {
|
||||
"seed": Fixture.FOG_SEED,
|
||||
"last_minute": Fixture.FOG_LAST_MINUTE,
|
||||
"straight_final_state_sha256": Fixture.sha256(final_state),
|
||||
"straight_final_state_base64": Marshalls.raw_to_base64(final_state),
|
||||
"after_states": after,
|
||||
"saves": saves,
|
||||
}
|
||||
|
||||
|
||||
## The exact save packages and the compared state at each save minute.
|
||||
func _packages(minutes: Array[int], paths: Dictionary, states: Dictionary, path_format: String) -> Array:
|
||||
var saves: Array = []
|
||||
for minute: int in minutes:
|
||||
var directory := ProjectSettings.globalize_path(str(paths.get(minute, path_format % minute)))
|
||||
_expect(DirAccess.dir_exists_absolute(directory) and DirAccess.get_directories_at(directory).is_empty(), "save package %s holds only files" % (path_format % 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 %s has its manifest and world" % (path_format % minute))
|
||||
var state: PackedByteArray = states.get(minute, PackedByteArray())
|
||||
saves.append({"minute": minute, "files": files, "state_sha256": Fixture.sha256(state), "state_base64": Marshalls.raw_to_base64(state)})
|
||||
return saves
|
||||
|
||||
|
||||
func _expect(condition: bool, message: String) -> void:
|
||||
_checks += 1
|
||||
if not condition:
|
||||
|
||||
@@ -3,7 +3,10 @@ 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.
|
||||
## state, with every remaining 06:00 draw happening exactly once. The fog scene
|
||||
## (WX7) packages must also equal the writer's state one minute after each save,
|
||||
## so the fog mana yield and the adult intimacy apply exactly once across the
|
||||
## process boundary.
|
||||
|
||||
const Fixture := preload("res://tests/helpers/p07_weather_restart_fixture.gd")
|
||||
|
||||
@@ -20,6 +23,7 @@ func _run() -> void:
|
||||
var handoff := _read_handoff()
|
||||
if not handoff.is_empty():
|
||||
_check_restarts(handoff)
|
||||
_check_fog(handoff.get("fog", {}))
|
||||
await process_frame
|
||||
if _failures == 0:
|
||||
print("PASS: %d checks" % _checks)
|
||||
@@ -54,18 +58,7 @@ func _check_restarts(handoff: Dictionary) -> void:
|
||||
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)
|
||||
_expect(_write_package(save, path), "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"]
|
||||
@@ -85,6 +78,50 @@ func _check_restarts(handoff: Dictionary) -> void:
|
||||
SaveService.delete_save(path)
|
||||
|
||||
|
||||
func _check_fog(fog: Dictionary) -> void:
|
||||
var fixtures := Fixture.new(_expect)
|
||||
_expect(int(fog.get("seed", -1)) == Fixture.FOG_SEED and int(fog.get("last_minute", -1)) == Fixture.FOG_LAST_MINUTE, "same fog scene and minute script")
|
||||
var final_state := Marshalls.base64_to_raw(str(fog.get("straight_final_state_base64", "")))
|
||||
_expect(not final_state.is_empty() and Fixture.sha256(final_state) == str(fog.get("straight_final_state_sha256", "")), "the writer's fog final state arrived intact")
|
||||
var saves: Array = fog.get("saves", [])
|
||||
_expect(saves.size() == Fixture.FOG_SAVE_MINUTES.size(), "every fog save package was handed off")
|
||||
var after: Dictionary = fog.get("after_states", {})
|
||||
for save: Variant in saves:
|
||||
var minute := int((save as Dictionary).get("minute", -1))
|
||||
var path := Fixture.FOG_READER_SAVE % minute
|
||||
_expect(_write_package(save, path), "fog save package at minute %d arrived byte-exact" % minute)
|
||||
var expected_state := Marshalls.base64_to_raw(str((save as Dictionary).get("state_base64", "")))
|
||||
var next_row: Dictionary = after.get(str(minute + 1), {})
|
||||
var next_state := Marshalls.base64_to_raw(str(next_row.get("base64", "")))
|
||||
_expect(not next_state.is_empty() and Fixture.sha256(next_state) == str(next_row.get("sha256", "")), "the writer's state one minute after %d arrived intact" % minute)
|
||||
var fixture := fixtures.new_fixture(false, Fixture.FOG_SEED)
|
||||
var runtime: Variant = fixture[&"runtime"]
|
||||
_expect(bool(runtime.load_from_path(path).get(&"ok", false)) and fixtures.state_bytes(runtime) == expected_state and fixture[&"clock"].absolute_minute == minute,
|
||||
"the reader resumes the minute %d fog save exactly" % minute)
|
||||
fixtures.play(fixture, minute, minute + 1, Callable())
|
||||
_expect(fixtures.state_bytes(runtime) == next_state, "one minute after the minute %d fog save the reader equals the uninterrupted run (no double fog yield or intimacy)" % minute)
|
||||
fixtures.play(fixture, minute + 1, Fixture.FOG_LAST_MINUTE, Callable())
|
||||
_expect(fixtures.state_bytes(runtime) == final_state, "continuing the minute %d fog save reaches the uninterrupted final state" % minute)
|
||||
fixtures.free_fixture(fixture)
|
||||
SaveService.delete_save(path)
|
||||
|
||||
|
||||
## Writes one handed-off save package byte for byte; true when every file matches its pin.
|
||||
func _write_package(save: Dictionary, path: String) -> bool:
|
||||
var directory := ProjectSettings.globalize_path(path)
|
||||
DirAccess.make_dir_recursive_absolute(directory)
|
||||
var files: Dictionary = save.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()
|
||||
return intact
|
||||
|
||||
|
||||
func _expect(condition: bool, message: String) -> void:
|
||||
_checks += 1
|
||||
if not condition:
|
||||
|
||||
@@ -29,6 +29,7 @@ func _run() -> void:
|
||||
_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()
|
||||
@@ -227,6 +228,24 @@ func _test_rain_stay_and_saves() -> void:
|
||||
_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 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.
|
||||
@@ -313,6 +332,16 @@ func _test_migration_and_rejection() -> void:
|
||||
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.
|
||||
@@ -339,6 +368,19 @@ func _test_hud_forecast() -> void:
|
||||
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")
|
||||
|
||||
|
||||
@@ -5,8 +5,12 @@ extends RefCounted
|
||||
## (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.
|
||||
## 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-v1"
|
||||
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"
|
||||
@@ -17,6 +21,13 @@ 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"
|
||||
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
|
||||
|
||||
@@ -25,18 +36,18 @@ func _init(expect: Callable) -> void:
|
||||
_expect = expect
|
||||
|
||||
|
||||
func new_fixture(with_guest := true) -> Dictionary:
|
||||
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")
|
||||
_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 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",
|
||||
@@ -65,22 +76,51 @@ func free_fixture(fixture: Dictionary) -> void:
|
||||
|
||||
|
||||
## Advances one minute at a time after `after_minute` through `through_minute`
|
||||
## and calls `on_minute` after each minute (the writer saves there).
|
||||
## 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, the reserve flow and the arrival totals.
|
||||
## 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"])
|
||||
@@ -89,6 +129,8 @@ func state_value(runtime: Variant) -> Dictionary:
|
||||
"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,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user