Added the progress bar to track the model loading progress

#2
Files changed (1) hide show
  1. index.html +834 -339
index.html CHANGED
@@ -1,245 +1,353 @@
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
-
52
- /* Custom Scrollbar */
53
- ::-webkit-scrollbar {
54
- width: 12px;
55
- height: 12px;
56
- }
57
-
58
- ::-webkit-scrollbar-track {
59
- background: #fff;
60
- border-left: 2px solid black;
61
- }
62
-
63
- ::-webkit-scrollbar-thumb {
64
- background: #000;
65
- border: 2px solid #fff;
66
- }
67
-
68
- ::-webkit-scrollbar-thumb:hover {
69
- background: #333;
70
- }
71
-
72
- .neo-border {
73
- border: 3px solid black;
74
- }
75
-
76
- .neo-btn {
77
- transition: all 0.1s ease-in-out;
78
- }
79
-
80
- .neo-btn:active {
81
- transform: translate(2px, 2px);
82
- box-shadow: 2px 2px 0px 0px #000000;
83
- }
84
-
85
- .level-card {
86
- transition: all 0.2s;
87
- }
88
-
89
- .level-card:hover:not(.locked) {
90
- transform: translate(-2px, -2px);
91
- box-shadow: 6px 6px 0px 0px #000000;
92
- }
93
-
94
- .locked {
95
- background-color: #e2e8f0;
96
- cursor: not-allowed;
97
- opacity: 0.7;
98
- background-image: repeating-linear-gradient(45deg, #cbd5e1 0, #cbd5e1 1px, transparent 0, transparent 50%);
99
- background-size: 10px 10px;
100
- }
101
-
102
- .code-editor {
103
- font-family: "Fira Code", monospace;
104
- background-color: #ffffff;
105
- color: #000000;
106
- line-height: 1.6;
107
- }
108
-
109
- .toggle-checkbox:checked {
110
- right: 0;
111
- border-color: #4ade80;
112
- }
113
-
114
- .toggle-checkbox:checked+.toggle-label {
115
- background-color: #4ade80;
116
- }
117
-
118
- /* Action Item Delete Button Transition */
119
- .action-item .btn-delete {
120
- opacity: 0;
121
- transition: opacity 0.2s;
122
- }
123
-
124
- .action-item:hover .btn-delete {
125
- opacity: 1;
126
- }
127
- </style>
128
- </head>
129
-
130
- <body class="h-screen w-screen flex flex-col md:flex-row p-4 gap-4">
131
- <!-- Loading Overlay -->
132
- <div id="loading-overlay" class="fixed inset-0 bg-black/90 z-50 flex flex-col items-center justify-center">
133
- <div class="animate-spin rounded-full h-16 w-16 border-t-4 border-b-4 border-neo-green mb-4"></div>
134
- <div class="text-white font-mono font-bold text-xl">LOADING MODEL...</div>
135
- <div class="text-gray-400 font-mono text-sm mt-2">This may take a moment</div>
136
- </div>
137
-
138
- <!-- Sidebar / Controls -->
139
- <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">
140
- <!-- Decorative Header Strip -->
141
- <div class="h-4 w-full bg-neo-black"></div>
142
-
143
- <div class="p-6 border-b-2 border-black bg-neo-yellow flex justify-between items-center shrink-0">
144
- <div>
145
- <h1 class="text-3xl font-bold text-black uppercase tracking-tighter">Function <span
146
- class="text-white bg-black px-1">Gemma</span></h1>
147
- <div class="text-xs font-mono font-bold text-black mt-1">PHYSICS PLAYGROUND</div>
148
  </div>
149
- </div>
150
 
151
- <!-- MENU: Level Select -->
152
- <div id="view-menu" class="flex-1 overflow-y-auto p-6 bg-white hidden">
153
- <h2 class="font-bold text-xl mb-4 border-b-2 border-black pb-2">SELECT LEVEL</h2>
154
- <div id="level-grid" class="grid grid-cols-2 gap-4">
155
- <!-- Injected via JS -->
 
 
 
 
 
 
 
 
 
156
  </div>
157
  </div>
158
 
159
- <!-- MENU: Game View -->
160
- <div id="view-game" class="flex-1 flex flex-col overflow-hidden relative">
161
- <div class="overflow-y-auto flex-1 p-6 flex flex-col gap-6">
162
- <!-- Level Header -->
 
 
 
 
 
 
163
  <div>
164
- <div class="flex items-center gap-2 mb-2">
165
- <button id="btn-back"
166
- class="neo-btn p-2 border-2 border-black shadow-neo-sm bg-white hover:bg-gray-100 text-xs font-bold"><i
167
- class="fas fa-arrow-left"></i> LEVELS</button>
168
- <div class="font-bold text-sm bg-black text-white px-2 py-1 ml-auto" id="level-indicator">LEVEL 1</div>
169
  </div>
 
 
170
 
171
- <!-- Level Info Card -->
172
- <div class="neo-border p-4 bg-neo-blue shadow-neo-sm relative overflow-hidden group">
173
- <div
174
- class="absolute -right-4 -top-4 w-16 h-16 bg-white/20 rounded-full group-hover:scale-150 transition-transform">
175
- </div>
176
- <div class="flex justify-between items-center mb-3 relative z-10">
177
- <h2 class="font-bold text-black text-lg border-b-2 border-black inline-block bg-white px-2"
178
- id="level-title">...</h2>
179
- <span id="timer-display"
180
- 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>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
181
  </div>
182
- <p class="text-sm text-black font-medium opacity-90 mb-2" id="level-desc">...</p>
183
-
184
- <!-- Collapsible Hint -->
185
- <div class="mt-2">
186
- <button id="btn-hint"
187
- 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
188
- class="fas fa-lightbulb text-yellow-500"></i> SHOW HINT</button>
189
- <div id="hint-content" class="mt-2 bg-white/50 p-2 border-2 border-black text-xs font-mono hidden"><i
190
- class="fas fa-code mr-1"></i> <span id="level-hint-text">...</span></div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
191
  </div>
192
  </div>
193
- </div>
194
 
195
- <!-- Editor / Command Input -->
196
- <div class="flex flex-col gap-2 shrink-0">
197
- <div class="flex justify-between items-end">
198
- <div class="flex gap-2 items-center">
199
- <label class="text-sm font-bold bg-black text-white px-2 py-0.5 shadow-neo-sm">COMMAND</label>
200
- <button id="btn-solution"
201
- 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"
202
- title="Load Example Solution"><i class="fas fa-magic"></i> VIEW SOLUTION</button>
203
- </div>
204
- <div class="text-xs font-bold text-gray-500" id="star-reqs">3★ < 2 items</div>
 
 
 
 
 
 
 
 
 
205
  </div>
206
  <div class="relative flex flex-col gap-2">
207
- <textarea id="code-input"
 
208
  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"
209
- spellcheck="false" placeholder="e.g., Add a circle in the middle. You can execute multiple commands by separating them with new lines."></textarea>
210
- <button id="btn-execute"
211
- 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
212
- class="fas fa-terminal"></i> EXECUTE</button>
213
- </div>
214
- <div id="error-log"
215
- class="text-white bg-neo-red border-2 border-black text-xs font-bold px-2 py-1 shadow-neo-sm hidden">
 
 
216
  </div>
 
 
 
 
217
  </div>
218
 
219
  <!-- Active Elements List -->
220
  <div class="flex-1 min-h-0 flex flex-col">
221
- <label class="text-sm font-bold bg-black text-white px-2 py-0.5 shadow-neo-sm w-max mb-1">SCENE
222
- OBJECTS</label>
223
- <div id="action-list"
224
- class="flex-1 overflow-y-auto border-2 border-black bg-gray-50 p-2 space-y-2 shadow-neo-sm min-h-[100px]">
225
- <div class="text-xs text-gray-400 text-center mt-4 italic">No elements added yet.</div>
 
 
 
 
 
 
226
  </div>
227
  </div>
228
  </div>
229
 
230
  <!-- Action Bar -->
231
- <div class="p-4 bg-gray-100 border-t-2 border-black flex gap-3 shrink-0">
232
- <button id="btn-play"
233
- 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
234
- class="fas fa-play"></i> RUN</button>
235
- <button id="btn-reset"
 
 
 
 
 
 
236
  class="neo-btn px-4 py-3 bg-white border-2 border-black shadow-neo text-black font-bold hover:bg-gray-100"
237
- title="Reset Simulation">
 
238
  <i class="fas fa-undo"></i>
239
  </button>
240
- <button id="btn-clear-all"
 
241
  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"
242
- title="Clear All Objects">
 
243
  <i class="fas fa-trash-alt"></i>
244
  </button>
245
  </div>
@@ -249,27 +357,44 @@
249
  <!-- Main Canvas Area -->
250
  <div class="flex-1 relative flex items-center justify-center p-2">
251
  <!-- Canvas Container -->
252
- <div id="canvas-container"
253
- class="w-full h-full bg-white neo-border shadow-neo flex justify-center items-center relative overflow-hidden">
 
 
254
  <!-- Win Overlay -->
255
- <div id="win-message"
256
- class="absolute z-50 hidden w-full h-full flex items-center justify-center bg-black/50 backdrop-blur-sm">
 
 
257
  <div
258
- 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">
 
259
  <h2 class="text-4xl font-black mb-2 text-black">LEVEL CLEAR!</h2>
260
 
261
- <div class="flex justify-center gap-2 mb-4 text-4xl text-white drop-shadow-md" id="result-stars">
 
 
 
262
  <!-- Stars injected here -->
263
  </div>
264
 
265
- <div class="text-sm font-bold mb-6 font-mono">ITEMS USED: <span id="result-items">0</span></div>
 
 
266
 
267
  <div class="flex flex-col gap-2">
268
- <button id="btn-next-level"
269
- 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
270
- LEVEL <i class="fas fa-arrow-right ml-2"></i></button>
271
- <button onclick="document.getElementById('btn-reset').click()"
272
- 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>
 
 
 
 
 
 
 
273
  </div>
274
  </div>
275
  </div>
@@ -279,15 +404,22 @@
279
 
280
  <!-- Status Badge -->
281
  <div class="absolute top-6 right-8 pointer-events-none z-20">
282
- <div id="status-badge"
283
- 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">
284
- <div class="w-3 h-3 bg-red-500 rounded-full border border-black animate-pulse"></div>
 
 
 
 
285
  READY
286
  </div>
287
  </div>
288
  </div>
289
  <script type="module">
290
- import { AutoModelForCausalLM, AutoTokenizer } from "https://cdn.jsdelivr.net/npm/@huggingface/transformers@3.8.1";
 
 
 
291
 
292
  // --- Game Constants ---
293
  const CONFIG = {
@@ -322,7 +454,16 @@
322
  stars: [0, 1],
323
  solution: `// No code needed!`,
324
  setup: (World, Bodies, Composite) => {
325
- const floor = Bodies.rectangle(CONFIG.width / 2, CONFIG.height + 25, CONFIG.width, 100, { isStatic: true, render: { fillStyle: CONFIG.colors.wall, ...strokeStyle } });
 
 
 
 
 
 
 
 
 
326
 
327
  const ball = Bodies.circle(pX(2), CONFIG.height - 50 - 20, 20, {
328
  restitution: 0.6,
@@ -334,12 +475,24 @@
334
 
335
  Matter.Body.setVelocity(ball, { x: 10, y: 0 });
336
 
337
- const goal = Bodies.rectangle(pX(18), CONFIG.height - 50 - 60, 100, 120, {
338
- isStatic: true,
339
- isSensor: true,
340
- label: "GoalZone",
341
- render: { fillStyle: CONFIG.colors.goal, opacity: 0.3, ...strokeStyle, lineWidth: 2, strokeStyle: "#000" },
342
- });
 
 
 
 
 
 
 
 
 
 
 
 
343
 
344
  World.add(Composite, [floor, ball, goal]);
345
  return { ball, goal };
@@ -354,8 +507,16 @@
354
  stars: [1, 2],
355
  solution: `add a long line in the middle`,
356
  setup: (World, Bodies, Composite) => {
357
- const p1 = Bodies.rectangle(pX(4), pY(4), pX(6), 20, { isStatic: true, angle: Math.PI / 8, render: { fillStyle: CONFIG.colors.wall, ...strokeStyle } });
358
- const p2 = Bodies.rectangle(pX(16), pY(12), pX(6), 20, { isStatic: true, angle: Math.PI / 8, render: { fillStyle: CONFIG.colors.wall, ...strokeStyle } });
 
 
 
 
 
 
 
 
359
 
360
  const ball = Bodies.circle(pX(3), pY(2), 20, {
361
  restitution: 0.2,
@@ -368,7 +529,11 @@
368
  isStatic: true,
369
  isSensor: true,
370
  label: "GoalZone",
371
- render: { fillStyle: CONFIG.colors.goal, opacity: 0.3, ...strokeStyle },
 
 
 
 
372
  });
373
 
374
  World.add(Composite, [p1, p2, ball, goal]);
@@ -384,7 +549,16 @@
384
  stars: [1, 3],
385
  solution: `Add a circle at 2,2`,
386
  setup: (World, Bodies, Composite) => {
387
- const floor = Bodies.rectangle(CONFIG.width / 2, CONFIG.height + 25, CONFIG.width, 100, { isStatic: true, render: { fillStyle: CONFIG.colors.wall, ...strokeStyle } });
 
 
 
 
 
 
 
 
 
388
 
389
  const ball = Bodies.circle(pX(2.2), CONFIG.height - 50 - 20, 20, {
390
  restitution: 0.6,
@@ -394,12 +568,24 @@
394
  render: { fillStyle: CONFIG.colors.ball, ...strokeStyle },
395
  });
396
 
397
- const goal = Bodies.rectangle(pX(18), CONFIG.height - 50 - 60, 100, 120, {
398
- isStatic: true,
399
- isSensor: true,
400
- label: "GoalZone",
401
- render: { fillStyle: CONFIG.colors.goal, opacity: 0.3, ...strokeStyle, lineWidth: 2, strokeStyle: "#000" },
402
- });
 
 
 
 
 
 
 
 
 
 
 
 
403
 
404
  World.add(Composite, [floor, ball, goal]);
405
  return { ball, goal };
@@ -415,7 +601,10 @@
415
  solution: `add a wide line at the bottom`,
416
  setup: (World, Bodies, Composite) => {
417
  // Wall in middle top
418
- const wall = Bodies.rectangle(pX(10), pY(3), 20, pY(6), { isStatic: true, render: { fillStyle: CONFIG.colors.wall, ...strokeStyle } });
 
 
 
419
 
420
  const ball = Bodies.circle(pX(2), pY(1.2), 20, {
421
  restitution: 1.0,
@@ -431,7 +620,11 @@
431
  isStatic: true,
432
  isSensor: true,
433
  label: "GoalZone",
434
- render: { fillStyle: CONFIG.colors.goal, opacity: 0.3, ...strokeStyle },
 
 
 
 
435
  });
436
 
437
  World.add(Composite, [wall, ball, goal]);
@@ -444,7 +637,7 @@
444
  difficulty: 3,
445
  desc: "Ambush! Ninja stars are incoming. Protect the ball's path so it can land safely.",
446
  hint: "Add a few heavy blocks at certain locations to shield the ball from the stars.",
447
- stars: [3, 5],
448
  solution: `add a heavy block at 5, 3\nadd a heavy block at 15, 5\nadd a heavy block at 5, 7`,
449
  setup: (World, Bodies, Composite, addEvent) => {
450
  const ball = Bodies.circle(pX(10), pY(1), 20, {
@@ -458,16 +651,32 @@
458
  isStatic: true,
459
  isSensor: true,
460
  label: "GoalZone",
461
- render: { fillStyle: CONFIG.colors.goal, opacity: 0.3, ...strokeStyle },
 
 
 
 
462
  });
463
 
464
  const createNinjaStar = (x, y, vx, vy, angVel, delay) => {
465
- 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');
466
- const star = Matter.Bodies.fromVertices(x, y, starVerts, {
467
- render: { fillStyle: "#ef4444", strokeStyle: "#ef4444", lineWidth: 1 },
468
- restitution: 0.8,
469
- isStatic: true
470
- }, true);
 
 
 
 
 
 
 
 
 
 
 
 
471
  Matter.Body.scale(star, 0.5, 0.5);
472
  Matter.Body.setVelocity(star, { x: vx, y: vy });
473
  Matter.Body.setAngularVelocity(star, angVel);
@@ -485,7 +694,7 @@
485
  // Ninja Stars
486
  createNinjaStar(pX(2), pY(5), 15, -5, 0.3, 0.1);
487
  createNinjaStar(pX(18), pY(8), -15, -5, -0.3, 0.3);
488
- createNinjaStar(pX(2), pY(11), 15, -5, 0.3, 0.50);
489
 
490
  World.add(Composite, [ball, goal]);
491
  return { ball, goal };
@@ -503,13 +712,17 @@
503
  // Rotating Cross
504
  const cross = Matter.Body.create({
505
  parts: [
506
- Bodies.rectangle(pX(8), pY(4), pX(7), 10, { render: { fillStyle: CONFIG.colors.wall, ...strokeStyle } }),
507
- Bodies.rectangle(pX(8), pY(4), 10, pX(7), { render: { fillStyle: CONFIG.colors.wall, ...strokeStyle } })
 
 
 
 
508
  ],
509
  isStatic: true,
510
  });
511
 
512
- Matter.Events.on(STATE.engine, 'beforeUpdate', () => {
513
  if (STATE.isPlaying) {
514
  Matter.Body.rotate(cross, -0.01);
515
  }
@@ -518,9 +731,9 @@
518
  World.add(Composite, cross);
519
 
520
  // Platform
521
- const platform = Bodies.rectangle(pX(8), pY(8)-10, pX(14), 20, {
522
  isStatic: true,
523
- render: { fillStyle: CONFIG.colors.wall, ...strokeStyle }
524
  });
525
 
526
  // Ball
@@ -535,7 +748,11 @@
535
  isStatic: true,
536
  isSensor: true,
537
  label: "GoalZone",
538
- render: { fillStyle: CONFIG.colors.goal, opacity: 0.3, ...strokeStyle },
 
 
 
 
539
  });
540
 
541
  // Goal Movement Logic
@@ -549,7 +766,7 @@
549
  Matter.Body.setPosition(goal, { x: x, y: pY(13) });
550
  };
551
 
552
- Matter.Events.on(STATE.engine, 'beforeUpdate', updateGoal);
553
 
554
  World.add(Composite, [platform, ball, goal]);
555
  return { ball, goal };
@@ -570,7 +787,7 @@
570
  const ramp = Bodies.rectangle(pX(2), pY(11.5), pX(4), 5, {
571
  isStatic: true,
572
  angle: Math.PI / 16,
573
- render: { fillStyle: CONFIG.colors.wall, ...strokeStyle }
574
  });
575
 
576
  // Catapult
@@ -579,13 +796,13 @@
579
  const catapult = Bodies.rectangle(pivotX, pivotY - 10, pX(12), 10, {
580
  collisionFilter: { group: group },
581
  density: 0.0001,
582
- render: { fillStyle: CONFIG.colors.wall, ...strokeStyle }
583
  });
584
 
585
  const pivot = Bodies.rectangle(pivotX, pivotY + 20, 20, 60, {
586
  isStatic: true,
587
  collisionFilter: { group: group },
588
- render: { fillStyle: CONFIG.colors.wall, ...strokeStyle }
589
  });
590
 
591
  const constraint = Matter.Constraint.create({
@@ -593,7 +810,7 @@
593
  pointB: { x: pivotX, y: pivotY - 10 },
594
  stiffness: 1.0,
595
  length: 0,
596
- render: { visible: true }
597
  });
598
 
599
  // Ball
@@ -609,10 +826,21 @@
609
  isStatic: true,
610
  isSensor: true,
611
  label: "GoalZone",
612
- render: { fillStyle: CONFIG.colors.goal, opacity: 0.3, ...strokeStyle },
 
 
 
 
613
  });
614
 
615
- World.add(Composite, [ramp, catapult, pivot, constraint, ball, goal]);
 
 
 
 
 
 
 
616
  return { ball, goal };
617
  },
618
  },
@@ -625,21 +853,29 @@
625
  stars: [2, 4],
626
  solution: `Add a heavy circle in the top left.\nAdd a heavy circle in the top right, delayed by 10 seconds.`,
627
  setup: (World, Bodies, Composite) => {
628
- const xx = pX(8), yy = pY(2), number = 5, size = 25, length = pY(4);
 
 
 
 
629
 
630
  const separation = 2.1;
631
  for (let i = 0; i < number; i++) {
632
  const x = xx + i * (size * separation);
633
  const circle = Bodies.circle(x, yy + length, size, {
634
- inertia: Infinity, restitution: 0.1, friction: 0, frictionAir: 0, slop: size * 0.02,
635
- render: { fillStyle: CONFIG.colors.wall, ...strokeStyle }
 
 
 
 
636
  });
637
  const constraint = Matter.Constraint.create({
638
  pointA: { x: x, y: yy },
639
  bodyB: circle,
640
  stiffness: 1,
641
  length: length,
642
- render: { strokeStyle: '#000', lineWidth: 1 }
643
  });
644
  World.add(Composite, [circle, constraint]);
645
  }
@@ -648,42 +884,65 @@
648
  friction: 0.01,
649
  frictionStatic: 0.01,
650
  restitution: 0.1,
651
- isStatic: true, angle: Math.PI / 12, render: { fillStyle: CONFIG.colors.wall, ...strokeStyle }
 
 
652
  });
653
 
654
  // Tunnel walls
655
  const p1 = Bodies.rectangle(pX(14.5), yy + length + 30, pX(3), 10, {
656
- isStatic: true, render: { fillStyle: CONFIG.colors.wall, ...strokeStyle }
657
- });
658
- const p2 = Bodies.rectangle(pX(15.25), yy + length + 30 - pY(1) - 10, pX(4.5), 10, {
659
- isStatic: true, render: { fillStyle: CONFIG.colors.wall, ...strokeStyle }
660
  });
 
 
 
 
 
 
 
 
 
 
661
  const p3 = Bodies.rectangle(pX(17.85), pY(8), pX(4), 10, {
662
- isStatic: true, render: { fillStyle: CONFIG.colors.wall, ...strokeStyle }
 
663
  });
664
  const p4 = Bodies.rectangle(pX(19.75), pY(6.1), pX(4), 10, {
665
  // make vertical
666
  angle: Math.PI / 2,
667
  restitution: 0.1,
668
- isStatic: true, render: { fillStyle: CONFIG.colors.wall, ...strokeStyle }
 
669
  });
670
 
671
- const ball = Bodies.circle(xx + number * (size * separation), yy + length, 20, {
672
- restitution: 0.1, friction: 0.0, frictionAir: 0.0,
673
- render: { fillStyle: CONFIG.colors.ball, ...strokeStyle },
674
- });
 
 
 
 
 
 
 
675
  const goal = Bodies.rectangle(pX(6.5), pY(12.5), pX(8), pY(4), {
676
  isStatic: true,
677
  isSensor: true,
678
  label: "GoalZone",
679
- render: { fillStyle: CONFIG.colors.goal, opacity: 0.3, ...strokeStyle },
 
 
 
 
680
  });
681
 
682
  World.add(Composite, [ramp, p1, p2, p3, p4, ball, goal]);
683
 
684
  return { ball, goal };
685
  },
686
- }
687
  ];
688
 
689
  // --- State Management ---
@@ -779,7 +1038,11 @@
779
 
780
  if (action.delay > 0) {
781
  context.fillStyle = "rgba(0, 0, 0, 0.7)";
782
- context.fillText(`in ${action.delay}s`, x, body.bounds.max.y + 20);
 
 
 
 
783
  }
784
  if (action.params.velocity) {
785
  const vx = action.params.velocity[0],
@@ -792,15 +1055,24 @@
792
  // Draw velocity arrows for any dynamic body in the world (Level setup items)
793
  Matter.Composite.allBodies(STATE.engine.world).forEach((body) => {
794
  // Skip planned action previews
795
- if (STATE.plannedActions.some((a) => a.previewBody === body)) return;
 
796
 
797
  const vx = body.velocity.x || 0;
798
  const vy = body.velocity.y || 0;
799
  if (Math.hypot(vx, vy) > 0.1) {
800
  let color = body.render.fillStyle;
801
  // Use red for dark objects (enemies), otherwise use body color
802
- if (color === "#333" || color === CONFIG.colors.wall) color = "#ef4444";
803
- drawVelocityArrow(context, body.position.x, body.position.y, vx, vy, color);
 
 
 
 
 
 
 
 
804
  }
805
  });
806
  }
@@ -863,7 +1135,10 @@
863
  resizeObserver.observe(container);
864
  window.requestAnimationFrame(() => resizeCanvas());
865
 
866
- STATE.runner = Matter.Runner.create({ isFixed: true, delta: 1000 / 60 });
 
 
 
867
  Matter.Events.on(STATE.engine, "beforeUpdate", handleUpdate);
868
  Matter.Render.run(STATE.render);
869
  }
