CLAUDE.md
CLAUDE.md
Notatki dla przyszłych sesji Claude’a pracujących nad tym projektem.
Czym jest projekt
Webowa gra w czystym JS ucząca dwóch chłopców (3 i 4 lata) numerów telefonów do rodziców.
- Strona statyczna, bez build-stepu, bez zależności, bez frameworka.
- Tekst UI i komentarze w kodzie po polsku.
- UI dopasowane do tabletu ~9”. Skaluje się w dół przez
clamp()i responsywne stałe w JS. - Działa z dowolnego statycznego hostingu (np. GitHub Pages w podkatalogu Jekylla). Uwaga:
gra.htmlma front matter Jekylla (wersjonowanie assetów dla Cloudflare cache-bustingu), więc bezpośrednie otwarcie przezfile://pokazuje surowe Liquid tagi na górze i ładuje skrypty z?v=w URL. Do testów lokalnych użyjbundle exec jekyll servez roota bloga albo otwórz_site/numerki/gra.htmlpo buildzie.
Struktura plików
index.html – szkielet, ładuje skrypty w kolejności
styles.css – wszystkie style w jednym pliku
CLAUDE.md – ten plik
js/
storage.js – Storage: numery + ustawienia w localStorage
sounds.js – Sounds: piknięcia/melodie (Web Audio API, bez plików)
speech.js – Speech: polski głos (Web Speech API), wymowa cyfr i słów
celebrate.js – Celebrate: animacja nagrody (emoji, "Brawo!", duża maskotka)
phone-game.js – PhoneGame: minigra "telefon" + tryb "listen" (Posłuchaj i wpisz)
drag-game.js – DragGame: minigra "układanka"
quiz-game.js – QuizGame: "Wybierz właściwy numer" (3 rundy)
whack-game.js – WhackGame: bąbelki / whack-a-mole
maze-game.js – MazeGame: labirynt (rysowanie linii palcem)
app.js – router, ekran startowy, wybór gry, ekran "po wygranej" (IIFE, startuje sam)
Kolejność skryptów w index.html ma znaczenie — app.js musi być ostatni, speech.js / sounds.js / celebrate.js muszą być przed grami.
Architektura
- Brak frameworka, wszystko przez bezpośrednie DOM API.
- Moduły jako IIFE pod globalnymi nazwami:
Storage,Sounds,Speech,Celebrate,PhoneGame,DragGame,QuizGame,WhackGame,MazeGame. - Router (
app.js) czyści#appi mount-uje widok. KażdarenderXxxrysuje swój widok od zera (renderHome,renderGamePicker,renderPhoneGame,renderDragGame,renderQuizGame,renderListenGame,renderWhackGame,renderAfterWin). - Każda gra eksponuje to samo API:
GameName.start(rootEl, targetDigitsString, { onBack, onWin, fullHints, label, icon, ... }) onWinz gry prowadzi dorenderAfterWin(item, gameType)— ekran z 3 przyciskami: jeszcze raz / inna gra / koniec.GAME_RENDERERSmapujegameTypena funkcję. Typy gier:phone,drag,quiz,listen,whack,maze.- Wszystkie ścieżki w HTML są względne — działa z dowolnego subpath.
Logika minigier
Telefon (phone-game.js)
- Opcje:
fullHints,listen,label. - Stan:
entered[],failCount. - Zielona słuchawka aktywna dopiero przy
entered.length === target.length. - Po błędzie: shake +
cross-overlay,entered = [],failCount++, fail-dźwięk, Speech mówi “Spróbuj jeszcze raz”. - Cienie cyfr:
ghostsVisible() = fullHints ? digits.length : max(0, failCount - 1). - Każda cyfra przy wciśnięciu jest wymawiana (
Speech.sayDigit). - Tryb
listen: posetTimeout(450)auto-wymowa całego numeru. Topbar ma przycisk 🔊 do powtórki (klasa.speaker, obsłużony w głównymwrap.addEventListener('click')).
Układanka (drag-game.js)
- Opcje:
fullHints. - Kafelki = cyfry numeru + do 4 nadmiarowych (spoza zbioru).
- Drag-and-drop przez pointer events.
touch-action: nonena.piece. - Kafelki
position: absolutew.pool(overflow: visible). - Sloty:
flex: 1 1 0; max-width: 84px; aspect-ratio: 4/5. - Hover slotu ma 3 stany w JS:
.hover+.hover-match(zielony glow) lub.hover-nomatch(czerwony tint). Wymagatransition: ... transform, box-shadowna.slot. - Po sukcesie kafelek “leci” do slotu — animacja
translate + scale(cubic-bezier z bounce), potem opacity 0; w slocie pop-in tekstu.slot-value. Implementacja wplaceInSlot: wyciągatranslate(x,y)z bieżącego transformu (regex nastyle.transform) i dodaje delta do środka slotu obliczonego zgetBoundingClientRect.Speech.sayDigitw momencie placement. fullHints: po utworzeniu slotów od razu wstawia.ghost-hintz cyfrą.handleFail: znika 1 nadmiarowa cyfra + cień w pierwszym pustym slocie bez podpowiedzi (jak wcześniej).
Quiz (quiz-game.js)
- 3 rundy. Każda runda: prompt “Który to numer Mamy?” (Speech czyta) + 2 przyciski z numerami (jeden prawdziwy, drugi z 1-2 zmienionymi cyframi).
makeFake(target): kopia ze zmienionyminumChangeslosowymi cyframi (każda zmieniona na inną niż oryginał).- Polski numer 9-cyfrowy formatowany jako
123 456 789. Krótsze: 7-cyfrowy123 4567, inne — grupowanie po 3. capitalizeVoc(label): prosta mapa imion na dopełniacz (“Mama” → “Mamy”). Fallback: bez zmiany.- Sukces po 3 prawidłowych w rzędzie →
Celebrate.show. Błąd: shake + krzyżyk, ale runda się nie kończy — dziecko próbuje dalej.
Bąbelki (whack-game.js)
Styl wizualny: wanna widziana z lotu ptaka + zaparowane lustro nad nią jako pasek postępu.
- Stan:
expectedIndex,inTransition(lockuje tapy podczas animacji między rundami). - Lustro (
.mirror→.mirror-inner→ N ×.mirror-cell): każda komórka ma tylko.mirror-digit(cyfra na srebrzystym tle szyby ze smugą światła z::after). Efekt “nieodgadnięte” robifilter: blur(22px)na samej cyfrze – wygląda jak rozmazany ciemny cień, nieczytelny. Stany:- default (niewybrana):
blur(22px), opacity 0.55 – bardzo rozmazany cień. .hint(tryb pełnych podpowiedzi):blur(4px), opacity 0.8 – czytelny cień cyfry..revealing:blur(0), opacity 1,scale(1.15)– chwilowy “punch” przy trafieniu..revealed:blur(0), opacity 1, scale 1 – ostra cyfra końcowa.- Przejścia obsługuje
transition: filter, opacity, transform(0.7 s ease), więc te same klasy działają w obu trybach bez glitcha (z hint→revealed jest gładko).
- default (niewybrana):
- Klasa
.nextpulsuje na komórce do odgadnięcia (nextPulsebox-shadow). Pulsuje też w trybie.hint. - Wanna (
.bathtub): rounded-rect z białym brzegiem (porcelana), gradient niebieski wewnątrz (woda), inset shadow dla głębi. - Piana (
.foam→FOAM_COUNT×.foam-bubble): małe białe kulki w losowych pozycjach%, animacjafoamFloat.pointer-events: noneżeby nie blokowała tapów. - Duże bąbelki (
.big-bubble): zawsze 4 sztuki. 1 poprawna + 3 RÓŻNE błędne cyfry (bez duplikatów). Rozkład 2×2 grid z jitterem (pickPositions) — zawsze bez nakładania. - Rozmiar bąbelka skalowany od
tub.clientWidth: 150 / 120 / 96 px. - Tap poprawny:
revealCell(idx)ściera mgłę w odpowiedniej komórce lustra,expectedIndex++, wszystkie 4 bąbelki dostają.poppingze stagger 70 ms, po 800 msspawnBubbles()wsadza nowy zestaw 4 zbubbleEnteranimacją. - Tap zły: tylko wybrany bąbelek
shake, fail dźwięk, krzyżyk; stan się nie zmienia. - Wygrana po odgadnięciu wszystkich cyfr →
Celebrate.showz opóźnieniem 1.1 s żeby animacja popu zdążyła się skończyć. inTransition = truena czas popu+spawn, blokuje wielokrotne tapnięcia.fullHints: niewytypowane komórki dostają.hintzamiast pełnego rozmycia – cyfra widoczna jako delikatny cień (blur 4 px). Po trafieniurevealCellzdejmuje.hinti.next, dodaje.revealing(chwilowy scale 1.15 + blur 0), po 700 ms.revealed. Dzięki transitionom przejście hint→revealed jest płynne, bez chwilowego pojawienia się mgły.
Labirynt (maze-game.js)
- Plansza 11×13 (
COLS/ROWS), generowana algorytmem DFS perfect maze przy każdym uruchomieniu. Gwarantuje dokładnie jedną ścieżkę między dowolnymi dwoma komórkami. - Model ścian: każda komórka to korytarz; ściany leżą na krawędziach. Dwie macierze:
hWalls[r][c](poziome, między (c,r-1) a (c,r); rozmiar(rows+1) × cols) ivWalls[r][c](pionowe; rozmiarrows × (cols+1)).isPassable(maze, fromC, fromR, toC, toR)sprawdza out-of-bounds + brak odpowiedniej ściany. To zupełnie inny model niż “blokowe” gry — ważne przy zmianach. - Start/meta: środkowa kolumna
floor(COLS/2), wiersz0(start) iROWS-1(meta). Strzałka rysowana na zewnątrz nad startem, kotek pod metą —viewBoxrozszerzony oCELLz góry i z dołu (-CELLdo(ROWS+2)*CELL). Wlot u góry i wylot u dołu wizualnie nie mają ściany (pomijane wdrawWalls), aleisPassableblokuje wyjście poza siatkę. - Cyfry:
pickDigitCells(path, n)rozkładancyfr równomiernie wzdłuż unikalnej ścieżki start→meta (pomija pierwszą i ostatnią komórkę).findPathto BFS — w perfect-maze zwraca jedyną drogę. Pozycje błędnych cyfr (wrongCells) to losowe dead-endy (findDeadEnds) poza główną ścieżką. Cyfry błędne losowane jako0–9niezależnie od targetu. - Filtry generatora:
MIN_PATH_LEN/MAX_PATH_LEN(28–80) + min liczba dead-endów; doGEN_RETRIES(30) prób, potem fallback bierze ostatni wygenerowany. - Rysowanie linii: SVG
<polyline>zielony,stroke-width: 14. KażdypointermovewołaconstrainMove(x0,y0,x1,y1)— iteracyjny sweep, w którym ruch jest dzielony na segmenty na krawędziach komórek: przekraczamy krawędź jeśliisPassablelub clampujemy do ściany i kontynuujemy resztę wektora wzdłuż tej osi. Zwraca teżvisited— listę kolejno odwiedzonych komórek (potrzebne, bo szybki pointermove może przeskoczyć kilka komórek na raz). - Checkpoint i błąd: po każdej trafionej prawidłowej cyfrze zapisujemy
lastCheckpoint = { pointsLength, cell, revealedCount }. Wjazd w komórkęwrongCells(tylko w trybiefullHints=false) →handleError:Sounds.fail, krzyż SVG (mazeCrossPop),.shakena svg, po 750 mslinePoints.slice(0, pointsLength)i powrót do checkpointu. Pointerdown blokowany przezerrorBlockw trakcie animacji. - Kontynuacja po podniesieniu palca: kolejny
pointerdownmusi być w obrębiePOINTER_DOWN_TOL = CELL*0.75od ostatniego punktu linii — inaczej ignorujemy (nie restartujemy od strzałki). fullHints: ON = łatwy (wszystkie cyfry widoczne w pasku, brak błędnych cyfr na odnogach). OFF = trudny (cyfry odkrywane jedna po drugiej, błędne cyfry na dead-endach, błąd cofa linię).- Pasek cyfr nad planszą (
.maze-digits): kropki•lub odkryta cyfra; klasa.nextpulsuje na najbliższej do odkrycia (tylko w trybie hard). - Skalowanie:
viewBox = 0 -CELL COLS*CELL (ROWS+2)*CELL.aspect-ratio: 11/15w CSS.clientToSvgzna offset (-CELL na y) — pamiętaj przy zmianie wymiarów.
Mowa (speech.js)
- Polski głos wybierany przez
pl-PLzgetVoices().onvoiceschangedre-pickuje, bo niektóre przeglądarki ładują głosy asynchronicznie. say(text, { rate, pitch, cancel, onEnd })— pojedyncza wypowiedź.sayDigit(d)— cyfra jako słowo (mapowanie 0-9 → “zero”…”dziewięć”). Kasuje poprzednie (cancel: true), żeby przy szybkim klikaniu nie kumulowało się.sayNumber(numStr, opts)— łączy cyfry przecinkami w jedną wypowiedź → naturalne pauzy. Wolniejsze (rate: 0.85).cancel()— wycisz wszystko (wołane wrenderHomeirenderGamePicker, żeby przy nawigacji speech się nie nakładał).
Dźwięki (sounds.js)
- AudioContext lazy.
Sounds.unlock()przy pierwszej interakcji odblokowuje iOS. - Klawisze: melodyjna paleta (
KEY_FREQS), nie prawdziwy DTMF.
Storage (storage.js)
- localStorage. Klucze:
ppn_numbers_v1(lista),ppn_settings_v1(ustawienia). - Schema numeru:
{ id, label, number, icon }.numberjako same cyfry. - API:
Storage.list/add/remove/get/move/getSetting/setSetting. move(id, delta)swapuje element z sąsiadem (delta = -1w górę,+1w dół). Brak operacji jeśli poza zakresem.- Ustawienie
fullHints(bool) jest czytane przezapp.jsi przekazywane do gier jako opcja.
Ekran startowy: zarządzanie listą
Zaprojektowany tak, żeby dzieci nie mogły przypadkiem usunąć ani dodać numerów.
- Karta numeru pokazuje ikonę, imię (
label) i sformatowany numer (formatNumber: 9 cyfr → “123 456 789”, 7 cyfr → “123 4567”, inne → grupowanie po 3). - Krótki tap na karcie = uruchomienie ekranu wyboru gry (zwykły flow).
- Przytrzymanie 5 s na karcie (
LONG_PRESS_MS) = wejście w tryb zarządzania (selectedId = item.id). Wizualnie: pomarańczowy pasek wypełniający kartę (.press-fill) podczas trzymania. Po 5 s wibracja (jeśli dostępna) +Sounds.pop(). - W trybie zarządzania: karta dostaje pomarańczową obwódkę i
selectedPulse, pozostałe karty są przygaszone (.dimmed, opacity 0.35). Pod kartą widoczne 4 akcje:- ↑/↓ →
Storage.move(id, -1/+1), re-render - ✕ usuwa (bez confirma, bo long-press był bramką)
- ✓ wychodzi z trybu (
selectedId = null) - Tap na tło wybranej karty lub na przygaszoną kartę też wychodzi z trybu.
- ↑/↓ →
- Brak inline-formularza dodawania – formularz jest w modalu.
Floating Action Button (FAB) i modal dodawania
- FAB
+w prawym dolnym rogu (position: fixed; bottom: 28px; right: 28px). - Krótki tap na FAB = pokaz podpowiedzi
"Przytrzymaj 5 sekund"(.fab-hint, znika po 1.8 s). - Przytrzymanie 5 s na FAB = otwarcie modala dodawania numeru. Postęp pokazywany jako żółty SVG ring obwodu (
stroke-dashoffsetod 289 do 0, animacja 5 s linear). - Modal: backdrop półprzezroczysty (z animacją fade-in), karta modala (scale + opacity in). Pole imienia + numeru + chipy presetów. Zamykanie:
Escape, klik w backdrop,Anuluj, lub poprawnyZapisz.
setupLongPress helper
setupLongPress(el, { durationMs, onTap, onLongPress }) w app.js:
- Po
pointerdownrusza timer nadurationMsi dodaje klasę.pressing. pointermovepoza progiem 18 px → kasuje (palec się ślizga = nie przytrzymanie).pointerupprzed timerem → krótki tap (onTap).pointeruppo timerze → long-press (onLongPress).pointerleave/pointercancel→ kasuje.
Używany dla kart numerów (tap=gra, long-press=tryb zarządzania) i FAB (tap=hint, long-press=modal).
Maskotka
- Mała w rogu (
mascot-corner):position: fixedlewy-dół, animacjamascotBounce, dodawana do#appwrenderHome(rodzic robi append dorootpowrap). - Duża po wygranej (
mascot-big): używana wrenderAfterWin— wskakuje od dołu, dłonie do góry (mascotJump). - Wielka w celebracji (
celebrate-mascot): podczasCelebrate.show()wskakuje od dołu, kręci się, wraca na dół (mascotCelebrate). Losowa zMASCOTS = ['🐶','🐱','🐼','🦊','🐯','🦁','🐵'].
Tryb “pełne podpowiedzi”
- Toggle w ekranie wyboru gry (
.hint-togglez<input type="checkbox">). Zapisywany w settings pod kluczemfullHints. - Włączony: Telefon pokazuje wszystkie cienie od początku, Układanka ma cień w każdym slocie od startu, Labirynt pokazuje cały numer w pasku i nie ma błędnych cyfr na odnogach.
- Bąbelki używają w trybie “hint” (cyfry-cień przez blur). Quiz nie używa.
Konwencje
- Tekst UI po polsku, komentarze w kodzie po polsku.
- Emoji intencjonalne: presety (
👩 👨 👵 👴), klawisze (📞 ⌫ 🔊), gry (🧩 🤔 👂 🫧), maskotki, nagroda. - Bez build-stepu — nie wprowadzaj
npm,webpack,sass,ts-configbez wyraźnej zgody. Surowe pliki to cecha projektu. - Brak testów automatycznych. Testowanie ręczne w przeglądarce.
Jak uruchomić
- Najlepiej:
bundle exec jekyll servez roota bloga, potemhttp://localhost:4000/numerki/gra.html. Tylko ten tryb prawidłowo zrenderuje wersjonowanie?v=. - Alternatywa bez Jekylla:
python3 -m http.serverw katalogunumerki/— strona zadziała, ale na górze pojawią się surowe Liquid tagi i?v=w URLach (assety i tak się załadują, bo serwery ignorują query stringi przy plikach statycznych). file://już nie działa czysto z powodu front matter wgra.html.- Sprawdzenie składni JS:
for f in js/*.js; do node --check "$f"; done.
Deploy
- W blogu Jekylla na GitHub Pages: wrzucić folder jako
numerki/do repo bloga.gra.htmlma front matter (layout: null,permalink: /numerki/gra.html) — Jekyll przetwarza Liquid i serwuje pod tym samym URL. Reszta plików (JS/CSS) bez front matter jest kopiowana “as is”. - Cache-busting: w
<head>jest ``, a wszystkie<script>/<link>mają?v=9e450e95ce13e23488d81b48c921f6baa16b0589. Każdy push generuje nowy build_revision → Cloudflare/przeglądarka zaciągają świeże assety bez ręcznego purge. Plus metaCache-Control: no-cache, no-store, must-revalidatena samym HTML. - Osobne repo z GitHub Pages: Settings → Pages → branch
main/ root.
Pułapki / już-naprawione rzeczy
- Sloty zawijały się dla 9-cyfrowych numerów →
flex: 1 1 0+ tekst w slocie zamiast kafelka. pooloverflow: hidden→ zmienione navisible, inaczej dragged piece był cięty.- Letter-spacing między
<span>-ami w.digits-rownie działa — dodanygap. - Audio na iOS —
Sounds.unlock()w user gesture. - Drag i scroll na iOS —
touch-action: nonena.piecei.bubble. - Speech na iOS — pierwsza wypowiedź musi być w handlerze gestu; obsłużone (Speech wywołujemy z click handlerów).
- Voiceschanged — bez
speechSynthesis.onvoiceschangedFirefox/Safari mogą nie znaleźć głosu PL przy pierwszym wywołaniu.
TODO / pomysły na rozwój
Zaimplementowane jako kolejny krok:
- PWA —
manifest.json, ikona, prosty service worker. Wtedy “Dodaj do ekranu głównego” daje fullscreen. - Wake Lock API — ekran tabletu nie gaśnie w trakcie gry.
- Vibration API — krótki buzz przy błędzie.
- PIN rodzicielski na ekran dodawania/usuwania (np. long-press 3s + 4 cyfry).
- Profile dziecka — przełącznik między dziećmi, osobne listy + statystyki.
- Statystyki — ile prób / pomyłek per numer per gra, kiedy ostatnia sesja.
- Eksport/import listy numerów (JSON lub kod QR).
- Memory / Pexeso jako kolejna gra (pary cyfr z numeru).
- Tracing cyfr — szlaczek do obrysowania palcem.
- Kolekcja naklejek za sukcesy — gamifikacja.
- Większa różnorodność dźwięków sukcesu (random z 3-4 melodii).
Co warto sprawdzić przed wprowadzaniem zmian
- Zmiana rozmiarów slotów / liczby cyfr: sprawdź na wąskim ekranie (~320px) i tablecie (~768-1024px).
- Zmiana w
phone-game.js: wpis poniżej długości (przycisk nieaktywny), zły numer (shake/krzyżyk/czyszczenie), cienie po kolejnych błędach, sukces, tryblisten(głośnik, auto-mowa). - Zmiana w
drag-game.js: pointer capture na touchscreen, kolejnośćhandleFail, kasowanie hover na end-drag, animacja lotu kafelka (delta zgetBoundingClientRect). - Zmiana w
app.js: każdyrender*zaczynaj odclear(); po nawigacji wołajSpeech.cancel()żeby stara wypowiedź się nie kończyła na nowym ekranie. - Zmiana w
whack-game.js: pamiętajclearInterval(refreshTimer)przy wygranej i powrocie. Inaczej zombie timer. - Zmiana w
quiz-game.js: nie blokuj wszystkich przycisków po dobrej odpowiedzi — wystarczy lock samego wybranego (data-locked), inaczej szybkie podwójne kliknięcie psuje stan. - Zmiana w
maze-game.js: jeśli zmieniaszCOLS/ROWS/CELL, sprawdźaspect-ratiow CSS orazclientToSvg(offset y =-CELL). Jeśli zmieniasz model ścian, pamiętaj żeconstrainMoveużywaisPassable, a nie sprawdzania “czy komórka jest korytarzem” — wszystkie komórki w siatce to korytarze.