diff --git a/PLANS.md b/PLANS.md index c4cdd6f..89ab2ba 100644 --- a/PLANS.md +++ b/PLANS.md @@ -1267,7 +1267,7 @@ confirmation remains pending. ## Milestone 015 — Pre-reader child-friendly game experience -**Status:** `NOT_STARTED` +**Status:** `IN_PROGRESS` ### Objective @@ -1363,15 +1363,42 @@ rules. ### Evidence -Completion evidence is not yet available because implementation has not -started. A baseline inspection of the live game at a `412 x 915` viewport on -2026-09-08 found that the visible maze occupied only `80 x 140` pixels inside a -`381 x 729` play area, status labels were approximately `8--10.56` pixels, -status icons were `23 x 23` pixels, the player was identified primarily as -`P1`, and no directional touch controls were visible in the game area. These -observations define the principal redesign targets. The planned joystick -replaces directional-arrow buttons rather than adding them. Expected completion -evidence includes before-and-after viewport captures, deterministic browser -tests, `bash scripts/run_host_tests.sh`, a reproducible `buildfs` size report, -and the facilitated child-usability confirmation or an explicitly approved -deviation. +The implementation and automated verification are complete; the required +representative pre-reader observation or owner-approved evidence deviation +remains pending. + +- The player screen now uses circle and diamond character shapes instead of + `P1`/`P2`, icon-and-shape ownership markers for keys and exits, dynamic + projection sizing from 32 to 72 CSS pixels per cell, a 180 CSS-pixel + joystick (172 on phones), and an icon-only visible mute, reduced-motion, and + onboarding-replay control group. The mobile directional-button cluster stays + hidden while keyboard and swipe input remain available. +- The joystick clamps its handle to the circular base, has a 20-pixel dead + zone and 18% axis hysteresis, emits only cardinal movement intentions, and + schedules repeats from 360 to 170 ms. It resets on pointer release or + cancellation, pause, disconnect, browser blur, and hidden-document events; + the fastest local repeat remains above the 150 ms authoritative cooldown. +- First active player projection now opens an eight-step, icon-first Russian + walkthrough for drag, release, walls, key, vision, compass, exit, and the + other player. It can be replayed or skipped locally. The new + `docs/PRE_READER_USABILITY.md` records the final privacy-safe observation + protocol without collecting child-identifying data. +- On 2026-09-09, `bash scripts/run_host_tests.sh` passed every host suite, + including 15 browser tests. The focused browser coverage verifies joystick + cardinal direction and bounded travel, dead zone, monotonic repeat rate, + hysteresis, dynamic mobile cell sizing, onboarding bounds, reset paths, + mute and reduced-motion controls, local assets, Russian UI, and existing + protocol/observer constraints. Web asset reproducibility reported SHA-256 + `13f95418169fb38d277d86f3324a4dbc3fa8b5e5e2e9fb7bde9439aa2f7b75e4` + using 23,950 of 1,572,864 allowed compressed bytes. +- On 2026-09-09, `pio run -e esp32-c6-devkitm-1` passed with 58,868 of + 327,680 RAM bytes and 1,027,934 of 2,097,152 application bytes. Its + `firmware.bin` SHA-256 was + `87cfffaaa9b119794707c9caedc3da7f3f015d540fc1149d74e7cc0a1ecd2841`. + `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. diff --git a/data/index.html.gz b/data/index.html.gz index d07cf3a..b0805d7 100644 Binary files a/data/index.html.gz and b/data/index.html.gz differ diff --git a/docs/PRE_READER_USABILITY.md b/docs/PRE_READER_USABILITY.md new file mode 100644 index 0000000..6c87868 --- /dev/null +++ b/docs/PRE_READER_USABILITY.md @@ -0,0 +1,38 @@ +# Pre-reader usability observation guide + +## Purpose + +Use this short, consent-based observation to verify the player interface with +a child who does not rely on reading. It is an acceptance activity for +Milestone 015, not a source of analytics. Do not record names, images, audio, +contact details, or any other identifying information. + +## Setup + +- Use the player screen at a `412 x 916` CSS-pixel viewport with sound enabled. +- Start a normal two-player match with the observer screen hidden. +- An adult may read a prompt once but must not point to the answer or operate + the child's device. + +## Prompts + +Ask the child to demonstrate each task. Mark it as completed only when the +child succeeds without reading the Russian support text. + +| Task | Expected non-reading cue | +| --- | --- | +| Move the hero | circular joystick and character movement | +| Find and pick up a key | key shape and pickup feedback | +| Recognize farther vision | eye symbol and map expansion | +| Follow the compass | arrow direction and compass icon | +| Find the exit | outlined exit shape and owner character shape | +| Recognize a wall | wall shape plus rejection feedback | +| Recognize the result | result icon/overlay plus local feedback | + +## Record + +Record only the date, viewport, browser, whether Russian labels were read, +and the seven task outcomes. Summarize unclear cues and any adult assistance. +If no representative child observation is feasible, the project owner may +approve a documented deviation instead; include the reason and the remaining +risk in the active milestone evidence. diff --git a/docs/UI_STYLE.md b/docs/UI_STYLE.md index 62a7f13..9cdc96b 100644 --- a/docs/UI_STYLE.md +++ b/docs/UI_STYLE.md @@ -49,11 +49,13 @@ are not MVP requirements. - Use frequent but purposeful animation for screen transitions, movement, pickups, game events, and humorous feedback. - Pending controls blink until their command is resolved. -- A reduced-motion mode is not required for the MVP. +- A visible reduced-motion toggle is required in player mode. It must suppress + non-essential animation without changing gameplay feedback or authority. - Sound and vibration are enabled by default. They begin only after the first user interaction when required by the browser, and are used only when the browser and device support them. -- Dedicated sound and vibration toggles are not required for the MVP. +- A visible mute toggle is required in player mode. It disables local sound and + vibration feedback only; it must never change protocol behavior. ## 5. Shared controls @@ -62,10 +64,14 @@ are not MVP requirements. - Visible keyboard-focus outlines are not required for the MVP. - On computers, movement works from the keyboard and from an on-screen directional control cluster fixed in the lower-right corner. -- On tablets, an on-screen directional control cluster is fixed in the - lower-right corner for right-handed use. Touch swipes remain supported. -- On phones, movement uses directional swipes and no on-screen directional - buttons are shown. +- On touch devices, movement uses an always-visible circular joystick at least + 160 by 160 CSS pixels. Its drag stays within the control, returns to center + on release, cancellation, pause, disconnect, and browser blur, and sends + only cardinal movement intentions. +- The joystick has a dead zone and directional hysteresis. Its locally + scheduled repeat interval is bounded and never faster than the server's + movement cooldown. Touch swipes remain supported. Phones do not show a + directional-button cluster. - The always-visible key-drop control is labelled `OK`, includes a key icon, and is positioned closer to the upper edge of the screen than the directional controls. Dropping does not require confirmation. @@ -78,6 +84,9 @@ are not MVP requirements. - A fixed status panel occupies the top of the screen. - The persistent player status shows the player name, carried key when one is held, active vision effect, and match state. +- The player identity is represented by a distinct character shape, not `P1` + or `P2`. Object, blocked-move, pause, and result feedback must pair a shape + or icon with its color. - Connection details and the Wi-Fi settings link belong in a secondary panel, not in the persistent status row. - Countdown, pause, and result states appear as modal overlays above the map. @@ -93,9 +102,10 @@ are not MVP requirements. boundaries prevent it. - Render unknown space as a solid background. Never infer, retain, animate, or decorate maze geometry outside the server-provided visible projection. -- Use a nominal cell size of 20 CSS pixels on phones and 40 CSS pixels on - tablets and computers. Minor adjustment is allowed to fit browser chrome and - safe areas without making the map unusable. +- Compute cell size from the current server projection and available map area. + At the minimum supported phone viewport a common 7 by 7 projection must use + at least 46 CSS-pixel cells, while smaller projections may grow to 72 pixels. + Do not alter, retain, or infer hidden maze cells to fill spare space. - Player zoom and pan controls are not part of the MVP. - Use different colors plus labels, outlines, shapes, or icons to distinguish player ownership and object types. @@ -108,6 +118,9 @@ are not MVP requirements. - Support both portrait and landscape orientations. Suggest landscape when it materially improves play, but do not block portrait play. - Respect display safe areas and keep essential status and controls visible. +- At `412 x 916`, the map is the dominant visual region; controls overlay it + instead of creating an empty lower region. Status remains icon-first and + readable without requiring reading as the normal interaction path. - Scale icons and controls up on tablets and computers while preserving map priority and fixed panel placement. - Validate the MVP against the current stable releases of Chrome, Safari, @@ -143,3 +156,19 @@ are not MVP requirements. message, and semantic color role manually and through focused browser tests where practical. - Golden-image or screenshot-regression tests are not required for the MVP. + +## 12. Pre-reader onboarding and usability + +- On a player's first active projection, show an eight-step, wordless-first + walkthrough for joystick drag and release, walls, key, vision, compass, + exit, and meeting the other player. Each step uses a large icon; short + Russian text is secondary support for an adult. +- The walkthrough provides a replay button and an explicit Russian skip button + for adults. It is browser-local UI only and never changes server state. +- A child must be able to distinguish their character, the other character, + key, vision, compass, exit, blocked movement, and results through at least + two channels such as shape/icon, position, motion, outline, color, sound, or + haptic feedback. +- Before release, record a representative pre-reader observation using + `docs/PRE_READER_USABILITY.md`, or record a project-owner-approved deviation + with its reason and impact. diff --git a/test/web/test_player_app.js b/test/web/test_player_app.js index 7ba0ff9..5543e27 100644 --- a/test/web/test_player_app.js +++ b/test/web/test_player_app.js @@ -22,6 +22,56 @@ test("keyboard and swipe controls produce equivalent directions", () => { assert.equal(player.directionFromSwipe(0, 0, 12, 4), null); }); +test("joystick produces only cardinal, bounded movement intentions", () => { + assert.equal(player.joystickDirection(10, 10, null), null); + assert.equal(player.joystickDirection(44, 8, null), "east"); + assert.equal(player.joystickDirection(-8, -44, null), "north"); + assert.equal(player.joystickDirection(42, 38, "east"), "east"); + assert.equal(player.joystickDirection(38, 42, "north"), "south"); + const vector = player.joystickVector(300, 0, 57, null); + assert.equal(vector.direction, "east"); + assert.equal(vector.x, 57); + assert.equal(vector.y, 0); +}); + +test("joystick repeat interval is monotonic and remains slower than cooldown", () => { + assert.equal(player.joystickRepeatMs(player.JOYSTICK_DEAD_ZONE, 60), null); + const near = player.joystickRepeatMs(25, 60); + const far = player.joystickRepeatMs(60, 60); + assert.ok(near > far); + assert.ok(far >= player.JOYSTICK_FAST_REPEAT_MS); + assert.ok(player.JOYSTICK_FAST_REPEAT_MS > 150); +}); + +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); + assert.ok(sevenBySeven >= 46); + assert.ok(fourByFour > sevenBySeven); + assert.ok(fourByFour <= 72); +}); + +test("onboarding has bounded, replayable steps", () => { + assert.equal(player.ONBOARDING_STEPS.length, 8); + assert.equal(player.nextOnboardingStep(0), 1); + assert.equal(player.nextOnboardingStep(6), 7); + assert.equal(player.nextOnboardingStep(7), null); +}); + +test("child controls reset safely and retain accessible alternatives", () => { + const html = fs.readFileSync(path.join(project, "web/index.html"), "utf8"); + const script = fs.readFileSync(path.join(project, "web/player.js"), "utf8"); + assert.match(script, /addEventListener\("pointerup", stopJoystick\)/); + assert.match(script, /addEventListener\("pointercancel", stopJoystick\)/); + assert.match(script, /addEventListener\("blur", resetJoystick\)/); + assert.match(script, /connectionState !== "online"\) resetJoystick\(\)/); + assert.match(script, /snapshot\.phase === "paused"\) \{ resetJoystick\(\)/); + assert.match(script, /setMuted\(!state\.muted\)/); + assert.match(script, /setReducedMotion\(!state\.reducedMotion\)/); + assert.match(script, /onboarding-replay/); + assert.match(html, /id="onboarding-dots"[\s\S]*?<\/span><\/span><\/span><\/span><\/span><\/span><\/span><\/span>/); +}); + test("frontend name suggestions randomize both words and remain bounded", () => { const picks = [3, 4]; const suggestion = player.randomSuggestedName([], () => picks.shift()); @@ -112,6 +162,12 @@ test("player assets are local, Russian, accessible, and responsive", () => { assert.match(css, /@media \(min-width: 721px\) and \(max-width: 980px\)/); assert.match(css, /height: calc\(100dvh - 78px\)/); assert.match(css, /\.dpad \{ display: none; \}/); + assert.match(html, /id="joystick"/); + assert.match(html, /id="onboarding"/); + assert.match(html, /id="mute-toggle"/); + assert.match(css, /\.joystick \{[^}]*width: 180px; height: 180px/); + assert.match(script, /resetJoystick/); + assert.match(script, /playerCellSize/); assert.match(css, /\(hover: none\) and \(pointer: coarse\) and \(max-height: 720px\)/); assert.match(icons, /id="key"/); assert.match(icons, /id="maze"/); diff --git a/web/icons.svg b/web/icons.svg index 658a90e..63be1aa 100644 --- a/web/icons.svg +++ b/web/icons.svg @@ -5,5 +5,11 @@ + + + + + + diff --git a/web/index.html b/web/index.html index 12ab9a5..a73fe48 100644 --- a/web/index.html +++ b/web/index.html @@ -44,7 +44,7 @@