@@ -889,8 +1164,14 @@
889
  const headLen = 12;
890
  context.beginPath();
891
  context.moveTo(endX, endY);
892
- context.lineTo(endX - headLen * Math.cos(angle - Math.PI / 6), endY - headLen * Math.sin(angle - Math.PI / 6));
893
- context.lineTo(endX - headLen * Math.cos(angle + Math.PI / 6), endY - headLen * Math.sin(angle + Math.PI / 6));
 
 
 
 
 
 
894
  context.lineTo(endX, endY);
895
  context.fillStyle = color;
896
  context.fill();
@@ -898,13 +1179,18 @@
898
  context.fillStyle = color;
899
  context.font = "bold 12px 'Fira Code', monospace";
900
  context.textAlign = "center";
901
- context.fillText(`${speed.toFixed(1)}`, endX + (vx / speed) * 15, endY + (vy / speed) * 15);
 
 
 
 
902
  }
903
 
904
  function handleUpdate() {
905
  if (!STATE.isPlaying || STATE.isFinished) return;
906
  STATE.time = performance.now() - STATE.startTime;
907
- document.getElementById("timer-display").innerText = (STATE.time / 1000).toFixed(2) + "s";
 
908
  STATE.eventQueue
909
  .filter((e) => e.delay <= STATE.time && !e.executed)
910
  .forEach((e) => {
@@ -925,22 +1211,40 @@
925
  const level = LEVELS[index];
926
 
927
  const diffs = ["", "EASY", "MEDIUM", "HARD", "EXTREME"];
928
- const diffColors = ["", "bg-neo-green", "bg-neo-yellow", "bg-neo-red", "bg-neo-purple"];
 
 
 
 
 
 
929
  const d = level.difficulty || 1;
930
- 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>`;
 
 
 
 
931
  document.getElementById("level-title").innerText = level.title;
932
  document.getElementById("level-desc").innerText = level.desc;
933
  document.getElementById("level-hint-text").innerText = level.hint;
934
- document.getElementById("star-reqs").innerText = `3★ < ${level.stars[0] + 1} items`;
 
 
935
  document.getElementById("hint-content").classList.add("hidden");
936
 
937
  resetSimulationState();
938
  document.getElementById("code-input").value = "";
939
 
940
- const { ball, goal } = level.setup(Matter.World, Matter.Bodies, STATE.engine.world, () => {});
 
 
 
 
 
941
 
942
  STATE.currentBall = ball;
943
- STATE.winCondition = () => Matter.Collision.collides(ball, goal) !== null;
 
944
  Matter.Render.world(STATE.render);
945
  }
946
 
@@ -959,7 +1263,9 @@
959
  document.getElementById("timer-display").innerText = "0.00s";
960
  document.getElementById("win-message").style.display = "none";
961
  document.getElementById("error-log").classList.add("hidden");
962
- document.getElementById("status-badge").innerHTML = `<div class="w-3 h-3 bg-red-500 rounded-full border border-black animate-pulse"></div> READY`;
 
 
963
 
964
  updateActionList();
965
  }
@@ -970,15 +1276,21 @@
970
  Matter.Runner.stop(STATE.runner);
971
 
972
  if (success) {
973
- document.getElementById("status-badge").innerHTML = `<div class="w-3 h-3 bg-green-500 rounded-full border border-black"></div> COMPLETE`;
 
 
974
  const used = STATE.plannedActions.length;
975
  const level = LEVELS[STATE.currentLevelIndex];
976
  let stars = 1;
977
  if (used <= level.stars[0]) stars = 3;
978
  else if (used <= level.stars[1]) stars = 2;
979
 
980
- if (stars > (STATE.progress.stars[level.id] || 0)) STATE.progress.stars[level.id] = stars;
981
- if (STATE.currentLevelIndex >= STATE.progress.unlockedIndex && STATE.currentLevelIndex < LEVELS.length - 1) {
 
 
 
 
982
  STATE.progress.unlockedIndex = STATE.currentLevelIndex + 1;
983
  }
984
  saveProgress();
@@ -987,7 +1299,9 @@
987
  starContainer.innerHTML = "";
988
  for (let i = 0; i < 3; i++) {
989
  const filled = i < stars;
990
- starContainer.innerHTML += `<i class="fas fa-star ${filled ? "text-yellow-400" : "text-gray-600"}"></i>`;
 
 
991
  }
992
  document.getElementById("result-items").innerText = used;
993
 
@@ -1012,16 +1326,19 @@
1012
  }
1013
  STATE.plannedActions.forEach((action, index) => {
1014
  const div = document.createElement("div");
1015
- 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";
1016
-
 
1017
  const p = action.params;
1018
  const details = [];
1019
  if (p.size !== 1) details.push(`size:${p.size}`);
1020
  if (p.weight !== 1) details.push(`mass:${p.weight}`);
1021
  if (p.restitution !== 0) details.push(`bounce:${p.restitution}`);
1022
  if (p.angle !== 0) details.push(`angle:${p.angle}°`);
1023
- if (p.velocity && (p.velocity[0] !== 0 || p.velocity[1] !== 0)) details.push(`v:[${p.velocity}]`);
1024
- if (p.isStatic !== (p.shape === 'line')) details.push(p.isStatic ? 'static' : 'dynamic');
 
 
1025
  if (p.delay > 0) details.push(`delay:${p.delay}s`);
1026
 
1027
  const typeName = p.shape;
@@ -1032,14 +1349,23 @@
1032
  <div class="flex flex-col">
1033
  <div class="flex items-center gap-2">
1034
  <span class="text-xs font-bold uppercase">${typeName}</span>
1035
- <span class="text-[10px] text-gray-500 font-mono">@ [${p.location[0]}, ${p.location[1]}]</span>
 
 
1036
  </div>
1037
- ${details.length > 0 ? `<div class="text-[9px] text-gray-400 font-mono leading-tight uppercase">${details.join(' | ')}</div>` : ''}
 
 
 
 
 
 
1038
  </div>
1039
  </div>
1040
  `;
1041
  const btnDelete = document.createElement("button");
1042
- 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";
 
1043
  btnDelete.innerHTML = `<i class="fas fa-times text-xs"></i>`;
1044
  btnDelete.onclick = () => removeAction(index);
1045
  div.appendChild(btnDelete);
@@ -1049,7 +1375,8 @@
1049
 
1050
  function removeAction(index) {
1051
  const action = STATE.plannedActions[index];
1052
- if (action.previewBody) Matter.World.remove(STATE.engine.world, action.previewBody);
 
1053
  STATE.plannedActions.splice(index, 1);
1054
  updateActionList();
1055
  }
@@ -1086,13 +1413,22 @@
1086
  angle = params.angle || rotation,
1087
  velocity = [0, 0],
1088
  static: isStaticParam,
1089
- isStatic = params.isStatic !== undefined ? params.isStatic : (isStaticParam !== undefined ? isStaticParam : ["platform", "line"].includes(shape)),
 
 
 
 
1090
  } = params;
1091
 
1092
  // Handle location as string (comma-separated or descriptive)
1093
- if (typeof location === 'string') {
1094
  const parts = location.match(/\d+/g);
1095
- if (parts && parts.length === 2 && !isNaN(parts[0]) && !isNaN(parts[1])) {
 
 
 
 
 
1096
  location = [parseFloat(parts[0]), parseFloat(parts[1])];
1097
  } else {
1098
  const loc = location.toLowerCase();
@@ -1101,16 +1437,19 @@
1101
  const margin = 2;
1102
 
1103
  const locations = {
1104
- "center": [midX, midY],
1105
  "top-left": [margin, margin],
1106
  "top-center": [midX, margin],
1107
  "top-right": [CONFIG.gridWidth - margin, margin],
1108
  "center-left": [margin, midY],
1109
- "center": [midX, midY],
1110
  "center-right": [CONFIG.gridWidth - margin, midY],
1111
  "bottom-left": [margin, CONFIG.gridHeight - margin],
1112
  "bottom-center": [midX, CONFIG.gridHeight - margin],
1113
- "bottom-right": [CONFIG.gridWidth - margin, CONFIG.gridHeight - margin],
 
 
 
1114
  };
1115
  location = locations[loc] || [midX, midY];
1116
  }
@@ -1178,19 +1517,128 @@
1178
  const MODEL_ID = "Xenova/functiongemma-270m-game";
1179
  let tokenizer, model;
1180
 
1181
- 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."}}}];
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1182
 
1183
  async function initModel() {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1184
  try {
1185
- tokenizer = await AutoTokenizer.from_pretrained(MODEL_ID);
 
 
 
 
1186
  model = await AutoModelForCausalLM.from_pretrained(MODEL_ID, {
1187
  device: "webgpu",
1188
  dtype: "q4",
 
1189
  });
 
1190
  document.getElementById("loading-overlay").classList.add("hidden");
1191
  } catch (e) {
1192
  console.error(e);
1193
- document.getElementById("loading-overlay").innerHTML = `<div class="text-red-500 font-bold p-4 bg-white border-2 border-black">Error: ${e.message}</div>`;
 
 
 
 
 
1194
  }
1195
  }
1196
 
@@ -1210,7 +1658,10 @@
1210
  const systemPrompt = `You are a model that can do function calling with the following functions`;
1211
 
1212
  try {
1213
- const lines = input.split('\n').map(l => l.trim()).filter(l => l.length > 0 && !l.startsWith("//"));
 
 
 
1214
 
1215
  for (const line of lines) {
1216
  // 2. Prepare Messages
@@ -1228,8 +1679,15 @@
1228
  });
1229
 
1230
  // 4. Generate
1231
- const output = await model.generate({ ...inputs, max_new_tokens: 128, do_sample: false });
1232
- const decoded = tokenizer.decode(output.slice(0, [inputs.input_ids.dims[1], null]), { skip_special_tokens: false });
 
 
 
 
 
 
 
1233
 
1234
  // 5. Parse Output
1235
  // Format: <start_function_call>call:add{...}<end_function_call>
@@ -1239,7 +1697,10 @@
1239
  const endIndex = decoded.indexOf(endTag);
1240
 
1241
  if (startIndex !== -1 && endIndex !== -1) {
1242
- let callStr = decoded.substring(startIndex + startTag.length, endIndex);
 
 
 
1243
  if (callStr.startsWith("call:add")) {
1244
  // Extract JSON-like string: {location:[...],shape:<escape>...<escape>}
1245
  let argsStr = callStr.substring(callStr.indexOf("{"));
@@ -1255,7 +1716,7 @@
1255
  throw new Error("Model did not generate a valid add command.");
1256
  }
1257
  } else {
1258
- throw new Error(`Could not understand command: "${line}"`);
1259
  }
1260
  }
1261
  document.getElementById("code-input").value = "";
@@ -1291,14 +1752,30 @@
1291
  });
