P23-03: 生活の観察
This commit is contained in:
+56
@@ -0,0 +1,56 @@
|
||||
extends RefCounted
|
||||
## Sparse whole typed captures. No acceptance flags and no truncation.
|
||||
const Codec := preload("res://tests/fixtures/p08-02/mixed_world.gd")
|
||||
var directory: String
|
||||
var manifest: FileAccess
|
||||
|
||||
func open(path: String) -> bool:
|
||||
directory = path
|
||||
if DirAccess.dir_exists_absolute(path):
|
||||
return false
|
||||
if DirAccess.make_dir_recursive_absolute(path.path_join("objects")) != OK:
|
||||
return false
|
||||
manifest = FileAccess.open(path.path_join("manifest.jsonl"), FileAccess.WRITE)
|
||||
return manifest != null
|
||||
|
||||
func object(value: Variant) -> String:
|
||||
var text := JSON.stringify(Codec.new().typed(value)) + "\n"
|
||||
var digest := text.sha256_text()
|
||||
var path := directory.path_join("objects").path_join(digest + ".json")
|
||||
if FileAccess.file_exists(path):
|
||||
return digest if FileAccess.get_sha256(path) == digest else ""
|
||||
var file := FileAccess.open(path, FileAccess.WRITE)
|
||||
if file == null:
|
||||
return ""
|
||||
file.store_string(text)
|
||||
file.close()
|
||||
return digest if FileAccess.get_sha256(path) == digest else ""
|
||||
|
||||
func capture(label: String, runtime: Variant, view: WorldPreview) -> bool:
|
||||
if manifest == null:
|
||||
return false
|
||||
var state: Dictionary = runtime.create_snapshot()
|
||||
var report := view.get_life_observation_report()
|
||||
var state_hash := object(state)
|
||||
var report_hash := object(report)
|
||||
if state_hash.is_empty() or report_hash.is_empty():
|
||||
return false
|
||||
manifest.store_line(JSON.stringify({"label": label, "pid": OS.get_process_id(),
|
||||
"ticks_usec": Time.get_ticks_usec(), "state": state_hash, "report": report_hash,
|
||||
"input_method": "diagnostic", "acceptance": "pending"}))
|
||||
manifest.flush()
|
||||
return var_to_bytes(state) == var_to_bytes(runtime.create_snapshot())
|
||||
|
||||
func close() -> void:
|
||||
if manifest != null:
|
||||
manifest.close()
|
||||
manifest = null
|
||||
|
||||
func record(label: String, value: Dictionary) -> bool:
|
||||
if manifest == null: return false
|
||||
var digest := object(value)
|
||||
if digest.is_empty(): return false
|
||||
manifest.store_line(JSON.stringify({"label": label, "pid": OS.get_process_id(),
|
||||
"ticks_usec": Time.get_ticks_usec(), "object": digest}))
|
||||
manifest.flush()
|
||||
return true
|
||||
@@ -0,0 +1 @@
|
||||
uid://d23lifestr03
|
||||
+104
@@ -0,0 +1,104 @@
|
||||
extends RefCounted
|
||||
## Public-command experiment; no synthetic state, costs or capacities.
|
||||
## Predeclared metric: total destination-arrival minutes of the entire cohort.
|
||||
## B must be strictly smaller. Incomplete cohorts cannot establish improvement.
|
||||
const Stream := preload("res://tests/fixtures/p23-03/diagnostic_stream.gd")
|
||||
const SEED := 806042
|
||||
const WINDOW := 480
|
||||
const START := Vector3i(2, 0, 0)
|
||||
const GOAL := Vector3i(2, 1, 0)
|
||||
var stream: RefCounted
|
||||
|
||||
func declaration() -> Dictionary:
|
||||
var rng := RandomNumberGenerator.new()
|
||||
rng.seed = SEED
|
||||
var arrivals: Array[Dictionary] = []
|
||||
for index: int in range(48):
|
||||
arrivals.append({&"id": StringName("p23-%03d" % index),
|
||||
&"offset": (index / 4) * 15 + rng.randi_range(0, 2)})
|
||||
arrivals.sort_custom(func(a: Dictionary, b: Dictionary) -> bool:
|
||||
return a[&"offset"] < b[&"offset"] or (a[&"offset"] == b[&"offset"] and String(a[&"id"]) < String(b[&"id"])))
|
||||
return {&"seed": SEED, &"initial_clock": [1, 6, 0], &"budget": &"GameState.reset",
|
||||
&"production": &"Runtime.initialize defaults; production operations not enabled",
|
||||
&"arrivals": arrivals, &"from": START, &"to": GOAL, &"window": WINDOW,
|
||||
&"metric": &"sum_destination_arrival_minutes_entire_cohort",
|
||||
&"improvement": {&"definition_id": &"guest_elevator", &"origin": Vector3i(4, 0, 0)}}
|
||||
|
||||
func run(tree: SceneTree, directory: String, layout: String) -> bool:
|
||||
if layout not in ["A", "B"]: return false
|
||||
var global_clock := tree.root.get_node_or_null("GameClock")
|
||||
if global_clock != null: global_clock.set_paused(true)
|
||||
stream = Stream.new()
|
||||
if not stream.open(directory): return false
|
||||
var input := declaration()
|
||||
if not stream.record("declaration", input):
|
||||
stream.close()
|
||||
return false
|
||||
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, 6, 0)
|
||||
state.reset()
|
||||
var ok: bool = runtime.initialize(clock, state, SEED)[&"valid"]
|
||||
clock.set_paused(true)
|
||||
runtime.configure_save_policy({&"enabled": false, &"daily_checkpoint_enabled": false})
|
||||
ok = stream.record("initial", runtime.create_snapshot()) and ok
|
||||
var builds: Array = [[&"guest_elevator", Vector3i.ZERO],
|
||||
[&"guest_corridor", Vector3i(2, 1, 0)], [&"guest_corridor", Vector3i(3, 1, 0)]]
|
||||
if layout == "B": builds.append([input[&"improvement"][&"definition_id"], input[&"improvement"][&"origin"]])
|
||||
var construction_minutes := 0
|
||||
# Identical preparation time derived from the same registered definitions.
|
||||
for id: StringName in [&"guest_elevator", &"guest_corridor"]:
|
||||
construction_minutes = maxi(construction_minutes, runtime.definition_registry.get_facility(id).build_minutes)
|
||||
for index: int in range(builds.size()):
|
||||
var payload := {&"definition_id": builds[index][0], &"origin": builds[index][1]}
|
||||
var result: CommandResult = runtime.submit_command(&"build_facility", payload,
|
||||
&"player", &"", 0, clock.absolute_minute, StringName("p23.build.%d" % index))
|
||||
ok = stream.record("build-%d" % index, {&"payload": payload, &"result": result.to_dictionary()}) and result.is_success() and ok
|
||||
clock.advance_minutes(construction_minutes + 1)
|
||||
ok = stream.record("prepared", runtime.create_snapshot()) and ok
|
||||
runtime.domain_events.subscribe(&"*", &"p23.capture", Callable(self, "_event"))
|
||||
var view := WorldPreview.new()
|
||||
view.size = Vector2(1440, 810)
|
||||
view.set_runtime(runtime)
|
||||
tree.root.add_child(view)
|
||||
var finished: Dictionary = {}
|
||||
for offset: int in range(WINDOW + 1):
|
||||
if not ok: break
|
||||
if offset > 0: clock.advance_minutes(1)
|
||||
for arrival: Dictionary in input[&"arrivals"]:
|
||||
if arrival[&"offset"] != offset: continue
|
||||
var payload := {&"guest_id": arrival[&"id"], &"guest_type_id": &"traveler", &"gold": 100,
|
||||
&"options": {&"current_cell": START, &"state": GuestRuntime.State.MOVING,
|
||||
&"state_entered_at": clock.absolute_minute - 1, &"planned_exit_at": clock.absolute_minute + 2000,
|
||||
&"path_handle": {&"target_cell": GOAL}}}
|
||||
var result: CommandResult = runtime.submit_command(&"spawn_guest", payload,
|
||||
&"world", arrival[&"id"], 10, -1, StringName("p23.spawn.%s" % arrival[&"id"]))
|
||||
ok = stream.record("arrival-%s" % arrival[&"id"], {&"offset": offset, &"payload": payload, &"result": result.to_dictionary()}) and result.is_success() and ok
|
||||
# Typed per-minute projections retain paths, queues, density and actual
|
||||
# completed services. Whole aggregate snapshots are only sampled sparsely.
|
||||
ok = stream.record("tick-%03d" % offset, {&"minute": clock.absolute_minute,
|
||||
&"guests": runtime.guest_service.create_snapshot(), &"path": runtime.path_service.create_snapshot(),
|
||||
&"elevators": runtime.elevators.create_snapshot(), &"facilities": runtime.facility_service.create_snapshot()}) and ok
|
||||
for guest: GuestRuntime in runtime.guest_service.get_guests():
|
||||
if guest.current_cell != GOAL or finished.has(guest.guest_id): continue
|
||||
finished[guest.guest_id] = offset
|
||||
var leaving: Dictionary = runtime.guest_service.begin_leaving(guest.guest_id, clock.absolute_minute, &"COMPARISON_DESTINATION_REACHED")
|
||||
var despawn: Dictionary = runtime.guest_service.despawn_guest(guest.guest_id, clock.absolute_minute)
|
||||
ok = bool(leaving.get(&"ok", false)) and bool(despawn.get(&"ok", false)) and ok
|
||||
if offset % 30 == 0:
|
||||
view.queue_redraw()
|
||||
await tree.process_frame
|
||||
await tree.process_frame
|
||||
ok = stream.capture("frame-%03d" % offset, runtime, view) and ok
|
||||
ok = stream.record("final", runtime.create_snapshot()) and ok
|
||||
stream.close()
|
||||
view.free()
|
||||
runtime.free()
|
||||
clock.free()
|
||||
state.free()
|
||||
return ok
|
||||
|
||||
func _event(event: SimulationDomainEvent) -> void:
|
||||
stream.record("event", {&"type": event.event_type, &"minute": event.absolute_minute,
|
||||
&"entities": event.entity_ids, &"payload": event.payload.duplicate(true)})
|
||||
@@ -0,0 +1,77 @@
|
||||
extends SceneTree
|
||||
## Component and read-only rendering checks; never OS or same-demand proof.
|
||||
const Presentation := preload("res://scripts/ui/room_presentation.gd")
|
||||
const Stream := preload("res://tests/fixtures/p23-03/diagnostic_stream.gd")
|
||||
var checks := 0
|
||||
var failures := 0
|
||||
|
||||
func _initialize() -> void:
|
||||
call_deferred("run")
|
||||
|
||||
func expect(value: bool, message: String) -> void:
|
||||
checks += 1
|
||||
if not value:
|
||||
failures += 1
|
||||
push_error("P23 life observation: " + message)
|
||||
|
||||
func run() -> void:
|
||||
var clock: Variant = root.get_node("GameClock")
|
||||
clock.set_paused(true)
|
||||
# Isolated real typed components test state labels, without claiming gameplay.
|
||||
var labels: Dictionary = {}
|
||||
for state: int in FacilityInstance.State.values():
|
||||
var room := FacilityInstance.new().configure(&"component", &"reception")
|
||||
room.state = state as FacilityInstance.State
|
||||
var description := Presentation.describe(room)
|
||||
expect(description[&"state_name"] == FacilityInstance.State.keys()[state], "enum name preserved")
|
||||
var text := Presentation.business_label(description)
|
||||
expect(not text.is_empty() and not labels.has(text), "distinct visible business reason")
|
||||
labels[text] = true
|
||||
for reason: StringName in [&"RAIDED", &"DESTROYED"]:
|
||||
var room := FacilityInstance.new().configure(&"component", &"reception", {&"suspension_reason": reason})
|
||||
var description := Presentation.describe(room)
|
||||
expect(description[&"operation"] == FacilityInstance.OperationState.SUSPENDED, "actual suspension operation")
|
||||
var text := Presentation.business_label(description)
|
||||
expect(not labels.has(text), "raid and damage have separate visible labels")
|
||||
labels[text] = true
|
||||
var room := FacilityInstance.new().configure(&"component", &"reception", {&"customer_capacity": 3})
|
||||
var service := ServiceInstance.new()
|
||||
service.guest_ids.assign([&"b", &"a"])
|
||||
room.active_services[&"service"] = service
|
||||
room.queue_entries.assign([&"queue-only"])
|
||||
var description := Presentation.describe(room)
|
||||
expect(description[&"people"] == [&"a", &"b"] and description[&"count"] == 2, "queue entries are not room occupants")
|
||||
description[&"people"].clear()
|
||||
expect(service.guest_ids.size() == 2, "presentation owns its arrays")
|
||||
var runtime: Variant = root.get_node("SimulationRuntime")
|
||||
var view := WorldPreview.new()
|
||||
view.size = Vector2(1440, 810)
|
||||
view.set_runtime(runtime)
|
||||
root.add_child(view)
|
||||
var before: PackedByteArray = var_to_bytes(runtime.create_snapshot())
|
||||
var tracks: PackedByteArray = var_to_bytes([view._actor_tracks, view._elevator_tracks])
|
||||
var model := view._build_life_observation()
|
||||
expect(var_to_bytes(runtime.create_snapshot()) == before, "builder does not mutate Runtime")
|
||||
expect(var_to_bytes([view._actor_tracks, view._elevator_tracks]) == tracks, "builder does not advance visual tracks")
|
||||
expect(model.has(&"rooms") and model.has(&"landing_badges") and model.has(&"elevators") and model.has(&"staff") and model.has(&"goods") and model.has(&"badges"), "all six category fields exist")
|
||||
expect(model[&"traffic"].size() > 0 and not view.show_grid, "normal view observes real walkable traffic")
|
||||
await process_frame
|
||||
await process_frame
|
||||
var report := view.get_life_observation_report()
|
||||
expect(not report.is_empty(), "actual drawing published an observation")
|
||||
# The getter exposes the last actual draw and must not create another model.
|
||||
var original: PackedByteArray = var_to_bytes(report)
|
||||
if report.has(&"traffic"):
|
||||
report[&"traffic"].clear()
|
||||
expect(var_to_bytes(view.get_life_observation_report()) == original, "nested report arrays are isolated")
|
||||
report.clear()
|
||||
expect(var_to_bytes(view.get_life_observation_report()) == original, "public report is a deep copy")
|
||||
var output := OS.get_environment("P23_LIFE_DIAGNOSTIC_ROOT")
|
||||
if not output.is_empty():
|
||||
var stream := Stream.new()
|
||||
expect(stream.open(output), "fresh sparse diagnostic stream")
|
||||
expect(stream.capture("initial", runtime, view), "whole typed state and draw report capture")
|
||||
stream.close()
|
||||
view.free()
|
||||
print("P23 life observation: %d checks, %d failures" % [checks, failures])
|
||||
quit(0 if failures == 0 else 1)
|
||||
@@ -0,0 +1 @@
|
||||
uid://d23lifeobs03
|
||||
Reference in New Issue
Block a user