feat(diagnostics): correlate movement acknowledgements with painted views
This commit is contained in:
@@ -1267,7 +1267,7 @@ confirmation remains pending.
|
||||
|
||||
## Milestone 015 — Pre-reader child-friendly game experience
|
||||
|
||||
**Status:** `IN_PROGRESS`
|
||||
**Status:** `COMPLETE`
|
||||
|
||||
### Objective
|
||||
|
||||
@@ -1363,9 +1363,9 @@ rules.
|
||||
|
||||
### Evidence
|
||||
|
||||
The implementation and automated verification are complete; the required
|
||||
representative pre-reader observation or owner-approved evidence deviation
|
||||
remains pending.
|
||||
The implementation and automated verification are complete. The project owner
|
||||
approved the required evidence deviation after manual gameplay, as recorded
|
||||
below.
|
||||
|
||||
- The player screen now uses circle and diamond character shapes instead of
|
||||
`P1`/`P2`, icon-and-shape ownership markers for keys and exits, dynamic
|
||||
@@ -1398,7 +1398,273 @@ remains pending.
|
||||
`pio run -e esp32-c6-devkitm-1 --target buildfs` passed; the fixed LittleFS
|
||||
image used 2,031,616 of 2,031,616 bytes and SHA-256
|
||||
`ea6813e603d883a1f6b79a9f9efbd20e7d02f010052210716a89d7e863716415`.
|
||||
- Acceptance criterion 11 still requires the facilitated representative
|
||||
pre-reader observation from `docs/PRE_READER_USABILITY.md`, or a
|
||||
project-owner-approved documented deviation. Therefore Milestone 015 remains
|
||||
`IN_PROGRESS`; Milestone 016 has not been started.
|
||||
- On 2026-09-09, the project owner manually exercised the gameplay and
|
||||
confirmed that all host, firmware, and filesystem checks above pass. The
|
||||
owner also found the separate on-screen joystick imitation uncomfortable and
|
||||
insufficiently responsive for real play. The owner explicitly approved the
|
||||
criterion-11 evidence deviation: no representative pre-reader observation is
|
||||
required for this milestone. Movement-latency diagnosis is deferred to
|
||||
Milestone 016, and the resulting avatar-centered control refinement is
|
||||
deferred to Milestone 017. Criteria 1--10 have the recorded implementation
|
||||
and automated evidence above; criterion 11 has this documented owner
|
||||
approval. Milestone 015 is therefore `COMPLETE`.
|
||||
|
||||
## Milestone 016 — End-to-end movement latency diagnosis
|
||||
|
||||
**Status:** `IN_PROGRESS`
|
||||
|
||||
### Objective
|
||||
|
||||
Measure and attribute the delay from a player's touch to the corresponding
|
||||
authoritative map update, determine whether the unacceptable delays originate
|
||||
in the local Wi-Fi network, browser input and rendering, ESP firmware, or their
|
||||
interaction under load, and produce evidence that selects the remediation for
|
||||
Milestone 017.
|
||||
|
||||
### Scope
|
||||
|
||||
- Define separate latency intervals for local pointer feedback, browser command
|
||||
scheduling, WebSocket uplink, ESP receive/parse/game processing, outbound
|
||||
queueing and serialization, WebSocket downlink, complete-view assembly, DOM
|
||||
update, and the next painted frame.
|
||||
- Add bounded diagnostic correlation data and timing counters where existing
|
||||
metrics cannot isolate an interval. Diagnostics must not make browser time
|
||||
authoritative, expose hidden maze state, or create unbounded logs, queues,
|
||||
allocations, or protocol messages.
|
||||
- Measure ICMP and application-level round-trip baselines on the same local
|
||||
Wi-Fi path, including distribution and jitter rather than only an average.
|
||||
- Exercise accepted movement, blocked movement, direction changes, sustained
|
||||
movement, release, and cooldown rejection with two active players, first
|
||||
without observers and then at the six-session limit with four observers.
|
||||
- Compare WebSocket acknowledgement time with the time at which the matching
|
||||
authoritative view has been completely assembled and painted. Record view
|
||||
chunk count, socket buffered bytes, gateway queue depth, send duration, and
|
||||
browser parse, layout, and paint cost for the same test intervals.
|
||||
- Profile the current sequential per-session view broadcast and the browser's
|
||||
complete maze DOM replacement without changing their behavior during the
|
||||
baseline run.
|
||||
- Classify each observed delay by its dominant interval, document reproducible
|
||||
test conditions, and select bounded frontend, protocol, firmware, and/or
|
||||
network remediations for Milestone 017.
|
||||
|
||||
### Deliverables
|
||||
|
||||
- A documented pointer-to-paint latency model with unambiguous measurement
|
||||
points and command/view correlation.
|
||||
- A repeatable local-device measurement procedure and bounded diagnostic
|
||||
instrumentation for browser and firmware.
|
||||
- Results for baseline and maximum supported session load, including latency
|
||||
percentiles, maxima, network jitter, queue high-water marks, view sizes, and
|
||||
browser rendering costs.
|
||||
- A root-cause report that ranks measured contributors and records the selected
|
||||
remediation scope and performance budget for Milestone 017.
|
||||
- Automated tests for correlation, bounded diagnostics, counter reset/wrap
|
||||
behavior, and preservation of authoritative and hidden-state contracts.
|
||||
|
||||
### Acceptance criteria
|
||||
|
||||
1. The test procedure records at least 200 accepted movement samples for both
|
||||
the two-player/no-observer case and the two-player/four-observer case, with
|
||||
`p50`, `p95`, `p99`, and maximum values for every measurable latency
|
||||
interval.
|
||||
2. Local pointer-feedback, command-to-acknowledgement,
|
||||
command-to-complete-view, and command-to-painted-frame latency are measured
|
||||
separately; ESP processing and send durations use the ESP clock, while
|
||||
browser intervals use the browser clock, without assuming synchronized
|
||||
clocks across devices.
|
||||
3. The same report includes at least 100 network round-trip samples with packet
|
||||
loss, minimum, average, maximum, jitter, and the access-point and client
|
||||
conditions needed to reproduce the run.
|
||||
4. Tests cover accepted and rejected commands, blocked movement, direction
|
||||
changes, sustained movement, release, two-player load, and maximum supported
|
||||
observer load without disrupting server authority or exposing hidden state.
|
||||
5. The report determines whether network transport, firmware processing and
|
||||
broadcast, frontend scheduling and rendering, or multiple contributors are
|
||||
responsible for the observed delays, and supports the conclusion with
|
||||
correlated measurements rather than inference alone.
|
||||
6. Diagnostic messages, counters, queues, retries, and allocations are bounded;
|
||||
production behavior remains safe when diagnostics are disabled, a client
|
||||
disconnects, or correlation identifiers wrap.
|
||||
7. All host and browser tests pass, firmware and filesystem images remain
|
||||
within their partitions, and device measurements record exact build identity
|
||||
and relevant commands.
|
||||
|
||||
### Evidence
|
||||
|
||||
Implementation and initial device validation are in progress. The following
|
||||
planning-time observations justify the milestone but do not satisfy its
|
||||
controlled measurement criteria:
|
||||
|
||||
- On 2026-09-09, the project owner reported intermittent input delays up to
|
||||
approximately 500 ms and approved active browser and WebSocket measurements
|
||||
against the device at `192.168.1.50`.
|
||||
- A live `/diagnostics` sample with two connections and 24 accepted game
|
||||
commands reported no dropped, failed, malformed, oversized, or rate-limited
|
||||
messages; maximum handler time was 7,800 microseconds and maximum send time
|
||||
was 1,571 microseconds. This low-load sample does not measure end-to-end
|
||||
latency or rule out sequential broadcast delays under session load.
|
||||
- A 20-packet ICMP sample to `192.168.1.50` had zero loss but measured
|
||||
`60.673 / 147.866 / 246.995 ms` minimum/average/maximum round-trip time with
|
||||
`49.350 ms` deviation. The network alone therefore already exceeds the ideal
|
||||
100 ms target in this setup, but correlation is still required before
|
||||
assigning the reported 500 ms delays.
|
||||
- Static inspection found that each accepted movement synchronously broadcasts
|
||||
authoritative view chunks to sessions, while the player browser waits for a
|
||||
complete view and replaces the maze DOM before the updated frame is visible.
|
||||
These are candidate contributors to measure, not yet established root causes.
|
||||
- The browser now stores at most 256 local movement samples and correlates each
|
||||
sent movement request with its acknowledgement's authoritative
|
||||
`stateSequence`, the complete matching view, and the next painted frame.
|
||||
The gateway includes that state sequence in accepted game acknowledgements;
|
||||
no browser timestamp is sent to the ESP and no hidden state is added to a
|
||||
player projection. `docs/M016_LATENCY_PROCEDURE.md` specifies the required
|
||||
controlled runs and interval boundaries.
|
||||
- On 2026-09-09, the focused browser test plus `bash scripts/run_host_tests.sh`
|
||||
passed with 16 browser tests, including bounded latency correlation. Firmware
|
||||
and filesystem builds passed: application usage was 1,027,966 of 2,097,152
|
||||
bytes and the fixed LittleFS image used 2,031,616 of 2,031,616 bytes.
|
||||
- On 2026-09-09, the diagnostics firmware and filesystem image were flashed to
|
||||
the confirmed ESP32-C6FH4 on `/dev/ttyACM0`; the health endpoint reported
|
||||
`serving`. A six-session smoke run recorded 10 accepted commands. Its
|
||||
post-run diagnostics reported a 23,842 microsecond maximum handler duration,
|
||||
a 575,875 microsecond maximum send duration, queue high water of 8/12, and
|
||||
five dropped plus four failed sends. This is not the required 200-sample
|
||||
controlled measurement and cannot identify a root cause.
|
||||
- M016 remains `IN_PROGRESS`. The two 200-accepted-move browser/device runs,
|
||||
100-sample ICMP and application round trips, interval percentiles, and
|
||||
evidence-based remediation selection are still required. Milestone 017 has
|
||||
not been started.
|
||||
- On 2026-09-09, an initial manual run had no drops, send failures, malformed,
|
||||
oversized, or rate-limited messages. Before the run, diagnostics reported
|
||||
8 accepted commands, a 10,779 microsecond maximum handler duration, and a
|
||||
1,591 microsecond maximum send duration. Afterwards, it reported 165
|
||||
accepted commands, a 9,800 microsecond maximum handler duration, a
|
||||
1,572 microsecond maximum send duration, queue high water of 4/12, and no
|
||||
message drop or failure. One browser retained 65 correlated samples: its
|
||||
command-to-acknowledgement values ranged from 19 to 548 ms and its
|
||||
command-to-painted-view values ranged from 30 to 551 ms. An interrupted
|
||||
18-packet ICMP sample had zero loss but measured
|
||||
`35.904 / 121.461 / 219.597 ms` minimum/average/maximum round trip with
|
||||
`51.715 ms` deviation. These partial data point to Wi-Fi transport and/or
|
||||
browser scheduling rather than the measured ESP handler or send path, but
|
||||
they are insufficient for the required percentile-based conclusion.
|
||||
- The attempted six-session application run was invalid because three browser
|
||||
sessions were already active; opening four observers would exceed the
|
||||
six-connection limit. A second attempt then timed out while joining. Close
|
||||
all existing player and observer tabs or restart the device before the clean
|
||||
six-session run.
|
||||
- On 2026-09-09, a later interrupted 57-packet ICMP run still had zero loss
|
||||
but measured `26.403 / 114.039 / 343.368 ms` minimum/average/maximum round
|
||||
trip with `54.637 ms` deviation. A complete 100-request `/health`
|
||||
application-round-trip run measured 7.5 ms minimum, 186.7 ms average,
|
||||
508.0 ms p95, 713.3 ms p99, and 985.4 ms maximum. The M010 six-session
|
||||
script again timed out waiting for the first player `join` response, so it
|
||||
produced no valid application-load samples. These results establish an
|
||||
unacceptable local network tail but do not replace the required clean
|
||||
WebSocket load run or browser interval percentiles.
|
||||
- On 2026-09-09, the project owner approved skipping the M010 six-session
|
||||
script for M016 evidence after repeated timeouts before the first `join`
|
||||
response. The run is recorded as unavailable on the present Wi-Fi path, not
|
||||
as a passing load result. The available ICMP, HTTP, browser, and ESP timing
|
||||
evidence remains the basis for selecting the network remediation; Milestone
|
||||
017 is not started by this deviation.
|
||||
|
||||
## Milestone 017 — Avatar-centered drag steering and latency remediation
|
||||
|
||||
**Status:** `NOT_STARTED`
|
||||
|
||||
### Objective
|
||||
|
||||
Replace the separate on-screen joystick with direct dragging of the centered
|
||||
player avatar, keep the avatar's small local deflection as immediate wordless
|
||||
direction and speed feedback while the authoritative map moves underneath it,
|
||||
and apply the measured Milestone 016 remediations so normal local play feels
|
||||
immediate and never approaches the observed 500 ms delay.
|
||||
|
||||
### Scope
|
||||
|
||||
- Make the visible centered player avatar the movement handle, backed by a
|
||||
generous transparent touch target that does not obscure nearby maze cells.
|
||||
- Keep the avatar visually near the viewport center. During a drag, offset it
|
||||
only within a small bounded radius in the drag direction; on release or any
|
||||
reset condition, animate it back to the exact center.
|
||||
- Convert drag angle to exactly one of the four existing cardinal movement
|
||||
intentions with a dead zone and diagonal hysteresis. Map displacement
|
||||
magnitude monotonically to a bounded movement cadence: a small intentional
|
||||
offset moves slowly and a larger offset approaches, but never exceeds, the
|
||||
authoritative 150 ms cooldown.
|
||||
- Move the maze beneath the centered avatar only when server-approved state is
|
||||
received. Local avatar deflection is input feedback and must never predict a
|
||||
successful cell transition, inventory change, visibility change, or result.
|
||||
- Coalesce rapidly changing movement intent and bound in-flight commands so a
|
||||
slow socket, delayed acknowledgement, direction reversal, or release cannot
|
||||
leave stale movement queued in the browser or gateway.
|
||||
- Apply the frontend, protocol, firmware, and/or deployment-network changes
|
||||
selected by Milestone 016. Optimize the measured bottlenecks, including view
|
||||
assembly, rendering, or broadcast only when supported by its evidence.
|
||||
- Preserve keyboard and swipe alternatives, observer read-only behavior,
|
||||
hidden-state protection, Russian accessibility labels, mute and reduced
|
||||
motion behavior, and all pause, disconnect, and focus-loss reset paths.
|
||||
- Update the canonical UI and pre-reader usability documents and validate the
|
||||
interaction on representative mobile touch hardware.
|
||||
|
||||
### Deliverables
|
||||
|
||||
- Avatar-centered drag control with bounded deflection, direction hysteresis,
|
||||
displacement-based speed, release-to-center animation, and safe reset paths.
|
||||
- Authoritative map-follow behavior and bounded latest-intent command flow.
|
||||
- The evidence-selected latency optimizations across browser, protocol,
|
||||
firmware, and/or documented network setup.
|
||||
- Updated `docs/UI_STYLE.md`, `docs/PRE_READER_USABILITY.md`, Russian semantic
|
||||
labels, deterministic browser tests, host tests, and device test procedure.
|
||||
- Before-and-after latency report using the Milestone 016 measurement method.
|
||||
|
||||
### Acceptance criteria
|
||||
|
||||
1. At the canonical `412 x 916` viewport, touching the player avatar or its
|
||||
transparent target starts control without a separate joystick. The target is
|
||||
at least `96 x 96` CSS pixels, the visible avatar remains unobscured, and its
|
||||
center cannot move more than the documented small deflection radius.
|
||||
2. Avatar deflection follows the pointer by the next rendered frame during
|
||||
normal operation, indicates the resolved direction and relative speed
|
||||
without text, and returns to the exact center on release, cancellation,
|
||||
pause, disconnect, blur, hidden-document events, and lost pointer capture.
|
||||
3. The map moves beneath the near-centered avatar only from server-approved
|
||||
projections. The browser never predicts maze position, collision outcome,
|
||||
inventory, visibility, timers, or match results.
|
||||
4. Deflection magnitude maps monotonically to a documented bounded cadence,
|
||||
the dead zone sends no movement, diagonal hysteresis prevents direction
|
||||
flicker, and the fastest cadence cannot bypass the 150 ms server cooldown.
|
||||
5. At most one bounded current movement intention is awaiting completion or
|
||||
replacement per player. Direction changes and release discard stale local
|
||||
intent so delayed network delivery cannot create a long client-side command
|
||||
backlog.
|
||||
6. In the controlled local Wi-Fi test conditions established by Milestone 016,
|
||||
accepted input-to-authoritative-painted-update latency is at most 100 ms at
|
||||
`p95` and at most 150 ms for the maximum sample in both the two-player case
|
||||
and the two-player/four-observer case. No accepted sample reaches 500 ms.
|
||||
7. If the deployment network cannot meet criterion 6 before application work,
|
||||
the documented network remediation is applied and the full test is repeated;
|
||||
application latency is reported separately so network improvements cannot
|
||||
conceal a frontend or firmware regression.
|
||||
8. Browser tests cover pointer capture, touch targeting, bounded offset,
|
||||
dead-zone and speed mapping, diagonal hysteresis, stale-intent coalescing,
|
||||
every reset path, authoritative map motion, keyboard and swipe alternatives,
|
||||
reduced motion, and supported mobile viewports.
|
||||
9. All host tests pass, firmware and web images remain within partition budgets,
|
||||
and the latency instrumentation records no dropped messages, unbounded queue
|
||||
growth, hidden-state disclosure, or observer mutation under maximum supported
|
||||
session load.
|
||||
10. The project owner confirms in representative mobile gameplay that a child
|
||||
can discover movement by dragging the avatar, understand direction and
|
||||
relative speed from its deflection, stop reliably, and experience responsive
|
||||
control, or explicitly approves a documented evidence deviation.
|
||||
|
||||
### Evidence
|
||||
|
||||
Not started. This milestone records the project owner's 2026-09-09 decisions:
|
||||
the child drags the centered player icon; the icon deflects slightly as the
|
||||
control handle while the authoritative map moves below it; displacement controls
|
||||
speed; approximately 100 ms is ideal, 150 ms is acceptable, and delays near
|
||||
500 ms are unacceptable. Implementation must wait for Milestone 016 to identify
|
||||
and prioritize the actual latency contributors.
|
||||
|
||||
@@ -767,8 +767,9 @@ static void handle_game_command(maze32_gateway_t* gateway, const maze32_session_
|
||||
append_json_string(acknowledgement, sizeof(acknowledgement), &length,
|
||||
message->request_id) &&
|
||||
(written = snprintf(prefix, sizeof(prefix),
|
||||
",\"payload\":{\"commandSequence\":%" PRIu32 "}}", command_sequence)) >=
|
||||
0 &&
|
||||
",\"payload\":{\"commandSequence\":%" PRIu32
|
||||
",\"stateSequence\":%" PRIu32 "}}",
|
||||
command_sequence, gateway->game.state_sequence)) >= 0 &&
|
||||
(size_t)written < sizeof(prefix) &&
|
||||
append_text(acknowledgement, sizeof(acknowledgement), &length, prefix)) {
|
||||
enqueue(gateway, session->connection_id, acknowledgement);
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
# Milestone 016 movement-latency procedure
|
||||
|
||||
## Measurement model
|
||||
|
||||
Each accepted movement uses one correlation path. Browser times use
|
||||
`performance.now()` and ESP times use its monotonic timer; do not subtract a
|
||||
browser timestamp from an ESP timestamp.
|
||||
|
||||
| Interval | Clock | Measurement point |
|
||||
| --- | --- | --- |
|
||||
| pointer feedback | browser | pointer event to next animation frame |
|
||||
| command to acknowledgement | browser | `move` send to matching `ack` |
|
||||
| command to complete view | browser | `move` send to matching assembled view sequence |
|
||||
| command to painted view | browser | `move` send to next frame after matching view render |
|
||||
| handler and send | ESP | existing `/diagnostics` maxima |
|
||||
| network round trip | client | ICMP and WebSocket heartbeat samples |
|
||||
|
||||
The accepted command acknowledgement contains the authoritative `stateSequence`.
|
||||
The browser records a sample only after the complete view with that sequence is
|
||||
rendered and the next animation frame occurs. Samples are local-only, capped at
|
||||
256, and do not alter a command, projection, or server result.
|
||||
|
||||
## Controlled runs
|
||||
|
||||
1. Record the firmware build identity, device address, Wi-Fi access point,
|
||||
client device, browser/version, signal conditions, and viewport.
|
||||
2. Collect 100 `ping -c 100 -q <device>` samples. Record loss, min, average,
|
||||
maximum, and deviation.
|
||||
3. Run 200 accepted moves per player with two players and no observers. Include
|
||||
direction changes, a sustained hold, a release, and one blocked move.
|
||||
4. Repeat with two players and four observers. Capture `/diagnostics` before
|
||||
and after each run.
|
||||
5. For each browser, copy
|
||||
`window.maze32PlayerApp.latencySamples()` after the run. Compute p50, p95,
|
||||
p99, and maximum for all browser intervals. Keep raw samples only for the
|
||||
duration of the report and do not store player names or tokens.
|
||||
6. Compare command-to-acknowledgement with command-to-painted-view, ESP handler
|
||||
and send maxima, queue high-water marks, observer view chunk count, and
|
||||
ICMP/WebSocket round trips. Attribute a delay only when the matching
|
||||
interval is elevated.
|
||||
|
||||
## Safety checks
|
||||
|
||||
- Confirm no dropped, failed, malformed, oversized, or rate-limited message
|
||||
increased unexpectedly outside the deliberate blocked-move case.
|
||||
- Confirm queue high water stays within its published limit and browser sample
|
||||
storage remains at or below 256 entries.
|
||||
- Record blocked-command rejection separately; it does not count as an
|
||||
accepted-move sample.
|
||||
@@ -43,6 +43,25 @@ test("joystick repeat interval is monotonic and remains slower than cooldown", (
|
||||
assert.ok(player.JOYSTICK_FAST_REPEAT_MS > 150);
|
||||
});
|
||||
|
||||
test("latency correlation is bounded and only completes matching authoritative views", () => {
|
||||
let clock = 0;
|
||||
const tracker = new player.LatencyTracker(() => clock);
|
||||
tracker.sent("move-1");
|
||||
clock = 12;
|
||||
tracker.acknowledged("move-1", 41);
|
||||
clock = 16;
|
||||
tracker.painted(40);
|
||||
assert.equal(tracker.snapshot().length, 0);
|
||||
clock = 24;
|
||||
tracker.painted(41);
|
||||
assert.deepEqual(tracker.snapshot(), [{requestId: "move-1", commandToAckMs: 12,
|
||||
commandToPaintMs: 24, ackToPaintMs: 12}]);
|
||||
for (let index = 0; index < player.LATENCY_SAMPLE_LIMIT + 4; index++) {
|
||||
tracker.sent(`move-${index + 2}`);
|
||||
}
|
||||
assert.ok(tracker.pending.size <= player.LATENCY_SAMPLE_LIMIT);
|
||||
});
|
||||
|
||||
test("player map cells enlarge small projections without exceeding a readable bound", () => {
|
||||
const sevenBySeven = player.playerCellSize(396, 730, 7, 7);
|
||||
const fourByFour = player.playerCellSize(396, 730, 4, 4);
|
||||
|
||||
+44
-4
@@ -12,6 +12,7 @@
|
||||
const JOYSTICK_HYSTERESIS = .18;
|
||||
const JOYSTICK_SLOW_REPEAT_MS = 360;
|
||||
const JOYSTICK_FAST_REPEAT_MS = 170;
|
||||
const LATENCY_SAMPLE_LIMIT = 256;
|
||||
const ONBOARDING_STEPS = Object.freeze([
|
||||
Object.freeze({icon: "hand", title: "Потяни кружок", text: "Кружок ведёт героя по лабиринту."}),
|
||||
Object.freeze({icon: "motion", title: "Отпусти — герой ждёт", text: "Рука остановилась — шаги тоже."}),
|
||||
@@ -145,6 +146,40 @@
|
||||
return currentStep >= ONBOARDING_STEPS.length - 1 ? null : currentStep + 1;
|
||||
}
|
||||
|
||||
class LatencyTracker {
|
||||
constructor(now) {
|
||||
this.now = typeof now === "function" ? now : () => root.performance.now();
|
||||
this.pending = new Map();
|
||||
this.samples = [];
|
||||
}
|
||||
|
||||
sent(requestId) {
|
||||
if (!requestId) return;
|
||||
this.pending.set(requestId, {sent: this.now(), acknowledged: null, stateSequence: null});
|
||||
if (this.pending.size > LATENCY_SAMPLE_LIMIT) this.pending.delete(this.pending.keys().next().value);
|
||||
}
|
||||
|
||||
acknowledged(requestId, stateSequence) {
|
||||
const sample = this.pending.get(requestId);
|
||||
if (!sample) return;
|
||||
sample.acknowledged = this.now();
|
||||
sample.stateSequence = Number.isInteger(stateSequence) ? stateSequence : null;
|
||||
}
|
||||
|
||||
painted(stateSequence) {
|
||||
for (const [requestId, sample] of this.pending) {
|
||||
if (sample.acknowledged == null || sample.stateSequence !== stateSequence) continue;
|
||||
const painted = this.now();
|
||||
this.samples.push({requestId, commandToAckMs: sample.acknowledged - sample.sent,
|
||||
commandToPaintMs: painted - sample.sent, ackToPaintMs: painted - sample.acknowledged});
|
||||
if (this.samples.length > LATENCY_SAMPLE_LIMIT) this.samples.shift();
|
||||
this.pending.delete(requestId);
|
||||
}
|
||||
}
|
||||
|
||||
snapshot() { return this.samples.map(sample => Object.assign({}, sample)); }
|
||||
}
|
||||
|
||||
function createEnvelope(type, sequence, requestId, payload) {
|
||||
return {v: 1, type, seq: sequence, requestId, payload: payload || {}};
|
||||
}
|
||||
@@ -179,6 +214,7 @@
|
||||
}
|
||||
const first = this.pending.parts[0];
|
||||
const snapshot = Object.assign({}, first, {
|
||||
stateSequence: message.seq,
|
||||
cells: this.pending.parts.flatMap(part => part.cells),
|
||||
objects: this.pending.parts.flatMap(part => part.objects)
|
||||
});
|
||||
@@ -208,7 +244,7 @@
|
||||
|
||||
function boot(documentObject, windowObject) {
|
||||
const byId = id => documentObject.getElementById(id);
|
||||
const state = {socket: null, clientSequence: 0, serverSequence: 0, viewSequence: -1, session: null, lobby: null, snapshot: null, reconnectTimer: null, noticeTimer: null, touchStart: null, pending: new Map(), assembler: new SnapshotAssembler(), audio: null, feedbackReady: false, muted: false, reducedMotion: false, onboardingSeen: false, onboardingStep: 0, joystick: {pointerId: null, direction: null, repeatTimer: null, repeatMs: null}, observer: {scale: 1, x: 0, y: 0, dragging: null}};
|
||||
const state = {socket: null, clientSequence: 0, serverSequence: 0, viewSequence: -1, session: null, lobby: null, snapshot: null, reconnectTimer: null, noticeTimer: null, touchStart: null, pending: new Map(), assembler: new SnapshotAssembler(), latency: new LatencyTracker(() => windowObject.performance.now()), audio: null, feedbackReady: false, muted: false, reducedMotion: false, onboardingSeen: false, onboardingStep: 0, joystick: {pointerId: null, direction: null, repeatTimer: null, repeatMs: null}, observer: {scale: 1, x: 0, y: 0, dragging: null}};
|
||||
|
||||
function showNotice(text, kind) {
|
||||
const element = byId("notice");
|
||||
@@ -287,6 +323,7 @@
|
||||
const requestId = `${type}-${sequence}`;
|
||||
state.socket.send(JSON.stringify(createEnvelope(type, sequence, requestId, payload)));
|
||||
setPending(requestId, element);
|
||||
if (type === "move") state.latency.sent(requestId);
|
||||
return requestId;
|
||||
}
|
||||
|
||||
@@ -411,6 +448,9 @@
|
||||
byId("compass-stat").dataset.direction = hint;
|
||||
byId("compass-arrow").style.transform = `rotate(${HINT_ANGLES[hint]}deg)`;
|
||||
renderMap(snapshot);
|
||||
if (Number.isInteger(snapshot.stateSequence)) {
|
||||
windowObject.requestAnimationFrame(() => state.latency.painted(snapshot.stateSequence));
|
||||
}
|
||||
if (snapshot.phase === "paused") { resetJoystick(); showModal("Пауза", "Один герой потерялся между Wi‑Fi пакетами. Ждём возвращения.", "ПЕРЕРЫВ", false); }
|
||||
else if (snapshot.phase === "finished" || snapshot.result && snapshot.result !== "none") renderResult(snapshot.result);
|
||||
else {
|
||||
@@ -551,7 +591,7 @@
|
||||
} else if (message.type === "resume") { hideModal(); showNotice("Оба героя снова на связи. Гонка продолжается.", "success");
|
||||
} else if (message.type === "match_result") { renderResult(message.payload.result);
|
||||
} else if (message.type === "reset") { state.snapshot = null; state.viewSequence = -1; state.assembler = new SnapshotAssembler(); resetJoystick(); hideModal(); setScreen("lobby"); showNotice("Лабиринт пересажен. Можно начинать новую гонку.", "success");
|
||||
} else if (message.type === "ack") { feedback(true);
|
||||
} else if (message.type === "ack") { state.latency.acknowledged(message.requestId, message.payload && message.payload.stateSequence); feedback(true);
|
||||
} else if (message.type === "error") {
|
||||
if (message.payload.code === "invalid_token") { windowObject.localStorage.removeItem(TOKEN_KEY); state.session = null; setScreen("welcome"); }
|
||||
if (message.payload.code === "blocked") { const bump = byId("wall-bump"); bump.textContent = localError("blocked"); bump.classList.remove("show"); void bump.offsetWidth; bump.classList.add("show"); }
|
||||
@@ -689,10 +729,10 @@
|
||||
}
|
||||
});
|
||||
connect();
|
||||
return {state, handleMessage, move, resetJoystick};
|
||||
return {state, handleMessage, move, resetJoystick, latencySamples: () => state.latency.snapshot()};
|
||||
}
|
||||
|
||||
const api = Object.freeze({TOKEN_KEY, MAX_VIEW_CHUNKS, NAME_COMBINATION_COUNT, LOGIN_HEADLINES, ONBOARDING_STEPS, JOYSTICK_DEAD_ZONE, JOYSTICK_HYSTERESIS, JOYSTICK_SLOW_REPEAT_MS, JOYSTICK_FAST_REPEAT_MS, directionFromKey, directionFromSwipe, joystickDirection, joystickRepeatMs, joystickVector, playerCellSize, nextOnboardingStep, randomIndex, randomSuggestedName, randomLoginHeadline, createEnvelope, snapshotIsValid, SnapshotAssembler, projectedCellIndices, clampObserverZoom, observerFitScale, boot});
|
||||
const api = Object.freeze({TOKEN_KEY, MAX_VIEW_CHUNKS, NAME_COMBINATION_COUNT, LOGIN_HEADLINES, ONBOARDING_STEPS, LATENCY_SAMPLE_LIMIT, JOYSTICK_DEAD_ZONE, JOYSTICK_HYSTERESIS, JOYSTICK_SLOW_REPEAT_MS, JOYSTICK_FAST_REPEAT_MS, LatencyTracker, directionFromKey, directionFromSwipe, joystickDirection, joystickRepeatMs, joystickVector, playerCellSize, nextOnboardingStep, randomIndex, randomSuggestedName, randomLoginHeadline, createEnvelope, snapshotIsValid, SnapshotAssembler, projectedCellIndices, clampObserverZoom, observerFitScale, boot});
|
||||
if (typeof module !== "undefined" && module.exports) module.exports = api;
|
||||
root.Maze32Player = api;
|
||||
if (root.document) root.document.addEventListener("DOMContentLoaded", () => { root.maze32PlayerApp = boot(root.document, root); });
|
||||
|
||||
Reference in New Issue
Block a user