Orbit Chase Deploy commited on
Commit
1b2ff95
·
1 Parent(s): c06f99f

Deploy Orbit Chase 2026-08-18T01:46:19Z

Browse files
README.md CHANGED
@@ -4,8 +4,7 @@ emoji: 🎮
4
  colorFrom: blue
5
  colorTo: purple
6
  sdk: static
7
- app_build_command: npm run build
8
- app_file: dist/index.html
9
  pinned: false
10
  license: mit
11
  ---
 
4
  colorFrom: blue
5
  colorTo: purple
6
  sdk: static
7
+ app_file: index.html
 
8
  pinned: false
9
  license: mit
10
  ---
assets/index-DRZiR9In.js ADDED
The diff for this file is too large to render. See raw diff
 
assets/index-DVflwj4H.css ADDED
@@ -0,0 +1 @@
 
 
1
+ :root{color:#e8f5ff;background:#071426;font-family:Inter,ui-sans-serif,system-ui,sans-serif}*{box-sizing:border-box}body{min-width:320px;min-height:100vh;margin:0;display:grid;place-items:center}main{width:min(100vw,1180px);padding:16px}.app-shell{display:flex;align-items:flex-start;justify-content:center;gap:14px}.game-column{width:min(100%,800px)}.game-toolbar{display:flex;justify-content:flex-end;gap:8px;margin:0 0 8px}.game-toolbar button{border:1px solid #447b9b;border-radius:7px;background:#173a55;color:#e8f5ff;cursor:pointer;font:inherit;font-size:14px;padding:7px 10px}.game-toolbar button:hover{background:#214d6d}.game-toolbar button:disabled{opacity:.55;cursor:default}#game{overflow:hidden;border:1px solid #2d607e;border-radius:14px;box-shadow:0 20px 70px #0008}canvas{display:block;width:100%!important;height:auto!important}#debug-panel{width:300px;max-height:640px;overflow:auto;border:1px solid #2d607e;border-radius:14px;background:#0a1c2d;box-shadow:0 20px 70px #0008;padding:16px}.debug-heading{display:flex;align-items:baseline;justify-content:space-between;gap:10px;border-bottom:1px solid #28546f;padding-bottom:10px}.debug-heading h2{color:#91efff;font-size:17px;margin:0}.debug-heading span{color:#8daaba;font-size:11px;white-space:nowrap}#debug-content{color:#d9efff;font:12px/1.6 ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;margin:13px 0 0;white-space:pre-wrap}@media(max-width:1080px){.app-shell{align-items:stretch;flex-direction:column}.game-column{width:100%}#debug-panel{width:100%;max-height:340px}}
index.html CHANGED
@@ -5,6 +5,8 @@
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
  <meta name="theme-color" content="#071426" />
7
  <title>Orbit Chase</title>
 
 
8
  </head>
9
  <body>
10
  <main class="app-shell">
@@ -23,6 +25,5 @@
23
  <pre id="debug-content">Waiting for game state…</pre>
24
  </aside>
25
  </main>
26
- <script type="module" src="/src/main.ts"></script>
27
  </body>
28
  </html>
 
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
  <meta name="theme-color" content="#071426" />
7
  <title>Orbit Chase</title>
8
+ <script type="module" crossorigin src="/assets/index-DRZiR9In.js"></script>
9
+ <link rel="stylesheet" crossorigin href="/assets/index-DVflwj4H.css">
10
  </head>
11
  <body>
12
  <main class="app-shell">
 
25
  <pre id="debug-content">Waiting for game state…</pre>
26
  </aside>
27
  </main>
 
28
  </body>
29
  </html>
{public/models → models}/linear-sarsa-20260815-030554-ep8000.json RENAMED
File without changes
package-lock.json DELETED
@@ -1,1197 +0,0 @@
1
- {
2
- "name": "orbit-chase",
3
- "version": "0.1.0",
4
- "lockfileVersion": 3,
5
- "requires": true,
6
- "packages": {
7
- "": {
8
- "name": "orbit-chase",
9
- "version": "0.1.0",
10
- "dependencies": {
11
- "phaser": "3.90.0"
12
- },
13
- "devDependencies": {
14
- "typescript": "~5.7.2",
15
- "vite": "~6.3.1"
16
- }
17
- },
18
- "node_modules/@esbuild/aix-ppc64": {
19
- "version": "0.25.12",
20
- "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz",
21
- "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==",
22
- "cpu": [
23
- "ppc64"
24
- ],
25
- "dev": true,
26
- "license": "MIT",
27
- "optional": true,
28
- "os": [
29
- "aix"
30
- ],
31
- "engines": {
32
- "node": ">=18"
33
- }
34
- },
35
- "node_modules/@esbuild/android-arm": {
36
- "version": "0.25.12",
37
- "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz",
38
- "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==",
39
- "cpu": [
40
- "arm"
41
- ],
42
- "dev": true,
43
- "license": "MIT",
44
- "optional": true,
45
- "os": [
46
- "android"
47
- ],
48
- "engines": {
49
- "node": ">=18"
50
- }
51
- },
52
- "node_modules/@esbuild/android-arm64": {
53
- "version": "0.25.12",
54
- "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz",
55
- "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==",
56
- "cpu": [
57
- "arm64"
58
- ],
59
- "dev": true,
60
- "license": "MIT",
61
- "optional": true,
62
- "os": [
63
- "android"
64
- ],
65
- "engines": {
66
- "node": ">=18"
67
- }
68
- },
69
- "node_modules/@esbuild/android-x64": {
70
- "version": "0.25.12",
71
- "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz",
72
- "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==",
73
- "cpu": [
74
- "x64"
75
- ],
76
- "dev": true,
77
- "license": "MIT",
78
- "optional": true,
79
- "os": [
80
- "android"
81
- ],
82
- "engines": {
83
- "node": ">=18"
84
- }
85
- },
86
- "node_modules/@esbuild/darwin-arm64": {
87
- "version": "0.25.12",
88
- "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz",
89
- "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==",
90
- "cpu": [
91
- "arm64"
92
- ],
93
- "dev": true,
94
- "license": "MIT",
95
- "optional": true,
96
- "os": [
97
- "darwin"
98
- ],
99
- "engines": {
100
- "node": ">=18"
101
- }
102
- },
103
- "node_modules/@esbuild/darwin-x64": {
104
- "version": "0.25.12",
105
- "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz",
106
- "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==",
107
- "cpu": [
108
- "x64"
109
- ],
110
- "dev": true,
111
- "license": "MIT",
112
- "optional": true,
113
- "os": [
114
- "darwin"
115
- ],
116
- "engines": {
117
- "node": ">=18"
118
- }
119
- },
120
- "node_modules/@esbuild/freebsd-arm64": {
121
- "version": "0.25.12",
122
- "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz",
123
- "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==",
124
- "cpu": [
125
- "arm64"
126
- ],
127
- "dev": true,
128
- "license": "MIT",
129
- "optional": true,
130
- "os": [
131
- "freebsd"
132
- ],
133
- "engines": {
134
- "node": ">=18"
135
- }
136
- },
137
- "node_modules/@esbuild/freebsd-x64": {
138
- "version": "0.25.12",
139
- "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz",
140
- "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==",
141
- "cpu": [
142
- "x64"
143
- ],
144
- "dev": true,
145
- "license": "MIT",
146
- "optional": true,
147
- "os": [
148
- "freebsd"
149
- ],
150
- "engines": {
151
- "node": ">=18"
152
- }
153
- },
154
- "node_modules/@esbuild/linux-arm": {
155
- "version": "0.25.12",
156
- "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz",
157
- "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==",
158
- "cpu": [
159
- "arm"
160
- ],
161
- "dev": true,
162
- "license": "MIT",
163
- "optional": true,
164
- "os": [
165
- "linux"
166
- ],
167
- "engines": {
168
- "node": ">=18"
169
- }
170
- },
171
- "node_modules/@esbuild/linux-arm64": {
172
- "version": "0.25.12",
173
- "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz",
174
- "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==",
175
- "cpu": [
176
- "arm64"
177
- ],
178
- "dev": true,
179
- "license": "MIT",
180
- "optional": true,
181
- "os": [
182
- "linux"
183
- ],
184
- "engines": {
185
- "node": ">=18"
186
- }
187
- },
188
- "node_modules/@esbuild/linux-ia32": {
189
- "version": "0.25.12",
190
- "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz",
191
- "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==",
192
- "cpu": [
193
- "ia32"
194
- ],
195
- "dev": true,
196
- "license": "MIT",
197
- "optional": true,
198
- "os": [
199
- "linux"
200
- ],
201
- "engines": {
202
- "node": ">=18"
203
- }
204
- },
205
- "node_modules/@esbuild/linux-loong64": {
206
- "version": "0.25.12",
207
- "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz",
208
- "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==",
209
- "cpu": [
210
- "loong64"
211
- ],
212
- "dev": true,
213
- "license": "MIT",
214
- "optional": true,
215
- "os": [
216
- "linux"
217
- ],
218
- "engines": {
219
- "node": ">=18"
220
- }
221
- },
222
- "node_modules/@esbuild/linux-mips64el": {
223
- "version": "0.25.12",
224
- "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz",
225
- "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==",
226
- "cpu": [
227
- "mips64el"
228
- ],
229
- "dev": true,
230
- "license": "MIT",
231
- "optional": true,
232
- "os": [
233
- "linux"
234
- ],
235
- "engines": {
236
- "node": ">=18"
237
- }
238
- },
239
- "node_modules/@esbuild/linux-ppc64": {
240
- "version": "0.25.12",
241
- "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz",
242
- "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==",
243
- "cpu": [
244
- "ppc64"
245
- ],
246
- "dev": true,
247
- "license": "MIT",
248
- "optional": true,
249
- "os": [
250
- "linux"
251
- ],
252
- "engines": {
253
- "node": ">=18"
254
- }
255
- },
256
- "node_modules/@esbuild/linux-riscv64": {
257
- "version": "0.25.12",
258
- "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz",
259
- "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==",
260
- "cpu": [
261
- "riscv64"
262
- ],
263
- "dev": true,
264
- "license": "MIT",
265
- "optional": true,
266
- "os": [
267
- "linux"
268
- ],
269
- "engines": {
270
- "node": ">=18"
271
- }
272
- },
273
- "node_modules/@esbuild/linux-s390x": {
274
- "version": "0.25.12",
275
- "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz",
276
- "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==",
277
- "cpu": [
278
- "s390x"
279
- ],
280
- "dev": true,
281
- "license": "MIT",
282
- "optional": true,
283
- "os": [
284
- "linux"
285
- ],
286
- "engines": {
287
- "node": ">=18"
288
- }
289
- },
290
- "node_modules/@esbuild/linux-x64": {
291
- "version": "0.25.12",
292
- "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz",
293
- "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==",
294
- "cpu": [
295
- "x64"
296
- ],
297
- "dev": true,
298
- "license": "MIT",
299
- "optional": true,
300
- "os": [
301
- "linux"
302
- ],
303
- "engines": {
304
- "node": ">=18"
305
- }
306
- },
307
- "node_modules/@esbuild/netbsd-arm64": {
308
- "version": "0.25.12",
309
- "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz",
310
- "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==",
311
- "cpu": [
312
- "arm64"
313
- ],
314
- "dev": true,
315
- "license": "MIT",
316
- "optional": true,
317
- "os": [
318
- "netbsd"
319
- ],
320
- "engines": {
321
- "node": ">=18"
322
- }
323
- },
324
- "node_modules/@esbuild/netbsd-x64": {
325
- "version": "0.25.12",
326
- "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz",
327
- "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==",
328
- "cpu": [
329
- "x64"
330
- ],
331
- "dev": true,
332
- "license": "MIT",
333
- "optional": true,
334
- "os": [
335
- "netbsd"
336
- ],
337
- "engines": {
338
- "node": ">=18"
339
- }
340
- },
341
- "node_modules/@esbuild/openbsd-arm64": {
342
- "version": "0.25.12",
343
- "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz",
344
- "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==",
345
- "cpu": [
346
- "arm64"
347
- ],
348
- "dev": true,
349
- "license": "MIT",
350
- "optional": true,
351
- "os": [
352
- "openbsd"
353
- ],
354
- "engines": {
355
- "node": ">=18"
356
- }
357
- },
358
- "node_modules/@esbuild/openbsd-x64": {
359
- "version": "0.25.12",
360
- "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz",
361
- "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==",
362
- "cpu": [
363
- "x64"
364
- ],
365
- "dev": true,
366
- "license": "MIT",
367
- "optional": true,
368
- "os": [
369
- "openbsd"
370
- ],
371
- "engines": {
372
- "node": ">=18"
373
- }
374
- },
375
- "node_modules/@esbuild/openharmony-arm64": {
376
- "version": "0.25.12",
377
- "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz",
378
- "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==",
379
- "cpu": [
380
- "arm64"
381
- ],
382
- "dev": true,
383
- "license": "MIT",
384
- "optional": true,
385
- "os": [
386
- "openharmony"
387
- ],
388
- "engines": {
389
- "node": ">=18"
390
- }
391
- },
392
- "node_modules/@esbuild/sunos-x64": {
393
- "version": "0.25.12",
394
- "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz",
395
- "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==",
396
- "cpu": [
397
- "x64"
398
- ],
399
- "dev": true,
400
- "license": "MIT",
401
- "optional": true,
402
- "os": [
403
- "sunos"
404
- ],
405
- "engines": {
406
- "node": ">=18"
407
- }
408
- },
409
- "node_modules/@esbuild/win32-arm64": {
410
- "version": "0.25.12",
411
- "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz",
412
- "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==",
413
- "cpu": [
414
- "arm64"
415
- ],
416
- "dev": true,
417
- "license": "MIT",
418
- "optional": true,
419
- "os": [
420
- "win32"
421
- ],
422
- "engines": {
423
- "node": ">=18"
424
- }
425
- },
426
- "node_modules/@esbuild/win32-ia32": {
427
- "version": "0.25.12",
428
- "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz",
429
- "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==",
430
- "cpu": [
431
- "ia32"
432
- ],
433
- "dev": true,
434
- "license": "MIT",
435
- "optional": true,
436
- "os": [
437
- "win32"
438
- ],
439
- "engines": {
440
- "node": ">=18"
441
- }
442
- },
443
- "node_modules/@esbuild/win32-x64": {
444
- "version": "0.25.12",
445
- "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz",
446
- "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==",
447
- "cpu": [
448
- "x64"
449
- ],
450
- "dev": true,
451
- "license": "MIT",
452
- "optional": true,
453
- "os": [
454
- "win32"
455
- ],
456
- "engines": {
457
- "node": ">=18"
458
- }
459
- },
460
- "node_modules/@napi-rs/lzma-linux-x64-gnu": {
461
- "version": "1.5.1",
462
- "resolved": "https://registry.npmjs.org/@napi-rs/lzma-linux-x64-gnu/-/lzma-linux-x64-gnu-1.5.1.tgz",
463
- "integrity": "sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ==",
464
- "cpu": [
465
- "x64"
466
- ],
467
- "dev": true,
468
- "libc": [
469
- "glibc"
470
- ],
471
- "license": "MIT",
472
- "optional": true,
473
- "os": [
474
- "linux"
475
- ],
476
- "engines": {
477
- "node": "^22.20 || ^24.12 || >=25"
478
- }
479
- },
480
- "node_modules/@rollup/rollup-android-arm-eabi": {
481
- "version": "4.62.4",
482
- "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.4.tgz",
483
- "integrity": "sha512-RrPokAb7dmbxFoeO3TloqHyOjgye8RkBhSqmp4aJMIex4c9r46ZstPnleDQOq1t46VOVjwIuwNogIqbodV1Vvg==",
484
- "cpu": [
485
- "arm"
486
- ],
487
- "dev": true,
488
- "license": "MIT",
489
- "optional": true,
490
- "os": [
491
- "android"
492
- ]
493
- },
494
- "node_modules/@rollup/rollup-android-arm64": {
495
- "version": "4.62.4",
496
- "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.4.tgz",
497
- "integrity": "sha512-JKuJc+pnpks2pjy7L/N3v/cAkZxYlnmuZoD840ldbMI5KDbC4iO9NKwPKYdjYFCMAIIlBzYSFHxIJVYzRo2/8A==",
498
- "cpu": [
499
- "arm64"
500
- ],
501
- "dev": true,
502
- "license": "MIT",
503
- "optional": true,
504
- "os": [
505
- "android"
506
- ]
507
- },
508
- "node_modules/@rollup/rollup-darwin-arm64": {
509
- "version": "4.62.4",
510
- "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.4.tgz",
511
- "integrity": "sha512-krw5uS2STmvJ02x0uTXHbqQNuz+9eZ1iw+qXk9dmW2gvV4jV7O2hEoOnuhFrpOPiel1mBFtqbxYZZtC46hXLOw==",
512
- "cpu": [
513
- "arm64"
514
- ],
515
- "dev": true,
516
- "license": "MIT",
517
- "optional": true,
518
- "os": [
519
- "darwin"
520
- ]
521
- },
522
- "node_modules/@rollup/rollup-darwin-x64": {
523
- "version": "4.62.4",
524
- "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.4.tgz",
525
- "integrity": "sha512-wsTxtgApb4PrOsNJIm0FZ1h3WvCC+k9uxLJ4ad75hgoS4NiRes2SoJFlDAyMwiUY8IssDqGcHbXuN0sx1tfF1A==",
526
- "cpu": [
527
- "x64"
528
- ],
529
- "dev": true,
530
- "license": "MIT",
531
- "optional": true,
532
- "os": [
533
- "darwin"
534
- ]
535
- },
536
- "node_modules/@rollup/rollup-freebsd-arm64": {
537
- "version": "4.62.4",
538
- "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.4.tgz",
539
- "integrity": "sha512-GUOnQlyZe3yAXhWOtOMsn5Qkrv5E5mZXa0thbARWi5Ei2szlVXJFQhddZ4HbAzh8q92w5twp+CQvs/eFanz9YQ==",
540
- "cpu": [
541
- "arm64"
542
- ],
543
- "dev": true,
544
- "license": "MIT",
545
- "optional": true,
546
- "os": [
547
- "freebsd"
548
- ]
549
- },
550
- "node_modules/@rollup/rollup-freebsd-x64": {
551
- "version": "4.62.4",
552
- "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.4.tgz",
553
- "integrity": "sha512-/Y7f3QuxjzPKsjA/rfEDa3+0vXqyjmJ50Ln8dPpCmWkKTrUoWHG1cWhTqaAMLob2m2nESWuC7yGrREz019Ztqg==",
554
- "cpu": [
555
- "x64"
556
- ],
557
- "dev": true,
558
- "license": "MIT",
559
- "optional": true,
560
- "os": [
561
- "freebsd"
562
- ]
563
- },
564
- "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
565
- "version": "4.62.4",
566
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.4.tgz",
567
- "integrity": "sha512-81wiiX3v7aqy+T+bT61TJ78yJjRquqFFTTbAPt08imfQQzkPIW8t6aJbkTagtCCrXMNc9D66+geqlK7ydLPNqA==",
568
- "cpu": [
569
- "arm"
570
- ],
571
- "dev": true,
572
- "libc": [
573
- "glibc"
574
- ],
575
- "license": "MIT",
576
- "optional": true,
577
- "os": [
578
- "linux"
579
- ]
580
- },
581
- "node_modules/@rollup/rollup-linux-arm-musleabihf": {
582
- "version": "4.62.4",
583
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.4.tgz",
584
- "integrity": "sha512-9kmDIvNZqdoHOBZgNtpTBeLWYO/LVipM3H/j62P8848/l/VPEQL6N3uxU9pvP1oZAsXyC2MEnFP3ovRjo7WYNQ==",
585
- "cpu": [
586
- "arm"
587
- ],
588
- "dev": true,
589
- "libc": [
590
- "musl"
591
- ],
592
- "license": "MIT",
593
- "optional": true,
594
- "os": [
595
- "linux"
596
- ]
597
- },
598
- "node_modules/@rollup/rollup-linux-arm64-gnu": {
599
- "version": "4.62.4",
600
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.4.tgz",
601
- "integrity": "sha512-CcnXHWnXg69g+DX5VWL3FHts3qMRN2uVEHX+BZvGLdd07/gXkn3ePjYtO1LDJvxkGKVHMclKBRa1QUTH+6toYQ==",
602
- "cpu": [
603
- "arm64"
604
- ],
605
- "dev": true,
606
- "libc": [
607
- "glibc"
608
- ],
609
- "license": "MIT",
610
- "optional": true,
611
- "os": [
612
- "linux"
613
- ]
614
- },
615
- "node_modules/@rollup/rollup-linux-arm64-musl": {
616
- "version": "4.62.4",
617
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.4.tgz",
618
- "integrity": "sha512-iFOibiHnTRuhrWLlRsOQFdZJJIa7S8OwkneJr4ocALP16u5yk6lWLINFwhHaEqBFMsKDUZofLkGos7+CPzGB3g==",
619
- "cpu": [
620
- "arm64"
621
- ],
622
- "dev": true,
623
- "libc": [
624
- "musl"
625
- ],
626
- "license": "MIT",
627
- "optional": true,
628
- "os": [
629
- "linux"
630
- ]
631
- },
632
- "node_modules/@rollup/rollup-linux-loong64-gnu": {
633
- "version": "4.62.4",
634
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.4.tgz",
635
- "integrity": "sha512-XnWYMI7euHlb5a871xPja+Gm7DRCFU+FGRrtS2sMq9N8FvqtpagUy6gD4YOemC5MRk9xbh8+jYMEJbigFQwsgA==",
636
- "cpu": [
637
- "loong64"
638
- ],
639
- "dev": true,
640
- "libc": [
641
- "glibc"
642
- ],
643
- "license": "MIT",
644
- "optional": true,
645
- "os": [
646
- "linux"
647
- ]
648
- },
649
- "node_modules/@rollup/rollup-linux-loong64-musl": {
650
- "version": "4.62.4",
651
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.4.tgz",
652
- "integrity": "sha512-qGDAlO0U8xedCcsdRm9oaoQY8DAx/QT7uIxJWhCdx0ceIWX783UC9QSYkdpzAe29wNiVfp24+bZdQmn49o45SQ==",
653
- "cpu": [
654
- "loong64"
655
- ],
656
- "dev": true,
657
- "libc": [
658
- "musl"
659
- ],
660
- "license": "MIT",
661
- "optional": true,
662
- "os": [
663
- "linux"
664
- ]
665
- },
666
- "node_modules/@rollup/rollup-linux-ppc64-gnu": {
667
- "version": "4.62.4",
668
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.4.tgz",
669
- "integrity": "sha512-ru4H6ezD7ysA5EiEK6qkkaEb4modH8CTej6kUy/gQi20u3kB3G7Zn8snXXkeJSCOFKG/rbPPtM/+9Wgas1961w==",
670
- "cpu": [
671
- "ppc64"
672
- ],
673
- "dev": true,
674
- "libc": [
675
- "glibc"
676
- ],
677
- "license": "MIT",
678
- "optional": true,
679
- "os": [
680
- "linux"
681
- ]
682
- },
683
- "node_modules/@rollup/rollup-linux-ppc64-musl": {
684
- "version": "4.62.4",
685
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.4.tgz",
686
- "integrity": "sha512-2W4MO5WQVJnbJaZdvDb9rhBDuFU1nKIepPFpJUBsTh2k1YY2g+ODViaWuyOAjQ5cOP7NvrvLzt3wvHOoiAvc7w==",
687
- "cpu": [
688
- "ppc64"
689
- ],
690
- "dev": true,
691
- "libc": [
692
- "musl"
693
- ],
694
- "license": "MIT",
695
- "optional": true,
696
- "os": [
697
- "linux"
698
- ]
699
- },
700
- "node_modules/@rollup/rollup-linux-riscv64-gnu": {
701
- "version": "4.62.4",
702
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.4.tgz",
703
- "integrity": "sha512-+fxjfuoAmVMCYV5QyjoIpu0cp5DOiOTeqYFk1AVaxGr+/ravWLX89XfQmptsoWcaVy/TGf2hexzbUOrCQIL1CQ==",
704
- "cpu": [
705
- "riscv64"
706
- ],
707
- "dev": true,
708
- "libc": [
709
- "glibc"
710
- ],
711
- "license": "MIT",
712
- "optional": true,
713
- "os": [
714
- "linux"
715
- ]
716
- },
717
- "node_modules/@rollup/rollup-linux-riscv64-musl": {
718
- "version": "4.62.4",
719
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.4.tgz",
720
- "integrity": "sha512-jTn8JfHGL4djjFxPuM06LmNUJDsst2jeVlsd9OmIH6zc5sC9K6rIuO4YajXatLUpBmBKl6b35ro1QZocLi+tcA==",
721
- "cpu": [
722
- "riscv64"
723
- ],
724
- "dev": true,
725
- "libc": [
726
- "musl"
727
- ],
728
- "license": "MIT",
729
- "optional": true,
730
- "os": [
731
- "linux"
732
- ]
733
- },
734
- "node_modules/@rollup/rollup-linux-s390x-gnu": {
735
- "version": "4.62.4",
736
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.4.tgz",
737
- "integrity": "sha512-oCJCJL4pXsoDcP2QZ+JVlPTIRc6266zsIaeJJsWImmF7HO0W8nb6HuSgZlMWxJwaPf8ehbSw8yo0EUw925hKsA==",
738
- "cpu": [
739
- "s390x"
740
- ],
741
- "dev": true,
742
- "libc": [
743
- "glibc"
744
- ],
745
- "license": "MIT",
746
- "optional": true,
747
- "os": [
748
- "linux"
749
- ]
750
- },
751
- "node_modules/@rollup/rollup-linux-x64-gnu": {
752
- "version": "4.62.4",
753
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.4.tgz",
754
- "integrity": "sha512-W69hukhZ3KKNRCaMIEzKvcFye42hh0FE1+YoYaf5+Ikacuftoco6yO/xouz0hc5d5W/s3yBro5jRiuEE/Q5vUw==",
755
- "cpu": [
756
- "x64"
757
- ],
758
- "dev": true,
759
- "libc": [
760
- "glibc"
761
- ],
762
- "license": "MIT",
763
- "optional": true,
764
- "os": [
765
- "linux"
766
- ]
767
- },
768
- "node_modules/@rollup/rollup-linux-x64-musl": {
769
- "version": "4.62.4",
770
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.4.tgz",
771
- "integrity": "sha512-qiXbGG2jkjXhzXpsFZSR2Xpb8DN/UaxYsbb/STbuR/6fpaDgRmmaq1B/LmtF2wQFOFOSsK2jdE0RZ3a0zHn4QA==",
772
- "cpu": [
773
- "x64"
774
- ],
775
- "dev": true,
776
- "libc": [
777
- "musl"
778
- ],
779
- "license": "MIT",
780
- "optional": true,
781
- "os": [
782
- "linux"
783
- ]
784
- },
785
- "node_modules/@rollup/rollup-openbsd-x64": {
786
- "version": "4.62.4",
787
- "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.4.tgz",
788
- "integrity": "sha512-nWeM//hxv8mIo6jD7Hu4o48DVmV9pbV6gsKaWU+4NFyqHoPKwrkRiZGLKUhOBk8qNmDmpwFtPKg80Bo/Tn4xiQ==",
789
- "cpu": [
790
- "x64"
791
- ],
792
- "dev": true,
793
- "license": "MIT",
794
- "optional": true,
795
- "os": [
796
- "openbsd"
797
- ]
798
- },
799
- "node_modules/@rollup/rollup-openharmony-arm64": {
800
- "version": "4.62.4",
801
- "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.4.tgz",
802
- "integrity": "sha512-s62SQ/vgsRSvMwDkOEfTqfgASF0f26ZNaQuTA6Aok5lrikf89yI2W0gFHvZb2Jpgc6N8JnOKZgCK2iciO3CsxQ==",
803
- "cpu": [
804
- "arm64"
805
- ],
806
- "dev": true,
807
- "license": "MIT",
808
- "optional": true,
809
- "os": [
810
- "openharmony"
811
- ]
812
- },
813
- "node_modules/@rollup/rollup-win32-arm64-msvc": {
814
- "version": "4.62.4",
815
- "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.4.tgz",
816
- "integrity": "sha512-J6wGf8TVGbXJq+HH+ttTvrcfNKPbuZecV6KT1B8I18BC5IURUh5kl4Yl5OEP5eFIUoI5BWxCsyYMhFsDx8kekw==",
817
- "cpu": [
818
- "arm64"
819
- ],
820
- "dev": true,
821
- "license": "MIT",
822
- "optional": true,
823
- "os": [
824
- "win32"
825
- ]
826
- },
827
- "node_modules/@rollup/rollup-win32-ia32-msvc": {
828
- "version": "4.62.4",
829
- "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.4.tgz",
830
- "integrity": "sha512-zmfrQd/0wu6oJs8Vq8KwY/YtsKSsLtKe/HwAP4Wqy8LhWjeT55fHRAkOhYQ12wI3ayS4Tt12d5CDRD7N96SAYQ==",
831
- "cpu": [
832
- "ia32"
833
- ],
834
- "dev": true,
835
- "license": "MIT",
836
- "optional": true,
837
- "os": [
838
- "win32"
839
- ]
840
- },
841
- "node_modules/@rollup/rollup-win32-x64-gnu": {
842
- "version": "4.62.4",
843
- "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.4.tgz",
844
- "integrity": "sha512-qPzHqdj9rfUD+w79dtE07zi/kFwKyCJqplp5K5ygeLTp7jLpAoc16OAH39HSmRC9UpozaecsleI8uAdEj6v2yw==",
845
- "cpu": [
846
- "x64"
847
- ],
848
- "dev": true,
849
- "license": "MIT",
850
- "optional": true,
851
- "os": [
852
- "win32"
853
- ]
854
- },
855
- "node_modules/@rollup/rollup-win32-x64-msvc": {
856
- "version": "4.62.4",
857
- "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.4.tgz",
858
- "integrity": "sha512-zD6NdeWEByGE9QF9vCrlJ5YQB4oq9q91kPZS37Jwj5hOkvR1lTBSpsKhKDw4IJtbQ35LsTS1HD9DZYGKIshU1Q==",
859
- "cpu": [
860
- "x64"
861
- ],
862
- "dev": true,
863
- "license": "MIT",
864
- "optional": true,
865
- "os": [
866
- "win32"
867
- ]
868
- },
869
- "node_modules/@types/estree": {
870
- "version": "1.0.9",
871
- "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz",
872
- "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==",
873
- "dev": true,
874
- "license": "MIT"
875
- },
876
- "node_modules/esbuild": {
877
- "version": "0.25.12",
878
- "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz",
879
- "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==",
880
- "dev": true,
881
- "hasInstallScript": true,
882
- "license": "MIT",
883
- "bin": {
884
- "esbuild": "bin/esbuild"
885
- },
886
- "engines": {
887
- "node": ">=18"
888
- },
889
- "optionalDependencies": {
890
- "@esbuild/aix-ppc64": "0.25.12",
891
- "@esbuild/android-arm": "0.25.12",
892
- "@esbuild/android-arm64": "0.25.12",
893
- "@esbuild/android-x64": "0.25.12",
894
- "@esbuild/darwin-arm64": "0.25.12",
895
- "@esbuild/darwin-x64": "0.25.12",
896
- "@esbuild/freebsd-arm64": "0.25.12",
897
- "@esbuild/freebsd-x64": "0.25.12",
898
- "@esbuild/linux-arm": "0.25.12",
899
- "@esbuild/linux-arm64": "0.25.12",
900
- "@esbuild/linux-ia32": "0.25.12",
901
- "@esbuild/linux-loong64": "0.25.12",
902
- "@esbuild/linux-mips64el": "0.25.12",
903
- "@esbuild/linux-ppc64": "0.25.12",
904
- "@esbuild/linux-riscv64": "0.25.12",
905
- "@esbuild/linux-s390x": "0.25.12",
906
- "@esbuild/linux-x64": "0.25.12",
907
- "@esbuild/netbsd-arm64": "0.25.12",
908
- "@esbuild/netbsd-x64": "0.25.12",
909
- "@esbuild/openbsd-arm64": "0.25.12",
910
- "@esbuild/openbsd-x64": "0.25.12",
911
- "@esbuild/openharmony-arm64": "0.25.12",
912
- "@esbuild/sunos-x64": "0.25.12",
913
- "@esbuild/win32-arm64": "0.25.12",
914
- "@esbuild/win32-ia32": "0.25.12",
915
- "@esbuild/win32-x64": "0.25.12"
916
- }
917
- },
918
- "node_modules/eventemitter3": {
919
- "version": "5.0.4",
920
- "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz",
921
- "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==",
922
- "license": "MIT"
923
- },
924
- "node_modules/fdir": {
925
- "version": "6.5.0",
926
- "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
927
- "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
928
- "dev": true,
929
- "license": "MIT",
930
- "engines": {
931
- "node": ">=12.0.0"
932
- },
933
- "peerDependencies": {
934
- "picomatch": "^3 || ^4"
935
- },
936
- "peerDependenciesMeta": {
937
- "picomatch": {
938
- "optional": true
939
- }
940
- }
941
- },
942
- "node_modules/fsevents": {
943
- "version": "2.3.3",
944
- "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
945
- "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
946
- "dev": true,
947
- "hasInstallScript": true,
948
- "license": "MIT",
949
- "optional": true,
950
- "os": [
951
- "darwin"
952
- ],
953
- "engines": {
954
- "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
955
- }
956
- },
957
- "node_modules/nanoid": {
958
- "version": "3.3.18",
959
- "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz",
960
- "integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==",
961
- "dev": true,
962
- "funding": [
963
- {
964
- "type": "github",
965
- "url": "https://github.com/sponsors/ai"
966
- }
967
- ],
968
- "license": "MIT",
969
- "bin": {
970
- "nanoid": "bin/nanoid.cjs"
971
- },
972
- "engines": {
973
- "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
974
- }
975
- },
976
- "node_modules/phaser": {
977
- "version": "3.90.0",
978
- "resolved": "https://registry.npmjs.org/phaser/-/phaser-3.90.0.tgz",
979
- "integrity": "sha512-/cziz/5ZIn02uDkC9RzN8VF9x3Gs3XdFFf9nkiMEQT3p7hQlWuyjy4QWosU802qqno2YSLn2BfqwOKLv/sSVfQ==",
980
- "license": "MIT",
981
- "dependencies": {
982
- "eventemitter3": "^5.0.1"
983
- }
984
- },
985
- "node_modules/picocolors": {
986
- "version": "1.1.1",
987
- "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
988
- "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
989
- "dev": true,
990
- "license": "ISC"
991
- },
992
- "node_modules/picomatch": {
993
- "version": "4.0.5",
994
- "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz",
995
- "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==",
996
- "dev": true,
997
- "license": "MIT",
998
- "engines": {
999
- "node": ">=12"
1000
- },
1001
- "funding": {
1002
- "url": "https://github.com/sponsors/jonschlinkert"
1003
- }
1004
- },
1005
- "node_modules/postcss": {
1006
- "version": "8.5.26",
1007
- "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.26.tgz",
1008
- "integrity": "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==",
1009
- "dev": true,
1010
- "funding": [
1011
- {
1012
- "type": "opencollective",
1013
- "url": "https://opencollective.com/postcss/"
1014
- },
1015
- {
1016
- "type": "tidelift",
1017
- "url": "https://tidelift.com/funding/github/npm/postcss"
1018
- },
1019
- {
1020
- "type": "github",
1021
- "url": "https://github.com/sponsors/ai"
1022
- }
1023
- ],
1024
- "license": "MIT",
1025
- "dependencies": {
1026
- "nanoid": "^3.3.17",
1027
- "picocolors": "^1.1.1",
1028
- "source-map-js": "^1.2.1"
1029
- },
1030
- "engines": {
1031
- "node": "^10 || ^12 || >=14"
1032
- }
1033
- },
1034
- "node_modules/rollup": {
1035
- "version": "4.62.4",
1036
- "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.4.tgz",
1037
- "integrity": "sha512-RXOqwaPsBGjMNMa4sQjDjHieHEZDFoj/Rdr46l2MU5DfEs16wHJPC2RPTPHWhNl+M3aI472LLqFkFKut4SblOg==",
1038
- "dev": true,
1039
- "license": "MIT",
1040
- "dependencies": {
1041
- "@types/estree": "1.0.9"
1042
- },
1043
- "bin": {
1044
- "rollup": "dist/bin/rollup"
1045
- },
1046
- "engines": {
1047
- "node": ">=18.0.0",
1048
- "npm": ">=8.0.0"
1049
- },
1050
- "optionalDependencies": {
1051
- "@napi-rs/lzma-linux-x64-gnu": "1.5.1",
1052
- "@rollup/rollup-android-arm-eabi": "4.62.4",
1053
- "@rollup/rollup-android-arm64": "4.62.4",
1054
- "@rollup/rollup-darwin-arm64": "4.62.4",
1055
- "@rollup/rollup-darwin-x64": "4.62.4",
1056
- "@rollup/rollup-freebsd-arm64": "4.62.4",
1057
- "@rollup/rollup-freebsd-x64": "4.62.4",
1058
- "@rollup/rollup-linux-arm-gnueabihf": "4.62.4",
1059
- "@rollup/rollup-linux-arm-musleabihf": "4.62.4",
1060
- "@rollup/rollup-linux-arm64-gnu": "4.62.4",
1061
- "@rollup/rollup-linux-arm64-musl": "4.62.4",
1062
- "@rollup/rollup-linux-loong64-gnu": "4.62.4",
1063
- "@rollup/rollup-linux-loong64-musl": "4.62.4",
1064
- "@rollup/rollup-linux-ppc64-gnu": "4.62.4",
1065
- "@rollup/rollup-linux-ppc64-musl": "4.62.4",
1066
- "@rollup/rollup-linux-riscv64-gnu": "4.62.4",
1067
- "@rollup/rollup-linux-riscv64-musl": "4.62.4",
1068
- "@rollup/rollup-linux-s390x-gnu": "4.62.4",
1069
- "@rollup/rollup-linux-x64-gnu": "4.62.4",
1070
- "@rollup/rollup-linux-x64-musl": "4.62.4",
1071
- "@rollup/rollup-openbsd-x64": "4.62.4",
1072
- "@rollup/rollup-openharmony-arm64": "4.62.4",
1073
- "@rollup/rollup-win32-arm64-msvc": "4.62.4",
1074
- "@rollup/rollup-win32-ia32-msvc": "4.62.4",
1075
- "@rollup/rollup-win32-x64-gnu": "4.62.4",
1076
- "@rollup/rollup-win32-x64-msvc": "4.62.4",
1077
- "fsevents": "~2.3.2"
1078
- }
1079
- },
1080
- "node_modules/source-map-js": {
1081
- "version": "1.2.1",
1082
- "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
1083
- "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
1084
- "dev": true,
1085
- "license": "BSD-3-Clause",
1086
- "engines": {
1087
- "node": ">=0.10.0"
1088
- }
1089
- },
1090
- "node_modules/tinyglobby": {
1091
- "version": "0.2.17",
1092
- "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz",
1093
- "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==",
1094
- "dev": true,
1095
- "license": "MIT",
1096
- "dependencies": {
1097
- "fdir": "^6.5.0",
1098
- "picomatch": "^4.0.4"
1099
- },
1100
- "engines": {
1101
- "node": ">=12.0.0"
1102
- },
1103
- "funding": {
1104
- "url": "https://github.com/sponsors/SuperchupuDev"
1105
- }
1106
- },
1107
- "node_modules/typescript": {
1108
- "version": "5.7.3",
1109
- "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz",
1110
- "integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==",
1111
- "dev": true,
1112
- "license": "Apache-2.0",
1113
- "bin": {
1114
- "tsc": "bin/tsc",
1115
- "tsserver": "bin/tsserver"
1116
- },
1117
- "engines": {
1118
- "node": ">=14.17"
1119
- }
1120
- },
1121
- "node_modules/vite": {
1122
- "version": "6.3.7",
1123
- "resolved": "https://registry.npmjs.org/vite/-/vite-6.3.7.tgz",
1124
- "integrity": "sha512-mQYaKepA0NGMBsz8Xktt3tJUG5ELE2iT7IJ+ssXI6nxVdE2sFc/d/6w/JByqMLvWg8hNKHpPgzjgOkrhpKFnrA==",
1125
- "dev": true,
1126
- "license": "MIT",
1127
- "dependencies": {
1128
- "esbuild": "^0.25.0",
1129
- "fdir": "^6.4.4",
1130
- "picomatch": "^4.0.2",
1131
- "postcss": "^8.5.3",
1132
- "rollup": "^4.34.9",
1133
- "tinyglobby": "^0.2.13"
1134
- },
1135
- "bin": {
1136
- "vite": "bin/vite.js"
1137
- },
1138
- "engines": {
1139
- "node": "^18.0.0 || ^20.0.0 || >=22.0.0"
1140
- },
1141
- "funding": {
1142
- "url": "https://github.com/vitejs/vite?sponsor=1"
1143
- },
1144
- "optionalDependencies": {
1145
- "fsevents": "~2.3.3"
1146
- },
1147
- "peerDependencies": {
1148
- "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0",
1149
- "jiti": ">=1.21.0",
1150
- "less": "*",
1151
- "lightningcss": "^1.21.0",
1152
- "sass": "*",
1153
- "sass-embedded": "*",
1154
- "stylus": "*",
1155
- "sugarss": "*",
1156
- "terser": "^5.16.0",
1157
- "tsx": "^4.8.1",
1158
- "yaml": "^2.4.2"
1159
- },
1160
- "peerDependenciesMeta": {
1161
- "@types/node": {
1162
- "optional": true
1163
- },
1164
- "jiti": {
1165
- "optional": true
1166
- },
1167
- "less": {
1168
- "optional": true
1169
- },
1170
- "lightningcss": {
1171
- "optional": true
1172
- },
1173
- "sass": {
1174
- "optional": true
1175
- },
1176
- "sass-embedded": {
1177
- "optional": true
1178
- },
1179
- "stylus": {
1180
- "optional": true
1181
- },
1182
- "sugarss": {
1183
- "optional": true
1184
- },
1185
- "terser": {
1186
- "optional": true
1187
- },
1188
- "tsx": {
1189
- "optional": true
1190
- },
1191
- "yaml": {
1192
- "optional": true
1193
- }
1194
- }
1195
- }
1196
- }
1197
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
package.json DELETED
@@ -1,22 +0,0 @@
1
- {
2
- "name": "orbit-chase",
3
- "private": true,
4
- "version": "0.1.0",
5
- "type": "module",
6
- "scripts": {
7
- "dev": "vite",
8
- "build": "tsc --noEmit && vite build",
9
- "preview": "vite preview",
10
- "test": "tsc -p tsconfig.test.json && node --experimental-specifier-resolution=node --test test/*.test.mjs",
11
- "test:python": ".venv/bin/python -m pytest",
12
- "test:parity": "tsc -p tsconfig.test.json && PYTHONPATH=rl .venv/bin/python rl/parity/compare.py",
13
- "deploy:hf": "bash deploy/huggingface/deploy.sh"
14
- },
15
- "dependencies": {
16
- "phaser": "3.90.0"
17
- },
18
- "devDependencies": {
19
- "typescript": "~5.7.2",
20
- "vite": "~6.3.1"
21
- }
22
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/debugPanel.ts DELETED
@@ -1,41 +0,0 @@
1
- import { GameSimulation } from './game/simulation.js';
2
-
3
- const panel = document.querySelector<HTMLElement>('#debug-panel');
4
- const content = document.querySelector<HTMLPreElement>('#debug-content');
5
- const toggle = document.querySelector<HTMLButtonElement>('#debug-toggle');
6
- let lastRender = 0;
7
-
8
- if (toggle && panel) {
9
- toggle.addEventListener('click', () => {
10
- panel.hidden = !panel.hidden;
11
- toggle.textContent = panel.hidden ? 'Show debug' : 'Hide debug';
12
- toggle.setAttribute('aria-expanded', String(!panel.hidden));
13
- lastRender = 0;
14
- });
15
- }
16
-
17
- const point = (x: number, y: number): string => `(${x.toFixed(1)}, ${y.toFixed(1)})`;
18
-
19
- export function updateDebugPanel(simulation: GameSimulation): void {
20
- // Keep a fresh snapshot while hidden, so opening the panel after a capture
21
- // immediately shows the last live state instead of a placeholder.
22
- if (!panel || !content || performance.now() - lastRender < 120) return;
23
- lastRender = performance.now();
24
- const debug = simulation.enemyDebugState;
25
- const distance = Math.hypot(simulation.player.x - simulation.enemy.x, simulation.player.y - simulation.enemy.y);
26
- const fields = [
27
- ['Player', simulation.playerControllerName],
28
- ['Player direction', simulation.playerDirection],
29
- ['Enemy controller', simulation.enemyControllerName],
30
- ['Time remaining', `${Math.max(0, simulation.timeRemaining).toFixed(2)} s`],
31
- ['Direct distance', `${distance.toFixed(1)} px`],
32
- ['Player position', point(simulation.player.x, simulation.player.y)],
33
- ['Player velocity', point(simulation.playerVelocity.x, simulation.playerVelocity.y)],
34
- ['Enemy position', point(simulation.enemy.x, simulation.enemy.y)],
35
- ['Enemy direction', simulation.enemyDirection],
36
- ['Pellets / orbs', `${simulation.pellets.length} / ${simulation.powerOrbs.length}`],
37
- ['Surge remaining', `${simulation.surgeRemaining.toFixed(2)} s`],
38
- ...Object.entries(debug).map(([label, value]) => [label, String(value)]),
39
- ];
40
- content.textContent = fields.map(([label, value]) => `${label.padEnd(18)} ${value}`).join('\n');
41
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/game/PlayScene.ts DELETED
@@ -1,174 +0,0 @@
1
- import Phaser from 'phaser';
2
- import { GameSimulation, directions } from './simulation.js';
3
- import { Point } from './arena.js';
4
- import { ArcadeAudio } from './audio.js';
5
- import { updateDebugPanel } from '../debugPanel.js';
6
- import { SarsaPlayerController } from './SarsaPlayerController.js';
7
-
8
- const TRAINED_PLAYER_URL = '/models/linear-sarsa-20260815-030554-ep8000.json';
9
-
10
- const palette = { background: 0x071426, arena: 0x0b2941, outline: 0x84dcff, wall: 0x2b86bb, wallEdge: 0x82dcff, pellet: 0xf5dc83, player: 0xffda45, enemy: 0xfa637d };
11
- type TrailDot = Point & { radius: number; alpha: number; color: number };
12
- type Spark = Point & { vx: number; vy: number; life: number; color: number };
13
-
14
- export class PlayScene extends Phaser.Scene {
15
- private simulation = new GameSimulation();
16
- private arenaGraphics!: Phaser.GameObjects.Graphics;
17
- private actorGraphics!: Phaser.GameObjects.Graphics;
18
- private hud!: Phaser.GameObjects.Text;
19
- private message!: Phaser.GameObjects.Text;
20
- private status!: Phaser.GameObjects.Text;
21
- private keys!: Record<'up' | 'down' | 'left' | 'right' | 'w' | 'a' | 's' | 'd', Phaser.Input.Keyboard.Key>;
22
- private touchDirection: Point = { x: 0, y: 0 };
23
- private finished = false;
24
- private playerTrail: TrailDot[] = [];
25
- private enemyTrail: TrailDot[] = [];
26
- private sparks: Spark[] = [];
27
- private audio = new ArcadeAudio();
28
- private nextDangerSound = 0;
29
- private trainedPlayer: SarsaPlayerController | null = null;
30
- private trainedEnabled = false;
31
-
32
- constructor() { super('play'); }
33
-
34
- create(): void {
35
- this.arenaGraphics = this.add.graphics();
36
- this.actorGraphics = this.add.graphics();
37
- this.hud = this.add.text(28, 24, '', { fontFamily: 'system-ui', fontSize: '18px', color: '#e8f5ff' });
38
- this.add.text(28, 52, 'Collect every pellet. Avoid the red enemy.', { fontFamily: 'system-ui', fontSize: '14px', color: '#a6c8da' });
39
- this.status = this.add.text(400, 52, '', { fontFamily: 'system-ui', fontSize: '14px', color: '#ffd971' }).setOrigin(0.5, 0);
40
- this.message = this.add.text(400, 318, '', { fontFamily: 'system-ui', fontSize: '34px', fontStyle: 'bold', color: '#ffffff', align: 'center' }).setOrigin(0.5).setDepth(3);
41
- this.keys = this.input.keyboard!.addKeys({ up: 'UP', down: 'DOWN', left: 'LEFT', right: 'RIGHT', w: 'W', a: 'A', s: 'S', d: 'D' }) as Record<'up' | 'down' | 'left' | 'right' | 'w' | 'a' | 's' | 'd', Phaser.Input.Keyboard.Key>;
42
- this.input.keyboard!.on('keydown', () => this.audio.unlock());
43
- this.input.keyboard!.addKey('R').on('down', () => this.restart());
44
- this.createTouchPad();
45
- this.bindPlayerToggle();
46
- this.restart();
47
- void this.loadTrainedPlayer();
48
- }
49
-
50
- update(_: number, delta: number): void {
51
- if (this.finished) return;
52
- const input = this.currentInput();
53
- const result = this.simulation.step(delta / 1000, input);
54
- for (const pellet of result.pelletsCollected) { this.burst(pellet, palette.pellet, 7); this.audio.pellet(); }
55
- for (const orb of result.powerCollected) { this.burst(orb, 0x91efff, 18); this.audio.surge(); }
56
- this.playerTrail.unshift({ ...this.simulation.player, radius: 9, alpha: 0.42, color: palette.player });
57
- this.enemyTrail.unshift({ ...this.simulation.enemy, radius: 9, alpha: 0.3, color: palette.enemy });
58
- this.playerTrail = this.playerTrail.slice(0, 12).map((dot) => ({ ...dot, alpha: dot.alpha * 0.84 }));
59
- this.enemyTrail = this.enemyTrail.slice(0, 10).map((dot) => ({ ...dot, alpha: dot.alpha * 0.82 }));
60
- this.sparks = this.sparks.map((spark) => ({ ...spark, x: spark.x + spark.vx * delta / 1000, y: spark.y + spark.vy * delta / 1000, life: spark.life - delta / 1000 })).filter((spark) => spark.life > 0);
61
- const danger = Math.hypot(this.simulation.player.x - this.simulation.enemy.x, this.simulation.player.y - this.simulation.enemy.y) < 105;
62
- if (danger && this.time.now >= this.nextDangerSound) { this.audio.danger(); this.nextDangerSound = this.time.now + 650; }
63
- if (result.captured) { this.audio.captured(); this.endRound('Captured!'); }
64
- else if (result.cleared) { this.audio.cleared(); this.endRound('Arena cleared!'); }
65
- else if (result.timedOut) { this.audio.timeout(); this.endRound('Time up!'); }
66
- this.render();
67
- }
68
-
69
- private restart(): void {
70
- this.audio.unlock(); this.finished = false; this.touchDirection = { x: 0, y: 0 }; this.playerTrail = []; this.enemyTrail = []; this.sparks = []; this.nextDangerSound = 0;
71
- this.simulation.reset(Math.floor(Math.random() * 0xffffffff));
72
- this.message.setText(''); this.render();
73
- }
74
-
75
- private endRound(text: string): void {
76
- this.finished = true;
77
- this.message.setText(`${text}\nScore ${this.simulation.score}\n\nPress R or Restart`);
78
- }
79
-
80
- private currentInput(): Point {
81
- let x = this.touchDirection.x;
82
- let y = this.touchDirection.y;
83
- if (this.keys.left.isDown || this.keys.a.isDown) x -= 1;
84
- if (this.keys.right.isDown || this.keys.d.isDown) x += 1;
85
- if (this.keys.up.isDown || this.keys.w.isDown) y -= 1;
86
- if (this.keys.down.isDown || this.keys.s.isDown) y += 1;
87
- return { x, y };
88
- }
89
-
90
- private render(): void {
91
- const { arena, pellets, powerOrbs, player, enemy, timeRemaining, score, multiplier, comboRemaining, surgeRemaining } = this.simulation;
92
- const g = this.arenaGraphics.clear();
93
- g.fillStyle(palette.arena).fillCircle(arena.center.x, arena.center.y, arena.radius);
94
- g.lineStyle(4, palette.outline).strokeCircle(arena.center.x, arena.center.y, arena.radius);
95
- g.lineStyle(1, palette.outline, 0.15).strokeCircle(arena.center.x, arena.center.y, 155);
96
- for (const bar of arena.bars) {
97
- g.lineStyle(bar.width, palette.wall).lineBetween(bar.from.x, bar.from.y, bar.to.x, bar.to.y);
98
- g.lineStyle(2, palette.wallEdge).lineBetween(bar.from.x, bar.from.y, bar.to.x, bar.to.y);
99
- }
100
- g.fillStyle(palette.wall).fillCircle(arena.center.x, arena.center.y, arena.coreRadius);
101
- g.lineStyle(2, palette.wallEdge).strokeCircle(arena.center.x, arena.center.y, arena.coreRadius);
102
- g.fillStyle(palette.pellet); for (const pellet of pellets) g.fillCircle(pellet.x, pellet.y, 4);
103
- const pulse = 9 + Math.sin(this.time.now / 120) * 2;
104
- for (const orb of powerOrbs) {
105
- g.fillStyle(0x72dfff, 0.16).fillCircle(orb.x, orb.y, pulse + 6);
106
- g.fillStyle(0x91efff).fillCircle(orb.x, orb.y, pulse);
107
- g.lineStyle(2, 0xffffff, 0.8).strokeCircle(orb.x, orb.y, pulse);
108
- }
109
- const actors = this.actorGraphics.clear();
110
- for (const dot of this.enemyTrail) actors.fillStyle(dot.color, dot.alpha).fillCircle(dot.x, dot.y, dot.radius);
111
- for (const dot of this.playerTrail) actors.fillStyle(dot.color, dot.alpha).fillCircle(dot.x, dot.y, dot.radius);
112
- for (const spark of this.sparks) actors.fillStyle(spark.color, Math.min(1, spark.life * 2)).fillCircle(spark.x, spark.y, 3);
113
- if (surgeRemaining > 0) actors.fillStyle(0x91efff, 0.22).fillCircle(player.x, player.y, player.radius + 13 + Math.sin(this.time.now / 90) * 3);
114
- actors.fillStyle(palette.player).fillCircle(player.x, player.y, player.radius);
115
- actors.lineStyle(2, 0xfff0a9).strokeCircle(player.x, player.y, player.radius);
116
- actors.fillStyle(palette.enemy).fillCircle(enemy.x, enemy.y, enemy.radius);
117
- actors.lineStyle(2, 0xffb2be).strokeCircle(enemy.x, enemy.y, enemy.radius);
118
- this.hud.setText(`Score ${score.toString().padStart(4, '0')} • Pellets ${pellets.length} • Time ${Math.max(0, Math.ceil(timeRemaining))} • Player ${this.trainedEnabled ? 'Trained' : 'Human'} • Enemy ${this.simulation.enemyControllerName}`);
119
- if (surgeRemaining > 0) this.status.setText(`SURGE ×${multiplier} · ${surgeRemaining.toFixed(1)}s`);
120
- else if (comboRemaining > 0 && multiplier > 1) this.status.setText(`STREAK ×${multiplier} · ${comboRemaining.toFixed(1)}s`);
121
- else if (Math.hypot(player.x - enemy.x, player.y - enemy.y) < 105) this.status.setText('DANGER ZONE');
122
- else this.status.setText('');
123
- updateDebugPanel(this.simulation);
124
- }
125
-
126
- private createTouchPad(): void {
127
- const positions: Record<keyof typeof directions, Point> = {
128
- 'up-left': { x: 638, y: 492 }, up: { x: 688, y: 492 }, 'up-right': { x: 738, y: 492 },
129
- left: { x: 638, y: 542 }, right: { x: 738, y: 542 },
130
- 'down-left': { x: 638, y: 592 }, down: { x: 688, y: 592 }, 'down-right': { x: 738, y: 592 },
131
- };
132
- for (const [direction, position] of Object.entries(positions) as [keyof typeof directions, Point][]) {
133
- const button = this.add.circle(position.x, position.y, 25, 0x173a55, 0.95).setStrokeStyle(1, 0x6baed0).setInteractive({ useHandCursor: true });
134
- this.add.text(position.x, position.y - 2, { up: '↑', 'up-right': '↗', right: '→', 'down-right': '↘', down: '↓', 'down-left': '↙', left: '←', 'up-left': '↖' }[direction], { fontFamily: 'system-ui', fontSize: '21px', color: '#e8f5ff' }).setOrigin(0.5);
135
- button.on('pointerdown', () => { this.audio.unlock(); this.touchDirection = directions[direction]; });
136
- button.on('pointerup', () => { this.touchDirection = { x: 0, y: 0 }; });
137
- button.on('pointerout', () => { this.touchDirection = { x: 0, y: 0 }; });
138
- }
139
- const restart = this.add.text(52, 584, 'Restart', { fontFamily: 'system-ui', fontSize: '17px', color: '#e8f5ff', backgroundColor: '#173a55', padding: { x: 12, y: 7 } }).setInteractive({ useHandCursor: true });
140
- restart.on('pointerdown', () => this.restart());
141
- const sound = this.add.text(165, 584, 'Sound: On', { fontFamily: 'system-ui', fontSize: '17px', color: '#e8f5ff', backgroundColor: '#173a55', padding: { x: 12, y: 7 } }).setInteractive({ useHandCursor: true });
142
- sound.on('pointerdown', () => { this.audio.unlock(); this.audio.enabled = !this.audio.enabled; sound.setText(`Sound: ${this.audio.enabled ? 'On' : 'Off'}`); });
143
- }
144
-
145
- private bindPlayerToggle(): void {
146
- document.getElementById('player-toggle')?.addEventListener('click', () => this.toggleTrainedPlayer());
147
- }
148
-
149
- private async loadTrainedPlayer(): Promise<void> {
150
- try {
151
- this.trainedPlayer = await SarsaPlayerController.load(TRAINED_PLAYER_URL);
152
- const toggle = document.getElementById('player-toggle');
153
- if (toggle instanceof HTMLButtonElement) toggle.disabled = false;
154
- } catch (error) {
155
- console.error('Could not load the trained player.', error);
156
- }
157
- }
158
-
159
- private toggleTrainedPlayer(): void {
160
- if (this.trainedPlayer === null) return;
161
- this.trainedEnabled = !this.trainedEnabled;
162
- this.simulation.setPlayerController(this.trainedEnabled ? this.trainedPlayer : null);
163
- const toggle = document.getElementById('player-toggle');
164
- if (toggle) toggle.textContent = this.trainedEnabled ? 'Player: Trained' : 'Player: Human';
165
- }
166
-
167
- private burst(point: Point, color: number, amount: number): void {
168
- for (let index = 0; index < amount; index += 1) {
169
- const angle = Phaser.Math.FloatBetween(0, Math.PI * 2);
170
- const speed = Phaser.Math.Between(45, 150);
171
- this.sparks.push({ x: point.x, y: point.y, vx: Math.cos(angle) * speed, vy: Math.sin(angle) * speed, life: Phaser.Math.FloatBetween(0.35, 0.8), color });
172
- }
173
- }
174
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/game/SarsaPlayerController.ts DELETED
@@ -1,327 +0,0 @@
1
- import {
2
- Direction,
3
- GameSimulation,
4
- PLAYER_OBSERVATION_SIZE,
5
- PlayerController,
6
- PlayerObservation,
7
- actionOrder,
8
- } from './simulation.js';
9
-
10
- export const BROWSER_LINEAR_SARSA_FORMAT = 'orbit-chase-linear-sarsa-browser-v1';
11
-
12
- const GROUP_PLAYER_POSITION = 1;
13
- const GROUP_PLAYER_VELOCITY = 2;
14
- const GROUP_ENEMY_RELATIVE = 3;
15
- const GROUP_ENEMY_CLOCK = 4;
16
- const GROUP_PELLET_RANK = 10;
17
- const GROUP_ORB_RANK = 20;
18
- const GROUP_BAR = 30;
19
- const GROUP_SURGE_TIME = 40;
20
- const GROUP_ENEMY_HEADING = 50;
21
- const GROUP_ACTION_MASK = 60;
22
- const NEAREST_PELLET_COUNT = 4;
23
- const PELLET_COUNT = 32;
24
- const SURGE_ORB_COUNT = 3;
25
- const OCCUPANCY_SALT = -1;
26
- const FNV_OFFSET = 0xCBF29CE484222325n;
27
- const FNV_PRIME = 0x100000001B3n;
28
- const UINT64_MASK = 0xFFFFFFFFFFFFFFFFn;
29
-
30
- type EncoderConfig = { capacity: number; bins: number; tilings: number };
31
- type WeightRow = { indices: number[]; values: number[] };
32
-
33
- export type BrowserSarsaArtifact = {
34
- format: string;
35
- source_format?: string;
36
- checkpoint?: string;
37
- observation_size: number;
38
- action_order: string[];
39
- encoder: EncoderConfig;
40
- weights: WeightRow[];
41
- };
42
-
43
- /** Sparse hashed tile encoder matching `orbit_chase.sarsa.FeatureEncoder`. */
44
- export class FeatureEncoder {
45
- constructor(
46
- readonly capacity: number,
47
- readonly bins: number,
48
- readonly tilings: number,
49
- ) {}
50
-
51
- encode(observation: readonly number[], actionMask: readonly boolean[]): number[] {
52
- if (observation.length !== PLAYER_OBSERVATION_SIZE) {
53
- throw new Error(`Expected observation length ${PLAYER_OBSERVATION_SIZE}, got ${observation.length}.`);
54
- }
55
- if (actionMask.length !== actionOrder.length) {
56
- throw new Error(`Expected action mask length ${actionOrder.length}, got ${actionMask.length}.`);
57
- }
58
- if (actionMask.some((value) => value !== true && value !== false)) {
59
- throw new Error('Action mask values must be binary.');
60
- }
61
- if (!actionMask.some(Boolean)) {
62
- throw new Error('Action mask must contain at least one valid action.');
63
- }
64
-
65
- const features = new Set<number>();
66
- const player = [observation[0], observation[1]];
67
- this.addTiles(features, GROUP_PLAYER_POSITION, player, -2, 2);
68
- this.addTiles(features, GROUP_PLAYER_VELOCITY, [observation[2], observation[3]], -1, 1);
69
- this.addTiles(features, GROUP_ENEMY_RELATIVE, [observation[5] - player[0], observation[6] - player[1]], -2, 2);
70
- this.addTiles(features, GROUP_ENEMY_CLOCK, [observation[15]], 0, 1);
71
- this.addTiles(features, GROUP_SURGE_TIME, [observation[4], observation[129]], 0, 1);
72
- this.addActiveDirectionFeatures(features, GROUP_ENEMY_HEADING, observation.slice(7, 15));
73
- this.addActiveDirectionFeatures(features, GROUP_ACTION_MASK, actionMask.map((valid) => valid ? 1 : 0));
74
- this.addNearestCollectibles(features, observation.slice(24, 120), PELLET_COUNT, NEAREST_PELLET_COUNT, player, GROUP_PELLET_RANK);
75
- this.addNearestCollectibles(features, observation.slice(120, 129), SURGE_ORB_COUNT, SURGE_ORB_COUNT, player, GROUP_ORB_RANK);
76
- this.addBarFeatures(features, observation.slice(16, 24), player);
77
- return [...features].sort((left, right) => left - right);
78
- }
79
-
80
- private addTiles(features: Set<number>, group: number, values: readonly number[], lower: number, upper: number): void {
81
- const normalized = values.map((value) => (clip(value, lower, upper) - lower) / (upper - lower));
82
- for (let tiling = 0; tiling < this.tilings; tiling += 1) {
83
- const offset = tiling / this.tilings;
84
- const coordinates = normalized.map((value) => clip(Math.floor(value * this.bins + offset), 0, this.bins - 1));
85
- features.add(this.hash(group, tiling, ...coordinates));
86
- }
87
- }
88
-
89
- private addActiveDirectionFeatures(features: Set<number>, group: number, values: readonly number[]): void {
90
- values.forEach((value, direction) => {
91
- if (value > 0) features.add(this.hash(group, direction));
92
- });
93
- }
94
-
95
- private addNearestCollectibles(
96
- features: Set<number>,
97
- encoded: readonly number[],
98
- count: number,
99
- nearestCount: number,
100
- player: readonly number[],
101
- group: number,
102
- ): void {
103
- const active: number[][] = [];
104
- for (let index = 0; index < count; index += 1) {
105
- const offset = index * 3;
106
- if (encoded[offset + 2] > 0.5) active.push([encoded[offset], encoded[offset + 1]]);
107
- }
108
- active.sort((left, right) => comparePoints(left, right, player));
109
- for (let rank = 0; rank < nearestCount; rank += 1) {
110
- const groupId = group + rank;
111
- const occupied = rank < active.length;
112
- features.add(this.hash(groupId, OCCUPANCY_SALT, occupied ? 1 : 0));
113
- if (occupied) {
114
- this.addTiles(features, groupId, [active[rank][0] - player[0], active[rank][1] - player[1]], -2, 2);
115
- }
116
- }
117
- }
118
-
119
- private addBarFeatures(features: Set<number>, encoded: readonly number[], player: readonly number[]): void {
120
- const bars: Array<[number[], number[]]> = [];
121
- for (let index = 0; index < 2; index += 1) {
122
- const offset = index * 4;
123
- let start = [encoded[offset], encoded[offset + 1]];
124
- let end = [encoded[offset + 2], encoded[offset + 3]];
125
- if (lexLess(end, start)) [start, end] = [end, start];
126
- bars.push([start, end]);
127
- }
128
- bars.sort((left, right) => compareBars(left, right));
129
- bars.forEach(([start, end], index) => {
130
- const closest = closestPoint(start, end, player);
131
- this.addTiles(features, GROUP_BAR + index, [closest[0] - player[0], closest[1] - player[1]], -2, 2);
132
- });
133
- }
134
-
135
- private hash(...components: number[]): number {
136
- let value = FNV_OFFSET;
137
- const bytes = new Uint8Array(8);
138
- const view = new DataView(bytes.buffer);
139
- for (const component of components) {
140
- view.setBigInt64(0, BigInt(component), true);
141
- for (let index = 0; index < 8; index += 1) {
142
- value ^= BigInt(bytes[index]);
143
- value = (value * FNV_PRIME) & UINT64_MASK;
144
- }
145
- }
146
- return Number(value % BigInt(this.capacity));
147
- }
148
- }
149
-
150
- /** Browser inference for a frozen linear Sarsa player checkpoint. */
151
- export class SarsaPlayerController implements PlayerController {
152
- readonly name = 'linear-sarsa';
153
- private readonly lookups: ReadonlyArray<ReadonlyMap<number, number>>;
154
-
155
- private constructor(
156
- private readonly encoder: FeatureEncoder,
157
- lookups: ReadonlyArray<ReadonlyMap<number, number>>,
158
- ) {
159
- this.lookups = lookups;
160
- }
161
-
162
- static fromArtifact(artifact: unknown): SarsaPlayerController {
163
- const document = validateArtifact(artifact);
164
- const encoder = new FeatureEncoder(document.encoder.capacity, document.encoder.bins, document.encoder.tilings);
165
- const lookups = document.weights.map((row) => {
166
- const lookup = new Map<number, number>();
167
- row.indices.forEach((index, offset) => lookup.set(index, row.values[offset]));
168
- return lookup;
169
- });
170
- return new SarsaPlayerController(encoder, lookups);
171
- }
172
-
173
- static async load(url: string): Promise<SarsaPlayerController> {
174
- const response = await fetch(url);
175
- if (!response.ok) throw new Error(`Could not load linear Sarsa artifact (${response.status}).`);
176
- return SarsaPlayerController.fromArtifact(await response.json());
177
- }
178
-
179
- encode(observation: PlayerObservation): number[] {
180
- return this.encoder.encode(observation.features, observation.actionMask);
181
- }
182
-
183
- qValues(indices: readonly number[]): number[] {
184
- return this.lookups.map((lookup) => {
185
- let total = 0;
186
- for (const index of indices) total += lookup.get(index) ?? 0;
187
- return total;
188
- });
189
- }
190
-
191
- choose(observation: PlayerObservation): Direction {
192
- const indices = this.encode(observation);
193
- return actionOrder[selectGreedyAction(this.qValues(indices), observation.actionMask)];
194
- }
195
-
196
- selectAction(simulation: GameSimulation): Direction {
197
- return this.choose(simulation.observe());
198
- }
199
- }
200
-
201
- /** Masked greedy action: highest valid Q, lowest index on exact ties. */
202
- export function selectGreedyAction(qValues: readonly number[], actionMask: readonly boolean[]): number {
203
- if (qValues.length !== actionOrder.length || actionMask.length !== actionOrder.length) {
204
- throw new Error(`Expected ${actionOrder.length} Q-values and mask entries.`);
205
- }
206
- let bestIndex = -1;
207
- let bestValue = -Infinity;
208
- for (let index = 0; index < actionOrder.length; index += 1) {
209
- if (!actionMask[index]) continue;
210
- if (bestIndex < 0 || qValues[index] > bestValue) {
211
- bestIndex = index;
212
- bestValue = qValues[index];
213
- }
214
- }
215
- if (bestIndex < 0) throw new Error('Action mask must contain at least one valid action.');
216
- return bestIndex;
217
- }
218
-
219
- function validateArtifact(artifact: unknown): BrowserSarsaArtifact {
220
- if (!isRecord(artifact)) throw new Error('Linear Sarsa artifact must be an object.');
221
- if (artifact.format !== BROWSER_LINEAR_SARSA_FORMAT) {
222
- throw new Error(`Unsupported browser artifact format: ${String(artifact.format)}.`);
223
- }
224
- if (artifact.observation_size !== PLAYER_OBSERVATION_SIZE) {
225
- throw new Error(`Browser artifact observation_size must be ${PLAYER_OBSERVATION_SIZE}.`);
226
- }
227
- if (!Array.isArray(artifact.action_order) || artifact.action_order.join(',') !== actionOrder.join(',')) {
228
- throw new Error('Browser artifact action_order does not match the game contract.');
229
- }
230
- const encoder = artifact.encoder;
231
- if (!isRecord(encoder) || !isPositiveInt(encoder.capacity) || !isPositiveInt(encoder.bins) || !isPositiveInt(encoder.tilings)) {
232
- throw new Error('Browser artifact encoder metadata is invalid.');
233
- }
234
- const capacity = encoder.capacity;
235
- const bins = encoder.bins;
236
- const tilings = encoder.tilings;
237
- if (!Array.isArray(artifact.weights) || artifact.weights.length !== actionOrder.length) {
238
- throw new Error(`Browser artifact must contain ${actionOrder.length} weight rows.`);
239
- }
240
- const rows = artifact.weights.map((row, action) => validateWeightRow(row, capacity, action));
241
- return {
242
- format: BROWSER_LINEAR_SARSA_FORMAT,
243
- source_format: typeof artifact.source_format === 'string' ? artifact.source_format : undefined,
244
- checkpoint: typeof artifact.checkpoint === 'string' ? artifact.checkpoint : undefined,
245
- observation_size: PLAYER_OBSERVATION_SIZE,
246
- action_order: [...actionOrder],
247
- encoder: { capacity, bins, tilings },
248
- weights: rows,
249
- };
250
- }
251
-
252
- function validateWeightRow(row: unknown, capacity: number, action: number): WeightRow {
253
- if (!isRecord(row) || !Array.isArray(row.indices) || !Array.isArray(row.values)) {
254
- throw new Error(`Weight row ${action} must contain indices and values arrays.`);
255
- }
256
- const rawIndices = row.indices;
257
- const rawValues = row.values;
258
- if (rawIndices.length !== rawValues.length) {
259
- throw new Error(`Weight row ${action} indices and values must have matching lengths.`);
260
- }
261
- const indices: number[] = [];
262
- const values: number[] = [];
263
- rawIndices.forEach((index, offset) => {
264
- if (typeof index !== 'number' || !Number.isInteger(index) || index < 0 || index >= capacity) {
265
- throw new Error(`Weight row ${action} has an index outside [0, ${capacity}).`);
266
- }
267
- if (offset > 0 && !(index > indices[offset - 1])) {
268
- throw new Error(`Weight row ${action} indices must be strictly increasing.`);
269
- }
270
- const value = rawValues[offset];
271
- if (typeof value !== 'number' || !Number.isFinite(value)) {
272
- throw new Error(`Weight row ${action} values must be finite numbers.`);
273
- }
274
- indices.push(index);
275
- values.push(value);
276
- });
277
- return { indices, values };
278
- }
279
-
280
- function isRecord(value: unknown): value is Record<string, unknown> {
281
- return typeof value === 'object' && value !== null && !Array.isArray(value);
282
- }
283
-
284
- function isPositiveInt(value: unknown): value is number {
285
- return typeof value === 'number' && Number.isInteger(value) && value > 0;
286
- }
287
-
288
- function clip(value: number, lower: number, upper: number): number {
289
- return Math.min(upper, Math.max(lower, value));
290
- }
291
-
292
- function squaredDistance(point: readonly number[], player: readonly number[]): number {
293
- const dx = point[0] - player[0];
294
- const dy = point[1] - player[1];
295
- return dx * dx + dy * dy;
296
- }
297
-
298
- function comparePoints(left: readonly number[], right: readonly number[], player: readonly number[]): number {
299
- const distance = squaredDistance(left, player) - squaredDistance(right, player);
300
- if (distance !== 0) return distance;
301
- if (left[0] !== right[0]) return left[0] < right[0] ? -1 : 1;
302
- if (left[1] !== right[1]) return left[1] < right[1] ? -1 : 1;
303
- return 0;
304
- }
305
-
306
- function lexLess(left: readonly number[], right: readonly number[]): boolean {
307
- if (left[0] !== right[0]) return left[0] < right[0];
308
- return left[1] < right[1];
309
- }
310
-
311
- function compareBars(left: [number[], number[]], right: [number[], number[]]): number {
312
- const keysLeft = [...left[0], ...left[1]];
313
- const keysRight = [...right[0], ...right[1]];
314
- for (let index = 0; index < keysLeft.length; index += 1) {
315
- if (keysLeft[index] !== keysRight[index]) return keysLeft[index] < keysRight[index] ? -1 : 1;
316
- }
317
- return 0;
318
- }
319
-
320
- function closestPoint(start: readonly number[], end: readonly number[], point: readonly number[]): number[] {
321
- const sx = end[0] - start[0];
322
- const sy = end[1] - start[1];
323
- const lengthSquared = sx * sx + sy * sy;
324
- if (lengthSquared === 0) return [start[0], start[1]];
325
- const fraction = clip(((point[0] - start[0]) * sx + (point[1] - start[1]) * sy) / lengthSquared, 0, 1);
326
- return [start[0] + fraction * sx, start[1] + fraction * sy];
327
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/game/arena.ts DELETED
@@ -1,58 +0,0 @@
1
- export type Point = { x: number; y: number };
2
- export type Segment = { from: Point; to: Point; width: number };
3
-
4
- export type Arena = {
5
- center: Point;
6
- radius: number;
7
- coreRadius: number;
8
- bars: Segment[];
9
- };
10
-
11
- export function seededRandom(seed: number): () => number {
12
- let value = seed >>> 0;
13
- return () => {
14
- value += 0x6d2b79f5;
15
- let result = value;
16
- result = Math.imul(result ^ (result >>> 15), result | 1);
17
- result ^= result + Math.imul(result ^ (result >>> 7), result | 61);
18
- return ((result ^ (result >>> 14)) >>> 0) / 4294967296;
19
- };
20
- }
21
-
22
- export function makeArena(seed: number): Arena {
23
- const random = seededRandom(seed);
24
- const center = { x: 400, y: 322 };
25
- const angle = random() * Math.PI * 2;
26
- const unit = { x: Math.cos(angle), y: Math.sin(angle) };
27
- const perpendicular = { x: -unit.y, y: unit.x };
28
- const makeBar = (sign: number): Segment => {
29
- const offset = 92 * sign;
30
- const length = 112 + random() * 38;
31
- return {
32
- from: {
33
- x: center.x + unit.x * offset - perpendicular.x * length / 2,
34
- y: center.y + unit.y * offset - perpendicular.y * length / 2,
35
- },
36
- to: {
37
- x: center.x + unit.x * offset + perpendicular.x * length / 2,
38
- y: center.y + unit.y * offset + perpendicular.y * length / 2,
39
- },
40
- width: 18,
41
- };
42
- };
43
- return { center, radius: 242, coreRadius: 36, bars: [makeBar(-1), makeBar(1)] };
44
- }
45
-
46
- export function pointSegmentDistance(point: Point, segment: Segment): number {
47
- const dx = segment.to.x - segment.from.x;
48
- const dy = segment.to.y - segment.from.y;
49
- const denominator = dx * dx + dy * dy;
50
- const t = denominator === 0 ? 0 : Math.max(0, Math.min(1, ((point.x - segment.from.x) * dx + (point.y - segment.from.y) * dy) / denominator));
51
- return Math.hypot(point.x - (segment.from.x + t * dx), point.y - (segment.from.y + t * dy));
52
- }
53
-
54
- export function isBlocked(arena: Arena, point: Point, bodyRadius: number): boolean {
55
- if (Math.hypot(point.x - arena.center.x, point.y - arena.center.y) > arena.radius - bodyRadius) return true;
56
- if (Math.hypot(point.x - arena.center.x, point.y - arena.center.y) < arena.coreRadius + bodyRadius) return true;
57
- return arena.bars.some((bar) => pointSegmentDistance(point, bar) < bar.width / 2 + bodyRadius);
58
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/game/audio.ts DELETED
@@ -1,32 +0,0 @@
1
- export class ArcadeAudio {
2
- private context?: AudioContext;
3
- enabled = true;
4
-
5
- unlock(): void {
6
- this.context ??= new AudioContext();
7
- if (this.context.state === 'suspended') void this.context.resume();
8
- }
9
-
10
- pellet(): void { this.tone(700, 0.055, 'square', 0.045, 980); }
11
- surge(): void { this.tone(310, 0.11, 'triangle', 0.07, 620); window.setTimeout(() => this.tone(780, 0.14, 'triangle', 0.06, 1180), 75); }
12
- danger(): void { this.tone(150, 0.06, 'sine', 0.025, 135); }
13
- captured(): void { this.tone(260, 0.35, 'sawtooth', 0.09, 65); }
14
- cleared(): void { this.tone(420, 0.1, 'triangle', 0.055, 620); window.setTimeout(() => this.tone(620, 0.18, 'triangle', 0.055, 980), 105); }
15
- timeout(): void { this.tone(240, 0.16, 'sine', 0.05, 180); }
16
-
17
- private tone(start: number, duration: number, type: OscillatorType, gain: number, end: number): void {
18
- if (!this.enabled || !this.context || this.context.state !== 'running') return;
19
- const now = this.context.currentTime;
20
- const oscillator = this.context.createOscillator();
21
- const envelope = this.context.createGain();
22
- oscillator.type = type;
23
- oscillator.frequency.setValueAtTime(start, now);
24
- oscillator.frequency.exponentialRampToValueAtTime(Math.max(1, end), now + duration);
25
- envelope.gain.setValueAtTime(0.0001, now);
26
- envelope.gain.exponentialRampToValueAtTime(gain, now + 0.012);
27
- envelope.gain.exponentialRampToValueAtTime(0.0001, now + duration);
28
- oscillator.connect(envelope).connect(this.context.destination);
29
- oscillator.start(now);
30
- oscillator.stop(now + duration + 0.02);
31
- }
32
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/game/simulation.ts DELETED
@@ -1,252 +0,0 @@
1
- import { Arena, Point, isBlocked, makeArena, seededRandom } from './arena.js';
2
-
3
- export type Direction = 'up' | 'up-right' | 'right' | 'down-right' | 'down' | 'down-left' | 'left' | 'up-left';
4
- export const directions: Record<Direction, Point> = {
5
- up: { x: 0, y: -1 }, 'up-right': { x: 1, y: -1 }, right: { x: 1, y: 0 }, 'down-right': { x: 1, y: 1 },
6
- down: { x: 0, y: 1 }, 'down-left': { x: -1, y: 1 }, left: { x: -1, y: 0 }, 'up-left': { x: -1, y: -1 },
7
- };
8
-
9
- export const actionOrder = Object.keys(directions) as Direction[];
10
- export const FIXED_TIMESTEP = 0.01;
11
- export const PLAYER_DECISION_INTERVAL = 0.1;
12
- export const ENEMY_DECISION_INTERVAL = 0.28;
13
- export const PLAYER_OBSERVATION_SIZE = 130;
14
-
15
- export type Actor = Point & { radius: number; speed: number };
16
- export type CollectibleSlot = { point: Point; active: boolean };
17
- export type StepResult = { captured: boolean; cleared: boolean; timedOut: boolean; pelletsCollected: Point[]; powerCollected: Point[] };
18
- export type EnemyDebugState = Record<string, string | number | boolean>;
19
- export interface EnemyController { readonly name: string; selectAction(simulation: GameSimulation): Direction; getDebugState?(): EnemyDebugState; }
20
- export interface PlayerController { readonly name: string; selectAction(simulation: GameSimulation): Direction; }
21
-
22
- /** Exact, fixed-order input supplied to the player policy. */
23
- export type PlayerObservation = { features: number[]; actionMask: boolean[] };
24
-
25
- const emptyResult = (): StepResult => ({ captured: false, cleared: false, timedOut: false, pelletsCollected: [], powerCollected: [] });
26
-
27
- /**
28
- * Deterministic game rules shared by the browser UI and future headless agents.
29
- * `step()` accepts browser elapsed time but advances only in fixed 10 ms ticks;
30
- * training code should call `stepFixed()` directly.
31
- */
32
- export class GameSimulation {
33
- arena: Arena;
34
- player: Actor;
35
- enemy: Actor;
36
- pellets: Point[] = [];
37
- powerOrbs: Point[] = [];
38
- pelletSlots: CollectibleSlot[] = [];
39
- orbSlots: CollectibleSlot[] = [];
40
- playerVelocity: Point = { x: 0, y: 0 };
41
- timeRemaining = 60;
42
- score = 0;
43
- multiplier = 1;
44
- comboRemaining = 0;
45
- surgeRemaining = 0;
46
- enemyDirection: Direction = 'left';
47
- playerDirection: Direction = 'up';
48
- private enemyDecisionRemaining = 0;
49
- private playerDecisionTicksRemaining = 0;
50
- private elapsedAccumulator = 0;
51
- private enemyController: EnemyController | null = null;
52
- private playerController: PlayerController | null = null;
53
-
54
- constructor(seed = Date.now()) {
55
- this.arena = makeArena(seed);
56
- this.player = { x: 0, y: 0, radius: 12, speed: 175 };
57
- this.enemy = { x: 0, y: 0, radius: 13, speed: 110 };
58
- this.reset(seed);
59
- }
60
-
61
- /** Reset every procedural element from `seed`, making episode starts reproducible. */
62
- reset(seed = Date.now()): void {
63
- this.arena = makeArena(seed);
64
- const { center } = this.arena;
65
- this.player = { x: center.x - 165, y: center.y + 85, radius: 12, speed: 175 };
66
- this.enemy = { x: center.x + 165, y: center.y - 85, radius: 13, speed: 110 };
67
- this.enemyDirection = 'left';
68
- this.playerDirection = 'up';
69
- this.enemyDecisionRemaining = 0;
70
- this.playerDecisionTicksRemaining = 0;
71
- this.elapsedAccumulator = 0;
72
- this.timeRemaining = 60;
73
- this.pellets = []; this.powerOrbs = []; this.pelletSlots = []; this.orbSlots = [];
74
- this.playerVelocity = { x: 0, y: 0 };
75
- this.score = 0; this.multiplier = 1; this.comboRemaining = 0; this.surgeRemaining = 0;
76
- const random = seededRandom(seed ^ 0xa5a5a5a5);
77
- while (this.pellets.length < 32) {
78
- const angle = random() * Math.PI * 2;
79
- const distance = 52 + random() * 158;
80
- const candidate = { x: center.x + Math.cos(angle) * distance, y: center.y + Math.sin(angle) * distance };
81
- if (!isBlocked(this.arena, candidate, 8) && Math.hypot(candidate.x - this.player.x, candidate.y - this.player.y) > 32 && Math.hypot(candidate.x - this.enemy.x, candidate.y - this.enemy.y) > 32) {
82
- this.pellets.push(candidate); this.pelletSlots.push({ point: candidate, active: true });
83
- }
84
- }
85
- while (this.powerOrbs.length < 3) {
86
- const angle = random() * Math.PI * 2;
87
- const distance = 65 + random() * 125;
88
- const candidate = { x: center.x + Math.cos(angle) * distance, y: center.y + Math.sin(angle) * distance };
89
- if (!isBlocked(this.arena, candidate, 13) && this.pellets.every((pellet) => Math.hypot(pellet.x - candidate.x, pellet.y - candidate.y) > 34)) {
90
- this.powerOrbs.push(candidate); this.orbSlots.push({ point: candidate, active: true });
91
- }
92
- }
93
- }
94
-
95
- setEnemyController(controller: EnemyController | null): void { this.enemyController = controller; }
96
- setPlayerController(controller: PlayerController | null): void {
97
- this.playerController = controller;
98
- this.playerDecisionTicksRemaining = 0;
99
- }
100
- get enemyControllerName(): string { return this.enemyController?.name ?? 'Greedy'; }
101
- get playerControllerName(): string { return this.playerController?.name ?? 'Human'; }
102
- get enemyDebugState(): EnemyDebugState { return this.enemyController?.getDebugState?.() ?? { Navigation: 'Built-in greedy pursuit' }; }
103
- get enemyDecisionFraction(): number { return Math.max(0, this.enemyDecisionRemaining) / ENEMY_DECISION_INTERVAL; }
104
-
105
- /** Advance browser time in fixed ticks, preserving sub-tick remainder between render frames. */
106
- step(dt: number, playerInput: Point): StepResult {
107
- this.elapsedAccumulator += Math.min(Math.max(0, dt), 0.25);
108
- const result = emptyResult();
109
- while (this.elapsedAccumulator + 1e-9 >= FIXED_TIMESTEP && !result.captured && !result.cleared && !result.timedOut) {
110
- this.elapsedAccumulator -= FIXED_TIMESTEP;
111
- this.mergeResult(result, this.stepFixed(playerInput));
112
- }
113
- return result;
114
- }
115
-
116
- /** Advance precisely one physics tick. This is the headless-training contract. */
117
- stepFixed(playerInput: Point): StepResult {
118
- const dt = FIXED_TIMESTEP;
119
- this.comboRemaining = Math.max(0, this.comboRemaining - dt);
120
- if (this.comboRemaining === 0) this.multiplier = 1;
121
- this.surgeRemaining = Math.max(0, this.surgeRemaining - dt);
122
- const playerMultiplier = this.surgeRemaining > 0 ? 1.32 : 1;
123
- let movement = playerInput;
124
- if (this.playerController !== null) {
125
- if (this.playerDecisionTicksRemaining <= 0) {
126
- this.playerDirection = this.playerController.selectAction(this);
127
- this.playerDecisionTicksRemaining = Math.round(PLAYER_DECISION_INTERVAL / FIXED_TIMESTEP);
128
- }
129
- movement = directions[this.playerDirection];
130
- }
131
- this.move(this.player, movement, dt, playerMultiplier);
132
- this.playerVelocity = this.velocityFor(movement, this.player.speed * playerMultiplier);
133
- if (this.playerController !== null) this.playerDecisionTicksRemaining -= 1;
134
-
135
- if (this.enemyDecisionRemaining <= 0) {
136
- this.enemyDirection = this.enemyController?.selectAction(this) ?? this.chooseBaselineAction();
137
- this.enemyDecisionRemaining = ENEMY_DECISION_INTERVAL;
138
- }
139
- this.move(this.enemy, directions[this.enemyDirection], dt, this.surgeRemaining > 0 ? 0.78 : 1);
140
- this.enemyDecisionRemaining -= dt;
141
-
142
- const pelletsCollected = this.pelletSlots.filter((slot) => slot.active && Math.hypot(slot.point.x - this.player.x, slot.point.y - this.player.y) <= 18).map((slot) => slot.point);
143
- if (pelletsCollected.length) {
144
- this.score += 10 * this.multiplier * pelletsCollected.length;
145
- this.multiplier = Math.min(5, this.multiplier + pelletsCollected.length);
146
- this.comboRemaining = 2.4;
147
- for (const slot of this.pelletSlots) if (pelletsCollected.includes(slot.point)) slot.active = false;
148
- this.pellets = this.pelletSlots.filter((slot) => slot.active).map((slot) => slot.point);
149
- }
150
- const powerCollected = this.orbSlots.filter((slot) => slot.active && Math.hypot(slot.point.x - this.player.x, slot.point.y - this.player.y) <= 22).map((slot) => slot.point);
151
- if (powerCollected.length) {
152
- this.score += 75 * this.multiplier * powerCollected.length;
153
- this.surgeRemaining = 4;
154
- for (const slot of this.orbSlots) if (powerCollected.includes(slot.point)) slot.active = false;
155
- this.powerOrbs = this.orbSlots.filter((slot) => slot.active).map((slot) => slot.point);
156
- }
157
- this.timeRemaining -= dt;
158
- return { captured: Math.hypot(this.player.x - this.enemy.x, this.player.y - this.enemy.y) < this.player.radius + this.enemy.radius, cleared: this.pellets.length === 0, timedOut: this.timeRemaining <= 0, pelletsCollected, powerCollected };
159
- }
160
-
161
- /** Vector observation and swept-path action mask for a player policy. */
162
- observe(): PlayerObservation {
163
- const normalized = (point: Point): [number, number] => [(point.x - this.arena.center.x) / this.arena.radius, (point.y - this.arena.center.y) / this.arena.radius];
164
- const features: number[] = [
165
- ...normalized(this.player), this.playerVelocity.x / this.player.speed, this.playerVelocity.y / this.player.speed, this.surgeRemaining / 4,
166
- ...normalized(this.enemy), ...actionOrder.map((direction) => direction === this.enemyDirection ? 1 : 0), this.enemyDecisionFraction,
167
- ];
168
- for (const bar of this.arena.bars) features.push(...normalized(bar.from), ...normalized(bar.to));
169
- for (const slot of this.pelletSlots) features.push(...normalized(slot.point), slot.active ? 1 : 0);
170
- for (const slot of this.orbSlots) features.push(...normalized(slot.point), slot.active ? 1 : 0);
171
- features.push(Math.max(0, this.timeRemaining) / 60);
172
- if (features.length !== PLAYER_OBSERVATION_SIZE) throw new Error(`Player observation mismatch: expected ${PLAYER_OBSERVATION_SIZE}, got ${features.length}.`);
173
- return { features, actionMask: this.validActions() };
174
- }
175
-
176
- /** Actions safe for the player's next 100 ms decision interval. */
177
- validActions(): boolean[] {
178
- return actionOrder.map((direction) => this.canCompletePlayerDecision(directions[direction]));
179
- }
180
-
181
- /** Dry-run ten ticks of player motion with mid-interval Surge expiry and Orb pickups. */
182
- private canCompletePlayerDecision(input: Point): boolean {
183
- const player: Actor = { x: this.player.x, y: this.player.y, radius: this.player.radius, speed: this.player.speed };
184
- let surge = this.surgeRemaining;
185
- const orbs = this.orbSlots.map((slot) => slot.active);
186
- const ticks = Math.round(PLAYER_DECISION_INTERVAL / FIXED_TIMESTEP);
187
- for (let tick = 0; tick < ticks; tick += 1) {
188
- surge = Math.max(0, surge - FIXED_TIMESTEP);
189
- const multiplier = surge > 0 ? 1.32 : 1;
190
- if (!this.canTravel(player, input, FIXED_TIMESTEP, multiplier)) return false;
191
- const length = Math.hypot(input.x, input.y);
192
- if (length > 0) {
193
- const travel = player.speed * multiplier * FIXED_TIMESTEP;
194
- player.x += input.x / length * travel;
195
- player.y += input.y / length * travel;
196
- }
197
- for (let index = 0; index < this.orbSlots.length; index += 1) {
198
- if (orbs[index] && Math.hypot(this.orbSlots[index].point.x - player.x, this.orbSlots[index].point.y - player.y) <= 22) {
199
- orbs[index] = false;
200
- surge = 4;
201
- }
202
- }
203
- }
204
- return true;
205
- }
206
-
207
- private mergeResult(target: StepResult, next: StepResult): void {
208
- target.captured ||= next.captured; target.cleared ||= next.cleared; target.timedOut ||= next.timedOut;
209
- target.pelletsCollected.push(...next.pelletsCollected); target.powerCollected.push(...next.powerCollected);
210
- }
211
-
212
- private canTravel(actor: Actor, input: Point, duration: number, speedMultiplier = 1): boolean {
213
- const length = Math.hypot(input.x, input.y);
214
- if (length === 0) return true;
215
- const distance = actor.speed * speedMultiplier * duration;
216
- const steps = Math.max(1, Math.ceil(distance / 4));
217
- for (let step = 1; step <= steps; step += 1) {
218
- const candidate = { x: actor.x + input.x / length * distance * step / steps, y: actor.y + input.y / length * distance * step / steps };
219
- if (isBlocked(this.arena, candidate, actor.radius)) return false;
220
- }
221
- return true;
222
- }
223
-
224
- private move(actor: Actor, input: Point, dt: number, speedMultiplier = 1): void {
225
- const length = Math.hypot(input.x, input.y);
226
- if (length === 0) return;
227
- const speed = actor.speed * speedMultiplier;
228
- const steps = Math.max(1, Math.ceil(speed * dt / 4));
229
- const dx = input.x / length * speed * dt / steps;
230
- const dy = input.y / length * speed * dt / steps;
231
- for (let index = 0; index < steps; index += 1) {
232
- const candidate = { x: actor.x + dx, y: actor.y + dy };
233
- if (isBlocked(this.arena, candidate, actor.radius)) return;
234
- actor.x = candidate.x; actor.y = candidate.y;
235
- }
236
- }
237
-
238
- private velocityFor(input: Point, speed: number): Point {
239
- const length = Math.hypot(input.x, input.y);
240
- return length === 0 ? { x: 0, y: 0 } : { x: input.x / length * speed, y: input.y / length * speed };
241
- }
242
-
243
- private chooseBaselineAction(): Direction {
244
- const choices = actionOrder.filter((direction) => this.canTravel(this.enemy, directions[direction], ENEMY_DECISION_INTERVAL));
245
- if (choices.length === 0) return this.enemyDirection;
246
- choices.sort((a, b) => {
247
- const av = directions[a]; const bv = directions[b];
248
- return Math.hypot(this.player.x - (this.enemy.x + av.x * 42), this.player.y - (this.enemy.y + av.y * 42)) - Math.hypot(this.player.x - (this.enemy.x + bv.x * 42), this.player.y - (this.enemy.y + bv.y * 42));
249
- });
250
- return choices[0];
251
- }
252
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/main.ts DELETED
@@ -1,18 +0,0 @@
1
- import Phaser from 'phaser';
2
- import './styles.css';
3
- import { PlayScene } from './game/PlayScene.js';
4
-
5
- new Phaser.Game({
6
- type: Phaser.AUTO,
7
- parent: 'game',
8
- width: 800,
9
- height: 640,
10
- backgroundColor: '#071426',
11
- scene: [PlayScene],
12
- scale: {
13
- mode: Phaser.Scale.FIT,
14
- autoCenter: Phaser.Scale.CENTER_BOTH,
15
- width: 800,
16
- height: 640,
17
- },
18
- });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/styles.css DELETED
@@ -1,18 +0,0 @@
1
- :root { color: #e8f5ff; background: #071426; font-family: Inter, ui-sans-serif, system-ui, sans-serif; }
2
- * { box-sizing: border-box; }
3
- body { min-width: 320px; min-height: 100vh; margin: 0; display: grid; place-items: center; }
4
- main { width: min(100vw, 1180px); padding: 16px; }
5
- .app-shell { display: flex; align-items: flex-start; justify-content: center; gap: 14px; }
6
- .game-column { width: min(100%, 800px); }
7
- .game-toolbar { display: flex; justify-content: flex-end; gap: 8px; margin: 0 0 8px; }
8
- .game-toolbar button { border: 1px solid #447b9b; border-radius: 7px; background: #173a55; color: #e8f5ff; cursor: pointer; font: inherit; font-size: 14px; padding: 7px 10px; }
9
- .game-toolbar button:hover { background: #214d6d; }
10
- .game-toolbar button:disabled { opacity: 0.55; cursor: default; }
11
- #game { overflow: hidden; border: 1px solid #2d607e; border-radius: 14px; box-shadow: 0 20px 70px #0008; }
12
- canvas { display: block; width: 100% !important; height: auto !important; }
13
- #debug-panel { width: 300px; max-height: 640px; overflow: auto; border: 1px solid #2d607e; border-radius: 14px; background: #0a1c2d; box-shadow: 0 20px 70px #0008; padding: 16px; }
14
- .debug-heading { display: flex; align-items: baseline; justify-content: space-between; gap: 10px; border-bottom: 1px solid #28546f; padding-bottom: 10px; }
15
- .debug-heading h2 { color: #91efff; font-size: 17px; margin: 0; }
16
- .debug-heading span { color: #8daaba; font-size: 11px; white-space: nowrap; }
17
- #debug-content { color: #d9efff; font: 12px/1.6 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; margin: 13px 0 0; white-space: pre-wrap; }
18
- @media (max-width: 1080px) { .app-shell { align-items: stretch; flex-direction: column; } .game-column { width: 100%; } #debug-panel { width: 100%; max-height: 340px; } }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
tsconfig.json DELETED
@@ -1,14 +0,0 @@
1
- {
2
- "compilerOptions": {
3
- "target": "ES2022",
4
- "useDefineForClassFields": true,
5
- "module": "ESNext",
6
- "lib": ["ES2022", "DOM"],
7
- "skipLibCheck": true,
8
- "moduleResolution": "bundler",
9
- "allowImportingTsExtensions": false,
10
- "noEmit": true,
11
- "strict": true
12
- },
13
- "include": ["src"]
14
- }