naider 1.23.0 → 1.24.1
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 +60 -7
- package/mcp/server.js +1 -1
- package/package.json +1 -1
- package/src/gen.js +807 -18
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.
|
|
11
|
+
const NAIDE_VERSION = '1.24.1';
|
|
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
|
|
274
|
-
if (!flags.output && !isUpdate)
|
|
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.
|
|
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,11 +309,54 @@ 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
|
}
|
|
307
317
|
|
|
318
|
+
// Multi-target generation (D6)
|
|
319
|
+
if (flags.target && flags.target !== 'naide') {
|
|
320
|
+
const { generateForTarget, generateAllTargets } = await import('../src/gen.js');
|
|
321
|
+
if (flags.target === 'all') {
|
|
322
|
+
const result = await generateAllTargets(instruction);
|
|
323
|
+
if (!result.valid) {
|
|
324
|
+
process.stderr.write(` Error: generated NAIDE code is invalid\n`);
|
|
325
|
+
process.exit(1);
|
|
326
|
+
}
|
|
327
|
+
const succeeded = Object.entries(result.targets).filter(([, v]) => !v.error);
|
|
328
|
+
const failed = Object.entries(result.targets).filter(([, v]) => v.error);
|
|
329
|
+
process.stderr.write(`\n ── Multi-Target Generation (${succeeded.length}/${Object.keys(result.targets).length} targets) ──\n`);
|
|
330
|
+
for (const [t, v] of succeeded) {
|
|
331
|
+
if (flags.output) {
|
|
332
|
+
const outPath = flags.output.replace(/\.\w+$/, v.ext);
|
|
333
|
+
writeFileSync(outPath, v.code, 'utf-8');
|
|
334
|
+
console.log(` ${t}: ${outPath}`);
|
|
335
|
+
} else {
|
|
336
|
+
console.log(`\n ── ${t} (${v.ext}) ──\n`);
|
|
337
|
+
console.log(v.code);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
if (failed.length > 0) {
|
|
341
|
+
process.stderr.write(` Failed: ${failed.map(([t, v]) => `${t} (${v.error.slice(0, 50)})`).join(', ')}\n`);
|
|
342
|
+
}
|
|
343
|
+
process.exit(0);
|
|
344
|
+
}
|
|
345
|
+
const result = await generateForTarget(instruction, flags.target);
|
|
346
|
+
if (result.compiled) {
|
|
347
|
+
if (flags.output) {
|
|
348
|
+
writeFileSync(flags.output, result.compiled, 'utf-8');
|
|
349
|
+
console.log(` Generated: ${flags.output} (${flags.target})`);
|
|
350
|
+
} else {
|
|
351
|
+
console.log(result.compiled);
|
|
352
|
+
}
|
|
353
|
+
} else {
|
|
354
|
+
process.stderr.write(` Error compiling to ${flags.target}: ${result.compileError || 'unknown'}\n`);
|
|
355
|
+
console.log(result.code);
|
|
356
|
+
}
|
|
357
|
+
process.exit(0);
|
|
358
|
+
}
|
|
359
|
+
|
|
308
360
|
const result = generate(instruction);
|
|
309
361
|
|
|
310
362
|
if (flags.output) {
|
|
@@ -1039,10 +1091,11 @@ if (flags.help) {
|
|
|
1039
1091
|
-h, --help Show this help
|
|
1040
1092
|
|
|
1041
1093
|
~~ Keyword Cheat Sheet:
|
|
1042
|
-
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
|
|
1043
1095
|
Composite todo blog chat shop fullstack board
|
|
1096
|
+
Game tapping quiz memory snake typing reaction breakout
|
|
1044
1097
|
Platform discord slack telegram line
|
|
1045
|
-
JP Intent サーバー 認証 ログイン 会員 CRUD 管理 データベース ボット テスト ページ
|
|
1098
|
+
JP Intent サーバー 認証 ログイン 会員 CRUD 管理 データベース ボット テスト ページ ゲーム
|
|
1046
1099
|
JP Entity ユーザー 商品 記事 タスク 注文 コメント イベント 問い合わせ 掲示板 決済
|
|
1047
1100
|
Fields "with name email age done" → auto-inferred types
|
|
1048
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.
|
|
195
|
+
serverInfo: { name: 'naide-mcp', version: '1.24.1' }
|
|
196
196
|
});
|
|
197
197
|
break;
|
|
198
198
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "naider",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.24.1",
|
|
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
|
|
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
|
];
|
|
@@ -1202,13 +1240,48 @@ function passwordResetRoutes() {
|
|
|
1202
1240
|
|
|
1203
1241
|
// ── WebSocket Rooms (B11) ──────────────────────────────────
|
|
1204
1242
|
|
|
1205
|
-
function wsRoomsBlock() {
|
|
1206
|
-
|
|
1243
|
+
function wsRoomsBlock(hasAuth) {
|
|
1244
|
+
const lines = [
|
|
1207
1245
|
`ws "/ws":`,
|
|
1208
|
-
` on "connection" (socket):`,
|
|
1246
|
+
` on "connection" (socket, req):`,
|
|
1247
|
+
];
|
|
1248
|
+
if (hasAuth) {
|
|
1249
|
+
lines.push(
|
|
1250
|
+
` # WS auth: verify token from query string`,
|
|
1251
|
+
` str wsToken = ""`,
|
|
1252
|
+
` if req.url and req.url.includes("token="):`,
|
|
1253
|
+
` str qs = req.url.split("token=")[1] or ""`,
|
|
1254
|
+
` int ampIdx = qs.indexOf("&")`,
|
|
1255
|
+
` if ampIdx > 0:`,
|
|
1256
|
+
` wsToken = qs.slice(0, ampIdx)`,
|
|
1257
|
+
` else:`,
|
|
1258
|
+
` wsToken = qs`,
|
|
1259
|
+
` if wsToken:`,
|
|
1260
|
+
` any wsDecoded = jwt.verify(wsToken, JWT_SECRET)`,
|
|
1261
|
+
` if wsDecoded:`,
|
|
1262
|
+
` socket.user = wsDecoded`,
|
|
1263
|
+
` socket.authenticated = true`,
|
|
1264
|
+
` else:`,
|
|
1265
|
+
` socket.authenticated = false`,
|
|
1266
|
+
` socket.send(JSON.stringify({type: "error", message: "Invalid token"}))`,
|
|
1267
|
+
` socket.close()`,
|
|
1268
|
+
` else:`,
|
|
1269
|
+
` socket.authenticated = false`,
|
|
1270
|
+
);
|
|
1271
|
+
}
|
|
1272
|
+
lines.push(
|
|
1209
1273
|
` socket.room = "general"`,
|
|
1210
1274
|
` log("Client connected to room: general")`,
|
|
1211
1275
|
` on "message" (data, socket):`,
|
|
1276
|
+
);
|
|
1277
|
+
if (hasAuth) {
|
|
1278
|
+
lines.push(
|
|
1279
|
+
` if not socket.authenticated:`,
|
|
1280
|
+
` socket.send(JSON.stringify({type: "error", message: "Not authenticated"}))`,
|
|
1281
|
+
` ret`,
|
|
1282
|
+
);
|
|
1283
|
+
}
|
|
1284
|
+
lines.push(
|
|
1212
1285
|
` any msg = JSON.parse(data)`,
|
|
1213
1286
|
` if msg.type == "join":`,
|
|
1214
1287
|
` socket.room = msg.room`,
|
|
@@ -1221,7 +1294,8 @@ function wsRoomsBlock() {
|
|
|
1221
1294
|
` broadcast(msg.data)`,
|
|
1222
1295
|
` on "close" (socket):`,
|
|
1223
1296
|
` log("Client disconnected")`,
|
|
1224
|
-
|
|
1297
|
+
);
|
|
1298
|
+
return lines;
|
|
1225
1299
|
}
|
|
1226
1300
|
|
|
1227
1301
|
// ── Rate Limiting (B12) ────────────────────────────────────
|
|
@@ -1250,6 +1324,33 @@ function loggingBlock() {
|
|
|
1250
1324
|
next()`;
|
|
1251
1325
|
}
|
|
1252
1326
|
|
|
1327
|
+
// ── RBAC Middleware (D1) ──────────────────────────────────
|
|
1328
|
+
|
|
1329
|
+
function rbacBlock() {
|
|
1330
|
+
return `fn requireRole(list allowed):
|
|
1331
|
+
ret fn(req, res, next):
|
|
1332
|
+
if not req.user:
|
|
1333
|
+
ret.status(401) {error: "Not authenticated"}
|
|
1334
|
+
str userRole = req.user.role or "user"
|
|
1335
|
+
if not allowed.includes(userRole):
|
|
1336
|
+
ret.status(403) {error: "Insufficient permissions"}
|
|
1337
|
+
next()`;
|
|
1338
|
+
}
|
|
1339
|
+
|
|
1340
|
+
function rbacRouteGuards(entity) {
|
|
1341
|
+
const lower = entity.name.toLowerCase();
|
|
1342
|
+
const plural = pluralize(lower);
|
|
1343
|
+
return [
|
|
1344
|
+
`# Role guards for ${plural}`,
|
|
1345
|
+
`del "/api/${plural}/:id" (req, res):`,
|
|
1346
|
+
` str role = req.user.role or "user"`,
|
|
1347
|
+
` if role != "admin" and role != "moderator":`,
|
|
1348
|
+
` ret.status(403) {error: "Only admin/moderator can delete"}`,
|
|
1349
|
+
` ${entity.name}Store.remove(req.params.id)`,
|
|
1350
|
+
` ret {deleted: true}`,
|
|
1351
|
+
];
|
|
1352
|
+
}
|
|
1353
|
+
|
|
1253
1354
|
// ── Caching Headers (C18) ─────────────────────────────────
|
|
1254
1355
|
|
|
1255
1356
|
function cachingRoute() {
|
|
@@ -1288,7 +1389,11 @@ th{background:var(--bg);font-weight:600;font-size:.875rem;color:var(--muted)}
|
|
|
1288
1389
|
.search input{flex:1;padding:.5rem .75rem;border:1px solid var(--border);border-radius:var(--radius)}
|
|
1289
1390
|
.pagination{display:flex;gap:.5rem;justify-content:center;margin-top:1rem;align-items:center}
|
|
1290
1391
|
.toast{position:fixed;top:1rem;right:1rem;background:#22c55e;color:#fff;padding:.75rem 1.5rem;border-radius:var(--radius);opacity:0;transition:opacity .3s;z-index:999}
|
|
1291
|
-
.toast.show{opacity:1}.
|
|
1392
|
+
.toast.show{opacity:1}.toast.error{background:var(--danger)}.toast.warn{background:#f59e0b}
|
|
1393
|
+
.loading{text-align:center;padding:2rem;color:var(--muted)}.loading::after{content:"";display:inline-block;width:1.5rem;height:1.5rem;border:2px solid var(--border);border-top-color:var(--primary);border-radius:50%;animation:spin .6s linear infinite;margin-left:.5rem}
|
|
1394
|
+
@keyframes spin{to{transform:rotate(360deg)}}
|
|
1395
|
+
.error-banner{background:#fef2f2;border:1px solid #fecaca;color:#991b1b;padding:.75rem 1rem;border-radius:var(--radius);margin-bottom:1rem;font-size:.875rem}
|
|
1396
|
+
.modal-bg{position:fixed;inset:0;background:rgba(0,0,0,.4);display:flex;align-items:center;justify-content:center;z-index:100}
|
|
1292
1397
|
.modal{background:var(--card);border-radius:var(--radius);padding:2rem;width:90%;max-width:500px;box-shadow:0 20px 60px rgba(0,0,0,.2)}
|
|
1293
1398
|
.modal h3{margin-bottom:1rem}.modal-actions{display:flex;gap:.5rem;justify-content:flex-end;margin-top:1.5rem}
|
|
1294
1399
|
.badge{display:inline-block;padding:.15rem .5rem;border-radius:99px;font-size:.75rem;font-weight:500;background:var(--border)}
|
|
@@ -1338,30 +1443,36 @@ th{background:var(--bg);font-weight:600;font-size:.875rem;color:var(--muted)}
|
|
|
1338
1443
|
L.push('<script>');
|
|
1339
1444
|
L.push(`const API="";let currentPage={};let _token=null;let _isRegister=false;`);
|
|
1340
1445
|
L.push(`function authHeaders(){const h={"Content-Type":"application/json"};if(_token)h.Authorization="Bearer "+_token;return h}`);
|
|
1341
|
-
L.push(`function toast(m){const t=document.getElementById("toast");t.textContent=m;t.
|
|
1446
|
+
L.push(`function toast(m,type){const t=document.getElementById("toast");t.textContent=m;t.className="toast show"+(type?" "+type:"");setTimeout(()=>{t.classList.remove("show")},2500)}`);
|
|
1447
|
+
L.push(`function showError(msg){toast(msg,"error")}`);
|
|
1448
|
+
L.push(`async function apiFetch(url,opts){try{const r=await fetch(url,opts||{headers:authHeaders()});if(r.status===401){${hasAuth?'showError("Session expired — please log in again");doLogout();':''} return null}if(!r.ok){const e=await r.json().catch(()=>({error:"Request failed"}));showError(e.error||"Error "+r.status);return null}return await r.json()}catch(e){showError("Network error: "+e.message);return null}}`);
|
|
1449
|
+
L.push(`function setLoading(id,on){const el=document.getElementById(id);if(el)el.innerHTML=on?'<div class="loading">Loading...</div>':""}`);
|
|
1450
|
+
L.push(`function showBanner(containerId,msg){const el=document.getElementById(containerId);if(!el)return;let b=el.querySelector(".error-banner");if(!b){b=document.createElement("div");b.className="error-banner";el.prepend(b)}b.textContent=msg;setTimeout(()=>b.remove(),5000)}`);
|
|
1342
1451
|
L.push(`function showSection(name){document.querySelectorAll('[id^="section-"]').forEach(s=>s.classList.add("hidden"));const el=document.getElementById("section-"+name);if(el)el.classList.remove("hidden");document.querySelectorAll("nav a").forEach(a=>a.classList.remove("active"));const n=document.getElementById("nav-"+name);if(n)n.classList.add("active");if(window[name+"Load"])window[name+"Load"]()}`);
|
|
1343
1452
|
if (hasAuth) {
|
|
1344
1453
|
L.push(`function toggleAuthMode(){_isRegister=!_isRegister;document.getElementById("auth-title").textContent=_isRegister?"Register":"Login";document.getElementById("auth-name-group").classList.toggle("hidden",!_isRegister)}`);
|
|
1345
|
-
L.push(`async function doAuth(){const email=document.getElementById("auth-email").value;const password=document.getElementById("auth-password").value;const url=_isRegister?"/api/auth/register":"/api/auth/login";const body=_isRegister?{name:document.getElementById("auth-name").value,email,password}:{email,password};try{const r=await fetch(API+url,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(body)});const d=await r.json();if(d.token){_token=d.token;document.getElementById("user-info").textContent="Hi, "+(d.user?.name||email);document.getElementById("user-info").classList.remove("hidden");document.getElementById("logout-btn").classList.remove("hidden");document.getElementById("nav-auth").classList.add("hidden");toast(_isRegister?"Registered!":"Logged in!");showSection("${entities[0].name.toLowerCase()}")}else{
|
|
1346
|
-
L.push(`function doLogout(){_token=null;document.getElementById("user-info").classList.add("hidden");document.getElementById("logout-btn").classList.add("hidden");document.getElementById("nav-auth").classList.remove("hidden");toast("Logged out");showSection("auth")}`);
|
|
1454
|
+
L.push(`async function doAuth(){const email=document.getElementById("auth-email").value;const password=document.getElementById("auth-password").value;if(!email||!password){showError("Email and password are required");return}const url=_isRegister?"/api/auth/register":"/api/auth/login";const body=_isRegister?{name:document.getElementById("auth-name").value,email,password}:{email,password};try{const r=await fetch(API+url,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(body)});const d=await r.json();if(!r.ok){showError(d.error||"Auth failed ("+r.status+")");return}if(d.token){_token=d.token;localStorage.setItem("_token",d.token);document.getElementById("user-info").textContent="Hi, "+(d.user?.name||email);document.getElementById("user-info").classList.remove("hidden");document.getElementById("logout-btn").classList.remove("hidden");document.getElementById("nav-auth").classList.add("hidden");toast(_isRegister?"Registered!":"Logged in!");showSection("${entities[0].name.toLowerCase()}")}else{showError(d.error||"No token received")}}catch(e){showError("Network error: "+e.message)}}`);
|
|
1455
|
+
L.push(`function doLogout(){_token=null;localStorage.removeItem("_token");document.getElementById("user-info").classList.add("hidden");document.getElementById("logout-btn").classList.add("hidden");document.getElementById("nav-auth").classList.remove("hidden");toast("Logged out");showSection("auth")}`);
|
|
1456
|
+
L.push(`(function(){const t=localStorage.getItem("_token");if(t){_token=t;document.getElementById("user-info").textContent="Logged in";document.getElementById("user-info").classList.remove("hidden");document.getElementById("logout-btn").classList.remove("hidden");document.getElementById("nav-auth").classList.add("hidden")}})()`);
|
|
1347
1457
|
}
|
|
1348
1458
|
for (const entity of entities) {
|
|
1349
1459
|
const lower = entity.name.toLowerCase();
|
|
1350
1460
|
const plural = pluralize(lower);
|
|
1351
1461
|
const fields = entity.fields.filter(f => f.type !== 'auto');
|
|
1352
1462
|
L.push(`async function ${lower}Load(page){page=page||1;currentPage["${lower}"]=page;const s=document.getElementById("${lower}-search").value;const sort=document.getElementById("${lower}-sort").value;const ord=document.getElementById("${lower}-order").value;`);
|
|
1353
|
-
L.push(`
|
|
1463
|
+
L.push(`setLoading("${lower}-tbody",true);`);
|
|
1464
|
+
L.push(`const d=await apiFetch(API+"/api/${plural}?page="+page+"&limit=10&search="+encodeURIComponent(s)+"&sort="+sort+"&order="+ord);if(!d){setLoading("${lower}-tbody",false);return}const items=d.items||d;`);
|
|
1354
1465
|
L.push(`document.getElementById("${lower}-tbody").innerHTML=items.map(i=>"<tr>${entity.fields.map(f => `<td>"+(i.${f.name}!=null?i.${f.name}:"")+"</td>`).join('')}<td><button class='btn btn-sm btn-primary' onclick='${lower}Edit("+i.id+")'>Edit</button> <button class='btn btn-sm btn-danger' onclick='${lower}Del("+i.id+")'>Del</button></td></tr>").join("");`);
|
|
1355
1466
|
L.push(`const tp=Math.ceil((d.total||items.length)/10);let pg="";for(let p=1;p<=tp;p++)pg+="<button class='btn btn-sm "+(p===page?"btn-primary":"")+"' onclick='${lower}Load("+p+")'>"+p+"</button>";document.getElementById("${lower}-pager").innerHTML=pg}`);
|
|
1356
1467
|
L.push(`function ${lower}ShowModal(id){document.getElementById("${lower}-edit-id").value=id||"";document.getElementById("${lower}-modal-title").textContent=id?"Edit ${entity.name}":"New ${entity.name}";${fields.map(f => f.type === 'bool' ? `document.getElementById("${lower}-f-${f.name}").checked=false` : `document.getElementById("${lower}-f-${f.name}").value=""`).join(';')};document.getElementById("${lower}-modal").classList.remove("hidden")}`);
|
|
1357
|
-
L.push(`async function ${lower}Edit(id){const
|
|
1468
|
+
L.push(`async function ${lower}Edit(id){const d=await apiFetch(API+"/api/${plural}/"+id);if(!d)return;const i=d.data||d;${lower}ShowModal(id);${fields.map(f => f.type === 'bool' ? `document.getElementById("${lower}-f-${f.name}").checked=!!i.${f.name}` : `document.getElementById("${lower}-f-${f.name}").value=i.${f.name}||""`).join(';')}}`);
|
|
1358
1469
|
L.push(`async function ${lower}Save(){const id=document.getElementById("${lower}-edit-id").value;const body={${fields.map(f => {
|
|
1359
1470
|
if (f.type === 'bool') return `${f.name}:document.getElementById("${lower}-f-${f.name}").checked`;
|
|
1360
1471
|
if (f.type === 'int') return `${f.name}:+document.getElementById("${lower}-f-${f.name}").value`;
|
|
1361
1472
|
return `${f.name}:document.getElementById("${lower}-f-${f.name}").value`;
|
|
1362
1473
|
}).join(',')}};`);
|
|
1363
|
-
L.push(`const method=id?"PUT":"POST";const url=API+"/api/${plural}"+(id?"/"+id:"");await
|
|
1364
|
-
L.push(`async function ${lower}Del(id){if(!confirm("Delete this ${lower}?"))return;await
|
|
1474
|
+
L.push(`const method=id?"PUT":"POST";const url=API+"/api/${plural}"+(id?"/"+id:"");const d=await apiFetch(url,{method,headers:authHeaders(),body:JSON.stringify(body)});if(!d){showBanner("section-${lower}","Save failed — check required fields");return}document.getElementById("${lower}-modal").classList.add("hidden");toast(id?"Updated!":"Created!");${lower}Load()}`);
|
|
1475
|
+
L.push(`async function ${lower}Del(id){if(!confirm("Delete this ${lower}?"))return;const d=await apiFetch(API+"/api/${plural}/"+id,{method:"DELETE",headers:authHeaders()});if(!d)return;toast("Deleted!");${lower}Load()}`);
|
|
1365
1476
|
}
|
|
1366
1477
|
const initSection = hasAuth ? 'auth' : entities[0].name.toLowerCase();
|
|
1367
1478
|
L.push(`showSection("${initSection}")`);
|
|
@@ -1501,9 +1612,559 @@ function envLines(intents, params) {
|
|
|
1501
1612
|
return lines;
|
|
1502
1613
|
}
|
|
1503
1614
|
|
|
1615
|
+
// ── Test Runner (D2) ─────────────────────────────────────
|
|
1616
|
+
|
|
1617
|
+
function testRunnerFile(entities, intents, port) {
|
|
1618
|
+
const lines = [];
|
|
1619
|
+
lines.push(`import { describe, it } from 'node:test';`);
|
|
1620
|
+
lines.push(`import assert from 'node:assert/strict';`);
|
|
1621
|
+
lines.push('');
|
|
1622
|
+
lines.push(`const BASE = process.env.TEST_URL || 'http://localhost:${port}';`);
|
|
1623
|
+
lines.push(`const headers = {'Content-Type': 'application/json'};`);
|
|
1624
|
+
lines.push(`let authToken = null;`);
|
|
1625
|
+
lines.push('');
|
|
1626
|
+
lines.push(`async function api(path, opts = {}) {`);
|
|
1627
|
+
lines.push(` const h = {...headers};`);
|
|
1628
|
+
lines.push(` if (authToken) h.Authorization = 'Bearer ' + authToken;`);
|
|
1629
|
+
lines.push(` const r = await fetch(BASE + path, {...opts, headers: {...h, ...opts.headers}});`);
|
|
1630
|
+
lines.push(` const body = await r.json().catch(() => null);`);
|
|
1631
|
+
lines.push(` return {status: r.status, body, ok: r.ok};`);
|
|
1632
|
+
lines.push(`}`);
|
|
1633
|
+
lines.push('');
|
|
1634
|
+
lines.push(`describe('Health', () => {`);
|
|
1635
|
+
lines.push(` it('GET /api/health returns ok', async () => {`);
|
|
1636
|
+
lines.push(` const r = await api('/api/health');`);
|
|
1637
|
+
lines.push(` assert.equal(r.status, 200);`);
|
|
1638
|
+
lines.push(` assert.equal(r.body.status, 'ok');`);
|
|
1639
|
+
lines.push(` });`);
|
|
1640
|
+
lines.push(`});`);
|
|
1641
|
+
lines.push('');
|
|
1642
|
+
if (intents.has('auth')) {
|
|
1643
|
+
lines.push(`describe('Auth', () => {`);
|
|
1644
|
+
lines.push(` const email = 'test' + Date.now() + '@test.com';`);
|
|
1645
|
+
lines.push(` it('POST /api/auth/register creates user', async () => {`);
|
|
1646
|
+
lines.push(` const r = await api('/api/auth/register', {method: 'POST', body: JSON.stringify({name: 'Test', email, password: 'test1234'})});`);
|
|
1647
|
+
lines.push(` assert.equal(r.status, 200);`);
|
|
1648
|
+
lines.push(` assert.ok(r.body.token);`);
|
|
1649
|
+
lines.push(` authToken = r.body.token;`);
|
|
1650
|
+
lines.push(` });`);
|
|
1651
|
+
lines.push(` it('POST /api/auth/login returns token', async () => {`);
|
|
1652
|
+
lines.push(` const r = await api('/api/auth/login', {method: 'POST', body: JSON.stringify({email, password: 'test1234'})});`);
|
|
1653
|
+
lines.push(` assert.equal(r.status, 200);`);
|
|
1654
|
+
lines.push(` assert.ok(r.body.token);`);
|
|
1655
|
+
lines.push(` authToken = r.body.token;`);
|
|
1656
|
+
lines.push(` });`);
|
|
1657
|
+
lines.push(` it('GET /api/auth/me returns current user', async () => {`);
|
|
1658
|
+
lines.push(` const r = await api('/api/auth/me');`);
|
|
1659
|
+
lines.push(` assert.equal(r.status, 200);`);
|
|
1660
|
+
lines.push(` assert.equal(r.body.email || r.body.data?.email, email);`);
|
|
1661
|
+
lines.push(` });`);
|
|
1662
|
+
lines.push(` it('rejects invalid login', async () => {`);
|
|
1663
|
+
lines.push(` const r = await api('/api/auth/login', {method: 'POST', body: JSON.stringify({email: 'no@no.com', password: 'wrong'})});`);
|
|
1664
|
+
lines.push(` assert.equal(r.status, 401);`);
|
|
1665
|
+
lines.push(` });`);
|
|
1666
|
+
lines.push(`});`);
|
|
1667
|
+
lines.push('');
|
|
1668
|
+
}
|
|
1669
|
+
for (const entity of entities) {
|
|
1670
|
+
const lower = entity.name.toLowerCase();
|
|
1671
|
+
const plural = pluralize(lower);
|
|
1672
|
+
const requiredFields = entity.fields.filter(f => f.modifiers.includes('required') && f.type !== 'auto');
|
|
1673
|
+
const sampleData = {};
|
|
1674
|
+
for (const f of requiredFields) {
|
|
1675
|
+
if (f.type === 'int') sampleData[f.name] = 1;
|
|
1676
|
+
else if (f.type === 'bool') sampleData[f.name] = true;
|
|
1677
|
+
else sampleData[f.name] = 'test_' + f.name;
|
|
1678
|
+
}
|
|
1679
|
+
if (Object.keys(sampleData).length === 0) {
|
|
1680
|
+
const first = entity.fields.find(f => f.type !== 'auto' && f.type !== 'bool');
|
|
1681
|
+
if (first) sampleData[first.name] = first.type === 'int' ? 1 : 'test';
|
|
1682
|
+
}
|
|
1683
|
+
lines.push(`describe('${entity.name} CRUD', () => {`);
|
|
1684
|
+
lines.push(` let createdId;`);
|
|
1685
|
+
lines.push(` it('POST /api/${plural} creates', async () => {`);
|
|
1686
|
+
lines.push(` const r = await api('/api/${plural}', {method: 'POST', body: JSON.stringify(${JSON.stringify(sampleData)})});`);
|
|
1687
|
+
lines.push(` assert.ok(r.ok, 'status ' + r.status);`);
|
|
1688
|
+
lines.push(` createdId = r.body.id || r.body.data?.id;`);
|
|
1689
|
+
lines.push(` assert.ok(createdId, 'has id');`);
|
|
1690
|
+
lines.push(` });`);
|
|
1691
|
+
lines.push(` it('GET /api/${plural} lists', async () => {`);
|
|
1692
|
+
lines.push(` const r = await api('/api/${plural}');`);
|
|
1693
|
+
lines.push(` assert.equal(r.status, 200);`);
|
|
1694
|
+
lines.push(` const items = r.body.items || r.body;`);
|
|
1695
|
+
lines.push(` assert.ok(Array.isArray(items));`);
|
|
1696
|
+
lines.push(` });`);
|
|
1697
|
+
lines.push(` it('GET /api/${plural}/:id finds', async () => {`);
|
|
1698
|
+
lines.push(` const r = await api('/api/${plural}/' + createdId);`);
|
|
1699
|
+
lines.push(` assert.equal(r.status, 200);`);
|
|
1700
|
+
lines.push(` });`);
|
|
1701
|
+
lines.push(` it('PUT /api/${plural}/:id updates', async () => {`);
|
|
1702
|
+
lines.push(` const r = await api('/api/${plural}/' + createdId, {method: 'PUT', body: JSON.stringify(${JSON.stringify(sampleData)})});`);
|
|
1703
|
+
lines.push(` assert.ok(r.ok);`);
|
|
1704
|
+
lines.push(` });`);
|
|
1705
|
+
lines.push(` it('DELETE /api/${plural}/:id deletes', async () => {`);
|
|
1706
|
+
lines.push(` const r = await api('/api/${plural}/' + createdId, {method: 'DELETE'});`);
|
|
1707
|
+
lines.push(` assert.ok(r.ok);`);
|
|
1708
|
+
lines.push(` });`);
|
|
1709
|
+
lines.push(`});`);
|
|
1710
|
+
lines.push('');
|
|
1711
|
+
}
|
|
1712
|
+
return lines.join('\n');
|
|
1713
|
+
}
|
|
1714
|
+
|
|
1715
|
+
// ── DB Migration (D3) ───────────────────────────────────────
|
|
1716
|
+
|
|
1717
|
+
function migrationFile(entities, relationships, m2mRels) {
|
|
1718
|
+
const lines = [];
|
|
1719
|
+
const ts = new Date().toISOString().replace(/[-:T]/g, '').slice(0, 14);
|
|
1720
|
+
lines.push(`# Migration: ${ts}`);
|
|
1721
|
+
lines.push(`# Auto-generated by NAIDE ~~ engine`);
|
|
1722
|
+
lines.push(`# Run: naide migrate.naide`);
|
|
1723
|
+
lines.push('');
|
|
1724
|
+
lines.push('use "node:fs"');
|
|
1725
|
+
lines.push('');
|
|
1726
|
+
lines.push(`fn migrate():`);
|
|
1727
|
+
for (const entity of entities) {
|
|
1728
|
+
const lower = entity.name.toLowerCase();
|
|
1729
|
+
const plural = pluralize(lower);
|
|
1730
|
+
lines.push(` log("Creating table: ${plural}")`);
|
|
1731
|
+
const cols = entity.fields.map(f => {
|
|
1732
|
+
let col = f.name;
|
|
1733
|
+
if (f.type === 'auto') col += ' INTEGER PRIMARY KEY AUTOINCREMENT';
|
|
1734
|
+
else if (f.type === 'int') col += ' INTEGER';
|
|
1735
|
+
else if (f.type === 'bool') col += ' BOOLEAN DEFAULT false';
|
|
1736
|
+
else col += ' TEXT';
|
|
1737
|
+
if (f.modifiers.includes('required') && f.type !== 'auto') col += ' NOT NULL';
|
|
1738
|
+
if (f.modifiers.includes('unique')) col += ' UNIQUE';
|
|
1739
|
+
return col;
|
|
1740
|
+
});
|
|
1741
|
+
lines.push(` str sql_${lower} = "CREATE TABLE IF NOT EXISTS ${plural} (${cols.join(', ')}"`);
|
|
1742
|
+
const fks = relationships.filter(r => r.child === entity.name);
|
|
1743
|
+
if (fks.length > 0) {
|
|
1744
|
+
for (const fk of fks) {
|
|
1745
|
+
const parentPlural = pluralize(fk.parent.toLowerCase());
|
|
1746
|
+
lines.push(` sql_${lower} = sql_${lower} + ", FOREIGN KEY (${fk.fk}) REFERENCES ${parentPlural}(id)"`);
|
|
1747
|
+
}
|
|
1748
|
+
}
|
|
1749
|
+
lines.push(` sql_${lower} = sql_${lower} + ")"`);
|
|
1750
|
+
lines.push(` log(sql_${lower})`);
|
|
1751
|
+
lines.push('');
|
|
1752
|
+
}
|
|
1753
|
+
for (const rel of m2mRels) {
|
|
1754
|
+
const jLower = rel.junction.toLowerCase();
|
|
1755
|
+
lines.push(` log("Creating junction table: ${jLower}")`);
|
|
1756
|
+
const lp = pluralize(rel.left.toLowerCase());
|
|
1757
|
+
const rp = pluralize(rel.right.toLowerCase());
|
|
1758
|
+
lines.push(` str sql_${jLower} = "CREATE TABLE IF NOT EXISTS ${jLower} (id INTEGER PRIMARY KEY AUTOINCREMENT, ${rel.fkLeft} INTEGER NOT NULL, ${rel.fkRight} INTEGER NOT NULL, created TEXT DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (${rel.fkLeft}) REFERENCES ${lp}(id), FOREIGN KEY (${rel.fkRight}) REFERENCES ${rp}(id), UNIQUE(${rel.fkLeft}, ${rel.fkRight}))"`);
|
|
1759
|
+
lines.push(` log(sql_${jLower})`);
|
|
1760
|
+
lines.push('');
|
|
1761
|
+
}
|
|
1762
|
+
lines.push(' log("Migration complete")');
|
|
1763
|
+
lines.push('');
|
|
1764
|
+
lines.push(`fn rollback():`);
|
|
1765
|
+
for (const entity of [...entities].reverse()) {
|
|
1766
|
+
const plural = pluralize(entity.name.toLowerCase());
|
|
1767
|
+
lines.push(` log("Dropping table: ${plural}")`);
|
|
1768
|
+
lines.push(` str drop_${entity.name.toLowerCase()} = "DROP TABLE IF EXISTS ${plural}"`);
|
|
1769
|
+
lines.push(` log(drop_${entity.name.toLowerCase()})`);
|
|
1770
|
+
}
|
|
1771
|
+
for (const rel of m2mRels) {
|
|
1772
|
+
const jLower = rel.junction.toLowerCase();
|
|
1773
|
+
lines.push(` str drop_${jLower} = "DROP TABLE IF EXISTS ${jLower}"`);
|
|
1774
|
+
lines.push(` log(drop_${jLower})`);
|
|
1775
|
+
}
|
|
1776
|
+
lines.push(' log("Rollback complete")');
|
|
1777
|
+
lines.push('');
|
|
1778
|
+
lines.push('migrate()');
|
|
1779
|
+
return lines.join('\n') + '\n';
|
|
1780
|
+
}
|
|
1781
|
+
|
|
1504
1782
|
// ── Code Composer ───────────────────────────────────────────
|
|
1505
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
|
+
|
|
1506
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
|
+
|
|
1507
2168
|
const sections = [];
|
|
1508
2169
|
const entities = params.entities.length > 0 ? params.entities : [{ name: params.schemaName || 'Item', fields: params.fields }];
|
|
1509
2170
|
const primaryName = entities[0].name;
|
|
@@ -1531,6 +2192,11 @@ function compose(intents, params) {
|
|
|
1531
2192
|
sections.push(loggingBlock());
|
|
1532
2193
|
}
|
|
1533
2194
|
|
|
2195
|
+
// RBAC middleware (D1)
|
|
2196
|
+
if (hasAuth) {
|
|
2197
|
+
sections.push(rbacBlock());
|
|
2198
|
+
}
|
|
2199
|
+
|
|
1534
2200
|
// Input validation functions (A4)
|
|
1535
2201
|
if (intents.has('schema') && intents.has('crud')) {
|
|
1536
2202
|
for (const entity of entities) {
|
|
@@ -1640,9 +2306,9 @@ function compose(intents, params) {
|
|
|
1640
2306
|
body.push('');
|
|
1641
2307
|
}
|
|
1642
2308
|
|
|
1643
|
-
// WebSocket with rooms (B11)
|
|
2309
|
+
// WebSocket with rooms + auth (B11, D4)
|
|
1644
2310
|
if (intents.has('websocket')) {
|
|
1645
|
-
body.push(...wsRoomsBlock());
|
|
2311
|
+
body.push(...wsRoomsBlock(hasAuth));
|
|
1646
2312
|
body.push('');
|
|
1647
2313
|
}
|
|
1648
2314
|
|
|
@@ -1666,6 +2332,14 @@ function compose(intents, params) {
|
|
|
1666
2332
|
}
|
|
1667
2333
|
}
|
|
1668
2334
|
|
|
2335
|
+
// RBAC role-guarded delete routes (D1)
|
|
2336
|
+
if (hasAuth && intents.has('crud') && intents.has('schema')) {
|
|
2337
|
+
for (const entity of entities) {
|
|
2338
|
+
body.push(...rbacRouteGuards(entity));
|
|
2339
|
+
body.push('');
|
|
2340
|
+
}
|
|
2341
|
+
}
|
|
2342
|
+
|
|
1669
2343
|
// File upload
|
|
1670
2344
|
if (intents.has('crud')) {
|
|
1671
2345
|
body.push(...uploadRoute());
|
|
@@ -1818,8 +2492,9 @@ function heal(code, err) {
|
|
|
1818
2492
|
// ── Suggestions ─────────────────────────────────────────────
|
|
1819
2493
|
|
|
1820
2494
|
const KEYWORD_CHEATSHEET = {
|
|
1821
|
-
'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',
|
|
1822
2496
|
'Composite': 'todo, blog, chat, shop, fullstack, board, crm, inventory, booking, sns',
|
|
2497
|
+
'Game': 'tapping, quiz, memory, snake, typing, reaction, breakout',
|
|
1823
2498
|
'Platform': 'discord, slack, telegram, line',
|
|
1824
2499
|
'DB': 'sqlite, postgres',
|
|
1825
2500
|
'Japanese': 'サーバー, 認証, ログイン, 会員, CRUD, 管理, データベース, ボット, テスト, ページ',
|
|
@@ -1841,6 +2516,31 @@ export function generateProject(instruction, options = {}) {
|
|
|
1841
2516
|
const result = generate(instruction, options);
|
|
1842
2517
|
const files = [];
|
|
1843
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
|
+
|
|
1844
2544
|
const entities = result.entities || [];
|
|
1845
2545
|
const primaryName = (entities[0] || 'app').toLowerCase();
|
|
1846
2546
|
const projectName = safeName(primaryName) + '-app';
|
|
@@ -1866,8 +2566,10 @@ export function generateProject(instruction, options = {}) {
|
|
|
1866
2566
|
dev: 'naide app.naide',
|
|
1867
2567
|
build: 'naide app.naide --emit -o dist/app.mjs',
|
|
1868
2568
|
start: 'node dist/app.mjs',
|
|
1869
|
-
test: '
|
|
2569
|
+
test: 'node --test test.mjs',
|
|
2570
|
+
'test:unit': 'naide test app.naide',
|
|
1870
2571
|
seed: 'naide app.naide --run-seed',
|
|
2572
|
+
migrate: 'naide migrate.naide',
|
|
1871
2573
|
},
|
|
1872
2574
|
dependencies: deps,
|
|
1873
2575
|
};
|
|
@@ -1997,6 +2699,17 @@ CMD ["node", "dist/app.mjs"]
|
|
|
1997
2699
|
// GitHub Actions CI (C16)
|
|
1998
2700
|
files.push({ path: '.github/workflows/ci.yml', content: githubActionsCI(projectName) });
|
|
1999
2701
|
|
|
2702
|
+
// Test runner (D2)
|
|
2703
|
+
if (intents.has('server') && entityObjs.length > 0) {
|
|
2704
|
+
files.push({ path: 'test.mjs', content: testRunnerFile(entityObjs, intents, result.port) });
|
|
2705
|
+
}
|
|
2706
|
+
|
|
2707
|
+
// DB migration (D3)
|
|
2708
|
+
if (intents.has('database') && entityObjs.length > 0) {
|
|
2709
|
+
const m2mRels = detectManyToMany(entities.map(e => e));
|
|
2710
|
+
files.push({ path: 'migrate.naide', content: migrationFile(entityObjs, result.relationships || [], m2mRels) });
|
|
2711
|
+
}
|
|
2712
|
+
|
|
2000
2713
|
return { ...result, files };
|
|
2001
2714
|
}
|
|
2002
2715
|
|
|
@@ -2312,6 +3025,22 @@ export function expandDirectives(source) {
|
|
|
2312
3025
|
function buildAnalysis(instruction, intents, params) {
|
|
2313
3026
|
const plan = [];
|
|
2314
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
|
+
|
|
2315
3044
|
if (params.entities.length > 0) {
|
|
2316
3045
|
for (const e of params.entities) {
|
|
2317
3046
|
plan.push(`Schema "${e.name}" (${e.fields.map(f => f.name).join(', ')})`);
|
|
@@ -2332,7 +3061,8 @@ function buildAnalysis(instruction, intents, params) {
|
|
|
2332
3061
|
if (params.relationships.length > 0) plan.push(`Nested routes: ${params.relationships.map(r => `/${pluralize(r.parent.toLowerCase())}/:id/${pluralize(r.child.toLowerCase())}`).join(', ')}`);
|
|
2333
3062
|
const m2m = detectManyToMany(params.entities.map(e => e.name));
|
|
2334
3063
|
if (m2m.length > 0) plan.push(`M:N relations: ${m2m.map(r => `${r.left}↔${r.right} via ${r.junction}`).join(', ')}`);
|
|
2335
|
-
if (intents.has('
|
|
3064
|
+
if (intents.has('auth')) plan.push('RBAC: requireRole() middleware for admin/moderator/user');
|
|
3065
|
+
if (intents.has('websocket'))plan.push('WebSocket: rooms + broadcast' + (intents.has('auth') ? ' (token auth)' : ''));
|
|
2336
3066
|
if (intents.has('ai')) plan.push('AI endpoint: /api/ask');
|
|
2337
3067
|
if (intents.has('bot')) plan.push(`Bot: ${params.platform || 'discord'}`);
|
|
2338
3068
|
if (intents.has('mail')) plan.push('Mail: SMTP configuration');
|
|
@@ -2388,3 +3118,62 @@ export function generate(instruction, options = {}) {
|
|
|
2388
3118
|
analysis,
|
|
2389
3119
|
};
|
|
2390
3120
|
}
|
|
3121
|
+
|
|
3122
|
+
// ── Multi-Target Generation (D6) ─────────────────────────────
|
|
3123
|
+
|
|
3124
|
+
const TARGET_EXTENSIONS = {
|
|
3125
|
+
node: '.mjs', python: '.py', bun: '.mjs', typescript: '.ts',
|
|
3126
|
+
go: '.go', java: '.java', rust: '.rs', cpp: '.cpp', c: '.c',
|
|
3127
|
+
csharp: '.cs', kotlin: '.kt', swift: '.swift', dart: '.dart',
|
|
3128
|
+
php: '.php', ruby: '.rb',
|
|
3129
|
+
};
|
|
3130
|
+
|
|
3131
|
+
const ALL_TARGETS = Object.keys(TARGET_EXTENSIONS);
|
|
3132
|
+
|
|
3133
|
+
export async function generateForTarget(instruction, target, options = {}) {
|
|
3134
|
+
const result = generate(instruction, options);
|
|
3135
|
+
if (!result.valid) return { ...result, compiled: null, target };
|
|
3136
|
+
|
|
3137
|
+
let compiled;
|
|
3138
|
+
try {
|
|
3139
|
+
const { compile, compileAsync } = await import('./index.js');
|
|
3140
|
+
if (target === 'node') {
|
|
3141
|
+
const r = compile(result.code);
|
|
3142
|
+
compiled = r.js;
|
|
3143
|
+
} else {
|
|
3144
|
+
const r = await compileAsync(result.code, { target });
|
|
3145
|
+
compiled = r.code;
|
|
3146
|
+
}
|
|
3147
|
+
} catch (e) {
|
|
3148
|
+
compiled = null;
|
|
3149
|
+
result.compileError = e.message;
|
|
3150
|
+
}
|
|
3151
|
+
|
|
3152
|
+
return { ...result, compiled, target, extension: TARGET_EXTENSIONS[target] || '.txt' };
|
|
3153
|
+
}
|
|
3154
|
+
|
|
3155
|
+
export async function generateAllTargets(instruction, options = {}) {
|
|
3156
|
+
const result = generate(instruction, options);
|
|
3157
|
+
if (!result.valid) return { ...result, targets: {} };
|
|
3158
|
+
|
|
3159
|
+
const targets = {};
|
|
3160
|
+
try {
|
|
3161
|
+
const { compile, compileAsync } = await import('./index.js');
|
|
3162
|
+
for (const target of ALL_TARGETS) {
|
|
3163
|
+
try {
|
|
3164
|
+
if (target === 'node') {
|
|
3165
|
+
targets[target] = { code: compile(result.code).js, ext: '.mjs' };
|
|
3166
|
+
} else {
|
|
3167
|
+
const r = await compileAsync(result.code, { target });
|
|
3168
|
+
targets[target] = { code: r.code, ext: TARGET_EXTENSIONS[target] };
|
|
3169
|
+
}
|
|
3170
|
+
} catch (e) {
|
|
3171
|
+
targets[target] = { error: e.message, ext: TARGET_EXTENSIONS[target] };
|
|
3172
|
+
}
|
|
3173
|
+
}
|
|
3174
|
+
} catch (e) {
|
|
3175
|
+
return { ...result, targets: {}, importError: e.message };
|
|
3176
|
+
}
|
|
3177
|
+
|
|
3178
|
+
return { ...result, targets };
|
|
3179
|
+
}
|