Compare commits

...
55 changed files with 7850 additions and 101 deletions
@@ -3702,6 +3702,17 @@
"scope": "normal placement; native and OS pending" "scope": "normal placement; native and OS pending"
} }
} }
,{
"test": "tests/ui/test_p23_life_observation.gd",
"template": "P23 life observation: <checks> checks, <failures> failures",
"required": true,
"fixed_check_count_used": false,
"source_sha256_at_schema_audit": "cc7049e1c3919585caeefd1747be12fbf3fa52141fcdb980929c8a16fd7e2ee1",
"authority": {
"path": "docs/spec/acceptance/P23-03.json",
"scope": "component and read-only observation only; native unexecuted; whole unit blocked"
}
}
], ],
"unknown_roster_suite_policy": "refuse, explicitly review its source/schema before accepting", "unknown_roster_suite_policy": "refuse, explicitly review its source/schema before accepting",
"roster_count_is_not_hardcoded": true, "roster_count_is_not_hardcoded": true,
@@ -0,0 +1,41 @@
# P23-03 Phase 0 — API and state map
Base: `860136f7099f5e3aee99da5798a627a77f2d8eea`. Static inspection only; no engine execution.
## Exact source references
Line numbers below refer to the base; SHA256 covers the complete file.
| Source | Lines / API | SHA256 |
| --- | --- | --- |
| autoload/simulation_runtime.gd | 203 initialize(clock,state,seed); 225 seed assignment; 466 submit_command; 508 get_rng_stream; 511 create_snapshot; 1483 enable_production_operations; 1780 default demand configuration; 2831 arrival boundary; 2949 apply arrivals; 4137 queue_manual_checkpoint; 4141 save_to_path; 4169 load_from_path; 5066 seed streams; 6103 spawn_guest executor; 7629 build_facility executor; 10859 staff management report; 12464 supply management report | 4696df4a77feb465a5a3ba8b806569f7bcbe1d03c88289acfb78a3928f307d67 |
| autoload/game_state.gd | 20 reset | cf0e884910220e54c56b5875983e53de439af9d893be1e1b65c2121d2a30722f |
| scripts/core/simulation_kernel.gd | 91 reset; 104 advance_minutes; 122 advance_one_minute; 175 set_paused | 341c0c878c4894adbac628d0db6362ff69fc3347bdd9e61e50936e15dc2d1edb |
| scripts/facility/facility_service.gd | 109 get_instances; 656 create_snapshot | ceb4a3e9224587423f273cdb2b56e6843ab176ac1fc4fe79dd498b5ed5c33b77 |
| scripts/guest/guest_service.gd | 170 get_guests; 982 create_snapshot | 56880410e9a00816f9a95d30821982955532de8dd976980934e12016a9f2b21e |
| scripts/staff/staff_service.gd | 166 get_staff_members; 682 get_tasks; 1064 create_snapshot | 355ad5c3c9f85dc4d4ffc3bc4293e36f81078c403d1662512a735b2c8d815fb3 |
| scripts/transport/elevator_service.gd | 345 get_report; 362 create_snapshot | cea09dd9204551c26b31b30cd3aa847cc77e3b75fdac31662b9694a95fb3580c |
| scripts/world/path_service.gd | 545 get_cell_traffic | 447135729a1106639a6eee6754af3195c8a8b458ca26980ae91fa39d80dcfbd4 |
| scripts/supply/supply_logistics_bridge.gd | 122 get_report | b9e394af788595d2744b71f1907c8e7c3d762d38ff49992b1ee74e63b9639e80 |
| scripts/facility/facility_instance.gd | 8 OperationState; 10 State; 30 state and subsequent live fields | 0c6aa493f3299611e05dd26aa429dda8cf7b5c479e4d09632036fb862f79a324 |
| tests/transport/test_runtime_transport_comparison.gd | 91 existing fixed-demand setup; 109 real paid build command | ba577397fa8db5a6cf92cfd398dd7a5ece49e6b59856f14772b67b51ddc0bde9 |
## Deterministic demand capability
An existing deterministic input path is available without Runtime edits: fresh clock/state, `initialize(clock,state,seed)`, then the same scheduled `submit_command(&"spawn_guest", payload, ..., execute_at, idempotency_key)` sequence. The executor validates registered definitions and real walkable cells, creates wallets and GuestRuntime actors, and verifies Path actor projection and movement credit. Explicit IDs must avoid the reserved `guest-auto-` namespace. Inputs and processed results must both be recorded; a seed/hash alone does not prove identical arrivals.
The existing transport comparison exercises public spawning, real paid construction and Runtime-owned movement. It is fixed-demand experimentation, not proof of natural autonomous demand or all six observation categories. Its numeric assertions are not adopted as new acceptance thresholds. Automatic production arrivals are a separate path (`enable_production_operations`): population, weather and terminal-state gates mean the same seed alone cannot guarantee identical accepted arrivals. Layouts must use identical production settings and compare actual arrival inputs/results.
`build_facility` resolves the registered definition, runs `_build_facility_precheck`, then calls `construction_service.start_construction` with `definition.build_minutes`. Construction cost, time and capacities must come from definitions. B may add only the predeclared existing corridor/EV through this command.
## Clock and observation
`reset` defines initial time. Paused manual `advance_minutes` is explicitly supported; abort/pause boundaries must be checked. Commands run before minute increment, then time boundaries, mana, construction, logistics, facility completion, vitals, incidents, queues, AI and publication.
Facility/guest/staff getters expose typed objects; presentation must copy values without mutating them. Elevator reports contain shafts/trips. Path traffic provides actual density/capacity/block fields. Logistics reports contain requests/deliveries, source/destination IDs, carrier, requirements, weight, state/reason and task ID. They do not contain warehouse stock: requested quantities must never be labelled stock.
## Formal save/load and evidence
Use `queue_manual_checkpoint` or `save_to_path`, then `load_from_path` under a distinct real PID. The load API pauses the clock and invokes SaveService package validation before restore. `restore_snapshot` alone is not formal resume. Preserve all package members, source/spec/engine/settings pins, whole typed sparse snapshots and actual CUA/process/cleanup evidence.
Original references read: BASELINE.txt lines 10,13,16,25,37,43,69,83,106,216; IMPLEMENTATION_PLAN.md line 1800; immutable P23-03 spec; recorded direction 83e836e. All seven conditions remain mandatory, with no partial credit. Root native/OS/resume/full-regression evidence is missing.
File diff suppressed because one or more lines are too long
@@ -0,0 +1,253 @@
# P23-03 offline repair — evidence gap
Status: blocked, not static-complete. No acceptance credit. The failed 0b7727a
OS run remains failure history. No receipts, mirror files, product, values,
styles or test gates were changed. No git writes or engine/OS/collector/full
runs were performed.
## Corrected direction
The three previous pixel assumptions are superseded:
1. `viewport_to_screen` maps to content coordinates, not screenshot pixels.
Actual request-0002 and request-0004 CUA receipt calibration records window
size 1440x810, screenshot 1440x842, titlebar 32, backing scale 1 and stretch 1.
The image lookup must apply the calibrated content origin and independently
check agreement with actual CUA click offsets; the number 32 must not become
a checker constant. Existing affine composition checks remain required.
2. Source RGB cannot be required unchanged after JPEG conversion, NO_STAFF
dimming, ghost outlines and antialiasing. Palette identity is source
provenance, not a screenshot color oracle. Pixel proof needs measured
within-image noise and independent expected-class/state/shift alternatives.
3. Every report row must be accounted for, but not every row is visible. Use
report/audit fields for visible, clipped/clamped, outside with reason,
ghost/inactive and facility-state dimming. Require eligible visible coverage
of all six categories on both FRONT and BACK. Never silently skip a row.
The spec requires draw-consumed rows, pixel rectangles and provenance to
correspond; it does not require unchanged source RGB. The function docstring
now explicitly warns that the implementation still uses the superseded checks.
## Verified material and blocking dependency
Mirror:
`/Users/korizatou114/Projects/TheTree-managed-worktrees/p23-03-code/.runtime/P23-03/offline-os-0b7727a-r07`
All 94 files named by MIRROR_INDEX.json were read and their SHA256 verified;
zero missing files or mismatches. The only indexed conversion record is
`os-pair/A/writer-request-0002/root-early-conversion.json`, with stdout/stderr.
The only decoded PNG is the corresponding `root-early-decoded.png`, SHA256
`206a9aafdc50d1f2f330690e14390de8c7ed5b0df024cedf8f2fbd35326f455a`.
The other three required stable screenshots are JPEG bytes despite .png names:
- `os-pair/A/writer-request-0002/screen-after.png`
- `os-pair/A/writer-request-0004/screen-before.png`
- `os-pair/A/writer-request-0004/screen-after.png`
Their magic starts `ffd8ffe000104a46`. No frozen decoded PNG/conversion records
for these files are in the mirror. Searching the original source root named
by MIRROR_INDEX for `*decoded*.png` and `*conversion*.json` also returned none.
The existing conversion verifier requires a separate PNG, original raw hash,
CUA call attribution and pinned `/usr/bin/sips` conversion record. Creating
replacement conversion assertions, rewriting receipt paths, or silently
changing that converter authority would not preserve this proof gate.
Minimal next input: Root supplies separate decoded PNGs and genuine conversion
records/logs for those three existing raw images, adds their hashes to a new
mirror inventory while preserving the original inventory and receipts. This
does not require another OS capture. Alternatively the commander must explicitly
specify an offline Python JPEG decoder authority and its provenance contract.
Worker must not execute sips under the Python-only direction.
Until then the requested full frozen before/after comparison, BACK cropped
pixel fixtures, calibrated noise and per-row pixel results cannot be submitted
as complete. Geometry/report preparation alone cannot meet all requested tests.
The behavioral repair and new fixtures/tests have not been implemented.
## Local commands and outputs
These read-only Python checks used the mirror path above as `p`:
```python
import json, pathlib, hashlib
p = pathlib.Path('/Users/korizatou114/Projects/TheTree-managed-worktrees/p23-03-code/.runtime/P23-03/offline-os-0b7727a-r07')
j = json.loads((p / 'MIRROR_INDEX.json').read_text())
bad = [r['path'] for r in j['files'] if not (p/r['path']).is_file()
or hashlib.sha256((p/r['path']).read_bytes()).hexdigest() != r['sha256']]
print('indexed', len(j['files']), 'mismatches', bad)
print('conversion records', [r['path'] for r in j['files'] if 'conversion' in r['path']])
```
Output:
```
indexed 94 mismatches []
conversion records ['os-pair/A/writer-request-0002/root-early-conversion.json', 'os-pair/A/writer-request-0002/root-early-conversion.json.stderr', 'os-pair/A/writer-request-0002/root-early-conversion.json.stdout']
```
Calling the existing `screenshot_pixels(p/name)` for each of the three paths
above (without manufacturing a conversion argument) produced:
```
writer-request-0002/screen-after.png: JPEG screenshot requires conversion record
writer-request-0004/screen-before.png: JPEG screenshot requires conversion record
writer-request-0004/screen-after.png: JPEG screenshot requires conversion record
```
Command: `python3 tools/test_p23_life_observation_evidence.py`
```
................................
----------------------------------------------------------------------
Ran 32 tests in 0.058s
OK
```
These are existing unit tests, not the requested new repair tests, and do not
prove the repair or acceptance. Root native/OS/full review remains pending.
Seven conditions, six categories, 30/88, no partial credit, timeout 900 and
cap null remain unchanged.
## Follow-on: 106-entry mirror (2026-10-15)
The missing-conversion dependency above is **resolved**, not a current blocker.
The prior text is retained as failure history. The worker read all 106 indexed
files and verified every byte count and SHA256, plus the preserved initial94
inventory SHA and unchanged entries. No mirror or original receipt was edited.
`OFFLINE_MIRROR_106_DIAGNOSTICS.json` contains the explicit original absolute
path -> mirrored absolute path -> equal indexed SHA256 mapping for all106 files,
including the eight named PNG/conversion files. Each conversion was checked
against its corresponding actual CUA screenshot call ID/raw hash, pinned sips
binary SHA, original argv, output PNG magic/dimensions/hash and stdout/stderr
hashes. Original raw/png path strings and argv remain unchanged. A read-only
resolver locates indexed mirrored bytes; it does not rewrite conversion records.
Direct reads of the source artifact directory were denied by the host
(`PermissionError: Operation not permitted`); equality to the frozen inventory
and conversion-record hashes was verified, not a fresh direct source-directory
comparison. This limitation is not represented as a missing PNG conversion.
New `--offline-mirror` preparation diagnostics check content/image dimensions,
backing scale, content origin derived from recorded titlebar, affine composition,
and agreement with the separately recorded click events. They independently
derive existing audit row identities/local rectangles from report positions and
source palette provenance. Every existing audit row has visible/clipped/outside
accounting and a calibrated screenshot rectangle. Room enum/operation names are
read from the actual FacilityInstance source. Dimming is derived from operation,
not merely the NO_STAFF state name. These are preparation diagnostics, not a
replacement acceptance pixel gate. In particular, audit font_width remains an
engine assertion, and receipt click events have not been verified against the
full helper-definition/tool trace. No source RGB tolerance was invented.
Actual frozen-frame results:
- FRONT request0002: 9 audit rows (3 room,1 queue,2 guest,3 business), all visible.
EV/staff/goods coverage pending. Two rooms are INACTIVE and one OPERATING.
One actual draw-consumed traffic overlay at cell[-6,2,0], local rect
[261,294,60,92], has no corresponding rectangle-audit row.
- BACK request0004: 4 audit rows (2 room,2 business), all visible and both rooms
INACTIVE. Queue/EV/staff/goods coverage pending; no eligible traffic overlay.
- Before/after temporal differences within all13 calibrated audit rectangles
are zero. This establishes stability only, not spatial JPEG error or correct
drawing class/state. It supplies no pixel acceptance credit.
- The existing independent typed-stream checker recomputed all14 frozen stream
records successfully; the CLI still returned blocked/pending(exit2), as required.
Current repair status: **incomplete / blocked**, not genuinely submitted.
The legacy `compare_rectangles` and its normal-OS call site have deliberately
not been relabelled as repaired. Remaining worker work includes an independent
expected-class/state/shift pixel discriminator with measured within-image noise,
state negatives, full ghost/inactive/traffic draw accounting, independent badge
width provenance and integration into the strict OS checker. The added
preparation path does not solve these items and must not be used for acceptance.
Missing frozen provenance includes the FRONT traffic-overlay rectangle audit,
opposite-layer ghost draw audit, and independent font measurements for badge
widths. Existing reports/source can support further preparation; these missing
records do not establish that a fresh OS capture is already necessary.
Local validation: Python AST syntax checks; 42 checker unit tests (existing32
plus10 preparation tests); actual mirrored checker commands above. Tests cover
wrong titlebar/click shifts, PID, nonfinite scales, clipped/outside rectangles,
measured temporal noise, report rectangle/identity/coverage/palette tampering.
They do not include the still-required pixel-state discriminator negatives.
No Godot, native, sips execution, collector, OS run, full regression, git write,
branch/integration, Runner action or agent delegation occurred. Root native,
complete A/B FRONT/BACK pan/zoom/distinct-PID OS matrix, mandatory full regression
and independent Opus review remain pending. Historical baselines remain32 tests,
17+17 semantic,319 native,7153/2585 and7/6/30/88; the new local42 count is not a
replacement acceptance baseline. Timeout900 and cap null are unchanged.
## Owner proof-method repair (2026-10-15)
The owner's recorded decision supersedes the proposed automatic class/state/
style discriminator, shift/swap alternative scoring, measured-noise gate and
independent badge-width measurement gate. They are no longer required work.
Palette RGB is provenance only. Font width remains the recorded engine value;
no independent font measurement or numeric image tolerance was introduced.
All earlier entries above are retained as history, not current requirements.
Implemented within the narrowed Python edit scope:
- Shared `frame_rows` now handles both offline and normal OS screenshot frames,
uses receipt calibration checked against actual click events, checks affine
composition, independently derives row identity/local geometry/text/palette,
validates room state/operation and actor enum names, and retains every audit
row with visibility, clipping reason, calibrated image rectangle and failures.
- The legacy `compare_rectangles` and its tests are removed and replaced by
grounded negatives. Fully outside eligible image crops and exactly uniform
crops fail; source RGB is never compared to screenshot RGB.
- `drawing_rectangle_rows` now expects every draw-consumed traffic overlay.
Missing/extra rows fail the mechanical frame, without synthesizing audit rows.
- Pure-Python PNG crops and review manifests are written for each frozen frame.
Manifests bind decoded whole-image/raw/conversion/request/receipt hashes,
per-row source hash, recorded font width, state, operation, layer and geometry.
A separate SHA file hashes the exact manifest bytes. Normal OS frames use the
same path and emit per-image review materials. Pixel categories receive no
credit without an independent direct-image review verdict bound to that SHA,
covering every eligible row with category/state/position verdicts. The worker
created no actual review verdict; synthetic verdict files exist only inside
temporary unit-test fixtures.
Current scope blocker, found by reading the actual dependencies:
`tests/fixtures/p23-03/passive_observation.gd:150` owns `draw_rectangles`, not
`world_preview.gd`. Its explicit room/queue/cabin/actor/badge loops at lines
152-171 do not consume traffic audit rows from the view or report. The transform
and palette audit path at lines172-182 only processes that locally built array.
`world_preview.gd:762` draws the traffic overlay, but has no existing rectangle
audit emitter or callback. Changing only world_preview cannot connect traffic
rows to the actual request's `rectangles` field without altering the fixture.
No inert emitter, mislabelled badge, or checker-generated observed audit was
added to pretend this connection exists. World source remains unchanged.
Minimal separate scope proposal: permit one read-only traffic-row loop in
`passive_observation.gd::draw_rectangles`, before its existing transform/palette
loop, consuming report traffic rows with density>0 or temporary_block. Use
category `traffic`, comma-separated cell coordinates as id, the report rectangle
and `scenes/main/world_preview.gd` as style source. Alternatively explicitly
authorize wiring a view-owned traffic audit emitter into that same fixture.
The broad contract lists this fixture, but the latest finish recipe explicitly
narrows edits to checker/tests and world only; the worker did not override it.
Step4 is therefore incomplete and this repair is **blocked**, not submitted.
Local validation: both Python files parse with `ast.parse`; 47 local checker
tests pass. New tests exercise blank crops, missing/extra/traffic rows, enum/name
errors, affine/click calibration shifts, image-outside crops, exact crop hashes,
wrong-manifest/missing/extra/rejected review rows and no credit without review.
The r07 offline CLI verified all106 indexed entries and exited2 as blocked:
- FRONT request0002: expected10 rows, observed9; mechanical failure
`offline rectangle coverage`. The traffic overlay at [-6,2,0] remains a genuine
unaudited overlay. EV/staff/goods coverage pending; zero pixel credit.
- BACK request0004: expected4/observed4; mechanical checks pass.
Queue/EV/staff/goods coverage pending; zero pixel credit.
Current review artifacts are in `offline-r07-review/`; complete diagnostics are
in `OFFLINE_REPAIR_OWNER_METHOD_DIAGNOSTICS.json`. The prior
`OFFLINE_MIRROR_106_DIAGNOSTICS.json` is preserved unchanged.
Still pending: Root frozen native checks, fresh A/B FRONT/BACK ordinary pan/zoom
OS observation and formal distinct-PID resume, the exact full177 regression,
and independent Opus direct-image/all-condition review. No Godot, Runner,
native/OS, full regression, git write, integration or agent execution occurred.
All7 conditions, all6 categories on both layers/layouts, source recomputation,
raw/request/CUA/conversion provenance, calibrated geometry, 30/88 without partial
credit, timeout900 and cap null remain unchanged. No acceptance PASS is claimed.
@@ -0,0 +1,72 @@
# Offline checker repair on 55c29f1
Source HEAD: 55c29f119234dd6f993c604c4edc6f377fe3951c. Registration base remains
860136f7099f5e3aee99da5798a627a77f2d8eea. No git writes or engine execution.
Read access: MIRROR_INDEX.json and all 2,622 named files were successfully read
under `/Users/korizatou114/Projects/TheTree-managed-worktrees/p23-03-code/.runtime/P23-03/offline-evidence-55c29f1-r06`.
No mirror bytes were changed. The initial shell heredoc could not create its
temporary file; the read check was executed using Python `-c` instead.
## First defects and production cause
Both layouts first failed at frame-030, checker line 328, with KeyError:
`shaft_id`. State objects:
- A: `layout-A/objects/ac3ed64a6e6162918e4d44c45b2f78d09e50d473b607f3469350a06074a7efb3.json`
- B: `layout-B/objects/97ea43afe3d1b21602ebe2a602e7b7d796ff05f03983cd4d1b3edc9157e39a08.json`
The exception came from `shaft['shaft_id']`, not `t['shaft_id']`. Every trip in
these failing frames has shaft_id. Saved shaft rows have it only in config.
There is no evidence of shaft-less QUEUED trips to support accepting that shape.
`autoload/simulation_runtime.gd:541` writes elevators.create_snapshot().
`scripts/transport/elevator_service.gd:362-367` copies the saved cars without
flattening config; get_report at 345-359 merges config into each report row before
counting queued trips. request at 169-171 assigns shaft_id on creation, and the
restore validator at 412-454 requires the exact trip keys and shaft/connector
agreement. The connector spelling is defined at 617-618; trips are ordered at
707-710. `scenes/main/world_preview.gd:973-975` consumes get_report and sorts
trips by enqueue time/actor. The checker now counts using the merged row identity.
## Reviewable changes
- `tools/check_p23_life_observation_evidence.py`: replace
`t['shaft_id'] == shaft['shaft_id']` with `t['shaft_id'] == row['shaft_id']`;
validate exact trip keys, companion identity, shaft/stops/connector attribution
and QUEUED/BOARDED state before recomputing. Whole report equality remains strict.
- `tests/fixtures/p23-03/queued_elevator.json`: retain one actual frame-030
queued trip and its shaft, without a cabin seat. Distilled unit data only.
- `tools/test_p23_life_observation_evidence.py`: queued shape positive and
shaft attribution, waiting_count, missing/extra key negatives; update two
pre-existing abbreviated synthetic fixtures to the actual shaft/trip shape.
- This repair report. The complete implementation diff is available through
`git diff -- tools/check_p23_life_observation_evidence.py tools/test_p23_life_observation_evidence.py`;
the new fixture and this file are untracked submission files.
## Local results
`inspect_stream` on each exact absolute mirror directory:
```
layout-A inspect_stream PASS 17 frames
layout-B inspect_stream PASS 17 frames
```
For both, labels are frame-000, frame-030, frame-060, frame-090, frame-120,
frame-150, frame-180, frame-210, frame-240, frame-270, frame-300, frame-330,
frame-360, frame-390, frame-420, frame-450, frame-480. Each returns fields:
guests, staff, tasks, elevators, room_service_fields, queues, traffic, goods,
cabin_geometry. Every frame also passes the existing two-motion-history-frame
check, unchanged. No later schema/semantic mismatch was observed.
Python syntax AST checks pass. Local checker tests: 32 tests, OK.
The initial test run exposed two abbreviated synthetic fixtures; subsequent
queue expectations were corrected for actual lift landing exclusion. Tests
were preserved. git diff --check passes; changed files are within allowed_paths.
completion_marker_contracts retains its one required P23 strict template entry;
no marker changes. Seven conditions, six categories and 30/88 remain unchanged.
Root native results (319 tests, A/B PASS), matched metric values (48 metrics,
7153/2585), production data and services were not modified. Root OS evidence
remains pending as expected at worker submission; this repair grants no acceptance.
@@ -0,0 +1,296 @@
# P23-03 worker status — static-complete submission
Contract base: `860136f7099f5e3aee99da5798a627a77f2d8eea`.
This finalization preserves every pre-existing modified and untracked file.
Static-complete is supported by the wiring inspection and local checks below.
No acceptance claim is made. No Godot, native, OS collector, Runner, full
regression, git write, branch change, or other worker was used.
## Current wiring confirmed by git diff and rg only
Line numbers refer to the current candidate.
- Checker `tools/check_p23_life_observation_evidence.py`: `check` at 1189
reaches `inspect_native_phases` (1162) at 1215 and `inspect_stream` (950)
at 1220/1226. The stream inspection invokes `compare_rest` (531) at 968,
`compare_motion` (738) at 974, and `compare_actor_geometry` (800) at 975,
alongside actor, room, queue, traffic, goods, badge and cabin checks.
- `check` reaches `inspect_os_receipt` (1038), `inspect_os_requests` (1092),
and `inspect_resume` (1070) at 1249, 1250 and 1257.
Request inspection calls `screenshot_pixels` (184) at 1143, which invokes
`inspect_conversion` (158) at 191 for JPEG evidence; rectangle/pixel
checking follows at 1154.
- `check` builds executed check sets at 1270–1272, then calls `BLOCKERS`
at 1273, reaching `condition_matrix` (982) through 1000. All original
seven CONDITIONS and six CATEGORIES (room, queue, EV, staff, goods,
business state, represented by the key `business`) remain required.
Missing executed check names produce pending/deficits; no fixed-true matrix.
- Collector `tools/run_p23_life_observation.py`: `collect_os` (110)
invokes `collect_screenshots` (26) at 186. JPEG collection invokes
`convert_screenshot` (73) at 55; conversion calls `inspect_conversion`
at 107. Raw CUA bytes and tool-call bindings are retained separately from
converted PNG and conversion receipts. This wiring was inspected, not run.
- `diagnostic_stream.gd` captures hash-addressed `rest_audit` and `motion`
at 70–78. `rest_audit` (89) records selection inputs, route/cost probes,
locks/transit and comparison-only Runtime output. No stored-rest field was
introduced. The source-grounded correction supersedes the old stored-state
proposal below. MotionObserver retains two draw frames, not whole Runtime
state per frame.
- Native route: `tests/ui/test_p23_life_observation.gd` at 18–21 invokes
`layout_comparison.gd`; the runner invokes that UI test at 309 for A/B.
Layout comparison preloads diagnostics at 5 and starts watching at 65.
- Normal OS route is separate from the native UI component test: runner
RECORDER at 24 is copied/launched at 145/152; `passive_os_recorder.gd`
preloads `passive_observation.gd` at 2 and calls its inherited run at 6.
`passive_observation.gd` extends `observation.gd` at 1;
`observation.gd` preloads diagnostics/layout comparison at 3–4 and
watches the real preview at 26. These fixtures are reached through the
OS runner, not directly preloaded by the native UI test.
- Existing mutation negatives cover conversion provenance, rest operands,
motion history, rectangle/pixel transforms, process/source/profile guards,
formal members/distinct PID/ACK, and every condition/category matrix cell.
No concrete missing call or negative was found; no code hardening was added.
## Exact local commands and real results
- `git status --short`, `git diff --stat`, and targeted `git diff --`
for the collector, UI test, diagnostic fixture and marker: inspected existing
changes without modifying git state.
- Targeted `rg -n` / `rg -n -A...` searches for function definitions,
calls, fixture preloads/extends, rest_audit/motion, CONDITIONS/CATEGORIES
and unittest methods in the three P23 tools and five P23 fixtures/UI test:
wiring results recorded above. No Runtime/source audit or Phase0 repetition.
- `python3 -m py_compile tools/run_p23_life_observation.py tools/check_p23_life_observation_evidence.py tools/test_p23_life_observation_evidence.py`:
initial attempt failed because the interpreter's default cache destination
under Library/Caches was not writable; no syntax failure was reported.
- `PYTHONPYCACHEPREFIX=docs/plans/original-completion-2026-10-02/execution/p23-03/.finalization-pycache python3 -m py_compile tools/run_p23_life_observation.py tools/check_p23_life_observation_evidence.py tools/test_p23_life_observation_evidence.py`:
PASS, exit 0 for all three tools.
- `python3 -B tools/test_p23_life_observation_evidence.py`:
**27 test methods run, 27 passed**, 0 failures/errors; reported 0.044 seconds.
These are synthetic checker tests, not native/OS evidence.
- `git diff --check`: PASS, exit 0; repeated after this status rewrite.
- Read-only `python3 -B -c` JSON/hash/path assertions using
`git show 860136f7099f5e3aee99da5798a627a77f2d8eea:<path>`,
`git diff --name-only <base>` and
`git ls-files --others --exclude-standard`: PASS.
All old marker metadata/rows equal base; exactly one appended P23-03
required source-authority entry; test SHA
`cc7049e1c3919585caeefd1747be12fbf3fa52141fcdb980929c8a16fd7e2ee1`
matches current bytes. suite-policy.json equals base byte-for-byte.
All 16 changed/untracked paths relative to base are allowed.
- The immutable spec SHA matches
`8568457df849ef587c731f072d77244ef5420fc4e1190c5069cfd6cfeea9c89f`
using the existing checker's `read_text().strip().encode()` convention.
An initial extra raw-byte SHA assertion failed (raw file SHA
`1a925e0d7f5c1c9f28670d29dd0fdc5aa48f4d5415946d6de4c15e1d83b5ed66`);
normalized verification passed and the spec has no diff against base.
- An initial inline validation here-document could not create its shell
temporary file; rerun via `python3 -B -c` succeeded.
- Removed only the temporary .finalization-pycache tree created by this run.
No pre-existing dirty/untracked material was removed.
## Root work still pending — expected, not worker blockers
Native Godot execution; actual OS collector with screenshots for both layouts
and all six categories, FRONT/BACK and ordinary pan/zoom; formal save and
distinct actual PID normal resume; one full exact-candidate **177-suite**
regression; independent Opus all-condition review. These must precede any
acceptance determination. Constructibility, visual correctness and same-demand
improvement have not been claimed from static checks.
## Superseded history — retained verbatim
The following earlier blocked statuses (including the 19-method and
"not yet wired" claims) describe previous candidates and are superseded by the
current wiring and results above.
# Current RUN continuation — blocked on stored-rest-destination contract
All seven pre-existing modified files and three untracked fixtures were retained.
No git writes, agents, Godot, collector execution, Runner or full regression.
This is not a genuine completed submission and makes no acceptance claim.
Root native/OS evidence remains **pending**, independently of this source blocker.
Changes completed in this continuation:
- Removed mandatory left-button drag and its pan-change assertion. Camera
screenshots now use ordinary-wheel-zoom, pan-mode-button, move-mode-wheel-pan.
Existing non-Ctrl event observation is preserved.
- Added stdlib PNG decoding with CRC/chunk/end checks, RGB/RGBA and filters 0–4.
JPEG dimensions reuse the P08 helper; no JPEG decoder or PIL was added.
- Added conversion receipt inspection and Root-only convert_screenshot helper:
separate output, original CUA hash/call binding, sips executable SHA/argv,
output digests, decoded PNG and magic-derived dimension equality. No actual
sips conversion was run here. This helper is not yet wired into collect_os.
- Added four image test methods including all filters, CRC corruption, trailing
data, converter SHA/argv, raw CUA hash, dimensions and missing-conversion negatives.
Actually executed: 19 checker unittest methods passed; py_compile passed for
the three P23 Python tools. Temporary compilation outputs were removed.
Synthetic image tests are checker tests, never native screenshot evidence.
Concrete contract gap (targeted dependency inspection, not a repeated Phase0 audit):
- autoload/simulation_runtime.gd:13109–13125 computes get_staff_rest_report by
scanning facilities and selecting a minimum-cost route on every call. The
selected facility and eligibility are local variables, not stored state.
- autoload/simulation_runtime.gd:8783–8785 calls that query and passes only its
cell to advance_staff_commute. At :12656–12690 the movement function either
returns without movement or emits a STAFF_MOVED event with a target_cell;
it does not persist a selected rest facility ID or eligibility. Movement
events cannot supply a current destination for stationary/nonmoving staff.
- scripts/staff/staff_runtime.gd:282–315 serializes current_cell, assignment,
task and path_handle but no rest destination/eligibility. Runtime snapshot
assembly at :511–570 has no stored rest-selection model. PathService's
snapshot at scripts/world/path_service.gd:1606 stores path/actor projections,
not the selected rest facility and its eligibility.
Thus recipe (3), "rest destination must equal the typed stored destination and
its eligibility; do not re-simulate AI", has no authoritative stored operand.
Calling get_staff_rest_report from a fixture would rerun selection, and copying
its output into a purported stored-state field would manufacture authority.
These Runtime/staff/path files are outside allowed_paths and remain unchanged.
Read source SHA256:
- Runtime: 4696df4a77feb465a5a3ba8b806569f7bcbe1d03c88289acfb78a3928f307d67
- StaffRuntime: 807a417bc7866c97cd6d8f5c1368b5480a2d9fd685a2a47a978e81482bf522b5
- PathService: 447135729a1106639a6eee6754af3195c8a8b458ca26980ae91fa39d80dcfbd4
Minimal separate proposal for Root/Opus, not applied: authorize an authoritative
rest-decision record at the existing Runtime selection point, including selected
facility/cell, eligibility and decision time, with explicit invalidation and
snapshot/restore semantics. Alternatively Root must identify an existing typed
stored field that has those semantics. No worker relaxation of this gate.
Remaining source implementation is explicit: collector conversion integration,
all row/rectangle bindings, motion history, distinct-PID/formal ACK checker,
dynamic matrix and the remaining requested negatives. Existing fail-closed
whole-unit behavior remains; no unconditional PASS or new fail stub was added.
The 30/88 denominator, seven conditions, six categories and old suite policies
are unchanged. Do not begin acceptance assuming only Root evidence is missing.
---
Previous status retained as history:
# Repair from source 03100be — blocked, implementation incomplete
Source checkout HEAD confirmed read-only: `03100beabeb55caac03f53db3d1f399b424df517`.
Contract base remains `860136f7099f5e3aee99da5798a627a77f2d8eea`.
No git writes, Godot, Runner, OS input, regression, or other workers were used.
No acceptance or partial credit is claimed. No out-of-scope defect or missing Runtime capability was established.
Changes in this repair:
- Added registered `--verify-only` CLI, concrete missing-index deficits, immutable fixture/spec/contract/baseline/AGENTS pins and a normative P23 plan-section hash. Full-plan SHA is separate provenance only. Existing diagnostic/layout CLI modes retained.
- Removed fixture-side begin_leaving/despawn_guest calls; Runtime owns continued guest behavior. Added A/B native entry to the existing component test and updated the existing single marker row SHA.
- Added observation.gd extending P08 placement helpers unchanged, with real supply scenario commands/settings, common definition-derived construction budget, seed806042, relocated common cafeteria/kitchen at x6 to avoid declared B elevator footprint, sparse typed captures, normal save/load and whole formal-model comparisons. These construction prechecks have not been executed.
- Added passive request/ACK endpoint and recorder for four layout/role combinations, live FRONT/BACK buttons and PanModeButton, ordinary non-Ctrl wheel zoom/pan and ordinary pan drag only. No deferred P23-02 held-gesture credit.
- Replaced the collector stub with an adaptation of P08 collect/collect_os using run_all_tests helpers and owned gates, fresh outputs/profiles, native import/parse/layout phases, four OS roles and formal reader copies. Records the current interpreter PIL availability; raw screenshot bytes are left untouched. Collector has not been executed.
- Checker now inspects available native/OS streams, comparisons and basic process/source/profile guards, but explicitly fails closed for outstanding whole-unit gates.
Local verification actually executed: 16 Python unittest cases PASS; ast.parse for the three changed Python tools PASS; one P23 marker row and exact source SHA PASS. No engine or evidence-collector execution.
Remaining IMPLEMENTATION, separate from missing Root evidence:
1. Complete screenshot byte-magic dimensions, stdlib PNG pixel/rect checks, and indexed Root JPEG-to-lossless-PNG conversion binding to raw hash/converter provenance. PIL absence is not waived.
2. Add prior-frame visual-position/frame-delta evidence and independent interpolation checks; compare rest destinations with typed stored destination/eligibility without re-simulating AI.
3. Bind all request report rows to viewport rectangles/affine and screenshots; current endpoint retains report plus viewport-to-screen transform but does not yet produce every required viewport row rectangle.
4. Finish independent distinct-PID/formal-member/resume-state/load-ACK gates. Fixture assertions do not replace checker proof.
5. Replace hard failure condition-matrix deficits with exact completed check names only after all seven independent gates exist; finish all requested category and infrastructure negatives. Current check intentionally cannot pass whole-unit integrity.
6. Statically review collector lifecycle/failure receipts and fixture category coverage; real constructibility and EV/passenger/queue/goods coverage remain unproved.
Root native, actual OS for A/B, formal distinct-PID resume, exact-final full regression, independent review, integration/final/ledger: all PENDING. This candidate is blocked on remaining in-scope implementation, not ready for acceptance or a claim that missing Root evidence is the only gap.
---
Previous worker status retained verbatim as history (superseded by the repair status above):
# P23-03 worker status: submitted static work, implementation incomplete
Base commit: `860136f7099f5e3aee99da5798a627a77f2d8eea`.
This submission preserves the previous dirty work and adds genuine static
implementation. It is **not acceptance-ready**. No external Runtime capability
gap or out-of-scope defect has been established. Remaining implementation is
listed separately from pending Root evidence; no acceptance credit is claimed.
## Inspection and changes in this repair
- Re-read the recorded direction, Phase 0 map, previous status and renderer diff.
The single frame builder supplies drawing and the deep-copy public report.
No renderer redesign or additional renderer was introduced. Existing renderer
and component test bytes were retained.
- Added `tests/fixtures/p23-03/layout_comparison.gd`. Both layouts initialize the
public Runtime with seed 806042, reset budget and clock, identical production
defaults and the same recorded 48-person arrival script. Commands build the
existing elevator and corridors; B adds only the declared second elevator.
Preparation time derives from registered definition build times.
- Predeclared metric: sum of destination-arrival minutes for the entire cohort.
The checker rejects incomplete cohorts, changed actual arrival payloads,
additional layout changes, changed initial state, and non-strict improvement.
No observed improvement is claimed: the fixture has not run.
- Extended diagnostic_stream with hash-addressed typed event/input records.
The fixture records actual command results, domain movement events and
per-minute guest/path/EV/facility projections, including service history,
queues and density. Whole aggregate state/report pairs are sparse (30-minute
intervals); the existing P08 typed encoder is reused without truncation.
- Extended independent checking of room service/rescue occupants and business
enum fields, queue allocation/overflow, shared transforms, traffic, logistics
report selection/order and cabin geometry. Added Rect2 and vector-key decode.
The checker still refuses whole-unit acceptance.
## Exact remaining implementation (not external blockers)
1. Finish rest destination selection from typed path costs and event/defense
locks. Current room check validates eligibility and reported rest references;
it does not independently prove which rest destination Runtime selected.
2. Finish every actor/passenger mode and geometry, including cleaning,
rest/rescue/service slots, visual track history, badge text and screenshot
rect/pixel provenance. Cabin endpoints and rectangles are checked, but
unpaused prior-frame interpolation origin is not independently established.
3. Complete A/B definition/source/production pins and command/event/path
cross-validation, invoke the fixture from the Root collector, and add
representative real operational scenarios for all six categories and BACK.
The added experiment covers the fixed-demand FRONT transport comparison;
it does not establish actual service purchasing or all-six-category coverage.
4. Replace run_p23_life_observation.py readiness stub with the actual Root
collector using P08 owned-process/profile/source/engine/ordinary-audio and
CUA request/ACK helpers. It remains unchanged; it launches nothing.
Formal save, distinct-PID normal resume, both-layout FRONT/BACK/pan/zoom,
complete CUA receipts, all-run cleanup/protection and failure history remain
to be wired and independently checked.
5. Add missing meaningful negatives, especially normal OS/distinct-PID resume,
visual provenance and the remaining rest/motion branches. Existing 12 local
tests are not a complete seven-condition test matrix.
Steps (2) and (3) of the repair direction are therefore still incomplete;
step (4) is not implemented. This is a partial static submission, not a claim
that the requested repair is finished. Do not begin acceptance on an assumption
that all worker deliverables are complete.
## Checks actually run in this repair
- `python3 -B tools/test_p23_life_observation_evidence.py`: 12 test methods,
zero failures. Synthetic Python examples validate rejection logic only.
- AST syntax: all three P23 Python tools parsed.
- Marker JSON: old rows and metadata unchanged; exactly one appended row;
its SHA matches the unchanged GDScript component test.
- `git diff --check`: passed.
- No Godot parsing/runtime, native, OS, Runner or full regression execution.
- No git writes, subagents, integration, new assets or production changes.
suite-policy.json was not changed; no new suite was registered.
## Source SHA256 at local checks
| File | SHA256 |
| --- | --- |
| scenes/main/world_preview.gd | add770377c190a5ccdde95379539de9753f7dabc703c1d58452ba590c6eda2f0 |
| scripts/ui/room_presentation.gd | 55340b7b623532e861be1d21225635fd9169e2e40928597c1cd0458b99524dd9 |
| tests/ui/test_p23_life_observation.gd | b9afa66ec73d3a5cadf86f9a71f0d13f5b085c3cc9a534dfe29b2bed0e2e76a0 |
| tests/fixtures/p23-03/diagnostic_stream.gd | ff178a094fc310b0640d923a15058c6f6fcf859551c14f6428a5709dcd24260e |
| tests/fixtures/p23-03/layout_comparison.gd | ddf00268b8e32e3c5c078ebb57ada321382ecc7c5fbd87efad7e9dfbe768df6b |
| tools/run_p23_life_observation.py | 53c2f0638d4a861cf4fcb664504c25df33ae5478207867654f1c8cdf58fa23c7 |
| tools/check_p23_life_observation_evidence.py | 318b253a08248a753b88902e382edc5f01cbce6f1ed8092359698b69d3bb0503 |
| tools/test_p23_life_observation_evidence.py | 0f20d3de20b44f46f8738c0d022fcc1179656014e85fe31754f00b87ec2c288a |
## Root evidence: pending
After genuine completion and freezing: both-layout native and actual normal OS
observation, full CUA trace, formal save and distinct-PID resume, mandatory exact
version full regression and independent Opus review. These absent Root results
are **pending**, not blocked. The fixed 30-package / 88-unit denominator and all
P08/P22/P25/Windows/held-gesture/performance exclusions remain unchanged.
@@ -0,0 +1,922 @@
{
"errors": [
"offline rectangle coverage"
],
"expected_row_count": 10,
"images": [
{
"conversion_sha256": "d9e92a5b1925d6e1855f6eaff2820007d7db8dd79a5348ec801a38251b40fe90",
"raw_sha256": "ab073ed1055ca2356a050fdd47176c454c109ebe5c008b194ea8a5f149efa7f5",
"receipt_sha256": "184d50e702232ce259ca75b830992cb8e91d5175289a21812661325bf0024860",
"request_sha256": "172dc9a8f4bb684583f7417ac4c544172ee66b4134845fe540326df5598e6167",
"whole_image_sha256": "206a9aafdc50d1f2f330690e14390de8c7ed5b0df024cedf8f2fbd35326f455a"
},
{
"conversion_sha256": "b25b2224963f9a02ad87675969ab51c90aff29891b136095198afc2fe95acb73",
"raw_sha256": "7027a7f1813aa8c9fcff38e6e4b022ed718d6f9c9fd13d31ecc3d6d10818ac10",
"receipt_sha256": "184d50e702232ce259ca75b830992cb8e91d5175289a21812661325bf0024860",
"request_sha256": "172dc9a8f4bb684583f7417ac4c544172ee66b4134845fe540326df5598e6167",
"whole_image_sha256": "e6ab48ad864a1b5fdca2da587d95fe7949ac0c59e664a4524db66e3e861a73aa"
}
],
"mechanical_pass": false,
"rows": [
{
"category": "room",
"crops": [
{
"image_index": 0,
"path": "row-0000-image-0.png",
"sha256": "0811d7a2ac82283233b2b5179ddc6ac32a86fac3a211febae2c706ba36513286"
},
{
"image_index": 1,
"path": "row-0000-image-1.png",
"sha256": "0811d7a2ac82283233b2b5179ddc6ac32a86fac3a211febae2c706ba36513286"
}
],
"dimmed": true,
"eligibility": "visible",
"failures": [
"offline rectangle coverage"
],
"font_width": null,
"id": "facility_00000002",
"layer": 0,
"local_rect": [
131.0,
388.0,
256.0,
192.0
],
"mechanical_pass": false,
"operation": "INACTIVE",
"reason": "whole rectangle in viewport",
"row_index": 0,
"screenshot_rect": [
210.0,
485.0,
256.0,
192.0
],
"state": "NO_STAFF",
"state_name": "NO_STAFF",
"style_colors": [
"34413b",
"8e6348",
"c39665",
"eee5c9",
"608d84",
"c6b999",
"a6b6a0",
"d4c7a5",
"acb9a5",
"897487",
"f1e9cb",
"b39b77",
"615563",
"a57852",
"587f7b",
"edc97c",
"d6ad52",
"709b94",
"a37b91",
"72b6b8",
"c1e0d4",
"445652",
"a2b2a3",
"d3aa8f",
"ca9c62",
"ffe9a8",
"6c8993",
"adc5b4",
"52836c",
"c7c6a6",
"a8b6aa",
"d8bf89",
"aec0bf",
"8a7155",
"617f82",
"3e514e",
"404f63",
"83958a",
"283a39",
"586c62",
"526177",
"dcc08b",
"9daa8d",
"879eaa",
"556e62",
"4d6472",
"e2bd6c",
"ba7157",
"bfbb9a",
"a9aa91",
"9ab9b3",
"d5e3d0",
"f2ca81",
"fff0d6",
"a67c53"
],
"style_source": "scripts/ui/pixel_tower_art.gd",
"style_source_sha256": "71df9b8400cb883a5bb1c89729f80c77e734df503fc54154be3e486adb8391df",
"text": null
},
{
"category": "room",
"crops": [
{
"image_index": 0,
"path": "row-0001-image-0.png",
"sha256": "bb79fa3c9cc46f2f7b9d31b2de85c3b6f6b4446718d3614cfeeba818ea87cb69"
},
{
"image_index": 1,
"path": "row-0001-image-1.png",
"sha256": "bb79fa3c9cc46f2f7b9d31b2de85c3b6f6b4446718d3614cfeeba818ea87cb69"
}
],
"dimmed": true,
"eligibility": "visible",
"failures": [
"offline rectangle coverage"
],
"font_width": null,
"id": "facility_00000003",
"layer": 0,
"local_rect": [
1027.0,
388.0,
256.0,
192.0
],
"mechanical_pass": false,
"operation": "INACTIVE",
"reason": "whole rectangle in viewport",
"row_index": 1,
"screenshot_rect": [
1106.0,
485.0,
256.0,
192.0
],
"state": "NO_STAFF",
"state_name": "NO_STAFF",
"style_colors": [
"34413b",
"8e6348",
"c39665",
"eee5c9",
"608d84",
"c6b999",
"a6b6a0",
"d4c7a5",
"acb9a5",
"897487",
"f1e9cb",
"b39b77",
"615563",
"a57852",
"587f7b",
"edc97c",
"d6ad52",
"709b94",
"a37b91",
"72b6b8",
"c1e0d4",
"445652",
"a2b2a3",
"d3aa8f",
"ca9c62",
"ffe9a8",
"6c8993",
"adc5b4",
"52836c",
"c7c6a6",
"a8b6aa",
"d8bf89",
"aec0bf",
"8a7155",
"617f82",
"3e514e",
"404f63",
"83958a",
"283a39",
"586c62",
"526177",
"dcc08b",
"9daa8d",
"879eaa",
"556e62",
"4d6472",
"e2bd6c",
"ba7157",
"bfbb9a",
"a9aa91",
"9ab9b3",
"d5e3d0",
"f2ca81",
"fff0d6",
"a67c53"
],
"style_source": "scripts/ui/pixel_tower_art.gd",
"style_source_sha256": "71df9b8400cb883a5bb1c89729f80c77e734df503fc54154be3e486adb8391df",
"text": null
},
{
"category": "room",
"crops": [
{
"image_index": 0,
"path": "row-0002-image-0.png",
"sha256": "b90d74efdd21be0eb69555138dccacb62100825a908c6f17958e17a46321056b"
},
{
"image_index": 1,
"path": "row-0002-image-1.png",
"sha256": "b90d74efdd21be0eb69555138dccacb62100825a908c6f17958e17a46321056b"
}
],
"dimmed": false,
"eligibility": "visible",
"failures": [
"offline rectangle coverage"
],
"font_width": null,
"id": "facility_00000007",
"layer": 0,
"local_rect": [
131.0,
292.0,
128.0,
96.0
],
"mechanical_pass": false,
"operation": "OPERATING",
"reason": "whole rectangle in viewport",
"row_index": 2,
"screenshot_rect": [
210.0,
389.0,
128.0,
96.0
],
"state": "OPEN",
"state_name": "OPEN",
"style_colors": [
"34413b",
"8e6348",
"c39665",
"eee5c9",
"608d84",
"c6b999",
"a6b6a0",
"d4c7a5",
"acb9a5",
"897487",
"f1e9cb",
"b39b77",
"615563",
"a57852",
"587f7b",
"edc97c",
"d6ad52",
"709b94",
"a37b91",
"72b6b8",
"c1e0d4",
"445652",
"a2b2a3",
"d3aa8f",
"ca9c62",
"ffe9a8",
"6c8993",
"adc5b4",
"52836c",
"c7c6a6",
"a8b6aa",
"d8bf89",
"aec0bf",
"8a7155",
"617f82",
"3e514e",
"404f63",
"83958a",
"283a39",
"586c62",
"526177",
"dcc08b",
"9daa8d",
"879eaa",
"556e62",
"4d6472",
"e2bd6c",
"ba7157",
"bfbb9a",
"a9aa91",
"9ab9b3",
"d5e3d0",
"f2ca81",
"fff0d6",
"a67c53"
],
"style_source": "scripts/ui/pixel_tower_art.gd",
"style_source_sha256": "71df9b8400cb883a5bb1c89729f80c77e734df503fc54154be3e486adb8391df",
"text": null
},
{
"category": "queue",
"crops": [
{
"image_index": 0,
"path": "row-0003-image-0.png",
"sha256": "eeb6b5a301d017693ba3acd47d22b63348bc6536c5ddcfd593aa0d593e256aac"
},
{
"image_index": 1,
"path": "row-0003-image-1.png",
"sha256": "eeb6b5a301d017693ba3acd47d22b63348bc6536c5ddcfd593aa0d593e256aac"
}
],
"dimmed": false,
"eligibility": "visible",
"failures": [
"offline rectangle coverage"
],
"font_width": 36.0,
"id": "0",
"layer": 0,
"local_rect": [
259.0,
276.0,
42.0,
14.0
],
"mechanical_pass": false,
"operation": null,
"reason": "whole rectangle in viewport",
"row_index": 3,
"screenshot_rect": [
338.0,
373.0,
42.0,
14.0
],
"state": {
"members": [
"guest/guest-auto-00000019"
],
"overflow": 0,
"visible_count": 1
},
"state_name": null,
"style_colors": [
"b5cbb5",
"273c49",
"526d68",
"233d40",
"8cae98",
"749981",
"567e68",
"c6d3b6",
"d5dec4",
"574d39",
"776445",
"8a7350",
"69583d",
"65583e",
"385d45",
"47463e",
"5a594b",
"aaa083",
"829a8d",
"b69465",
"514839",
"8d7951",
"e1bd6c",
"dfd4ad",
"2f493f",
"e4d9b9",
"58784c",
"4c7950",
"5e8b57",
"739957",
"95ad69",
"648957",
"e34332",
"e3a62f",
"55c96b",
"f5d582",
"34413b",
"fff2ce",
"3c4e40",
"f5d58a",
"304b41",
"f0e2b9",
"303c40",
"d69a56",
"4ca5c7",
"efbd72",
"79d3e7",
"e5f8fa",
"64d48c",
"e16262",
"8ff0ab",
"ff8a80",
"52bdd0",
"8ce6ee",
"d7ebef",
"29483d",
"f0e2bd",
"8ca4af",
"6f8b67",
"427582",
"a24b45",
"a77d3f",
"6b6258",
"89914d",
"6f9a55",
"67518e",
"ffd080"
],
"style_source": "scenes/main/world_preview.gd",
"style_source_sha256": "add770377c190a5ccdde95379539de9753f7dabc703c1d58452ba590c6eda2f0",
"text": "待ち1人"
},
{
"category": "guest",
"crops": [
{
"image_index": 0,
"path": "row-0004-image-0.png",
"sha256": "e08ebb7ae2eb1f2f66360df04d7c132bed923ece9ba5d406c72f23c16bc71e34"
},
{
"image_index": 1,
"path": "row-0004-image-1.png",
"sha256": "e08ebb7ae2eb1f2f66360df04d7c132bed923ece9ba5d406c72f23c16bc71e34"
}
],
"dimmed": false,
"eligibility": "visible",
"failures": [
"offline rectangle coverage"
],
"font_width": null,
"id": "guest/guest-auto-00000019",
"layer": 0,
"local_rect": [
267.0,
325.0,
48.0,
54.0
],
"mechanical_pass": false,
"operation": null,
"reason": "whole rectangle in viewport",
"row_index": 4,
"screenshot_rect": [
346.0,
422.0,
48.0,
54.0
],
"state": "QUEUING",
"state_name": "QUEUING",
"style_colors": [
"688da0",
"b87659",
"a69a63",
"89789e",
"638d72",
"edc49d",
"c99371",
"a56f50",
"303c40",
"719d94",
"eee5c9",
"608d84",
"eee6c7",
"e3dfc2",
"956940",
"d3ad6b",
"b48850",
"af9059",
"d8bc78",
"f3ebcf",
"efe7ce",
"805e44"
],
"style_source": "scripts/ui/pixel_people.gd",
"style_source_sha256": "a171703083aa2d00f8be2b63e1daef9c41717c8fe59d8056878bc4b7df1e0b0c",
"text": null
},
{
"category": "guest",
"crops": [
{
"image_index": 0,
"path": "row-0005-image-0.png",
"sha256": "8b4be9869042638da9a5afdd90ce982baa16623c2b5e4cbedc50109680867522"
},
{
"image_index": 1,
"path": "row-0005-image-1.png",
"sha256": "8b4be9869042638da9a5afdd90ce982baa16623c2b5e4cbedc50109680867522"
}
],
"dimmed": false,
"eligibility": "visible",
"failures": [
"offline rectangle coverage"
],
"font_width": null,
"id": "guest/guest-auto-00000020",
"layer": 0,
"local_rect": [
139.0,
301.0,
48.0,
54.0
],
"mechanical_pass": false,
"operation": null,
"reason": "whole rectangle in viewport",
"row_index": 5,
"screenshot_rect": [
218.0,
398.0,
48.0,
54.0
],
"state": "RECEIVING_SERVICE",
"state_name": "RECEIVING_SERVICE",
"style_colors": [
"688da0",
"b87659",
"a69a63",
"89789e",
"638d72",
"edc49d",
"c99371",
"a56f50",
"303c40",
"719d94",
"eee5c9",
"608d84",
"eee6c7",
"e3dfc2",
"956940",
"d3ad6b",
"b48850",
"af9059",
"d8bc78",
"f3ebcf",
"efe7ce",
"805e44"
],
"style_source": "scripts/ui/pixel_people.gd",
"style_source_sha256": "a171703083aa2d00f8be2b63e1daef9c41717c8fe59d8056878bc4b7df1e0b0c",
"text": null
},
{
"category": "business",
"crops": [
{
"image_index": 0,
"path": "row-0006-image-0.png",
"sha256": "62525c0b9f2c189545666c134eb6f83b660d6efbc0698278ce049ace7e708d0a"
},
{
"image_index": 1,
"path": "row-0006-image-1.png",
"sha256": "62525c0b9f2c189545666c134eb6f83b660d6efbc0698278ce049ace7e708d0a"
}
],
"dimmed": false,
"eligibility": "visible",
"failures": [
"offline rectangle coverage"
],
"font_width": 73.0,
"id": "0",
"layer": 0,
"local_rect": [
133.0,
390.0,
79.0,
14.0
],
"mechanical_pass": false,
"operation": "INACTIVE",
"reason": "whole rectangle in viewport",
"row_index": 6,
"screenshot_rect": [
212.0,
487.0,
79.0,
14.0
],
"state": "NO_STAFF",
"state_name": "NO_STAFF",
"style_colors": [
"b5cbb5",
"273c49",
"526d68",
"233d40",
"8cae98",
"749981",
"567e68",
"c6d3b6",
"d5dec4",
"574d39",
"776445",
"8a7350",
"69583d",
"65583e",
"385d45",
"47463e",
"5a594b",
"aaa083",
"829a8d",
"b69465",
"514839",
"8d7951",
"e1bd6c",
"dfd4ad",
"2f493f",
"e4d9b9",
"58784c",
"4c7950",
"5e8b57",
"739957",
"95ad69",
"648957",
"e34332",
"e3a62f",
"55c96b",
"f5d582",
"34413b",
"fff2ce",
"3c4e40",
"f5d58a",
"304b41",
"f0e2b9",
"303c40",
"d69a56",
"4ca5c7",
"efbd72",
"79d3e7",
"e5f8fa",
"64d48c",
"e16262",
"8ff0ab",
"ff8a80",
"52bdd0",
"8ce6ee",
"d7ebef",
"29483d",
"f0e2bd",
"8ca4af",
"6f8b67",
"427582",
"a24b45",
"a77d3f",
"6b6258",
"89914d",
"6f9a55",
"67518e",
"ffd080"
],
"style_source": "scenes/main/world_preview.gd",
"style_source_sha256": "add770377c190a5ccdde95379539de9753f7dabc703c1d58452ba590c6eda2f0",
"text": "職員不足 未稼働"
},
{
"category": "business",
"crops": [
{
"image_index": 0,
"path": "row-0007-image-0.png",
"sha256": "379c49ce02077552289ae503c74a4a0032150ff6cec3ea20c42045f6a059ec3d"
},
{
"image_index": 1,
"path": "row-0007-image-1.png",
"sha256": "379c49ce02077552289ae503c74a4a0032150ff6cec3ea20c42045f6a059ec3d"
}
],
"dimmed": false,
"eligibility": "visible",
"failures": [
"offline rectangle coverage"
],
"font_width": 73.0,
"id": "1",
"layer": 0,
"local_rect": [
1029.0,
390.0,
79.0,
14.0
],
"mechanical_pass": false,
"operation": "INACTIVE",
"reason": "whole rectangle in viewport",
"row_index": 7,
"screenshot_rect": [
1108.0,
487.0,
79.0,
14.0
],
"state": "NO_STAFF",
"state_name": "NO_STAFF",
"style_colors": [
"b5cbb5",
"273c49",
"526d68",
"233d40",
"8cae98",
"749981",
"567e68",
"c6d3b6",
"d5dec4",
"574d39",
"776445",
"8a7350",
"69583d",
"65583e",
"385d45",
"47463e",
"5a594b",
"aaa083",
"829a8d",
"b69465",
"514839",
"8d7951",
"e1bd6c",
"dfd4ad",
"2f493f",
"e4d9b9",
"58784c",
"4c7950",
"5e8b57",
"739957",
"95ad69",
"648957",
"e34332",
"e3a62f",
"55c96b",
"f5d582",
"34413b",
"fff2ce",
"3c4e40",
"f5d58a",
"304b41",
"f0e2b9",
"303c40",
"d69a56",
"4ca5c7",
"efbd72",
"79d3e7",
"e5f8fa",
"64d48c",
"e16262",
"8ff0ab",
"ff8a80",
"52bdd0",
"8ce6ee",
"d7ebef",
"29483d",
"f0e2bd",
"8ca4af",
"6f8b67",
"427582",
"a24b45",
"a77d3f",
"6b6258",
"89914d",
"6f9a55",
"67518e",
"ffd080"
],
"style_source": "scenes/main/world_preview.gd",
"style_source_sha256": "add770377c190a5ccdde95379539de9753f7dabc703c1d58452ba590c6eda2f0",
"text": "職員不足 未稼働"
},
{
"category": "business",
"crops": [
{
"image_index": 0,
"path": "row-0008-image-0.png",
"sha256": "79ea1c2d0ff2d4e8f6c7ff20f019a4e264c480438e7528b8de85709b8d8eeda5"
},
{
"image_index": 1,
"path": "row-0008-image-1.png",
"sha256": "79ea1c2d0ff2d4e8f6c7ff20f019a4e264c480438e7528b8de85709b8d8eeda5"
}
],
"dimmed": false,
"eligibility": "visible",
"failures": [
"offline rectangle coverage"
],
"font_width": 69.0,
"id": "2",
"layer": 0,
"local_rect": [
133.0,
294.0,
75.0,
14.0
],
"mechanical_pass": false,
"operation": "OPERATING",
"reason": "whole rectangle in viewport",
"row_index": 8,
"screenshot_rect": [
212.0,
391.0,
75.0,
14.0
],
"state": "OPEN",
"state_name": "OPEN",
"style_colors": [
"b5cbb5",
"273c49",
"526d68",
"233d40",
"8cae98",
"749981",
"567e68",
"c6d3b6",
"d5dec4",
"574d39",
"776445",
"8a7350",
"69583d",
"65583e",
"385d45",
"47463e",
"5a594b",
"aaa083",
"829a8d",
"b69465",
"514839",
"8d7951",
"e1bd6c",
"dfd4ad",
"2f493f",
"e4d9b9",
"58784c",
"4c7950",
"5e8b57",
"739957",
"95ad69",
"648957",
"e34332",
"e3a62f",
"55c96b",
"f5d582",
"34413b",
"fff2ce",
"3c4e40",
"f5d58a",
"304b41",
"f0e2b9",
"303c40",
"d69a56",
"4ca5c7",
"efbd72",
"79d3e7",
"e5f8fa",
"64d48c",
"e16262",
"8ff0ab",
"ff8a80",
"52bdd0",
"8ce6ee",
"d7ebef",
"29483d",
"f0e2bd",
"8ca4af",
"6f8b67",
"427582",
"a24b45",
"a77d3f",
"6b6258",
"89914d",
"6f9a55",
"67518e",
"ffd080"
],
"style_source": "scenes/main/world_preview.gd",
"style_source_sha256": "add770377c190a5ccdde95379539de9753f7dabc703c1d58452ba590c6eda2f0",
"text": "営業中 利用1人"
}
]
}
@@ -0,0 +1 @@
646334f4ef3c78a8cfe5d68d3738c4d772cdd71cbf80b2dba7c153693ead4419
@@ -0,0 +1,443 @@
{
"errors": [],
"expected_row_count": 4,
"images": [
{
"conversion_sha256": "363c2bdb1da77458226d01e971b9aea1220fcc81f70fb5c3c816a12b7039fac3",
"raw_sha256": "2abb4675f9978e7b86599c4334732ebadcb7cd5c5c51deeacaa7d0dee858e9b9",
"receipt_sha256": "b393e4e0afcdbc72f8cbb3c88cbe6cab7fa2548855ee648c69c5ac61f7fffb17",
"request_sha256": "1a9ace68b3cf0ca997ea3e28ba1da1c30aa0cff72bf5d433d008ce578c7de093",
"whole_image_sha256": "abf7d2e89413431438f35cc3eecb5534a7257cb033906c5397592678b2e9bce4"
},
{
"conversion_sha256": "d9f5e79103258a50bb5d0866725a8488017a9cc8ef150312d16de9043618b55c",
"raw_sha256": "e5a7ac0c3ba3f001baae7c771304d1a9a9cf024240176e8f8735a9dcf1091b31",
"receipt_sha256": "b393e4e0afcdbc72f8cbb3c88cbe6cab7fa2548855ee648c69c5ac61f7fffb17",
"request_sha256": "1a9ace68b3cf0ca997ea3e28ba1da1c30aa0cff72bf5d433d008ce578c7de093",
"whole_image_sha256": "b82e6ef061d9b4e8cbc3321405553f3f447c8abfb61ce38532254130a704ee95"
}
],
"mechanical_pass": true,
"rows": [
{
"category": "room",
"crops": [
{
"image_index": 0,
"path": "row-0000-image-0.png",
"sha256": "94beea581b9586cda35d6c16a0bb8dccc7cad6c6cd0a9842ead3b34d19066f43"
},
{
"image_index": 1,
"path": "row-0000-image-1.png",
"sha256": "94beea581b9586cda35d6c16a0bb8dccc7cad6c6cd0a9842ead3b34d19066f43"
}
],
"dimmed": true,
"eligibility": "visible",
"failures": [],
"font_width": null,
"id": "facility_00000004",
"layer": 1,
"local_rect": [
131.0,
388.0,
256.0,
192.0
],
"mechanical_pass": true,
"operation": "INACTIVE",
"reason": "whole rectangle in viewport",
"row_index": 0,
"screenshot_rect": [
210.0,
485.0,
256.0,
192.0
],
"state": "NO_STAFF",
"state_name": "NO_STAFF",
"style_colors": [
"34413b",
"8e6348",
"c39665",
"eee5c9",
"608d84",
"c6b999",
"a6b6a0",
"d4c7a5",
"acb9a5",
"897487",
"f1e9cb",
"b39b77",
"615563",
"a57852",
"587f7b",
"edc97c",
"d6ad52",
"709b94",
"a37b91",
"72b6b8",
"c1e0d4",
"445652",
"a2b2a3",
"d3aa8f",
"ca9c62",
"ffe9a8",
"6c8993",
"adc5b4",
"52836c",
"c7c6a6",
"a8b6aa",
"d8bf89",
"aec0bf",
"8a7155",
"617f82",
"3e514e",
"404f63",
"83958a",
"283a39",
"586c62",
"526177",
"dcc08b",
"9daa8d",
"879eaa",
"556e62",
"4d6472",
"e2bd6c",
"ba7157",
"bfbb9a",
"a9aa91",
"9ab9b3",
"d5e3d0",
"f2ca81",
"fff0d6",
"a67c53"
],
"style_source": "scripts/ui/pixel_tower_art.gd",
"style_source_sha256": "71df9b8400cb883a5bb1c89729f80c77e734df503fc54154be3e486adb8391df",
"text": null
},
{
"category": "room",
"crops": [
{
"image_index": 0,
"path": "row-0001-image-0.png",
"sha256": "713f0f370de8ff40def7b5fbe9f071b8f5be3db1604b18bce5c6cf7e8eb62c54"
},
{
"image_index": 1,
"path": "row-0001-image-1.png",
"sha256": "713f0f370de8ff40def7b5fbe9f071b8f5be3db1604b18bce5c6cf7e8eb62c54"
}
],
"dimmed": true,
"eligibility": "visible",
"failures": [],
"font_width": null,
"id": "facility_00000005",
"layer": 1,
"local_rect": [
1027.0,
388.0,
256.0,
192.0
],
"mechanical_pass": true,
"operation": "INACTIVE",
"reason": "whole rectangle in viewport",
"row_index": 1,
"screenshot_rect": [
1106.0,
485.0,
256.0,
192.0
],
"state": "NO_STAFF",
"state_name": "NO_STAFF",
"style_colors": [
"34413b",
"8e6348",
"c39665",
"eee5c9",
"608d84",
"c6b999",
"a6b6a0",
"d4c7a5",
"acb9a5",
"897487",
"f1e9cb",
"b39b77",
"615563",
"a57852",
"587f7b",
"edc97c",
"d6ad52",
"709b94",
"a37b91",
"72b6b8",
"c1e0d4",
"445652",
"a2b2a3",
"d3aa8f",
"ca9c62",
"ffe9a8",
"6c8993",
"adc5b4",
"52836c",
"c7c6a6",
"a8b6aa",
"d8bf89",
"aec0bf",
"8a7155",
"617f82",
"3e514e",
"404f63",
"83958a",
"283a39",
"586c62",
"526177",
"dcc08b",
"9daa8d",
"879eaa",
"556e62",
"4d6472",
"e2bd6c",
"ba7157",
"bfbb9a",
"a9aa91",
"9ab9b3",
"d5e3d0",
"f2ca81",
"fff0d6",
"a67c53"
],
"style_source": "scripts/ui/pixel_tower_art.gd",
"style_source_sha256": "71df9b8400cb883a5bb1c89729f80c77e734df503fc54154be3e486adb8391df",
"text": null
},
{
"category": "business",
"crops": [
{
"image_index": 0,
"path": "row-0002-image-0.png",
"sha256": "a5ba5edbf9706e2b8a3ceb1de3d61e11074fbb731316e9686e2cec5ea98fc2ae"
},
{
"image_index": 1,
"path": "row-0002-image-1.png",
"sha256": "a5ba5edbf9706e2b8a3ceb1de3d61e11074fbb731316e9686e2cec5ea98fc2ae"
}
],
"dimmed": false,
"eligibility": "visible",
"failures": [],
"font_width": 73.0,
"id": "0",
"layer": 1,
"local_rect": [
133.0,
390.0,
79.0,
14.0
],
"mechanical_pass": true,
"operation": "INACTIVE",
"reason": "whole rectangle in viewport",
"row_index": 2,
"screenshot_rect": [
212.0,
487.0,
79.0,
14.0
],
"state": "NO_STAFF",
"state_name": "NO_STAFF",
"style_colors": [
"b5cbb5",
"273c49",
"526d68",
"233d40",
"8cae98",
"749981",
"567e68",
"c6d3b6",
"d5dec4",
"574d39",
"776445",
"8a7350",
"69583d",
"65583e",
"385d45",
"47463e",
"5a594b",
"aaa083",
"829a8d",
"b69465",
"514839",
"8d7951",
"e1bd6c",
"dfd4ad",
"2f493f",
"e4d9b9",
"58784c",
"4c7950",
"5e8b57",
"739957",
"95ad69",
"648957",
"e34332",
"e3a62f",
"55c96b",
"f5d582",
"34413b",
"fff2ce",
"3c4e40",
"f5d58a",
"304b41",
"f0e2b9",
"303c40",
"d69a56",
"4ca5c7",
"efbd72",
"79d3e7",
"e5f8fa",
"64d48c",
"e16262",
"8ff0ab",
"ff8a80",
"52bdd0",
"8ce6ee",
"d7ebef",
"29483d",
"f0e2bd",
"8ca4af",
"6f8b67",
"427582",
"a24b45",
"a77d3f",
"6b6258",
"89914d",
"6f9a55",
"67518e",
"ffd080"
],
"style_source": "scenes/main/world_preview.gd",
"style_source_sha256": "add770377c190a5ccdde95379539de9753f7dabc703c1d58452ba590c6eda2f0",
"text": "職員不足 未稼働"
},
{
"category": "business",
"crops": [
{
"image_index": 0,
"path": "row-0003-image-0.png",
"sha256": "e977d7db178d1d61310fe7e33a9c25873b62b584337d93ea29960d6f8c5bb869"
},
{
"image_index": 1,
"path": "row-0003-image-1.png",
"sha256": "e977d7db178d1d61310fe7e33a9c25873b62b584337d93ea29960d6f8c5bb869"
}
],
"dimmed": false,
"eligibility": "visible",
"failures": [],
"font_width": 73.0,
"id": "1",
"layer": 1,
"local_rect": [
1029.0,
390.0,
79.0,
14.0
],
"mechanical_pass": true,
"operation": "INACTIVE",
"reason": "whole rectangle in viewport",
"row_index": 3,
"screenshot_rect": [
1108.0,
487.0,
79.0,
14.0
],
"state": "NO_STAFF",
"state_name": "NO_STAFF",
"style_colors": [
"b5cbb5",
"273c49",
"526d68",
"233d40",
"8cae98",
"749981",
"567e68",
"c6d3b6",
"d5dec4",
"574d39",
"776445",
"8a7350",
"69583d",
"65583e",
"385d45",
"47463e",
"5a594b",
"aaa083",
"829a8d",
"b69465",
"514839",
"8d7951",
"e1bd6c",
"dfd4ad",
"2f493f",
"e4d9b9",
"58784c",
"4c7950",
"5e8b57",
"739957",
"95ad69",
"648957",
"e34332",
"e3a62f",
"55c96b",
"f5d582",
"34413b",
"fff2ce",
"3c4e40",
"f5d58a",
"304b41",
"f0e2b9",
"303c40",
"d69a56",
"4ca5c7",
"efbd72",
"79d3e7",
"e5f8fa",
"64d48c",
"e16262",
"8ff0ab",
"ff8a80",
"52bdd0",
"8ce6ee",
"d7ebef",
"29483d",
"f0e2bd",
"8ca4af",
"6f8b67",
"427582",
"a24b45",
"a77d3f",
"6b6258",
"89914d",
"6f9a55",
"67518e",
"ffd080"
],
"style_source": "scenes/main/world_preview.gd",
"style_source_sha256": "add770377c190a5ccdde95379539de9753f7dabc703c1d58452ba590c6eda2f0",
"text": "職員不足 未稼働"
}
]
}
@@ -0,0 +1 @@
f013e0d1d6231833b656859cc73ad0b58b389047e421ad2c43cc598820e21067
+247 -100
View File
@@ -78,6 +78,7 @@ var _queue_positions: Dictionary = {}
var _queue_hidden: Dictionary = {} 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 _dragging := false var _dragging := false
var _drag_button := MOUSE_BUTTON_NONE var _drag_button := MOUSE_BUTTON_NONE
@@ -511,6 +512,7 @@ func _draw() -> void:
func _draw_contents() -> void: func _draw_contents() -> void:
_life_observation = {}
if _inactive_layer_art != null: if _inactive_layer_art != null:
_inactive_layer_art.visible = false _inactive_layer_art.visible = false
_draw_background() _draw_background()
@@ -521,15 +523,25 @@ func _draw_contents() -> void:
var map_rect := get_map_rect() var map_rect := get_map_rect()
if map_rect.size == Vector2.ZERO: if map_rect.size == Vector2.ZERO:
return return
_life_observation = _build_life_observation()
_room_presentations = _life_observation[&"rooms"]
_carried_patients = _life_observation[&"carried_patients"]
_treated_patients = _life_observation[&"treated_patients"]
_resting_staff = _life_observation[&"resting_staff"]
_queue_positions = _life_observation[&"queue_positions"]
_queue_hidden = _life_observation[&"queue_hidden"]
_landing_badges.assign(_life_observation[&"landing_badges"])
_transport_report = _life_observation[&"elevators"]
_boarded_actor_ids = _life_observation[&"boarded"]
_actor_tracks = _life_observation[&"actor_tracks"]
_elevator_tracks = _life_observation[&"elevator_tracks"]
_draw_world_cells(bounds, map_rect) _draw_world_cells(bounds, map_rect)
if show_grid:
_draw_congestion_overlay(bounds)
_prepare_room_presentations()
_prepare_queues()
_draw_world_entity_icons() _draw_world_entity_icons()
_draw_legacy_stairs() _draw_legacy_stairs()
_draw_elevators() _draw_elevators()
_draw_other_layer() _draw_other_layer()
_draw_congestion_overlay(bounds)
_draw_life_badges()
_draw_construction_labels() _draw_construction_labels()
_draw_facility_labels() _draw_facility_labels()
_draw_actors() _draw_actors()
@@ -540,6 +552,102 @@ func _draw_contents() -> void:
_draw_map_caption(map_rect) _draw_map_caption(map_rect)
## The report describes the last submitted draw, not an independently sampled world.
## Callers cannot mutate either the frame or its interpolation tracks.
func get_life_observation_report() -> Dictionary:
return _life_observation.duplicate(true)
## Pure with respect to Runtime and this Control: helpers mutate only fresh local
## dictionaries (including copies of visual tracks). No whole Runtime snapshot
## is copied per frame; whole typed snapshots belong to sparse diagnostics.
func _build_life_observation() -> Dictionary:
var model := _observe_rooms()
model.merge(_observe_queues())
model[&"layer"] = layer_mode
model[&"zoom"] = zoom_level
model[&"pan"] = pan_offset
model[&"viewport"] = size
model[&"animation_time"] = _animation_time
model[&"actor_tracks"] = _actor_tracks.duplicate(true)
model[&"elevator_tracks"] = _elevator_tracks.duplicate(true)
model[&"boarded"] = {}
var clock := get_node_or_null("/root/GameClock")
model[&"paused"] = clock != null and bool(clock.get("paused"))
model[&"cabins"] = _observe_elevators(model)
model[&"actor_drawings"] = _observe_actors(model)
var guests: Array[Dictionary] = []
var staff: Array[Dictionary] = []
var tasks: Array[Dictionary] = []
var guest_service: Variant = _service(&"guest_service")
if guest_service != null and guest_service.has_method(&"get_guests"):
for guest: GuestRuntime in guest_service.get_guests():
guests.append({&"id": guest.guest_id, &"state": GuestRuntime.State.keys()[guest.state],
&"cell": guest.current_cell, &"target": guest.target_facility_id,
&"service": guest.current_service_id, &"queue": guest.queue_request_id})
var staff_service: Variant = _service(&"staff_service")
if staff_service != null:
for member: StaffRuntime in staff_service.get_staff_members():
staff.append({&"id": member.staff_id, &"state": StaffRuntime.State.keys()[member.state],
&"cell": member.current_cell, &"task": member.current_task_id,
&"facility": member.assigned_facility_id, &"role": member.role_id,
&"carried_inventory_id": member.carried_inventory_id})
for task: StaffTask in staff_service.get_tasks():
tasks.append({&"id": task.task_id, &"state": StaffTask.State.keys()[task.state],
&"type": task.task_type, &"staff": task.assigned_staff_id,
&"target": task.target_entity_id, &"rescue_facility": task.rescue_facility_id})
model[&"guests"] = guests
model[&"staff"] = staff
model[&"tasks"] = tasks
var bridge: Variant = _service(&"supply_logistics_bridge")
var goods: Dictionary = bridge.get_report() if bridge != null else {&"requests": [], &"deliveries": []}
model[&"goods"] = goods
var badges: Array[Dictionary] = []
var facilities: Variant = _service(&"facility_service")
var world: Variant = _service(&"world_service")
if facilities != null and world != null:
for instance: FacilityInstance in facilities.get_instances():
if instance.origin.z != layer_mode:
continue
var cells: Array[Vector3i] = world.get_entity_cells(instance.instance_id)
if cells.is_empty():
continue
var rect := _cell_bounds_rect(cells)
var data: Dictionary = model[&"rooms"][instance.instance_id]
data[&"rect"] = rect
data[&"definition_id"] = instance.definition_id
var count := RoomPresentation.slots(rect, _cell_dimensions().x / 16.0, instance.definition_id).size()
badges.append({&"facility": instance.instance_id, &"kind": &"business",
&"at": rect.position + Vector2(2, 2),
&"text": RoomPresentation.business_label(data) + " " + RoomPresentation.label(data, count)})
var requests: Array[Dictionary] = []
for request: Dictionary in goods.get(&"requests", []):
if request[&"source_facility_id"] == instance.instance_id or request[&"destination_facility_id"] == instance.instance_id:
requests.append(request)
if not requests.is_empty():
badges.append({&"facility": instance.instance_id, &"kind": &"supply",
&"at": rect.position + Vector2(2, 16), &"requests": requests,
&"text": RoomPresentation.supply_label(requests)})
model[&"badges"] = badges
var traffic: Array[Dictionary] = []
var path: Variant = _service(&"path_service")
var bounds := _world_bounds()
if path != null and not bounds.is_empty():
for y in range(int(bounds[&"min_y"]), int(bounds[&"max_y"]) + 1):
for x in range(int(bounds[&"min_x"]), int(bounds[&"max_x"]) + 1):
var cell := Vector3i(x, y, layer_mode)
var row: Dictionary = path.get_cell_traffic(cell)
if not row.is_empty():
traffic.append({&"cell": cell, &"traffic": row, &"rect": get_cell_rect(cell).grow(-2.0)})
model[&"traffic"] = traffic
return model
func _draw_life_badges() -> void:
for badge: Dictionary in _life_observation.get(&"badges", []):
_draw_readable_badge(badge[&"at"], badge[&"text"])
func _draw_background() -> void: func _draw_background() -> void:
var night := false var night := false
var clock := get_node_or_null("/root/GameClock") var clock := get_node_or_null("/root/GameClock")
@@ -650,40 +758,23 @@ func _draw_leaf_cluster(center: Vector2, p: float, variation: int) -> void:
draw_rect(Rect2(c+offset,Vector2(3+i%2,2)*p),Color("95ad69") if i%3==0 else Color("648957")) 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_congestion_overlay(_bounds: Dictionary) -> void:
var path_service: Variant = _service(&"path_service") for row: Dictionary in _life_observation.get(&"traffic", []):
if path_service == null or not path_service.has_method(&"get_cell_traffic"): var rect: Rect2 = row[&"rect"]
return var traffic: Dictionary = row[&"traffic"]
for y in range(int(bounds[&"min_y"]), int(bounds[&"max_y"]) + 1): var density := int(traffic[&"current_density"])
for x in range(int(bounds[&"min_x"]), int(bounds[&"max_x"]) + 1): var blocked := bool(traffic[&"temporary_block"])
var cell := Vector3i(x, y, layer_mode) if density <= 0 and not blocked:
var traffic_value: Variant = path_service.call(&"get_cell_traffic", cell) continue
if not traffic_value is Dictionary or traffic_value.is_empty(): var ratio := float(density) / maxi(int(traffic[&"base_capacity"]), 1)
continue var color := Color("e34332") if blocked or ratio > 1.0 else Color("e3a62f") if ratio >= 0.65 else Color("55c96b")
var traffic: Dictionary = traffic_value var center := rect.get_center()
var density := int(traffic.get(&"current_density", 0)) draw_line(Vector2(rect.position.x + rect.size.x * 0.12, center.y),
var capacity := maxi(int(traffic.get(&"base_capacity", 1)), 1) Vector2(rect.end.x - rect.size.x * 0.12, center.y),
var blocked := bool(traffic.get(&"temporary_block", false)) Color(color, 0.90 if show_grid else 0.28), clampf(rect.size.y * 0.16, 2.0, 5.0))
if density <= 0 and not blocked: if blocked:
continue draw_line(rect.position, rect.end, Color(color, 0.90 if show_grid else 0.28), 2.0)
var ratio := float(density) / float(capacity) draw_line(Vector2(rect.end.x, rect.position.y), Vector2(rect.position.x, rect.end.y), Color(color, 0.90 if show_grid else 0.28), 2.0)
var color := Color("55c96b")
if blocked or ratio > 1.0:
color = Color("e34332")
elif ratio >= 0.65:
color = Color("e3a62f")
var rect := get_cell_rect(cell).grow(-2.0)
var center := rect.get_center()
var route_width := clampf(rect.size.y * 0.16, 2.0, 5.0)
draw_line(
Vector2(rect.position.x + rect.size.x * 0.12, center.y),
Vector2(rect.end.x - rect.size.x * 0.12, center.y),
Color(color, 0.90),
route_width
)
if blocked:
draw_line(rect.position, rect.end, color, 2.0)
draw_line(Vector2(rect.end.x, rect.position.y), Vector2(rect.position.x, rect.end.y), color, 2.0)
## Visual-only level comes from the existing live FacilityInstance, not the ## Visual-only level comes from the existing live FacilityInstance, not the
@@ -794,10 +885,6 @@ func _draw_world_entity_icons() -> void:
draw_rect(Rect2(rect.end.x-4*p,rect.position.y+2*p,2*p,2*p),_facility_color(instance_id)) draw_rect(Rect2(rect.end.x-4*p,rect.position.y+2*p,2*p,2*p),_facility_color(instance_id))
if instance_id==selected_entity_id: if instance_id==selected_entity_id:
draw_rect(rect.grow(-p),Color("f5d582"),false,2*p) draw_rect(rect.grow(-p),Color("f5d582"),false,2*p)
var data: Dictionary = _room_presentations.get(instance_id, {})
if not data.is_empty() and (rect.size.x >= 80 or instance_id == selected_entity_id) and id not in [&"guest_corridor", &"service_corridor", &"guest_stairs", &"service_stairs", &"guest_elevator", &"service_elevator"]:
var status := RoomPresentation.label(data, RoomPresentation.slots(rect, p, id).size())
_draw_readable_badge(rect.position+Vector2(2, 2), status.replace("利用", "治療") if id == &"infirmary" else status.replace("利用", "休憩") if id in [&"staff_waiting_room", &"staff_dormitory"] else status)
func _draw_readable_badge(at: Vector2, text: String) -> void: func _draw_readable_badge(at: Vector2, text: String) -> void:
@@ -810,13 +897,21 @@ func _draw_readable_badge(at: Vector2, text: String) -> void:
func _prepare_room_presentations() -> void: func _prepare_room_presentations() -> void:
_room_presentations.clear() var model := _observe_rooms()
_carried_patients.clear() _room_presentations = model[&"rooms"]
_treated_patients.clear() _carried_patients = model[&"carried_patients"]
_resting_staff.clear() _treated_patients = model[&"treated_patients"]
_resting_staff = model[&"resting_staff"]
func _observe_rooms() -> Dictionary:
var _room_presentations: Dictionary = {}
var _carried_patients: Dictionary = {}
var _treated_patients: Dictionary = {}
var _resting_staff: Dictionary = {}
var service: Variant = _service(&"facility_service") var service: Variant = _service(&"facility_service")
if service == null: if service == null:
return return {&"rooms": _room_presentations, &"carried_patients": _carried_patients, &"treated_patients": _treated_patients, &"resting_staff": _resting_staff}
for instance: Variant in service.get_instances(): for instance: Variant in service.get_instances():
if instance.origin.z == layer_mode: if instance.origin.z == layer_mode:
_room_presentations[instance.instance_id] = RoomPresentation.describe(instance) _room_presentations[instance.instance_id] = RoomPresentation.describe(instance)
@@ -844,12 +939,22 @@ func _prepare_room_presentations() -> void:
else: else:
_carried_patients[task.target_entity_id] = task.assigned_staff_id _carried_patients[task.target_entity_id] = task.assigned_staff_id
return {&"rooms": _room_presentations, &"carried_patients": _carried_patients, &"treated_patients": _treated_patients, &"resting_staff": _resting_staff}
func _prepare_queues() -> void: func _prepare_queues() -> void:
_queue_positions.clear() var model := _observe_queues()
_queue_hidden.clear() _queue_positions = model[&"queue_positions"]
_landing_badges.clear() _queue_hidden = model[&"queue_hidden"]
_transport_report.clear() _landing_badges.assign(model[&"landing_badges"])
_transport_report = model[&"elevators"]
func _observe_queues() -> Dictionary:
var _queue_positions: Dictionary = {}
var _queue_hidden: Dictionary = {}
var _landing_badges: Array[Dictionary] = []
var _transport_report: Dictionary = {}
var groups: Dictionary = {} var groups: Dictionary = {}
var facilities: Variant = _service(&"facility_service") var facilities: Variant = _service(&"facility_service")
var guests: Variant = _service(&"guest_service") var guests: Variant = _service(&"guest_service")
@@ -908,10 +1013,10 @@ func _prepare_queues() -> void:
index += 1 index += 1
for hidden: int in range(index, members.size()): for hidden: int in range(index, members.size()):
_queue_hidden[members[hidden]] = true _queue_hidden[members[hidden]] = true
var text := "待ち%d人" % members.size() var text := RoomPresentation.queue_label(members.size(), members.size()-index)
if index < members.size(): _landing_badges.append({&"at": get_cell_rect(cell).position + Vector2(-80 if lift_cells.has(cell) else 0, -16), &"text": text, &"cell": cell, &"members": members.duplicate(), &"visible_count": index, &"overflow": members.size()-index})
text += "(他%d人)" % (members.size()-index)
_landing_badges.append({&"at": get_cell_rect(cell).position + Vector2(-80 if lift_cells.has(cell) else 0, -16), &"text": text}) return {&"queue_positions": _queue_positions, &"queue_hidden": _queue_hidden, &"landing_badges": _landing_badges, &"elevators": _transport_report}
func _add_queue_member(groups: Dictionary, key: String, cell: Vector3i) -> void: func _add_queue_member(groups: Dictionary, key: String, cell: Vector3i) -> void:
@@ -1039,14 +1144,21 @@ func _draw_facility_labels() -> void:
func _draw_elevators() -> void: func _draw_elevators() -> void:
_boarded_actor_ids.clear() for row: Dictionary in _life_observation.get(&"cabins", []):
var transport: Variant = _service(&"elevators") PixelArt.elevator_cabin(self, row[&"rect"], row[&"pixel"], layer_mode == LayerMode.BACK, row[&"operational"])
if transport == null or not transport.has_method(&"get_report"): for actor: Dictionary in row[&"passengers"]:
_elevator_tracks.clear() _draw_observed_actor(actor)
return PixelArt.elevator_gate(self, row[&"rect"], row[&"pixel"], row[&"doors_open"])
var report_value: Variant = _transport_report _draw_readable_badge(row[&"badge_at"], row[&"badge"])
if not report_value is Dictionary: for badge: Dictionary in _life_observation.get(&"landing_badges", []):
return _draw_readable_badge(badge[&"at"], badge[&"text"])
func _observe_elevators(observation: Dictionary) -> Array[Dictionary]:
var cabins: Array[Dictionary] = []
var _boarded_actor_ids: Dictionary = observation[&"boarded"]
var _elevator_tracks: Dictionary = observation[&"elevator_tracks"]
var report_value: Dictionary = observation[&"elevators"]
var alive: Dictionary = {} var alive: Dictionary = {}
var p := _cell_dimensions().x/16.0 var p := _cell_dimensions().x/16.0
for value: Variant in report_value.get(&"shafts", []): for value: Variant in report_value.get(&"shafts", []):
@@ -1062,7 +1174,7 @@ func _draw_elevators() -> void:
for actor_id: Variant in passengers: for actor_id: Variant in passengers:
_boarded_actor_ids[_elevator_actor_key(String(actor_id))] = true _boarded_actor_ids[_elevator_actor_key(String(actor_id))] = true
var floor_value := float(shaft.get(&"visual_floor", shaft.get(&"current_floor", stops[0].y))) var floor_value := float(shaft.get(&"visual_floor", shaft.get(&"current_floor", stops[0].y)))
var visual_floor := _tracked_elevator_floor(shaft_id, floor_value) var visual_floor := _tracked_elevator_floor(shaft_id, floor_value, observation)
if stops[0].z != layer_mode: if stops[0].z != layer_mode:
continue continue
var origin := get_cell_rect(stops[0]) var origin := get_cell_rect(stops[0])
@@ -1074,7 +1186,7 @@ func _draw_elevators() -> void:
var operational := bool(shaft.get(&"operational", true)) var operational := bool(shaft.get(&"operational", true))
var phase := StringName(shaft.get(&"phase", &"IDLE")) var phase := StringName(shaft.get(&"phase", &"IDLE"))
var doors_open := operational and phase in [&"IDLE", &"BOARDING", &"ALIGHTING"] var doors_open := operational and phase in [&"IDLE", &"BOARDING", &"ALIGHTING"]
PixelArt.elevator_cabin(self,cabin,p,layer_mode==LayerMode.BACK,operational) var passengers_drawn: Array[Dictionary] = []
var seat_columns := maxi(int((cabin.size.x / p - 4.0) / 8.0), 1) var seat_columns := maxi(int((cabin.size.x / p - 4.0) / 8.0), 1)
var visible_count := mini(passengers.size(), seat_columns*2) var visible_count := mini(passengers.size(), seat_columns*2)
for index: int in range(visible_count): for index: int in range(visible_count):
@@ -1090,22 +1202,19 @@ func _draw_elevators() -> void:
var x := cabin.position.x + (2.0 + (index % seat_columns + 0.5)*8.0)*p var 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)*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)
_draw_pixel_actor(center,actor_key,is_staff,state,false,carrying,&"",true) passengers_drawn.append(_actor_drawing(center,actor_key,is_staff,state,false,carrying,&"",true,observation))
PixelArt.elevator_gate(self,cabin,p,doors_open) cabins.append({&"shaft_id": shaft_id, &"rect": cabin, &"pixel": p,
var capacity := int(shaft.get(&"capacity", 0)) &"operational": operational, &"doors_open": doors_open,
var load_text := "%d/%d人" % [passengers.size(), capacity] &"passengers": passengers_drawn,
if capacity > 0 and passengers.size() >= capacity: &"badge_at": Vector2(cabin.position.x, cabin.position.y-14),
load_text = "満員 " + load_text &"badge": RoomPresentation.elevator_label(shaft)})
if not operational:
load_text = "停止 " + load_text
_draw_readable_badge(Vector2(cabin.position.x, cabin.position.y-14), load_text)
for badge: Dictionary in _landing_badges:
_draw_readable_badge(badge[&"at"], badge[&"text"])
for shaft_id: Variant in _elevator_tracks.keys(): for shaft_id: Variant in _elevator_tracks.keys():
if not alive.has(shaft_id): if not alive.has(shaft_id):
_elevator_tracks.erase(shaft_id) _elevator_tracks.erase(shaft_id)
return cabins
func _elevator_actor_key(actor_id: String) -> String: func _elevator_actor_key(actor_id: String) -> String:
if actor_id.begins_with("guest:"): if actor_id.begins_with("guest:"):
@@ -1115,7 +1224,8 @@ func _elevator_actor_key(actor_id: String) -> String:
return actor_id return actor_id
func _tracked_elevator_floor(shaft_id: StringName, current_floor: float) -> float: func _tracked_elevator_floor(shaft_id: StringName, current_floor: float, observation: Dictionary = {}) -> float:
var _elevator_tracks: Dictionary = observation.get(&"elevator_tracks", self._elevator_tracks)
if not _elevator_tracks.has(shaft_id): if not _elevator_tracks.has(shaft_id):
_elevator_tracks[shaft_id] = {&"floor":current_floor,&"from":current_floor,&"at":_animation_time} _elevator_tracks[shaft_id] = {&"floor":current_floor,&"from":current_floor,&"at":_animation_time}
var track: Dictionary = _elevator_tracks[shaft_id] var track: Dictionary = _elevator_tracks[shaft_id]
@@ -1131,6 +1241,19 @@ func _tracked_elevator_floor(shaft_id: StringName, current_floor: float) -> floa
func _draw_actors() -> void: func _draw_actors() -> void:
for row: Dictionary in _life_observation.get(&"actor_drawings", []):
_draw_observed_actor(row)
func _observe_actors(observation: Dictionary) -> Array[Dictionary]:
var rows: Array[Dictionary] = []
var _actor_tracks: Dictionary = observation[&"actor_tracks"]
var _boarded_actor_ids: Dictionary = observation[&"boarded"]
var _queue_hidden: Dictionary = observation[&"queue_hidden"]
var _queue_positions: Dictionary = observation[&"queue_positions"]
var _carried_patients: Dictionary = observation[&"carried_patients"]
var _treated_patients: Dictionary = observation[&"treated_patients"]
var _resting_staff: Dictionary = observation[&"resting_staff"]
var counts: Dictionary = {} var counts: Dictionary = {}
var alive: Dictionary = {} var alive: Dictionary = {}
var service_slots: Dictionary = {} var service_slots: Dictionary = {}
@@ -1144,18 +1267,18 @@ func _draw_actors() -> void:
if _boarded_actor_ids.has(key) or _queue_hidden.has(key): if _boarded_actor_ids.has(key) or _queue_hidden.has(key):
_actor_tracks.erase(key) _actor_tracks.erase(key)
continue continue
var position := _tracked_actor_position(key,guest.current_cell) var position := _tracked_actor_position(key,guest.current_cell,observation)
if guest.current_cell.z==layer_mode: if guest.current_cell.z==layer_mode:
var receiving := not String(guest.current_service_id).is_empty() var receiving := not String(guest.current_service_id).is_empty()
if _queue_positions.has(key): if _queue_positions.has(key):
position = _queue_positions[key] position = _queue_positions[key]
elif receiving: elif receiving:
position = _service_use_position(guest,position,service_slots) position = _service_use_position(guest,position,service_slots,observation)
if not position.is_finite(): if not position.is_finite():
continue continue
else: else:
position += _actor_offset(guest.current_cell,counts) position += _actor_offset(guest.current_cell,counts)
_draw_pixel_actor(position,key,false,int(guest.state),receiving,false,_guest_activity(guest)) rows.append(_actor_drawing(position,key,false,int(guest.state),receiving,false,_guest_activity(guest),false,observation))
var staff_service: Variant = _service(&"staff_service") var staff_service: Variant = _service(&"staff_service")
if staff_service != null and staff_service.has_method(&"get_staff_members"): if staff_service != null and staff_service.has_method(&"get_staff_members"):
for staff: Variant in staff_service.call(&"get_staff_members"): for staff: Variant in staff_service.call(&"get_staff_members"):
@@ -1166,27 +1289,29 @@ func _draw_actors() -> void:
if _boarded_actor_ids.has(key) or _queue_hidden.has(key): if _boarded_actor_ids.has(key) or _queue_hidden.has(key):
_actor_tracks.erase(key) _actor_tracks.erase(key)
continue continue
var position := _tracked_actor_position(key,staff.current_cell) var position := _tracked_actor_position(key,staff.current_cell,observation)
if staff.current_cell.z==layer_mode: if staff.current_cell.z==layer_mode:
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:
position = _tracked_actor_position("staff/"+String(carrier.staff_id), carrier.current_cell) - Vector2(0, 12*_cell_dimensions().x/16.0) position = _tracked_actor_position("staff/"+String(carrier.staff_id), carrier.current_cell,observation) - Vector2(0, 12*_cell_dimensions().x/16.0)
_draw_pixel_actor(position,key,true,int(staff.state),false,false) rows.append(_actor_drawing(position,key,true,int(staff.state),false,false,&"",false,observation))
continue continue
if _resting_staff.has(staff.staff_id): if _resting_staff.has(staff.staff_id):
position = _resting_staff_position(staff.staff_id, position) position = _resting_staff_position(staff.staff_id, position,observation)
if position.is_finite(): _draw_pixel_actor(position,key,true,int(staff.state),false,false) if position.is_finite(): rows.append(_actor_drawing(position,key,true,int(staff.state),false,false,&"",false,observation))
continue continue
if _treated_patients.has(staff.staff_id): if _treated_patients.has(staff.staff_id):
position = _rescue_treatment_position(staff.staff_id, position) position = _rescue_treatment_position(staff.staff_id, position,observation)
if position.is_finite(): _draw_pixel_actor(position,key,true,int(staff.state),false,false) if position.is_finite(): rows.append(_actor_drawing(position,key,true,int(staff.state),false,false,&"",false,observation))
continue continue
_draw_pixel_actor(_queue_positions.get(key, _staff_work_position(staff, position+_actor_offset(staff.current_cell,counts))),key,true,int(staff.state),false,not String(staff.carried_inventory_id).is_empty(),_staff_work_animation(staff)) rows.append(_actor_drawing(_queue_positions.get(key, _staff_work_position(staff, position+_actor_offset(staff.current_cell,counts),observation)),key,true,int(staff.state),false,not String(staff.carried_inventory_id).is_empty(),_staff_work_animation(staff),false,observation))
for key: Variant in _actor_tracks.keys(): for key: Variant in _actor_tracks.keys():
if not alive.has(key): if not alive.has(key):
_actor_tracks.erase(key) _actor_tracks.erase(key)
return rows
func _staff_work_animation(staff: StaffRuntime) -> StringName: func _staff_work_animation(staff: StaffRuntime) -> StringName:
if staff.role_id != &"cleaner": if staff.role_id != &"cleaner":
@@ -1201,7 +1326,8 @@ func _staff_work_animation(staff: StaffRuntime) -> StringName:
return &"idle" return &"idle"
func _staff_work_position(staff: Variant, fallback: Vector2) -> Vector2: func _staff_work_position(staff: Variant, fallback: Vector2, observation: Dictionary = {}) -> Vector2:
var _room_presentations: Dictionary = observation.get(&"rooms", self._room_presentations)
if staff.state not in [StaffRuntime.State.WORKING, StaffRuntime.State.AVAILABLE]: if staff.state not in [StaffRuntime.State.WORKING, StaffRuntime.State.AVAILABLE]:
return fallback return fallback
var service: Variant = _service(&"facility_service") var service: Variant = _service(&"facility_service")
@@ -1227,7 +1353,8 @@ func _staff_work_position(staff: Variant, fallback: Vector2) -> Vector2:
return Vector2(rect.position.x+(slot+0.5)*rect.size.x/columns, rect.position.y+26*p-_cell_dimensions().y*0.5) return Vector2(rect.position.x+(slot+0.5)*rect.size.x/columns, rect.position.y+26*p-_cell_dimensions().y*0.5)
func _tracked_actor_position(key: String, cell: Vector3i) -> Vector2: func _tracked_actor_position(key: String, cell: Vector3i, observation: Dictionary = {}) -> Vector2:
var _actor_tracks: Dictionary = observation.get(&"actor_tracks", self._actor_tracks)
if not _actor_tracks.has(key): if not _actor_tracks.has(key):
_actor_tracks[key] = {&"cell":cell,&"from":cell,&"at":_animation_time,&"direction":1.0} _actor_tracks[key] = {&"cell":cell,&"from":cell,&"at":_animation_time,&"direction":1.0}
var track: Dictionary = _actor_tracks[key] var track: Dictionary = _actor_tracks[key]
@@ -1246,7 +1373,8 @@ func _tracked_actor_position(key: String, cell: Vector3i) -> Vector2:
return cell_to_canvas(track[&"from"]).lerp(cell_to_canvas(cell),t) return cell_to_canvas(track[&"from"]).lerp(cell_to_canvas(cell),t)
func _service_use_position(guest: Variant, fallback: Vector2, _slots: Dictionary) -> Vector2: func _service_use_position(guest: Variant, fallback: Vector2, _slots: Dictionary, observation: Dictionary = {}) -> Vector2:
var _room_presentations: Dictionary = observation.get(&"rooms", self._room_presentations)
# Guest paths stop at an access cell. While a service is active, project the # Guest paths stop at an access cell. While a service is active, project the
# occupant into that facility's use position; authoritative paths stay intact. # occupant into that facility's use position; authoritative paths stay intact.
var id := StringName(guest.target_facility_id) var id := StringName(guest.target_facility_id)
@@ -1281,6 +1409,19 @@ func _guest_activity(guest: Variant) -> StringName:
func _draw_pixel_actor(center: Vector2, key: String, staff: bool, state: int, receiving: bool, carrying: bool, activity: StringName = &"", riding := false) -> void: func _draw_pixel_actor(center: Vector2, key: String, staff: bool, state: int, receiving: bool, carrying: bool, activity: StringName = &"", riding := false) -> void:
_draw_observed_actor(_actor_drawing(center, key, staff, state, receiving, carrying, activity, riding))
func _draw_observed_actor(row: Dictionary) -> void:
var at: Vector2 = row[&"at"]
var p: float = row[&"scale"]
draw_rect(Rect2(at + Vector2(5, 17) * p, Vector2(8, 1) * p), Color("303c40", 0.2))
People.draw(self, at, p, row[&"mode"], row[&"frame"], row[&"variant"], row[&"staff"], row[&"flip"])
func _actor_drawing(center: Vector2, key: String, staff: bool, state: int, receiving: bool, carrying: bool, activity: StringName = &"", riding := false, observation: Dictionary = {}) -> Dictionary:
var _actor_tracks: Dictionary = observation.get(&"actor_tracks", self._actor_tracks)
var _queue_positions: Dictionary = observation.get(&"queue_positions", self._queue_positions)
var world_pixel := _cell_dimensions().x / 16.0 var world_pixel := _cell_dimensions().x / 16.0
var scale := 0.5 if riding else 0.75 var scale := 0.5 if riding else 0.75
var p := maxf(floorf(world_pixel * scale), 1.0) if world_pixel >= 2.0 else world_pixel * scale var p := maxf(floorf(world_pixel * scale), 1.0) if world_pixel >= 2.0 else world_pixel * scale
@@ -1307,8 +1448,10 @@ func _draw_pixel_actor(center: Vector2, key: String, staff: bool, state: int, re
elif _queue_positions.has(key) or (not staff and state == GuestRuntime.State.QUEUING): elif _queue_positions.has(key) or (not staff and state == GuestRuntime.State.QUEUING):
mode = &"wait" mode = &"wait"
var at := (center + Vector2(-8 * p, _cell_dimensions().y * 0.5 - 3 * world_pixel - 17 * p)).round() var at := (center + Vector2(-8 * p, _cell_dimensions().y * 0.5 - 3 * world_pixel - 17 * p)).round()
draw_rect(Rect2(at + Vector2(5, 17) * p, Vector2(8, 1) * p), Color("303c40", 0.2)) return {&"key": key, &"center": center, &"at": at, &"scale": p, &"mode": mode,
People.draw(self, at, p, mode, frame, variant, staff, float(track.get(&"direction", 1.0)) < 0.0) &"frame": frame, &"variant": variant, &"staff": staff, &"state": state,
&"receiving": receiving, &"carrying": carrying, &"activity": activity,
&"riding": riding, &"flip": float(track.get(&"direction", 1.0)) < 0.0}
func _draw_excavation_jobs() -> void: func _draw_excavation_jobs() -> void:
@@ -1421,14 +1564,14 @@ func _facility_color(instance_id: StringName) -> Color:
var instance: Variant = facility_service.call(&"get_instance", instance_id) var instance: Variant = facility_service.call(&"get_instance", instance_id)
if instance == null: if instance == null:
return Color("6f8b67") if layer_mode == LayerMode.FRONT else Color("427582") return Color("6f8b67") if layer_mode == LayerMode.FRONT else Color("427582")
match int(instance.state): match int(_room_presentations.get(instance_id, {}).get(&"state", instance.state)):
0: FacilityInstance.State.BROKEN:
return Color("a24b45") return Color("a24b45")
1, 2, 3: FacilityInstance.State.NO_ACCESS, FacilityInstance.State.NO_POWER, FacilityInstance.State.NO_STAFF:
return Color("a77d3f") return Color("a77d3f")
4: FacilityInstance.State.CLOSED:
return Color("6b6258") return Color("6b6258")
5: FacilityInstance.State.DEGRADED:
return Color("89914d") return Color("89914d")
_: _:
return Color("6f9a55") if layer_mode == LayerMode.FRONT else Color("67518e") return Color("6f9a55") if layer_mode == LayerMode.FRONT else Color("67518e")
@@ -1579,7 +1722,9 @@ func _normalized_rotation(value: int) -> int:
return posmod(value, 360) / 90 * 90 return posmod(value, 360) / 90 * 90
func _rescue_treatment_position(patient_id: StringName, fallback: Vector2) -> Vector2: func _rescue_treatment_position(patient_id: StringName, fallback: Vector2, observation: Dictionary = {}) -> Vector2:
var _room_presentations: Dictionary = observation.get(&"rooms", self._room_presentations)
var _treated_patients: Dictionary = observation.get(&"treated_patients", self._treated_patients)
var facility_id: StringName = _treated_patients[patient_id] var facility_id: StringName = _treated_patients[patient_id]
var world: Variant = _service(&"world_service") var world: Variant = _service(&"world_service")
var cells: Array[Vector3i] = world.get_entity_cells(facility_id) if world != null else [] var cells: Array[Vector3i] = world.get_entity_cells(facility_id) if world != null else []
@@ -1592,7 +1737,9 @@ func _rescue_treatment_position(patient_id: StringName, fallback: Vector2) -> Ve
return anchors[slot]+Vector2(0,-2*p-_cell_dimensions().y*0.5) return anchors[slot]+Vector2(0,-2*p-_cell_dimensions().y*0.5)
func _resting_staff_position(staff_id: StringName, fallback: Vector2) -> Vector2: func _resting_staff_position(staff_id: StringName, fallback: Vector2, observation: Dictionary = {}) -> Vector2:
var _room_presentations: Dictionary = observation.get(&"rooms", self._room_presentations)
var _resting_staff: Dictionary = observation.get(&"resting_staff", self._resting_staff)
var facility_id: StringName = _resting_staff[staff_id] var facility_id: StringName = _resting_staff[staff_id]
var world: Variant = _service(&"world_service") var world: Variant = _service(&"world_service")
var cells: Array[Vector3i] = world.get_entity_cells(facility_id) if world != null else [] var cells: Array[Vector3i] = world.get_entity_cells(facility_id) if world != null else []
+48 -1
View File
@@ -10,7 +10,9 @@ static func describe(instance: Variant) -> Dictionary:
if not ids.has(id): if not ids.has(id):
ids.append(id) ids.append(id)
ids.sort_custom(func(a: StringName, b: StringName) -> bool: return String(a) < String(b)) ids.sort_custom(func(a: StringName, b: StringName) -> bool: return String(a) < String(b))
return {&"operation": int(instance.operation_state), &"people": ids, return {&"state": int(instance.state), &"state_name": FacilityInstance.State.keys()[instance.state],
&"reasons": instance.state_reasons.duplicate(),
&"operation": int(instance.operation_state), &"people": ids,
&"count": ids.size(), &"limit": int(instance.customer_capacity), &"count": ids.size(), &"limit": int(instance.customer_capacity),
&"staff": instance.active_staff_ids.duplicate(), &"suspension": StringName(instance.suspension_reason)} &"staff": instance.active_staff_ids.duplicate(), &"suspension": StringName(instance.suspension_reason)}
@@ -50,3 +52,48 @@ static func label(data: Dictionary, visible_count: int) -> String:
if count > visible_count: if count > visible_count:
value += "(他%d人)" % (count - visible_count) value += "(他%d人)" % (count - visible_count)
return value return value
static func business_label(data: Dictionary) -> String:
if data.is_empty():
return ""
if int(data[&"operation"]) == FacilityInstance.OperationState.SUSPENDED:
return "休止・摘発" if data[&"suspension"] == &"RAIDED" else "休止・破壊"
match int(data[&"state"]):
FacilityInstance.State.BROKEN: return "破損"
FacilityInstance.State.NO_ACCESS: return "通路なし"
FacilityInstance.State.NO_POWER: return "魔力なし"
FacilityInstance.State.NO_STAFF: return "職員不足"
FacilityInstance.State.CLOSED: return "営業停止"
FacilityInstance.State.DEGRADED: return "低下運転"
FacilityInstance.State.OPEN: return "営業中"
return String(data[&"state_name"])
static func queue_label(count: int, overflow: int) -> String:
return "待ち%d人" % count + ("(他%d人)" % overflow if overflow > 0 else "")
static func elevator_label(shaft: Dictionary) -> String:
var count := (shaft[&"boarded"] as Array).size()
var capacity := int(shaft[&"capacity"])
var value := "%d/%d人" % [count, capacity]
if capacity > 0 and count >= capacity:
value = "満員 " + value
if not bool(shaft[&"operational"]):
value = "停止 " + value
return value
static func supply_label(requests: Array[Dictionary]) -> String:
# These are requests, never invented stock quantities. Preserve actual states.
var states: Dictionary = {}
for request: Dictionary in requests:
var state := String(request[&"state"])
states[state] = int(states.get(state, 0)) + 1
var names: Array = states.keys()
names.sort()
var parts: PackedStringArray = []
for state: String in names:
parts.append("%s:%d" % [state, states[state]])
return "物資依頼 " + " / ".join(parts)
+165
View File
@@ -0,0 +1,165 @@
extends RefCounted
## Sparse whole typed captures. No acceptance flags and no truncation.
const Codec := preload("res://tests/fixtures/p08-02/mixed_world.gd")
var directory: String
var manifest: FileAccess
var observer: MotionObserver
class MotionObserver extends Node:
var view: WorldPreview
var frames: Array = []
var elapsed := 0.0
var scaled_elapsed := 0.0
func _ready() -> void:
RenderingServer.frame_post_draw.connect(after_draw)
view.draw.connect(on_view_draw)
func _process(delta: float) -> void:
elapsed += delta
var clock := get_node_or_null("/root/GameClock")
if clock == null or not bool(clock.get("paused")):
scaled_elapsed += delta * maxf(float(clock.get("speed")) if clock != null else 1.0, 0.0)
func on_view_draw() -> void:
# Read the draw-consumed report after the real _draw completes.
after_draw.call_deferred()
func display_sample(report: Dictionary) -> Dictionary:
var sample := {}
for key: String in ["animation_time", "paused", "layer", "zoom", "pan", "viewport", "actor_tracks", "elevator_tracks", "actor_drawings", "cabins"]:
sample[key] = report[key]
return sample
func after_draw() -> void:
if not is_instance_valid(view): return
var frame := Engine.get_process_frames()
if not frames.is_empty() and frame <= int(frames[-1]["frame"]): return
var report := view.get_life_observation_report()
if report.is_empty(): return
var sample := display_sample(report)
if not frames.is_empty() and sample == frames[-1]["display"] and not sample["paused"]: return
# Keep only two draw frames, never whole Runtime copies per frame.
frames.append({"frame": frame, "delta": elapsed, "scaled_delta": scaled_elapsed, "display": sample})
if frames.size() > 2: frames.pop_front()
elapsed = 0.0
scaled_elapsed = 0.0
func _exit_tree() -> void:
if is_instance_valid(view) and view.draw.is_connected(on_view_draw): view.draw.disconnect(on_view_draw)
if RenderingServer.frame_post_draw.is_connected(after_draw): RenderingServer.frame_post_draw.disconnect(after_draw)
func watch(view: WorldPreview) -> void:
observer = MotionObserver.new()
observer.view = view
view.add_child(observer)
func await_draws(tree: SceneTree, view: WorldPreview, n: int = 2, bound: int = 120) -> bool:
var start := Engine.get_process_frames()
for unused: int in range(bound):
view.queue_redraw()
await tree.process_frame
if not is_instance_valid(observer): break
var report := view.get_life_observation_report()
if observer.frames.size() == n and not report.is_empty() and int(observer.frames[0]["frame"]) > start and observer.frames[-1]["display"] == observer.display_sample(report):
return true
# Persist timeout as a failure, never a skip.
record("draw-history-timeout", {"start_frame": start, "bound": bound, "required_frames": n})
return false
func open(path: String) -> bool:
directory = path
if DirAccess.dir_exists_absolute(path):
return false
if DirAccess.make_dir_recursive_absolute(path.path_join("objects")) != OK:
return false
manifest = FileAccess.open(path.path_join("manifest.jsonl"), FileAccess.WRITE)
return manifest != null
func object(value: Variant) -> String:
var text := JSON.stringify(Codec.new().typed(value)) + "\n"
var digest := text.sha256_text()
var path := directory.path_join("objects").path_join(digest + ".json")
if FileAccess.file_exists(path):
return digest if FileAccess.get_sha256(path) == digest else ""
var file := FileAccess.open(path, FileAccess.WRITE)
if file == null:
return ""
file.store_string(text)
file.close()
return digest if FileAccess.get_sha256(path) == digest else ""
func capture(label: String, runtime: Variant, view: WorldPreview) -> bool:
if manifest == null:
return false
var state: Dictionary = runtime.create_snapshot()
var report := view.get_life_observation_report()
var state_hash := object(state)
var report_hash := object(report)
var audit_hash := object(rest_audit(runtime))
var motion_hash := object({"frames": observer.frames.duplicate(true) if is_instance_valid(observer) else []})
var after: Dictionary = runtime.create_snapshot()
var after_hash := object(after)
if state_hash.is_empty() or report_hash.is_empty() or audit_hash.is_empty():
return false
manifest.store_line(JSON.stringify({"label": label, "pid": OS.get_process_id(),
"ticks_usec": Time.get_ticks_usec(), "state": state_hash, "report": report_hash,
"rest_audit": audit_hash, "motion": motion_hash, "state_after": after_hash,
"input_method": "diagnostic", "acceptance": "pending"}))
manifest.flush()
return var_to_bytes(state) == var_to_bytes(after)
func close() -> void:
if manifest != null:
manifest.close()
manifest = null
# Public selection inputs; Runtime output is only a comparison operand.
func rest_audit(runtime: Variant) -> Dictionary:
var rows: Array = []
for member: StaffRuntime in runtime.staff_service.get_staff_members():
var memberships: Array = []
for raid_id: StringName in runtime.defense_service.get_raid_ids():
var raid: Dictionary = runtime.defense_service.get_raid(raid_id)
if raid.get(&"combatants", {}).has(member.staff_id):
memberships.append({"raid_id": raid_id, "state": DefenseService.RaidState.keys()[raid[&"state"]],
"side": raid[&"combatants"][member.staff_id].get(&"side", &"")})
var facilities: Array = []
for room: FacilityInstance in runtime.facility_service.get_instances():
var probes: Array = []
var candidates: Dictionary = {}
var occupied := room.occupied_cells.duplicate()
if occupied.is_empty(): occupied.append(room.origin)
for cell: Vector3i in occupied:
for delta: Vector3i in [Vector3i.ZERO, Vector3i.LEFT, Vector3i.RIGHT]:
var present: bool = runtime.path_service.has_cell(cell + delta)
probes.append({"cell": cell + delta, "has_cell": present})
if present: candidates[cell + delta] = true
var ordered: Array = candidates.keys()
ordered.sort_custom(func(a: Vector3i, b: Vector3i) -> bool:
if a.z != b.z: return a.z < b.z
if a.y != b.y: return a.y < b.y
return a.x < b.x)
var routes: Array = []
for cell: Vector3i in ordered:
var path: Array = runtime.path_service.find_path(member.current_cell, cell,
[WorldService.LAYER_FRONT, WorldService.LAYER_BACK], [&"staff"])
var edges: Array = []
for index: int in range(1, path.size()):
# Empty reports remain explicit mismatch evidence.
edges.append(runtime.path_service.get_edge_report(path[index - 1], path[index], [&"staff"]))
routes.append({"cell": cell, "path": path, "edges": edges,
"calculate_path_cost": runtime.path_service.calculate_path_cost(path, [&"staff"]) if not path.is_empty() else -1})
facilities.append({"instance_id": room.instance_id, "definition_id": room.definition_id,
"operation_state": FacilityInstance.OperationState.keys()[room.operation_state],
"origin": room.origin, "occupied_cells": room.occupied_cells.duplicate(),
"probes": probes, "ordered": ordered, "routes": routes})
rows.append({"staff_id": member.staff_id, "state": StaffRuntime.State.keys()[member.state],
"health": member.health, "current_task_id": member.current_task_id, "current_cell": member.current_cell,
"defense_memberships": memberships, "actor_lock": runtime.event_service.get_actor_locks().has(member.staff_id),
"in_transit": runtime.is_staff_in_transit(member.staff_id), "facilities": facilities,
"comparison_only": runtime.get_staff_rest_report(member.staff_id)})
return {"staff": rows, "path_service_sha256": FileAccess.get_sha256("res://scripts/world/path_service.gd")}
func record(label: String, value: Dictionary) -> bool:
if manifest == null: return false
var digest := object(value)
if digest.is_empty(): return false
manifest.store_line(JSON.stringify({"label": label, "pid": OS.get_process_id(),
"ticks_usec": Time.get_ticks_usec(), "object": digest}))
manifest.flush()
return true
+1
View File
@@ -0,0 +1 @@
uid://d23lifestr03
+102
View File
@@ -0,0 +1,102 @@
extends RefCounted
## Public-command experiment; no synthetic state, costs or capacities.
## Predeclared metric: total destination-arrival minutes of the entire cohort.
## B must be strictly smaller. Incomplete cohorts cannot establish improvement.
const Stream := preload("res://tests/fixtures/p23-03/diagnostic_stream.gd")
const SEED := 806042
const WINDOW := 480
const START := Vector3i(2, 0, 0)
const GOAL := Vector3i(2, 1, 0)
var stream: RefCounted
func declaration() -> Dictionary:
var rng := RandomNumberGenerator.new()
rng.seed = SEED
var arrivals: Array[Dictionary] = []
for index: int in range(48):
arrivals.append({&"id": StringName("p23-%03d" % index),
&"offset": (index / 4) * 15 + rng.randi_range(0, 2)})
arrivals.sort_custom(func(a: Dictionary, b: Dictionary) -> bool:
return a[&"offset"] < b[&"offset"] or (a[&"offset"] == b[&"offset"] and String(a[&"id"]) < String(b[&"id"])))
return {&"seed": SEED, &"initial_clock": [1, 6, 0], &"budget": &"GameState.reset",
&"production": &"Runtime.initialize defaults; production operations not enabled",
&"arrivals": arrivals, &"from": START, &"to": GOAL, &"window": WINDOW,
&"metric": &"sum_destination_arrival_minutes_entire_cohort",
&"improvement": {&"definition_id": &"guest_elevator", &"origin": Vector3i(4, 0, 0)}}
func run(tree: SceneTree, directory: String, layout: String) -> bool:
if layout not in ["A", "B"]: return false
var global_clock := tree.root.get_node_or_null("GameClock")
if global_clock != null: global_clock.set_paused(true)
stream = Stream.new()
if not stream.open(directory): return false
var input := declaration()
if not stream.record("declaration", input):
stream.close()
return false
var runtime: Variant = load("res://autoload/simulation_runtime.gd").new()
var clock: Variant = load("res://autoload/game_clock.gd").new()
var state: Variant = load("res://autoload/game_state.gd").new()
clock.reset(1, 6, 0)
state.reset()
var ok: bool = runtime.initialize(clock, state, SEED)[&"valid"]
clock.set_paused(true)
runtime.configure_save_policy({&"enabled": false, &"daily_checkpoint_enabled": false})
ok = stream.record("initial", runtime.create_snapshot()) and ok
var builds: Array = [[&"guest_elevator", Vector3i.ZERO],
[&"guest_corridor", Vector3i(2, 1, 0)], [&"guest_corridor", Vector3i(3, 1, 0)]]
if layout == "B": builds.append([input[&"improvement"][&"definition_id"], input[&"improvement"][&"origin"]])
var construction_minutes := 0
# Identical preparation time derived from the same registered definitions.
for id: StringName in [&"guest_elevator", &"guest_corridor"]:
construction_minutes = maxi(construction_minutes, runtime.definition_registry.get_facility(id).build_minutes)
for index: int in range(builds.size()):
var payload := {&"definition_id": builds[index][0], &"origin": builds[index][1]}
var result: CommandResult = runtime.submit_command(&"build_facility", payload,
&"player", &"", 0, clock.absolute_minute, StringName("p23.build.%d" % index))
ok = stream.record("build-%d" % index, {&"payload": payload, &"result": result.to_dictionary()}) and result.is_success() and ok
clock.advance_minutes(construction_minutes + 1)
ok = stream.record("prepared", runtime.create_snapshot()) and ok
runtime.domain_events.subscribe(&"*", &"p23.capture", Callable(self, "_event"))
var view := WorldPreview.new()
view.size = Vector2(1440, 810)
view.set_runtime(runtime)
tree.root.add_child(view)
stream.watch(view)
var finished: Dictionary = {}
for offset: int in range(WINDOW + 1):
if not ok: break
if offset > 0: clock.advance_minutes(1)
for arrival: Dictionary in input[&"arrivals"]:
if arrival[&"offset"] != offset: continue
var payload := {&"guest_id": arrival[&"id"], &"guest_type_id": &"traveler", &"gold": 100,
&"options": {&"current_cell": START, &"state": GuestRuntime.State.MOVING,
&"state_entered_at": clock.absolute_minute - 1, &"planned_exit_at": clock.absolute_minute + 2000,
&"path_handle": {&"target_cell": GOAL}}}
var result: CommandResult = runtime.submit_command(&"spawn_guest", payload,
&"world", arrival[&"id"], 10, -1, StringName("p23.spawn.%s" % arrival[&"id"]))
ok = stream.record("arrival-%s" % arrival[&"id"], {&"offset": offset, &"payload": payload, &"result": result.to_dictionary()}) and result.is_success() and ok
# Typed per-minute projections retain paths, queues, density and actual
# completed services. Whole aggregate snapshots are only sampled sparsely.
var guest_records: Dictionary = runtime.guest_service.create_snapshot()
ok = stream.record("tick-%03d" % offset, {&"minute": clock.absolute_minute,
&"guests": guest_records, &"path": runtime.path_service.create_snapshot(),
&"elevators": runtime.elevators.create_snapshot(), &"facilities": runtime.facility_service.create_snapshot()}) and ok
for guest: Dictionary in guest_records[&"guests"]:
if guest[&"current_cell"] != [GOAL.x, GOAL.y, GOAL.z] or finished.has(guest[&"guest_id"]): continue
finished[guest[&"guest_id"]] = offset
if offset % 30 == 0:
var drawn: bool = await stream.await_draws(tree, view)
ok = drawn and stream.capture("frame-%03d" % offset, runtime, view) and ok
ok = stream.record("first-arrivals", {&"offsets": finished}) and ok
ok = stream.record("final", runtime.create_snapshot()) and ok
stream.close()
view.free()
runtime.free()
clock.free()
state.free()
return ok
func _event(event: SimulationDomainEvent) -> void:
stream.record("event", {&"type": event.event_type, &"minute": event.absolute_minute,
&"entities": event.entity_ids, &"payload": event.payload.duplicate(true)})
+1
View File
@@ -0,0 +1 @@
uid://dtxpm0n6cfom
+164
View File
@@ -0,0 +1,164 @@
extends "res://tests/fixtures/p08-03/placement.gd"
## Real Runtime commands; inherited formal codec/save-operation helpers unchanged.
const Stream := preload("res://tests/fixtures/p23-03/diagnostic_stream.gd")
const Comparison := preload("res://tests/fixtures/p23-03/layout_comparison.gd")
var stream: RefCounted
var layout := "A"
var facility_ids: Dictionary = {}
func setup() -> void:
clock.reset(1, 6, 0)
var state: Variant = tree.root.get_node("GameState")
state.reset()
expect(runtime.initialize(clock, state, 806042).get(&"valid", false), "initialize observation")
clock.set_paused(true)
tree.root.size = Vector2i(1440, 810)
layout = OS.get_environment("P23_LAYOUT")
expect(layout in ["A", "B"], "declared layout A or B")
main = load("res://scenes/main/main.tscn").instantiate()
main.manual_save_path = "user://saves/manual_ui"
if role == "reader":
import_package(directory.path_join("formal"), String(main.manual_save_path))
tree.root.add_child(main)
runtime.configure_save_policy({&"enabled": false, &"daily_checkpoint_enabled": false})
stream = Stream.new()
expect(stream.open(directory.path_join(role + "-stream")), "fresh observation stream")
stream.watch(main.world_preview)
await frames()
func command(id: StringName, payload: Dictionary) -> bool:
var result: CommandResult = runtime.submit_command(id, payload)
expect(result.is_success(), String(id) + ": " + str(result.code))
expect(stream.record("command", {&"command": id, &"payload": payload,
&"result": result.to_dictionary(), &"minute": clock.absolute_minute}), "record public command")
return result.is_success()
func advance(minutes: int) -> void:
for unused: int in range(minutes):
clock.advance_minutes(1)
await frames()
await observe_progress()
if clock.absolute_minute % 30 == 0:
expect(await stream.await_draws(tree, main.world_preview), "real draw history before capture")
expect(stream.capture("minute-%d" % clock.absolute_minute, runtime, main.world_preview), "sparse minute observation")
func observe_progress() -> void:
pass
func prepare() -> void:
# Both layouts receive the same definition-derived diagnostic construction budget.
# Cafeteria/kitchen move together to x=6, leaving x=4..5 for the declared B EV.
var builds: Array = [[&"reception", Vector3i(-8, 0, 0)],
[&"cafeteria", Vector3i(6, 0, 0)], [&"warehouse", Vector3i(-8, 0, 1)],
[&"kitchen", Vector3i(6, 0, 1)], [&"staff_door", Vector3i(0, 0, 1)],
[&"standard_room", Vector3i(-8, 2, 0)], [&"guest_stairs", Vector3i(-4, 1, 0)],
[&"guest_stairs", Vector3i(-4, 2, 0)], [&"guest_corridor", Vector3i(-5, 2, 0)],
[&"guest_corridor", Vector3i(-6, 2, 0)], [&"guest_elevator", Vector3i.ZERO],
[&"guest_corridor", Vector3i(2, 1, 0)], [&"guest_corridor", Vector3i(3, 1, 0)],
[&"staff_waiting_room", Vector3i(-4, 0, 1)]]
var improvement: Dictionary = Comparison.new().declaration()[&"improvement"]
var budget_builds := builds.duplicate(true)
budget_builds.append([improvement[&"definition_id"], improvement[&"origin"]])
var needed := {&"gold": 0, &"materials": 0}
var costs: Array = []
for row: Array in budget_builds:
var definition: FacilityDefinition = runtime.definition_registry.get_facility(row[0])
costs.append({&"definition": row[0], &"cost": definition.construction_cost.duplicate(true)})
for key: StringName in needed:
needed[key] += int(definition.construction_cost.get(key, 0))
var state: Variant = tree.root.get_node("GameState")
var deltas := {}
for key: StringName in needed:
if needed[key] > state.get_resource(key):
deltas[key] = needed[key] - state.get_resource(key)
budget = {"setup": "diagnostic", "costs": costs, "needed": needed,
"initial": state.resources.duplicate(true), "deltas": deltas}
if not deltas.is_empty(): command(&"change_resources", {&"deltas": deltas})
expect(stream.record("budget", budget), "common diagnostic budget")
if layout == "B": builds.append([improvement[&"definition_id"], improvement[&"origin"]])
# Preserve the supply scenario's staged support/construction sequence.
for index: int in range(builds.size()):
if index == 5: await advance(180)
if index == 7: await advance(60)
if index == 8: await advance(10)
command(&"build_facility", {&"definition_id": builds[index][0], &"origin": builds[index][1]})
var transport_minutes := 0
for id: StringName in [&"guest_elevator", &"guest_corridor"]:
transport_minutes = maxi(transport_minutes, runtime.definition_registry.get_facility(id).build_minutes)
await advance(maxi(10, transport_minutes))
for facility: FacilityInstance in runtime.facility_service.get_instances():
facility_ids[facility.definition_id] = facility.instance_id
command(&"set_facility_conditions", {&"facility_id": facility_ids[&"cafeteria"],
&"conditions": {&"related_facility_id": facility_ids[&"kitchen"]}})
for assignment: Array in [[&"staff_akane", &"reception"], [&"staff_gina", &"warehouse"],
[&"staff_chika", &"kitchen"], [&"staff_iris", &"cafeteria"], [&"staff_juno", &"cafeteria"]]:
command(&"hire_staff_candidate", {&"candidate_id": assignment[0]})
command(&"set_staff_work_assignment", {&"staff_id": assignment[0],
&"facility_id": facility_ids[assignment[1]], &"template_id": &"DAY"})
command(&"purchase_supplies", {&"warehouse_id": facility_ids[&"warehouse"],
&"item_id": SupplyCatalog.RAW_FOOD, &"quantity": 40})
command(&"set_kitchen_production", {&"facility_id": facility_ids[&"kitchen"],
&"enabled": true, &"target_stock": 8})
# The OS observation uses the already declared transport cohort, plus
# ordinary public arrivals whose service choice remains Runtime-owned.
# These commands are recorded and identical across A/B; no actor is moved,
# despawned, or inserted into a room/queue by the fixture.
var demand: Dictionary = Comparison.new().declaration()
expect(stream.record("observation-demand", {"cohort": demand[&"arrivals"],
"schedule": "all observation actors at the recorded current minute; offsets apply only to native layout comparison",
"minute": clock.absolute_minute}), "declared observation demand")
for arrival: Dictionary in demand[&"arrivals"]:
command(&"spawn_guest", {&"guest_id": StringName("os-" + String(arrival[&"id"])),
&"guest_type_id": &"traveler", &"gold": 100,
&"options": {&"current_cell": Comparison.START, &"state": GuestRuntime.State.MOVING,
&"state_entered_at": clock.absolute_minute - 1, &"planned_exit_at": clock.absolute_minute + 2000,
&"path_handle": {&"target_cell": Comparison.GOAL}}})
command(&"spawn_guest", {&"guest_id": StringName("service-" + String(arrival[&"id"])),
&"guest_type_id": &"traveler", &"gold": 100})
await advance(120)
func layer_observations(prefix: String) -> void:
await button(main.front_button)
expect(await stream.await_draws(tree, main.world_preview), "real draw history before capture")
expect(stream.capture(prefix + "-FRONT", runtime, main.world_preview), "FRONT state/report")
await button(main.back_button)
expect(await stream.await_draws(tree, main.world_preview), "real draw history before capture")
expect(stream.capture(prefix + "-BACK", runtime, main.world_preview), "BACK state/report")
func writer() -> void:
await prepare()
await layer_observations("writer")
capture("presave")
await button(main.find_child("ManualSaveButton", true, false))
expect(main.build_status_label.text == "手動セーブを保存しました。", "normal save")
capture("checkpoint")
export_package(String(main.manual_save_path), "formal")
var formal := inspect_formal()
var presave := FileAccess.get_file_as_bytes(directory.path_join("writer-presave.var"))
expect(presave == var_to_bytes(formal), "whole presave formal model")
expect(not operation_results.is_empty(), "save result recorded")
if not operation_results.is_empty():
verify_operation(formal, operation_results.back(), &"SAVE_CHECKPOINT_SUCCEEDED")
var members := {}
for member: String in MEMBERS:
members[member] = FileAccess.get_sha256(directory.path_join("formal/" + member))
json_file("save.json", {"pid": OS.get_process_id(), "members": members})
func reader(distinct: bool) -> void:
var saved: Dictionary = JSON.parse_string(FileAccess.get_file_as_string(directory.path_join("save.json")))
expect(distinct and saved.pid != OS.get_process_id(), "distinct actual reader PID")
await load_menu(main.find_child("SaveLoadMenu", true, false))
await frames()
expect(main.build_status_label.text == "手動セーブを読み込みました。", "normal resume")
capture("checkpoint")
var formal := inspect_formal()
expect(FileAccess.get_file_as_bytes(directory.path_join("writer-formal-model.var")) == var_to_bytes(formal), "whole formal resume model")
for member: String in MEMBERS:
expect(FileAccess.get_sha256(directory.path_join("formal/" + member)) == saved.members[member], "formal member " + member)
expect(not operation_results.is_empty(), "load result recorded")
if not operation_results.is_empty():
verify_operation(formal, operation_results.back(), &"SAVE_RECOVERY_LOADED")
await layer_observations("reader")
func finish_observations() -> void:
stream.close()
+1
View File
@@ -0,0 +1 @@
uid://dmoi8je5hse38
+263
View File
@@ -0,0 +1,263 @@
extends "res://tests/fixtures/p23-03/observation.gd"
## Passive endpoint implementation. All gameplay/input is performed by Root CUA.
## Shared parent contains the handler endpoints, but every such endpoint below
## is overridden; this class never emits UI signals or invokes product handlers.
var receipts: Array = []
var terminated := false
var terminal_record: Dictionary = {}
var terminal_cause_recorded := false
var reveal_rows: Array = []
var all_requests: Array = []
var input_observer: Node
var last_dispatch: Dictionary = {}
var observed_categories: Dictionary = {}
func observe_progress() -> void:
if role != "writer" or terminated: return
var wanted: Dictionary = {}
var report: Dictionary = main.world_preview.get_life_observation_report()
for room: Dictionary in report.get(&"rooms", {}).values():
if int(room.get(&"count", 0)) > 0: wanted["room-users"] = true
for badge: Dictionary in report.get(&"landing_badges", []):
if int(badge.get(&"overflow", 0)) > 0: wanted["queue-overflow"] = true
for cabin: Dictionary in report.get(&"cabins", []):
if not cabin[&"passengers"].is_empty(): wanted["EV-passengers"] = true
var elevators: Dictionary = runtime.elevators.get_report()
for shaft: Dictionary in elevators.get(&"shafts", []):
if not shaft.get(&"boarded", []).is_empty(): wanted["EV-passengers"] = true
for trip: Dictionary in elevators.get(&"trips", []):
if trip.get(&"state", &"") == &"QUEUED": wanted["queue-present"] = true
for member: StaffRuntime in runtime.staff_service.get_staff_members():
if not member.carried_inventory_id.is_empty(): wanted["carried-goods"] = true
if not report.get(&"resting_staff", {}).is_empty(): wanted["rest-position"] = true
for category: String in wanted:
if observed_categories.has(category): continue
observed_categories[category] = true
await layer_observations("actual-" + category)
if category == "EV-passengers": await motion_observations()
func motion_observations() -> void:
await button(main.front_button)
await button(main.pause_button)
expect(not clock.paused, "ordinary resume for motion observation")
var minute: int = clock.absolute_minute
while clock.absolute_minute == minute and not terminated:
await tree.process_frame
await frames()
expect(await stream.await_draws(tree, main.world_preview), "real draw history before capture")
expect(stream.capture("active-motion", runtime, main.world_preview), "active interpolation history")
await button(main.pause_button)
expect(clock.paused, "ordinary pause for stable observation")
await frames()
expect(await stream.await_draws(tree, main.world_preview), "real draw history before capture")
expect(stream.capture("paused-motion", runtime, main.world_preview), "paused interpolation history")
class InputObserver extends Node:
var events: Array = []
var endpoint: RefCounted = null
func _input(event: InputEvent) -> void:
if event is InputEventMouseButton or event is InputEventKey or event is InputEventMouseMotion:
var row := {"ticks_usec": Time.get_ticks_usec(), "text": event.as_text(), "pressed": event.is_pressed()}
if event is InputEventMouseButton:
row["button"] = event.button_index
row["position"] = [event.position.x, event.position.y]
row["ctrl"] = event.ctrl_pressed
if endpoint != null and event.pressed and event.button_index == MOUSE_BUTTON_LEFT:
row["dispatch"] = endpoint.dispatch_position(event.position)
events.append(row)
func expect(ok: bool, message: String) -> void:
if terminated and ok:
return
if terminated and terminal_cause_recorded:
if not ok:
terminal_record["suppressed_after_terminal"] += 1
json_file(role + "-terminal.json", terminal_record)
return
if terminated and not ok:
terminal_cause_recorded = true
super.expect(ok, message)
func terminate(stem: String, reason: String) -> void:
terminated = true
terminal_record = {"pid": OS.get_process_id(), "role": role, "sequence": serial,
"stem": stem, "reason": reason, "suppressed_after_terminal": 0}
json_file(role + "-terminal.json", terminal_record)
expect(false, reason)
tree.quit(1)
func request(action: Dictionary) -> void:
if terminated:
return
serial += 1
var stem := "%s-request-%04d" % [role, serial]
var folder := directory.path_join(stem)
DirAccess.make_dir_recursive_absolute(folder)
var window: Window = main.get_window()
var view: WorldPreview = main.world_preview
expect(await stream.await_draws(tree, view), "real draw history before capture")
action["report"] = Codec.new().typed(view.get_life_observation_report())
action["viewport_to_screen"] = affine(view.get_screen_transform() * view.get_global_transform_with_canvas().affine_inverse())
action["local_to_viewport"] = affine(view.get_global_transform_with_canvas())
action["local_to_screen"] = affine(view.get_screen_transform())
action["rectangles"] = Codec.new().typed(draw_rectangles(view.get_life_observation_report(), view))
action["requested_ticks_usec"] = Time.get_ticks_usec()
action["requested_unix_ns"] = int(Time.get_unix_time_from_system() * 1000000000.0)
action["screenshot_receipt_schema"] = {"screenshots": [{"path": "evidence-root-relative raw image path", "raw_sha256": "original byte SHA256", "tool_call_id": "actual CUA screenshot call ID"}]}
expect(stream.capture(stem, runtime, view), "request state/report")
action.merge({"sequence": serial, "target_pid": OS.get_process_id(), "role": role,
"state": "waiting_for_root_cua", "window_title": window.title,
"window_position": [window.position.x, window.position.y],
"window_size": [window.size.x, window.size.y], "screen_scale": DisplayServer.screen_get_scale(window.current_screen),
"content_scale_factor": window.content_scale_factor, "setup": "diagnostic"})
json_file(stem + "/request.json", action)
var path := folder.path_join("ack.json")
var first_event: int = input_observer.events.size()
var deadline := Time.get_ticks_msec() + 300000
print("P23_ROOT_CUA_WAIT " + folder.path_join("request.json"))
while not FileAccess.file_exists(path) and Time.get_ticks_msec() < deadline:
await tree.process_frame
if not FileAccess.file_exists(path):
terminate(stem, "Root CUA acknowledgement timeout")
return
var ack: Variant = JSON.parse_string(FileAccess.get_file_as_string(path))
if not ack is Dictionary or ack.get("target_pid") != OS.get_process_id() or ack.get("sequence") != serial or ack.get("request_sha256") != FileAccess.get_sha256(folder.path_join("request.json")):
terminate(stem, "Root CUA acknowledgement identity/hash")
return
await frames()
for event: Dictionary in input_observer.events.slice(first_event):
if event.has("dispatch"):
last_dispatch = event["dispatch"]
json_file(stem + "/observed.json", {"pid": OS.get_process_id(), "sequence": serial,
"ack_observed_ticks_usec": Time.get_ticks_usec(), "request_sha256": FileAccess.get_sha256(folder.path_join("request.json")),
"ack_observed_unix_ns": int(Time.get_unix_time_from_system() * 1000000000.0),
"observer_endpoint": "null_modal_receipts_only" if input_observer.endpoint == null else "passive_read_only_dispatch",
"events": input_observer.events.slice(first_event), "status": main.build_status_label.text})
receipts.append("os-pair/" + layout + "/" + stem + "/ack.json")
all_requests.append("os-pair/" + layout + "/" + stem + "/ack.json")
func annotate(row: Dictionary) -> void:
var view: WorldPreview = main.world_preview
row["rectangles"] = Codec.new().typed(draw_rectangles(view.get_life_observation_report(), view))
row["viewport_to_screen"] = affine(view.get_screen_transform() * view.get_global_transform_with_canvas().affine_inverse())
row["reveal"] = reveal_rows.duplicate(true)
reveal_rows.clear()
row["input_method"] = "native_os"
row["requests"] = receipts.duplicate()
receipts.clear()
# Rectangles describe existing draw commands; no second renderer is invoked.
func draw_rectangles(report: Dictionary, view: WorldPreview) -> Dictionary:
var rows: Array = []
for id: Variant in report.get(&"rooms", {}):
var room: Dictionary = report[&"rooms"][id]
if room.has(&"rect"):
rows.append({"category": "room", "id": id, "rect": room[&"rect"], "style_source": "scripts/ui/pixel_tower_art.gd"})
for index: int in range(report.get(&"landing_badges", []).size()):
var badge: Dictionary = report[&"landing_badges"][index]
rows.append(badge_rectangle("queue", str(index), badge[&"at"], badge[&"text"]))
for cabin: Dictionary in report.get(&"cabins", []):
rows.append({"category": "EV", "id": cabin[&"shaft_id"], "rect": cabin[&"rect"], "style_source": "scripts/ui/pixel_tower_art.gd"})
for actor: Dictionary in cabin[&"passengers"]:
rows.append(actor_rectangle(actor))
for actor: Dictionary in report.get(&"actor_drawings", []):
rows.append(actor_rectangle(actor))
if actor[&"staff"] and report[&"resting_staff"].has(StringName(String(actor[&"key"]).trim_prefix("staff/"))):
var rest := actor_rectangle(actor)
rest["category"] = "rest"
rows.append(rest)
for index: int in range(report.get(&"badges", []).size()):
var badge: Dictionary = report[&"badges"][index]
rows.append(badge_rectangle("goods" if badge[&"kind"] == &"supply" else "business", str(index), badge[&"at"], badge[&"text"]))
for entry: Dictionary in report.get(&"traffic", []):
var traffic: Dictionary = entry[&"traffic"]
if traffic[&"current_density"] > 0 or traffic[&"temporary_block"]:
var cell: Vector3i = entry[&"cell"]
rows.append({"category": "traffic", "id": str(cell.x) + "," + str(cell.y) + "," + str(cell.z),
"rect": entry[&"rect"], "style_source": "scenes/main/world_preview.gd"})
var transform := view.get_global_transform_with_canvas()
for row: Dictionary in rows:
row["viewport_rect"] = transform * (row["rect"] as Rect2)
row["screen_rect"] = view.get_screen_transform() * (row["rect"] as Rect2)
var source: String = FileAccess.get_file_as_string("res://" + row["style_source"])
var regex := RegEx.new()
regex.compile('Color\\("([0-9a-fA-F]{6})"')
var colors: Array = []
for found: RegExMatch in regex.search_all(source):
if not colors.has(found.get_string(1)): colors.append(found.get_string(1))
row["style_colors"] = colors
return {"rows": rows}
func badge_rectangle(category: String, id: String, at: Vector2, text: String) -> Dictionary:
var width := ThemeDB.fallback_font.get_string_size(text, HORIZONTAL_ALIGNMENT_LEFT, -1, 10).x + 6
return {"category": category, "id": id, "rect": Rect2(at, Vector2(width, 14)),
"text": text, "font_width": width - 6, "style_source": "scenes/main/world_preview.gd"}
func actor_rectangle(actor: Dictionary) -> Dictionary:
return {"category": "staff" if actor[&"staff"] else "guest", "id": actor[&"key"],
"rect": Rect2(actor[&"at"], Vector2(16, 18) * float(actor[&"scale"])), "style_source": "scripts/ui/pixel_people.gd"}
func point(node: Control) -> Dictionary:
var viewport: Vector2 = node.get_global_transform_with_canvas() * (node.size * 0.5)
var screen: Vector2 = node.get_screen_transform() * (node.size * 0.5)
return {"node": String(node.get_path()), "viewport": [viewport.x, viewport.y], "screen": [screen.x, screen.y], "window_id": node.get_window().get_window_id(),
"viewport_to_screen": affine(node.get_screen_transform() * node.get_global_transform_with_canvas().affine_inverse())}
func setup() -> void:
await super.setup()
input_method = "native_os"
expect(DisplayServer.get_name() != "headless", "real OS display")
tree.root.title = "The Tree P23-03 " + layout + " " + role + " PID " + str(OS.get_process_id())
input_observer = InputObserver.new()
# No handler dispatch: this observer only records incoming OS events.
tree.root.add_child(input_observer)
json_file(role + "-profile.json", {"pid": OS.get_process_id(),
"user_data_dir": OS.get_user_data_dir(), "engine": Engine.get_version_info(),
"display": DisplayServer.get_name()})
func button(node: BaseButton) -> void:
expect(node != null and not node.disabled, "reachable ordinary button")
if node == null or node.disabled: return
await request({"action": "click", "point": point(node), "button_text": str(node.get("text"))})
func load_menu(menu: MenuButton) -> void:
var action := point(menu)
action.merge({"action": "load_menu", "item_id": 0, "instructions": "Open this normal menu and choose manual load (item 0)."})
await request(action)
func screenshot(label: String) -> void:
await frames()
await request({"action": "screenshot", "id": label, "layout": layout})
func layer_observations(prefix: String) -> void:
if role == "writer" and prefix == "writer": await screenshot("calibration")
await button(main.front_button)
await screenshot(prefix + "-FRONT")
await button(main.back_button)
await screenshot(prefix + "-BACK")
if role == "writer" and prefix == "writer": await ordinary_camera()
func ordinary_camera() -> void:
var view: WorldPreview = main.world_preview
var at := point(view)
expect(not view.pan_mode, "ordinary zoom starts with move mode off")
var zoom: float = view.zoom_level
await request({"action": "wheel", "point": at, "direction": "down",
"ctrl": false, "move_mode": false})
expect(view.zoom_level != zoom, "ordinary wheel changes zoom")
await screenshot("ordinary-wheel-zoom")
await button(main.pan_button)
await screenshot("pan-mode-button")
expect(view.pan_mode, "PanModeButton enables move mode")
zoom = view.zoom_level
var pan: Vector2 = view.pan_offset
await request({"action": "wheel", "point": point(view), "direction": "up",
"ctrl": false, "move_mode": true})
expect(view.pan_offset != pan and view.zoom_level == zoom, "ordinary move-mode wheel pans")
await screenshot("move-mode-wheel-pan")
func finish_observations() -> void:
json_file(role + "-requests.json", {"requests": all_requests, "pid": OS.get_process_id()})
if is_instance_valid(input_observer): input_observer.queue_free()
super.finish_observations()
+1
View File
@@ -0,0 +1 @@
uid://d3oqtdp6dodu1
+8
View File
@@ -0,0 +1,8 @@
extends SceneTree
const Passive := preload("res://tests/fixtures/p23-03/passive_observation.gd")
func _initialize() -> void:
call_deferred("run")
func run() -> void:
var result: Dictionary = await Passive.new().run(self, OS.get_environment("P23_OBSERVATION_ROOT"), OS.get_environment("P23_OBSERVATION_ROLE"))
print("P23 passive OS observation: %d checks, %d failures" % [result.checks, result.failures])
quit(0 if result.failures == 0 else 1)
+1
View File
@@ -0,0 +1 @@
uid://mn33trympqu6
+27
View File
@@ -0,0 +1,27 @@
{
"provenance": "Distilled layout-A frame-030 state ac3ed64a6e6162918e4d44c45b2f78d09e50d473b607f3469350a06074a7efb3; retain first real queued trip, no cabin seat. Unit fixture only.",
"saved": {
"version": 1,
"current_minute": 691,
"request_sequence": 8,
"shafts": [{
"config": {
"shaft_id": "transport.shaft.0.0.0.2",
"module_ids": ["facility_00000003"],
"stops": [[0, 0, 0], [0, 1, 0]],
"width": 2, "capacity": 4, "travel_minutes_per_floor": 10, "access_id": ""
},
"operational": true, "operator_open": true, "powered": true,
"current_floor": 1, "target_floor": 1, "boarding_floor": 0,
"phase": "PICKUP", "remaining_minutes": 9, "phase_total_minutes": 10,
"phase_started_at": 690, "phase_origin_floor": 1, "phase_passenger_count": 0,
"boarded": []
}],
"trips": [{
"actor_id": "guest:p23-000", "request_id": "transport.trip.000000000002",
"shaft_id": "transport.shaft.0.0.0.2",
"connector_id": "transport.shaft.0.0.0.2.link.0.1",
"from": [0, 0, 0], "to": [0, 1, 0], "enqueued_at": 674, "state": "QUEUED"
}]
}
}
+85
View File
@@ -0,0 +1,85 @@
extends SceneTree
## Component and read-only rendering checks; never OS or same-demand proof.
const Presentation := preload("res://scripts/ui/room_presentation.gd")
const Stream := preload("res://tests/fixtures/p23-03/diagnostic_stream.gd")
var checks := 0
var failures := 0
func _initialize() -> void:
call_deferred("run")
func expect(value: bool, message: String) -> void:
checks += 1
if not value:
failures += 1
push_error("P23 life observation: " + message)
func run() -> void:
var layout := OS.get_environment("P23_LAYOUT")
var output_directory := OS.get_environment("P23_OUTPUT")
if layout in ["A", "B"] and not output_directory.is_empty():
var comparison := preload("res://tests/fixtures/p23-03/layout_comparison.gd").new()
expect(await comparison.run(self, output_directory, layout), "native layout " + layout)
print("P23 life observation: %d checks, %d failures" % [checks, failures])
quit(0 if failures == 0 else 1)
return
var clock: Variant = root.get_node("GameClock")
clock.set_paused(true)
# Isolated real typed components test state labels, without claiming gameplay.
var labels: Dictionary = {}
for state: int in FacilityInstance.State.values():
var room := FacilityInstance.new().configure(&"component", &"reception")
room.state = state as FacilityInstance.State
var description := Presentation.describe(room)
expect(description[&"state_name"] == FacilityInstance.State.keys()[state], "enum name preserved")
var text := Presentation.business_label(description)
expect(not text.is_empty() and not labels.has(text), "distinct visible business reason")
labels[text] = true
for reason: StringName in [&"RAIDED", &"DESTROYED"]:
var room := FacilityInstance.new().configure(&"component", &"reception", {&"suspension_reason": reason})
var description := Presentation.describe(room)
expect(description[&"operation"] == FacilityInstance.OperationState.SUSPENDED, "actual suspension operation")
var text := Presentation.business_label(description)
expect(not labels.has(text), "raid and damage have separate visible labels")
labels[text] = true
var room := FacilityInstance.new().configure(&"component", &"reception", {&"customer_capacity": 3})
var service := ServiceInstance.new()
service.guest_ids.assign([&"b", &"a"])
room.active_services[&"service"] = service
room.queue_entries.assign([&"queue-only"])
var description := Presentation.describe(room)
expect(description[&"people"] == [&"a", &"b"] and description[&"count"] == 2, "queue entries are not room occupants")
description[&"people"].clear()
expect(service.guest_ids.size() == 2, "presentation owns its arrays")
var runtime: Variant = root.get_node("SimulationRuntime")
var view := WorldPreview.new()
view.size = Vector2(1440, 810)
view.set_runtime(runtime)
root.add_child(view)
var before: PackedByteArray = var_to_bytes(runtime.create_snapshot())
var tracks: PackedByteArray = var_to_bytes([view._actor_tracks, view._elevator_tracks])
var model := view._build_life_observation()
expect(var_to_bytes(runtime.create_snapshot()) == before, "builder does not mutate Runtime")
expect(var_to_bytes([view._actor_tracks, view._elevator_tracks]) == tracks, "builder does not advance visual tracks")
expect(model.has(&"rooms") and model.has(&"landing_badges") and model.has(&"elevators") and model.has(&"staff") and model.has(&"goods") and model.has(&"badges"), "all six category fields exist")
expect(model[&"traffic"].size() > 0 and not view.show_grid, "normal view observes real walkable traffic")
await process_frame
await process_frame
var report := view.get_life_observation_report()
expect(not report.is_empty(), "actual drawing published an observation")
# The getter exposes the last actual draw and must not create another model.
var original: PackedByteArray = var_to_bytes(report)
if report.has(&"traffic"):
report[&"traffic"].clear()
expect(var_to_bytes(view.get_life_observation_report()) == original, "nested report arrays are isolated")
report.clear()
expect(var_to_bytes(view.get_life_observation_report()) == original, "public report is a deep copy")
var output := OS.get_environment("P23_LIFE_DIAGNOSTIC_ROOT")
if not output.is_empty():
var stream := Stream.new()
expect(stream.open(output), "fresh sparse diagnostic stream")
expect(stream.capture("initial", runtime, view), "whole typed state and draw report capture")
stream.close()
view.free()
print("P23 life observation: %d checks, %d failures" % [checks, failures])
quit(0 if failures == 0 else 1)
@@ -0,0 +1 @@
uid://d23lifeobs03
File diff suppressed because it is too large Load Diff
+354
View File
@@ -0,0 +1,354 @@
#!/usr/bin/env python3
"""Root-only native collection. Does not run regression or claim acceptance."""
import argparse
import hashlib
import importlib.util
import json
import os
from pathlib import Path
import shutil
import subprocess
import sys
import time
import uuid
import re
sys.dont_write_bytecode = True
HERE = Path(__file__).resolve().parent
CONTRACT = 'docs/plans/original-completion-2026-10-02/execution/completion_marker_contracts.json'
CONDITIONS = ('room_occupancy_and_operation', 'queues_and_overflow',
'elevator_state_and_passengers', 'staff_state_and_jobs',
'supplies_and_carried_goods', 'same_demand_route_improvement',
'normal_os_and_distinct_pid_resume')
OS_CHILD_TIMEOUT_SECONDS = 900
RECORDER = 'tests/fixtures/p23-03/passive_os_recorder.gd'
def collect_screenshots(out, pair, role):
"""Bind every external CUA screenshot to its request and decoded PNG.
The external receipt supplies screenshots[{path,raw_sha256,tool_call_id}].
Paths are relative to the evidence root; raw receipt bytes remain unchanged.
"""
checker = module('p23_screenshot_binding', HERE/'check_p23_life_observation_evidence.py')
bindings = []
requests = json.loads((pair/(role+'-requests.json')).read_text())['requests']
for ack_name in requests:
ack_path = out/ack_name
checker.require(ack_path.resolve().is_relative_to(out.resolve()), 'ACK path escape')
ack = json.loads(ack_path.read_text())
request_path = ack_path.with_name('request.json')
request = json.loads(request_path.read_text())
receipt_path = out/ack['input_receipt']
checker.require(receipt_path.resolve().is_relative_to(out.resolve()), 'CUA receipt path escape')
cua = json.loads(receipt_path.read_text())
checker.require(ack['request_sha256'] == checker.sha(request_path), 'CUA request hash')
checker.require(cua['screenshots'], 'CUA screenshots missing')
for number, shot in enumerate(cua['screenshots']):
raw = out/shot['path']
checker.require(raw.resolve().is_relative_to(out.resolve()) and not raw.is_symlink(), 'screenshot path escape')
checker.require(checker.sha(raw) == shot['raw_sha256'] and shot['tool_call_id'], 'raw screenshot CUA binding')
kind, _, _ = checker.image_kind(raw.read_bytes())
conversion = None
if kind == 'jpeg':
png = ack_path.with_name('decoded-%02d.png' % number)
receipt = ack_path.with_name('conversion-%02d.json' % number)
convert_screenshot(raw.resolve(), png.resolve(), shot, receipt.resolve())
conversion = str(receipt.relative_to(out))
else:
png = raw
width, height, _ = checker.png_pixels(png.read_bytes())
bindings.append(dict(request=str(request_path.relative_to(out)), request_sha256=checker.sha(request_path),
sequence=request['sequence'], target_pid=request['target_pid'],
input_receipt=ack['input_receipt'], input_receipt_sha256=checker.sha(receipt_path),
screenshot_index=number, tool_call_id=shot['tool_call_id'],
raw_path=shot['path'], raw_sha256=shot['raw_sha256'], conversion=conversion,
png_path=str(png.relative_to(out)), png_sha256=checker.sha(png), width=width, height=height))
checker.require(bindings, 'empty screenshot collection')
destination = pair/(role+'-screenshots.json')
with destination.open('x') as output:
json.dump(bindings, output, indent=2)
output.write('\n')
return str(destination.relative_to(out))
def convert_screenshot(raw_path, png_path, cua_receipt, receipt_path):
"""Root-only conversion; raw CUA bytes are never renamed or rewritten."""
checker = module('p23_conversion_checker', HERE/'check_p23_life_observation_evidence.py')
raw, png, receipt = map(Path, (raw_path, png_path, receipt_path))
checker.require(raw.is_absolute() and png.is_absolute() and receipt.is_absolute(),
'absolute conversion paths required')
checker.require(raw.is_file() and not raw.is_symlink() and not png.exists()
and not receipt.exists() and raw.resolve() != png.resolve(), 'fresh separate conversion output')
raw_hash = checker.sha(raw)
checker.require(raw_hash == cua_receipt['raw_sha256'] and cua_receipt['tool_call_id'],
'CUA conversion input hash/call mismatch')
kind, width, height = checker.image_kind(raw.read_bytes())
checker.require(kind == 'jpeg', 'conversion requires JPEG input')
converter = '/usr/bin/sips'
converter_hash = checker.sha(converter)
argv = [converter, '-s', 'format', 'png', str(raw), '--out', str(png)]
process = subprocess.run(argv, capture_output=True, timeout=900, check=False)
rec = dict(raw_path=str(raw), raw_sha256=raw_hash, raw_kind=kind,
tool_call_id=cua_receipt['tool_call_id'], converter=converter,
converter_sha256=converter_hash, argv=argv, exit_code=process.returncode,
stdout_sha256=hashlib.sha256(process.stdout).hexdigest(),
stderr_sha256=hashlib.sha256(process.stderr).hexdigest(),
png_path=str(png), png_sha256=checker.sha(png) if png.is_file() else None,
width=width, height=height)
# Preserve failure receipts too. These logs allow independent hash checking.
with receipt.open('x') as output:
json.dump(rec, output, indent=2)
output.write('\n')
with Path(str(receipt) + '.stdout').open('xb') as output:
output.write(process.stdout)
with Path(str(receipt) + '.stderr').open('xb') as output:
output.write(process.stderr)
checker.require(checker.sha(raw) == raw_hash, 'raw CUA bytes changed during conversion')
checker.require(checker.sha(converter) == converter_hash, 'converter changed during conversion')
checker.inspect_conversion(rec, cua_receipt, converter_hash)
return rec
def collect_os(r, root, out, stage, engine, commit, refs, protected, outer):
"""Adapted P08-03 collect_os protocol, retaining owned-process guards.
Root supplies CUA ack/receipt files in the requested directories. Neither
controller nor passive endpoints generate input or change permissions.
"""
r.require(not outer.native.helper['live_engines'](), 'external engine present')
checker = module('p23_os_pins', HERE/'check_p23_life_observation_evidence.py')
before = r.inventory(stage, stage=True)
public = (stage/'project.godot').read_bytes()
(out/'os-pair').mkdir()
phases = []
for label in ('A-writer', 'A-reader', 'B-writer', 'B-reader'):
layout, role = label.split('-')
pair = out/'os-pair'/layout
pair.mkdir(exist_ok=role == 'reader')
r.require(not outer.native.helper['live_engines'](), 'external engine present')
r.require(r.source_refs(root) == refs and r.default_player(root) == protected, 'OS before guards')
r.require(r.inventory(stage, stage=True) == before, 'OS stage before guard')
r.require(shutil.disk_usage(out).free >= 500*1024*1024, 'OS disk admission')
folder = out/'os-runs'/label
folder.mkdir(parents=True)
name = 'TheTree-PCK-tests-'+uuid.uuid4().hex+'-p23-observation-'+label.lower()
profile = r.profile_path(name)
r.require(not profile.exists(), 'fresh OS private profile')
formal_source = pair/'formal' if role == 'reader' else None
formal_before = r.inventory(formal_source) if formal_source else {}
if role == 'reader':
r.require(set(formal_before) == {'world.bin','manifest.json','thumbnail.png','journal.log'}, 'all formal inputs')
formal = profile/'saves/manual_ui'
formal.mkdir(parents=True)
for member, pin in formal_before.items():
r.copy_source(formal_source/member, formal/member)
r.require(r.sha(formal/member) == pin['sha256'], 'formal fixture copy SHA')
recorder = folder/'passive_os.gd'
r.copy_source(root/RECORDER, recorder)
proof = folder/'teardown'
(proof/'owned-audio').mkdir(parents=True)
env = r.fresh_environment(dict(os.environ))
env.update(P23_OBSERVATION_ROOT=str(pair), P23_OBSERVATION_ROLE=role, P23_LAYOUT=layout,
THE_TREE_AUDIO_TEARDOWN_PROOF_DIR=str(proof/'owned-audio'),
THE_TREE_AUDIO_FIXTURE_CONTROL_PROOF=str(proof/'control.json'))
command = [engine, '--path', str(stage), '--script', str(recorder)]
base = {'protocol':'p23-observation-native-os-v1', 'role':role, 'layout':layout, 'commit':commit,
'source_map':{n:v['sha256'] for n,v in refs.items()}, 'stage':str(stage),
'stage_before':before, 'profile':str(profile), 'profile_name':name,
'profile_before':{'existed':profile.exists(), 'files':r.inventory(profile) if profile.exists() else {}}, 'formal_input':formal_before,
'formal_source':str(formal_source) if formal_source else None, 'owner_pid':os.getpid(),
'default_before':protected, 'command':command, 'engine_sha256':r.sha(engine),
'controller_sha256':r.sha(root/'tools/run_p23_life_observation.py'),
'recorder_sha256':r.sha(recorder), 'formal_acceptance':False,
'timeout_seconds':OS_CHILD_TIMEOUT_SECONDS,
'inputs':checker.input_map(root), 'spec_sha256':checker.SPEC_SHA,
'settings_sha256':hashlib.sha256(public).hexdigest(),
'environment':{k:v for k,v in env.items() if k.startswith(('P23_', 'THE_TREE_'))},
'environment_sha256':hashlib.sha256(json.dumps(env,sort_keys=True).encode()).hexdigest()}
r.write(folder/'started.json', base)
error = None; children = []; guards = []; exit_code = None; owner_proof = None
try:
(stage/'project.godot').write_bytes(r.private_project(public, name))
with (folder/'engine-native.log').open('x') as log:
process = subprocess.Popen(command, cwd=stage, env=env, stdout=log, stderr=subprocess.STDOUT)
children.append(process)
r.write(folder/'process-started.json', dict(base, actual_pid=process.pid,
monotonic_ns=time.monotonic_ns(), state='waiting_for_root_cua_requests'))
print(json.dumps({'actual_pid':process.pid, 'request_directory':str(pair),
'state':'waiting_for_root_cua', 'role':role}), flush=True)
exit_code = process.wait(timeout=base['timeout_seconds'])
r.require(exit_code == 0, 'ordinary native OS exit0')
raw = (folder/'engine-native.log').read_text()
r.require('ERROR:' not in raw and 'SCRIPT ERROR:' not in raw, 'OS native diagnostics')
matches = re.findall(r'(?m)^P23 passive OS observation: ([0-9]+) checks, 0 failures$', raw)
r.require(len(matches) == 1 and int(matches[0]) > 0, 'passive observer completion')
observed = r.read(pair/(role+'-profile.json'))
r.require(observed['pid'] == process.pid and Path(observed['user_data_dir']).resolve() == profile.resolve()
and observed['display'] != 'headless', 'actual OS private profile/display')
collect_screenshots(out, pair, role)
owners = r.module('p23_os_owner_gate', r.SUPPORT/'ordinary_proof_gate.py')
owner_proof = owners.ordinary_gate(proof, process.pid, folder, ['single'])
except BaseException as caught:
error = type(caught).__name__+': '+str(caught)
finally:
try:
cleanup, cleanup_errors = outer.finish_captured(children)
finally:
(stage/'project.godot').write_bytes(public)
try:
r.require(r.source_refs(root) == refs, 'OS source unchanged')
r.require(r.inventory(stage, stage=True) == before, 'OS stage unchanged')
r.require(r.default_player(root) == protected, 'OS default player unchanged')
if formal_source:
r.require(r.inventory(formal_source) == formal_before, 'formal input unchanged')
r.require(r.sha(recorder) == base['recorder_sha256'], 'recorder unchanged')
except BaseException as caught:
guards.append(type(caught).__name__+': '+str(caught))
passed = not (error or guards or cleanup_errors) and len(children) == 1 and exit_code == 0
r.write(folder/'ended.json', dict(base, actual_pid=children[0].pid if children else None,
exit_code=exit_code, engine_ended=bool(cleanup) and all(v['engine_ended'] for v in cleanup),
error=error, guard_errors=guards, cleanup_errors=cleanup_errors, owned_cleanup=cleanup,
ordinary_owner_proof=owner_proof, stage_after=r.inventory(stage,stage=True),
engine_sha256_after=r.sha(engine), inputs_after=checker.input_map(root),
settings_sha256_after=hashlib.sha256((stage/'project.godot').read_bytes()).hexdigest(),
environment_after={k:v for k,v in env.items() if k.startswith(('P23_', 'THE_TREE_'))},
environment_sha256_after=hashlib.sha256(json.dumps(env,sort_keys=True).encode()).hexdigest(),
default_after=r.default_player(root), source_after=r.source_refs(root),
profile_after=r.inventory(profile), passed=passed))
phases.append('os-runs/'+label+'/ended.json')
r.require(passed, 'native OS collection failed: '+str(error or guards or cleanup_errors))
return phases
def module(name, path):
spec = importlib.util.spec_from_file_location(name, path)
obj = importlib.util.module_from_spec(spec)
spec.loader.exec_module(obj)
return obj
def absolute(value):
path = Path(value)
if not path.is_absolute():
raise argparse.ArgumentTypeError('absolute path required')
return path.resolve()
def collect(args):
root, out = args.project_root, args.output
if out.is_relative_to(root):
raise ValueError('evidence output must be outside the frozen source checkout')
r = module('p23_standard_runner', root/'tools/run_all_tests.py')
commit = subprocess.check_output(['git', '-C', str(root), 'rev-parse', 'HEAD'], text=True).strip()
r.require(commit == args.commit, 'actual final submitted commit required')
r.require(not subprocess.check_output(['git','-C',str(root),'status','--porcelain'], text=True), 'frozen clean worktree required')
r.require(not out.exists(), 'fresh output; never overwrite earlier proof')
refs = r.source_refs(root)
checker = module('p23_initial_inputs', root/'tools/check_p23_life_observation_evidence.py')
inputs_before = checker.input_map(root)
outer = r.module('p23_owned_outer', r.SUPPORT/'owned_outer.py')
r.require(not outer.native.helper['live_engines'](), 'one native engine')
engine = shutil.which(args.godot)
r.require(engine is not None, 'native engine exists')
engine = str(Path(engine).resolve())
out.mkdir(parents=True)
stage = out/'stage'; stage.mkdir()
(out/'starters').mkdir(); (out/'bootstrap').mkdir()
for name, pin in r.read(r.SUPPORT/'bootstrap-scripts.json').items():
data = pin['text'].encode()
r.require(hashlib.sha256(data).hexdigest() == pin['sha256'], 'bootstrap pin')
(out/'bootstrap'/name).write_bytes(data)
for name, pin in refs.items():
target = stage/name; target.parent.mkdir(parents=True, exist_ok=True)
r.copy_source(Path(pin['path']), target)
r.require(r.sha(target) == pin['sha256'], 'whole copied source '+name)
expected = {n: {'bytes':v['bytes'], 'sha256':v['sha256']} for n,v in refs.items()}
selected = {'root_before':refs, 'map':{n:v['sha256'] for n,v in refs.items()}, 'timeout':180}
r.require(r.inventory(stage, stage=True) == expected, 'complete private source')
protected = r.default_player(root); public = (stage/'project.godot').read_bytes()
paths = ['res://'+n for n in sorted(refs) if n.endswith('.gd')]
r.write(out/'parse-paths.json', paths)
markers = r.module('p23_strict_markers', root/'tools/strict_test_markers.py')
contracts = markers.load_contracts(root/CONTRACT)
policy = r.read(r.SUPPORT/'suite-policy.json')
diagnostics = r.module('p23_diagnostics', r.SUPPORT/'diagnostic_gate.py')
owners = r.module('p23_owners', r.SUPPORT/'ordinary_proof_gate.py')
audio = r.module('p23_audio', r.SUPPORT/'audio_evidence_gate.py')
r.write(out/'runner-input.json', {'protocol':'p23-normal-observation-v1', 'commit':commit,
'selected_source_map':selected['map'], 'engine':engine, 'engine_sha256':r.sha(engine),
'owner_pid':os.getpid(), 'default_player':protected, 'diagnostic_seed':806042,
'inputs':inputs_before, 'provenance':checker.input_provenance(root),
'pixel_decoder':{'interpreter':sys.executable,
'PIL_available':importlib.util.find_spec('PIL') is not None,
'raw_screenshots':'preserve bytes; Root lossless conversion required for JPEG without PIL'}})
phases = []; previous = {}; cases = []; error = None; os_proof = None
deadline = time.monotonic()+900
saved_env = {k:os.environ.get(k) for k in ('P23_LAYOUT','P23_OUTPUT')}
def phase(label, test):
remaining = int(deadline-time.monotonic())
r.require(remaining >= (180 if label == 'import' else 1), '900s collection deadline')
selected['timeout'] = min(180, remaining)
phase_out = out/'runs'/label
phase_out.mkdir(parents=True)
(phase_out/'starters').mkdir(); (phase_out/'bootstrap').mkdir()
for original in (out/'bootstrap').iterdir():
r.copy_source(original,phase_out/'bootstrap'/original.name)
r.require(r.sha(original)==r.sha(phase_out/'bootstrap'/original.name),'bootstrap full copy')
r.write(phase_out/'parse-paths.json',paths)
row = r.execute_phase(engine, stage, phase_out, label, test, selected, expected, previous,
policy, contracts, paths, public, protected, root, outer, markers, diagnostics, owners, audio)
# Supplement the existing runner receipt; preserve it verbatim.
r.write(phase_out/'p23-inputs.json', dict(source_map=selected['map'], source_after=r.source_refs(root),
engine_sha256=r.sha(engine), inputs=inputs_before, inputs_after=checker.input_map(root),
spec_sha256=checker.SPEC_SHA, settings_sha256=hashlib.sha256(public).hexdigest(),
stage_before=expected, stage_after=r.inventory(stage,stage=True),
default_before=protected, default_after=r.default_player(root),
receipt_sha256=r.sha(phase_out/(label+'-receipt.json'))))
phases.append('runs/'+label+'/'+label+'-receipt.json')
r.require(not row['failed'], 'strict phase failed: '+label)
try:
phase('import', None); phase('parse', None)
for layout in ('A', 'B'):
directory = out/('layout-'+layout)
os.environ.update(P23_LAYOUT=layout, P23_OUTPUT=str(directory))
phase('layout-'+layout.lower(), 'tests/ui/test_p23_life_observation.gd')
cases.append({'directory':directory.name, 'phase':phases[-1], 'files':r.inventory(directory)})
if args.input_mode == 'native_os':
os_phases = collect_os(r, root, out, stage, engine, commit, refs, protected, outer)
checker = module('p23_os_input_map', root/'tools/check_p23_life_observation_evidence.py')
os_proof = 'os-proof.json'
r.write(out/os_proof, {'source_map':selected['map'], 'inputs':checker.input_map(root),
'input_method':'native_os', 'observer_mutations':[], 'phases':os_phases, 'directory':'os-pair'})
except BaseException as caught:
error = type(caught).__name__+': '+str(caught)
finally:
cleanup, cleanup_errors = outer.finish_captured(outer.OWNED)
for key,value in saved_env.items():
if value is None: os.environ.pop(key, None)
else: os.environ[key] = value
if r.source_refs(root) != refs or r.default_player(root) != protected:
error = (error+'; ' if error else '')+'protected root/profile changed'
if checker.input_map(root) != inputs_before:
error = (error+'; ' if error else '')+'spec/fixture/controller inputs changed'
r.write(out/'runner-ended.json', {'error':error, 'cleanup':cleanup, 'cleanup_errors':cleanup_errors,
'phases':phases, 'formal_acceptance':False, 'native_Windows':'pending'})
checker = module('p23_observation_checker', root/'tools/check_p23_life_observation_evidence.py')
r.write(out/'index.json', {'schema':'p23-life-observation-v1', 'commit':commit,
'source_map':selected['map'], 'input_map':inputs_before,
'provenance':checker.input_provenance(root),
'marker_contract_sha256':r.sha(root/CONTRACT),
'spec_sha256':hashlib.sha256((root/'docs/spec/acceptance/P23-03.json').read_text().strip().encode()).hexdigest(),
'cases':cases, 'phases':phases, 'conditions':list(CONDITIONS),
'artifacts':{str(p.relative_to(out)):r.sha(p)
for p in out.rglob('*') if p.is_file() and 'stage' not in p.relative_to(out).parts},
'os_proof':os_proof, 'regression':None,
'acceptance':'pending', 'collection_error':error})
return 1 if error or cleanup_errors else 0
def main(argv=None):
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--project-root', type=absolute, required=True)
parser.add_argument('--output', type=absolute, required=True)
parser.add_argument('--commit', required=True)
parser.add_argument('--godot', default='godot')
parser.add_argument('--input-mode', choices=('handler', 'native_os'), default='handler')
parser.add_argument('--collect', action='store_true', required=True)
return collect(parser.parse_args(argv))
if __name__ == '__main__':
raise SystemExit(main())
+761
View File
@@ -0,0 +1,761 @@
#!/usr/bin/env python3
"""Local semantic-checker tests; synthetic examples are never game evidence."""
import copy
import ast
import unittest
import sys
import contextlib
import io
import json
import hashlib
import struct
import zlib
import tempfile
from pathlib import Path
from unittest import mock
sys.dont_write_bytecode = True
import check_p23_life_observation_evidence as check
class QueuedElevatorTests(unittest.TestCase):
def setUp(self):
fixture = json.loads((check.ROOT / 'tests/fixtures/p23-03/queued_elevator.json').read_text())
self.state = {'systems': {'runtime': {'elevators': fixture['saved']}}}
def test_queued_trip_without_cabin_seat_uses_config_shaft(self):
saved = self.state['systems']['runtime']['elevators']
self.assertNotIn('shaft_id', saved['shafts'][0])
self.assertEqual(saved['shafts'][0]['boarded'], [])
report = check.elevator_report(self.state)
self.assertEqual(report['shafts'][0]['waiting_count'], 1)
self.assertEqual(report['shafts'][0]['passenger_count'], 0)
self.assertAlmostEqual(report['shafts'][0]['visual_floor'], .9)
self.assertEqual(report['trips'], saved['trips'])
def test_tampered_shaft_attribution_fails(self):
self.state['systems']['runtime']['elevators']['trips'][0]['shaft_id'] = 'wrong.shaft'
with self.assertRaisesRegex(ValueError, 'shaft attribution'):
check.elevator_report(self.state)
def test_wrong_waiting_count_fails_whole_report_comparison(self):
expected = check.elevator_report(self.state)
tampered = copy.deepcopy(expected)
tampered['shafts'][0]['waiting_count'] = 0
directory = mock.MagicMock()
manifest = dict(label='queued-fixture', pid=123, state='state',
state_after='state', report='report')
directory.__truediv__.return_value.open.return_value = io.StringIO(json.dumps(manifest)+'\n')
# Exercise the actual stream comparison; isolate unrelated actor fields.
with mock.patch.object(check, 'read_object', side_effect=[self.state, self.state,
{'elevators': tampered}]), \
mock.patch.object(check, 'compare_actor_rows', return_value=[]):
with self.assertRaisesRegex(ValueError, 'typed elevator report mismatch'):
check.inspect_stream(directory)
def test_extra_and_missing_trip_keys_fail(self):
for mutation in ('extra', 'missing'):
with self.subTest(mutation=mutation):
state = copy.deepcopy(self.state)
trip = state['systems']['runtime']['elevators']['trips'][0]
if mutation == 'extra':
trip['unexpected'] = True
else:
del trip['shaft_id']
with self.assertRaisesRegex(ValueError, 'exact elevator trip keys'):
check.elevator_report(state)
class SnapshotCellTests(unittest.TestCase):
def test_layout_arrival_compares_serialized_cell_arrays(self):
source = (check.ROOT / 'tests/fixtures/p23-03/layout_comparison.gd').read_text()
serializer = (check.ROOT / 'scripts/guest/guest_runtime.gd').read_text()
self.assertIn('&"current_cell": _cell_to_array(current_cell)', serializer)
goal = check.re.search(r'const GOAL := Vector3i\(([^)]+)\)', source)
self.assertIsNotNone(goal)
coordinates = ast.literal_eval('[' + goal.group(1) + ']')
comparison = check.re.search(r'if guest\[&"current_cell"\] != (.+?) or finished\.has', source)
self.assertIsNotNone(comparison)
expression = comparison.group(1)
self.assertEqual(expression, '[GOAL.x, GOAL.y, GOAL.z]')
for axis, value in zip('xyz', coordinates):
expression = expression.replace('GOAL.' + axis, str(value))
target = ast.literal_eval(expression)
self.assertEqual(json.loads('[2, 1, 0]'), target)
for cell in ([2, 0, 0], [3, 1, 0], [2, 1, 1]):
self.assertNotEqual(cell, target)
class ImageTests(unittest.TestCase):
def test_collector_conversion_is_bound_to_each_cua_screenshot(self):
runner = check.module('p23_unit_collector', check.ROOT/'tools/run_p23_life_observation.py')
with tempfile.TemporaryDirectory(dir=check.ROOT/'tests/fixtures/p23-03') as temporary:
root = Path(temporary)
pair = root/'os-pair/A'
request_dir = pair/'writer-request-0001'
request_dir.mkdir(parents=True)
request = request_dir/'request.json'
request.write_text(json.dumps(dict(sequence=1, target_pid=123)))
raw = request_dir/'raw.jpg'
raw.write_bytes(b'\xff\xd8unit-only\xff\xd9')
shot = dict(path=str(raw.relative_to(root)), raw_sha256=check.sha(raw), tool_call_id='unit-shot-call')
receipt = request_dir/'cua.json'
receipt.write_text(json.dumps(dict(screenshots=[shot])))
ack = request_dir/'ack.json'
ack.write_text(json.dumps(dict(request_sha256=check.sha(request), input_receipt=str(receipt.relative_to(root)))))
(pair/'writer-requests.json').write_text(json.dumps(dict(requests=[str(ack.relative_to(root))])))
def convert(raw_path, png_path, cua, receipt_path):
self.assertEqual(cua, shot)
self.assertEqual(Path(raw_path).read_bytes(), b'\xff\xd8unit-only\xff\xd9')
Path(png_path).write_bytes(self.png())
Path(receipt_path).write_text('{}')
with mock.patch.object(runner, 'module', return_value=check), \
mock.patch.object(check, 'image_kind', return_value=('jpeg', 2, 2)), \
mock.patch.object(runner, 'convert_screenshot', side_effect=convert) as converter:
output = runner.collect_screenshots(root, pair, 'writer')
converter.assert_called_once()
rows = json.loads((root/output).read_text())
self.assertEqual(rows[0]['tool_call_id'], shot['tool_call_id'])
self.assertEqual(rows[0]['raw_sha256'], check.sha(raw))
self.assertEqual(rows[0]['png_sha256'], check.sha(root/rows[0]['png_path']))
self.assertTrue(rows[0]['conversion'])
@staticmethod
def png(mode=0, channels=3):
def chunk(kind, body):
return struct.pack('>I', len(body)) + kind + body + struct.pack('>I', zlib.crc32(kind + body))
rows = [bytes([10, 20, 30, 40][:channels] * 2),
bytes([50, 60, 70, 80][:channels] * 2)]
filtered = bytearray()
prior = bytes(2 * channels)
for row in rows:
filtered.append(mode)
for x, value in enumerate(row):
left, up, corner = (row[x-channels] if x >= channels else 0,
prior[x], prior[x-channels] if x >= channels else 0)
p = left + up - corner
distances = [abs(p-left), abs(p-up), abs(p-corner)]
predictors = [0, left, up, (left+up)//2,
[left, up, corner][distances.index(min(distances))]]
filtered.append((value-predictors[mode]) % 256)
prior = row
header = struct.pack('>IIBBBBB', 2, 2, 8, 2 if channels == 3 else 6, 0, 0, 0)
return (b'\x89PNG\r\n\x1a\n' + chunk(b'IHDR', header)
+ chunk(b'IDAT', zlib.compress(filtered)) + chunk(b'IEND', b''))
def test_all_png_filters_rgb_and_rgba(self):
for mode in range(5):
for channels in (3, 4):
with self.subTest(mode=mode, channels=channels):
w, h, rows = check.png_pixels(self.png(mode, channels))
self.assertEqual((w, h), (2, 2))
self.assertEqual(rows[0][0], tuple([10, 20, 30, 40][:channels]))
self.assertEqual(rows[1][1], tuple([50, 60, 70, 80][:channels]))
def test_crc_and_trailing_bytes_rejected(self):
data = bytearray(self.png())
data[29] ^= 1
with self.assertRaisesRegex(ValueError, 'CRC mismatch'):
check.png_pixels(bytes(data))
with self.assertRaisesRegex(ValueError, 'invalid IEND'):
check.png_pixels(self.png() + b'extra')
def conversion(self):
raw, png = b'\xff\xd8synthetic-unit-test\xff\xd9', self.png()
digest = lambda b: hashlib.sha256(b).hexdigest()
rec = dict(raw_path='/raw.png', raw_sha256=digest(raw), raw_kind='jpeg',
tool_call_id='unit-test-call', converter='/usr/bin/sips', converter_sha256='a'*64,
argv=['/usr/bin/sips', '-s', 'format', 'png', '/raw.png', '--out', '/decoded.png'],
exit_code=0, stdout_sha256=digest(b''), stderr_sha256=digest(b''),
png_path='/decoded.png', png_sha256=digest(png), width=2, height=2)
cua = dict(raw_sha256=digest(raw), tool_call_id='unit-test-call')
return rec, cua, {'/raw.png': raw, '/decoded.png': png}
def test_conversion_chain_and_specific_forgery_deficits(self):
rec, cua, files = self.conversion()
helper = mock.Mock()
helper.jpeg_dimensions.return_value = (2, 2)
with mock.patch.object(Path, 'read_bytes', lambda p: files[str(p)]), \
mock.patch.object(check, 'module', return_value=helper):
self.assertEqual(check.inspect_conversion(rec, cua, 'a'*64)[:2], (2, 2))
for key, value, deficit in [('converter_sha256', 'b'*64, 'converter SHA mismatch'),
('argv', [], 'converter argv mismatch'),
('raw_sha256', 'b'*64, 'CUA raw hash/call mismatch'),
('width', 3, 'conversion dimensions mismatch')]:
with self.subTest(key=key):
bad = dict(rec, **{key: value})
with self.assertRaisesRegex(ValueError, deficit):
check.inspect_conversion(bad, cua, 'a'*64)
with self.assertRaisesRegex(ValueError, 'JPEG screenshot requires conversion record'):
check.screenshot_pixels('/raw.png')
class RestAuditTests(unittest.TestCase):
def fixture(self):
member = dict(staff_id='s', state='OFF_DUTY', health=100, current_task_id='', current_cell=[0, 0, 0])
facilities = [dict(instance_id=fid, definition_id='staff_waiting_room', origin=[0, 0, 0],
occupied_cells=[], state='OPEN', suspension_reason='') for fid in ('a', 'b')]
runtime = dict(staff=dict(staff=[member]), facilities=dict(instances=facilities),
defense=dict(raids=[]), events=dict(actor_locks={}), elevators=dict(trips=[]),
path=dict(cell_traffic=[dict(cell=[x, 0, 0]) for x in (-1, 0, 1)]),
world=dict(bounds=dict(min_x=-1, max_x=1, min_y=0, max_y=0), occupancies=[]))
routes = [dict(cell=[x, 0, 0], path=[[0, 0, 0], [x, 0, 0]] if x else [[0, 0, 0]],
edges=[dict(**{'from': [0, 0, 0], 'to': [x, 0, 0]}, total_cost=0)] if x else [],
calculate_path_cost=0) for x in (-1, 0, 1)]
observed = []
for f in facilities:
observed.append({k: f[k] for k in ('instance_id', 'definition_id', 'origin', 'occupied_cells')})
observed[-1].update(operation_state='OPERATING', probes=[dict(cell=[x, 0, 0], has_cell=True) for x in (0, -1, 1)],
ordered=[[x, 0, 0] for x in (-1, 0, 1)], routes=copy.deepcopy(routes))
selected = dict(reachable=True, resting=True, cell=[-1, 0, 0], facility_id='a', label='待機室で休憩中')
audit = dict(path_service_sha256=check.sha(check.ROOT/'scripts/world/path_service.gd'),
staff=[dict(member, defense_memberships=[], actor_lock=False, in_transit=False,
facilities=observed, comparison_only=selected)])
report = dict(resting_staff={'s': 'a'}, actor_drawings=[], layer=0, carried_patients={}, rooms={},
viewport=[1440, 810], zoom=1, pan=[0, 0])
return dict(systems=dict(runtime=runtime)), report, audit
def test_selection_ties_and_nonmutation(self):
state, report, audit = self.fixture()
before = copy.deepcopy((state, report, audit))
self.assertEqual(check.compare_rest(state, report, audit)['s']['cell'], [-1, 0, 0])
self.assertEqual((state, report, audit), before)
def test_rest_forgeries(self):
mutations = {
'selection': lambda a: a['staff'][0]['comparison_only'].update(facility_id='b'),
'tie': lambda a: a['staff'][0]['comparison_only'].update(cell=[0, 0, 0]),
'lock': lambda a: a['staff'][0].update(actor_lock=True),
'transit': lambda a: a['staff'][0].update(in_transit=True),
'label': lambda a: a['staff'][0]['comparison_only'].update(label='宿舎で休憩中'),
'cost': lambda a: a['staff'][0]['facilities'][0]['routes'][0]['edges'][0].update(total_cost=9),
'facility_order': lambda a: a['staff'][0]['facilities'].reverse(),
'candidate_order': lambda a: a['staff'][0]['facilities'][0]['ordered'].reverse(),
'empty_edge': lambda a: a['staff'][0]['facilities'][0]['routes'][0]['edges'][0].clear(),
}
for name, mutate in mutations.items():
with self.subTest(name=name):
state, report, audit = self.fixture()
mutate(audit)
with self.assertRaises(ValueError):
check.compare_rest(state, report, audit)
def test_ineligible_and_transit_recomputed(self):
state, report, audit = self.fixture()
state['systems']['runtime']['events']['actor_locks']['s'] = 'event'
audit['staff'][0].update(actor_lock=True, comparison_only={})
report['resting_staff'] = {}
self.assertEqual(check.compare_rest(state, report, audit), {'s': {}})
state, report, audit = self.fixture()
state['systems']['runtime']['elevators']['trips'] = [dict(actor_id='staff:s')]
audit['staff'][0]['in_transit'] = True
audit['staff'][0]['comparison_only'].update(resting=False, label='休憩先へ移動中')
report['resting_staff'] = {}
self.assertFalse(check.compare_rest(state, report, audit)['s']['resting'])
class InfrastructureTests(unittest.TestCase):
def test_matrix_requires_each_condition_and_each_category(self):
executed = {c: {k: set(v) for k, v in categories.items()} for c, categories in check.REQUIRED_CHECKS.items()}
self.assertTrue(all(r['status'] == 'passed' for r in check.condition_matrix(executed)))
for condition in check.CONDITIONS:
for category in check.CATEGORIES:
with self.subTest(condition=condition, category=category):
broken = copy.deepcopy(executed)
broken[condition][category].clear()
matrix = check.condition_matrix(broken)
self.assertEqual(next(r for r in matrix if r['condition'] == condition)['status'], 'pending')
self.assertIn(category, check.BLOCKERS(broken)[condition])
def test_distinct_pid_ack_and_formal_hash_negatives(self):
members = {name: 'a'*64 for name in ('world.bin', 'manifest.json', 'thumbnail.png', 'journal.log')}
writer = dict(actual_pid=1)
reader = dict(actual_pid=2, formal_input={k: dict(sha256=v) for k, v in members.items()})
saved = dict(pid=1, members=members)
records = [dict(request=dict(action='load_menu', target_pid=2, sequence=1, requested_ticks_usec=10, requested_unix_ns=90),
ack=dict(target_pid=2, sequence=1, request_sha256='b'*64, timestamp_ns=110),
observed=dict(pid=2, sequence=1, request_sha256='b'*64, ack_observed_ticks_usec=20, ack_observed_unix_ns=120, status='手動セーブを読み込みました。'),
cua=dict(target_pid=2, method='native_os', tool_call_id='actual-call', timestamp_ns=100),
request_sha256='b'*64)]
check.inspect_resume(writer, reader, saved, records, members)
for change in ('pid', 'ack', 'time', 'hash', 'id'):
with self.subTest(change=change):
r, q = copy.deepcopy(reader), copy.deepcopy(records)
if change == 'pid': r['actual_pid'] = 1
if change == 'ack': q.clear()
if change == 'time': q[0]['observed']['ack_observed_ticks_usec'] = 9
if change == 'hash': r['formal_input']['world.bin']['sha256'] = 'c'*64
if change == 'id': q[0]['ack']['sequence'] = 2
with self.assertRaises(ValueError): check.inspect_resume(writer, r, saved, q, members)
def test_interpolation_history_tampering(self):
state = dict(systems=dict(runtime=dict(guests=dict(guests=[]), staff=dict(staff=[]),
elevators=dict(shafts=[], trips=[], current_minute=0))))
report = dict(animation_time=0.0, paused=True, layer=0, zoom=1, pan=[0,0], viewport=[100,100],
actor_tracks={}, elevator_tracks={}, actor_drawings=[], cabins=[])
audit = dict(frames=[dict(frame=1, delta=.01, scaled_delta=0, display=copy.deepcopy(report)),
dict(frame=2, delta=.01, scaled_delta=0, display=copy.deepcopy(report))])
check.compare_motion(state, report, audit)
for change in ('delta', 'frame', 'decreasing_frame', 'no_frames', 'one_frame', 'display', 'report_binding'):
with self.subTest(change=change):
bad = copy.deepcopy(audit)
if change == 'delta': bad['frames'][1]['scaled_delta'] = 1
if change == 'frame': bad['frames'][1]['frame'] = 1
if change == 'decreasing_frame': bad['frames'][1]['frame'] = 0
if change == 'no_frames': bad['frames'].clear()
if change == 'one_frame': bad['frames'].pop()
if change == 'report_binding': bad['frames'][1]['display']['zoom'] = 2
if change == 'display': bad['frames'][1]['display']['animation_time'] = 1
with self.assertRaises(ValueError): check.compare_motion(state, report, bad)
def test_verify_only_missing_index_is_a_concrete_deficit(self):
with mock.patch.dict(check.os.environ, {'BUILD_OUTPUT_DIR': '/missing-p23-proof'}, clear=True):
with contextlib.redirect_stdout(io.StringIO()) as output:
self.assertEqual(check.main(['--verify-only']), 1)
result = json.loads(output.getvalue())
self.assertEqual(result['integrity'], 'fail')
self.assertEqual(result['acceptance'], 'pending')
self.assertIn('index.json', result['deficits'][0])
self.assertNotIn('blocked', result['deficits'][0])
def test_verify_only_requires_build_output(self):
with mock.patch.dict(check.os.environ, {}, clear=True):
with contextlib.redirect_stderr(io.StringIO()) as output:
self.assertEqual(check.main(['--verify-only']), 2)
self.assertIn('BUILD_OUTPUT_DIR is required', output.getvalue())
def test_normative_plan_change_invalidates_but_progress_does_not(self):
root = check.ROOT
text = (root / check.PLAN).read_text()
original = check.input_map(root)
progress = text.splitlines(keepends=True)
progress[2] = 'Progress-only test mutation\n'
with mock.patch.object(Path, 'read_text', return_value=''.join(progress)):
self.assertEqual(check.input_map(root), original)
changed = text.replace('同需要の動線改善を観察できる。', 'altered normative acceptance')
self.assertNotEqual(changed, text)
with mock.patch.object(Path, 'read_text', return_value=changed):
self.assertNotEqual(check.input_map(root), original)
self.assertNotIn(check.PLAN, original)
self.assertIn('full_plan_sha256', check.input_provenance(root))
for path in (root / 'tests/fixtures/p23-03').rglob('*'):
if path.is_file():
self.assertIn(path.relative_to(root).as_posix(), original)
def test_os_process_guards_reject_engine_profile_and_source_drift(self):
source = {'source.gd': 'a' * 64}
receipt = dict(engine_ended=True, passed=True, exit_code=0, error=None,
guard_errors=[], cleanup_errors=[], default_before={}, default_after={},
stage_before={}, stage_after={}, source_map=source,
source_after={'source.gd': {'sha256': 'a' * 64}},
owned_cleanup=[{'engine_ended': True}], ordinary_owner_proof={'present': True})
receipt.update(spec_sha256=check.SPEC_SHA, inputs={'spec': 'a'*64}, inputs_after={'spec': 'a'*64},
environment={'P23_LAYOUT': 'A'}, environment_after={'P23_LAYOUT': 'A'}, actual_pid=123,
profile='/private/test', profile_name='test', profile_before={'existed': False, 'files': {}}, profile_after={})
for field in ('engine_sha256', 'settings_sha256', 'environment_sha256'):
receipt[field] = receipt[field+'_after'] = 'a'*64
check.inspect_os_receipt(receipt, source)
for field, value in [('engine_ended', False), ('default_after', {'changed': True}),
('source_after', {'source.gd': {'sha256': 'b' * 64}}),
('stage_after', {'changed': True}), ('owned_cleanup', []),
('environment_after', {}), ('profile_before', {}),
('engine_sha256_after', 'b'*64), ('spec_sha256', 'b'*64)]:
with self.subTest(field=field):
altered = copy.deepcopy(receipt)
altered[field] = value
with self.assertRaises(ValueError):
check.inspect_os_receipt(altered, source)
class SemanticTests(unittest.TestCase):
def setUp(self):
self.guest = dict(guest_id='g', state='QUEUING', current_cell=[1, 0, 0],
target_facility_id='f', current_service_id='', queue_request_id='q')
self.staff = dict(staff_id='s', state='WORKING', current_cell=[2, 0, 1],
current_task_id='t', assigned_facility_id='f', role_id='porter',
carried_inventory_id='cargo')
self.task = dict(task_id='t', state='WORKING', task_type='delivery',
assigned_staff_id='s', target_entity_id='request', rescue_facility_id='')
self.snapshot = {'systems': {'runtime': {'guests': {'guests': [self.guest]},
'staff': {'staff': [self.staff], 'tasks': [self.task]}}}}
self.report = dict(guests=[dict(id='g', state='QUEUING', cell=[1, 0, 0], target='f', service='', queue='q')],
staff=[dict(id='s', state='WORKING', cell=[2, 0, 1], task='t', facility='f',
role='porter', carried_inventory_id='cargo')],
tasks=[dict(id='t', state='WORKING', type='delivery', staff='s',
target='request', rescue_facility='')])
def test_real_schema_projection(self):
self.assertEqual(check.compare_actor_rows(self.snapshot, self.report), ['guests', 'staff', 'tasks'])
def test_changed_state_position_target_task_and_goods_are_rejected(self):
for category, field, replacement in [('guests', 'cell', [8, 0, 0]), ('guests', 'target', 'other'),
('guests', 'state', 'MOVING'), ('staff', 'task', ''),
('staff', 'carried_inventory_id', ''), ('tasks', 'staff', 'other')]:
with self.subTest(category=category, field=field):
report = copy.deepcopy(self.report)
report[category][0][field] = replacement
with self.assertRaises(ValueError):
check.compare_actor_rows(self.snapshot, report)
def test_missing_extra_and_boolean_only_rows_are_rejected(self):
for bad in [[], self.report['guests'] * 2, [True]]:
report = copy.deepcopy(self.report)
report['guests'] = bad
with self.assertRaises(ValueError):
check.compare_actor_rows(self.snapshot, report)
def test_unknown_enum_and_duplicate_id_are_rejected(self):
self.guest['state'] = 'FICTIONAL'
with self.assertRaises(ValueError):
check.compare_actor_rows(self.snapshot, self.report)
self.guest['state'] = 'QUEUING'
self.snapshot['systems']['runtime']['guests']['guests'].append(copy.deepcopy(self.guest))
with self.assertRaises(ValueError):
check.compare_actor_rows(self.snapshot, self.report)
def test_typed_rectangle_and_duplicate_vector_key(self):
self.assertEqual(check.decode(dict(type='Rect2', text='Rect2(1, 2, 16, 24)')), [1, 2, 16, 24])
with self.assertRaises(ValueError):
check.decode(dict(type='Rect2', text='Rect2(1, nan, 16, 24)'))
key = dict(type='Vector3i', text='Vector3i(1, 2, 0)')
value = dict(type='int', text='1')
with self.assertRaises(ValueError):
check.decode(dict(type='Dictionary', entries=[[key, value], [key, value]]))
def test_immutable_typed_input_is_not_modified(self):
before = copy.deepcopy(self.snapshot)
check.compare_actor_rows(self.snapshot, self.report)
self.assertEqual(before, self.snapshot)
class SpatialTests(unittest.TestCase):
def setUp(self):
self.runtime = dict(world=dict(bounds=dict(min_x=0, max_x=1, min_y=0, max_y=0),
walkable_cells=[[0, 0, 0], [1, 0, 0]], occupancies=[]),
path=dict(cell_traffic=[dict(cell=[0, 0, 0], base_capacity=2,
current_density=3, dynamic_cost=0, temporary_block=False)]),
facilities=dict(instances=[], requests=[]), guests=dict(guests=[]),
staff=dict(staff=[], tasks=[]), logistics_requests=[],
elevators=dict(current_minute=0, shafts=[], trips=[]))
self.snapshot = dict(systems=dict(runtime=self.runtime))
self.report = dict(viewport=[184, 164], zoom=1, pan=[0, 0], layer=0,
traffic=[dict(cell=[0, 0, 0], rect=[78, 72, 12, 20],
traffic=dict(base_capacity=2, current_density=3,
dynamic_cost=0, temporary_block=False))],
queue_positions={}, queue_hidden={}, landing_badges=[], rooms={},
treated_patients={}, carried_patients={}, resting_staff={},
goods=dict(requests=[], deliveries=[]))
def test_density_and_pan_are_independently_recomputed(self):
check.compare_traffic(self.snapshot, self.report)
for key, value in [('current_density', 0), ('base_capacity', 9)]:
changed = copy.deepcopy(self.report)
changed['traffic'][0]['traffic'][key] = value
with self.assertRaises(ValueError):
check.compare_traffic(self.snapshot, changed)
self.report['pan'][0] = 20
with self.assertRaises(ValueError):
check.compare_traffic(self.snapshot, self.report)
def test_missing_queue_overflow_rejected(self):
saved = json.loads((check.ROOT / 'tests/fixtures/p23-03/queued_elevator.json').read_text())['saved']
self.runtime['elevators'] = saved
saved['trips'] = [dict(copy.deepcopy(saved['trips'][0]), actor_id='guest:g'+str(i),
request_id='trip'+str(i), enqueued_at=i) for i in range(3)]
self.report.update(queue_positions={'guest/g0': [100, 82]},
queue_hidden={'guest/g1': True, 'guest/g2': True}, landing_badges=[dict(at=[-4, 54],
text='待ち3人(他2人)', cell=[0, 0, 0],
members=['guest/g0', 'guest/g1', 'guest/g2'], visible_count=1, overflow=2)])
check.compare_queues(self.snapshot, self.report)
self.report['landing_badges'][0]['overflow'] = 0
with self.assertRaises(ValueError):
check.compare_queues(self.snapshot, self.report)
def test_fictional_goods_and_rooms_rejected(self):
check.compare_goods(self.snapshot, self.report)
check.compare_room_services(self.snapshot, self.report)
self.report['goods']['requests'].append(dict(stock=100))
with self.assertRaises(ValueError):
check.compare_goods(self.snapshot, self.report)
self.report['rooms']['invented'] = dict(count=0)
with self.assertRaises(ValueError):
check.compare_room_services(self.snapshot, self.report)
def test_room_reason_and_rescue_occupancy(self):
room = dict(instance_id='clinic', origin=[0, 0, 0], definition_id='infirmary',
state='NO_POWER', state_reasons=['NO_POWER'], suspension_reason='',
customer_capacity=2, active_staff_ids=[], active_services=[], occupied_cells=[])
self.runtime['facilities']['instances'] = [room]
self.runtime['staff']['tasks'] = [dict(task_id='rescue', task_type='rescue_transport',
state='WORKING', assigned_staff_id='carrier', target_entity_id='patient', rescue_facility_id='clinic')]
self.report['treated_patients'] = {'patient': 'clinic'}
self.report['rooms'] = {'clinic': dict(state=2, state_name='NO_POWER', reasons=['NO_POWER'],
operation=0, people=['patient'], count=1, limit=2, staff=[], suspension='')}
check.compare_room_services(self.snapshot, self.report)
for field, wrong in [('state', 6), ('operation', 1), ('people', []), ('count', 0)]:
changed = copy.deepcopy(self.report)
changed['rooms']['clinic'][field] = wrong
with self.assertRaises(ValueError):
check.compare_room_services(self.snapshot, changed)
def test_cabin_geometry_and_phase(self):
shaft = dict(config=dict(shaft_id='lift', stops=[[0, 0, 0]], width=2, capacity=4),
current_floor=0, phase='IDLE', operational=True, boarded=[])
self.runtime['elevators']['shafts'] = [shaft]
self.report.update(paused=True, animation_time=0, elevator_tracks={'lift': dict(floor=0, at=0)},
cabins=[dict(shaft_id='lift', rect=[80, 72, 24, 19], pixel=1, operational=True,
doors_open=True, badge_at=[80, 58], badge='0/4人', passengers=[])])
check.compare_cabin_geometry(self.snapshot, self.report)
for field, value in [('rect', [80, 70, 24, 19]), ('doors_open', False), ('badge', '4/4人')]:
changed = copy.deepcopy(self.report)
changed['cabins'][0][field] = value
with self.assertRaises(ValueError):
check.compare_cabin_geometry(self.snapshot, changed)
class ComparisonTests(unittest.TestCase):
def pair(self):
declaration = dict(metric='sum_destination_arrival_minutes_entire_cohort',
arrivals=[dict(id='g', offset=0)], window=2, to=[2, 1, 0],
improvement=dict(definition_id='guest_elevator', origin=[4, 0, 0]))
a = dict(declaration=declaration, initial=dict(seed=1))
a['arrival-g'] = dict(offset=0, payload=dict(guest_id='g'), result=dict(code='OK'))
for minute in range(3):
a['tick-%03d' % minute] = dict(minute=minute, guests=dict(guests=[dict(guest_id='g',
current_cell=[2, 1, 0] if minute == 2 else [2, 0, 0])]))
b = copy.deepcopy(a)
b['build-0'] = dict(payload=declaration['improvement'], result=dict(code='OK'))
b['tick-001']['guests']['guests'][0]['current_cell'] = [2, 1, 0]
return a, b
def test_strict_metric_from_typed_cells(self):
a, b = self.pair()
self.assertEqual(check.compare_layouts(a, b)['B'], 1)
for mutation in ('demand', 'incomplete', 'no_improvement', 'extra_build'):
with self.subTest(mutation=mutation):
a, b = self.pair()
if mutation == 'demand':
b['arrival-g']['payload']['guest_id'] = 'other'
elif mutation == 'incomplete':
for minute in (1, 2):
b['tick-%03d' % minute]['guests']['guests'][0]['current_cell'] = [2, 0, 0]
elif mutation == 'no_improvement':
b['tick-001'] = copy.deepcopy(a['tick-001'])
else:
b['build-1'] = copy.deepcopy(b['build-0'])
with self.assertRaises(ValueError):
check.compare_layouts(a, b)
class OfflineGeometryTests(unittest.TestCase):
"""Synthetic calibration examples; no OS or acceptance credit."""
def setUp(self):
self.calibration = dict(window_size=[100, 80], window_position=[240, 150],
screenshot_pixels=[200, 190], backing_scale=2,
canvas_stretch=1, titlebar_pixels=30)
self.request = dict(window_size=[100, 80], window_position=[240, 150],
screen_scale=2, content_scale_factor=1, target_pid=123)
self.clicks = [dict(target_pid=123, events=[dict(action='click',
coordinates=[20, 70], calibration=copy.deepcopy(self.calibration),
point=dict(screen=[10, 20], viewport=[10, 20],
viewport_to_screen=[1, 0, 0, 1, 0, 0]))])]
def transform(self):
return check.screenshot_transform(self.calibration, self.request, [200, 190], self.clicks)
def test_uses_measured_scale_origin_not_desktop_position(self):
self.assertEqual(check.affine_rect(self.transform(), [10, 20, 5, 7]), [20, 70, 10, 14])
def test_shifted_titlebar_fails_independent_click(self):
self.calibration['titlebar_pixels'] += 1
self.calibration['screenshot_pixels'][1] += 1
self.clicks[0]['events'][0]['calibration'] = copy.deepcopy(self.calibration)
with self.assertRaises(ValueError):
check.screenshot_transform(self.calibration, self.request, [200, 191], self.clicks)
def test_wrong_click_offset_fails(self):
self.clicks[0]['events'][0]['coordinates'][1] -= 30
with self.assertRaisesRegex(ValueError, 'click screenshot offset'):
self.transform()
def test_wrong_pid_cannot_calibrate(self):
self.clicks[0]['target_pid'] += 1
with self.assertRaisesRegex(ValueError, 'missing independent click'):
self.transform()
def test_nonfinite_and_false_scale_rejected(self):
for scale in (False, float('nan'), 0, -1):
self.calibration['backing_scale'] = scale
with self.assertRaisesRegex(ValueError, 'invalid screenshot calibration'):
self.transform()
def test_all_visibility_cases_are_accounted_for(self):
self.assertEqual(check.rectangle_eligibility([2, 3, 4, 5], [10, 10]),
('visible', [2, 3, 4, 5]))
self.assertEqual(check.rectangle_eligibility([-2, 3, 4, 5], [10, 10]),
('clipped', [0, 3, 2, 5]))
self.assertEqual(check.rectangle_eligibility([10, 3, 4, 5], [10, 10])[0], 'outside')
self.assertEqual(check.rectangle_eligibility([2, 3, 0, 5], [10, 10])[0], 'outside')
def test_invalid_rectangles_rejected(self):
for rect in ([0, 0, -1, 4], [float('nan'), 0, 1, 4]):
with self.assertRaisesRegex(ValueError, 'invalid clipping rectangle'):
check.rectangle_eligibility(rect, [10, 10])
def test_noise_is_measured_and_does_not_assert_class(self):
before = (2, 2, [[(10, 20, 30)]*2 for _ in range(2)])
after = copy.deepcopy(before)
after[2][1][1] = (10, 27, 30)
noise = check.paired_crop_noise(before, after, [0, 0, 2, 2])
self.assertEqual(noise, dict(samples=4, max_channel_delta=7,
mean_channel_delta=1.75, unchanged=3))
self.assertNotIn('pass', noise)
self.assertNotIn('state', noise)
def test_wrong_dimensions_and_empty_crop_rejected(self):
image = (1, 1, [[(0, 0, 0)]])
with self.assertRaisesRegex(ValueError, 'paired image dimensions'):
check.paired_crop_noise(image, (2, 1, []), [0, 0, 1, 1])
with self.assertRaisesRegex(ValueError, 'empty noise crop'):
check.paired_crop_noise(image, image, [1, 0, 1, 1])
def test_report_positions_and_source_provenance_are_recomputed(self):
source = 'scripts/ui/pixel_people.gd'
report = dict(rooms={}, landing_badges=[], cabins=[], resting_staff={}, badges=[],
actor_drawings=[dict(key='staff/s', staff=True, at=[1, 1], scale=1)])
colors = list(dict.fromkeys(check.re.findall(r'Color\("([0-9a-fA-F]{6})"',
(check.ROOT/source).read_text())))
audit = dict(rows=[dict(category='staff', id='staff/s', rect=[1, 1, 16, 18],
style_source=source, style_colors=colors)])
check.validate_drawing_rows(report, audit)
for change in ('shift', 'identity', 'missing', 'palette'):
tampered = copy.deepcopy(audit)
if change == 'shift': tampered['rows'][0]['rect'][0] += 1
if change == 'identity': tampered['rows'][0]['category'] = 'guest'
if change == 'missing': tampered['rows'] = []
if change == 'palette': tampered['rows'][0]['style_colors'] = ['000000']
with self.subTest(change=change), self.assertRaises(ValueError):
check.validate_drawing_rows(report, tampered)
class FrameReviewTests(unittest.TestCase):
def setUp(self):
OfflineGeometryTests.setUp(self)
self.request.update(local_to_viewport=[1, 0, 0, 1, 0, 0],
viewport_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=[],
landing_badges=[], badges=[], resting_staff={}, traffic=[],
actor_drawings=[dict(key='staff/s', staff=True, at=[1, 1], scale=1, state=0)])
source = 'scripts/ui/pixel_people.gd'
colors = list(dict.fromkeys(check.re.findall(r'Color\("([0-9a-fA-F]{6})"',
(check.ROOT/source).read_text())))
self.audit = dict(rows=[dict(category='staff', id='staff/s', rect=[1, 1, 16, 18],
viewport_rect=[1, 1, 16, 18], screen_rect=[1, 1, 16, 18],
style_source=source, style_colors=colors)])
self.image = (200, 190, [[(x % 256, y % 256, 0) for x in range(200)] for y in range(190)])
def frame(self):
return check.frame_rows(self.report, self.audit, self.request,
dict(calibration=self.calibration), [self.image], self.clicks)
def test_blank_crop_fails_without_threshold(self):
self.image = (200, 190, [[(0, 0, 0)] * 200 for _ in range(190)])
result = self.frame()
self.assertFalse(result['mechanical_pass'])
self.assertIn('blank crop', result['rows'][0]['failures'])
def test_missing_extra_and_unaudited_traffic_fail(self):
for change in ('missing', 'extra', 'traffic'):
with self.subTest(change=change):
original = copy.deepcopy(self.audit)
if change == 'missing': self.audit['rows'].clear()
if change == 'extra': self.audit['rows'] *= 2
if change == 'traffic':
self.report['traffic'] = [dict(cell=[-6, 2, 0], rect=[1, 1, 5, 5],
traffic=dict(current_density=1, temporary_block=False))]
result = self.frame()
self.assertFalse(result['mechanical_pass'])
self.assertEqual(len(result['rows']), len(self.audit['rows']))
self.assertIn('offline rectangle coverage', result['errors'])
self.audit = original
def test_dense_or_blocked_traffic_row_closes_audit_gap(self):
source = 'scenes/main/world_preview.gd'
colors = list(dict.fromkeys(check.re.findall(r'Color\("([0-9a-fA-F]{6})"',
(check.ROOT/source).read_text())))
for density, blocked in ((1, False), (0, True), (0, False)):
with self.subTest(density=density, blocked=blocked):
self.report['traffic'] = [dict(cell=[-6, 2, 0], rect=[1, 1, 5, 5],
traffic=dict(current_density=density, temporary_block=blocked))]
active = density > 0 or blocked
missing = self.frame()
self.assertEqual(missing['mechanical_pass'], not active)
if active:
self.assertIn('offline rectangle coverage', missing['errors'])
self.audit['rows'].append(dict(category='traffic', id='-6,2,0',
rect=[1, 1, 5, 5], viewport_rect=[1, 1, 5, 5],
screen_rect=[1, 1, 5, 5], style_source=source, style_colors=colors))
complete = self.frame()
self.assertTrue(complete['mechanical_pass'], complete['errors'])
self.audit['rows'].pop()
def test_wrong_state_enum_and_name_fail(self):
source = 'scripts/ui/pixel_tower_art.gd'
self.report['actor_drawings'] = []
room = dict(rect=[1, 1, 16, 18], state=3, state_name='NO_STAFF', operation=0)
self.report['rooms'] = {'r': room}
self.audit['rows'][0].update(category='room', id='r', style_source=source,
style_colors=list(dict.fromkeys(check.re.findall(r'Color\("([0-9a-fA-F]{6})"',
(check.ROOT/source).read_text()))))
self.assertEqual(self.frame()['rows'][0]['operation'], 'INACTIVE')
for state, name in ((99, 'NO_STAFF'), (3, 'OPEN'), (True, 'NO_ACCESS')):
room.update(state=state, state_name=name)
with self.assertRaises(ValueError): self.frame()
def test_frame_rejects_affine_and_calibration_shift(self):
self.request['local_to_screen'][4] += 1
with self.assertRaisesRegex(ValueError, 'affine composition'): self.frame()
self.request['local_to_screen'][4] -= 1
self.clicks[0]['events'][0]['coordinates'][1] += 1
with self.assertRaisesRegex(ValueError, 'click screenshot offset'): self.frame()
def test_eligible_crop_fully_outside_image_fails(self):
for key in ('viewport_to_screen', 'local_to_screen'):
self.request[key][4] = 300
self.audit['rows'][0]['screen_rect'][0] += 300
result = self.frame()
self.assertFalse(result['mechanical_pass'])
self.assertIn('eligible crop outside image', result['rows'][0]['failures'])
def test_manifest_crop_and_independent_review_binding(self):
with tempfile.TemporaryDirectory() as temporary:
directory = Path(temporary)
manifest, digest = check.write_frame_review(directory, self.frame(),
[dict(whole_image_sha256='a'*64, raw_sha256='b'*64, conversion_sha256='c'*64)])
self.assertEqual(check.sha(directory/'manifest.json'), digest)
crop = manifest['rows'][0]['crops'][0]
self.assertEqual(check.sha(directory/crop['path']), crop['sha256'])
self.assertEqual(check.png_pixels((directory/crop['path']).read_bytes())[:2], (32, 36))
verdict_path = directory/'independent-verdict.json'
self.assertEqual(check.reviewed_pixel_categories(manifest, digest, verdict_path), set())
verdict = dict(manifest_sha256=digest, reviewer='independent-opus-direct-image',
rows=[dict(row_index=0, category=True, state=True, position=True)])
for change in ('wrong_sha', 'missing_row', 'extra_row', 'rejected_state'):
bad = copy.deepcopy(verdict)
if change == 'wrong_sha': bad['manifest_sha256'] = '0'*64
if change == 'missing_row': bad['rows'] = []
if change == 'extra_row': bad['rows'] *= 2
if change == 'rejected_state': bad['rows'][0]['state'] = False
verdict_path.write_text(json.dumps(bad))
with self.subTest(change=change), self.assertRaises(ValueError):
check.reviewed_pixel_categories(manifest, digest, verdict_path)
verdict_path.write_text(json.dumps(verdict))
self.assertEqual(check.reviewed_pixel_categories(manifest, digest, verdict_path), {'staff'})
manifest['mechanical_pass'] = False
self.assertEqual(check.reviewed_pixel_categories(manifest, digest, verdict_path), set())
if __name__ == '__main__':
unittest.main()