1292
  };
1293
 
1294
- const { ball, goal } = level.setup(Matter.World, Matter.Bodies, STATE.engine.world, addEvent);
 
 
 
 
 
1295
 
1296
  STATE.currentBall = ball;
1297
- STATE.winCondition = () => Matter.Collision.collides(ball, goal) !== null;
 
1298
 
1299
  // Real Action Executioner
1300
  const executeReal = (params) => {
1301
- const { shape, location, size, weight, restitution, friction, angle, velocity, isStatic } = params;
 
 
 
 
 
 
 
 
 
 
1302
  const x = pX(location[0]),
1303
  y = pY(location[1]);
1304
  const pixelSize = pX(size);
@@ -1333,7 +1810,9 @@
1333
 
1334
  STATE.isPlaying = true;
1335
  STATE.startTime = performance.now();
1336
- document.getElementById("status-badge").innerHTML = `<div class="w-3 h-3 bg-green-500 rounded-full border border-black animate-ping"></div> RUNNING`;
 
 
1337
  Matter.Runner.run(STATE.runner, STATE.engine);
1338
  }
1339
 
@@ -1352,7 +1831,9 @@
1352
  const stars = STATE.progress.stars[index] || 0;
1353
 
1354
  const card = document.createElement("div");
1355
- 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"}`;
 
 
1356
 
