P23-03: 生活の観察

This commit is contained in:
Agent Control Runner
2026-10-15 07:59:43 +09:00
parent 860136f709
commit 03100beabe
13 changed files with 1386 additions and 101 deletions
@@ -3702,6 +3702,17 @@
"scope": "normal placement; native and OS pending"
}
}
,{
"test": "tests/ui/test_p23_life_observation.gd",
"template": "P23 life observation: <checks> checks, <failures> failures",
"required": true,
"fixed_check_count_used": false,
"source_sha256_at_schema_audit": "b9afa66ec73d3a5cadf86f9a71f0d13f5b085c3cc9a534dfe29b2bed0e2e76a0",
"authority": {
"path": "docs/spec/acceptance/P23-03.json",
"scope": "component and read-only observation only; native unexecuted; whole unit blocked"
}
}
],
"unknown_roster_suite_policy": "refuse, explicitly review its source/schema before accepting",
"roster_count_is_not_hardcoded": true,
@@ -0,0 +1,41 @@
# P23-03 Phase 0 — API and state map
Base: `860136f7099f5e3aee99da5798a627a77f2d8eea`. Static inspection only; no engine execution.
## Exact source references
Line numbers below refer to the base; SHA256 covers the complete file.
| Source | Lines / API | SHA256 |
| --- | --- | --- |
| autoload/simulation_runtime.gd | 203 initialize(clock,state,seed); 225 seed assignment; 466 submit_command; 508 get_rng_stream; 511 create_snapshot; 1483 enable_production_operations; 1780 default demand configuration; 2831 arrival boundary; 2949 apply arrivals; 4137 queue_manual_checkpoint; 4141 save_to_path; 4169 load_from_path; 5066 seed streams; 6103 spawn_guest executor; 7629 build_facility executor; 10859 staff management report; 12464 supply management report | 4696df4a77feb465a5a3ba8b806569f7bcbe1d03c88289acfb78a3928f307d67 |
| autoload/game_state.gd | 20 reset | cf0e884910220e54c56b5875983e53de439af9d893be1e1b65c2121d2a30722f |
| scripts/core/simulation_kernel.gd | 91 reset; 104 advance_minutes; 122 advance_one_minute; 175 set_paused | 341c0c878c4894adbac628d0db6362ff69fc3347bdd9e61e50936e15dc2d1edb |
| scripts/facility/facility_service.gd | 109 get_instances; 656 create_snapshot | ceb4a3e9224587423f273cdb2b56e6843ab176ac1fc4fe79dd498b5ed5c33b77 |
| scripts/guest/guest_service.gd | 170 get_guests; 982 create_snapshot | 56880410e9a00816f9a95d30821982955532de8dd976980934e12016a9f2b21e |
| scripts/staff/staff_service.gd | 166 get_staff_members; 682 get_tasks; 1064 create_snapshot | 355ad5c3c9f85dc4d4ffc3bc4293e36f81078c403d1662512a735b2c8d815fb3 |
| scripts/transport/elevator_service.gd | 345 get_report; 362 create_snapshot | cea09dd9204551c26b31b30cd3aa847cc77e3b75fdac31662b9694a95fb3580c |
| scripts/world/path_service.gd | 545 get_cell_traffic | 447135729a1106639a6eee6754af3195c8a8b458ca26980ae91fa39d80dcfbd4 |
| scripts/supply/supply_logistics_bridge.gd | 122 get_report | b9e394af788595d2744b71f1907c8e7c3d762d38ff49992b1ee74e63b9639e80 |
| scripts/facility/facility_instance.gd | 8 OperationState; 10 State; 30 state and subsequent live fields | 0c6aa493f3299611e05dd26aa429dda8cf7b5c479e4d09632036fb862f79a324 |
| tests/transport/test_runtime_transport_comparison.gd | 91 existing fixed-demand setup; 109 real paid build command | ba577397fa8db5a6cf92cfd398dd7a5ece49e6b59856f14772b67b51ddc0bde9 |
## Deterministic demand capability
An existing deterministic input path is available without Runtime edits: fresh clock/state, `initialize(clock,state,seed)`, then the same scheduled `submit_command(&"spawn_guest", payload, ..., execute_at, idempotency_key)` sequence. The executor validates registered definitions and real walkable cells, creates wallets and GuestRuntime actors, and verifies Path actor projection and movement credit. Explicit IDs must avoid the reserved `guest-auto-` namespace. Inputs and processed results must both be recorded; a seed/hash alone does not prove identical arrivals.
The existing transport comparison exercises public spawning, real paid construction and Runtime-owned movement. It is fixed-demand experimentation, not proof of natural autonomous demand or all six observation categories. Its numeric assertions are not adopted as new acceptance thresholds. Automatic production arrivals are a separate path (`enable_production_operations`): population, weather and terminal-state gates mean the same seed alone cannot guarantee identical accepted arrivals. Layouts must use identical production settings and compare actual arrival inputs/results.
`build_facility` resolves the registered definition, runs `_build_facility_precheck`, then calls `construction_service.start_construction` with `definition.build_minutes`. Construction cost, time and capacities must come from definitions. B may add only the predeclared existing corridor/EV through this command.
## Clock and observation
`reset` defines initial time. Paused manual `advance_minutes` is explicitly supported; abort/pause boundaries must be checked. Commands run before minute increment, then time boundaries, mana, construction, logistics, facility completion, vitals, incidents, queues, AI and publication.
Facility/guest/staff getters expose typed objects; presentation must copy values without mutating them. Elevator reports contain shafts/trips. Path traffic provides actual density/capacity/block fields. Logistics reports contain requests/deliveries, source/destination IDs, carrier, requirements, weight, state/reason and task ID. They do not contain warehouse stock: requested quantities must never be labelled stock.
## Formal save/load and evidence
Use `queue_manual_checkpoint` or `save_to_path`, then `load_from_path` under a distinct real PID. The load API pauses the clock and invokes SaveService package validation before restore. `restore_snapshot` alone is not formal resume. Preserve all package members, source/spec/engine/settings pins, whole typed sparse snapshots and actual CUA/process/cleanup evidence.
Original references read: BASELINE.txt lines 10,13,16,25,37,43,69,83,106,216; IMPLEMENTATION_PLAN.md line 1800; immutable P23-03 spec; recorded direction 83e836e. All seven conditions remain mandatory, with no partial credit. Root native/OS/resume/full-regression evidence is missing.
@@ -0,0 +1,94 @@
# P23-03 worker status: submitted static work, implementation incomplete
Base commit: `860136f7099f5e3aee99da5798a627a77f2d8eea`.
This submission preserves the previous dirty work and adds genuine static
implementation. It is **not acceptance-ready**. No external Runtime capability
gap or out-of-scope defect has been established. Remaining implementation is
listed separately from pending Root evidence; no acceptance credit is claimed.
## Inspection and changes in this repair
- Re-read the recorded direction, Phase 0 map, previous status and renderer diff.
The single frame builder supplies drawing and the deep-copy public report.
No renderer redesign or additional renderer was introduced. Existing renderer
and component test bytes were retained.
- Added `tests/fixtures/p23-03/layout_comparison.gd`. Both layouts initialize the
public Runtime with seed 806042, reset budget and clock, identical production
defaults and the same recorded 48-person arrival script. Commands build the
existing elevator and corridors; B adds only the declared second elevator.
Preparation time derives from registered definition build times.
- Predeclared metric: sum of destination-arrival minutes for the entire cohort.
The checker rejects incomplete cohorts, changed actual arrival payloads,
additional layout changes, changed initial state, and non-strict improvement.
No observed improvement is claimed: the fixture has not run.
- Extended diagnostic_stream with hash-addressed typed event/input records.
The fixture records actual command results, domain movement events and
per-minute guest/path/EV/facility projections, including service history,
queues and density. Whole aggregate state/report pairs are sparse (30-minute
intervals); the existing P08 typed encoder is reused without truncation.
- Extended independent checking of room service/rescue occupants and business
enum fields, queue allocation/overflow, shared transforms, traffic, logistics
report selection/order and cabin geometry. Added Rect2 and vector-key decode.
The checker still refuses whole-unit acceptance.
## Exact remaining implementation (not external blockers)
1. Finish rest destination selection from typed path costs and event/defense
locks. Current room check validates eligibility and reported rest references;
it does not independently prove which rest destination Runtime selected.
2. Finish every actor/passenger mode and geometry, including cleaning,
rest/rescue/service slots, visual track history, badge text and screenshot
rect/pixel provenance. Cabin endpoints and rectangles are checked, but
unpaused prior-frame interpolation origin is not independently established.
3. Complete A/B definition/source/production pins and command/event/path
cross-validation, invoke the fixture from the Root collector, and add
representative real operational scenarios for all six categories and BACK.
The added experiment covers the fixed-demand FRONT transport comparison;
it does not establish actual service purchasing or all-six-category coverage.
4. Replace run_p23_life_observation.py readiness stub with the actual Root
collector using P08 owned-process/profile/source/engine/ordinary-audio and
CUA request/ACK helpers. It remains unchanged; it launches nothing.
Formal save, distinct-PID normal resume, both-layout FRONT/BACK/pan/zoom,
complete CUA receipts, all-run cleanup/protection and failure history remain
to be wired and independently checked.
5. Add missing meaningful negatives, especially normal OS/distinct-PID resume,
visual provenance and the remaining rest/motion branches. Existing 12 local
tests are not a complete seven-condition test matrix.
Steps (2) and (3) of the repair direction are therefore still incomplete;
step (4) is not implemented. This is a partial static submission, not a claim
that the requested repair is finished. Do not begin acceptance on an assumption
that all worker deliverables are complete.
## Checks actually run in this repair
- `python3 -B tools/test_p23_life_observation_evidence.py`: 12 test methods,
zero failures. Synthetic Python examples validate rejection logic only.
- AST syntax: all three P23 Python tools parsed.
- Marker JSON: old rows and metadata unchanged; exactly one appended row;
its SHA matches the unchanged GDScript component test.
- `git diff --check`: passed.
- No Godot parsing/runtime, native, OS, Runner or full regression execution.
- No git writes, subagents, integration, new assets or production changes.
suite-policy.json was not changed; no new suite was registered.
## Source SHA256 at local checks
| File | SHA256 |
| --- | --- |
| scenes/main/world_preview.gd | add770377c190a5ccdde95379539de9753f7dabc703c1d58452ba590c6eda2f0 |
| scripts/ui/room_presentation.gd | 55340b7b623532e861be1d21225635fd9169e2e40928597c1cd0458b99524dd9 |
| tests/ui/test_p23_life_observation.gd | b9afa66ec73d3a5cadf86f9a71f0d13f5b085c3cc9a534dfe29b2bed0e2e76a0 |
| tests/fixtures/p23-03/diagnostic_stream.gd | ff178a094fc310b0640d923a15058c6f6fcf859551c14f6428a5709dcd24260e |
| tests/fixtures/p23-03/layout_comparison.gd | ddf00268b8e32e3c5c078ebb57ada321382ecc7c5fbd87efad7e9dfbe768df6b |
| tools/run_p23_life_observation.py | 53c2f0638d4a861cf4fcb664504c25df33ae5478207867654f1c8cdf58fa23c7 |
| tools/check_p23_life_observation_evidence.py | 318b253a08248a753b88902e382edc5f01cbce6f1ed8092359698b69d3bb0503 |
| tools/test_p23_life_observation_evidence.py | 0f20d3de20b44f46f8738c0d022fcc1179656014e85fe31754f00b87ec2c288a |
## Root evidence: pending
After genuine completion and freezing: both-layout native and actual normal OS
observation, full CUA trace, formal save and distinct-PID resume, mandatory exact
version full regression and independent Opus review. These absent Root results
are **pending**, not blocked. The fixed 30-package / 88-unit denominator and all
P08/P22/P25/Windows/held-gesture/performance exclusions remain unchanged.
+247 -100
View File
@@ -78,6 +78,7 @@ var _queue_positions: Dictionary = {}
var _queue_hidden: Dictionary = {}
var _landing_badges: Array[Dictionary] = []
var _transport_report: Dictionary = {}
var _life_observation: Dictionary = {}
var _dragging := false
var _drag_button := MOUSE_BUTTON_NONE
@@ -511,6 +512,7 @@ func _draw() -> void:
func _draw_contents() -> void:
_life_observation = {}
if _inactive_layer_art != null:
_inactive_layer_art.visible = false
_draw_background()
@@ -521,15 +523,25 @@ func _draw_contents() -> void:
var map_rect := get_map_rect()
if map_rect.size == Vector2.ZERO:
return
_life_observation = _build_life_observation()
_room_presentations = _life_observation[&"rooms"]
_carried_patients = _life_observation[&"carried_patients"]
_treated_patients = _life_observation[&"treated_patients"]
_resting_staff = _life_observation[&"resting_staff"]
_queue_positions = _life_observation[&"queue_positions"]
_queue_hidden = _life_observation[&"queue_hidden"]
_landing_badges.assign(_life_observation[&"landing_badges"])
_transport_report = _life_observation[&"elevators"]
_boarded_actor_ids = _life_observation[&"boarded"]
_actor_tracks = _life_observation[&"actor_tracks"]
_elevator_tracks = _life_observation[&"elevator_tracks"]
_draw_world_cells(bounds, map_rect)
if show_grid:
_draw_congestion_overlay(bounds)
_prepare_room_presentations()
_prepare_queues()
_draw_world_entity_icons()
_draw_legacy_stairs()
_draw_elevators()
_draw_other_layer()
_draw_congestion_overlay(bounds)
_draw_life_badges()
_draw_construction_labels()
_draw_facility_labels()
_draw_actors()
@@ -540,6 +552,102 @@ func _draw_contents() -> void:
_draw_map_caption(map_rect)
## The report describes the last submitted draw, not an independently sampled world.
## Callers cannot mutate either the frame or its interpolation tracks.
func get_life_observation_report() -> Dictionary:
return _life_observation.duplicate(true)
## Pure with respect to Runtime and this Control: helpers mutate only fresh local
## dictionaries (including copies of visual tracks). No whole Runtime snapshot
## is copied per frame; whole typed snapshots belong to sparse diagnostics.
func _build_life_observation() -> Dictionary:
var model := _observe_rooms()
model.merge(_observe_queues())
model[&"layer"] = layer_mode
model[&"zoom"] = zoom_level
model[&"pan"] = pan_offset
model[&"viewport"] = size
model[&"animation_time"] = _animation_time
model[&"actor_tracks"] = _actor_tracks.duplicate(true)
model[&"elevator_tracks"] = _elevator_tracks.duplicate(true)
model[&"boarded"] = {}
var clock := get_node_or_null("/root/GameClock")
model[&"paused"] = clock != null and bool(clock.get("paused"))
model[&"cabins"] = _observe_elevators(model)
model[&"actor_drawings"] = _observe_actors(model)
var guests: Array[Dictionary] = []
var staff: Array[Dictionary] = []
var tasks: Array[Dictionary] = []
var guest_service: Variant = _service(&"guest_service")
if guest_service != null and guest_service.has_method(&"get_guests"):
for guest: GuestRuntime in guest_service.get_guests():
guests.append({&"id": guest.guest_id, &"state": GuestRuntime.State.keys()[guest.state],
&"cell": guest.current_cell, &"target": guest.target_facility_id,
&"service": guest.current_service_id, &"queue": guest.queue_request_id})
var staff_service: Variant = _service(&"staff_service")
if staff_service != null:
for member: StaffRuntime in staff_service.get_staff_members():
staff.append({&"id": member.staff_id, &"state": StaffRuntime.State.keys()[member.state],
&"cell": member.current_cell, &"task": member.current_task_id,
&"facility": member.assigned_facility_id, &"role": member.role_id,
&"carried_inventory_id": member.carried_inventory_id})
for task: StaffTask in staff_service.get_tasks():
tasks.append({&"id": task.task_id, &"state": StaffTask.State.keys()[task.state],
&"type": task.task_type, &"staff": task.assigned_staff_id,
&"target": task.target_entity_id, &"rescue_facility": task.rescue_facility_id})
model[&"guests"] = guests
model[&"staff"] = staff
model[&"tasks"] = tasks
var bridge: Variant = _service(&"supply_logistics_bridge")
var goods: Dictionary = bridge.get_report() if bridge != null else {&"requests": [], &"deliveries": []}
model[&"goods"] = goods
var badges: Array[Dictionary] = []
var facilities: Variant = _service(&"facility_service")
var world: Variant = _service(&"world_service")
if facilities != null and world != null:
for instance: FacilityInstance in facilities.get_instances():
if instance.origin.z != layer_mode:
continue
var cells: Array[Vector3i] = world.get_entity_cells(instance.instance_id)
if cells.is_empty():
continue
var rect := _cell_bounds_rect(cells)
var data: Dictionary = model[&"rooms"][instance.instance_id]
data[&"rect"] = rect
data[&"definition_id"] = instance.definition_id
var count := RoomPresentation.slots(rect, _cell_dimensions().x / 16.0, instance.definition_id).size()
badges.append({&"facility": instance.instance_id, &"kind": &"business",
&"at": rect.position + Vector2(2, 2),
&"text": RoomPresentation.business_label(data) + " " + RoomPresentation.label(data, count)})
var requests: Array[Dictionary] = []
for request: Dictionary in goods.get(&"requests", []):
if request[&"source_facility_id"] == instance.instance_id or request[&"destination_facility_id"] == instance.instance_id:
requests.append(request)
if not requests.is_empty():
badges.append({&"facility": instance.instance_id, &"kind": &"supply",
&"at": rect.position + Vector2(2, 16), &"requests": requests,
&"text": RoomPresentation.supply_label(requests)})
model[&"badges"] = badges
var traffic: Array[Dictionary] = []
var path: Variant = _service(&"path_service")
var bounds := _world_bounds()
if path != null and not bounds.is_empty():
for y in range(int(bounds[&"min_y"]), int(bounds[&"max_y"]) + 1):
for x in range(int(bounds[&"min_x"]), int(bounds[&"max_x"]) + 1):
var cell := Vector3i(x, y, layer_mode)
var row: Dictionary = path.get_cell_traffic(cell)
if not row.is_empty():
traffic.append({&"cell": cell, &"traffic": row, &"rect": get_cell_rect(cell).grow(-2.0)})
model[&"traffic"] = traffic
return model
func _draw_life_badges() -> void:
for badge: Dictionary in _life_observation.get(&"badges", []):
_draw_readable_badge(badge[&"at"], badge[&"text"])
func _draw_background() -> void:
var night := false
var clock := get_node_or_null("/root/GameClock")
@@ -650,40 +758,23 @@ func _draw_leaf_cluster(center: Vector2, p: float, variation: int) -> void:
draw_rect(Rect2(c+offset,Vector2(3+i%2,2)*p),Color("95ad69") if i%3==0 else Color("648957"))
func _draw_congestion_overlay(bounds: Dictionary) -> void:
var path_service: Variant = _service(&"path_service")
if path_service == null or not path_service.has_method(&"get_cell_traffic"):
return
for y in range(int(bounds[&"min_y"]), int(bounds[&"max_y"]) + 1):
for x in range(int(bounds[&"min_x"]), int(bounds[&"max_x"]) + 1):
var cell := Vector3i(x, y, layer_mode)
var traffic_value: Variant = path_service.call(&"get_cell_traffic", cell)
if not traffic_value is Dictionary or traffic_value.is_empty():
continue
var traffic: Dictionary = traffic_value
var density := int(traffic.get(&"current_density", 0))
var capacity := maxi(int(traffic.get(&"base_capacity", 1)), 1)
var blocked := bool(traffic.get(&"temporary_block", false))
if density <= 0 and not blocked:
continue
var ratio := float(density) / float(capacity)
var color := Color("55c96b")
if blocked or ratio > 1.0:
color = Color("e34332")
elif ratio >= 0.65:
color = Color("e3a62f")
var rect := get_cell_rect(cell).grow(-2.0)
var center := rect.get_center()
var route_width := clampf(rect.size.y * 0.16, 2.0, 5.0)
draw_line(
Vector2(rect.position.x + rect.size.x * 0.12, center.y),
Vector2(rect.end.x - rect.size.x * 0.12, center.y),
Color(color, 0.90),
route_width
)
if blocked:
draw_line(rect.position, rect.end, color, 2.0)
draw_line(Vector2(rect.end.x, rect.position.y), Vector2(rect.position.x, rect.end.y), color, 2.0)
func _draw_congestion_overlay(_bounds: Dictionary) -> void:
for row: Dictionary in _life_observation.get(&"traffic", []):
var rect: Rect2 = row[&"rect"]
var traffic: Dictionary = row[&"traffic"]
var density := int(traffic[&"current_density"])
var blocked := bool(traffic[&"temporary_block"])
if density <= 0 and not blocked:
continue
var ratio := float(density) / maxi(int(traffic[&"base_capacity"]), 1)
var color := Color("e34332") if blocked or ratio > 1.0 else Color("e3a62f") if ratio >= 0.65 else Color("55c96b")
var center := rect.get_center()
draw_line(Vector2(rect.position.x + rect.size.x * 0.12, center.y),
Vector2(rect.end.x - rect.size.x * 0.12, center.y),
Color(color, 0.90 if show_grid else 0.28), clampf(rect.size.y * 0.16, 2.0, 5.0))
if blocked:
draw_line(rect.position, rect.end, Color(color, 0.90 if show_grid else 0.28), 2.0)
draw_line(Vector2(rect.end.x, rect.position.y), Vector2(rect.position.x, rect.end.y), Color(color, 0.90 if show_grid else 0.28), 2.0)
## Visual-only level comes from the existing live FacilityInstance, not the
@@ -794,10 +885,6 @@ func _draw_world_entity_icons() -> void:
draw_rect(Rect2(rect.end.x-4*p,rect.position.y+2*p,2*p,2*p),_facility_color(instance_id))
if instance_id==selected_entity_id:
draw_rect(rect.grow(-p),Color("f5d582"),false,2*p)
var data: Dictionary = _room_presentations.get(instance_id, {})
if not data.is_empty() and (rect.size.x >= 80 or instance_id == selected_entity_id) and id not in [&"guest_corridor", &"service_corridor", &"guest_stairs", &"service_stairs", &"guest_elevator", &"service_elevator"]:
var status := RoomPresentation.label(data, RoomPresentation.slots(rect, p, id).size())
_draw_readable_badge(rect.position+Vector2(2, 2), status.replace("利用", "治療") if id == &"infirmary" else status.replace("利用", "休憩") if id in [&"staff_waiting_room", &"staff_dormitory"] else status)
func _draw_readable_badge(at: Vector2, text: String) -> void:
@@ -810,13 +897,21 @@ func _draw_readable_badge(at: Vector2, text: String) -> void:
func _prepare_room_presentations() -> void:
_room_presentations.clear()
_carried_patients.clear()
_treated_patients.clear()
_resting_staff.clear()
var model := _observe_rooms()
_room_presentations = model[&"rooms"]
_carried_patients = model[&"carried_patients"]
_treated_patients = model[&"treated_patients"]
_resting_staff = model[&"resting_staff"]
func _observe_rooms() -> Dictionary:
var _room_presentations: Dictionary = {}
var _carried_patients: Dictionary = {}
var _treated_patients: Dictionary = {}
var _resting_staff: Dictionary = {}
var service: Variant = _service(&"facility_service")
if service == null:
return
return {&"rooms": _room_presentations, &"carried_patients": _carried_patients, &"treated_patients": _treated_patients, &"resting_staff": _resting_staff}
for instance: Variant in service.get_instances():
if instance.origin.z == layer_mode:
_room_presentations[instance.instance_id] = RoomPresentation.describe(instance)
@@ -844,12 +939,22 @@ func _prepare_room_presentations() -> void:
else:
_carried_patients[task.target_entity_id] = task.assigned_staff_id
return {&"rooms": _room_presentations, &"carried_patients": _carried_patients, &"treated_patients": _treated_patients, &"resting_staff": _resting_staff}
func _prepare_queues() -> void:
_queue_positions.clear()
_queue_hidden.clear()
_landing_badges.clear()
_transport_report.clear()
var model := _observe_queues()
_queue_positions = model[&"queue_positions"]
_queue_hidden = model[&"queue_hidden"]
_landing_badges.assign(model[&"landing_badges"])
_transport_report = model[&"elevators"]
func _observe_queues() -> Dictionary:
var _queue_positions: Dictionary = {}
var _queue_hidden: Dictionary = {}
var _landing_badges: Array[Dictionary] = []
var _transport_report: Dictionary = {}
var groups: Dictionary = {}
var facilities: Variant = _service(&"facility_service")
var guests: Variant = _service(&"guest_service")
@@ -908,10 +1013,10 @@ func _prepare_queues() -> void:
index += 1
for hidden: int in range(index, members.size()):
_queue_hidden[members[hidden]] = true
var text := "待ち%d人" % members.size()
if index < members.size():
text += "(他%d人)" % (members.size()-index)
_landing_badges.append({&"at": get_cell_rect(cell).position + Vector2(-80 if lift_cells.has(cell) else 0, -16), &"text": text})
var text := RoomPresentation.queue_label(members.size(), members.size()-index)
_landing_badges.append({&"at": get_cell_rect(cell).position + Vector2(-80 if lift_cells.has(cell) else 0, -16), &"text": text, &"cell": cell, &"members": members.duplicate(), &"visible_count": index, &"overflow": members.size()-index})
return {&"queue_positions": _queue_positions, &"queue_hidden": _queue_hidden, &"landing_badges": _landing_badges, &"elevators": _transport_report}
func _add_queue_member(groups: Dictionary, key: String, cell: Vector3i) -> void:
@@ -1039,14 +1144,21 @@ func _draw_facility_labels() -> void:
func _draw_elevators() -> void:
_boarded_actor_ids.clear()
var transport: Variant = _service(&"elevators")
if transport == null or not transport.has_method(&"get_report"):
_elevator_tracks.clear()
return
var report_value: Variant = _transport_report
if not report_value is Dictionary:
return
for row: Dictionary in _life_observation.get(&"cabins", []):
PixelArt.elevator_cabin(self, row[&"rect"], row[&"pixel"], layer_mode == LayerMode.BACK, row[&"operational"])
for actor: Dictionary in row[&"passengers"]:
_draw_observed_actor(actor)
PixelArt.elevator_gate(self, row[&"rect"], row[&"pixel"], row[&"doors_open"])
_draw_readable_badge(row[&"badge_at"], row[&"badge"])
for badge: Dictionary in _life_observation.get(&"landing_badges", []):
_draw_readable_badge(badge[&"at"], badge[&"text"])
func _observe_elevators(observation: Dictionary) -> Array[Dictionary]:
var cabins: Array[Dictionary] = []
var _boarded_actor_ids: Dictionary = observation[&"boarded"]
var _elevator_tracks: Dictionary = observation[&"elevator_tracks"]
var report_value: Dictionary = observation[&"elevators"]
var alive: Dictionary = {}
var p := _cell_dimensions().x/16.0
for value: Variant in report_value.get(&"shafts", []):
@@ -1062,7 +1174,7 @@ func _draw_elevators() -> void:
for actor_id: Variant in passengers:
_boarded_actor_ids[_elevator_actor_key(String(actor_id))] = true
var floor_value := float(shaft.get(&"visual_floor", shaft.get(&"current_floor", stops[0].y)))
var visual_floor := _tracked_elevator_floor(shaft_id, floor_value)
var visual_floor := _tracked_elevator_floor(shaft_id, floor_value, observation)
if stops[0].z != layer_mode:
continue
var origin := get_cell_rect(stops[0])
@@ -1074,7 +1186,7 @@ func _draw_elevators() -> void:
var operational := bool(shaft.get(&"operational", true))
var phase := StringName(shaft.get(&"phase", &"IDLE"))
var doors_open := operational and phase in [&"IDLE", &"BOARDING", &"ALIGHTING"]
PixelArt.elevator_cabin(self,cabin,p,layer_mode==LayerMode.BACK,operational)
var passengers_drawn: Array[Dictionary] = []
var seat_columns := maxi(int((cabin.size.x / p - 4.0) / 8.0), 1)
var visible_count := mini(passengers.size(), seat_columns*2)
for index: int in range(visible_count):
@@ -1090,22 +1202,19 @@ func _draw_elevators() -> void:
var x := cabin.position.x + (2.0 + (index % seat_columns + 0.5)*8.0)*p
var feet := cabin.end.y - (2.0 + int(index / seat_columns)*9.0)*p
var center := Vector2(x, feet+3*p-_cell_dimensions().y*0.5)
_draw_pixel_actor(center,actor_key,is_staff,state,false,carrying,&"",true)
PixelArt.elevator_gate(self,cabin,p,doors_open)
var capacity := int(shaft.get(&"capacity", 0))
var load_text := "%d/%d人" % [passengers.size(), capacity]
if capacity > 0 and passengers.size() >= capacity:
load_text = "満員 " + load_text
if not operational:
load_text = "停止 " + load_text
_draw_readable_badge(Vector2(cabin.position.x, cabin.position.y-14), load_text)
for badge: Dictionary in _landing_badges:
_draw_readable_badge(badge[&"at"], badge[&"text"])
passengers_drawn.append(_actor_drawing(center,actor_key,is_staff,state,false,carrying,&"",true,observation))
cabins.append({&"shaft_id": shaft_id, &"rect": cabin, &"pixel": p,
&"operational": operational, &"doors_open": doors_open,
&"passengers": passengers_drawn,
&"badge_at": Vector2(cabin.position.x, cabin.position.y-14),
&"badge": RoomPresentation.elevator_label(shaft)})
for shaft_id: Variant in _elevator_tracks.keys():
if not alive.has(shaft_id):
_elevator_tracks.erase(shaft_id)
return cabins
func _elevator_actor_key(actor_id: String) -> String:
if actor_id.begins_with("guest:"):
@@ -1115,7 +1224,8 @@ func _elevator_actor_key(actor_id: String) -> String:
return actor_id
func _tracked_elevator_floor(shaft_id: StringName, current_floor: float) -> float:
func _tracked_elevator_floor(shaft_id: StringName, current_floor: float, observation: Dictionary = {}) -> float:
var _elevator_tracks: Dictionary = observation.get(&"elevator_tracks", self._elevator_tracks)
if not _elevator_tracks.has(shaft_id):
_elevator_tracks[shaft_id] = {&"floor":current_floor,&"from":current_floor,&"at":_animation_time}
var track: Dictionary = _elevator_tracks[shaft_id]
@@ -1131,6 +1241,19 @@ func _tracked_elevator_floor(shaft_id: StringName, current_floor: float) -> floa
func _draw_actors() -> void:
for row: Dictionary in _life_observation.get(&"actor_drawings", []):
_draw_observed_actor(row)
func _observe_actors(observation: Dictionary) -> Array[Dictionary]:
var rows: Array[Dictionary] = []
var _actor_tracks: Dictionary = observation[&"actor_tracks"]
var _boarded_actor_ids: Dictionary = observation[&"boarded"]
var _queue_hidden: Dictionary = observation[&"queue_hidden"]
var _queue_positions: Dictionary = observation[&"queue_positions"]
var _carried_patients: Dictionary = observation[&"carried_patients"]
var _treated_patients: Dictionary = observation[&"treated_patients"]
var _resting_staff: Dictionary = observation[&"resting_staff"]
var counts: Dictionary = {}
var alive: Dictionary = {}
var service_slots: Dictionary = {}
@@ -1144,18 +1267,18 @@ func _draw_actors() -> void:
if _boarded_actor_ids.has(key) or _queue_hidden.has(key):
_actor_tracks.erase(key)
continue
var position := _tracked_actor_position(key,guest.current_cell)
var position := _tracked_actor_position(key,guest.current_cell,observation)
if guest.current_cell.z==layer_mode:
var receiving := not String(guest.current_service_id).is_empty()
if _queue_positions.has(key):
position = _queue_positions[key]
elif receiving:
position = _service_use_position(guest,position,service_slots)
position = _service_use_position(guest,position,service_slots,observation)
if not position.is_finite():
continue
else:
position += _actor_offset(guest.current_cell,counts)
_draw_pixel_actor(position,key,false,int(guest.state),receiving,false,_guest_activity(guest))
rows.append(_actor_drawing(position,key,false,int(guest.state),receiving,false,_guest_activity(guest),false,observation))
var staff_service: Variant = _service(&"staff_service")
if staff_service != null and staff_service.has_method(&"get_staff_members"):
for staff: Variant in staff_service.call(&"get_staff_members"):
@@ -1166,27 +1289,29 @@ func _draw_actors() -> void:
if _boarded_actor_ids.has(key) or _queue_hidden.has(key):
_actor_tracks.erase(key)
continue
var position := _tracked_actor_position(key,staff.current_cell)
var position := _tracked_actor_position(key,staff.current_cell,observation)
if staff.current_cell.z==layer_mode:
if _carried_patients.has(staff.staff_id):
var carrier: Variant = staff_service.get_staff(_carried_patients[staff.staff_id])
if carrier != null:
position = _tracked_actor_position("staff/"+String(carrier.staff_id), carrier.current_cell) - Vector2(0, 12*_cell_dimensions().x/16.0)
_draw_pixel_actor(position,key,true,int(staff.state),false,false)
position = _tracked_actor_position("staff/"+String(carrier.staff_id), carrier.current_cell,observation) - Vector2(0, 12*_cell_dimensions().x/16.0)
rows.append(_actor_drawing(position,key,true,int(staff.state),false,false,&"",false,observation))
continue
if _resting_staff.has(staff.staff_id):
position = _resting_staff_position(staff.staff_id, position)
if position.is_finite(): _draw_pixel_actor(position,key,true,int(staff.state),false,false)
position = _resting_staff_position(staff.staff_id, position,observation)
if position.is_finite(): rows.append(_actor_drawing(position,key,true,int(staff.state),false,false,&"",false,observation))
continue
if _treated_patients.has(staff.staff_id):
position = _rescue_treatment_position(staff.staff_id, position)
if position.is_finite(): _draw_pixel_actor(position,key,true,int(staff.state),false,false)
position = _rescue_treatment_position(staff.staff_id, position,observation)
if position.is_finite(): rows.append(_actor_drawing(position,key,true,int(staff.state),false,false,&"",false,observation))
continue
_draw_pixel_actor(_queue_positions.get(key, _staff_work_position(staff, position+_actor_offset(staff.current_cell,counts))),key,true,int(staff.state),false,not String(staff.carried_inventory_id).is_empty(),_staff_work_animation(staff))
rows.append(_actor_drawing(_queue_positions.get(key, _staff_work_position(staff, position+_actor_offset(staff.current_cell,counts),observation)),key,true,int(staff.state),false,not String(staff.carried_inventory_id).is_empty(),_staff_work_animation(staff),false,observation))
for key: Variant in _actor_tracks.keys():
if not alive.has(key):
_actor_tracks.erase(key)
return rows
func _staff_work_animation(staff: StaffRuntime) -> StringName:
if staff.role_id != &"cleaner":
@@ -1201,7 +1326,8 @@ func _staff_work_animation(staff: StaffRuntime) -> StringName:
return &"idle"
func _staff_work_position(staff: Variant, fallback: Vector2) -> Vector2:
func _staff_work_position(staff: Variant, fallback: Vector2, observation: Dictionary = {}) -> Vector2:
var _room_presentations: Dictionary = observation.get(&"rooms", self._room_presentations)
if staff.state not in [StaffRuntime.State.WORKING, StaffRuntime.State.AVAILABLE]:
return fallback
var service: Variant = _service(&"facility_service")
@@ -1227,7 +1353,8 @@ func _staff_work_position(staff: Variant, fallback: Vector2) -> Vector2:
return Vector2(rect.position.x+(slot+0.5)*rect.size.x/columns, rect.position.y+26*p-_cell_dimensions().y*0.5)
func _tracked_actor_position(key: String, cell: Vector3i) -> Vector2:
func _tracked_actor_position(key: String, cell: Vector3i, observation: Dictionary = {}) -> Vector2:
var _actor_tracks: Dictionary = observation.get(&"actor_tracks", self._actor_tracks)
if not _actor_tracks.has(key):
_actor_tracks[key] = {&"cell":cell,&"from":cell,&"at":_animation_time,&"direction":1.0}
var track: Dictionary = _actor_tracks[key]
@@ -1246,7 +1373,8 @@ func _tracked_actor_position(key: String, cell: Vector3i) -> Vector2:
return cell_to_canvas(track[&"from"]).lerp(cell_to_canvas(cell),t)
func _service_use_position(guest: Variant, fallback: Vector2, _slots: Dictionary) -> Vector2:
func _service_use_position(guest: Variant, fallback: Vector2, _slots: Dictionary, observation: Dictionary = {}) -> Vector2:
var _room_presentations: Dictionary = observation.get(&"rooms", self._room_presentations)
# Guest paths stop at an access cell. While a service is active, project the
# occupant into that facility's use position; authoritative paths stay intact.
var id := StringName(guest.target_facility_id)
@@ -1281,6 +1409,19 @@ func _guest_activity(guest: Variant) -> StringName:
func _draw_pixel_actor(center: Vector2, key: String, staff: bool, state: int, receiving: bool, carrying: bool, activity: StringName = &"", riding := false) -> void:
_draw_observed_actor(_actor_drawing(center, key, staff, state, receiving, carrying, activity, riding))
func _draw_observed_actor(row: Dictionary) -> void:
var at: Vector2 = row[&"at"]
var p: float = row[&"scale"]
draw_rect(Rect2(at + Vector2(5, 17) * p, Vector2(8, 1) * p), Color("303c40", 0.2))
People.draw(self, at, p, row[&"mode"], row[&"frame"], row[&"variant"], row[&"staff"], row[&"flip"])
func _actor_drawing(center: Vector2, key: String, staff: bool, state: int, receiving: bool, carrying: bool, activity: StringName = &"", riding := false, observation: Dictionary = {}) -> Dictionary:
var _actor_tracks: Dictionary = observation.get(&"actor_tracks", self._actor_tracks)
var _queue_positions: Dictionary = observation.get(&"queue_positions", self._queue_positions)
var world_pixel := _cell_dimensions().x / 16.0
var scale := 0.5 if riding else 0.75
var p := maxf(floorf(world_pixel * scale), 1.0) if world_pixel >= 2.0 else world_pixel * scale
@@ -1307,8 +1448,10 @@ func _draw_pixel_actor(center: Vector2, key: String, staff: bool, state: int, re
elif _queue_positions.has(key) or (not staff and state == GuestRuntime.State.QUEUING):
mode = &"wait"
var at := (center + Vector2(-8 * p, _cell_dimensions().y * 0.5 - 3 * world_pixel - 17 * p)).round()
draw_rect(Rect2(at + Vector2(5, 17) * p, Vector2(8, 1) * p), Color("303c40", 0.2))
People.draw(self, at, p, mode, frame, variant, staff, float(track.get(&"direction", 1.0)) < 0.0)
return {&"key": key, &"center": center, &"at": at, &"scale": p, &"mode": mode,
&"frame": frame, &"variant": variant, &"staff": staff, &"state": state,
&"receiving": receiving, &"carrying": carrying, &"activity": activity,
&"riding": riding, &"flip": float(track.get(&"direction", 1.0)) < 0.0}
func _draw_excavation_jobs() -> void:
@@ -1421,14 +1564,14 @@ func _facility_color(instance_id: StringName) -> Color:
var instance: Variant = facility_service.call(&"get_instance", instance_id)
if instance == null:
return Color("6f8b67") if layer_mode == LayerMode.FRONT else Color("427582")
match int(instance.state):
0:
match int(_room_presentations.get(instance_id, {}).get(&"state", instance.state)):
FacilityInstance.State.BROKEN:
return Color("a24b45")
1, 2, 3:
FacilityInstance.State.NO_ACCESS, FacilityInstance.State.NO_POWER, FacilityInstance.State.NO_STAFF:
return Color("a77d3f")
4:
FacilityInstance.State.CLOSED:
return Color("6b6258")
5:
FacilityInstance.State.DEGRADED:
return Color("89914d")
_:
return Color("6f9a55") if layer_mode == LayerMode.FRONT else Color("67518e")
@@ -1579,7 +1722,9 @@ func _normalized_rotation(value: int) -> int:
return posmod(value, 360) / 90 * 90
func _rescue_treatment_position(patient_id: StringName, fallback: Vector2) -> Vector2:
func _rescue_treatment_position(patient_id: StringName, fallback: Vector2, observation: Dictionary = {}) -> Vector2:
var _room_presentations: Dictionary = observation.get(&"rooms", self._room_presentations)
var _treated_patients: Dictionary = observation.get(&"treated_patients", self._treated_patients)
var facility_id: StringName = _treated_patients[patient_id]
var world: Variant = _service(&"world_service")
var cells: Array[Vector3i] = world.get_entity_cells(facility_id) if world != null else []
@@ -1592,7 +1737,9 @@ func _rescue_treatment_position(patient_id: StringName, fallback: Vector2) -> Ve
return anchors[slot]+Vector2(0,-2*p-_cell_dimensions().y*0.5)
func _resting_staff_position(staff_id: StringName, fallback: Vector2) -> Vector2:
func _resting_staff_position(staff_id: StringName, fallback: Vector2, observation: Dictionary = {}) -> Vector2:
var _room_presentations: Dictionary = observation.get(&"rooms", self._room_presentations)
var _resting_staff: Dictionary = observation.get(&"resting_staff", self._resting_staff)
var facility_id: StringName = _resting_staff[staff_id]
var world: Variant = _service(&"world_service")
var cells: Array[Vector3i] = world.get_entity_cells(facility_id) if world != null else []
+48 -1
View File
@@ -10,7 +10,9 @@ static func describe(instance: Variant) -> Dictionary:
if not ids.has(id):
ids.append(id)
ids.sort_custom(func(a: StringName, b: StringName) -> bool: return String(a) < String(b))
return {&"operation": int(instance.operation_state), &"people": ids,
return {&"state": int(instance.state), &"state_name": FacilityInstance.State.keys()[instance.state],
&"reasons": instance.state_reasons.duplicate(),
&"operation": int(instance.operation_state), &"people": ids,
&"count": ids.size(), &"limit": int(instance.customer_capacity),
&"staff": instance.active_staff_ids.duplicate(), &"suspension": StringName(instance.suspension_reason)}
@@ -50,3 +52,48 @@ static func label(data: Dictionary, visible_count: int) -> String:
if count > visible_count:
value += "(他%d人)" % (count - visible_count)
return value
static func business_label(data: Dictionary) -> String:
if data.is_empty():
return ""
if int(data[&"operation"]) == FacilityInstance.OperationState.SUSPENDED:
return "休止・摘発" if data[&"suspension"] == &"RAIDED" else "休止・破壊"
match int(data[&"state"]):
FacilityInstance.State.BROKEN: return "破損"
FacilityInstance.State.NO_ACCESS: return "通路なし"
FacilityInstance.State.NO_POWER: return "魔力なし"
FacilityInstance.State.NO_STAFF: return "職員不足"
FacilityInstance.State.CLOSED: return "営業停止"
FacilityInstance.State.DEGRADED: return "低下運転"
FacilityInstance.State.OPEN: return "営業中"
return String(data[&"state_name"])
static func queue_label(count: int, overflow: int) -> String:
return "待ち%d人" % count + ("(他%d人)" % overflow if overflow > 0 else "")
static func elevator_label(shaft: Dictionary) -> String:
var count := (shaft[&"boarded"] as Array).size()
var capacity := int(shaft[&"capacity"])
var value := "%d/%d人" % [count, capacity]
if capacity > 0 and count >= capacity:
value = "満員 " + value
if not bool(shaft[&"operational"]):
value = "停止 " + value
return value
static func supply_label(requests: Array[Dictionary]) -> String:
# These are requests, never invented stock quantities. Preserve actual states.
var states: Dictionary = {}
for request: Dictionary in requests:
var state := String(request[&"state"])
states[state] = int(states.get(state, 0)) + 1
var names: Array = states.keys()
names.sort()
var parts: PackedStringArray = []
for state: String in names:
parts.append("%s:%d" % [state, states[state]])
return "物資依頼 " + " / ".join(parts)
+56
View File
@@ -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
+1
View File
@@ -0,0 +1 @@
uid://d23lifestr03
+104
View File
@@ -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)})
+77
View File
@@ -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
@@ -0,0 +1,491 @@
#!/usr/bin/env python3
"""Partial read-only semantic diagnostics. This version cannot accept P23-03.
Whole typed inputs are preserved. The existing typed codec is reused verbatim.
Unimplemented visual/OS checks are explicit blockers, never successful checks.
"""
import argparse
import copy
import hashlib
import importlib.util
import json
import math
from pathlib import Path
import re
import sys
sys.dont_write_bytecode = True
ROOT = Path(__file__).resolve().parent.parent
SPEC = 'docs/spec/acceptance/P23-03.json'
SPEC_SHA = '8568457df849ef587c731f072d77244ef5420fc4e1190c5069cfd6cfeea9c89f'
CONDITIONS = (
'room_occupancy_and_operation', 'queues_and_overflow',
'elevator_state_and_passengers', 'staff_state_and_jobs',
'supplies_and_carried_goods', 'same_demand_route_improvement',
'normal_os_and_distinct_pid_resume',
)
BLOCKERS = {
CONDITIONS[0]: 'Service/rescue occupancy, reason fields and room rectangles checked; rest route selection and badge pixels unfinished.',
CONDITIONS[1]: 'Queue lane allocation, overflow and positions checked; screenshot pixel provenance unfinished.',
CONDITIONS[2]: 'Shaft/trip and cabin geometry checked; prior-frame interpolation history and passenger geometry unfinished.',
CONDITIONS[3]: 'Typed staff/task projection implemented; motion/rest/rescue/cleaning drawing recomputation unfinished.',
CONDITIONS[4]: 'Carried inventory ID and logistics report checked; supply badge pixels unfinished.',
CONDITIONS[5]: 'Public-command A/B fixture and typed arrival metric added; native execution, definition pins and full command/path provenance checks pending.',
CONDITIONS[6]: 'P23 passive request/ACK collector and independent CUA/formal-resume gates unfinished; Root evidence missing.',
}
def module(name, path):
spec = importlib.util.spec_from_file_location(name, path)
result = importlib.util.module_from_spec(spec)
spec.loader.exec_module(result)
return result
codec = module('p23_existing_typed_codec', ROOT / 'tools/check_p08_migration_evidence.py')
def decode(node):
# Keep P08 scalar semantics, adding the renderer's Rect2 and vector keys.
if node['type'] == 'Dictionary':
result = {}
for key, value in node['entries']:
key = decode(key)
if isinstance(key, list):
key = tuple(key)
require(key not in result, 'duplicate typed dictionary key')
result[key] = decode(value)
return result
if node['type'] == 'Array':
return [decode(value) for value in node['items']]
if node['type'] == 'Rect2':
match = re.fullmatch(r'Rect2\(([^()]*)\)', node['text'])
require(match is not None, 'invalid typed Rect2')
values = [float(v.strip()) for v in match[1].split(',')]
require(len(values) == 4 and all(math.isfinite(v) for v in values), 'invalid rectangle')
return values
return codec.decode(node)
def require(value, message):
if not value:
raise ValueError(message)
def sha(path):
return hashlib.sha256(Path(path).read_bytes()).hexdigest()
def enum_names(root, relative, name):
source = (root / relative).read_text()
match = re.search(r'\benum\s+' + re.escape(name) + r'\s*\{([^}]+)\}', source)
require(match is not None, 'missing source enum ' + name)
names = [x.strip() for x in match[1].split(',') if x.strip()]
require(all(re.fullmatch(r'[A-Z][A-Z_0-9]*', x) for x in names), 'unsupported enum schema')
require(len(set(names)) == len(names), 'duplicate source enum')
return names
def projection(rows, mapping, states):
result = []
ids = set()
for row in rows:
value = {target: copy.deepcopy(row[source]) for target, source in mapping.items()}
require(value['id'] not in ids, 'duplicate actor/task ID')
ids.add(value['id'])
require(value['state'] in states, 'unknown typed state')
result.append(value)
return result
def compare_actor_rows(snapshot, report, root=ROOT):
runtime = snapshot['systems']['runtime']
guest_states = enum_names(root, 'scripts/guest/guest_runtime.gd', 'State')
staff_states = enum_names(root, 'scripts/staff/staff_runtime.gd', 'State')
task_states = enum_names(root, 'scripts/staff/staff_task.gd', 'State')
mappings = (
('guests', runtime['guests']['guests'], guest_states,
dict(id='guest_id', state='state', cell='current_cell', target='target_facility_id',
service='current_service_id', queue='queue_request_id')),
('staff', runtime['staff']['staff'], staff_states,
dict(id='staff_id', state='state', cell='current_cell', task='current_task_id',
facility='assigned_facility_id', role='role_id', carried_inventory_id='carried_inventory_id')),
('tasks', runtime['staff']['tasks'], task_states,
dict(id='task_id', state='state', type='task_type', staff='assigned_staff_id',
target='target_entity_id', rescue_facility='rescue_facility_id')),
)
for key, rows, states, mapping in mappings:
expected = projection(rows, mapping, states)
require(report[key] == expected, 'typed projection mismatch: ' + key)
return [key for key, *_ in mappings]
def elevator_report(snapshot):
saved = snapshot['systems']['runtime']['elevators']
rows = []
for shaft in saved['shafts']:
row = copy.deepcopy(shaft)
row.update(shaft['config'])
floor = float(shaft['current_floor'])
if shaft['phase'] in ('PICKUP', 'TRAVEL'):
destination = shaft['boarding_floor'] if shaft['phase'] == 'PICKUP' else shaft['target_floor']
progress = 1.0 - shaft['remaining_minutes'] / max(float(shaft['phase_total_minutes']), 1.0)
floor = shaft['phase_origin_floor'] + (destination - shaft['phase_origin_floor']) * progress
row.update(visual_floor=floor, passenger_count=len(shaft['boarded']),
passenger_ids=copy.deepcopy(shaft['boarded']),
waiting_count=sum(t['shaft_id'] == shaft['shaft_id'] and t['state'] == 'QUEUED'
for t in saved['trips']))
rows.append(row)
# WorldPreview sorts its report's trip copy for queue placement.
trips = sorted(copy.deepcopy(saved['trips']), key=lambda t: (t['enqueued_at'], t['actor_id']))
return dict(current_minute=saved['current_minute'], shafts=rows, trips=trips)
def close(actual, expected, label):
if isinstance(expected, (float, int)) and not isinstance(expected, bool):
require(type(actual) in (int, float) and math.isfinite(actual)
and math.isclose(actual, expected, abs_tol=0.0001, rel_tol=0.000001), label)
elif isinstance(expected, dict):
require(isinstance(actual, dict) and actual.keys() == expected.keys(), label + ' keys')
for key in expected:
close(actual[key], expected[key], label + '/' + str(key))
elif isinstance(expected, list):
require(isinstance(actual, list) and len(actual) == len(expected), label + ' length')
for a, e in zip(actual, expected):
close(a, e, label)
else:
require(type(actual) is type(expected) and actual == expected, label)
class Transform:
"""Independent port of the shared cell/map transform; no report rect input."""
def __init__(self, runtime, report):
self.bounds = runtime['world']['bounds']
b = self.bounds
columns, rows = b['max_x'] - b['min_x'] + 1, b['max_y'] - b['min_y'] + 1
width, height = report['viewport']
require(width > 0 and height > 0 and report['zoom'] > 0, 'camera dimensions')
fit = min(max(width - 152, 1) / (columns * 16), max(height - 140, 1) / (rows * 24))
pixel = max(math.floor(fit), 1) if fit >= 1 else max(fit, .25)
self.dim = [max(math.floor(v * pixel * report['zoom'] + .5), minimum)
for v, minimum in ((16, 4), (24, 6))]
self.origin = [math.floor((extent - count * dim) / 2 + pan)
for extent, count, dim, pan in zip((width, height), (columns, rows), self.dim, report['pan'])]
def rect(self, cell):
b = self.bounds
return [self.origin[0] + (cell[0] - b['min_x']) * self.dim[0],
self.origin[1] + (b['max_y'] - cell[1]) * self.dim[1], *self.dim]
def center(self, cell):
x, y, w, h = self.rect(cell)
return [x + w / 2, y + h / 2]
def room(self, cells):
rects = [self.rect(c) for c in cells]
x, y = min(r[0] for r in rects), min(r[1] for r in rects)
return [x, y, max(r[0] + r[2] for r in rects) - x, max(r[1] + r[3] for r in rects) - y]
def compare_traffic(snapshot, report):
runtime = snapshot['systems']['runtime']
transform = Transform(runtime, report)
rows = []
for saved in sorted(runtime['path']['cell_traffic'], key=lambda r: (r['cell'][1], r['cell'][0])):
cell = saved['cell']
if cell[2] != report['layer']:
continue
x, y, w, h = transform.rect(cell)
rows.append(dict(cell=cell, traffic={k: v for k, v in saved.items() if k != 'cell'},
rect=[x + 2, y + 2, w - 4, h - 4]))
close(report['traffic'], rows, 'traffic and cell rect')
def compare_queues(snapshot, report):
runtime = snapshot['systems']['runtime']
transform = Transform(runtime, report)
facilities = codec.indexed(runtime['facilities']['instances'], 'instance_id')
requests = codec.indexed(runtime['facilities']['requests'], 'request_id')
guests = codec.indexed(runtime['guests']['guests'], 'guest_id')
groups = {}
def add(key, cell):
if cell[2] != report['layer']:
return
members = groups.setdefault(tuple(cell), [])
if key not in members:
members.append(key)
for facility in facilities.values():
for rid in facility['queue_entries']:
request = requests.get(rid)
if request is None:
continue
for gid in request['guest_ids']:
guest = guests.get(gid)
if guest and guest['state'] == 'QUEUING':
add('guest/' + gid, guest['current_cell'])
elevators = elevator_report(snapshot)
for trip in elevators['trips']:
if trip['state'] == 'QUEUED':
add(trip['actor_id'].replace('guest:', 'guest/', 1).replace('staff:', 'staff/', 1), trip['from'])
lift_cells = {tuple(cell) for shaft in elevators['shafts'] for cell in shaft['stops']}
walkable = {tuple(cell) for cell in runtime['world']['walkable_cells']}
owners = {tuple(cell): row['entity_id'] for row in runtime['world']['occupancies'] for cell in row['cells']}
reserved, positions, hidden, badges = set(), {}, {}, []
for cell in sorted(groups, key=lambda c: (c[1], c[0])):
members = groups[cell]
lane = [] if cell in lift_cells else [cell]
for direction in (-1, 1):
for distance in range(1, 9):
target = (cell[0] + direction * distance, cell[1], cell[2])
facility = facilities.get(owners.get(target))
if target not in walkable or (facility and facility['definition_id'] not in ('guest_corridor', 'service_corridor')):
break
lane.append(target)
index = 0
for target in lane:
if index >= len(members):
break
if target in reserved:
continue
reserved.add(target)
positions[members[index]] = transform.center(target)
index += 1
hidden.update({member: True for member in members[index:]})
overflow = len(members) - index
rect = transform.rect(cell)
badges.append(dict(at=[rect[0] + (-80 if cell in lift_cells else 0), rect[1] - 16],
text=f'待ち{len(members)}人' + (f'(他{overflow}人)' if overflow else ''),
cell=list(cell), members=members, visible_count=index, overflow=overflow))
for key, expected in [('queue_positions', positions), ('queue_hidden', hidden), ('landing_badges', badges)]:
close(report[key], expected, key)
def compare_room_services(snapshot, report, root=ROOT):
"""Check authoritative service occupants and reasons; rest selection remains pending.
Extra rest/rescue occupants are checked separately below, never silently
counted as service users. This does not claim the missing path-cost proof.
"""
runtime = snapshot['systems']['runtime']
states = enum_names(root, 'scripts/facility/facility_instance.gd', 'State')
operations = enum_names(root, 'scripts/facility/facility_instance.gd', 'OperationState')
rooms = [r for r in runtime['facilities']['instances'] if r['origin'][2] == report['layer']]
treated, carried = {}, {}
for task in runtime['staff']['tasks']:
if task['task_type'] != 'rescue_transport' or task['state'] in ('COMPLETED', 'CANCELLED', 'FAILED') or not task['assigned_staff_id']:
continue
if task['state'] == 'WORKING':
treated[task['target_entity_id']] = task['rescue_facility_id']
else:
carried[task['target_entity_id']] = task['assigned_staff_id']
close(report['treated_patients'], treated, 'rescue treatment')
close(report['carried_patients'], carried, 'rescue carrier')
staff = codec.indexed(runtime['staff']['staff'], 'staff_id')
all_rooms = codec.indexed(runtime['facilities']['instances'], 'instance_id')
for sid, fid in report['resting_staff'].items():
require(sid in staff and fid in all_rooms, 'rest reference')
member, room = staff[sid], all_rooms[fid]
require(member['state'] == 'OFF_DUTY' and member['health'] > 0 and not member['current_task_id'], 'resting staff eligibility')
require(room['definition_id'] in ('staff_waiting_room', 'staff_dormitory')
and room['state'] in ('OPEN', 'DEGRADED') and not room['suspension_reason'], 'operating rest room')
require(set(report['rooms']) == {r['instance_id'] for r in rooms}, 'room identity')
for room in rooms:
observed = report['rooms'][room['instance_id']]
users = sorted({gid for service in room['active_services'] for gid in service['guest_ids']})
users += [sid for sid in staff if report['resting_staff'].get(sid) == room['instance_id']]
users += [task['target_entity_id'] for task in runtime['staff']['tasks']
if task['task_type'] == 'rescue_transport' and task['state'] == 'WORKING'
and task['assigned_staff_id'] and task['rescue_facility_id'] == room['instance_id']]
require(observed['people'] == users, 'service/rest/rescue occupant list')
require(observed['count'] == len(observed['people']), 'room count')
operation = 'SUSPENDED' if room['suspension_reason'] else ('OPERATING' if room['state'] in ('OPEN', 'DEGRADED') else 'INACTIVE')
expected = dict(state=states.index(room['state']), state_name=room['state'],
reasons=room['state_reasons'], operation=operations.index(operation),
limit=room['customer_capacity'], staff=room['active_staff_ids'], suspension=room['suspension_reason'])
for key, value in expected.items():
close(observed[key], value, 'room/' + key)
cells = next((r['cells'] for r in runtime['world']['occupancies'] if r['entity_id'] == room['instance_id']), [])
if cells:
close(observed['rect'], Transform(runtime, report).room(cells), 'room rect')
require(observed['definition_id'] == room['definition_id'], 'room definition')
def comparison_records(directory):
records = {}
for line in (directory / 'manifest.jsonl').read_text().splitlines():
row = json.loads(line)
if 'object' not in row:
continue
value = read_object(directory, row['object'])
if row['label'] == 'event':
continue
require(row['label'] not in records, 'duplicate experiment record')
records[row['label']] = value
return records
def compare_goods(snapshot, report, root=ROOT):
runtime = snapshot['systems']['runtime']
states = enum_names(root, 'scripts/inventory/logistics_request.gd', 'State')
candidates = codec.indexed(json.loads((root / 'data/characters/mvp_staff_candidates.json').read_text())['entries'], 'id')
staff = codec.indexed(runtime['staff']['staff'], 'staff_id')
facilities = runtime['facilities']['instances']
def owner(inventory):
return next((f['instance_id'] for f in facilities if f['local_inventory_id'] == inventory), '')
rows, active, recent = [], [], []
for request in sorted(runtime['logistics_requests'], key=lambda r: r['request_id']):
require(request['state'] in states, 'logistics enum')
task = next((t for t in runtime['staff']['tasks']
if t['target_entity_id'] == request['request_id']
and t['task_type'] in ('supply_pickup', 'supply_delivery', 'supply_return')
and t['state'] not in ('COMPLETED', 'CANCELLED', 'FAILED')), None)
carrier = request['assigned_carrier_id']
row = dict(source_facility_id=owner(request['source_inventory_id']),
destination_facility_id=owner(request['destination_inventory_id']),
carrier_name=candidates.get(carrier, {}).get('display_name', carrier) if carrier in staff else '',
reason_codes=[request['state_reason']] if request['state_reason'] else [],
request_id=request['request_id'], state=request['state'], reason=request['state_reason'],
carrier_id=carrier, source_inventory_id=request['source_inventory_id'],
destination_inventory_id=request['destination_inventory_id'],
item_requirements=request['item_requirements'], weight=request['total_weight'],
retry_count=request['retry_count'], task_id=task['task_id'] if task else '')
rows.append(row)
if request['state'] in ('COMPLETED', 'CANCELLED'):
recent.append((request['state_changed_at'], request['request_id'], row))
else:
active.append(row)
recent.sort(key=lambda r: (r[0], r[1]), reverse=True)
close(report['goods'], dict(requests=rows, deliveries=active + [r[2] for r in recent[:10]]), 'goods')
def compare_cabin_geometry(snapshot, report):
runtime = snapshot['systems']['runtime']
transform = Transform(runtime, report)
expected = []
p = transform.dim[0] / 16
for shaft in elevator_report(snapshot)['shafts']:
if not shaft['stops'] or shaft['stops'][0][2] != report['layer']:
continue
floor = shaft['visual_floor']
track = report['elevator_tracks'][shaft['shaft_id']]
close(track['floor'], floor, 'authoritative elevator track endpoint')
require(track['at'] <= report['animation_time'], 'elevator track time')
if not report['paused']:
# The prior-frame origin still needs a chained visual-history proof.
t = max(0, min(1, (report['animation_time'] - track['at']) / .42))
floor = track['from'] + (floor - track['from']) * t
x, y, w, h = transform.rect(shaft['stops'][0])
baseline = y + h - (floor - shaft['stops'][0][1]) * transform.dim[1] - 3 * p
def rounded(v):
return math.copysign(math.floor(abs(v) + .5), v)
rect = [rounded(x + 4*p), rounded(baseline - 19*p), max(shaft.get('width', 2), 1)*w - 8*p, 19*p]
if rect[0] >= report['viewport'][0] or rect[1] >= report['viewport'][1] or rect[0]+rect[2] <= 0 or rect[1]+rect[3] <= 0:
continue
count, capacity = len(shaft['boarded']), shaft['capacity']
badge = f'{count}/{capacity}人'
if capacity > 0 and count >= capacity:
badge = '満員 ' + badge
if not shaft['operational']:
badge = '停止 ' + badge
expected.append(dict(shaft_id=shaft['shaft_id'], rect=rect, pixel=p, operational=shaft['operational'],
doors_open=shaft['operational'] and shaft['phase'] in ('IDLE', 'BOARDING', 'ALIGHTING'),
badge_at=[rect[0], rect[1]-14], badge=badge))
require(len(report['cabins']) == len(expected), 'cabin count')
for row, wanted in zip(report['cabins'], expected):
for key, value in wanted.items():
close(row[key], value, 'cabin/' + key)
def compare_layouts(a, b):
require(a['declaration'] == b['declaration'], 'identical predeclared inputs')
declaration = a['declaration']
require(declaration['metric'] == 'sum_destination_arrival_minutes_entire_cohort', 'predeclared metric')
require(a['initial'] == b['initial'], 'identical seed budget clock definitions and initial state')
arrivals = {r['id']: r['offset'] for r in declaration['arrivals']}
require(len(arrivals) == len(declaration['arrivals']) and arrivals, 'whole nonempty cohort')
builds = []
metrics = []
for records in (a, b):
commands = [records[k] for k in sorted(records) if k.startswith('build-')]
require(all(r['result']['code'] == 'OK' for r in commands), 'paid construction succeeded')
builds.append([r['payload'] for r in commands])
require({k.removeprefix('arrival-') for k in records if k.startswith('arrival-')} == set(arrivals), 'arrival cohort identity')
for gid, offset in arrivals.items():
row = records['arrival-' + gid]
require(row['offset'] == offset and row['result']['code'] == 'OK', 'actual accepted arrival')
require(row['payload'] == a['arrival-' + gid]['payload'], 'same actual arrival command')
completed = {}
first_minute = records['tick-000']['minute']
for offset in range(declaration['window'] + 1):
tick = records['tick-%03d' % offset]
require(tick['minute'] == first_minute + offset, 'contiguous actual clock')
for guest in tick['guests']['guests']:
gid = guest['guest_id']
require(gid in arrivals and offset >= arrivals[gid], 'actual cohort membership')
if guest['current_cell'] == declaration['to']:
completed.setdefault(gid, offset - arrivals[gid])
require(set(completed) == set(arrivals), 'no unfinished or omitted journeys')
metrics.append(sum(completed.values()))
require(builds[1] == builds[0] + [declaration['improvement']], 'only predeclared layout improvement')
require(b['tick-000']['minute'] == a['tick-000']['minute'], 'same observation start')
require(metrics[1] < metrics[0], 'strict whole-cohort improvement')
return dict(metric=declaration['metric'], A=metrics[0], B=metrics[1])
def read_object(directory, digest):
require(isinstance(digest, str) and re.fullmatch('[0-9a-f]{64}', digest), 'invalid object digest')
path = directory / 'objects' / (digest + '.json')
require(not path.is_symlink() and sha(path) == digest, 'whole typed object hash')
typed = json.loads(path.read_bytes())
require(typed.get('type') == 'Dictionary', 'whole dictionary required')
return decode(typed)
def inspect_stream(directory, root=ROOT):
labels = set()
checked = []
with (directory / 'manifest.jsonl').open() as stream:
for line in stream:
row = json.loads(line)
if 'object' in row:
read_object(directory, row['object'])
continue
require(row['label'] not in labels, 'duplicate streamed label')
labels.add(row['label'])
require(type(row['pid']) is int and row['pid'] > 0, 'actual diagnostic PID required')
state = read_object(directory, row['state'])
report = read_object(directory, row['report'])
fields = compare_actor_rows(state, report, root)
require(report['elevators'] == elevator_report(state), 'typed elevator report mismatch')
compare_room_services(state, report, root)
compare_queues(state, report)
compare_traffic(state, report)
compare_goods(state, report, root)
compare_cabin_geometry(state, report)
checked.append(dict(label=row['label'], fields=fields + ['elevators', 'room_service_fields',
'queues', 'traffic', 'goods', 'cabin_geometry']))
require(checked, 'empty diagnostic stream')
return checked
def condition_matrix():
return [dict(condition=name, status='blocked', reason=BLOCKERS[name]) for name in CONDITIONS]
def main(argv=None):
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--diagnostics', type=Path)
parser.add_argument('--layout-a', type=Path)
parser.add_argument('--layout-b', type=Path)
args = parser.parse_args(argv)
# Registration hashes the immutable JSON after trimming outer whitespace.
require(hashlib.sha256((ROOT / SPEC).read_text().strip().encode()).hexdigest() == SPEC_SHA,
'immutable spec mismatch')
checked = inspect_stream(args.diagnostics.resolve()) if args.diagnostics else []
require(bool(args.layout_a) == bool(args.layout_b), 'both layout streams required')
comparison = compare_layouts(comparison_records(args.layout_a), comparison_records(args.layout_b)) if args.layout_a else None
print(json.dumps(dict(task_id='P23-03', status='blocked', semantic_diagnostics=checked,
layout_comparison=comparison, condition_matrix=condition_matrix(), root_evidence='pending'), ensure_ascii=False))
return 2
if __name__ == '__main__':
raise SystemExit(main())
+25
View File
@@ -0,0 +1,25 @@
#!/usr/bin/env python3
"""Read-only P23 readiness report. Collection is blocked in this candidate.
No runner, engine, profile, subprocess, browser or OS input is started. Root must
not mistake this preparation interface for a submitted native/OS collector.
"""
import json
import sys
sys.dont_write_bytecode = True
from check_p23_life_observation_evidence import condition_matrix
def main():
print(json.dumps({'task_id': 'P23-03', 'status': 'blocked', 'engine_runs': 0,
'condition_matrix': condition_matrix(),
'required_infrastructure': ['run_all_tests.py', 'strict_test_markers.py',
'regression/owned_outer.py',
'regression/ordinary_proof_gate.py'],
'root_evidence': 'missing'}, ensure_ascii=False))
return 2
if __name__ == '__main__':
raise SystemExit(main())
+190
View File
@@ -0,0 +1,190 @@
#!/usr/bin/env python3
"""Local semantic-checker tests; synthetic examples are never game evidence."""
import copy
import unittest
import sys
sys.dont_write_bytecode = True
import check_p23_life_observation_evidence as check
class SemanticTests(unittest.TestCase):
def setUp(self):
self.guest = dict(guest_id='g', state='QUEUING', current_cell=[1, 0, 0],
target_facility_id='f', current_service_id='', queue_request_id='q')
self.staff = dict(staff_id='s', state='WORKING', current_cell=[2, 0, 1],
current_task_id='t', assigned_facility_id='f', role_id='porter',
carried_inventory_id='cargo')
self.task = dict(task_id='t', state='WORKING', task_type='delivery',
assigned_staff_id='s', target_entity_id='request', rescue_facility_id='')
self.snapshot = {'systems': {'runtime': {'guests': {'guests': [self.guest]},
'staff': {'staff': [self.staff], 'tasks': [self.task]}}}}
self.report = dict(guests=[dict(id='g', state='QUEUING', cell=[1, 0, 0], target='f', service='', queue='q')],
staff=[dict(id='s', state='WORKING', cell=[2, 0, 1], task='t', facility='f',
role='porter', carried_inventory_id='cargo')],
tasks=[dict(id='t', state='WORKING', type='delivery', staff='s',
target='request', rescue_facility='')])
def test_real_schema_projection(self):
self.assertEqual(check.compare_actor_rows(self.snapshot, self.report), ['guests', 'staff', 'tasks'])
def test_changed_state_position_target_task_and_goods_are_rejected(self):
for category, field, replacement in [('guests', 'cell', [8, 0, 0]), ('guests', 'target', 'other'),
('guests', 'state', 'MOVING'), ('staff', 'task', ''),
('staff', 'carried_inventory_id', ''), ('tasks', 'staff', 'other')]:
with self.subTest(category=category, field=field):
report = copy.deepcopy(self.report)
report[category][0][field] = replacement
with self.assertRaises(ValueError):
check.compare_actor_rows(self.snapshot, report)
def test_missing_extra_and_boolean_only_rows_are_rejected(self):
for bad in [[], self.report['guests'] * 2, [True]]:
report = copy.deepcopy(self.report)
report['guests'] = bad
with self.assertRaises(ValueError):
check.compare_actor_rows(self.snapshot, report)
def test_unknown_enum_and_duplicate_id_are_rejected(self):
self.guest['state'] = 'FICTIONAL'
with self.assertRaises(ValueError):
check.compare_actor_rows(self.snapshot, self.report)
self.guest['state'] = 'QUEUING'
self.snapshot['systems']['runtime']['guests']['guests'].append(copy.deepcopy(self.guest))
with self.assertRaises(ValueError):
check.compare_actor_rows(self.snapshot, self.report)
def test_typed_rectangle_and_duplicate_vector_key(self):
self.assertEqual(check.decode(dict(type='Rect2', text='Rect2(1, 2, 16, 24)')), [1, 2, 16, 24])
with self.assertRaises(ValueError):
check.decode(dict(type='Rect2', text='Rect2(1, nan, 16, 24)'))
key = dict(type='Vector3i', text='Vector3i(1, 2, 0)')
value = dict(type='int', text='1')
with self.assertRaises(ValueError):
check.decode(dict(type='Dictionary', entries=[[key, value], [key, value]]))
def test_immutable_typed_input_is_not_modified(self):
before = copy.deepcopy(self.snapshot)
check.compare_actor_rows(self.snapshot, self.report)
self.assertEqual(before, self.snapshot)
class SpatialTests(unittest.TestCase):
def setUp(self):
self.runtime = dict(world=dict(bounds=dict(min_x=0, max_x=1, min_y=0, max_y=0),
walkable_cells=[[0, 0, 0], [1, 0, 0]], occupancies=[]),
path=dict(cell_traffic=[dict(cell=[0, 0, 0], base_capacity=2,
current_density=3, dynamic_cost=0, temporary_block=False)]),
facilities=dict(instances=[], requests=[]), guests=dict(guests=[]),
staff=dict(staff=[], tasks=[]), logistics_requests=[],
elevators=dict(current_minute=0, shafts=[], trips=[]))
self.snapshot = dict(systems=dict(runtime=self.runtime))
self.report = dict(viewport=[184, 164], zoom=1, pan=[0, 0], layer=0,
traffic=[dict(cell=[0, 0, 0], rect=[78, 72, 12, 20],
traffic=dict(base_capacity=2, current_density=3,
dynamic_cost=0, temporary_block=False))],
queue_positions={}, queue_hidden={}, landing_badges=[], rooms={},
treated_patients={}, carried_patients={}, resting_staff={},
goods=dict(requests=[], deliveries=[]))
def test_density_and_pan_are_independently_recomputed(self):
check.compare_traffic(self.snapshot, self.report)
for key, value in [('current_density', 0), ('base_capacity', 9)]:
changed = copy.deepcopy(self.report)
changed['traffic'][0]['traffic'][key] = value
with self.assertRaises(ValueError):
check.compare_traffic(self.snapshot, changed)
self.report['pan'][0] = 20
with self.assertRaises(ValueError):
check.compare_traffic(self.snapshot, self.report)
def test_missing_queue_overflow_rejected(self):
self.runtime['elevators']['trips'] = [dict(actor_id='guest:g'+str(i), enqueued_at=i,
state='QUEUED', **{'from': [0, 0, 0]}) for i in range(3)]
self.report.update(queue_positions={'guest/g0': [84, 82], 'guest/g1': [100, 82]},
queue_hidden={'guest/g2': True}, landing_badges=[dict(at=[76, 54],
text='待ち3人(他1人)', cell=[0, 0, 0],
members=['guest/g0', 'guest/g1', 'guest/g2'], visible_count=2, overflow=1)])
check.compare_queues(self.snapshot, self.report)
self.report['landing_badges'][0]['overflow'] = 0
with self.assertRaises(ValueError):
check.compare_queues(self.snapshot, self.report)
def test_fictional_goods_and_rooms_rejected(self):
check.compare_goods(self.snapshot, self.report)
check.compare_room_services(self.snapshot, self.report)
self.report['goods']['requests'].append(dict(stock=100))
with self.assertRaises(ValueError):
check.compare_goods(self.snapshot, self.report)
self.report['rooms']['invented'] = dict(count=0)
with self.assertRaises(ValueError):
check.compare_room_services(self.snapshot, self.report)
def test_room_reason_and_rescue_occupancy(self):
room = dict(instance_id='clinic', origin=[0, 0, 0], definition_id='infirmary',
state='NO_POWER', state_reasons=['NO_POWER'], suspension_reason='',
customer_capacity=2, active_staff_ids=[], active_services=[], occupied_cells=[])
self.runtime['facilities']['instances'] = [room]
self.runtime['staff']['tasks'] = [dict(task_id='rescue', task_type='rescue_transport',
state='WORKING', assigned_staff_id='carrier', target_entity_id='patient', rescue_facility_id='clinic')]
self.report['treated_patients'] = {'patient': 'clinic'}
self.report['rooms'] = {'clinic': dict(state=2, state_name='NO_POWER', reasons=['NO_POWER'],
operation=0, people=['patient'], count=1, limit=2, staff=[], suspension='')}
check.compare_room_services(self.snapshot, self.report)
for field, wrong in [('state', 6), ('operation', 1), ('people', []), ('count', 0)]:
changed = copy.deepcopy(self.report)
changed['rooms']['clinic'][field] = wrong
with self.assertRaises(ValueError):
check.compare_room_services(self.snapshot, changed)
def test_cabin_geometry_and_phase(self):
shaft = dict(shaft_id='lift', config=dict(stops=[[0, 0, 0]], width=2, capacity=4),
current_floor=0, phase='IDLE', operational=True, boarded=[])
self.runtime['elevators']['shafts'] = [shaft]
self.report.update(paused=True, animation_time=0, elevator_tracks={'lift': dict(floor=0, at=0)},
cabins=[dict(shaft_id='lift', rect=[80, 72, 24, 19], pixel=1, operational=True,
doors_open=True, badge_at=[80, 58], badge='0/4人', passengers=[])])
check.compare_cabin_geometry(self.snapshot, self.report)
for field, value in [('rect', [80, 70, 24, 19]), ('doors_open', False), ('badge', '4/4人')]:
changed = copy.deepcopy(self.report)
changed['cabins'][0][field] = value
with self.assertRaises(ValueError):
check.compare_cabin_geometry(self.snapshot, changed)
class ComparisonTests(unittest.TestCase):
def pair(self):
declaration = dict(metric='sum_destination_arrival_minutes_entire_cohort',
arrivals=[dict(id='g', offset=0)], window=2, to=[2, 1, 0],
improvement=dict(definition_id='guest_elevator', origin=[4, 0, 0]))
a = dict(declaration=declaration, initial=dict(seed=1))
a['arrival-g'] = dict(offset=0, payload=dict(guest_id='g'), result=dict(code='OK'))
for minute in range(3):
a['tick-%03d' % minute] = dict(minute=minute, guests=dict(guests=[dict(guest_id='g',
current_cell=[2, 1, 0] if minute == 2 else [2, 0, 0])]))
b = copy.deepcopy(a)
b['build-0'] = dict(payload=declaration['improvement'], result=dict(code='OK'))
b['tick-001']['guests']['guests'][0]['current_cell'] = [2, 1, 0]
return a, b
def test_strict_metric_from_typed_cells(self):
a, b = self.pair()
self.assertEqual(check.compare_layouts(a, b)['B'], 1)
for mutation in ('demand', 'incomplete', 'no_improvement', 'extra_build'):
with self.subTest(mutation=mutation):
a, b = self.pair()
if mutation == 'demand':
b['arrival-g']['payload']['guest_id'] = 'other'
elif mutation == 'incomplete':
for minute in (1, 2):
b['tick-%03d' % minute]['guests']['guests'][0]['current_cell'] = [2, 0, 0]
elif mutation == 'no_improvement':
b['tick-001'] = copy.deepcopy(a['tick-001'])
else:
b['build-1'] = copy.deepcopy(b['build-0'])
with self.assertRaises(ValueError):
check.compare_layouts(a, b)
if __name__ == '__main__':
unittest.main()