naider 1.24.0 → 1.24.2

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.0';
11
+ const NAIDE_VERSION = '1.24.2';
12
12
 
13
13
  function crashReport(err, context = {}) {
14
14
  const info = [
@@ -270,19 +270,28 @@ if (files[0] === 'gen' || files[0] === '~~') {
270
270
  result = generateProject(instruction);
271
271
  }
272
272
 
273
- const primaryName = (result.entities[0] || 'app').toLowerCase().replace(/[^a-z0-9]/g, '');
274
- if (!flags.output && !isUpdate) dir = primaryName + '-app';
273
+ const isGame = result.analysis && result.analysis.gameType;
274
+ if (isGame && !flags.output && !isUpdate) {
275
+ dir = result.analysis.gameType + '-game';
276
+ } else {
277
+ const primaryName = (result.entities[0] || 'app').toLowerCase().replace(/[^a-z0-9]/g, '');
278
+ if (!flags.output && !isUpdate) dir = primaryName + '-app';
279
+ }
275
280
  const finalDir = resolve(dir);
276
281
  if (!existsSync(finalDir)) mkdirSync(finalDir, { recursive: true });
277
282
  for (const f of result.files) {
278
283
  const fp = resolve(finalDir, f.path);
284
+ const fpDir = resolve(fp, '..');
285
+ if (!existsSync(fpDir)) mkdirSync(fpDir, { recursive: true });
279
286
  writeFileSync(fp, f.content, 'utf-8');
280
287
  }
281
288
  const a = result.analysis;
282
289
  const mode = result.action === 'removed' ? 'Removed' : isUpdate ? 'Updated' : 'Created';
283
290
  process.stderr.write(`\n ── NAIDE Agent (${mode}) ────────────────────────────────\n`);
284
291
  process.stderr.write(` Input: "${a.instruction || instruction}"\n`);
285
- if (a.entities.length > 0) {
292
+ if (a.gameType) {
293
+ process.stderr.write(` Type: Game (${a.gameType})\n`);
294
+ } else if (a.entities.length > 0) {
286
295
  process.stderr.write(` Entities: ${a.entities.map(e => `${e.name} (${e.fieldCount} fields)`).join(', ')}\n`);
287
296
  }
288
297
  if (a.relationships && a.relationships.length > 0) {
@@ -300,7 +309,8 @@ if (files[0] === 'gen' || files[0] === '~~') {
300
309
  process.stderr.write(' ────────────────────────────────────────────────────────\n\n');
301
310
  console.log(` Project ${mode.toLowerCase()}: ${dir}/`);
302
311
  for (const f of result.files) console.log(` ${f.path}`);
303
- if (!isUpdate) console.log(`\n cd ${dir} && npm install && npm run dev\n`);
312
+ if (isGame && !isUpdate) console.log(`\n Open ${dir}/index.html in a browser, or:\n cd ${dir} && npm install && npm run dev\n`);
313
+ else if (!isUpdate) console.log(`\n cd ${dir} && npm install && npm run dev\n`);
304
314
  else console.log(`\n Files regenerated. Run: npm run dev\n`);
305
315
  process.exit(0);
306
316
  }
@@ -1081,10 +1091,11 @@ if (flags.help) {
1081
1091
  -h, --help Show this help
1082
1092
 
1083
1093
  ~~ Keyword Cheat Sheet:
1084
- Intent server api rest bot cli page test database ai crud auth ws mail graphql
1094
+ Intent server api rest bot cli page test database ai crud auth ws mail graphql game
1085
1095
  Composite todo blog chat shop fullstack board
1096
+ Game tapping quiz memory snake typing reaction breakout
1086
1097
  Platform discord slack telegram line
1087
- JP Intent サーバー 認証 ログイン 会員 CRUD 管理 データベース ボット テスト ページ
1098
+ JP Intent サーバー 認証 ログイン 会員 CRUD 管理 データベース ボット テスト ページ ゲーム
1088
1099
  JP Entity ユーザー 商品 記事 タスク 注文 コメント イベント 問い合わせ 掲示板 決済
1089
1100
  Fields "with name email age done" → auto-inferred types
1090
1101
  `);
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.0' }
195
+ serverInfo: { name: 'naide-mcp', version: '1.24.2' }
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.0",
3
+ "version": "1.24.2",
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
@@ -20,6 +20,7 @@ const INTENT_WORDS = {
20
20
  websocket: ['websocket', 'ws', 'realtime', 'real-time', 'socket', 'live', 'push-notification', 'pubsub', 'pub-sub'],
21
21
  mail: ['mail', 'smtp', 'mailer', 'sendmail', 'newsletter', 'notification'],
22
22
  graphql: ['graphql', 'gql', 'query', 'mutation', 'resolver', 'subscription'],
23
+ game: ['game', 'arcade', 'play', 'score', 'highscore', 'player', 'level', 'gameover'],
23
24
  };
24
25
 
25
26
  const JA_INTENTS = {
@@ -40,6 +41,7 @@ const JA_INTENTS = {
40
41
  websocket: ['ウェブソケット', 'リアルタイム', 'ソケット', 'WebSocket', 'websocket',
41
42
  'リアルタイム通信', 'プッシュ通知', 'ライブ'],
42
43
  mail: ['メール', 'メール送信', 'メール機能', 'SMTP', '通知メール', 'メール通知'],
44
+ game: ['ゲーム', 'アーケード', 'プレイ', 'スコア', 'ハイスコア', 'ゲームオーバー'],
43
45
  };
44
46
 
45
47
  const JA_ENTITIES = {
@@ -90,12 +92,25 @@ const COMPOSITE_WORDS = {
90
92
  sns: ['sns', 'SNS', 'ソーシャル', 'social', 'social-media', 'ソーシャルメディア'],
91
93
  };
92
94
 
95
+ const GAME_TYPES = {
96
+ tapping: ['tapping', 'tap', 'clicker', 'click', 'タップ', 'タッピング', 'クリッカー', '連打'],
97
+ quiz: ['quiz', 'trivia', 'question', 'クイズ', 'トリビア', '問題'],
98
+ memory: ['memory', 'matching', 'pairs', 'card', '神経衰弱', 'メモリー', 'カードゲーム'],
99
+ typing: ['typing', 'タイピング', 'タイプ'],
100
+ reaction: ['reaction', 'reflex', 'リアクション', '反射', '反応'],
101
+ snake: ['snake', 'スネーク', 'ヘビ'],
102
+ breakout: ['breakout', 'brick', 'block', 'ブロック崩し', 'ブレイクアウト'],
103
+ };
104
+
93
105
  const NOISE = new Set([
94
106
  'a', 'an', 'the', 'for', 'with', 'and', 'or', 'on', 'at', 'to', 'of', 'in', 'by',
95
107
  'that', 'which', 'this', 'it', 'its', 'my', 'is', 'are', 'has', 'have', 'do', 'does',
96
108
  'make', 'create', 'build', 'generate', 'add', 'implement', 'simple', 'basic', 'new', 'remove', 'delete', 'drop',
97
109
  'please', 'want', 'need', 'like', 'just', 'some', 'using', 'use', 'based',
98
110
  'setup', 'set', 'up', 'get', 'should', 'can', 'could', 'would',
111
+ 'game', 'play', 'player', 'score', 'level', 'arcade', 'highscore',
112
+ 'tapping', 'tap', 'clicker', 'click', 'quiz', 'trivia', 'memory', 'matching', 'typing',
113
+ 'reaction', 'reflex', 'snake', 'breakout', 'brick', 'block', 'pong', 'puzzle',
99
114
  'の', 'を', 'に', 'で', 'は', 'が', 'と', 'も', 'から', 'まで', 'より', 'って',
100
115
  'な', 'ような', 'みたいな', 'っぽい', 'ような感じ',
101
116
  '作って', '作る', '作成', '生成', '追加', '実装', 'ください', 'して', 'つけて',
@@ -104,6 +119,16 @@ const NOISE = new Set([
104
119
  '簡単な', 'シンプルな', '基本的な', '新しい',
105
120
  ]);
106
121
 
122
+ function detectGameType(words, raw) {
123
+ const lower = raw.toLowerCase();
124
+ for (const [type, kws] of Object.entries(GAME_TYPES)) {
125
+ for (const kw of kws) {
126
+ if (lower.includes(kw)) return type;
127
+ }
128
+ }
129
+ return 'tapping';
130
+ }
131
+
107
132
  // ── Field Normalization ─────────────────────────────────────
108
133
 
109
134
  const FIELD_SYNONYMS = {
@@ -493,6 +518,12 @@ function classify(words, raw) {
493
518
  if (intents.has('schema') && intents.has('server')) intents.add('database');
494
519
  if (intents.has('bot') && !mods.platform) mods.platform = 'discord';
495
520
 
521
+ // Game intent: don't auto-cascade into server/crud unless explicitly requested
522
+ if (intents.has('game') && !intents.has('server') && !intents.has('crud')) {
523
+ mods.gameType = detectGameType(words, raw);
524
+ mods.isGame = true;
525
+ }
526
+
496
527
  // Default fallback: if nothing detected, server + schema
497
528
  if (intents.size === 0) {
498
529
  intents.add('server'); intents.add('schema'); intents.add('crud'); intents.add('database');
@@ -507,6 +538,12 @@ function extractParams(words, raw, intents, mods) {
507
538
  const params = { port: 3000, schemaName: null, fields: [], entities: [], relationships: [],
508
539
  features: intents, platform: mods.platform || null, dbType: mods.dbType || null, commands: [] };
509
540
 
541
+ if (mods.isGame) {
542
+ params.gameType = mods.gameType;
543
+ params.gameTitle = raw.replace(/\b(make|create|build|generate|simple|basic|new|a|an|the)\b/gi, '').trim();
544
+ return params;
545
+ }
546
+
510
547
  // Extract port number
511
548
  for (let i = 0; i < words.length; i++) {
512
549
  const n = parseInt(words[i]);
@@ -1159,7 +1196,8 @@ function validatedCreateRoute(entity) {
1159
1196
  function swaggerUIRoute() {
1160
1197
  return [
1161
1198
  `get "/docs" (req, res):`,
1162
- ` str html = "<!DOCTYPE html><html><head><title>API Docs</title><link rel=stylesheet href=https://unpkg.com/swagger-ui-dist/swagger-ui.css></head><body><div id=swagger-ui></div><script src=https://unpkg.com/swagger-ui-dist/swagger-ui-bundle.js></script><script>SwaggerUIBundle({url:'/api/openapi.json',dom_id:'#swagger-ui'})</script></body></html>"`,
1199
+ ` str swaggerOpts = JSON.stringify({url:"/api/openapi.json",dom_id:"#swagger-ui"})`,
1200
+ ` str html = "<!DOCTYPE html><html><head><title>API Docs</title><link rel=stylesheet href=https://unpkg.com/swagger-ui-dist/swagger-ui.css></head><body><div id=swagger-ui></div><script src=https://unpkg.com/swagger-ui-dist/swagger-ui-bundle.js></script><script>SwaggerUIBundle(" + swaggerOpts + ")</script></body></html>"`,
1163
1201
  ` res.type("html")`,
1164
1202
  ` ret html`,
1165
1203
  ];
@@ -1743,7 +1781,390 @@ function migrationFile(entities, relationships, m2mRels) {
1743
1781
 
1744
1782
  // ── Code Composer ───────────────────────────────────────────
1745
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)}
1800
+ @keyframes pop{0%{transform:scale(1)}50%{transform:scale(1.15)}100%{transform:scale(1)}}
1801
+ @keyframes fadeUp{0%{opacity:1;transform:translateY(0)}100%{opacity:0;transform:translateY(-60px)}}`;
1802
+
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>
1835
+ </div>
1836
+ <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>
1938
+ <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>
2029
+ <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>
2075
+ <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,
2143
+ };
2144
+
2145
+ function gameHTML(type, title) {
2146
+ const gen = GAME_GENERATORS[type] || tappingGameHTML;
2147
+ return gen(title);
2148
+ }
2149
+
2150
+ function gameNaideCode(title, port) {
2151
+ return `server game port ${port}:
2152
+ cors "*"
2153
+
2154
+ get "/" (req, res):
2155
+ res.sendFile("index.html", {root: "."})
2156
+
2157
+ get "/api/health" (req, res):
2158
+ ret {status: "ok", game: "${title || 'game'}"}
2159
+ `;
2160
+ }
2161
+
1746
2162
  function compose(intents, params) {
2163
+ // Game intent: generate minimal server code (game HTML is generated separately in project mode)
2164
+ if (params.gameType && intents.has('game') && !intents.has('crud')) {
2165
+ return gameNaideCode(params.gameTitle || params.gameType, params.port);
2166
+ }
2167
+
1747
2168
  const sections = [];
1748
2169
  const entities = params.entities.length > 0 ? params.entities : [{ name: params.schemaName || 'Item', fields: params.fields }];
1749
2170
  const primaryName = entities[0].name;
@@ -2071,8 +2492,9 @@ function heal(code, err) {
2071
2492
  // ── Suggestions ─────────────────────────────────────────────
2072
2493
 
2073
2494
  const KEYWORD_CHEATSHEET = {
2074
- 'Intent': 'server, api, rest, bot, cli, page, test, database, ai, crud, auth, websocket, mail, graphql',
2495
+ 'Intent': 'server, api, rest, bot, cli, page, test, database, ai, crud, auth, websocket, mail, graphql, game',
2075
2496
  'Composite': 'todo, blog, chat, shop, fullstack, board, crm, inventory, booking, sns',
2497
+ 'Game': 'tapping, quiz, memory, snake, typing, reaction, breakout',
2076
2498
  'Platform': 'discord, slack, telegram, line',
2077
2499
  'DB': 'sqlite, postgres',
2078
2500
  'Japanese': 'サーバー, 認証, ログイン, 会員, CRUD, 管理, データベース, ボット, テスト, ページ',
@@ -2094,6 +2516,31 @@ export function generateProject(instruction, options = {}) {
2094
2516
  const result = generate(instruction, options);
2095
2517
  const files = [];
2096
2518
  const intents = new Set(result.intents);
2519
+
2520
+ // Game project: generate standalone HTML game
2521
+ if (intents.has('game') && result.analysis && result.analysis.gameType) {
2522
+ const gt = result.analysis.gameType;
2523
+ const title = result.analysis.gameTitle || gt;
2524
+ const projectName = safeName(gt) + '-game';
2525
+ const html = gameHTML(gt, title);
2526
+
2527
+ files.push({ path: 'index.html', content: html });
2528
+ files.push({ path: 'app.naide', content: result.code });
2529
+ files.push({ path: 'package.json', content: JSON.stringify({
2530
+ name: projectName, version: '1.0.0', type: 'module',
2531
+ scripts: { dev: 'naide app.naide', start: 'naide app.naide', open: 'start index.html' },
2532
+ dependencies: { express: '^4.21.0', naider: '^1.21.0' },
2533
+ }, null, 2) + '\n' });
2534
+ files.push({ path: '.gitignore', content: 'node_modules/\n' });
2535
+ files.push({ path: 'README.md', content: `# ${title}\n\nGenerated by NAIDE Agent Coder (~~)\n\n## Play\n\nOpen \`index.html\` directly in a browser, or:\n\n\`\`\`bash\nnpm install && npm run dev\n\`\`\`\n\nThen visit http://localhost:${result.port}\n` });
2536
+
2537
+ return {
2538
+ ...result,
2539
+ files,
2540
+ analysis: { ...result.analysis, plan: [`Game: ${gt}`, `Standalone HTML + CSS + JS`, `High scores saved in localStorage`, `Touch & keyboard support`, `Server on port ${result.port}`] },
2541
+ };
2542
+ }
2543
+
2097
2544
  const entities = result.entities || [];
2098
2545
  const primaryName = (entities[0] || 'app').toLowerCase();
2099
2546
  const projectName = safeName(primaryName) + '-app';
@@ -2578,6 +3025,22 @@ export function expandDirectives(source) {
2578
3025
  function buildAnalysis(instruction, intents, params) {
2579
3026
  const plan = [];
2580
3027
 
3028
+ if (params.gameType) {
3029
+ plan.push(`Game: ${params.gameType}`);
3030
+ plan.push('Standalone HTML + CSS + JS');
3031
+ plan.push('High scores saved in localStorage');
3032
+ plan.push('Touch & keyboard support');
3033
+ return {
3034
+ instruction,
3035
+ entities: [],
3036
+ relationships: [],
3037
+ features: [...intents],
3038
+ plan,
3039
+ gameType: params.gameType,
3040
+ gameTitle: params.gameTitle,
3041
+ };
3042
+ }
3043
+
2581
3044
  if (params.entities.length > 0) {
2582
3045
  for (const e of params.entities) {
2583
3046
  plan.push(`Schema "${e.name}" (${e.fields.map(f => f.name).join(', ')})`);