Vocabulary · Challenge · Glory
◈ SELECT WORD LENGTH ◈
9
LETTERS PER WORD
51015
/* ── WORD BANK loaded from words.csv ── */ let WORDS = {}; // populated by loadWordBank() async function loadWordBank() { try { const res = await fetch('words.csv'); if (!res.ok) throw new Error('CSV not found'); const text = await res.text(); const lines = text.trim().split('\n').slice(1); // skip header WORDS = {}; lines.forEach(line => { // parse CSV properly (handle quoted commas) const cols = []; let cur = '', inQ = false; for (let i = 0; i < line.length; i++) { const ch = line[i]; if (ch === '"') { inQ = !inQ; } else if (ch === ',' && !inQ) { cols.push(cur.trim()); cur = ''; } else { cur += ch; } } cols.push(cur.trim()); const len = parseInt(cols[0]); if (!len || !cols[1]) return; if (!WORDS[len]) WORDS[len] = []; WORDS[len].push([cols[1], cols[2], cols[3], cols[4], cols[5]]); }); return true; } catch(e) { $('load-err').textContent = 'Could not load words.csv — make sure it is in the same folder as index.html.'; $('load-err').style.display = 'block'; return false; } } const AC=new(window.AudioContext||window.webkitAudioContext)(); function tone(f,t,d,v=.13){const o=AC.createOscillator(),g=AC.createGain();o.connect(g);g.connect(AC.destination);o.type=t;o.frequency.setValueAtTime(f,AC.currentTime);g.gain.setValueAtTime(v,AC.currentTime);g.gain.exponentialRampToValueAtTime(.001,AC.currentTime+d);o.start();o.stop(AC.currentTime+d);} function sfxCorrect(){tone(523,'sine',.12,.12);setTimeout(()=>tone(659,'sine',.12,.12),100);setTimeout(()=>tone(784,'sine',.2,.14),200);setTimeout(()=>tone(1046,'sine',.28,.12),320);} function sfxWrong(){tone(220,'sawtooth',.08,.1);setTimeout(()=>tone(180,'sawtooth',.18,.1),80);} function sfxClick(){tone(880,'sine',.06,.05);} function sfxStart(){[261,329,392,523].forEach((f,i)=>setTimeout(()=>tone(f,'triangle',.18,.1),i*80));} function sfxFinish(s){const n=s>=15?[523,659,784,1046,1318]:s>=10?[392,494,587]:[220,196];n.forEach((f,i)=>setTimeout(()=>tone(f,'sine',.28,.12),i*120));} function sfxTick(){tone(1200,'sine',.04,.04);} const cv=document.getElementById('fx'),cx=cv.getContext('2d'); let pts=[]; function resize(){cv.width=innerWidth;cv.height=innerHeight;} window.addEventListener('resize',resize);resize(); function burst(x,y,col,n=22){for(let i=0;ip.life>0);pts.forEach(p=>{p.x+=p.vx;p.y+=p.vy;p.vy+=.12;p.life-=p.decay;p.vx*=.97;cx.save();cx.globalAlpha=p.life;cx.fillStyle=p.col;cx.shadowColor=p.col;cx.shadowBlur=6;cx.beginPath();cx.arc(p.x,p.y,p.size*p.life,0,Math.PI*2);cx.fill();cx.restore();});requestAnimationFrame(drawPts);} drawPts(); let score=0,qIndex=0,wordLen=9,answered=false,deck=[],history=[]; let timerStart=0,timerElapsed=0,timerInterval=null; function fmtTime(ms){ const tot=Math.floor(ms/100); const tenths=tot%10; const secs=Math.floor(tot/10)%60; const mins=Math.floor(tot/600); return String(mins).padStart(2,'0')+':'+String(secs).padStart(2,'0')+'.'+tenths; } function tickTimer(){ timerElapsed=Date.now()-timerStart; $('timer-disp').textContent=fmtTime(timerElapsed); } function startTimer(){clearInterval(timerInterval);timerStart=Date.now();timerElapsed=0;timerInterval=setInterval(tickTimer,100);} function stopTimer(){clearInterval(timerInterval);timerElapsed=Date.now()-timerStart;$('timer-disp').textContent=fmtTime(timerElapsed);} const $=id=>document.getElementById(id); $('len-slider').oninput=function(){$('len-num').textContent=this.value;sfxTick();}; function show(id){['setup','game','results'].forEach(s=>$(s).classList.toggle('hidden',s!==id));} function confirmAbort(){ document.getElementById('abort-body').innerHTML=`Are you sure you want to end the game early?
Your score so far: ${score} / ${qIndex}`; document.getElementById('abort-modal').classList.remove('hidden'); } function closeAbort(){document.getElementById('abort-modal').classList.add('hidden');} function doAbort(){ closeAbort(); stopTimer(); showResults(true); } function showSetup(){show('setup');} /* ── SPEAK ── */ let currentWord=''; function speakWord(){ if(!currentWord)return; window.speechSynthesis.cancel(); const u=new SpeechSynthesisUtterance(currentWord.toLowerCase()); u.lang='en-GB';u.rate=0.85;u.pitch=1; // try to pick a British voice const voices=window.speechSynthesis.getVoices(); const brit=voices.find(v=>v.lang==='en-GB')||voices.find(v=>v.lang.startsWith('en-GB'))||null; if(brit)u.voice=brit; const btn=$('speak-btn'); btn.classList.add('speaking'); u.onend=()=>btn.classList.remove('speaking'); u.onerror=()=>btn.classList.remove('speaking'); window.speechSynthesis.speak(u); } // pre-load voices (some browsers need this trigger) if(window.speechSynthesis.onvoiceschanged!==undefined)window.speechSynthesis.onvoiceschanged=()=>window.speechSynthesis.getVoices(); function buildDeck(len){ const src=WORDS[len]||WORDS[9]; const pool=[...src]; for(let i=pool.length-1;i>0;i--){const j=Math.floor(Math.random()*(i+1));[pool[i],pool[j]]=[pool[j],pool[i]];} const out=[];while(out.length<20)out.push(...pool); return out.slice(0,20).map(entry=>{ const[word,correct,...wrongs]=entry; const opts=[correct,...wrongs]; for(let i=opts.length-1;i>0;i--){const j=Math.floor(Math.random()*(i+1));[opts[i],opts[j]]=[opts[j],opts[i]];} return{word,options:opts,correct:opts.indexOf(correct)}; }); } async function startGame(){ if(AC.state==='suspended')AC.resume(); $('load-err').style.display='none'; const ok = await loadWordBank(); if(!ok) return; wordLen=parseInt($('len-slider').value); score=0;qIndex=0;answered=false;history=[]; deck=buildDeck(wordLen); $('len-disp').textContent=wordLen; updateHUD();show('game');sfxStart();startTimer();renderQ(); } function renderQ(){ answered=false; const q=deck[qIndex]; currentWord=q.word; $('feedback').textContent='';$('feedback').className='feedback'; const h=$('hint');h.textContent='';h.classList.add('hidden'); $('btn-next').classList.add('hidden'); $('speak-btn').classList.remove('speaking'); updateHUD(); const wrap=$('word-tiles');wrap.innerHTML=''; q.word.split('').forEach((ch,i)=>{ const b=document.createElement('div'); b.className='tile';b.textContent=ch;b.style.animationDelay=(i*.05)+'s'; wrap.appendChild(b); }); $('word-badge').textContent=q.word.length+'-letter word'; const keys=['A','B','C','D']; $('q-body').innerHTML='
◈ CHOOSE THE CORRECT DEFINITION
'+ q.options.map((opt,i)=>`
${keys[i]}
${opt}
`).join(''); // auto-speak after tiles animate in setTimeout(speakWord, 600); } function answer(chosen){ if(answered)return;answered=true; if(AC.state==='suspended')AC.resume(); const q=deck[qIndex];const correct=q.correct; document.querySelectorAll('.opt').forEach(o=>o.classList.add('locked')); const el=document.getElementById('opt'+chosen); const rect=el.getBoundingClientRect(); const bx=rect.left+rect.width/2,by=rect.top+rect.height/2; const isCorrect=(chosen===correct); history.push({word:q.word,correct:isCorrect,definition:q.options[correct]}); if(isCorrect){ el.classList.add('correct');$('feedback').textContent='CORRECT!';$('feedback').className='feedback c'; score++;sfxCorrect();burst(bx,by,'#00FF88',28);burst(bx,by,'#FFD700',16); }else{ el.classList.add('wrong');document.getElementById('opt'+correct).classList.add('correct'); $('feedback').textContent='INCORRECT';$('feedback').className='feedback w';sfxWrong();burst(bx,by,'#FF3355',20); const h=$('hint');h.textContent='Answer: '+q.options[correct];h.classList.remove('hidden'); } $('score-disp').textContent=score;$('btn-next').classList.remove('hidden'); } function nextQ(){ sfxClick();qIndex++; if(qIndex>=20){showResults();return;} $('prog').style.width=(qIndex/20*100)+'%';renderQ(); } function updateHUD(){ $('q-num').textContent=(qIndex+1)+'/20';$('score-disp').textContent=score; $('prog').style.width=(qIndex/20*100)+'%'; } function showResults(aborted=false){ stopTimer(); const durStr=fmtTime(timerElapsed); const total=aborted?qIndex:20; show('results');$('final-score').textContent=score;sfxFinish(score); const tiers=[{min:0,stars:1,txt:'KEEP PRACTISING'},{min:5,stars:2,txt:'NOT BAD, CADET'},{min:10,stars:3,txt:'WELL PLAYED!'},{min:15,stars:4,txt:'IMPRESSIVE!'},{min:18,stars:5,txt:'WORDMASTER!'}]; const emojis=['💀','😤','⚡','🔥','🏆']; const r=[...tiers].reverse().find(t=>score>=t.min)||tiers[0]; const ei=tiers.indexOf(r); $('trophy').innerHTML=aborted?'⏹':emojis[ei]; $('res-msg').textContent=aborted?'GAME ABORTED — '+score+'/'+total+' ANSWERED':r.txt; $('duration-disp').textContent=durStr; $('stars-row').innerHTML=aborted?'':Array(5).fill(0).map((_,i)=>``).join(''); const rows=history.map((h,i)=>`${i+1}${h.word}${h.definition}${h.correct?'✓':'✗'}`).join(''); $('breakdown-wrap').innerHTML=`${rows}
#WORDCORRECT DEFINITIONRESULT

TIME: ${durStr} | SCORE: ${score}/${total} | LENGTH: ${wordLen}${aborted?' | ABORTED':''}

`; if(!aborted&&score>=10){ const cols=['#FFD700','#00FFFF','#FF69B4','#00FF88','#FF8C00']; for(let i=0;i<10;i++)setTimeout(()=>burst(Math.random()*innerWidth,Math.random()*innerHeight*.5,cols[Math.floor(Math.random()*cols.length)],15),i*140); } }