1357
  if (unlocked) {
1358
  card.onclick = () => loadLevel(index);
@@ -1360,9 +1841,16 @@
1360
  <div class="text-3xl font-black">${index + 1}</div>
1361
  <div class="flex gap-1 text-xs">
1362
  ${Array(3)
1363
- .fill(0)
1364
- .map((_, i) => `<i class="fas fa-star ${i < stars ? "text-neo-yellow" : "text-gray-300"}"></i>`)
1365
- .join("")}
 
 
 
 
 
 
 
1366
  </div>
1367
  `;
1368
  } else {
@@ -1380,7 +1868,12 @@
1380
  Matter.Runner.stop(STATE.runner);
1381
  const level = LEVELS[STATE.currentLevelIndex];
1382
  Matter.Composite.clear(STATE.engine.world);
1383
- const { ball, goal } = level.setup(Matter.World, Matter.Bodies, STATE.engine.world, () => {});
 
 
 
 
 
1384
 
1385
  STATE.currentBall = ball;
1386
 
@@ -1389,7 +1882,9 @@
1389
  });
1390
 
1391
  STATE.isPlaying = false;
1392
- document.getElementById("status-badge").innerHTML = `<div class="w-3 h-3 bg-red-500 rounded-full border border-black animate-pulse"></div> READY`;
 
 
1393
  document.getElementById("timer-display").innerText = "0.00s";
1394
  document.getElementById("win-message").style.display = "none";
1395
  };
@@ -1397,7 +1892,8 @@
1397
  document.getElementById("btn-clear-all").onclick = () => {
1398
  document.getElementById("code-input").value = "";
1399
  STATE.plannedActions.forEach((action) => {
1400
- if (action.previewBody) Matter.World.remove(STATE.engine.world, action.previewBody);
 
1401
  });
1402
  STATE.plannedActions = [];
1403
  updateActionList();
@@ -1424,6 +1920,5 @@
1424
  initModel();
1425
  };
1426
  </script>
1427
- </body>
1428
-
1429
- </html>
 
1
+ <!DOCTYPE html>
2
  <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>Physics Playground</title>
7
+ <link
8
+ rel="shortcut icon"
9
+ href="https://cdn-uploads.huggingface.co/production/uploads/61b253b7ac5ecaae3d1efe0c/EGE2Dknlxt-AZZ9vOYc18.png"
10
+ type="image/x-png"
11
+ />
12
+ <script src="https://cdn.tailwindcss.com"></script>
13
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/matter-js/0.19.0/matter.min.js"></script>
14
+ <script src="https://cdn.jsdelivr.net/npm/poly-decomp@0.3.0/build/decomp.min.js"></script>
15
+ <link
16
+ href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css"
17
+ rel="stylesheet"
18
+ />
19
+ <link
20
+ href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;700&family=Fira+Code:wght@500;700&display=swap"
21
+ rel="stylesheet"
22
+ />
23
+ <script>
24
+ tailwind.config = {
25
+ theme: {
26
+ extend: {
27
+ fontFamily: {
28
+ sans: ["Space Grotesk", "sans-serif"],
29
+ mono: ["Fira Code", "monospace"],
30
+ },
31
+ colors: {
32
+ "neo-bg": "#f0f0f0",
33
+ "neo-black": "#1a1a1a",
34
+ "neo-white": "#ffffff",
35
+ "neo-purple": "#a78bfa",
36
+ "neo-yellow": "#facc15",
37
+ "neo-green": "#4ade80",
38
+ "neo-red": "#f87171",
39
+ "neo-blue": "#60a5fa",
40
+ "neo-gray": "#94a3b8",
41
+ },
42
+ boxShadow: {
43
+ neo: "4px 4px 0px 0px #000000",
44
+ "neo-sm": "2px 2px 0px 0px #000000",
45
+ "neo-lg": "8px 8px 0px 0px #000000",
46
+ },
47
  },
48
  },
49
+ };
50
+ </script>
51
+
52
+ <style>
53
+ body {
54
+ overflow: hidden;
55
+ background-color: #e0e7ff;
56
+ background-image: radial-gradient(#a5b4fc 1px, transparent 1px);
57
+ background-size: 20px 20px;
58
+ }
59
+
60
+ /* Custom Scrollbar */
61
+ ::-webkit-scrollbar {
62
+ width: 12px;
63
+ height: 12px;
64
+ }
65
+
66
+ ::-webkit-scrollbar-track {
67
+ background: #fff;
68
+ border-left: 2px solid black;
69
+ }
70
+
71
+ ::-webkit-scrollbar-thumb {
72
+ background: #000;
73
+ border: 2px solid #fff;
74
+ }
75
+
76
+ ::-webkit-scrollbar-thumb:hover {
77
+ background: #333;
78
+ }
79
+
80
+ .neo-border {
81
+ border: 3px solid black;
82
+ }
83
+
84
+ .neo-btn {
85
+ transition: all 0.1s ease-in-out;
86
+ }
87
+
88
+ .neo-btn:active {
89
+ transform: translate(2px, 2px);
90
+ box-shadow: 2px 2px 0px 0px #000000;
91
+ }
92
+
93
+ .level-card {
94
+ transition: all 0.2s;
95
+ }
96
+
97
+ .level-card:hover:not(.locked) {
98
+ transform: translate(-2px, -2px);
99
+ box-shadow: 6px 6px 0px 0px #000000;
100
+ }
101
+
102
+ .locked {
103
+ background-color: #e2e8f0;
104
+ cursor: not-allowed;
105
+ opacity: 0.7;
106
+ background-image: repeating-linear-gradient(
107
+ 45deg,
108
+ #cbd5e1 0,
109
+ #cbd5e1 1px,
110
+ transparent 0,
111
+ transparent 50%
112
+ );
113
+ background-size: 10px 10px;
114
+ }
115
+
116
+ .code-editor {
117
+ font-family: "Fira Code", monospace;
118
+ background-color: #ffffff;
119
+ color: #000000;
120
+ line-height: 1.6;
121
+ }
122
+
123
+ .toggle-checkbox:checked {
124
+ right: 0;
125
+ border-color: #4ade80;
126
+ }
127
+
128
+ .toggle-checkbox:checked + .toggle-label {
129
+ background-color: #4ade80;
130
+ }
131
+
132
+ /* Action Item Delete Button Transition */
133
+ .action-item .btn-delete {
134
+ opacity: 0;
135
+ transition: opacity 0.2s;
136
+ }
137
+
138
+ .action-item:hover .btn-delete {
139
+ opacity: 1;
140
+ }
141
+ </style>
142
+ </head>
143
+
144
+ <body class="h-screen w-screen flex flex-col md:flex-row p-4 gap-4">
145
+ <!-- Loading Overlay -->
146
+ <div
147
+ id="loading-overlay"
148
+ class="fixed inset-0 bg-black/90 z-50 flex flex-col items-center justify-center"
149
+ >
150
+ <div
151
+ class="animate-spin rounded-full h-16 w-16 border-t-4 border-b-4 border-neo-green mb-4"
152
+ ></div>
153
+ <div
154
+ class="text-white font-mono font-bold text-xl uppercase tracking-widest"
155
+ >
156
+ Initializing Gemma...
157
  </div>
 
158
 
159
+ <div
160
+ class="w-64 h-6 bg-gray-800 border-2 border-white mt-6 relative overflow-hidden shadow-neo-sm"
161
+ >
162
+ <div
163
+ id="loading-bar"
164
+ class="h-full bg-neo-green w-0 transition-all duration-300 ease-out"
165
+ ></div>
166
+ </div>
167
+
168
+ <div
169
+ id="loading-status"
170
+ class="text-neo-green font-mono text-xs mt-2 uppercase"
171
+ >
172
+ Starting Download... 0%
173
  </div>
174
  </div>
175
 
176
+ <!-- Sidebar / Controls -->
177
+ <div
178
+ 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"
179
+ >
180
+ <!-- Decorative Header Strip -->
181
+ <div class="h-4 w-full bg-neo-black"></div>
182
+
183
+ <div
184
+ class="p-6 border-b-2 border-black bg-neo-yellow flex justify-between items-center shrink-0"
185
+ >
186
  <div>
187
+ <h1 class="text-3xl font-bold text-black uppercase tracking-tighter">
188
+ Function <span class="text-white bg-black px-1">Gemma</span>
189
+ </h1>
190
+ <div class="text-xs font-mono font-bold text-black mt-1">
191
+ PHYSICS PLAYGROUND
192
  </div>
193
+ </div>
194
+ </div>
195
 
196
+ <!-- MENU: Level Select -->
197
+ <div id="view-menu" class="flex-1 overflow-y-auto p-6 bg-white hidden">
198
+ <h2 class="font-bold text-xl mb-4 border-b-2 border-black pb-2">
199
+ SELECT LEVEL
200
+ </h2>
201
+ <div id="level-grid" class="grid grid-cols-2 gap-4">
202
+ <!-- Injected via JS -->
203
+ </div>
204
+ </div>
205
+
206
+ <!-- MENU: Game View -->
207
+ <div id="view-game" class="flex-1 flex flex-col overflow-hidden relative">
208
+ <div class="overflow-y-auto flex-1 p-6 flex flex-col gap-6">
209
+ <!-- Level Header -->
210
+ <div>
211
+ <div class="flex items-center gap-2 mb-2">
212
+ <button
213
+ id="btn-back"
214
+ class="neo-btn p-2 border-2 border-black shadow-neo-sm bg-white hover:bg-gray-100 text-xs font-bold"
215
+ >
216
+ <i class="fas fa-arrow-left"></i> LEVELS
217
+ </button>
218
+ <div
219
+ class="font-bold text-sm bg-black text-white px-2 py-1 ml-auto"
220
+ id="level-indicator"
221
+ >
222
+ LEVEL 1
223
+ </div>
224
  </div>
225
+
226
+ <!-- Level Info Card -->
227
+ <div
228
+ class="neo-border p-4 bg-neo-blue shadow-neo-sm relative overflow-hidden group"
229
+ >
230
+ <div
231
+ class="absolute -right-4 -top-4 w-16 h-16 bg-white/20 rounded-full group-hover:scale-150 transition-transform"
232
+ ></div>
233
+ <div class="flex justify-between items-center mb-3 relative z-10">
234
+ <h2
235
+ class="font-bold text-black text-lg border-b-2 border-black inline-block bg-white px-2"
236
+ id="level-title"
237
+ >
238
+ ...
239
+ </h2>
240
+ <span
241
+ id="timer-display"
242
+ 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]"
243
+ >0.00s</span
244
+ >
245
+ </div>
246
+ <p
247
+ class="text-sm text-black font-medium opacity-90 mb-2"
248
+ id="level-desc"
249
+ >
250
+ ...
251
+ </p>
252
+
253
+ <!-- Collapsible Hint -->
254
+ <div class="mt-2">
255
+ <button
256
+ id="btn-hint"
257
+ 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"
258
+ >
259
+ <i class="fas fa-lightbulb text-yellow-500"></i> SHOW HINT
260
+ </button>
261
+ <div
262
+ id="hint-content"
263
+ class="mt-2 bg-white/50 p-2 border-2 border-black text-xs font-mono hidden"
264
+ >
265
+ <i class="fas fa-code mr-1"></i>
266
+ <span id="level-hint-text">...</span>
267
+ </div>
268
+ </div>
269
  </div>
270
  </div>
 
271
 
272
+ <!-- Editor / Command Input -->
273
+ <div class="flex flex-col gap-2 shrink-0">
274
+ <div class="flex justify-between items-end">
275
+ <div class="flex gap-2 items-center">
276
+ <label
277
+ class="text-sm font-bold bg-black text-white px-2 py-0.5 shadow-neo-sm"
278
+ >COMMAND</label
279
+ >
280
+ <button
281
+ id="btn-solution"
282
+ 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"
283
+ title="Load Example Solution"
284
+ >
285
+ <i class="fas fa-magic"></i> VIEW SOLUTION
286
+ </button>
287
+ </div>
288
+ <div class="text-xs font-bold text-gray-500" id="star-reqs">
289
+ 3★ < 2 items
290
+ </div>
291
  </div>
292
  <div class="relative flex flex-col gap-2">
293
+ <textarea
294
+ id="code-input"
295
  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"
296
+ spellcheck="false"
297
+ placeholder="e.g., Add a circle in the middle. You can execute multiple commands by separating them with new lines."
298
+ ></textarea>
299
+ <button
300
+ id="btn-execute"
301
+ 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"
302
+ >
303
+ <i class="fas fa-terminal"></i> EXECUTE
304
+ </button>
305
  </div>
306
+ <div
307
+ id="error-log"
308
+ class="text-white bg-neo-red border-2 border-black text-xs font-bold px-2 py-1 shadow-neo-sm hidden"
309
+ ></div>
310
  </div>
311
 
312
  <!-- Active Elements List -->
313
  <div class="flex-1 min-h-0 flex flex-col">
314
+ <label
315
+ class="text-sm font-bold bg-black text-white px-2 py-0.5 shadow-neo-sm w-max mb-1"
316
+ >SCENE OBJECTS</label
317
+ >
318
+ <div
319
+ id="action-list"
320
+ class="flex-1 overflow-y-auto border-2 border-black bg-gray-50 p-2 space-y-2 shadow-neo-sm min-h-[100px]"
321
+ >
322
+ <div class="text-xs text-gray-400 text-center mt-4 italic">
323
+ No elements added yet.
324
+ </div>
325
  </div>
326
  </div>
327
  </div>
328
 
329
  <!-- Action Bar -->
330
+ <div
331
+ class="p-4 bg-gray-100 border-t-2 border-black flex gap-3 shrink-0"
332
+ >
333
+ <button
334
+ id="btn-play"
335
+ 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"
336
+ >
337
+ <i class="fas fa-play"></i> RUN
338
+ </button>
339
+ <button
340
+ id="btn-reset"
341
  class="neo-btn px-4 py-3 bg-white border-2 border-black shadow-neo text-black font-bold hover:bg-gray-100"
342
+ title="Reset Simulation"
343
+ >
344
  <i class="fas fa-undo"></i>
345
  </button>
346
+ <button
347
+ id="btn-clear-all"
348
  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"
349
+ title="Clear All Objects"
350
+ >
351
  <i class="fas fa-trash-alt"></i>
352
  </button>
353
  </div>
 
357
  <!-- Main Canvas Area -->
358
  <div class="flex-1 relative flex items-center justify-center p-2">
359
  <!-- Canvas Container -->
360
+ <div
361
+ id="canvas-container"
362
+ class="w-full h-full bg-white neo-border shadow-neo flex justify-center items-center relative overflow-hidden"
363
+ >
364
  <!-- Win Overlay -->
365
+ <div
366
+ id="win-message"
367
+ class="absolute z-50 hidden w-full h-full flex items-center justify-center bg-black/50 backdrop-blur-sm"
368
+ >
369
  <div
370
+ 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"
371
+ >
372
  <h2 class="text-4xl font-black mb-2 text-black">LEVEL CLEAR!</h2>
373
 
374
+ <div
375
+ class="flex justify-center gap-2 mb-4 text-4xl text-white drop-shadow-md"
376
+ id="result-stars"
377
+ >
378
  <!-- Stars injected here -->
379
  </div>
380
 
381
+ <div class="text-sm font-bold mb-6 font-mono">
382
+ ITEMS USED: <span id="result-items">0</span>
383
+ </div>
384
 
385
  <div class="flex flex-col gap-2">
386
+ <button
387
+ id="btn-next-level"
388
+ 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"
389
+ >
390
+ NEXT LEVEL <i class="fas fa-arrow-right ml-2"></i>
391
+ </button>
392
+ <button
393
+ onclick="document.getElementById('btn-reset').click()"
394
+ class="bg-white text-black font-bold py-2 px-6 border-2 border-black hover:bg-gray-100 transition-colors shadow-neo-sm"
395
+ >
396
+ REPLAY
397
+ </button>
398
  </div>
399
  </div>
400
  </div>
 
404
 
405
  <!-- Status Badge -->
406
  <div class="absolute top-6 right-8 pointer-events-none z-20">
407
+ <div
408
+ id="status-badge"
409
+ 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"
410
+ >
411
+ <div
412
+ class="w-3 h-3 bg-red-500 rounded-full border border-black animate-pulse"
413
+ ></div>
414
  READY
415
  </div>
416
  </div>
417
  </div>
418
  <script type="module">
419
+ import {
420
+ AutoModelForCausalLM,
421
+ AutoTokenizer,
422
+ } from "https://cdn.jsdelivr.net/npm/@huggingface/transformers@3.8.1";
423
 
424
  // --- Game Constants ---
425
  const CONFIG = {
 
454
  stars: [0, 1],
455
  solution: `// No code needed!`,
456
  setup: (World, Bodies, Composite) => {
457
+ const floor = Bodies.rectangle(
458
+ CONFIG.width / 2,
459
+ CONFIG.height + 25,
460
+ CONFIG.width,
461
+ 100,
462
+ {
463
+ isStatic: true,
464
+ render: { fillStyle: CONFIG.colors.wall, ...strokeStyle },
465
+ }
466
+ );
467
 
468
  const ball = Bodies.circle(pX(2), CONFIG.height - 50 - 20, 20, {
469
  restitution: 0.6,
 
475
 
476
  Matter.Body.setVelocity(ball, { x: 10, y: 0 });
477
 
478
+ const goal = Bodies.rectangle(
479
+ pX(18),
480
+ CONFIG.height - 50 - 60,
481
+ 100,
482
+ 120,
483
+ {
484
+ isStatic: true,
485
+ isSensor: true,
486
+ label: "GoalZone",
487
+ render: {
488
+ fillStyle: CONFIG.colors.goal,
489
+ opacity: 0.3,
490
+ ...strokeStyle,
491
+ lineWidth: 2,
492
+ strokeStyle: "#000",
493
+ },
494
+ }
495
+ );
496
 
497
  World.add(Composite, [floor, ball, goal]);
498
  return { ball, goal };
 
507
  stars: [1, 2],
508
  solution: `add a long line in the middle`,
509
  setup: (World, Bodies, Composite) => {
510
+ const p1 = Bodies.rectangle(pX(4), pY(4), pX(6), 20, {
511
+ isStatic: true,
512
+ angle: Math.PI / 8,
513
+ render: { fillStyle: CONFIG.colors.wall, ...strokeStyle },
514
+ });
515
+ const p2 = Bodies.rectangle(pX(16), pY(12), pX(6), 20, {
516
+ isStatic: true,
517
+ angle: Math.PI / 8,
518
+ render: { fillStyle: CONFIG.colors.wall, ...strokeStyle },
519
+ });
520
 
521
  const ball = Bodies.circle(pX(3), pY(2), 20, {
522
  restitution: 0.2,
 
529
  isStatic: true,
530
  isSensor: true,
531
  label: "GoalZone",
532
+ render: {
533
+ fillStyle: CONFIG.colors.goal,
534
+ opacity: 0.3,
535
+ ...strokeStyle,
536
+ },
537
  });
538
 
539
  World.add(Composite, [p1, p2, ball, goal]);
 
549
  stars: [1, 3],
550
  solution: `Add a circle at 2,2`,
551
  setup: (World, Bodies, Composite) => {
552
+ const floor = Bodies.rectangle(
553
+ CONFIG.width / 2,
554
+ CONFIG.height + 25,
555
+ CONFIG.width,
556
+ 100,
557
+ {
558
+ isStatic: true,
559
+ render: { fillStyle: CONFIG.colors.wall, ...strokeStyle },
560
+ }
561
+ );
562
 
563
  const ball = Bodies.circle(pX(2.2), CONFIG.height - 50 - 20, 20, {
564
  restitution: 0.6,
 
568
  render: { fillStyle: CONFIG.colors.ball, ...strokeStyle },
569
  });
570
 
571
+ const goal = Bodies.rectangle(
572
+ pX(18),
573
+ CONFIG.height - 50 - 60,
574
+ 100,
575
+ 120,
576
+ {
577
+ isStatic: true,
578
+ isSensor: true,
579
+ label: "GoalZone",
580
+ render: {
581
+ fillStyle: CONFIG.colors.goal,
582
+ opacity: 0.3,
583
+ ...strokeStyle,
584
+ lineWidth: 2,
585
+ strokeStyle: "#000",
586
+ },
587
+ }
588
+ );
589
 
590
  World.add(Composite, [floor, ball, goal]);
591
  return { ball, goal };
 
601
  solution: `add a wide line at the bottom`,
602
  setup: (World, Bodies, Composite) => {
603
  // Wall in middle top
604
+ const wall = Bodies.rectangle(pX(10), pY(3), 20, pY(6), {
605
+ isStatic: true,
606
+ render: { fillStyle: CONFIG.colors.wall, ...strokeStyle },
607
+ });
608
 
609
  const ball = Bodies.circle(pX(2), pY(1.2), 20, {
610
  restitution: 1.0,
 
620
  isStatic: true,
621
  isSensor: true,
622
  label: "GoalZone",
623
+ render: {
624
+ fillStyle: CONFIG.colors.goal,
625
+ opacity: 0.3,
626
+ ...strokeStyle,
627
+ },
628
  });
629
 
630
  World.add(Composite, [wall, ball, goal]);
 
637
  difficulty: 3,
638
  desc: "Ambush! Ninja stars are incoming. Protect the ball's path so it can land safely.",
639
  hint: "Add a few heavy blocks at certain locations to shield the ball from the stars.",
640
+ stars: [3, 5],
641
  solution: `add a heavy block at 5, 3\nadd a heavy block at 15, 5\nadd a heavy block at 5, 7`,
642
  setup: (World, Bodies, Composite, addEvent) => {
643
  const ball = Bodies.circle(pX(10), pY(1), 20, {
 
651
  isStatic: true,
652
  isSensor: true,
653
  label: "GoalZone",
654
+ render: {
655
+ fillStyle: CONFIG.colors.goal,
656
+ opacity: 0.3,
657
+ ...strokeStyle,
658
+ },
659
  });
660
 
661
  const createNinjaStar = (x, y, vx, vy, angVel, delay) => {
662
+ const starVerts = Matter.Vertices.fromPath(
663
+ "50 0 63 38 100 38 69 59 82 100 50 75 18 100 31 59 0 38 37 38"
664
+ );
665
+ const star = Matter.Bodies.fromVertices(
666
+ x,
667
+ y,
668
+ starVerts,
669
+ {
670
+ render: {
671
+ fillStyle: "#ef4444",
672
+ strokeStyle: "#ef4444",
673
+ lineWidth: 1,
674
+ },
675
+ restitution: 0.8,
676
+ isStatic: true,
677
+ },
678
+ true
679
+ );
680
  Matter.Body.scale(star, 0.5, 0.5);
681
  Matter.Body.setVelocity(star, { x: vx, y: vy });
682
  Matter.Body.setAngularVelocity(star, angVel);
 
694
  // Ninja Stars
695
  createNinjaStar(pX(2), pY(5), 15, -5, 0.3, 0.1);
696
  createNinjaStar(pX(18), pY(8), -15, -5, -0.3, 0.3);
697
+ createNinjaStar(pX(2), pY(11), 15, -5, 0.3, 0.5);
698
 
699
  World.add(Composite, [ball, goal]);
700
  return { ball, goal };
 
712
  // Rotating Cross
713
  const cross = Matter.Body.create({
714
  parts: [
715
+ Bodies.rectangle(pX(8), pY(4), pX(7), 10, {
716
+ render: { fillStyle: CONFIG.colors.wall, ...strokeStyle },
717
+ }),
718
+ Bodies.rectangle(pX(8), pY(4), 10, pX(7), {
719
+ render: { fillStyle: CONFIG.colors.wall, ...strokeStyle },
720
+ }),
721
  ],
722
  isStatic: true,
723
  });
724
 
725
+ Matter.Events.on(STATE.engine, "beforeUpdate", () => {
726
  if (STATE.isPlaying) {
727
  Matter.Body.rotate(cross, -0.01);
728
  }
 
731
  World.add(Composite, cross);
732
 
733
  // Platform
734
+ const platform = Bodies.rectangle(pX(8), pY(8) - 10, pX(14), 20, {
735
  isStatic: true,
736
+ render: { fillStyle: CONFIG.colors.wall, ...strokeStyle },
737
  });
738
 
739
  // Ball
 
748
  isStatic: true,
749
  isSensor: true,
750
  label: "GoalZone",
751
+ render: {
752
+ fillStyle: CONFIG.colors.goal,
753
+ opacity: 0.3,
754
+ ...strokeStyle,
755
+ },
756
  });
757
 
758
  // Goal Movement Logic
 
766
  Matter.Body.setPosition(goal, { x: x, y: pY(13) });
767
  };
768
 
769
+ Matter.Events.on(STATE.engine, "beforeUpdate", updateGoal);
770
 
771
  World.add(Composite, [platform, ball, goal]);
772
  return { ball, goal };
 
787
  const ramp = Bodies.rectangle(pX(2), pY(11.5), pX(4), 5, {
788
  isStatic: true,
789
  angle: Math.PI / 16,
790
+ render: { fillStyle: CONFIG.colors.wall, ...strokeStyle },
791
  });
792
 
793
  // Catapult
 
796
  const catapult = Bodies.rectangle(pivotX, pivotY - 10, pX(12), 10, {
797
  collisionFilter: { group: group },
798
  density: 0.0001,
799
+ render: { fillStyle: CONFIG.colors.wall, ...strokeStyle },
800
  });
801
 
802
  const pivot = Bodies.rectangle(pivotX, pivotY + 20, 20, 60, {
803
  isStatic: true,
804
  collisionFilter: { group: group },
805
+ render: { fillStyle: CONFIG.colors.wall, ...strokeStyle },
806
  });
807
 
808
  const constraint = Matter.Constraint.create({
 
810
  pointB: { x: pivotX, y: pivotY - 10 },
811
  stiffness: 1.0,
812
  length: 0,
813
+ render: { visible: true },
814
  });
815
 
816
  // Ball
 
826
  isStatic: true,
827
  isSensor: true,
828
  label: "GoalZone",
829
+ render: {
830
+ fillStyle: CONFIG.colors.goal,
831
+ opacity: 0.3,
832
+ ...strokeStyle,
833
+ },
834
  });
835
 
836
+ World.add(Composite, [
837
+ ramp,
838
+ catapult,
839
+ pivot,
840
+ constraint,
841
+ ball,
842
+ goal,
843
+ ]);
844
  return { ball, goal };
845
  },
846
  },
 
853
  stars: [2, 4],
854
  solution: `Add a heavy circle in the top left.\nAdd a heavy circle in the top right, delayed by 10 seconds.`,
855
  setup: (World, Bodies, Composite) => {
856
+ const xx = pX(8),
857
+ yy = pY(2),
858
+ number = 5,
859
+ size = 25,
860
+ length = pY(4);
861
 
862
  const separation = 2.1;
863
  for (let i = 0; i < number; i++) {
864
  const x = xx + i * (size * separation);
865
  const circle = Bodies.circle(x, yy + length, size, {
866
+ inertia: Infinity,
867
+ restitution: 0.1,
868
+ friction: 0,
869
+ frictionAir: 0,
870
+ slop: size * 0.02,
871
+ render: { fillStyle: CONFIG.colors.wall, ...strokeStyle },
872
  });
873
  const constraint = Matter.Constraint.create({
874
  pointA: { x: x, y: yy },
875
  bodyB: circle,
876
  stiffness: 1,
877
  length: length,
878
+ render: { strokeStyle: "#000", lineWidth: 1 },
879
  });
880
  World.add(Composite, [circle, constraint]);
881
  }
 
884
  friction: 0.01,
885
  frictionStatic: 0.01,
886
  restitution: 0.1,
887
+ isStatic: true,
888
+ angle: Math.PI / 12,
889
+ render: { fillStyle: CONFIG.colors.wall, ...strokeStyle },
890
  });
891
 
892
  // Tunnel walls
893
  const p1 = Bodies.rectangle(pX(14.5), yy + length + 30, pX(3), 10, {
894
+ isStatic: true,
895
+ render: { fillStyle: CONFIG.colors.wall, ...strokeStyle },
 
 
896
  });
897
+ const p2 = Bodies.rectangle(
898
+ pX(15.25),
899
+ yy + length + 30 - pY(1) - 10,
900
+ pX(4.5),
901
+ 10,
902
+ {
903
+ isStatic: true,
904
+ render: { fillStyle: CONFIG.colors.wall, ...strokeStyle },
905
+ }
906
+ );
907
  const p3 = Bodies.rectangle(pX(17.85), pY(8), pX(4), 10, {
908
+ isStatic: true,
909
+ render: { fillStyle: CONFIG.colors.wall, ...strokeStyle },
910
  });
911
  const p4 = Bodies.rectangle(pX(19.75), pY(6.1), pX(4), 10, {
912
  // make vertical
913
  angle: Math.PI / 2,
914
  restitution: 0.1,
915
+ isStatic: true,
916
+ render: { fillStyle: CONFIG.colors.wall, ...strokeStyle },
917
  });
918
 
919
+ const ball = Bodies.circle(
920
+ xx + number * (size * separation),
921
+ yy + length,
922
+ 20,
923
+ {
924
+ restitution: 0.1,
925
+ friction: 0.0,
926
+ frictionAir: 0.0,
927
+ render: { fillStyle: CONFIG.colors.ball, ...strokeStyle },
928
+ }
929
+ );
930
  const goal = Bodies.rectangle(pX(6.5), pY(12.5), pX(8), pY(4), {
931
  isStatic: true,
932
  isSensor: true,
933
  label: "GoalZone",
934
+ render: {
935
+ fillStyle: CONFIG.colors.goal,
936
+ opacity: 0.3,
937
+ ...strokeStyle,
938
+ },
939
  });
940
 
941
  World.add(Composite, [ramp, p1, p2, p3, p4, ball, goal]);
942
 
943
  return { ball, goal };
944
  },
945
+ },
946
  ];
