<!DOCTYPE html>
<html lang=”en”>
<head>
<meta charset=”UTF-8″>
<meta name=”viewport” content=”width=device-width, initial-scale=1.0″>
<title>Letter Adventure!</title>
<link href=”https://fonts.googleapis.com/css2?family=Fredoka+One&family=Nunito:wght@400;600;700;800&display=swap” rel=”stylesheet”>
<style>
:root{–sky:#fff8e7;–primary:#ff6b35;–secondary:#4ecdc4;–accent:#ffe66d;–purple:#a855f7;–green:#22c55e;–dark:#2d2d2d;–card:#ffffff;}
*{margin:0;padding:0;box-sizing:border-box;}
body{font-family:’Nunito’,sans-serif;background:var(–sky);min-height:100vh;overflow-x:hidden;}
body::before{content:”;position:fixed;inset:0;background-image:radial-gradient(circle,#ffcc0022 2px,transparent 2px),radial-gradient(circle,#ff6b3511 2px,transparent 2px);background-size:60px 60px,90px 90px;background-position:0 0,30px 30px;pointer-events:none;z-index:0;}
.container{max-width:680px;margin:0 auto;padding:20px;position:relative;z-index:1;}
/* SETUP */
.setup-card{background:var(–card);border-radius:28px;padding:34px 30px;box-shadow:0 8px 32px rgba(0,0,0,0.08);border:3px solid #f3f4f6;}
.setup-title{font-family:’Fredoka One’,cursive;font-size:2.1rem;color:var(–primary);text-align:center;margin-bottom:6px;}
.setup-sub{text-align:center;color:#777;font-size:0.95rem;font-weight:600;margin-bottom:28px;line-height:1.6;}
.section-heading{font-family:’Fredoka One’,cursive;font-size:1.1rem;color:var(–purple);margin:22px 0 12px;display:flex;align-items:center;gap:8px;}
.section-heading::after{content:”;flex:1;height:2px;background:linear-gradient(90deg,#e2e8f0,transparent);}
.voice-scroll{max-height:340px;overflow-y:auto;padding-right:4px;}
.voice-scroll::-webkit-scrollbar{width:5px;}
.voice-scroll::-webkit-scrollbar-track{background:#f1f5f9;border-radius:99px;}
.voice-scroll::-webkit-scrollbar-thumb{background:#cbd5e1;border-radius:99px;}
.voice-row{background:linear-gradient(135deg,#fff8e7,#fff3ec);border-radius:16px;border:2px solid #fde68a;padding:12px 16px;margin-bottom:8px;display:flex;align-items:center;gap:10px;flex-wrap:wrap;}
.voice-label{font-weight:800;font-size:0.9rem;color:var(–dark);flex:1;min-width:150px;}
.voice-label small{display:block;font-size:0.74rem;color:#d97706;font-weight:700;margin-top:2px;}
.rbtn{font-family:’Fredoka One’,cursive;font-size:0.8rem;padding:6px 12px;border-radius:99px;border:none;cursor:pointer;transition:all 0.15s;box-shadow:0 3px 0 rgba(0,0,0,0.1);}
.rbtn:hover{transform:translateY(-1px);}
.rbtn.rstart{background:var(–primary);color:white;}
.rbtn.rstop{background:#ef4444;color:white;animation:prec 0.7s infinite;}
.rbtn.rplay{background:var(–secondary);color:white;}
@keyframes prec{0%,100%{box-shadow:0 3px 0 rgba(239,68,68,0.3)}50%{box-shadow:0 3px 14px rgba(239,68,68,0.5)}}
.vstatus{font-size:0.76rem;font-weight:700;padding:3px 9px;border-radius:99px;white-space:nowrap;}
.vstatus.done{background:#dcfce7;color:var(–green);}
.vstatus.miss{background:#fef3c7;color:#d97706;}
.rec-progress{text-align:center;margin-top:12px;font-size:0.85rem;font-weight:700;color:#94a3b8;}
.rec-progress span{color:var(–primary);}
.setup-footer{text-align:center;margin-top:22px;display:flex;gap:12px;justify-content:center;flex-wrap:wrap;}
.btn-skip{background:none;border:2px solid #e2e8f0;color:#94a3b8;font-family:’Fredoka One’,cursive;font-size:1rem;padding:10px 22px;border-radius:99px;cursor:pointer;}
.btn-skip:hover{border-color:var(–secondary);color:var(–secondary);}
/* GAME */
header{text-align:center;padding:22px 0 10px;}
header h1{font-family:’Fredoka One’,cursive;font-size:2.6rem;color:var(–primary);text-shadow:3px 3px 0 #ffd166,6px 6px 0 #ffb3c1;letter-spacing:1px;}
header p{font-size:1rem;color:#666;margin-top:3px;font-weight:600;}
.letter-progress{font-family:’Fredoka One’,cursive;font-size:1rem;color:var(–purple);margin-top:6px;}
.speaking-badge{display:inline-flex;align-items:center;gap:6px;background:#fff3ec;border:2px solid var(–primary);border-radius:99px;padding:4px 14px;font-size:0.82rem;font-weight:700;color:var(–primary);margin:6px 0 3px;opacity:0;transition:opacity 0.3s;}
.speaking-badge.show{opacity:1;}
.dot{width:8px;height:8px;background:var(–primary);border-radius:50%;animation:prec 0.6s infinite;}
.stars-row{display:flex;justify-content:center;gap:5px;margin-top:6px;flex-wrap:wrap;}
.star{font-size:1.1rem;transition:transform 0.3s;}
.star.earned{animation:pop 0.4s ease;}
@keyframes pop{0%{transform:scale(1)}50%{transform:scale(1.5)}100%{transform:scale(1)}}
.progress-bar{background:#e5e7eb;border-radius:99px;height:12px;margin:12px 0 20px;overflow:hidden;box-shadow:inset 0 2px 4px rgba(0,0,0,0.1);}
.progress-fill{height:100%;background:linear-gradient(90deg,var(–secondary),var(–primary));border-radius:99px;transition:width 0.5s ease;}
.stage-label{text-align:center;font-family:’Fredoka One’,cursive;font-size:1.15rem;color:var(–purple);margin-bottom:16px;letter-spacing:1px;}
.game-card{background:var(–card);border-radius:28px;padding:30px 26px;box-shadow:0 8px 32px rgba(0,0,0,0.08);text-align:center;border:3px solid #f3f4f6;}
.question-text{font-size:1.3rem;font-weight:800;color:var(–dark);margin-bottom:8px;}
.big-letter{font-family:’Fredoka One’,cursive;font-size:5rem;color:var(–primary);text-shadow:4px 4px 0 var(–accent);margin:8px 0 22px;display:inline-block;animation:bounce 0.6s ease;cursor:pointer;}
.big-letter:hover{transform:scale(1.08);}
@keyframes bounce{0%,100%{transform:translateY(0)}40%{transform:translateY(-12px)}}
.choices-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:12px;max-width:360px;margin:0 auto;}
.choice-btn{background:#f8fafc;border:3px solid #e2e8f0;border-radius:18px;font-family:’Fredoka One’,cursive;font-size:2.2rem;color:var(–dark);padding:16px 0;cursor:pointer;transition:all 0.15s;box-shadow:0 4px 0 #e2e8f0;}
.choice-btn:hover{transform:translateY(-3px);box-shadow:0 7px 0 #e2e8f0;border-color:var(–secondary);background:#f0fffe;}
.choice-btn.correct{background:#dcfce7;border-color:var(–green);box-shadow:0 4px 0 #86efac;color:var(–green);animation:wiggle 0.4s ease;}
.choice-btn.wrong{background:#fee2e2;border-color:#f87171;box-shadow:0 4px 0 #fca5a5;color:#ef4444;animation:shake 0.4s ease;}
@keyframes wiggle{0%,100%{transform:rotate(0)}25%{transform:rotate(-6deg)}75%{transform:rotate(6deg)}}
@keyframes shake{0%,100%{transform:translateX(0)}25%{transform:translateX(-8px)}75%{transform:translateX(8px)}}
.trace-instr{font-size:1.1rem;font-weight:700;color:#555;margin-bottom:14px;}
.canvas-wrap{position:relative;display:inline-block;border-radius:20px;overflow:hidden;box-shadow:0 4px 20px rgba(0,0,0,0.1);border:3px dashed var(–secondary);background:#f0fffe;}
#traceCanvas{display:block;cursor:crosshair;touch-action:none;}
.trace-ghost{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;font-family:’Fredoka One’,cursive;font-size:11rem;color:rgba(78,205,196,0.18);pointer-events:none;user-select:none;line-height:1;}
.trace-actions{display:flex;gap:12px;justify-content:center;margin-top:18px;flex-wrap:wrap;}
.btn{font-family:’Fredoka One’,cursive;font-size:1.05rem;padding:11px 26px;border-radius:99px;border:none;cursor:pointer;transition:all 0.15s;box-shadow:0 4px 0 rgba(0,0,0,0.15);letter-spacing:0.5px;}
.btn:hover{transform:translateY(-2px);box-shadow:0 6px 0 rgba(0,0,0,0.15);}
.btn:active{transform:translateY(2px);box-shadow:0 2px 0 rgba(0,0,0,0.15);}
.btn-primary{background:var(–primary);color:white;}
.btn-clear{background:#f1f5f9;color:#64748b;}
.word-instr{font-size:1.1rem;font-weight:700;color:#555;margin-bottom:20px;}
.word-card{background:linear-gradient(135deg,#f0fffe,#fff8e7);border:3px solid #e2e8f0;border-radius:22px;padding:24px 20px;margin-bottom:16px;transition:all 0.3s;}
.word-card.active{border-color:var(–secondary);box-shadow:0 0 0 4px rgba(78,205,196,0.2);}
.word-card.solved{border-color:var(–green);background:linear-gradient(135deg,#dcfce7,#f0fffe);opacity:0.85;}
.word-pic{font-size:3rem;margin-bottom:10px;display:block;animation:float 3s ease-in-out infinite;}
@keyframes float{0%,100%{transform:translateY(0)}50%{transform:translateY(-6px)}}
.word-display{display:flex;align-items:center;justify-content:center;gap:5px;margin-bottom:14px;flex-wrap:wrap;}
.lbox{width:50px;height:58px;border-radius:13px;border:3px solid #d1d5db;background:white;font-family:’Fredoka One’,cursive;font-size:2rem;display:flex;align-items:center;justify-content:center;color:var(–dark);box-shadow:0 3px 0 #d1d5db;}
.lbox.blank{border-color:var(–primary);background:#fff3ec;box-shadow:0 3px 0 #ffb38a;color:transparent;}
.lbox.cfill{border-color:var(–green);background:#dcfce7;color:var(–green);box-shadow:0 3px 0 #86efac;}
.lbox.wfill{border-color:#f87171;background:#fee2e2;color:#ef4444;animation:shake 0.3s ease;}
.word-options{display:flex;gap:8px;justify-content:center;flex-wrap:wrap;}
.lopt{width:48px;height:48px;border-radius:13px;border:3px solid #e2e8f0;background:white;font-family:’Fredoka One’,cursive;font-size:1.7rem;color:var(–dark);cursor:pointer;transition:all 0.15s;box-shadow:0 4px 0 #e2e8f0;display:flex;align-items:center;justify-content:center;}
.lopt:hover{transform:translateY(-3px);border-color:var(–primary);background:#fff3ec;box-shadow:0 7px 0 #ffb38a;color:var(–primary);}
.lopt.used{opacity:0.3;pointer-events:none;transform:none;}
.celebration{position:fixed;inset:0;background:rgba(255,248,231,0.93);display:flex;flex-direction:column;align-items:center;justify-content:center;z-index:100;animation:fadeIn 0.4s ease;}
@keyframes fadeIn{from{opacity:0;transform:scale(0.9)}to{opacity:1;transform:scale(1)}}
.cel-emoji{font-size:5.5rem;margin-bottom:14px;animation:bounce 0.8s ease infinite;}
.cel-title{font-family:’Fredoka One’,cursive;font-size:2.6rem;color:var(–primary);text-align:center;text-shadow:3px 3px 0 var(–accent);margin-bottom:8px;}
.cel-sub{font-size:1.05rem;color:#777;font-weight:600;margin-bottom:28px;text-align:center;padding:0 20px;}
.toast{position:fixed;bottom:28px;left:50%;transform:translateX(-50%) translateY(20px);background:#1e293b;color:white;padding:11px 26px;border-radius:99px;font-family:’Fredoka One’,cursive;font-size:1rem;opacity:0;transition:all 0.3s;z-index:200;pointer-events:none;}
.toast.show{opacity:1;transform:translateX(-50%) translateY(0);}
.hidden{display:none!important;}
.confetti-container{position:fixed;inset:0;pointer-events:none;z-index:99;overflow:hidden;}
.conf{position:absolute;width:11px;height:11px;border-radius:2px;animation:cfall linear forwards;}
@keyframes cfall{0%{transform:translateY(-20px) rotate(0deg);opacity:1}100%{transform:translateY(110vh) rotate(720deg);opacity:0}}
.settings-fab{position:fixed;bottom:22px;right:22px;z-index:50;background:var(–purple);border:none;border-radius:50%;width:50px;height:50px;font-size:1.2rem;cursor:pointer;box-shadow:0 4px 12px rgba(168,85,247,0.4);transition:all 0.2s;display:flex;align-items:center;justify-content:center;}
.settings-fab:hover{transform:scale(1.1);}
.mute-fab{position:fixed;bottom:22px;left:22px;z-index:50;background:var(–secondary);border:none;border-radius:50%;width:50px;height:50px;font-size:1.2rem;cursor:pointer;box-shadow:0 4px 12px rgba(78,205,196,0.4);transition:all 0.2s;display:flex;align-items:center;justify-content:center;}
.mute-fab:hover{transform:scale(1.1);}
</style>
</head>
<body>
<!– SETUP –>
<div class=”container” id=”setupContainer”>
<div class=”setup-card”>
<div class=”setup-title”>
Game Setup</div>
<div class=”setup-sub”>Music is built in!
Just record these short phrases in your voice — one for each letter. You can record as many or as few as you like!</div>
<div class=”section-heading”>
Your Voice — “This is [Letter]! Now trace the letter [Letter]!”</div>
<div class=”voice-scroll” id=”voiceRowsContainer”></div>
<div class=”rec-progress” id=”recProgress”>Recorded: <span id=”recCount”>0</span> / 26</div>
<div class=”setup-footer”>
<button class=”btn btn-primary” onclick=”startGame()”>
Start the Game!</button>
<button class=”btn-skip” onclick=”startGame()”>Skip (use computer voice)</button>
</div>
</div>
</div>
<!– GAME –>
<div class=”container hidden” id=”gameContainer”>
<header>
<h1>
Letter Adventure!</h1>
<p>Learn your letters A to Z!
</p>
<div class=”letter-progress” id=”letterProgress”>Letter 1 of 26</div>
<div class=”speaking-badge” id=”speakingBadge”><span class=”dot”></span> Listen…</div>
<div class=”stars-row” id=”starsRow”></div>
</header>
<div class=”progress-bar”><div class=”progress-fill” id=”progressFill” style=”width:0%”></div></div>
<div id=”stage1″ class=”game-card”>
<div class=”stage-label”>
Stage 1 — Find the Letter!</div>
<div class=”question-text”>Which letter is this?
</div>
<div class=”big-letter” id=”targetLetter” onclick=”replayStageName()”>A</div>
<div class=”choices-grid” id=”choicesGrid”></div>
</div>
<div id=”stage2″ class=”game-card hidden”>
<div class=”stage-label”>
Stage 2 — Trace the Letter!</div>
<div class=”trace-instr”>Trace the letter with your finger or mouse!
</div>
<div class=”canvas-wrap”>
<canvas id=”traceCanvas” width=”300″ height=”220″></canvas>
<div class=”trace-ghost” id=”traceGhost”>A</div>
</div>
<div class=”trace-actions”>
<button class=”btn btn-clear” onclick=”clearCanvas()”>
Clear</button>
<button class=”btn btn-primary” onclick=”doneTracing()”>Done!
</button>
</div>
</div>
<div id=”stage3″ class=”game-card hidden”>
<div class=”stage-label”>
Stage 3 — Fill in the Blank!</div>
<div class=”word-instr”>Put the missing letter in each word!
</div>
<div id=”wordsList”></div>
</div>
</div>
<button class=”settings-fab hidden” id=”settingsFab” title=”Back to setup” onclick=”goSetup()”>
</button>
<button class=”mute-fab hidden” id=”muteFab” title=”Mute/Unmute music” onclick=”toggleMute()”>
</button>
<div class=”celebration hidden” id=”celebration”>
<div class=”confetti-container” id=”confettiCont”></div>
<div class=”cel-emoji” id=”celEmoji”>
</div>
<div class=”cel-title” id=”celTitle”>Amazing!</div>
<div class=”cel-sub” id=”celSub”>You did it!</div>
<button class=”btn btn-primary” id=”celBtn” onclick=”nextStage()”>Next Stage
</button>
</div>
<div class=”toast” id=”toast”></div>
<script>
// ═══════════════════════════════════════
// FULL A–Z WORD DATA
// ═══════════════════════════════════════
const WORDS = {
A:[{word:’BAG’, blank:1,emoji:’
‘},{word:’CAT’,blank:1,emoji:’
‘},{word:’HAT’,blank:1,emoji:’
‘},{word:’MAP’,blank:1,emoji:’
‘},{word:’JAM’,blank:1,emoji:’
‘}],
B:[{word:’BAT’,blank:0,emoji:’
‘},{word:’BUS’,blank:0,emoji:’
‘},{word:’BED’,blank:0,emoji:’
‘},{word:’BOX’,blank:0,emoji:’
‘},{word:’BEE’,blank:0,emoji:’
‘}],
C:[{word:’CAP’,blank:0,emoji:’
‘},{word:’CUP’,blank:0,emoji:’
‘},{word:’CAR’,blank:0,emoji:’
‘},{word:’COW’,blank:0,emoji:’
‘},{word:’CAN’,blank:0,emoji:’
‘}],
D:[{word:’DOG’,blank:0,emoji:’
‘},{word:’DIG’,blank:0,emoji:’
‘},{word:’DOT’,blank:0,emoji:’
‘},{word:’DAD’,blank:0,emoji:’
‘},{word:’DEN’,blank:0,emoji:’
‘}],
E:[{word:’EGG’,blank:0,emoji:’
‘},{word:’ELF’,blank:0,emoji:’
‘},{word:’EEL’,blank:0,emoji:’
‘},{word:’END’,blank:0,emoji:’
‘},{word:’EAR’,blank:0,emoji:’
‘}],
F:[{word:’FAN’,blank:0,emoji:’
‘},{word:’FIG’,blank:0,emoji:’
‘},{word:’FOX’,blank:0,emoji:’
‘},{word:’FLY’,blank:0,emoji:’
‘},{word:’FIN’,blank:0,emoji:’
‘}],
G:[{word:’GUM’,blank:0,emoji:’
‘},{word:’GAS’,blank:0,emoji:’
‘},{word:’GEM’,blank:0,emoji:’
‘},{word:’GOT’,blank:0,emoji:’
‘},{word:’GUN’,blank:0,emoji:’
‘}],
H:[{word:’HAM’,blank:0,emoji:’
‘},{word:’HEN’,blank:0,emoji:’
‘},{word:’HIT’,blank:0,emoji:’
‘},{word:’HOG’,blank:0,emoji:’
‘},{word:’HUG’,blank:0,emoji:’
‘}],
I:[{word:’INN’,blank:0,emoji:’
‘},{word:’INK’,blank:0,emoji:’
‘},{word:’ICE’,blank:0,emoji:’
‘},{word:’ILL’,blank:0,emoji:’
‘},{word:’IVY’,blank:0,emoji:’
‘}],
J:[{word:’JAR’,blank:0,emoji:’
‘},{word:’JOG’,blank:0,emoji:’
‘},{word:’JET’,blank:0,emoji:’
‘},{word:’JOY’,blank:0,emoji:’
‘},{word:’JAB’,blank:0,emoji:’
‘}],
K:[{word:’KEY’,blank:0,emoji:’
‘},{word:’KID’,blank:0,emoji:’
‘},{word:’KIT’,blank:0,emoji:’
‘},{word:’KEG’,blank:0,emoji:’
‘},{word:’KOI’,blank:0,emoji:’
‘}],
L:[{word:’LEG’,blank:0,emoji:’
‘},{word:’LIP’,blank:0,emoji:’
‘},{word:’LOG’,blank:0,emoji:’
‘},{word:’LAP’,blank:0,emoji:’
‘},{word:’LID’,blank:0,emoji:’
‘}],
M:[{word:’MOP’,blank:0,emoji:’
‘},{word:’MUD’,blank:0,emoji:’
‘},{word:’MAN’,blank:0,emoji:’
‘},{word:’MIX’,blank:0,emoji:’
‘},{word:’MOO’,blank:0,emoji:’
‘}],
N:[{word:’NET’,blank:0,emoji:’
‘},{word:’NUT’,blank:0,emoji:’
‘},{word:’NAP’,blank:0,emoji:’
‘},{word:’NOD’,blank:0,emoji:’
‘},{word:’NIP’,blank:0,emoji:’
‘}],
O:[{word:’OAK’,blank:0,emoji:’
‘},{word:’OWL’,blank:0,emoji:’
‘},{word:’OAR’,blank:0,emoji:’
‘},{word:’ODD’,blank:0,emoji:’
‘},{word:’ORE’,blank:0,emoji:’
‘}],
P:[{word:’PIG’,blank:0,emoji:’
‘},{word:’PAN’,blank:0,emoji:’
‘},{word:’PET’,blank:0,emoji:’
‘},{word:’POT’,blank:0,emoji:’
‘},{word:’PEA’,blank:0,emoji:’
‘}],
Q:[{word:’QUA’,blank:0,emoji:’
‘},{word:’QIT’,blank:0,emoji:’
‘},{word:’QOP’,blank:0,emoji:’
‘},{word:’QEF’,blank:0,emoji:’
‘},{word:’QUB’,blank:0,emoji:’
‘}],
R:[{word:’RAT’,blank:0,emoji:’
‘},{word:’RUG’,blank:0,emoji:’
‘},{word:’ROD’,blank:0,emoji:’
‘},{word:’RIB’,blank:0,emoji:’
‘},{word:’RUN’,blank:0,emoji:’
‘}],
S:[{word:’SUN’,blank:0,emoji:’
‘},{word:’SAP’,blank:0,emoji:’
‘},{word:’SIT’,blank:0,emoji:’
‘},{word:’SOB’,blank:0,emoji:’
‘},{word:’SIP’,blank:0,emoji:’
‘}],
T:[{word:’TAP’,blank:0,emoji:’
‘},{word:’TIN’,blank:0,emoji:’
‘},{word:’TOY’,blank:0,emoji:’
‘},{word:’TUB’,blank:0,emoji:’
‘},{word:’TAN’,blank:0,emoji:’
‘}],
U:[{word:’UMP’,blank:0,emoji:’
‘},{word:’URN’,blank:0,emoji:’
‘},{word:’USE’,blank:0,emoji:’
‘},{word:’UDO’,blank:0,emoji:’
‘},{word:’UTE’,blank:0,emoji:’
‘}],
V:[{word:’VAN’,blank:0,emoji:’
‘},{word:’VET’,blank:0,emoji:’
‘},{word:’VAT’,blank:0,emoji:’
‘},{word:’VIM’,blank:0,emoji:’
‘},{word:’VOW’,blank:0,emoji:’
‘}],
W:[{word:’WEB’,blank:0,emoji:’
‘},{word:’WIG’,blank:0,emoji:’
‘},{word:’WAX’,blank:0,emoji:’
‘},{word:’WIN’,blank:0,emoji:’
‘},{word:’WOK’,blank:0,emoji:’
‘}],
X:[{word:’XIS’,blank:0,emoji:’
‘},{word:’XEN’,blank:0,emoji:’
‘},{word:’XIT’,blank:0,emoji:’
‘},{word:’XAP’,blank:0,emoji:’
‘},{word:’XIB’,blank:0,emoji:’
‘}],
Y:[{word:’YAK’,blank:0,emoji:’
‘},{word:’YAM’,blank:0,emoji:’
‘},{word:’YET’,blank:0,emoji:’
‘},{word:’YEW’,blank:0,emoji:’
‘},{word:’YIP’,blank:0,emoji:’
‘}],
Z:[{word:’ZAP’,blank:0,emoji:’
‘},{word:’ZEN’,blank:0,emoji:’
‘},{word:’ZIP’,blank:0,emoji:’
‘},{word:’ZIT’,blank:0,emoji:’
‘},{word:’ZOO’,blank:0,emoji:’
‘}],
};
const GAME_LETTERS=’ABCDEFGHIJKLMNOPQRSTUVWXYZ’.split(”);
const ALL=GAME_LETTERS;
// ═══════════════════════════════════════
// VOICE RECORDING — 26 phrases
// ═══════════════════════════════════════
const voiceRecs={};
let activeRecorder=null;
let recDoneCount=0;
function buildVoiceRows(){
const cont=document.getElementById(‘voiceRowsContainer’);
cont.innerHTML=”;
GAME_LETTERS.forEach(letter=>{
const id=’found_’+letter;
const row=document.createElement(‘div’);
row.className=’voice-row’;
const lbl=document.createElement(‘div’);
lbl.className=’voice-label’;
lbl.innerHTML=`
<b>Letter ${letter}</b><small>”This is ${letter}! Now trace the letter ${letter}!”</small>`;
const acts=document.createElement(‘div’);
acts.style.cssText=’display:flex;gap:6px;align-items:center;flex-wrap:wrap;flex-shrink:0′;
const st=document.createElement(‘span’);
st.className=’vstatus miss’; st.id=’vst_’+id; st.textContent=’Not yet’;
const startBtn=document.createElement(‘button’);
startBtn.className=’rbtn rstart’; startBtn.textContent=’
Record’;
const stopBtn=document.createElement(‘button’);
stopBtn.className=’rbtn rstop hidden’; stopBtn.textContent=’
Stop’;
stopBtn.onclick=()=>{if(activeRecorder&&activeRecorder.state!==’inactive’)activeRecorder.stop();};
const playBtn=document.createElement(‘button’);
playBtn.className=’rbtn rplay hidden’; playBtn.id=’vplay_’+id; playBtn.textContent=’
‘;
playBtn.onclick=()=>new Audio(voiceRecs[id].url).play();
startBtn.onclick=()=>startVoiceRec(id,startBtn,stopBtn,playBtn,st);
acts.append(st,startBtn,stopBtn,playBtn);
row.append(lbl,acts);
cont.appendChild(row);
});
}
async function startVoiceRec(id,startBtn,stopBtn,playBtn,stEl){
try{
const stream=await navigator.mediaDevices.getUserMedia({audio:true});
const rec=new MediaRecorder(stream);
const chunks=[];
rec.ondataavailable=e=>chunks.push(e.data);
rec.onstop=()=>{
stream.getTracks().forEach(t=>t.stop());
const wasNew=!voiceRecs[id];
const blob=new Blob(chunks,{type:’audio/webm’});
voiceRecs[id]={url:URL.createObjectURL(blob)};
stEl.textContent=’✓ Done’; stEl.className=’vstatus done’;
startBtn.textContent=’
Re-record’; startBtn.className=’rbtn rstart’;
stopBtn.classList.add(‘hidden’); playBtn.classList.remove(‘hidden’);
startBtn.onclick=()=>startVoiceRec(id,startBtn,stopBtn,playBtn,stEl);
if(wasNew){recDoneCount++;document.getElementById(‘recCount’).textContent=recDoneCount;}
};
rec.start(); activeRecorder=rec;
startBtn.textContent=’● Rec…’; startBtn.className=’rbtn rstop’;
stopBtn.classList.remove(‘hidden’);
}catch(e){alert(‘Please allow microphone access and try again.’);}
}
function startGame(){
document.getElementById(‘setupContainer’).classList.add(‘hidden’);
document.getElementById(‘gameContainer’).classList.remove(‘hidden’);
document.getElementById(‘settingsFab’).classList.remove(‘hidden’);
document.getElementById(‘muteFab’).classList.remove(‘hidden’);
getAC();
playIntroJingle(()=>{startBg();loadS1();});
}
function goSetup(){
stopBg();
document.getElementById(‘gameContainer’).classList.add(‘hidden’);
document.getElementById(‘setupContainer’).classList.remove(‘hidden’);
document.getElementById(‘settingsFab’).classList.add(‘hidden’);
document.getElementById(‘muteFab’).classList.add(‘hidden’);
}
// ═══════════════════════════════════════
// WEB AUDIO ENGINE
// ═══════════════════════════════════════
let AC=null; let muted=false;
function getAC(){if(!AC)AC=new(window.AudioContext||window.webkitAudioContext)();return AC;}
function tone(freq,t,dur,vol=0.18,type=’sine’,ac){
const c=ac||getAC();
const o=c.createOscillator(),g=c.createGain();
o.connect(g);g.connect(c.destination);
o.type=type;o.frequency.setValueAtTime(freq,t);
g.gain.setValueAtTime(0,t);g.gain.linearRampToValueAtTime(vol,t+0.01);
g.gain.exponentialRampToValueAtTime(0.001,t+dur);
o.start(t);o.stop(t+dur+0.05);
}
function chord(freqs,t,dur,vol=0.08,ac){freqs.forEach(f=>tone(f,t,dur,vol,’sine’,ac||getAC()));}
function playIntroJingle(onEnd){
if(muted){if(onEnd)setTimeout(onEnd,100);return;}
const c=getAC(),now=c.currentTime+0.1;
[[523.25,0],[587.33,.24],[659.25,.48],[783.99,.72],[880,.96],[1046.5,1.2,.45],[783.99,1.68,.22],[659.25,1.92,.22],[783.99,2.16,.45],[880,2.64,.22],[1046.5,2.88,.6]].forEach(([f,t,d=0.22])=>tone(f,now+t,d,0.22,’triangle’,c));
[[261.63,329.63,392],[0,0.9],[[293.66,369.99,440],0.96,0.9],[[261.63,329.63,392],1.92,0.9],[[261.63,329.63,392],2.88,1]].forEach(v=>{if(Array.isArray(v[0]))chord(v[0],now+v[1],v[2],0.07,c);});
chord([261.63,329.63,392],now+0,0.9,0.07,c);chord([293.66,369.99,440],now+0.96,0.9,0.07,c);chord([261.63,329.63,392],now+1.92,0.9,0.07,c);chord([261.63,329.63,392],now+2.88,1,0.07,c);
[1318.5,1567.98,2093].forEach((f,i)=>tone(f,now+0.5+i*0.3,0.4,0.05,’sine’,c));
setTimeout(()=>{if(onEnd)onEnd();},3900);
}
let bgTimer=null,bgOn=false;
function playBgLoop(){
if(!bgOn||muted)return;
const c=getAC(),now=c.currentTime+0.05,beat=60/100,v=0.055;
[[523.25,0],[659.25,1],[783.99,2],[659.25,3],[698.46,4],[880,5],[1046.5,6],[880,7],[783.99,8],[987.77,9],[1174.7,10],[987.77,11],[523.25,12],[659.25,13],[783.99,14],[783.99,15]].forEach(([f,b])=>tone(f,now+b*beat,beat*0.7,v,’triangle’,c));
[0,2,4,6,8,10,12,14].forEach(b=>tone(130.81,now+b*beat,beat*0.4,0.03,’sine’,c));
bgTimer=setTimeout(()=>{if(bgOn&&!muted)playBgLoop();},16*beat*1000-80);
}
function startBg(){if(bgOn)return;bgOn=true;playBgLoop();}
function stopBg(){bgOn=false;if(bgTimer){clearTimeout(bgTimer);bgTimer=null;}}
function toggleMute(){
muted=!muted;
document.getElementById(‘muteFab’).textContent=muted?’
‘:’
‘;
if(!muted&&bgOn)playBgLoop();
if(muted&&bgTimer){clearTimeout(bgTimer);bgTimer=null;}
}
function playWinFanfare(onEnd){
if(muted){if(onEnd)setTimeout(onEnd,100);return;}
const c=getAC(),now=c.currentTime+0.1;
[[523.25,0,.15],[659.25,.16,.15],[783.99,.32,.15],[1046.5,.48,.5],[880,1,.15],[1046.5,1.16,.15],[1174.7,1.32,.15],[1318.5,1.48,.7]].forEach(([f,t,d])=>tone(f,now+t,d,0.25,’triangle’,c));
chord([261.63,329.63,392,523.25],now+0.48,0.45,0.09,c);chord([329.63,415.30,523.25,659.25],now+1.48,0.65,0.09,c);
[1046.5,1318.5,1567.98,2093].forEach((f,i)=>tone(f,now+0.5+i*0.18,0.3,0.05,’sine’,c));
setTimeout(()=>{if(onEnd)onEnd();},2400);
}
function playCorrectSFX(){if(muted)return;const c=getAC(),n=c.currentTime;tone(880,n,.12,.14,’sine’,c);tone(1046.5,n+.13,.12,.14,’sine’,c);tone(1318.5,n+.26,.2,.14,’sine’,c);}
function playWrongSFX(){if(muted)return;const c=getAC(),n=c.currentTime;tone(220,n,.15,.11,’sawtooth’,c);tone(196,n+.16,.2,.09,’sawtooth’,c);}
// ═══════════════════════════════════════
// TTS VOICE
// ═══════════════════════════════════════
const TTS_MAP={
correct:”Great job! That’s correct!”,
wrong:”Oops! Try again — you can do it!”,
trace_done:”Awesome tracing! Now let’s fill in some words!”,
words_done:”Amazing work! You finished all the words!”,
all_done:”You are a superstar! You finished all 26 letters! I am so proud of you!”,
};
GAME_LETTERS.forEach(l=>{
TTS_MAP[‘ask_’+l]=`Which letter is ${l}? Can you find it?`;
TTS_MAP[‘found_’+l]=`This is ${l}! Now trace the letter ${l}!`;
});
// Pick best female voice
let femaleVoice=null;
function pickFemaleVoice(){
const voices=window.speechSynthesis.getVoices();
if(!voices.length)return;
const fem=[‘zira’,’susan’,’kate’,’karen’,’moira’,’samantha’,’victoria’,’fiona’,’tessa’,’veena’,’allison’,’ava’,’nova’,’aria’,’jenny’,’sonia’,’libby’,’hazel’,’natasha’,’female’,’woman’];
let v=voices.find(v=>v.lang.startsWith(‘en’)&&fem.some(k=>v.name.toLowerCase().includes(k)));
if(!v)v=voices.find(v=>v.lang.startsWith(‘en-GB’));
if(!v)v=voices.find(v=>v.lang.startsWith(‘en’));
if(v)femaleVoice=v;
}
if(window.speechSynthesis){pickFemaleVoice();window.speechSynthesis.onvoiceschanged=pickFemaleVoice;}
function playVoice(id,onEnd){
if(voiceRecs[id]){
showBadge(true);
const a=new Audio(voiceRecs[id].url);
a.play().catch(()=>{});
a.onended=()=>{showBadge(false);if(onEnd)onEnd();};
return;
}
const text=TTS_MAP[id];
if(!text||!window.speechSynthesis){if(onEnd)onEnd();return;}
window.speechSynthesis.cancel();
const u=new SpeechSynthesisUtterance(text);
if(femaleVoice)u.voice=femaleVoice;
u.rate=0.9;u.pitch=1.2;u.volume=1;
showBadge(true);
u.onend=()=>{showBadge(false);if(onEnd)onEnd();};
window.speechSynthesis.speak(u);
}
function showBadge(v){document.getElementById(‘speakingBadge’).classList.toggle(‘show’,v);}
// ═══════════════════════════════════════
// GAME STATE
// ═══════════════════════════════════════
let li=0,stars=0,wOk=0,drawing=false,lx=0,ly=0;
const canvas=document.getElementById(‘traceCanvas’);
const ctx=canvas.getContext(‘2d’);
function curL(){return GAME_LETTERS[li];}
function renderStars(){
const total=GAME_LETTERS.length*3;
const row=document.getElementById(‘starsRow’);row.innerHTML=”;
// Show stars in groups of 26 to keep it compact — just show earned vs total as mini dots
for(let i=0;i<total;i++){
const s=document.createElement(‘span’);
s.className=’star’+(i<stars?’ earned’:”);
s.textContent=i<stars?’
‘:’·’;
s.style.fontSize=i<stars?’1rem’:’0.8rem’;
row.appendChild(s);
}
}
function earnStar(){
stars++;
renderStars();
document.getElementById(‘progressFill’).style.width=(stars/(GAME_LETTERS.length*3)*100)+’%’;
}
function updateLetterProgress(){
document.getElementById(‘letterProgress’).textContent=`Letter ${li+1} of 26 — ${curL()}`;
}
// ── Stage 1 ──
function loadS1(){
showSec(‘stage1’);
updateLetterProgress();
const letter=curL();
document.getElementById(‘targetLetter’).textContent=letter;
const wrong=ALL.filter(l=>l!==letter).sort(()=>Math.random()-0.5).slice(0,5);
const choices=[letter,…wrong].sort(()=>Math.random()-0.5);
const grid=document.getElementById(‘choicesGrid’);grid.innerHTML=”;
choices.forEach(ch=>{
const b=document.createElement(‘button’);b.className=’choice-btn’;b.textContent=ch;
b.onclick=()=>checkChoice(b,ch);grid.appendChild(b);
});
setTimeout(()=>playVoice(‘ask_’+letter),600);
}
function replayStageName(){playVoice(‘ask_’+curL());}
function checkChoice(btn,chosen){
if(chosen===curL()){
btn.classList.add(‘correct’);playCorrectSFX();playVoice(‘correct’);showToast(‘Great job!
‘);
setTimeout(()=>celebrate(‘
‘,`You found ${curL()}!`,”Now let’s trace it!”,()=>{earnStar();loadS2();}),700);
}else{
btn.classList.add(‘wrong’);playWrongSFX();playVoice(‘wrong’);showToast(‘Try again!
‘);
setTimeout(()=>btn.classList.remove(‘wrong’),500);
}
}
// ── Stage 2 ──
function loadS2(){
showSec(‘stage2’);
document.getElementById(‘traceGhost’).textContent=curL();
clearCanvas();setupCanvas();
setTimeout(()=>playVoice(‘found_’+curL()),400);
}
function setupCanvas(){
ctx.strokeStyle=’#ff6b35′;ctx.lineWidth=6;ctx.lineCap=’round’;ctx.lineJoin=’round’;
canvas.onmousedown=e=>{drawing=true;[lx,ly]=cpos(e);};
canvas.onmousemove=e=>{if(!drawing)return;cdraw(e);};
canvas.onmouseup=()=>drawing=false;canvas.onmouseleave=()=>drawing=false;
canvas.ontouchstart=e=>{e.preventDefault();drawing=true;[lx,ly]=cpos(e);};
canvas.ontouchmove=e=>{e.preventDefault();if(!drawing)return;cdraw(e);};
canvas.ontouchend=()=>drawing=false;
}
function cpos(e){const r=canvas.getBoundingClientRect(),s=e.touches?e.touches[0]:e;return[s.clientX-r.left,s.clientY-r.top];}
function cdraw(e){const[x,y]=cpos(e);ctx.beginPath();ctx.moveTo(lx,ly);ctx.lineTo(x,y);ctx.stroke();[lx,ly]=[x,y];}
function clearCanvas(){ctx.clearRect(0,0,canvas.width,canvas.height);}
function doneTracing(){
const d=ctx.getImageData(0,0,canvas.width,canvas.height).data;
if(!d.some((v,i)=>i%4===3&&v>0)){showToast(‘Try tracing the letter first!
‘);return;}
playCorrectSFX();playVoice(‘trace_done’);
celebrate(‘
‘,’Awesome Tracing!’,’Now fill in the words!’,()=>{earnStar();loadS3();});
}
// ── Stage 3 ──
function loadS3(){
showSec(‘stage3’);wOk=0;
const letter=curL(),wlist=WORDS[letter]||WORDS[‘A’];
const cont=document.getElementById(‘wordsList’);cont.innerHTML=”;
wlist.forEach((w,wi)=>{
const card=document.createElement(‘div’);
card.className=’word-card’+(wi===0?’ active’:”);card.id=’wc_’+wi;
const em=document.createElement(‘span’);em.className=’word-pic’;em.textContent=w.emoji;card.appendChild(em);
const wd=document.createElement(‘div’);wd.className=’word-display’;
for(let i=0;i<w.word.length;i++){
const b=document.createElement(‘div’);
b.className=’lbox’+(i===w.blank?’ blank’:”);
if(i===w.blank){b.id=’bl_’+wi;b.textContent=’_’;}else b.textContent=w.word[i];
wd.appendChild(b);
}
card.appendChild(wd);
const opts=document.createElement(‘div’);opts.className=’word-options’;opts.id=’op_’+wi;
const cor=w.word[w.blank];
const wrg=ALL.filter(l=>l!==cor).sort(()=>Math.random()-0.5).slice(0,3);
[cor,…wrg].sort(()=>Math.random()-0.5).forEach(opt=>{
const lb=document.createElement(‘button’);lb.className=’lopt’;lb.textContent=opt;
lb.onclick=()=>checkWord(wi,opt,lb,w);opts.appendChild(lb);
});
card.appendChild(opts);cont.appendChild(card);
});
}
function checkWord(wi,chosen,btn,word){
const cor=word.word[word.blank],bl=document.getElementById(‘bl_’+wi);
bl.textContent=chosen;
if(chosen!==cor){
bl.classList.add(‘wfill’);playWrongSFX();playVoice(‘wrong’);showToast(‘Oops! Try another letter!
‘);
setTimeout(()=>{bl.classList.remove(‘wfill’);bl.textContent=’_’;},600);
return;
}
bl.classList.add(‘cfill’);
document.querySelectorAll(‘#op_’+wi+’ .lopt’).forEach(b=>b.classList.add(‘used’));
document.getElementById(‘wc_’+wi).classList.replace(‘active’,’solved’);
wOk++;playCorrectSFX();playVoice(‘correct’);showToast(‘Correct!
‘);
const wlist=WORDS[curL()]||WORDS[‘A’];
if(wOk===wlist.length){
setTimeout(()=>{
earnStar();
if(li+1<GAME_LETTERS.length){
playWinFanfare(()=>{
playVoice(‘words_done’);
celebrate(‘
‘,’Amazing Work!’,`Ready for letter ${GAME_LETTERS[li+1]}?`,()=>{li++;loadS1();},’Next Letter
‘);
});
}else{
stopBg();
playWinFanfare(()=>{
playVoice(‘all_done’);
celebrate(‘
‘,’You Are a STAR!
‘,’You finished ALL 26 letters! A to Z!’,()=>{li=0;stars=0;renderStars();document.getElementById(‘progressFill’).style.width=’0%’;startBg();loadS1();},’Play Again
‘);
});
}
},700);
}else{
const nxt=document.getElementById(‘wc_’+(wi+1));
if(nxt)setTimeout(()=>nxt.classList.add(‘active’),300);
}
}
// ── Celebrate ──
let celCb=null;
function celebrate(emoji,title,sub,cb,btnTxt=’Next Stage
‘){
celCb=cb;
document.getElementById(‘celEmoji’).textContent=emoji;
document.getElementById(‘celTitle’).textContent=title;
document.getElementById(‘celSub’).textContent=sub;
document.getElementById(‘celBtn’).textContent=btnTxt;
document.getElementById(‘celebration’).classList.remove(‘hidden’);
spawnConfetti();
}
function nextStage(){
document.getElementById(‘celebration’).classList.add(‘hidden’);
document.getElementById(‘confettiCont’).innerHTML=”;
if(celCb)celCb();
}
function spawnConfetti(){
const c=document.getElementById(‘confettiCont’);c.innerHTML=”;
const cols=[‘#ff6b35′,’#4ecdc4′,’#ffe66d’,’#a855f7′,’#f472b6′,’#22c55e’];
for(let i=0;i<70;i++){
const p=document.createElement(‘div’);p.className=’conf’;
p.style.left=Math.random()*100+’vw’;p.style.top=’-20px’;
p.style.background=cols[Math.floor(Math.random()*cols.length)];
p.style.animationDuration=(1.5+Math.random()*2)+’s’;
p.style.animationDelay=(Math.random()*0.8)+’s’;
p.style.transform=`rotate(${Math.random()*360}deg)`;
c.appendChild(p);
}
}
let tTimer;
function showToast(msg){const t=document.getElementById(‘toast’);t.textContent=msg;t.classList.add(‘show’);clearTimeout(tTimer);tTimer=setTimeout(()=>t.classList.remove(‘show’),1600);}
function showSec(id){[‘stage1′,’stage2′,’stage3’].forEach(s=>document.getElementById(s).classList.toggle(‘hidden’,s!==id));}
buildVoiceRows();
renderStars();
</script>
</body>
</html>