P23-03: 生活の観察

This commit is contained in:
Agent Control Runner
2026-10-15 18:01:00 +09:00
parent 49445d4a4d
commit 19fc3f50c7
8 changed files with 1888 additions and 71 deletions
@@ -0,0 +1,443 @@
# P23-03 repair inspection
## Repair 26 — submitted for Root validation; no acceptance credit
This section supersedes the status below. Preserved the five premodified files,
both R13 fixtures, all 86 existing tests, and every historical section. Historical
49 and the twelve false R13 judgments are unchanged. Seven conditions, six
categories and 30/88 are unchanged. No Runtime, gameplay, save, economy, mana,
asset or UI-entry source was changed.
Implemented the remaining local work:
- Both independent packers now rebuild vertical components from fitted feet and
pose cores after every scale reduction. Scale ceilings decrease monotonically,
so termination has a finite measure and no iteration-limit exemption. Stable
components use the existing ordered forward/backward fitting and p=1 retained
member aggregation. A positive separates a previously intersecting component
at p=2; retaining the stale component would incorrectly reduce it to p=1.
- Whole-state fixtures cover queue lanes plus work staff, queue precedence and
order, the actual R13 cafeteria/access-cell geometry, carried and treated
patients, occupied rest facilities, queued staff, passenger/floor coexistence
on FRONT/BACK, and independently calculated no-fit aggregates. Negatives
include stale groups, pose/feet, carrier-relative positions, category/region/
transit spoofing, missing identities and displaced aggregation badges.
- Cabin candidates no longer silently truncate after two seat rows. Additional
identities reuse the existing two-row anchor pattern and participate in the
same independent fit/no-fit decision. The synthetic saturation configuration
retains every boarded identity in member lists; it changes no definition.
- OS and offline callers use the shared hash-bound drawing_context loader.
Tests cover valid context, absent fields/objects, malformed raw/rest context,
report binding and object hash mismatches. Frame tests include valid transit
and occupied-rest authority, missing/malformed rest authority, and conservative
rejection of overlap without raw context. No flags grant exemptions.
- GDScript component fixture now includes four occupants, scale-driven regroup,
and actual _fit_life_actors aggregation with nine drawn and ten retained IDs,
badge text and position. This fixture has NOT been executed by Godot.
Source/consumer audit:
- _build_life_observation calls fitting before publishing; _draw_life_overlay
consumes fitted floor/passenger rows and the aggregation badge at/text.
_draw_observed_actor consumes at/scale/mode/frame/flip. Logical cell, preferred
center and state remain separate. Existing click handling selects world
facilities/construction/stairs; there is no actor hit-test to relocate.
- passive_observation.draw_rectangles and Python drawing_rectangle_rows consume
fitted actor extents and actual aggregation badge coordinates. frame_rows
carries all member identities and drawn_keys into review metadata. Whole typed
diagnostic captures preserve the same report; compare_actor_geometry rebuilds
classification, bounds, fit and aggregation independently. compare_rest invokes
complete geometry for occupied resting facilities. compare_motion binds the
actual published report/history, not unfitted preferred positions.
- inspect_offline_mirror and inspect_os_requests both pass drawing_context's
hash-bound state/rest objects to frame_rows. inspect_stream separately runs
rest, cabin, motion and complete geometry checks. Missing objects raise and
cannot become a successful exemption. Unit tests exercise this shared loader;
no real OS/offline evidence was rerun or relabeled.
- strict_test_markers.load_contracts rejects duplicate suite rows. The existing
single P23 row already declares the exact two source print sites' template
`P23 life observation: <checks> checks, <failures> failures`. parse_raw obtains
counts only from actual logs; source_sha256_at_schema_audit is historical
metadata, not a current-source acceptance bypass. No marker-contract append
is required, no suite-policy row was added, and no marker was fabricated.
Root's source map must bind the final modified fixture and all changed files.
Local verification: 87, 91, 93, 95, 97, 98 and 99 tests passed as work progressed;
the final rerun passed **99 tests in 0.213s**, exit 0. The original expected R13 row11/29 rejection
is retained. Python compile checks for checker, tests and runner passed without
bytecode output. git diff --check passed. No tests were deleted or weakened.
Read-only discovery encountered inaccessible sibling worktrees and nonexistent
BUILD_OUTPUT_DIR shorthand; the actual allocated build directory supplied both
R13 direction files. A shell glob lookup had no match; no source was lost.
Root native/OS evidence, the 177-test full regression and independent Opus
all-condition review remain missing, as expected for worker submission.
Submission is not acceptance and earns no unit credit. No Godot, native/OS,
full regression, Runner, provider, git write or agent was run.
## Repair 25 continuation — current status: blocked, implementation incomplete
This section supersedes the status of the historical sections below. There is
no specification, permission, or floor-interpretation gap. The remaining blocker
is unfinished implementation and verification, not missing Root evidence alone.
All 809 pre-existing added lines were preserved and extended. No candidate is
claimed implemented or ready for acceptance. Seven conditions, six categories,
and 30/88 remain exact; historical 49 is preserved without relabeling.
Read before editing: AGENTS.md, Phase 0 API_AND_STATE_MAP, recorded concrete
direction, both authoritative R13 geometry/region directions, renderer,
checker/tests, diagnostic/passive collectors, Runtime initialize/spawn/save/load
entry points, enums, marker parser and existing contract. No new assets or
Runtime/gameplay/save/UI-entry changes.
Changes in this continuation:
- Drawing-row validation accepts the existing raw-state/rest-audit context and
invokes complete geometry classification and fitting. OS frames pass the
hash-bound stream objects; offline image review loads the matching writer
capture and checks its report binding before passing the same objects.
- Track validation checks the raw actor endpoint, source-valid adjacent origin,
finite nonfuture times and boolean pause. A drawing's region/in_transit flags
cannot grant an exemption. Without raw context the old conservative check
remains, and frame output explicitly says `absent: fail closed, no transit
exemptions`. Historical images have not been rerun or relabeled.
- Rest selection still independently validates routes, costs, locks and
eligibility. For actual occupied rest facilities its geometry now runs through
complete cross-kind fitting, including queue precedence and independently
reconstructed aggregation identities/no-fit, instead of requiring every
rest-slot identity to have an unshifted drawing row.
- Added the actual +m badge to Python and GDScript image-audit rectangle lists;
frame metadata retains all aggregate members and drawn keys. This additional
diagnostic row type does not change the six acceptance categories.
- Added full state-to-geometry positives for actual track interpolation,
staff/guest mixed packing on FRONT/BACK, fractional low-world-pixel scale and
saturated aggregation with all identities retained. Added track/time/pause,
forged-region/out-of-cell, aggregate identity loss and badge omission checks.
R13 row11/29 remains an expected FAIL. Existing tests were not removed.
Precise outstanding TODOs before an implemented submission:
1. Complete full state-to-geometry positives for queue lane + work staff,
passenger + floor, carried/resting/treated staff and cabin aggregation.
Exercise compare_rest with an actual occupied facility, legitimate no-fit
aggregate and queued staff; its new integration lacks those full fixtures.
2. Complete full semantic negatives for queue precedence/order and forcing the
receiving R13 guest back into its access cell. Existing primitive receiving
fixture tests and overlay negatives are retained but are not substitutes for
every required state-to-geometry case.
3. Audit and implement regrouping after scale changes in BOTH packers. They
currently form vertical groups before fitting; fitted feet/cores can change
vertical intersections. Do not claim the required regrouping positive passes.
Confirm carrier-relative pose/feet, cabin member coverage, and badge placement
through the required semantic cases before declaring the renderer finished.
4. Add full frame/offline/OS-context unit cases (including rest authority and
malformed/missing stream context). Standalone/full transit paths have tests;
production caller wiring was read but no OS/offline evidence run occurred.
5. Finish the GDScript UI fixture and source-authority integration audit. Existing
four-occupant UI preparation is retained and unexecuted. Marker contracts and
suite policies are unchanged; do not append a duplicate suite row or fabricate
an acceptance marker. If an authority append is necessary, only the single
strict contract-permitted entry is authorized.
Local checks actually executed:
- Checker unit command `PYTHONDONTWRITEBYTECODE=1 python3
tools/test_p23_life_observation_evidence.py`: 78 tests OK (0.104s), 80 OK
(0.112s), 83 OK (0.134s), 85 OK (0.112s), then **86 OK (0.124s)**.
No checker unit failure occurred in this continuation.
- No-bytecode `compile(Path(p).read_bytes(), p, "exec")` syntax check for
checker, checker tests and Root runner: all three OK, exit 0.
- Read-only `git diff --check`: exit 0, no output.
- One shell heredoc edit attempt failed before execution (`can't create temp
file for here document: operation not permitted`); applied the intended edit
with apply_patch afterward. One read-only search named a nonexistent
`tools/regression/suite_policy.py`; read the actual JSON policy and runner
directly. These were tool/read failures, not test passes.
No Godot/native/OS, full regression, Runner, provider, git write, integration or
other agent was run. Root's **new native/OS evidence**, the **177-test full
regression**, and the **independent Opus all-condition review** are still missing.
Those expected later proofs alone are not the reason for this blocked status.
## Repair 24 continuation — current status: blocked, implementation incomplete
The floor/anchored-region interpretation is RESOLVED. No clarification or
permission is requested. This status is an incomplete implementation, not an
external specification blocker and not a submitted candidate. Preserve this
worktree and every earlier uncommitted change. Historical sections below are
retained as history, not current interpretations.
Read the current renderer, RoomPresentation, the recorded concrete direction,
both R13 direction files, Phase 0 API map, Runtime initialization/spawn/formal
save/load sources, and cleaning pose dependencies. No domain inputs or assets
were changed. Seven conditions, six categories and 30/88 remain exact.
Implemented preparation in this continuation:
- Added per-actor cell/track, lane, facility, work-column and carried-patient
bounds, inset by world pixel; added original logical cell to drawing rows.
Staff queue placement now precedes the special staff anchors.
- Added a cross-kind vertical grouping and forward/backward interval packer,
integer scale trials and deterministic retained-member aggregation with a
readable +m badge. Cabin rows participate. Preferred centers and pose fields
remain separate from fitted drawing coordinates. This GDScript has NOT been
executed and is not established as native-correct.
- Added independent Python interval fitting and replaced original-scale
assertions with reconstructed centers, bounds, poses, fitted rows and
aggregation comparison. Queue and room reports are cross-checked before
geometry classification; boarded keys are rebuilt from EV reports.
- Added tests for the 56px four-mover case (all p=2), mixed actor kinds,
saturation with all identities retained, individual intervals, and R13's
receiving cafeteria guest outside its unchanged access cell. Added a full
synthetic state-to-geometry four-mover case and mutations for cell, state,
scale, position, missing/duplicate actors and unnecessary aggregation.
- Added the four-occupant GDScript component fixture. It is NOT native evidence.
Known remaining implementation work (must be resolved before submission):
1. The full geometry entry point passes independently derived transit keys to
pairwise checking, but standalone drawing-row/offline checks still use the
conservative default with no transit exemptions. Reconcile those paths
using authoritative inputs; never trust an in_transit/region flag.
2. Existing compare_rest still expects each visible slot in actor_drawings and
its original rest center. Reconcile legitimate aggregation and the new
staff queue precedence without bypassing the independent no-fit check.
3. Finish full classification tests for queue/work mixed groups, queue
precedence/order, spoofed category, forced access-cell receiving position,
carried/resting/treated staff, passengers and FRONT/BACK. Existing primitive
packing positives are not a substitute for these full semantic cases.
4. Audit pose/feet and regrouping after scale changes, low-world-pixel views,
carried patients and cabin aggregation; validate the actual GDScript and
report consumers through Root only after worker readiness. No new native
correctness or complete collision-removal claim is made here.
Checks actually executed in this continuation, all local:
- `PYTHONDONTWRITEBYTECODE=1 python3 tools/test_p23_life_observation_evidence.py`:
71 tests OK (0.093s), 76 tests OK (0.102s), then **78 tests OK (0.111s)**.
Existing R13 row11/29 remains an expected rejection. No unit test failure
occurred during this continuation; earlier failures remain recorded below.
- No-bytecode `compile(Path(p).read_bytes(), p, "exec")` for checker,
checker tests and Root runner: all three syntax checks OK, exit 0.
- One dependency lookup used the wrong cleaning-bridge directory and returned
an rg missing-file error; located and read the actual source under
scripts/environment. This was a read failure, not a passing test.
No Godot/native/OS, full regression, Runner, provider, git write or agent was
run. Root's **new native/OS evidence**, the **177-test full regression**, and
the **independent Opus all-condition review** are still missing. Historical
49 remains preserved and is not relabeled. No partial acceptance is claimed.
The inspection below is retained history. The repair 24 section above records
the current status; the former full-box impossibility argument is superseded.
Scope: the seven immutable acceptance conditions and existing allowed paths.
Validation: local Python syntax and checker unit tests only; no Godot execution.
Read the immutable acceptance spec, recorded concrete implementation direction,
existing Phase 0 API map, renderer, actor art, and independent checker.
The R13 provider stdout, command, and prompt SHA256 values match 01-actual.json.
The actual structured review contains 8 frames and 132 rows. Its summary's
168-row claim is not used to replace the structured rows. False judgments remain
false. The reader-request-0003 whole screenshot confirms overlapping actors and
obscured traffic presentation. Historical runtime evidence is unchanged.
Root native/OS evidence for a new candidate, the 177-test full regression, and
independent Opus all-condition review remain required and are not worker tests.
## Status: blocked; partial independent checker preparation only
The renderer repair and traffic geometry/layer audit are **not implemented**.
No claim of submission readiness or acceptance is made. No renderer, Runtime,
policy, immutable spec, source authority pin, or historical evidence was changed.
### Placement specification gap and concrete failing evidence
In the original `os-pair/A/reader-request-0003/request.json`, SHA256
`7dc3ee0bcc428e9d103726ee2448c767f1dd4107f9490833dc860c0b5140068c`,
the decoded report contains guests `os-p23-012` through `os-p23-015`, all
`MOVING`, logical cell `[1,1,0]`, with no service or queue request. Their
tracks' `from` and `cell` are both `[1,1,0]`. The current cell is 64x96 pixels
(`traffic` row 40's inset rect is `[709,390,60,92]`). Each actor's current
draw/report footprint is 48x54 pixels: the 16x18 PixelPeople grid at scale 3.
The four rectangles require area 10368, while the cell has area 6144.
Consequently offsets alone, preserving those sizes and containing all four
rectangles in that cell, cannot satisfy the requested nonoverlap invariant.
This is already present in the supplied evidence, not a hypothetical load.
Source references: `world_preview.gd::_cell_dimensions`, `_actor_drawing`,
`_tracked_actor_position`; checker `drawing_rectangle_rows` and
`compare_actor_geometry`; `pixel_people.gd`'s 16x18 drawing grid.
The direction specifies offsets inside room/cell art, containment consistent
with logical cell/state, retained motion, and no visual redesign. It does not
resolve what presentation change is permitted when existing footprints cannot
fit. Shrinking actors or expanding the permitted projection area could solve
this capacity issue, but neither is silently treated here as authorized by an
offset repair. This is a question about the admissible presentation, not a
claim that every possible rendering solution is impossible.
Minimal proposal for the owner: specify whether deterministic scale reduction
within the original logical cell is permitted (including the required readable
minimum and behavior when it still cannot fit), or define an allowed room/lane
projection for these non-queued moving actors. No Runtime capacity change,
actor omission, or acceptance waiver is proposed. Retain all seven conditions.
### Independent work preserved
Added `validate_actor_nonoverlap` to both the semantic geometry path and offline
drawing-row gate. It recomputes full footprints from raw `at`/`scale`, includes
cabin passengers, rejects duplicate identities and invalid numeric geometry,
and does not use eligibility labels or asserted booleans. Edge contact is
allowed; positive-area overlap is rejected with actor identities.
Five added unit tests cover edge contact, overlap despite forged verdicts and
off-screen labels, cabin/floor overlap, invalid footprints, and the actual R13
false row 11 against staff row 29. The small historical-negative fixture copies
only the needed raw drawing inputs and preserves `[11,false,false,false]` with
request/manifest hashes. Its inputs were checked against the original typed
report. The original review files and all their rows remain untouched.
The original manifest hash matches
`53aa8e2f12e5d0a089529457fcd55ee1ba0600e8dffff2225bb8b057d007fe1a`;
all crops in that manifest pass SHA verification. Opened the whole decoded image
and crops 11, 29, 40 directly; crops 11 and 29 show the same covered figure.
Still missing: collision-free renderer placement, draw-consumed band rectangle,
actual layer/geometry audits, and the requested additional cell/state, band,
layer, and stale-frame negatives. Passing these local tests does not resolve
those missing deliverables.
### Local checks actually run
Before edits:
`PYTHONDONTWRITEBYTECODE=1 python3 tools/test_p23_life_observation_evidence.py`
returned exit 0, `Ran 51 tests in 0.142s`, `OK`.
After edits the same command returned exit 0,
`Ran 56 tests in 0.093s`, `OK`.
Syntax command (no bytecode output):
```
python3 -c 'from pathlib import Path; files=["tools/check_p23_life_observation_evidence.py","tools/test_p23_life_observation_evidence.py","tools/run_p23_life_observation.py"]; [(compile(Path(p).read_bytes(),p,"exec"),print("syntax OK:",p)) for p in files]'
```
Exit 0; printed `syntax OK:` for each of those three files.
`git diff --check` returned exit 0 with no output (read-only check).
No Godot, native/OS, full regression, provider, Runner, git write, or agent
execution occurred. The initial shell heredoc attempt failed before execution
because the shell could not create its temporary file; subsequent inspection
used `python3 -c`.
## Recorded geometry direction continuation — blocked pending placement clarification
Read the complete `BUILD_OUTPUT_DIR/negative-image-review-r13/
OPUS_GEOMETRY_RESOLUTION_DIRECTION.json`, the owner run direction, the current
PixelPeople and WorldPreview sources, RoomPresentation slots, the existing
Phase 0 audit and the actual initialize/spawn/save/load API sources. The spec
still matches its contracted **stripped-text** SHA256. No spec, Runtime, save,
asset, UI entry, gameplay value, authority entry or suite policy was changed.
### Implemented independent preparation
- Replaced the full-box overlap invariant with identity cores. The table is
pinned to PixelPeople SHA256
`a171703083aa2d00f8be2b63e1daef9c41717c8fe59d8056878bc4b7df1e0b0c`.
Upright walking/carry-walking phases 2 and 6 shift the core up one grid unit.
Standard-room sleep phases 2, 3 and 4 extend its top from y=11 to y=10,
keeping the bottom at y=16, as required by the actual breathing marks.
Flips use the actual 16-grid mirror. Missing pose inputs now fail closed.
- Preserved the edge, forged-verdict, cabin, invalid-geometry and R13 tests.
Edge coordinates now use 6x12 core extents instead of the rejected 16x18
full-box extents; the deliberate overlap moves from [15,17] to [5,11].
Added a passing full-box-overlap/disjoint-core case and bob, breathing,
flip, missing-pose, duplicate, forged-transit and source-pin tests.
R13 row11/29 still fails: both recomputed cores are [669,520,18,36].
The fixture's newly required mode/frame/flip values were copied from the
original request and checked against it. False judgments remain false.
- Added the LifeOverlay Node2D between InactiveLayerRooms and LayerLens,
connected its draw signal, and queued its redraw from the parent draw.
The overlay draws traffic first, then cabin passengers and floor actors.
Existing inactive-art alpha remains 0.22. Actor rows and animation inputs
are unchanged. The report records observation/consumed serials, actual
child indices and the passes executed in the callback.
- Added the prescribed band rectangle at cell_bottom - 2*world_pixel,
with existing thickness clamp, colour thresholds and 0.90 opacity.
The blocked X is contained in the band pass. `compare_traffic` independently
recomputes both cell and band rectangles from typed state. Overlay checks
reject stale serial, reversed passes, child-order errors, a core covered
by the band, and forged band geometry. Source wiring/order is also checked,
so valid report flags do not excuse the tested reversed source callback.
FRONT/BACK local synthetic checks and GDScript serial/order assertions were
added. GDScript was **not executed**.
### Exact outstanding interpretation
Placement rule 1 gives service/rest/treatment anchors a facility region, while
rule 2 says a floor actor must *also* remain in its own logical cell. It is
unclear whether “floor actor” excludes these facility-anchored actors or means
every non-passenger. This is consequential in the actual R13 request, not only
in a fabricated layout:
- `guest-auto-00000044` receives `service_00000009` at cafeteria
`facility_00000003`. Its unchanged logical cell is [5,0,0]. The recorded
traffic inset [965,486,60,92] reconstructs cell [963,484,64,96].
The facility rect is [1027,388,256,192], and its four-pixel inset starts at
x=1031. There is no common x interval with the logical cell ending x=1027.
- `guest-auto-00000019` receives `service_00000002` at standard room
`facility_00000007`, rect [131,292,128,96]. Its logical access cell [-6,2,0]
reconstructs to [259,292,64,96]. The facility inset ends x=255; again there
is no common x interval. Its room pose is standard_room_sleep.
`r13_service_region_gap.json` preserves the needed original report fields,
with source request SHA256
`7dc3ee0bcc428e9d103726ee2448c767f1dd4107f9490833dc860c0b5140068c`.
Direct comparison verified every copied guest, traffic and room rectangle.
A diagnostic unit test demonstrates the empty intersection for both examples.
These extracted fields are not substituted for whole typed acceptance evidence.
This does **not** establish that the new specification is impossible: rule 7
allows aggregation when p=1 cannot fit. If floor means all non-passengers, these
otherwise roomy facilities must aggregate their service guests with zero drawn
members. If floor means unanchored actors, those guests can be packed and drawn
in their facility region. The initial commentary's “constraint conflict” is
therefore narrowed to this interpretation question. No cell restriction was
silently removed and no zero-drawn service aggregation was silently adopted.
An asynchronous question asks the owner to select the intended interpretation.
### Remaining implementation and evidence
Ordered placement, scale fitting, no-fit aggregation, queue-order preservation,
carrier-relative fitted placement, transit recomputation/exemptions and their
checker integration remain unimplemented. The checker remains conservative:
an asserted in_transit flag never bypasses overlap, but the required genuine
transit exception is not implemented. The existing geometry checker still
expects original actor positions/scales. The four-occupant GDScript fixture,
positive packing cases (staff/guest, passenger/floor, queue, saturation and
carried patient), and the new containment/aggregation/queue-order negatives
remain outstanding. The partial renderer changes are not a submitted candidate
and are not claimed to solve R13 actor collisions. All existing tests remain.
Still missing: Root **new native/OS evidence**, the **177-test full regression**,
and **independent Opus all-condition review** for the final candidate.
Historical 49 is preserved and is not relabeled or credited to these edits.
Seven immutable conditions and all6/30/88 remain exact, without partial credit.
### Checks actually run in this continuation
- `PYTHONDONTWRITEBYTECODE=1 python3 tools/test_p23_life_observation_evidence.py`:
first core run: 63 tests, 7 failures and 1 error because older synthetic
reports omitted pose fields. Updated those fixtures with explicit poses;
63 tests then passed (0.089s), followed by 64 (0.094s) after the bounds
diagnostic. First overlay run: 71 tests, 1 error because the older spatial
fixture lacked the new band rectangle. Updated that fixture with explicit
band geometry and consumed-frame fields. Final run: **71 tests, OK, 0.081s**.
- The existing no-bytecode `compile(Path(p).read_bytes(), p, "exec")` syntax
check passed for checker, checker tests and Root runner script (exit 0).
- Direct Python read-only comparisons verified both extracted R13 fixtures
against the original SHA-bound request and verified the immutable spec pin.
- A shell heredoc read attempt failed before execution with “can't create
temp file”; inspection continued with `python3 -c`.
No Godot, native/OS, full regression, Runner, nested provider, git write or
other-agent execution occurred. Only allowed files were edited. The worktree's
previous uncommitted work was retained and extended.
+252 -24
View File
@@ -79,6 +79,8 @@ var _queue_hidden: Dictionary = {}
var _landing_badges: Array[Dictionary] = []
var _transport_report: Dictionary = {}
var _life_observation: Dictionary = {}
var _life_overlay: Node2D
var _life_observation_serial := 0
var _dragging := false
var _drag_button := MOUSE_BUTTON_NONE
@@ -104,6 +106,10 @@ func _ready() -> void:
_inactive_layer_art = InactiveLayerRooms.new()
_inactive_layer_art.name = "InactiveLayerRooms"
add_child(_inactive_layer_art)
_life_overlay = Node2D.new()
_life_overlay.name = "LifeOverlay"
_life_overlay.draw.connect(_draw_life_overlay)
add_child(_life_overlay)
_layer_lens = ColorRect.new()
_layer_lens.name = "LayerLens"
_layer_lens.mouse_filter = Control.MOUSE_FILTER_IGNORE
@@ -513,6 +519,8 @@ func _draw() -> void:
func _draw_contents() -> void:
_life_observation = {}
if _life_overlay != null:
_life_overlay.queue_redraw()
if _inactive_layer_art != null:
_inactive_layer_art.visible = false
_draw_background()
@@ -524,6 +532,9 @@ func _draw_contents() -> void:
if map_rect.size == Vector2.ZERO:
return
_life_observation = _build_life_observation()
_life_observation_serial += 1
_life_observation[&"observation_serial"] = _life_observation_serial
_life_observation[&"overlay_consumed_serial"] = -1
_room_presentations = _life_observation[&"rooms"]
_carried_patients = _life_observation[&"carried_patients"]
_treated_patients = _life_observation[&"treated_patients"]
@@ -540,11 +551,9 @@ func _draw_contents() -> void:
_draw_legacy_stairs()
_draw_elevators()
_draw_other_layer()
_draw_congestion_overlay(bounds)
_draw_life_badges()
_draw_construction_labels()
_draw_facility_labels()
_draw_actors()
_draw_excavation_jobs()
_draw_build_preview()
_draw_excavation_preview()
@@ -599,6 +608,7 @@ func _build_life_observation() -> Dictionary:
model[&"guests"] = guests
model[&"staff"] = staff
model[&"tasks"] = tasks
_fit_life_actors(model)
var bridge: Variant = _service(&"supply_logistics")
var goods: Dictionary = bridge.get_report() if bridge != null else {&"requests": [], &"deliveries": []}
model[&"goods"] = goods
@@ -638,7 +648,14 @@ func _build_life_observation() -> Dictionary:
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)})
var cell_rect := get_cell_rect(cell)
var rect := cell_rect.grow(-2.0)
var world_pixel := _cell_dimensions().x / 16.0
var thickness := clampf(rect.size.y * 0.16, 2.0, 5.0)
var band := Rect2(Vector2(rect.position.x + rect.size.x * 0.12,
cell_rect.end.y - 2 * world_pixel - thickness * 0.5),
Vector2(rect.size.x * 0.76, thickness))
traffic.append({&"cell": cell, &"traffic": row, &"rect": rect, &"band_rect": band})
model[&"traffic"] = traffic
return model
@@ -758,9 +775,27 @@ 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:
func _draw_life_overlay() -> void:
if _life_observation.is_empty():
return
# These are actual child indices and the passes executed by this callback.
_life_observation[&"overlay_child_indices"] = [_inactive_layer_art.get_index(), _life_overlay.get_index(), _layer_lens.get_index()]
_life_observation[&"overlay_passes"] = []
_life_observation[&"overlay_passes"].append(&"traffic")
_draw_congestion_overlay(_life_overlay)
_life_observation[&"overlay_passes"].append(&"actors")
for cabin: Dictionary in _life_observation.get(&"cabins", []):
for actor: Dictionary in cabin[&"passengers"]:
_draw_observed_actor(actor, _life_overlay)
_draw_actors(_life_overlay)
for badge: Dictionary in _life_observation.get(&"actor_aggregations", []):
_draw_readable_badge(badge[&"at"], badge[&"text"], _life_overlay)
_life_observation[&"overlay_consumed_serial"] = _life_observation[&"observation_serial"]
func _draw_congestion_overlay(canvas: CanvasItem) -> void:
for row: Dictionary in _life_observation.get(&"traffic", []):
var rect: Rect2 = row[&"rect"]
var band: Rect2 = row[&"band_rect"]
var traffic: Dictionary = row[&"traffic"]
var density := int(traffic[&"current_density"])
var blocked := bool(traffic[&"temporary_block"])
@@ -768,13 +803,11 @@ func _draw_congestion_overlay(_bounds: Dictionary) -> void:
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))
canvas.draw_rect(band, Color(color, 0.90))
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)
var cross := band.grow(-1.0)
canvas.draw_line(cross.position, cross.end, Color(color, 0.90), 2.0)
canvas.draw_line(Vector2(cross.end.x, cross.position.y), Vector2(cross.position.x, cross.end.y), Color(color, 0.90), 2.0)
## Visual-only level comes from the existing live FacilityInstance, not the
@@ -887,13 +920,15 @@ func _draw_world_entity_icons() -> void:
draw_rect(rect.grow(-p),Color("f5d582"),false,2*p)
func _draw_readable_badge(at: Vector2, text: String) -> void:
func _draw_readable_badge(at: Vector2, text: String, canvas: CanvasItem = null) -> void:
if canvas == null:
canvas = self
var width := ThemeDB.fallback_font.get_string_size(text, HORIZONTAL_ALIGNMENT_LEFT, -1, 10).x+6
var rect := Rect2(at, Vector2(width, 14))
if not rect.intersects(Rect2(Vector2.ZERO, size)):
return
draw_rect(rect, Color("34413b", 0.94))
draw_string(ThemeDB.fallback_font, at+Vector2(3, 11), text, HORIZONTAL_ALIGNMENT_LEFT, -1, 10, Color("fff2ce"))
canvas.draw_rect(rect, Color("34413b", 0.94))
canvas.draw_string(ThemeDB.fallback_font, at+Vector2(3, 11), text, HORIZONTAL_ALIGNMENT_LEFT, -1, 10, Color("fff2ce"))
func _prepare_room_presentations() -> void:
@@ -1146,8 +1181,6 @@ func _draw_facility_labels() -> void:
func _draw_elevators() -> void:
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", []):
@@ -1188,8 +1221,7 @@ func _observe_elevators(observation: Dictionary) -> Array[Dictionary]:
var doors_open := operational and phase in [&"IDLE", &"BOARDING", &"ALIGHTING"]
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):
for index: int in range(passengers.size()):
var actor_key := _elevator_actor_key(String(passengers[index]))
var is_staff := actor_key.begins_with("staff/")
var person: Variant = null
@@ -1200,7 +1232,7 @@ func _observe_elevators(observation: Dictionary) -> Array[Dictionary]:
var carrying := is_staff and person != null and not String(person.carried_inventory_id).is_empty()
var state := int(person.state) if person != null else 0
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 feet := cabin.end.y - (2.0 + (int(index / seat_columns) % 2)*9.0)*p
var center := Vector2(x, feet+3*p-_cell_dimensions().y*0.5)
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,
@@ -1240,9 +1272,9 @@ func _tracked_elevator_floor(shaft_id: StringName, current_floor: float, observa
return lerpf(float(track[&"from"]),current_floor,t)
func _draw_actors() -> void:
func _draw_actors(canvas: CanvasItem = null) -> void:
for row: Dictionary in _life_observation.get(&"actor_drawings", []):
_draw_observed_actor(row)
_draw_observed_actor(row, canvas)
func _observe_actors(observation: Dictionary) -> Array[Dictionary]:
@@ -1291,6 +1323,9 @@ func _observe_actors(observation: Dictionary) -> Array[Dictionary]:
continue
var position := _tracked_actor_position(key,staff.current_cell,observation)
if staff.current_cell.z==layer_mode:
if _queue_positions.has(key):
rows.append(_actor_drawing(_queue_positions[key],key,true,int(staff.state),false,not String(staff.carried_inventory_id).is_empty(),_staff_work_animation(staff),false,observation))
continue
if _carried_patients.has(staff.staff_id):
var carrier: Variant = staff_service.get_staff(_carried_patients[staff.staff_id])
if carrier != null:
@@ -1412,11 +1447,13 @@ func _draw_pixel_actor(center: Vector2, key: String, staff: bool, state: int, re
_draw_observed_actor(_actor_drawing(center, key, staff, state, receiving, carrying, activity, riding))
func _draw_observed_actor(row: Dictionary) -> void:
func _draw_observed_actor(row: Dictionary, canvas: CanvasItem = null) -> void:
if canvas == null:
canvas = self
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"])
canvas.draw_rect(Rect2(at + Vector2(5, 17) * p, Vector2(8, 1) * p), Color("303c40", 0.2))
People.draw(canvas, 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:
@@ -1454,6 +1491,197 @@ func _actor_drawing(center: Vector2, key: String, staff: bool, state: int, recei
&"riding": riding, &"flip": float(track.get(&"direction", 1.0)) < 0.0}
## Identity cores are tied to the unchanged PixelPeople poses, not sprite boxes.
func _life_core(row: Dictionary) -> Rect2:
var phase := int(row[&"frame"]) % 8
var core := Rect2(6, 1, 6, 12)
if row[&"mode"] in [&"sleep", &"collapsed", &"standard_room_sleep"]:
core = Rect2(2, 11, 13, 5)
if row[&"mode"] == &"standard_room_sleep" and phase in [2, 3, 4]:
core.position.y -= 1
core.size.y += 1
elif row[&"mode"] in [&"walk", &"carry_walk"] and phase in [2, 6]:
core.position.y -= 1
if row[&"flip"]:
core.position.x = 16-core.end.x
return Rect2(row[&"at"]+core.position*row[&"scale"], core.size*row[&"scale"])
func _life_track_region(key: String, cell: Vector3i, model: Dictionary) -> Dictionary:
var track: Dictionary = model[&"actor_tracks"].get(key, {})
var transit: bool = not model[&"paused"] and track.get(&"from", cell) != cell and _animation_time-float(track.get(&"at", 0.0)) < 0.42
var rect := get_cell_rect(cell)
if transit:
rect = rect.merge(get_cell_rect(track[&"from"]))
return {&"bounds": rect, &"transit": transit}
func _life_actor_region(row: Dictionary, person: Dictionary, model: Dictionary) -> Dictionary:
var key: String = row[&"key"]
var cell: Vector3i = person[&"cell"]
var result := _life_track_region(key, cell, model)
var p := _cell_dimensions().x/16.0
var world: Variant = _service(&"world_service")
var facility_id: StringName = &""
if model[&"queue_positions"].has(key):
result = {&"bounds": Rect2(model[&"queue_positions"][key]-_cell_dimensions()*0.5, _cell_dimensions()), &"transit": false}
elif not row[&"staff"]:
if not String(person[&"service"]).is_empty():
var room: Dictionary = model[&"rooms"].get(person[&"target"], {})
if (room.get(&"people", []) as Array).has(person[&"id"]):
facility_id = person[&"target"]
else:
var id: StringName = person[&"id"]
if model[&"carried_patients"].has(id):
for carrier: Dictionary in model[&"staff"]:
if carrier[&"id"] == model[&"carried_patients"][id]:
result = _life_track_region("staff/"+String(carrier[&"id"]), carrier[&"cell"], model)
var rect: Rect2 = result[&"bounds"]
rect.position.y -= 12*p
result[&"bounds"] = rect
elif model[&"resting_staff"].has(id):
facility_id = model[&"resting_staff"][id]
elif model[&"treated_patients"].has(id):
facility_id = model[&"treated_patients"][id]
elif person[&"state"] in ["WORKING", "AVAILABLE"] and world != null:
var cells: Array[Vector3i] = world.get_entity_cells(person[&"facility"])
var room: Dictionary = model[&"rooms"].get(person[&"facility"], {})
var slot := (room.get(&"staff", []) as Array).find(id)
var rect := _cell_bounds_rect(cells)
var columns := maxi(int(rect.size.x/(16*p)), 1)
if cells.has(cell) and slot >= 0 and slot < columns and rect.size.y >= 40*p:
result = {&"bounds": Rect2(rect.position+Vector2(slot*rect.size.x/columns, 0), Vector2(rect.size.x/columns, rect.size.y)), &"transit": false}
if not facility_id.is_empty() and world != null:
var cells: Array[Vector3i] = world.get_entity_cells(facility_id)
if not cells.is_empty():
result = {&"bounds": _cell_bounds_rect(cells), &"transit": false}
result[&"bounds"] = (result[&"bounds"] as Rect2).grow(-p)
return result
func _life_scaled(row: Dictionary, scale: float) -> Dictionary:
var result := row.duplicate(true)
result[&"scale"] = scale
result[&"at"] = (row[&"center"]+Vector2(-8*scale, _cell_dimensions().y*0.5-3*_cell_dimensions().x/16.0-17*scale)).round()
return result
## Forward/backward packing keeps each member's own interval. No relocation.
func _life_pack(group: Array, scale: float) -> Array:
var trial: Array = []
var right := -INF
for member: Dictionary in group:
var row := _life_scaled(member[&"row"], minf(scale, member[&"row"][&"scale"]))
var core := _life_core(row)
var bounds: Rect2 = member[&"bounds"]
var x := maxf(maxf(core.position.x, bounds.position.x), right)
row[&"at"].x += x-core.position.x
right = x+core.size.x
trial.append(row)
var left := INF
for i: int in range(trial.size()-1, -1, -1):
var row: Dictionary = trial[i]
var core := _life_core(row)
var bounds: Rect2 = group[i][&"bounds"]
var x := minf(core.position.x, minf(bounds.end.x, left)-core.size.x)
row[&"at"].x += x-core.position.x
left = x
if x < bounds.position.x or core.position.y < bounds.position.y or core.end.y > bounds.end.y:
return []
return trial
## Rebuild vertical components at every fitted scale. Ceilings only decrease;
## once none decreases, the feet/cores and therefore the partition are fixed.
func _life_fitted_groups(members: Array) -> Array:
var groups: Array = []
while true:
for member: Dictionary in members:
member[&"row"].merge(_life_scaled(member[&"row"], member[&"row"][&"scale"]), true)
members.sort_custom(func(a: Dictionary, b: Dictionary) -> bool:
var ay := _life_core(a[&"row"]).position.y
var by := _life_core(b[&"row"]).position.y
return ay < by or (ay == by and a[&"row"][&"key"] < b[&"row"][&"key"]))
groups = []
var bottom := -INF
for member: Dictionary in members:
var core := _life_core(member[&"row"])
if groups.is_empty() or core.position.y >= bottom:
groups.append([])
bottom = core.end.y
else:
bottom = maxf(bottom, core.end.y)
groups.back().append(member)
var reduced := false
for group: Array in groups:
group.sort_custom(func(a: Dictionary, b: Dictionary) -> bool: return a[&"row"][&"center"].x < b[&"row"][&"center"].x or (a[&"row"][&"center"].x == b[&"row"][&"center"].x and a[&"row"][&"key"] < b[&"row"][&"key"]))
var ceiling := 1.0
for member: Dictionary in group:
ceiling = maxf(ceiling, member[&"row"][&"scale"])
if _life_pack(group, ceiling).is_empty() and ceiling > 1:
for member: Dictionary in group:
var scale := minf(member[&"row"][&"scale"], maxf(1, int(ceiling)-1))
member[&"row"].merge(_life_scaled(member[&"row"], scale), true)
reduced = true
if not reduced:
return groups
return groups
func _fit_life_actors(model: Dictionary) -> void:
var people: Dictionary = {}
for person: Dictionary in model[&"guests"]:
people["guest/"+String(person[&"id"])] = person
for person: Dictionary in model[&"staff"]:
people["staff/"+String(person[&"id"])] = person
var members: Array = []
var all_rows: Array = model[&"actor_drawings"].duplicate()
var passenger_bounds: Dictionary = {}
for cabin: Dictionary in model[&"cabins"]:
for row: Dictionary in cabin[&"passengers"]:
all_rows.append(row)
passenger_bounds[row[&"key"]] = (cabin[&"rect"] as Rect2).grow(-_cell_dimensions().x/16.0)
for row: Dictionary in all_rows:
var person: Dictionary = people[row[&"key"]]
row[&"cell"] = person[&"cell"]
var region := _life_actor_region(row, person, model) if not row[&"riding"] else {&"bounds": passenger_bounds[row[&"key"]], &"transit": false}
region[&"row"] = row
if not region[&"transit"]:
members.append(region)
var groups := _life_fitted_groups(members)
model[&"actor_aggregations"] = []
var hidden: Dictionary = {}
for group: Array in groups:
group.sort_custom(func(a: Dictionary, b: Dictionary) -> bool: return a[&"row"][&"center"].x < b[&"row"][&"center"].x or (a[&"row"][&"center"].x == b[&"row"][&"center"].x and a[&"row"][&"key"] < b[&"row"][&"key"]))
var base := 1.0
for member: Dictionary in group:
base = maxf(base, member[&"row"][&"scale"])
var fitted := _life_pack(group, base)
var retained: Array = group
if fitted.is_empty():
retained = []
var identities: Array = []
var drawn: Array = []
for member: Dictionary in group:
var row: Dictionary = member[&"row"]
identities.append({&"key": row[&"key"], &"id": people[row[&"key"]][&"id"], &"state": row[&"state"], &"cell": row[&"cell"], &"mode": row[&"mode"]})
var attempt := retained.duplicate()
attempt.append(member)
var packed := _life_pack(attempt, 1)
if not packed.is_empty():
retained = attempt
fitted = packed
drawn.append(row[&"key"])
else:
hidden[row[&"key"]] = true
model[&"actor_aggregations"].append({&"members": identities, &"drawn_keys": drawn, &"at": (group[0][&"bounds"] as Rect2).position, &"text": "+%d" % (group.size()-retained.size())})
for i: int in fitted.size():
retained[i][&"row"].merge(fitted[i], true)
model[&"actor_drawings"] = model[&"actor_drawings"].filter(func(row: Dictionary) -> bool: return not hidden.has(row[&"key"]))
for cabin: Dictionary in model[&"cabins"]:
cabin[&"passengers"] = cabin[&"passengers"].filter(func(row: Dictionary) -> bool: return not hidden.has(row[&"key"]))
func _draw_excavation_jobs() -> void:
var excavation: Variant = _service(&"excavation_service")
if excavation == null or not excavation.has_method(&"get_jobs"):
+3
View File
@@ -166,6 +166,9 @@ func draw_rectangles(report: Dictionary, view: WorldPreview) -> Dictionary:
var rest := actor_rectangle(actor)
rest["category"] = "rest"
rows.append(rest)
for index: int in range(report.get(&"actor_aggregations", []).size()):
var badge: Dictionary = report[&"actor_aggregations"][index]
rows.append(badge_rectangle("aggregation", str(index), badge[&"at"], badge[&"text"]))
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"]))
+15
View File
@@ -0,0 +1,15 @@
{
"purpose": "Historical negative only; not fresh acceptance evidence",
"source_request": "os-pair/A/reader-request-0003/request.json",
"source_request_sha256": "7dc3ee0bcc428e9d103726ee2448c767f1dd4107f9490833dc860c0b5140068c",
"manifest_sha256": "53aa8e2f12e5d0a089529457fcd55ee1ba0600e8dffff2225bb8b057d007fe1a",
"independent_judgment": [11, false, false, false],
"source_rows": [11, 29],
"report_inputs": {
"actor_drawings": [
{"key": "guest/guest-auto-00000041", "at": [651.0, 517.0], "scale": 3.0, "mode": "wait", "frame": 12, "flip": false},
{"key": "staff/staff_gina", "at": [651.0, 517.0], "scale": 3.0, "mode": "idle", "frame": 3, "flip": false}
],
"cabins": []
}
}
+17
View File
@@ -0,0 +1,17 @@
{
"purpose": "Historical diagnostic input for placement-rule clarification; not fresh acceptance evidence",
"source_request": "os-pair/A/reader-request-0003/request.json",
"source_request_sha256": "7dc3ee0bcc428e9d103726ee2448c767f1dd4107f9490833dc860c0b5140068c",
"guests": [
{"id": "guest-auto-00000044", "state": "RECEIVING_SERVICE", "cell": [5, 0, 0], "target": "facility_00000003", "service": "service_00000009", "queue": ""},
{"id": "guest-auto-00000019", "state": "RECEIVING_SERVICE", "cell": [-6, 2, 0], "target": "facility_00000007", "service": "service_00000002", "queue": ""}
],
"room_rects": {
"facility_00000003": [1027.0, 388.0, 256.0, 192.0],
"facility_00000007": [131.0, 292.0, 128.0, 96.0]
},
"traffic": [
{"cell": [5, 0, 0], "traffic": {"base_capacity": 4, "current_density": 6, "dynamic_cost": 0, "temporary_block": false}, "rect": [965.0, 486.0, 60.0, 92.0]},
{"cell": [-6, 2, 0], "traffic": {"base_capacity": 4, "current_density": 3, "dynamic_cost": 0, "temporary_block": false}, "rect": [261.0, 294.0, 60.0, 92.0]}
]
}
+59
View File
@@ -56,6 +56,61 @@ func run() -> void:
view.size = Vector2(1440, 810)
view.set_runtime(runtime)
root.add_child(view)
# R13's four same-cell MOVING guests. Component fixture only, no Runtime edit.
var crowd: Array = []
var offsets := [0, 16, -16, 8]
for i: int in 4:
crowd.append({&"row": {&"key": "guest/os-p23-%03d" % (12+i),
&"center": Vector2(108+offsets[i], 118-4*(i%2)),
&"at": Vector2(84+offsets[i], 103-4*(i%2)), &"scale": 3.0,
&"mode": &"idle", &"frame": 0, &"flip": false,
&"state": GuestRuntime.State.MOVING, &"cell": Vector3i(1,1,0)},
&"bounds": Rect2(80,74,56,88), &"transit": false})
crowd.sort_custom(func(a: Dictionary, b: Dictionary) -> bool: return a[&"row"][&"center"].x < b[&"row"][&"center"].x)
# _life_pack uses the view's cell dimensions for the unchanged feet formula.
# The x-capacity assertion is independent of the current default world size.
expect(view._life_pack(crowd, 3).is_empty(), "four cores cannot fit 72px in 56px")
var saved_size := view.size
var saved_zoom := view.zoom_level
var dim: Vector2 = view._cell_dimensions()
view.zoom_level *= 64.0/dim.x
var fitted: Array = view._life_pack(crowd, 2)
expect(fitted.size() == 4, "all four R13 occupants fit at p=2")
for row: Dictionary in fitted:
expect(row[&"scale"] == 2.0 and row[&"cell"] == Vector3i(1,1,0), "fitting preserves cell and uses p=2")
var regroup: Array = crowd.slice(0, 2).duplicate(true)
for i: int in 2:
regroup[i][&"row"][&"center"] = Vector2(108, 118+26*i)
regroup[i][&"bounds"] = Rect2(80,74,20,130)
var regrouped: Array = view._life_fitted_groups(regroup)
expect(regrouped.size() == 2, "scale change splits vertical component")
for group: Array in regrouped:
expect(group.size() == 1 and group[0][&"row"][&"scale"] == 2.0, "regroup avoids stale p=1 fit")
var saturated: Array = []
for i: int in 10:
var member: Dictionary = crowd[0].duplicate(true)
member[&"row"][&"key"] = "guest/saturation-%02d" % i
member[&"row"][&"center"] = Vector2(108,118)
saturated.append(member)
var saturated_groups: Array = view._life_fitted_groups(saturated)
expect(saturated_groups.size() == 1, "saturation keeps one vertical component")
expect(view._life_pack(saturated_groups[0], 1).is_empty(), "ten cores cannot fit 60px in 56px")
expect(view._life_pack(saturated_groups[0].slice(0,9), 1).size() == 9, "nine cores fit without losing identities")
var aggregate_model := {&"guests": [], &"staff": [], &"actor_drawings": [], &"cabins": []}
var aggregate_rows: Array = []
for member: Dictionary in saturated:
var row: Dictionary = member[&"row"].duplicate(true)
row[&"riding"] = true
aggregate_rows.append(row)
aggregate_model[&"guests"].append({&"id": String(row[&"key"]).trim_prefix("guest/"), &"cell": Vector3i(1,1,0)})
aggregate_model[&"cabins"].append({&"rect": Rect2(76,70,64,96), &"passengers": aggregate_rows})
view._fit_life_actors(aggregate_model)
expect(aggregate_model[&"cabins"][0][&"passengers"].size() == 9, "aggregation draws all nine fitting members")
var aggregate: Dictionary = aggregate_model[&"actor_aggregations"][0]
expect(aggregate[&"members"].size() == 10 and aggregate[&"drawn_keys"].size() == 9, "aggregation retains every identity")
expect(aggregate[&"text"] == "+1" and aggregate[&"at"] == Vector2(80,74), "actual aggregation badge position and text")
view.zoom_level = saved_zoom
view.size = saved_size
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()
@@ -67,6 +122,10 @@ func run() -> void:
await process_frame
var report := view.get_life_observation_report()
expect(not report.is_empty(), "actual drawing published an observation")
expect(report.get(&"observation_serial", -1) == report.get(&"overlay_consumed_serial", -2), "LifeOverlay consumed this observation")
expect(report.get(&"overlay_passes", []) == [&"traffic", &"actors"], "traffic drawn before actors")
var indices: Array = report.get(&"overlay_child_indices", [])
expect(indices.size() == 3 and indices[0] < indices[1] and indices[1] < indices[2], "LifeOverlay above inactive art and below lens")
# The getter exposes the last actual draw and must not create another model.
var original: PackedByteArray = var_to_bytes(report)
if report.has(&"traffic"):
+372 -44
View File
@@ -329,6 +329,8 @@ def drawing_rectangle_rows(report):
actor(a)
if a['staff'] and a['key'].removeprefix('staff/') in report['resting_staff']:
actor(a, 'rest')
for i, b in enumerate(report.get('actor_aggregations', [])):
badge('aggregation', str(i), b)
for i, b in enumerate(report['badges']):
badge('goods' if b['kind'] == 'supply' else 'business', str(i), b)
for row in report.get('traffic', []):
@@ -337,7 +339,77 @@ def drawing_rectangle_rows(report):
return expected
def validate_drawing_rows(report, audit, root=ROOT):
PIXEL_PEOPLE_CORE_SHA256 = 'a171703083aa2d00f8be2b63e1daef9c41717c8fe59d8056878bc4b7df1e0b0c'
def actor_identity_core(actor):
"""Grid identity bounds from the pinned PixelPeople pose, including bob.
Standard-room breathing extends the upper edge, not the lower edge.
Arms, legs, props and the sleep symbol are outside the identity core.
"""
key, at, scale = actor['key'], actor['at'], actor['scale']
require(len(at) == 2 and all(type(v) in (int, float) and math.isfinite(v) for v in at)
and type(scale) in (int, float) and math.isfinite(scale) and scale > 0,
'invalid actor footprint: ' + key)
require(isinstance(actor.get('mode'), str) and type(actor.get('frame')) is int
and type(actor.get('flip')) is bool, 'missing actor pose: ' + key)
mode, phase = actor['mode'], actor['frame'] % 8
if mode in ('sleep', 'collapsed', 'standard_room_sleep'):
breathing = -1 if mode == 'standard_room_sleep' and phase in (2, 3, 4) else 0
x, y, w, h = 2, 11 + breathing, 13, 5 - breathing
else:
bob = -1 if mode in ('walk', 'carry_walk') and phase in (2, 6) else 0
x, y, w, h = 6, 1 + bob, 6, 12
if actor['flip']:
x = 16 - x - w
return [at[0] + x * scale, at[1] + y * scale, w * scale, h * scale]
def validate_actor_nonoverlap(report, root=ROOT, transit_keys=()):
"""Recompute identity cores from drawing inputs, including cabin passengers.
Do not consume audit eligibility or boolean judgments: neither an off-screen
label nor a claimed pass exempts a submitted actor from this invariant.
Transparent margins may overlap. No asserted transit flag grants exemption.
"""
require(hashlib.sha256((root / 'scripts/ui/pixel_people.gd').read_bytes()).hexdigest()
== PIXEL_PEOPLE_CORE_SHA256, 'PixelPeople core source SHA256')
actors = list(report['actor_drawings']) + [
actor for cabin in report['cabins'] for actor in cabin['passengers']]
rects = []
keys = set()
for actor in actors:
key = actor['key']
require(key not in keys, 'duplicate actor drawing: ' + key)
keys.add(key)
rect = actor_identity_core(actor)
if key in transit_keys:
continue
for other_key, other in rects:
intersects = (rect[0] < other[0] + other[2] and other[0] < rect[0] + rect[2]
and rect[1] < other[1] + other[3] and other[1] < rect[1] + rect[3])
require(not intersects, 'actor-actor core overlap: ' + other_key + ' / ' + key)
rects.append((key, rect))
def validate_drawing_rows(report, audit, root=ROOT, state=None, rest_audit=None):
# Without raw inputs this is deliberately conservative: NO transit exemption.
# With inputs, use the same state-derived classification and fit proof as the
# stream checker, never a drawing's region/in_transit assertion.
if state is None:
validate_actor_nonoverlap(report, root)
else:
require(isinstance(state, dict) and isinstance(state.get('systems', {}).get('runtime'), dict),
'malformed raw state context')
if report['resting_staff']:
require(rest_audit is not None, 'missing rest authority for drawing geometry')
if rest_audit is not None:
require(isinstance(rest_audit, dict) and isinstance(rest_audit.get('staff'), list),
'malformed rest authority')
compare_rest(state, report, rest_audit, root)
compare_cabin_geometry(state, report)
compare_actor_geometry(state, report)
expected = drawing_rectangle_rows(report)
require(len(audit['rows']) == len(expected), 'offline rectangle coverage')
for row, (category, identifier, rect, source, text) in zip(audit['rows'], expected):
@@ -367,15 +439,17 @@ def encode_png(rows):
+ chunk(b'IDAT', zlib.compress(raw)) + chunk(b'IEND', b''))
def frame_rows(report, audit, request, receipt, images, clicks, root=ROOT):
def frame_rows(report, audit, request, receipt, images, clicks, root=ROOT, state=None, rest_audit=None):
"""One mechanical path for frozen diagnostics and actual OS frames.
Errors are retained as failures, including incomplete frozen audits. Every
supplied row is accounted for; no palette value is an image oracle.
"""
errors, rows = [], []
transit_authority = ('raw state, track, time and pause' if state is not None
else 'absent: fail closed, no transit exemptions')
try:
validate_drawing_rows(report, audit, root)
validate_drawing_rows(report, audit, root, state, rest_audit)
except (ValueError, KeyError, TypeError) as exc:
errors.append(str(exc))
calibration = screenshot_transform(receipt['calibration'], request, images[0][:2], clicks)
@@ -415,6 +489,9 @@ def frame_rows(report, audit, request, receipt, images, clicks, root=ROOT):
if row['category'] == 'queue':
badge = report['landing_badges'][int(row['id'])]
state = {k: badge[k] for k in ('members', 'visible_count', 'overflow')}
if row['category'] == 'aggregation':
badge = report['actor_aggregations'][int(row['id'])]
state = {k: badge[k] for k in ('members', 'drawn_keys')}
if row['category'] == 'EV':
cabin = next(c for c in report['cabins'] if c['shaft_id'] == row['id'])
state = {k: cabin[k] for k in ('operational', 'doors_open')}
@@ -448,7 +525,8 @@ def frame_rows(report, audit, request, receipt, images, clicks, root=ROOT):
result['failures'] = failures
rows.append(result)
return dict(rows=rows, errors=errors, mechanical_pass=not errors and all(r['mechanical_pass'] for r in rows),
expected_row_count=len(drawing_rectangle_rows(report)))
expected_row_count=len(drawing_rectangle_rows(report)),
transit_authority=transit_authority)
def write_frame_review(directory, frame, provenance):
@@ -551,7 +629,14 @@ def inspect_offline_mirror(directory):
for original in (str(Path(index['source'])/conversion_path.relative_to(directory)),
conversion['png_path']):
inspected.append(by_original[original])
frame = frame_rows(report, audit, request, receipt, images, clicks, ROOT)
stream = pair/'writer-stream'
captures = [row for row in map(json.loads, (stream/'manifest.jsonl').read_text().splitlines())
if row.get('label') == name and 'report' in row]
require(len(captures) == 1, 'offline raw-state capture coverage')
capture = captures[0]
state, rest_audit = drawing_context(stream, capture, report)
frame = frame_rows(report, audit, request, receipt, images, clicks, ROOT,
state, rest_audit)
provenance = []
for number, suffix in enumerate(('', '-after')):
conversion_path = folder/('root-early-conversion'+suffix+'.json')
@@ -720,9 +805,56 @@ def compare_traffic(snapshot, report):
if cell[2] != report['layer']:
continue
x, y, w, h = transform.rect(cell)
rect = [x + 2, y + 2, w - 4, h - 4]
thickness = max(2, min(5, (h - 4) * .16))
band = [rect[0] + rect[2] * .12, y + h - 2 * (w / 16) - thickness / 2,
rect[2] * .76, thickness]
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]))
rect=rect, band_rect=band))
close(report['traffic'], rows, 'traffic and cell rect')
validate_life_overlay(report)
def validate_life_overlay(report, root=ROOT):
"""Check consumed frame and source wiring, not merely reported order flags."""
serial = report.get('observation_serial')
require(type(serial) is int and serial > 0
and type(report.get('overlay_consumed_serial')) is int
and report['overlay_consumed_serial'] == serial, 'stale LifeOverlay serial')
require(report.get('overlay_passes') == ['traffic', 'actors'], 'LifeOverlay pass order')
indices = report.get('overlay_child_indices', [])
require(len(indices) == 3 and all(type(i) is int and i >= 0 for i in indices)
and indices[0] < indices[1] < indices[2], 'LifeOverlay child order')
source = (root / 'scenes/main/world_preview.gd').read_text()
def body(name):
match = re.search(r'^func ' + name + r'\([^\n]*\n(.*?)(?=^func |\Z)', source, re.M | re.S)
require(match is not None, 'missing overlay source function: ' + name)
return match[1]
ready, draw, overlay = body('_ready'), body('_draw_contents'), body('_draw_life_overlay')
ordered = [ready.find('add_child(' + node + ')')
for node in ('_inactive_layer_art', '_life_overlay', '_layer_lens')]
require(all(i >= 0 for i in ordered) and ordered == sorted(set(ordered)), 'LifeOverlay source child order')
require('_life_overlay.draw.connect(_draw_life_overlay)' in ready
and '_life_overlay.queue_redraw()' in draw, 'LifeOverlay source connection')
require('_draw_congestion_overlay(' not in draw and '_draw_actors(' not in draw
and '_draw_observed_actor(' not in body('_draw_elevators'), 'actor or band drawn in parent')
require(0 <= overlay.find('_draw_congestion_overlay(_life_overlay)')
< overlay.find('_draw_observed_actor(actor, _life_overlay)')
< overlay.find('_draw_actors(_life_overlay)'), 'LifeOverlay source pass order')
actors = list(report['actor_drawings']) + [a for c in report['cabins'] for a in c['passengers']]
for row in report.get('traffic', []):
x, y, w, h = row['rect']
thickness = max(2, min(5, h * .16))
expected = [x + w * .12, y + h + 2 - 2 * ((w + 4) / 16) - thickness / 2,
w * .76, thickness]
close(row['band_rect'], expected, 'traffic band rectangle')
if row['traffic']['current_density'] <= 0 and not row['traffic']['temporary_block']:
continue
for actor in actors:
core = actor_identity_core(actor)
intersects = (core[0] < expected[0] + expected[2] and expected[0] < core[0] + core[2]
and core[1] < expected[1] + expected[3] and expected[1] < core[1] + core[3])
require(not intersects, 'traffic band covers actor core: ' + actor['key'])
def compare_queues(snapshot, report):
@@ -935,27 +1067,12 @@ def compare_rest(state, report, audit, root=ROOT):
if selected.get('resting'):
resting[sid] = selected['facility_id']
close(report['resting_staff'], resting, 'draw resting_staff')
transform = Transform(runtime, report)
drawings = {r['key']: r for r in report['actor_drawings']}
for sid, fid in resting.items():
member = next(m for m in members if m['staff_id'] == sid)
if member['current_cell'][2] != report['layer'] or sid in report['carried_patients']:
continue
room = report['rooms'].get(fid, {})
cells = next((r['cells'] for r in runtime['world']['occupancies'] if r['entity_id'] == fid), [])
if not cells:
continue # renderer uses its ordinary tracked fallback
require(sid in room.get('people', []), 'rest room occupant missing')
slot = room['people'].index(sid)
anchors = room_slots(transform.room(cells), transform.dim[0] / 16)
key = 'staff/' + sid
if key in report['boarded'] or key in report['queue_hidden']:
require(key not in drawings, 'hidden resting drawing')
elif slot >= len(anchors):
require(key not in drawings, 'rest slot overflow drawing')
else:
close(drawings[key]['center'], [anchors[slot][0], anchors[slot][1] - transform.dim[1] * .5],
'resting staff position')
# Fitting is cross-kind, and queue precedence applies to resting staff too.
# Reconstruct the complete group and independently prove p=1 no-fit before
# allowing an identity to appear only in an aggregation's member list.
if any(r['cells'] for r in runtime['world']['occupancies']
if r['entity_id'] in resting.values()):
compare_actor_geometry(state, report)
return selections
@@ -1129,8 +1246,105 @@ def compare_motion(state, report, audit):
return ['motion_history', 'interpolation', 'pause']
def fitted_actor_layout(members, world_pixel, height):
"""Independent interval solver. Inputs are reconstructed preferred rows/bounds."""
def scaled(member, scale):
row = copy.deepcopy(member['row'])
row['scale'] = min(row['scale'], scale)
s = row['scale']
row['at'] = [math.copysign(math.floor(abs(v)+.5), v) for v in
(row['center'][0]-8*s, row['center'][1]+height/2-3*world_pixel-17*s)]
return row
def pack(group, scale):
trial = [scaled(m, scale) for m in group]
right = -math.inf
for member, row in zip(group, trial):
x, y, w, h = actor_identity_core(row)
left = max(x, member['bounds'][0], right)
row['at'][0] += left-x
right = left+w
left = math.inf
for member, row in reversed(list(zip(group, trial))):
x, y, w, h = actor_identity_core(row)
bx, by, bw, bh = member['bounds']
target = min(x, bx+bw-w, left-w)
row['at'][0] += target-x
left = target
if target < bx or y < by or y+h > by+bh:
return None
return trial
active = copy.deepcopy([m for m in members if not m['transit']])
# A smaller sprite keeps its feet, so its vertical interval moves. Rebuild
# components after EVERY trial, including unsuccessful trials. Scale ceilings
# only decrease, making termination independent of an arbitrary iteration cap.
while True:
groups, bottom = [], -math.inf
for member in active:
member['row'] = scaled(member, member['row']['scale'])
for member in sorted(active, key=lambda m: (actor_identity_core(m['row'])[1], m['row']['key'])):
x, y, w, h = actor_identity_core(member['row'])
if y >= bottom:
groups.append([])
bottom = y+h
else:
bottom = max(bottom, y+h)
groups[-1].append(member)
reduced = False
for group in groups:
group.sort(key=lambda m: (m['row']['center'][0], m['row']['key']))
ceiling = max(1, *(m['row']['scale'] for m in group))
if pack(group, ceiling) is None and ceiling > 1:
for member in group:
member['row'] = scaled(member, max(1, int(ceiling)-1))
reduced = True
if not reduced:
break
result = {m['row']['key']: copy.deepcopy(m['row']) for m in members if m['transit']}
aggregations = []
for group in groups:
group.sort(key=lambda m: (m['row']['center'][0], m['row']['key']))
base = max(1, *(m['row']['scale'] for m in group))
fitted = pack(group, base)
if fitted is None:
retained, fitted, identities = [], [], []
for member in group:
row = member['row']
identities.append(dict(key=row['key'], id=row['key'].split('/', 1)[1],
state=row['state'], cell=row['cell'], mode=row['mode']))
attempt = pack(retained+[member], 1)
if attempt is not None:
retained.append(member)
fitted = attempt
aggregations.append(dict(members=identities, drawn_keys=[r['key'] for r in fitted],
at=group[0]['bounds'][:2], text='+'+str(len(group)-len(fitted))))
result.update({row['key']: row for row in fitted})
return result, aggregations
def actor_track_transit(report, key, cell):
"""Track endpoint is bound to raw state by the caller, not to a draw flag."""
require(type(report['paused']) is bool, 'actor pause must be boolean')
track = report['actor_tracks'][key]
close(track['cell'], cell, 'actor authoritative cell')
origin = track['from']
require(isinstance(origin, list) and len(origin) == 3
and all(type(v) is int for v in origin)
and origin[2] == cell[2]
and abs(origin[0]-cell[0])+abs(origin[1]-cell[1]) <= 1,
'actor track is not a source-valid adjacent step')
now, started = report['animation_time'], track['at']
require(all(type(v) in (int, float) and math.isfinite(v) for v in (now, started))
and now >= started, 'invalid or future actor track time')
return not report['paused'] and origin != cell and now-started < .42
def compare_actor_geometry(state, report):
runtime = state['systems']['runtime']
compare_queues(state, report)
compare_room_services(state, report)
boarded = {actor.replace('staff:', 'staff/', 1).replace('guest:', 'guest/', 1): True
for shaft in elevator_report(state)['shafts'] for actor in shaft['boarded']}
close(report['boarded'], boarded, 'boarded actor coverage')
transform = Transform(runtime, report)
p, height = transform.dim[0]/16, transform.dim[1]
facilities = codec.indexed(runtime['facilities']['instances'], 'instance_id')
@@ -1139,7 +1353,7 @@ def compare_actor_geometry(state, report):
counts, expected = {}, []
def track_position(key, cell):
track = report['actor_tracks'][key]
close(track['cell'], cell, 'actor authoritative cell')
actor_track_transit(report, key, cell)
t = 1 if report['paused'] else max(0, min(1, (report['animation_time']-track['at'])/.42))
a, b = transform.center(track['from']), transform.center(cell)
return [a[i]+(b[i]-a[i])*t for i in (0, 1)]
@@ -1182,7 +1396,10 @@ def compare_actor_geometry(state, report):
continue
center = track_position(key, cell)
special = True
if sid in report['carried_patients']:
if key in report['queue_positions']:
special = False
center = report['queue_positions'][key]
elif sid in report['carried_patients']:
carrier = staff.get(report['carried_patients'][sid])
if carrier:
center = track_position('staff/'+carrier['staff_id'], carrier['current_cell'])
@@ -1206,31 +1423,127 @@ def compare_actor_geometry(state, report):
center = report['queue_positions'].get(key, center)
if center is not None:
expected.append((key, center, True, False, bool(member['carried_inventory_id']) and not special, False))
require([r['key'] for r in report['actor_drawings']] == [r[0] for r in expected], 'actor drawing coverage/order')
pairs = list(zip(report['actor_drawings'], expected))
floor_keys = [r[0] for r in expected]
cabin_bounds = {}
shafts = {s['shaft_id']: s for s in elevator_report(state)['shafts']}
for cabin in report['cabins']:
shaft = shafts[cabin['shaft_id']]
x,y,w,h = cabin['rect']
columns = max(int((w/p-4)/8), 1)
passengers = shaft['boarded'][:columns*2]
require(len(cabin['passengers']) == len(passengers), 'passenger coverage')
for i, (actor_id, row) in enumerate(zip(passengers, cabin['passengers'])):
passengers = shaft['boarded']
for i, actor_id in enumerate(passengers):
key = actor_id.replace('staff:', 'staff/', 1).replace('guest:', 'guest/', 1)
is_staff = key.startswith('staff/')
center = [x+(2+(i%columns+.5)*8)*p, y+h-(2+(i//columns)*9)*p+3*p-height*.5]
center = [x+(2+(i%columns+.5)*8)*p, y+h-(2+((i//columns)%2)*9)*p+3*p-height*.5]
carried = bool(staff.get(key.removeprefix('staff/'), {}).get('carried_inventory_id')) if is_staff else False
pairs.append((row, (key, center, is_staff, False, carried, True)))
for row, (key, center, is_staff, receiving, carrying, riding) in pairs:
close(row['center'], center, 'actor center/interpolation')
expected.append((key, center, is_staff, False, carried, True))
cabin_bounds[key] = cabin['rect']
people = {'guest/'+r['guest_id']: r for r in runtime['guests']['guests']}
people.update({'staff/'+sid: r for sid, r in staff.items()})
def tracked_bounds(key, cell):
track = report['actor_tracks'][key]
transit = actor_track_transit(report, key, cell)
rect = transform.rect(cell)
if transit:
other = transform.rect(track['from'])
x, y = min(rect[0], other[0]), min(rect[1], other[1])
rect = [x, y, max(rect[0]+rect[2], other[0]+other[2])-x,
max(rect[1]+rect[3], other[1]+other[3])-y]
return rect, transit
members = []
for key, center, is_staff, receiving, carrying, riding in expected:
person = people[key]
cell = person['current_cell']
track = report['actor_tracks'].get(key, {})
region, transit = (cabin_bounds[key], False) if riding else tracked_bounds(key, cell)
fid = ''
if not riding:
if key in report['queue_positions']:
cx, cy = report['queue_positions'][key]
region, transit = [cx-transform.dim[0]/2, cy-height/2, transform.dim[0], height], False
elif not is_staff:
target = person['target_facility_id']
if receiving and target in facilities and key.split('/', 1)[1] in report['rooms'].get(target, {}).get('people', []):
fid = target
else:
sid = person['staff_id']
if sid in report['carried_patients']:
carrier = staff[report['carried_patients'][sid]]
region, transit = tracked_bounds('staff/'+carrier['staff_id'], carrier['current_cell'])
region[1] -= 12*p
elif sid in report['resting_staff']:
fid = report['resting_staff'][sid]
elif sid in report['treated_patients']:
fid = report['treated_patients'][sid]
else:
target = person['assigned_facility_id']
cells = occupancy.get(target, [])
workers = report['rooms'].get(target, {}).get('staff', [])
if person['state'] in ('WORKING', 'AVAILABLE') and target in facilities and cell in cells and sid in workers:
rect = transform.room(cells)
columns = max(int(rect[2]/(16*p)), 1)
slot = workers.index(sid)
if rect[3] >= 40*p and slot < columns:
region, transit = [rect[0]+slot*rect[2]/columns, rect[1], rect[2]/columns, rect[3]], False
if fid and occupancy.get(fid):
region, transit = transform.room(occupancy[fid]), False
scale = .5 if riding else .75
scale = max(math.floor(p*scale), 1) if p >= 2 else p*scale
close(row['scale'], scale, 'actor scale')
at = [center[0]-8*scale, center[1]+height*.5-3*p-17*scale]
rounded = [math.copysign(math.floor(abs(v)+.5), v) for v in at]
close(row['at'], rounded, 'actor drawing position')
require((row['key'], row['staff'], row['receiving'], row['carrying'], row['riding']) ==
(key, is_staff, receiving, carrying, riding), 'actor drawing attributes')
hashed = 5381
for char in key:
hashed = (hashed*33+ord(char)) & 0xffffffff
variant = hashed % 15
frame = int(report['animation_time']*12)+variant
activity = facilities.get(person.get('target_facility_id'), {}).get('definition_id', '') if not is_staff else 'work'
if is_staff and person['role_id'] == 'cleaner':
activity = 'idle'
task = next((t for t in runtime['staff']['tasks'] if t['task_id'] == person['current_task_id']), None)
room = facilities.get(person['assigned_facility_id'], {})
if (person['state'] == 'WORKING' and person['health'] > 0 and not person['is_retiring']
and room.get('definition_id') == 'cleaning_room' and room.get('state') in ('OPEN', 'DEGRADED')
and not room.get('suspension_reason') and task and task['task_type'] == 'facility_clean'
and task['state'] == 'WORKING' and task['assigned_staff_id'] == person['staff_id']
and cell == task['target_cell'] and any(r['cell'] == cell for r in runtime['path']['cell_traffic'])
and not any(t['actor_id'] == 'staff:'+person['staff_id'] for t in runtime['elevators']['trips'])
and task['target_entity_id'] in facilities
and any(r['id'] == task['target_entity_id'] for r in runtime['facility_cleanliness']['facilities'])):
activity = 'clean'
moving = (not receiving and key not in report['queue_positions'] and not riding
and not report['paused'] and track.get('cell') != track.get('from')
and report['animation_time']-track.get('at', 0) < .42)
mode = 'idle'
if is_staff and person['state'] == 'INCAPACITATED':
mode = 'collapsed'
elif receiving and activity in ('standard_room', 'premium_room'):
mode = 'standard_room_sleep' if activity == 'standard_room' else 'sleep'
elif moving:
mode = 'carry_walk' if carrying else 'walk'
elif carrying:
mode = 'carry'
elif is_staff and person['state'] == 'WORKING' and not riding:
mode = activity if activity in ('clean', 'idle', 'work') else 'work'
elif receiving and activity in ('cafeteria', 'tavern'):
mode = 'eat'
elif key in report['queue_positions'] or (not is_staff and person['state'] == 'QUEUING'):
mode = 'wait'
states = enum_names(ROOT, 'scripts/staff/staff_runtime.gd' if is_staff else 'scripts/guest/guest_runtime.gd', 'State')
row = dict(key=key, center=center, at=rounded, scale=scale, cell=cell, mode=mode,
frame=frame, variant=variant, flip=track.get('direction', 1) < 0,
staff=is_staff, state=states.index(person['state']), receiving=receiving,
carrying=carrying, riding=riding)
members.append(dict(row=row, bounds=[region[0]+p, region[1]+p, region[2]-2*p, region[3]-2*p], transit=transit))
fitted, aggregations = fitted_actor_layout(members, p, height)
close(report.get('actor_aggregations'), aggregations, 'actor aggregation/no-fit')
require([r['key'] for r in report['actor_drawings']] == [k for k in floor_keys if k in fitted], 'actor drawing coverage/order')
actual = list(report['actor_drawings']) + [r for c in report['cabins'] for r in c['passengers']]
require(len(actual) == len(fitted) and {r['key'] for r in actual} == set(fitted), 'missing or duplicate actor')
for row in actual:
for field, value in fitted[row['key']].items():
close(row[field], value, 'actor fitted '+field)
exemptions = {m['row']['key'] for m in members if m['transit']}
validate_actor_nonoverlap(report, transit_keys=exemptions)
return ['actor_geometry', 'passenger_geometry']
@@ -1270,6 +1583,20 @@ def compare_layouts(a, b):
return dict(metric=declaration['metric'], A=metrics[0], B=metrics[1])
def drawing_context(stream, capture, report):
"""Shared OS/offline binding: a missing stream never grants exemptions."""
require(isinstance(capture, dict) and all(k in capture for k in ('report', 'state', 'rest_audit')),
'missing raw drawing context')
close(read_object(stream, capture['report']), report, 'raw-state report binding')
state = read_object(stream, capture['state'])
rest = read_object(stream, capture['rest_audit'])
require(isinstance(state, dict) and isinstance(state.get('systems', {}).get('runtime'), dict),
'malformed raw state context')
require(isinstance(rest, dict) and isinstance(rest.get('staff'), list)
and 'path_service_sha256' in rest, 'malformed rest authority')
return state, rest
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')
@@ -1458,7 +1785,7 @@ def inspect_os_requests(directory, layout, role, pid, root=ROOT):
require(request['ctrl'] is False and any(e.get('ctrl') is False and e.get('pressed') for e in observed['events']), 'ordinary wheel evidence')
report = decode(request['report'])
capture = captures[request_path.parent.name]
close(read_object(stream, capture['report']), report, 'request draw report binding')
state, rest_audit = drawing_context(stream, capture, report)
for number, shot in enumerate(cua['screenshots']):
matches = [b for b in bindings if b['request'] == str(request_path.relative_to(directory)) and b['screenshot_index'] == number]
require(len(matches) == 1, 'screenshot binding coverage')
@@ -1485,7 +1812,8 @@ def inspect_os_requests(directory, layout, role, pid, root=ROOT):
if request['action'] == 'screenshot':
clicks = [document(json.loads(path(name).read_bytes())['input_receipt'])
for name in listing['requests']]
frame = frame_rows(report, decode(request['rectangles']), request, cua, [image], clicks, root)
frame = frame_rows(report, decode(request['rectangles']), request, cua, [image], clicks, root,
state, rest_audit)
review_dir = request_path.parent/('review-image-' + str(number))
manifest, manifest_sha = write_frame_review(review_dir, frame, [dict(
whole_image_sha256=binding['png_sha256'], raw_sha256=shot['raw_sha256'],
+727 -3
View File
@@ -18,6 +18,165 @@ sys.dont_write_bytecode = True
import check_p23_life_observation_evidence as check
class ActorNonoverlapTests(unittest.TestCase):
def report(self, second_at):
return dict(actor_drawings=[
dict(key='guest/a', at=[0, 0], scale=1, mode='idle', frame=0, flip=False),
dict(key='staff/b', at=second_at, scale=1, mode='idle', frame=0, flip=False)], cabins=[])
def test_edge_contact_is_not_overlap(self):
for at in ([6, 0], [0, 12], [-6, 0], [0, -12]):
with self.subTest(at=at):
check.validate_actor_nonoverlap(self.report(at))
def test_overlap_ignores_asserted_verdicts_and_offscreen_labels(self):
report = self.report([5, 11])
for actor in report['actor_drawings']:
actor.update(mechanical_pass=True, eligibility='offscreen', rect=[999, 999, 1, 1])
with self.assertRaisesRegex(ValueError, 'actor-actor core overlap: guest/a / staff/b'):
check.validate_actor_nonoverlap(report)
def test_cabin_passenger_is_checked_against_floor_actor(self):
report = self.report([0, 0])
report['cabins'] = [dict(passengers=[report['actor_drawings'].pop()])]
with self.assertRaisesRegex(ValueError, 'actor-actor core overlap'):
check.validate_actor_nonoverlap(report)
def test_invalid_footprints_are_not_silently_skipped(self):
for scale in (0, -1, float('nan'), float('inf'), True):
report = self.report([100, 100])
report['actor_drawings'][0]['scale'] = scale
with self.subTest(scale=scale), self.assertRaisesRegex(ValueError, 'invalid actor footprint'):
check.validate_actor_nonoverlap(report)
def test_r13_false_row_11_overlaps_staff_row_29(self):
fixture = json.loads((check.ROOT / 'tests/fixtures/p23-03/r13_actor_overlap.json').read_text())
self.assertEqual(fixture['independent_judgment'], [11, False, False, False])
with self.assertRaisesRegex(ValueError,
'actor-actor core overlap: guest/guest-auto-00000041 / staff/staff_gina'):
check.validate_actor_nonoverlap(fixture['report_inputs'])
# Exercise the actual offline evidence gate, before any audit assertions.
with self.assertRaisesRegex(ValueError, 'actor-actor core overlap'):
check.validate_drawing_rows(fixture['report_inputs'], {'rows': []})
def test_overlapping_full_boxes_with_disjoint_cores_pass(self):
# Both 16x18 boxes overlap by 9x18, but their six-pixel cores do not.
check.validate_actor_nonoverlap(self.report([7, 0]))
def test_walk_bob_is_part_of_core(self):
report = self.report([0, 12])
report['actor_drawings'][1].update(mode='walk', frame=2)
with self.assertRaisesRegex(ValueError, 'actor-actor core overlap'):
check.validate_actor_nonoverlap(report)
report['actor_drawings'][1]['frame'] = 3
check.validate_actor_nonoverlap(report)
def test_flipped_core_and_lying_breathing_bounds(self):
actor = self.report([0, 0])['actor_drawings'][0]
actor.update(flip=True, scale=3)
self.assertEqual(check.actor_identity_core(actor), [12, 3, 18, 36])
actor.update(mode='standard_room_sleep', frame=2)
self.assertEqual(check.actor_identity_core(actor), [3, 30, 39, 18])
actor['frame'] = 5
self.assertEqual(check.actor_identity_core(actor), [3, 33, 39, 15])
actor.update(mode='collapsed', flip=False)
self.assertEqual(check.actor_identity_core(actor), [6, 33, 39, 15])
def test_missing_pose_is_rejected(self):
report = self.report([100, 0])
del report['actor_drawings'][0]['frame']
with self.assertRaisesRegex(ValueError, 'missing actor pose'):
check.validate_actor_nonoverlap(report)
def test_duplicate_actor_is_rejected(self):
report = self.report([100, 0])
report['actor_drawings'][1]['key'] = 'guest/a'
with self.assertRaisesRegex(ValueError, 'duplicate actor drawing'):
check.validate_actor_nonoverlap(report)
def test_forged_transit_flag_does_not_exempt_overlap(self):
report = self.report([0, 0])
report['actor_drawings'][1]['in_transit'] = True
with self.assertRaisesRegex(ValueError, 'actor-actor core overlap'):
check.validate_actor_nonoverlap(report)
def test_core_table_requires_exact_art_source(self):
with mock.patch.object(check, 'PIXEL_PEOPLE_CORE_SHA256', '0' * 64):
with self.assertRaisesRegex(ValueError, 'PixelPeople core source SHA256'):
check.validate_actor_nonoverlap(self.report([100, 0]))
def test_recorded_service_region_and_logical_cell_have_no_common_area(self):
# Diagnostic of the unresolved simultaneous-bounds requirement, not
# permission to hide these occupants or bypass containment checks.
fixture = json.loads((check.ROOT / 'tests/fixtures/p23-03/r13_service_region_gap.json').read_text())
for guest in fixture['guests']:
traffic = next(r for r in fixture['traffic'] if r['cell'] == guest['cell'])
x, y, w, h = traffic['rect']
cell = [x - 2, y - 2, w + 4, h + 4] # get_cell_rect(...).grow(-2)
pixel = cell[2] / 16
x, y, w, h = fixture['room_rects'][guest['target']]
region = [x + pixel, y + pixel, w - 2 * pixel, h - 2 * pixel]
with self.subTest(guest=guest['id']):
self.assertLessEqual(min(cell[0] + cell[2], region[0] + region[2])
- max(cell[0], region[0]), 0)
class LifeOverlayTests(unittest.TestCase):
def report(self):
return dict(observation_serial=7, overlay_consumed_serial=7,
overlay_passes=['traffic', 'actors'], overlay_child_indices=[0, 1, 2],
actor_drawings=[dict(key='guest/a', at=[20, 30], scale=3,
mode='idle', frame=0, flip=False)], cabins=[],
traffic=[dict(rect=[2, 2, 60, 92], band_rect=[9.2, 85.5, 45.6, 5],
traffic=dict(current_density=1, temporary_block=False))])
def test_front_and_back_valid_consumed_band(self):
for layer in (0, 1):
report = self.report()
report['layer'] = layer
check.validate_life_overlay(report)
def test_stale_serial_fails(self):
report = self.report()
report['overlay_consumed_serial'] = 6
with self.assertRaisesRegex(ValueError, 'stale LifeOverlay serial'):
check.validate_life_overlay(report)
def test_band_after_actors_fails(self):
report = self.report()
report['overlay_passes'].reverse()
with self.assertRaisesRegex(ValueError, 'LifeOverlay pass order'):
check.validate_life_overlay(report)
def test_overlay_below_inactive_art_fails(self):
report = self.report()
report['overlay_child_indices'] = [1, 0, 2]
with self.assertRaisesRegex(ValueError, 'LifeOverlay child order'):
check.validate_life_overlay(report)
def test_core_crossed_by_actual_band_fails(self):
report = self.report()
report['actor_drawings'][0]['at'][1] = 60
with self.assertRaisesRegex(ValueError, 'traffic band covers actor core'):
check.validate_life_overlay(report)
def test_forged_band_geometry_fails(self):
report = self.report()
report['traffic'][0]['band_rect'][1] = 50
with self.assertRaisesRegex(ValueError, 'traffic band rectangle'):
check.validate_life_overlay(report)
def test_report_cannot_excuse_wrong_source_order(self):
source = (check.ROOT / 'scenes/main/world_preview.gd').read_text()
# Claimed pass order stays valid; actual callback source is inverted.
broken = source.replace('_draw_congestion_overlay(_life_overlay)', 'SWAP')
broken = broken.replace('_draw_actors(_life_overlay)', '_draw_congestion_overlay(_life_overlay)')
broken = broken.replace('SWAP', '_draw_actors(_life_overlay)')
with mock.patch.object(Path, 'read_text', return_value=broken):
with self.assertRaisesRegex(ValueError, 'LifeOverlay source pass order'):
check.validate_life_overlay(self.report())
class SetupBudgetTests(unittest.TestCase):
def setUp(self):
self.source = (check.ROOT / 'tests/fixtures/p23-03/observation.gd').read_text()
@@ -452,6 +611,83 @@ class SemanticTests(unittest.TestCase):
self.assertEqual(before, self.snapshot)
class PackingTests(unittest.TestCase):
def test_scale_trial_rebuilds_vertical_groups(self):
members = self.members(2)
for member, y in zip(members, (118, 144)):
member['row'].update(center=[108, y], at=[84, y-15])
member['bounds'] = [80, 74, 20, 130]
# At p=3 the vertical intervals intersect; at p=2 they separate.
# Keeping the stale component would incorrectly force both down to p=1.
rows, badges = check.fitted_actor_layout(members, 4, 96)
self.assertEqual(badges, [])
self.assertEqual([rows['guest/'+str(i)]['scale'] for i in range(2)], [2, 2])
self.assertEqual([rows['guest/'+str(i)]['at'] for i in range(2)], [[76, 120], [76, 146]])
check.validate_actor_nonoverlap(dict(actor_drawings=list(rows.values()), cabins=[]))
def members(self, count=4):
# R13 cell dimensions and the actual _actor_offset sequence; no transit.
offsets = [0, 16, -16, 8, -8, 24, -24]
return [dict(row=dict(key='guest/'+str(i), center=[108+offsets[i % 7], 118-4*(i % 2)],
at=[84+offsets[i % 7], 103-4*(i % 2)], scale=3,
mode='idle', frame=0, flip=False, state=3, cell=[1, 1, 0]),
bounds=[80, 74, 56, 88], transit=False) for i in range(count)]
def test_four_movers_fit_at_two_not_three(self):
rows, badges = check.fitted_actor_layout(self.members(), 4, 96)
self.assertEqual(badges, [])
self.assertEqual([rows['guest/'+str(i)]['scale'] for i in range(4)], [2]*4)
self.assertEqual([rows['guest/'+str(i)]['at'] for i in range(4)],
[[88, 120], [112, 116], [76, 120], [100, 116]])
check.validate_actor_nonoverlap(dict(actor_drawings=list(rows.values()), cabins=[]))
def test_cross_kind_group_and_passenger(self):
members = self.members()
members[1]['row']['key'] = 'staff/worker'
members[2]['row']['key'] = 'guest/passenger'
members[2]['row']['scale'] = 2
members[2]['row']['at'] = [76, 120]
rows, badges = check.fitted_actor_layout(members, 4, 96)
self.assertEqual(badges, [])
self.assertEqual(len(rows), 4)
check.validate_actor_nonoverlap(dict(actor_drawings=list(rows.values()), cabins=[]))
def test_saturation_keeps_all_identities(self):
rows, badges = check.fitted_actor_layout(self.members(12), 4, 96)
self.assertEqual(len(rows), 9)
self.assertEqual(len(badges), 1)
self.assertEqual(len(badges[0]['members']), 12)
self.assertEqual(badges[0]['text'], '+3')
self.assertEqual(set(badges[0]['drawn_keys']), set(rows))
self.assertTrue(all(r['scale'] == 1 for r in rows.values()))
check.validate_actor_nonoverlap(dict(actor_drawings=list(rows.values()), cabins=[]))
def test_each_actor_keeps_own_bounds(self):
members = self.members(2)
members[0]['bounds'] = [80, 74, 12, 88]
members[1]['bounds'] = [124, 74, 12, 88]
rows, badges = check.fitted_actor_layout(members, 4, 96)
self.assertEqual(badges, [])
self.assertEqual(check.actor_identity_core(rows['guest/0'])[0:3:2], [80, 12])
self.assertEqual(check.actor_identity_core(rows['guest/1'])[0:3:2], [124, 12])
def test_receiving_cafeteria_uses_r13_room_not_access_cell(self):
fixture = json.loads((check.ROOT/'tests/fixtures/p23-03/r13_service_region_gap.json').read_text())
guest = fixture['guests'][0]
rect = fixture['room_rects'][guest['target']]
anchor = check.room_slots(rect, 4, 'cafeteria')[0]
center = [anchor[0], anchor[1]+12-48]
member = dict(row=dict(key='guest/'+guest['id'], center=center,
at=[center[0]-24, center[1]-15], scale=3,
mode='eat', frame=0, flip=False, state=5, cell=guest['cell']),
bounds=[rect[0]+4, rect[1]+4, rect[2]-8, rect[3]-8], transit=False)
rows, badges = check.fitted_actor_layout([member], 4, 96)
self.assertFalse(badges)
core = check.actor_identity_core(next(iter(rows.values())))
self.assertGreaterEqual(core[0], 1031)
self.assertEqual(next(iter(rows.values()))['cell'], [5, 0, 0])
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),
@@ -464,11 +700,178 @@ class SpatialTests(unittest.TestCase):
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],
band_rect=[79.44, 90.4, 9.12, 3.2],
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=[]))
goods=dict(requests=[], deliveries=[]), actor_drawings=[], cabins=[],
observation_serial=1, overlay_consumed_serial=1,
overlay_passes=['traffic', 'actors'], overlay_child_indices=[0, 1, 2])
def four_movers(self):
self.runtime['world'].update(bounds=dict(min_x=1, max_x=1, min_y=1, max_y=1), walkable_cells=[[1, 1, 0]])
self.report.update(viewport=[216, 236], paused=True, animation_time=0,
actor_tracks={}, boarded={}, actor_aggregations=[])
for i, (center, at) in enumerate(zip([[108,118], [124,114], [92,118], [116,114]],
[[88,120], [112,116], [76,120], [100,116]])):
key = 'guest/'+str(i)
self.runtime['guests']['guests'].append(dict(guest_id=str(i), current_cell=[1,1,0],
current_service_id='', target_facility_id='', state='MOVING'))
self.report['actor_tracks'][key] = dict(cell=[1,1,0], **{'from':[1,1,0]}, at=0, direction=1)
hashed = 5381
for char in key:
hashed = (hashed*33+ord(char)) & 0xffffffff
self.report['actor_drawings'].append(dict(key=key, center=center, at=at, scale=2,
cell=[1,1,0], mode='idle', frame=hashed%15, variant=hashed%15, flip=False,
staff=False, state=3, receiving=False, carrying=False, riding=False))
def test_four_movers_complete_geometry(self):
self.four_movers()
check.compare_actor_geometry(self.snapshot, self.report)
def moving_four(self):
self.four_movers()
self.report.update(paused=False, animation_time=.21, badges=[])
for row in self.report['actor_drawings']:
self.report['actor_tracks'][row['key']]['from'] = [0, 1, 0]
row['center'][0] -= 32
row.update(scale=3, mode='walk', frame=row['variant']+2)
row['at'] = [row['center'][0]-24, row['center'][1]-15]
def drawing_audit(self):
rows = []
for category, identifier, rect, source, text in check.drawing_rectangle_rows(self.report):
self.assertIsNone(text)
colors = list(dict.fromkeys(check.re.findall(r'Color\("([0-9a-fA-F]{6})"',
(check.ROOT/source).read_text())))
rows.append(dict(category=category, id=identifier, rect=rect,
style_source=source, style_colors=colors))
return dict(rows=rows)
def test_transit_raw_state_in_full_and_standalone_paths(self):
self.moving_four()
check.compare_actor_geometry(self.snapshot, self.report)
check.validate_drawing_rows(self.report, self.drawing_audit(), state=self.snapshot)
# Without authoritative context the overlapping moving cores fail closed.
with self.assertRaisesRegex(ValueError, 'core overlap'):
check.validate_drawing_rows(self.report, self.drawing_audit())
def test_transit_cannot_be_claimed_by_flags_or_invalid_tracks(self):
self.moving_four()
mutations = {
'paused': lambda r: r.update(paused=True),
'expired': lambda r: r.update(animation_time=.42),
'nonboolean_pause': lambda r: r.update(paused=0),
'future': lambda r: r['actor_tracks']['guest/0'].update(at=1),
'nonfinite': lambda r: r.update(animation_time=float('nan')),
'wrong_endpoint': lambda r: r['actor_tracks']['guest/0'].update(cell=[0, 1, 0]),
'teleport_origin': lambda r: r['actor_tracks']['guest/0'].update({'from': [-9, 1, 0]}),
}
for name, mutate in mutations.items():
report = copy.deepcopy(self.report)
mutate(report)
for row in report['actor_drawings']:
row.update(in_transit=True, region='transit')
with self.subTest(name=name), self.assertRaises(ValueError):
check.validate_drawing_rows(report, self.drawing_audit(), state=self.snapshot)
def test_staff_and_guests_share_complete_geometry_front_and_back(self):
self.four_movers()
self.runtime['guests']['guests'].pop()
self.runtime['staff']['staff'] = [dict(staff_id='3', current_cell=[1, 1, 0],
state='AVAILABLE', assigned_facility_id='', carried_inventory_id='', role_id='guard')]
self.report['actor_tracks']['staff/3'] = self.report['actor_tracks'].pop('guest/3')
row = self.report['actor_drawings'][-1]
hashed = 5381
for char in 'staff/3':
hashed = (hashed*33+ord(char)) & 0xffffffff
row.update(key='staff/3', staff=True, state=2, variant=hashed%15, frame=hashed%15)
for layer in (0, 1):
self.report['layer'] = layer
for member in self.runtime['guests']['guests'] + self.runtime['staff']['staff']:
member['current_cell'][2] = layer
for track in self.report['actor_tracks'].values():
track['cell'][2] = layer
track['from'][2] = layer
for drawing in self.report['actor_drawings']:
drawing['cell'][2] = layer
with self.subTest(layer=layer):
check.compare_actor_geometry(self.snapshot, self.report)
def test_spoofed_region_does_not_allow_core_outside_own_cell(self):
self.four_movers()
self.report['actor_drawings'][0].update(region='facility', in_transit=True, at=[500, 120])
with self.assertRaises(ValueError):
check.compare_actor_geometry(self.snapshot, self.report)
def test_aggregation_badge_is_required_in_drawing_audit(self):
self.four_movers()
self.report['badges'] = []
audit = self.drawing_audit()
self.report['actor_aggregations'] = [dict(at=[80, 74], text='+1',
members=[], drawn_keys=[])]
# This low-level test covers badge provenance only. Full geometry already
# rejects the unnecessary aggregate in test_fitted_geometry_rejects_tampering.
with self.assertRaisesRegex(ValueError, 'offline rectangle coverage'):
check.validate_drawing_rows(self.report, audit)
def test_low_world_pixel_complete_geometry_keeps_fractional_base(self):
self.four_movers()
self.runtime['guests']['guests'] = self.runtime['guests']['guests'][:1]
self.report.update(viewport=[156, 146], actor_drawings=self.report['actor_drawings'][:1])
self.report['actor_drawings'][0].update(center=[78, 73], at=[77, 72], scale=.1875)
check.compare_actor_geometry(self.snapshot, self.report)
def test_saturated_complete_geometry_keeps_every_identity(self):
self.four_movers()
prototype = copy.deepcopy(self.report['actor_drawings'][0])
self.runtime['guests']['guests'] = []
self.report['actor_drawings'] = []
# 10 upright cores need 60px at p=1, but the interval is only 56px.
# The first nine in preferred-x/key order occupy x82..136; guest/5
# cannot be retained. This expected layout is independent of the solver.
order = [6, 2, 9, 4, 0, 7, 3, 1, 8, 5]
identities = {}
for i in range(10):
key = 'guest/'+str(i)
cell = [1, 1, 0]
self.runtime['guests']['guests'].append(dict(guest_id=str(i), current_cell=cell,
current_service_id='', target_facility_id='', state='MOVING'))
self.report['actor_tracks'][key] = dict(cell=cell, **{'from': cell}, at=0, direction=1)
identities[i] = dict(key=key, id=str(i), cell=cell, state=3, mode='idle')
if i == 5:
continue
hashed = 5381
for char in key:
hashed = (hashed*33+ord(char)) & 0xffffffff
center = [108+[0,16,-16,8,-8,24,-24][i%7], 118-4*(i%2)]
self.report['actor_drawings'].append(dict(prototype, key=key, center=center,
at=[76+6*order.index(i), center[1]+19], scale=1, frame=hashed%15, variant=hashed%15))
self.report['actor_aggregations'] = [dict(members=[identities[i] for i in order],
drawn_keys=['guest/'+str(i) for i in order[:-1]], at=[80,74], text='+1')]
check.compare_actor_geometry(self.snapshot, self.report)
self.report['actor_aggregations'][0]['members'].pop()
with self.assertRaisesRegex(ValueError, 'aggregation/no-fit'):
check.compare_actor_geometry(self.snapshot, self.report)
def test_fitted_geometry_rejects_tampering(self):
self.four_movers()
for field, value in [('cell', [0,1,0]), ('state', 4), ('scale', 3), ('at', [999,120])]:
report = copy.deepcopy(self.report)
report['actor_drawings'][0][field] = value
with self.subTest(field=field), self.assertRaises(ValueError):
check.compare_actor_geometry(self.snapshot, report)
for mutation in ('missing', 'duplicate', 'aggregate'):
report = copy.deepcopy(self.report)
if mutation == 'missing':
report['actor_drawings'].pop()
elif mutation == 'duplicate':
report['actor_drawings'].append(copy.deepcopy(report['actor_drawings'][0]))
else:
report['actor_aggregations'] = [dict(drawn_keys=[], text='+4')]
with self.subTest(mutation=mutation), self.assertRaises(ValueError):
check.compare_actor_geometry(self.snapshot, report)
def test_density_and_pan_are_independently_recomputed(self):
check.compare_traffic(self.snapshot, self.report)
@@ -537,6 +940,247 @@ class SpatialTests(unittest.TestCase):
check.compare_cabin_geometry(self.snapshot, changed)
class AnchoredGeometryTests(unittest.TestCase):
"""Whole state classification fixtures, with independently chosen draw rows."""
def setUp(self):
SpatialTests.setUp(self)
self.runtime['world'].update(bounds=dict(min_x=0, max_x=3, min_y=0, max_y=1),
walkable_cells=[[x, y, 0] for y in range(2) for x in range(4)])
self.report.update(viewport=[408, 332], paused=True, animation_time=0,
actor_tracks={}, boarded={}, actor_aggregations=[], badges=[], traffic=[])
self.runtime.update(defense=dict(raids=[]), events=dict(actor_locks={}))
def person(self, key, cell, state='MOVING', **fields):
kind, identifier = key.split('/')
if kind == 'guest':
person = dict(guest_id=identifier, current_cell=cell, state=state,
current_service_id='', target_facility_id='')
self.runtime['guests']['guests'].append(person)
else:
person = dict(staff_id=identifier, current_cell=cell, state=state, health=100,
assigned_facility_id='', current_task_id='', carried_inventory_id='', role_id='guard')
self.runtime['staff']['staff'].append(person)
person.update(fields)
self.report['actor_tracks'][key] = dict(cell=cell[:], **{'from': cell[:]}, at=0, direction=1)
return person
def drawing(self, key, center, mode='idle', scale=3, receiving=False, riding=False, at=None):
kind, identifier = key.split('/')
source = self.runtime['staff']['staff'] if kind == 'staff' else self.runtime['guests']['guests']
person = next(r for r in source if r[kind+'_id'] == identifier)
hashed = 5381
for char in key:
hashed = (hashed*33+ord(char)) & 0xffffffff
row = dict(key=key, center=center, at=at or [center[0]-8*scale, center[1]+36-17*scale],
scale=scale, mode=mode, frame=hashed%15, variant=hashed%15, flip=False,
staff=kind == 'staff', state=check.enum_names(check.ROOT,
'scripts/'+kind+'/'+kind+'_runtime.gd', 'State').index(person['state']),
cell=person['current_cell'][:], receiving=receiving, carrying=False, riding=riding)
if not riding:
self.report['actor_drawings'].append(row)
return row
def room(self, fid, definition, cells, users=(), workers=()):
facility = dict(instance_id=fid, definition_id=definition, origin=cells[0], occupied_cells=cells,
state='OPEN', state_reasons=[], suspension_reason='', customer_capacity=10,
active_services=[dict(guest_ids=list(users))] if users else [], active_staff_ids=list(workers), queue_entries=[])
self.runtime['facilities']['instances'].append(facility)
self.runtime['world']['occupancies'].append(dict(entity_id=fid, cells=cells))
self.report['rooms'][fid] = dict(people=list(users), count=len(users), staff=list(workers),
state=check.enum_names(check.ROOT, 'scripts/facility/facility_instance.gd', 'State').index('OPEN'),
state_name='OPEN', reasons=[], operation=1, limit=10, suspension='', definition_id=definition,
rect=check.Transform(self.runtime, self.report).room(cells))
return facility
def queue_work(self):
facility = self.room('work', 'cafeteria', [[x,y,0] for y in (0,1) for x in (2,3)], workers=['w'])
for key in ('a', 'b'):
self.person('guest/'+key, [1,1,0], 'QUEUING', current_service_id='service', target_facility_id='work')
# Queue takes precedence even over a nonempty current_service_id.
facility['active_services'] = [dict(guest_ids=['a','b'])]
self.report['rooms']['work'].update(people=['a','b'], count=2)
facility['queue_entries'] = ['request']
self.runtime['facilities']['requests'] = [dict(request_id='request', guest_ids=['a','b'])]
self.person('staff/w', [2,1,0], 'WORKING', assigned_facility_id='work')
self.report['queue_positions'] = {'guest/a':[172,118], 'guest/b':[108,118]}
self.report['landing_badges'] = [dict(at=[140,54], text='待ち2人', cell=[1,1,0],
members=['guest/a','guest/b'], visible_count=2, overflow=0)]
self.drawing('guest/a', [172,118], 'eat', receiving=True)
self.drawing('guest/b', [108,118], 'eat', receiving=True)
self.drawing('staff/w', [236,126], 'work')
def test_queue_lane_and_work_staff(self):
self.queue_work()
check.compare_actor_geometry(self.snapshot, self.report)
def test_queue_precedence_order_and_category_forgeries(self):
self.queue_work()
for mutation in ('precedence', 'order', 'category', 'feet', 'pose'):
report = copy.deepcopy(self.report)
if mutation == 'precedence': report['actor_drawings'][0].update(center=[268,210], at=[244,195])
if mutation == 'order':
report['queue_positions']['guest/a'], report['queue_positions']['guest/b'] = report['queue_positions']['guest/b'], report['queue_positions']['guest/a']
if mutation == 'category': report['actor_drawings'][0].update(staff=True, region='work', in_transit=True)
if mutation == 'feet': report['actor_drawings'][0]['at'][1] -= 17
if mutation == 'pose': report['actor_drawings'][0]['mode'] = 'wait'
with self.subTest(mutation=mutation), self.assertRaises(ValueError):
check.compare_actor_geometry(self.snapshot, report)
def test_receiving_r13_guest_cannot_be_forced_into_access_cell(self):
fixture = json.loads((check.ROOT/'tests/fixtures/p23-03/r13_service_region_gap.json').read_text())
guest = fixture['guests'][0]
self.runtime['world']['bounds'].update(max_x=9)
self.report.update(viewport=[792,332], pan=[567,318])
fid = guest['target']
self.room(fid, 'cafeteria', [[x,y,0] for y in (0,1) for x in range(6,10)], users=[guest['id']])
self.person('guest/'+guest['id'], guest['cell'], 'RECEIVING_SERVICE', current_service_id='service', target_facility_id=fid)
# Exact historical room x1027..1283; access cell x963..1027.
self.assertEqual(self.report['rooms'][fid]['rect'], fixture['room_rects'][fid])
row = self.drawing('guest/'+guest['id'], [1069+2/3,528], 'eat', receiving=True, at=[1046,513])
check.compare_actor_geometry(self.snapshot, self.report)
row.update(center=[995,532], at=[971,517], region='floor')
with self.assertRaises(ValueError): check.compare_actor_geometry(self.snapshot, self.report)
def test_carried_patient_and_treated_staff(self):
self.person('staff/carrier', [0,0,0], 'AVAILABLE')
self.person('staff/patient', [0,0,0], 'INCAPACITATED')
self.person('staff/treated', [2,0,0], 'INCAPACITATED')
self.room('clinic', 'infirmary', [[2,0,0]])
self.runtime['staff']['tasks'] = [dict(task_type='rescue_transport', state=phase,
assigned_staff_id='carrier', target_entity_id=patient, rescue_facility_id='clinic')
for patient, phase in [('patient','MOVING'), ('treated','WORKING')]]
self.report.update(carried_patients={'patient':'carrier'}, treated_patients={'treated':'clinic'})
self.report['rooms']['clinic'].update(people=['treated'], count=1)
self.drawing('staff/carrier', [108,214])
self.drawing('staff/patient', [108,166], 'collapsed')
row = self.drawing('staff/treated', [236,190], 'collapsed')
check.compare_actor_geometry(self.snapshot, self.report)
self.report['actor_drawings'][1]['center'][1] += 4
self.report['actor_drawings'][1]['at'][1] += 4
with self.assertRaises(ValueError): check.compare_actor_geometry(self.snapshot, self.report)
def cabin(self, count):
shaft = json.loads((check.ROOT/'tests/fixtures/p23-03/queued_elevator.json').read_text())['saved']['shafts'][0]
shaft.update(current_floor=0, phase='IDLE', boarded=['guest:p%02d'%i for i in range(count)])
# Synthetic saved configuration, not a production definition or capacity edit.
shaft['config']['capacity'] = max(4, count)
self.runtime['elevators']['shafts'] = [shaft]
sid = shaft['config']['shaft_id']
self.report['elevator_tracks'] = {sid: dict(floor=0, **{'from':0}, at=0)}
cabin = dict(shaft_id=sid, rect=[92,174,96,76], pixel=4, operational=True, doors_open=True,
badge_at=[92,160], badge=('満員 ' if count >= 4 else '')+f'{count}/{max(4,count)}人', passengers=[])
self.report['cabins'] = [cabin]
for i in range(count):
key = 'guest/p%02d'%i
self.person(key, [0,0,0])
self.report['boarded'][key] = True
cabin['passengers'].append(self.drawing(key, [116+32*(i%2),206-36*((i//2)%2)], scale=2, riding=True))
return cabin
def occupied_rest(self):
state, _, audit = RestAuditTests().fixture()
old = state['systems']['runtime']
self.runtime['path'] = old['path']
self.person('staff/s', [0,0,0], 'OFF_DUTY')
self.room('a', 'staff_waiting_room', [[0,0,0]])
self.room('b', 'staff_waiting_room', [[0,0,0]])
for facility in self.runtime['facilities']['instances']:
facility['occupied_cells'] = [] # Public selector falls back to origin.
self.report['resting_staff'] = {'s':'a'}
self.report['rooms']['a'].update(people=['s'], count=1)
self.drawing('staff/s', [108,198])
return audit
def test_compare_rest_with_actual_occupied_facility(self):
audit = self.occupied_rest()
self.assertTrue(check.compare_rest(self.snapshot, self.report, audit)['s']['resting'])
self.report['actor_drawings'][0]['at'][1] += 1
with self.assertRaises(ValueError): check.compare_rest(self.snapshot, self.report, audit)
def test_compare_rest_independently_proves_no_fit(self):
audit = self.occupied_rest()
rest = self.report['actor_drawings'].pop()
rest.update(scale=1, at=[100,217])
order = [6,2,9,4,0,7,3,1,8,5]
identities = {}
for i in range(10):
key = 'guest/'+str(i)
self.person(key, [0,0,0])
center = [108+[0,16,-16,8,-8,24,-24][i%7],214-4*(i%2)]
row = self.drawing(key, center, scale=1, at=[76+6*order.index(i),center[1]+19])
identities[i] = dict(key=key, id=str(i), state=3, cell=[0,0,0], mode='idle')
self.report['actor_drawings'] = [r for r in self.report['actor_drawings'] if r['key'] != 'guest/5']+[rest]
self.report['actor_aggregations'] = [dict(members=[identities[i] for i in order],
drawn_keys=['guest/'+str(i) for i in order[:9]], at=[80,170], text='+1')]
check.compare_rest(self.snapshot, self.report, audit)
stale = copy.deepcopy(self.report)
stale['actor_aggregations'][0]['members'].append(dict(key='staff/s', id='s', state=0, cell=[0,0,0], mode='idle'))
with self.assertRaises(ValueError): check.compare_rest(self.snapshot, stale, audit)
self.report['actor_aggregations'][0]['members'].pop()
with self.assertRaises(ValueError): check.compare_rest(self.snapshot, self.report, audit)
def test_compare_rest_with_queued_staff(self):
audit = self.occupied_rest()
self.person('staff/q', [0,0,0], 'OFF_DUTY')
saved = json.loads((check.ROOT/'tests/fixtures/p23-03/queued_elevator.json').read_text())['saved']
saved['trips'][0]['actor_id'] = 'staff:q'
self.runtime['elevators'] = saved
self.runtime['world']['walkable_cells'].append([-1,0,0])
self.report['queue_positions'] = {'staff/q':[44,214]}
self.report['landing_badges'] = [dict(at=[-4,150], text='待ち1人', cell=[0,0,0],
members=['staff/q'], visible_count=1, overflow=0)]
queued = copy.deepcopy(audit['staff'][0])
queued.update(staff_id='q', in_transit=True)
queued['comparison_only'].update(resting=False, label='休憩先へ移動中')
audit['staff'].append(queued)
self.drawing('staff/q', [44,214], 'wait')
result = check.compare_rest(self.snapshot, self.report, audit)
self.assertTrue(result['s']['resting'])
self.assertFalse(result['q']['resting'])
self.report['actor_drawings'][-1].update(center=[108,198], at=[84,183])
with self.assertRaises(ValueError): check.compare_rest(self.snapshot, self.report, audit)
def test_passenger_and_floor_actor_front_and_back(self):
cabin = self.cabin(1)
self.person('guest/floor', [0,0,0])
self.drawing('guest/floor', [108,214])
cabin['passengers'][0]['at'][0] = 108
for layer in (0,1):
self.report['layer'] = layer
for guest in self.runtime['guests']['guests']: guest['current_cell'][2] = layer
for cell in self.runtime['elevators']['shafts'][0]['config']['stops']: cell[2] = layer
for row in self.report['actor_drawings']+cabin['passengers']: row['cell'][2] = layer
for track in self.report['actor_tracks'].values():
track['cell'][2] = track['from'][2] = layer
with self.subTest(layer=layer):
check.compare_cabin_geometry(self.snapshot, self.report)
check.compare_actor_geometry(self.snapshot, self.report)
def test_cabin_aggregation_retains_full_boarded_membership(self):
cabin = self.cabin(40)
originals = copy.deepcopy(cabin['passengers'])
retained = {}
for y in (170,206):
group = sorted([r for r in originals if r['center'][1] == y], key=lambda r:(r['center'][0],r['key']))
drawn = group[:14] # 88px holds fourteen 6px cores, never fifteen.
for i, row in enumerate(drawn):
row.update(scale=1, at=[94+6*i,y+19])
retained[row['key']] = row
self.report['actor_aggregations'].append(dict(
members=[dict(key=r['key'], id=r['key'][6:], state=r['state'], cell=r['cell'], mode=r['mode']) for r in group],
drawn_keys=[r['key'] for r in drawn], at=[96,178], text='+6'))
cabin['passengers'] = [retained[r['key']] for r in originals if r['key'] in retained]
check.compare_cabin_geometry(self.snapshot, self.report)
check.compare_actor_geometry(self.snapshot, self.report)
self.assertEqual(sum(len(a['members']) for a in self.report['actor_aggregations']), 40)
for mutation in ('lost_identity','badge'):
report = copy.deepcopy(self.report)
if mutation == 'lost_identity': report['actor_aggregations'][0]['members'].pop()
else: report['actor_aggregations'][0]['at'][0] += 1
with self.subTest(mutation=mutation), self.assertRaises(ValueError):
check.compare_actor_geometry(self.snapshot, report)
class ComparisonTests(unittest.TestCase):
def pair(self):
declaration = dict(metric='sum_destination_arrival_minutes_entire_cohort',
@@ -647,7 +1291,8 @@ class OfflineGeometryTests(unittest.TestCase):
def test_report_positions_and_source_provenance_are_recomputed(self):
source = 'scripts/ui/pixel_people.gd'
report = dict(rooms={}, landing_badges=[], cabins=[], resting_staff={}, badges=[],
actor_drawings=[dict(key='staff/s', staff=True, at=[1, 1], scale=1)])
actor_drawings=[dict(key='staff/s', staff=True, at=[1, 1], scale=1,
mode='idle', frame=0, flip=False)])
colors = list(dict.fromkeys(check.re.findall(r'Color\("([0-9a-fA-F]{6})"',
(check.ROOT/source).read_text())))
audit = dict(rows=[dict(category='staff', id='staff/s', rect=[1, 1, 16, 18],
@@ -664,6 +1309,79 @@ class OfflineGeometryTests(unittest.TestCase):
class FrameReviewTests(unittest.TestCase):
def test_frame_raw_context_valid_malformed_and_missing_rest(self):
fixture = SpatialTests()
fixture.setUp()
fixture.moving_four()
fixture.report.update(viewport=[216,236], traffic=[])
audit = fixture.drawing_audit()
for row in audit['rows']:
row.update(viewport_rect=row['rect'][:], screen_rect=row['rect'][:])
for context, rest, passes in ((fixture.snapshot,None,True), ({},None,False)):
result = check.frame_rows(fixture.report, audit, self.request, dict(calibration=self.calibration),
[self.image], self.clicks, state=context, rest_audit=rest)
self.assertEqual(not result['errors'], passes)
fixture.report['resting_staff'] = {'s':'a'}
for rest in (None, [], {}):
result = check.frame_rows(fixture.report, audit, self.request, dict(calibration=self.calibration),
[self.image], self.clicks, state=fixture.snapshot, rest_audit=rest)
self.assertFalse(result['mechanical_pass'])
fixture.report['resting_staff'] = {}
result = check.frame_rows(fixture.report, audit, self.request, dict(calibration=self.calibration),
[self.image], self.clicks)
self.assertFalse(result['mechanical_pass'])
self.assertIn('core overlap', str(result['errors']))
def test_frame_with_valid_occupied_rest_authority(self):
fixture = AnchoredGeometryTests()
fixture.setUp()
rest = fixture.occupied_rest()
rows = []
for category, identifier, rect, source, text in check.drawing_rectangle_rows(fixture.report):
self.assertIsNone(text)
colors = list(dict.fromkeys(check.re.findall(r'Color\("([0-9a-fA-F]{6})"', (check.ROOT/source).read_text())))
rows.append(dict(category=category, id=identifier, rect=rect, viewport_rect=rect[:],
screen_rect=rect[:], style_source=source, style_colors=colors))
result = check.frame_rows(fixture.report, dict(rows=rows), self.request, dict(calibration=self.calibration),
[self.image], self.clicks, state=fixture.snapshot, rest_audit=rest)
self.assertEqual(result['errors'], [])
rest['path_service_sha256'] = '0'*64
result = check.frame_rows(fixture.report, dict(rows=rows), self.request, dict(calibration=self.calibration),
[self.image], self.clicks, state=fixture.snapshot, rest_audit=rest)
self.assertFalse(result['mechanical_pass'])
self.assertIn('rest PathService source pin', result['errors'])
def test_os_offline_raw_context_binding_and_hashes(self):
def typed(value):
if isinstance(value, dict): return dict(type='Dictionary', entries=[[typed(k),typed(v)] for k,v in value.items()])
if isinstance(value, list): return dict(type='Array', items=[typed(v) for v in value])
return dict(type={str:'String',int:'int'}[type(value)], text=json.dumps(value))
with tempfile.TemporaryDirectory() as temporary:
stream = Path(temporary)
(stream/'objects').mkdir()
def put(value):
raw = json.dumps(typed(value)).encode()
digest = hashlib.sha256(raw).hexdigest()
(stream/'objects'/(digest+'.json')).write_bytes(raw)
return digest
report = {'actor_drawings':[]}
state = {'systems':{'runtime':{}}}
rest = {'staff':[], 'path_service_sha256':check.sha(check.ROOT/'scripts/world/path_service.gd')}
capture = dict(report=put(report), state=put(state), rest_audit=put(rest))
self.assertEqual(check.drawing_context(stream, capture, report), (state,rest))
for field in ('state','rest_audit','report'):
missing = dict(capture)
del missing[field]
with self.assertRaises(ValueError): check.drawing_context(stream, missing, report)
for field in ('state','rest_audit'):
bad = dict(capture, **{field:put({})})
with self.assertRaises(ValueError): check.drawing_context(stream, bad, report)
with self.assertRaises(ValueError): check.drawing_context(stream, capture, {'wrong':1})
with self.assertRaises(FileNotFoundError):
check.drawing_context(stream, dict(capture, state='0'*64), report)
(stream/'objects'/(capture['state']+'.json')).write_text('{}')
with self.assertRaisesRegex(ValueError, 'object hash'): check.drawing_context(stream, capture, report)
def setUp(self):
OfflineGeometryTests.setUp(self)
self.request.update(local_to_viewport=[1, 0, 0, 1, 0, 0],
@@ -671,7 +1389,8 @@ class FrameReviewTests(unittest.TestCase):
local_to_screen=[1, 0, 0, 1, 0, 0])
self.report = dict(layer=0, viewport=[100, 80], rooms={}, cabins=[],
landing_badges=[], badges=[], resting_staff={}, traffic=[],
actor_drawings=[dict(key='staff/s', staff=True, at=[1, 1], scale=1, state=0)])
actor_drawings=[dict(key='staff/s', staff=True, at=[1, 1], scale=1, state=0,
mode='idle', frame=0, flip=False)])
source = 'scripts/ui/pixel_people.gd'
colors = list(dict.fromkeys(check.re.findall(r'Color\("([0-9a-fA-F]{6})"',
(check.ROOT/source).read_text())))
@@ -684,6 +1403,11 @@ class FrameReviewTests(unittest.TestCase):
return check.frame_rows(self.report, self.audit, self.request,
dict(calibration=self.calibration), [self.image], self.clicks)
def test_frame_without_raw_state_explicitly_denies_transit_exemptions(self):
self.report['actor_drawings'][0]['in_transit'] = True
self.assertEqual(self.frame()['transit_authority'],
'absent: fail closed, no transit exemptions')
def test_blank_crop_fails_without_threshold(self):
self.image = (200, 190, [[(0, 0, 0)] * 200 for _ in range(190)])
result = self.frame()