947
 
948
  // --- State Management ---
 
1038
 
1039
  if (action.delay > 0) {
1040
  context.fillStyle = "rgba(0, 0, 0, 0.7)";
1041
+ context.fillText(
1042
+ `in ${action.delay}s`,
1043
+ x,
1044
+ body.bounds.max.y + 20
1045
+ );
1046
  }
1047
  if (action.params.velocity) {
1048
  const vx = action.params.velocity[0],
 
1055
  // Draw velocity arrows for any dynamic body in the world (Level setup items)
1056
  Matter.Composite.allBodies(STATE.engine.world).forEach((body) => {
1057
  // Skip planned action previews
1058
+ if (STATE.plannedActions.some((a) => a.previewBody === body))
1059
+ return;
1060
 
1061
  const vx = body.velocity.x || 0;
1062
  const vy = body.velocity.y || 0;
1063
  if (Math.hypot(vx, vy) > 0.1) {
1064
  let color = body.render.fillStyle;
1065
  // Use red for dark objects (enemies), otherwise use body color
1066
+ if (color === "#333" || color === CONFIG.colors.wall)
1067
+ color = "#ef4444";
1068
+ drawVelocityArrow(
1069
+ context,
1070
+ body.position.x,
1071
+ body.position.y,
1072
+ vx,
1073
+ vy,
1074
+ color
1075
+ );
1076
  }
1077
  });
1078
  }
 
