Files
fishing-game/docs/BROWSER-TESTING.md
andrey 36f54109a7 Рыба-босс в каждой локации (Этап 37): BOSS_BY_LOCATION={0:'sturge',1:'crayfish',2:'swordfish',3:'whale'} — босс = существующий вид текущей локации (Осётр/Гигантский рак/Рыба-меч/Синий кит, новых видов в SPECIES нет), условие в startBite() по save.location, тот же паттерн M4 (клон с boss:true, 30% или forceBoss). Единое правило веса: любой босс 20–50 кг (hook(): w=sp.boss?20+rand*30:rollWeight(sp), до d0/ov/st0, тот же w в reel.w/catchFish) — карманного рака нет, но босс до 50 кг > maxW Титана 40: при w>40 в режиме «держи кнопку» возможен настоящий обрыв (вываживается контролем натяжения) — зафиксировано в DEVLOG как материал баланс-этапа. Награда ×5/фазы/карточка «-БОСС» — без изменений, все вид-независимы; старые M4-пины (§9k/§9l(3)/M6(4)) прошли байт-в-байт без правок (награда читает фактический w, фазы нормированы на d0). Ачивки: boss_first — нейтральный текст «Победи рыбу-босса», +3 новые lake_boss 🦞 / sea_boss 🦈 / ocean_boss 🐳 (check по save.bosses[id], БЕЗ coinPct — пин §10c(8) achBonus 0.16 жив; bonus:{} — bonusText/achBonus корректны) — 31→34. save.bosses={} — дефолт save/freshSave + миграция loadSave + запись в boss-ветке catchFish. Тесты: новый блок §9k2 (3 локации × старт/вес 20–50 по фактическому reel.w/улов ×5/save.bosses/ачивка/карточка) + миграция сейва, a22d 31→34 — 193/193 PASS (2 прогона); check.mjs OK; emoji-font --check OK (🦞/🦈/🐳 уже в субсете); balance.mjs — без сдвига (босс вне ролла). Доки: README/AGENTS/KODA/DEVLOG (Этап 37), BROWSER-TESTING и orca-playtest — цифры босса 5–40→20–50 кг.
2026-09-19 21:31:54 +03:00

