P23-03: 生活の観察

This commit is contained in:
Agent Control Runner
2026-10-15 09:36:36 +09:00
parent 03100beabe
commit 4c8a18aa75
11 changed files with 2207 additions and 35 deletions
+88 -2
View File
@@ -3,6 +3,40 @@ extends RefCounted
const Codec := preload("res://tests/fixtures/p08-02/mixed_world.gd")
var directory: String
var manifest: FileAccess
var observer: MotionObserver
class MotionObserver extends Node:
var view: WorldPreview
var frames: Array = []
var elapsed := 0.0
var scaled_elapsed := 0.0
func _ready() -> void:
RenderingServer.frame_post_draw.connect(after_draw)
func _process(delta: float) -> void:
elapsed += delta
var clock := get_node_or_null("/root/GameClock")
if clock == null or not bool(clock.get("paused")):
scaled_elapsed += delta * maxf(float(clock.get("speed")) if clock != null else 1.0, 0.0)
func after_draw() -> void:
if not is_instance_valid(view): return
var report := view.get_life_observation_report()
if report.is_empty(): return
var sample := {}
for key: String in ["animation_time", "paused", "layer", "zoom", "pan", "viewport", "actor_tracks", "elevator_tracks", "actor_drawings", "cabins"]:
sample[key] = report[key]
if not frames.is_empty() and sample == frames[-1]["display"] and not sample["paused"]: return
# Keep only two draw frames, never whole Runtime copies per frame.
frames.append({"frame": Engine.get_process_frames(), "delta": elapsed, "scaled_delta": scaled_elapsed, "display": sample})
if frames.size() > 2: frames.pop_front()
elapsed = 0.0
scaled_elapsed = 0.0
func _exit_tree() -> void:
if RenderingServer.frame_post_draw.is_connected(after_draw): RenderingServer.frame_post_draw.disconnect(after_draw)
func watch(view: WorldPreview) -> void:
observer = MotionObserver.new()
observer.view = view
view.add_child(observer)
func open(path: String) -> bool:
directory = path
@@ -33,19 +67,71 @@ func capture(label: String, runtime: Variant, view: WorldPreview) -> bool:
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():
var audit_hash := object(rest_audit(runtime))
var motion_hash := object({"frames": observer.frames.duplicate(true) if is_instance_valid(observer) else []})
var after: Dictionary = runtime.create_snapshot()
var after_hash := object(after)
if state_hash.is_empty() or report_hash.is_empty() or audit_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,
"rest_audit": audit_hash, "motion": motion_hash, "state_after": after_hash,
"input_method": "diagnostic", "acceptance": "pending"}))
manifest.flush()
return var_to_bytes(state) == var_to_bytes(runtime.create_snapshot())
return var_to_bytes(state) == var_to_bytes(after)
func close() -> void:
if manifest != null:
manifest.close()
manifest = null
# Public selection inputs; Runtime output is only a comparison operand.
func rest_audit(runtime: Variant) -> Dictionary:
var rows: Array = []
for member: StaffRuntime in runtime.staff_service.get_staff_members():
var memberships: Array = []
for raid_id: StringName in runtime.defense_service.get_raid_ids():
var raid: Dictionary = runtime.defense_service.get_raid(raid_id)
if raid.get(&"combatants", {}).has(member.staff_id):
memberships.append({"raid_id": raid_id, "state": DefenseService.RaidState.keys()[raid[&"state"]],
"side": raid[&"combatants"][member.staff_id].get(&"side", &"")})
var facilities: Array = []
for room: FacilityInstance in runtime.facility_service.get_instances():
var probes: Array = []
var candidates: Dictionary = {}
var occupied := room.occupied_cells.duplicate()
if occupied.is_empty(): occupied.append(room.origin)
for cell: Vector3i in occupied:
for delta: Vector3i in [Vector3i.ZERO, Vector3i.LEFT, Vector3i.RIGHT]:
var present: bool = runtime.path_service.has_cell(cell + delta)
probes.append({"cell": cell + delta, "has_cell": present})
if present: candidates[cell + delta] = true
var ordered: Array = candidates.keys()
ordered.sort_custom(func(a: Vector3i, b: Vector3i) -> bool:
if a.z != b.z: return a.z < b.z
if a.y != b.y: return a.y < b.y
return a.x < b.x)
var routes: Array = []
for cell: Vector3i in ordered:
var path: Array = runtime.path_service.find_path(member.current_cell, cell,
[WorldService.LAYER_FRONT, WorldService.LAYER_BACK], [&"staff"])
var edges: Array = []
for index: int in range(1, path.size()):
# Empty reports remain explicit mismatch evidence.
edges.append(runtime.path_service.get_edge_report(path[index - 1], path[index], [&"staff"]))
routes.append({"cell": cell, "path": path, "edges": edges,
"calculate_path_cost": runtime.path_service.calculate_path_cost(path, [&"staff"]) if not path.is_empty() else -1})
facilities.append({"instance_id": room.instance_id, "definition_id": room.definition_id,
"operation_state": FacilityInstance.OperationState.keys()[room.operation_state],
"origin": room.origin, "occupied_cells": room.occupied_cells.duplicate(),
"probes": probes, "ordered": ordered, "routes": routes})
rows.append({"staff_id": member.staff_id, "state": StaffRuntime.State.keys()[member.state],
"health": member.health, "current_task_id": member.current_task_id, "current_cell": member.current_cell,
"defense_memberships": memberships, "actor_lock": runtime.event_service.get_actor_locks().has(member.staff_id),
"in_transit": runtime.is_staff_in_transit(member.staff_id), "facilities": facilities,
"comparison_only": runtime.get_staff_rest_report(member.staff_id)})
return {"staff": rows, "path_service_sha256": FileAccess.get_sha256("res://scripts/world/path_service.gd")}
func record(label: String, value: Dictionary) -> bool:
if manifest == null: return false
var digest := object(value)
+7 -7
View File
@@ -62,6 +62,7 @@ func run(tree: SceneTree, directory: String, layout: String) -> bool:
view.size = Vector2(1440, 810)
view.set_runtime(runtime)
tree.root.add_child(view)
stream.watch(view)
var finished: Dictionary = {}
for offset: int in range(WINDOW + 1):
if not ok: break
@@ -77,20 +78,19 @@ func run(tree: SceneTree, directory: String, layout: String) -> bool:
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.
var guest_records: Dictionary = runtime.guest_service.create_snapshot()
ok = stream.record("tick-%03d" % offset, {&"minute": clock.absolute_minute,
&"guests": runtime.guest_service.create_snapshot(), &"path": runtime.path_service.create_snapshot(),
&"guests": guest_records, &"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
for guest: Dictionary in guest_records[&"guests"]:
if guest[&"current_cell"] != GOAL or finished.has(guest[&"guest_id"]): continue
finished[guest[&"guest_id"]] = offset
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("first-arrivals", {&"offsets": finished}) and ok
ok = stream.record("final", runtime.create_snapshot()) and ok
stream.close()
view.free()
+161
View File
@@ -0,0 +1,161 @@
extends "res://tests/fixtures/p08-03/placement.gd"
## Real Runtime commands; inherited formal codec/save-operation helpers unchanged.
const Stream := preload("res://tests/fixtures/p23-03/diagnostic_stream.gd")
const Comparison := preload("res://tests/fixtures/p23-03/layout_comparison.gd")
var stream: RefCounted
var layout := "A"
var facility_ids: Dictionary = {}
func setup() -> void:
clock.reset(1, 6, 0)
var state: Variant = tree.root.get_node("GameState")
state.reset()
expect(runtime.initialize(clock, state, 806042).get(&"valid", false), "initialize observation")
clock.set_paused(true)
tree.root.size = Vector2i(1440, 810)
layout = OS.get_environment("P23_LAYOUT")
expect(layout in ["A", "B"], "declared layout A or B")
main = load("res://scenes/main/main.tscn").instantiate()
main.manual_save_path = "user://saves/manual_ui"
if role == "reader":
import_package(directory.path_join("formal"), String(main.manual_save_path))
tree.root.add_child(main)
runtime.configure_save_policy({&"enabled": false, &"daily_checkpoint_enabled": false})
stream = Stream.new()
expect(stream.open(directory.path_join(role + "-stream")), "fresh observation stream")
stream.watch(main.world_preview)
await frames()
func command(id: StringName, payload: Dictionary) -> bool:
var result: CommandResult = runtime.submit_command(id, payload)
expect(result.is_success(), String(id) + ": " + String(result.code))
expect(stream.record("command", {&"command": id, &"payload": payload,
&"result": result.to_dictionary(), &"minute": clock.absolute_minute}), "record public command")
return result.is_success()
func advance(minutes: int) -> void:
for unused: int in range(minutes):
clock.advance_minutes(1)
await frames()
await observe_progress()
if clock.absolute_minute % 30 == 0:
expect(stream.capture("minute-%d" % clock.absolute_minute, runtime, main.world_preview), "sparse minute observation")
func observe_progress() -> void:
pass
func prepare() -> void:
# Both layouts receive the same definition-derived diagnostic construction budget.
# Cafeteria/kitchen move together to x=6, leaving x=4..5 for the declared B EV.
var builds: Array = [[&"reception", Vector3i(-8, 0, 0)],
[&"cafeteria", Vector3i(6, 0, 0)], [&"warehouse", Vector3i(-8, 0, 1)],
[&"kitchen", Vector3i(6, 0, 1)], [&"staff_door", Vector3i(0, 0, 1)],
[&"standard_room", Vector3i(-8, 2, 0)], [&"guest_stairs", Vector3i(-4, 1, 0)],
[&"guest_stairs", Vector3i(-4, 2, 0)], [&"guest_corridor", Vector3i(-5, 2, 0)],
[&"guest_corridor", Vector3i(-6, 2, 0)], [&"guest_elevator", Vector3i.ZERO],
[&"guest_corridor", Vector3i(2, 1, 0)], [&"guest_corridor", Vector3i(3, 1, 0)],
[&"staff_waiting_room", Vector3i(-4, 0, 1)]]
var improvement: Dictionary = Comparison.new().declaration()[&"improvement"]
var budget_builds := builds.duplicate(true)
budget_builds.append([improvement[&"definition_id"], improvement[&"origin"]])
var needed := {&"gold": 0, &"materials": 0}
var costs: Array = []
for row: Array in budget_builds:
var definition: FacilityDefinition = runtime.definition_registry.get_facility(row[0])
costs.append({&"definition": row[0], &"cost": definition.construction_cost.duplicate(true)})
for key: StringName in needed:
needed[key] += int(definition.construction_cost.get(key, 0))
var state: Variant = tree.root.get_node("GameState")
var deltas := {}
for key: StringName in needed:
if needed[key] > state.get_resource(key):
deltas[key] = needed[key] - state.get_resource(key)
budget = {"setup": "diagnostic", "costs": costs, "needed": needed,
"initial": state.resources.duplicate(true), "deltas": deltas}
if not deltas.is_empty(): command(&"change_resources", {&"deltas": deltas})
expect(stream.record("budget", budget), "common diagnostic budget")
if layout == "B": builds.append([improvement[&"definition_id"], improvement[&"origin"]])
# Preserve the supply scenario's staged support/construction sequence.
for index: int in range(builds.size()):
if index == 5: await advance(180)
if index == 7: await advance(60)
if index == 8: await advance(10)
command(&"build_facility", {&"definition_id": builds[index][0], &"origin": builds[index][1]})
var transport_minutes := 0
for id: StringName in [&"guest_elevator", &"guest_corridor"]:
transport_minutes = maxi(transport_minutes, runtime.definition_registry.get_facility(id).build_minutes)
await advance(maxi(10, transport_minutes))
for facility: FacilityInstance in runtime.facility_service.get_instances():
facility_ids[facility.definition_id] = facility.instance_id
command(&"set_facility_conditions", {&"facility_id": facility_ids[&"cafeteria"],
&"conditions": {&"related_facility_id": facility_ids[&"kitchen"]}})
for assignment: Array in [[&"staff_akane", &"reception"], [&"staff_gina", &"warehouse"],
[&"staff_chika", &"kitchen"], [&"staff_iris", &"cafeteria"], [&"staff_juno", &"cafeteria"]]:
command(&"hire_staff_candidate", {&"candidate_id": assignment[0]})
command(&"set_staff_work_assignment", {&"staff_id": assignment[0],
&"facility_id": facility_ids[assignment[1]], &"template_id": &"DAY"})
command(&"purchase_supplies", {&"warehouse_id": facility_ids[&"warehouse"],
&"item_id": SupplyCatalog.RAW_FOOD, &"quantity": 40})
command(&"set_kitchen_production", {&"facility_id": facility_ids[&"kitchen"],
&"enabled": true, &"target_stock": 8})
# The OS observation uses the already declared transport cohort, plus
# ordinary public arrivals whose service choice remains Runtime-owned.
# These commands are recorded and identical across A/B; no actor is moved,
# despawned, or inserted into a room/queue by the fixture.
var demand: Dictionary = Comparison.new().declaration()
expect(stream.record("observation-demand", {"cohort": demand[&"arrivals"],
"schedule": "all observation actors at the recorded current minute; offsets apply only to native layout comparison",
"minute": clock.absolute_minute}), "declared observation demand")
for arrival: Dictionary in demand[&"arrivals"]:
command(&"spawn_guest", {&"guest_id": StringName("os-" + String(arrival[&"id"])),
&"guest_type_id": &"traveler", &"gold": 100,
&"options": {&"current_cell": Comparison.START, &"state": GuestRuntime.State.MOVING,
&"state_entered_at": clock.absolute_minute - 1, &"planned_exit_at": clock.absolute_minute + 2000,
&"path_handle": {&"target_cell": Comparison.GOAL}}})
command(&"spawn_guest", {&"guest_id": StringName("service-" + String(arrival[&"id"])),
&"guest_type_id": &"traveler", &"gold": 100})
await advance(120)
func layer_observations(prefix: String) -> void:
await button(main.front_button)
expect(stream.capture(prefix + "-FRONT", runtime, main.world_preview), "FRONT state/report")
await button(main.back_button)
expect(stream.capture(prefix + "-BACK", runtime, main.world_preview), "BACK state/report")
func writer() -> void:
await prepare()
await layer_observations("writer")
capture("presave")
await button(main.find_child("ManualSaveButton", true, false))
expect(main.build_status_label.text == "手動セーブを保存しました。", "normal save")
capture("checkpoint")
export_package(String(main.manual_save_path), "formal")
var formal := inspect_formal()
var presave := FileAccess.get_file_as_bytes(directory.path_join("writer-presave.var"))
expect(presave == var_to_bytes(formal), "whole presave formal model")
expect(not operation_results.is_empty(), "save result recorded")
if not operation_results.is_empty():
verify_operation(formal, operation_results.back(), &"SAVE_CHECKPOINT_SUCCEEDED")
var members := {}
for member: String in MEMBERS:
members[member] = FileAccess.get_sha256(directory.path_join("formal/" + member))
json_file("save.json", {"pid": OS.get_process_id(), "members": members})
func reader(distinct: bool) -> void:
var saved: Dictionary = JSON.parse_string(FileAccess.get_file_as_string(directory.path_join("save.json")))
expect(distinct and saved.pid != OS.get_process_id(), "distinct actual reader PID")
await load_menu(main.find_child("SaveLoadMenu", true, false))
await frames()
expect(main.build_status_label.text == "手動セーブを読み込みました。", "normal resume")
capture("checkpoint")
var formal := inspect_formal()
expect(FileAccess.get_file_as_bytes(directory.path_join("writer-formal-model.var")) == var_to_bytes(formal), "whole formal resume model")
for member: String in MEMBERS:
expect(FileAccess.get_sha256(directory.path_join("formal/" + member)) == saved.members[member], "formal member " + member)
expect(not operation_results.is_empty(), "load result recorded")
if not operation_results.is_empty():
verify_operation(formal, operation_results.back(), &"SAVE_RECOVERY_LOADED")
await layer_observations("reader")
func finish_observations() -> void:
stream.close()
+254
View File
@@ -0,0 +1,254 @@
extends "res://tests/fixtures/p23-03/observation.gd"
## Passive endpoint implementation. All gameplay/input is performed by Root CUA.
## Shared parent contains the handler endpoints, but every such endpoint below
## is overridden; this class never emits UI signals or invokes product handlers.
var receipts: Array = []
var terminated := false
var terminal_record: Dictionary = {}
var terminal_cause_recorded := false
var reveal_rows: Array = []
var all_requests: Array = []
var input_observer: Node
var last_dispatch: Dictionary = {}
var observed_categories: Dictionary = {}
func observe_progress() -> void:
if role != "writer" or terminated: return
var wanted: Dictionary = {}
var report: Dictionary = main.world_preview.get_life_observation_report()
for room: Dictionary in report.get(&"rooms", {}).values():
if int(room.get(&"count", 0)) > 0: wanted["room-users"] = true
for badge: Dictionary in report.get(&"landing_badges", []):
if int(badge.get(&"overflow", 0)) > 0: wanted["queue-overflow"] = true
for cabin: Dictionary in report.get(&"cabins", []):
if not cabin[&"passengers"].is_empty(): wanted["EV-passengers"] = true
var elevators: Dictionary = runtime.elevators.get_report()
for shaft: Dictionary in elevators.get(&"shafts", []):
if not shaft.get(&"boarded", []).is_empty(): wanted["EV-passengers"] = true
for trip: Dictionary in elevators.get(&"trips", []):
if trip.get(&"state", &"") == &"QUEUED": wanted["queue-present"] = true
for member: StaffRuntime in runtime.staff_service.get_staff_members():
if not member.carried_inventory_id.is_empty(): wanted["carried-goods"] = true
if not report.get(&"resting_staff", {}).is_empty(): wanted["rest-position"] = true
for category: String in wanted:
if observed_categories.has(category): continue
observed_categories[category] = true
await layer_observations("actual-" + category)
if category == "EV-passengers": await motion_observations()
func motion_observations() -> void:
await button(main.front_button)
await button(main.pause_button)
expect(not clock.paused, "ordinary resume for motion observation")
var minute: int = clock.absolute_minute
while clock.absolute_minute == minute and not terminated:
await tree.process_frame
await frames()
expect(stream.capture("active-motion", runtime, main.world_preview), "active interpolation history")
await button(main.pause_button)
expect(clock.paused, "ordinary pause for stable observation")
await frames()
expect(stream.capture("paused-motion", runtime, main.world_preview), "paused interpolation history")
class InputObserver extends Node:
var events: Array = []
var endpoint: RefCounted = null
func _input(event: InputEvent) -> void:
if event is InputEventMouseButton or event is InputEventKey or event is InputEventMouseMotion:
var row := {"ticks_usec": Time.get_ticks_usec(), "text": event.as_text(), "pressed": event.is_pressed()}
if event is InputEventMouseButton:
row["button"] = event.button_index
row["position"] = [event.position.x, event.position.y]
row["ctrl"] = event.ctrl_pressed
if endpoint != null and event.pressed and event.button_index == MOUSE_BUTTON_LEFT:
row["dispatch"] = endpoint.dispatch_position(event.position)
events.append(row)
func expect(ok: bool, message: String) -> void:
if terminated and ok:
return
if terminated and terminal_cause_recorded:
if not ok:
terminal_record["suppressed_after_terminal"] += 1
json_file(role + "-terminal.json", terminal_record)
return
if terminated and not ok:
terminal_cause_recorded = true
super.expect(ok, message)
func terminate(stem: String, reason: String) -> void:
terminated = true
terminal_record = {"pid": OS.get_process_id(), "role": role, "sequence": serial,
"stem": stem, "reason": reason, "suppressed_after_terminal": 0}
json_file(role + "-terminal.json", terminal_record)
expect(false, reason)
tree.quit(1)
func request(action: Dictionary) -> void:
if terminated:
return
serial += 1
var stem := "%s-request-%04d" % [role, serial]
var folder := directory.path_join(stem)
DirAccess.make_dir_recursive_absolute(folder)
var window: Window = main.get_window()
var view: WorldPreview = main.world_preview
action["report"] = Codec.new().typed(view.get_life_observation_report())
action["viewport_to_screen"] = affine(view.get_screen_transform() * view.get_global_transform_with_canvas().affine_inverse())
action["local_to_viewport"] = affine(view.get_global_transform_with_canvas())
action["local_to_screen"] = affine(view.get_screen_transform())
action["rectangles"] = Codec.new().typed(draw_rectangles(view.get_life_observation_report(), view))
action["requested_ticks_usec"] = Time.get_ticks_usec()
action["requested_unix_ns"] = int(Time.get_unix_time_from_system() * 1000000000.0)
action["screenshot_receipt_schema"] = {"screenshots": [{"path": "evidence-root-relative raw image path", "raw_sha256": "original byte SHA256", "tool_call_id": "actual CUA screenshot call ID"}]}
expect(stream.capture(stem, runtime, view), "request state/report")
action.merge({"sequence": serial, "target_pid": OS.get_process_id(), "role": role,
"state": "waiting_for_root_cua", "window_title": window.title,
"window_position": [window.position.x, window.position.y],
"window_size": [window.size.x, window.size.y], "screen_scale": DisplayServer.screen_get_scale(window.current_screen),
"content_scale_factor": window.content_scale_factor, "setup": "diagnostic"})
json_file(stem + "/request.json", action)
var path := folder.path_join("ack.json")
var first_event: int = input_observer.events.size()
var deadline := Time.get_ticks_msec() + 300000
print("P23_ROOT_CUA_WAIT " + folder.path_join("request.json"))
while not FileAccess.file_exists(path) and Time.get_ticks_msec() < deadline:
await tree.process_frame
if not FileAccess.file_exists(path):
terminate(stem, "Root CUA acknowledgement timeout")
return
var ack: Variant = JSON.parse_string(FileAccess.get_file_as_string(path))
if not ack is Dictionary or ack.get("target_pid") != OS.get_process_id() or ack.get("sequence") != serial or ack.get("request_sha256") != FileAccess.get_sha256(folder.path_join("request.json")):
terminate(stem, "Root CUA acknowledgement identity/hash")
return
await frames()
for event: Dictionary in input_observer.events.slice(first_event):
if event.has("dispatch"):
last_dispatch = event["dispatch"]
json_file(stem + "/observed.json", {"pid": OS.get_process_id(), "sequence": serial,
"ack_observed_ticks_usec": Time.get_ticks_usec(), "request_sha256": FileAccess.get_sha256(folder.path_join("request.json")),
"ack_observed_unix_ns": int(Time.get_unix_time_from_system() * 1000000000.0),
"observer_endpoint": "null_modal_receipts_only" if input_observer.endpoint == null else "passive_read_only_dispatch",
"events": input_observer.events.slice(first_event), "status": main.build_status_label.text})
receipts.append("os-pair/" + layout + "/" + stem + "/ack.json")
all_requests.append("os-pair/" + layout + "/" + stem + "/ack.json")
func annotate(row: Dictionary) -> void:
var view: WorldPreview = main.world_preview
row["rectangles"] = Codec.new().typed(draw_rectangles(view.get_life_observation_report(), view))
row["viewport_to_screen"] = affine(view.get_screen_transform() * view.get_global_transform_with_canvas().affine_inverse())
row["reveal"] = reveal_rows.duplicate(true)
reveal_rows.clear()
row["input_method"] = "native_os"
row["requests"] = receipts.duplicate()
receipts.clear()
# Rectangles describe existing draw commands; no second renderer is invoked.
func draw_rectangles(report: Dictionary, view: WorldPreview) -> Dictionary:
var rows: Array = []
for id: Variant in report.get(&"rooms", {}):
var room: Dictionary = report[&"rooms"][id]
if room.has(&"rect"):
rows.append({"category": "room", "id": id, "rect": room[&"rect"], "style_source": "scripts/ui/pixel_tower_art.gd"})
for index: int in range(report.get(&"landing_badges", []).size()):
var badge: Dictionary = report[&"landing_badges"][index]
rows.append(badge_rectangle("queue", str(index), badge[&"at"], badge[&"text"]))
for cabin: Dictionary in report.get(&"cabins", []):
rows.append({"category": "EV", "id": cabin[&"shaft_id"], "rect": cabin[&"rect"], "style_source": "scripts/ui/pixel_tower_art.gd"})
for actor: Dictionary in cabin[&"passengers"]:
rows.append(actor_rectangle(actor))
for actor: Dictionary in report.get(&"actor_drawings", []):
rows.append(actor_rectangle(actor))
if actor[&"staff"] and report[&"resting_staff"].has(StringName(String(actor[&"key"]).trim_prefix("staff/"))):
var rest := actor_rectangle(actor)
rest["category"] = "rest"
rows.append(rest)
for index: int in range(report.get(&"badges", []).size()):
var badge: Dictionary = report[&"badges"][index]
rows.append(badge_rectangle("goods" if badge[&"kind"] == &"supply" else "business", str(index), badge[&"at"], badge[&"text"]))
var transform := view.get_global_transform_with_canvas()
for row: Dictionary in rows:
row["viewport_rect"] = transform * (row["rect"] as Rect2)
row["screen_rect"] = view.get_screen_transform() * (row["rect"] as Rect2)
var source: String = FileAccess.get_file_as_string("res://" + row["style_source"])
var regex := RegEx.new()
regex.compile('Color\\("([0-9a-fA-F]{6})"')
var colors: Array = []
for found: RegExMatch in regex.search_all(source):
if not colors.has(found.get_string(1)): colors.append(found.get_string(1))
row["style_colors"] = colors
return {"rows": rows}
func badge_rectangle(category: String, id: String, at: Vector2, text: String) -> Dictionary:
var width := ThemeDB.fallback_font.get_string_size(text, HORIZONTAL_ALIGNMENT_LEFT, -1, 10).x + 6
return {"category": category, "id": id, "rect": Rect2(at, Vector2(width, 14)),
"text": text, "font_width": width - 6, "style_source": "scenes/main/world_preview.gd"}
func actor_rectangle(actor: Dictionary) -> Dictionary:
return {"category": "staff" if actor[&"staff"] else "guest", "id": actor[&"key"],
"rect": Rect2(actor[&"at"], Vector2(16, 18) * float(actor[&"scale"])), "style_source": "scripts/ui/pixel_people.gd"}
func point(node: Control) -> Dictionary:
var viewport: Vector2 = node.get_global_transform_with_canvas() * (node.size * 0.5)
var screen: Vector2 = node.get_screen_transform() * (node.size * 0.5)
return {"node": String(node.get_path()), "viewport": [viewport.x, viewport.y], "screen": [screen.x, screen.y], "window_id": node.get_window().get_window_id(),
"viewport_to_screen": affine(node.get_screen_transform() * node.get_global_transform_with_canvas().affine_inverse())}
func setup() -> void:
await super.setup()
input_method = "native_os"
expect(DisplayServer.get_name() != "headless", "real OS display")
tree.root.title = "The Tree P23-03 " + layout + " " + role + " PID " + str(OS.get_process_id())
input_observer = InputObserver.new()
# No handler dispatch: this observer only records incoming OS events.
tree.root.add_child(input_observer)
json_file(role + "-profile.json", {"pid": OS.get_process_id(),
"user_data_dir": OS.get_user_data_dir(), "engine": Engine.get_version_info(),
"display": DisplayServer.get_name()})
func button(node: BaseButton) -> void:
expect(node != null and not node.disabled, "reachable ordinary button")
if node == null or node.disabled: return
await request({"action": "click", "point": point(node), "button_text": str(node.get("text"))})
func load_menu(menu: MenuButton) -> void:
var action := point(menu)
action.merge({"action": "load_menu", "item_id": 0, "instructions": "Open this normal menu and choose manual load (item 0)."})
await request(action)
func screenshot(label: String) -> void:
await frames()
await request({"action": "screenshot", "id": label, "layout": layout})
func layer_observations(prefix: String) -> void:
if role == "writer" and prefix == "writer": await screenshot("calibration")
await button(main.front_button)
await screenshot(prefix + "-FRONT")
await button(main.back_button)
await screenshot(prefix + "-BACK")
if role == "writer" and prefix == "writer": await ordinary_camera()
func ordinary_camera() -> void:
var view: WorldPreview = main.world_preview
var at := point(view)
expect(not view.pan_mode, "ordinary zoom starts with move mode off")
var zoom: float = view.zoom_level
await request({"action": "wheel", "point": at, "direction": "down",
"ctrl": false, "move_mode": false})
expect(view.zoom_level != zoom, "ordinary wheel changes zoom")
await screenshot("ordinary-wheel-zoom")
await button(main.pan_button)
await screenshot("pan-mode-button")
expect(view.pan_mode, "PanModeButton enables move mode")
zoom = view.zoom_level
var pan: Vector2 = view.pan_offset
await request({"action": "wheel", "point": point(view), "direction": "up",
"ctrl": false, "move_mode": true})
expect(view.pan_offset != pan and view.zoom_level == zoom, "ordinary move-mode wheel pans")
await screenshot("move-mode-wheel-pan")
func finish_observations() -> void:
json_file(role + "-requests.json", {"requests": all_requests, "pid": OS.get_process_id()})
if is_instance_valid(input_observer): input_observer.queue_free()
super.finish_observations()
+8
View File
@@ -0,0 +1,8 @@
extends SceneTree
const Passive := preload("res://tests/fixtures/p23-03/passive_observation.gd")
func _initialize() -> void:
call_deferred("run")
func run() -> void:
var result: Dictionary = await Passive.new().run(self, OS.get_environment("P23_OBSERVATION_ROOT"), OS.get_environment("P23_OBSERVATION_ROLE"))
print("P23 passive OS observation: %d checks, %d failures" % [result.checks, result.failures])
quit(0 if result.failures == 0 else 1)
+8
View File
@@ -15,6 +15,14 @@ func expect(value: bool, message: String) -> void:
push_error("P23 life observation: " + message)
func run() -> void:
var layout := OS.get_environment("P23_LAYOUT")
var output_directory := OS.get_environment("P23_OUTPUT")
if layout in ["A", "B"] and not output_directory.is_empty():
var comparison := preload("res://tests/fixtures/p23-03/layout_comparison.gd").new()
expect(await comparison.run(self, output_directory, layout), "native layout " + layout)
print("P23 life observation: %d checks, %d failures" % [checks, failures])
quit(0 if failures == 0 else 1)
return
var clock: Variant = root.get_node("GameClock")
clock.set_paused(true)
# Isolated real typed components test state labels, without claiming gameplay.