1135
  resizeObserver.observe(container);
1136
  window.requestAnimationFrame(() => resizeCanvas());
1137
 
1138
+ STATE.runner = Matter.Runner.create({
1139
+ isFixed: true,
1140
+ delta: 1000 / 60,
1141
+ });
1142
  Matter.Events.on(STATE.engine, "beforeUpdate", handleUpdate);
1143
  Matter.Render.run(STATE.render);
1144
  }
 
1164
  const headLen = 12;
1165
  context.beginPath();
1166
  context.moveTo(endX, endY);
1167
+ context.lineTo(
1168
+ endX - headLen * Math.cos(angle - Math.PI / 6),
1169
+ endY - headLen * Math.sin(angle - Math.PI / 6)
1170
+ );
1171
+ context.lineTo(
1172
+ endX - headLen * Math.cos(angle + Math.PI / 6),
1173
+ endY - headLen * Math.sin(angle + Math.PI / 6)
1174
+ );
1175
  context.lineTo(endX, endY);
1176
  context.fillStyle = color;
1177
  context.fill();
 
1179
  context.fillStyle = color;
1180
  context.font = "bold 12px 'Fira Code', monospace";
1181
  context.textAlign = "center";
1182
+ context.fillText(
1183
+ `${speed.toFixed(1)}`,
1184
+ endX + (vx / speed) * 15,
1185
+ endY + (vy / speed) * 15
1186
+ );
1187
  }
