captainspock commited on
Commit
2a028fc
·
verified ·
1 Parent(s): 4cdfa60

Delete index.html

Browse files
Files changed (1) hide show
  1. index.html +0 -1349
index.html DELETED
@@ -1,1349 +0,0 @@
1
- <!doctype html>
2
- <html lang="en">
3
-
4
- <head>
5
- <meta charset="UTF-8" />
6
- <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
- <title>FunctionGemma Physics Playground</title>
8
- <script src="https://cdn.tailwindcss.com"></script>
9
- <script src="https://cdnjs.cloudflare.com/ajax/libs/matter-js/0.19.0/matter.min.js"></script>
10
- <script src="https://cdn.jsdelivr.net/npm/poly-decomp@0.3.0/build/decomp.min.js"></script>
11
- <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet" />
12
- <link
13
- href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;700&family=Fira+Code:wght@500;700&display=swap"
14
- rel="stylesheet" />
15
- <script>
16
- tailwind.config = {
17
- theme: {
18
- extend: {
19
- fontFamily: {
20
- sans: ["Space Grotesk", "sans-serif"],
21
- mono: ["Fira Code", "monospace"],
22
- },
23
- colors: {
24
- "neo-bg": "#f0f0f0",
25
- "neo-black": "#1a1a1a",
26
- "neo-white": "#ffffff",
27
- "neo-purple": "#a78bfa",
28
- "neo-yellow": "#facc15",
29
- "neo-green": "#4ade80",
30
- "neo-red": "#f87171",
31
- "neo-blue": "#60a5fa",
32
- "neo-gray": "#94a3b8",
33
- },
34
- boxShadow: {
35
- neo: "4px 4px 0px 0px #000000",
36
- "neo-sm": "2px 2px 0px 0px #000000",
37
- "neo-lg": "8px 8px 0px 0px #000000",
38
- },
39
- },
40
- },
41
- };
42
- </script>
43
-
44
- <style>
45
- body {
46
- overflow: hidden;
47
- background-color: #e0e7ff;
48
- background-image: radial-gradient(#a5b4fc 1px, transparent 1px);
49
- background-size: 20px 20px;
50
- }
51
- /* Custom Scrollbar */
52
- ::-webkit-scrollbar {
53
- width: 12px;
54
- height: 12px;
55
- }
56
- ::-webkit-scrollbar-track {
57
- background: #fff;
58
- border-left: 2px solid black;
59
- }
60
- ::-webkit-scrollbar-thumb {
61
- background: #000;
62
- border: 2px solid #fff;
63
- }
64
- ::-webkit-scrollbar-thumb:hover {
65
- background: #333;
66
- }
67
- .neo-border {
68
- border: 3px solid black;
69
- }
70
- .neo-btn {
71
- transition: all 0.1s ease-in-out;
72
- }
73
- .neo-btn:active {
74
- transform: translate(2px, 2px);
75
- box-shadow: 2px 2px 0px 0px #000000;
76
- }
77
- .level-card {
78
- transition: all 0.2s;
79
- }
80
- .level-card:hover:not(.locked) {
81
- transform: translate(-2px, -2px);
82
- box-shadow: 6px 6px 0px 0px #000000;
83
- }
84
- .locked {
85
- background-color: #e2e8f0;
86
- cursor: not-allowed;
87
- opacity: 0.7;
88
- background-image: repeating-linear-gradient(45deg, #cbd5e1 0, #cbd5e1 1px, transparent 0, transparent 50%);
89
- background-size: 10px 10px;
90
- }
91
- .code-editor {
92
- font-family: "Fira Code", monospace;
93
- background-color: #ffffff;
94
- color: #000000;
95
- line-height: 1.6;
96
- }
97
- .toggle-checkbox:checked {
98
- right: 0;
99
- border-color: #4ade80;
100
- }
101
- .toggle-checkbox:checked+.toggle-label {
102
- background-color: #4ade80;
103
- }
104
- /* Action Item Delete Button Transition */
105
- .action-item .btn-delete {
106
- opacity: 0;
107
- transition: opacity 0.2s;
108
- }
109
- .action-item:hover .btn-delete {
110
- opacity: 1;
111
- }
112
- </style>
113
- </head>
114
-
115
- <body class="h-screen w-screen flex flex-col md:flex-row p-4 gap-4">
116
- <!-- Loading Overlay -->
117
- <div id="loading-overlay" class="fixed inset-0 bg-black/90 z-50 flex flex-col items-center justify-center">
118
- <div class="animate-spin rounded-full h-16 w-16 border-t-4 border-b-4 border-neo-green mb-4"></div>
119
- <div class="text-white font-mono font-bold text-xl">LOADING MODEL...</div>
120
- <div class="text-gray-400 font-mono text-sm mt-2">This may take a moment</div>
121
- </div>
122
-
123
- <!-- Sidebar / Controls -->
124
- <div class="w-full md:w-1/3 lg:w-1/4 flex flex-col neo-border shadow-neo bg-white h-full z-10 relative">
125
- <!-- Decorative Header Strip -->
126
- <div class="h-4 w-full bg-neo-black"></div>
127
-
128
- <div class="p-6 border-b-2 border-black bg-neo-yellow flex justify-between items-center shrink-0">
129
- <div>
130
- <h1 class="text-3xl font-bold text-black uppercase tracking-tighter">Function <span
131
- class="text-white bg-black px-1">Gemma</span></h1>
132
- <div class="text-xs font-mono font-bold text-black mt-1">PHYSICS PLAYGROUND</div>
133
- </div>
134
- </div>
135
-
136
- <!-- MENU: Level Select -->
137
- <div id="view-menu" class="flex-1 overflow-y-auto p-6 bg-white hidden">
138
- <h2 class="font-bold text-xl mb-4 border-b-2 border-black pb-2">SELECT LEVEL</h2>
139
- <div id="level-grid" class="grid grid-cols-2 gap-4">
140
- <!-- Injected via JS -->
141
- </div>
142
- </div>
143
-
144
- <!-- MENU: Game View -->
145
- <div id="view-game" class="flex-1 flex flex-col overflow-hidden relative">
146
- <div class="overflow-y-auto flex-1 p-6 flex flex-col gap-6">
147
- <!-- Level Header -->
148
- <div>
149
- <div class="flex items-center gap-2 mb-2">
150
- <button id="btn-back"
151
- class="neo-btn p-2 border-2 border-black shadow-neo-sm bg-white hover:bg-gray-100 text-xs font-bold"><i
152
- class="fas fa-arrow-left"></i> LEVELS</button>
153
- <div class="font-bold text-sm bg-black text-white px-2 py-1 ml-auto" id="level-indicator">LEVEL 1</div>
154
- </div>
155
-
156
- <!-- Level Info Card -->
157
- <div class="neo-border p-4 bg-neo-blue shadow-neo-sm relative overflow-hidden group">
158
- <div
159
- class="absolute -right-4 -top-4 w-16 h-16 bg-white/20 rounded-full group-hover:scale-150 transition-transform">
160
- </div>
161
- <div class="flex justify-between items-center mb-3 relative z-10">
162
- <h2 class="font-bold text-black text-lg border-b-2 border-black inline-block bg-white px-2"
163
- id="level-title">...</h2>
164
- <span id="timer-display"
165
- class="font-mono text-xl font-bold bg-black text-neo-green px-2 py-0.5 border-2 border-black shadow-[2px_2px_0px_0px_#fff]">0.00s</span>
166
- </div>
167
- <p class="text-sm text-black font-medium opacity-90 mb-2" id="level-desc">...</p>
168
-
169
- <!-- Collapsible Hint -->
170
- <div class="mt-2">
171
- <button id="btn-hint"
172
- class="text-xs font-bold border-2 border-black px-2 py-1 bg-white hover:bg-yellow-100 shadow-neo-sm flex items-center gap-2"><i
173
- class="fas fa-lightbulb text-yellow-500"></i> SHOW HINT</button>
174
- <div id="hint-content" class="mt-2 bg-white/50 p-2 border-2 border-black text-xs font-mono hidden"><i
175
- class="fas fa-code mr-1"></i> <span id="level-hint-text">...</span></div>
176
- </div>
177
- </div>
178
- </div>
179
-
180
- <!-- Editor / Command Input -->
181
- <div class="flex flex-col gap-2 shrink-0">
182
- <div class="flex justify-between items-end">
183
- <div class="flex gap-2 items-center">
184
- <label class="text-sm font-bold bg-black text-white px-2 py-0.5 shadow-neo-sm">COMMAND</label>
185
- <button id="btn-solution"
186
- class="text-xs font-bold border-2 border-black px-2 py-0.5 hover:bg-black hover:text-white transition-colors bg-white shadow-neo-sm"
187
- title="Load Example Solution"><i class="fas fa-magic"></i> VIEW SOLUTION</button>
188
- </div>
189
- <div class="text-xs font-bold text-gray-500" id="star-reqs">3★ < 2 items</div>
190
- </div>
191
- <div class="relative flex flex-col gap-2">
192
- <textarea id="code-input"
193
- class="code-editor w-full h-24 p-4 border-2 border-black focus:outline-none focus:ring-4 focus:ring-neo-purple/50 resize-none text-sm shadow-neo-sm"
194
- spellcheck="false" placeholder="e.g., Add a circle in the middle. You can execute multiple commands by separating them with new lines."></textarea>
195
- <button id="btn-execute"
196
- class="neo-btn bg-neo-purple border-2 border-black shadow-neo-sm text-black font-bold py-2 hover:bg-purple-400 disabled:opacity-50 disabled:cursor-not-allowed"><i
197
- class="fas fa-terminal"></i> EXECUTE</button>
198
- </div>
199
- <div id="error-log"
200
- class="text-white bg-neo-red border-2 border-black text-xs font-bold px-2 py-1 shadow-neo-sm hidden">
201
- </div>
202
- </div>
203
-
204
- <!-- Active Elements List -->
205
- <div class="flex-1 min-h-0 flex flex-col">
206
- <label class="text-sm font-bold bg-black text-white px-2 py-0.5 shadow-neo-sm w-max mb-1">SCENE
207
- OBJECTS</label>
208
- <div id="action-list"
209
- class="flex-1 overflow-y-auto border-2 border-black bg-gray-50 p-2 space-y-2 shadow-neo-sm min-h-[100px]">
210
- <div class="text-xs text-gray-400 text-center mt-4 italic">No elements added yet.</div>
211
- </div>
212
- </div>
213
- </div>
214
-
215
- <!-- Performance Metrics -->
216
- <div class="p-3 bg-gray-50 border-2 border-black shadow-neo-sm">
217
- <div class="text-xs font-bold mb-2 flex items-center gap-2">
218
- <i class="fas fa-tachometer-alt"></i> PERFORMANCE
219
- </div>
220
- <div class="grid grid-cols-3 gap-2 text-[10px] font-mono">
221
- <div class="bg-white p-1 border border-black">
222
- <div class="text-gray-500 uppercase">Model Load</div>
223
- <div class="font-bold" id="metric-compilation">-</div>
224
- </div>
225
- <div class="bg-white p-1 border border-black">
226
- <div class="text-gray-500 uppercase">TTFT</div>
227
- <div class="font-bold" id="metric-ttft">-</div>
228
- </div>
229
- <div class="bg-white p-1 border border-black">
230
- <div class="text-gray-500 uppercase">TPS</div>
231
- <div class="font-bold" id="metric-tps">-</div>
232
- </div>
233
- </div>
234
- </div>
235
-
236
- <!-- Action Bar -->
237
- <div class="p-4 bg-gray-100 border-t-2 border-black flex gap-3 shrink-0">
238
- <button id="btn-play"
239
- class="neo-btn flex-1 bg-neo-green border-2 border-black shadow-neo text-black font-bold py-3 px-4 flex items-center justify-center gap-2 hover:bg-green-400"><i
240
- class="fas fa-play"></i> RUN</button>
241
- <button id="btn-reset"
242
- class="neo-btn px-4 py-3 bg-white border-2 border-black shadow-neo text-black font-bold hover:bg-gray-100"
243
- title="Reset Simulation">
244
- <i class="fas fa-undo"></i>
245
- </button>
246
- <button id="btn-clear-all"
247
- class="neo-btn px-4 py-3 bg-white border-2 border-black shadow-neo text-black font-bold hover:bg-neo-red hover:text-white transition-colors"
248
- title="Clear All Objects">
249
- <i class="fas fa-trash-alt"></i>
250
- </button>
251
- </div>
252
- </div>
253
- </div>
254
-
255
- <!-- Main Canvas Area -->
256
- <div class="flex-1 relative flex items-center justify-center p-2">
257
- <!-- Canvas Container -->
258
- <div id="canvas-container"
259
- class="w-full h-full bg-white neo-border shadow-neo flex justify-center items-center relative overflow-hidden">
260
- <!-- Win Overlay -->
261
- <div id="win-message"
262
- class="absolute z-50 hidden w-full h-full flex items-center justify-center bg-black/50 backdrop-blur-sm">
263
- <div
264
- class="bg-neo-yellow border-4 border-black p-8 shadow-neo-lg text-center transform rotate-2 max-w-md w-full m-4">
265
- <h2 class="text-4xl font-black mb-2 text-black">LEVEL CLEAR!</h2>
266
-
267
- <div class="flex justify-center gap-2 mb-4 text-4xl text-white drop-shadow-md" id="result-stars">
268
- <!-- Stars injected here -->
269
- </div>
270
-
271
- <div class="text-sm font-bold mb-6 font-mono">ITEMS USED: <span id="result-items">0</span></div>
272
-
273
- <div class="flex flex-col gap-2">
274
- <button id="btn-next-level"
275
- class="bg-black text-white font-bold py-3 px-6 border-2 border-transparent hover:bg-neo-green hover:text-black hover:border-black transition-colors shadow-neo-sm">NEXT
276
- LEVEL <i class="fas fa-arrow-right ml-2"></i></button>
277
- <button onclick="document.getElementById('btn-reset').click()"
278
- class="bg-white text-black font-bold py-2 px-6 border-2 border-black hover:bg-gray-100 transition-colors shadow-neo-sm">REPLAY</button>
279
- </div>
280
- </div>
281
- </div>
282
-
283
- <!-- Canvas Injected Here -->
284
- </div>
285
-
286
- <!-- Status Badge -->
287
- <div class="absolute top-6 right-8 pointer-events-none z-20">
288
- <div id="status-badge"
289
- class="bg-white border-2 border-black shadow-neo-sm px-4 py-2 font-black text-sm uppercase tracking-widest flex items-center gap-2">
290
- <div class="w-3 h-3 bg-red-500 rounded-full border border-black animate-pulse"></div>
291
- READY
292
- </div>
293
- </div>
294
- </div>
295
- <script type="module">
296
- import { AutoModelForCausalLM, AutoTokenizer } from "https://cdn.jsdelivr.net/npm/@huggingface/transformers@3.8.1";
297
- // --- Game Constants ---
298
- const CONFIG = {
299
- width: 1000,
300
- height: 750,
301
- gridWidth: 20,
302
- gridHeight: 15,
303
- colors: {
304
- background: "#ffffff",
305
- wall: "#1a1a1a",
306
- ball: "#60a5fa",
307
- goal: "#4ade80",
308
- userShape: "#facc15",
309
- },
310
- };
311
- const STORAGE_KEY = "functiongemma_save_v1";
312
- // --- Helper Functions ---
313
- const pX = (units) => (units * CONFIG.width) / CONFIG.gridWidth;
314
- const pY = (units) => (units * CONFIG.height) / CONFIG.gridHeight;
315
- const strokeStyle = { strokeStyle: "#000000", lineWidth: 3 };
316
- // --- LEVEL DEFINITIONS ---
317
- const LEVELS = [
318
- {
319
- id: 0,
320
- title: "Tutorial",
321
- difficulty: 1,
322
- desc: "Welcome! Press RUN to start the simulation. The ball will move on its own.",
323
- hint: "Just press the green RUN button!",
324
- stars: [0, 1],
325
- solution: `// No code needed!`,
326
- setup: (World, Bodies, Composite) => {
327
- const floor = Bodies.rectangle(CONFIG.width / 2, CONFIG.height + 25, CONFIG.width, 100, { isStatic: true, render: { fillStyle: CONFIG.colors.wall, ...strokeStyle } });
328
- const ball = Bodies.circle(pX(2), CONFIG.height - 50 - 20, 20, {
329
- restitution: 0.6,
330
- friction: 0,
331
- frictionAir: 0,
332
- frictionStatic: 0,
333
- render: { fillStyle: CONFIG.colors.ball, ...strokeStyle },
334
- });
335
- Matter.Body.setVelocity(ball, { x: 10, y: 0 });
336
- const goal = Bodies.rectangle(pX(18), CONFIG.height - 50 - 60, 100, 120, {
337
- isStatic: true,
338
- isSensor: true,
339
- label: "GoalZone",
340
- render: { fillStyle: CONFIG.colors.goal, opacity: 0.3, ...strokeStyle, lineWidth: 2, strokeStyle: "#000" },
341
- });
342
- World.add(Composite, [floor, ball, goal]);
343
- return { ball, goal };
344
- },
345
- },
346
- {
347
- id: 1,
348
- title: "The Bridge",
349
- difficulty: 1,
350
- desc: "There is a gap in the path. Build a bridge so the ball can roll across.",
351
- hint: "Add a wide line in the center to connect the platforms.",
352
- stars: [1, 2],
353
- solution: `add a long line in the middle`,
354
- setup: (World, Bodies, Composite) => {
355
- const p1 = Bodies.rectangle(pX(4), pY(4), pX(6), 20, { isStatic: true, angle: Math.PI / 8, render: { fillStyle: CONFIG.colors.wall, ...strokeStyle } });
356
- const p2 = Bodies.rectangle(pX(16), pY(12), pX(6), 20, { isStatic: true, angle: Math.PI / 8, render: { fillStyle: CONFIG.colors.wall, ...strokeStyle } });
357
- const ball = Bodies.circle(pX(3), pY(2), 20, {
358
- restitution: 0.2,
359
- friction: 0,
360
- frictionAir: 0,
361
- render: { fillStyle: CONFIG.colors.ball, ...strokeStyle },
362
- });
363
- const goal = Bodies.rectangle(pX(18), pY(11), 80, 80, {
364
- isStatic: true,
365
- isSensor: true,
366
- label: "GoalZone",
367
- render: { fillStyle: CONFIG.colors.goal, opacity: 0.3, ...strokeStyle },
368
- });
369
- World.add(Composite, [p1, p2, ball, goal]);
370
- return { ball, goal };
371
- },
372
- },
373
- {
374
- id: 2,
375
- title: "A Little Push",
376
- difficulty: 2,
377
- desc: "Oh no, we're stuck! Give the ball a nudge to get it moving towards the goal.",
378
- hint: "Drop a heavy object in the top left to push it towards the goal.",
379
- stars: [1, 3],
380
- solution: `Add a circle at 2,2`,
381
- setup: (World, Bodies, Composite) => {
382
- const floor = Bodies.rectangle(CONFIG.width / 2, CONFIG.height + 25, CONFIG.width, 100, { isStatic: true, render: { fillStyle: CONFIG.colors.wall, ...strokeStyle } });
383
- const ball = Bodies.circle(pX(2.2), CONFIG.height - 50 - 20, 20, {
384
- restitution: 0.6,
385
- friction: 0,
386
- frictionAir: 0,
387
- frictionStatic: 0,
388
- render: { fillStyle: CONFIG.colors.ball, ...strokeStyle },
389
- });
390
- const goal = Bodies.rectangle(pX(18), CONFIG.height - 50 - 60, 100, 120, {
391
- isStatic: true,
392
- isSensor: true,
393
- label: "GoalZone",
394
- render: { fillStyle: CONFIG.colors.goal, opacity: 0.3, ...strokeStyle, lineWidth: 2, strokeStyle: "#000" },
395
- });
396
- World.add(Composite, [floor, ball, goal]);
397
- return { ball, goal };
398
- },
399
- },
400
- {
401
- id: 3,
402
- title: "The Bounce",
403
- difficulty: 2,
404
- desc: "High velocity incoming! How can you redirect the ball into the goal?",
405
- hint: "Add a centered platform at the bottom to reflect the ball towards the goal.",
406
- stars: [1, 2],
407
- solution: `add a wide line at the bottom`,
408
- setup: (World, Bodies, Composite) => {
409
- // Wall in middle top
410
- const wall = Bodies.rectangle(pX(10), pY(3), 20, pY(6), { isStatic: true, render: { fillStyle: CONFIG.colors.wall, ...strokeStyle } });
411
- const ball = Bodies.circle(pX(2), pY(1.2), 20, {
412
- restitution: 1.0,
413
- friction: 0,
414
- frictionAir: 0.001,
415
- render: { fillStyle: CONFIG.colors.ball, ...strokeStyle },
416
- });
417
- // Initial Velocity: Down and Right
418
- Matter.Body.setVelocity(ball, { x: 12, y: 12 });
419
- const goal = Bodies.rectangle(pX(18), pY(2), pX(2), pY(2), {
420
- isStatic: true,
421
- isSensor: true,
422
- label: "GoalZone",
423
- render: { fillStyle: CONFIG.colors.goal, opacity: 0.3, ...strokeStyle },
424
- });
425
- World.add(Composite, [wall, ball, goal]);
426
- return { ball, goal };
427
- },
428
- },
429
- {
430
- id: 4,
431
- title: "Protect",
432
- difficulty: 3,
433
- desc: "Ambush! Ninja stars are incoming. Protect the ball's path so it can land safely.",
434
- hint: "Add a few heavy blocks at certain locations to shield the ball from the stars.",
435
- stars: [3, 5],
436
- solution: `add a heavy block at 5, 3\nadd a heavy block at 15, 5\nadd a heavy block at 5, 7`,
437
- setup: (World, Bodies, Composite, addEvent) => {
438
- const ball = Bodies.circle(pX(10), pY(1), 20, {
439
- restitution: 0.5,
440
- friction: 0.001,
441
- frictionAir: 0.001,
442
- render: { fillStyle: CONFIG.colors.ball, ...strokeStyle },
443
- });
444
- const goal = Bodies.rectangle(pX(10), pY(14), pX(2), pY(2), {
445
- isStatic: true,
446
- isSensor: true,
447
- label: "GoalZone",
448
- render: { fillStyle: CONFIG.colors.goal, opacity: 0.3, ...strokeStyle },
449
- });
450
- const createNinjaStar = (x, y, vx, vy, angVel, delay) => {
451
- const starVerts = Matter.Vertices.fromPath('50 0 63 38 100 38 69 59 82 100 50 75 18 100 31 59 0 38 37 38');
452
- const star = Matter.Bodies.fromVertices(x, y, starVerts, {
453
- render: { fillStyle: "#ef4444", strokeStyle: "#ef4444", lineWidth: 1 },
454
- restitution: 0.8,
455
- isStatic: true
456
- }, true);
457
- Matter.Body.scale(star, 0.5, 0.5);
458
- Matter.Body.setVelocity(star, { x: vx, y: vy });
459
- Matter.Body.setAngularVelocity(star, angVel);
460
- World.add(Composite, star);
461
- if (addEvent) {
462
- addEvent(delay, () => {
463
- Matter.Body.setStatic(star, false);
464
- });
465
- }
466
- return star;
467
- };
468
- // Ninja Stars
469
- createNinjaStar(pX(2), pY(5), 15, -5, 0.3, 0.1);
470
- createNinjaStar(pX(18), pY(8), -15, -5, -0.3, 0.3);
471
- createNinjaStar(pX(2), pY(11), 15, -5, 0.3, 0.50);
472
- World.add(Composite, [ball, goal]);
473
- return { ball, goal };
474
- },
475
- },
476
- {
477
- id: 5,
478
- title: "Timing is Key",
479
- difficulty: 3,
480
- desc: "The goal is moving! Push the ball off the ledge so it lands in the moving target.",
481
- hint: "Add a heavy ball on the top left after a short delay to drop the ball onto the platform.",
482
- stars: [1, 2],
483
- solution: `add a heavy ball in the top left after 1 second`,
484
- setup: (World, Bodies, Composite) => {
485
- // Rotating Cross
486
- const cross = Matter.Body.create({
487
- parts: [
488
- Bodies.rectangle(pX(8), pY(4), pX(7), 10, { render: { fillStyle: CONFIG.colors.wall, ...strokeStyle } }),
489
- Bodies.rectangle(pX(8), pY(4), 10, pX(7), { render: { fillStyle: CONFIG.colors.wall, ...strokeStyle } })
490
- ],
491
- isStatic: true,
492
- });
493
- Matter.Events.on(STATE.engine, 'beforeUpdate', () => {
494
- if (STATE.isPlaying) {
495
- Matter.Body.rotate(cross, -0.01);
496
- }
497
- });
498
- World.add(Composite, cross);
499
- // Platform
500
- const platform = Bodies.rectangle(pX(8), pY(8)-10, pX(14), 20, {
501
- isStatic: true,
502
- render: { fillStyle: CONFIG.colors.wall, ...strokeStyle }
503
- });
504
- // Ball
505
- const ball = Bodies.circle(pX(2.1), pY(7), 20, {
506
- restitution: 0.5,
507
- friction: 0.1,
508
- render: { fillStyle: CONFIG.colors.ball, ...strokeStyle },
509
- });
510
- // Moving Goal
511
- const goal = Bodies.rectangle(pX(10), pY(13), 120, 120, {
512
- isStatic: true,
513
- isSensor: true,
514
- label: "GoalZone",
515
- render: { fillStyle: CONFIG.colors.goal, opacity: 0.3, ...strokeStyle },
516
- });
517
- // Goal Movement Logic
518
- const updateGoal = () => {
519
- if (!STATE.isPlaying) return;
520
- const time = STATE.time;
521
- const speed = 0.002;
522
- const range = pX(8);
523
- const center = pX(10);
524
- const x = center + Math.sin(time * speed) * range;
525
- Matter.Body.setPosition(goal, { x: x, y: pY(13) });
526
- };
527
- Matter.Events.on(STATE.engine, 'beforeUpdate', updateGoal);
528
- World.add(Composite, [platform, ball, goal]);
529
- return { ball, goal };
530
- },
531
- },
532
- {
533
- id: 6,
534
- title: "Catapult",
535
- difficulty: 3,
536
- desc: "Launch the ball into the goal! Can you time it right?",
537
- hint: "Drop a heavy object in the right place at the right time to send the ball flying.",
538
- stars: [1, 2],
539
- solution: `add a heavy square at 14, 1, delayed by 2 seconds`,
540
- setup: (World, Bodies, Composite) => {
541
- const group = Matter.Body.nextGroup(true);
542
- // Ramp for ball
543
- const ramp = Bodies.rectangle(pX(2), pY(11.5), pX(4), 5, {
544
- isStatic: true,
545
- angle: Math.PI / 16,
546
- render: { fillStyle: CONFIG.colors.wall, ...strokeStyle }
547
- });
548
- // Catapult
549
- const pivotX = pX(10);
550
- const pivotY = pY(13);
551
- const catapult = Bodies.rectangle(pivotX, pivotY - 10, pX(12), 10, {
552
- collisionFilter: { group: group },
553
- density: 0.0001,
554
- render: { fillStyle: CONFIG.colors.wall, ...strokeStyle }
555
- });
556
- const pivot = Bodies.rectangle(pivotX, pivotY + 20, 20, 60, {
557
- isStatic: true,
558
- collisionFilter: { group: group },
559
- render: { fillStyle: CONFIG.colors.wall, ...strokeStyle }
560
- });
561
- const constraint = Matter.Constraint.create({
562
- bodyA: catapult,
563
- pointB: { x: pivotX, y: pivotY - 10 },
564
- stiffness: 1.0,
565
- length: 0,
566
- render: { visible: true }
567
- });
568
- // Ball
569
- const ball = Bodies.circle(pX(1), pY(8), 20, {
570
- restitution: 0.0,
571
- friction: 0.0,
572
- density: 0.00001,
573
- render: { fillStyle: CONFIG.colors.ball, ...strokeStyle },
574
- });
575
- // Goal
576
- const goal = Bodies.rectangle(pX(18), pY(2), pX(4), pY(4), {
577
- isStatic: true,
578
- isSensor: true,
579
- label: "GoalZone",
580
- render: { fillStyle: CONFIG.colors.goal, opacity: 0.3, ...strokeStyle },
581
- });
582
- World.add(Composite, [ramp, catapult, pivot, constraint, ball, goal]);
583
- return { ball, goal };
584
- },
585
- },
586
- {
587
- id: 7,
588
- title: "Newton's Cradle",
589
- difficulty: 4,
590
- desc: "A chain reaction! The ball is part of a Newton's cradle. How can we get things moving?",
591
- hint: "Add heavy circles on the left and right to start the motion. You may need to time them carefully.",
592
- stars: [2, 4],
593
- solution: `Add a heavy circle in the top left.\nAdd a heavy circle in the top right, delayed by 10 seconds.`,
594
- setup: (World, Bodies, Composite) => {
595
- const xx = pX(8), yy = pY(2), number = 5, size = 25, length = pY(4);
596
- const separation = 2.1;
597
- for (let i = 0; i < number; i++) {
598
- const x = xx + i * (size * separation);
599
- const circle = Bodies.circle(x, yy + length, size, {
600
- inertia: Infinity, restitution: 0.1, friction: 0, frictionAir: 0, slop: size * 0.02,
601
- render: { fillStyle: CONFIG.colors.wall, ...strokeStyle }
602
- });
603
- const constraint = Matter.Constraint.create({
604
- pointA: { x: x, y: yy },
605
- bodyB: circle,
606
- stiffness: 1,
607
- length: length,
608
- render: { strokeStyle: '#000', lineWidth: 1 }
609
- });
610
- World.add(Composite, [circle, constraint]);
611
- }
612
- const ramp = Bodies.rectangle(pX(4), pY(5.5), pX(7), 10, {
613
- friction: 0.01,
614
- frictionStatic: 0.01,
615
- restitution: 0.1,
616
- isStatic: true, angle: Math.PI / 12, render: { fillStyle: CONFIG.colors.wall, ...strokeStyle }
617
- });
618
- // Tunnel walls
619
- const p1 = Bodies.rectangle(pX(14.5), yy + length + 30, pX(3), 10, {
620
- isStatic: true, render: { fillStyle: CONFIG.colors.wall, ...strokeStyle }
621
- });
622
- const p2 = Bodies.rectangle(pX(15.25), yy + length + 30 - pY(1) - 10, pX(4.5), 10, {
623
- isStatic: true, render: { fillStyle: CONFIG.colors.wall, ...strokeStyle }
624
- });
625
- const p3 = Bodies.rectangle(pX(17.85), pY(8), pX(4), 10, {
626
- isStatic: true, render: { fillStyle: CONFIG.colors.wall, ...strokeStyle }
627
- });
628
- const p4 = Bodies.rectangle(pX(19.75), pY(6.1), pX(4), 10, {
629
- // make vertical
630
- angle: Math.PI / 2,
631
- restitution: 0.1,
632
- isStatic: true, render: { fillStyle: CONFIG.colors.wall, ...strokeStyle }
633
- });
634
- const ball = Bodies.circle(xx + number * (size * separation), yy + length, 20, {
635
- restitution: 0.1, friction: 0.0, frictionAir: 0.0,
636
- render: { fillStyle: CONFIG.colors.ball, ...strokeStyle },
637
- });
638
- const goal = Bodies.rectangle(pX(6.5), pY(12.5), pX(8), pY(4), {
639
- isStatic: true,
640
- isSensor: true,
641
- label: "GoalZone",
642
- render: { fillStyle: CONFIG.colors.goal, opacity: 0.3, ...strokeStyle },
643
- });
644
- World.add(Composite, [ramp, p1, p2, p3, p4, ball, goal]);
645
- return { ball, goal };
646
- },
647
- }
648
- ];
649
- // --- State Management ---
650
- const STATE = {
651
- engine: null,
652
- render: null,
653
- runner: null,
654
- time: 0,
655
- startTime: 0,
656
- isPlaying: false,
657
- isFinished: false,
658
- eventQueue: [],
659
- plannedActions: [],
660
- currentLevelIndex: 0,
661
- currentBall: null,
662
- progress: { unlockedIndex: 0, stars: {} },
663
- winCondition: null,
664
- // Performance metrics
665
- modelCompilationTime: 0,
666
- lastTTFT: 0,
667
- lastTPS: 0,
668
- };
669
- function loadProgress() {
670
- const saved = localStorage.getItem(STORAGE_KEY);
671
- if (saved) {
672
- try {
673
- STATE.progress = JSON.parse(saved);
674
- } catch (e) {
675
- console.error("Save Corrupt");
676
- }
677
- }
678
- }
679
- function saveProgress() {
680
- localStorage.setItem(STORAGE_KEY, JSON.stringify(STATE.progress));
681
- }
682
- // --- Engine Initialization ---
683
- function initPhysics() {
684
- const container = document.getElementById("canvas-container");
685
- STATE.engine = Matter.Engine.create();
686
- STATE.engine.world.gravity.y = 1;
687
- STATE.engine.timing.timeScale = 0.5;
688
- STATE.render = Matter.Render.create({
689
- element: container,
690
- engine: STATE.engine,
691
- options: {
692
- width: CONFIG.width,
693
- height: CONFIG.height,
694
- wireframes: false,
695
- background: CONFIG.colors.background,
696
- pixelRatio: window.devicePixelRatio,
697
- showAngleIndicator: false,
698
- },
699
- });
700
- Matter.Events.on(STATE.render, "afterRender", function () {
701
- const context = STATE.render.context;
702
- const width = STATE.render.options.width;
703
- const height = STATE.render.options.height;
704
- // Grid
705
- context.beginPath();
706
- context.strokeStyle = "rgba(0, 0, 0, 0.1)";
707
- context.lineWidth = 1;
708
- context.font = "bold 10px 'Fira Code'";
709
- context.fillStyle = "rgba(0, 0, 0, 0.3)";
710
- context.textAlign = "center";
711
- for (let i = 0; i <= CONFIG.gridWidth; i++) {
712
- const x = (width * i) / CONFIG.gridWidth;
713
- context.moveTo(x, 0);
714
- context.lineTo(x, height);
715
- if (i > 0 && i < CONFIG.gridWidth) context.fillText(i, x, 15);
716
- }
717
- for (let i = 0; i <= CONFIG.gridHeight; i++) {
718
- const y = (height * i) / CONFIG.gridHeight;
719
- context.moveTo(0, y);
720
- context.lineTo(width, y);
721
- if (i > 0 && i < CONFIG.gridHeight) context.fillText(i, 15, y + 4);
722
- }
723
- context.stroke();
724
- // Custom Objects
725
- if (!STATE.isPlaying && !STATE.isFinished) {
726
- context.font = "bold 14px 'Fira Code', monospace";
727
- context.textAlign = "center";
728
- context.textBaseline = "middle";
729
- STATE.plannedActions.forEach((action) => {
730
- if (action.previewBody) {
731
- const body = action.previewBody;
732
- const x = body.position.x;
733
- const y = body.position.y;
734
- if (action.delay > 0) {
735
- context.fillStyle = "rgba(0, 0, 0, 0.7)";
736
- context.fillText(`in ${action.delay}s`, x, body.bounds.max.y + 20);
737
- }
738
- if (action.params.velocity) {
739
- const vx = action.params.velocity[0],
740
- vy = action.params.velocity[1];
741
- drawVelocityArrow(context, x, y, vx, vy, "#ef4444");
742
- }
743
- }
744
- });
745
- // Draw velocity arrows for any dynamic body in the world (Level setup items)
746
- Matter.Composite.allBodies(STATE.engine.world).forEach((body) => {
747
- // Skip planned action previews
748
- if (STATE.plannedActions.some((a) => a.previewBody === body)) return;
749
- const vx = body.velocity.x || 0;
750
- const vy = body.velocity.y || 0;
751
- if (Math.hypot(vx, vy) > 0.1) {
752
- let color = body.render.fillStyle;
753
- // Use red for dark objects (enemies), otherwise use body color
754
- if (color === "#333" || color === CONFIG.colors.wall) color = "#ef4444";
755
- drawVelocityArrow(context, body.position.x, body.position.y, vx, vy, color);
756
- }
757
- });
758
- }
759
- // Goal Zone
760
- const bodies = Matter.Composite.allBodies(STATE.engine.world);
761
- bodies.forEach((body) => {
762
- if (body.label === "GoalZone") {
763
- const bounds = body.bounds;
764
- const w = bounds.max.x - bounds.min.x;
765
- const h = bounds.max.y - bounds.min.y;
766
- const cx = (bounds.min.x + bounds.max.x) / 2;
767
- const cy = (bounds.min.y + bounds.max.y) / 2;
768
- context.save();
769
- context.translate(cx, cy);
770
- context.beginPath();
771
- context.rect(-w / 2, -h / 2, w, h);
772
- context.strokeStyle = "#4ade80";
773
- context.lineWidth = 3;
774
- context.setLineDash([15, 10]);
775
- context.stroke();
776
- context.fillStyle = "#1a1a1a";
777
- context.fillRect(-2, -20, 4, 40);
778
- context.beginPath();
779
- context.moveTo(2, -20);
780
- context.lineTo(25, -10);
781
- context.lineTo(2, 0);
782
- context.fill();
783
- context.font = "bold 14px 'Space Grotesk'";
784
- context.textAlign = "center";
785
- context.fillText("GOAL", 0, 35);
786
- context.restore();
787
- }
788
- });
789
- });
790
- const canvas = STATE.render.canvas;
791
- canvas.style.border = "3px solid black";
792
- const resizeCanvas = () => {
793
- const cw = container.clientWidth - 40;
794
- const ch = container.clientHeight - 40;
795
- const tr = CONFIG.width / CONFIG.height;
796
- const cr = cw / ch;
797
- if (cr > tr) {
798
- canvas.style.height = `${ch}px`;
799
- canvas.style.width = `${ch * tr}px`;
800
- } else {
801
- canvas.style.width = `${cw}px`;
802
- canvas.style.height = `${cw / tr}px`;
803
- }
804
- };
805
- const resizeObserver = new ResizeObserver(() => {
806
- window.requestAnimationFrame(() => resizeCanvas());
807
- });
808
- resizeObserver.observe(container);
809
- window.requestAnimationFrame(() => resizeCanvas());
810
- STATE.runner = Matter.Runner.create({ isFixed: true, delta: 1000 / 60 });
811
- Matter.Events.on(STATE.engine, "beforeUpdate", handleUpdate);
812
- Matter.Render.run(STATE.render);
813
- }
814
- function drawVelocityArrow(context, x, y, vx, vy, color) {
815
- if (vx === 0 && vy === 0) return;
816
- const speed = Math.hypot(vx, vy);
817
- const arrowScale = 20;
818
- const maxLength = 150;
819
- const rawLen = speed * arrowScale;
820
- const scaleFactor = Math.min(rawLen, maxLength) / speed;
821
- const endX = x + vx * scaleFactor;
822
- const endY = y + vy * scaleFactor;
823
- context.beginPath();
824
- context.moveTo(x, y);
825
- context.lineTo(endX, endY);
826
- context.strokeStyle = color;
827
- context.lineWidth = 3;
828
- context.stroke();
829
- const angle = Math.atan2(endY - y, endX - x);
830
- const headLen = 12;
831
- context.beginPath();
832
- context.moveTo(endX, endY);
833
- context.lineTo(endX - headLen * Math.cos(angle - Math.PI / 6), endY - headLen * Math.sin(angle - Math.PI / 6));
834
- context.lineTo(endX - headLen * Math.cos(angle + Math.PI / 6), endY - headLen * Math.sin(angle + Math.PI / 6));
835
- context.lineTo(endX, endY);
836
- context.fillStyle = color;
837
- context.fill();
838
- context.fillStyle = color;
839
- context.font = "bold 12px 'Fira Code', monospace";
840
- context.textAlign = "center";
841
- context.fillText(`${speed.toFixed(1)}`, endX + (vx / speed) * 15, endY + (vy / speed) * 15);
842
- }
843
- function handleUpdate() {
844
- if (!STATE.isPlaying || STATE.isFinished) return;
845
- STATE.time = performance.now() - STATE.startTime;
846
- document.getElementById("timer-display").innerText = (STATE.time / 1000).toFixed(2) + "s";
847
- STATE.eventQueue
848
- .filter((e) => e.delay <= STATE.time && !e.executed)
849
- .forEach((e) => {
850
- e.action();
851
- e.executed = true;
852
- });
853
- if (STATE.winCondition && STATE.winCondition()) endGame(true);
854
- }
855
- function loadLevel(index) {
856
- if (STATE.runner) Matter.Runner.stop(STATE.runner);
857
- document.getElementById("view-menu").classList.add("hidden");
858
- document.getElementById("view-game").classList.remove("hidden");
859
- document.getElementById("view-game").classList.add("flex");
860
- STATE.currentLevelIndex = index;
861
- const level = LEVELS[index];
862
- const diffs = ["", "EASY", "MEDIUM", "HARD", "EXTREME"];
863
- const diffColors = ["", "bg-neo-green", "bg-neo-yellow", "bg-neo-red", "bg-neo-purple"];
864
- const d = level.difficulty || 1;
865
- document.getElementById("level-indicator").innerHTML = `LEVEL ${index + 1} <span class="ml-2 px-1 ${diffColors[d]} text-black text-[10px] border border-black">${diffs[d]}</span>`;
866
- document.getElementById("level-title").innerText = level.title;
867
- document.getElementById("level-desc").innerText = level.desc;
868
- document.getElementById("level-hint-text").innerText = level.hint;
869
- document.getElementById("star-reqs").innerText = `3★ < ${level.stars[0] + 1} items`;
870
- document.getElementById("hint-content").classList.add("hidden");
871
- resetSimulationState();
872
- document.getElementById("code-input").value = "";
873
- const { ball, goal } = level.setup(Matter.World, Matter.Bodies, STATE.engine.world, () => {});
874
- STATE.currentBall = ball;
875
- STATE.winCondition = () => Matter.Collision.collides(ball, goal) !== null;
876
- Matter.Render.world(STATE.render);
877
- }
878
- function resetSimulationState() {
879
- Matter.Composite.clear(STATE.engine.world);
880
- STATE.engine.events = {};
881
- Matter.Events.on(STATE.engine, "beforeUpdate", handleUpdate);
882
- STATE.plannedActions = [];
883
- STATE.eventQueue = [];
884
- STATE.time = 0;
885
- STATE.isFinished = false;
886
- STATE.isPlaying = false;
887
- STATE.currentBall = null;
888
- document.getElementById("timer-display").innerText = "0.00s";
889
- document.getElementById("win-message").style.display = "none";
890
- document.getElementById("error-log").classList.add("hidden");
891
- document.getElementById("status-badge").innerHTML = `<div class="w-3 h-3 bg-red-500 rounded-full border border-black animate-pulse"></div> READY`;
892
- updateActionList();
893
- }
894
- function endGame(success) {
895
- STATE.isFinished = true;
896
- STATE.isPlaying = false;
897
- Matter.Runner.stop(STATE.runner);
898
- if (success) {
899
- document.getElementById("status-badge").innerHTML = `<div class="w-3 h-3 bg-green-500 rounded-full border border-black"></div> COMPLETE`;
900
- const used = STATE.plannedActions.length;
901
- const level = LEVELS[STATE.currentLevelIndex];
902
- let stars = 1;
903
- if (used <= level.stars[0]) stars = 3;
904
- else if (used <= level.stars[1]) stars = 2;
905
- if (stars > (STATE.progress.stars[level.id] || 0)) STATE.progress.stars[level.id] = stars;
906
- if (STATE.currentLevelIndex >= STATE.progress.unlockedIndex && STATE.currentLevelIndex < LEVELS.length - 1) {
907
- STATE.progress.unlockedIndex = STATE.currentLevelIndex + 1;
908
- }
909
- saveProgress();
910
- const starContainer = document.getElementById("result-stars");
911
- starContainer.innerHTML = "";
912
- for (let i = 0; i < 3; i++) {
913
- const filled = i < stars;
914
- starContainer.innerHTML += `<i class="fas fa-star ${filled ? "text-yellow-400" : "text-gray-600"}"></i>`;
915
- }
916
- document.getElementById("result-items").innerText = used;
917
- const btnNext = document.getElementById("btn-next-level");
918
- if (STATE.currentLevelIndex >= LEVELS.length - 1) {
919
- // Update Max
920
- btnNext.style.display = "none";
921
- } else {
922
- btnNext.style.display = "inline-block";
923
- btnNext.onclick = () => loadLevel(STATE.currentLevelIndex + 1);
924
- }
925
- document.getElementById("win-message").style.display = "flex";
926
- }
927
- }
928
- function updateActionList() {
929
- const list = document.getElementById("action-list");
930
- list.innerHTML = "";
931
- if (STATE.plannedActions.length === 0) {
932
- list.innerHTML = `<div class="text-xs text-gray-400 text-center mt-4 italic">No elements added yet.</div>`;
933
- return;
934
- }
935
- STATE.plannedActions.forEach((action, index) => {
936
- const div = document.createElement("div");
937
- div.className = "action-item flex justify-between items-center bg-white border border-black p-2 shadow-[2px_2px_0px_0px_rgba(0,0,0,0.1)] hover:shadow-[2px_2px_0px_0px_#000] transition-shadow group";
938
-
939
- const p = action.params;
940
- const details = [];
941
- if (p.size !== 1) details.push(`size:${p.size}`);
942
- if (p.weight !== 1) details.push(`mass:${p.weight}`);
943
- if (p.restitution !== 0) details.push(`bounce:${p.restitution}`);
944
- if (p.angle !== 0) details.push(`angle:${p.angle}°`);
945
- if (p.velocity && (p.velocity[0] !== 0 || p.velocity[1] !== 0)) details.push(`v:[${p.velocity}]`);
946
- if (p.isStatic !== (p.shape === 'line')) details.push(p.isStatic ? 'static' : 'dynamic');
947
- if (p.delay > 0) details.push(`delay:${p.delay}s`);
948
- const typeName = p.shape;
949
- const color = p.color || "#000";
950
- div.innerHTML = `
951
- <div class="flex items-center gap-2">
952
- <div class="w-3 h-3 border border-black" style="background-color: ${color}"></div>
953
- <div class="flex flex-col">
954
- <div class="flex items-center gap-2">
955
- <span class="text-xs font-bold uppercase">${typeName}</span>
956
- <span class="text-[10px] text-gray-500 font-mono">@ [${p.location[0]}, ${p.location[1]}]</span>
957
- </div>
958
- ${details.length > 0 ? `<div class="text-[9px] text-gray-400 font-mono leading-tight uppercase">${details.join(' | ')}</div>` : ''}
959
- </div>
960
- </div>
961
- `;
962
- const btnDelete = document.createElement("button");
963
- btnDelete.className = "btn-delete w-6 h-6 flex items-center justify-center bg-red-100 hover:bg-red-500 hover:text-white text-red-500 border border-black rounded transition-colors";
964
- btnDelete.innerHTML = `<i class="fas fa-times text-xs"></i>`;
965
- btnDelete.onclick = () => removeAction(index);
966
- div.appendChild(btnDelete);
967
- list.appendChild(div);
968
- });
969
- }
970
- function removeAction(index) {
971
- const action = STATE.plannedActions[index];
972
- if (action.previewBody) Matter.World.remove(STATE.engine.world, action.previewBody);
973
- STATE.plannedActions.splice(index, 1);
974
- updateActionList();
975
- }
976
- function createBody(shape, x, y, pixelSize, options) {
977
- switch (shape) {
978
- case "circle":
979
- case "ball":
980
- return Matter.Bodies.circle(x, y, pixelSize / 2, options);
981
- case "triangle":
982
- return Matter.Bodies.polygon(x, y, 3, pixelSize / 1.5, options);
983
- case "line":
984
- const thickness = 5;
985
- return Matter.Bodies.rectangle(x, y, pixelSize, thickness, options);
986
- default: // square, rectangle, etc.
987
- return Matter.Bodies.rectangle(x, y, pixelSize, pixelSize, options);
988
- }
989
- }
990
- // --- UNIFIED TOOL IMPLEMENTATION ---
991
- const Tools = {
992
- add: (params) => {
993
- let {
994
- shape = "square",
995
- location = [CONFIG.gridWidth / 2, CONFIG.gridHeight / 2],
996
- size = 1,
997
- color = CONFIG.colors.userShape,
998
- mass = 1,
999
- weight = params.weight || mass,
1000
- delay = 0,
1001
- restitution = 0,
1002
- friction = 0.1,
1003
- rotation = 0,
1004
- angle = params.angle || rotation,
1005
- velocity = [0, 0],
1006
- static: isStaticParam,
1007
- isStatic = params.isStatic !== undefined ? params.isStatic : (isStaticParam !== undefined ? isStaticParam : ["platform", "line"].includes(shape)),
1008
- } = params;
1009
- // Handle location as string (comma-separated or descriptive)
1010
- if (typeof location === 'string') {
1011
- const parts = location.match(/\d+/g);
1012
- if (parts && parts.length === 2 && !isNaN(parts[0]) && !isNaN(parts[1])) {
1013
- location = [parseFloat(parts[0]), parseFloat(parts[1])];
1014
- } else {
1015
- const loc = location.toLowerCase();
1016
- const midX = CONFIG.gridWidth / 2;
1017
- const midY = CONFIG.gridHeight / 2;
1018
- const margin = 2;
1019
- const locations = {
1020
- "center": [midX, midY],
1021
- "top-left": [margin, margin],
1022
- "top-center": [midX, margin],
1023
- "top-right": [CONFIG.gridWidth - margin, margin],
1024
- "center-left": [margin, midY],
1025
- "center": [midX, midY],
1026
- "center-right": [CONFIG.gridWidth - margin, midY],
1027
- "bottom-left": [margin, CONFIG.gridHeight - margin],
1028
- "bottom-center": [midX, CONFIG.gridHeight - margin],
1029
- "bottom-right": [CONFIG.gridWidth - margin, CONFIG.gridHeight - margin],
1030
- };
1031
- location = locations[loc] || [midX, midY];
1032
- }
1033
- }
1034
- // Ensure location is an array
1035
- if (!Array.isArray(location) || location.length !== 2) {
1036
- console.warn("Invalid location format. Defaulting to center.");
1037
- location = [CONFIG.gridWidth / 2, CONFIG.gridHeight / 2];
1038
- }
1039
- const finalParams = {
1040
- shape,
1041
- location,
1042
- size,
1043
- color,
1044
- weight,
1045
- delay,
1046
- restitution,
1047
- friction,
1048
- angle,
1049
- velocity,
1050
- isStatic,
1051
- };
1052
- const x = pX(location[0]);
1053
- const y = pY(location[1]);
1054
- const pixelSize = pX(size); // Use grid-relative sizing
1055
- const commonProps = {
1056
- angle: (angle * Math.PI) / 180,
1057
- restitution: restitution,
1058
- friction: friction,
1059
- render: { fillStyle: color, ...strokeStyle },
1060
- density: weight * 0.005,
1061
- };
1062
- // For preview (ghosts)
1063
- const previewProps = {
1064
- ...commonProps,
1065
- isStatic: true,
1066
- isSensor: true,
1067
- render: {
1068
- fillStyle: color,
1069
- opacity: 0.4,
1070
- strokeStyle: "#000000",
1071
- lineWidth: 2,
1072
- },
1073
- };
1074
- const body = createBody(shape, x, y, pixelSize, previewProps);
1075
- Matter.World.add(STATE.engine.world, body);
1076
- STATE.plannedActions.push({
1077
- type: "add",
1078
- params: finalParams, // Use the complete object
1079
- delay: delay || 0,
1080
- previewBody: body,
1081
- });
1082
- },
1083
- };
1084
- // --- AI Model Setup ---
1085
- const MODEL_ID = "Xenova/functiongemma-270m-game";
1086
- let tokenizer, model;
1087
- const TOOL_SCHEMA = [{"type": "function", "function": {"name": "add", "description": "Add a shape into the game scene.", "parameters": {"type": "object", "properties": {"shape": {"type": "string", "enum": ["circle", "square", "triangle", "star", "rectangle", "line", "ellipse"], "description": "The kind shape to add. Required."}, "location": {"type": "string", "description": "The [x, y] coordinates where the shape will be placed or a descriptive string. Required."}, "size": {"type": "number", "description": "The size of the object (between 0.1 and 10.0). Default is 1.0."}, "rotation": {"type": "integer", "description": "The initial clockwise rotation of the object in degrees (0-360). Default is 0."}, "friction": {"type": "number", "description": "The friction of the object (between 0.0 and 1.0). Default is 0.0."}, "restitution": {"type": "number", "description": "The bounciness of the object (between 0.0 and 1.0). Default is 0.0."}, "mass": {"type": "number", "description": "The mass of the object (between 1.0 and 10.0). Default is 1.0."}, "delay": {"type": "number", "description": "The time in seconds to wait before the object appears in the scene. Default is 0.0."}, "static": {"type": "boolean", "description": "Whether the object is static (immovable) or dynamic. Default is False."}, "velocity": {"type": "array", "items": {"type": "number"}, "description": "The initial [vx, vy] velocity vector of the object (values between -10.0 and 10.0). Default is [0.0, 0.0]."}, "color": {"type": "string", "description": "The color of the object as a string or hex code (e.g., \"red\", \"blue\", \"#FF00FF\"). Default is \"red\"."}}, "required": ["shape", "location"]}, "return": {"type": "string", "description": "A unique identifier for the added shape."}}}];
1088
- async function initModel() {
1089
- try {
1090
- const compilationStart = performance.now();
1091
- console.log("⏱️ Starting model compilation...");
1092
-
1093
- tokenizer = await AutoTokenizer.from_pretrained(MODEL_ID);
1094
- model = await AutoModelForCausalLM.from_pretrained(MODEL_ID, {
1095
- device: "webgpu",
1096
- dtype: "q4",
1097
- });
1098
-
1099
- const compilationEnd = performance.now();
1100
- STATE.modelCompilationTime = compilationEnd - compilationStart;
1101
- console.log(`✅ Model compiled in ${STATE.modelCompilationTime.toFixed(2)}ms (${(STATE.modelCompilationTime / 1000).toFixed(2)}s)`);
1102
-
1103
- // Update UI
1104
- document.getElementById("metric-compilation").innerText = `${(STATE.modelCompilationTime / 1000).toFixed(2)}s`;
1105
- document.getElementById("loading-overlay").classList.add("hidden");
1106
- } catch (e) {
1107
- console.error(e);
1108
- document.getElementById("loading-overlay").innerHTML = `<div class="text-red-500 font-bold p-4 bg-white border-2 border-black">Error: ${e.message}</div>`;
1109
- }
1110
- }
1111
- // --- Command Execution ---
1112
- async function executeCommand() {
1113
- const input = document.getElementById("code-input").value.trim();
1114
- if (!input) return;
1115
- const btn = document.getElementById("btn-execute");
1116
- const errorLog = document.getElementById("error-log");
1117
- // UI Loading State
1118
- btn.disabled = true;
1119
- btn.innerHTML = `<i class="fas fa-spinner fa-spin"></i> THINKING...`;
1120
- errorLog.classList.add("hidden");
1121
- const systemPrompt = `You are a model that can do function calling with the following functions`;
1122
- try {
1123
- const lines = input.split('\n').map(l => l.trim()).filter(l => l.length > 0 && !l.startsWith("//"));
1124
- for (const line of lines) {
1125
- console.log(`\n🎯 Processing command: "${line}"`);
1126
-
1127
- // 2. Prepare Messages
1128
- const messages = [
1129
- { role: "developer", content: systemPrompt },
1130
- { role: "user", content: line },
1131
- ];
1132
- // 3. Apply Template
1133
- const inputs = tokenizer.apply_chat_template(messages, {
1134
- tools: TOOL_SCHEMA,
1135
- tokenize: true,
1136
- add_generation_prompt: true,
1137
- return_dict: true,
1138
- });
1139
-
1140
- // 4. Generate with performance tracking
1141
- const generationStart = performance.now();
1142
- let ttftRecorded = false;
1143
- let ttft = 0;
1144
- let firstTokenTime = 0;
1145
-
1146
- const output = await model.generate({
1147
- ...inputs,
1148
- max_new_tokens: 128,
1149
- do_sample: false,
1150
- callback_function: (tokens) => {
1151
- // Record TTFT on first token
1152
- if (!ttftRecorded && tokens.length > 0) {
1153
- firstTokenTime = performance.now();
1154
- ttft = firstTokenTime - generationStart;
1155
- ttftRecorded = true;
1156
- console.log(`⚡ Time to First Token (TTFT): ${ttft.toFixed(2)}ms`);
1157
- }
1158
- }
1159
- });
1160
-
1161
- const generationEnd = performance.now();
1162
- const totalTime = generationEnd - generationStart;
1163
- const inputTokenCount = inputs.input_ids.dims[1];
1164
- // Handle tensor/array output properly - output is [batch_size, sequence_length]
1165
- const totalTokenCount = output.dims ? output.dims[1] : (Array.isArray(output[0]) ? output[0].length : output[0].size);
1166
- const outputTokenCount = Math.max(0, totalTokenCount - inputTokenCount);
1167
- const tps = outputTokenCount / (totalTime / 1000);
1168
-
1169
- // Store metrics
1170
- STATE.lastTTFT = ttft;
1171
- STATE.lastTPS = tps;
1172
-
1173
- // Log performance
1174
- console.log(`📊 Performance Metrics:`);
1175
- console.log(` - Total generation time: ${totalTime.toFixed(2)}ms`);
1176
- console.log(` - Input tokens: ${inputTokenCount}`);
1177
- console.log(` - Total tokens: ${totalTokenCount}`);
1178
- console.log(` - Output tokens: ${outputTokenCount}`);
1179
- console.log(` - Tokens Per Second (TPS): ${tps.toFixed(2)} tok/s`);
1180
-
1181
- // Update UI
1182
- document.getElementById("metric-ttft").innerText = `${ttft.toFixed(0)}ms`;
1183
- document.getElementById("metric-tps").innerText = `${tps.toFixed(1)} t/s`;
1184
-
1185
- const decoded = tokenizer.decode(output[0].slice(inputs.input_ids.dims[1]), { skip_special_tokens: false });
1186
- // 5. Parse Output
1187
- // Format: <start_function_call>call:add{...}<end_function_call>
1188
- const startTag = "<start_function_call>";
1189
- const endTag = "<end_function_call>";
1190
- const startIndex = decoded.indexOf(startTag);
1191
- const endIndex = decoded.indexOf(endTag);
1192
- if (startIndex !== -1 && endIndex !== -1) {
1193
- let callStr = decoded.substring(startIndex + startTag.length, endIndex);
1194
- if (callStr.startsWith("call:add")) {
1195
- // Extract JSON-like string: {location:[...],shape:<escape>...<escape>}
1196
- let argsStr = callStr.substring(callStr.indexOf("{"));
1197
- // Sanitize to valid JSON
1198
- argsStr = argsStr
1199
- .replace(/<escape>(.*?)<escape>/g, '"$1"') // Handle string escapes
1200
- .replace(/(\w+):/g, '"$1":'); // Quote keys
1201
- const args = JSON.parse(argsStr);
1202
- Tools.add(args);
1203
- } else {
1204
- throw new Error("Model did not generate a valid add command.");
1205
- }
1206
- } else {
1207
- throw new Error(`Could not understand command: "${line}"`);
1208
- }
1209
- }
1210
- document.getElementById("code-input").value = "";
1211
- } catch (err) {
1212
- errorLog.innerText = err.message;
1213
- errorLog.classList.remove("hidden");
1214
- console.error(err);
1215
- } finally {
1216
- btn.disabled = false;
1217
- btn.innerHTML = `<i class="fas fa-terminal"></i> EXECUTE`;
1218
- updateActionList();
1219
- }
1220
- }
1221
- // --- Real Simulation Run ---
1222
- function runSimulation() {
1223
- if (STATE.runner) Matter.Runner.stop(STATE.runner);
1224
- const storedActions = [...STATE.plannedActions];
1225
- const level = LEVELS[STATE.currentLevelIndex];
1226
- Matter.Composite.clear(STATE.engine.world);
1227
- STATE.engine.events = {};
1228
- Matter.Events.on(STATE.engine, "beforeUpdate", handleUpdate);
1229
- STATE.eventQueue = [];
1230
- STATE.isFinished = false;
1231
- const addEvent = (delay, action) => {
1232
- STATE.eventQueue.push({
1233
- delay: delay * 1000,
1234
- action: action,
1235
- executed: false,
1236
- });
1237
- };
1238
- const { ball, goal } = level.setup(Matter.World, Matter.Bodies, STATE.engine.world, addEvent);
1239
- STATE.currentBall = ball;
1240
- STATE.winCondition = () => Matter.Collision.collides(ball, goal) !== null;
1241
- // Real Action Executioner
1242
- const executeReal = (params) => {
1243
- const { shape, location, size, weight, restitution, friction, angle, velocity, isStatic } = params;
1244
- const x = pX(location[0]),
1245
- y = pY(location[1]);
1246
- const pixelSize = pX(size);
1247
- const props = {
1248
- angle: (angle * Math.PI) / 180,
1249
- restitution,
1250
- friction,
1251
- isStatic,
1252
- render: strokeStyle,
1253
- density: weight * 0.005,
1254
- };
1255
- const body = createBody(shape, x, y, pixelSize, props);
1256
- if (!isStatic && (velocity[0] !== 0 || velocity[1] !== 0)) {
1257
- Matter.Body.setVelocity(body, { x: velocity[0], y: velocity[1] });
1258
- }
1259
- Matter.World.add(STATE.engine.world, body);
1260
- };
1261
- storedActions.forEach((action) => {
1262
- STATE.eventQueue.push({
1263
- delay: action.delay * 1000,
1264
- action: () => executeReal(action.params),
1265
- executed: false,
1266
- });
1267
- });
1268
- STATE.plannedActions = storedActions;
1269
- STATE.isPlaying = true;
1270
- STATE.startTime = performance.now();
1271
- document.getElementById("status-badge").innerHTML = `<div class="w-3 h-3 bg-green-500 rounded-full border border-black animate-ping"></div> RUNNING`;
1272
- Matter.Runner.run(STATE.runner, STATE.engine);
1273
- }
1274
- function renderMenu() {
1275
- if (STATE.runner) Matter.Runner.stop(STATE.runner);
1276
- document.getElementById("view-game").classList.add("hidden");
1277
- document.getElementById("view-game").classList.remove("flex");
1278
- document.getElementById("view-menu").classList.remove("hidden");
1279
- const grid = document.getElementById("level-grid");
1280
- grid.innerHTML = "";
1281
- LEVELS.forEach((level, index) => {
1282
- const unlocked = index <= STATE.progress.unlockedIndex;
1283
- const stars = STATE.progress.stars[index] || 0;
1284
- const card = document.createElement("div");
1285
- card.className = `level-card neo-border p-4 flex flex-col items-center justify-center gap-2 aspect-square ${unlocked ? "bg-white cursor-pointer" : "locked"}`;
1286
- if (unlocked) {
1287
- card.onclick = () => loadLevel(index);
1288
- card.innerHTML = `
1289
- <div class="text-3xl font-black">${index + 1}</div>
1290
- <div class="flex gap-1 text-xs">
1291
- ${Array(3)
1292
- .fill(0)
1293
- .map((_, i) => `<i class="fas fa-star ${i < stars ? "text-neo-yellow" : "text-gray-300"}"></i>`)
1294
- .join("")}
1295
- </div>
1296
- `;
1297
- } else {
1298
- card.innerHTML = `<i class="fas fa-lock text-gray-400 text-2xl"></i>`;
1299
- }
1300
- grid.appendChild(card);
1301
- });
1302
- }
1303
- // --- Bindings ---
1304
- document.getElementById("btn-execute").onclick = executeCommand;
1305
- document.getElementById("btn-play").onclick = runSimulation;
1306
- document.getElementById("btn-reset").onclick = () => {
1307
- Matter.Runner.stop(STATE.runner);
1308
- const level = LEVELS[STATE.currentLevelIndex];
1309
- Matter.Composite.clear(STATE.engine.world);
1310
- const { ball, goal } = level.setup(Matter.World, Matter.Bodies, STATE.engine.world, () => {});
1311
- STATE.currentBall = ball;
1312
- STATE.plannedActions.forEach((action) => {
1313
- Matter.World.add(STATE.engine.world, action.previewBody);
1314
- });
1315
- STATE.isPlaying = false;
1316
- document.getElementById("status-badge").innerHTML = `<div class="w-3 h-3 bg-red-500 rounded-full border border-black animate-pulse"></div> READY`;
1317
- document.getElementById("timer-display").innerText = "0.00s";
1318
- document.getElementById("win-message").style.display = "none";
1319
- };
1320
- document.getElementById("btn-clear-all").onclick = () => {
1321
- document.getElementById("code-input").value = "";
1322
- STATE.plannedActions.forEach((action) => {
1323
- if (action.previewBody) Matter.World.remove(STATE.engine.world, action.previewBody);
1324
- });
1325
- STATE.plannedActions = [];
1326
- updateActionList();
1327
- document.getElementById("btn-reset").click();
1328
- };
1329
- document.getElementById("btn-back").onclick = renderMenu;
1330
- document.getElementById("btn-hint").onclick = () => {
1331
- const content = document.getElementById("hint-content");
1332
- content.classList.toggle("hidden");
1333
- };
1334
- document.getElementById("btn-solution").addEventListener("click", () => {
1335
- const level = LEVELS[STATE.currentLevelIndex];
1336
- if (level.solution) {
1337
- document.getElementById("code-input").value = level.solution;
1338
- }
1339
- });
1340
- window.onload = () => {
1341
- loadProgress();
1342
- initPhysics();
1343
- renderMenu();
1344
- initModel();
1345
- };
1346
- </script>
1347
- </body>
1348
-
1349
- </html>