179 lines
24 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Браузерный плейтест через Orca CLI
`tests/orca-playtest.js` — плейтест игры **в настоящем браузере**: он открывает вкладку встроенного браузера Orca app, сбрасывает сейв (чистое состояние) и прогоняет полный геймплей (меню → бросок → клёв → подсечка → улов → лавка (дневник, 🎯, 🏅, 🎨 скины, 🗺️ локации) → золотая рыба в дождь + туман (M8) → near-miss → ночь → фонарик в лавке → босс-рыба → хаб «Меню» (Э29)) через Orca CLI и проверяет состояния, DOM, ачивки и консоль.
В отличие от puppeteer-теста (`tests/test.js`), здесь нет отдельного headless Chromium — управление идёт командами CLI против браузера, которым пользуются люди. Это ловит то, что headless пропускает: отличия среды Orca-браузера, троттлинг фоновых вкладок, реальные CLI-нюансы.
## Когда запускать
- **Перед коммитом**, рядом с остальными проверками (см. AGENTS.md, раздел «Проверка»):
1. `node --check` на вынутый из `index.html` JS;
2. `cd tests && node test.js` — **главный рубеж** (headless, всегда доступен);
3. `node tests/orca-playtest.js` — браузерный плейтест (требует запущенный Orca app);
4. `node tests/gen-screenshots.js` — скриншоты (15 кадров × 3 вьюпорта, 24-битный PNG).
- **В отличие от puppeteer-теста, требует запущенный Orca app.** Если Orca не запущен — скрипт честно падает на шаге 0 с понятной ошибкой; в таком случае шаг **пропускается и не блокирует** коммит (главный рубеж — `tests/test.js`).
## Требования
| Что | Зачем | Как проверить |
|---|---|---|
| Запущенный Orca app (desktop) | Вкладка создаётся в его встроенном браузере | `orca-ide status --json` → `result.runtime.state === "ready"` |
| `orca-ide` в PATH **или** переменная `ORCA_CLI_COMMAND` | Скрипт вызывает CLI через `process.env.ORCA_CLI_COMMAND \|\| 'orca-ide'` | `which orca-ide` |
> На Linux голое имя `orca` может резолвиться в GNOME screen reader — НЕ используйте его; в терминалах Orca алиас `orca` работает, но скрипт намеренно берёт `orca-ide` (или переопределение через `ORCA_CLI_COMMAND`).
Запуск (из каталога репо):
```bash
node tests/orca-playtest.js # скриншоты orca-*.png кладутся в CWD
```
Только встроенные модули Node (`child_process`, `fs`, `path`) — `npm install` не нужен.
## Таблица шагов
| # | Шаг | Что проверяется (ok()) |
|---|---|---|
| 0 | `status` | runtime готов; иначе «запусти Orca app» и exit 1 |
| 1 | `tab create --url file://…/index.html` | получился `browserPageId` |
| 1b | установка error-хуков (`window.__errs`, `error` + `unhandledrejection`) | хуки стоят до старта геймплея (polling переживает eval-ошибки грузящейся страницы) |
| 2 | ожидание DOM | меню (`#menu`) показано |
| 2b | сброс сейва — настоящий UI-путь хаба «Меню» (Э29): «Играть» → ⏸ → «В меню» → 🗑️ «Очистить» → «Сбросить» (`save = freshSave()` + persist) | `#gamemenu` открывается из паузы (`#pause` скрыт, `state !== "MENU"` — сессия сохранена), после сброса — `#menu` (инструкции), `#gamemenu` скрыт, `save.coins === 0` |
| 3 | клик «Играть» (`.click()` в DOM) | `state === "IDLE"` |
| 4 | `pointerdown` на canvas → polling `CHARGE` → удержание ~0.7 c → `pointerup` | `CHARGE`, затем `WAIT` (бросок) |
| 5 | `biteTimer = 0.05` → polling `BITE` (через APPROACH) **с расширением окна `biteT = 5` в том же eval** → скриншот → тап | `BITE`, затем `REEL` (подсечка) |
| 6 | `reel.d = 0.5` + удержание ~2 c → polling `CARD` (30 c) → скриншот | `CARD`, в карточке имя вида и «+N» |
| 7 | «Отлично!» → «🛒» → polling `#shop` → скриншот → вкладка «📔» → скриншот → вкладка «🎯» → скриншот → вкладка «🏅» → скриншот → «×» | лавка открыта/закрыта, дневник/квесты/достижения рендерятся |
| 7c | вкладка «🎨» → polling рендера → `save.coins = 200000` (выдача) → покупка поплавка «Красный» (**клик и чтение состояния в одном eval**) | группы «Лодка»/«Поплавок», списание (−500), авто-экипировка `save.bobberSkin=1` |
| 7d | вкладка «🗺️» → покупка «Озёра» (**клик + состояние + тост в одном eval**) → скриншот → возврат на «Реку» | авто-переход `save.location=1`, тост «Добро пожаловать на Озёра!», списание (−100000), выбор купленной |
| 8 | `forceWeather = "rain"` → пилюля 🌧️ → ratio `variantChance(gold)` дождь/ясно (**в одном eval**) → `SHINY_CHANCE = 1; forceVariant = "gold"` → полный цикл → скриншот → проверка DOM → ачивки → `SHINY_CHANCE = 0.06; forceVariant = null; forceWeather = null` | бейдж «ЗОЛОТАЯ» + `gold-card`; ×3 (M8); ачивки Этап 22 `first_shiny`+`rain_variant` (wxRainV=1)+`skin_first`+`lake_first` — выдаются НА УЛОВЕ (checkAchievements в catchFish, не в лавке) |
| 8b | `forceWeather = "fog"` → пилюля 🌫️ → ratio `biteChance()` туман/ясно (**в одном eval**) → снятие форса → polling скрытия пилюли | клёв ×1,5 (M8), пилюля показ/скрытие |
| 9 | цикл до `REEL` → `reel.d = reel.d0 * 1.45 + 2` → polling `WAIT` **с чтением тоста в том же eval** | тост содержит «Сбежала» |
| 9e | хаб «Меню» (Э29): монеты → лавка → × → ⏸ → «В меню» → «Магазин»/«Достижения»/«Дневник» (× шаг назад в хаб) → «Инструкции» → «Играть» → ⏸ → «В меню» → 🔊 ×2 → «Продолжить»; скриншот `orca-gamemenu.png` | монеты открывают лавку на «Лавке» (tabTackle), из хаба — свои вкладки 🏅/📔, × возвращает в хаб (`uiOpen=true`, геймплей не стартует), Инструкции = `#menu`, mute-тогл на каждом клике, «Продолжить» → `uiOpen=false`, `state=IDLE` |
| 10 | `setDayTime(0.85)` **с чтением `dayPhase` в том же eval** → скриншот | `dayPhase === "night"` |
| 11 | «🛒» → вкладка «🎣» → поиск «фонарик» в `#shopBody` | в снастях есть апгрейд-фонарик (⬆ 70 🪙, пометка про ночь) |
| 12 | `forceBoss = true` + `pickSpecies` → осётр → выдача «Титана» через сейв (`save.rod=3`; см. п. 12 «Подводных камней») → полный цикл: `REEL` (старт боя: `stateBoss`, `bossPhase=1`) → 3 фазы (в каждой `reel.d = 0.4` → polling **персистентного `bossPhase===фаза+1` со сбросом `holding` в том же eval**, см. п. 13 → авто-`REEL` следующей фазы; фаза 3 → `CARD`) → скриншот → `forceBoss = false` в `finally` | `rod().maxW ≥ 40` (стартовая удочка не выдерживает вес босса), старт босс-боя, карточка после 3 фаз (`stateBoss=false`), имя «Осётр-БОСС» с бейджем |
| 13 | `console --limit 100` | нет сообщений уровня `error` |
| 13b | чтение `window.__errs` | нет pageerror за прогон (JS-ошибки и unhandledrejection) |
| 14 | `tab close` (в `finally`, 2 попытки — close идемпотентен) | вкладка закрыта при любом исходе |
Итог — сводка `ИТОГ: N/N` и exit code: `0` все PASS, `1` есть FAIL, `2` краш сценария, `3` глобальный таймаут (~4 мин).
## Пример вывода
```
PASS | Orca runtime готов
PASS | вкладка с игрой открыта | pageId=d94df318
PASS | меню показано
PASS | пауза → «В меню»: открыт хаб #gamemenu, #pause скрыт, сессия сохранена (state!=="MENU")
PASS | сброс сейва через «Меню»: coins=0, #gamemenu скрыт, возврат к инструкциям
PASS | день перед геймплеем: dayPhase!=="night" (сейв профиля мог прийти с ночью)
PASS | старт: state=IDLE
PASS | зарядка: state=CHARGE
PASS | бросок: state=WAIT
PASS | клёв: state=BITE (через APPROACH)
PASS | скриншот orca-bite.png
PASS | подсечка: state=REEL
PASS | улов: state=CARD
PASS | скриншот orca-card.png
PASS | карточка улова: имя вида и «+N» | {"hidden":false,"name":"Карась","val":"+2 🪙"}
PASS | лавка открыта (вкладка Снасти)
PASS | скриншот orca-shop.png
PASS | дневник показан
PASS | скриншот orca-journal.png
PASS | 🎯 вкладка: стрик и квесты дня
PASS | скриншот orca-rewards.png
PASS | 🏅 вкладка: список достижений
PASS | скриншот orca-ach.png
PASS | 🎨 вкладка: скины лодки и поплавка
PASS | скины: покупка «Красный» (500 🪙) — списание и авто-экипировка | {"base":199500,"coins":199500,"owned":true,"skin":1}
PASS | скриншот orca-skins.png
PASS | 🗺️ вкладка: локации (Река / Озёра)
PASS | локации: покупка «Озёра» (100000 🪙) — авто-переход, тост, списание | {"base":99500,"coins":99500,"loc":1,"owned":true,"toast":"Добро пожаловать на Озёра!"}
PASS | скриншот orca-locations.png
PASS | локации: возврат на Реку (выбор купленной)
PASS | лавка закрыта
PASS | погода rain: HUD-пилюля 🌧️
PASS | погода rain: variantChance(gold) ×3 (WEATHER.rain.variantMult) | 3
PASS | зарядка: state=CHARGE
PASS | бросок: state=WAIT
PASS | клёв: state=BITE (через APPROACH)
PASS | подсечка: state=REEL
PASS | золотая рыба: улов получен (state=CARD)
PASS | скриншот orca-shiny.png
PASS | золотая рыба: бейдж «ЗОЛОТАЯ» и золотая карточка (gold-card) | {"badge":"ЗОЛОТАЯ","gold":true,"hidden":false}
PASS | ачивки Этап 22: first_shiny + rain_variant (улов в дождь) + skin_first + lake_first | {"lake":true,"rain":true,"shiny":true,"skin":true,"wx":1}
PASS | погода fog: HUD-пилюля 🌫️
PASS | погода fog: biteChance ×1.5 (WEATHER.fog.biteMult) | 1.5
PASS | погода снята: пилюля скрыта
PASS | зарядка: state=CHARGE
PASS | бросок: state=WAIT
PASS | клёв: state=BITE (через APPROACH)
PASS | подсечка: state=REEL
PASS | near-miss: тост «Сбежала» после схода | Сбежала! 🐠 Окунь 0.2 кг
PASS | скриншот orca-escape.png
PASS | ночь: dayPhase=night при dayTime 0.85 | night
PASS | скриншот orca-night.png
PASS | лавка: фонарик — апгрейд 1 удилища (⬆ 70, пометка про ночь)
PASS | босс: выдан «Титан» (до 40 кг) — стартовая удочка не выдерживает вес босса
PASS | зарядка: state=CHARGE
PASS | бросок: state=WAIT
PASS | клёв: state=BITE (через APPROACH)
PASS | подсечка: state=REEL
PASS | босс: старт боя (stateBoss=true, bossPhase=1, reel.boss) | {"b":true,"ph":1,"rb":true}
PASS | босс: карточка после 3 фаз (stateBoss=false)
PASS | скриншот orca-boss.png
PASS | босс: карточка «Осётр-БОСС» с бейджем 🏆 | Осётр-БОСС | 🏆 БОСС!
PASS | монеты → лавка открыта на вкладке «Лавка» (tabTackle active)
PASS | × из лавки (открыта монетами) → uiOpen=false, возврат в игру
PASS | скриншот orca-gamemenu.png
PASS | ⏸ → «В меню»: хаб открыт, #pause скрыт, сессия сохранена (state!=="MENU")
PASS | «Магазин» → лавка на «Лавке» (tabTackle), #gamemenu скрыт
PASS | × из лавки → возврат в «Меню» (uiOpen=true, геймплей не стартует)
PASS | «Достижения» → лавка на вкладке 🏅
PASS | «Дневник» → лавка на вкладке 📔
PASS | «Инструкции» → #menu виден, хаб скрыт
PASS | «Играть» из инструкций → state=IDLE, #gamemenu скрыт
PASS | 🔊 «Звук» в «Меню»: каждый клик — тогл | {"m0":false,"m1":true,"m2":false}
PASS | «Продолжить» → хаб скрыт, uiOpen=false, state=IDLE, #btnPause виден
PASS | нет ошибок в консоли (console --limit 100)
PASS | нет pageerror за прогон (window.onerror / unhandledrejection) | []
===== ИТОГ: 76/76 =====
```
## Подводные камни
1. **Троттлинг rAF в фоновой вкладке.** Вкладка Orca обычно не на переднем плане → `requestAnimationFrame` троттлится → игровое время идёт **медленнее** реального (кадр = максимум 0.05 c игрового). Поэтому **никаких фиксированных sleep в ожидании состояний**: все ожидания — polling с таймаутом (`waitCond`/`waitState`, шаг ~300 мс, таймауты 10–30 с). Фиксированные `sleep` допустимы только как «удержание кнопки» (зарядка ~0.7 c, улов ~2 c).
2. **Окно подсечки короче CLI-roundtrip.** Окно BITE — 0.35–0.8 с, а один CLI-вызов стоит ~0.3–0.5 с. Рецепт стабильного перехвата: `biteTimer = 0.05` → polling, где **в том же eval, что детекция `BITE`, сразу выставляется `biteT = 5`** (отдельный eval не успевает: на первопланарной вкладке игра идёт в реальном времени, и окно закроется до его применения) → СНАЧАЛА скриншот, потом тап.
3. **Цитирование: только `execFile` с массивом аргументов, без shell.** JS-выражения содержат кавычки — при передаче одной строкой через shell (PTY) они ломаются. В скрипте все вызовы — `child_process.execFile(ORCA, argsArray, …)`; никакого `exec("orca-ide …")`.
4. **Pointer Events с уникальным `pointerId`.** Игра слушает `pointerdown` на canvas и `pointerup` на window. Каждое взаимодействие (down/up-пара) получает свой `pointerId` (счётчик в скрипте) — так события не путаются с реальными.
5. **Тосты недолгие (1.6 с).** Текст тоста near-miss читается **в том же eval**, что и ожидание `WAIT` (`state === "WAIT" ? toast.textContent : ""`) — отдельный последующий вызов может опоздать. Скриншот `orca-escape.png` — best-effort (тост мог погаснуть); ok() ставится по тексту.
6. **CLI может transient-повиснуть.** Наблюдались единичные зависания `eval` (~1 мин), проходящие при повторе. Поэтому `orca()` ретраит вызовы (3 раза, линейный бэкофф 0.5/1 c; повторы пишутся в **stderr**, чтобы не мешать stdout с PASS/FAIL), а `execFile` жжёт зависший процесс таймаутом 20 с. **Исключение — `tab create`: ретраить нельзя** (получится дубль вкладки), он идёт через `orcaCall` один раз. `tab close` в `finally` ретраится 2 раза — вызов идемпотентен.
7. **Эмодзи — квадраты без шрифта.** Без `font-noto-emoji` эмодзи в скриншотах (и в окне Orca) рендерятся тофу. На скринах это ОК — проверка не по пикселям; для красивых скриншотов поставьте шрифт.
8. **`eval` возвращает объекты JSON-строкой.** `result.result` для объекта/массива — двойная сериализация (строка-JSON); `evalPage` в скрипте декодирует. Притом строка-«MENU» парсится как есть.
9. **Босс-фазы: `SNAP` не сбрасывает `reel.holding`.** `onUp` слушает только `CHARGE`/`REEL` — отпускание пальца в состоянии `SNAP` (босс-обрыв фазы) оставляет `holding = true`, и после авто-перехода в следующую фазу игра «втягивает» сама. Поэтому в шаге 12 сброс `reel.holding = false` делается **в том же eval, что детекция перехода фазы** (см. п. 13; тот же приём, что `biteT = 5` в п. 2). Сам босс-бой — цикл из 3 фаз с polling'ом, `forceCatch()` тут не подходит: на фазах 1–2 вместо `CARD` будет босс-обрыв.
10. **Профиль Orca переживает localStorage между прогонами.** В отличие от `test.js` (свежий headless-профиль в каждом запуске), встроенный браузер Orca помнит сейв (`fishing-save-v1`) — и прошлый прогресс ломает шаги, завязанные на чистое состояние: после M5 «фонарик» — не отдельная строка, а апгрейд-кнопка на удилище 0 (только при 1–3 ур.; на 4 ур. кнопки нет → шаг 11 падал «no flask»). Поэтому шаг 2b сбрасывает сейв **через игру** (пауза → «В меню» → 🗑️ = `save = freshSave()` + persist + сброс всех тест-форсов) **до старта геймплея**, без перезагрузки страницы — error-хуки шага 1b остаются на месте.
11. **Сейв профиля может прийти с НОЧЬЮ — таймаут на клёве (шаг 5).** Сброс 2b обнуляет данные, но НЕ глобальный `dayTime` (день после сброса продолжается — так задумано), а прошлый прогон оставлял в сейве `dayTime≈0.85` (шаг 10 «ночь»). Ночью без фонарика клёва нет совсем: ветка клёва стоит ЗА гейтом `hasLight()` в `updateGame()` — не срабатывает даже `forceBite` (он внутри else-if). Симптом: `timeout: state="BITE" (last: state=WAIT)` при живом game-loop (бросок прошёл). Лечится шагом 2c: `setDayTime(0.3)` + проверка `dayPhase !== "night"` до старта геймплея (шаг 10 сам ставит ночь через `setDayTime(0.85)` и не зависит от входного dayTime).
12. **Босс 20–50 кг (Этап 37) рвёт стартовую удочку — обрыв вместо босс-фаз (шаг 12).** Предел «Дедовой удочки» 3 кг: босс весит минимум 20 кг → `over=w/maxW ≥ 6.7` (до ≈16.7 при 50 кг) — босс рвёт слабые удочки ещё жёстче, чем в M4: натяжение достигает прочности лески РАНЬШЕ, чем `d≤0` даёт босс-обрыв фазы → настоящий SNAP (`stateBoss=false` → IDLE), симптом `timeout: state="REEL" (last: state=IDLE)`. Лечится выдачей «Титана» через сейв (`save.rod=3`, до 40 кг): купить по UI нельзя — M5-гейтинг требует прокачки всей цепочки удочек. В headless `test.js` той же роли играет прокачанная леска — §M4 идёт после апгрейд-тестов M5.
13. **SNAP-окно 0.6 с vs CLI-roundtrip ~0.5 с — ждать `bossPhase`, не `SNAP` (шаг 12).** Фазовый обрыв даёт `state='SNAP'` на `snapT=0.6` игрового времени; poll может проскочить это окно, а оставшийся зажатым указатель спокойно дотягивает босса до `CARD` уже в фазе 3 (симптом: `timeout: "SNAP фазы 2" (last: state=CARD)`). Ждём персистентный `bossPhase===фаза+1` (инкремент в том же переходе, что авто-`REEL`) со сбросом `holding` в том же eval. В headless-тесте window короткий и CDP-раундтрип ~20 мс — там работает и ожидание SNAP.
## Как добавить новый шаг
1. В секции «Прогон» в нужном месте добавьте действия через хелперы (`evalPage`, `waitCond`/`waitState`, `pointerDown/Up`, `clickByText`, `shot`) — фиксированные sleep на ожидание состояний не писать (пункт 1 выше).
2. После каждого шага — `ok('название', условие, extra)`.
3. Если шаг форсирует игру (например, `SHINY_CHANCE = 1`), возвращайте исходное значение в `finally` — иначе сломаете следующие циклы и сейв вкладки.
4. Скриншот — `shot('orca-<имя>.png')`: файл уходит в CWD, не путать с `shot-*.png` из `tests/test.js` (автотесты) и `screenshots-wiki/` из `tests/gen-screenshots.js`.
5. Обновите таблицу шагов в этом документе и число проверок в AGENTS.md, если менялось общее количество ok().
## Справочник CLI (используемые команды)
| Команда | Что даёт (в `--json`) |
|---|---|
| `orca-ide status --json` | `result.runtime.state === "ready"` |
| `orca-ide tab create --url <url> --json` | `result.browserPageId` (дальше везде `--page <id>`) |
| `orca-ide eval --expression <js> --page <id> --json` | `result.result` (объекты — JSON-строкой) |
| `orca-ide screenshot --page <id> --json` | `result.data` = base64 PNG |
| `orca-ide console --limit N --page <id> --json` | `result.messages: [{text, type}]`, `type: "error"` — ошибки |
| `orca-ide tab close --page <id> --json` | `result.closed` |