1188
 
1189
  function handleUpdate() {
1190
  if (!STATE.isPlaying || STATE.isFinished) return;
1191
  STATE.time = performance.now() - STATE.startTime;
1192
+ document.getElementById("timer-display").innerText =
1193
+ (STATE.time / 1000).toFixed(2) + "s";
1194
  STATE.eventQueue
1195
  .filter((e) => e.delay <= STATE.time && !e.executed)
1196
  .forEach((e) => {
 
1211
  const level = LEVELS[index];
1212
 
1213
  const diffs = ["", "EASY", "MEDIUM", "HARD", "EXTREME"];
1214
+ const diffColors = [
1215
+ "",
1216
+ "bg-neo-green",
1217
+ "bg-neo-yellow",
1218
+ "bg-neo-red",
1219
+ "bg-neo-purple",
1220
+ ];
1221
  const d = level.difficulty || 1;
1222
+ document.getElementById("level-indicator").innerHTML = `LEVEL ${
1223
+ index + 1
1224
+ } <span class="ml-2 px-1 ${
1225
+ diffColors[d]
1226
+ } text-black text-[10px] border border-black">${diffs[d]}</span>`;
1227
  document.getElementById("level-title").innerText = level.title;
1228
  document.getElementById("level-desc").innerText = level.desc;
1229
  document.getElementById("level-hint-text").innerText = level.hint;
1230
+ document.getElementById("star-reqs").innerText = `3★ < ${
1231
+ level.stars[0] + 1
1232
+ } items`;
1233
  document.getElementById("hint-content").classList.add("hidden");
1234
 
1235
  resetSimulationState();
1236
  document.getElementById("code-input").value = "";
1237
 
1238
+ const { ball, goal } = level.setup(
1239
+ Matter.World,
1240
+ Matter.Bodies,
1241
+ STATE.engine.world,
1242
+ () => {}
1243
+ );
1244
 
1245
  STATE.currentBall = ball;
1246
+ STATE.winCondition = () =>
1247
+ Matter.Collision.collides(ball, goal) !== null;
1248
  Matter.Render.world(STATE.render);
1249
  }
1250
 
 
1263
  document.getElementById("timer-display").innerText = "0.00s";
1264
  document.getElementById("win-message").style.display = "none";
1265
  document.getElementById("error-log").classList.add("hidden");
1266
+ document.getElementById(
1267
+ "status-badge"
1268
+ ).innerHTML = `<div class="w-3 h-3 bg-red-500 rounded-full border border-black animate-pulse"></div> READY`;
1269
 
1270
  updateActionList();
1271
  }
 
1276
  Matter.Runner.stop(STATE.runner);
1277
 
1278
  if (success) {
1279
+ document.getElementById(
1280
+ "status-badge"
1281
+ ).innerHTML = `<div class="w-3 h-3 bg-green-500 rounded-full border border-black"></div> COMPLETE`;
1282
  const used = STATE.plannedActions.length;
1283
  const level = LEVELS[STATE.currentLevelIndex];
1284
  let stars = 1;
1285
  if (used <= level.stars[0]) stars = 3;
1286
  else if (used <= level.stars[1]) stars = 2;
1287
 
1288
+ if (stars > (STATE.progress.stars[level.id] || 0))
1289
+ STATE.progress.stars[level.id] = stars;
1290
+ if (
1291
+ STATE.currentLevelIndex >= STATE.progress.unlockedIndex &&
1292
+ STATE.currentLevelIndex < LEVELS.length - 1
1293
+ ) {
1294
  STATE.progress.unlockedIndex = STATE.currentLevelIndex + 1;
1295
  }
1296
  saveProgress();
 
1299
  starContainer.innerHTML = "";
1300
  for (let i = 0; i < 3; i++) {
1301
  const filled = i < stars;
1302
+ starContainer.innerHTML += `<i class="fas fa-star ${
1303
+ filled ? "text-yellow-400" : "text-gray-600"
1304
+ }"></i>`;
1305
  }
1306
  document.getElementById("result-items").innerText = used;
1307
 
 
1326
  }
1327
  STATE.plannedActions.forEach((action, index) => {
1328
  const div = document.createElement("div");
1329
+ div.className =
1330
+ "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";
1331
+
1332
  const p = action.params;
1333
  const details = [];
1334
  if (p.size !== 1) details.push(`size:${p.size}`);
1335
  if (p.weight !== 1) details.push(`mass:${p.weight}`);
1336
  if (p.restitution !== 0) details.push(`bounce:${p.restitution}`);
1337
  if (p.angle !== 0) details.push(`angle:${p.angle}°`);
1338
+ if (p.velocity && (p.velocity[0] !== 0 || p.velocity[1] !== 0))
1339
+ details.push(`v:[${p.velocity}]`);
1340
+ if (p.isStatic !== (p.shape === "line"))
1341
+ details.push(p.isStatic ? "static" : "dynamic");
1342
  if (p.delay > 0) details.push(`delay:${p.delay}s`);
1343
 
1344
  const typeName = p.shape;
 
1349
  <div class="flex flex-col">
1350
  <div class="flex items-center gap-2">
1351
  <span class="text-xs font-bold uppercase">${typeName}</span>
1352
+ <span class="text-[10px] text-gray-500 font-mono">@ [${
1353
+ p.location[0]
1354
+ }, ${p.location[1]}]</span>
1355
  </div>
1356
+ ${
1357
+ details.length > 0
1358
+ ? `<div class="text-[9px] text-gray-400 font-mono leading-tight uppercase">${details.join(
1359
+ " | "
1360
+ )}</div>`
1361
+ : ""
1362
+ }
1363
  </div>
1364
  </div>
1365
  `;
1366
  const btnDelete = document.createElement("button");
1367
+ btnDelete.className =
1368
+ "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";
1369
  btnDelete.innerHTML = `<i class="fas fa-times text-xs"></i>`;
1370
  btnDelete.onclick = () => removeAction(index);
1371
  div.appendChild(btnDelete);
 
1375
 
1376
  function removeAction(index) {
1377
  const action = STATE.plannedActions[index];
1378
+ if (action.previewBody)
1379
+ Matter.World.remove(STATE.engine.world, action.previewBody);
1380
  STATE.plannedActions.splice(index, 1);
1381
  updateActionList();
1382
  }
 
1413
  angle = params.angle || rotation,
1414
  velocity = [0, 0],
1415
  static: isStaticParam,
1416
+ isStatic = params.isStatic !== undefined
1417
+ ? params.isStatic
1418
+ : isStaticParam !== undefined
1419
+ ? isStaticParam
1420
+ : ["platform", "line"].includes(shape),
1421
  } = params;
1422
 
1423
  // Handle location as string (comma-separated or descriptive)
1424
+ if (typeof location === "string") {
1425
  const parts = location.match(/\d+/g);
1426
+ if (
1427
+ parts &&
1428
+ parts.length === 2 &&
1429
+ !isNaN(parts[0]) &&
1430
+ !isNaN(parts[1])
1431
+ ) {
1432
  location = [parseFloat(parts[0]), parseFloat(parts[1])];
1433
  } else {
1434
  const loc = location.toLowerCase();
 
1437
  const margin = 2;
1438
 
1439
  const locations = {
1440
+ center: [midX, midY],
1441
  "top-left": [margin, margin],
1442
  "top-center": [midX, margin],
1443
  "top-right": [CONFIG.gridWidth - margin, margin],
1444
  "center-left": [margin, midY],
1445
+ center: [midX, midY],
1446
  "center-right": [CONFIG.gridWidth - margin, midY],
1447
  "bottom-left": [margin, CONFIG.gridHeight - margin],
1448
  "bottom-center": [midX, CONFIG.gridHeight - margin],
1449
+ "bottom-right": [
1450
+ CONFIG.gridWidth - margin,
1451
+ CONFIG.gridHeight - margin,
1452
+ ],
1453
  };
1454
  location = locations[loc] || [midX, midY];
1455
  }
 
1517
  const MODEL_ID = "Xenova/functiongemma-270m-game";
1518
  let tokenizer, model;
1519
 
1520
+ const TOOL_SCHEMA = [
1521
+ {
1522
+ type: "function",
1523
+ function: {
1524
+ name: "add",
1525
+ description: "Add a shape into the game scene.",
1526
+ parameters: {
1527
+ type: "object",
1528
+ properties: {
1529
+ shape: {
1530
+ type: "string",
1531
+ enum: [
1532
+ "circle",
1533
+ "square",
1534
+ "triangle",
1535
+ "star",
1536
+ "rectangle",
1537
+ "line",
1538
+ "ellipse",
1539
+ ],
1540
+ description: "The kind shape to add. Required.",
1541
+ },
1542
+ location: {
1543
+ type: "string",
1544
+ description:
1545
+ "The [x, y] coordinates where the shape will be placed or a descriptive string. Required.",
1546
+ },
1547
+ size: {
1548
+ type: "number",
1549
+ description:
1550
+ "The size of the object (between 0.1 and 10.0). Default is 1.0.",
1551
+ },
1552
+ rotation: {
1553
+ type: "integer",
1554
+ description:
1555
+ "The initial clockwise rotation of the object in degrees (0-360). Default is 0.",
1556
+ },
1557
+ friction: {
1558
+ type: "number",
1559
+ description:
1560
+ "The friction of the object (between 0.0 and 1.0). Default is 0.0.",
1561
+ },
1562
+ restitution: {
1563
+ type: "number",
1564
+ description:
1565
+ "The bounciness of the object (between 0.0 and 1.0). Default is 0.0.",
1566
+ },
1567
+ mass: {
1568
+ type: "number",
1569
+ description:
1570
+ "The mass of the object (between 1.0 and 10.0). Default is 1.0.",
1571
+ },
1572
+ delay: {
1573
+ type: "number",
1574
+ description:
1575
+ "The time in seconds to wait before the object appears in the scene. Default is 0.0.",
1576
+ },
1577
+ static: {
1578
+ type: "boolean",
1579
+ description:
1580
+ "Whether the object is static (immovable) or dynamic. Default is False.",
1581
+ },
1582
+ velocity: {
1583
+ type: "array",
1584
+ items: { type: "number" },
1585
+ description:
1586
+ "The initial [vx, vy] velocity vector of the object (values between -10.0 and 10.0). Default is [0.0, 0.0].",
1587
+ },
1588
+ color: {
1589
+ type: "string",
1590
+ description:
1591
+ 'The color of the object as a string or hex code (e.g., "red", "blue", "#FF00FF"). Default is "red".',
1592
+ },
1593
+ },
1594
+ required: ["shape", "location"],
1595
+ },
1596
+ return: {
1597
+ type: "string",
1598
+ description: "A unique identifier for the added shape.",
1599
+ },
1600
+ },
1601
+ },
1602
+ ];
1603
 
1604
  async function initModel() {
1605
+ const loadingBar = document.getElementById("loading-bar");
1606
+ const loadingStatus = document.getElementById("loading-status");
1607
+
1608
+ // Progress callback for Transformers.js
1609
+ const progress_callback = (data) => {
1610
+ if (data.status === "progress") {
1611
+ const progress = Math.round(data.progress);
1612
+ loadingBar.style.width = `${progress}%`;
1613
+ loadingStatus.innerText = `Downloading: ${progress}%`;
1614
+ } else if (data.status === "done") {
1615
+ loadingStatus.innerText = "Processing Model...";
1616
+ } else if (data.status === "ready") {
1617
+ loadingStatus.innerText = "Model Ready!";
1618
+ }
1619
+ };
1620
+
1621
  try {
1622
+ // Both Tokenizer and Model support the progress_callback
1623
+ tokenizer = await AutoTokenizer.from_pretrained(MODEL_ID, {
1624
+ progress_callback,
1625
+ });
1626
+
1627
  model = await AutoModelForCausalLM.from_pretrained(MODEL_ID, {
1628
  device: "webgpu",
1629
  dtype: "q4",
1630
+ progress_callback, // This will track the actual heavy lifting (model weights)
1631
  });
1632
+
1633
  document.getElementById("loading-overlay").classList.add("hidden");
1634
  } catch (e) {
1635
  console.error(e);
1636
+ document.getElementById("loading-overlay").innerHTML = `
1637
+ <div class="text-neo-red font-bold p-6 bg-white border-4 border-black shadow-neo">
1638
+ <div class="text-xl mb-2">INITIALIZATION ERROR</div>
1639
+ <div class="font-mono text-sm">${e.message}</div>
1640
+ <button onclick="location.reload()" class="mt-4 bg-black text-white px-4 py-2 text-xs">RETRY</button>
1641
+ </div>`;
1642
  }
1643
  }
1644
 
 
1658
  const systemPrompt = `You are a model that can do function calling with the following functions`;
1659
 
1660
  try {
1661
+ const lines = input
1662
+ .split("\n")
1663
+ .map((l) => l.trim())
1664
+ .filter((l) => l.length > 0 && !l.startsWith("//"));
1665
 
1666
  for (const line of lines) {
1667
  // 2. Prepare Messages
 
1679
  });
1680
 
1681
  // 4. Generate
1682
+ const output = await model.generate({
1683
+ ...inputs,
1684
+ max_new_tokens: 128,
1685
+ do_sample: false,
1686
+ });
1687
+ const decoded = tokenizer.decode(
1688
+ output.slice(0, [inputs.input_ids.dims[1], null]),
1689
+ { skip_special_tokens: false }
1690
+ );
1691
 
1692
  // 5. Parse Output
1693
  // Format: <start_function_call>call:add{...}<end_function_call>
 
1697
  const endIndex = decoded.indexOf(endTag);
1698
 
1699
  if (startIndex !== -1 && endIndex !== -1) {
1700
+ let callStr = decoded.substring(
1701
+ startIndex + startTag.length,
1702
+ endIndex
1703
+ );
1704
  if (callStr.startsWith("call:add")) {
1705
  // Extract JSON-like string: {location:[...],shape:<escape>...<escape>}
1706
  let argsStr = callStr.substring(callStr.indexOf("{"));
 
1716
  throw new Error("Model did not generate a valid add command.");
1717
  }
1718
  } else {
1719
+ throw new Error(`Could not understand command: "${line}"`);
1720
  }
1721
  }
1722
  document.getElementById("code-input").value = "";
 
1752
  });
1753
  };
1754
 
1755
+ const { ball, goal } = level.setup(
1756
+ Matter.World,
1757
+ Matter.Bodies,
1758
+ STATE.engine.world,
1759
+ addEvent
1760
+ );
1761
 
1762
  STATE.currentBall = ball;
1763
+ STATE.winCondition = () =>
1764
+ Matter.Collision.collides(ball, goal) !== null;
1765
 
1766
  // Real Action Executioner
1767
  const executeReal = (params) => {
1768
+ const {
1769
+ shape,
1770
+ location,
1771
+ size,
1772
+ weight,
1773
+ restitution,
1774
+ friction,
1775
+ angle,
1776
+ velocity,
1777
+ isStatic,
1778
+ } = params;
1779
  const x = pX(location[0]),
1780
  y = pY(location[1]);
1781
  const pixelSize = pX(size);
 
1810
 
1811
  STATE.isPlaying = true;
1812
  STATE.startTime = performance.now();
1813
+ document.getElementById(
1814
+ "status-badge"
1815
+ ).innerHTML = `<div class="w-3 h-3 bg-green-500 rounded-full border border-black animate-ping"></div> RUNNING`;
1816
  Matter.Runner.run(STATE.runner, STATE.engine);
1817
  }
1818
 
 
1831
  const stars = STATE.progress.stars[index] || 0;
1832
 
1833
  const card = document.createElement("div");
1834
+ card.className = `level-card neo-border p-4 flex flex-col items-center justify-center gap-2 aspect-square ${
1835
+ unlocked ? "bg-white cursor-pointer" : "locked"
1836
+ }`;
1837
 
1838
  if (unlocked) {
1839
  card.onclick = () => loadLevel(index);
 
1841
  <div class="text-3xl font-black">${index + 1}</div>
1842
  <div class="flex gap-1 text-xs">
1843
  ${Array(3)
1844
+ .fill(0)
1845
+ .map(
1846
+ (_, i) =>
1847
+ `<i class="fas fa-star ${
1848
+ i < stars
1849
+ ? "text-neo-yellow"
1850
+ : "text-gray-300"
1851
+ }"></i>`
1852
+ )
1853
+ .join("")}
1854
  </div>
