naider 1.24.2 → 1.24.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/naide.js CHANGED
@@ -8,7 +8,7 @@ import { spawn } from 'child_process';
8
8
  import { createRequire } from 'module';
9
9
  const _require = createRequire(import.meta.url);
10
10
 
11
- const NAIDE_VERSION = '1.24.2';
11
+ const NAIDE_VERSION = '1.24.3';
12
12
 
13
13
  function crashReport(err, context = {}) {
14
14
  const info = [
package/mcp/server.js CHANGED
@@ -192,7 +192,7 @@ async function handleMessage(msg) {
192
192
  respond(msg.id, {
193
193
  protocolVersion: '2024-11-05',
194
194
  capabilities: { tools: {} },
195
- serverInfo: { name: 'naide-mcp', version: '1.24.2' }
195
+ serverInfo: { name: 'naide-mcp', version: '1.24.3' }
196
196
  });
197
197
  break;
198
198
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "naider",
3
- "version": "1.24.2",
3
+ "version": "1.24.3",
4
4
  "description": "NAIDE - Simpler than Python, compiles to 15 targets. AI-specialized language with 35+ built-in functions, syntax sugar (unless/until/repeat/swap/is/isnt), and 47 features. Targets: Node.js, Python, TypeScript, C, C++, Java, Go, Rust, PHP, Ruby, Kotlin, Swift, Dart, C#, Bun.",
5
5
  "main": "src/index.js",
6
6
  "exports": {
package/src/gen.js CHANGED
@@ -1781,370 +1781,403 @@ function migrationFile(entities, relationships, m2mRels) {
1781
1781
 
1782
1782
  // ── Code Composer ───────────────────────────────────────────
1783
1783
 
1784
- // ── Game HTML Generators ──────────────────────────────────
1785
-
1786
- const GAME_CSS_BASE = `*{margin:0;padding:0;box-sizing:border-box}
1787
- :root{--bg:#0a0a1a;--surface:#141428;--primary:#6C63FF;--secondary:#FF6584;--accent:#00D4AA;--text:#f0f0f0;--muted:#888}
1788
- body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',system-ui,sans-serif;background:var(--bg);color:var(--text);min-height:100vh;display:flex;flex-direction:column;align-items:center;justify-content:center;overflow:hidden;user-select:none;-webkit-user-select:none;-webkit-tap-highlight-color:transparent}
1789
- .container{width:min(420px,92vw);text-align:center}
1790
- h1{font-size:1.8rem;margin-bottom:.5rem;background:linear-gradient(135deg,var(--primary),var(--secondary));-webkit-background-clip:text;-webkit-text-fill-color:transparent;background-clip:text}
1791
- .score{font-size:3rem;font-weight:800;margin:.5rem 0;font-variant-numeric:tabular-nums}
1792
- .btn{display:inline-block;padding:.75rem 2rem;border:none;border-radius:12px;font-size:1.1rem;font-weight:600;cursor:pointer;transition:transform .1s,box-shadow .2s;color:#fff;background:linear-gradient(135deg,var(--primary),#8B5CF6)}
1793
- .btn:active{transform:scale(.95)}
1794
- .btn:hover{box-shadow:0 0 20px rgba(108,99,255,.4)}
1795
- .stats{display:flex;gap:1.5rem;justify-content:center;margin:.75rem 0;font-size:.9rem;color:var(--muted)}
1796
- .stats span{display:flex;flex-direction:column;align-items:center;gap:.2rem}
1797
- .stats b{color:var(--text);font-size:1.1rem}
1798
- .hidden{display:none!important}
1799
- .high{color:var(--accent)}
1784
+ // ── Game Engine (compositional) ──────────────────────────
1785
+
1786
+ const GAME_PALETTES = {
1787
+ tapping: { bg:'#1a0a2e', sf:'#2d1a42', pr:'#FF6B35', sc:'#FF3864', ac:'#FFD700', tx:'#fff', mt:'#a89cc8' },
1788
+ quiz: { bg:'#0d1b2a', sf:'#1b2838', pr:'#3A86FF', sc:'#8338EC', ac:'#06D6A0', tx:'#e8ecf1', mt:'#7b8ca3' },
1789
+ memory: { bg:'#1a0f25', sf:'#2d1f3d', pr:'#C77DFF', sc:'#E05297', ac:'#FF6B9D', tx:'#f5f0ff', mt:'#9b8bb4' },
1790
+ snake: { bg:'#071a0e', sf:'#0f2918', pr:'#4ADE80', sc:'#22D3EE', ac:'#FCD34D', tx:'#e8f5e9', mt:'#6b8a7a' },
1791
+ typing: { bg:'#0a192f', sf:'#112240', pr:'#64FFDA', sc:'#00B4D8', ac:'#CCD6F6', tx:'#CCD6F6', mt:'#5c7a8a' },
1792
+ reaction: { bg:'#18181b', sf:'#27272a', pr:'#F87171', sc:'#EF4444', ac:'#4ADE80', tx:'#fafafa', mt:'#71717a' },
1793
+ breakout: { bg:'#0c0a1d', sf:'#1c1833', pr:'#A78BFA', sc:'#8B5CF6', ac:'#F59E0B', tx:'#f0f0ff', mt:'#8f85a3' },
1794
+ };
1795
+
1796
+ function extractGameOpts(raw) {
1797
+ const o = {};
1798
+ const d = raw.match(/(\d+)\s*(?:sec|seconds|秒|s\b)/i);
1799
+ if (d) o.duration = Math.min(120, Math.max(3, parseInt(d[1])));
1800
+ const sz = raw.match(/(\d+)\s*x\s*(\d+)/i);
1801
+ if (sz) { o.cols = parseInt(sz[1]); o.rows = parseInt(sz[2]); }
1802
+ if (/fast|高速|はやい/i.test(raw)) o.speed = 'fast';
1803
+ if (/hard|難しい|ハード/i.test(raw)) o.difficulty = 'hard';
1804
+ if (/easy|簡単|イージー/i.test(raw)) o.difficulty = 'easy';
1805
+ return o;
1806
+ }
1807
+
1808
+ function cssVars(p) {
1809
+ return `:root{--bg:${p.bg};--sf:${p.sf};--pr:${p.pr};--sc:${p.sc};--ac:${p.ac};--tx:${p.tx};--mt:${p.mt}}`;
1810
+ }
1811
+
1812
+ function cssBase(p) {
1813
+ return `*{margin:0;padding:0;box-sizing:border-box}
1814
+ body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',system-ui,sans-serif;background:${p.bg};color:${p.tx};min-height:100vh;display:flex;flex-direction:column;align-items:center;justify-content:center;overflow:hidden;user-select:none;-webkit-user-select:none;-webkit-tap-highlight-color:transparent}
1815
+ .ct{width:min(420px,92vw);text-align:center}
1816
+ h1{font-size:1.8rem;margin-bottom:.5rem;background:linear-gradient(135deg,${p.pr},${p.sc});-webkit-background-clip:text;-webkit-text-fill-color:transparent;background-clip:text}
1817
+ .sc{font-size:3rem;font-weight:800;margin:.5rem 0;font-variant-numeric:tabular-nums}
1818
+ .btn{display:inline-block;padding:.75rem 2rem;border:none;border-radius:12px;font-size:1.1rem;font-weight:600;cursor:pointer;transition:transform .1s,box-shadow .2s;color:#fff;background:linear-gradient(135deg,${p.pr},${p.sc})}
1819
+ .btn:active{transform:scale(.95)}.btn:hover{box-shadow:0 0 24px ${p.pr}66}
1820
+ .st{display:flex;gap:1.5rem;justify-content:center;margin:.75rem 0;font-size:.9rem;color:${p.mt}}
1821
+ .st span{display:flex;flex-direction:column;align-items:center;gap:.2rem}.st b{color:${p.tx};font-size:1.1rem}
1822
+ .hd{display:none!important}.hi{color:${p.ac}}
1800
1823
  @keyframes pop{0%{transform:scale(1)}50%{transform:scale(1.15)}100%{transform:scale(1)}}
1801
1824
  @keyframes fadeUp{0%{opacity:1;transform:translateY(0)}100%{opacity:0;transform:translateY(-60px)}}`;
1825
+ }
1802
1826
 
1803
- function tappingGameHTML(title) {
1804
- return `<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1.0,user-scalable=no"><title>${title || 'Tapping Game'}</title>
1805
- <style>${GAME_CSS_BASE}
1806
- .tap-area{width:min(320px,80vw);height:min(320px,80vw);border-radius:50%;background:radial-gradient(circle,var(--primary),#4338CA);display:flex;align-items:center;justify-content:center;margin:1.5rem auto;cursor:pointer;position:relative;transition:box-shadow .15s;font-size:4rem;font-weight:900}
1807
- .tap-area:active{box-shadow:0 0 60px rgba(108,99,255,.6)}
1808
- .tap-area.go{animation:pulse 1s infinite}
1809
- @keyframes pulse{0%,100%{box-shadow:0 0 0 0 rgba(108,99,255,.5)}50%{box-shadow:0 0 0 20px rgba(108,99,255,0)}}
1810
- .ripple{position:absolute;border-radius:50%;background:rgba(255,255,255,.3);animation:rippleAnim .5s ease-out forwards;pointer-events:none}
1811
- @keyframes rippleAnim{0%{width:0;height:0;opacity:.6}100%{width:200px;height:200px;opacity:0}}
1812
- .combo{position:fixed;font-weight:800;font-size:1.5rem;color:var(--accent);pointer-events:none;animation:fadeUp .6s ease-out forwards}
1813
- .timer-bar{width:100%;height:6px;background:var(--surface);border-radius:3px;margin:.5rem 0;overflow:hidden}
1814
- .timer-bar .fill{height:100%;background:linear-gradient(90deg,var(--accent),var(--primary));transition:width .1s linear;border-radius:3px}
1815
- .result{font-size:1.2rem;line-height:2}
1816
- </style></head><body>
1817
- <div class="container">
1818
- <h1>${title || 'Tapping Game'}</h1>
1819
- <div id="state-menu">
1820
- <p style="color:var(--muted);margin-bottom:1.5rem">Tap as fast as you can!</p>
1821
- <div class="stats"><span>Best<b id="best">0</b></span><span>Last<b id="last">-</b></span></div>
1822
- <button class="btn" onclick="startGame()">Start</button>
1823
- </div>
1824
- <div id="state-play" class="hidden">
1825
- <div class="timer-bar"><div class="fill" id="bar" style="width:100%"></div></div>
1826
- <div class="score" id="count">0</div>
1827
- <div class="tap-area go" id="tap" ontouchstart="doTap(event)" onmousedown="doTap(event)">TAP</div>
1828
- <div class="stats"><span>Combo<b id="combo">x1</b></span><span>TPS<b id="tps">0</b></span></div>
1829
- </div>
1830
- <div id="state-result" class="hidden">
1831
- <div class="score" id="final">0</div>
1832
- <div class="result" id="summary"></div>
1833
- <button class="btn" style="margin-top:1rem" onclick="startGame()">Play Again</button>
1834
- </div>
1827
+ function cssTimerBar(p) {
1828
+ return `.bar{width:100%;height:6px;background:${p.sf};border-radius:3px;margin:.5rem 0;overflow:hidden}
1829
+ .bar .fl{height:100%;background:linear-gradient(90deg,${p.ac},${p.pr});transition:width .1s linear;border-radius:3px}`;
1830
+ }
1831
+
1832
+ function jsCore(storageKey) {
1833
+ return `const $=id=>document.getElementById(id);
1834
+ let best=+(localStorage.getItem('${storageKey}')||0);
1835
+ if($('best'))$('best').textContent=best;
1836
+ function show(s){document.querySelectorAll('[id^=s-]').forEach(e=>e.classList.add('hd'));$('s-'+s).classList.remove('hd')}
1837
+ function saveBest(v){if(v>best){best=v;localStorage.setItem('${storageKey}',best);if($('best'))$('best').textContent=best;return true}return false}`;
1838
+ }
1839
+
1840
+ function htmlWrap(title, css, sections, js) {
1841
+ return `<!DOCTYPE html>
1842
+ <html lang="en">
1843
+ <head>
1844
+ <meta charset="UTF-8">
1845
+ <meta name="viewport" content="width=device-width,initial-scale=1.0,user-scalable=no">
1846
+ <title>${title}</title>
1847
+ <style>
1848
+ ${css}
1849
+ </style>
1850
+ </head>
1851
+ <body>
1852
+ <div class="ct">
1853
+ <h1>${title}</h1>
1854
+ ${sections}
1835
1855
  </div>
1836
1856
  <script>
1837
- const DURATION=10000;
1838
- let taps,startT,timer,comboN,lastTap,best=+(localStorage.getItem('tap_best')||0);
1839
- document.getElementById('best').textContent=best;
1840
- function show(id){document.querySelectorAll('[id^=state-]').forEach(e=>e.classList.add('hidden'));document.getElementById('state-'+id).classList.remove('hidden')}
1841
- function startGame(){taps=0;comboN=1;lastTap=0;startT=Date.now();document.getElementById('count').textContent='0';document.getElementById('combo').textContent='x1';document.getElementById('bar').style.width='100%';show('play');timer=requestAnimationFrame(tick)}
1842
- function tick(){const el=Date.now()-startT,pct=Math.max(0,1-el/DURATION)*100;document.getElementById('bar').style.width=pct+'%';const sec=Math.max(0,(DURATION-el)/1000);document.getElementById('tps').textContent=(taps/Math.max(.1,(el/1000))).toFixed(1);if(el>=DURATION)return endGame();timer=requestAnimationFrame(tick)}
1843
- function doTap(e){e.preventDefault();if(Date.now()-startT>DURATION)return;taps++;const now=Date.now(),gap=now-lastTap;if(gap<200)comboN=Math.min(comboN+1,99);else if(gap>500)comboN=1;lastTap=now;document.getElementById('count').textContent=taps;document.getElementById('combo').textContent='x'+comboN;document.getElementById('count').style.animation='none';void document.getElementById('count').offsetWidth;document.getElementById('count').style.animation='pop .15s';const r=document.createElement('div');r.className='ripple';const rect=document.getElementById('tap').getBoundingClientRect();const x=(e.touches?e.touches[0].clientX:e.clientX)-rect.left;const y=(e.touches?e.touches[0].clientY:e.clientY)-rect.top;r.style.left=x-100+'px';r.style.top=y-100+'px';document.getElementById('tap').appendChild(r);setTimeout(()=>r.remove(),500);if(comboN>=3){const c=document.createElement('div');c.className='combo';c.textContent='x'+comboN;c.style.left=Math.random()*60+20+'%';c.style.top=Math.random()*30+20+'%';document.body.appendChild(c);setTimeout(()=>c.remove(),600)}}
1844
- function endGame(){cancelAnimationFrame(timer);const tps=(taps/(DURATION/1000)).toFixed(1);if(taps>best){best=taps;localStorage.setItem('tap_best',best);document.getElementById('best').textContent=best}document.getElementById('last').textContent=taps;document.getElementById('final').textContent=taps;const r=taps>=100?'Insane!':taps>=70?'Amazing!':taps>=50?'Great!':taps>=30?'Nice!':'Keep trying!';document.getElementById('summary').innerHTML=r+'<br>'+tps+' taps/sec'+(taps>=best?' <span class="high">★ New Best!</span>':'');show('result')}
1845
- </script></body></html>`;
1846
- }
1847
-
1848
- function quizGameHTML(title) {
1849
- return `<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1.0,user-scalable=no"><title>${title || 'Quiz Game'}</title>
1850
- <style>${GAME_CSS_BASE}
1851
- .question{font-size:1.3rem;margin:1.5rem 0;min-height:3em;line-height:1.5}
1852
- .choices{display:flex;flex-direction:column;gap:.75rem;margin:1rem 0}
1853
- .choice{padding:1rem;border:2px solid var(--surface);border-radius:12px;background:var(--surface);cursor:pointer;font-size:1rem;text-align:left;transition:border-color .2s,background .2s}
1854
- .choice:hover{border-color:var(--primary)}
1855
- .choice.correct{border-color:var(--accent);background:rgba(0,212,170,.15)}
1856
- .choice.wrong{border-color:var(--secondary);background:rgba(255,101,132,.15)}
1857
- .progress{display:flex;gap:4px;justify-content:center;margin-bottom:1rem}
1858
- .dot{width:10px;height:10px;border-radius:50%;background:var(--surface)}
1859
- .dot.done{background:var(--accent)}.dot.fail{background:var(--secondary)}.dot.cur{background:var(--primary);box-shadow:0 0 8px var(--primary)}
1860
- .timer{font-size:2.5rem;font-weight:800;font-variant-numeric:tabular-nums}
1861
- </style></head><body>
1862
- <div class="container">
1863
- <h1>${title || 'Quiz Game'}</h1>
1864
- <div id="state-menu">
1865
- <p style="color:var(--muted);margin:1rem 0">Test your knowledge — 10 questions, 15 seconds each</p>
1866
- <div class="stats"><span>Best<b id="best">0</b></span><span>Last<b id="last">-</b></span></div>
1867
- <button class="btn" onclick="startGame()">Start Quiz</button>
1868
- </div>
1869
- <div id="state-play" class="hidden">
1870
- <div class="progress" id="dots"></div>
1871
- <div class="timer" id="timer">15</div>
1872
- <div class="question" id="question"></div>
1873
- <div class="choices" id="choices"></div>
1874
- </div>
1875
- <div id="state-result" class="hidden">
1876
- <div class="score" id="final">0/10</div>
1877
- <div class="result" id="summary" style="font-size:1.1rem;margin:1rem 0"></div>
1878
- <button class="btn" onclick="startGame()">Play Again</button>
1879
- </div>
1880
- </div>
1881
- <script>
1882
- const QUESTIONS=[
1883
- {q:"What does HTML stand for?",a:["HyperText Markup Language","High Tech Modern Language","Home Tool Markup Language","Hyper Transfer Markup Language"],c:0},
1884
- {q:"Which planet is closest to the Sun?",a:["Mercury","Venus","Mars","Earth"],c:0},
1885
- {q:"What is the largest ocean on Earth?",a:["Pacific","Atlantic","Indian","Arctic"],c:0},
1886
- {q:"In JavaScript, which keyword declares a constant?",a:["const","let","var","static"],c:0},
1887
- {q:"What year did the World Wide Web become public?",a:["1991","1989","1995","2000"],c:0},
1888
- {q:"What is the chemical symbol for gold?",a:["Au","Ag","Go","Gd"],c:0},
1889
- {q:"How many bits are in a byte?",a:["8","4","16","32"],c:0},
1890
- {q:"Which language is NAIDE designed to replace for AI coding?",a:["JavaScript","Python","Ruby","Go"],c:0},
1891
- {q:"What does CSS stand for?",a:["Cascading Style Sheets","Computer Style Sheets","Creative Style System","Coded Style Sheets"],c:0},
1892
- {q:"What is the speed of light (approx km/s)?",a:["300,000","150,000","500,000","1,000,000"],c:0},
1893
- {q:"Which data structure uses FIFO?",a:["Queue","Stack","Tree","Graph"],c:0},
1894
- {q:"What is the square root of 144?",a:["12","14","10","16"],c:0},
1895
- {q:"Who painted the Mona Lisa?",a:["Da Vinci","Picasso","Van Gogh","Monet"],c:0},
1896
- {q:"What gas do plants absorb?",a:["CO2","O2","N2","H2"],c:0},
1897
- {q:"Which protocol is used for secure web browsing?",a:["HTTPS","FTP","SMTP","SSH"],c:0}
1898
- ];
1899
- let cur,score,answers,tmr,timeLeft,best=+(localStorage.getItem('quiz_best')||0),pool;
1900
- document.getElementById('best').textContent=best;
1901
- function shuffle(a){for(let i=a.length-1;i>0;i--){const j=Math.floor(Math.random()*(i+1));[a[i],a[j]]=[a[j],a[i]]}return a}
1902
- function show(id){document.querySelectorAll('[id^=state-]').forEach(e=>e.classList.add('hidden'));document.getElementById('state-'+id).classList.remove('hidden')}
1903
- function startGame(){pool=shuffle([...QUESTIONS]).slice(0,10);cur=0;score=0;answers=[];document.getElementById('dots').innerHTML=pool.map((_,i)=>'<div class="dot" id="d'+i+'"></div>').join('');show('play');showQ()}
1904
- function showQ(){if(cur>=pool.length)return endGame();document.getElementById('d'+cur).classList.add('cur');const q=pool[cur];const mixed=q.a.map((t,i)=>({t,ok:i===q.c}));shuffle(mixed);document.getElementById('question').textContent=q.q;const ch=document.getElementById('choices');ch.innerHTML='';mixed.forEach((m,i)=>{const d=document.createElement('div');d.className='choice';d.textContent=m.t;d.onclick=()=>pick(d,m.ok,ch);ch.appendChild(d)});timeLeft=15;document.getElementById('timer').textContent=timeLeft;clearInterval(tmr);tmr=setInterval(()=>{timeLeft--;document.getElementById('timer').textContent=Math.max(0,timeLeft);if(timeLeft<=0){clearInterval(tmr);timeout()}},1000)}
1905
- function pick(el,ok,ch){clearInterval(tmr);ch.querySelectorAll('.choice').forEach(c=>{c.onclick=null;if(c===el)c.classList.add(ok?'correct':'wrong');else if(!ok){const t=pool[cur].a[pool[cur].c];if(c.textContent===t)c.classList.add('correct')}});const d=document.getElementById('d'+cur);d.classList.remove('cur');if(ok){score++;d.classList.add('done')}else{d.classList.add('fail')}answers.push(ok);cur++;setTimeout(showQ,800)}
1906
- function timeout(){const ch=document.getElementById('choices');ch.querySelectorAll('.choice').forEach(c=>{c.onclick=null;const t=pool[cur].a[pool[cur].c];if(c.textContent===t)c.classList.add('correct')});document.getElementById('d'+cur).classList.remove('cur');document.getElementById('d'+cur).classList.add('fail');answers.push(false);cur++;setTimeout(showQ,800)}
1907
- function endGame(){clearInterval(tmr);if(score>best){best=score;localStorage.setItem('quiz_best',best);document.getElementById('best').textContent=best}document.getElementById('last').textContent=score;document.getElementById('final').textContent=score+'/'+pool.length;const pct=Math.round(score/pool.length*100);const r=pct===100?'Perfect!':pct>=80?'Excellent!':pct>=60?'Good!':pct>=40?'Not bad!':'Study more!';document.getElementById('summary').innerHTML=r+'<br>'+pct+'% correct'+(score>=best?' <span class="high">★ New Best!</span>':'');show('result')}
1908
- </script></body></html>`;
1909
- }
1910
-
1911
- function memoryGameHTML(title) {
1912
- return `<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1.0,user-scalable=no"><title>${title || 'Memory Game'}</title>
1913
- <style>${GAME_CSS_BASE}
1914
- .grid{display:grid;grid-template-columns:repeat(4,1fr);gap:8px;margin:1rem auto;width:min(360px,88vw)}
1915
- .card{aspect-ratio:1;border-radius:12px;cursor:pointer;perspective:600px;-webkit-perspective:600px}
1916
- .card .inner{position:relative;width:100%;height:100%;transition:transform .4s;transform-style:preserve-3d;-webkit-transform-style:preserve-3d}
1917
- .card.flip .inner{transform:rotateY(180deg)}
1918
- .card .front,.card .back{position:absolute;width:100%;height:100%;border-radius:12px;display:flex;align-items:center;justify-content:center;font-size:2rem;backface-visibility:hidden;-webkit-backface-visibility:hidden}
1919
- .card .front{background:linear-gradient(135deg,var(--primary),#4338CA)}
1920
- .card .back{background:var(--surface);transform:rotateY(180deg);border:2px solid var(--primary)}
1921
- .card.matched .front{background:linear-gradient(135deg,var(--accent),#059669)}
1922
- .card.matched{pointer-events:none}
1923
- .moves{font-size:2.5rem;font-weight:800}
1924
- </style></head><body>
1925
- <div class="container">
1926
- <h1>${title || 'Memory Game'}</h1>
1927
- <div id="state-menu">
1928
- <p style="color:var(--muted);margin:1rem 0">Match all pairs with fewest moves</p>
1929
- <div class="stats"><span>Best<b id="best">-</b></span></div>
1930
- <button class="btn" onclick="startGame()">Start</button>
1931
- </div>
1932
- <div id="state-play" class="hidden">
1933
- <div class="stats"><span>Moves<b id="moves">0</b></span><span>Pairs<b id="pairs">0/8</b></span><span>Time<b id="time">0s</b></span></div>
1934
- <div class="grid" id="grid"></div>
1935
- </div>
1936
- <div id="state-result" class="hidden">
1937
- <div class="moves" id="finalMoves">0</div>
1857
+ ${js}
1858
+ </script>
1859
+ </body>
1860
+ </html>`;
1861
+ }
1862
+
1863
+ // ── Tapping Game Builder ──
1864
+
1865
+ function buildTapping(title, opts) {
1866
+ const p = GAME_PALETTES.tapping;
1867
+ const dur = opts.duration || 10;
1868
+
1869
+ const css = [cssVars(p), cssBase(p), cssTimerBar(p),
1870
+ `.tap{width:min(300px,75vw);height:min(300px,75vw);border-radius:50%;background:radial-gradient(circle at 35% 35%,${p.pr},${p.sc});display:flex;align-items:center;justify-content:center;margin:1.5rem auto;cursor:pointer;position:relative;font-size:3.5rem;font-weight:900;transition:box-shadow .15s;letter-spacing:-.05em}`,
1871
+ `.tap:active{box-shadow:0 0 60px ${p.pr}88}`,
1872
+ `.tap.on{animation:pulse 1s infinite}`,
1873
+ `@keyframes pulse{0%,100%{box-shadow:0 0 0 0 ${p.pr}66}50%{box-shadow:0 0 0 20px ${p.pr}00}}`,
1874
+ `.rp{position:absolute;border-radius:50%;background:rgba(255,255,255,.25);animation:rpAn .5s ease-out forwards;pointer-events:none}`,
1875
+ `@keyframes rpAn{0%{width:0;height:0;opacity:.6}100%{width:200px;height:200px;opacity:0}}`,
1876
+ `.cb{position:fixed;font-weight:800;font-size:1.5rem;color:${p.ac};pointer-events:none;animation:fadeUp .6s ease-out forwards}`,
1877
+ ].join('\n');
1878
+
1879
+ const html = `<div id="s-menu">
1880
+ <p style="color:${p.mt};margin-bottom:1.5rem">Tap as fast as you can — ${dur} seconds!</p>
1881
+ <div class="st"><span>Best<b id="best">0</b></span><span>Last<b id="last">-</b></span></div>
1882
+ <button class="btn" onclick="go()">Start</button></div>
1883
+ <div id="s-play" class="hd">
1884
+ <div class="bar"><div class="fl" id="bar" style="width:100%"></div></div>
1885
+ <div class="sc" id="cnt">0</div>
1886
+ <div class="tap on" id="tap" ontouchstart="hit(event)" onmousedown="hit(event)">TAP</div>
1887
+ <div class="st"><span>Combo<b id="cmb">x1</b></span><span>TPS<b id="tps">0</b></span></div></div>
1888
+ <div id="s-end" class="hd">
1889
+ <div class="sc" id="fin">0</div>
1890
+ <div id="msg" style="font-size:1.2rem;line-height:2"></div>
1891
+ <button class="btn" style="margin-top:1rem" onclick="go()">Again</button></div>`;
1892
+
1893
+ const js = [jsCore('tap_best'),
1894
+ `const DUR=${dur*1000};let taps,t0,raf,combo,lh;`,
1895
+ `function go(){taps=0;combo=1;lh=0;t0=Date.now();$('cnt').textContent='0';$('cmb').textContent='x1';$('bar').style.width='100%';show('play');raf=requestAnimationFrame(tick)}`,
1896
+ `function tick(){const e=Date.now()-t0,pc=Math.max(0,1-e/DUR)*100;$('bar').style.width=pc+'%';$('tps').textContent=(taps/Math.max(.1,e/1000)).toFixed(1);if(e>=DUR)return end();raf=requestAnimationFrame(tick)}`,
1897
+ `function hit(ev){ev.preventDefault();if(Date.now()-t0>DUR)return;taps++;const now=Date.now(),gap=now-lh;combo=gap<200?Math.min(combo+1,99):gap>500?1:combo;lh=now;$('cnt').textContent=taps;$('cmb').textContent='x'+combo;$('cnt').style.animation='none';void $('cnt').offsetWidth;$('cnt').style.animation='pop .15s';const el=$('tap'),rc=el.getBoundingClientRect(),cx=(ev.touches?ev.touches[0].clientX:ev.clientX)-rc.left,cy=(ev.touches?ev.touches[0].clientY:ev.clientY)-rc.top;const r=document.createElement('div');r.className='rp';r.style.cssText='left:'+(cx-100)+'px;top:'+(cy-100)+'px';el.appendChild(r);setTimeout(()=>r.remove(),500);if(combo>=3){const c=document.createElement('div');c.className='cb';c.textContent='x'+combo;c.style.cssText='left:'+Math.random()*60+20+'%;top:'+Math.random()*30+20+'%';document.body.appendChild(c);setTimeout(()=>c.remove(),600)}}`,
1898
+ `function end(){cancelAnimationFrame(raf);const tps=(taps/(DUR/1000)).toFixed(1);const nb=saveBest(taps);$('last').textContent=taps;$('fin').textContent=taps;const r=taps>=100?'Insane!':taps>=70?'Amazing!':taps>=50?'Great!':taps>=30?'Nice!':'Keep trying!';$('msg').innerHTML=r+'<br>'+tps+' taps/sec'+(nb?' <span class=hi>★ New Best!</span>':'');show('end')}`,
1899
+ ].join('\n');
1900
+
1901
+ return htmlWrap(title, css, html, js);
1902
+ }
1903
+
1904
+ // ── Quiz Game Builder ──
1905
+
1906
+ function buildQuiz(title, opts) {
1907
+ const p = GAME_PALETTES.quiz;
1908
+ const perQ = opts.duration || 15;
1909
+
1910
+ const css = [cssVars(p), cssBase(p),
1911
+ `.q{font-size:1.3rem;margin:1.5rem 0;min-height:3em;line-height:1.5}`,
1912
+ `.ch{display:flex;flex-direction:column;gap:.75rem;margin:1rem 0}`,
1913
+ `.ch div{padding:1rem;border:2px solid ${p.sf};border-radius:12px;background:${p.sf};cursor:pointer;font-size:1rem;text-align:left;transition:border-color .2s,background .2s}`,
1914
+ `.ch div:hover{border-color:${p.pr}}`,
1915
+ `.ch .ok{border-color:${p.ac};background:${p.ac}22}`,
1916
+ `.ch .no{border-color:#F87171;background:#F8717122}`,
1917
+ `.dots{display:flex;gap:4px;justify-content:center;margin-bottom:1rem}`,
1918
+ `.dot{width:10px;height:10px;border-radius:50%;background:${p.sf}}`,
1919
+ `.dot.pass{background:${p.ac}}.dot.fail{background:#F87171}.dot.now{background:${p.pr};box-shadow:0 0 8px ${p.pr}}`,
1920
+ `.tm{font-size:2.5rem;font-weight:800;font-variant-numeric:tabular-nums}`,
1921
+ ].join('\n');
1922
+
1923
+ const html = `<div id="s-menu">
1924
+ <p style="color:${p.mt};margin:1rem 0">10 questions, ${perQ} seconds each</p>
1925
+ <div class="st"><span>Best<b id="best">0</b></span><span>Last<b id="last">-</b></span></div>
1926
+ <button class="btn" onclick="go()">Start Quiz</button></div>
1927
+ <div id="s-play" class="hd">
1928
+ <div class="dots" id="dots"></div>
1929
+ <div class="tm" id="timer">${perQ}</div>
1930
+ <div class="q" id="qtext"></div>
1931
+ <div class="ch" id="choices"></div></div>
1932
+ <div id="s-end" class="hd">
1933
+ <div class="sc" id="fin">0/10</div>
1934
+ <div id="msg" style="font-size:1.1rem;margin:1rem 0"></div>
1935
+ <button class="btn" onclick="go()">Play Again</button></div>`;
1936
+
1937
+ const js = [jsCore('quiz_best'),
1938
+ `const QS=[{q:"What does HTML stand for?",a:["HyperText Markup Language","High Tech Modern Language","Home Tool Markup Language","Hyper Transfer Markup Language"],c:0},{q:"Which planet is closest to the Sun?",a:["Mercury","Venus","Mars","Earth"],c:0},{q:"What is the largest ocean on Earth?",a:["Pacific","Atlantic","Indian","Arctic"],c:0},{q:"In JavaScript, which keyword declares a constant?",a:["const","let","var","static"],c:0},{q:"What year did the World Wide Web go public?",a:["1991","1989","1995","2000"],c:0},{q:"What is the chemical symbol for gold?",a:["Au","Ag","Go","Gd"],c:0},{q:"How many bits are in a byte?",a:["8","4","16","32"],c:0},{q:"What does CSS stand for?",a:["Cascading Style Sheets","Computer Style Sheets","Creative Style System","Coded Style Sheets"],c:0},{q:"What is the speed of light (approx km/s)?",a:["300,000","150,000","500,000","1,000,000"],c:0},{q:"Which data structure uses FIFO?",a:["Queue","Stack","Tree","Graph"],c:0},{q:"What is the square root of 144?",a:["12","14","10","16"],c:0},{q:"Who painted the Mona Lisa?",a:["Da Vinci","Picasso","Van Gogh","Monet"],c:0},{q:"What gas do plants absorb?",a:["CO2","O2","N2","H2"],c:0},{q:"Which protocol is used for secure browsing?",a:["HTTPS","FTP","SMTP","SSH"],c:0},{q:"What is 2^10?",a:["1024","512","2048","4096"],c:0}];`,
1939
+ `const PER_Q=${perQ};let cur,score,pool,tmr,tl;`,
1940
+ `function shuf(a){for(let i=a.length-1;i>0;i--){const j=Math.floor(Math.random()*(i+1));[a[i],a[j]]=[a[j],a[i]]}return a}`,
1941
+ `function go(){pool=shuf([...QS]).slice(0,10);cur=0;score=0;$('dots').innerHTML=pool.map((_,i)=>'<div class="dot" id="d'+i+'"></div>').join('');show('play');next()}`,
1942
+ `function next(){if(cur>=pool.length)return end();$('d'+cur).classList.add('now');const q=pool[cur],mx=q.a.map((t,i)=>({t,ok:i===q.c}));shuf(mx);$('qtext').textContent=q.q;const ch=$('choices');ch.innerHTML='';mx.forEach(m=>{const d=document.createElement('div');d.textContent=m.t;d.onclick=()=>pick(d,m.ok,ch);ch.appendChild(d)});tl=PER_Q;$('timer').textContent=tl;clearInterval(tmr);tmr=setInterval(()=>{tl--;$('timer').textContent=Math.max(0,tl);if(tl<=0){clearInterval(tmr);timeout()}},1000)}`,
1943
+ `function pick(el,ok,ch){clearInterval(tmr);ch.querySelectorAll('div').forEach(c=>{c.onclick=null;if(c===el)c.classList.add(ok?'ok':'no');else if(!ok){const t=pool[cur].a[pool[cur].c];if(c.textContent===t)c.classList.add('ok')}});$('d'+cur).classList.remove('now');$('d'+cur).classList.add(ok?'pass':'fail');if(ok)score++;cur++;setTimeout(next,800)}`,
1944
+ `function timeout(){const ch=$('choices');ch.querySelectorAll('div').forEach(c=>{c.onclick=null;if(c.textContent===pool[cur].a[pool[cur].c])c.classList.add('ok')});$('d'+cur).classList.remove('now');$('d'+cur).classList.add('fail');cur++;setTimeout(next,800)}`,
1945
+ `function end(){clearInterval(tmr);const nb=saveBest(score);$('last').textContent=score;$('fin').textContent=score+'/'+pool.length;const pc=Math.round(score/pool.length*100);const r=pc===100?'Perfect!':pc>=80?'Excellent!':pc>=60?'Good!':pc>=40?'Not bad!':'Study more!';$('msg').innerHTML=r+'<br>'+pc+'% correct'+(nb?' <span class=hi>★ New Best!</span>':'');show('end')}`,
1946
+ ].join('\n');
1947
+
1948
+ return htmlWrap(title, css, html, js);
1949
+ }
1950
+
1951
+ // ── Memory Game Builder ──
1952
+
1953
+ function buildMemory(title, opts) {
1954
+ const p = GAME_PALETTES.memory;
1955
+ const cols = opts.cols || 4;
1956
+ const pairs = Math.floor((cols * (opts.rows || cols)) / 2);
1957
+
1958
+ const css = [cssVars(p), cssBase(p),
1959
+ `.grid{display:grid;grid-template-columns:repeat(${cols},1fr);gap:8px;margin:1rem auto;width:min(${cols*90}px,88vw)}`,
1960
+ `.card{aspect-ratio:1;border-radius:12px;cursor:pointer;perspective:600px;-webkit-perspective:600px}`,
1961
+ `.card .inn{position:relative;width:100%;height:100%;transition:transform .4s;transform-style:preserve-3d;-webkit-transform-style:preserve-3d}`,
1962
+ `.card.flip .inn{transform:rotateY(180deg)}`,
1963
+ `.card .fr,.card .bk{position:absolute;width:100%;height:100%;border-radius:12px;display:flex;align-items:center;justify-content:center;font-size:${cols<=4?'2rem':'1.4rem'};backface-visibility:hidden;-webkit-backface-visibility:hidden}`,
1964
+ `.card .fr{background:linear-gradient(135deg,${p.pr},${p.sc})}`,
1965
+ `.card .bk{background:${p.sf};transform:rotateY(180deg);border:2px solid ${p.pr}}`,
1966
+ `.card.done .fr{background:linear-gradient(135deg,${p.ac},#059669)}`,
1967
+ `.card.done{pointer-events:none}`,
1968
+ ].join('\n');
1969
+
1970
+ const html = `<div id="s-menu">
1971
+ <p style="color:${p.mt};margin:1rem 0">Match all ${pairs} pairs with fewest moves</p>
1972
+ <div class="st"><span>Best<b id="best">-</b></span></div>
1973
+ <button class="btn" onclick="go()">Start</button></div>
1974
+ <div id="s-play" class="hd">
1975
+ <div class="st"><span>Moves<b id="mv">0</b></span><span>Pairs<b id="pr">0/${pairs}</b></span><span>Time<b id="tm">0s</b></span></div>
1976
+ <div class="grid" id="grid"></div></div>
1977
+ <div id="s-end" class="hd">
1978
+ <div class="sc" id="fin">0</div>
1938
1979
  <p style="font-size:1.2rem;margin:.5rem 0">moves</p>
1939
- <div id="summary" style="font-size:1.1rem;margin:1rem 0"></div>
1940
- <button class="btn" onclick="startGame()">Play Again</button>
1941
- </div>
1942
- </div>
1943
- <script>
1944
- const EMOJIS=['🎮','🎯','🚀','💎','🔥','⚡','🌟','🎵','🎨','🏆','🍕','🌈','🦊','🐙','🎃','🍀'];
1945
- let cards,flipped,matched,moves,pairsN,tmr,startT,best=localStorage.getItem('mem_best');
1946
- document.getElementById('best').textContent=best||'-';
1947
- function show(id){document.querySelectorAll('[id^=state-]').forEach(e=>e.classList.add('hidden'));document.getElementById('state-'+id).classList.remove('hidden')}
1948
- function shuffle(a){for(let i=a.length-1;i>0;i--){const j=Math.floor(Math.random()*(i+1));[a[i],a[j]]=[a[j],a[i]]}return a}
1949
- function startGame(){const pick=shuffle([...EMOJIS]).slice(0,8);cards=shuffle([...pick,...pick]);flipped=[];matched=0;moves=0;pairsN=8;document.getElementById('moves').textContent=0;document.getElementById('pairs').textContent='0/'+pairsN;const g=document.getElementById('grid');g.innerHTML='';cards.forEach((em,i)=>{const d=document.createElement('div');d.className='card';d.innerHTML='<div class="inner"><div class="front">?</div><div class="back">'+em+'</div></div>';d.onclick=()=>flipCard(d,i);g.appendChild(d)});startT=Date.now();tmr=setInterval(()=>{document.getElementById('time').textContent=Math.floor((Date.now()-startT)/1000)+'s'},200);show('play')}
1950
- function flipCard(el,i){if(flipped.length>=2||el.classList.contains('flip')||el.classList.contains('matched'))return;el.classList.add('flip');flipped.push({el,i,val:cards[i]});if(flipped.length===2){moves++;document.getElementById('moves').textContent=moves;const[a,b]=flipped;if(a.val===b.val){a.el.classList.add('matched');b.el.classList.add('matched');matched++;document.getElementById('pairs').textContent=matched+'/'+pairsN;flipped=[];if(matched===pairsN)endGame()}else{setTimeout(()=>{a.el.classList.remove('flip');b.el.classList.remove('flip');flipped=[]},700)}}}
1951
- function endGame(){clearInterval(tmr);const sec=Math.floor((Date.now()-startT)/1000);if(!best||moves<+best){best=moves;localStorage.setItem('mem_best',best);document.getElementById('best').textContent=best}document.getElementById('finalMoves').textContent=moves;const r=moves<=pairsN+2?'Perfect memory!':moves<=pairsN*2?'Great!':moves<=pairsN*3?'Good!':'Keep practicing!';document.getElementById('summary').innerHTML=r+'<br>Completed in '+sec+'s'+(moves<=+best?' <span class="high">★ New Best!</span>':'');show('result')}
1952
- </script></body></html>`;
1953
- }
1954
-
1955
- function snakeGameHTML(title) {
1956
- return `<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1.0,user-scalable=no"><title>${title || 'Snake Game'}</title>
1957
- <style>${GAME_CSS_BASE}
1958
- canvas{border-radius:12px;background:var(--surface);display:block;margin:1rem auto;image-rendering:pixelated;touch-action:none}
1959
- .controls{display:grid;grid-template-columns:repeat(3,56px);grid-template-rows:repeat(2,56px);gap:6px;justify-content:center;margin:1rem auto}
1960
- .controls button{border:none;border-radius:10px;background:var(--surface);color:var(--text);font-size:1.4rem;cursor:pointer;display:flex;align-items:center;justify-content:center}
1961
- .controls button:active{background:var(--primary)}
1962
- </style></head><body>
1963
- <div class="container">
1964
- <h1>${title || 'Snake'}</h1>
1965
- <div id="state-menu">
1966
- <p style="color:var(--muted);margin:1rem 0">Arrow keys or swipe to control</p>
1967
- <div class="stats"><span>Best<b id="best">0</b></span></div>
1968
- <button class="btn" onclick="startGame()">Play</button>
1969
- </div>
1970
- <div id="state-play" class="hidden">
1971
- <div class="stats"><span>Score<b id="score">0</b></span><span>Best<b id="hiscore">0</b></span></div>
1972
- <canvas id="cv" width="320" height="320"></canvas>
1973
- <div class="controls">
1974
- <div></div><button ontouchstart="setDir(0,-1)" onclick="setDir(0,-1)">▲</button><div></div>
1975
- <button ontouchstart="setDir(-1,0)" onclick="setDir(-1,0)">◀</button><button ontouchstart="setDir(0,1)" onclick="setDir(0,1)">▼</button><button ontouchstart="setDir(1,0)" onclick="setDir(1,0)">▶</button>
1976
- </div>
1977
- </div>
1978
- <div id="state-result" class="hidden">
1979
- <div class="score" id="final">0</div>
1980
- <div id="summary" style="font-size:1.1rem;margin:1rem 0"></div>
1981
- <button class="btn" onclick="startGame()">Play Again</button>
1982
- </div>
1983
- </div>
1984
- <script>
1985
- const S=20,W=16,cv=document.getElementById('cv'),ctx=cv.getContext('2d');
1986
- let snake,dir,food,sc,best=+(localStorage.getItem('snake_best')||0),loop,nextDir;
1987
- document.getElementById('best').textContent=best;document.getElementById('hiscore').textContent=best;
1988
- function show(id){document.querySelectorAll('[id^=state-]').forEach(e=>e.classList.add('hidden'));document.getElementById('state-'+id).classList.remove('hidden')}
1989
- function rand(){let p;do{p={x:Math.floor(Math.random()*W),y:Math.floor(Math.random()*W)}}while(snake.some(s=>s.x===p.x&&s.y===p.y));return p}
1990
- function startGame(){snake=[{x:8,y:8},{x:7,y:8},{x:6,y:8}];dir={x:1,y:0};nextDir={x:1,y:0};food=rand();sc=0;document.getElementById('score').textContent=0;show('play');clearInterval(loop);loop=setInterval(tick,120)}
1991
- function setDir(x,y){if(x===-dir.x&&y===-dir.y)return;nextDir={x,y}}
1992
- function tick(){dir=nextDir;const h={x:snake[0].x+dir.x,y:snake[0].y+dir.y};if(h.x<0||h.x>=W||h.y<0||h.y>=W||snake.some(s=>s.x===h.x&&s.y===h.y))return endGame();snake.unshift(h);if(h.x===food.x&&h.y===food.y){sc++;document.getElementById('score').textContent=sc;food=rand()}else{snake.pop()}draw()}
1993
- function draw(){ctx.fillStyle='#141428';ctx.fillRect(0,0,320,320);ctx.fillStyle='#FF6584';ctx.beginPath();ctx.arc(food.x*S+S/2,food.y*S+S/2,S/2-2,0,Math.PI*2);ctx.fill();snake.forEach((s,i)=>{const t=i/snake.length;ctx.fillStyle=i===0?'#6C63FF':'hsl('+(260+t*40)+',70%,'+(60-t*15)+'%)';ctx.beginPath();ctx.roundRect(s.x*S+1,s.y*S+1,S-2,S-2,4);ctx.fill()})}
1994
- function endGame(){clearInterval(loop);if(sc>best){best=sc;localStorage.setItem('snake_best',best);document.getElementById('best').textContent=best;document.getElementById('hiscore').textContent=best}document.getElementById('final').textContent=sc;const r=sc>=30?'Legend!':sc>=20?'Amazing!':sc>=10?'Great!':sc>=5?'Good!':'Try again!';document.getElementById('summary').innerHTML=r+(sc>=best&&sc>0?' <span class="high">★ New Best!</span>':'');show('result')}
1995
- document.addEventListener('keydown',e=>{if(e.key==='ArrowUp')setDir(0,-1);if(e.key==='ArrowDown')setDir(0,1);if(e.key==='ArrowLeft')setDir(-1,0);if(e.key==='ArrowRight')setDir(1,0)});
1996
- let tx,ty;cv.addEventListener('touchstart',e=>{tx=e.touches[0].clientX;ty=e.touches[0].clientY},{passive:true});
1997
- cv.addEventListener('touchend',e=>{const dx=e.changedTouches[0].clientX-tx,dy=e.changedTouches[0].clientY-ty;if(Math.abs(dx)>Math.abs(dy)){setDir(dx>0?1:-1,0)}else{setDir(0,dy>0?1:-1)}},{passive:true});
1998
- </script></body></html>`;
1999
- }
2000
-
2001
- function reactionGameHTML(title) {
2002
- return `<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1.0,user-scalable=no"><title>${title || 'Reaction Test'}</title>
2003
- <style>${GAME_CSS_BASE}
2004
- .zone{width:min(340px,85vw);height:min(340px,85vw);border-radius:24px;display:flex;align-items:center;justify-content:center;margin:1.5rem auto;cursor:pointer;font-size:1.2rem;font-weight:600;transition:background .2s}
2005
- .zone.wait{background:var(--surface)}
2006
- .zone.ready{background:var(--secondary);color:#fff}
2007
- .zone.go{background:var(--accent);color:var(--bg)}
2008
- .zone.early{background:#EF4444}
2009
- .times{display:flex;flex-wrap:wrap;gap:6px;justify-content:center;margin:1rem 0}
2010
- .times span{background:var(--surface);padding:.3rem .6rem;border-radius:6px;font-size:.85rem;font-variant-numeric:tabular-nums}
2011
- .ms{font-size:3.5rem;font-weight:900;font-variant-numeric:tabular-nums}
2012
- </style></head><body>
2013
- <div class="container">
2014
- <h1>${title || 'Reaction Test'}</h1>
2015
- <div id="state-menu">
2016
- <p style="color:var(--muted);margin:1rem 0">Tap when the color turns green — 5 rounds</p>
2017
- <div class="stats"><span>Best Avg<b id="best">-</b></span></div>
2018
- <button class="btn" onclick="startGame()">Start</button>
2019
- </div>
2020
- <div id="state-play" class="hidden">
2021
- <div class="stats"><span>Round<b id="round">1/5</b></span><span>Avg<b id="avg">-</b></span></div>
2022
- <div class="zone wait" id="zone" onmousedown="tap()" ontouchstart="tap()">
2023
- <span id="zoneText">Wait...</span>
2024
- </div>
2025
- <div class="times" id="times"></div>
2026
- </div>
2027
- <div id="state-result" class="hidden">
2028
- <div class="ms" id="finalMs">0</div>
1980
+ <div id="msg" style="font-size:1.1rem;margin:1rem 0"></div>
1981
+ <button class="btn" onclick="go()">Play Again</button></div>`;
1982
+
1983
+ const js = [jsCore('mem_best'),
1984
+ `const EM=['🎮','🎯','🚀','💎','🔥','⚡','🌟','🎵','🎨','🏆','🍕','🌈','🦊','🐙','🎃','🍀','🎲','🌸'];`,
1985
+ `const PAIRS=${pairs};let cards,flp,done,moves,tmr,t0;`,
1986
+ `function shuf(a){for(let i=a.length-1;i>0;i--){const j=Math.floor(Math.random()*(i+1));[a[i],a[j]]=[a[j],a[i]]}return a}`,
1987
+ `function go(){const pick=shuf([...EM]).slice(0,PAIRS);cards=shuf([...pick,...pick]);flp=[];done=0;moves=0;$('mv').textContent=0;$('pr').textContent='0/'+PAIRS;const g=$('grid');g.innerHTML='';cards.forEach((em,i)=>{const d=document.createElement('div');d.className='card';d.innerHTML='<div class="inn"><div class="fr">?</div><div class="bk">'+em+'</div></div>';d.onclick=()=>flip(d,i);g.appendChild(d)});t0=Date.now();tmr=setInterval(()=>{$('tm').textContent=Math.floor((Date.now()-t0)/1000)+'s'},200);show('play')}`,
1988
+ `function flip(el,i){if(flp.length>=2||el.classList.contains('flip')||el.classList.contains('done'))return;el.classList.add('flip');flp.push({el,i,v:cards[i]});if(flp.length===2){moves++;$('mv').textContent=moves;const[a,b]=flp;if(a.v===b.v){a.el.classList.add('done');b.el.classList.add('done');done++;$('pr').textContent=done+'/'+PAIRS;flp=[];if(done===PAIRS)end()}else{setTimeout(()=>{a.el.classList.remove('flip');b.el.classList.remove('flip');flp=[]},700)}}}`,
1989
+ `function end(){clearInterval(tmr);const sec=Math.floor((Date.now()-t0)/1000);const nb=saveBest(moves);$('fin').textContent=moves;const r=moves<=PAIRS+2?'Perfect memory!':moves<=PAIRS*2?'Great!':moves<=PAIRS*3?'Good!':'Keep practicing!';$('msg').innerHTML=r+'<br>Completed in '+sec+'s'+(nb?' <span class=hi>★ New Best!</span>':'');show('end')}`,
1990
+ ].join('\n');
1991
+
1992
+ return htmlWrap(title, css, html, js);
1993
+ }
1994
+
1995
+ // ── Snake Game Builder ──
1996
+
1997
+ function buildSnake(title, opts) {
1998
+ const p = GAME_PALETTES.snake;
1999
+ const spd = opts.speed === 'fast' ? 80 : opts.difficulty === 'hard' ? 90 : 120;
2000
+ const W = 16, S = 20;
2001
+
2002
+ const css = [cssVars(p), cssBase(p),
2003
+ `canvas{border-radius:12px;background:${p.sf};display:block;margin:1rem auto;image-rendering:pixelated;touch-action:none}`,
2004
+ `.ctl{display:grid;grid-template-columns:repeat(3,56px);grid-template-rows:repeat(2,56px);gap:6px;justify-content:center;margin:1rem auto}`,
2005
+ `.ctl button{border:none;border-radius:10px;background:${p.sf};color:${p.tx};font-size:1.4rem;cursor:pointer;display:flex;align-items:center;justify-content:center}`,
2006
+ `.ctl button:active{background:${p.pr}}`,
2007
+ ].join('\n');
2008
+
2009
+ const html = `<div id="s-menu">
2010
+ <p style="color:${p.mt};margin:1rem 0">Arrow keys or swipe to control</p>
2011
+ <div class="st"><span>Best<b id="best">0</b></span></div>
2012
+ <button class="btn" onclick="go()">Play</button></div>
2013
+ <div id="s-play" class="hd">
2014
+ <div class="st"><span>Score<b id="score">0</b></span><span>Best<b id="hi">0</b></span></div>
2015
+ <canvas id="cv" width="${W*S}" height="${W*S}"></canvas>
2016
+ <div class="ctl">
2017
+ <div></div><button ontouchstart="sd(0,-1)" onclick="sd(0,-1)">▲</button><div></div>
2018
+ <button ontouchstart="sd(-1,0)" onclick="sd(-1,0)">◀</button><button ontouchstart="sd(0,1)" onclick="sd(0,1)">▼</button><button ontouchstart="sd(1,0)" onclick="sd(1,0)">▶</button></div></div>
2019
+ <div id="s-end" class="hd">
2020
+ <div class="sc" id="fin">0</div>
2021
+ <div id="msg" style="font-size:1.1rem;margin:1rem 0"></div>
2022
+ <button class="btn" onclick="go()">Play Again</button></div>`;
2023
+
2024
+ const js = [jsCore('snake_best'),
2025
+ `const GS=${S},GW=${W},cv=$('cv'),ctx=cv.getContext('2d');`,
2026
+ `let sn,dir,nd,food,sc,lp;$('hi').textContent=best;`,
2027
+ `function rnd(){let p;do{p={x:Math.floor(Math.random()*GW),y:Math.floor(Math.random()*GW)}}while(sn.some(s=>s.x===p.x&&s.y===p.y));return p}`,
2028
+ `function go(){sn=[{x:8,y:8},{x:7,y:8},{x:6,y:8}];dir={x:1,y:0};nd={x:1,y:0};food=rnd();sc=0;$('score').textContent=0;show('play');clearInterval(lp);lp=setInterval(tick,${spd})}`,
2029
+ `function sd(x,y){if(x===-dir.x&&y===-dir.y)return;nd={x,y}}`,
2030
+ `function tick(){dir=nd;const h={x:sn[0].x+dir.x,y:sn[0].y+dir.y};if(h.x<0||h.x>=GW||h.y<0||h.y>=GW||sn.some(s=>s.x===h.x&&s.y===h.y))return end();sn.unshift(h);if(h.x===food.x&&h.y===food.y){sc++;$('score').textContent=sc;food=rnd()}else sn.pop();draw()}`,
2031
+ `function draw(){ctx.fillStyle='${p.sf}';ctx.fillRect(0,0,GW*GS,GW*GS);ctx.fillStyle='${p.ac}';ctx.beginPath();ctx.arc(food.x*GS+GS/2,food.y*GS+GS/2,GS/2-2,0,Math.PI*2);ctx.fill();sn.forEach((s,i)=>{const t=i/sn.length;ctx.fillStyle=i===0?'${p.pr}':'hsl('+(140+t*40)+',70%,'+(55-t*20)+'%)';ctx.beginPath();ctx.roundRect(s.x*GS+1,s.y*GS+1,GS-2,GS-2,4);ctx.fill()})}`,
2032
+ `function end(){clearInterval(lp);const nb=saveBest(sc);$('hi').textContent=best;$('fin').textContent=sc;const r=sc>=30?'Legend!':sc>=20?'Amazing!':sc>=10?'Great!':sc>=5?'Good!':'Try again!';$('msg').innerHTML=r+(nb&&sc>0?' <span class=hi>★ New Best!</span>':'');show('end')}`,
2033
+ `document.addEventListener('keydown',e=>{if(e.key==='ArrowUp')sd(0,-1);if(e.key==='ArrowDown')sd(0,1);if(e.key==='ArrowLeft')sd(-1,0);if(e.key==='ArrowRight')sd(1,0)});`,
2034
+ `let tx,ty;cv.addEventListener('touchstart',e=>{tx=e.touches[0].clientX;ty=e.touches[0].clientY},{passive:true});cv.addEventListener('touchend',e=>{const dx=e.changedTouches[0].clientX-tx,dy=e.changedTouches[0].clientY-ty;if(Math.abs(dx)>Math.abs(dy))sd(dx>0?1:-1,0);else sd(0,dy>0?1:-1)},{passive:true});`,
2035
+ ].join('\n');
2036
+
2037
+ return htmlWrap(title, css, html, js);
2038
+ }
2039
+
2040
+ // ── Reaction Game Builder ──
2041
+
2042
+ function buildReaction(title, opts) {
2043
+ const p = GAME_PALETTES.reaction;
2044
+ const rounds = opts.difficulty === 'hard' ? 10 : 5;
2045
+
2046
+ const css = [cssVars(p), cssBase(p),
2047
+ `.zone{width:min(340px,85vw);height:min(340px,85vw);border-radius:24px;display:flex;align-items:center;justify-content:center;margin:1.5rem auto;cursor:pointer;font-size:1.2rem;font-weight:600;transition:background .2s}`,
2048
+ `.zone.wait{background:${p.sf}}`,
2049
+ `.zone.ready{background:${p.pr};color:#fff}`,
2050
+ `.zone.go{background:${p.ac};color:${p.bg}}`,
2051
+ `.zone.early{background:#DC2626}`,
2052
+ `.times{display:flex;flex-wrap:wrap;gap:6px;justify-content:center;margin:1rem 0}`,
2053
+ `.times span{background:${p.sf};padding:.3rem .6rem;border-radius:6px;font-size:.85rem;font-variant-numeric:tabular-nums}`,
2054
+ `.ms{font-size:3.5rem;font-weight:900;font-variant-numeric:tabular-nums}`,
2055
+ ].join('\n');
2056
+
2057
+ const html = `<div id="s-menu">
2058
+ <p style="color:${p.mt};margin:1rem 0">Tap when it turns green — ${rounds} rounds</p>
2059
+ <div class="st"><span>Best Avg<b id="best">-</b></span></div>
2060
+ <button class="btn" onclick="go()">Start</button></div>
2061
+ <div id="s-play" class="hd">
2062
+ <div class="st"><span>Round<b id="rnd">1/${rounds}</b></span><span>Avg<b id="avg">-</b></span></div>
2063
+ <div class="zone wait" id="zone" onmousedown="tap()" ontouchstart="tap()"><span id="ztxt">Wait...</span></div>
2064
+ <div class="times" id="times"></div></div>
2065
+ <div id="s-end" class="hd">
2066
+ <div class="ms" id="fin">0</div>
2029
2067
  <p style="font-size:1.2rem;margin:.5rem 0">ms average</p>
2030
- <div id="summary" style="margin:1rem 0"></div>
2031
- <div class="times" id="finalTimes"></div>
2032
- <button class="btn" style="margin-top:1rem" onclick="startGame()">Try Again</button>
2033
- </div>
2034
- </div>
2035
- <script>
2036
- const ROUNDS=5;
2037
- let round,results,phase,greenAt,tmr,best=localStorage.getItem('react_best');
2038
- document.getElementById('best').textContent=best?best+'ms':'-';
2039
- function show(id){document.querySelectorAll('[id^=state-]').forEach(e=>e.classList.add('hidden'));document.getElementById('state-'+id).classList.remove('hidden')}
2040
- function startGame(){round=0;results=[];document.getElementById('times').innerHTML='';show('play');nextRound()}
2041
- function nextRound(){if(round>=ROUNDS)return endGame();document.getElementById('round').textContent=(round+1)+'/'+ROUNDS;const z=document.getElementById('zone');z.className='zone ready';document.getElementById('zoneText').textContent='Wait for green...';phase='wait';const delay=1500+Math.random()*3000;clearTimeout(tmr);tmr=setTimeout(()=>{z.className='zone go';document.getElementById('zoneText').textContent='TAP NOW!';greenAt=Date.now();phase='go'},delay)}
2042
- function tap(){if(phase==='wait'){clearTimeout(tmr);const z=document.getElementById('zone');z.className='zone early';document.getElementById('zoneText').textContent='Too early! Tap to retry';phase='early'}else if(phase==='early'){nextRound()}else if(phase==='go'){const ms=Date.now()-greenAt;results.push(ms);const t=document.createElement('span');t.textContent=ms+'ms';document.getElementById('times').appendChild(t);const avg=Math.round(results.reduce((a,b)=>a+b,0)/results.length);document.getElementById('avg').textContent=avg+'ms';round++;phase='done';setTimeout(nextRound,600)}}
2043
- function endGame(){const avg=Math.round(results.reduce((a,b)=>a+b,0)/results.length);if(!best||avg<+best){best=avg;localStorage.setItem('react_best',best);document.getElementById('best').textContent=best+'ms'}document.getElementById('finalMs').textContent=avg;document.getElementById('finalTimes').innerHTML=results.map((r,i)=>'<span>R'+(i+1)+': '+r+'ms</span>').join('');const r=avg<=200?'Superhuman!':avg<=250?'Lightning!':avg<=300?'Fast!':avg<=400?'Average':'Keep practicing!';document.getElementById('summary').innerHTML='<span style="font-size:1.2rem">'+r+'</span>'+(avg<=+best?' <span class="high">★ New Best!</span>':'');show('result')}
2044
- </script></body></html>`;
2045
- }
2046
-
2047
- function typingGameHTML(title) {
2048
- return `<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1.0,user-scalable=no"><title>${title || 'Typing Game'}</title>
2049
- <style>${GAME_CSS_BASE}
2050
- .word-display{font-size:2rem;font-weight:700;letter-spacing:.1em;margin:1.5rem 0;min-height:3rem;display:flex;justify-content:center;gap:2px;flex-wrap:wrap}
2051
- .word-display .ch{transition:color .1s}
2052
- .word-display .correct{color:var(--accent)}
2053
- .word-display .wrong{color:var(--secondary)}
2054
- .word-display .cursor{border-bottom:3px solid var(--primary);animation:blink 1s step-end infinite}
2055
- @keyframes blink{50%{border-color:transparent}}
2056
- input.type-input{background:var(--surface);border:2px solid var(--primary);border-radius:12px;padding:.75rem 1rem;font-size:1.2rem;color:var(--text);width:100%;text-align:center;outline:none;caret-color:var(--primary)}
2057
- .word-queue{color:var(--muted);font-size:1rem;margin:.5rem 0;height:2em;overflow:hidden;display:flex;gap:1rem;justify-content:center;flex-wrap:wrap}
2058
- </style></head><body>
2059
- <div class="container">
2060
- <h1>${title || 'Typing Speed'}</h1>
2061
- <div id="state-menu">
2062
- <p style="color:var(--muted);margin:1rem 0">Type as many words as you can in 30 seconds</p>
2063
- <div class="stats"><span>Best WPM<b id="best">0</b></span></div>
2064
- <button class="btn" onclick="startGame()">Start</button>
2065
- </div>
2066
- <div id="state-play" class="hidden">
2067
- <div class="stats"><span>Time<b id="timer">30</b></span><span>Words<b id="words">0</b></span><span>WPM<b id="wpm">0</b></span></div>
2068
- <div class="timer-bar"><div class="fill" id="bar" style="width:100%"></div></div>
2069
- <div class="word-display" id="display"></div>
2070
- <div class="word-queue" id="queue"></div>
2071
- <input class="type-input" id="inp" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false" placeholder="Type here...">
2072
- </div>
2073
- <div id="state-result" class="hidden">
2074
- <div class="score" id="finalWpm">0</div>
2068
+ <div id="msg" style="margin:1rem 0"></div>
2069
+ <div class="times" id="ftimes"></div>
2070
+ <button class="btn" style="margin-top:1rem" onclick="go()">Try Again</button></div>`;
2071
+
2072
+ const js = [jsCore('react_best'),
2073
+ `if(best)$('best').textContent=best+'ms';`,
2074
+ `const RDS=${rounds};let rd,res,phase,gAt,tmr;`,
2075
+ `function go(){rd=0;res=[];$('times').innerHTML='';show('play');nr()}`,
2076
+ `function nr(){if(rd>=RDS)return end();$('rnd').textContent=(rd+1)+'/'+RDS;const z=$('zone');z.className='zone ready';$('ztxt').textContent='Wait for green...';phase='wait';clearTimeout(tmr);tmr=setTimeout(()=>{z.className='zone go';$('ztxt').textContent='TAP NOW!';gAt=Date.now();phase='go'},1500+Math.random()*3000)}`,
2077
+ `function tap(){if(phase==='wait'){clearTimeout(tmr);$('zone').className='zone early';$('ztxt').textContent='Too early! Tap to retry';phase='early'}else if(phase==='early'){nr()}else if(phase==='go'){const ms=Date.now()-gAt;res.push(ms);const t=document.createElement('span');t.textContent=ms+'ms';$('times').appendChild(t);$('avg').textContent=Math.round(res.reduce((a,b)=>a+b,0)/res.length)+'ms';rd++;phase='done';setTimeout(nr,600)}}`,
2078
+ `function end(){const avg=Math.round(res.reduce((a,b)=>a+b,0)/res.length);const nb=saveBest(avg);if(nb)$('best').textContent=avg+'ms';$('fin').textContent=avg;$('ftimes').innerHTML=res.map((r,i)=>'<span>R'+(i+1)+': '+r+'ms</span>').join('');const r=avg<=200?'Superhuman!':avg<=250?'Lightning!':avg<=300?'Fast!':avg<=400?'Average':'Keep practicing!';$('msg').innerHTML='<span style="font-size:1.2rem">'+r+'</span>'+(nb?' <span class=hi>★ New Best!</span>':'');show('end')}`,
2079
+ ].join('\n');
2080
+
2081
+ return htmlWrap(title, css, html, js);
2082
+ }
2083
+
2084
+ // ── Typing Game Builder ──
2085
+
2086
+ function buildTyping(title, opts) {
2087
+ const p = GAME_PALETTES.typing;
2088
+ const dur = opts.duration || 30;
2089
+
2090
+ const css = [cssVars(p), cssBase(p), cssTimerBar(p),
2091
+ `.wd{font-size:2rem;font-weight:700;letter-spacing:.1em;margin:1.5rem 0;min-height:3rem;display:flex;justify-content:center;gap:2px;flex-wrap:wrap}`,
2092
+ `.wd .c{transition:color .1s}.wd .y{color:${p.ac}}.wd .n{color:#F87171}`,
2093
+ `.wd .cur{border-bottom:3px solid ${p.pr};animation:blink 1s step-end infinite}`,
2094
+ `@keyframes blink{50%{border-color:transparent}}`,
2095
+ `.ti{background:${p.sf};border:2px solid ${p.pr};border-radius:12px;padding:.75rem 1rem;font-size:1.2rem;color:${p.tx};width:100%;text-align:center;outline:none;caret-color:${p.pr};font-family:inherit}`,
2096
+ `.wq{color:${p.mt};font-size:1rem;margin:.5rem 0;height:2em;overflow:hidden;display:flex;gap:1rem;justify-content:center;flex-wrap:wrap}`,
2097
+ ].join('\n');
2098
+
2099
+ const html = `<div id="s-menu">
2100
+ <p style="color:${p.mt};margin:1rem 0">Type as many words as you can in ${dur} seconds</p>
2101
+ <div class="st"><span>Best WPM<b id="best">0</b></span></div>
2102
+ <button class="btn" onclick="go()">Start</button></div>
2103
+ <div id="s-play" class="hd">
2104
+ <div class="st"><span>Time<b id="timer">${dur}</b></span><span>Words<b id="wc">0</b></span><span>WPM<b id="wpm">0</b></span></div>
2105
+ <div class="bar"><div class="fl" id="bar" style="width:100%"></div></div>
2106
+ <div class="wd" id="disp"></div>
2107
+ <div class="wq" id="queue"></div>
2108
+ <input class="ti" id="inp" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false" placeholder="Type here..."></div>
2109
+ <div id="s-end" class="hd">
2110
+ <div class="sc" id="fin">0</div>
2075
2111
  <p style="font-size:1.2rem;margin:.5rem 0">WPM</p>
2076
- <div id="summary" style="font-size:1.1rem;margin:1rem 0"></div>
2077
- <button class="btn" onclick="startGame()">Play Again</button>
2078
- </div>
2079
- </div>
2080
- <script>
2081
- const WORDS=["the","be","to","of","and","in","that","have","it","for","not","on","with","he","as","you","do","at","this","but","his","by","from","they","we","say","her","she","or","an","will","my","one","all","would","there","their","what","so","up","out","if","about","who","get","which","go","me","when","make","can","like","time","no","just","him","know","take","people","into","year","your","good","some","could","them","see","other","than","then","now","look","only","come","its","over","think","also","back","after","use","two","how","our","work","first","well","way","even","new","want","give","most","find","here","thing","many","code","fast","type","game","play","next","open","read","test","data","file","line","word","text","long","name","part","best","help","made","move","hand","high","keep","last","same","tell","does","set","each","much","head","turn","real","show","full","form","left","start","might","run","need","home","life","old","big","end","point","still","call","live","away","right","hard","plan","team","late","mind","wait","stop","must","land","close","draw","press","mark","step","pick","rock","blue","deep","dark","hold","four","week","room","free","fall","base","rest","less","note","hear","ever"];
2082
- const DUR=30000;let pool,cur,pos,done,startT,tmr,best=+(localStorage.getItem('type_best')||0);
2083
- document.getElementById('best').textContent=best;
2084
- function show(id){document.querySelectorAll('[id^=state-]').forEach(e=>e.classList.add('hidden'));document.getElementById('state-'+id).classList.remove('hidden')}
2085
- function shuffle(a){for(let i=a.length-1;i>0;i--){const j=Math.floor(Math.random()*(i+1));[a[i],a[j]]=[a[j],a[i]]}return a}
2086
- function startGame(){pool=shuffle([...WORDS]);cur=0;pos=0;done=0;startT=Date.now();document.getElementById('words').textContent=0;document.getElementById('wpm').textContent=0;const inp=document.getElementById('inp');inp.value='';show('play');inp.focus();renderWord();tmr=setInterval(tick,100)}
2087
- function renderWord(){const w=pool[cur%pool.length];const d=document.getElementById('display');d.innerHTML=w.split('').map((c,i)=>{let cls=i<pos?(document.getElementById('inp').value[i]===c?'correct':'wrong'):'';if(i===pos)cls+=' cursor';return '<span class="ch '+cls+'">'+c+'</span>'}).join('');const q=document.getElementById('queue');q.innerHTML='';for(let i=1;i<=5;i++){const nw=pool[(cur+i)%pool.length];q.innerHTML+='<span>'+nw+'</span>'}}
2088
- function tick(){const el=Date.now()-startT;const pct=Math.max(0,1-el/DUR)*100;document.getElementById('bar').style.width=pct+'%';document.getElementById('timer').textContent=Math.max(0,Math.ceil((DUR-el)/1000));if(el>=DUR)endGame()}
2089
- document.getElementById('inp').addEventListener('input',function(){if(Date.now()-startT>DUR)return;const w=pool[cur%pool.length];const v=this.value;pos=v.length;if(v.endsWith(' ')||v.length>=w.length){if(v.trim()===w){done++;document.getElementById('words').textContent=done;const wpm=Math.round(done/((Date.now()-startT)/60000));document.getElementById('wpm').textContent=wpm}cur++;pos=0;this.value=''}renderWord()});
2090
- function endGame(){clearInterval(tmr);document.getElementById('inp').blur();const wpm=Math.round(done/((DUR)/60000));if(wpm>best){best=wpm;localStorage.setItem('type_best',best);document.getElementById('best').textContent=best}document.getElementById('finalWpm').textContent=wpm;const r=wpm>=80?'Blazing fast!':wpm>=60?'Excellent!':wpm>=40?'Good!':wpm>=20?'Decent!':'Keep practicing!';document.getElementById('summary').innerHTML=r+'<br>'+done+' words in 30s'+(wpm>=best&&wpm>0?' <span class="high">★ New Best!</span>':'');show('result')}
2091
- </script></body></html>`;
2092
- }
2093
-
2094
- function breakoutGameHTML(title) {
2095
- return `<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1.0,user-scalable=no"><title>${title || 'Breakout'}</title>
2096
- <style>${GAME_CSS_BASE}
2097
- canvas{border-radius:12px;background:var(--surface);display:block;margin:1rem auto;touch-action:none}
2098
- </style></head><body>
2099
- <div class="container">
2100
- <h1>${title || 'Breakout'}</h1>
2101
- <div id="state-menu">
2102
- <p style="color:var(--muted);margin:1rem 0">Move the paddle to break all blocks</p>
2103
- <div class="stats"><span>Best<b id="best">0</b></span></div>
2104
- <button class="btn" onclick="startGame()">Play</button>
2105
- </div>
2106
- <div id="state-play" class="hidden">
2107
- <div class="stats"><span>Score<b id="score">0</b></span><span>Lives<b id="lives">3</b></span></div>
2108
- <canvas id="cv" width="360" height="480"></canvas>
2109
- </div>
2110
- <div id="state-result" class="hidden">
2111
- <div class="score" id="final">0</div>
2112
- <div id="summary" style="font-size:1.1rem;margin:1rem 0"></div>
2113
- <button class="btn" onclick="startGame()">Play Again</button>
2114
- </div>
2115
- </div>
2116
- <script>
2117
- const cv=document.getElementById('cv'),ctx=cv.getContext('2d'),W=360,H=480;
2118
- const COLS=8,ROWS=5,BW=W/COLS-4,BH=18,PAD_W=70,PAD_H=12,BR=6;
2119
- let px,ball,dx,dy,bricks,sc,lives,best=+(localStorage.getItem('brk_best')||0),raf;
2120
- document.getElementById('best').textContent=best;
2121
- function show(id){document.querySelectorAll('[id^=state-]').forEach(e=>e.classList.add('hidden'));document.getElementById('state-'+id).classList.remove('hidden')}
2122
- const COLORS=['#6C63FF','#8B5CF6','#A78BFA','#FF6584','#00D4AA'];
2123
- function initBricks(){bricks=[];for(let r=0;r<ROWS;r++)for(let c=0;c<COLS;c++)bricks.push({x:c*(BW+4)+2,y:r*(BH+4)+40,w:BW,h:BH,color:COLORS[r],alive:true})}
2124
- function startGame(){px=W/2;ball={x:W/2,y:H-60};dx=3*(Math.random()>.5?1:-1);dy=-3.5;sc=0;lives=3;initBricks();document.getElementById('score').textContent=0;document.getElementById('lives').textContent=3;show('play');cancelAnimationFrame(raf);tick()}
2125
- function tick(){update();draw();if(lives>0&&bricks.some(b=>b.alive))raf=requestAnimationFrame(tick);else endGame()}
2126
- function update(){ball.x+=dx;ball.y+=dy;if(ball.x<=BR||ball.x>=W-BR)dx=-dx;if(ball.y<=BR)dy=-dy;if(ball.y>=H-PAD_H-BR-8&&ball.y<=H-8&&ball.x>=px-PAD_W/2&&ball.x<=px+PAD_W/2){dy=-Math.abs(dy);dx+=((ball.x-px)/(PAD_W/2))*2}if(ball.y>H){lives--;document.getElementById('lives').textContent=lives;if(lives>0){ball={x:W/2,y:H-60};dx=3*(Math.random()>.5?1:-1);dy=-3.5}}for(const b of bricks){if(!b.alive)continue;if(ball.x+BR>b.x&&ball.x-BR<b.x+b.w&&ball.y+BR>b.y&&ball.y-BR<b.y+b.h){b.alive=false;dy=-dy;sc+=10;document.getElementById('score').textContent=sc}}}
2127
- function draw(){ctx.clearRect(0,0,W,H);for(const b of bricks){if(!b.alive)continue;ctx.fillStyle=b.color;ctx.beginPath();ctx.roundRect(b.x,b.y,b.w,b.h,4);ctx.fill()}ctx.fillStyle='#6C63FF';ctx.beginPath();ctx.roundRect(px-PAD_W/2,H-PAD_H-8,PAD_W,PAD_H,6);ctx.fill();ctx.fillStyle='#FF6584';ctx.beginPath();ctx.arc(ball.x,ball.y,BR,0,Math.PI*2);ctx.fill()}
2128
- function endGame(){cancelAnimationFrame(raf);const won=!bricks.some(b=>b.alive);if(sc>best){best=sc;localStorage.setItem('brk_best',best);document.getElementById('best').textContent=best}document.getElementById('final').textContent=sc;document.getElementById('summary').innerHTML=(won?'You cleared it!':'Game Over')+(sc>=best&&sc>0?' <span class="high">★ New Best!</span>':'');show('result')}
2129
- cv.addEventListener('mousemove',e=>{const r=cv.getBoundingClientRect();px=Math.max(PAD_W/2,Math.min(W-PAD_W/2,(e.clientX-r.left)*(W/r.width)))});
2130
- cv.addEventListener('touchmove',e=>{e.preventDefault();const r=cv.getBoundingClientRect();px=Math.max(PAD_W/2,Math.min(W-PAD_W/2,(e.touches[0].clientX-r.left)*(W/r.width)))},{passive:false});
2131
- document.addEventListener('keydown',e=>{if(e.key==='ArrowLeft')px=Math.max(PAD_W/2,px-20);if(e.key==='ArrowRight')px=Math.min(W-PAD_W/2,px+20)});
2132
- </script></body></html>`;
2133
- }
2134
-
2135
- const GAME_GENERATORS = {
2136
- tapping: tappingGameHTML,
2137
- quiz: quizGameHTML,
2138
- memory: memoryGameHTML,
2139
- snake: snakeGameHTML,
2140
- reaction: reactionGameHTML,
2141
- typing: typingGameHTML,
2142
- breakout: breakoutGameHTML,
2112
+ <div id="msg" style="font-size:1.1rem;margin:1rem 0"></div>
2113
+ <button class="btn" onclick="go()">Play Again</button></div>`;
2114
+
2115
+ const js = [jsCore('type_best'),
2116
+ `const WDS=["the","be","to","and","in","that","have","for","not","with","you","this","but","from","they","say","she","or","will","my","one","all","would","there","what","so","up","out","if","about","who","get","which","go","when","make","can","like","time","just","know","take","people","into","year","good","some","could","them","see","other","than","then","now","look","only","come","its","over","think","also","back","after","use","two","how","work","first","well","even","new","want","give","most","find","here","thing","code","fast","type","game","play","next","open","read","test","data","file","line","word","text","long","name","part","best","help","made","move","hand","high","keep","last","same","tell","set","each","much","turn","real","show","full","form","left","start","might","run","need","home","life","old","big","end","point","still","call","live","away","right","hard","plan","team","late","mind","wait","stop","must","land","close","draw","press","mark","step","pick","rock","blue","deep","dark","hold","four","week","room","free","fall","base","rest","less","note","hear"];`,
2117
+ `const DUR=${dur*1000};let pool,ci,pos,done,t0,tmr;`,
2118
+ `function shuf(a){for(let i=a.length-1;i>0;i--){const j=Math.floor(Math.random()*(i+1));[a[i],a[j]]=[a[j],a[i]]}return a}`,
2119
+ `function go(){pool=shuf([...WDS]);ci=0;pos=0;done=0;t0=Date.now();$('wc').textContent=0;$('wpm').textContent=0;$('inp').value='';show('play');$('inp').focus();render();tmr=setInterval(tick,100)}`,
2120
+ `function render(){const w=pool[ci%pool.length];const v=$('inp').value;$('disp').innerHTML=w.split('').map((c,i)=>{let cl=i<pos?(v[i]===c?'y':'n'):'';if(i===pos)cl+=' cur';return'<span class="c '+cl+'">'+c+'</span>'}).join('');$('queue').innerHTML='';for(let i=1;i<=5;i++)$('queue').innerHTML+='<span>'+pool[(ci+i)%pool.length]+'</span>'}`,
2121
+ `function tick(){const e=Date.now()-t0,pc=Math.max(0,1-e/DUR)*100;$('bar').style.width=pc+'%';$('timer').textContent=Math.max(0,Math.ceil((DUR-e)/1000));if(e>=DUR)end()}`,
2122
+ `$('inp').addEventListener('input',function(){if(Date.now()-t0>DUR)return;const w=pool[ci%pool.length],v=this.value;pos=v.length;if(v.endsWith(' ')||v.length>=w.length){if(v.trim()===w){done++;$('wc').textContent=done;$('wpm').textContent=Math.round(done/((Date.now()-t0)/60000))}ci++;pos=0;this.value=''}render()});`,
2123
+ `function end(){clearInterval(tmr);$('inp').blur();const wpm=Math.round(done/(DUR/60000));const nb=saveBest(wpm);$('fin').textContent=wpm;const r=wpm>=80?'Blazing!':wpm>=60?'Excellent!':wpm>=40?'Good!':wpm>=20?'Decent!':'Keep practicing!';$('msg').innerHTML=r+'<br>'+done+' words in ${dur}s'+(nb&&wpm>0?' <span class=hi>★ New Best!</span>':'');show('end')}`,
2124
+ ].join('\n');
2125
+
2126
+ return htmlWrap(title, css, html, js);
2127
+ }
2128
+
2129
+ // ── Breakout Game Builder ──
2130
+
2131
+ function buildBreakout(title, opts) {
2132
+ const p = GAME_PALETTES.breakout;
2133
+ const COLS = 8, ROWS = opts.difficulty === 'hard' ? 7 : 5;
2134
+
2135
+ const css = [cssVars(p), cssBase(p),
2136
+ `canvas{border-radius:12px;background:${p.sf};display:block;margin:1rem auto;touch-action:none}`,
2137
+ ].join('\n');
2138
+
2139
+ const html = `<div id="s-menu">
2140
+ <p style="color:${p.mt};margin:1rem 0">Move the paddle to break all ${COLS * ROWS} blocks</p>
2141
+ <div class="st"><span>Best<b id="best">0</b></span></div>
2142
+ <button class="btn" onclick="go()">Play</button></div>
2143
+ <div id="s-play" class="hd">
2144
+ <div class="st"><span>Score<b id="score">0</b></span><span>Lives<b id="lives">3</b></span></div>
2145
+ <canvas id="cv" width="360" height="480"></canvas></div>
2146
+ <div id="s-end" class="hd">
2147
+ <div class="sc" id="fin">0</div>
2148
+ <div id="msg" style="font-size:1.1rem;margin:1rem 0"></div>
2149
+ <button class="btn" onclick="go()">Play Again</button></div>`;
2150
+
2151
+ const brickColors = `['${p.pr}','${p.sc}','#A78BFA','${p.ac}','#EC4899']`;
2152
+ const js = [jsCore('brk_best'),
2153
+ `const cv=$('cv'),ctx=cv.getContext('2d'),CW=360,CH=480,COLS=${COLS},ROWS=${ROWS},BW=CW/COLS-4,BH=18,PW=70,PH=12,BR=6;`,
2154
+ `const BC=${brickColors};`,
2155
+ `let px,ball,dx,dy,bricks,sc,lives,raf;`,
2156
+ `function mkBricks(){bricks=[];for(let r=0;r<ROWS;r++)for(let c=0;c<COLS;c++)bricks.push({x:c*(BW+4)+2,y:r*(BH+4)+40,w:BW,h:BH,color:BC[r%BC.length],alive:true})}`,
2157
+ `function go(){px=CW/2;ball={x:CW/2,y:CH-60};dx=3*(Math.random()>.5?1:-1);dy=-3.5;sc=0;lives=3;mkBricks();$('score').textContent=0;$('lives').textContent=3;show('play');cancelAnimationFrame(raf);tick()}`,
2158
+ `function tick(){update();draw();if(lives>0&&bricks.some(b=>b.alive))raf=requestAnimationFrame(tick);else end()}`,
2159
+ `function update(){ball.x+=dx;ball.y+=dy;if(ball.x<=BR||ball.x>=CW-BR)dx=-dx;if(ball.y<=BR)dy=-dy;if(ball.y>=CH-PH-BR-8&&ball.y<=CH-8&&ball.x>=px-PW/2&&ball.x<=px+PW/2){dy=-Math.abs(dy);dx+=((ball.x-px)/(PW/2))*2}if(ball.y>CH){lives--;$('lives').textContent=lives;if(lives>0){ball={x:CW/2,y:CH-60};dx=3*(Math.random()>.5?1:-1);dy=-3.5}}for(const b of bricks){if(!b.alive)continue;if(ball.x+BR>b.x&&ball.x-BR<b.x+b.w&&ball.y+BR>b.y&&ball.y-BR<b.y+b.h){b.alive=false;dy=-dy;sc+=10;$('score').textContent=sc}}}`,
2160
+ `function draw(){ctx.clearRect(0,0,CW,CH);for(const b of bricks){if(!b.alive)continue;ctx.fillStyle=b.color;ctx.beginPath();ctx.roundRect(b.x,b.y,b.w,b.h,4);ctx.fill()}ctx.fillStyle='${p.pr}';ctx.beginPath();ctx.roundRect(px-PW/2,CH-PH-8,PW,PH,6);ctx.fill();ctx.fillStyle='${p.ac}';ctx.beginPath();ctx.arc(ball.x,ball.y,BR,0,Math.PI*2);ctx.fill()}`,
2161
+ `function end(){cancelAnimationFrame(raf);const won=!bricks.some(b=>b.alive);const nb=saveBest(sc);$('fin').textContent=sc;$('msg').innerHTML=(won?'You cleared it!':'Game Over')+(nb&&sc>0?' <span class=hi>★ New Best!</span>':'');show('end')}`,
2162
+ `cv.addEventListener('mousemove',e=>{const r=cv.getBoundingClientRect();px=Math.max(PW/2,Math.min(CW-PW/2,(e.clientX-r.left)*(CW/r.width)))});`,
2163
+ `cv.addEventListener('touchmove',e=>{e.preventDefault();const r=cv.getBoundingClientRect();px=Math.max(PW/2,Math.min(CW-PW/2,(e.touches[0].clientX-r.left)*(CW/r.width)))},{passive:false});`,
2164
+ `document.addEventListener('keydown',e=>{if(e.key==='ArrowLeft')px=Math.max(PW/2,px-20);if(e.key==='ArrowRight')px=Math.min(CW-PW/2,px+20)});`,
2165
+ ].join('\n');
2166
+
2167
+ return htmlWrap(title, css, html, js);
2168
+ }
2169
+
2170
+ // ── Game Router ──
2171
+
2172
+ const GAME_BUILDERS = {
2173
+ tapping: buildTapping, quiz: buildQuiz, memory: buildMemory,
2174
+ snake: buildSnake, reaction: buildReaction, typing: buildTyping, breakout: buildBreakout,
2143
2175
  };
2144
2176
 
2145
- function gameHTML(type, title) {
2146
- const gen = GAME_GENERATORS[type] || tappingGameHTML;
2147
- return gen(title);
2177
+ function gameHTML(type, title, rawInstruction) {
2178
+ const opts = extractGameOpts(rawInstruction || '');
2179
+ const builder = GAME_BUILDERS[type] || buildTapping;
2180
+ return builder(title || type, opts);
2148
2181
  }
2149
2182
 
2150
2183
  function gameNaideCode(title, port) {
@@ -2522,7 +2555,7 @@ export function generateProject(instruction, options = {}) {
2522
2555
  const gt = result.analysis.gameType;
2523
2556
  const title = result.analysis.gameTitle || gt;
2524
2557
  const projectName = safeName(gt) + '-game';
2525
- const html = gameHTML(gt, title);
2558
+ const html = gameHTML(gt, title, instruction);
2526
2559
 
2527
2560
  files.push({ path: 'index.html', content: html });
2528
2561
  files.push({ path: 'app.naide', content: result.code });