P23-03: 生活の観察
This commit is contained in:
@@ -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
@@ -79,6 +79,8 @@ var _queue_hidden: Dictionary = {}
|
|||||||
var _landing_badges: Array[Dictionary] = []
|
var _landing_badges: Array[Dictionary] = []
|
||||||
var _transport_report: Dictionary = {}
|
var _transport_report: Dictionary = {}
|
||||||
var _life_observation: Dictionary = {}
|
var _life_observation: Dictionary = {}
|
||||||
|
var _life_overlay: Node2D
|
||||||
|
var _life_observation_serial := 0
|
||||||
var _dragging := false
|
var _dragging := false
|
||||||
var _drag_button := MOUSE_BUTTON_NONE
|
var _drag_button := MOUSE_BUTTON_NONE
|
||||||
|
|
||||||
@@ -104,6 +106,10 @@ func _ready() -> void:
|
|||||||
_inactive_layer_art = InactiveLayerRooms.new()
|
_inactive_layer_art = InactiveLayerRooms.new()
|
||||||
_inactive_layer_art.name = "InactiveLayerRooms"
|
_inactive_layer_art.name = "InactiveLayerRooms"
|
||||||
add_child(_inactive_layer_art)
|
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 = ColorRect.new()
|
||||||
_layer_lens.name = "LayerLens"
|
_layer_lens.name = "LayerLens"
|
||||||
_layer_lens.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
_layer_lens.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||||
@@ -513,6 +519,8 @@ func _draw() -> void:
|
|||||||
|
|
||||||
func _draw_contents() -> void:
|
func _draw_contents() -> void:
|
||||||
_life_observation = {}
|
_life_observation = {}
|
||||||
|
if _life_overlay != null:
|
||||||
|
_life_overlay.queue_redraw()
|
||||||
if _inactive_layer_art != null:
|
if _inactive_layer_art != null:
|
||||||
_inactive_layer_art.visible = false
|
_inactive_layer_art.visible = false
|
||||||
_draw_background()
|
_draw_background()
|
||||||
@@ -524,6 +532,9 @@ func _draw_contents() -> void:
|
|||||||
if map_rect.size == Vector2.ZERO:
|
if map_rect.size == Vector2.ZERO:
|
||||||
return
|
return
|
||||||
_life_observation = _build_life_observation()
|
_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"]
|
_room_presentations = _life_observation[&"rooms"]
|
||||||
_carried_patients = _life_observation[&"carried_patients"]
|
_carried_patients = _life_observation[&"carried_patients"]
|
||||||
_treated_patients = _life_observation[&"treated_patients"]
|
_treated_patients = _life_observation[&"treated_patients"]
|
||||||
@@ -540,11 +551,9 @@ func _draw_contents() -> void:
|
|||||||
_draw_legacy_stairs()
|
_draw_legacy_stairs()
|
||||||
_draw_elevators()
|
_draw_elevators()
|
||||||
_draw_other_layer()
|
_draw_other_layer()
|
||||||
_draw_congestion_overlay(bounds)
|
|
||||||
_draw_life_badges()
|
_draw_life_badges()
|
||||||
_draw_construction_labels()
|
_draw_construction_labels()
|
||||||
_draw_facility_labels()
|
_draw_facility_labels()
|
||||||
_draw_actors()
|
|
||||||
_draw_excavation_jobs()
|
_draw_excavation_jobs()
|
||||||
_draw_build_preview()
|
_draw_build_preview()
|
||||||
_draw_excavation_preview()
|
_draw_excavation_preview()
|
||||||
@@ -599,6 +608,7 @@ func _build_life_observation() -> Dictionary:
|
|||||||
model[&"guests"] = guests
|
model[&"guests"] = guests
|
||||||
model[&"staff"] = staff
|
model[&"staff"] = staff
|
||||||
model[&"tasks"] = tasks
|
model[&"tasks"] = tasks
|
||||||
|
_fit_life_actors(model)
|
||||||
var bridge: Variant = _service(&"supply_logistics")
|
var bridge: Variant = _service(&"supply_logistics")
|
||||||
var goods: Dictionary = bridge.get_report() if bridge != null else {&"requests": [], &"deliveries": []}
|
var goods: Dictionary = bridge.get_report() if bridge != null else {&"requests": [], &"deliveries": []}
|
||||||
model[&"goods"] = goods
|
model[&"goods"] = goods
|
||||||
@@ -638,7 +648,14 @@ func _build_life_observation() -> Dictionary:
|
|||||||
var cell := Vector3i(x, y, layer_mode)
|
var cell := Vector3i(x, y, layer_mode)
|
||||||
var row: Dictionary = path.get_cell_traffic(cell)
|
var row: Dictionary = path.get_cell_traffic(cell)
|
||||||
if not row.is_empty():
|
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
|
model[&"traffic"] = traffic
|
||||||
return model
|
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"))
|
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", []):
|
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 traffic: Dictionary = row[&"traffic"]
|
||||||
var density := int(traffic[&"current_density"])
|
var density := int(traffic[&"current_density"])
|
||||||
var blocked := bool(traffic[&"temporary_block"])
|
var blocked := bool(traffic[&"temporary_block"])
|
||||||
@@ -768,13 +803,11 @@ func _draw_congestion_overlay(_bounds: Dictionary) -> void:
|
|||||||
continue
|
continue
|
||||||
var ratio := float(density) / maxi(int(traffic[&"base_capacity"]), 1)
|
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 color := Color("e34332") if blocked or ratio > 1.0 else Color("e3a62f") if ratio >= 0.65 else Color("55c96b")
|
||||||
var center := rect.get_center()
|
canvas.draw_rect(band, Color(color, 0.90))
|
||||||
draw_line(Vector2(rect.position.x + rect.size.x * 0.12, center.y),
|
|
||||||
Vector2(rect.end.x - rect.size.x * 0.12, center.y),
|
|
||||||
Color(color, 0.90 if show_grid else 0.28), clampf(rect.size.y * 0.16, 2.0, 5.0))
|
|
||||||
if blocked:
|
if blocked:
|
||||||
draw_line(rect.position, rect.end, Color(color, 0.90 if show_grid else 0.28), 2.0)
|
var cross := band.grow(-1.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)
|
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
|
## 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)
|
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 width := ThemeDB.fallback_font.get_string_size(text, HORIZONTAL_ALIGNMENT_LEFT, -1, 10).x+6
|
||||||
var rect := Rect2(at, Vector2(width, 14))
|
var rect := Rect2(at, Vector2(width, 14))
|
||||||
if not rect.intersects(Rect2(Vector2.ZERO, size)):
|
if not rect.intersects(Rect2(Vector2.ZERO, size)):
|
||||||
return
|
return
|
||||||
draw_rect(rect, Color("34413b", 0.94))
|
canvas.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_string(ThemeDB.fallback_font, at+Vector2(3, 11), text, HORIZONTAL_ALIGNMENT_LEFT, -1, 10, Color("fff2ce"))
|
||||||
|
|
||||||
|
|
||||||
func _prepare_room_presentations() -> void:
|
func _prepare_room_presentations() -> void:
|
||||||
@@ -1146,8 +1181,6 @@ func _draw_facility_labels() -> void:
|
|||||||
func _draw_elevators() -> void:
|
func _draw_elevators() -> void:
|
||||||
for row: Dictionary in _life_observation.get(&"cabins", []):
|
for row: Dictionary in _life_observation.get(&"cabins", []):
|
||||||
PixelArt.elevator_cabin(self, row[&"rect"], row[&"pixel"], layer_mode == LayerMode.BACK, row[&"operational"])
|
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"])
|
PixelArt.elevator_gate(self, row[&"rect"], row[&"pixel"], row[&"doors_open"])
|
||||||
_draw_readable_badge(row[&"badge_at"], row[&"badge"])
|
_draw_readable_badge(row[&"badge_at"], row[&"badge"])
|
||||||
for badge: Dictionary in _life_observation.get(&"landing_badges", []):
|
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 doors_open := operational and phase in [&"IDLE", &"BOARDING", &"ALIGHTING"]
|
||||||
var passengers_drawn: Array[Dictionary] = []
|
var passengers_drawn: Array[Dictionary] = []
|
||||||
var seat_columns := maxi(int((cabin.size.x / p - 4.0) / 8.0), 1)
|
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(passengers.size()):
|
||||||
for index: int in range(visible_count):
|
|
||||||
var actor_key := _elevator_actor_key(String(passengers[index]))
|
var actor_key := _elevator_actor_key(String(passengers[index]))
|
||||||
var is_staff := actor_key.begins_with("staff/")
|
var is_staff := actor_key.begins_with("staff/")
|
||||||
var person: Variant = null
|
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 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 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 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)
|
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))
|
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,
|
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)
|
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", []):
|
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]:
|
func _observe_actors(observation: Dictionary) -> Array[Dictionary]:
|
||||||
@@ -1291,6 +1323,9 @@ func _observe_actors(observation: Dictionary) -> Array[Dictionary]:
|
|||||||
continue
|
continue
|
||||||
var position := _tracked_actor_position(key,staff.current_cell,observation)
|
var position := _tracked_actor_position(key,staff.current_cell,observation)
|
||||||
if staff.current_cell.z==layer_mode:
|
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):
|
if _carried_patients.has(staff.staff_id):
|
||||||
var carrier: Variant = staff_service.get_staff(_carried_patients[staff.staff_id])
|
var carrier: Variant = staff_service.get_staff(_carried_patients[staff.staff_id])
|
||||||
if carrier != null:
|
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))
|
_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 at: Vector2 = row[&"at"]
|
||||||
var p: float = row[&"scale"]
|
var p: float = row[&"scale"]
|
||||||
draw_rect(Rect2(at + Vector2(5, 17) * p, Vector2(8, 1) * p), Color("303c40", 0.2))
|
canvas.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"])
|
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:
|
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}
|
&"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:
|
func _draw_excavation_jobs() -> void:
|
||||||
var excavation: Variant = _service(&"excavation_service")
|
var excavation: Variant = _service(&"excavation_service")
|
||||||
if excavation == null or not excavation.has_method(&"get_jobs"):
|
if excavation == null or not excavation.has_method(&"get_jobs"):
|
||||||
|
|||||||
@@ -166,6 +166,9 @@ func draw_rectangles(report: Dictionary, view: WorldPreview) -> Dictionary:
|
|||||||
var rest := actor_rectangle(actor)
|
var rest := actor_rectangle(actor)
|
||||||
rest["category"] = "rest"
|
rest["category"] = "rest"
|
||||||
rows.append(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()):
|
for index: int in range(report.get(&"badges", []).size()):
|
||||||
var badge: Dictionary = report[&"badges"][index]
|
var badge: Dictionary = report[&"badges"][index]
|
||||||
rows.append(badge_rectangle("goods" if badge[&"kind"] == &"supply" else "business", str(index), badge[&"at"], badge[&"text"]))
|
rows.append(badge_rectangle("goods" if badge[&"kind"] == &"supply" else "business", str(index), badge[&"at"], badge[&"text"]))
|
||||||
|
|||||||
+15
@@ -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": []
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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]}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -56,6 +56,61 @@ func run() -> void:
|
|||||||
view.size = Vector2(1440, 810)
|
view.size = Vector2(1440, 810)
|
||||||
view.set_runtime(runtime)
|
view.set_runtime(runtime)
|
||||||
root.add_child(view)
|
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 before: PackedByteArray = var_to_bytes(runtime.create_snapshot())
|
||||||
var tracks: PackedByteArray = var_to_bytes([view._actor_tracks, view._elevator_tracks])
|
var tracks: PackedByteArray = var_to_bytes([view._actor_tracks, view._elevator_tracks])
|
||||||
var model := view._build_life_observation()
|
var model := view._build_life_observation()
|
||||||
@@ -67,6 +122,10 @@ func run() -> void:
|
|||||||
await process_frame
|
await process_frame
|
||||||
var report := view.get_life_observation_report()
|
var report := view.get_life_observation_report()
|
||||||
expect(not report.is_empty(), "actual drawing published an observation")
|
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.
|
# The getter exposes the last actual draw and must not create another model.
|
||||||
var original: PackedByteArray = var_to_bytes(report)
|
var original: PackedByteArray = var_to_bytes(report)
|
||||||
if report.has(&"traffic"):
|
if report.has(&"traffic"):
|
||||||
|
|||||||
@@ -329,6 +329,8 @@ def drawing_rectangle_rows(report):
|
|||||||
actor(a)
|
actor(a)
|
||||||
if a['staff'] and a['key'].removeprefix('staff/') in report['resting_staff']:
|
if a['staff'] and a['key'].removeprefix('staff/') in report['resting_staff']:
|
||||||
actor(a, 'rest')
|
actor(a, 'rest')
|
||||||
|
for i, b in enumerate(report.get('actor_aggregations', [])):
|
||||||
|
badge('aggregation', str(i), b)
|
||||||
for i, b in enumerate(report['badges']):
|
for i, b in enumerate(report['badges']):
|
||||||
badge('goods' if b['kind'] == 'supply' else 'business', str(i), b)
|
badge('goods' if b['kind'] == 'supply' else 'business', str(i), b)
|
||||||
for row in report.get('traffic', []):
|
for row in report.get('traffic', []):
|
||||||
@@ -337,7 +339,77 @@ def drawing_rectangle_rows(report):
|
|||||||
return expected
|
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)
|
expected = drawing_rectangle_rows(report)
|
||||||
require(len(audit['rows']) == len(expected), 'offline rectangle coverage')
|
require(len(audit['rows']) == len(expected), 'offline rectangle coverage')
|
||||||
for row, (category, identifier, rect, source, text) in zip(audit['rows'], expected):
|
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''))
|
+ 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.
|
"""One mechanical path for frozen diagnostics and actual OS frames.
|
||||||
|
|
||||||
Errors are retained as failures, including incomplete frozen audits. Every
|
Errors are retained as failures, including incomplete frozen audits. Every
|
||||||
supplied row is accounted for; no palette value is an image oracle.
|
supplied row is accounted for; no palette value is an image oracle.
|
||||||
"""
|
"""
|
||||||
errors, rows = [], []
|
errors, rows = [], []
|
||||||
|
transit_authority = ('raw state, track, time and pause' if state is not None
|
||||||
|
else 'absent: fail closed, no transit exemptions')
|
||||||
try:
|
try:
|
||||||
validate_drawing_rows(report, audit, root)
|
validate_drawing_rows(report, audit, root, state, rest_audit)
|
||||||
except (ValueError, KeyError, TypeError) as exc:
|
except (ValueError, KeyError, TypeError) as exc:
|
||||||
errors.append(str(exc))
|
errors.append(str(exc))
|
||||||
calibration = screenshot_transform(receipt['calibration'], request, images[0][:2], clicks)
|
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':
|
if row['category'] == 'queue':
|
||||||
badge = report['landing_badges'][int(row['id'])]
|
badge = report['landing_badges'][int(row['id'])]
|
||||||
state = {k: badge[k] for k in ('members', 'visible_count', 'overflow')}
|
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':
|
if row['category'] == 'EV':
|
||||||
cabin = next(c for c in report['cabins'] if c['shaft_id'] == row['id'])
|
cabin = next(c for c in report['cabins'] if c['shaft_id'] == row['id'])
|
||||||
state = {k: cabin[k] for k in ('operational', 'doors_open')}
|
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
|
result['failures'] = failures
|
||||||
rows.append(result)
|
rows.append(result)
|
||||||
return dict(rows=rows, errors=errors, mechanical_pass=not errors and all(r['mechanical_pass'] for r in rows),
|
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):
|
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)),
|
for original in (str(Path(index['source'])/conversion_path.relative_to(directory)),
|
||||||
conversion['png_path']):
|
conversion['png_path']):
|
||||||
inspected.append(by_original[original])
|
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 = []
|
provenance = []
|
||||||
for number, suffix in enumerate(('', '-after')):
|
for number, suffix in enumerate(('', '-after')):
|
||||||
conversion_path = folder/('root-early-conversion'+suffix+'.json')
|
conversion_path = folder/('root-early-conversion'+suffix+'.json')
|
||||||
@@ -720,9 +805,56 @@ def compare_traffic(snapshot, report):
|
|||||||
if cell[2] != report['layer']:
|
if cell[2] != report['layer']:
|
||||||
continue
|
continue
|
||||||
x, y, w, h = transform.rect(cell)
|
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'},
|
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')
|
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):
|
def compare_queues(snapshot, report):
|
||||||
@@ -935,27 +1067,12 @@ def compare_rest(state, report, audit, root=ROOT):
|
|||||||
if selected.get('resting'):
|
if selected.get('resting'):
|
||||||
resting[sid] = selected['facility_id']
|
resting[sid] = selected['facility_id']
|
||||||
close(report['resting_staff'], resting, 'draw resting_staff')
|
close(report['resting_staff'], resting, 'draw resting_staff')
|
||||||
transform = Transform(runtime, report)
|
# Fitting is cross-kind, and queue precedence applies to resting staff too.
|
||||||
drawings = {r['key']: r for r in report['actor_drawings']}
|
# Reconstruct the complete group and independently prove p=1 no-fit before
|
||||||
for sid, fid in resting.items():
|
# allowing an identity to appear only in an aggregation's member list.
|
||||||
member = next(m for m in members if m['staff_id'] == sid)
|
if any(r['cells'] for r in runtime['world']['occupancies']
|
||||||
if member['current_cell'][2] != report['layer'] or sid in report['carried_patients']:
|
if r['entity_id'] in resting.values()):
|
||||||
continue
|
compare_actor_geometry(state, report)
|
||||||
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')
|
|
||||||
return selections
|
return selections
|
||||||
|
|
||||||
|
|
||||||
@@ -1129,8 +1246,105 @@ def compare_motion(state, report, audit):
|
|||||||
return ['motion_history', 'interpolation', 'pause']
|
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):
|
def compare_actor_geometry(state, report):
|
||||||
runtime = state['systems']['runtime']
|
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)
|
transform = Transform(runtime, report)
|
||||||
p, height = transform.dim[0]/16, transform.dim[1]
|
p, height = transform.dim[0]/16, transform.dim[1]
|
||||||
facilities = codec.indexed(runtime['facilities']['instances'], 'instance_id')
|
facilities = codec.indexed(runtime['facilities']['instances'], 'instance_id')
|
||||||
@@ -1139,7 +1353,7 @@ def compare_actor_geometry(state, report):
|
|||||||
counts, expected = {}, []
|
counts, expected = {}, []
|
||||||
def track_position(key, cell):
|
def track_position(key, cell):
|
||||||
track = report['actor_tracks'][key]
|
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))
|
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)
|
a, b = transform.center(track['from']), transform.center(cell)
|
||||||
return [a[i]+(b[i]-a[i])*t for i in (0, 1)]
|
return [a[i]+(b[i]-a[i])*t for i in (0, 1)]
|
||||||
@@ -1182,7 +1396,10 @@ def compare_actor_geometry(state, report):
|
|||||||
continue
|
continue
|
||||||
center = track_position(key, cell)
|
center = track_position(key, cell)
|
||||||
special = True
|
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])
|
carrier = staff.get(report['carried_patients'][sid])
|
||||||
if carrier:
|
if carrier:
|
||||||
center = track_position('staff/'+carrier['staff_id'], carrier['current_cell'])
|
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)
|
center = report['queue_positions'].get(key, center)
|
||||||
if center is not None:
|
if center is not None:
|
||||||
expected.append((key, center, True, False, bool(member['carried_inventory_id']) and not special, False))
|
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')
|
floor_keys = [r[0] for r in expected]
|
||||||
pairs = list(zip(report['actor_drawings'], expected))
|
cabin_bounds = {}
|
||||||
shafts = {s['shaft_id']: s for s in elevator_report(state)['shafts']}
|
shafts = {s['shaft_id']: s for s in elevator_report(state)['shafts']}
|
||||||
for cabin in report['cabins']:
|
for cabin in report['cabins']:
|
||||||
shaft = shafts[cabin['shaft_id']]
|
shaft = shafts[cabin['shaft_id']]
|
||||||
x,y,w,h = cabin['rect']
|
x,y,w,h = cabin['rect']
|
||||||
columns = max(int((w/p-4)/8), 1)
|
columns = max(int((w/p-4)/8), 1)
|
||||||
passengers = shaft['boarded'][:columns*2]
|
passengers = shaft['boarded']
|
||||||
require(len(cabin['passengers']) == len(passengers), 'passenger coverage')
|
for i, actor_id in enumerate(passengers):
|
||||||
for i, (actor_id, row) in enumerate(zip(passengers, cabin['passengers'])):
|
|
||||||
key = actor_id.replace('staff:', 'staff/', 1).replace('guest:', 'guest/', 1)
|
key = actor_id.replace('staff:', 'staff/', 1).replace('guest:', 'guest/', 1)
|
||||||
is_staff = key.startswith('staff/')
|
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
|
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)))
|
expected.append((key, center, is_staff, False, carried, True))
|
||||||
for row, (key, center, is_staff, receiving, carrying, riding) in pairs:
|
cabin_bounds[key] = cabin['rect']
|
||||||
close(row['center'], center, 'actor center/interpolation')
|
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 = .5 if riding else .75
|
||||||
scale = max(math.floor(p*scale), 1) if p >= 2 else p*scale
|
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]
|
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]
|
rounded = [math.copysign(math.floor(abs(v)+.5), v) for v in at]
|
||||||
close(row['at'], rounded, 'actor drawing position')
|
hashed = 5381
|
||||||
require((row['key'], row['staff'], row['receiving'], row['carrying'], row['riding']) ==
|
for char in key:
|
||||||
(key, is_staff, receiving, carrying, riding), 'actor drawing attributes')
|
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']
|
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])
|
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):
|
def read_object(directory, digest):
|
||||||
require(isinstance(digest, str) and re.fullmatch('[0-9a-f]{64}', digest), 'invalid object digest')
|
require(isinstance(digest, str) and re.fullmatch('[0-9a-f]{64}', digest), 'invalid object digest')
|
||||||
path = directory / 'objects' / (digest + '.json')
|
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')
|
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'])
|
report = decode(request['report'])
|
||||||
capture = captures[request_path.parent.name]
|
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']):
|
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]
|
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')
|
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':
|
if request['action'] == 'screenshot':
|
||||||
clicks = [document(json.loads(path(name).read_bytes())['input_receipt'])
|
clicks = [document(json.loads(path(name).read_bytes())['input_receipt'])
|
||||||
for name in listing['requests']]
|
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))
|
review_dir = request_path.parent/('review-image-' + str(number))
|
||||||
manifest, manifest_sha = write_frame_review(review_dir, frame, [dict(
|
manifest, manifest_sha = write_frame_review(review_dir, frame, [dict(
|
||||||
whole_image_sha256=binding['png_sha256'], raw_sha256=shot['raw_sha256'],
|
whole_image_sha256=binding['png_sha256'], raw_sha256=shot['raw_sha256'],
|
||||||
|
|||||||
@@ -18,6 +18,165 @@ sys.dont_write_bytecode = True
|
|||||||
import check_p23_life_observation_evidence as check
|
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):
|
class SetupBudgetTests(unittest.TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.source = (check.ROOT / 'tests/fixtures/p23-03/observation.gd').read_text()
|
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)
|
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):
|
class SpatialTests(unittest.TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.runtime = dict(world=dict(bounds=dict(min_x=0, max_x=1, min_y=0, max_y=0),
|
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.snapshot = dict(systems=dict(runtime=self.runtime))
|
||||||
self.report = dict(viewport=[184, 164], zoom=1, pan=[0, 0], layer=0,
|
self.report = dict(viewport=[184, 164], zoom=1, pan=[0, 0], layer=0,
|
||||||
traffic=[dict(cell=[0, 0, 0], rect=[78, 72, 12, 20],
|
traffic=[dict(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,
|
traffic=dict(base_capacity=2, current_density=3,
|
||||||
dynamic_cost=0, temporary_block=False))],
|
dynamic_cost=0, temporary_block=False))],
|
||||||
queue_positions={}, queue_hidden={}, landing_badges=[], rooms={},
|
queue_positions={}, queue_hidden={}, landing_badges=[], rooms={},
|
||||||
treated_patients={}, carried_patients={}, resting_staff={},
|
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):
|
def test_density_and_pan_are_independently_recomputed(self):
|
||||||
check.compare_traffic(self.snapshot, self.report)
|
check.compare_traffic(self.snapshot, self.report)
|
||||||
@@ -537,6 +940,247 @@ class SpatialTests(unittest.TestCase):
|
|||||||
check.compare_cabin_geometry(self.snapshot, changed)
|
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):
|
class ComparisonTests(unittest.TestCase):
|
||||||
def pair(self):
|
def pair(self):
|
||||||
declaration = dict(metric='sum_destination_arrival_minutes_entire_cohort',
|
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):
|
def test_report_positions_and_source_provenance_are_recomputed(self):
|
||||||
source = 'scripts/ui/pixel_people.gd'
|
source = 'scripts/ui/pixel_people.gd'
|
||||||
report = dict(rooms={}, landing_badges=[], cabins=[], resting_staff={}, badges=[],
|
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})"',
|
colors = list(dict.fromkeys(check.re.findall(r'Color\("([0-9a-fA-F]{6})"',
|
||||||
(check.ROOT/source).read_text())))
|
(check.ROOT/source).read_text())))
|
||||||
audit = dict(rows=[dict(category='staff', id='staff/s', rect=[1, 1, 16, 18],
|
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):
|
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):
|
def setUp(self):
|
||||||
OfflineGeometryTests.setUp(self)
|
OfflineGeometryTests.setUp(self)
|
||||||
self.request.update(local_to_viewport=[1, 0, 0, 1, 0, 0],
|
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])
|
local_to_screen=[1, 0, 0, 1, 0, 0])
|
||||||
self.report = dict(layer=0, viewport=[100, 80], rooms={}, cabins=[],
|
self.report = dict(layer=0, viewport=[100, 80], rooms={}, cabins=[],
|
||||||
landing_badges=[], badges=[], resting_staff={}, traffic=[],
|
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'
|
source = 'scripts/ui/pixel_people.gd'
|
||||||
colors = list(dict.fromkeys(check.re.findall(r'Color\("([0-9a-fA-F]{6})"',
|
colors = list(dict.fromkeys(check.re.findall(r'Color\("([0-9a-fA-F]{6})"',
|
||||||
(check.ROOT/source).read_text())))
|
(check.ROOT/source).read_text())))
|
||||||
@@ -684,6 +1403,11 @@ class FrameReviewTests(unittest.TestCase):
|
|||||||
return check.frame_rows(self.report, self.audit, self.request,
|
return check.frame_rows(self.report, self.audit, self.request,
|
||||||
dict(calibration=self.calibration), [self.image], self.clicks)
|
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):
|
def test_blank_crop_fails_without_threshold(self):
|
||||||
self.image = (200, 190, [[(0, 0, 0)] * 200 for _ in range(190)])
|
self.image = (200, 190, [[(0, 0, 0)] * 200 for _ in range(190)])
|
||||||
result = self.frame()
|
result = self.frame()
|
||||||
|
|||||||
Reference in New Issue
Block a user