1855
  `;
1856
  } else {
 
1868
  Matter.Runner.stop(STATE.runner);
1869
  const level = LEVELS[STATE.currentLevelIndex];
1870
  Matter.Composite.clear(STATE.engine.world);
1871
+ const { ball, goal } = level.setup(
1872
+ Matter.World,
1873
+ Matter.Bodies,
1874
+ STATE.engine.world,
1875
+ () => {}
1876
+ );
1877
 
1878
  STATE.currentBall = ball;
1879
 
 
1882
  });
1883
 
1884
  STATE.isPlaying = false;
1885
+ document.getElementById(
1886
+ "status-badge"
1887
+ ).innerHTML = `<div class="w-3 h-3 bg-red-500 rounded-full border border-black animate-pulse"></div> READY`;
1888
  document.getElementById("timer-display").innerText = "0.00s";
1889
  document.getElementById("win-message").style.display = "none";
1890
  };
 
1892
  document.getElementById("btn-clear-all").onclick = () => {
1893
  document.getElementById("code-input").value = "";
1894
  STATE.plannedActions.forEach((action) => {
1895
+ if (action.previewBody)
1896
+ Matter.World.remove(STATE.engine.world, action.previewBody);
1897
  });
1898
  STATE.plannedActions = [];
1899
  updateActionList();
 
1920
  initModel();
1921
  };
1922
  </script>
1923
+ </body>
1924
+ </html>