diff --git a/PLANS.md b/PLANS.md index 09746d6..2a2b698 100644 --- a/PLANS.md +++ b/PLANS.md @@ -1772,7 +1772,7 @@ When all criteria pass, set Milestone 022 to `DONE`, append its execution record ## Milestone 023 — Verify multi-client reset recovery and document the emergency workflow -**Status:** `READY` +**Status:** `DONE` **Depends on:** Milestone 022 ### Objective @@ -1833,3 +1833,363 @@ Also document the API routes/commands, authorization, stable reset reason codes, ### Completion action When all criteria pass, set Milestone 023 to `DONE` and append its execution record. Add later post-MVP work only after Milestone 023 without renumbering existing milestones. + +### Execution record + +- Date: 2026-08-31 +- Board model and revision: ESP32-C6FH4 QFN32, revision v0.2. +- Toolchain and library versions: PlatformIO Core 6.1.19; `espressif32` 7.0.1; ESP-IDF 6.0.1; `esp_littlefs` 1.20.4. +- Result: PASS. +- Evidence: Added `docs/RECOVERY_GUIDE.md`, a concise Russian emergency workflow covering leave, local-only profile clearing while offline, player-only full reset, effects on other users, and explicit non-erasure of Wi-Fi/firmware settings. Added a deterministic fifty-cycle mixed leave/bot-abort/full-reset lifecycle regression. All ten host suites passed, including the existing API, synchronization, human integration, and robustness coverage. All 17 web tests and `node --check data/app.js` passed. Firmware and LittleFS builds passed; firmware uses 39,604 / 327,680 B RAM (12.1%) and 1,020,920 / 2,097,152 B flash (48.7%); the fixed LittleFS partition image is 2,031,616 B. +- Physical verification: The user confirmed every required manual target-device scenario passed, including the two-player-plus-spectator flows, WebSocket and HTTP fallback convergence, temporarily disconnected browser recovery, recovery-menu layouts and hold safety, stale-tab rejection, and fifty mixed recovery cycles without a hang, reboot, memory decline, stuck seat, socket leak, stale state, duplicate command, or orphaned bot action. Wi-Fi credentials and firmware settings remained unchanged. +- Next action: Milestone 024 remains blocked and was not started. + +--- + +## Milestone 024 — Build a bounded browser Web Audio engine and sound controls + +**Status:** `BLOCKED` +**Depends on:** Milestone 023 + +### Objective + +Add a compact, framework-free Web Audio engine that synthesizes all game sounds in the phone or tablet browser without storing prerecorded audio files on the ESP32. + +The engine must be explicitly activated by the user, remain optional, consume bounded browser resources, and fail silently when Web Audio is unavailable. This milestone builds only the reusable audio foundation and settings; the large game-reaction library is added in Milestone 025. + +### Architecture + +- Implement the engine as a small maintainable browser module rather than mixing oscillator code throughout `app.js`. +- Use the Web Audio API only after a user gesture has enabled sound. +- Generate sound from bounded combinations of: + - oscillators with sine, triangle, square, and sawtooth waveforms; + - one reusable in-memory noise buffer; + - gain envelopes; + - pitch ramps; + - low-pass, high-pass, and band-pass filters; + - short delays only when they materially improve a cue; + - a master gain and dynamics-compressor/limiter stage. +- Do not add MP3, AAC, OGG, WAV, sampled meme, CDN, or internet assets. +- Do not synthesize audio on the ESP32. The ESP32 only serves the browser code; the client device produces the sound. +- Do not require a server protocol change solely for sound. Derive cues from the same authoritative, role-safe state changes already used for visual feedback. + +### Audio-context lifecycle + +- Create or resume `AudioContext` only after an explicit sound-toggle or other clearly associated user action. +- Handle browsers that begin with a suspended context. +- Reuse one context instead of creating a new context for every effect. +- Suspend or stop scheduling sound when the page is hidden for a sustained period, the session is reset, the user leaves, or sound is muted. +- Resume safely after the page becomes active and the browser permits it. +- Disconnect and release every temporary node after its envelope completes. +- Cancel scheduled voices and reset the engine during profile reset, full game reset, navigation back to registration, or browser teardown. +- Degrade to a silent no-op implementation when Web Audio is unsupported or initialization fails. + +### Resource limits + +- Define a hard maximum simultaneous voice count, initially no more than eight voices. +- Reuse a single bounded noise buffer instead of allocating noise per playback. +- Limit ordinary cues to approximately 100–900 ms and major victory/sinking cues to a maximum of approximately 2.5 seconds. +- Prevent unbounded timers, event listeners, audio nodes, buffers, and promise chains. +- Drop or replace low-priority UI cues when the voice limit is reached; never delay an important hit, sinking, combo, or reset cue behind a long queue. +- Prevent duplicate state snapshots and HTTP/WebSocket retransmissions from replaying the same sound. +- Record the added raw/gzip JavaScript size and resulting LittleFS usage. + +### Child-safe sound controls + +- Add a clear `Sound on / Sound off` control using a local SVG speaker icon and a complete Russian accessible label. +- Make the sound control available from the lobby through game, result, rematch, and recovery screens. +- Preserve the choice in a bounded `battleship.*` browser preference. +- Default to sound off until the user explicitly enables it for the first time. +- Provide three simple volume levels: quiet, normal, and loud, with normal as the recommended maximum for children. +- Cap master output so overlapping voices do not clip or become startling. +- Avoid sustained very high frequencies, sudden full-scale gain jumps, and long low-frequency rumbles. +- Keep essential game information visible; sound must enhance feedback, never become the only indication of turn, hit, miss, sinking, victory, error, or reset. +- Provide an immediate mute action that stops currently scheduled nonessential audio. +- Keep sound preferences independent from `prefers-reduced-motion`; also provide a separate reduced-intensity sound option for users who are sensitive to strong effects. + +### Originality and tone + +- Create original synthesized cues only. +- Do not copy recognizable sounds from YouTube, Roblox, games, films, social-media memes, or commercial sound libraries. +- The interaction style may use the short, energetic rhythm familiar to modern children's games, but the melodies, timing, and synthesis presets must be original to this project. +- Avoid frightening alarms, realistic gunfire, screams, mocking failure sounds, or punishment-like audio. + +### Tests + +Add browser/unit tests with a fake or instrumented audio context covering at least: + +1. No context creation before explicit enablement. +2. Context creation/resume after a valid user gesture. +3. Persistent mute, volume, and reduced-intensity preferences. +4. Master gain caps for every volume level. +5. Bounded voice allocation and priority replacement. +6. Noise-buffer reuse. +7. Node disconnection and timer cleanup after playback. +8. Duplicate event/version suppression. +9. Cleanup on leave, profile reset, full reset, and registration transition. +10. Page hide/resume behavior. +11. Graceful no-op fallback when Web Audio is unavailable or throws. +12. No effect on existing game state, transport, role, or recovery behavior. + +### Acceptance criteria + +- Sound can be explicitly enabled, muted immediately, and adjusted without reloading the game. +- The engine plays a deterministic test cue through a bounded reusable graph. +- No audio context or sound begins before user activation. +- The implementation introduces no prerecorded audio asset and no internet dependency. +- Simultaneous voices, node lifetimes, timers, and buffers remain within documented limits. +- The browser remains fully playable with sound disabled or unsupported. +- All existing host and browser suites continue to pass. +- Firmware, gzip, and LittleFS budgets remain acceptable. + +### Completion action + +When all criteria pass, set Milestone 024 to `DONE`, append its execution record, and change Milestone 025 from `BLOCKED` to `READY`. Do not start Milestone 025 in the same task unless explicitly requested. + +--- + +## Milestone 025 — Create a large randomized library of playful game sounds and audio combos + +**Status:** `BLOCKED` +**Depends on:** Milestone 024 + +### Objective + +Create a broad, original, randomized set of short synthesized reactions that keeps children engaged without becoming repetitive, chaotic, exhausting, or distracting from gameplay. + +Audio should reinforce the existing randomized visual/meme reactions. Each event family needs multiple clearly related but nonidentical variants, and the same variant must not play twice consecutively for the same event. + +### Event families and minimum variety + +Implement at least the following synthesized cue families: + +1. **Target selection** — at least 4 quiet variants: + - sonar tick; + - short radar ping; + - soft bubble click; + - tiny aiming chirp. + +2. **Shot launch** — at least 8 energetic variants: + - compact cannon pop; + - rising charge followed by a burst; + - fast whoosh; + - comic `pew`-style pitch drop; + - double pop; + - low naval thump; + - short sparkling launch; + - rapid arcade-like sweep. + +3. **Miss** — at least 10 light and funny variants: + - several splash shapes; + - bubbles rising; + - soft `bloop`; + - water drop; + - descending whistle into water; + - wave wash; + - tiny fish-like chirp; + - comic empty echo. + +4. **Hit** — at least 10 satisfying variants: + - filtered impact; + - short explosion; + - metallic thud; + - impact plus bright confirmation ping; + - two-stage `boom-ding`; + - bass pop; + - crunchy noise burst; + - short critical-hit sparkle; + - rising confirmation tone; + - compact layered impact. + +5. **Ship sunk** — at least 8 multi-stage variants: + - heavy impact followed by bubbles; + - descending hull tone and splash; + - short explosion sequence; + - victory sparkle over a low splash; + - collapsing pitch sweep; + - three-hit dramatic cadence; + - deep thump with bright finish; + - comic large `bloop` followed by stars. + +6. **Incoming miss / dodge** — at least 6 encouraging variants that sound different from the local player's miss. + +7. **Incoming hit** — at least 6 clear but nonfrightening variants using lower, softer timbres than the player's successful hit. + +8. **Turn ready** — at least 6 brief attention cues that invite action without sounding like an alarm. + +9. **Waiting / opponent turn** — at least 4 optional low-priority ambient ticks, disabled in reduced-intensity mode and rate-limited so they never loop continuously. + +10. **Game start** — at least 6 short original launch stingers. + +11. **Victory** — at least 8 original fanfare variants with different rhythms and instrument-like oscillator combinations. + +12. **Defeat / rematch invitation** — at least 5 friendly, encouraging cues that do not mock the child. + +13. **Recovery actions** — separate calm cues for menu open, cancel, leave, profile reset, and full game reset. Destructive actions must not sound rewarding or resemble a gameplay explosion. + +### Randomization rules + +- Use bounded variation of preset selection, pitch, envelope duration, filter cutoff, pan when supported, and particle-like voice timing. +- Keep pitch and duration variation within ranges that preserve the meaning of each event. +- Never allow two consecutive uses of an event family to select the same complete preset. +- Avoid a short repeating pattern such as alternating between only two variants when more are available. +- Seed selection from browser randomness; do not use game-board randomness or reveal any hidden server state. +- Do not let random choices affect authoritative game logic, timing, network messages, or visual state. +- Store only the last few selected preset identifiers needed for anti-repetition; do not build an unbounded history. + +### Combo and streak audio + +- Track only the local presentation streak already derived from authoritative hit/sunk events. +- Add escalating original combo layers: + - combo ×2: an extra bright confirmation note; + - mega-combo ×3: a three-note rising motif; + - ultra-combo ×4 and above: a bounded fanfare layer with stronger particles/visual synchronization. +- Increase excitement through rhythm, harmony, and layering rather than unlimited volume. +- Reset the audio combo exactly when the visual combo resets. +- Ensure a sinking or victory cue remains recognizable when combined with a streak layer. +- Cap combo layering inside the global voice and gain limits. + +### Synchronization with visual reactions + +- Select visual text/animation and audio from compatible event families without requiring an exact one-to-one preset pairing. +- Begin launch sound immediately after an accepted local Fire action. +- Play hit, miss, sinking, dodge, damage, turn, and result sounds only after the authoritative state transition is accepted. +- Spectators receive public hit/miss/sinking cues without player-private orientation or target information. +- Do not replay sounds when the same state version arrives through both WebSocket and HTTP polling. +- When several changes arrive in one snapshot, apply explicit priority: victory, sinking, hit/damage, miss/dodge, turn, then low-priority UI sound. +- Rate-limit bursts caused by reconnect snapshots so a returning client receives one summary cue instead of a backlog of historical sounds. + +### Attention without overload + +- Keep ordinary gameplay cues short enough that the next action is never delayed. +- Alternate timbre, rhythm, and spatial impression, not only pitch. +- Reserve the richest sounds for sinking, high combos, and victory so rewards retain meaning. +- Keep selection and menu sounds much quieter than gameplay results. +- Do not add continuous background music in this milestone. +- Do not play repeated waiting sounds more often than a documented safe interval. +- When many events occur rapidly, prioritize the newest important event and suppress obsolete low-priority audio. +- Reduced-intensity mode must remove waiting ticks, simplify combos, reduce bass/noise layers, and shorten victory/sinking cues. + +### Tests + +Add deterministic tests using injected randomness and a fake audio clock covering at least: + +1. Minimum preset count for every event family. +2. No immediate preset repetition. +3. Bounded anti-repeat history. +4. Pitch, duration, filter, pan, gain, and voice-count limits across many randomized selections. +5. Correct priority when one state snapshot contains multiple changes. +6. No duplicate sound for repeated versions or WebSocket/HTTP duplicates. +7. Correct local-player, opponent, and spectator sound perspective. +8. Shot sound only for an accepted local action. +9. Combo ×2, mega-combo ×3, ultra-combo ×4+, and reset behavior. +10. Reduced-intensity substitutions and suppression. +11. Reconnect summary cue without historical sound storms. +12. Recovery cues that cannot be confused with shot, hit, or victory. +13. Thousands of randomized cue selections without an exception, leaked voice, or limit violation. + +### Acceptance criteria + +- Every required event family meets or exceeds its minimum variant count. +- Repeated shots and outcomes feel varied in preset, rhythm, timbre, and motion synchronization. +- Important events remain immediately distinguishable without reading text. +- Combos become progressively more exciting without exceeding gain or voice limits. +- Failure and defeat sounds remain playful and encouraging. +- No copyrighted or recognizable third-party sound is included or imitated. +- Audio never reveals hidden board information or changes gameplay timing. +- Sound-disabled and reduced-intensity modes remain complete and usable. +- Automated anti-repetition, range, priority, and stress tests pass. + +### Completion action + +When all criteria pass, set Milestone 025 to `DONE`, append its execution record, and change Milestone 026 from `BLOCKED` to `READY`. Do not start Milestone 026 in the same task unless explicitly requested. + +--- + +## Milestone 026 — Validate Web Audio engagement, compatibility, and long-run stability on real devices + +**Status:** `BLOCKED` +**Depends on:** Milestone 025 + +### Objective + +Verify that the synthesized sound system is entertaining, understandable, compatible with the target phones/tablets, and stable during long games without harming network responsiveness or browser performance. + +### Device matrix + +Test at minimum: + +- one current Android phone in a Chromium-based browser; +- one Android tablet or second Android device; +- one iPhone or iPad using Safari when available; +- one laptop browser for keyboard and accessibility checks; +- sound on, muted, quiet, normal, loud, and reduced-intensity modes; +- portrait and landscape orientation; +- WebSocket operation and HTTP polling fallback. + +### Functional scenarios + +Verify on real devices: + +1. First-time sound enablement from the lobby after an explicit tap. +2. Persistent sound preference after reload and normal session resume. +3. Immediate mute during a currently playing cue. +4. Target, shot, miss, hit, sinking, incoming result, turn, combo, victory, defeat, and recovery sounds. +5. At least 30 consecutive shots without an immediate same-family preset repeat. +6. Combo ×2, mega-combo ×3, and ultra-combo ×4+ synchronization with the visual badges. +7. Page background/foreground transition without broken or queued audio. +8. Screen lock/unlock and browser-tab switching when supported. +9. WebSocket loss followed by HTTP fallback and WebSocket recovery without duplicate sounds. +10. Leave, profile reset, and full game reset while a sound is active. +11. Spectator audio without hidden-information leakage. +12. Graceful silent operation when Web Audio is blocked or unavailable. + +### Child-engagement review + +Run a short supervised usability review with age-appropriate participants or adult proxies acting from the child's perspective. + +Record whether: + +- shot, miss, hit, sinking, and victory can be distinguished without reading the message; +- repeated play still feels varied after a complete match; +- combo sounds feel more rewarding than ordinary hits; +- any cue is frightening, painfully sharp, too bass-heavy, mocking, or excessively loud; +- waiting sounds become annoying; +- mute and volume controls can be found quickly; +- the sounds help attention without causing the child to tap randomly or miss the actual turn cue. + +Do not record children, collect identifying data, or conduct unsupervised testing as part of this project. Record only anonymous design observations supplied by a responsible adult. + +### Performance and stability + +- Complete at least 20 representative games with sound enabled. +- Run at least one 60-minute browser session containing repeated shots, combos, resets, reconnects, and tab visibility changes. +- Record browser console errors, approximate active voice count, peak scheduled-node count, audio-context state transitions, and any delayed or dropped important cue. +- Confirm no persistent growth in active nodes, timers, event listeners, buffers, or browser memory attributable to audio. +- Confirm ESP32 free heap, HTTP latency, WebSocket delivery, and connected-client capacity remain within the established budgets. +- Measure final raw/gzip sizes for the audio engine and preset library and the resulting LittleFS usage. +- Confirm the firmware image does not materially grow unless a documented server change was required. + +### Accessibility and safety review + +- Verify every event remains understandable visually with sound muted. +- Verify screen-reader labels for sound controls and state changes do not duplicate or fight with audio cues. +- Verify reduced-intensity mode meaningfully reduces layers and loudness. +- Verify the master limiter prevents clipping during the largest combo/victory stack. +- Verify no cue exceeds the documented duration, gain, voice-count, and repetition limits. +- Verify no sound starts unexpectedly on registration, reload, or background resume. + +### Acceptance criteria + +- Required cues play reliably on the tested phone and tablet browsers after explicit activation. +- A complete match remains varied and understandable without copied audio assets. +- No immediate same-event preset repetition is observed in the recorded sequence. +- Users can mute audio immediately and retain their preference. +- Reduced-intensity mode is noticeably calmer while preserving event meaning. +- Twenty games and the 60-minute session complete without audio-node leaks, browser errors, gameplay stalls, ESP32 instability, or transport regressions. +- Audio remains synchronized with authoritative state through WebSocket, HTTP fallback, reconnect, and reset flows. +- The final browser assets and LittleFS image remain inside the established budgets. + +### Completion action + +When all criteria pass, set Milestone 026 to `DONE` and append its execution record. Add later milestones only after Milestone 026 without renumbering existing milestones. diff --git a/docs/API_CONTRACT.md b/docs/API_CONTRACT.md index 5e37c8e..d416473 100644 --- a/docs/API_CONTRACT.md +++ b/docs/API_CONTRACT.md @@ -79,6 +79,8 @@ session, clears match and cumulative statistics, and starts a fresh game generation. Successful recovery responses contain `resetReason` (`session_left`, `profile_reset`, or `game_reset`) and `generation`. Invalidated-token state polling returns `SESSION_INVALIDATED` with the same bounded recovery metadata. +The recovery generation and `gameId` make delayed commands stale; retrying a +completed recovery is harmless and cannot mutate a newly registered session. ## Role-safe state event diff --git a/docs/RECOVERY_GUIDE.md b/docs/RECOVERY_GUIDE.md new file mode 100644 index 0000000..d15c1f3 --- /dev/null +++ b/docs/RECOVERY_GUIDE.md @@ -0,0 +1,40 @@ +# Если игра застряла + +Кнопка с спасательным кругом «Помощь» доступна после подключения к игре. Она +не стреляет и не меняет поле сама по себе: сначала выберите действие, затем +подтвердите его. + +## Выйти из игры + +Выберите «Выйти из игры», чтобы освободить своё место. Имя на этом устройстве +останется, поэтому можно быстро подключиться снова. Если игрок выходит во +время партии, партия безопасно прекращается и оставшийся игрок возвращается в +лобби. + +## Сбросить мой профиль + +Этот пункт удаляет только данные текущего браузера: имя, выбранного героя и +токен. Данные других игроков не меняются. Без связи профиль всё равно можно +очистить локально; место на ESP32 освободится после восстановления связи или +обычного таймаута. + +## Сбросить всю игру + +Этот пункт виден только игроку. Он возвращает всех пользователей к первому +экрану и удаляет текущую партию и статистику текущего запуска. Для защиты от +случайного нажатия подтверждение нужно удерживать около двух секунд. + +Старые вкладки и токены после этого не могут изменить новую партию. + +## Что не удаляется + +Эти действия не стирают Wi‑Fi, пароль сети, прошивку, файлы игры, flash или +настройки платы. Перезагрузка ESP32 не нужна. + +## Проверка на устройстве + +Проверьте с двумя игроками и зрителем выход, локальный профиль, полный сброс +из лобби/игры/результата, WebSocket и HTTP fallback. После 50 смешанных +циклов сравните `/api/health`: свободная куча, largest free block, число +клиентов и причина перезагрузки не должны показывать утечку, зависание или +перезагрузку. diff --git a/test/host/test_game_lifecycle.c b/test/host/test_game_lifecycle.c index 7ba78af..a4c7871 100644 --- a/test/host/test_game_lifecycle.c +++ b/test/host/test_game_lifecycle.c @@ -128,11 +128,32 @@ static void test_leave_and_full_reset(void) { assert(game_lifecycle_reset(&lifecycle, player_1, reset_game_id) == LIFECYCLE_RESULT_FORBIDDEN_ROLE); } +static void test_fifty_mixed_recovery_cycles(void) { + test_random_t random = {.value = 41U}; + game_lifecycle_t lifecycle = new_lifecycle(&random); + for (uint8_t cycle = 0; cycle < 50U; ++cycle) { + uint8_t player = kSessionCapacity; + assert(game_lifecycle_join(&lifecycle, ROLE_AUTO_PLAYER, "Alice", &player) == LIFECYCLE_RESULT_OK && player == 0U); + const uint32_t game_id = lifecycle.game.state.game_id; + if (cycle % 3U == 0U) { + assert(game_lifecycle_leave(&lifecycle, player) == LIFECYCLE_RESULT_OK); + } else if (cycle % 3U == 1U) { + assert(game_lifecycle_configure(&lifecycle, player, game_id, MODE_BOT) == LIFECYCLE_RESULT_OK); + assert(game_lifecycle_start(&lifecycle, player, game_id) == LIFECYCLE_RESULT_OK); + assert(game_lifecycle_leave(&lifecycle, player) == LIFECYCLE_RESULT_OK); + } else { + assert(game_lifecycle_reset(&lifecycle, player, game_id) == LIFECYCLE_RESULT_OK); + } + assert(lifecycle.game.state.phase == PHASE_LOBBY && !lifecycle.sessions.entries[0].occupied); + } +} + int main(void) { test_capacity_sanitize_and_resume(); test_human_lifecycle_and_guards(); test_bot_lifecycle_and_stale_actions(); test_leave_and_full_reset(); + test_fifty_mixed_recovery_cycles(); puts("game lifecycle tests passed"); return 0; }