k-l-lambda commited on
Commit
502af73
·
1 Parent(s): cccfc29
Files changed (46) hide show
  1. .gitignore +2 -0
  2. trigo-web/.husky/pre-commit +1 -0
  3. trigo-web/README.md +16 -1
  4. trigo-web/app/package-lock.json +73 -1596
  5. trigo-web/app/src/App.vue +7 -4
  6. trigo-web/app/src/components/LayoutFrame.vue +29 -0
  7. trigo-web/app/src/components/SidebarMenu.vue +235 -0
  8. trigo-web/app/src/composables/useSocket.ts +24 -17
  9. trigo-web/app/src/composables/useTrigoAgent.ts +171 -0
  10. trigo-web/app/src/main.ts +10 -9
  11. trigo-web/app/src/router/index.ts +40 -5
  12. trigo-web/app/src/services/onnxInferencer.ts +68 -0
  13. trigo-web/app/src/services/trigoViewport.ts +301 -138
  14. trigo-web/app/src/stores/gameStore.ts +21 -29
  15. trigo-web/app/src/utils/TrigoGameFrontend.ts +1 -28
  16. trigo-web/app/src/utils/storage.ts +261 -0
  17. trigo-web/app/src/views/OnnxTestView.vue +511 -0
  18. trigo-web/app/src/views/TrigoAgentTestView.vue +711 -0
  19. trigo-web/app/src/views/TrigoTreeTestView.vue +1083 -0
  20. trigo-web/app/src/views/TrigoView.vue +1560 -1171
  21. trigo-web/app/test_capture.js +2 -2
  22. trigo-web/app/vite.config.ts +27 -3
  23. trigo-web/backend/src/server.ts +7 -3
  24. trigo-web/backend/src/services/gameManager.ts +5 -7
  25. trigo-web/backend/src/sockets/gameSocket.ts +4 -2
  26. trigo-web/backend/tsconfig.json +2 -7
  27. trigo-web/inc/modelInferencer.ts +465 -0
  28. trigo-web/inc/tgn/README.md +12 -10
  29. trigo-web/inc/tgn/tgn.jison +2 -2
  30. trigo-web/inc/tgn/tgnParser.ts +11 -21
  31. trigo-web/inc/trigo/ab0yz.ts +19 -13
  32. trigo-web/inc/trigo/game.ts +135 -68
  33. trigo-web/inc/trigo/gameUtils.ts +15 -49
  34. trigo-web/inc/trigo/index.ts +0 -1
  35. trigo-web/inc/trigo/parserInit.ts +9 -8
  36. trigo-web/inc/trigo/typeAdapters.ts +3 -12
  37. trigo-web/inc/trigoAgent.ts +262 -0
  38. trigo-web/inc/trigoTreeAgent.ts +415 -0
  39. trigo-web/inc/tsconfig.json +1 -1
  40. trigo-web/package-lock.json +1253 -799
  41. trigo-web/package.json +11 -1
  42. trigo-web/public/lib/tgnParser.cjs +1 -1
  43. trigo-web/tools/README.md +2 -2
  44. trigo-web/tools/migrateTGN.ts +291 -0
  45. trigo-web/vitest.config.ts +1 -1
  46. trigo-web/yarn.lock +517 -39
.gitignore ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ node_modules/
2
+ dist/
trigo-web/.husky/pre-commit ADDED
@@ -0,0 +1 @@
 
 
1
+ npm test
trigo-web/README.md CHANGED
@@ -52,12 +52,14 @@ trigo-web/
52
  ### Installation
53
 
54
  1. Clone the repository:
 
55
  ```bash
56
  git clone [repository-url]
57
  cd trigo-web
58
  ```
59
 
60
  2. Install all dependencies:
 
61
  ```bash
62
  npm run install:all
63
  ```
@@ -95,26 +97,31 @@ npm run build
95
  The project includes comprehensive unit tests for the game logic (TrigoGame class).
96
 
97
  **Run all tests once:**
 
98
  ```bash
99
  npm run test:run
100
  ```
101
 
102
  **Run tests in watch mode (auto-rerun on file changes):**
 
103
  ```bash
104
  npm test
105
  ```
106
 
107
  **Run tests with UI dashboard:**
 
108
  ```bash
109
  npm run test:ui
110
  ```
111
 
112
  **Run specific test file:**
 
113
  ```bash
114
  npm exec vitest -- run tests/game/trigoGame.core.test.ts
115
  ```
116
 
117
  **Test Coverage:**
 
118
  - **109/109 tests passing (100%)**
119
  - Core functionality (35 tests) - Drop, pass, surrender, reset
120
  - History management (21 tests) - Undo, redo, jump to step
@@ -122,6 +129,7 @@ npm exec vitest -- run tests/game/trigoGame.core.test.ts
122
  - State management (32 tests) - Serialization, callbacks, session storage
123
 
124
  **Test Files Location:** `tests/game/`
 
125
  - `trigoGame.core.test.ts` - Basic game operations
126
  - `trigoGame.history.test.ts` - History and navigation
127
  - `trigoGame.rules.test.ts` - Go game rules implementation
@@ -144,6 +152,7 @@ npm run format:check
144
  ## Technology Stack
145
 
146
  ### Frontend
 
147
  - **Vue 3** - Progressive JavaScript framework
148
  - **TypeScript** - Type-safe JavaScript
149
  - **Three.js** - 3D graphics library for WebGL
@@ -154,6 +163,7 @@ npm run format:check
154
  - **SASS** - CSS preprocessor
155
 
156
  ### Backend
 
157
  - **Node.js** - JavaScript runtime
158
  - **Express** - Web framework
159
  - **Socket.io** - Real-time bidirectional communication
@@ -172,6 +182,7 @@ Trigo extends the traditional Go game into three dimensions:
172
  ## API Endpoints
173
 
174
  ### REST API
 
175
  - `GET /health` - Health check
176
  - `GET /api/rooms` - List active game rooms
177
  - `GET /api/rooms/:roomId` - Get specific room details
@@ -179,6 +190,7 @@ Trigo extends the traditional Go game into three dimensions:
179
  ### WebSocket Events
180
 
181
  #### Client → Server
 
182
  - `joinRoom` - Join or create a game room
183
  - `leaveRoom` - Leave current room
184
  - `makeMove` - Make a game move
@@ -187,6 +199,7 @@ Trigo extends the traditional Go game into three dimensions:
187
  - `chatMessage` - Send chat message
188
 
189
  #### Server → Client
 
190
  - `roomJoined` - Successfully joined room
191
  - `gameUpdate` - Game state update
192
  - `playerJoined` - Another player joined
@@ -197,6 +210,7 @@ Trigo extends the traditional Go game into three dimensions:
197
  ## Development Guidelines
198
 
199
  ### Code Style
 
200
  - Uses Prettier for consistent formatting
201
  - Tab indentation (following prototype style)
202
  - Double quotes for strings
@@ -204,6 +218,7 @@ Trigo extends the traditional Go game into three dimensions:
204
  - Semicolons always
205
 
206
  ### Git Workflow
 
207
  1. Create feature branch from `main`
208
  2. Make changes and test locally
209
  3. Format code with `npm run format`
@@ -224,4 +239,4 @@ Contributions are welcome! Please read the contributing guidelines before submit
224
 
225
  ## Support
226
 
227
- For issues, questions, or suggestions, please open an issue on GitHub.
 
52
  ### Installation
53
 
54
  1. Clone the repository:
55
+
56
  ```bash
57
  git clone [repository-url]
58
  cd trigo-web
59
  ```
60
 
61
  2. Install all dependencies:
62
+
63
  ```bash
64
  npm run install:all
65
  ```
 
97
  The project includes comprehensive unit tests for the game logic (TrigoGame class).
98
 
99
  **Run all tests once:**
100
+
101
  ```bash
102
  npm run test:run
103
  ```
104
 
105
  **Run tests in watch mode (auto-rerun on file changes):**
106
+
107
  ```bash
108
  npm test
109
  ```
110
 
111
  **Run tests with UI dashboard:**
112
+
113
  ```bash
114
  npm run test:ui
115
  ```
116
 
117
  **Run specific test file:**
118
+
119
  ```bash
120
  npm exec vitest -- run tests/game/trigoGame.core.test.ts
121
  ```
122
 
123
  **Test Coverage:**
124
+
125
  - **109/109 tests passing (100%)**
126
  - Core functionality (35 tests) - Drop, pass, surrender, reset
127
  - History management (21 tests) - Undo, redo, jump to step
 
129
  - State management (32 tests) - Serialization, callbacks, session storage
130
 
131
  **Test Files Location:** `tests/game/`
132
+
133
  - `trigoGame.core.test.ts` - Basic game operations
134
  - `trigoGame.history.test.ts` - History and navigation
135
  - `trigoGame.rules.test.ts` - Go game rules implementation
 
152
  ## Technology Stack
153
 
154
  ### Frontend
155
+
156
  - **Vue 3** - Progressive JavaScript framework
157
  - **TypeScript** - Type-safe JavaScript
158
  - **Three.js** - 3D graphics library for WebGL
 
163
  - **SASS** - CSS preprocessor
164
 
165
  ### Backend
166
+
167
  - **Node.js** - JavaScript runtime
168
  - **Express** - Web framework
169
  - **Socket.io** - Real-time bidirectional communication
 
182
  ## API Endpoints
183
 
184
  ### REST API
185
+
186
  - `GET /health` - Health check
187
  - `GET /api/rooms` - List active game rooms
188
  - `GET /api/rooms/:roomId` - Get specific room details
 
190
  ### WebSocket Events
191
 
192
  #### Client → Server
193
+
194
  - `joinRoom` - Join or create a game room
195
  - `leaveRoom` - Leave current room
196
  - `makeMove` - Make a game move
 
199
  - `chatMessage` - Send chat message
200
 
201
  #### Server → Client
202
+
203
  - `roomJoined` - Successfully joined room
204
  - `gameUpdate` - Game state update
205
  - `playerJoined` - Another player joined
 
210
  ## Development Guidelines
211
 
212
  ### Code Style
213
+
214
  - Uses Prettier for consistent formatting
215
  - Tab indentation (following prototype style)
216
  - Double quotes for strings
 
218
  - Semicolons always
219
 
220
  ### Git Workflow
221
+
222
  1. Create feature branch from `main`
223
  2. Make changes and test locally
224
  3. Format code with `npm run format`
 
239
 
240
  ## Support
241
 
242
+ For issues, questions, or suggestions, please open an issue on GitHub.
trigo-web/app/package-lock.json CHANGED
@@ -17,53 +17,12 @@
17
  "devDependencies": {
18
  "@types/three": "^0.156.0",
19
  "@vitejs/plugin-vue": "^5.2.4",
20
- "@vitest/ui": "^4.0.6",
21
- "jsdom": "^27.1.0",
22
  "sass-embedded": "^1.93.2",
23
  "typescript": "^5.2.2",
24
  "vite": "^5.4.21",
25
- "vitest": "^4.0.6",
26
  "vue-tsc": "^2.2.12"
27
  }
28
  },
29
- "node_modules/@acemir/cssom": {
30
- "version": "0.9.19",
31
- "resolved": "https://registry.npmjs.org/@acemir/cssom/-/cssom-0.9.19.tgz",
32
- "integrity": "sha512-Pp2gAQXPZ2o7lt4j0IMwNRXqQ3pagxtDj5wctL5U2Lz4oV0ocDNlkgx4DpxfyKav4S/bePuI+SMqcBSUHLy9kg==",
33
- "dev": true
34
- },
35
- "node_modules/@asamuzakjp/css-color": {
36
- "version": "4.0.5",
37
- "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-4.0.5.tgz",
38
- "integrity": "sha512-lMrXidNhPGsDjytDy11Vwlb6OIGrT3CmLg3VWNFyWkLWtijKl7xjvForlh8vuj0SHGjgl4qZEQzUmYTeQA2JFQ==",
39
- "dev": true,
40
- "dependencies": {
41
- "@csstools/css-calc": "^2.1.4",
42
- "@csstools/css-color-parser": "^3.1.0",
43
- "@csstools/css-parser-algorithms": "^3.0.5",
44
- "@csstools/css-tokenizer": "^3.0.4",
45
- "lru-cache": "^11.2.1"
46
- }
47
- },
48
- "node_modules/@asamuzakjp/dom-selector": {
49
- "version": "6.7.4",
50
- "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-6.7.4.tgz",
51
- "integrity": "sha512-buQDjkm+wDPXd6c13534URWZqbz0RP5PAhXZ+LIoa5LgwInT9HVJvGIJivg75vi8I13CxDGdTnz+aY5YUJlIAA==",
52
- "dev": true,
53
- "dependencies": {
54
- "@asamuzakjp/nwsapi": "^2.3.9",
55
- "bidi-js": "^1.0.3",
56
- "css-tree": "^3.1.0",
57
- "is-potential-custom-element-name": "^1.0.1",
58
- "lru-cache": "^11.2.2"
59
- }
60
- },
61
- "node_modules/@asamuzakjp/nwsapi": {
62
- "version": "2.3.9",
63
- "resolved": "https://registry.npmjs.org/@asamuzakjp/nwsapi/-/nwsapi-2.3.9.tgz",
64
- "integrity": "sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==",
65
- "dev": true
66
- },
67
  "node_modules/@babel/helper-string-parser": {
68
  "version": "7.27.1",
69
  "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
@@ -112,135 +71,6 @@
112
  "integrity": "sha512-fdRs9PSrBF7QUntpZpq6BTw58fhgGJojgg39m9oFOJGZT+nip9b0so5cYY1oWl5pvemDLr0cPPsH46vwThEbpQ==",
113
  "dev": true
114
  },
115
- "node_modules/@csstools/color-helpers": {
116
- "version": "5.1.0",
117
- "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.1.0.tgz",
118
- "integrity": "sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==",
119
- "dev": true,
120
- "funding": [
121
- {
122
- "type": "github",
123
- "url": "https://github.com/sponsors/csstools"
124
- },
125
- {
126
- "type": "opencollective",
127
- "url": "https://opencollective.com/csstools"
128
- }
129
- ],
130
- "engines": {
131
- "node": ">=18"
132
- }
133
- },
134
- "node_modules/@csstools/css-calc": {
135
- "version": "2.1.4",
136
- "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.4.tgz",
137
- "integrity": "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==",
138
- "dev": true,
139
- "funding": [
140
- {
141
- "type": "github",
142
- "url": "https://github.com/sponsors/csstools"
143
- },
144
- {
145
- "type": "opencollective",
146
- "url": "https://opencollective.com/csstools"
147
- }
148
- ],
149
- "engines": {
150
- "node": ">=18"
151
- },
152
- "peerDependencies": {
153
- "@csstools/css-parser-algorithms": "^3.0.5",
154
- "@csstools/css-tokenizer": "^3.0.4"
155
- }
156
- },
157
- "node_modules/@csstools/css-color-parser": {
158
- "version": "3.1.0",
159
- "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.1.0.tgz",
160
- "integrity": "sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==",
161
- "dev": true,
162
- "funding": [
163
- {
164
- "type": "github",
165
- "url": "https://github.com/sponsors/csstools"
166
- },
167
- {
168
- "type": "opencollective",
169
- "url": "https://opencollective.com/csstools"
170
- }
171
- ],
172
- "dependencies": {
173
- "@csstools/color-helpers": "^5.1.0",
174
- "@csstools/css-calc": "^2.1.4"
175
- },
176
- "engines": {
177
- "node": ">=18"
178
- },
179
- "peerDependencies": {
180
- "@csstools/css-parser-algorithms": "^3.0.5",
181
- "@csstools/css-tokenizer": "^3.0.4"
182
- }
183
- },
184
- "node_modules/@csstools/css-parser-algorithms": {
185
- "version": "3.0.5",
186
- "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.5.tgz",
187
- "integrity": "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==",
188
- "dev": true,
189
- "funding": [
190
- {
191
- "type": "github",
192
- "url": "https://github.com/sponsors/csstools"
193
- },
194
- {
195
- "type": "opencollective",
196
- "url": "https://opencollective.com/csstools"
197
- }
198
- ],
199
- "engines": {
200
- "node": ">=18"
201
- },
202
- "peerDependencies": {
203
- "@csstools/css-tokenizer": "^3.0.4"
204
- }
205
- },
206
- "node_modules/@csstools/css-syntax-patches-for-csstree": {
207
- "version": "1.0.15",
208
- "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.0.15.tgz",
209
- "integrity": "sha512-q0p6zkVq2lJnmzZVPR33doA51G7YOja+FBvRdp5ISIthL0MtFCgYHHhR563z9WFGxcOn0WfjSkPDJ5Qig3H3Sw==",
210
- "dev": true,
211
- "funding": [
212
- {
213
- "type": "github",
214
- "url": "https://github.com/sponsors/csstools"
215
- },
216
- {
217
- "type": "opencollective",
218
- "url": "https://opencollective.com/csstools"
219
- }
220
- ],
221
- "engines": {
222
- "node": ">=18"
223
- }
224
- },
225
- "node_modules/@csstools/css-tokenizer": {
226
- "version": "3.0.4",
227
- "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz",
228
- "integrity": "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==",
229
- "dev": true,
230
- "funding": [
231
- {
232
- "type": "github",
233
- "url": "https://github.com/sponsors/csstools"
234
- },
235
- {
236
- "type": "opencollective",
237
- "url": "https://opencollective.com/csstools"
238
- }
239
- ],
240
- "engines": {
241
- "node": ">=18"
242
- }
243
- },
244
  "node_modules/@esbuild/aix-ppc64": {
245
  "version": "0.21.5",
246
  "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz",
@@ -513,22 +343,6 @@
513
  "node": ">=12"
514
  }
515
  },
516
- "node_modules/@esbuild/netbsd-arm64": {
517
- "version": "0.25.12",
518
- "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz",
519
- "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==",
520
- "cpu": [
521
- "arm64"
522
- ],
523
- "dev": true,
524
- "optional": true,
525
- "os": [
526
- "netbsd"
527
- ],
528
- "engines": {
529
- "node": ">=18"
530
- }
531
- },
532
  "node_modules/@esbuild/netbsd-x64": {
533
  "version": "0.21.5",
534
  "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz",
@@ -545,22 +359,6 @@
545
  "node": ">=12"
546
  }
547
  },
548
- "node_modules/@esbuild/openbsd-arm64": {
549
- "version": "0.25.12",
550
- "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz",
551
- "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==",
552
- "cpu": [
553
- "arm64"
554
- ],
555
- "dev": true,
556
- "optional": true,
557
- "os": [
558
- "openbsd"
559
- ],
560
- "engines": {
561
- "node": ">=18"
562
- }
563
- },
564
  "node_modules/@esbuild/openbsd-x64": {
565
  "version": "0.21.5",
566
  "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz",
@@ -577,22 +375,6 @@
577
  "node": ">=12"
578
  }
579
  },
580
- "node_modules/@esbuild/openharmony-arm64": {
581
- "version": "0.25.12",
582
- "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz",
583
- "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==",
584
- "cpu": [
585
- "arm64"
586
- ],
587
- "dev": true,
588
- "optional": true,
589
- "os": [
590
- "openharmony"
591
- ],
592
- "engines": {
593
- "node": ">=18"
594
- }
595
- },
596
  "node_modules/@esbuild/sunos-x64": {
597
  "version": "0.21.5",
598
  "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz",
@@ -958,12 +740,6 @@
958
  "url": "https://opencollective.com/parcel"
959
  }
960
  },
961
- "node_modules/@polka/url": {
962
- "version": "1.0.0-next.29",
963
- "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz",
964
- "integrity": "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==",
965
- "dev": true
966
- },
967
  "node_modules/@rollup/rollup-android-arm-eabi": {
968
  "version": "4.52.5",
969
  "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.52.5.tgz",
@@ -1255,34 +1031,23 @@
1255
  "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz",
1256
  "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA=="
1257
  },
1258
- "node_modules/@standard-schema/spec": {
1259
- "version": "1.0.0",
1260
- "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.0.0.tgz",
1261
- "integrity": "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==",
1262
- "dev": true
1263
- },
1264
- "node_modules/@types/chai": {
1265
- "version": "5.2.3",
1266
- "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz",
1267
- "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==",
1268
- "dev": true,
1269
- "dependencies": {
1270
- "@types/deep-eql": "*",
1271
- "assertion-error": "^2.0.1"
1272
- }
1273
- },
1274
- "node_modules/@types/deep-eql": {
1275
- "version": "4.0.2",
1276
- "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz",
1277
- "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==",
1278
- "dev": true
1279
- },
1280
  "node_modules/@types/estree": {
1281
  "version": "1.0.8",
1282
  "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
1283
  "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
1284
  "dev": true
1285
  },
 
 
 
 
 
 
 
 
 
 
 
1286
  "node_modules/@types/stats.js": {
1287
  "version": "0.17.4",
1288
  "resolved": "https://registry.npmjs.org/@types/stats.js/-/stats.js-0.17.4.tgz",
@@ -1320,111 +1085,6 @@
1320
  "vue": "^3.2.25"
1321
  }
1322
  },
1323
- "node_modules/@vitest/expect": {
1324
- "version": "4.0.6",
1325
- "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.0.6.tgz",
1326
- "integrity": "sha512-5j8UUlBVhOjhj4lR2Nt9sEV8b4WtbcYh8vnfhTNA2Kn5+smtevzjNq+xlBuVhnFGXiyPPNzGrOVvmyHWkS5QGg==",
1327
- "dev": true,
1328
- "dependencies": {
1329
- "@standard-schema/spec": "^1.0.0",
1330
- "@types/chai": "^5.2.2",
1331
- "@vitest/spy": "4.0.6",
1332
- "@vitest/utils": "4.0.6",
1333
- "chai": "^6.0.1",
1334
- "tinyrainbow": "^3.0.3"
1335
- },
1336
- "funding": {
1337
- "url": "https://opencollective.com/vitest"
1338
- }
1339
- },
1340
- "node_modules/@vitest/pretty-format": {
1341
- "version": "4.0.6",
1342
- "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.0.6.tgz",
1343
- "integrity": "sha512-4vptgNkLIA1W1Nn5X4x8rLJBzPiJwnPc+awKtfBE5hNMVsoAl/JCCPPzNrbf+L4NKgklsis5Yp2gYa+XAS442g==",
1344
- "dev": true,
1345
- "dependencies": {
1346
- "tinyrainbow": "^3.0.3"
1347
- },
1348
- "funding": {
1349
- "url": "https://opencollective.com/vitest"
1350
- }
1351
- },
1352
- "node_modules/@vitest/runner": {
1353
- "version": "4.0.6",
1354
- "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.0.6.tgz",
1355
- "integrity": "sha512-trPk5qpd7Jj+AiLZbV/e+KiiaGXZ8ECsRxtnPnCrJr9OW2mLB72Cb824IXgxVz/mVU3Aj4VebY+tDTPn++j1Og==",
1356
- "dev": true,
1357
- "dependencies": {
1358
- "@vitest/utils": "4.0.6",
1359
- "pathe": "^2.0.3"
1360
- },
1361
- "funding": {
1362
- "url": "https://opencollective.com/vitest"
1363
- }
1364
- },
1365
- "node_modules/@vitest/snapshot": {
1366
- "version": "4.0.6",
1367
- "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.0.6.tgz",
1368
- "integrity": "sha512-PaYLt7n2YzuvxhulDDu6c9EosiRuIE+FI2ECKs6yvHyhoga+2TBWI8dwBjs+IeuQaMtZTfioa9tj3uZb7nev1g==",
1369
- "dev": true,
1370
- "dependencies": {
1371
- "@vitest/pretty-format": "4.0.6",
1372
- "magic-string": "^0.30.19",
1373
- "pathe": "^2.0.3"
1374
- },
1375
- "funding": {
1376
- "url": "https://opencollective.com/vitest"
1377
- }
1378
- },
1379
- "node_modules/@vitest/spy": {
1380
- "version": "4.0.6",
1381
- "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.0.6.tgz",
1382
- "integrity": "sha512-g9jTUYPV1LtRPRCQfhbMintW7BTQz1n6WXYQYRQ25qkyffA4bjVXjkROokZnv7t07OqfaFKw1lPzqKGk1hmNuQ==",
1383
- "dev": true,
1384
- "funding": {
1385
- "url": "https://opencollective.com/vitest"
1386
- }
1387
- },
1388
- "node_modules/@vitest/ui": {
1389
- "version": "4.0.6",
1390
- "resolved": "https://registry.npmjs.org/@vitest/ui/-/ui-4.0.6.tgz",
1391
- "integrity": "sha512-1ekpBsYNUm0Xv/0YsTvoSRmiRkmzz9Pma7qQ3Ui76sg2gwp2/ewSWqx4W/HfaN5dF0E8iBbidFo1wGaeqXYIrQ==",
1392
- "dev": true,
1393
- "dependencies": {
1394
- "@vitest/utils": "4.0.6",
1395
- "fflate": "^0.8.2",
1396
- "flatted": "^3.3.3",
1397
- "pathe": "^2.0.3",
1398
- "sirv": "^3.0.2",
1399
- "tinyglobby": "^0.2.15",
1400
- "tinyrainbow": "^3.0.3"
1401
- },
1402
- "funding": {
1403
- "url": "https://opencollective.com/vitest"
1404
- },
1405
- "peerDependencies": {
1406
- "vitest": "4.0.6"
1407
- }
1408
- },
1409
- "node_modules/@vitest/ui/node_modules/fflate": {
1410
- "version": "0.8.2",
1411
- "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz",
1412
- "integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==",
1413
- "dev": true
1414
- },
1415
- "node_modules/@vitest/utils": {
1416
- "version": "4.0.6",
1417
- "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.0.6.tgz",
1418
- "integrity": "sha512-bG43VS3iYKrMIZXBo+y8Pti0O7uNju3KvNn6DrQWhQQKcLavMB+0NZfO1/QBAEbq0MaQ3QjNsnnXlGQvsh0Z6A==",
1419
- "dev": true,
1420
- "dependencies": {
1421
- "@vitest/pretty-format": "4.0.6",
1422
- "tinyrainbow": "^3.0.3"
1423
- },
1424
- "funding": {
1425
- "url": "https://opencollective.com/vitest"
1426
- }
1427
- },
1428
  "node_modules/@volar/language-core": {
1429
  "version": "2.4.15",
1430
  "resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-2.4.15.tgz",
@@ -1581,45 +1241,18 @@
1581
  "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.22.tgz",
1582
  "integrity": "sha512-F4yc6palwq3TT0u+FYf0Ns4Tfl9GRFURDN2gWG7L1ecIaS/4fCIuFOjMTnCyjsu/OK6vaDKLCrGAa+KvvH+h4w=="
1583
  },
1584
- "node_modules/agent-base": {
1585
- "version": "7.1.4",
1586
- "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz",
1587
- "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==",
1588
- "dev": true,
1589
- "engines": {
1590
- "node": ">= 14"
1591
- }
1592
- },
1593
  "node_modules/alien-signals": {
1594
  "version": "1.0.13",
1595
  "resolved": "https://registry.npmjs.org/alien-signals/-/alien-signals-1.0.13.tgz",
1596
  "integrity": "sha512-OGj9yyTnJEttvzhTUWuscOvtqxq5vrhF7vL9oS0xJ2mK0ItPYP1/y+vCFebfxoEyAz0++1AIwJ5CMr+Fk3nDmg==",
1597
  "dev": true
1598
  },
1599
- "node_modules/assertion-error": {
1600
- "version": "2.0.1",
1601
- "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz",
1602
- "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==",
1603
- "dev": true,
1604
- "engines": {
1605
- "node": ">=12"
1606
- }
1607
- },
1608
  "node_modules/balanced-match": {
1609
  "version": "1.0.2",
1610
  "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
1611
  "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
1612
  "dev": true
1613
  },
1614
- "node_modules/bidi-js": {
1615
- "version": "1.0.3",
1616
- "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz",
1617
- "integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==",
1618
- "dev": true,
1619
- "dependencies": {
1620
- "require-from-string": "^2.0.2"
1621
- }
1622
- },
1623
  "node_modules/brace-expansion": {
1624
  "version": "2.0.2",
1625
  "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
@@ -1648,15 +1281,6 @@
1648
  "integrity": "sha512-7VPMEPuYznPSoR21NE1zvd2Xna6c/CloiZCfcMXR1Jny6PjX0N4Nsa38zcBFo/FMK+BlA+FLKbJCQ0i2yxp+Xg==",
1649
  "dev": true
1650
  },
1651
- "node_modules/chai": {
1652
- "version": "6.2.0",
1653
- "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.0.tgz",
1654
- "integrity": "sha512-aUTnJc/JipRzJrNADXVvpVqi6CO0dn3nx4EVPxijri+fj3LUUDyZQOgVeW54Ob3Y1Xh9Iz8f+CgaCl8v0mn9bA==",
1655
- "dev": true,
1656
- "engines": {
1657
- "node": ">=18"
1658
- }
1659
- },
1660
  "node_modules/chokidar": {
1661
  "version": "4.0.3",
1662
  "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz",
@@ -1679,51 +1303,11 @@
1679
  "integrity": "sha512-twmVoizEW7ylZSN32OgKdXRmo1qg+wT5/6C3xu5b9QsWzSFAhHLn2xd8ro0diCsKfCj1RdaTP/nrcW+vAoQPIw==",
1680
  "dev": true
1681
  },
1682
- "node_modules/css-tree": {
1683
- "version": "3.1.0",
1684
- "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.1.0.tgz",
1685
- "integrity": "sha512-0eW44TGN5SQXU1mWSkKwFstI/22X2bG1nYzZTYMAWjylYURhse752YgbE4Cx46AC+bAvI+/dYTPRk1LqSUnu6w==",
1686
- "dev": true,
1687
- "dependencies": {
1688
- "mdn-data": "2.12.2",
1689
- "source-map-js": "^1.0.1"
1690
- },
1691
- "engines": {
1692
- "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0"
1693
- }
1694
- },
1695
- "node_modules/cssstyle": {
1696
- "version": "5.3.2",
1697
- "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-5.3.2.tgz",
1698
- "integrity": "sha512-zDMqXh8Vs1CdRYZQ2M633m/SFgcjlu8RB8b/1h82i+6vpArF507NSYIWJHGlJaTWoS+imcnctmEz43txhbVkOw==",
1699
- "dev": true,
1700
- "dependencies": {
1701
- "@asamuzakjp/css-color": "^4.0.3",
1702
- "@csstools/css-syntax-patches-for-csstree": "^1.0.14",
1703
- "css-tree": "^3.1.0"
1704
- },
1705
- "engines": {
1706
- "node": ">=20"
1707
- }
1708
- },
1709
  "node_modules/csstype": {
1710
  "version": "3.1.3",
1711
  "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
1712
  "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="
1713
  },
1714
- "node_modules/data-urls": {
1715
- "version": "6.0.0",
1716
- "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-6.0.0.tgz",
1717
- "integrity": "sha512-BnBS08aLUM+DKamupXs3w2tJJoqU+AkaE/+6vQxi/G/DPmIZFJJp9Dkb1kM03AZx8ADehDUZgsNxju3mPXZYIA==",
1718
- "dev": true,
1719
- "dependencies": {
1720
- "whatwg-mimetype": "^4.0.0",
1721
- "whatwg-url": "^15.0.0"
1722
- },
1723
- "engines": {
1724
- "node": ">=20"
1725
- }
1726
- },
1727
  "node_modules/de-indent": {
1728
  "version": "1.0.2",
1729
  "resolved": "https://registry.npmjs.org/de-indent/-/de-indent-1.0.2.tgz",
@@ -1746,12 +1330,6 @@
1746
  }
1747
  }
1748
  },
1749
- "node_modules/decimal.js": {
1750
- "version": "10.6.0",
1751
- "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz",
1752
- "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==",
1753
- "dev": true
1754
- },
1755
  "node_modules/detect-libc": {
1756
  "version": "1.0.3",
1757
  "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz",
@@ -1796,12 +1374,6 @@
1796
  "url": "https://github.com/fb55/entities?sponsor=1"
1797
  }
1798
  },
1799
- "node_modules/es-module-lexer": {
1800
- "version": "1.7.0",
1801
- "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz",
1802
- "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==",
1803
- "dev": true
1804
- },
1805
  "node_modules/esbuild": {
1806
  "version": "0.21.5",
1807
  "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz",
@@ -1845,15 +1417,6 @@
1845
  "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz",
1846
  "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="
1847
  },
1848
- "node_modules/expect-type": {
1849
- "version": "1.2.2",
1850
- "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.2.2.tgz",
1851
- "integrity": "sha512-JhFGDVJ7tmDJItKhYgJCGLOWjuK9vPxiXoUFLwLDc99NlmklilbiQJwoctZtt13+xMw91MCk/REan6MWHqDjyA==",
1852
- "dev": true,
1853
- "engines": {
1854
- "node": ">=12.0.0"
1855
- }
1856
- },
1857
  "node_modules/fflate": {
1858
  "version": "0.6.10",
1859
  "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.6.10.tgz",
@@ -1873,12 +1436,6 @@
1873
  "node": ">=8"
1874
  }
1875
  },
1876
- "node_modules/flatted": {
1877
- "version": "3.3.3",
1878
- "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz",
1879
- "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==",
1880
- "dev": true
1881
- },
1882
  "node_modules/fsevents": {
1883
  "version": "2.3.3",
1884
  "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
@@ -1911,80 +1468,30 @@
1911
  "he": "bin/he"
1912
  }
1913
  },
1914
- "node_modules/html-encoding-sniffer": {
1915
- "version": "4.0.0",
1916
- "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz",
1917
- "integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==",
 
 
 
 
 
 
1918
  "dev": true,
1919
- "dependencies": {
1920
- "whatwg-encoding": "^3.1.1"
1921
- },
1922
  "engines": {
1923
- "node": ">=18"
1924
  }
1925
  },
1926
- "node_modules/http-proxy-agent": {
1927
- "version": "7.0.2",
1928
- "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz",
1929
- "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==",
1930
  "dev": true,
 
1931
  "dependencies": {
1932
- "agent-base": "^7.1.0",
1933
- "debug": "^4.3.4"
1934
- },
1935
- "engines": {
1936
- "node": ">= 14"
1937
- }
1938
- },
1939
- "node_modules/https-proxy-agent": {
1940
- "version": "7.0.6",
1941
- "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz",
1942
- "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==",
1943
- "dev": true,
1944
- "dependencies": {
1945
- "agent-base": "^7.1.2",
1946
- "debug": "4"
1947
- },
1948
- "engines": {
1949
- "node": ">= 14"
1950
- }
1951
- },
1952
- "node_modules/iconv-lite": {
1953
- "version": "0.6.3",
1954
- "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
1955
- "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
1956
- "dev": true,
1957
- "dependencies": {
1958
- "safer-buffer": ">= 2.1.2 < 3.0.0"
1959
- },
1960
- "engines": {
1961
- "node": ">=0.10.0"
1962
- }
1963
- },
1964
- "node_modules/immutable": {
1965
- "version": "5.1.4",
1966
- "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.4.tgz",
1967
- "integrity": "sha512-p6u1bG3YSnINT5RQmx/yRZBpenIl30kVxkTLDyHLIMk0gict704Q9n+thfDI7lTRm9vXdDYutVzXhzcThxTnXA==",
1968
- "dev": true
1969
- },
1970
- "node_modules/is-extglob": {
1971
- "version": "2.1.1",
1972
- "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
1973
- "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
1974
- "dev": true,
1975
- "optional": true,
1976
- "engines": {
1977
- "node": ">=0.10.0"
1978
- }
1979
- },
1980
- "node_modules/is-glob": {
1981
- "version": "4.0.3",
1982
- "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
1983
- "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
1984
- "dev": true,
1985
- "optional": true,
1986
- "dependencies": {
1987
- "is-extglob": "^2.1.1"
1988
  },
1989
  "engines": {
1990
  "node": ">=0.10.0"
@@ -2000,81 +1507,6 @@
2000
  "node": ">=0.12.0"
2001
  }
2002
  },
2003
- "node_modules/is-potential-custom-element-name": {
2004
- "version": "1.0.1",
2005
- "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz",
2006
- "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==",
2007
- "dev": true
2008
- },
2009
- "node_modules/jsdom": {
2010
- "version": "27.1.0",
2011
- "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-27.1.0.tgz",
2012
- "integrity": "sha512-Pcfm3eZ+eO4JdZCXthW9tCDT3nF4K+9dmeZ+5X39n+Kqz0DDIABRP5CAEOHRFZk8RGuC2efksTJxrjp8EXCunQ==",
2013
- "dev": true,
2014
- "dependencies": {
2015
- "@acemir/cssom": "^0.9.19",
2016
- "@asamuzakjp/dom-selector": "^6.7.3",
2017
- "cssstyle": "^5.3.2",
2018
- "data-urls": "^6.0.0",
2019
- "decimal.js": "^10.6.0",
2020
- "html-encoding-sniffer": "^4.0.0",
2021
- "http-proxy-agent": "^7.0.2",
2022
- "https-proxy-agent": "^7.0.6",
2023
- "is-potential-custom-element-name": "^1.0.1",
2024
- "parse5": "^8.0.0",
2025
- "saxes": "^6.0.0",
2026
- "symbol-tree": "^3.2.4",
2027
- "tough-cookie": "^6.0.0",
2028
- "w3c-xmlserializer": "^5.0.0",
2029
- "webidl-conversions": "^8.0.0",
2030
- "whatwg-encoding": "^3.1.1",
2031
- "whatwg-mimetype": "^4.0.0",
2032
- "whatwg-url": "^15.1.0",
2033
- "ws": "^8.18.3",
2034
- "xml-name-validator": "^5.0.0"
2035
- },
2036
- "engines": {
2037
- "node": "^20.19.0 || ^22.12.0 || >=24.0.0"
2038
- },
2039
- "peerDependencies": {
2040
- "canvas": "^3.0.0"
2041
- },
2042
- "peerDependenciesMeta": {
2043
- "canvas": {
2044
- "optional": true
2045
- }
2046
- }
2047
- },
2048
- "node_modules/jsdom/node_modules/ws": {
2049
- "version": "8.18.3",
2050
- "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz",
2051
- "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==",
2052
- "dev": true,
2053
- "engines": {
2054
- "node": ">=10.0.0"
2055
- },
2056
- "peerDependencies": {
2057
- "bufferutil": "^4.0.1",
2058
- "utf-8-validate": ">=5.0.2"
2059
- },
2060
- "peerDependenciesMeta": {
2061
- "bufferutil": {
2062
- "optional": true
2063
- },
2064
- "utf-8-validate": {
2065
- "optional": true
2066
- }
2067
- }
2068
- },
2069
- "node_modules/lru-cache": {
2070
- "version": "11.2.2",
2071
- "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.2.tgz",
2072
- "integrity": "sha512-F9ODfyqML2coTIsQpSkRHnLSZMtkU8Q+mSfcaIyKwy58u+8k5nvAYeiNhsyMARvzNcXJ9QfWVrcPsC9e9rAxtg==",
2073
- "dev": true,
2074
- "engines": {
2075
- "node": "20 || >=22"
2076
- }
2077
- },
2078
  "node_modules/magic-string": {
2079
  "version": "0.30.19",
2080
  "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.19.tgz",
@@ -2083,12 +1515,6 @@
2083
  "@jridgewell/sourcemap-codec": "^1.5.5"
2084
  }
2085
  },
2086
- "node_modules/mdn-data": {
2087
- "version": "2.12.2",
2088
- "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.12.2.tgz",
2089
- "integrity": "sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA==",
2090
- "dev": true
2091
- },
2092
  "node_modules/meshoptimizer": {
2093
  "version": "0.18.1",
2094
  "resolved": "https://registry.npmjs.org/meshoptimizer/-/meshoptimizer-0.18.1.tgz",
@@ -2124,15 +1550,6 @@
2124
  "url": "https://github.com/sponsors/isaacs"
2125
  }
2126
  },
2127
- "node_modules/mrmime": {
2128
- "version": "2.0.1",
2129
- "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz",
2130
- "integrity": "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==",
2131
- "dev": true,
2132
- "engines": {
2133
- "node": ">=10"
2134
- }
2135
- },
2136
  "node_modules/ms": {
2137
  "version": "2.1.3",
2138
  "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
@@ -2168,42 +1585,12 @@
2168
  "dev": true,
2169
  "optional": true
2170
  },
2171
- "node_modules/parse5": {
2172
- "version": "8.0.0",
2173
- "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.0.tgz",
2174
- "integrity": "sha512-9m4m5GSgXjL4AjumKzq1Fgfp3Z8rsvjRNbnkVwfu2ImRqE5D0LnY2QfDen18FSY9C573YU5XxSapdHZTZ2WolA==",
2175
- "dev": true,
2176
- "dependencies": {
2177
- "entities": "^6.0.0"
2178
- },
2179
- "funding": {
2180
- "url": "https://github.com/inikulin/parse5?sponsor=1"
2181
- }
2182
- },
2183
- "node_modules/parse5/node_modules/entities": {
2184
- "version": "6.0.1",
2185
- "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz",
2186
- "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==",
2187
- "dev": true,
2188
- "engines": {
2189
- "node": ">=0.12"
2190
- },
2191
- "funding": {
2192
- "url": "https://github.com/fb55/entities?sponsor=1"
2193
- }
2194
- },
2195
  "node_modules/path-browserify": {
2196
  "version": "1.0.1",
2197
  "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz",
2198
  "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==",
2199
  "dev": true
2200
  },
2201
- "node_modules/pathe": {
2202
- "version": "2.0.3",
2203
- "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz",
2204
- "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==",
2205
- "dev": true
2206
- },
2207
  "node_modules/picocolors": {
2208
  "version": "1.1.1",
2209
  "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
@@ -2270,15 +1657,6 @@
2270
  "node": "^10 || ^12 || >=14"
2271
  }
2272
  },
2273
- "node_modules/punycode": {
2274
- "version": "2.3.1",
2275
- "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
2276
- "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
2277
- "dev": true,
2278
- "engines": {
2279
- "node": ">=6"
2280
- }
2281
- },
2282
  "node_modules/readdirp": {
2283
  "version": "4.1.2",
2284
  "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz",
@@ -2293,15 +1671,6 @@
2293
  "url": "https://paulmillr.com/funding/"
2294
  }
2295
  },
2296
- "node_modules/require-from-string": {
2297
- "version": "2.0.2",
2298
- "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
2299
- "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",
2300
- "dev": true,
2301
- "engines": {
2302
- "node": ">=0.10.0"
2303
- }
2304
- },
2305
  "node_modules/rollup": {
2306
  "version": "4.52.5",
2307
  "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.52.5.tgz",
@@ -2352,12 +1721,6 @@
2352
  "tslib": "^2.1.0"
2353
  }
2354
  },
2355
- "node_modules/safer-buffer": {
2356
- "version": "2.1.2",
2357
- "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
2358
- "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
2359
- "dev": true
2360
- },
2361
  "node_modules/sass": {
2362
  "version": "1.93.2",
2363
  "resolved": "https://registry.npmjs.org/sass/-/sass-1.93.2.tgz",
@@ -2709,38 +2072,6 @@
2709
  "node": ">=14.0.0"
2710
  }
2711
  },
2712
- "node_modules/saxes": {
2713
- "version": "6.0.0",
2714
- "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz",
2715
- "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==",
2716
- "dev": true,
2717
- "dependencies": {
2718
- "xmlchars": "^2.2.0"
2719
- },
2720
- "engines": {
2721
- "node": ">=v12.22.7"
2722
- }
2723
- },
2724
- "node_modules/siginfo": {
2725
- "version": "2.0.0",
2726
- "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz",
2727
- "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==",
2728
- "dev": true
2729
- },
2730
- "node_modules/sirv": {
2731
- "version": "3.0.2",
2732
- "resolved": "https://registry.npmjs.org/sirv/-/sirv-3.0.2.tgz",
2733
- "integrity": "sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==",
2734
- "dev": true,
2735
- "dependencies": {
2736
- "@polka/url": "^1.0.0-next.24",
2737
- "mrmime": "^2.0.0",
2738
- "totalist": "^3.0.0"
2739
- },
2740
- "engines": {
2741
- "node": ">=18"
2742
- }
2743
- },
2744
  "node_modules/socket.io-client": {
2745
  "version": "4.8.1",
2746
  "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.8.1.tgz",
@@ -2775,18 +2106,6 @@
2775
  "node": ">=0.10.0"
2776
  }
2777
  },
2778
- "node_modules/stackback": {
2779
- "version": "0.0.2",
2780
- "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz",
2781
- "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==",
2782
- "dev": true
2783
- },
2784
- "node_modules/std-env": {
2785
- "version": "3.10.0",
2786
- "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz",
2787
- "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==",
2788
- "dev": true
2789
- },
2790
  "node_modules/supports-color": {
2791
  "version": "8.1.1",
2792
  "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz",
@@ -2802,12 +2121,6 @@
2802
  "url": "https://github.com/chalk/supports-color?sponsor=1"
2803
  }
2804
  },
2805
- "node_modules/symbol-tree": {
2806
- "version": "3.2.4",
2807
- "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz",
2808
- "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==",
2809
- "dev": true
2810
- },
2811
  "node_modules/sync-child-process": {
2812
  "version": "1.0.2",
2813
  "resolved": "https://registry.npmjs.org/sync-child-process/-/sync-child-process-1.0.2.tgz",
@@ -2834,90 +2147,6 @@
2834
  "resolved": "https://registry.npmjs.org/three/-/three-0.156.1.tgz",
2835
  "integrity": "sha512-kP7H0FK9d/k6t/XvQ9FO6i+QrePoDcNhwl0I02+wmUJRNSLCUIDMcfObnzQvxb37/0Uc9TDT0T1HgsRRrO6SYQ=="
2836
  },
2837
- "node_modules/tinybench": {
2838
- "version": "2.9.0",
2839
- "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz",
2840
- "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==",
2841
- "dev": true
2842
- },
2843
- "node_modules/tinyexec": {
2844
- "version": "0.3.2",
2845
- "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz",
2846
- "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==",
2847
- "dev": true
2848
- },
2849
- "node_modules/tinyglobby": {
2850
- "version": "0.2.15",
2851
- "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
2852
- "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==",
2853
- "dev": true,
2854
- "dependencies": {
2855
- "fdir": "^6.5.0",
2856
- "picomatch": "^4.0.3"
2857
- },
2858
- "engines": {
2859
- "node": ">=12.0.0"
2860
- },
2861
- "funding": {
2862
- "url": "https://github.com/sponsors/SuperchupuDev"
2863
- }
2864
- },
2865
- "node_modules/tinyglobby/node_modules/fdir": {
2866
- "version": "6.5.0",
2867
- "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
2868
- "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
2869
- "dev": true,
2870
- "engines": {
2871
- "node": ">=12.0.0"
2872
- },
2873
- "peerDependencies": {
2874
- "picomatch": "^3 || ^4"
2875
- },
2876
- "peerDependenciesMeta": {
2877
- "picomatch": {
2878
- "optional": true
2879
- }
2880
- }
2881
- },
2882
- "node_modules/tinyglobby/node_modules/picomatch": {
2883
- "version": "4.0.3",
2884
- "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
2885
- "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
2886
- "dev": true,
2887
- "engines": {
2888
- "node": ">=12"
2889
- },
2890
- "funding": {
2891
- "url": "https://github.com/sponsors/jonschlinkert"
2892
- }
2893
- },
2894
- "node_modules/tinyrainbow": {
2895
- "version": "3.0.3",
2896
- "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.0.3.tgz",
2897
- "integrity": "sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q==",
2898
- "dev": true,
2899
- "engines": {
2900
- "node": ">=14.0.0"
2901
- }
2902
- },
2903
- "node_modules/tldts": {
2904
- "version": "7.0.17",
2905
- "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.0.17.tgz",
2906
- "integrity": "sha512-Y1KQBgDd/NUc+LfOtKS6mNsC9CCaH+m2P1RoIZy7RAPo3C3/t8X45+zgut31cRZtZ3xKPjfn3TkGTrctC2TQIQ==",
2907
- "dev": true,
2908
- "dependencies": {
2909
- "tldts-core": "^7.0.17"
2910
- },
2911
- "bin": {
2912
- "tldts": "bin/cli.js"
2913
- }
2914
- },
2915
- "node_modules/tldts-core": {
2916
- "version": "7.0.17",
2917
- "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.0.17.tgz",
2918
- "integrity": "sha512-DieYoGrP78PWKsrXr8MZwtQ7GLCUeLxihtjC1jZsW1DnvSMdKPitJSe8OSYDM2u5H6g3kWJZpePqkp43TfLh0g==",
2919
- "dev": true
2920
- },
2921
  "node_modules/to-regex-range": {
2922
  "version": "5.0.1",
2923
  "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
@@ -2931,39 +2160,6 @@
2931
  "node": ">=8.0"
2932
  }
2933
  },
2934
- "node_modules/totalist": {
2935
- "version": "3.0.1",
2936
- "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz",
2937
- "integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==",
2938
- "dev": true,
2939
- "engines": {
2940
- "node": ">=6"
2941
- }
2942
- },
2943
- "node_modules/tough-cookie": {
2944
- "version": "6.0.0",
2945
- "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.0.tgz",
2946
- "integrity": "sha512-kXuRi1mtaKMrsLUxz3sQYvVl37B0Ns6MzfrtV5DvJceE9bPyspOqk9xxv7XbZWcfLWbFmm997vl83qUWVJA64w==",
2947
- "dev": true,
2948
- "dependencies": {
2949
- "tldts": "^7.0.5"
2950
- },
2951
- "engines": {
2952
- "node": ">=16"
2953
- }
2954
- },
2955
- "node_modules/tr46": {
2956
- "version": "6.0.0",
2957
- "resolved": "https://registry.npmjs.org/tr46/-/tr46-6.0.0.tgz",
2958
- "integrity": "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==",
2959
- "dev": true,
2960
- "dependencies": {
2961
- "punycode": "^2.3.1"
2962
- },
2963
- "engines": {
2964
- "node": ">=20"
2965
- }
2966
- },
2967
  "node_modules/tslib": {
2968
  "version": "2.8.1",
2969
  "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
@@ -2983,6 +2179,14 @@
2983
  "node": ">=14.17"
2984
  }
2985
  },
 
 
 
 
 
 
 
 
2986
  "node_modules/varint": {
2987
  "version": "6.0.0",
2988
  "resolved": "https://registry.npmjs.org/varint/-/varint-6.0.0.tgz",
@@ -3048,694 +2252,53 @@
3048
  }
3049
  }
3050
  },
3051
- "node_modules/vitest": {
3052
- "version": "4.0.6",
3053
- "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.0.6.tgz",
3054
- "integrity": "sha512-gR7INfiVRwnEOkCk47faros/9McCZMp5LM+OMNWGLaDBSvJxIzwjgNFufkuePBNaesGRnLmNfW+ddbUJRZn0nQ==",
3055
- "dev": true,
 
 
 
 
 
3056
  "dependencies": {
3057
- "@vitest/expect": "4.0.6",
3058
- "@vitest/mocker": "4.0.6",
3059
- "@vitest/pretty-format": "4.0.6",
3060
- "@vitest/runner": "4.0.6",
3061
- "@vitest/snapshot": "4.0.6",
3062
- "@vitest/spy": "4.0.6",
3063
- "@vitest/utils": "4.0.6",
3064
- "debug": "^4.4.3",
3065
- "es-module-lexer": "^1.7.0",
3066
- "expect-type": "^1.2.2",
3067
- "magic-string": "^0.30.19",
3068
- "pathe": "^2.0.3",
3069
- "picomatch": "^4.0.3",
3070
- "std-env": "^3.9.0",
3071
- "tinybench": "^2.9.0",
3072
- "tinyexec": "^0.3.2",
3073
- "tinyglobby": "^0.2.15",
3074
- "tinyrainbow": "^3.0.3",
3075
- "vite": "^6.0.0 || ^7.0.0",
3076
- "why-is-node-running": "^2.3.0"
3077
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3078
  "bin": {
3079
- "vitest": "vitest.mjs"
 
3080
  },
3081
  "engines": {
3082
- "node": "^20.0.0 || ^22.0.0 || >=24.0.0"
3083
  },
3084
  "funding": {
3085
- "url": "https://opencollective.com/vitest"
3086
  },
3087
  "peerDependencies": {
3088
- "@edge-runtime/vm": "*",
3089
- "@types/debug": "^4.1.12",
3090
- "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0",
3091
- "@vitest/browser-playwright": "4.0.6",
3092
- "@vitest/browser-preview": "4.0.6",
3093
- "@vitest/browser-webdriverio": "4.0.6",
3094
- "@vitest/ui": "4.0.6",
3095
- "happy-dom": "*",
3096
- "jsdom": "*"
3097
  },
3098
  "peerDependenciesMeta": {
3099
- "@edge-runtime/vm": {
3100
- "optional": true
3101
- },
3102
- "@types/debug": {
3103
- "optional": true
3104
- },
3105
- "@types/node": {
3106
- "optional": true
3107
- },
3108
- "@vitest/browser-playwright": {
3109
- "optional": true
3110
- },
3111
- "@vitest/browser-preview": {
3112
- "optional": true
3113
- },
3114
- "@vitest/browser-webdriverio": {
3115
- "optional": true
3116
- },
3117
- "@vitest/ui": {
3118
- "optional": true
3119
- },
3120
- "happy-dom": {
3121
- "optional": true
3122
- },
3123
- "jsdom": {
3124
- "optional": true
3125
- }
3126
- }
3127
- },
3128
- "node_modules/vitest/node_modules/@esbuild/aix-ppc64": {
3129
- "version": "0.25.12",
3130
- "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz",
3131
- "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==",
3132
- "cpu": [
3133
- "ppc64"
3134
- ],
3135
- "dev": true,
3136
- "optional": true,
3137
- "os": [
3138
- "aix"
3139
- ],
3140
- "engines": {
3141
- "node": ">=18"
3142
- }
3143
- },
3144
- "node_modules/vitest/node_modules/@esbuild/android-arm": {
3145
- "version": "0.25.12",
3146
- "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz",
3147
- "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==",
3148
- "cpu": [
3149
- "arm"
3150
- ],
3151
- "dev": true,
3152
- "optional": true,
3153
- "os": [
3154
- "android"
3155
- ],
3156
- "engines": {
3157
- "node": ">=18"
3158
- }
3159
- },
3160
- "node_modules/vitest/node_modules/@esbuild/android-arm64": {
3161
- "version": "0.25.12",
3162
- "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz",
3163
- "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==",
3164
- "cpu": [
3165
- "arm64"
3166
- ],
3167
- "dev": true,
3168
- "optional": true,
3169
- "os": [
3170
- "android"
3171
- ],
3172
- "engines": {
3173
- "node": ">=18"
3174
- }
3175
- },
3176
- "node_modules/vitest/node_modules/@esbuild/android-x64": {
3177
- "version": "0.25.12",
3178
- "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz",
3179
- "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==",
3180
- "cpu": [
3181
- "x64"
3182
- ],
3183
- "dev": true,
3184
- "optional": true,
3185
- "os": [
3186
- "android"
3187
- ],
3188
- "engines": {
3189
- "node": ">=18"
3190
- }
3191
- },
3192
- "node_modules/vitest/node_modules/@esbuild/darwin-arm64": {
3193
- "version": "0.25.12",
3194
- "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz",
3195
- "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==",
3196
- "cpu": [
3197
- "arm64"
3198
- ],
3199
- "dev": true,
3200
- "optional": true,
3201
- "os": [
3202
- "darwin"
3203
- ],
3204
- "engines": {
3205
- "node": ">=18"
3206
- }
3207
- },
3208
- "node_modules/vitest/node_modules/@esbuild/darwin-x64": {
3209
- "version": "0.25.12",
3210
- "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz",
3211
- "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==",
3212
- "cpu": [
3213
- "x64"
3214
- ],
3215
- "dev": true,
3216
- "optional": true,
3217
- "os": [
3218
- "darwin"
3219
- ],
3220
- "engines": {
3221
- "node": ">=18"
3222
- }
3223
- },
3224
- "node_modules/vitest/node_modules/@esbuild/freebsd-arm64": {
3225
- "version": "0.25.12",
3226
- "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz",
3227
- "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==",
3228
- "cpu": [
3229
- "arm64"
3230
- ],
3231
- "dev": true,
3232
- "optional": true,
3233
- "os": [
3234
- "freebsd"
3235
- ],
3236
- "engines": {
3237
- "node": ">=18"
3238
- }
3239
- },
3240
- "node_modules/vitest/node_modules/@esbuild/freebsd-x64": {
3241
- "version": "0.25.12",
3242
- "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz",
3243
- "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==",
3244
- "cpu": [
3245
- "x64"
3246
- ],
3247
- "dev": true,
3248
- "optional": true,
3249
- "os": [
3250
- "freebsd"
3251
- ],
3252
- "engines": {
3253
- "node": ">=18"
3254
- }
3255
- },
3256
- "node_modules/vitest/node_modules/@esbuild/linux-arm": {
3257
- "version": "0.25.12",
3258
- "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz",
3259
- "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==",
3260
- "cpu": [
3261
- "arm"
3262
- ],
3263
- "dev": true,
3264
- "optional": true,
3265
- "os": [
3266
- "linux"
3267
- ],
3268
- "engines": {
3269
- "node": ">=18"
3270
- }
3271
- },
3272
- "node_modules/vitest/node_modules/@esbuild/linux-arm64": {
3273
- "version": "0.25.12",
3274
- "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz",
3275
- "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==",
3276
- "cpu": [
3277
- "arm64"
3278
- ],
3279
- "dev": true,
3280
- "optional": true,
3281
- "os": [
3282
- "linux"
3283
- ],
3284
- "engines": {
3285
- "node": ">=18"
3286
- }
3287
- },
3288
- "node_modules/vitest/node_modules/@esbuild/linux-ia32": {
3289
- "version": "0.25.12",
3290
- "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz",
3291
- "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==",
3292
- "cpu": [
3293
- "ia32"
3294
- ],
3295
- "dev": true,
3296
- "optional": true,
3297
- "os": [
3298
- "linux"
3299
- ],
3300
- "engines": {
3301
- "node": ">=18"
3302
- }
3303
- },
3304
- "node_modules/vitest/node_modules/@esbuild/linux-loong64": {
3305
- "version": "0.25.12",
3306
- "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz",
3307
- "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==",
3308
- "cpu": [
3309
- "loong64"
3310
- ],
3311
- "dev": true,
3312
- "optional": true,
3313
- "os": [
3314
- "linux"
3315
- ],
3316
- "engines": {
3317
- "node": ">=18"
3318
- }
3319
- },
3320
- "node_modules/vitest/node_modules/@esbuild/linux-mips64el": {
3321
- "version": "0.25.12",
3322
- "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz",
3323
- "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==",
3324
- "cpu": [
3325
- "mips64el"
3326
- ],
3327
- "dev": true,
3328
- "optional": true,
3329
- "os": [
3330
- "linux"
3331
- ],
3332
- "engines": {
3333
- "node": ">=18"
3334
- }
3335
- },
3336
- "node_modules/vitest/node_modules/@esbuild/linux-ppc64": {
3337
- "version": "0.25.12",
3338
- "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz",
3339
- "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==",
3340
- "cpu": [
3341
- "ppc64"
3342
- ],
3343
- "dev": true,
3344
- "optional": true,
3345
- "os": [
3346
- "linux"
3347
- ],
3348
- "engines": {
3349
- "node": ">=18"
3350
- }
3351
- },
3352
- "node_modules/vitest/node_modules/@esbuild/linux-riscv64": {
3353
- "version": "0.25.12",
3354
- "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz",
3355
- "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==",
3356
- "cpu": [
3357
- "riscv64"
3358
- ],
3359
- "dev": true,
3360
- "optional": true,
3361
- "os": [
3362
- "linux"
3363
- ],
3364
- "engines": {
3365
- "node": ">=18"
3366
- }
3367
- },
3368
- "node_modules/vitest/node_modules/@esbuild/linux-s390x": {
3369
- "version": "0.25.12",
3370
- "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz",
3371
- "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==",
3372
- "cpu": [
3373
- "s390x"
3374
- ],
3375
- "dev": true,
3376
- "optional": true,
3377
- "os": [
3378
- "linux"
3379
- ],
3380
- "engines": {
3381
- "node": ">=18"
3382
- }
3383
- },
3384
- "node_modules/vitest/node_modules/@esbuild/linux-x64": {
3385
- "version": "0.25.12",
3386
- "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz",
3387
- "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==",
3388
- "cpu": [
3389
- "x64"
3390
- ],
3391
- "dev": true,
3392
- "optional": true,
3393
- "os": [
3394
- "linux"
3395
- ],
3396
- "engines": {
3397
- "node": ">=18"
3398
- }
3399
- },
3400
- "node_modules/vitest/node_modules/@esbuild/netbsd-x64": {
3401
- "version": "0.25.12",
3402
- "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz",
3403
- "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==",
3404
- "cpu": [
3405
- "x64"
3406
- ],
3407
- "dev": true,
3408
- "optional": true,
3409
- "os": [
3410
- "netbsd"
3411
- ],
3412
- "engines": {
3413
- "node": ">=18"
3414
- }
3415
- },
3416
- "node_modules/vitest/node_modules/@esbuild/openbsd-x64": {
3417
- "version": "0.25.12",
3418
- "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz",
3419
- "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==",
3420
- "cpu": [
3421
- "x64"
3422
- ],
3423
- "dev": true,
3424
- "optional": true,
3425
- "os": [
3426
- "openbsd"
3427
- ],
3428
- "engines": {
3429
- "node": ">=18"
3430
- }
3431
- },
3432
- "node_modules/vitest/node_modules/@esbuild/sunos-x64": {
3433
- "version": "0.25.12",
3434
- "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz",
3435
- "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==",
3436
- "cpu": [
3437
- "x64"
3438
- ],
3439
- "dev": true,
3440
- "optional": true,
3441
- "os": [
3442
- "sunos"
3443
- ],
3444
- "engines": {
3445
- "node": ">=18"
3446
- }
3447
- },
3448
- "node_modules/vitest/node_modules/@esbuild/win32-arm64": {
3449
- "version": "0.25.12",
3450
- "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz",
3451
- "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==",
3452
- "cpu": [
3453
- "arm64"
3454
- ],
3455
- "dev": true,
3456
- "optional": true,
3457
- "os": [
3458
- "win32"
3459
- ],
3460
- "engines": {
3461
- "node": ">=18"
3462
- }
3463
- },
3464
- "node_modules/vitest/node_modules/@esbuild/win32-ia32": {
3465
- "version": "0.25.12",
3466
- "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz",
3467
- "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==",
3468
- "cpu": [
3469
- "ia32"
3470
- ],
3471
- "dev": true,
3472
- "optional": true,
3473
- "os": [
3474
- "win32"
3475
- ],
3476
- "engines": {
3477
- "node": ">=18"
3478
- }
3479
- },
3480
- "node_modules/vitest/node_modules/@esbuild/win32-x64": {
3481
- "version": "0.25.12",
3482
- "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz",
3483
- "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==",
3484
- "cpu": [
3485
- "x64"
3486
- ],
3487
- "dev": true,
3488
- "optional": true,
3489
- "os": [
3490
- "win32"
3491
- ],
3492
- "engines": {
3493
- "node": ">=18"
3494
- }
3495
- },
3496
- "node_modules/vitest/node_modules/@vitest/mocker": {
3497
- "version": "4.0.6",
3498
- "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.0.6.tgz",
3499
- "integrity": "sha512-3COEIew5HqdzBFEYN9+u0dT3i/NCwppLnO1HkjGfAP1Vs3vti1Hxm/MvcbC4DAn3Szo1M7M3otiAaT83jvqIjA==",
3500
- "dev": true,
3501
- "dependencies": {
3502
- "@vitest/spy": "4.0.6",
3503
- "estree-walker": "^3.0.3",
3504
- "magic-string": "^0.30.19"
3505
- },
3506
- "funding": {
3507
- "url": "https://opencollective.com/vitest"
3508
- },
3509
- "peerDependencies": {
3510
- "msw": "^2.4.9",
3511
- "vite": "^6.0.0 || ^7.0.0-0"
3512
- },
3513
- "peerDependenciesMeta": {
3514
- "msw": {
3515
- "optional": true
3516
- },
3517
- "vite": {
3518
- "optional": true
3519
- }
3520
- }
3521
- },
3522
- "node_modules/vitest/node_modules/debug": {
3523
- "version": "4.4.3",
3524
- "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
3525
- "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
3526
- "dev": true,
3527
- "dependencies": {
3528
- "ms": "^2.1.3"
3529
- },
3530
- "engines": {
3531
- "node": ">=6.0"
3532
- },
3533
- "peerDependenciesMeta": {
3534
- "supports-color": {
3535
- "optional": true
3536
- }
3537
- }
3538
- },
3539
- "node_modules/vitest/node_modules/esbuild": {
3540
- "version": "0.25.12",
3541
- "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz",
3542
- "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==",
3543
- "dev": true,
3544
- "hasInstallScript": true,
3545
- "bin": {
3546
- "esbuild": "bin/esbuild"
3547
- },
3548
- "engines": {
3549
- "node": ">=18"
3550
- },
3551
- "optionalDependencies": {
3552
- "@esbuild/aix-ppc64": "0.25.12",
3553
- "@esbuild/android-arm": "0.25.12",
3554
- "@esbuild/android-arm64": "0.25.12",
3555
- "@esbuild/android-x64": "0.25.12",
3556
- "@esbuild/darwin-arm64": "0.25.12",
3557
- "@esbuild/darwin-x64": "0.25.12",
3558
- "@esbuild/freebsd-arm64": "0.25.12",
3559
- "@esbuild/freebsd-x64": "0.25.12",
3560
- "@esbuild/linux-arm": "0.25.12",
3561
- "@esbuild/linux-arm64": "0.25.12",
3562
- "@esbuild/linux-ia32": "0.25.12",
3563
- "@esbuild/linux-loong64": "0.25.12",
3564
- "@esbuild/linux-mips64el": "0.25.12",
3565
- "@esbuild/linux-ppc64": "0.25.12",
3566
- "@esbuild/linux-riscv64": "0.25.12",
3567
- "@esbuild/linux-s390x": "0.25.12",
3568
- "@esbuild/linux-x64": "0.25.12",
3569
- "@esbuild/netbsd-arm64": "0.25.12",
3570
- "@esbuild/netbsd-x64": "0.25.12",
3571
- "@esbuild/openbsd-arm64": "0.25.12",
3572
- "@esbuild/openbsd-x64": "0.25.12",
3573
- "@esbuild/openharmony-arm64": "0.25.12",
3574
- "@esbuild/sunos-x64": "0.25.12",
3575
- "@esbuild/win32-arm64": "0.25.12",
3576
- "@esbuild/win32-ia32": "0.25.12",
3577
- "@esbuild/win32-x64": "0.25.12"
3578
- }
3579
- },
3580
- "node_modules/vitest/node_modules/estree-walker": {
3581
- "version": "3.0.3",
3582
- "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz",
3583
- "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==",
3584
- "dev": true,
3585
- "dependencies": {
3586
- "@types/estree": "^1.0.0"
3587
- }
3588
- },
3589
- "node_modules/vitest/node_modules/fdir": {
3590
- "version": "6.5.0",
3591
- "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
3592
- "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
3593
- "dev": true,
3594
- "engines": {
3595
- "node": ">=12.0.0"
3596
- },
3597
- "peerDependencies": {
3598
- "picomatch": "^3 || ^4"
3599
- },
3600
- "peerDependenciesMeta": {
3601
- "picomatch": {
3602
- "optional": true
3603
- }
3604
- }
3605
- },
3606
- "node_modules/vitest/node_modules/picomatch": {
3607
- "version": "4.0.3",
3608
- "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
3609
- "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
3610
- "dev": true,
3611
- "engines": {
3612
- "node": ">=12"
3613
- },
3614
- "funding": {
3615
- "url": "https://github.com/sponsors/jonschlinkert"
3616
- }
3617
- },
3618
- "node_modules/vitest/node_modules/vite": {
3619
- "version": "7.1.12",
3620
- "resolved": "https://registry.npmjs.org/vite/-/vite-7.1.12.tgz",
3621
- "integrity": "sha512-ZWyE8YXEXqJrrSLvYgrRP7p62OziLW7xI5HYGWFzOvupfAlrLvURSzv/FyGyy0eidogEM3ujU+kUG1zuHgb6Ug==",
3622
- "dev": true,
3623
- "dependencies": {
3624
- "esbuild": "^0.25.0",
3625
- "fdir": "^6.5.0",
3626
- "picomatch": "^4.0.3",
3627
- "postcss": "^8.5.6",
3628
- "rollup": "^4.43.0",
3629
- "tinyglobby": "^0.2.15"
3630
- },
3631
- "bin": {
3632
- "vite": "bin/vite.js"
3633
- },
3634
- "engines": {
3635
- "node": "^20.19.0 || >=22.12.0"
3636
- },
3637
- "funding": {
3638
- "url": "https://github.com/vitejs/vite?sponsor=1"
3639
- },
3640
- "optionalDependencies": {
3641
- "fsevents": "~2.3.3"
3642
- },
3643
- "peerDependencies": {
3644
- "@types/node": "^20.19.0 || >=22.12.0",
3645
- "jiti": ">=1.21.0",
3646
- "less": "^4.0.0",
3647
- "lightningcss": "^1.21.0",
3648
- "sass": "^1.70.0",
3649
- "sass-embedded": "^1.70.0",
3650
- "stylus": ">=0.54.8",
3651
- "sugarss": "^5.0.0",
3652
- "terser": "^5.16.0",
3653
- "tsx": "^4.8.1",
3654
- "yaml": "^2.4.2"
3655
- },
3656
- "peerDependenciesMeta": {
3657
- "@types/node": {
3658
- "optional": true
3659
- },
3660
- "jiti": {
3661
- "optional": true
3662
- },
3663
- "less": {
3664
- "optional": true
3665
- },
3666
- "lightningcss": {
3667
- "optional": true
3668
- },
3669
- "sass": {
3670
- "optional": true
3671
- },
3672
- "sass-embedded": {
3673
- "optional": true
3674
- },
3675
- "stylus": {
3676
- "optional": true
3677
- },
3678
- "sugarss": {
3679
- "optional": true
3680
- },
3681
- "terser": {
3682
- "optional": true
3683
- },
3684
- "tsx": {
3685
- "optional": true
3686
- },
3687
- "yaml": {
3688
- "optional": true
3689
- }
3690
- }
3691
- },
3692
- "node_modules/vscode-uri": {
3693
- "version": "3.1.0",
3694
- "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.1.0.tgz",
3695
- "integrity": "sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==",
3696
- "dev": true
3697
- },
3698
- "node_modules/vue": {
3699
- "version": "3.5.22",
3700
- "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.22.tgz",
3701
- "integrity": "sha512-toaZjQ3a/G/mYaLSbV+QsQhIdMo9x5rrqIpYRObsJ6T/J+RyCSFwN2LHNVH9v8uIcljDNa3QzPVdv3Y6b9hAJQ==",
3702
- "dependencies": {
3703
- "@vue/compiler-dom": "3.5.22",
3704
- "@vue/compiler-sfc": "3.5.22",
3705
- "@vue/runtime-dom": "3.5.22",
3706
- "@vue/server-renderer": "3.5.22",
3707
- "@vue/shared": "3.5.22"
3708
- },
3709
- "peerDependencies": {
3710
- "typescript": "*"
3711
- },
3712
- "peerDependenciesMeta": {
3713
- "typescript": {
3714
- "optional": true
3715
- }
3716
- }
3717
- },
3718
- "node_modules/vue-demi": {
3719
- "version": "0.14.10",
3720
- "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.10.tgz",
3721
- "integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==",
3722
- "hasInstallScript": true,
3723
- "bin": {
3724
- "vue-demi-fix": "bin/vue-demi-fix.js",
3725
- "vue-demi-switch": "bin/vue-demi-switch.js"
3726
- },
3727
- "engines": {
3728
- "node": ">=12"
3729
- },
3730
- "funding": {
3731
- "url": "https://github.com/sponsors/antfu"
3732
- },
3733
- "peerDependencies": {
3734
- "@vue/composition-api": "^1.0.0-rc.1",
3735
- "vue": "^3.0.0-0 || ^2.6.0"
3736
- },
3737
- "peerDependenciesMeta": {
3738
- "@vue/composition-api": {
3739
  "optional": true
3740
  }
3741
  }
@@ -3770,77 +2333,6 @@
3770
  "typescript": ">=5.0.0"
3771
  }
3772
  },
3773
- "node_modules/w3c-xmlserializer": {
3774
- "version": "5.0.0",
3775
- "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz",
3776
- "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==",
3777
- "dev": true,
3778
- "dependencies": {
3779
- "xml-name-validator": "^5.0.0"
3780
- },
3781
- "engines": {
3782
- "node": ">=18"
3783
- }
3784
- },
3785
- "node_modules/webidl-conversions": {
3786
- "version": "8.0.0",
3787
- "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.0.tgz",
3788
- "integrity": "sha512-n4W4YFyz5JzOfQeA8oN7dUYpR+MBP3PIUsn2jLjWXwK5ASUzt0Jc/A5sAUZoCYFJRGF0FBKJ+1JjN43rNdsQzA==",
3789
- "dev": true,
3790
- "engines": {
3791
- "node": ">=20"
3792
- }
3793
- },
3794
- "node_modules/whatwg-encoding": {
3795
- "version": "3.1.1",
3796
- "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz",
3797
- "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==",
3798
- "dev": true,
3799
- "dependencies": {
3800
- "iconv-lite": "0.6.3"
3801
- },
3802
- "engines": {
3803
- "node": ">=18"
3804
- }
3805
- },
3806
- "node_modules/whatwg-mimetype": {
3807
- "version": "4.0.0",
3808
- "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz",
3809
- "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==",
3810
- "dev": true,
3811
- "engines": {
3812
- "node": ">=18"
3813
- }
3814
- },
3815
- "node_modules/whatwg-url": {
3816
- "version": "15.1.0",
3817
- "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-15.1.0.tgz",
3818
- "integrity": "sha512-2ytDk0kiEj/yu90JOAp44PVPUkO9+jVhyf+SybKlRHSDlvOOZhdPIrr7xTH64l4WixO2cP+wQIcgujkGBPPz6g==",
3819
- "dev": true,
3820
- "dependencies": {
3821
- "tr46": "^6.0.0",
3822
- "webidl-conversions": "^8.0.0"
3823
- },
3824
- "engines": {
3825
- "node": ">=20"
3826
- }
3827
- },
3828
- "node_modules/why-is-node-running": {
3829
- "version": "2.3.0",
3830
- "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz",
3831
- "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==",
3832
- "dev": true,
3833
- "dependencies": {
3834
- "siginfo": "^2.0.0",
3835
- "stackback": "0.0.2"
3836
- },
3837
- "bin": {
3838
- "why-is-node-running": "cli.js"
3839
- },
3840
- "engines": {
3841
- "node": ">=8"
3842
- }
3843
- },
3844
  "node_modules/ws": {
3845
  "version": "8.17.1",
3846
  "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz",
@@ -3861,21 +2353,6 @@
3861
  }
3862
  }
3863
  },
3864
- "node_modules/xml-name-validator": {
3865
- "version": "5.0.0",
3866
- "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz",
3867
- "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==",
3868
- "dev": true,
3869
- "engines": {
3870
- "node": ">=18"
3871
- }
3872
- },
3873
- "node_modules/xmlchars": {
3874
- "version": "2.2.0",
3875
- "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz",
3876
- "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==",
3877
- "dev": true
3878
- },
3879
  "node_modules/xmlhttprequest-ssl": {
3880
  "version": "2.1.2",
3881
  "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.1.2.tgz",
 
17
  "devDependencies": {
18
  "@types/three": "^0.156.0",
19
  "@vitejs/plugin-vue": "^5.2.4",
 
 
20
  "sass-embedded": "^1.93.2",
21
  "typescript": "^5.2.2",
22
  "vite": "^5.4.21",
 
23
  "vue-tsc": "^2.2.12"
24
  }
25
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
26
  "node_modules/@babel/helper-string-parser": {
27
  "version": "7.27.1",
28
  "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
 
71
  "integrity": "sha512-fdRs9PSrBF7QUntpZpq6BTw58fhgGJojgg39m9oFOJGZT+nip9b0so5cYY1oWl5pvemDLr0cPPsH46vwThEbpQ==",
72
  "dev": true
73
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
74
  "node_modules/@esbuild/aix-ppc64": {
75
  "version": "0.21.5",
76
  "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz",
 
343
  "node": ">=12"
344
  }
345
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
346
  "node_modules/@esbuild/netbsd-x64": {
347
  "version": "0.21.5",
348
  "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz",
 
359
  "node": ">=12"
360
  }
361
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
362
  "node_modules/@esbuild/openbsd-x64": {
363
  "version": "0.21.5",
364
  "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz",
 
375
  "node": ">=12"
376
  }
377
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
378
  "node_modules/@esbuild/sunos-x64": {
379
  "version": "0.21.5",
380
  "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz",
 
740
  "url": "https://opencollective.com/parcel"
741
  }
742
  },
 
 
 
 
 
 
743
  "node_modules/@rollup/rollup-android-arm-eabi": {
744
  "version": "4.52.5",
745
  "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.52.5.tgz",
 
1031
  "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz",
1032
  "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA=="
1033
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1034
  "node_modules/@types/estree": {
1035
  "version": "1.0.8",
1036
  "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
1037
  "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
1038
  "dev": true
1039
  },
1040
+ "node_modules/@types/node": {
1041
+ "version": "24.10.1",
1042
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.1.tgz",
1043
+ "integrity": "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==",
1044
+ "dev": true,
1045
+ "optional": true,
1046
+ "peer": true,
1047
+ "dependencies": {
1048
+ "undici-types": "~7.16.0"
1049
+ }
1050
+ },
1051
  "node_modules/@types/stats.js": {
1052
  "version": "0.17.4",
1053
  "resolved": "https://registry.npmjs.org/@types/stats.js/-/stats.js-0.17.4.tgz",
 
1085
  "vue": "^3.2.25"
1086
  }
1087
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1088
  "node_modules/@volar/language-core": {
1089
  "version": "2.4.15",
1090
  "resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-2.4.15.tgz",
 
1241
  "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.22.tgz",
1242
  "integrity": "sha512-F4yc6palwq3TT0u+FYf0Ns4Tfl9GRFURDN2gWG7L1ecIaS/4fCIuFOjMTnCyjsu/OK6vaDKLCrGAa+KvvH+h4w=="
1243
  },
 
 
 
 
 
 
 
 
 
1244
  "node_modules/alien-signals": {
1245
  "version": "1.0.13",
1246
  "resolved": "https://registry.npmjs.org/alien-signals/-/alien-signals-1.0.13.tgz",
1247
  "integrity": "sha512-OGj9yyTnJEttvzhTUWuscOvtqxq5vrhF7vL9oS0xJ2mK0ItPYP1/y+vCFebfxoEyAz0++1AIwJ5CMr+Fk3nDmg==",
1248
  "dev": true
1249
  },
 
 
 
 
 
 
 
 
 
1250
  "node_modules/balanced-match": {
1251
  "version": "1.0.2",
1252
  "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
1253
  "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
1254
  "dev": true
1255
  },
 
 
 
 
 
 
 
 
 
1256
  "node_modules/brace-expansion": {
1257
  "version": "2.0.2",
1258
  "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
 
1281
  "integrity": "sha512-7VPMEPuYznPSoR21NE1zvd2Xna6c/CloiZCfcMXR1Jny6PjX0N4Nsa38zcBFo/FMK+BlA+FLKbJCQ0i2yxp+Xg==",
1282
  "dev": true
1283
  },
 
 
 
 
 
 
 
 
 
1284
  "node_modules/chokidar": {
1285
  "version": "4.0.3",
1286
  "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz",
 
1303
  "integrity": "sha512-twmVoizEW7ylZSN32OgKdXRmo1qg+wT5/6C3xu5b9QsWzSFAhHLn2xd8ro0diCsKfCj1RdaTP/nrcW+vAoQPIw==",
1304
  "dev": true
1305
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1306
  "node_modules/csstype": {
1307
  "version": "3.1.3",
1308
  "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
1309
  "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="
1310
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
1311
  "node_modules/de-indent": {
1312
  "version": "1.0.2",
1313
  "resolved": "https://registry.npmjs.org/de-indent/-/de-indent-1.0.2.tgz",
 
1330
  }
1331
  }
1332
  },
 
 
 
 
 
 
1333
  "node_modules/detect-libc": {
1334
  "version": "1.0.3",
1335
  "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz",
 
1374
  "url": "https://github.com/fb55/entities?sponsor=1"
1375
  }
1376
  },
 
 
 
 
 
 
1377
  "node_modules/esbuild": {
1378
  "version": "0.21.5",
1379
  "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz",
 
1417
  "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz",
1418
  "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="
1419
  },
 
 
 
 
 
 
 
 
 
1420
  "node_modules/fflate": {
1421
  "version": "0.6.10",
1422
  "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.6.10.tgz",
 
1436
  "node": ">=8"
1437
  }
1438
  },
 
 
 
 
 
 
1439
  "node_modules/fsevents": {
1440
  "version": "2.3.3",
1441
  "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
 
1468
  "he": "bin/he"
1469
  }
1470
  },
1471
+ "node_modules/immutable": {
1472
+ "version": "5.1.4",
1473
+ "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.4.tgz",
1474
+ "integrity": "sha512-p6u1bG3YSnINT5RQmx/yRZBpenIl30kVxkTLDyHLIMk0gict704Q9n+thfDI7lTRm9vXdDYutVzXhzcThxTnXA==",
1475
+ "dev": true
1476
+ },
1477
+ "node_modules/is-extglob": {
1478
+ "version": "2.1.1",
1479
+ "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
1480
+ "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
1481
  "dev": true,
1482
+ "optional": true,
 
 
1483
  "engines": {
1484
+ "node": ">=0.10.0"
1485
  }
1486
  },
1487
+ "node_modules/is-glob": {
1488
+ "version": "4.0.3",
1489
+ "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
1490
+ "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
1491
  "dev": true,
1492
+ "optional": true,
1493
  "dependencies": {
1494
+ "is-extglob": "^2.1.1"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1495
  },
1496
  "engines": {
1497
  "node": ">=0.10.0"
 
1507
  "node": ">=0.12.0"
1508
  }
1509
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1510
  "node_modules/magic-string": {
1511
  "version": "0.30.19",
1512
  "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.19.tgz",
 
1515
  "@jridgewell/sourcemap-codec": "^1.5.5"
1516
  }
1517
  },
 
 
 
 
 
 
1518
  "node_modules/meshoptimizer": {
1519
  "version": "0.18.1",
1520
  "resolved": "https://registry.npmjs.org/meshoptimizer/-/meshoptimizer-0.18.1.tgz",
 
1550
  "url": "https://github.com/sponsors/isaacs"
1551
  }
1552
  },
 
 
 
 
 
 
 
 
 
1553
  "node_modules/ms": {
1554
  "version": "2.1.3",
1555
  "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
 
1585
  "dev": true,
1586
  "optional": true
1587
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1588
  "node_modules/path-browserify": {
1589
  "version": "1.0.1",
1590
  "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz",
1591
  "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==",
1592
  "dev": true
1593
  },
 
 
 
 
 
 
1594
  "node_modules/picocolors": {
1595
  "version": "1.1.1",
1596
  "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
 
1657
  "node": "^10 || ^12 || >=14"
1658
  }
1659
  },
 
 
 
 
 
 
 
 
 
1660
  "node_modules/readdirp": {
1661
  "version": "4.1.2",
1662
  "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz",
 
1671
  "url": "https://paulmillr.com/funding/"
1672
  }
1673
  },
 
 
 
 
 
 
 
 
 
1674
  "node_modules/rollup": {
1675
  "version": "4.52.5",
1676
  "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.52.5.tgz",
 
1721
  "tslib": "^2.1.0"
1722
  }
1723
  },
 
 
 
 
 
 
1724
  "node_modules/sass": {
1725
  "version": "1.93.2",
1726
  "resolved": "https://registry.npmjs.org/sass/-/sass-1.93.2.tgz",
 
2072
  "node": ">=14.0.0"
2073
  }
2074
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2075
  "node_modules/socket.io-client": {
2076
  "version": "4.8.1",
2077
  "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.8.1.tgz",
 
2106
  "node": ">=0.10.0"
2107
  }
2108
  },
 
 
 
 
 
 
 
 
 
 
 
 
2109
  "node_modules/supports-color": {
2110
  "version": "8.1.1",
2111
  "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz",
 
2121
  "url": "https://github.com/chalk/supports-color?sponsor=1"
2122
  }
2123
  },
 
 
 
 
 
 
2124
  "node_modules/sync-child-process": {
2125
  "version": "1.0.2",
2126
  "resolved": "https://registry.npmjs.org/sync-child-process/-/sync-child-process-1.0.2.tgz",
 
2147
  "resolved": "https://registry.npmjs.org/three/-/three-0.156.1.tgz",
2148
  "integrity": "sha512-kP7H0FK9d/k6t/XvQ9FO6i+QrePoDcNhwl0I02+wmUJRNSLCUIDMcfObnzQvxb37/0Uc9TDT0T1HgsRRrO6SYQ=="
2149
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2150
  "node_modules/to-regex-range": {
2151
  "version": "5.0.1",
2152
  "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
 
2160
  "node": ">=8.0"
2161
  }
2162
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2163
  "node_modules/tslib": {
2164
  "version": "2.8.1",
2165
  "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
 
2179
  "node": ">=14.17"
2180
  }
2181
  },
2182
+ "node_modules/undici-types": {
2183
+ "version": "7.16.0",
2184
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz",
2185
+ "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==",
2186
+ "dev": true,
2187
+ "optional": true,
2188
+ "peer": true
2189
+ },
2190
  "node_modules/varint": {
2191
  "version": "6.0.0",
2192
  "resolved": "https://registry.npmjs.org/varint/-/varint-6.0.0.tgz",
 
2252
  }
2253
  }
2254
  },
2255
+ "node_modules/vscode-uri": {
2256
+ "version": "3.1.0",
2257
+ "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.1.0.tgz",
2258
+ "integrity": "sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==",
2259
+ "dev": true
2260
+ },
2261
+ "node_modules/vue": {
2262
+ "version": "3.5.22",
2263
+ "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.22.tgz",
2264
+ "integrity": "sha512-toaZjQ3a/G/mYaLSbV+QsQhIdMo9x5rrqIpYRObsJ6T/J+RyCSFwN2LHNVH9v8uIcljDNa3QzPVdv3Y6b9hAJQ==",
2265
  "dependencies": {
2266
+ "@vue/compiler-dom": "3.5.22",
2267
+ "@vue/compiler-sfc": "3.5.22",
2268
+ "@vue/runtime-dom": "3.5.22",
2269
+ "@vue/server-renderer": "3.5.22",
2270
+ "@vue/shared": "3.5.22"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2271
  },
2272
+ "peerDependencies": {
2273
+ "typescript": "*"
2274
+ },
2275
+ "peerDependenciesMeta": {
2276
+ "typescript": {
2277
+ "optional": true
2278
+ }
2279
+ }
2280
+ },
2281
+ "node_modules/vue-demi": {
2282
+ "version": "0.14.10",
2283
+ "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.10.tgz",
2284
+ "integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==",
2285
+ "hasInstallScript": true,
2286
  "bin": {
2287
+ "vue-demi-fix": "bin/vue-demi-fix.js",
2288
+ "vue-demi-switch": "bin/vue-demi-switch.js"
2289
  },
2290
  "engines": {
2291
+ "node": ">=12"
2292
  },
2293
  "funding": {
2294
+ "url": "https://github.com/sponsors/antfu"
2295
  },
2296
  "peerDependencies": {
2297
+ "@vue/composition-api": "^1.0.0-rc.1",
2298
+ "vue": "^3.0.0-0 || ^2.6.0"
 
 
 
 
 
 
 
2299
  },
2300
  "peerDependenciesMeta": {
2301
+ "@vue/composition-api": {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2302
  "optional": true
2303
  }
2304
  }
 
2333
  "typescript": ">=5.0.0"
2334
  }
2335
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2336
  "node_modules/ws": {
2337
  "version": "8.17.1",
2338
  "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz",
 
2353
  }
2354
  }
2355
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2356
  "node_modules/xmlhttprequest-ssl": {
2357
  "version": "2.1.2",
2358
  "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.1.2.tgz",
trigo-web/app/src/App.vue CHANGED
@@ -1,10 +1,12 @@
1
  <template>
2
  <div id="app">
3
- <router-view />
4
  </div>
5
  </template>
6
 
7
- <script setup lang="ts"></script>
 
 
8
 
9
  <style lang="scss">
10
  * {
@@ -16,7 +18,8 @@
16
  html,
17
  body {
18
  height: 100%;
19
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial,
 
20
  sans-serif;
21
  }
22
 
@@ -24,4 +27,4 @@
24
  height: 100vh;
25
  background: #f5f5f5;
26
  }
27
- </style>
 
1
  <template>
2
  <div id="app">
3
+ <LayoutFrame />
4
  </div>
5
  </template>
6
 
7
+ <script setup lang="ts">
8
+ import LayoutFrame from "./components/LayoutFrame.vue";
9
+ </script>
10
 
11
  <style lang="scss">
12
  * {
 
18
  html,
19
  body {
20
  height: 100%;
21
+ font-family:
22
+ -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial,
23
  sans-serif;
24
  }
25
 
 
27
  height: 100vh;
28
  background: #f5f5f5;
29
  }
30
+ </style>
trigo-web/app/src/components/LayoutFrame.vue ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <template>
2
+ <div class="layout-frame">
3
+ <SidebarMenu />
4
+ <div class="content-area">
5
+ <router-view />
6
+ </div>
7
+ </div>
8
+ </template>
9
+
10
+ <script setup lang="ts">
11
+ import SidebarMenu from "./SidebarMenu.vue";
12
+ </script>
13
+
14
+ <style lang="scss" scoped>
15
+ .layout-frame {
16
+ display: flex;
17
+ height: 100vh;
18
+ width: 100vw;
19
+ background: #f5f5f5;
20
+ overflow: hidden;
21
+ }
22
+
23
+ .content-area {
24
+ flex: 1;
25
+ display: flex;
26
+ flex-direction: column;
27
+ overflow: hidden;
28
+ }
29
+ </style>
trigo-web/app/src/components/SidebarMenu.vue ADDED
@@ -0,0 +1,235 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <template>
2
+ <nav
3
+ class="sidebar-menu"
4
+ :class="{ expanded: isExpanded }"
5
+ @mouseenter="isExpanded = true"
6
+ @mouseleave="isExpanded = false"
7
+ >
8
+ <div
9
+ class="logo-section"
10
+ @click="handleLogoClick"
11
+ title="Click to test WebSocket connection"
12
+ >
13
+ <img src="@/assets/logo.png" alt="Trigo" class="logo clickable" />
14
+ <h1 class="app-title">Trigo</h1>
15
+ </div>
16
+
17
+ <div class="menu-tabs">
18
+ <router-link
19
+ v-for="tab in tabs"
20
+ :key="tab.path"
21
+ :to="tab.path"
22
+ class="tab-button"
23
+ :class="{ active: isActiveTab(tab.path) }"
24
+ >
25
+ <span class="tab-icon">{{ tab.icon }}</span>
26
+ <span class="tab-label">{{ tab.label }}</span>
27
+ </router-link>
28
+ </div>
29
+
30
+ <div class="sidebar-footer">
31
+ <p class="version">v1.0.0</p>
32
+ </div>
33
+ </nav>
34
+ </template>
35
+
36
+ <script setup lang="ts">
37
+ import { ref, computed } from "vue";
38
+ import { useRoute } from "vue-router";
39
+ import { useSocket } from "@/composables/useSocket";
40
+
41
+ interface Tab {
42
+ path: string;
43
+ label: string;
44
+ icon: string;
45
+ }
46
+
47
+ const tabs: Tab[] = [
48
+ { path: "/", label: "Single Game", icon: "🎮" },
49
+ { path: "/vs-ai", label: "VS AI", icon: "🤖" },
50
+ { path: "/vs-people", label: "VS People", icon: "👥" },
51
+ { path: "/library", label: "Game Library", icon: "📚" }
52
+ ];
53
+
54
+ const route = useRoute();
55
+ const isExpanded = ref(false);
56
+
57
+ const isActiveTab = (path: string): boolean => {
58
+ return route.path === path;
59
+ };
60
+
61
+ // WebSocket echo test
62
+ const { sendEcho, connected: socketConnected } = useSocket();
63
+
64
+ const handleLogoClick = async () => {
65
+ try {
66
+ console.log("[Echo Test] Sending echo request...");
67
+ console.log("[Echo Test] Socket connected:", socketConnected.value);
68
+
69
+ const response = await sendEcho("Echo test from Trigo sidebar!");
70
+ console.log("[Echo Test] Response:", response);
71
+
72
+ // Show alert with the response
73
+ alert(`WebSocket Echo Test\n\n${response}`);
74
+ } catch (error) {
75
+ console.error("[Echo Test] Error:", error);
76
+ alert(
77
+ `WebSocket Echo Test Failed\n\n${error instanceof Error ? error.message : String(error)}`
78
+ );
79
+ }
80
+ };
81
+ </script>
82
+
83
+ <style lang="scss" scoped>
84
+ .sidebar-menu {
85
+ width: 70px; // Collapsed width (icon only)
86
+ background: #2a2a2a;
87
+ display: flex;
88
+ flex-direction: column;
89
+ border-right: 1px solid #404040;
90
+ padding: 20px 0;
91
+ flex-shrink: 0;
92
+ transition: width 0.3s ease;
93
+ overflow: hidden;
94
+ position: relative;
95
+
96
+ &.expanded {
97
+ width: 220px; // Expanded width (icon + text)
98
+ }
99
+ }
100
+
101
+ .logo-section {
102
+ display: flex;
103
+ flex-direction: column;
104
+ align-items: center;
105
+ padding: 0 10px 30px;
106
+ border-bottom: 1px solid #404040;
107
+ margin-bottom: 20px;
108
+ min-height: 110px; // Prevent layout shift
109
+ cursor: pointer;
110
+ transition: background-color 0.2s ease;
111
+
112
+ &:hover {
113
+ background-color: rgba(255, 255, 255, 0.05);
114
+ }
115
+
116
+ &:active {
117
+ background-color: rgba(255, 255, 255, 0.1);
118
+ }
119
+ }
120
+
121
+ .logo {
122
+ width: 50px;
123
+ height: 50px;
124
+ margin-bottom: 12px;
125
+ border-radius: 8px;
126
+ flex-shrink: 0;
127
+ transition:
128
+ transform 0.2s ease,
129
+ opacity 0.2s ease;
130
+
131
+ &.clickable {
132
+ .logo-section:hover & {
133
+ transform: scale(1.1);
134
+ opacity: 0.9;
135
+ }
136
+
137
+ .logo-section:active & {
138
+ transform: scale(0.95);
139
+ }
140
+ }
141
+ }
142
+
143
+ .app-title {
144
+ font-size: 20px;
145
+ font-weight: 600;
146
+ color: #e0e0e0;
147
+ margin: 0;
148
+ white-space: nowrap;
149
+ opacity: 0;
150
+ transition: opacity 0.2s ease 0.1s;
151
+
152
+ .expanded & {
153
+ opacity: 1;
154
+ }
155
+ }
156
+
157
+ .menu-tabs {
158
+ flex: 1;
159
+ display: flex;
160
+ flex-direction: column;
161
+ gap: 4px;
162
+ padding: 0 10px;
163
+ }
164
+
165
+ .tab-button {
166
+ display: flex;
167
+ align-items: center;
168
+ gap: 12px;
169
+ padding: 14px 12px;
170
+ border-radius: 8px;
171
+ background: transparent;
172
+ color: #a0a0a0;
173
+ text-decoration: none;
174
+ font-size: 15px;
175
+ font-weight: 500;
176
+ cursor: pointer;
177
+ transition: all 0.2s ease;
178
+ border-left: 3px solid transparent;
179
+ position: relative;
180
+ white-space: nowrap;
181
+
182
+ &:hover {
183
+ background: #353535;
184
+ color: #e0e0e0;
185
+ }
186
+
187
+ &.active {
188
+ background: #3a3a3a;
189
+ color: #e94560;
190
+ border-left-color: #e94560;
191
+
192
+ .tab-icon {
193
+ transform: scale(1.1);
194
+ }
195
+ }
196
+ }
197
+
198
+ .tab-icon {
199
+ font-size: 22px;
200
+ line-height: 1;
201
+ transition: transform 0.2s ease;
202
+ min-width: 28px;
203
+ text-align: center;
204
+ flex-shrink: 0;
205
+ }
206
+
207
+ .tab-label {
208
+ flex: 1;
209
+ opacity: 0;
210
+ transition: opacity 0.2s ease 0.1s;
211
+
212
+ .expanded & {
213
+ opacity: 1;
214
+ }
215
+ }
216
+
217
+ .sidebar-footer {
218
+ padding: 20px 10px;
219
+ border-top: 1px solid #404040;
220
+ text-align: center;
221
+ }
222
+
223
+ .version {
224
+ font-size: 11px;
225
+ color: #606060;
226
+ margin: 0;
227
+ white-space: nowrap;
228
+ opacity: 0;
229
+ transition: opacity 0.2s ease 0.1s;
230
+
231
+ .expanded & {
232
+ opacity: 1;
233
+ }
234
+ }
235
+ </style>
trigo-web/app/src/composables/useSocket.ts CHANGED
@@ -1,24 +1,31 @@
1
  import { ref, onUnmounted } from "vue";
2
  import { io, Socket } from "socket.io-client";
3
 
4
-
5
  // Singleton socket instance
6
  let socketInstance: Socket | null = null;
7
 
8
-
9
  export function useSocket() {
10
  const connected = ref(false);
11
  const error = ref<string | null>(null);
12
 
13
-
14
  // Get or create socket instance
15
  const getSocket = (): Socket => {
16
  if (!socketInstance) {
17
  // Determine the server URL based on environment
18
- const isDev = import.meta.env?.DEV ?? (import.meta.env?.MODE === "development");
19
- const serverUrl = isDev
20
- ? "http://localhost:3000" // Development: backend port
21
- : window.location.origin; // Production: same origin
 
 
 
 
 
 
 
 
 
 
22
 
23
  socketInstance = io(serverUrl, {
24
  autoConnect: true,
@@ -48,7 +55,6 @@ export function useSocket() {
48
  return socketInstance;
49
  };
50
 
51
-
52
  // Send echo test message
53
  const sendEcho = (message: string): Promise<string> => {
54
  return new Promise((resolve, reject) => {
@@ -60,13 +66,17 @@ export function useSocket() {
60
  }
61
 
62
  // Send echo request and wait for response
63
- socket.emit("echo", { message, timestamp: new Date().toISOString() }, (response: any) => {
64
- if (response.error) {
65
- reject(new Error(response.error));
66
- } else {
67
- resolve(response.message);
 
 
 
 
68
  }
69
- });
70
 
71
  // Timeout after 5 seconds
72
  setTimeout(() => {
@@ -75,14 +85,12 @@ export function useSocket() {
75
  });
76
  };
77
 
78
-
79
  // Clean up on unmount
80
  onUnmounted(() => {
81
  // Note: We don't disconnect the singleton instance
82
  // It will be reused by other components
83
  });
84
 
85
-
86
  return {
87
  socket: getSocket(),
88
  connected,
@@ -91,7 +99,6 @@ export function useSocket() {
91
  };
92
  }
93
 
94
-
95
  // Export function to manually disconnect (for cleanup)
96
  export function disconnectSocket() {
97
  if (socketInstance) {
 
1
  import { ref, onUnmounted } from "vue";
2
  import { io, Socket } from "socket.io-client";
3
 
 
4
  // Singleton socket instance
5
  let socketInstance: Socket | null = null;
6
 
 
7
  export function useSocket() {
8
  const connected = ref(false);
9
  const error = ref<string | null>(null);
10
 
 
11
  // Get or create socket instance
12
  const getSocket = (): Socket => {
13
  if (!socketInstance) {
14
  // Determine the server URL based on environment
15
+ const isDev = import.meta.env?.DEV ?? import.meta.env?.MODE === "development";
16
+
17
+ let serverUrl: string;
18
+ if (isDev) {
19
+ // Development: Use same host as frontend, port 3000
20
+ // This allows accessing from local IP (e.g., 192.168.x.x:5173)
21
+ const currentHost = window.location.hostname;
22
+ serverUrl = `http://${currentHost}:3000`;
23
+ } else {
24
+ // Production: same origin
25
+ serverUrl = window.location.origin;
26
+ }
27
+
28
+ console.log("[Socket.io] Connecting to:", serverUrl);
29
 
30
  socketInstance = io(serverUrl, {
31
  autoConnect: true,
 
55
  return socketInstance;
56
  };
57
 
 
58
  // Send echo test message
59
  const sendEcho = (message: string): Promise<string> => {
60
  return new Promise((resolve, reject) => {
 
66
  }
67
 
68
  // Send echo request and wait for response
69
+ socket.emit(
70
+ "echo",
71
+ { message, timestamp: new Date().toISOString() },
72
+ (response: any) => {
73
+ if (response.error) {
74
+ reject(new Error(response.error));
75
+ } else {
76
+ resolve(response.message);
77
+ }
78
  }
79
+ );
80
 
81
  // Timeout after 5 seconds
82
  setTimeout(() => {
 
85
  });
86
  };
87
 
 
88
  // Clean up on unmount
89
  onUnmounted(() => {
90
  // Note: We don't disconnect the singleton instance
91
  // It will be reused by other components
92
  });
93
 
 
94
  return {
95
  socket: getSocket(),
96
  connected,
 
99
  };
100
  }
101
 
 
102
  // Export function to manually disconnect (for cleanup)
103
  export function disconnectSocket() {
104
  if (socketInstance) {
trigo-web/app/src/composables/useTrigoAgent.ts ADDED
@@ -0,0 +1,171 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Vue Composable for Trigo AI Agent
3
+ *
4
+ * Provides reactive interface to the ONNX-based AI agent for move generation.
5
+ * Features:
6
+ * - Lazy initialization (only loads when needed)
7
+ * - Reactive state tracking (ready, thinking, errors)
8
+ * - Automatic cleanup on unmount
9
+ * - Move generation with error handling
10
+ * - Uses tree agent with evaluation mode for efficient parallel move evaluation
11
+ */
12
+
13
+ import { ref, onUnmounted } from "vue";
14
+ import { TrigoTreeAgent } from "@inc/trigoTreeAgent";
15
+ import { OnnxInferencer } from "@/services/onnxInferencer";
16
+ import type { TrigoGame } from "@inc/trigo/game";
17
+ import type { Position } from "@inc/trigo/types";
18
+
19
+ /**
20
+ * Composable for using the Trigo AI agent in Vue components
21
+ */
22
+ export function useTrigoAgent() {
23
+ // Reactive state
24
+ const isReady = ref(false);
25
+ const isThinking = ref(false);
26
+ const error = ref<string | null>(null);
27
+ const lastMoveTime = ref<number>(0);
28
+
29
+ // Agent instance (created lazily)
30
+ let agent: TrigoTreeAgent | null = null;
31
+ let inferencer: OnnxInferencer | null = null;
32
+
33
+ /**
34
+ * Initialize the AI agent
35
+ * - Loads ONNX evaluation model and prepares for inference
36
+ * - Call this when entering VS AI mode
37
+ * - Safe to call multiple times (won't re-initialize)
38
+ */
39
+ const initialize = async (): Promise<void> => {
40
+ if (isReady.value) {
41
+ console.log("[useTrigoAgent] Already initialized");
42
+ return;
43
+ }
44
+
45
+ error.value = null;
46
+
47
+ try {
48
+ console.log("[useTrigoAgent] Initializing tree agent with evaluation mode...");
49
+
50
+ // Create OnnxInferencer with evaluation model
51
+ inferencer = new OnnxInferencer({
52
+ modelPath: "/onnx/GPT2CausalLM_ep0015_evaluation.onnx",
53
+ vocabSize: 259,
54
+ seqLen: 2048 // Evaluation models support longer sequences
55
+ });
56
+
57
+ // Initialize ONNX model
58
+ await inferencer.initialize();
59
+
60
+ // Create tree agent with the inferencer
61
+ agent = new TrigoTreeAgent(inferencer);
62
+
63
+ isReady.value = true;
64
+ console.log("[useTrigoAgent] ✓ Tree agent ready");
65
+ } catch (err) {
66
+ const errorMessage =
67
+ err instanceof Error ? err.message : "Failed to initialize AI agent";
68
+ error.value = errorMessage;
69
+ console.error("[useTrigoAgent] Initialization failed:", err);
70
+ throw err;
71
+ }
72
+ };
73
+
74
+ /**
75
+ * Generate the next move for the AI player
76
+ *
77
+ * @param game - Current game instance
78
+ * @returns The selected move position, or null if no valid moves or pass move
79
+ * @throws Error if agent is not initialized
80
+ */
81
+ const generateMove = async (game: TrigoGame): Promise<Position | null> => {
82
+ if (!agent) {
83
+ throw new Error("AI agent not initialized. Call initialize() first.");
84
+ }
85
+
86
+ if (!isReady.value) {
87
+ throw new Error("AI agent is not ready yet.");
88
+ }
89
+
90
+ if (isThinking.value) {
91
+ console.warn("[useTrigoAgent] Already generating a move, ignoring request");
92
+ return null;
93
+ }
94
+
95
+ error.value = null;
96
+ isThinking.value = true;
97
+
98
+ try {
99
+ console.log("[useTrigoAgent] Generating move with tree agent...");
100
+ const startTime = performance.now();
101
+
102
+ // Use tree agent to select best move
103
+ const move = await agent.selectBestMove(game);
104
+
105
+ const elapsed = performance.now() - startTime;
106
+ lastMoveTime.value = elapsed;
107
+ console.log(`[useTrigoAgent] ✓ Move generated in ${elapsed.toFixed(2)}ms`);
108
+
109
+ // Convert Move to Position (return null if pass move)
110
+ if (!move || move.isPass) {
111
+ console.log("[useTrigoAgent] AI chose to pass");
112
+ return null;
113
+ }
114
+
115
+ if (move.x !== undefined && move.y !== undefined && move.z !== undefined) {
116
+ return { x: move.x, y: move.y, z: move.z };
117
+ }
118
+
119
+ console.warn("[useTrigoAgent] Move has undefined coordinates:", move);
120
+ return null;
121
+ } catch (err) {
122
+ const errorMessage = err instanceof Error ? err.message : "Failed to generate move";
123
+ error.value = errorMessage;
124
+ console.error("[useTrigoAgent] Move generation failed:", err);
125
+ throw err;
126
+ } finally {
127
+ isThinking.value = false;
128
+ }
129
+ };
130
+
131
+ /**
132
+ * Check if the agent is initialized and ready
133
+ */
134
+ const checkIsReady = (): boolean => {
135
+ return agent !== null && inferencer !== null;
136
+ };
137
+
138
+ /**
139
+ * Clean up resources when component unmounts
140
+ */
141
+ const cleanup = (): void => {
142
+ if (agent || inferencer) {
143
+ console.log("[useTrigoAgent] Cleaning up...");
144
+ agent = null;
145
+ inferencer = null;
146
+ isReady.value = false;
147
+ isThinking.value = false;
148
+ error.value = null;
149
+ lastMoveTime.value = 0;
150
+ }
151
+ };
152
+
153
+ // Automatic cleanup on component unmount
154
+ onUnmounted(() => {
155
+ cleanup();
156
+ });
157
+
158
+ return {
159
+ // State
160
+ isReady,
161
+ isThinking,
162
+ error,
163
+ lastMoveTime,
164
+
165
+ // Methods
166
+ initialize,
167
+ generateMove,
168
+ checkIsReady,
169
+ cleanup
170
+ };
171
+ }
trigo-web/app/src/main.ts CHANGED
@@ -5,7 +5,6 @@ import App from "./App.vue";
5
  import router from "./router";
6
  import { initializeParsers } from "@inc/trigo/parserInit";
7
 
8
-
9
  const app = createApp(App);
10
  const pinia = createPinia();
11
 
@@ -13,11 +12,13 @@ app.use(pinia);
13
  app.use(router);
14
 
15
  // Initialize parsers before mounting (required for TGN functionality)
16
- initializeParsers().then(() => {
17
- app.mount("#app");
18
- }).catch((error) => {
19
- console.error("Failed to initialize parsers:", error);
20
- // Still mount app even if parser initialization fails
21
- // (parser will throw error when TGN features are used)
22
- app.mount("#app");
23
- });
 
 
 
5
  import router from "./router";
6
  import { initializeParsers } from "@inc/trigo/parserInit";
7
 
 
8
  const app = createApp(App);
9
  const pinia = createPinia();
10
 
 
12
  app.use(router);
13
 
14
  // Initialize parsers before mounting (required for TGN functionality)
15
+ initializeParsers()
16
+ .then(() => {
17
+ app.mount("#app");
18
+ })
19
+ .catch((error) => {
20
+ console.error("Failed to initialize parsers:", error);
21
+ // Still mount app even if parser initialization fails
22
+ // (parser will throw error when TGN features are used)
23
+ app.mount("#app");
24
+ });
trigo-web/app/src/router/index.ts CHANGED
@@ -1,17 +1,52 @@
1
  import { createRouter, createWebHistory } from "vue-router";
2
  import TrigoView from "@/views/TrigoView.vue";
3
-
 
 
4
 
5
  const router = createRouter({
6
  history: createWebHistory(import.meta.env.BASE_URL),
7
  routes: [
8
  {
9
  path: "/",
10
- name: "home",
11
- component: TrigoView
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
12
  }
13
  ]
14
  });
15
 
16
-
17
- export default router;
 
1
  import { createRouter, createWebHistory } from "vue-router";
2
  import TrigoView from "@/views/TrigoView.vue";
3
+ import OnnxTestView from "@/views/OnnxTestView.vue";
4
+ import TrigoAgentTestView from "@/views/TrigoAgentTestView.vue";
5
+ import TrigoTreeTestView from "@/views/TrigoTreeTestView.vue";
6
 
7
  const router = createRouter({
8
  history: createWebHistory(import.meta.env.BASE_URL),
9
  routes: [
10
  {
11
  path: "/",
12
+ name: "single-game",
13
+ component: TrigoView,
14
+ meta: { mode: "single" }
15
+ },
16
+ {
17
+ path: "/vs-ai",
18
+ name: "vs-ai",
19
+ component: TrigoView,
20
+ meta: { mode: "vs-ai" }
21
+ },
22
+ {
23
+ path: "/vs-people",
24
+ name: "vs-people",
25
+ component: TrigoView,
26
+ meta: { mode: "vs-people" }
27
+ },
28
+ {
29
+ path: "/library",
30
+ name: "library",
31
+ component: TrigoView,
32
+ meta: { mode: "library" }
33
+ },
34
+ {
35
+ path: "/onnx-test",
36
+ name: "onnx-test",
37
+ component: OnnxTestView
38
+ },
39
+ {
40
+ path: "/agent-test",
41
+ name: "agent-test",
42
+ component: TrigoAgentTestView
43
+ },
44
+ {
45
+ path: "/tree-test",
46
+ name: "tree-test",
47
+ component: TrigoTreeTestView
48
  }
49
  ]
50
  });
51
 
52
+ export default router;
 
trigo-web/app/src/services/onnxInferencer.ts ADDED
@@ -0,0 +1,68 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * ONNX Inferencer for Browser (Frontend Wrapper)
3
+ *
4
+ * This file imports onnxruntime-web and creates sessions,
5
+ * then injects them into the platform-agnostic inferencer.
6
+ */
7
+
8
+ // IMPORTANT: Import onnxruntime-web first to register backends
9
+ import * as ort from "onnxruntime-web";
10
+
11
+ // Configure ONNX Runtime Web environment BEFORE creating any sessions
12
+ ort.env.wasm.numThreads = 1;
13
+ ort.env.wasm.simd = true;
14
+
15
+ // Import the common inferencer class AFTER onnxruntime-web is loaded
16
+ import { ModelInferencer, type InferencerConfig, type InferenceResult } from "@inc/modelInferencer";
17
+
18
+ /**
19
+ * Web-specific configuration for the inferencer
20
+ */
21
+ export interface WebInferencerConfig extends Partial<InferencerConfig> {
22
+ modelPath: string;
23
+ executionProviders?: ("wasm" | "webgl" | "webgpu")[];
24
+ sessionOptions?: ort.InferenceSession.SessionOptions;
25
+ }
26
+
27
+ /**
28
+ * ONNX Inferencer for Browser with Web-specific defaults
29
+ */
30
+ export class OnnxInferencer extends ModelInferencer {
31
+ private modelPath: string;
32
+ private sessionOptions?: ort.InferenceSession.SessionOptions;
33
+
34
+ constructor(config: WebInferencerConfig) {
35
+ const { modelPath, executionProviders = ["wasm"], sessionOptions, ...baseConfig } = config;
36
+
37
+ // Pass ort.Tensor constructor to base class
38
+ super(ort.Tensor as any, baseConfig);
39
+
40
+ this.modelPath = modelPath;
41
+ this.sessionOptions = {
42
+ executionProviders: executionProviders,
43
+ graphOptimizationLevel: "all",
44
+ ...sessionOptions
45
+ };
46
+ }
47
+
48
+ /**
49
+ * Initialize the inference session
50
+ */
51
+ async initialize(): Promise<void> {
52
+ console.log("[OnnxInferencer] Initializing...");
53
+ console.log("[OnnxInferencer] Model path:", this.modelPath);
54
+
55
+ try {
56
+ const session = await ort.InferenceSession.create(this.modelPath, this.sessionOptions);
57
+
58
+ // Inject session into base class
59
+ this.setSession(session as any);
60
+ } catch (error) {
61
+ console.error("[OnnxInferencer] Failed to create session:", error);
62
+ throw error;
63
+ }
64
+ }
65
+ }
66
+
67
+ // Re-export types
68
+ export type { InferencerConfig, InferenceResult };
trigo-web/app/src/services/trigoViewport.ts CHANGED
@@ -9,16 +9,16 @@ const COLORS = {
9
  SCENE_CLEAR: 0x505055,
10
 
11
  // Chess frame colors (three-tiered system)
12
- FRAME_CREST: 0xff4d4d, // Red-tinted for edges/corners
13
- FRAME_SURFACE: 0xe6b380, // Orange/yellow-tinted for face edges
14
- FRAME_INTERIOR: 0x999999, // Gray for interior lines
15
 
16
  // intersection point colors
17
  POINT_DEFAULT: 0x4a90e2,
18
  POINT_HOVERED: 0x00ff00,
19
  POINT_HOVERED_DISABLED: 0xff0000,
20
  POINT_AXIS_ALIGNED: 0xffaa00,
21
- POINT_AIR_PATCH: 0x80e680, // Semi-transparent green for liberties in inspect mode
22
 
23
  // Stone colors
24
  STONE_BLACK: 0x070707,
@@ -32,7 +32,7 @@ const COLORS = {
32
  AMBIENT_LIGHT: 0xffffff,
33
  DIRECTIONAL_LIGHT: 0xffffff,
34
  HEMISPHERE_LIGHT_SKY: 0xeefaff,
35
- HEMISPHERE_LIGHT_GROUND: 0x20201a,
36
  } as const;
37
 
38
  // Opacity constants
@@ -46,22 +46,21 @@ const OPACITY = {
46
  POINT_DEFAULT: 0.1,
47
  POINT_HOVERED: 0.8,
48
  POINT_AXIS_ALIGNED: 0.8,
49
- POINT_AIR_PATCH: 0.24, // Semi-transparent for liberty visualization
50
  PREVIEW_STONE: 0.5,
51
  PREVIEW_JOINT_BLACK: 0.5,
52
  PREVIEW_JOINT_WHITE: 0.6,
53
  DIMMED: 0.3,
54
  DOMAIN_BLACK: 0.3,
55
- DOMAIN_WHITE: 0.3,
56
  } as const;
57
 
58
  // Shininess constants for stone materials
59
  const SHININESS = {
60
  STONE_BLACK: 120,
61
- STONE_WHITE: 30,
62
  } as const;
63
 
64
-
65
  // Geometric size constants
66
  const SIZES = {
67
  // Stone and point sizes (relative to grid spacing)
@@ -74,37 +73,33 @@ const SIZES = {
74
  // Sphere detail (number of segments)
75
  STONE_SEGMENTS: 32,
76
  POINT_SEGMENTS: 8,
77
- JOINT_SEGMENTS: 6,
78
  } as const;
79
 
80
-
81
  // Camera and scene constants
82
  const CAMERA = {
83
  FOV: 70,
84
  NEAR: 0.1,
85
  FAR: 1000,
86
  DISTANCE_MULTIPLIER: 1.1,
87
- HEIGHT_RATIO: 0.8,
88
  } as const;
89
 
90
-
91
  // Lighting intensity constants
92
  const LIGHTING = {
93
  AMBIENT_INTENSITY: 0.2,
94
  DIRECTIONAL_MAIN_INTENSITY: 0.8,
95
  DIRECTIONAL_FILL_INTENSITY: 0.3,
96
- HEMISPHERE_INTENSITY: 0.8,
97
  } as const;
98
 
99
-
100
  // Fog constants
101
  const FOG = {
102
  NEAR_FACTOR: 0.2,
103
  FAR_FACTOR: 0.8,
104
- MIN_NEAR: 0.1,
105
  } as const;
106
 
107
-
108
  // Last stone highlight constants
109
  const SHINING = {
110
  FLICKER_SPEED: 0.0048,
@@ -112,10 +107,9 @@ const SHINING = {
112
  BASE_INTENSITY_WHITE: 0.2,
113
  BASE_INTENSITY_BLACK: 0.06,
114
  FLICKER_INTENSITY_WHITE: 0.6,
115
- FLICKER_INTENSITY_BLACK: 0.1,
116
  } as const;
117
 
118
-
119
  export interface Stone {
120
  position: { x: number; y: number; z: number };
121
  color: "black" | "white";
@@ -172,7 +166,7 @@ export class TrigoViewport {
172
  private inspectMode: boolean = false;
173
  private ctrlKeyDown: boolean = false;
174
  private highlightedGroup: Set<string> | null = null;
175
- private airPatch: Set<string> | null = null; // Liberty positions for highlighted group
176
  private lastMouseEvent: MouseEvent | null = null;
177
 
178
  // Domain visibility for territory display
@@ -181,7 +175,11 @@ export class TrigoViewport {
181
  private blackDomain: Set<string> | null = null;
182
  private whiteDomain: Set<string> | null = null;
183
 
184
- constructor(canvas: HTMLCanvasElement, boardShape: BoardShape = { x: 5, y: 5, z: 5 }, callbacks: ViewportCallbacks = {}) {
 
 
 
 
185
  this.canvas = canvas;
186
  this.boardShape = boardShape;
187
  this.callbacks = callbacks;
@@ -265,12 +263,17 @@ export class TrigoViewport {
265
  }
266
 
267
  private createPreviewStone(): void {
268
- const geometry = new THREE.SphereGeometry(SIZES.STONE_RADIUS * this.gridSpacing, SIZES.STONE_SEGMENTS, SIZES.STONE_SEGMENTS);
 
 
 
 
269
  const material = new THREE.MeshPhongMaterial({
270
  color: this.currentPlayerColor === "black" ? COLORS.STONE_BLACK : COLORS.STONE_WHITE,
271
  transparent: true,
272
  opacity: OPACITY.PREVIEW_STONE,
273
- shininess: this.currentPlayerColor === "black" ? SHININESS.STONE_BLACK : SHININESS.STONE_WHITE
 
274
  });
275
  this.previewStone = new THREE.Mesh(geometry, material);
276
  this.previewStone.visible = false; // Hidden by default
@@ -280,8 +283,11 @@ export class TrigoViewport {
280
  private updatePreviewStoneColor(): void {
281
  if (!this.previewStone) return;
282
  const material = this.previewStone.material as THREE.MeshPhongMaterial;
283
- material.color.set(this.currentPlayerColor === "black" ? COLORS.STONE_BLACK : COLORS.STONE_WHITE);
284
- material.shininess = this.currentPlayerColor === "black" ? SHININESS.STONE_BLACK : SHININESS.STONE_WHITE;
 
 
 
285
  }
286
 
287
  private setupFog(): void {
@@ -329,22 +335,35 @@ export class TrigoViewport {
329
 
330
  private setupLighting(): void {
331
  // Ambient light
332
- const ambientLight = new THREE.AmbientLight(COLORS.AMBIENT_LIGHT, LIGHTING.AMBIENT_INTENSITY);
 
 
 
333
  this.scene.add(ambientLight);
334
 
335
  // Directional light (main)
336
- const directionalLight1 = new THREE.DirectionalLight(COLORS.DIRECTIONAL_LIGHT, LIGHTING.DIRECTIONAL_MAIN_INTENSITY);
 
 
 
337
  directionalLight1.position.set(10, 20, 10);
338
  directionalLight1.castShadow = true;
339
  this.scene.add(directionalLight1);
340
 
341
  // Directional light (fill)
342
- const directionalLight2 = new THREE.DirectionalLight(COLORS.DIRECTIONAL_LIGHT, LIGHTING.DIRECTIONAL_FILL_INTENSITY);
 
 
 
343
  directionalLight2.position.set(-10, -10, -10);
344
  this.scene.add(directionalLight2);
345
 
346
  // Hemisphere light for softer ambient
347
- const hemisphereLight = new THREE.HemisphereLight(COLORS.HEMISPHERE_LIGHT_SKY, COLORS.HEMISPHERE_LIGHT_GROUND, LIGHTING.HEMISPHERE_INTENSITY);
 
 
 
 
348
  hemisphereLight.position.set(0, 20, 0);
349
  this.scene.add(hemisphereLight);
350
  }
@@ -380,16 +399,16 @@ export class TrigoViewport {
380
 
381
  // Helper function to determine material based on border conditions
382
  const getLineMaterial = (border1: boolean, border2: boolean): THREE.LineBasicMaterial => {
383
- if (border1 && border2) return crestMaterial; // Both borders -> crest
384
- if (border1 || border2) return surfaceMaterial; // One border -> surface
385
- return interiorMaterial; // No borders -> interior
386
  };
387
 
388
  // X-axis lines (parallel to X)
389
  for (let y = 0; y < sizeY; y++) {
390
  for (let z = 0; z < sizeZ; z++) {
391
- const border1 = (y === 0) || (y === sizeY - 1);
392
- const border2 = (z === 0) || (z === sizeZ - 1);
393
  const material = getLineMaterial(border1, border2);
394
 
395
  const points = [];
@@ -411,8 +430,8 @@ export class TrigoViewport {
411
  // Y-axis lines (parallel to Y)
412
  for (let x = 0; x < sizeX; x++) {
413
  for (let z = 0; z < sizeZ; z++) {
414
- const border1 = (x === 0) || (x === sizeX - 1);
415
- const border2 = (z === 0) || (z === sizeZ - 1);
416
  const material = getLineMaterial(border1, border2);
417
 
418
  const points = [];
@@ -435,8 +454,8 @@ export class TrigoViewport {
435
  if (sizeZ >= 3) {
436
  for (let x = 0; x < sizeX; x++) {
437
  for (let y = 0; y < sizeY; y++) {
438
- const border1 = (x === 0) || (x === sizeX - 1);
439
- const border2 = (y === 0) || (y === sizeY - 1);
440
  const material = getLineMaterial(border1, border2);
441
 
442
  const points = [];
@@ -468,23 +487,17 @@ export class TrigoViewport {
468
  const axisLength = maxOffset * 1.2;
469
  const axesMaterial = [
470
  new THREE.LineBasicMaterial({ color: 0xff0000 }), // X axis - red
471
- new THREE.LineBasicMaterial({ color: 0x00ff00 }) // Y axis - green
472
  ];
473
 
474
  // X axis
475
- const xPoints = [
476
- new THREE.Vector3(0, 0, 0),
477
- new THREE.Vector3(axisLength, 0, 0)
478
- ];
479
  const xGeometry = new THREE.BufferGeometry().setFromPoints(xPoints);
480
  const xLine = new THREE.Line(xGeometry, axesMaterial[0]);
481
  this.gridGroup.add(xLine);
482
 
483
  // Y axis
484
- const yPoints = [
485
- new THREE.Vector3(0, 0, 0),
486
- new THREE.Vector3(0, axisLength, 0)
487
- ];
488
  const yGeometry = new THREE.BufferGeometry().setFromPoints(yPoints);
489
  const yLine = new THREE.Line(yGeometry, axesMaterial[1]);
490
  this.gridGroup.add(yLine);
@@ -499,7 +512,11 @@ export class TrigoViewport {
499
  const offsetZ = ((sizeZ - 1) * spacing) / 2;
500
 
501
  // Create small spheres at each grid intersection
502
- const pointGeometry = new THREE.SphereGeometry(SIZES.INTERSECTION_POINT_RADIUS, SIZES.POINT_SEGMENTS, SIZES.POINT_SEGMENTS);
 
 
 
 
503
 
504
  for (let x = 0; x < sizeX; x++) {
505
  for (let y = 0; y < sizeY; y++) {
@@ -523,7 +540,6 @@ export class TrigoViewport {
523
  }
524
  }
525
 
526
-
527
  private createJoints(): void {
528
  const { x: sizeX, y: sizeY, z: sizeZ } = this.boardShape;
529
  const spacing = this.gridSpacing;
@@ -544,7 +560,12 @@ export class TrigoViewport {
544
 
545
  // Create X-axis joint (between current and next X position)
546
  if (x < sizeX - 1) {
547
- const geometry = new THREE.CylinderGeometry(jointRadius, jointRadius, jointLength, SIZES.JOINT_SEGMENTS);
 
 
 
 
 
548
  const material = new THREE.MeshPhongMaterial({
549
  color: COLORS.STONE_BLACK,
550
  shininess: SHININESS.STONE_BLACK,
@@ -567,7 +588,12 @@ export class TrigoViewport {
567
 
568
  // Create Y-axis joint (between current and next Y position)
569
  if (y < sizeY - 1) {
570
- const geometry = new THREE.CylinderGeometry(jointRadius, jointRadius, jointLength, SIZES.JOINT_SEGMENTS);
 
 
 
 
 
571
  const material = new THREE.MeshPhongMaterial({
572
  color: COLORS.STONE_BLACK,
573
  shininess: SHININESS.STONE_BLACK,
@@ -590,7 +616,12 @@ export class TrigoViewport {
590
 
591
  // Create Z-axis joint (between current and next Z position)
592
  if (z < sizeZ - 1) {
593
- const geometry = new THREE.CylinderGeometry(jointRadius, jointRadius, jointLength, SIZES.JOINT_SEGMENTS);
 
 
 
 
 
594
  const material = new THREE.MeshPhongMaterial({
595
  color: COLORS.STONE_BLACK,
596
  shininess: SHININESS.STONE_BLACK,
@@ -617,7 +648,6 @@ export class TrigoViewport {
617
  }
618
  }
619
 
620
-
621
  private createDomainCubes(): void {
622
  const { x: sizeX, y: sizeY, z: sizeZ } = this.boardShape;
623
  const spacing = this.gridSpacing;
@@ -642,7 +672,7 @@ export class TrigoViewport {
642
  color: COLORS.STONE_BLACK,
643
  transparent: true,
644
  opacity: OPACITY.DOMAIN_BLACK,
645
- depthWrite: false // Prevent z-fighting with stones
646
  });
647
 
648
  const cube = new THREE.Mesh(geometry, material);
@@ -682,7 +712,11 @@ export class TrigoViewport {
682
  const offsetZ = ((this.boardShape.z - 1) * spacing) / 2;
683
 
684
  // Create stone geometry
685
- const geometry = new THREE.SphereGeometry(SIZES.STONE_RADIUS * this.gridSpacing, SIZES.STONE_SEGMENTS, SIZES.STONE_SEGMENTS);
 
 
 
 
686
  const material = new THREE.MeshPhongMaterial({
687
  color: color === "black" ? COLORS.STONE_BLACK : COLORS.STONE_WHITE,
688
  shininess: color === "black" ? SHININESS.STONE_BLACK : SHININESS.STONE_WHITE,
@@ -692,11 +726,7 @@ export class TrigoViewport {
692
  });
693
 
694
  const stoneMesh = new THREE.Mesh(geometry, material);
695
- stoneMesh.position.set(
696
- x * spacing - offsetX,
697
- y * spacing - offsetY,
698
- z * spacing - offsetZ
699
- );
700
 
701
  const stone: Stone = {
702
  position: { x, y, z },
@@ -768,7 +798,6 @@ export class TrigoViewport {
768
  return this.stones.has(this.getStoneKey(x, y, z));
769
  }
770
 
771
-
772
  private refreshJoints(): void {
773
  const { x: sizeX, y: sizeY, z: sizeZ } = this.boardShape;
774
 
@@ -790,9 +819,20 @@ export class TrigoViewport {
790
 
791
  if (nextStone && nextStone.color === centerStone.color) {
792
  const material = jointNodes.X.material as THREE.MeshPhongMaterial;
793
- material.color.set(centerStone.color === "black" ? COLORS.STONE_BLACK : COLORS.STONE_WHITE);
794
- material.shininess = centerStone.color === "black" ? SHININESS.STONE_BLACK : SHININESS.STONE_WHITE;
795
- material.specular.set(centerStone.color === "black" ? COLORS.STONE_BLACK_SPECULAR : COLORS.STONE_WHITE_SPECULAR);
 
 
 
 
 
 
 
 
 
 
 
796
  material.opacity = 1.0;
797
  material.transparent = false;
798
  jointNodes.X.visible = true;
@@ -812,9 +852,20 @@ export class TrigoViewport {
812
 
813
  if (nextStone && nextStone.color === centerStone.color) {
814
  const material = jointNodes.Y.material as THREE.MeshPhongMaterial;
815
- material.color.set(centerStone.color === "black" ? COLORS.STONE_BLACK : COLORS.STONE_WHITE);
816
- material.shininess = centerStone.color === "black" ? SHININESS.STONE_BLACK : SHININESS.STONE_WHITE;
817
- material.specular.set(centerStone.color === "black" ? COLORS.STONE_BLACK_SPECULAR : COLORS.STONE_WHITE_SPECULAR);
 
 
 
 
 
 
 
 
 
 
 
818
  material.opacity = 1.0;
819
  material.transparent = false;
820
  jointNodes.Y.visible = true;
@@ -834,9 +885,20 @@ export class TrigoViewport {
834
 
835
  if (nextStone && nextStone.color === centerStone.color) {
836
  const material = jointNodes.Z.material as THREE.MeshPhongMaterial;
837
- material.color.set(centerStone.color === "black" ? COLORS.STONE_BLACK : COLORS.STONE_WHITE);
838
- material.shininess = centerStone.color === "black" ? SHININESS.STONE_BLACK : SHININESS.STONE_WHITE;
839
- material.specular.set(centerStone.color === "black" ? COLORS.STONE_BLACK_SPECULAR : COLORS.STONE_WHITE_SPECULAR);
 
 
 
 
 
 
 
 
 
 
 
840
  material.opacity = 1.0;
841
  material.transparent = false;
842
  jointNodes.Z.visible = true;
@@ -849,7 +911,8 @@ export class TrigoViewport {
849
  }
850
 
851
  // Show preview joints if hovering over this position
852
- const isHovered = this.hoveredPosition &&
 
853
  this.hoveredPosition.x === x &&
854
  this.hoveredPosition.y === y &&
855
  this.hoveredPosition.z === z;
@@ -857,7 +920,10 @@ export class TrigoViewport {
857
  if (isHovered && this.isGameActive && !centerStone) {
858
  // Preview joints connecting to adjacent stones of current player's color
859
  const previewColor = this.currentPlayerColor;
860
- const previewOpacity = previewColor === "black" ? OPACITY.PREVIEW_JOINT_BLACK : OPACITY.PREVIEW_JOINT_WHITE;
 
 
 
861
 
862
  // Check -X direction (left neighbor)
863
  if (x > 0) {
@@ -866,10 +932,22 @@ export class TrigoViewport {
866
  if (leftStone && leftStone.color === previewColor) {
867
  const leftJointNodes = this.joints.get(leftKey);
868
  if (leftJointNodes?.X) {
869
- const material = leftJointNodes.X.material as THREE.MeshPhongMaterial;
870
- material.color.set(previewColor === "black" ? COLORS.STONE_BLACK : COLORS.STONE_WHITE);
871
- material.shininess = previewColor === "black" ? SHININESS.STONE_BLACK : SHININESS.STONE_WHITE;
872
- material.specular.set(previewColor === "black" ? COLORS.STONE_BLACK_SPECULAR : COLORS.STONE_WHITE_SPECULAR);
 
 
 
 
 
 
 
 
 
 
 
 
873
  material.opacity = previewOpacity;
874
  material.transparent = true;
875
  leftJointNodes.X.visible = true;
@@ -884,10 +962,22 @@ export class TrigoViewport {
884
  if (bottomStone && bottomStone.color === previewColor) {
885
  const bottomJointNodes = this.joints.get(bottomKey);
886
  if (bottomJointNodes?.Y) {
887
- const material = bottomJointNodes.Y.material as THREE.MeshPhongMaterial;
888
- material.color.set(previewColor === "black" ? COLORS.STONE_BLACK : COLORS.STONE_WHITE);
889
- material.shininess = previewColor === "black" ? SHININESS.STONE_BLACK : SHININESS.STONE_WHITE;
890
- material.specular.set(previewColor === "black" ? COLORS.STONE_BLACK_SPECULAR : COLORS.STONE_WHITE_SPECULAR);
 
 
 
 
 
 
 
 
 
 
 
 
891
  material.opacity = previewOpacity;
892
  material.transparent = true;
893
  bottomJointNodes.Y.visible = true;
@@ -902,10 +992,22 @@ export class TrigoViewport {
902
  if (backStone && backStone.color === previewColor) {
903
  const backJointNodes = this.joints.get(backKey);
904
  if (backJointNodes?.Z) {
905
- const material = backJointNodes.Z.material as THREE.MeshPhongMaterial;
906
- material.color.set(previewColor === "black" ? COLORS.STONE_BLACK : COLORS.STONE_WHITE);
907
- material.shininess = previewColor === "black" ? SHININESS.STONE_BLACK : SHININESS.STONE_WHITE;
908
- material.specular.set(previewColor === "black" ? COLORS.STONE_BLACK_SPECULAR : COLORS.STONE_WHITE_SPECULAR);
 
 
 
 
 
 
 
 
 
 
 
 
909
  material.opacity = previewOpacity;
910
  material.transparent = true;
911
  backJointNodes.Z.visible = true;
@@ -919,9 +1021,20 @@ export class TrigoViewport {
919
  const rightStone = this.stones.get(rightKey);
920
  if (rightStone && rightStone.color === previewColor) {
921
  const material = jointNodes.X.material as THREE.MeshPhongMaterial;
922
- material.color.set(previewColor === "black" ? COLORS.STONE_BLACK : COLORS.STONE_WHITE);
923
- material.shininess = previewColor === "black" ? SHININESS.STONE_BLACK : SHININESS.STONE_WHITE;
924
- material.specular.set(previewColor === "black" ? COLORS.STONE_BLACK_SPECULAR : COLORS.STONE_WHITE_SPECULAR);
 
 
 
 
 
 
 
 
 
 
 
925
  material.opacity = previewOpacity;
926
  material.transparent = true;
927
  jointNodes.X.visible = true;
@@ -934,9 +1047,20 @@ export class TrigoViewport {
934
  const topStone = this.stones.get(topKey);
935
  if (topStone && topStone.color === previewColor) {
936
  const material = jointNodes.Y.material as THREE.MeshPhongMaterial;
937
- material.color.set(previewColor === "black" ? COLORS.STONE_BLACK : COLORS.STONE_WHITE);
938
- material.shininess = previewColor === "black" ? SHININESS.STONE_BLACK : SHININESS.STONE_WHITE;
939
- material.specular.set(previewColor === "black" ? COLORS.STONE_BLACK_SPECULAR : COLORS.STONE_WHITE_SPECULAR);
 
 
 
 
 
 
 
 
 
 
 
940
  material.opacity = previewOpacity;
941
  material.transparent = true;
942
  jointNodes.Y.visible = true;
@@ -949,9 +1073,20 @@ export class TrigoViewport {
949
  const frontStone = this.stones.get(frontKey);
950
  if (frontStone && frontStone.color === previewColor) {
951
  const material = jointNodes.Z.material as THREE.MeshPhongMaterial;
952
- material.color.set(previewColor === "black" ? COLORS.STONE_BLACK : COLORS.STONE_WHITE);
953
- material.shininess = previewColor === "black" ? SHININESS.STONE_BLACK : SHININESS.STONE_WHITE;
954
- material.specular.set(previewColor === "black" ? COLORS.STONE_BLACK_SPECULAR : COLORS.STONE_WHITE_SPECULAR);
 
 
 
 
 
 
 
 
 
 
 
955
  material.opacity = previewOpacity;
956
  material.transparent = true;
957
  jointNodes.Z.visible = true;
@@ -963,7 +1098,6 @@ export class TrigoViewport {
963
  }
964
  }
965
 
966
-
967
  public setCurrentPlayer(color: "black" | "white"): void {
968
  this.currentPlayerColor = color;
969
  this.updatePreviewStoneColor();
@@ -973,7 +1107,6 @@ export class TrigoViewport {
973
  this.isGameActive = active;
974
  }
975
 
976
-
977
  public hidePreviewStone(): void {
978
  if (this.previewStone) {
979
  this.previewStone.visible = false;
@@ -982,11 +1115,14 @@ export class TrigoViewport {
982
  this.refreshJoints();
983
  }
984
 
985
-
986
  public setLastPlacedStone(x: number | null, y: number | null, z: number | null): void {
987
  // Clear previous stone's emissive glow
988
  if (this.lastPlacedStone) {
989
- const prevKey = this.getStoneKey(this.lastPlacedStone.x, this.lastPlacedStone.y, this.lastPlacedStone.z);
 
 
 
 
990
  const prevStone = this.stones.get(prevKey);
991
  if (prevStone && prevStone.mesh) {
992
  const prevMaterial = prevStone.mesh.material as THREE.MeshPhongMaterial;
@@ -1003,7 +1139,6 @@ export class TrigoViewport {
1003
  }
1004
  }
1005
 
1006
-
1007
  // Domain visibility methods (for territory display)
1008
  public setBlackDomainVisible(visible: boolean): void {
1009
  if (this.blackDomainVisible !== visible) {
@@ -1048,9 +1183,13 @@ export class TrigoViewport {
1048
  this.setWhiteDomainVisible(false);
1049
  }
1050
 
1051
-
1052
  public setBoardShape(shape: BoardShape): void {
1053
- if (shape.x === this.boardShape.x && shape.y === this.boardShape.y && shape.z === this.boardShape.z) return;
 
 
 
 
 
1054
 
1055
  this.boardShape = shape;
1056
 
@@ -1145,7 +1284,7 @@ export class TrigoViewport {
1145
  }
1146
 
1147
  // Check if mouse is not hovering over canvas and cleanup
1148
- if (!this.canvas.matches(':hover')) {
1149
  // Hide preview stone when mouse is outside canvas
1150
  if (this.previewStone) {
1151
  this.previewStone.visible = false;
@@ -1202,8 +1341,11 @@ export class TrigoViewport {
1202
 
1203
  // Remove previous highlight
1204
  if (this.highlightedPoint) {
1205
- (this.highlightedPoint.material as THREE.MeshBasicMaterial).color.set(COLORS.POINT_DEFAULT);
1206
- (this.highlightedPoint.material as THREE.MeshBasicMaterial).opacity = OPACITY.POINT_DEFAULT;
 
 
 
1207
  this.highlightedPoint = null;
1208
  }
1209
 
@@ -1225,7 +1367,9 @@ export class TrigoViewport {
1225
 
1226
  this.highlightedPoint = point;
1227
  // Use green for valid/droppable, red for invalid (game inactive or violates rules)
1228
- const hoverColor = isDroppable ? COLORS.POINT_HOVERED : COLORS.POINT_HOVERED_DISABLED;
 
 
1229
  (point.material as THREE.MeshBasicMaterial).color.set(hoverColor);
1230
  (point.material as THREE.MeshBasicMaterial).opacity = OPACITY.POINT_HOVERED;
1231
 
@@ -1338,7 +1482,7 @@ export class TrigoViewport {
1338
 
1339
  // Update last placed stone highlight effect only when mouse is over canvas
1340
  // Using :hover pseudo-class check for better accuracy
1341
- if (this.canvas.matches && this.canvas.matches(':hover')) {
1342
  this.updateLastStoneHighlight();
1343
  }
1344
 
@@ -1358,7 +1502,9 @@ export class TrigoViewport {
1358
  this.lastCameraDistance = cameraDistance;
1359
 
1360
  // Calculate diagonal distance of the board
1361
- const diagonal = Math.sqrt(this.boardShape.x ** 2 + this.boardShape.y ** 2 + this.boardShape.z ** 2);
 
 
1362
  const boardDiagonal = diagonal * this.gridSpacing;
1363
 
1364
  // Update fog near and far based on camera distance +/- diagonal
@@ -1389,8 +1535,14 @@ export class TrigoViewport {
1389
  const emissiveColor = new THREE.Color(...SHINING.EMISSIVE_COLOR);
1390
 
1391
  // Scale intensity based on stone color (white stones get brighter glow)
1392
- const baseIntensity = stone.color === "white" ? SHINING.BASE_INTENSITY_WHITE : SHINING.BASE_INTENSITY_BLACK;
1393
- const flickerIntensity = stone.color === "white" ? SHINING.FLICKER_INTENSITY_WHITE : SHINING.FLICKER_INTENSITY_BLACK;
 
 
 
 
 
 
1394
  const intensity = flicker * flickerIntensity + baseIntensity;
1395
 
1396
  material.emissive = emissiveColor;
@@ -1411,7 +1563,7 @@ export class TrigoViewport {
1411
 
1412
  if (intersects.length > 0) {
1413
  // Find the position of the clicked stone
1414
- let clickedPosition: {x: number, y: number, z: number} | null = null;
1415
 
1416
  for (const [, stone] of this.stones.entries()) {
1417
  if (stone.mesh === intersects[0].object) {
@@ -1430,7 +1582,10 @@ export class TrigoViewport {
1430
 
1431
  // Notify callback with group info
1432
  if (this.callbacks.onInspectGroup) {
1433
- this.callbacks.onInspectGroup(this.highlightedGroup.size, libertiesResult.count);
 
 
 
1434
  }
1435
  }
1436
  } else {
@@ -1443,7 +1598,7 @@ export class TrigoViewport {
1443
  }
1444
  }
1445
 
1446
- private findConnectedGroup(startPos: {x: number, y: number, z: number}): Set<string> {
1447
  const group = new Set<string>();
1448
  const startKey = this.getStoneKey(startPos.x, startPos.y, startPos.z);
1449
  const startStone = this.stones.get(startKey);
@@ -1451,7 +1606,7 @@ export class TrigoViewport {
1451
  if (!startStone) return group;
1452
 
1453
  const color = startStone.color;
1454
- const queue: {x: number, y: number, z: number}[] = [startPos];
1455
  const visited = new Set<string>();
1456
 
1457
  while (queue.length > 0) {
@@ -1468,19 +1623,24 @@ export class TrigoViewport {
1468
 
1469
  // Check all 6 neighbors in 3D space
1470
  const neighbors = [
1471
- {x: pos.x + 1, y: pos.y, z: pos.z},
1472
- {x: pos.x - 1, y: pos.y, z: pos.z},
1473
- {x: pos.x, y: pos.y + 1, z: pos.z},
1474
- {x: pos.x, y: pos.y - 1, z: pos.z},
1475
- {x: pos.x, y: pos.y, z: pos.z + 1},
1476
- {x: pos.x, y: pos.y, z: pos.z - 1}
1477
  ];
1478
 
1479
  for (const neighbor of neighbors) {
1480
  // Check if neighbor is within board bounds
1481
- if (neighbor.x >= 0 && neighbor.x < this.boardShape.x &&
1482
- neighbor.y >= 0 && neighbor.y < this.boardShape.y &&
1483
- neighbor.z >= 0 && neighbor.z < this.boardShape.z) {
 
 
 
 
 
1484
  const neighborKey = this.getStoneKey(neighbor.x, neighbor.y, neighbor.z);
1485
  if (!visited.has(neighborKey)) {
1486
  queue.push(neighbor);
@@ -1497,24 +1657,29 @@ export class TrigoViewport {
1497
 
1498
  // For each stone in the group, check its neighbors for empty positions
1499
  for (const key of group) {
1500
- const parts = key.split(',').map(Number);
1501
- const pos = {x: parts[0], y: parts[1], z: parts[2]};
1502
 
1503
  // Check all 6 neighbors
1504
  const neighbors = [
1505
- {x: pos.x + 1, y: pos.y, z: pos.z},
1506
- {x: pos.x - 1, y: pos.y, z: pos.z},
1507
- {x: pos.x, y: pos.y + 1, z: pos.z},
1508
- {x: pos.x, y: pos.y - 1, z: pos.z},
1509
- {x: pos.x, y: pos.y, z: pos.z + 1},
1510
- {x: pos.x, y: pos.y, z: pos.z - 1}
1511
  ];
1512
 
1513
  for (const neighbor of neighbors) {
1514
  // Check if neighbor is within bounds
1515
- if (neighbor.x >= 0 && neighbor.x < this.boardShape.x &&
1516
- neighbor.y >= 0 && neighbor.y < this.boardShape.y &&
1517
- neighbor.z >= 0 && neighbor.z < this.boardShape.z) {
 
 
 
 
 
1518
  const neighborKey = this.getStoneKey(neighbor.x, neighbor.y, neighbor.z);
1519
  // If neighbor is empty (not in stones map), it's a liberty
1520
  if (!this.stones.has(neighborKey)) {
@@ -1527,7 +1692,6 @@ export class TrigoViewport {
1527
  return { count: liberties.size, positions: liberties };
1528
  }
1529
 
1530
-
1531
  private updateStoneOpacity(): void {
1532
  // Update opacity of all stones based on inspect mode
1533
  this.stones.forEach((stone, key) => {
@@ -1558,7 +1722,6 @@ export class TrigoViewport {
1558
  this.updateAirPatchVisualization();
1559
  }
1560
 
1561
-
1562
  private updateAirPatchVisualization(): void {
1563
  // Reset all intersection points to default color
1564
  this.intersectionPoints.children.forEach((child) => {
@@ -1591,8 +1754,10 @@ export class TrigoViewport {
1591
  });
1592
  }
1593
 
1594
-
1595
- private updateDomainCubesVisualization(blackDomain: Set<string> | null, whiteDomain: Set<string> | null): void {
 
 
1596
  // Update domain cube visibility based on territory and air patch
1597
  this.domainCubes.forEach((cube, key) => {
1598
  const material = cube.material as THREE.MeshBasicMaterial;
@@ -1620,7 +1785,6 @@ export class TrigoViewport {
1620
  });
1621
  }
1622
 
1623
-
1624
  private onKeyDown(event: KeyboardEvent): void {
1625
  // Ctrl key (17) or Meta key (91/93) for Mac
1626
  if (event.ctrlKey || event.metaKey) {
@@ -1651,7 +1815,6 @@ export class TrigoViewport {
1651
  }
1652
  }
1653
 
1654
-
1655
  public destroy(): void {
1656
  this.isDestroyed = true;
1657
 
 
9
  SCENE_CLEAR: 0x505055,
10
 
11
  // Chess frame colors (three-tiered system)
12
+ FRAME_CREST: 0xff4d4d, // Red-tinted for edges/corners
13
+ FRAME_SURFACE: 0xe6b380, // Orange/yellow-tinted for face edges
14
+ FRAME_INTERIOR: 0x999999, // Gray for interior lines
15
 
16
  // intersection point colors
17
  POINT_DEFAULT: 0x4a90e2,
18
  POINT_HOVERED: 0x00ff00,
19
  POINT_HOVERED_DISABLED: 0xff0000,
20
  POINT_AXIS_ALIGNED: 0xffaa00,
21
+ POINT_AIR_PATCH: 0x80e680, // Semi-transparent green for liberties in inspect mode
22
 
23
  // Stone colors
24
  STONE_BLACK: 0x070707,
 
32
  AMBIENT_LIGHT: 0xffffff,
33
  DIRECTIONAL_LIGHT: 0xffffff,
34
  HEMISPHERE_LIGHT_SKY: 0xeefaff,
35
+ HEMISPHERE_LIGHT_GROUND: 0x20201a
36
  } as const;
37
 
38
  // Opacity constants
 
46
  POINT_DEFAULT: 0.1,
47
  POINT_HOVERED: 0.8,
48
  POINT_AXIS_ALIGNED: 0.8,
49
+ POINT_AIR_PATCH: 0.24, // Semi-transparent for liberty visualization
50
  PREVIEW_STONE: 0.5,
51
  PREVIEW_JOINT_BLACK: 0.5,
52
  PREVIEW_JOINT_WHITE: 0.6,
53
  DIMMED: 0.3,
54
  DOMAIN_BLACK: 0.3,
55
+ DOMAIN_WHITE: 0.3
56
  } as const;
57
 
58
  // Shininess constants for stone materials
59
  const SHININESS = {
60
  STONE_BLACK: 120,
61
+ STONE_WHITE: 30
62
  } as const;
63
 
 
64
  // Geometric size constants
65
  const SIZES = {
66
  // Stone and point sizes (relative to grid spacing)
 
73
  // Sphere detail (number of segments)
74
  STONE_SEGMENTS: 32,
75
  POINT_SEGMENTS: 8,
76
+ JOINT_SEGMENTS: 6
77
  } as const;
78
 
 
79
  // Camera and scene constants
80
  const CAMERA = {
81
  FOV: 70,
82
  NEAR: 0.1,
83
  FAR: 1000,
84
  DISTANCE_MULTIPLIER: 1.1,
85
+ HEIGHT_RATIO: 0.8
86
  } as const;
87
 
 
88
  // Lighting intensity constants
89
  const LIGHTING = {
90
  AMBIENT_INTENSITY: 0.2,
91
  DIRECTIONAL_MAIN_INTENSITY: 0.8,
92
  DIRECTIONAL_FILL_INTENSITY: 0.3,
93
+ HEMISPHERE_INTENSITY: 0.8
94
  } as const;
95
 
 
96
  // Fog constants
97
  const FOG = {
98
  NEAR_FACTOR: 0.2,
99
  FAR_FACTOR: 0.8,
100
+ MIN_NEAR: 0.1
101
  } as const;
102
 
 
103
  // Last stone highlight constants
104
  const SHINING = {
105
  FLICKER_SPEED: 0.0048,
 
107
  BASE_INTENSITY_WHITE: 0.2,
108
  BASE_INTENSITY_BLACK: 0.06,
109
  FLICKER_INTENSITY_WHITE: 0.6,
110
+ FLICKER_INTENSITY_BLACK: 0.1
111
  } as const;
112
 
 
113
  export interface Stone {
114
  position: { x: number; y: number; z: number };
115
  color: "black" | "white";
 
166
  private inspectMode: boolean = false;
167
  private ctrlKeyDown: boolean = false;
168
  private highlightedGroup: Set<string> | null = null;
169
+ private airPatch: Set<string> | null = null; // Liberty positions for highlighted group
170
  private lastMouseEvent: MouseEvent | null = null;
171
 
172
  // Domain visibility for territory display
 
175
  private blackDomain: Set<string> | null = null;
176
  private whiteDomain: Set<string> | null = null;
177
 
178
+ constructor(
179
+ canvas: HTMLCanvasElement,
180
+ boardShape: BoardShape = { x: 5, y: 5, z: 5 },
181
+ callbacks: ViewportCallbacks = {}
182
+ ) {
183
  this.canvas = canvas;
184
  this.boardShape = boardShape;
185
  this.callbacks = callbacks;
 
263
  }
264
 
265
  private createPreviewStone(): void {
266
+ const geometry = new THREE.SphereGeometry(
267
+ SIZES.STONE_RADIUS * this.gridSpacing,
268
+ SIZES.STONE_SEGMENTS,
269
+ SIZES.STONE_SEGMENTS
270
+ );
271
  const material = new THREE.MeshPhongMaterial({
272
  color: this.currentPlayerColor === "black" ? COLORS.STONE_BLACK : COLORS.STONE_WHITE,
273
  transparent: true,
274
  opacity: OPACITY.PREVIEW_STONE,
275
+ shininess:
276
+ this.currentPlayerColor === "black" ? SHININESS.STONE_BLACK : SHININESS.STONE_WHITE
277
  });
278
  this.previewStone = new THREE.Mesh(geometry, material);
279
  this.previewStone.visible = false; // Hidden by default
 
283
  private updatePreviewStoneColor(): void {
284
  if (!this.previewStone) return;
285
  const material = this.previewStone.material as THREE.MeshPhongMaterial;
286
+ material.color.set(
287
+ this.currentPlayerColor === "black" ? COLORS.STONE_BLACK : COLORS.STONE_WHITE
288
+ );
289
+ material.shininess =
290
+ this.currentPlayerColor === "black" ? SHININESS.STONE_BLACK : SHININESS.STONE_WHITE;
291
  }
292
 
293
  private setupFog(): void {
 
335
 
336
  private setupLighting(): void {
337
  // Ambient light
338
+ const ambientLight = new THREE.AmbientLight(
339
+ COLORS.AMBIENT_LIGHT,
340
+ LIGHTING.AMBIENT_INTENSITY
341
+ );
342
  this.scene.add(ambientLight);
343
 
344
  // Directional light (main)
345
+ const directionalLight1 = new THREE.DirectionalLight(
346
+ COLORS.DIRECTIONAL_LIGHT,
347
+ LIGHTING.DIRECTIONAL_MAIN_INTENSITY
348
+ );
349
  directionalLight1.position.set(10, 20, 10);
350
  directionalLight1.castShadow = true;
351
  this.scene.add(directionalLight1);
352
 
353
  // Directional light (fill)
354
+ const directionalLight2 = new THREE.DirectionalLight(
355
+ COLORS.DIRECTIONAL_LIGHT,
356
+ LIGHTING.DIRECTIONAL_FILL_INTENSITY
357
+ );
358
  directionalLight2.position.set(-10, -10, -10);
359
  this.scene.add(directionalLight2);
360
 
361
  // Hemisphere light for softer ambient
362
+ const hemisphereLight = new THREE.HemisphereLight(
363
+ COLORS.HEMISPHERE_LIGHT_SKY,
364
+ COLORS.HEMISPHERE_LIGHT_GROUND,
365
+ LIGHTING.HEMISPHERE_INTENSITY
366
+ );
367
  hemisphereLight.position.set(0, 20, 0);
368
  this.scene.add(hemisphereLight);
369
  }
 
399
 
400
  // Helper function to determine material based on border conditions
401
  const getLineMaterial = (border1: boolean, border2: boolean): THREE.LineBasicMaterial => {
402
+ if (border1 && border2) return crestMaterial; // Both borders -> crest
403
+ if (border1 || border2) return surfaceMaterial; // One border -> surface
404
+ return interiorMaterial; // No borders -> interior
405
  };
406
 
407
  // X-axis lines (parallel to X)
408
  for (let y = 0; y < sizeY; y++) {
409
  for (let z = 0; z < sizeZ; z++) {
410
+ const border1 = y === 0 || y === sizeY - 1;
411
+ const border2 = z === 0 || z === sizeZ - 1;
412
  const material = getLineMaterial(border1, border2);
413
 
414
  const points = [];
 
430
  // Y-axis lines (parallel to Y)
431
  for (let x = 0; x < sizeX; x++) {
432
  for (let z = 0; z < sizeZ; z++) {
433
+ const border1 = x === 0 || x === sizeX - 1;
434
+ const border2 = z === 0 || z === sizeZ - 1;
435
  const material = getLineMaterial(border1, border2);
436
 
437
  const points = [];
 
454
  if (sizeZ >= 3) {
455
  for (let x = 0; x < sizeX; x++) {
456
  for (let y = 0; y < sizeY; y++) {
457
+ const border1 = x === 0 || x === sizeX - 1;
458
+ const border2 = y === 0 || y === sizeY - 1;
459
  const material = getLineMaterial(border1, border2);
460
 
461
  const points = [];
 
487
  const axisLength = maxOffset * 1.2;
488
  const axesMaterial = [
489
  new THREE.LineBasicMaterial({ color: 0xff0000 }), // X axis - red
490
+ new THREE.LineBasicMaterial({ color: 0x00ff00 }) // Y axis - green
491
  ];
492
 
493
  // X axis
494
+ const xPoints = [new THREE.Vector3(0, 0, 0), new THREE.Vector3(axisLength, 0, 0)];
 
 
 
495
  const xGeometry = new THREE.BufferGeometry().setFromPoints(xPoints);
496
  const xLine = new THREE.Line(xGeometry, axesMaterial[0]);
497
  this.gridGroup.add(xLine);
498
 
499
  // Y axis
500
+ const yPoints = [new THREE.Vector3(0, 0, 0), new THREE.Vector3(0, axisLength, 0)];
 
 
 
501
  const yGeometry = new THREE.BufferGeometry().setFromPoints(yPoints);
502
  const yLine = new THREE.Line(yGeometry, axesMaterial[1]);
503
  this.gridGroup.add(yLine);
 
512
  const offsetZ = ((sizeZ - 1) * spacing) / 2;
513
 
514
  // Create small spheres at each grid intersection
515
+ const pointGeometry = new THREE.SphereGeometry(
516
+ SIZES.INTERSECTION_POINT_RADIUS,
517
+ SIZES.POINT_SEGMENTS,
518
+ SIZES.POINT_SEGMENTS
519
+ );
520
 
521
  for (let x = 0; x < sizeX; x++) {
522
  for (let y = 0; y < sizeY; y++) {
 
540
  }
541
  }
542
 
 
543
  private createJoints(): void {
544
  const { x: sizeX, y: sizeY, z: sizeZ } = this.boardShape;
545
  const spacing = this.gridSpacing;
 
560
 
561
  // Create X-axis joint (between current and next X position)
562
  if (x < sizeX - 1) {
563
+ const geometry = new THREE.CylinderGeometry(
564
+ jointRadius,
565
+ jointRadius,
566
+ jointLength,
567
+ SIZES.JOINT_SEGMENTS
568
+ );
569
  const material = new THREE.MeshPhongMaterial({
570
  color: COLORS.STONE_BLACK,
571
  shininess: SHININESS.STONE_BLACK,
 
588
 
589
  // Create Y-axis joint (between current and next Y position)
590
  if (y < sizeY - 1) {
591
+ const geometry = new THREE.CylinderGeometry(
592
+ jointRadius,
593
+ jointRadius,
594
+ jointLength,
595
+ SIZES.JOINT_SEGMENTS
596
+ );
597
  const material = new THREE.MeshPhongMaterial({
598
  color: COLORS.STONE_BLACK,
599
  shininess: SHININESS.STONE_BLACK,
 
616
 
617
  // Create Z-axis joint (between current and next Z position)
618
  if (z < sizeZ - 1) {
619
+ const geometry = new THREE.CylinderGeometry(
620
+ jointRadius,
621
+ jointRadius,
622
+ jointLength,
623
+ SIZES.JOINT_SEGMENTS
624
+ );
625
  const material = new THREE.MeshPhongMaterial({
626
  color: COLORS.STONE_BLACK,
627
  shininess: SHININESS.STONE_BLACK,
 
648
  }
649
  }
650
 
 
651
  private createDomainCubes(): void {
652
  const { x: sizeX, y: sizeY, z: sizeZ } = this.boardShape;
653
  const spacing = this.gridSpacing;
 
672
  color: COLORS.STONE_BLACK,
673
  transparent: true,
674
  opacity: OPACITY.DOMAIN_BLACK,
675
+ depthWrite: false // Prevent z-fighting with stones
676
  });
677
 
678
  const cube = new THREE.Mesh(geometry, material);
 
712
  const offsetZ = ((this.boardShape.z - 1) * spacing) / 2;
713
 
714
  // Create stone geometry
715
+ const geometry = new THREE.SphereGeometry(
716
+ SIZES.STONE_RADIUS * this.gridSpacing,
717
+ SIZES.STONE_SEGMENTS,
718
+ SIZES.STONE_SEGMENTS
719
+ );
720
  const material = new THREE.MeshPhongMaterial({
721
  color: color === "black" ? COLORS.STONE_BLACK : COLORS.STONE_WHITE,
722
  shininess: color === "black" ? SHININESS.STONE_BLACK : SHININESS.STONE_WHITE,
 
726
  });
727
 
728
  const stoneMesh = new THREE.Mesh(geometry, material);
729
+ stoneMesh.position.set(x * spacing - offsetX, y * spacing - offsetY, z * spacing - offsetZ);
 
 
 
 
730
 
731
  const stone: Stone = {
732
  position: { x, y, z },
 
798
  return this.stones.has(this.getStoneKey(x, y, z));
799
  }
800
 
 
801
  private refreshJoints(): void {
802
  const { x: sizeX, y: sizeY, z: sizeZ } = this.boardShape;
803
 
 
819
 
820
  if (nextStone && nextStone.color === centerStone.color) {
821
  const material = jointNodes.X.material as THREE.MeshPhongMaterial;
822
+ material.color.set(
823
+ centerStone.color === "black"
824
+ ? COLORS.STONE_BLACK
825
+ : COLORS.STONE_WHITE
826
+ );
827
+ material.shininess =
828
+ centerStone.color === "black"
829
+ ? SHININESS.STONE_BLACK
830
+ : SHININESS.STONE_WHITE;
831
+ material.specular.set(
832
+ centerStone.color === "black"
833
+ ? COLORS.STONE_BLACK_SPECULAR
834
+ : COLORS.STONE_WHITE_SPECULAR
835
+ );
836
  material.opacity = 1.0;
837
  material.transparent = false;
838
  jointNodes.X.visible = true;
 
852
 
853
  if (nextStone && nextStone.color === centerStone.color) {
854
  const material = jointNodes.Y.material as THREE.MeshPhongMaterial;
855
+ material.color.set(
856
+ centerStone.color === "black"
857
+ ? COLORS.STONE_BLACK
858
+ : COLORS.STONE_WHITE
859
+ );
860
+ material.shininess =
861
+ centerStone.color === "black"
862
+ ? SHININESS.STONE_BLACK
863
+ : SHININESS.STONE_WHITE;
864
+ material.specular.set(
865
+ centerStone.color === "black"
866
+ ? COLORS.STONE_BLACK_SPECULAR
867
+ : COLORS.STONE_WHITE_SPECULAR
868
+ );
869
  material.opacity = 1.0;
870
  material.transparent = false;
871
  jointNodes.Y.visible = true;
 
885
 
886
  if (nextStone && nextStone.color === centerStone.color) {
887
  const material = jointNodes.Z.material as THREE.MeshPhongMaterial;
888
+ material.color.set(
889
+ centerStone.color === "black"
890
+ ? COLORS.STONE_BLACK
891
+ : COLORS.STONE_WHITE
892
+ );
893
+ material.shininess =
894
+ centerStone.color === "black"
895
+ ? SHININESS.STONE_BLACK
896
+ : SHININESS.STONE_WHITE;
897
+ material.specular.set(
898
+ centerStone.color === "black"
899
+ ? COLORS.STONE_BLACK_SPECULAR
900
+ : COLORS.STONE_WHITE_SPECULAR
901
+ );
902
  material.opacity = 1.0;
903
  material.transparent = false;
904
  jointNodes.Z.visible = true;
 
911
  }
912
 
913
  // Show preview joints if hovering over this position
914
+ const isHovered =
915
+ this.hoveredPosition &&
916
  this.hoveredPosition.x === x &&
917
  this.hoveredPosition.y === y &&
918
  this.hoveredPosition.z === z;
 
920
  if (isHovered && this.isGameActive && !centerStone) {
921
  // Preview joints connecting to adjacent stones of current player's color
922
  const previewColor = this.currentPlayerColor;
923
+ const previewOpacity =
924
+ previewColor === "black"
925
+ ? OPACITY.PREVIEW_JOINT_BLACK
926
+ : OPACITY.PREVIEW_JOINT_WHITE;
927
 
928
  // Check -X direction (left neighbor)
929
  if (x > 0) {
 
932
  if (leftStone && leftStone.color === previewColor) {
933
  const leftJointNodes = this.joints.get(leftKey);
934
  if (leftJointNodes?.X) {
935
+ const material = leftJointNodes.X
936
+ .material as THREE.MeshPhongMaterial;
937
+ material.color.set(
938
+ previewColor === "black"
939
+ ? COLORS.STONE_BLACK
940
+ : COLORS.STONE_WHITE
941
+ );
942
+ material.shininess =
943
+ previewColor === "black"
944
+ ? SHININESS.STONE_BLACK
945
+ : SHININESS.STONE_WHITE;
946
+ material.specular.set(
947
+ previewColor === "black"
948
+ ? COLORS.STONE_BLACK_SPECULAR
949
+ : COLORS.STONE_WHITE_SPECULAR
950
+ );
951
  material.opacity = previewOpacity;
952
  material.transparent = true;
953
  leftJointNodes.X.visible = true;
 
962
  if (bottomStone && bottomStone.color === previewColor) {
963
  const bottomJointNodes = this.joints.get(bottomKey);
964
  if (bottomJointNodes?.Y) {
965
+ const material = bottomJointNodes.Y
966
+ .material as THREE.MeshPhongMaterial;
967
+ material.color.set(
968
+ previewColor === "black"
969
+ ? COLORS.STONE_BLACK
970
+ : COLORS.STONE_WHITE
971
+ );
972
+ material.shininess =
973
+ previewColor === "black"
974
+ ? SHININESS.STONE_BLACK
975
+ : SHININESS.STONE_WHITE;
976
+ material.specular.set(
977
+ previewColor === "black"
978
+ ? COLORS.STONE_BLACK_SPECULAR
979
+ : COLORS.STONE_WHITE_SPECULAR
980
+ );
981
  material.opacity = previewOpacity;
982
  material.transparent = true;
983
  bottomJointNodes.Y.visible = true;
 
992
  if (backStone && backStone.color === previewColor) {
993
  const backJointNodes = this.joints.get(backKey);
994
  if (backJointNodes?.Z) {
995
+ const material = backJointNodes.Z
996
+ .material as THREE.MeshPhongMaterial;
997
+ material.color.set(
998
+ previewColor === "black"
999
+ ? COLORS.STONE_BLACK
1000
+ : COLORS.STONE_WHITE
1001
+ );
1002
+ material.shininess =
1003
+ previewColor === "black"
1004
+ ? SHININESS.STONE_BLACK
1005
+ : SHININESS.STONE_WHITE;
1006
+ material.specular.set(
1007
+ previewColor === "black"
1008
+ ? COLORS.STONE_BLACK_SPECULAR
1009
+ : COLORS.STONE_WHITE_SPECULAR
1010
+ );
1011
  material.opacity = previewOpacity;
1012
  material.transparent = true;
1013
  backJointNodes.Z.visible = true;
 
1021
  const rightStone = this.stones.get(rightKey);
1022
  if (rightStone && rightStone.color === previewColor) {
1023
  const material = jointNodes.X.material as THREE.MeshPhongMaterial;
1024
+ material.color.set(
1025
+ previewColor === "black"
1026
+ ? COLORS.STONE_BLACK
1027
+ : COLORS.STONE_WHITE
1028
+ );
1029
+ material.shininess =
1030
+ previewColor === "black"
1031
+ ? SHININESS.STONE_BLACK
1032
+ : SHININESS.STONE_WHITE;
1033
+ material.specular.set(
1034
+ previewColor === "black"
1035
+ ? COLORS.STONE_BLACK_SPECULAR
1036
+ : COLORS.STONE_WHITE_SPECULAR
1037
+ );
1038
  material.opacity = previewOpacity;
1039
  material.transparent = true;
1040
  jointNodes.X.visible = true;
 
1047
  const topStone = this.stones.get(topKey);
1048
  if (topStone && topStone.color === previewColor) {
1049
  const material = jointNodes.Y.material as THREE.MeshPhongMaterial;
1050
+ material.color.set(
1051
+ previewColor === "black"
1052
+ ? COLORS.STONE_BLACK
1053
+ : COLORS.STONE_WHITE
1054
+ );
1055
+ material.shininess =
1056
+ previewColor === "black"
1057
+ ? SHININESS.STONE_BLACK
1058
+ : SHININESS.STONE_WHITE;
1059
+ material.specular.set(
1060
+ previewColor === "black"
1061
+ ? COLORS.STONE_BLACK_SPECULAR
1062
+ : COLORS.STONE_WHITE_SPECULAR
1063
+ );
1064
  material.opacity = previewOpacity;
1065
  material.transparent = true;
1066
  jointNodes.Y.visible = true;
 
1073
  const frontStone = this.stones.get(frontKey);
1074
  if (frontStone && frontStone.color === previewColor) {
1075
  const material = jointNodes.Z.material as THREE.MeshPhongMaterial;
1076
+ material.color.set(
1077
+ previewColor === "black"
1078
+ ? COLORS.STONE_BLACK
1079
+ : COLORS.STONE_WHITE
1080
+ );
1081
+ material.shininess =
1082
+ previewColor === "black"
1083
+ ? SHININESS.STONE_BLACK
1084
+ : SHININESS.STONE_WHITE;
1085
+ material.specular.set(
1086
+ previewColor === "black"
1087
+ ? COLORS.STONE_BLACK_SPECULAR
1088
+ : COLORS.STONE_WHITE_SPECULAR
1089
+ );
1090
  material.opacity = previewOpacity;
1091
  material.transparent = true;
1092
  jointNodes.Z.visible = true;
 
1098
  }
1099
  }
1100
 
 
1101
  public setCurrentPlayer(color: "black" | "white"): void {
1102
  this.currentPlayerColor = color;
1103
  this.updatePreviewStoneColor();
 
1107
  this.isGameActive = active;
1108
  }
1109
 
 
1110
  public hidePreviewStone(): void {
1111
  if (this.previewStone) {
1112
  this.previewStone.visible = false;
 
1115
  this.refreshJoints();
1116
  }
1117
 
 
1118
  public setLastPlacedStone(x: number | null, y: number | null, z: number | null): void {
1119
  // Clear previous stone's emissive glow
1120
  if (this.lastPlacedStone) {
1121
+ const prevKey = this.getStoneKey(
1122
+ this.lastPlacedStone.x,
1123
+ this.lastPlacedStone.y,
1124
+ this.lastPlacedStone.z
1125
+ );
1126
  const prevStone = this.stones.get(prevKey);
1127
  if (prevStone && prevStone.mesh) {
1128
  const prevMaterial = prevStone.mesh.material as THREE.MeshPhongMaterial;
 
1139
  }
1140
  }
1141
 
 
1142
  // Domain visibility methods (for territory display)
1143
  public setBlackDomainVisible(visible: boolean): void {
1144
  if (this.blackDomainVisible !== visible) {
 
1183
  this.setWhiteDomainVisible(false);
1184
  }
1185
 
 
1186
  public setBoardShape(shape: BoardShape): void {
1187
+ if (
1188
+ shape.x === this.boardShape.x &&
1189
+ shape.y === this.boardShape.y &&
1190
+ shape.z === this.boardShape.z
1191
+ )
1192
+ return;
1193
 
1194
  this.boardShape = shape;
1195
 
 
1284
  }
1285
 
1286
  // Check if mouse is not hovering over canvas and cleanup
1287
+ if (!this.canvas.matches(":hover")) {
1288
  // Hide preview stone when mouse is outside canvas
1289
  if (this.previewStone) {
1290
  this.previewStone.visible = false;
 
1341
 
1342
  // Remove previous highlight
1343
  if (this.highlightedPoint) {
1344
+ (this.highlightedPoint.material as THREE.MeshBasicMaterial).color.set(
1345
+ COLORS.POINT_DEFAULT
1346
+ );
1347
+ (this.highlightedPoint.material as THREE.MeshBasicMaterial).opacity =
1348
+ OPACITY.POINT_DEFAULT;
1349
  this.highlightedPoint = null;
1350
  }
1351
 
 
1367
 
1368
  this.highlightedPoint = point;
1369
  // Use green for valid/droppable, red for invalid (game inactive or violates rules)
1370
+ const hoverColor = isDroppable
1371
+ ? COLORS.POINT_HOVERED
1372
+ : COLORS.POINT_HOVERED_DISABLED;
1373
  (point.material as THREE.MeshBasicMaterial).color.set(hoverColor);
1374
  (point.material as THREE.MeshBasicMaterial).opacity = OPACITY.POINT_HOVERED;
1375
 
 
1482
 
1483
  // Update last placed stone highlight effect only when mouse is over canvas
1484
  // Using :hover pseudo-class check for better accuracy
1485
+ if (this.canvas.matches && this.canvas.matches(":hover")) {
1486
  this.updateLastStoneHighlight();
1487
  }
1488
 
 
1502
  this.lastCameraDistance = cameraDistance;
1503
 
1504
  // Calculate diagonal distance of the board
1505
+ const diagonal = Math.sqrt(
1506
+ this.boardShape.x ** 2 + this.boardShape.y ** 2 + this.boardShape.z ** 2
1507
+ );
1508
  const boardDiagonal = diagonal * this.gridSpacing;
1509
 
1510
  // Update fog near and far based on camera distance +/- diagonal
 
1535
  const emissiveColor = new THREE.Color(...SHINING.EMISSIVE_COLOR);
1536
 
1537
  // Scale intensity based on stone color (white stones get brighter glow)
1538
+ const baseIntensity =
1539
+ stone.color === "white"
1540
+ ? SHINING.BASE_INTENSITY_WHITE
1541
+ : SHINING.BASE_INTENSITY_BLACK;
1542
+ const flickerIntensity =
1543
+ stone.color === "white"
1544
+ ? SHINING.FLICKER_INTENSITY_WHITE
1545
+ : SHINING.FLICKER_INTENSITY_BLACK;
1546
  const intensity = flicker * flickerIntensity + baseIntensity;
1547
 
1548
  material.emissive = emissiveColor;
 
1563
 
1564
  if (intersects.length > 0) {
1565
  // Find the position of the clicked stone
1566
+ let clickedPosition: { x: number; y: number; z: number } | null = null;
1567
 
1568
  for (const [, stone] of this.stones.entries()) {
1569
  if (stone.mesh === intersects[0].object) {
 
1582
 
1583
  // Notify callback with group info
1584
  if (this.callbacks.onInspectGroup) {
1585
+ this.callbacks.onInspectGroup(
1586
+ this.highlightedGroup.size,
1587
+ libertiesResult.count
1588
+ );
1589
  }
1590
  }
1591
  } else {
 
1598
  }
1599
  }
1600
 
1601
+ private findConnectedGroup(startPos: { x: number; y: number; z: number }): Set<string> {
1602
  const group = new Set<string>();
1603
  const startKey = this.getStoneKey(startPos.x, startPos.y, startPos.z);
1604
  const startStone = this.stones.get(startKey);
 
1606
  if (!startStone) return group;
1607
 
1608
  const color = startStone.color;
1609
+ const queue: { x: number; y: number; z: number }[] = [startPos];
1610
  const visited = new Set<string>();
1611
 
1612
  while (queue.length > 0) {
 
1623
 
1624
  // Check all 6 neighbors in 3D space
1625
  const neighbors = [
1626
+ { x: pos.x + 1, y: pos.y, z: pos.z },
1627
+ { x: pos.x - 1, y: pos.y, z: pos.z },
1628
+ { x: pos.x, y: pos.y + 1, z: pos.z },
1629
+ { x: pos.x, y: pos.y - 1, z: pos.z },
1630
+ { x: pos.x, y: pos.y, z: pos.z + 1 },
1631
+ { x: pos.x, y: pos.y, z: pos.z - 1 }
1632
  ];
1633
 
1634
  for (const neighbor of neighbors) {
1635
  // Check if neighbor is within board bounds
1636
+ if (
1637
+ neighbor.x >= 0 &&
1638
+ neighbor.x < this.boardShape.x &&
1639
+ neighbor.y >= 0 &&
1640
+ neighbor.y < this.boardShape.y &&
1641
+ neighbor.z >= 0 &&
1642
+ neighbor.z < this.boardShape.z
1643
+ ) {
1644
  const neighborKey = this.getStoneKey(neighbor.x, neighbor.y, neighbor.z);
1645
  if (!visited.has(neighborKey)) {
1646
  queue.push(neighbor);
 
1657
 
1658
  // For each stone in the group, check its neighbors for empty positions
1659
  for (const key of group) {
1660
+ const parts = key.split(",").map(Number);
1661
+ const pos = { x: parts[0], y: parts[1], z: parts[2] };
1662
 
1663
  // Check all 6 neighbors
1664
  const neighbors = [
1665
+ { x: pos.x + 1, y: pos.y, z: pos.z },
1666
+ { x: pos.x - 1, y: pos.y, z: pos.z },
1667
+ { x: pos.x, y: pos.y + 1, z: pos.z },
1668
+ { x: pos.x, y: pos.y - 1, z: pos.z },
1669
+ { x: pos.x, y: pos.y, z: pos.z + 1 },
1670
+ { x: pos.x, y: pos.y, z: pos.z - 1 }
1671
  ];
1672
 
1673
  for (const neighbor of neighbors) {
1674
  // Check if neighbor is within bounds
1675
+ if (
1676
+ neighbor.x >= 0 &&
1677
+ neighbor.x < this.boardShape.x &&
1678
+ neighbor.y >= 0 &&
1679
+ neighbor.y < this.boardShape.y &&
1680
+ neighbor.z >= 0 &&
1681
+ neighbor.z < this.boardShape.z
1682
+ ) {
1683
  const neighborKey = this.getStoneKey(neighbor.x, neighbor.y, neighbor.z);
1684
  // If neighbor is empty (not in stones map), it's a liberty
1685
  if (!this.stones.has(neighborKey)) {
 
1692
  return { count: liberties.size, positions: liberties };
1693
  }
1694
 
 
1695
  private updateStoneOpacity(): void {
1696
  // Update opacity of all stones based on inspect mode
1697
  this.stones.forEach((stone, key) => {
 
1722
  this.updateAirPatchVisualization();
1723
  }
1724
 
 
1725
  private updateAirPatchVisualization(): void {
1726
  // Reset all intersection points to default color
1727
  this.intersectionPoints.children.forEach((child) => {
 
1754
  });
1755
  }
1756
 
1757
+ private updateDomainCubesVisualization(
1758
+ blackDomain: Set<string> | null,
1759
+ whiteDomain: Set<string> | null
1760
+ ): void {
1761
  // Update domain cube visibility based on territory and air patch
1762
  this.domainCubes.forEach((cube, key) => {
1763
  const material = cube.material as THREE.MeshBasicMaterial;
 
1785
  });
1786
  }
1787
 
 
1788
  private onKeyDown(event: KeyboardEvent): void {
1789
  // Ctrl key (17) or Meta key (91/93) for Mac
1790
  if (event.ctrlKey || event.metaKey) {
 
1815
  }
1816
  }
1817
 
 
1818
  public destroy(): void {
1819
  this.isDestroyed = true;
1820
 
trigo-web/app/src/stores/gameStore.ts CHANGED
@@ -10,7 +10,6 @@ import type {
10
  TerritoryResult
11
  } from "../../../inc/trigo";
12
 
13
-
14
  /**
15
  * Game Store State
16
  *
@@ -21,7 +20,6 @@ export interface GameState {
21
  config: GameConfig;
22
  }
23
 
24
-
25
  /**
26
  * Pinia store for game state management
27
  *
@@ -36,7 +34,6 @@ export const useGameStore = defineStore("game", {
36
  }
37
  }),
38
 
39
-
40
  getters: {
41
  // Board state
42
  board: (state): Stone[][][] => state.game.getBoard(),
@@ -67,7 +64,7 @@ export const useGameStore = defineStore("game", {
67
  const counts = state.game.getCapturedCounts();
68
  return {
69
  black: counts.white, // Black captured white stones
70
- white: counts.black // White captured black stones
71
  };
72
  },
73
 
@@ -83,18 +80,23 @@ export const useGameStore = defineStore("game", {
83
  },
84
 
85
  // Position checks
86
- isValidPosition: (state) => (x: number, y: number, z: number): boolean => {
87
- return state.game.isValidPosition(x, y, z);
88
- },
89
- isEmpty: (state) => (x: number, y: number, z: number): boolean => {
90
- return state.game.isEmpty(x, y, z);
91
- },
92
- getStone: (state) => (x: number, y: number, z: number): Stone => {
93
- return state.game.getStoneAt(x, y, z) as Stone;
94
- }
 
 
 
 
 
 
95
  },
96
 
97
-
98
  actions: {
99
  /**
100
  * Initialize a new game
@@ -105,7 +107,6 @@ export const useGameStore = defineStore("game", {
105
  this.saveToSessionStorage();
106
  },
107
 
108
-
109
  /**
110
  * Start the game
111
  */
@@ -114,11 +115,14 @@ export const useGameStore = defineStore("game", {
114
  this.saveToSessionStorage();
115
  },
116
 
117
-
118
  /**
119
  * Make a move
120
  */
121
- makeMove(x: number, y: number, z: number): { success: boolean; capturedPositions?: Position[] } {
 
 
 
 
122
  // Check if game is active
123
  if (!this.game.isGameActive()) {
124
  console.warn("Game is not active");
@@ -142,7 +146,6 @@ export const useGameStore = defineStore("game", {
142
  return { success: false };
143
  },
144
 
145
-
146
  /**
147
  * Pass turn
148
  */
@@ -158,7 +161,6 @@ export const useGameStore = defineStore("game", {
158
  return success;
159
  },
160
 
161
-
162
  /**
163
  * Resign/surrender
164
  */
@@ -174,7 +176,6 @@ export const useGameStore = defineStore("game", {
174
  return success;
175
  },
176
 
177
-
178
  /**
179
  * Undo last move
180
  */
@@ -190,7 +191,6 @@ export const useGameStore = defineStore("game", {
190
  return success;
191
  },
192
 
193
-
194
  /**
195
  * Redo next move
196
  */
@@ -206,7 +206,6 @@ export const useGameStore = defineStore("game", {
206
  return success;
207
  },
208
 
209
-
210
  /**
211
  * Jump to specific move in history
212
  */
@@ -218,7 +217,6 @@ export const useGameStore = defineStore("game", {
218
  return success;
219
  },
220
 
221
-
222
  /**
223
  * Reset game to initial state
224
  */
@@ -227,7 +225,6 @@ export const useGameStore = defineStore("game", {
227
  this.saveToSessionStorage();
228
  },
229
 
230
-
231
  /**
232
  * Change board shape (only when game is idle)
233
  */
@@ -241,7 +238,6 @@ export const useGameStore = defineStore("game", {
241
  return true;
242
  },
243
 
244
-
245
  /**
246
  * Calculate territory
247
  */
@@ -249,7 +245,6 @@ export const useGameStore = defineStore("game", {
249
  return this.game.computeTerritory();
250
  },
251
 
252
-
253
  /**
254
  * Get neighboring positions (6 directions in 3D)
255
  */
@@ -266,7 +261,6 @@ export const useGameStore = defineStore("game", {
266
  return neighbors.filter((pos) => this.isValidPosition(pos.x, pos.y, pos.z));
267
  },
268
 
269
-
270
  /**
271
  * Save game state to session storage
272
  */
@@ -278,7 +272,6 @@ export const useGameStore = defineStore("game", {
278
  }
279
  },
280
 
281
-
282
  /**
283
  * Load game state from session storage
284
  */
@@ -295,7 +288,6 @@ export const useGameStore = defineStore("game", {
295
  }
296
  },
297
 
298
-
299
  /**
300
  * Clear saved game state
301
  */
 
10
  TerritoryResult
11
  } from "../../../inc/trigo";
12
 
 
13
  /**
14
  * Game Store State
15
  *
 
20
  config: GameConfig;
21
  }
22
 
 
23
  /**
24
  * Pinia store for game state management
25
  *
 
34
  }
35
  }),
36
 
 
37
  getters: {
38
  // Board state
39
  board: (state): Stone[][][] => state.game.getBoard(),
 
64
  const counts = state.game.getCapturedCounts();
65
  return {
66
  black: counts.white, // Black captured white stones
67
+ white: counts.black // White captured black stones
68
  };
69
  },
70
 
 
80
  },
81
 
82
  // Position checks
83
+ isValidPosition:
84
+ (state) =>
85
+ (x: number, y: number, z: number): boolean => {
86
+ return state.game.isValidPosition(x, y, z);
87
+ },
88
+ isEmpty:
89
+ (state) =>
90
+ (x: number, y: number, z: number): boolean => {
91
+ return state.game.isEmpty(x, y, z);
92
+ },
93
+ getStone:
94
+ (state) =>
95
+ (x: number, y: number, z: number): Stone => {
96
+ return state.game.getStoneAt(x, y, z) as Stone;
97
+ }
98
  },
99
 
 
100
  actions: {
101
  /**
102
  * Initialize a new game
 
107
  this.saveToSessionStorage();
108
  },
109
 
 
110
  /**
111
  * Start the game
112
  */
 
115
  this.saveToSessionStorage();
116
  },
117
 
 
118
  /**
119
  * Make a move
120
  */
121
+ makeMove(
122
+ x: number,
123
+ y: number,
124
+ z: number
125
+ ): { success: boolean; capturedPositions?: Position[] } {
126
  // Check if game is active
127
  if (!this.game.isGameActive()) {
128
  console.warn("Game is not active");
 
146
  return { success: false };
147
  },
148
 
 
149
  /**
150
  * Pass turn
151
  */
 
161
  return success;
162
  },
163
 
 
164
  /**
165
  * Resign/surrender
166
  */
 
176
  return success;
177
  },
178
 
 
179
  /**
180
  * Undo last move
181
  */
 
191
  return success;
192
  },
193
 
 
194
  /**
195
  * Redo next move
196
  */
 
206
  return success;
207
  },
208
 
 
209
  /**
210
  * Jump to specific move in history
211
  */
 
217
  return success;
218
  },
219
 
 
220
  /**
221
  * Reset game to initial state
222
  */
 
225
  this.saveToSessionStorage();
226
  },
227
 
 
228
  /**
229
  * Change board shape (only when game is idle)
230
  */
 
238
  return true;
239
  },
240
 
 
241
  /**
242
  * Calculate territory
243
  */
 
245
  return this.game.computeTerritory();
246
  },
247
 
 
248
  /**
249
  * Get neighboring positions (6 directions in 3D)
250
  */
 
261
  return neighbors.filter((pos) => this.isValidPosition(pos.x, pos.y, pos.z));
262
  },
263
 
 
264
  /**
265
  * Save game state to session storage
266
  */
 
272
  }
273
  },
274
 
 
275
  /**
276
  * Load game state from session storage
277
  */
 
288
  }
289
  },
290
 
 
291
  /**
292
  * Clear saved game state
293
  */
trigo-web/app/src/utils/TrigoGameFrontend.ts CHANGED
@@ -18,7 +18,6 @@ import {
18
  type GameResult
19
  } from "../../../inc/trigo";
20
 
21
-
22
  /**
23
  * TrigoGameFrontend - Extended TrigoGame with frontend-friendly interfaces
24
  */
@@ -35,7 +34,6 @@ export class TrigoGameFrontend extends TrigoGame {
35
  return this.drop(makePosition(x, y, z));
36
  }
37
 
38
-
39
  /**
40
  * Get current player as string
41
  *
@@ -45,7 +43,6 @@ export class TrigoGameFrontend extends TrigoGame {
45
  return stoneToPlayer(this.getCurrentPlayer());
46
  }
47
 
48
-
49
  /**
50
  * Get move history in frontend format
51
  *
@@ -55,7 +52,6 @@ export class TrigoGameFrontend extends TrigoGame {
55
  return stepsToMoves(this.getHistory());
56
  }
57
 
58
-
59
  /**
60
  * Get stone at position, returns number format for frontend
61
  *
@@ -68,7 +64,6 @@ export class TrigoGameFrontend extends TrigoGame {
68
  return this.getStone(makePosition(x, y, z)) as number;
69
  }
70
 
71
-
72
  /**
73
  * Check if position is valid
74
  *
@@ -79,17 +74,9 @@ export class TrigoGameFrontend extends TrigoGame {
79
  */
80
  isValidPosition(x: number, y: number, z: number): boolean {
81
  const shape = this.getShape();
82
- return (
83
- x >= 0 &&
84
- x < shape.x &&
85
- y >= 0 &&
86
- y < shape.y &&
87
- z >= 0 &&
88
- z < shape.z
89
- );
90
  }
91
 
92
-
93
  /**
94
  * Check if position is empty
95
  *
@@ -105,7 +92,6 @@ export class TrigoGameFrontend extends TrigoGame {
105
  return this.getStoneAt(x, y, z) === 0;
106
  }
107
 
108
-
109
  /**
110
  * Check if a move is valid at position
111
  *
@@ -118,7 +104,6 @@ export class TrigoGameFrontend extends TrigoGame {
118
  return this.isValidMove(makePosition(x, y, z));
119
  }
120
 
121
-
122
  /**
123
  * Get opponent player
124
  *
@@ -129,7 +114,6 @@ export class TrigoGameFrontend extends TrigoGame {
129
  return current === "black" ? "white" : "black";
130
  }
131
 
132
-
133
  /**
134
  * Check if undo is available
135
  *
@@ -139,7 +123,6 @@ export class TrigoGameFrontend extends TrigoGame {
139
  return this.getCurrentStep() > 0;
140
  }
141
 
142
-
143
  /**
144
  * Undo move (convenience wrapper)
145
  *
@@ -149,7 +132,6 @@ export class TrigoGameFrontend extends TrigoGame {
149
  return this.undo();
150
  }
151
 
152
-
153
  /**
154
  * Redo move (convenience wrapper)
155
  *
@@ -159,7 +141,6 @@ export class TrigoGameFrontend extends TrigoGame {
159
  return this.redo();
160
  }
161
 
162
-
163
  /**
164
  * Jump to specific move in history
165
  *
@@ -170,7 +151,6 @@ export class TrigoGameFrontend extends TrigoGame {
170
  return this.jumpToStep(index);
171
  }
172
 
173
-
174
  /**
175
  * Get total move count
176
  *
@@ -180,7 +160,6 @@ export class TrigoGameFrontend extends TrigoGame {
180
  return this.getCurrentStep();
181
  }
182
 
183
-
184
  /**
185
  * Get current move index
186
  *
@@ -190,7 +169,6 @@ export class TrigoGameFrontend extends TrigoGame {
190
  return this.getCurrentStep();
191
  }
192
 
193
-
194
  /**
195
  * Get board shape
196
  *
@@ -200,7 +178,6 @@ export class TrigoGameFrontend extends TrigoGame {
200
  return this.getShape();
201
  }
202
 
203
-
204
  /**
205
  * Initialize game with board shape
206
  *
@@ -216,7 +193,6 @@ export class TrigoGameFrontend extends TrigoGame {
216
  }
217
  }
218
 
219
-
220
  /**
221
  * Compute territory (convenience wrapper)
222
  *
@@ -226,7 +202,6 @@ export class TrigoGameFrontend extends TrigoGame {
226
  return this.getTerritory();
227
  }
228
 
229
-
230
  /**
231
  * Get game status
232
  *
@@ -236,7 +211,6 @@ export class TrigoGameFrontend extends TrigoGame {
236
  return this.getGameStatus();
237
  }
238
 
239
-
240
  /**
241
  * Get game result
242
  *
@@ -246,7 +220,6 @@ export class TrigoGameFrontend extends TrigoGame {
246
  return this.getGameResult();
247
  }
248
 
249
-
250
  /**
251
  * Get consecutive pass count
252
  *
 
18
  type GameResult
19
  } from "../../../inc/trigo";
20
 
 
21
  /**
22
  * TrigoGameFrontend - Extended TrigoGame with frontend-friendly interfaces
23
  */
 
34
  return this.drop(makePosition(x, y, z));
35
  }
36
 
 
37
  /**
38
  * Get current player as string
39
  *
 
43
  return stoneToPlayer(this.getCurrentPlayer());
44
  }
45
 
 
46
  /**
47
  * Get move history in frontend format
48
  *
 
52
  return stepsToMoves(this.getHistory());
53
  }
54
 
 
55
  /**
56
  * Get stone at position, returns number format for frontend
57
  *
 
64
  return this.getStone(makePosition(x, y, z)) as number;
65
  }
66
 
 
67
  /**
68
  * Check if position is valid
69
  *
 
74
  */
75
  isValidPosition(x: number, y: number, z: number): boolean {
76
  const shape = this.getShape();
77
+ return x >= 0 && x < shape.x && y >= 0 && y < shape.y && z >= 0 && z < shape.z;
 
 
 
 
 
 
 
78
  }
79
 
 
80
  /**
81
  * Check if position is empty
82
  *
 
92
  return this.getStoneAt(x, y, z) === 0;
93
  }
94
 
 
95
  /**
96
  * Check if a move is valid at position
97
  *
 
104
  return this.isValidMove(makePosition(x, y, z));
105
  }
106
 
 
107
  /**
108
  * Get opponent player
109
  *
 
114
  return current === "black" ? "white" : "black";
115
  }
116
 
 
117
  /**
118
  * Check if undo is available
119
  *
 
123
  return this.getCurrentStep() > 0;
124
  }
125
 
 
126
  /**
127
  * Undo move (convenience wrapper)
128
  *
 
132
  return this.undo();
133
  }
134
 
 
135
  /**
136
  * Redo move (convenience wrapper)
137
  *
 
141
  return this.redo();
142
  }
143
 
 
144
  /**
145
  * Jump to specific move in history
146
  *
 
151
  return this.jumpToStep(index);
152
  }
153
 
 
154
  /**
155
  * Get total move count
156
  *
 
160
  return this.getCurrentStep();
161
  }
162
 
 
163
  /**
164
  * Get current move index
165
  *
 
169
  return this.getCurrentStep();
170
  }
171
 
 
172
  /**
173
  * Get board shape
174
  *
 
178
  return this.getShape();
179
  }
180
 
 
181
  /**
182
  * Initialize game with board shape
183
  *
 
193
  }
194
  }
195
 
 
196
  /**
197
  * Compute territory (convenience wrapper)
198
  *
 
202
  return this.getTerritory();
203
  }
204
 
 
205
  /**
206
  * Get game status
207
  *
 
211
  return this.getGameStatus();
212
  }
213
 
 
214
  /**
215
  * Get game result
216
  *
 
220
  return this.getGameResult();
221
  }
222
 
 
223
  /**
224
  * Get consecutive pass count
225
  *
trigo-web/app/src/utils/storage.ts ADDED
@@ -0,0 +1,261 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Frontend Storage Utility
3
+ *
4
+ * Provides a centralized API for storing and retrieving data in browser storage.
5
+ * Supports both sessionStorage and localStorage with type-safe operations.
6
+ *
7
+ * Features:
8
+ * - Type-safe storage operations
9
+ * - Automatic JSON serialization/deserialization
10
+ * - Error handling with fallback values
11
+ * - Namespaced keys to avoid collisions
12
+ * - Support for both session and local storage
13
+ */
14
+
15
+
16
+ /**
17
+ * Storage type enum
18
+ */
19
+ export enum StorageType {
20
+ SESSION = "session",
21
+ LOCAL = "local"
22
+ }
23
+
24
+
25
+ /**
26
+ * Storage keys enum - Add new keys here
27
+ */
28
+ export enum StorageKey {
29
+ // Game state
30
+ GAME_STATE = "trigoGameState",
31
+
32
+ // AI settings
33
+ AI_PLAYER_COLOR = "trigoAIPlayerColor",
34
+
35
+ // User preferences (examples for future use)
36
+ // THEME = "trigoTheme",
37
+ // LANGUAGE = "trigoLanguage",
38
+ // BOARD_SHAPE = "trigoBoardShape",
39
+ // CAMERA_POSITION = "trigoCameraPosition",
40
+ }
41
+
42
+
43
+ /**
44
+ * Storage configuration
45
+ */
46
+ interface StorageConfig {
47
+ type: StorageType;
48
+ prefix?: string; // Optional namespace prefix for all keys
49
+ }
50
+
51
+
52
+ /**
53
+ * Default configuration
54
+ */
55
+ const DEFAULT_CONFIG: StorageConfig = {
56
+ type: StorageType.SESSION,
57
+ prefix: "" // No prefix by default, keys already have 'trigo' prefix
58
+ };
59
+
60
+
61
+ /**
62
+ * Get the appropriate storage instance
63
+ */
64
+ function getStorage(type: StorageType): Storage {
65
+ return type === StorageType.SESSION ? sessionStorage : localStorage;
66
+ }
67
+
68
+
69
+ /**
70
+ * Build full storage key with optional prefix
71
+ */
72
+ function buildKey(key: StorageKey, prefix?: string): string {
73
+ return prefix ? `${prefix}${key}` : key;
74
+ }
75
+
76
+
77
+ /**
78
+ * Storage Manager Class
79
+ */
80
+ export class StorageManager {
81
+ private config: StorageConfig;
82
+
83
+ constructor(config: Partial<StorageConfig> = {}) {
84
+ this.config = { ...DEFAULT_CONFIG, ...config };
85
+ }
86
+
87
+ /**
88
+ * Get storage instance based on configuration
89
+ */
90
+ private getStorageInstance(): Storage {
91
+ return getStorage(this.config.type);
92
+ }
93
+
94
+ /**
95
+ * Set a value in storage
96
+ * @param key - Storage key
97
+ * @param value - Value to store (will be JSON stringified)
98
+ * @returns true if successful, false otherwise
99
+ */
100
+ set<T>(key: StorageKey, value: T): boolean {
101
+ try {
102
+ const storage = this.getStorageInstance();
103
+ const fullKey = buildKey(key, this.config.prefix);
104
+ const serialized = JSON.stringify(value);
105
+ storage.setItem(fullKey, serialized);
106
+ return true;
107
+ } catch (error) {
108
+ console.warn(`[Storage] Failed to set ${key}:`, error);
109
+ return false;
110
+ }
111
+ }
112
+
113
+ /**
114
+ * Get a value from storage
115
+ * @param key - Storage key
116
+ * @param defaultValue - Default value if key doesn't exist or parsing fails
117
+ * @returns Stored value or default value
118
+ */
119
+ get<T>(key: StorageKey, defaultValue: T): T {
120
+ try {
121
+ const storage = this.getStorageInstance();
122
+ const fullKey = buildKey(key, this.config.prefix);
123
+ const item = storage.getItem(fullKey);
124
+
125
+ if (item === null) {
126
+ return defaultValue;
127
+ }
128
+
129
+ return JSON.parse(item) as T;
130
+ } catch (error) {
131
+ console.warn(`[Storage] Failed to get ${key}:`, error);
132
+ return defaultValue;
133
+ }
134
+ }
135
+
136
+ /**
137
+ * Get a string value directly (no JSON parsing)
138
+ * Useful for simple string values
139
+ */
140
+ getString(key: StorageKey, defaultValue: string = ""): string {
141
+ try {
142
+ const storage = this.getStorageInstance();
143
+ const fullKey = buildKey(key, this.config.prefix);
144
+ return storage.getItem(fullKey) ?? defaultValue;
145
+ } catch (error) {
146
+ console.warn(`[Storage] Failed to get string ${key}:`, error);
147
+ return defaultValue;
148
+ }
149
+ }
150
+
151
+ /**
152
+ * Set a string value directly (no JSON stringification)
153
+ * Useful for simple string values
154
+ */
155
+ setString(key: StorageKey, value: string): boolean {
156
+ try {
157
+ const storage = this.getStorageInstance();
158
+ const fullKey = buildKey(key, this.config.prefix);
159
+ storage.setItem(fullKey, value);
160
+ return true;
161
+ } catch (error) {
162
+ console.warn(`[Storage] Failed to set string ${key}:`, error);
163
+ return false;
164
+ }
165
+ }
166
+
167
+ /**
168
+ * Remove a value from storage
169
+ * @param key - Storage key
170
+ * @returns true if successful, false otherwise
171
+ */
172
+ remove(key: StorageKey): boolean {
173
+ try {
174
+ const storage = this.getStorageInstance();
175
+ const fullKey = buildKey(key, this.config.prefix);
176
+ storage.removeItem(fullKey);
177
+ return true;
178
+ } catch (error) {
179
+ console.warn(`[Storage] Failed to remove ${key}:`, error);
180
+ return false;
181
+ }
182
+ }
183
+
184
+ /**
185
+ * Check if a key exists in storage
186
+ */
187
+ has(key: StorageKey): boolean {
188
+ try {
189
+ const storage = this.getStorageInstance();
190
+ const fullKey = buildKey(key, this.config.prefix);
191
+ return storage.getItem(fullKey) !== null;
192
+ } catch (error) {
193
+ console.warn(`[Storage] Failed to check ${key}:`, error);
194
+ return false;
195
+ }
196
+ }
197
+
198
+ /**
199
+ * Clear all items from storage
200
+ * WARNING: This clears ALL items in the storage, not just trigo-related ones
201
+ */
202
+ clear(): boolean {
203
+ try {
204
+ const storage = this.getStorageInstance();
205
+ storage.clear();
206
+ return true;
207
+ } catch (error) {
208
+ console.warn("[Storage] Failed to clear storage:", error);
209
+ return false;
210
+ }
211
+ }
212
+
213
+ /**
214
+ * Get the current storage type
215
+ */
216
+ getStorageType(): StorageType {
217
+ return this.config.type;
218
+ }
219
+
220
+ /**
221
+ * Switch storage type
222
+ */
223
+ setStorageType(type: StorageType): void {
224
+ this.config.type = type;
225
+ }
226
+ }
227
+
228
+
229
+ /**
230
+ * Default storage manager instance (sessionStorage)
231
+ */
232
+ export const storage = new StorageManager();
233
+
234
+
235
+ /**
236
+ * Local storage manager instance
237
+ * Note: Named localStorageManager to avoid conflict with built-in localStorage
238
+ */
239
+ export const localStorageManager = new StorageManager({ type: StorageType.LOCAL });
240
+
241
+
242
+ /**
243
+ * Example helper functions for future use:
244
+ *
245
+ * export function saveTheme(theme: "light" | "dark"): boolean {
246
+ * return localStorageManager.setString(StorageKey.THEME, theme);
247
+ * }
248
+ *
249
+ * export function loadTheme(): "light" | "dark" {
250
+ * const stored = localStorageManager.getString(StorageKey.THEME);
251
+ * return stored === "dark" ? "dark" : "light";
252
+ * }
253
+ *
254
+ * export function saveBoardShape(shape: { x: number; y: number; z: number }): boolean {
255
+ * return storage.set(StorageKey.BOARD_SHAPE, shape);
256
+ * }
257
+ *
258
+ * export function loadBoardShape(): { x: number; y: number; z: number } | null {
259
+ * return storage.get(StorageKey.BOARD_SHAPE, null);
260
+ * }
261
+ */
trigo-web/app/src/views/OnnxTestView.vue ADDED
@@ -0,0 +1,511 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <template>
2
+ <div class="onnx-test-view">
3
+ <div class="test-header">
4
+ <h1>ONNX Model Inferencer Test</h1>
5
+ <p class="subtitle">Testing GPT-2 Causal LM with ONNX Runtime Web</p>
6
+ </div>
7
+
8
+ <div class="test-body">
9
+ <!-- Model Status -->
10
+ <div class="test-section">
11
+ <h2>Model Status</h2>
12
+ <div class="status-info">
13
+ <div
14
+ class="status-item"
15
+ :class="{ ready: isInitialized, loading: isInitializing }"
16
+ >
17
+ <span class="status-label">Status:</span>
18
+ <span class="status-value">
19
+ {{
20
+ isInitialized
21
+ ? "✓ Ready"
22
+ : isInitializing
23
+ ? "⏳ Initializing..."
24
+ : "⚪ Not initialized"
25
+ }}
26
+ </span>
27
+ </div>
28
+ <div v-if="modelInfo" class="status-item">
29
+ <span class="status-label">Inputs:</span>
30
+ <span class="status-value">{{ modelInfo.inputs.join(", ") }}</span>
31
+ </div>
32
+ <div v-if="modelInfo" class="status-item">
33
+ <span class="status-label">Outputs:</span>
34
+ <span class="status-value">{{ modelInfo.outputs.join(", ") }}</span>
35
+ </div>
36
+ <div v-if="error" class="error-message">❌ Error: {{ error }}</div>
37
+ </div>
38
+
39
+ <button
40
+ class="btn btn-primary"
41
+ @click="initialize"
42
+ :disabled="isInitializing || isInitialized"
43
+ >
44
+ {{ isInitialized ? "Initialized" : "Initialize Model" }}
45
+ </button>
46
+ </div>
47
+
48
+ <!-- Basic Inference Test -->
49
+ <div class="test-section">
50
+ <h2>Test 1: Basic Inference</h2>
51
+ <p>Run a single forward pass with random input</p>
52
+
53
+ <button
54
+ class="btn btn-test"
55
+ @click="runBasicTest"
56
+ :disabled="!isInitialized || isRunning"
57
+ >
58
+ {{ isRunning ? "Running..." : "Run Basic Test" }}
59
+ </button>
60
+
61
+ <div v-if="basicTestResult" class="test-result">
62
+ <h3>Results:</h3>
63
+ <div class="result-item">
64
+ <span class="label">Inference Time:</span>
65
+ <span class="value">{{ basicTestResult.inferenceTime.toFixed(2) }}ms</span>
66
+ </div>
67
+ <div class="result-item">
68
+ <span class="label">Sample Tokens:</span>
69
+ <span class="value code"
70
+ >{{ basicTestResult.tokens.slice(0, 20).join(", ") }}...</span
71
+ >
72
+ </div>
73
+ <div class="result-item">
74
+ <span class="label">Sample Text:</span>
75
+ <span class="value code">{{ basicTestResult.text.slice(0, 100) }}...</span>
76
+ </div>
77
+ </div>
78
+ </div>
79
+
80
+ <!-- Text Generation Test -->
81
+ <div class="test-section">
82
+ <h2>Test 2: Text Generation</h2>
83
+ <p>Generate tokens autoregressively from a prompt</p>
84
+
85
+ <div class="input-group">
86
+ <label for="prompt">Prompt:</label>
87
+ <input
88
+ id="prompt"
89
+ v-model="prompt"
90
+ type="text"
91
+ placeholder="Enter prompt text..."
92
+ :disabled="!isInitialized"
93
+ />
94
+ </div>
95
+
96
+ <div class="input-group">
97
+ <label for="numTokens">Number of Tokens:</label>
98
+ <input
99
+ id="numTokens"
100
+ v-model.number="numTokens"
101
+ type="number"
102
+ min="1"
103
+ max="50"
104
+ :disabled="!isInitialized"
105
+ />
106
+ </div>
107
+
108
+ <button
109
+ class="btn btn-test"
110
+ @click="runGenerationTest"
111
+ :disabled="!isInitialized || isRunning || !prompt"
112
+ >
113
+ {{ isRunning ? "Generating..." : "Generate Text" }}
114
+ </button>
115
+
116
+ <div v-if="generationResult" class="test-result">
117
+ <h3>Generated Output:</h3>
118
+ <div class="result-item">
119
+ <span class="label">Avg Inference Time:</span>
120
+ <span class="value">{{ generationResult.inferenceTime.toFixed(2) }}ms</span>
121
+ </div>
122
+ <div class="result-item">
123
+ <span class="label">Tokens/sec:</span>
124
+ <span class="value">{{
125
+ (1000 / generationResult.inferenceTime).toFixed(2)
126
+ }}</span>
127
+ </div>
128
+ <div class="result-item full-width">
129
+ <span class="label">Token Sequence:</span>
130
+ <span class="value code">{{ generationResult.tokens.join(", ") }}</span>
131
+ </div>
132
+ <div class="result-item full-width">
133
+ <span class="label">Generated Text:</span>
134
+ <div class="generated-text">{{ generationResult.text }}</div>
135
+ </div>
136
+ </div>
137
+ </div>
138
+
139
+ <!-- Console Output -->
140
+ <div class="test-section">
141
+ <h2>Console Output</h2>
142
+ <p>Check the browser console (F12) for detailed logs</p>
143
+ <button class="btn btn-secondary" @click="clearConsole">Clear Console</button>
144
+ </div>
145
+ </div>
146
+ </div>
147
+ </template>
148
+
149
+ <script setup lang="ts">
150
+ import { ref } from "vue";
151
+ import { OnnxInferencer, type InferenceResult } from "@/services/onnxInferencer";
152
+
153
+ // Inferencer instance
154
+ let inferencer: OnnxInferencer | null = null;
155
+
156
+ // State
157
+ const isInitializing = ref(false);
158
+ const isInitialized = ref(false);
159
+ const isRunning = ref(false);
160
+ const error = ref<string | null>(null);
161
+ const modelInfo = ref<{ inputs: string[]; outputs: string[] } | null>(null);
162
+
163
+ // Test state
164
+ const basicTestResult = ref<InferenceResult | null>(null);
165
+ const generationResult = ref<InferenceResult | null>(null);
166
+ const prompt = ref("[Board 5x5]");
167
+ const numTokens = ref(10);
168
+
169
+ /**
170
+ * Initialize the inferencer
171
+ */
172
+ const initialize = async () => {
173
+ isInitializing.value = true;
174
+ error.value = null;
175
+
176
+ try {
177
+ console.log("=".repeat(80));
178
+ console.log("Initializing ONNX Inferencer...");
179
+ console.log("=".repeat(80));
180
+
181
+ inferencer = new OnnxInferencer({
182
+ modelPath: "/onnx/GPT2CausalLM_ep0015_int8_seq2048_int8.onnx",
183
+ vocabSize: 259,
184
+ seqLen: 2048,
185
+ executionProviders: ["wasm"] // Use WebAssembly for compatibility
186
+ });
187
+
188
+ await inferencer.initialize();
189
+
190
+ modelInfo.value = inferencer.getModelInfo();
191
+ isInitialized.value = true;
192
+
193
+ console.log("✓ Initialization complete!");
194
+ console.log("=".repeat(80));
195
+ } catch (err) {
196
+ error.value = err instanceof Error ? err.message : "Unknown error";
197
+ console.error("Initialization failed:", err);
198
+ } finally {
199
+ isInitializing.value = false;
200
+ }
201
+ };
202
+
203
+ /**
204
+ * Run basic inference test
205
+ */
206
+ const runBasicTest = async () => {
207
+ if (!inferencer) return;
208
+
209
+ isRunning.value = true;
210
+ error.value = null;
211
+ basicTestResult.value = null;
212
+
213
+ try {
214
+ console.log("\n" + "=".repeat(80));
215
+ console.log("Running Basic Inference Test...");
216
+ console.log("=".repeat(80));
217
+
218
+ const result = await inferencer.testBasicInference();
219
+ basicTestResult.value = result;
220
+
221
+ console.log("✓ Basic test complete!");
222
+ console.log("=".repeat(80));
223
+ } catch (err) {
224
+ error.value = err instanceof Error ? err.message : "Test failed";
225
+ console.error("Basic test failed:", err);
226
+ } finally {
227
+ isRunning.value = false;
228
+ }
229
+ };
230
+
231
+ /**
232
+ * Run text generation test
233
+ */
234
+ const runGenerationTest = async () => {
235
+ if (!inferencer) return;
236
+
237
+ isRunning.value = true;
238
+ error.value = null;
239
+ generationResult.value = null;
240
+
241
+ try {
242
+ console.log("\n" + "=".repeat(80));
243
+ console.log("Running Text Generation Test...");
244
+ console.log("=".repeat(80));
245
+
246
+ const result = await inferencer.generateText(prompt.value, numTokens.value);
247
+ generationResult.value = result;
248
+
249
+ console.log("✓ Generation test complete!");
250
+ console.log("=".repeat(80));
251
+ } catch (err) {
252
+ error.value = err instanceof Error ? err.message : "Generation failed";
253
+ console.error("Generation test failed:", err);
254
+ } finally {
255
+ isRunning.value = false;
256
+ }
257
+ };
258
+
259
+ /**
260
+ * Clear browser console
261
+ */
262
+ const clearConsole = () => {
263
+ console.clear();
264
+ console.log("Console cleared");
265
+ };
266
+ </script>
267
+
268
+ <style lang="scss" scoped>
269
+ .onnx-test-view {
270
+ display: flex;
271
+ flex-direction: column;
272
+ height: 100%;
273
+ background-color: #f5f5f5;
274
+ overflow: auto;
275
+ }
276
+
277
+ .test-header {
278
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
279
+ color: white;
280
+ padding: 2rem;
281
+ text-align: center;
282
+
283
+ h1 {
284
+ margin: 0 0 0.5rem 0;
285
+ font-size: 2rem;
286
+ font-weight: 700;
287
+ }
288
+
289
+ .subtitle {
290
+ margin: 0;
291
+ opacity: 0.9;
292
+ font-size: 1.1rem;
293
+ }
294
+ }
295
+
296
+ .test-body {
297
+ flex: 1;
298
+ padding: 2rem;
299
+ max-width: 1000px;
300
+ margin: 0 auto;
301
+ width: 100%;
302
+ }
303
+
304
+ .test-section {
305
+ background: white;
306
+ border-radius: 12px;
307
+ padding: 1.5rem;
308
+ margin-bottom: 1.5rem;
309
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
310
+
311
+ h2 {
312
+ margin: 0 0 0.5rem 0;
313
+ color: #333;
314
+ font-size: 1.3rem;
315
+ font-weight: 600;
316
+ }
317
+
318
+ p {
319
+ margin: 0 0 1rem 0;
320
+ color: #666;
321
+ }
322
+ }
323
+
324
+ .status-info {
325
+ background: #f8f9fa;
326
+ border-radius: 8px;
327
+ padding: 1rem;
328
+ margin-bottom: 1rem;
329
+
330
+ .status-item {
331
+ display: flex;
332
+ justify-content: space-between;
333
+ padding: 0.5rem 0;
334
+ border-bottom: 1px solid #e9ecef;
335
+
336
+ &:last-child {
337
+ border-bottom: none;
338
+ }
339
+
340
+ &.ready {
341
+ .status-value {
342
+ color: #28a745;
343
+ font-weight: 600;
344
+ }
345
+ }
346
+
347
+ &.loading {
348
+ .status-value {
349
+ color: #ffc107;
350
+ font-weight: 600;
351
+ }
352
+ }
353
+
354
+ .status-label {
355
+ font-weight: 600;
356
+ color: #666;
357
+ }
358
+
359
+ .status-value {
360
+ color: #333;
361
+ }
362
+ }
363
+ }
364
+
365
+ .error-message {
366
+ background: #fff3cd;
367
+ border: 1px solid #ffeaa7;
368
+ border-radius: 6px;
369
+ padding: 0.75rem;
370
+ color: #856404;
371
+ margin-top: 1rem;
372
+ }
373
+
374
+ .btn {
375
+ padding: 0.75rem 1.5rem;
376
+ border: none;
377
+ border-radius: 8px;
378
+ font-weight: 600;
379
+ font-size: 1rem;
380
+ cursor: pointer;
381
+ transition: all 0.3s ease;
382
+
383
+ &:disabled {
384
+ opacity: 0.5;
385
+ cursor: not-allowed;
386
+ }
387
+
388
+ &.btn-primary {
389
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
390
+ color: white;
391
+
392
+ &:hover:not(:disabled) {
393
+ transform: translateY(-2px);
394
+ box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
395
+ }
396
+ }
397
+
398
+ &.btn-test {
399
+ background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
400
+ color: white;
401
+
402
+ &:hover:not(:disabled) {
403
+ transform: translateY(-2px);
404
+ box-shadow: 0 4px 12px rgba(245, 87, 108, 0.4);
405
+ }
406
+ }
407
+
408
+ &.btn-secondary {
409
+ background: #6c757d;
410
+ color: white;
411
+
412
+ &:hover:not(:disabled) {
413
+ background: #5a6268;
414
+ }
415
+ }
416
+ }
417
+
418
+ .input-group {
419
+ margin-bottom: 1rem;
420
+
421
+ label {
422
+ display: block;
423
+ margin-bottom: 0.5rem;
424
+ font-weight: 600;
425
+ color: #333;
426
+ }
427
+
428
+ input {
429
+ width: 100%;
430
+ padding: 0.75rem;
431
+ border: 2px solid #e9ecef;
432
+ border-radius: 8px;
433
+ font-size: 1rem;
434
+ transition: border-color 0.3s ease;
435
+
436
+ &:focus {
437
+ outline: none;
438
+ border-color: #667eea;
439
+ }
440
+
441
+ &:disabled {
442
+ background-color: #f8f9fa;
443
+ cursor: not-allowed;
444
+ }
445
+ }
446
+ }
447
+
448
+ .test-result {
449
+ background: #f8f9fa;
450
+ border-radius: 8px;
451
+ padding: 1rem;
452
+ margin-top: 1rem;
453
+
454
+ h3 {
455
+ margin: 0 0 1rem 0;
456
+ color: #333;
457
+ font-size: 1.1rem;
458
+ }
459
+
460
+ .result-item {
461
+ display: flex;
462
+ justify-content: space-between;
463
+ align-items: flex-start;
464
+ padding: 0.5rem 0;
465
+ border-bottom: 1px solid #e9ecef;
466
+
467
+ &:last-child {
468
+ border-bottom: none;
469
+ }
470
+
471
+ &.full-width {
472
+ flex-direction: column;
473
+ gap: 0.5rem;
474
+ }
475
+
476
+ .label {
477
+ font-weight: 600;
478
+ color: #666;
479
+ min-width: 150px;
480
+ }
481
+
482
+ .value {
483
+ color: #333;
484
+ flex: 1;
485
+ text-align: right;
486
+
487
+ &.code {
488
+ font-family: "Courier New", monospace;
489
+ background: #fff;
490
+ padding: 0.25rem 0.5rem;
491
+ border-radius: 4px;
492
+ font-size: 0.9rem;
493
+ text-align: left;
494
+ }
495
+ }
496
+ }
497
+
498
+ .generated-text {
499
+ background: white;
500
+ border: 2px solid #e9ecef;
501
+ border-radius: 8px;
502
+ padding: 1rem;
503
+ font-family: "Courier New", monospace;
504
+ font-size: 0.95rem;
505
+ line-height: 1.6;
506
+ color: #333;
507
+ white-space: pre-wrap;
508
+ word-break: break-word;
509
+ }
510
+ }
511
+ </style>
trigo-web/app/src/views/TrigoAgentTestView.vue ADDED
@@ -0,0 +1,711 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <template>
2
+ <div class="agent-test-view">
3
+ <div class="test-header">
4
+ <h1>Trigo AI Agent Test</h1>
5
+ <p class="subtitle">Testing ONNX-based Move Generation</p>
6
+ </div>
7
+
8
+ <div class="test-body">
9
+ <!-- Agent Status -->
10
+ <div class="test-section">
11
+ <h2>Agent Status</h2>
12
+ <div class="status-info">
13
+ <div
14
+ class="status-item"
15
+ :class="{ ready: isInitialized, loading: isInitializing }"
16
+ >
17
+ <span class="status-label">Status:</span>
18
+ <span class="status-value">
19
+ {{
20
+ isInitialized
21
+ ? "✓ Ready"
22
+ : isInitializing
23
+ ? "⏳ Initializing..."
24
+ : "⚪ Not initialized"
25
+ }}
26
+ </span>
27
+ </div>
28
+ <div v-if="error" class="error-message">❌ Error: {{ error }}</div>
29
+ </div>
30
+
31
+ <button
32
+ class="btn btn-primary"
33
+ @click="initialize"
34
+ :disabled="isInitializing || isInitialized"
35
+ >
36
+ {{ isInitialized ? "Initialized" : "Initialize Agent" }}
37
+ </button>
38
+ </div>
39
+
40
+ <!-- Game State -->
41
+ <div class="test-section">
42
+ <h2>Test Game State</h2>
43
+ <div class="game-info">
44
+ <div class="info-item">
45
+ <span class="label">Board Shape:</span>
46
+ <span class="value"
47
+ >{{ boardShape.x }}×{{ boardShape.y }}×{{ boardShape.z }}</span
48
+ >
49
+ </div>
50
+ <div class="info-item">
51
+ <span class="label">Current Player:</span>
52
+ <span class="value" :class="currentPlayer">{{ currentPlayer }}</span>
53
+ </div>
54
+ <div class="info-item">
55
+ <span class="label">Move Count:</span>
56
+ <span class="value">{{ moveHistory.length }}</span>
57
+ </div>
58
+ <div class="info-item">
59
+ <span class="label">Valid Moves:</span>
60
+ <span class="value">{{ validMovesCount }}</span>
61
+ </div>
62
+ </div>
63
+
64
+ <div class="game-controls">
65
+ <button class="btn btn-secondary" @click="resetGame">Reset Game</button>
66
+ <button
67
+ class="btn btn-secondary"
68
+ @click="makeRandomMove"
69
+ :disabled="!gameStarted"
70
+ >
71
+ Make Random Move
72
+ </button>
73
+ </div>
74
+
75
+ <!-- Current TGN -->
76
+ <div class="tgn-display">
77
+ <h3>Current TGN:</h3>
78
+ <pre class="tgn-content">{{ currentTGN }}</pre>
79
+ </div>
80
+ </div>
81
+
82
+ <!-- Move Generation Test -->
83
+ <div class="test-section">
84
+ <h2>Test: Generate AI Move</h2>
85
+ <p>Generate the best move for the current position using the AI agent</p>
86
+
87
+ <button
88
+ class="btn btn-test"
89
+ @click="testGenerateMove"
90
+ :disabled="!isInitialized || isGenerating || !gameStarted"
91
+ >
92
+ {{ isGenerating ? "Generating..." : "Generate Best Move" }}
93
+ </button>
94
+
95
+ <div v-if="moveResult" class="test-result">
96
+ <h3>Generated Move:</h3>
97
+ <div class="result-item">
98
+ <span class="label">Position:</span>
99
+ <span class="value code">{{ moveResult.position }}</span>
100
+ </div>
101
+ <div class="result-item">
102
+ <span class="label">TGN Notation:</span>
103
+ <span class="value code">{{ moveResult.notation }}</span>
104
+ </div>
105
+ <div class="result-item">
106
+ <span class="label">Generation Time:</span>
107
+ <span class="value">{{ moveResult.time.toFixed(2) }}ms</span>
108
+ </div>
109
+ <div class="result-item">
110
+ <span class="label">Valid Moves Evaluated:</span>
111
+ <span class="value">{{ moveResult.movesEvaluated }}</span>
112
+ </div>
113
+
114
+ <button class="btn btn-apply" @click="applyMove">Apply Move to Board</button>
115
+ </div>
116
+ </div>
117
+
118
+ <!-- Move History -->
119
+ <div class="test-section">
120
+ <h2>Move History</h2>
121
+ <div class="move-history">
122
+ <div v-if="moveHistory.length === 0" class="empty-state">No moves yet</div>
123
+ <div v-else class="move-list">
124
+ <div
125
+ v-for="(move, index) in moveHistory"
126
+ :key="index"
127
+ class="move-item"
128
+ :class="move.player"
129
+ >
130
+ <span class="move-number">{{ index + 1 }}.</span>
131
+ <span class="move-player">{{ move.player }}</span>
132
+ <span class="move-notation">{{ move.notation }}</span>
133
+ <span class="move-pos">{{ move.position }}</span>
134
+ </div>
135
+ </div>
136
+ </div>
137
+ </div>
138
+
139
+ <!-- Console Output -->
140
+ <div class="test-section">
141
+ <h2>Console Output</h2>
142
+ <p>Check the browser console (F12) for detailed logs</p>
143
+ <button class="btn btn-secondary" @click="clearConsole">Clear Console</button>
144
+ </div>
145
+ </div>
146
+ </div>
147
+ </template>
148
+
149
+ <script setup lang="ts">
150
+ import "onnxruntime-web";
151
+
152
+ import { ref, computed, shallowRef } from "vue";
153
+ import { OnnxInferencer } from "@/services/onnxInferencer";
154
+ import { TrigoAgent } from "@inc/trigoAgent";
155
+ import { TrigoGame } from "@inc/trigo/game";
156
+ import type { Position } from "@inc/trigo/types";
157
+ import { encodeAb0yz } from "@inc/trigo/ab0yz";
158
+
159
+ // Inferencer instance
160
+ let inferencer: OnnxInferencer | null = null;
161
+
162
+ // Agent instance
163
+ let agent: TrigoAgent | null = null;
164
+
165
+ // Game instance (use shallowRef for reactivity)
166
+ const game = shallowRef<TrigoGame | null>(null);
167
+
168
+ // State
169
+ const isInitializing = ref(false);
170
+ const isInitialized = ref(false);
171
+ const isGenerating = ref(false);
172
+ const error = ref<string | null>(null);
173
+
174
+ // Game state
175
+ const boardShape = ref({ x: 5, y: 5, z: 5 });
176
+ const currentPlayer = ref<"black" | "white">("black");
177
+ const moveHistory = ref<
178
+ Array<{
179
+ player: "black" | "white";
180
+ notation: string;
181
+ position: string;
182
+ }>
183
+ >([]);
184
+ const gameStarted = ref(false);
185
+
186
+ // Move generation result
187
+ const moveResult = ref<{
188
+ position: string;
189
+ notation: string;
190
+ time: number;
191
+ movesEvaluated: number;
192
+ rawPosition?: Position;
193
+ } | null>(null);
194
+
195
+ // Computed
196
+ const validMovesCount = computed(() => {
197
+ console.log("validMovesCount:", game);
198
+ if (!game.value) return 0;
199
+ return getValidMovesCount();
200
+ });
201
+
202
+ const currentTGN = computed(() => {
203
+ if (!game.value) return "[Board 5x5] *";
204
+ return game.value.toTGN();
205
+ });
206
+
207
+ /**
208
+ * Initialize the AI agent
209
+ */
210
+ const initialize = async () => {
211
+ isInitializing.value = true;
212
+ error.value = null;
213
+
214
+ try {
215
+ console.log("=".repeat(80));
216
+ console.log("[TrigoAgentTest] Initializing AI agent...");
217
+ console.log("=".repeat(80));
218
+
219
+ // Create and initialize inferencer
220
+ inferencer = new OnnxInferencer({
221
+ modelPath: "/onnx/GPT2CausalLM_ep0015_int8_seq2048_int8.onnx",
222
+ vocabSize: 259,
223
+ seqLen: 2048
224
+ });
225
+
226
+ await inferencer.initialize();
227
+
228
+ // Create agent with inferencer
229
+ agent = new TrigoAgent(inferencer);
230
+
231
+ isInitialized.value = true;
232
+
233
+ // Initialize game
234
+ resetGame();
235
+
236
+ console.log("[TrigoAgentTest] ✓ Agent initialized successfully!");
237
+ console.log("=".repeat(80));
238
+ } catch (err) {
239
+ error.value = err instanceof Error ? err.message : "Unknown error";
240
+ console.error("[TrigoAgentTest] Initialization failed:", err);
241
+ } finally {
242
+ isInitializing.value = false;
243
+ }
244
+ };
245
+
246
+ /**
247
+ * Reset the game to initial state
248
+ */
249
+ const resetGame = () => {
250
+ console.log("[TrigoAgentTest] Resetting game.value...");
251
+
252
+ game.value = new TrigoGame(boardShape.value);
253
+ currentPlayer.value = "black";
254
+ moveHistory.value = [];
255
+ gameStarted.value = true;
256
+ moveResult.value = null;
257
+
258
+ console.log("[TrigoAgentTest] ✓ Game reset");
259
+ };
260
+
261
+ /**
262
+ * Get count of valid moves (using efficient batch query)
263
+ */
264
+ const getValidMovesCount = (): number => {
265
+ console.debug("getValidMovesCount:", game);
266
+ if (!game.value) return 0;
267
+ return game.value.validMovePositions().length;
268
+ };
269
+
270
+ /**
271
+ * Make a random move (for testing, using efficient batch query)
272
+ */
273
+ const makeRandomMove = () => {
274
+ if (!game.value) return;
275
+
276
+ // Get all valid moves efficiently
277
+ const validMoves = game.value.validMovePositions();
278
+
279
+ if (validMoves.length === 0) {
280
+ console.log("[TrigoAgentTest] No valid moves available");
281
+ return;
282
+ }
283
+
284
+ // Pick random move
285
+ const move = validMoves[Math.floor(Math.random() * validMoves.length)];
286
+ const shape = game.value.getShape();
287
+ const notation = encodeAb0yz([move.x, move.y, move.z], [shape.x, shape.y, shape.z]);
288
+
289
+ // Make move
290
+ game.value.drop(move);
291
+
292
+ // Record in history
293
+ moveHistory.value.push({
294
+ player: currentPlayer.value,
295
+ notation,
296
+ position: `(${move.x}, ${move.y}, ${move.z})`
297
+ });
298
+
299
+ // Switch player
300
+ currentPlayer.value = currentPlayer.value === "black" ? "white" : "black";
301
+
302
+ console.log(`[TrigoAgentTest] Random move: ${notation} at ${move.x},${move.y},${move.z}`);
303
+ };
304
+
305
+ /**
306
+ * Test: Generate best move using AI
307
+ */
308
+ const testGenerateMove = async () => {
309
+ if (!agent || !game) return;
310
+
311
+ isGenerating.value = true;
312
+ error.value = null;
313
+ moveResult.value = null;
314
+
315
+ try {
316
+ console.log("\n" + "=".repeat(80));
317
+ console.log("[TrigoAgentTest] Generating AI move...");
318
+ console.log(`[TrigoAgentTest] Current TGN: ${game.value.toTGN()}`);
319
+ console.log(`[TrigoAgentTest] Valid moves to evaluate: ${validMovesCount.value}`);
320
+ console.log("=".repeat(80));
321
+
322
+ const startTime = performance.now();
323
+ const position = await agent.selectBestMove(game.value);
324
+ const elapsed = performance.now() - startTime;
325
+
326
+ if (!position) {
327
+ error.value = "No valid moves found";
328
+ console.error("[TrigoAgentTest] No valid moves found");
329
+ return;
330
+ }
331
+
332
+ const notation = encodeAb0yz(
333
+ [position.x, position.y, position.z],
334
+ [boardShape.value.x, boardShape.value.y, boardShape.value.z]
335
+ );
336
+
337
+ moveResult.value = {
338
+ position: `(${position.x}, ${position.y}, ${position.z})`,
339
+ notation,
340
+ time: elapsed,
341
+ movesEvaluated: validMovesCount.value,
342
+ rawPosition: position
343
+ };
344
+
345
+ console.log("[TrigoAgentTest] ✓ Move generated successfully!");
346
+ console.log(
347
+ `[TrigoAgentTest] Best move: ${notation} at ${position.x},${position.y},${position.z}`
348
+ );
349
+ console.log(`[TrigoAgentTest] Generation time: ${elapsed.toFixed(2)}ms`);
350
+ console.log("=".repeat(80));
351
+ } catch (err) {
352
+ error.value = err instanceof Error ? err.message : "Generation failed";
353
+ console.error("[TrigoAgentTest] Move generation failed:", err);
354
+ } finally {
355
+ isGenerating.value = false;
356
+ }
357
+ };
358
+
359
+ /**
360
+ * Apply the generated move to the board
361
+ */
362
+ const applyMove = () => {
363
+ if (!moveResult.value || !moveResult.value.rawPosition || !game) return;
364
+
365
+ const pos = moveResult.value.rawPosition;
366
+
367
+ // Make move
368
+ game.value.drop(pos);
369
+
370
+ // Record in history
371
+ moveHistory.value.push({
372
+ player: currentPlayer.value,
373
+ notation: moveResult.value.notation,
374
+ position: moveResult.value.position
375
+ });
376
+
377
+ // Switch player
378
+ currentPlayer.value = currentPlayer.value === "black" ? "white" : "black";
379
+
380
+ // Clear result
381
+ moveResult.value = null;
382
+
383
+ console.log(`[TrigoAgentTest] Move applied`);
384
+ };
385
+
386
+ /**
387
+ * Clear browser console
388
+ */
389
+ const clearConsole = () => {
390
+ console.clear();
391
+ console.log("[TrigoAgentTest] Console cleared");
392
+ };
393
+ </script>
394
+
395
+ <style lang="scss" scoped>
396
+ .agent-test-view {
397
+ display: flex;
398
+ flex-direction: column;
399
+ height: 100%;
400
+ background-color: #f5f5f5;
401
+ overflow: auto;
402
+ }
403
+
404
+ .test-header {
405
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
406
+ color: white;
407
+ padding: 2rem;
408
+ text-align: center;
409
+
410
+ h1 {
411
+ margin: 0 0 0.5rem 0;
412
+ font-size: 2rem;
413
+ font-weight: 700;
414
+ }
415
+
416
+ .subtitle {
417
+ margin: 0;
418
+ opacity: 0.9;
419
+ font-size: 1.1rem;
420
+ }
421
+ }
422
+
423
+ .test-body {
424
+ flex: 1;
425
+ padding: 2rem;
426
+ max-width: 1200px;
427
+ margin: 0 auto;
428
+ width: 100%;
429
+ }
430
+
431
+ .test-section {
432
+ background: white;
433
+ border-radius: 12px;
434
+ padding: 1.5rem;
435
+ margin-bottom: 1.5rem;
436
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
437
+
438
+ h2 {
439
+ margin: 0 0 0.5rem 0;
440
+ color: #333;
441
+ font-size: 1.3rem;
442
+ font-weight: 600;
443
+ }
444
+
445
+ p {
446
+ margin: 0 0 1rem 0;
447
+ color: #666;
448
+ }
449
+ }
450
+
451
+ .status-info {
452
+ background: #f8f9fa;
453
+ border-radius: 8px;
454
+ padding: 1rem;
455
+ margin-bottom: 1rem;
456
+
457
+ .status-item {
458
+ display: flex;
459
+ justify-content: space-between;
460
+ padding: 0.5rem 0;
461
+
462
+ &.ready .status-value {
463
+ color: #28a745;
464
+ font-weight: 600;
465
+ }
466
+
467
+ &.loading .status-value {
468
+ color: #ffc107;
469
+ font-weight: 600;
470
+ }
471
+
472
+ .status-label {
473
+ font-weight: 600;
474
+ color: #666;
475
+ }
476
+
477
+ .status-value {
478
+ color: #333;
479
+ }
480
+ }
481
+ }
482
+
483
+ .error-message {
484
+ background: #fff3cd;
485
+ border: 1px solid #ffeaa7;
486
+ border-radius: 6px;
487
+ padding: 0.75rem;
488
+ color: #856404;
489
+ margin-top: 1rem;
490
+ }
491
+
492
+ .game-info {
493
+ background: #f8f9fa;
494
+ border-radius: 8px;
495
+ padding: 1rem;
496
+ margin-bottom: 1rem;
497
+
498
+ .info-item {
499
+ display: flex;
500
+ justify-content: space-between;
501
+ padding: 0.5rem 0;
502
+ border-bottom: 1px solid #e9ecef;
503
+
504
+ &:last-child {
505
+ border-bottom: none;
506
+ }
507
+
508
+ .label {
509
+ font-weight: 600;
510
+ color: #666;
511
+ }
512
+
513
+ .value {
514
+ color: #333;
515
+ font-weight: 500;
516
+
517
+ &.black {
518
+ color: #2c2c2c;
519
+ }
520
+
521
+ &.white {
522
+ color: #666;
523
+ }
524
+ }
525
+ }
526
+ }
527
+
528
+ .game-controls {
529
+ display: flex;
530
+ gap: 0.5rem;
531
+ margin-bottom: 1rem;
532
+ }
533
+
534
+ .tgn-display {
535
+ h3 {
536
+ margin: 0 0 0.5rem 0;
537
+ color: #333;
538
+ font-size: 1rem;
539
+ }
540
+
541
+ .tgn-content {
542
+ background: #2a2a2a;
543
+ color: #e0e0e0;
544
+ padding: 1rem;
545
+ border-radius: 8px;
546
+ font-family: "Courier New", monospace;
547
+ font-size: 0.9rem;
548
+ line-height: 1.6;
549
+ overflow-x: auto;
550
+ white-space: pre-wrap;
551
+ word-break: break-all;
552
+ }
553
+ }
554
+
555
+ .btn {
556
+ padding: 0.75rem 1.5rem;
557
+ border: none;
558
+ border-radius: 8px;
559
+ font-weight: 600;
560
+ font-size: 1rem;
561
+ cursor: pointer;
562
+ transition: all 0.3s ease;
563
+
564
+ &:disabled {
565
+ opacity: 0.5;
566
+ cursor: not-allowed;
567
+ }
568
+
569
+ &.btn-primary {
570
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
571
+ color: white;
572
+
573
+ &:hover:not(:disabled) {
574
+ transform: translateY(-2px);
575
+ box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
576
+ }
577
+ }
578
+
579
+ &.btn-test {
580
+ background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
581
+ color: white;
582
+
583
+ &:hover:not(:disabled) {
584
+ transform: translateY(-2px);
585
+ box-shadow: 0 4px 12px rgba(245, 87, 108, 0.4);
586
+ }
587
+ }
588
+
589
+ &.btn-secondary {
590
+ background: #6c757d;
591
+ color: white;
592
+
593
+ &:hover:not(:disabled) {
594
+ background: #5a6268;
595
+ }
596
+ }
597
+
598
+ &.btn-apply {
599
+ background: #28a745;
600
+ color: white;
601
+ margin-top: 1rem;
602
+
603
+ &:hover:not(:disabled) {
604
+ background: #218838;
605
+ transform: translateY(-2px);
606
+ box-shadow: 0 4px 12px rgba(40, 167, 69, 0.4);
607
+ }
608
+ }
609
+ }
610
+
611
+ .test-result {
612
+ background: #f8f9fa;
613
+ border-radius: 8px;
614
+ padding: 1rem;
615
+ margin-top: 1rem;
616
+
617
+ h3 {
618
+ margin: 0 0 1rem 0;
619
+ color: #333;
620
+ font-size: 1.1rem;
621
+ }
622
+
623
+ .result-item {
624
+ display: flex;
625
+ justify-content: space-between;
626
+ padding: 0.5rem 0;
627
+ border-bottom: 1px solid #e9ecef;
628
+
629
+ &:last-of-type {
630
+ border-bottom: none;
631
+ }
632
+
633
+ .label {
634
+ font-weight: 600;
635
+ color: #666;
636
+ }
637
+
638
+ .value {
639
+ color: #333;
640
+
641
+ &.code {
642
+ font-family: "Courier New", monospace;
643
+ background: #fff;
644
+ padding: 0.25rem 0.5rem;
645
+ border-radius: 4px;
646
+ font-size: 0.9rem;
647
+ }
648
+ }
649
+ }
650
+ }
651
+
652
+ .move-history {
653
+ background: #f8f9fa;
654
+ border-radius: 8px;
655
+ padding: 1rem;
656
+ max-height: 400px;
657
+ overflow-y: auto;
658
+
659
+ .empty-state {
660
+ text-align: center;
661
+ color: #999;
662
+ padding: 2rem;
663
+ font-style: italic;
664
+ }
665
+
666
+ .move-list {
667
+ display: flex;
668
+ flex-direction: column;
669
+ gap: 0.5rem;
670
+ }
671
+
672
+ .move-item {
673
+ display: grid;
674
+ grid-template-columns: 40px 80px 100px 1fr;
675
+ gap: 0.5rem;
676
+ padding: 0.5rem;
677
+ background: white;
678
+ border-radius: 6px;
679
+ border-left: 3px solid;
680
+
681
+ &.black {
682
+ border-color: #2c2c2c;
683
+ }
684
+
685
+ &.white {
686
+ border-color: #999;
687
+ }
688
+
689
+ .move-number {
690
+ color: #999;
691
+ font-weight: 600;
692
+ }
693
+
694
+ .move-player {
695
+ font-weight: 600;
696
+ text-transform: capitalize;
697
+ }
698
+
699
+ .move-notation {
700
+ font-family: "Courier New", monospace;
701
+ font-weight: 600;
702
+ }
703
+
704
+ .move-pos {
705
+ color: #666;
706
+ font-family: "Courier New", monospace;
707
+ font-size: 0.9rem;
708
+ }
709
+ }
710
+ }
711
+ </style>
trigo-web/app/src/views/TrigoTreeTestView.vue ADDED
@@ -0,0 +1,1083 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <template>
2
+ <div class="tree-test-container">
3
+ <h1>Tree Attention Test - Trigo AI Agent</h1>
4
+
5
+ <div class="info-section">
6
+ <h2>Evaluation Mode with Tree Attention</h2>
7
+ <p>
8
+ This test uses the evaluation mode ONNX model to evaluate all valid moves in
9
+ parallel using tree attention. Each move is an independent branch that can only see
10
+ the prefix and itself.
11
+ </p>
12
+ </div>
13
+
14
+ <!-- Agent Status -->
15
+ <div class="status-section">
16
+ <h3>Agent Status</h3>
17
+ <div class="status-grid">
18
+ <div class="status-item">
19
+ <span class="label">Model:</span>
20
+ <span class="value">{{ modelPath }}</span>
21
+ </div>
22
+ <div class="status-item">
23
+ <span class="label">Status:</span>
24
+ <span class="value" :class="statusClass">{{ agentStatus }}</span>
25
+ </div>
26
+ <div class="status-item">
27
+ <span class="label">Vocab Size:</span>
28
+ <span class="value">{{ vocabSize }}</span>
29
+ </div>
30
+ </div>
31
+ <button
32
+ @click="initializeAgent"
33
+ :disabled="isInitializing || isReady"
34
+ class="btn-primary"
35
+ >
36
+ {{
37
+ isInitializing
38
+ ? "Initializing..."
39
+ : isReady
40
+ ? "✓ Agent Ready"
41
+ : "Initialize Agent"
42
+ }}
43
+ </button>
44
+ </div>
45
+
46
+ <!-- Game Board -->
47
+ <div class="board-section">
48
+ <h3>Game Board (5×5×5)</h3>
49
+ <div class="board-info">
50
+ <div class="info-item">
51
+ <span class="label">Current Player:</span>
52
+ <span class="value">{{ currentPlayer }}</span>
53
+ </div>
54
+ <div class="info-item">
55
+ <span class="label">Move Count:</span>
56
+ <span class="value">{{ moveCount }}</span>
57
+ </div>
58
+ <div class="info-item">
59
+ <span class="label">Valid Moves:</span>
60
+ <span class="value">{{ validMovesCount }}</span>
61
+ </div>
62
+ </div>
63
+ <div class="board-display">
64
+ <pre>{{ boardDisplay }}</pre>
65
+ </div>
66
+ <button @click="resetGame" class="btn-secondary">Reset Game</button>
67
+ </div>
68
+
69
+ <!-- Move Generation -->
70
+ <div class="generation-section">
71
+ <h3>Tree Attention Move Generation</h3>
72
+ <div class="button-group">
73
+ <button
74
+ @click="generateMoves"
75
+ :disabled="!isReady || isGenerating"
76
+ class="btn-primary btn-large"
77
+ >
78
+ {{ isGenerating ? "Generating..." : "Score All Moves (Tree Attention)" }}
79
+ </button>
80
+ <button
81
+ @click="generateBestMove"
82
+ :disabled="!isReady || isGenerating"
83
+ class="btn-primary btn-large"
84
+ >
85
+ {{ isGenerating ? "Generating..." : "Generate & Apply Best Move" }}
86
+ </button>
87
+ </div>
88
+
89
+ <div v-if="generationTime !== null" class="timing-info">
90
+ <strong>Generation Time:</strong> {{ generationTime }}ms
91
+ <span class="timing-detail">
92
+ (~{{ (generationTime / validMovesCount).toFixed(1) }}ms per move)
93
+ </span>
94
+ </div>
95
+
96
+ <div v-if="bestMoveApplied" class="success-info">
97
+ <strong>Best Move Applied:</strong> {{ bestMoveApplied }}
98
+ </div>
99
+
100
+ <!-- Scored Moves Display -->
101
+ <div v-if="scoredMoves.length > 0" class="moves-section">
102
+ <h4>Scored Moves (Top 20)</h4>
103
+ <div class="moves-table-container">
104
+ <table class="moves-table">
105
+ <thead>
106
+ <tr>
107
+ <th>Rank</th>
108
+ <th>Notation</th>
109
+ <th>Position</th>
110
+ <th>Log Prob</th>
111
+ <th>Probability</th>
112
+ <th>Action</th>
113
+ </tr>
114
+ </thead>
115
+ <tbody>
116
+ <tr
117
+ v-for="(scoredMove, index) in topMoves"
118
+ :key="index"
119
+ :class="{ 'top-move': index === 0 }"
120
+ >
121
+ <td>{{ index + 1 }}</td>
122
+ <td class="notation">{{ scoredMove.notation }}</td>
123
+ <td class="position">{{ formatPosition(scoredMove.move) }}</td>
124
+ <td class="score">{{ scoredMove.score.toFixed(3) }}</td>
125
+ <td class="probability">
126
+ {{ ((scoredMove.probability ?? 0) * 100).toFixed(4) }}%
127
+ </td>
128
+ <td>
129
+ <button @click="applyMove(scoredMove.move)" class="btn-small">
130
+ Apply
131
+ </button>
132
+ </td>
133
+ </tr>
134
+ </tbody>
135
+ </table>
136
+ </div>
137
+ </div>
138
+
139
+ <!-- Error Display -->
140
+ <div v-if="errorMessage" class="error-section">
141
+ <h4>Error</h4>
142
+ <pre>{{ errorMessage }}</pre>
143
+ </div>
144
+ </div>
145
+
146
+ <!-- Tree Visualization -->
147
+ <div v-if="treeVisualization" class="visualization-section">
148
+ <h3>Tree Structure Visualization</h3>
149
+
150
+ <!-- Move Details -->
151
+ <div class="move-details">
152
+ <h4>Move Details</h4>
153
+ <div class="details-grid">
154
+ <div
155
+ v-for="(move, index) in treeVisualization.moveData"
156
+ :key="index"
157
+ class="move-detail-item"
158
+ >
159
+ <span class="move-notation">{{ move.notation }}</span>
160
+ <span class="move-positions"
161
+ >parent={{ move.parentPos }}, leaf={{ move.leafPos }}</span
162
+ >
163
+ </div>
164
+ </div>
165
+ </div>
166
+
167
+ <!-- Token Sequence -->
168
+ <div class="token-sequence">
169
+ <h4>
170
+ Evaluated Token Sequence ({{ treeVisualization.evaluatedIds.length }} tokens)
171
+ </h4>
172
+ <div class="tokens-display">
173
+ <div
174
+ v-for="(tokenId, index) in treeVisualization.evaluatedIds"
175
+ :key="index"
176
+ class="token-item"
177
+ >
178
+ <div class="token-pos">{{ index }}</div>
179
+ <div class="token-char">{{ String.fromCharCode(tokenId) }}</div>
180
+ <div class="token-id">{{ tokenId }}</div>
181
+ </div>
182
+ </div>
183
+ </div>
184
+
185
+ <!-- Attention Mask Matrix -->
186
+ <div class="mask-matrix">
187
+ <h4>
188
+ Attention Mask Matrix ({{ Math.sqrt(treeVisualization.mask.length) }}×{{
189
+ Math.sqrt(treeVisualization.mask.length)
190
+ }})
191
+ </h4>
192
+ <div class="matrix-container">
193
+ <table class="matrix-table">
194
+ <thead>
195
+ <tr>
196
+ <th class="corner-cell"></th>
197
+ <th
198
+ v-for="(tokenId, col) in treeVisualization.evaluatedIds"
199
+ :key="col"
200
+ class="header-cell"
201
+ >
202
+ <div class="header-content">
203
+ <div class="header-pos">{{ col }}</div>
204
+ <div class="header-char">
205
+ {{ String.fromCharCode(tokenId) }}
206
+ </div>
207
+ </div>
208
+ </th>
209
+ </tr>
210
+ </thead>
211
+ <tbody>
212
+ <tr v-for="(tokenId, row) in treeVisualization.evaluatedIds" :key="row">
213
+ <th class="row-header">
214
+ <div class="row-content">
215
+ <div class="row-pos">{{ row }}</div>
216
+ <div class="row-char">
217
+ {{ String.fromCharCode(tokenId) }}
218
+ </div>
219
+ </div>
220
+ </th>
221
+ <td
222
+ v-for="col in treeVisualization.evaluatedIds.length"
223
+ :key="col - 1"
224
+ :class="getMaskCellClass(row, col - 1)"
225
+ :title="`[${row},${col - 1}] = ${getMaskValue(row, col - 1)}`"
226
+ >
227
+ {{ getMaskValue(row, col - 1) }}
228
+ </td>
229
+ </tr>
230
+ </tbody>
231
+ </table>
232
+ </div>
233
+ <div class="matrix-legend">
234
+ <span class="legend-item"
235
+ ><span class="legend-box active"></span> 1 (can attend)</span
236
+ >
237
+ <span class="legend-item"
238
+ ><span class="legend-box inactive"></span> 0 (cannot attend)</span
239
+ >
240
+ </div>
241
+ </div>
242
+ </div>
243
+
244
+ <!-- Debug Info -->
245
+ <div class="debug-section">
246
+ <h3>Debug Information</h3>
247
+ <div class="debug-content">
248
+ <div class="debug-item">
249
+ <span class="label">Current TGN:</span>
250
+ <pre class="debug-value">{{ currentTGN }}</pre>
251
+ </div>
252
+ </div>
253
+ </div>
254
+ </div>
255
+ </template>
256
+
257
+ <script setup lang="ts">
258
+ import { ref, computed, onMounted } from "vue";
259
+ import * as ort from "onnxruntime-web";
260
+ import { TrigoGame } from "../../../inc/trigo/game";
261
+ import { ModelInferencer } from "../../../inc/modelInferencer";
262
+ import { TrigoTreeAgent } from "../../../inc/trigoTreeAgent";
263
+ import type { ScoredMove } from "../../../inc/trigoTreeAgent";
264
+ import type { Move } from "../../../inc/trigo/types";
265
+
266
+ // Configuration
267
+ const modelPath = "/onnx/GPT2CausalLM_ep0015_evaluation.onnx";
268
+ const vocabSize = 259;
269
+
270
+ // State
271
+ const game = ref<TrigoGame>(new TrigoGame({ x: 5, y: 5, z: 5 }, {}));
272
+ const inferencer = ref<ModelInferencer | null>(null);
273
+ const agent = ref<TrigoTreeAgent | null>(null);
274
+
275
+ const isInitializing = ref(false);
276
+ const isReady = ref(false);
277
+ const isGenerating = ref(false);
278
+
279
+ const scoredMoves = ref<ScoredMove[]>([]);
280
+ const generationTime = ref<number | null>(null);
281
+ const errorMessage = ref<string>("");
282
+ const bestMoveApplied = ref<string>("");
283
+
284
+ // Tree visualization data
285
+ const treeVisualization = ref<{
286
+ evaluatedIds: number[];
287
+ mask: number[];
288
+ moveData: Array<{ notation: string; parentPos: number; leafPos: number }>;
289
+ } | null>(null);
290
+
291
+ // Computed properties
292
+ const agentStatus = computed(() => {
293
+ if (isReady.value) return "Ready";
294
+ if (isInitializing.value) return "Initializing...";
295
+ return "Not initialized";
296
+ });
297
+
298
+ const statusClass = computed(() => {
299
+ if (isReady.value) return "status-ready";
300
+ if (isInitializing.value) return "status-loading";
301
+ return "status-error";
302
+ });
303
+
304
+ const currentPlayer = computed(() => {
305
+ const player = game.value.getCurrentPlayer();
306
+ return player === 1 ? "Black" : "White";
307
+ });
308
+
309
+ const moveCount = computed(() => game.value.stepHistory.length);
310
+
311
+ const validMovesCount = computed(() => {
312
+ return game.value.validMovePositions().length + 1; // +1 for pass
313
+ });
314
+
315
+ const currentTGN = computed(() => game.value.toTGN());
316
+
317
+ // Compute normalized probabilities for scored moves (softmax across move dimension)
318
+ const movesWithProbability = computed(() => {
319
+ if (scoredMoves.value.length === 0) {
320
+ return [];
321
+ }
322
+
323
+ // Find max score for numerical stability
324
+ const maxScore = Math.max(...scoredMoves.value.map((m) => m.score));
325
+
326
+ // Compute exp(score - maxScore) for each move
327
+ const expScores = scoredMoves.value.map((m) => Math.exp(m.score - maxScore));
328
+
329
+ // Compute sum of exp scores
330
+ const sumExp = expScores.reduce((sum, exp) => sum + exp, 0);
331
+
332
+ // Return moves with normalized probabilities
333
+ return scoredMoves.value.map((move, i) => ({
334
+ ...move,
335
+ probability: expScores[i] / sumExp
336
+ }));
337
+ });
338
+
339
+ const topMoves = computed(() => movesWithProbability.value.slice(0, 20));
340
+
341
+ const boardDisplay = computed(() => {
342
+ // Simple text representation of the board
343
+ const shape = game.value.getShape();
344
+ let display = `Board: ${shape.x}×${shape.y}×${shape.z}\n\n`;
345
+ display += `Moves played: ${moveCount.value}\n`;
346
+ display += `Current player: ${currentPlayer.value}\n`;
347
+ display += `Valid positions: ${validMovesCount.value - 1} (+ pass)\n`;
348
+ return display;
349
+ });
350
+
351
+ // Methods
352
+ async function initializeAgent() {
353
+ isInitializing.value = true;
354
+ errorMessage.value = "";
355
+
356
+ try {
357
+ console.log("Loading evaluation model:", modelPath);
358
+
359
+ // Create inference session
360
+ const session = await ort.InferenceSession.create(modelPath);
361
+ console.log("✓ Model loaded successfully");
362
+
363
+ // Create inferencer
364
+ const inf = new ModelInferencer(ort.Tensor as any, {
365
+ vocabSize,
366
+ seqLen: 2048,
367
+ modelPath
368
+ });
369
+ inf.setSession(session as any);
370
+ inferencer.value = inf;
371
+
372
+ // Create agent
373
+ agent.value = new TrigoTreeAgent(inf);
374
+
375
+ isReady.value = true;
376
+ console.log("✓ Agent initialized");
377
+ } catch (error) {
378
+ console.error("Failed to initialize agent:", error);
379
+ errorMessage.value = String(error);
380
+ isReady.value = false;
381
+ } finally {
382
+ isInitializing.value = false;
383
+ }
384
+ }
385
+
386
+ async function generateMoves() {
387
+ if (!agent.value || !isReady.value) {
388
+ errorMessage.value = "Agent not ready";
389
+ return;
390
+ }
391
+
392
+ isGenerating.value = true;
393
+ errorMessage.value = "";
394
+ scoredMoves.value = [];
395
+ generationTime.value = null;
396
+
397
+ try {
398
+ console.log("Generating moves with tree attention...");
399
+ const startTime = performance.now();
400
+
401
+ // Get all valid moves
402
+ const currentPlayer = game.value.getCurrentPlayer() === 1 ? "black" : "white";
403
+ const validPositions = game.value.validMovePositions();
404
+ const moves: Move[] = validPositions.map((pos) => ({
405
+ x: pos.x,
406
+ y: pos.y,
407
+ z: pos.z,
408
+ player: currentPlayer
409
+ }));
410
+ moves.push({ player: currentPlayer, isPass: true }); // Add pass
411
+
412
+ console.log(`Scoring ${moves.length} moves...`);
413
+
414
+ // Get tree structure for visualization
415
+ const treeStructure = agent.value.getTreeStructure(game.value, moves);
416
+ treeVisualization.value = {
417
+ evaluatedIds: treeStructure.evaluatedIds,
418
+ mask: treeStructure.mask,
419
+ moveData: treeStructure.moveData.map((m) => ({
420
+ notation: m.notation,
421
+ parentPos: m.parentPos,
422
+ leafPos: m.leafPos
423
+ }))
424
+ };
425
+
426
+ // Score all moves using tree attention
427
+ const scored = await agent.value.scoreMoves(game.value, moves);
428
+ const endTime = performance.now();
429
+
430
+ generationTime.value = Math.round(endTime - startTime);
431
+ scoredMoves.value = scored.sort((a, b) => b.score - a.score);
432
+
433
+ console.log(`✓ Scored ${scored.length} moves in ${generationTime.value}ms`);
434
+ console.log(
435
+ "Top 5 moves:",
436
+ movesWithProbability.value.slice(0, 5).map((m) => ({
437
+ notation: m.notation,
438
+ score: m.score.toFixed(3),
439
+ prob: ((m.probability ?? 0) * 100).toFixed(4) + "%"
440
+ }))
441
+ );
442
+ } catch (error) {
443
+ console.error("Failed to generate moves:", error);
444
+ errorMessage.value = String(error);
445
+ } finally {
446
+ isGenerating.value = false;
447
+ }
448
+ }
449
+
450
+ async function generateBestMove() {
451
+ if (!agent.value || !isReady.value) {
452
+ errorMessage.value = "Agent not ready";
453
+ return;
454
+ }
455
+
456
+ isGenerating.value = true;
457
+ errorMessage.value = "";
458
+ bestMoveApplied.value = "";
459
+ scoredMoves.value = [];
460
+ generationTime.value = null;
461
+
462
+ try {
463
+ console.log("Generating best move with tree attention...");
464
+ const startTime = performance.now();
465
+
466
+ // Use selectBestMove to get the best move
467
+ const bestMove = await agent.value.selectBestMove(game.value);
468
+ const endTime = performance.now();
469
+
470
+ generationTime.value = Math.round(endTime - startTime);
471
+
472
+ if (!bestMove) {
473
+ errorMessage.value = "No valid moves available";
474
+ return;
475
+ }
476
+
477
+ // Apply the best move
478
+ let success = false;
479
+ let moveNotation = "";
480
+ if (bestMove.isPass) {
481
+ success = game.value.pass();
482
+ moveNotation = "pass";
483
+ } else if (
484
+ bestMove.x !== undefined &&
485
+ bestMove.y !== undefined &&
486
+ bestMove.z !== undefined
487
+ ) {
488
+ success = game.value.drop({ x: bestMove.x, y: bestMove.y, z: bestMove.z });
489
+ const shape = game.value.getShape();
490
+ const posArray = [bestMove.x, bestMove.y, bestMove.z];
491
+ const shapeArray = [shape.x, shape.y, shape.z];
492
+ // Import encodeAb0yz at the top if needed, or just show position
493
+ moveNotation = `(${bestMove.x}, ${bestMove.y}, ${bestMove.z})`;
494
+ }
495
+
496
+ if (success) {
497
+ bestMoveApplied.value = moveNotation;
498
+ console.log(`✓ Best move applied: ${moveNotation} in ${generationTime.value}ms`);
499
+ } else {
500
+ errorMessage.value = "Failed to apply best move";
501
+ }
502
+ } catch (error) {
503
+ console.error("Failed to generate best move:", error);
504
+ errorMessage.value = String(error);
505
+ } finally {
506
+ isGenerating.value = false;
507
+ }
508
+ }
509
+
510
+ function applyMove(move: Move) {
511
+ try {
512
+ let success = false;
513
+ if (move.isPass) {
514
+ success = game.value.pass();
515
+ } else if (move.x !== undefined && move.y !== undefined && move.z !== undefined) {
516
+ success = game.value.drop({ x: move.x, y: move.y, z: move.z });
517
+ }
518
+
519
+ if (success) {
520
+ console.log("✓ Move applied");
521
+ scoredMoves.value = [];
522
+ generationTime.value = null;
523
+ } else {
524
+ errorMessage.value = "Failed to apply move";
525
+ }
526
+ } catch (error) {
527
+ console.error("Error applying move:", error);
528
+ errorMessage.value = String(error);
529
+ }
530
+ }
531
+
532
+ function resetGame() {
533
+ game.value = new TrigoGame({ x: 5, y: 5, z: 5 }, {});
534
+ scoredMoves.value = [];
535
+ generationTime.value = null;
536
+ errorMessage.value = "";
537
+ bestMoveApplied.value = "";
538
+ console.log("✓ Game reset");
539
+ }
540
+
541
+ function formatPosition(move: Move): string {
542
+ if (move.isPass) return "Pass";
543
+ if (move.x !== undefined && move.y !== undefined && move.z !== undefined) {
544
+ return `(${move.x}, ${move.y}, ${move.z})`;
545
+ }
546
+ return "Unknown";
547
+ }
548
+
549
+ // Mask matrix helper functions
550
+ function getMaskValue(row: number, col: number): number {
551
+ if (!treeVisualization.value) return 0;
552
+ const m = treeVisualization.value.evaluatedIds.length;
553
+ return treeVisualization.value.mask[row * m + col];
554
+ }
555
+
556
+ function getMaskCellClass(row: number, col: number): string {
557
+ const value = getMaskValue(row, col);
558
+ return value === 1 ? "mask-cell mask-active" : "mask-cell mask-inactive";
559
+ }
560
+
561
+ // Lifecycle
562
+ onMounted(() => {
563
+ console.log("TrigoTreeTestView mounted");
564
+ });
565
+ </script>
566
+
567
+ <style scoped>
568
+ .tree-test-container {
569
+ max-width: 1200px;
570
+ margin: 0 auto;
571
+ padding: 2rem;
572
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
573
+ min-height: 100vh;
574
+ overflow-y: auto;
575
+ }
576
+
577
+ h1 {
578
+ color: #2c3e50;
579
+ margin-bottom: 1rem;
580
+ }
581
+
582
+ h2,
583
+ h3,
584
+ h4 {
585
+ color: #34495e;
586
+ margin-top: 2rem;
587
+ margin-bottom: 1rem;
588
+ }
589
+
590
+ .info-section {
591
+ background: #f8f9fa;
592
+ padding: 1.5rem;
593
+ border-radius: 8px;
594
+ margin-bottom: 2rem;
595
+ }
596
+
597
+ .info-section p {
598
+ margin: 0;
599
+ line-height: 1.6;
600
+ color: #555;
601
+ }
602
+
603
+ .status-section,
604
+ .board-section,
605
+ .generation-section,
606
+ .debug-section {
607
+ background: white;
608
+ border: 1px solid #e1e4e8;
609
+ border-radius: 8px;
610
+ padding: 1.5rem;
611
+ margin-bottom: 2rem;
612
+ }
613
+
614
+ .status-grid {
615
+ display: grid;
616
+ grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
617
+ gap: 1rem;
618
+ margin-bottom: 1rem;
619
+ }
620
+
621
+ .status-item,
622
+ .info-item {
623
+ display: flex;
624
+ justify-content: space-between;
625
+ padding: 0.5rem;
626
+ background: #f8f9fa;
627
+ border-radius: 4px;
628
+ }
629
+
630
+ .label {
631
+ font-weight: 600;
632
+ color: #666;
633
+ }
634
+
635
+ .value {
636
+ color: #2c3e50;
637
+ }
638
+
639
+ .status-ready {
640
+ color: #28a745;
641
+ font-weight: 600;
642
+ }
643
+
644
+ .status-loading {
645
+ color: #ffc107;
646
+ font-weight: 600;
647
+ }
648
+
649
+ .status-error {
650
+ color: #dc3545;
651
+ font-weight: 600;
652
+ }
653
+
654
+ .board-info {
655
+ display: grid;
656
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
657
+ gap: 1rem;
658
+ margin-bottom: 1rem;
659
+ }
660
+
661
+ .board-display {
662
+ background: #f8f9fa;
663
+ border: 1px solid #e1e4e8;
664
+ border-radius: 4px;
665
+ padding: 1rem;
666
+ margin-bottom: 1rem;
667
+ }
668
+
669
+ .board-display pre {
670
+ margin: 0;
671
+ font-family: "Courier New", monospace;
672
+ font-size: 0.9rem;
673
+ color: #2c3e50;
674
+ }
675
+
676
+ .button-group {
677
+ display: flex;
678
+ gap: 1rem;
679
+ flex-wrap: wrap;
680
+ margin-bottom: 1rem;
681
+ }
682
+
683
+ .btn-primary,
684
+ .btn-secondary,
685
+ .btn-small {
686
+ padding: 0.75rem 1.5rem;
687
+ border: none;
688
+ border-radius: 4px;
689
+ font-size: 1rem;
690
+ font-weight: 600;
691
+ cursor: pointer;
692
+ transition: all 0.2s;
693
+ }
694
+
695
+ .btn-primary {
696
+ background: #007bff;
697
+ color: white;
698
+ }
699
+
700
+ .btn-primary:hover:not(:disabled) {
701
+ background: #0056b3;
702
+ }
703
+
704
+ .btn-primary:disabled {
705
+ background: #6c757d;
706
+ cursor: not-allowed;
707
+ opacity: 0.6;
708
+ }
709
+
710
+ .btn-secondary {
711
+ background: #6c757d;
712
+ color: white;
713
+ }
714
+
715
+ .btn-secondary:hover {
716
+ background: #545b62;
717
+ }
718
+
719
+ .btn-large {
720
+ padding: 1rem 2rem;
721
+ font-size: 1.1rem;
722
+ }
723
+
724
+ .btn-small {
725
+ padding: 0.25rem 0.75rem;
726
+ font-size: 0.875rem;
727
+ }
728
+
729
+ .timing-info {
730
+ margin-top: 1rem;
731
+ padding: 1rem;
732
+ background: #e7f5ff;
733
+ border-left: 4px solid #007bff;
734
+ border-radius: 4px;
735
+ }
736
+
737
+ .timing-detail {
738
+ color: #666;
739
+ font-size: 0.9rem;
740
+ margin-left: 0.5rem;
741
+ }
742
+
743
+ .success-info {
744
+ margin-top: 1rem;
745
+ padding: 1rem;
746
+ background: #d4edda;
747
+ border-left: 4px solid #28a745;
748
+ border-radius: 4px;
749
+ color: #155724;
750
+ font-weight: 600;
751
+ }
752
+
753
+ .moves-section {
754
+ margin-top: 2rem;
755
+ }
756
+
757
+ .moves-table-container {
758
+ overflow-x: auto;
759
+ margin-top: 1rem;
760
+ }
761
+
762
+ .moves-table {
763
+ width: 100%;
764
+ border-collapse: collapse;
765
+ font-size: 0.9rem;
766
+ }
767
+
768
+ .moves-table thead {
769
+ background: #f8f9fa;
770
+ }
771
+
772
+ .moves-table th {
773
+ padding: 0.75rem;
774
+ text-align: left;
775
+ font-weight: 600;
776
+ color: #495057;
777
+ border-bottom: 2px solid #dee2e6;
778
+ }
779
+
780
+ .moves-table td {
781
+ padding: 0.75rem;
782
+ border-bottom: 1px solid #dee2e6;
783
+ }
784
+
785
+ .moves-table tbody tr:hover {
786
+ background: #f8f9fa;
787
+ }
788
+
789
+ .moves-table .top-move {
790
+ background: #d4edda;
791
+ font-weight: 600;
792
+ }
793
+
794
+ .notation {
795
+ font-family: "Courier New", monospace;
796
+ font-weight: 600;
797
+ color: #007bff;
798
+ }
799
+
800
+ .position {
801
+ font-family: "Courier New", monospace;
802
+ color: #666;
803
+ }
804
+
805
+ .score {
806
+ font-family: "Courier New", monospace;
807
+ text-align: right;
808
+ }
809
+
810
+ .probability {
811
+ font-family: "Courier New", monospace;
812
+ text-align: right;
813
+ color: #28a745;
814
+ }
815
+
816
+ .error-section {
817
+ margin-top: 1rem;
818
+ padding: 1rem;
819
+ background: #f8d7da;
820
+ border: 1px solid #f5c6cb;
821
+ border-radius: 4px;
822
+ color: #721c24;
823
+ }
824
+
825
+ .error-section pre {
826
+ margin: 0.5rem 0 0 0;
827
+ white-space: pre-wrap;
828
+ word-wrap: break-word;
829
+ font-size: 0.9rem;
830
+ }
831
+
832
+ .debug-section {
833
+ background: #f8f9fa;
834
+ }
835
+
836
+ .debug-content {
837
+ display: flex;
838
+ flex-direction: column;
839
+ gap: 1rem;
840
+ }
841
+
842
+ .debug-item {
843
+ display: flex;
844
+ flex-direction: column;
845
+ gap: 0.5rem;
846
+ }
847
+
848
+ .debug-value {
849
+ background: white;
850
+ border: 1px solid #e1e4e8;
851
+ border-radius: 4px;
852
+ padding: 1rem;
853
+ margin: 0;
854
+ font-family: "Courier New", monospace;
855
+ font-size: 0.85rem;
856
+ overflow-x: auto;
857
+ color: #2c3e50;
858
+ }
859
+
860
+ /* Tree Visualization Styles */
861
+ .visualization-section {
862
+ background: white;
863
+ border: 1px solid #e1e4e8;
864
+ border-radius: 8px;
865
+ padding: 1.5rem;
866
+ margin-bottom: 2rem;
867
+ }
868
+
869
+ .move-details {
870
+ margin-bottom: 2rem;
871
+ }
872
+
873
+ .details-grid {
874
+ display: grid;
875
+ grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
876
+ gap: 0.5rem;
877
+ margin-top: 1rem;
878
+ }
879
+
880
+ .move-detail-item {
881
+ display: flex;
882
+ flex-direction: column;
883
+ padding: 0.5rem;
884
+ background: #f8f9fa;
885
+ border-radius: 4px;
886
+ font-size: 0.85rem;
887
+ }
888
+
889
+ .move-notation {
890
+ font-family: "Courier New", monospace;
891
+ font-weight: 600;
892
+ color: #007bff;
893
+ font-size: 1rem;
894
+ }
895
+
896
+ .move-positions {
897
+ font-family: "Courier New", monospace;
898
+ color: #666;
899
+ font-size: 0.75rem;
900
+ margin-top: 0.25rem;
901
+ }
902
+
903
+ .token-sequence {
904
+ margin-bottom: 2rem;
905
+ }
906
+
907
+ .tokens-display {
908
+ display: flex;
909
+ flex-wrap: wrap;
910
+ gap: 0.5rem;
911
+ margin-top: 1rem;
912
+ }
913
+
914
+ .token-item {
915
+ display: flex;
916
+ flex-direction: column;
917
+ align-items: center;
918
+ padding: 0.5rem;
919
+ background: #e7f5ff;
920
+ border: 1px solid #007bff;
921
+ border-radius: 4px;
922
+ min-width: 50px;
923
+ }
924
+
925
+ .token-pos {
926
+ font-size: 0.7rem;
927
+ color: #666;
928
+ font-weight: 600;
929
+ }
930
+
931
+ .token-char {
932
+ font-family: "Courier New", monospace;
933
+ font-size: 1.2rem;
934
+ font-weight: 600;
935
+ color: #007bff;
936
+ margin: 0.25rem 0;
937
+ }
938
+
939
+ .token-id {
940
+ font-size: 0.7rem;
941
+ color: #666;
942
+ font-family: "Courier New", monospace;
943
+ }
944
+
945
+ .mask-matrix {
946
+ margin-top: 2rem;
947
+ }
948
+
949
+ .matrix-container {
950
+ overflow: auto;
951
+ margin-top: 1rem;
952
+ max-height: 600px;
953
+ border: 1px solid #e1e4e8;
954
+ border-radius: 4px;
955
+ }
956
+
957
+ .matrix-table {
958
+ border-collapse: collapse;
959
+ font-size: 0.75rem;
960
+ min-width: 100%;
961
+ }
962
+
963
+ .corner-cell {
964
+ background: #f8f9fa;
965
+ border: 1px solid #dee2e6;
966
+ position: sticky;
967
+ left: 0;
968
+ top: 0;
969
+ z-index: 3;
970
+ }
971
+
972
+ .header-cell {
973
+ background: #f8f9fa;
974
+ border: 1px solid #dee2e6;
975
+ padding: 0.5rem;
976
+ position: sticky;
977
+ top: 0;
978
+ z-index: 2;
979
+ text-align: center;
980
+ min-width: 40px;
981
+ }
982
+
983
+ .header-content {
984
+ display: flex;
985
+ flex-direction: column;
986
+ align-items: center;
987
+ }
988
+
989
+ .header-pos {
990
+ font-size: 0.65rem;
991
+ color: #666;
992
+ font-weight: 600;
993
+ }
994
+
995
+ .header-char {
996
+ font-family: "Courier New", monospace;
997
+ font-size: 0.9rem;
998
+ font-weight: 600;
999
+ color: #007bff;
1000
+ margin-top: 0.1rem;
1001
+ }
1002
+
1003
+ .row-header {
1004
+ background: #f8f9fa;
1005
+ border: 1px solid #dee2e6;
1006
+ padding: 0.5rem;
1007
+ position: sticky;
1008
+ left: 0;
1009
+ z-index: 1;
1010
+ text-align: center;
1011
+ min-width: 50px;
1012
+ }
1013
+
1014
+ .row-content {
1015
+ display: flex;
1016
+ flex-direction: column;
1017
+ align-items: center;
1018
+ }
1019
+
1020
+ .row-pos {
1021
+ font-size: 0.65rem;
1022
+ color: #666;
1023
+ font-weight: 600;
1024
+ }
1025
+
1026
+ .row-char {
1027
+ font-family: "Courier New", monospace;
1028
+ font-size: 0.9rem;
1029
+ font-weight: 600;
1030
+ color: #007bff;
1031
+ margin-top: 0.1rem;
1032
+ }
1033
+
1034
+ .mask-cell {
1035
+ border: 1px solid #dee2e6;
1036
+ padding: 0.5rem;
1037
+ text-align: center;
1038
+ font-family: "Courier New", monospace;
1039
+ font-weight: 600;
1040
+ font-size: 0.8rem;
1041
+ }
1042
+
1043
+ .mask-active {
1044
+ background: #d4edda;
1045
+ color: #155724;
1046
+ }
1047
+
1048
+ .mask-inactive {
1049
+ background: #f8f9fa;
1050
+ color: #ccc;
1051
+ }
1052
+
1053
+ .matrix-legend {
1054
+ display: flex;
1055
+ gap: 1.5rem;
1056
+ margin-top: 1rem;
1057
+ padding: 0.75rem;
1058
+ background: #f8f9fa;
1059
+ border-radius: 4px;
1060
+ }
1061
+
1062
+ .legend-item {
1063
+ display: flex;
1064
+ align-items: center;
1065
+ gap: 0.5rem;
1066
+ font-size: 0.85rem;
1067
+ }
1068
+
1069
+ .legend-box {
1070
+ width: 20px;
1071
+ height: 20px;
1072
+ border: 1px solid #dee2e6;
1073
+ border-radius: 3px;
1074
+ }
1075
+
1076
+ .legend-box.active {
1077
+ background: #d4edda;
1078
+ }
1079
+
1080
+ .legend-box.inactive {
1081
+ background: #f8f9fa;
1082
+ }
1083
+ </style>
trigo-web/app/src/views/TrigoView.vue CHANGED
@@ -1,15 +1,67 @@
1
  <template>
2
  <div class="trigo-view">
3
  <div class="view-header">
4
- <div class="logo-container" @click="handleLogoClick">
5
- <img :src="logoImage" alt="Trigo Logo" class="app-logo" />
6
- </div>
7
- <div class="view-status">
8
- <span class="turn-indicator" :class="{ black: currentPlayer === 'black', white: currentPlayer === 'white' }">
 
9
  {{ currentPlayer === "black" ? "Black" : "White" }}'s Turn
10
  </span>
11
  <span class="move-count">Move: {{ moveCount }}</span>
12
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
13
  </div>
14
 
15
  <div class="view-body">
@@ -23,9 +75,15 @@
23
  <!-- Inspect Mode Tooltip -->
24
  <div class="inspect-tooltip" v-if="inspectInfo.visible">
25
  <div class="tooltip-content">
26
- <span class="tooltip-label">{{ inspectInfo.groupSize }} stone{{ inspectInfo.groupSize > 1 ? 's' : '' }}</span>
 
 
 
 
27
  <span class="tooltip-divider">|</span>
28
- <span class="tooltip-label">Liberties: {{ inspectInfo.liberties }}</span>
 
 
29
  </div>
30
  </div>
31
  </div>
@@ -34,19 +92,32 @@
34
  <!-- Game Controls & Info Panel (Right) -->
35
  <div class="controls-panel">
36
  <!-- Score Display (Captured/Territory) -->
37
- <div class="panel-section score-section" :class="{ 'show-territory': showTerritoryMode }">
38
- <h3 class="section-title">{{ showTerritoryMode ? 'Territory' : 'Captured' }}</h3>
 
 
 
 
 
39
  <div class="score-display">
40
  <button class="score-button black" :disabled="!gameStarted">
41
  <span class="color-indicator black-stone"></span>
42
- <span class="score">{{ showTerritoryMode ? blackScore : capturedStones.black }}</span>
 
 
43
  </button>
44
  <button class="score-button white" :disabled="!gameStarted">
45
  <span class="color-indicator white-stone"></span>
46
- <span class="score">{{ showTerritoryMode ? whiteScore : capturedStones.white }}</span>
 
 
47
  </button>
48
  </div>
49
- <button class="compute-territory" @click="computeTerritory" :disabled="!gameStarted">
 
 
 
 
50
  Compute Territory
51
  </button>
52
  </div>
@@ -55,7 +126,11 @@
55
  <div class="panel-section routine-section">
56
  <h3 class="section-title">
57
  Move History
58
- <button class="btn-view-tgn" @click="showTGNModal = true" title="View TGN Notation">
 
 
 
 
59
  TGN
60
  </button>
61
  </h3>
@@ -83,7 +158,9 @@
83
  @click="jumpToMove(round.blackIndex)"
84
  >
85
  <span class="stone-icon black"></span>
86
- <span class="move-coords" v-if="!round.black.isPass">{{ formatMoveCoords(round.black) }}</span>
 
 
87
  <span class="move-label" v-else>pass</span>
88
  </div>
89
  <div
@@ -93,7 +170,9 @@
93
  @click="jumpToMove(round.whiteIndex)"
94
  >
95
  <span class="stone-icon white"></span>
96
- <span class="move-coords" v-if="!round.white.isPass">{{ formatMoveCoords(round.white) }}</span>
 
 
97
  <span class="move-label" v-else>pass</span>
98
  </div>
99
  <div v-else class="move-cell empty"></div>
@@ -142,7 +221,12 @@
142
  <div class="setting-item">
143
  <label for="board-shape">
144
  Board Shape:
145
- <span v-if="isBoardShapeDirty" class="dirty-indicator" title="Board shape will change on next game">*</span>
 
 
 
 
 
146
  </label>
147
  <select id="board-shape" v-model="selectedBoardShape">
148
  <option value="3*3*3">3×3×3</option>
@@ -193,1432 +277,1737 @@
193
  </template>
194
 
195
  <script setup lang="ts">
196
- import { ref, onMounted, onUnmounted, watch, computed, nextTick } from "vue";
197
- import { TrigoViewport } from "@/services/trigoViewport";
198
- import { useGameStore } from "@/stores/gameStore";
199
- import { storeToRefs } from "pinia";
200
- import type { BoardShape } from "../../../inc/trigo";
201
- import { Stone, validateMove, StoneType, validateTGN } from "../../../inc/trigo";
202
- import { TrigoGameFrontend } from "@/utils/TrigoGameFrontend";
203
- import { encodeAb0yz } from "../../../inc/trigo/ab0yz";
204
- import logoImage from "@/assets/logo.png";
205
- import { useSocket } from "@/composables/useSocket";
206
-
207
- // Helper functions for board shape parsing
208
- const parseBoardShape = (shapeStr: string): BoardShape => {
209
- const parts = shapeStr.split(/[^\d]+/).filter(Boolean).map(Number);
210
- return { x: parts[0] || 5, y: parts[1] || 5, z: parts[2] || 5 };
211
- };
212
-
213
- const printBoardShape = (shape: BoardShape): string => {
214
- return `${shape.x}*${shape.y}*${shape.z}`;
215
- };
216
-
217
- // Format move coordinates using TGN notation
218
- const formatMoveCoords = (move: { x: number; y: number; z: number }): string => {
219
- const shape = boardShape.value;
220
- return encodeAb0yz([move.x, move.y, move.z], [shape.x, shape.y, shape.z]);
221
- };
222
-
223
- // Use game store
224
- const gameStore = useGameStore();
225
- const {
226
- currentPlayer,
227
- moveHistory,
228
- currentMoveIndex,
229
- capturedStones,
230
- gameStatus,
231
- boardShape,
232
- moveCount,
233
- isGameActive,
234
- passCount
235
- } = storeToRefs(gameStore);
236
-
237
- // Local state
238
- const hoveredPosition = ref<string | null>(null);
239
- const blackScore = ref(0);
240
- const whiteScore = ref(0);
241
- const isLoading = ref(true);
242
- const selectedBoardShape = ref<string>(printBoardShape(boardShape.value));
243
- const showTerritoryMode = ref(false);
244
- const showTGNModal = ref(false);
245
- const inspectInfo = ref({
246
- visible: false,
247
- groupSize: 0,
248
- liberties: 0
249
- });
250
-
251
- // TGN Editor state
252
- const editableTGNContent = ref<string>('');
253
- const tgnValidationState = ref<'idle' | 'valid' | 'invalid'>('idle');
254
- const tgnValidationError = ref<string>('');
255
- let tgnValidationTimeout: ReturnType<typeof setTimeout> | null = null;
256
-
257
-
258
- // Socket.io connection
259
- const { sendEcho, connected: socketConnected } = useSocket();
260
-
261
-
262
- // Canvas reference and viewport
263
- const viewportCanvas = ref<HTMLCanvasElement | null>(null);
264
- const moveHistoryContainer = ref<HTMLDivElement | null>(null);
265
- let viewport: TrigoViewport | null = null;
266
-
267
- // Computed properties
268
- const gameStarted = computed(() => isGameActive.value);
269
-
270
- // Group moves into rounds (pairs of black and white moves)
271
- const moveRounds = computed(() => {
272
- const rounds: Array<{
273
- black: any;
274
- white: any | null;
275
- blackIndex: number;
276
- whiteIndex: number | null;
277
- }> = [];
278
-
279
- for (let i = 0; i < moveHistory.value.length; i += 2) {
280
- const blackMove = moveHistory.value[i];
281
- const whiteMove = moveHistory.value[i + 1] || null;
282
-
283
- rounds.push({
284
- black: blackMove,
285
- white: whiteMove,
286
- blackIndex: i + 1, // Convert array index to "moves applied" count
287
- whiteIndex: whiteMove ? i + 2 : null // Convert array index to "moves applied" count
288
- });
289
- }
290
-
291
- return rounds;
292
- });
293
-
294
- // Check if selected board shape differs from current game board shape
295
- const isBoardShapeDirty = computed(() => {
296
- const selectedShape = parseBoardShape(selectedBoardShape.value);
297
- const currentShape = boardShape.value;
298
- return selectedShape.x !== currentShape.x ||
299
- selectedShape.y !== currentShape.y ||
300
- selectedShape.z !== currentShape.z;
301
- });
302
-
303
- // Generate TGN content
304
- const tgnContent = computed(() => {
305
- return gameStore.game?.toTGN({
306
- application: 'Trigo Demo v1.0',
307
- date: new Date().toISOString().split('T')[0].replace(/-/g, '.')
308
- }) || '';
309
- });
310
-
311
- // TGN validation computed properties
312
- const tgnIsValid = computed(() => tgnValidationState.value === 'valid');
313
-
314
- const tgnValidationClass = computed(() => {
315
- if (tgnValidationState.value === 'idle') return 'idle';
316
- if (tgnValidationState.value === 'valid') return 'valid';
317
- return 'invalid';
318
- });
319
-
320
- const tgnValidationMessage = computed(() => {
321
- if (tgnValidationState.value === 'idle') return 'Ready to validate';
322
- if (tgnValidationState.value === 'valid') return '✓ Valid TGN';
323
- return `✗ ${tgnValidationError.value}`;
324
- });
325
-
326
- // Debounced TGN validation (synchronous)
327
- const onTGNEdit = () => {
328
- // Clear existing timeout
329
- if (tgnValidationTimeout) {
330
- clearTimeout(tgnValidationTimeout);
331
- }
332
 
333
- // Set validation state to idle while waiting for debounce
334
- tgnValidationState.value = 'idle';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
335
 
336
- // Set new debounce timeout (300ms)
337
- tgnValidationTimeout = setTimeout(() => {
338
- const result = validateTGN(editableTGNContent.value);
339
 
340
- if (result.valid) {
341
- tgnValidationState.value = 'valid';
342
- tgnValidationError.value = '';
343
- } else {
344
- tgnValidationState.value = 'invalid';
345
- tgnValidationError.value = result.error || 'Invalid TGN format';
346
- }
347
- }, 300);
348
- };
 
349
 
350
- // Apply TGN and update game state (synchronous)
351
- const applyTGN = () => {
352
- if (!tgnIsValid.value) return;
 
 
 
 
 
 
353
 
354
- try {
355
- const newGame = TrigoGameFrontend.fromTGN(editableTGNContent.value);
356
 
357
- // Update game store with the new TrigoGameFrontend instance
358
- gameStore.game = newGame;
 
 
 
359
 
360
- // Save to session storage
361
- gameStore.saveToSessionStorage();
 
 
 
362
 
363
- // Update viewport with new board state
364
- // The getters will automatically compute the new values from gameStore.game
365
- if (viewport) {
366
- viewport.setBoardShape(newGame.getShape());
367
- syncViewportWithStore();
368
  }
369
 
370
- // Close modal
371
- showTGNModal.value = false;
372
- } catch (err) {
373
- console.error('Failed to apply TGN:', err);
374
- tgnValidationState.value = 'invalid';
375
- tgnValidationError.value = err instanceof Error ? err.message : 'Failed to apply TGN';
376
- }
377
- };
378
 
379
- // Handle stone placement
380
- const handleStoneClick = (x: number, y: number, z: number) => {
381
- if (!gameStarted.value)
382
- return;
383
 
384
- // Make move in store
385
- const result = gameStore.makeMove(x, y, z);
 
 
 
 
 
 
 
386
 
387
- if (result.success && viewport) {
388
- // Add stone to viewport (store already switched player, so use opponent)
389
- const stoneColor = gameStore.opponentPlayer;
390
- viewport.addStone(x, y, z, stoneColor);
391
 
392
- // Remove captured stones from viewport
393
- if (result.capturedPositions && result.capturedPositions.length > 0) {
394
- result.capturedPositions.forEach(pos => {
395
- viewport.removeStone(pos.x, pos.y, pos.z);
396
- });
397
- console.log(`Captured ${result.capturedPositions.length} stone(s)`);
398
- }
399
 
400
- // Hide domain visualization and switch back to captured display after move
401
- viewport.hideDomainCubes();
402
- showTerritoryMode.value = false;
403
- }
404
- };
405
-
406
- // Handle position hover
407
- const handlePositionHover = (x: number | null, y: number | null, z: number | null) => {
408
- if (x !== null && y !== null && z !== null) {
409
- hoveredPosition.value = `(${x}, ${y}, ${z})`;
410
- } else {
411
- hoveredPosition.value = null;
412
- }
413
- };
414
-
415
- // Check if a position is droppable (validates with game rules)
416
- const isPositionDroppable = (x: number, y: number, z: number): boolean => {
417
- const pos = { x, y, z };
418
- const playerColor = currentPlayer.value === "black" ? StoneType.BLACK : StoneType.WHITE;
419
-
420
- const validation = validateMove(
421
- pos,
422
- playerColor,
423
- gameStore.board,
424
- boardShape.value,
425
- gameStore.lastCapturedPositions
426
- );
427
-
428
- return validation.valid;
429
- };
430
-
431
- // Handle inspect mode callback
432
- const handleInspectGroup = (groupSize: number, liberties: number) => {
433
- if (groupSize > 0) {
434
- inspectInfo.value = {
435
- visible: true,
436
- groupSize,
437
- liberties
438
- };
439
- } else {
440
- inspectInfo.value = {
441
- visible: false,
442
- groupSize: 0,
443
- liberties: 0
444
- };
445
- }
446
- };
447
 
 
 
448
 
449
- // Handle logo click - WebSocket echo test
450
- const handleLogoClick = async () => {
451
- try {
452
- console.log("[Echo Test] Sending echo request...");
453
- console.log("[Echo Test] Socket connected:", socketConnected.value);
 
454
 
455
- const response = await sendEcho("Echo test from Trigo!");
456
- console.log("[Echo Test] Response:", response);
457
- } catch (error) {
458
- console.error("[Echo Test] Error:", error);
459
- }
460
- };
 
 
461
 
462
 
463
- // Game control methods
464
- const newGame = () => {
465
- // Parse selected board shape
466
- const shape = parseBoardShape(selectedBoardShape.value);
 
467
 
468
- // Initialize game in store
469
- gameStore.initializeGame(shape);
470
- gameStore.startGame();
471
 
472
- // Update viewport with new board shape
473
- if (viewport) {
474
- viewport.setBoardShape(shape);
475
- viewport.clearBoard();
476
- viewport.setGameActive(true);
477
 
478
- // Exit territory mode if active
479
- viewport.hideDomainCubes();
480
- }
481
 
482
- // Reset scores and territory mode
483
- blackScore.value = 0;
484
- whiteScore.value = 0;
485
- showTerritoryMode.value = false;
486
 
487
- console.log(
488
- `Starting new game with board shape ${shape.x}×${shape.y}×${shape.z}`
489
- );
490
- };
491
 
492
- const pass = () => {
493
- const previousPlayer = currentPlayer.value;
494
- const success = gameStore.pass();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
495
 
496
- if (success) {
497
- // Check if game ended due to double pass
498
- //if (gameStore.gameResult?.reason === "double-pass") {
499
- // showGameResult();
500
- //} else {
501
- console.log(`${previousPlayer} passed (Pass count: ${gameStore.passCount})`);
502
- //}
503
- }
504
- };
505
 
 
 
 
506
 
507
- const resign = () => {
508
- // Confirm resignation
509
- const confirmed = confirm(
510
- `Are you sure ${currentPlayer.value} wants to resign?\n\nThis will end the game immediately.`
511
- );
512
 
513
- if (!confirmed) return;
 
 
 
 
 
 
 
 
 
 
 
514
 
515
- //const success = gameStore.resign();
 
 
516
 
517
- //if (success) {
518
- // showGameResult();
519
- //}
520
- };
 
 
 
 
521
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
522
 
523
- const showGameResult = () => {
524
- const result = gameStore.gameResult;
525
- if (!result) return;
526
 
527
- // Don't set game as inactive - allow continued analysis
528
- // if (viewport) {
529
- // viewport.setGameActive(false);
530
- // }
531
 
532
- let message = "";
533
- if (result.reason === "resignation") {
534
- message = `${result.winner === "black" ? "Black" : "White"} wins by resignation!\n\nGame continues for analysis.`;
535
- } else if (result.reason === "double-pass") {
536
- // Calculate final scores for double pass
537
- const territory = gameStore.computeTerritory();
538
- const blackTotal = territory.black + capturedStones.value.black;
539
- const whiteTotal = territory.white + capturedStones.value.white;
540
 
541
- blackScore.value = blackTotal;
542
- whiteScore.value = whiteTotal;
543
 
544
- if (blackTotal > whiteTotal) {
545
- message = `Black wins by ${blackTotal - whiteTotal} points!\n\nBlack: ${blackTotal} points\nWhite: ${whiteTotal} points\n\nGame continues for analysis.`;
546
- } else if (whiteTotal > blackTotal) {
547
- message = `White wins by ${whiteTotal - blackTotal} points!\n\nWhite: ${whiteTotal} points\nBlack: ${blackTotal} points\n\nGame continues for analysis.`;
548
- } else {
549
- message = `Game is a draw!\n\nBoth players: ${blackTotal} points\n\nGame continues for analysis.`;
550
  }
551
- }
552
 
553
- setTimeout(() => {
554
- alert(message);
555
- }, 100);
556
- };
557
 
 
 
 
 
558
 
559
- const computeTerritory = () => {
560
- if (!gameStarted.value) return;
 
561
 
562
- // Toggle territory mode
563
- if (showTerritoryMode.value) {
564
- // Exit territory mode
565
  if (viewport) {
 
 
 
 
 
566
  viewport.hideDomainCubes();
567
  }
568
- showTerritoryMode.value = false;
569
- // Reset scores to captured stones count
570
  blackScore.value = 0;
571
  whiteScore.value = 0;
572
- return;
573
- }
574
 
575
- // Enter territory mode - Use store's territory calculation
576
- const territory = gameStore.computeTerritory();
577
- blackScore.value = territory.black + capturedStones.value.black;
578
- whiteScore.value = territory.white + capturedStones.value.white;
579
 
580
- // Switch to territory display mode
581
- showTerritoryMode.value = true;
 
 
 
 
 
582
 
583
- // Convert territory arrays to Sets of position keys for viewport
584
- if (viewport) {
585
- const blackDomain = new Set<string>();
586
- const whiteDomain = new Set<string>();
587
 
588
- // Use the calculated territory positions from the territory result
589
- territory.blackTerritory.forEach(pos => {
590
- const key = `${pos.x},${pos.y},${pos.z}`;
591
- blackDomain.add(key);
592
- });
 
 
 
 
593
 
594
- territory.whiteTerritory.forEach(pos => {
595
- const key = `${pos.x},${pos.y},${pos.z}`;
596
- whiteDomain.add(key);
597
- });
 
598
 
599
- // Set domain data and show both domains
600
- viewport.setDomainData(blackDomain, whiteDomain);
601
- viewport.setBlackDomainVisible(true);
602
- viewport.setWhiteDomainVisible(true);
603
- }
604
 
605
- console.log("Territory computed:", territory);
606
- };
607
 
608
- const previousMove = () => {
609
- const success = gameStore.undoMove();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
610
 
611
- if (success && viewport) {
612
- // Rebuild viewport from current board state
613
- syncViewportWithStore();
614
- }
615
- };
616
 
617
- const nextMove = () => {
618
- const success = gameStore.redoMove();
619
 
620
- if (success && viewport) {
621
- // Rebuild viewport from current board state
622
- syncViewportWithStore();
623
- }
624
- };
 
 
 
 
 
 
 
625
 
626
- const jumpToMove = (index: number) => {
627
- const success = gameStore.jumpToMove(index);
 
 
628
 
629
- if (success && viewport) {
630
- // Rebuild viewport from current board state
631
- syncViewportWithStore();
632
 
633
- // Exit territory mode when navigating move history
634
- viewport.hideDomainCubes();
635
- showTerritoryMode.value = false;
636
- // Reset scores to 0 when exiting territory mode
637
- blackScore.value = 0;
638
- whiteScore.value = 0;
639
- }
640
- };
641
-
642
- // Sync viewport with store's board state
643
- const syncViewportWithStore = () => {
644
- if (!viewport) return;
645
-
646
- // Clear viewport
647
- viewport.clearBoard();
648
-
649
- // Read the actual board state from the store (which has captures applied)
650
- const board = gameStore.board;
651
- const shape = boardShape.value;
652
-
653
- // Add all stones that exist on the board
654
- for (let x = 0; x < shape.x; x++) {
655
- for (let y = 0; y < shape.y; y++) {
656
- for (let z = 0; z < shape.z; z++) {
657
- const stone = board[x][y][z];
658
- if (stone === Stone.Black) {
659
- viewport.addStone(x, y, z, "black");
660
- } else if (stone === Stone.White) {
661
- viewport.addStone(x, y, z, "white");
662
- }
663
- }
664
  }
665
- }
666
 
667
- // Set the last placed stone highlight based on current move index
668
- // currentMoveIndex represents the number of moves applied (not array index)
669
- // So the last applied move is at array index (currentMoveIndex - 1)
670
- if (currentMoveIndex.value > 0 && currentMoveIndex.value <= moveHistory.value.length) {
671
- const lastMove = moveHistory.value[currentMoveIndex.value - 1];
672
- viewport.setLastPlacedStone(lastMove.x, lastMove.y, lastMove.z);
673
- } else {
674
- // No moves applied or at START position
675
- viewport.setLastPlacedStone(null, null, null);
676
- }
677
- };
678
 
679
- // Watch for current player changes to update viewport preview
680
- watch(currentPlayer, (newPlayer) => {
681
- if (viewport) {
682
- viewport.setCurrentPlayer(newPlayer);
683
- }
684
- });
685
-
686
- // Watch currentMoveIndex to auto-scroll move history to keep current move visible
687
- watch(currentMoveIndex, () => {
688
- // Use nextTick to ensure DOM is updated before scrolling
689
- nextTick(() => {
690
- if (moveHistoryContainer.value) {
691
- // Find the active move element
692
- const activeElement = moveHistoryContainer.value.querySelector('.active');
693
- if (activeElement) {
694
- // Scroll the active element into view smoothly
695
- activeElement.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
696
- }
697
  }
698
- });
699
- });
700
-
701
- // Watch TGN modal to populate editable content when opened
702
- watch(showTGNModal, (isVisible) => {
703
- if (isVisible) {
704
- // Populate with current game's TGN when modal opens
705
- editableTGNContent.value = tgnContent.value;
706
- tgnValidationState.value = 'valid'; // Current TGN is always valid
707
- tgnValidationError.value = '';
708
- }
709
- });
710
 
 
 
711
 
712
- // Lifecycle hooks
713
- onMounted(() => {
714
- console.log("TrigoDemo component mounted");
 
 
715
 
716
- // Try to restore game state from session storage
717
- const restoredFromStorage = gameStore.loadFromSessionStorage();
718
 
719
- // If not restored from storage, initialize new game
720
- if (!restoredFromStorage) {
721
- // Parse initial board shape
722
- const shape = parseBoardShape(selectedBoardShape.value);
723
 
724
- // Initialize game store
725
- gameStore.initializeGame(shape);
726
- } else {
727
- // Update selected board shape to match restored state
728
- selectedBoardShape.value = printBoardShape(boardShape.value);
729
- console.log("Restored game state from session storage");
730
- }
 
731
 
732
- // Initialize Three.js viewport with current board shape
733
- if (viewportCanvas.value) {
734
- viewport = new TrigoViewport(viewportCanvas.value, boardShape.value, {
735
- onStoneClick: handleStoneClick,
736
- onPositionHover: handlePositionHover,
737
- isPositionDroppable: isPositionDroppable,
738
- onInspectGroup: handleInspectGroup
739
- });
740
- console.log("TrigoViewport initialized");
741
 
742
- // Hide loading overlay after viewport is initialized
743
- isLoading.value = false;
744
 
745
- // If game was restored, sync viewport with restored board state
746
- if (restoredFromStorage) {
747
- syncViewportWithStore();
748
- viewport.setGameActive(isGameActive.value);
 
 
 
 
 
 
 
 
 
 
 
 
749
  }
750
- }
751
 
752
- // Start game automatically if not restored or was playing
753
- if (!restoredFromStorage || gameStore.gameStatus === "idle") {
754
- gameStore.startGame();
 
 
 
 
 
 
 
 
 
 
 
755
  if (viewport) {
756
- viewport.setGameActive(true);
757
  }
758
- }
759
 
760
- // Add keyboard shortcuts
761
- window.addEventListener("keydown", handleKeyPress);
762
- });
 
 
 
 
 
 
 
 
 
 
 
763
 
 
 
 
 
 
 
 
 
 
764
 
765
- onUnmounted(() => {
766
- console.log("TrigoDemo component unmounted");
 
767
 
768
- // Remove keyboard shortcuts
769
- window.removeEventListener("keydown", handleKeyPress);
770
 
771
- // Cleanup Three.js resources
772
- if (viewport) {
773
- viewport.destroy();
774
- viewport = null;
775
- }
776
- });
 
 
 
 
 
 
777
 
 
 
 
 
 
 
 
 
 
778
 
779
- // Keyboard shortcuts handler
780
- const handleKeyPress = (event: KeyboardEvent) => {
781
- // Ignore if typing in an input field
782
- if (event.target instanceof HTMLInputElement || event.target instanceof HTMLTextAreaElement) {
783
- return;
784
- }
785
 
786
- switch (event.key.toLowerCase()) {
787
- case "p": // Pass
788
- if (gameStarted.value) {
789
- pass();
790
  }
791
- break;
792
- case "n": // New game
793
- newGame();
794
- break;
795
- case "r": // Resign
796
- if (gameStarted.value) {
797
- resign();
798
- }
799
- break;
800
- case "arrowleft": // Previous move
801
- previousMove();
802
- event.preventDefault();
803
- break;
804
- case "arrowright": // Next move
805
- nextMove();
806
- event.preventDefault();
807
- break;
808
- case "t": // Compute territory
809
- if (gameStarted.value) {
810
- computeTerritory();
811
- }
812
- break;
813
- }
814
- };
815
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
816
 
817
- // TGN Modal Methods
818
- const selectAllTGN = (event: Event) => {
819
- const textarea = event.target as HTMLTextAreaElement;
820
- textarea.select();
821
- };
822
 
823
- const copyTGN = async () => {
824
- try {
825
- // Try modern clipboard API first
826
- if (navigator.clipboard && navigator.clipboard.writeText) {
827
- await navigator.clipboard.writeText(tgnContent.value);
828
- alert('TGN copied to clipboard!');
829
- } else {
830
- // Fallback for older browsers or non-secure contexts
831
- const textarea = document.createElement('textarea');
832
- textarea.value = tgnContent.value;
833
- textarea.style.position = 'fixed';
834
- textarea.style.opacity = '0';
835
- document.body.appendChild(textarea);
836
- textarea.select();
837
-
838
- try {
839
- document.execCommand('copy');
840
- alert('TGN copied to clipboard!');
841
- } catch (fallbackErr) {
842
- console.error('Fallback copy failed:', fallbackErr);
843
- alert('Failed to copy TGN. Please select and copy manually.');
844
- } finally {
845
- document.body.removeChild(textarea);
846
  }
847
  }
848
- } catch (err) {
849
- console.error('Failed to copy TGN:', err);
850
- alert('Failed to copy TGN. Please select and copy manually.');
851
- }
852
- };
853
- </script>
854
 
855
- <style lang="scss" scoped>
856
- .trigo-view {
857
- display: flex;
858
- flex-direction: column;
859
- height: 100%;
860
- background-color: #404040;
861
- color: #e0e0e0;
862
- overflow: hidden;
863
-
864
- .view-header {
865
- display: flex;
866
- justify-content: space-between;
867
- align-items: center;
868
- padding: 1rem 2rem;
869
- background: linear-gradient(135deg, #505050 0%, #454545 100%);
870
- border-bottom: 2px solid #606060;
871
- box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
872
 
873
- .logo-container {
874
- display: flex;
875
- align-items: center;
876
- gap: 0.75rem;
877
 
878
- .app-logo {
879
- width: 40px;
880
- height: 40px;
881
- object-fit: contain;
882
- }
883
 
884
- .app-title {
885
- font-size: 1.5rem;
886
- font-weight: 700;
887
- color: #e94560;
888
- }
889
- }
890
 
891
- .view-title {
892
- font-size: 1.5rem;
893
- font-weight: 700;
894
- margin: 0;
895
- color: #e94560;
896
  }
897
 
898
- .view-status {
899
- display: flex;
900
- gap: 2rem;
901
- align-items: center;
 
 
902
 
903
- .turn-indicator {
904
- padding: 0.5rem 1rem;
905
- border-radius: 8px;
906
- font-weight: 600;
907
- transition: all 0.3s ease;
 
 
 
 
908
 
909
- &.black {
910
- background-color: #2c2c2c;
911
- color: #fff;
912
- box-shadow: 0 0 10px rgba(255, 255, 255, 0.3);
913
  }
914
-
915
- &.white {
916
- background-color: #f0f0f0;
917
- color: #000;
918
- box-shadow: 0 0 10px rgba(240, 240, 240, 0.3);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
919
  }
920
  }
921
-
922
- .move-count {
923
- font-size: 1rem;
924
- color: #a0a0a0;
925
- }
926
  }
927
- }
 
928
 
929
- .view-body {
 
930
  display: flex;
931
- flex: 1;
 
 
 
932
  overflow: hidden;
933
 
934
- .board-container {
935
- flex: 1;
936
  display: flex;
937
- align-items: center;
938
  justify-content: center;
939
- background-color: #484848;
940
- padding: 1rem;
941
- position: relative;
942
-
943
- .viewport-wrapper {
944
- width: 100%;
945
- height: 100%;
946
- position: relative;
947
- border-radius: 8px;
948
- overflow: hidden;
949
- box-shadow: 0 4px 16px rgba(0, 0, 0, 0.5);
950
-
951
- .viewport-canvas {
952
- width: 100%;
953
- height: 100%;
954
- display: block;
955
- background-color: #50505a;
956
- }
957
-
958
- .viewport-overlay {
959
- position: absolute;
960
- top: 0;
961
- left: 0;
962
- width: 100%;
963
- height: 100%;
964
- display: flex;
965
- align-items: center;
966
- justify-content: center;
967
- pointer-events: none;
968
 
969
- .loading-text {
970
- color: rgba(255, 255, 255, 0.5);
971
- font-size: 1.2rem;
972
- animation: pulse 2s ease-in-out infinite;
973
- }
974
- }
975
 
976
- .inspect-tooltip {
977
- position: absolute;
978
- top: 1rem;
979
- left: 1rem;
980
- background-color: rgba(255, 241, 176, 0.95);
981
- color: #111;
982
- padding: 0.5rem 1rem;
983
- border-radius: 8px;
984
- font-size: 0.9rem;
985
- font-weight: 600;
986
- pointer-events: none;
987
- box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
988
- z-index: 100;
989
-
990
- .tooltip-content {
991
- display: flex;
992
- align-items: center;
993
- gap: 0.5rem;
994
 
995
- .tooltip-label {
996
- white-space: nowrap;
 
 
997
  }
998
 
999
- .tooltip-divider {
1000
- color: #888;
 
 
1001
  }
1002
  }
1003
- }
1004
- }
1005
- }
1006
-
1007
- .controls-panel {
1008
- width: 320px;
1009
- background-color: #3a3a3a;
1010
- border-left: 2px solid #606060;
1011
- display: flex;
1012
- flex-direction: column;
1013
- overflow: hidden;
1014
- box-shadow: -4px 0 16px rgba(0, 0, 0, 0.3);
1015
-
1016
- .panel-section {
1017
- padding: 0.8rem;
1018
- border-bottom: 1px solid #505050;
1019
- position: relative;
1020
-
1021
- .section-title {
1022
- font-size: 0.7rem;
1023
- font-weight: 600;
1024
- color: #f0bcc5;
1025
- text-transform: uppercase;
1026
- letter-spacing: 1px;
1027
- position: absolute;
1028
- top: 0;
1029
- left: 0.8rem;
1030
- opacity: 0;
1031
- transition: opacity 0.3s ease-in;
1032
- }
1033
 
1034
- &:hover .section-title {
1035
- opacity: 0.6;
 
1036
  }
1037
- }
1038
 
1039
- .score-section {
1040
- // Default style (captured mode) - lighter background
1041
- .score-display {
1042
- display: flex;
1043
- gap: 0.5rem;
1044
- margin-bottom: 1rem;
1045
-
1046
- .score-button {
1047
- flex: 1;
1048
  display: flex;
1049
  align-items: center;
1050
- justify-content: center;
1051
  gap: 0.5rem;
1052
- padding: 1rem;
1053
- border: 2px solid #505050;
1054
  border-radius: 8px;
1055
- background-color: #484848;
1056
- cursor: default;
1057
  transition: all 0.3s ease;
1058
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1059
  &.black {
1060
- .color-indicator {
1061
- background-color: #2c2c2c;
1062
- border: 2px solid #fff;
1063
- }
1064
  }
1065
 
1066
  &.white {
1067
- .color-indicator {
1068
- background-color: #f0f0f0;
1069
- border: 2px solid #000;
1070
- }
1071
  }
 
1072
 
1073
- .color-indicator {
1074
- width: 24px;
1075
- height: 24px;
1076
- border-radius: 50%;
1077
- }
1078
 
1079
- .score {
1080
- font-size: 1.5rem;
1081
- font-weight: 700;
1082
- color: #e0e0e0;
1083
  }
1084
 
1085
- &:disabled {
1086
- opacity: 0.5;
1087
  }
1088
  }
1089
- }
1090
 
1091
- .compute-territory {
1092
- width: 100%;
1093
- padding: 0.75rem;
1094
- background-color: #505050;
1095
- color: #e0e0e0;
1096
- border: none;
1097
- border-radius: 8px;
1098
- font-weight: 600;
1099
- cursor: pointer;
1100
- transition: all 0.3s ease;
1101
 
1102
- &:hover:not(:disabled) {
1103
- background-color: #606060;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1104
  }
 
1105
 
1106
- &:disabled {
1107
- opacity: 0.5;
1108
- cursor: not-allowed;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1109
  }
1110
  }
1111
 
1112
- // Territory mode - darker background with glow
1113
- &.show-territory {
1114
- .score-display .score-button {
 
 
 
 
1115
  background-color: #3a3a3a;
1116
- border-color: #606060;
1117
- box-shadow: 0 0 12px rgba(233, 69, 96, 0.3);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1118
  }
1119
  }
1120
  }
 
1121
 
 
 
 
 
1122
 
1123
-
1124
- .routine-section {
1125
  flex: 1;
1126
  display: flex;
1127
- flex-direction: column;
1128
- min-height: 0;
 
 
 
1129
 
1130
- .routine-content {
1131
- flex: 1;
1132
- overflow-y: auto;
1133
- background-color: #2a2a2a;
1134
  border-radius: 8px;
1135
- padding: 0.5rem;
 
1136
 
1137
- .move-list {
1138
- padding: 0;
1139
- margin: 0;
 
 
 
1140
 
1141
- .move-row {
1142
- display: grid;
1143
- grid-template-columns: 30px 1fr 1fr;
1144
- gap: 0.25rem;
1145
- margin-bottom: 0.25rem;
1146
- align-items: center;
 
 
 
 
1147
 
1148
- &.start-row {
1149
- grid-template-columns: 30px 1fr;
1150
- padding: 0.5rem;
1151
- border-radius: 4px;
1152
- cursor: pointer;
1153
- transition: all 0.2s ease;
1154
 
1155
- &:hover {
1156
- background-color: #484848;
1157
- }
 
 
 
 
 
 
 
 
 
 
1158
 
1159
- &.active {
1160
- background-color: #505050;
1161
- border-left: 3px solid #e94560;
1162
- }
1163
 
1164
- .open-label {
1165
- font-weight: 700;
1166
- color: #e94560;
1167
- text-transform: uppercase;
1168
- letter-spacing: 1px;
1169
- }
1170
  }
1171
 
1172
- .round-number {
1173
- color: #808080;
1174
- font-size: 0.9em;
1175
- text-align: right;
1176
- padding-right: 0.25rem;
1177
  }
 
 
 
 
1178
 
1179
- .move-cell {
1180
- padding: 0.5rem;
1181
- border-radius: 4px;
1182
- cursor: pointer;
1183
- transition: all 0.2s ease;
1184
- display: flex;
1185
- align-items: center;
1186
- gap: 0.5rem;
1187
- background-color: #1a1a1a;
1188
 
1189
- &:hover:not(.empty) {
1190
- background-color: #484848;
1191
- }
 
1192
 
1193
- &.active {
1194
- background-color: #505050;
1195
- border-left: 3px solid #e94560;
1196
- }
 
 
 
 
 
 
 
 
1197
 
1198
- &.empty {
1199
- background-color: transparent;
1200
- cursor: default;
1201
- }
1202
 
1203
- .stone-icon {
1204
- width: 16px;
1205
- height: 16px;
1206
- border-radius: 50%;
1207
- flex-shrink: 0;
 
1208
 
1209
- &.black {
1210
- background-color: #2c2c2c;
1211
- border: 2px solid #fff;
1212
- }
 
 
 
 
 
 
 
 
1213
 
1214
- &.white {
1215
- background-color: #f0f0f0;
1216
- border: 2px solid #000;
1217
- }
1218
  }
 
1219
 
1220
- .move-coords {
1221
- color: #a0a0a0;
1222
- font-family: monospace;
1223
- font-size: 0.9em;
1224
  }
 
1225
 
1226
- .move-label {
1227
- color: #60a5fa;
1228
- font-weight: 600;
1229
- font-size: 0.85em;
1230
- text-transform: lowercase;
1231
- }
1232
  }
1233
- }
1234
- }
1235
- }
1236
- }
1237
 
1238
- .controls-section {
1239
- .control-buttons {
1240
- display: flex;
1241
- flex-direction: column;
1242
- gap: 1rem;
1243
 
1244
- .play-controls,
1245
- .history-controls {
1246
- display: flex;
1247
- gap: 0.5rem;
1248
  }
1249
 
1250
- .btn {
1251
- padding: 0.75rem 1.5rem;
 
 
 
1252
  border: none;
1253
  border-radius: 8px;
1254
  font-weight: 600;
1255
  cursor: pointer;
1256
  transition: all 0.3s ease;
1257
- flex: 1;
 
 
 
1258
 
1259
  &:disabled {
1260
  opacity: 0.5;
1261
  cursor: not-allowed;
1262
  }
 
1263
 
1264
- &.btn-pass {
1265
- background-color: #2d4059;
1266
- color: #e0e0e0;
1267
-
1268
- &:hover:not(:disabled) {
1269
- background-color: #3d5069;
1270
- }
1271
  }
 
 
1272
 
1273
- &.btn-resign {
1274
- background-color: #c23b22;
1275
- color: #fff;
 
 
1276
 
1277
- &:hover:not(:disabled) {
1278
- background-color: #d44b32;
1279
- }
1280
- }
 
 
1281
 
1282
- &.btn-icon {
1283
- background-color: #505050;
1284
- color: #e0e0e0;
1285
- padding: 0.75rem;
1286
- font-size: 1.2rem;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1287
 
1288
- &:hover:not(:disabled) {
1289
- background-color: #606060;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1290
  }
1291
  }
1292
  }
1293
  }
1294
- }
1295
-
1296
- .settings-section {
1297
- .settings-content {
1298
- display: flex;
1299
- flex-direction: column;
1300
- gap: 1rem;
1301
 
1302
- .setting-item {
 
1303
  display: flex;
1304
- align-items: center;
1305
- justify-content: space-between;
1306
 
1307
- label {
1308
- font-weight: 600;
1309
- color: #a0a0a0;
1310
  display: flex;
1311
- align-items: center;
1312
- gap: 0.3rem;
1313
-
1314
- .dirty-indicator {
1315
- color: #e94560;
1316
- font-size: 1.2rem;
1317
- font-weight: 700;
1318
- animation: pulse 2s ease-in-out infinite;
1319
- }
1320
  }
1321
 
1322
- select {
1323
- padding: 0.5rem;
1324
- border: 2px solid #505050;
1325
  border-radius: 8px;
1326
- background-color: #484848;
1327
- color: #e0e0e0;
1328
- cursor: pointer;
1329
  font-weight: 600;
 
 
 
1330
 
1331
  &:disabled {
1332
  opacity: 0.5;
1333
  cursor: not-allowed;
1334
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1335
  }
1336
  }
 
1337
 
1338
- .btn-new-game {
1339
- width: 100%;
1340
- padding: 0.75rem;
1341
- background-color: #e94560;
1342
- color: #fff;
1343
- border: none;
1344
- border-radius: 8px;
1345
- font-weight: 700;
1346
- cursor: pointer;
1347
- transition: all 0.3s ease;
1348
- text-transform: uppercase;
1349
 
1350
- &:hover {
1351
- background-color: #f95670;
1352
- transform: translateY(-2px);
1353
- box-shadow: 0 4px 12px rgba(233, 69, 96, 0.4);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1354
  }
1355
  }
1356
  }
1357
  }
1358
  }
1359
  }
1360
- }
1361
-
1362
- /* TGN Button and Modal Styles */
1363
- .btn-view-tgn {
1364
- position: absolute;
1365
- top: 0;
1366
- right: 0.8rem;
1367
- padding: 0.25rem 0.5rem;
1368
- background-color: transparent;
1369
- color: #a0a0a0;
1370
- border: 1px solid #505050;
1371
- border-radius: 4px;
1372
- font-size: 0.65rem;
1373
- font-weight: 600;
1374
- text-transform: uppercase;
1375
- letter-spacing: 0.5px;
1376
- cursor: pointer;
1377
- transition: all 0.2s ease;
1378
- opacity: 0;
1379
-
1380
- &:hover {
1381
- background-color: #505050;
1382
- color: #e0e0e0;
1383
- border-color: #606060;
 
 
 
 
1384
  }
1385
- }
1386
-
1387
- .routine-section:hover .btn-view-tgn {
1388
- opacity: 1;
1389
- }
1390
-
1391
- .tgn-modal {
1392
- position: fixed;
1393
- top: 0;
1394
- left: 0;
1395
- width: 100vw;
1396
- height: 100vh;
1397
- background-color: rgba(0, 0, 0, 0.7);
1398
- display: flex;
1399
- align-items: center;
1400
- justify-content: center;
1401
- z-index: 1000;
1402
- backdrop-filter: blur(4px);
1403
-
1404
- .tgn-modal-content {
1405
- background-color: #3a3a3a;
1406
- border-radius: 12px;
1407
- box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5);
1408
- width: 90%;
1409
- max-width: 600px;
1410
- max-height: 80vh;
1411
  display: flex;
1412
- flex-direction: column;
1413
- overflow: hidden;
 
 
1414
 
1415
- .tgn-modal-header {
 
 
 
 
 
 
1416
  display: flex;
1417
- justify-content: space-between;
1418
- align-items: center;
1419
- padding: 1rem 1.5rem;
1420
- background-color: #2a2a2a;
1421
- border-bottom: 1px solid #505050;
1422
-
1423
- h3 {
1424
- margin: 0;
1425
- font-size: 1.2rem;
1426
- font-weight: 600;
1427
- color: #e0e0e0;
1428
- }
1429
 
1430
- .tgn-status {
1431
- font-size: 0.85rem;
1432
- font-weight: 600;
1433
- padding: 0.4rem 0.8rem;
1434
- border-radius: 4px;
1435
- white-space: nowrap;
1436
- transition: all 0.3s ease;
1437
 
1438
- &.idle {
1439
- background-color: #505050;
1440
- color: #a0a0a0;
 
 
1441
  }
1442
 
1443
- &.valid {
1444
- background-color: rgba(74, 222, 128, 0.15);
1445
- color: #4ade80;
1446
- border: 1px solid rgba(74, 222, 128, 0.4);
1447
- }
 
 
 
 
 
 
 
1448
 
1449
- &.invalid {
1450
- background-color: rgba(239, 68, 68, 0.15);
1451
- color: #ef4444;
1452
- border: 1px solid rgba(239, 68, 68, 0.4);
 
 
 
 
 
 
 
1453
  }
1454
- }
1455
 
1456
- .btn-close {
1457
- background: none;
1458
- border: none;
1459
- color: #a0a0a0;
1460
- font-size: 1.5rem;
1461
- cursor: pointer;
1462
- padding: 0;
1463
- width: 32px;
1464
- height: 32px;
1465
- display: flex;
1466
- align-items: center;
1467
- justify-content: center;
1468
- border-radius: 4px;
1469
- transition: all 0.2s ease;
1470
 
1471
- &:hover {
1472
- background-color: #505050;
1473
- color: #e0e0e0;
 
1474
  }
1475
  }
1476
- }
1477
 
1478
- .tgn-modal-body {
1479
- flex: 1;
1480
- padding: 1.5rem;
1481
- overflow: auto;
1482
 
1483
- .tgn-textarea {
1484
- width: 100%;
1485
- height: 100%;
1486
- min-height: 300px;
1487
- background-color: #2a2a2a;
1488
- color: #e0e0e0;
1489
- border: 2px solid #505050;
1490
- border-radius: 8px;
1491
- padding: 1rem;
1492
- font-family: 'Courier New', Courier, monospace;
1493
- font-size: 0.9rem;
1494
- line-height: 1.6;
1495
- resize: vertical;
1496
- cursor: text;
1497
- transition: border-color 0.3s ease, box-shadow 0.3s ease;
1498
-
1499
- &:focus {
1500
- outline: none;
1501
- border-color: #e94560;
1502
- }
 
 
1503
 
1504
- &.valid {
1505
- border-color: #4ade80;
1506
- box-shadow: 0 0 8px rgba(74, 222, 128, 0.2);
1507
- }
1508
 
1509
- &.invalid {
1510
- border-color: #ef4444;
1511
- box-shadow: 0 0 8px rgba(239, 68, 68, 0.2);
1512
- }
1513
 
1514
- &.idle {
1515
- border-color: #505050;
 
1516
  }
1517
  }
1518
- }
1519
 
1520
- .tgn-modal-footer {
1521
- display: flex;
1522
- gap: 0.5rem;
1523
- padding: 1rem 1.5rem;
1524
- background-color: #2a2a2a;
1525
- border-top: 1px solid #505050;
1526
 
1527
- .btn {
1528
- flex: 1;
1529
- padding: 0.75rem;
1530
- border: none;
1531
- border-radius: 8px;
1532
- font-weight: 600;
1533
- cursor: pointer;
1534
- transition: all 0.3s ease;
1535
-
1536
- &:disabled {
1537
- opacity: 0.5;
1538
- cursor: not-allowed;
1539
- }
1540
 
1541
- &.btn-apply {
1542
- background-color: #4ade80;
1543
- color: #000;
 
 
 
 
 
1544
 
1545
- &:hover:not(:disabled) {
1546
- background-color: #22c55e;
1547
- transform: translateY(-2px);
1548
- box-shadow: 0 4px 12px rgba(74, 222, 128, 0.4);
 
1549
  }
1550
- }
1551
 
1552
- &.btn-copy {
1553
- background-color: #e94560;
1554
- color: #fff;
1555
 
1556
- &:hover {
1557
- background-color: #f95670;
1558
- transform: translateY(-2px);
1559
- box-shadow: 0 4px 12px rgba(233, 69, 96, 0.4);
 
1560
  }
1561
- }
1562
 
1563
- &.btn-close-modal {
1564
- background-color: #505050;
1565
- color: #e0e0e0;
1566
 
1567
- &:hover {
1568
- background-color: #606060;
 
1569
  }
1570
  }
1571
  }
1572
  }
1573
  }
1574
- }
1575
 
1576
- @keyframes pulse {
1577
- 0%,
1578
- 100% {
1579
- opacity: 0.5;
1580
- }
1581
- 50% {
1582
- opacity: 1;
 
1583
  }
1584
- }
1585
 
1586
- /* Scrollbar styling */
1587
- .controls-panel {
1588
- &::-webkit-scrollbar {
1589
- width: 8px;
1590
- }
1591
 
1592
- &::-webkit-scrollbar-track {
1593
- background: #2a2a2a;
1594
- }
1595
 
1596
- &::-webkit-scrollbar-thumb {
1597
- background: #505050;
1598
- border-radius: 4px;
1599
 
1600
- &:hover {
1601
- background: #606060;
 
1602
  }
1603
  }
1604
- }
1605
 
1606
- .routine-content {
1607
- &::-webkit-scrollbar {
1608
- width: 6px;
1609
- }
1610
 
1611
- &::-webkit-scrollbar-track {
1612
- background: #2a2a2a;
1613
- }
1614
 
1615
- &::-webkit-scrollbar-thumb {
1616
- background: #505050;
1617
- border-radius: 3px;
1618
 
1619
- &:hover {
1620
- background: #606060;
 
 
 
 
 
 
 
 
 
 
 
 
1621
  }
1622
  }
1623
- }
1624
  </style>
 
1
  <template>
2
  <div class="trigo-view">
3
  <div class="view-header">
4
+ <!-- Single Game Mode: Current Player & Move Count -->
5
+ <div v-if="gameMode === 'single'" class="view-status">
6
+ <span
7
+ class="turn-indicator"
8
+ :class="{ black: currentPlayer === 'black', white: currentPlayer === 'white' }"
9
+ >
10
  {{ currentPlayer === "black" ? "Black" : "White" }}'s Turn
11
  </span>
12
  <span class="move-count">Move: {{ moveCount }}</span>
13
  </div>
14
+
15
+ <!-- VS AI Mode: Player Info & AI Status -->
16
+ <div v-else-if="gameMode === 'vs-ai'" class="view-status ai-mode">
17
+ <div class="player-info" :class="{ 'on-turn': currentPlayer === humanPlayerColor }">
18
+ <span class="player-label">You:</span>
19
+ <span
20
+ class="stone-icon"
21
+ :class="humanPlayerColor === 'black' ? 'black' : 'white'"
22
+ ></span>
23
+ </div>
24
+ <button class="btn-swap-colors" @click="swapColors">⇄ Swap</button>
25
+ <div class="ai-status" :class="{ 'on-turn': currentPlayer === aiPlayerColor && !aiThinking }">
26
+ <span class="ai-indicator">🤖 AI:</span>
27
+ <span v-if="!aiReady" class="ai-level">Loading...</span>
28
+ <span v-else-if="aiThinking" class="ai-level ai-thinking">Thinking...</span>
29
+ <span v-else-if="aiError" class="ai-level ai-error">Error</span>
30
+ <span
31
+ v-else
32
+ class="stone-icon"
33
+ :class="aiPlayerColor === 'black' ? 'black' : 'white'"
34
+ ></span>
35
+ </div>
36
+ <span class="move-count">Move: {{ moveCount }}</span>
37
+ <span v-if="lastMoveTime > 0" class="ai-time">({{ lastMoveTime.toFixed(0) }}ms)</span>
38
+ </div>
39
+
40
+ <!-- VS People Mode: Room & Players -->
41
+ <div v-else-if="gameMode === 'vs-people'" class="view-status people-mode">
42
+ <div class="room-info">
43
+ <span class="room-label">Room:</span>
44
+ <span class="room-code">ABC123</span>
45
+ </div>
46
+ <div class="players-info">
47
+ <span class="player-name">You (Black)</span>
48
+ <span class="vs-divider">vs</span>
49
+ <span class="player-name">Waiting...</span>
50
+ </div>
51
+ <span class="connection-status connected">🟢 Connected</span>
52
+ </div>
53
+
54
+ <!-- Library Mode: Game Info & Controls -->
55
+ <div v-else-if="gameMode === 'library'" class="view-status library-mode">
56
+ <div class="game-info">
57
+ <span class="game-title">Game #12345</span>
58
+ <span class="game-date">2025-01-18</span>
59
+ </div>
60
+ <div class="library-actions">
61
+ <button class="btn-library btn-load" title="Load Game">📂 Load</button>
62
+ <button class="btn-library btn-export" title="Export TGN">📤 Export</button>
63
+ </div>
64
+ </div>
65
  </div>
66
 
67
  <div class="view-body">
 
75
  <!-- Inspect Mode Tooltip -->
76
  <div class="inspect-tooltip" v-if="inspectInfo.visible">
77
  <div class="tooltip-content">
78
+ <span class="tooltip-label"
79
+ >{{ inspectInfo.groupSize }} stone{{
80
+ inspectInfo.groupSize > 1 ? "s" : ""
81
+ }}</span
82
+ >
83
  <span class="tooltip-divider">|</span>
84
+ <span class="tooltip-label"
85
+ >Liberties: {{ inspectInfo.liberties }}</span
86
+ >
87
  </div>
88
  </div>
89
  </div>
 
92
  <!-- Game Controls & Info Panel (Right) -->
93
  <div class="controls-panel">
94
  <!-- Score Display (Captured/Territory) -->
95
+ <div
96
+ class="panel-section score-section"
97
+ :class="{ 'show-territory': showTerritoryMode }"
98
+ >
99
+ <h3 class="section-title">
100
+ {{ showTerritoryMode ? "Territory" : "Captured" }}
101
+ </h3>
102
  <div class="score-display">
103
  <button class="score-button black" :disabled="!gameStarted">
104
  <span class="color-indicator black-stone"></span>
105
+ <span class="score">{{
106
+ showTerritoryMode ? blackScore : capturedStones.black
107
+ }}</span>
108
  </button>
109
  <button class="score-button white" :disabled="!gameStarted">
110
  <span class="color-indicator white-stone"></span>
111
+ <span class="score">{{
112
+ showTerritoryMode ? whiteScore : capturedStones.white
113
+ }}</span>
114
  </button>
115
  </div>
116
+ <button
117
+ class="compute-territory"
118
+ @click="computeTerritory"
119
+ :disabled="!gameStarted"
120
+ >
121
  Compute Territory
122
  </button>
123
  </div>
 
126
  <div class="panel-section routine-section">
127
  <h3 class="section-title">
128
  Move History
129
+ <button
130
+ class="btn-view-tgn"
131
+ @click="showTGNModal = true"
132
+ title="View TGN Notation"
133
+ >
134
  TGN
135
  </button>
136
  </h3>
 
158
  @click="jumpToMove(round.blackIndex)"
159
  >
160
  <span class="stone-icon black"></span>
161
+ <span class="move-coords" v-if="!round.black.isPass">{{
162
+ formatMoveCoords(round.black)
163
+ }}</span>
164
  <span class="move-label" v-else>pass</span>
165
  </div>
166
  <div
 
170
  @click="jumpToMove(round.whiteIndex)"
171
  >
172
  <span class="stone-icon white"></span>
173
+ <span class="move-coords" v-if="!round.white.isPass">{{
174
+ formatMoveCoords(round.white)
175
+ }}</span>
176
  <span class="move-label" v-else>pass</span>
177
  </div>
178
  <div v-else class="move-cell empty"></div>
 
221
  <div class="setting-item">
222
  <label for="board-shape">
223
  Board Shape:
224
+ <span
225
+ v-if="isBoardShapeDirty"
226
+ class="dirty-indicator"
227
+ title="Board shape will change on next game"
228
+ >*</span
229
+ >
230
  </label>
231
  <select id="board-shape" v-model="selectedBoardShape">
232
  <option value="3*3*3">3×3×3</option>
 
277
  </template>
278
 
279
  <script setup lang="ts">
280
+ import { ref, onMounted, onUnmounted, watch, computed, nextTick } from "vue";
281
+ import { useRoute } from "vue-router";
282
+ import { TrigoViewport } from "@/services/trigoViewport";
283
+ import { useGameStore } from "@/stores/gameStore";
284
+ import { useTrigoAgent } from "@/composables/useTrigoAgent";
285
+ import { storeToRefs } from "pinia";
286
+ import type { BoardShape } from "../../../inc/trigo";
287
+ import { Stone, validateMove, StoneType, validateTGN } from "../../../inc/trigo";
288
+ import { TrigoGameFrontend } from "@/utils/TrigoGameFrontend";
289
+ import { encodeAb0yz } from "../../../inc/trigo/ab0yz";
290
+ import { storage, StorageKey } from "@/utils/storage";
291
+
292
+ // Get current route to determine game mode
293
+ const route = useRoute();
294
+ const gameMode = computed(() => (route.meta.mode as string) || "single");
295
+
296
+ // Helper functions for board shape parsing
297
+ const parseBoardShape = (shapeStr: string): BoardShape => {
298
+ const parts = shapeStr
299
+ .split(/[^\d]+/)
300
+ .filter(Boolean)
301
+ .map(Number);
302
+ return { x: parts[0] || 5, y: parts[1] || 5, z: parts[2] || 5 };
303
+ };
304
+
305
+ const printBoardShape = (shape: BoardShape): string => {
306
+ return `${shape.x}*${shape.y}*${shape.z}`;
307
+ };
308
+
309
+ // Format move coordinates using TGN notation
310
+ const formatMoveCoords = (move: { x: number; y: number; z: number }): string => {
311
+ const shape = boardShape.value;
312
+ return encodeAb0yz([move.x, move.y, move.z], [shape.x, shape.y, shape.z]);
313
+ };
314
+
315
+ // Use game store
316
+ const gameStore = useGameStore();
317
+ const {
318
+ currentPlayer,
319
+ moveHistory,
320
+ currentMoveIndex,
321
+ capturedStones,
322
+ gameStatus,
323
+ boardShape,
324
+ moveCount,
325
+ isGameActive,
326
+ passCount
327
+ } = storeToRefs(gameStore);
328
+
329
+
330
+ // AI Agent (for VS AI mode)
331
+ const aiAgent = useTrigoAgent();
332
+ const { isReady: aiReady, isThinking: aiThinking, error: aiError, lastMoveTime } = aiAgent;
333
+
334
+ // AI player color with session storage persistence
335
+ const loadAIColor = (): "black" | "white" => {
336
+ const stored = storage.getString(StorageKey.AI_PLAYER_COLOR);
337
+ return stored === "black" || stored === "white" ? stored : "white";
338
+ };
339
+
340
+ const aiPlayerColor = ref<"black" | "white">(loadAIColor());
341
+ const humanPlayerColor = computed(() => (aiPlayerColor.value === "white" ? "black" : "white"));
342
+
343
+ // Local state
344
+ const hoveredPosition = ref<string | null>(null);
345
+ const blackScore = ref(0);
346
+ const whiteScore = ref(0);
347
+ const isLoading = ref(true);
348
+ const selectedBoardShape = ref<string>(printBoardShape(boardShape.value));
349
+ const showTerritoryMode = ref(false);
350
+ const showTGNModal = ref(false);
351
+ const inspectInfo = ref({
352
+ visible: false,
353
+ groupSize: 0,
354
+ liberties: 0
355
+ });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
356
 
357
+ // TGN Editor state
358
+ const editableTGNContent = ref<string>("");
359
+ const tgnValidationState = ref<"idle" | "valid" | "invalid">("idle");
360
+ const tgnValidationError = ref<string>("");
361
+ let tgnValidationTimeout: ReturnType<typeof setTimeout> | null = null;
362
+
363
+ // Canvas reference and viewport
364
+ const viewportCanvas = ref<HTMLCanvasElement | null>(null);
365
+ const moveHistoryContainer = ref<HTMLDivElement | null>(null);
366
+ let viewport: TrigoViewport | null = null;
367
+ let resizeObserver: ResizeObserver | null = null;
368
+
369
+ // Computed properties
370
+ const gameStarted = computed(() => isGameActive.value);
371
+
372
+ // Group moves into rounds (pairs of black and white moves)
373
+ const moveRounds = computed(() => {
374
+ const rounds: Array<{
375
+ black: any;
376
+ white: any | null;
377
+ blackIndex: number;
378
+ whiteIndex: number | null;
379
+ }> = [];
380
+
381
+ for (let i = 0; i < moveHistory.value.length; i += 2) {
382
+ const blackMove = moveHistory.value[i];
383
+ const whiteMove = moveHistory.value[i + 1] || null;
384
+
385
+ rounds.push({
386
+ black: blackMove,
387
+ white: whiteMove,
388
+ blackIndex: i + 1, // Convert array index to "moves applied" count
389
+ whiteIndex: whiteMove ? i + 2 : null // Convert array index to "moves applied" count
390
+ });
391
+ }
392
 
393
+ return rounds;
394
+ });
 
395
 
396
+ // Check if selected board shape differs from current game board shape
397
+ const isBoardShapeDirty = computed(() => {
398
+ const selectedShape = parseBoardShape(selectedBoardShape.value);
399
+ const currentShape = boardShape.value;
400
+ return (
401
+ selectedShape.x !== currentShape.x ||
402
+ selectedShape.y !== currentShape.y ||
403
+ selectedShape.z !== currentShape.z
404
+ );
405
+ });
406
 
407
+ // Generate TGN content
408
+ const tgnContent = computed(() => {
409
+ return (
410
+ gameStore.game?.toTGN({
411
+ application: "Trigo Demo v1.0",
412
+ date: new Date().toISOString().split("T")[0].replace(/-/g, ".")
413
+ }) || ""
414
+ );
415
+ });
416
 
417
+ // TGN validation computed properties
418
+ const tgnIsValid = computed(() => tgnValidationState.value === "valid");
419
 
420
+ const tgnValidationClass = computed(() => {
421
+ if (tgnValidationState.value === "idle") return "idle";
422
+ if (tgnValidationState.value === "valid") return "valid";
423
+ return "invalid";
424
+ });
425
 
426
+ const tgnValidationMessage = computed(() => {
427
+ if (tgnValidationState.value === "idle") return "Ready to validate";
428
+ if (tgnValidationState.value === "valid") return "✓ Valid TGN";
429
+ return `✗ ${tgnValidationError.value}`;
430
+ });
431
 
432
+ // Debounced TGN validation (synchronous)
433
+ const onTGNEdit = () => {
434
+ // Clear existing timeout
435
+ if (tgnValidationTimeout) {
436
+ clearTimeout(tgnValidationTimeout);
437
  }
438
 
439
+ // Set validation state to idle while waiting for debounce
440
+ tgnValidationState.value = "idle";
 
 
 
 
 
 
441
 
442
+ // Set new debounce timeout (300ms)
443
+ tgnValidationTimeout = setTimeout(() => {
444
+ const result = validateTGN(editableTGNContent.value);
 
445
 
446
+ if (result.valid) {
447
+ tgnValidationState.value = "valid";
448
+ tgnValidationError.value = "";
449
+ } else {
450
+ tgnValidationState.value = "invalid";
451
+ tgnValidationError.value = result.error || "Invalid TGN format";
452
+ }
453
+ }, 300);
454
+ };
455
 
456
+ // Apply TGN and update game state (synchronous)
457
+ const applyTGN = () => {
458
+ if (!tgnIsValid.value) return;
 
459
 
460
+ try {
461
+ const newGame = TrigoGameFrontend.fromTGN(editableTGNContent.value);
 
 
 
 
 
462
 
463
+ // Update game store with the new TrigoGameFrontend instance
464
+ gameStore.game = newGame;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
465
 
466
+ // Save to session storage
467
+ gameStore.saveToSessionStorage();
468
 
469
+ // Update viewport with new board state
470
+ // The getters will automatically compute the new values from gameStore.game
471
+ if (viewport) {
472
+ viewport.setBoardShape(newGame.getShape());
473
+ syncViewportWithStore();
474
+ }
475
 
476
+ // Close modal
477
+ showTGNModal.value = false;
478
+ } catch (err) {
479
+ console.error("Failed to apply TGN:", err);
480
+ tgnValidationState.value = "invalid";
481
+ tgnValidationError.value = err instanceof Error ? err.message : "Failed to apply TGN";
482
+ }
483
+ };
484
 
485
 
486
+ // Generate and apply AI move
487
+ const generateAIMove = async () => {
488
+ if (!aiReady.value || aiThinking.value || !gameStarted.value) {
489
+ return;
490
+ }
491
 
492
+ try {
493
+ console.log("[TrigoView] Generating AI move...");
 
494
 
495
+ // Get AI move - pass the game instance directly from store
496
+ const aiMove = await aiAgent.generateMove(gameStore.game);
 
 
 
497
 
498
+ // Apply AI move if valid
499
+ if (aiMove) {
500
+ console.log(`[TrigoView] AI suggests move at (${aiMove.x}, ${aiMove.y}, ${aiMove.z})`);
501
 
502
+ // Make move in store
503
+ const result = gameStore.makeMove(aiMove.x, aiMove.y, aiMove.z);
 
 
504
 
505
+ if (result.success && viewport) {
506
+ // Add AI stone to viewport
507
+ const stoneColor = gameStore.opponentPlayer;
508
+ viewport.addStone(aiMove.x, aiMove.y, aiMove.z, stoneColor);
509
 
510
+ // Remove captured stones
511
+ if (result.capturedPositions && result.capturedPositions.length > 0) {
512
+ result.capturedPositions.forEach((pos: any) => {
513
+ viewport.removeStone(pos.x, pos.y, pos.z);
514
+ });
515
+ console.log(`[TrigoView] AI captured ${result.capturedPositions.length} stone(s)`);
516
+ }
517
+
518
+ console.log(`[TrigoView] AI move applied successfully (${lastMoveTime.value.toFixed(0)}ms)`);
519
+ }
520
+ } else {
521
+ console.log("[TrigoView] AI chose to pass");
522
+ // Handle AI pass if needed
523
+ }
524
+ } catch (err) {
525
+ console.error("[TrigoView] Failed to generate AI move:", err);
526
+ }
527
+ };
528
 
 
 
 
 
 
 
 
 
 
529
 
530
+ // Handle stone placement
531
+ const handleStoneClick = async (x: number, y: number, z: number) => {
532
+ if (!gameStarted.value) return;
533
 
534
+ // Make move in store
535
+ const result = gameStore.makeMove(x, y, z);
 
 
 
536
 
537
+ if (result.success && viewport) {
538
+ // Add stone to viewport (store already switched player, so use opponent)
539
+ const stoneColor = gameStore.opponentPlayer;
540
+ viewport.addStone(x, y, z, stoneColor);
541
+
542
+ // Remove captured stones from viewport
543
+ if (result.capturedPositions && result.capturedPositions.length > 0) {
544
+ result.capturedPositions.forEach((pos) => {
545
+ viewport.removeStone(pos.x, pos.y, pos.z);
546
+ });
547
+ console.log(`Captured ${result.capturedPositions.length} stone(s)`);
548
+ }
549
 
550
+ // Hide domain visualization and switch back to captured display after move
551
+ viewport.hideDomainCubes();
552
+ showTerritoryMode.value = false;
553
 
554
+ // VS AI mode: Generate AI move after player move if it's AI's turn
555
+ if (gameMode.value === "vs-ai" && aiReady.value && currentPlayer.value === aiPlayerColor.value) {
556
+ setTimeout(() => {
557
+ generateAIMove();
558
+ }, 100);
559
+ }
560
+ }
561
+ };
562
 
563
+ // Handle position hover
564
+ const handlePositionHover = (x: number | null, y: number | null, z: number | null) => {
565
+ if (x !== null && y !== null && z !== null) {
566
+ hoveredPosition.value = `(${x}, ${y}, ${z})`;
567
+ } else {
568
+ hoveredPosition.value = null;
569
+ }
570
+ };
571
+
572
+ // Check if a position is droppable (validates with game rules)
573
+ const isPositionDroppable = (x: number, y: number, z: number): boolean => {
574
+ const pos = { x, y, z };
575
+ const playerColor = currentPlayer.value === "black" ? StoneType.BLACK : StoneType.WHITE;
576
+
577
+ const validation = validateMove(
578
+ pos,
579
+ playerColor,
580
+ gameStore.board,
581
+ boardShape.value,
582
+ gameStore.lastCapturedPositions
583
+ );
584
+
585
+ return validation.valid;
586
+ };
587
+
588
+ // Handle inspect mode callback
589
+ const handleInspectGroup = (groupSize: number, liberties: number) => {
590
+ if (groupSize > 0) {
591
+ inspectInfo.value = {
592
+ visible: true,
593
+ groupSize,
594
+ liberties
595
+ };
596
+ } else {
597
+ inspectInfo.value = {
598
+ visible: false,
599
+ groupSize: 0,
600
+ liberties: 0
601
+ };
602
+ }
603
+ };
604
 
 
 
 
605
 
606
+ // Swap AI player color
607
+ const swapColors = () => {
608
+ // Swap the color
609
+ aiPlayerColor.value = aiPlayerColor.value === "white" ? "black" : "white";
610
 
611
+ // Save to storage
612
+ storage.setString(StorageKey.AI_PLAYER_COLOR, aiPlayerColor.value);
 
 
 
 
 
 
613
 
614
+ console.log(`[TrigoView] AI color swapped to: ${aiPlayerColor.value}`);
 
615
 
616
+ // If game is started and it's now AI's turn, trigger AI move immediately
617
+ if (gameStarted.value && aiReady.value && currentPlayer.value === aiPlayerColor.value) {
618
+ setTimeout(() => {
619
+ generateAIMove();
620
+ }, 100);
 
621
  }
622
+ };
623
 
 
 
 
 
624
 
625
+ // Game control methods
626
+ const newGame = () => {
627
+ // Parse selected board shape
628
+ const shape = parseBoardShape(selectedBoardShape.value);
629
 
630
+ // Initialize game in store
631
+ gameStore.initializeGame(shape);
632
+ gameStore.startGame();
633
 
634
+ // Update viewport with new board shape
 
 
635
  if (viewport) {
636
+ viewport.setBoardShape(shape);
637
+ viewport.clearBoard();
638
+ viewport.setGameActive(true);
639
+
640
+ // Exit territory mode if active
641
  viewport.hideDomainCubes();
642
  }
643
+
644
+ // Reset scores and territory mode
645
  blackScore.value = 0;
646
  whiteScore.value = 0;
647
+ showTerritoryMode.value = false;
 
648
 
649
+ console.log(`Starting new game with board shape ${shape.x}×${shape.y}×${shape.z}`);
 
 
 
650
 
651
+ // VS AI mode: If AI plays black, generate first move
652
+ if (gameMode.value === "vs-ai" && aiPlayerColor.value === "black" && aiReady.value) {
653
+ setTimeout(() => {
654
+ generateAIMove();
655
+ }, 100);
656
+ }
657
+ };
658
 
659
+ const pass = () => {
660
+ const previousPlayer = currentPlayer.value;
661
+ const success = gameStore.pass();
 
662
 
663
+ if (success) {
664
+ // Check if game ended due to double pass
665
+ //if (gameStore.gameResult?.reason === "double-pass") {
666
+ // showGameResult();
667
+ //} else {
668
+ console.log(`${previousPlayer} passed (Pass count: ${gameStore.passCount})`);
669
+ //}
670
+ }
671
+ };
672
 
673
+ const resign = () => {
674
+ // Confirm resignation
675
+ const confirmed = confirm(
676
+ `Are you sure ${currentPlayer.value} wants to resign?\n\nThis will end the game immediately.`
677
+ );
678
 
679
+ if (!confirmed) return;
 
 
 
 
680
 
681
+ //const success = gameStore.resign();
 
682
 
683
+ //if (success) {
684
+ // showGameResult();
685
+ //}
686
+ };
687
+
688
+ const showGameResult = () => {
689
+ const result = gameStore.gameResult;
690
+ if (!result) return;
691
+
692
+ // Don't set game as inactive - allow continued analysis
693
+ // if (viewport) {
694
+ // viewport.setGameActive(false);
695
+ // }
696
+
697
+ let message = "";
698
+ if (result.reason === "resignation") {
699
+ message = `${result.winner === "black" ? "Black" : "White"} wins by resignation!\n\nGame continues for analysis.`;
700
+ } else if (result.reason === "double-pass") {
701
+ // Calculate final scores for double pass
702
+ const territory = gameStore.computeTerritory();
703
+ const blackTotal = territory.black + capturedStones.value.black;
704
+ const whiteTotal = territory.white + capturedStones.value.white;
705
+
706
+ blackScore.value = blackTotal;
707
+ whiteScore.value = whiteTotal;
708
+
709
+ if (blackTotal > whiteTotal) {
710
+ message = `Black wins by ${blackTotal - whiteTotal} points!\n\nBlack: ${blackTotal} points\nWhite: ${whiteTotal} points\n\nGame continues for analysis.`;
711
+ } else if (whiteTotal > blackTotal) {
712
+ message = `White wins by ${whiteTotal - blackTotal} points!\n\nWhite: ${whiteTotal} points\nBlack: ${blackTotal} points\n\nGame continues for analysis.`;
713
+ } else {
714
+ message = `Game is a draw!\n\nBoth players: ${blackTotal} points\n\nGame continues for analysis.`;
715
+ }
716
+ }
717
 
718
+ setTimeout(() => {
719
+ alert(message);
720
+ }, 100);
721
+ };
 
722
 
723
+ const computeTerritory = () => {
724
+ if (!gameStarted.value) return;
725
 
726
+ // Toggle territory mode
727
+ if (showTerritoryMode.value) {
728
+ // Exit territory mode
729
+ if (viewport) {
730
+ viewport.hideDomainCubes();
731
+ }
732
+ showTerritoryMode.value = false;
733
+ // Reset scores to captured stones count
734
+ blackScore.value = 0;
735
+ whiteScore.value = 0;
736
+ return;
737
+ }
738
 
739
+ // Enter territory mode - Use store's territory calculation
740
+ const territory = gameStore.computeTerritory();
741
+ blackScore.value = territory.black + capturedStones.value.black;
742
+ whiteScore.value = territory.white + capturedStones.value.white;
743
 
744
+ // Switch to territory display mode
745
+ showTerritoryMode.value = true;
 
746
 
747
+ // Convert territory arrays to Sets of position keys for viewport
748
+ if (viewport) {
749
+ const blackDomain = new Set<string>();
750
+ const whiteDomain = new Set<string>();
751
+
752
+ // Use the calculated territory positions from the territory result
753
+ territory.blackTerritory.forEach((pos) => {
754
+ const key = `${pos.x},${pos.y},${pos.z}`;
755
+ blackDomain.add(key);
756
+ });
757
+
758
+ territory.whiteTerritory.forEach((pos) => {
759
+ const key = `${pos.x},${pos.y},${pos.z}`;
760
+ whiteDomain.add(key);
761
+ });
762
+
763
+ // Set domain data and show both domains
764
+ viewport.setDomainData(blackDomain, whiteDomain);
765
+ viewport.setBlackDomainVisible(true);
766
+ viewport.setWhiteDomainVisible(true);
 
 
 
 
 
 
 
 
 
 
 
767
  }
 
768
 
769
+ console.log("Territory computed:", territory);
770
+ };
 
 
 
 
 
 
 
 
 
771
 
772
+ const previousMove = () => {
773
+ const success = gameStore.undoMove();
774
+
775
+ if (success && viewport) {
776
+ // Rebuild viewport from current board state
777
+ syncViewportWithStore();
 
 
 
 
 
 
 
 
 
 
 
 
778
  }
779
+ };
 
 
 
 
 
 
 
 
 
 
 
780
 
781
+ const nextMove = () => {
782
+ const success = gameStore.redoMove();
783
 
784
+ if (success && viewport) {
785
+ // Rebuild viewport from current board state
786
+ syncViewportWithStore();
787
+ }
788
+ };
789
 
790
+ const jumpToMove = (index: number) => {
791
+ const success = gameStore.jumpToMove(index);
792
 
793
+ if (success && viewport) {
794
+ // Rebuild viewport from current board state
795
+ syncViewportWithStore();
 
796
 
797
+ // Exit territory mode when navigating move history
798
+ viewport.hideDomainCubes();
799
+ showTerritoryMode.value = false;
800
+ // Reset scores to 0 when exiting territory mode
801
+ blackScore.value = 0;
802
+ whiteScore.value = 0;
803
+ }
804
+ };
805
 
806
+ // Sync viewport with store's board state
807
+ const syncViewportWithStore = () => {
808
+ if (!viewport) return;
 
 
 
 
 
 
809
 
810
+ // Clear viewport
811
+ viewport.clearBoard();
812
 
813
+ // Read the actual board state from the store (which has captures applied)
814
+ const board = gameStore.board;
815
+ const shape = boardShape.value;
816
+
817
+ // Add all stones that exist on the board
818
+ for (let x = 0; x < shape.x; x++) {
819
+ for (let y = 0; y < shape.y; y++) {
820
+ for (let z = 0; z < shape.z; z++) {
821
+ const stone = board[x][y][z];
822
+ if (stone === Stone.Black) {
823
+ viewport.addStone(x, y, z, "black");
824
+ } else if (stone === Stone.White) {
825
+ viewport.addStone(x, y, z, "white");
826
+ }
827
+ }
828
+ }
829
  }
 
830
 
831
+ // Set the last placed stone highlight based on current move index
832
+ // currentMoveIndex represents the number of moves applied (not array index)
833
+ // So the last applied move is at array index (currentMoveIndex - 1)
834
+ if (currentMoveIndex.value > 0 && currentMoveIndex.value <= moveHistory.value.length) {
835
+ const lastMove = moveHistory.value[currentMoveIndex.value - 1];
836
+ viewport.setLastPlacedStone(lastMove.x, lastMove.y, lastMove.z);
837
+ } else {
838
+ // No moves applied or at START position
839
+ viewport.setLastPlacedStone(null, null, null);
840
+ }
841
+ };
842
+
843
+ // Watch for current player changes to update viewport preview
844
+ watch(currentPlayer, (newPlayer) => {
845
  if (viewport) {
846
+ viewport.setCurrentPlayer(newPlayer);
847
  }
848
+ });
849
 
850
+ // Watch currentMoveIndex to auto-scroll move history to keep current move visible
851
+ watch(currentMoveIndex, () => {
852
+ // Use nextTick to ensure DOM is updated before scrolling
853
+ nextTick(() => {
854
+ if (moveHistoryContainer.value) {
855
+ // Find the active move element
856
+ const activeElement = moveHistoryContainer.value.querySelector(".active");
857
+ if (activeElement) {
858
+ // Scroll the active element into view smoothly
859
+ activeElement.scrollIntoView({ behavior: "smooth", block: "nearest" });
860
+ }
861
+ }
862
+ });
863
+ });
864
 
865
+ // Watch TGN modal to populate editable content when opened
866
+ watch(showTGNModal, (isVisible) => {
867
+ if (isVisible) {
868
+ // Populate with current game's TGN when modal opens
869
+ editableTGNContent.value = tgnContent.value;
870
+ tgnValidationState.value = "valid"; // Current TGN is always valid
871
+ tgnValidationError.value = "";
872
+ }
873
+ });
874
 
875
+ // Lifecycle hooks
876
+ onMounted(() => {
877
+ console.log("TrigoDemo component mounted");
878
 
879
+ // Try to restore game state from session storage
880
+ const restoredFromStorage = gameStore.loadFromSessionStorage();
881
 
882
+ // If not restored from storage, initialize new game
883
+ if (!restoredFromStorage) {
884
+ // Parse initial board shape
885
+ const shape = parseBoardShape(selectedBoardShape.value);
886
+
887
+ // Initialize game store
888
+ gameStore.initializeGame(shape);
889
+ } else {
890
+ // Update selected board shape to match restored state
891
+ selectedBoardShape.value = printBoardShape(boardShape.value);
892
+ console.log("Restored game state from session storage");
893
+ }
894
 
895
+ // Initialize Three.js viewport with current board shape
896
+ if (viewportCanvas.value) {
897
+ viewport = new TrigoViewport(viewportCanvas.value, boardShape.value, {
898
+ onStoneClick: handleStoneClick,
899
+ onPositionHover: handlePositionHover,
900
+ isPositionDroppable: isPositionDroppable,
901
+ onInspectGroup: handleInspectGroup
902
+ });
903
+ console.log("TrigoViewport initialized");
904
 
905
+ // Hide loading overlay after viewport is initialized
906
+ isLoading.value = false;
 
 
 
 
907
 
908
+ // If game was restored, sync viewport with restored board state
909
+ if (restoredFromStorage) {
910
+ syncViewportWithStore();
911
+ viewport.setGameActive(isGameActive.value);
912
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
913
 
914
+ // Setup ResizeObserver to handle canvas resizing when sidebar expands/collapses
915
+ resizeObserver = new ResizeObserver((entries) => {
916
+ for (const entry of entries) {
917
+ if (entry.target === viewportCanvas.value && viewport) {
918
+ // Trigger viewport resize
919
+ const rect = viewportCanvas.value.getBoundingClientRect();
920
+ console.log(`Canvas resized: ${rect.width}x${rect.height}`);
921
+
922
+ // Call the viewport's resize handler directly
923
+ // The onWindowResize method updates camera aspect and renderer size
924
+ (viewport as any).onWindowResize();
925
+ }
926
+ }
927
+ });
928
 
929
+ // Observe the canvas element for size changes
930
+ resizeObserver.observe(viewportCanvas.value);
931
+ console.log("ResizeObserver attached to canvas");
932
+ }
 
933
 
934
+ // Start game automatically if not restored or was playing
935
+ if (!restoredFromStorage || gameStore.gameStatus === "idle") {
936
+ gameStore.startGame();
937
+ if (viewport) {
938
+ viewport.setGameActive(true);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
939
  }
940
  }
 
 
 
 
 
 
941
 
942
+ // Initialize AI agent in VS AI mode
943
+ if (gameMode.value === "vs-ai") {
944
+ console.log("[TrigoView] Initializing AI agent for VS AI mode...");
945
+ aiAgent.initialize().catch((err) => {
946
+ console.error("[TrigoView] Failed to initialize AI agent:", err);
947
+ });
948
+ }
 
 
 
 
 
 
 
 
 
 
949
 
950
+ // Add keyboard shortcuts
951
+ window.addEventListener("keydown", handleKeyPress);
952
+ });
 
953
 
954
+ onUnmounted(() => {
955
+ console.log("TrigoDemo component unmounted");
 
 
 
956
 
957
+ // Remove keyboard shortcuts
958
+ window.removeEventListener("keydown", handleKeyPress);
 
 
 
 
959
 
960
+ // Disconnect ResizeObserver
961
+ if (resizeObserver) {
962
+ resizeObserver.disconnect();
963
+ resizeObserver = null;
964
+ console.log("ResizeObserver disconnected");
965
  }
966
 
967
+ // Cleanup Three.js resources
968
+ if (viewport) {
969
+ viewport.destroy();
970
+ viewport = null;
971
+ }
972
+ });
973
 
974
+ // Keyboard shortcuts handler
975
+ const handleKeyPress = (event: KeyboardEvent) => {
976
+ // Ignore if typing in an input field
977
+ if (
978
+ event.target instanceof HTMLInputElement ||
979
+ event.target instanceof HTMLTextAreaElement
980
+ ) {
981
+ return;
982
+ }
983
 
984
+ switch (event.key.toLowerCase()) {
985
+ case "p": // Pass
986
+ if (gameStarted.value) {
987
+ pass();
988
  }
989
+ break;
990
+ case "n": // New game
991
+ newGame();
992
+ break;
993
+ case "r": // Resign
994
+ if (gameStarted.value) {
995
+ resign();
996
+ }
997
+ break;
998
+ case "arrowleft": // Previous move
999
+ previousMove();
1000
+ event.preventDefault();
1001
+ break;
1002
+ case "arrowright": // Next move
1003
+ nextMove();
1004
+ event.preventDefault();
1005
+ break;
1006
+ case "t": // Compute territory
1007
+ if (gameStarted.value) {
1008
+ computeTerritory();
1009
+ }
1010
+ break;
1011
+ }
1012
+ };
1013
+
1014
+ // TGN Modal Methods
1015
+ const selectAllTGN = (event: Event) => {
1016
+ const textarea = event.target as HTMLTextAreaElement;
1017
+ textarea.select();
1018
+ };
1019
+
1020
+ const copyTGN = async () => {
1021
+ try {
1022
+ // Try modern clipboard API first
1023
+ if (navigator.clipboard && navigator.clipboard.writeText) {
1024
+ await navigator.clipboard.writeText(tgnContent.value);
1025
+ alert("TGN copied to clipboard!");
1026
+ } else {
1027
+ // Fallback for older browsers or non-secure contexts
1028
+ const textarea = document.createElement("textarea");
1029
+ textarea.value = tgnContent.value;
1030
+ textarea.style.position = "fixed";
1031
+ textarea.style.opacity = "0";
1032
+ document.body.appendChild(textarea);
1033
+ textarea.select();
1034
+
1035
+ try {
1036
+ document.execCommand("copy");
1037
+ alert("TGN copied to clipboard!");
1038
+ } catch (fallbackErr) {
1039
+ console.error("Fallback copy failed:", fallbackErr);
1040
+ alert("Failed to copy TGN. Please select and copy manually.");
1041
+ } finally {
1042
+ document.body.removeChild(textarea);
1043
  }
1044
  }
1045
+ } catch (err) {
1046
+ console.error("Failed to copy TGN:", err);
1047
+ alert("Failed to copy TGN. Please select and copy manually.");
 
 
1048
  }
1049
+ };
1050
+ </script>
1051
 
1052
+ <style lang="scss" scoped>
1053
+ .trigo-view {
1054
  display: flex;
1055
+ flex-direction: column;
1056
+ height: 100%;
1057
+ background-color: #404040;
1058
+ color: #e0e0e0;
1059
  overflow: hidden;
1060
 
1061
+ .view-header {
 
1062
  display: flex;
 
1063
  justify-content: center;
1064
+ align-items: center;
1065
+ padding: 1rem 2rem;
1066
+ background: linear-gradient(135deg, #505050 0%, #454545 100%);
1067
+ border-bottom: 2px solid #606060;
1068
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1069
 
1070
+ .view-status {
1071
+ display: flex;
1072
+ gap: 2rem;
1073
+ align-items: center;
 
 
1074
 
1075
+ .turn-indicator {
1076
+ padding: 0.5rem 1rem;
1077
+ border-radius: 8px;
1078
+ font-weight: 600;
1079
+ transition: all 0.3s ease;
 
 
 
 
 
 
 
 
 
 
 
 
 
1080
 
1081
+ &.black {
1082
+ background-color: #2c2c2c;
1083
+ color: #fff;
1084
+ box-shadow: 0 0 10px rgba(255, 255, 255, 0.3);
1085
  }
1086
 
1087
+ &.white {
1088
+ background-color: #f0f0f0;
1089
+ color: #000;
1090
+ box-shadow: 0 0 10px rgba(240, 240, 240, 0.3);
1091
  }
1092
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1093
 
1094
+ .move-count {
1095
+ font-size: 1rem;
1096
+ color: #a0a0a0;
1097
  }
 
1098
 
1099
+ // VS AI Mode Styles
1100
+ &.ai-mode {
1101
+ .player-info,
1102
+ .ai-status {
 
 
 
 
 
1103
  display: flex;
1104
  align-items: center;
 
1105
  gap: 0.5rem;
1106
+ padding: 0.5rem 1rem;
1107
+ background-color: #3a3a3a;
1108
  border-radius: 8px;
1109
+ border: 2px solid transparent;
 
1110
  transition: all 0.3s ease;
1111
 
1112
+ &.on-turn {
1113
+ background-color: #4a4a4a;
1114
+ border-color: #60a5fa;
1115
+ box-shadow: 0 0 12px rgba(96, 165, 250, 0.4);
1116
+ }
1117
+ }
1118
+
1119
+ .player-label,
1120
+ .ai-indicator {
1121
+ color: #a0a0a0;
1122
+ font-weight: 500;
1123
+ }
1124
+
1125
+ .stone-icon {
1126
+ width: 20px;
1127
+ height: 20px;
1128
+ border-radius: 50%;
1129
+ flex-shrink: 0;
1130
+
1131
  &.black {
1132
+ background-color: #2c2c2c;
1133
+ border: 2px solid #fff;
 
 
1134
  }
1135
 
1136
  &.white {
1137
+ background-color: #f0f0f0;
1138
+ border: 2px solid #000;
 
 
1139
  }
1140
+ }
1141
 
1142
+ .ai-level {
1143
+ color: #60a5fa;
1144
+ font-weight: 600;
 
 
1145
 
1146
+ &.ai-thinking {
1147
+ color: #fbbf24;
1148
+ animation: pulse 1.5s ease-in-out infinite;
 
1149
  }
1150
 
1151
+ &.ai-error {
1152
+ color: #ef4444;
1153
  }
1154
  }
 
1155
 
1156
+ .ai-time {
1157
+ color: #9ca3af;
1158
+ font-size: 0.875rem;
1159
+ font-style: italic;
1160
+ }
 
 
 
 
 
1161
 
1162
+ .btn-swap-colors {
1163
+ padding: 0.4rem 0.8rem;
1164
+ background-color: #4b5563;
1165
+ color: #e5e7eb;
1166
+ border: none;
1167
+ border-radius: 6px;
1168
+ cursor: pointer;
1169
+ font-size: 0.875rem;
1170
+ font-weight: 600;
1171
+ transition: all 0.2s ease;
1172
+
1173
+ &:hover {
1174
+ background-color: #6b7280;
1175
+ transform: translateY(-1px);
1176
+ }
1177
+
1178
+ &:active {
1179
+ transform: translateY(0);
1180
+ }
1181
  }
1182
+ }
1183
 
1184
+ // VS People Mode Styles
1185
+ &.people-mode {
1186
+ .room-info {
1187
+ display: flex;
1188
+ align-items: center;
1189
+ gap: 0.5rem;
1190
+ padding: 0.5rem 1rem;
1191
+ background-color: #3a3a3a;
1192
+ border-radius: 8px;
1193
+ }
1194
+
1195
+ .room-label {
1196
+ color: #a0a0a0;
1197
+ font-weight: 500;
1198
+ }
1199
+
1200
+ .room-code {
1201
+ color: #e94560;
1202
+ font-weight: 700;
1203
+ font-family: monospace;
1204
+ font-size: 1.1rem;
1205
+ }
1206
+
1207
+ .players-info {
1208
+ display: flex;
1209
+ align-items: center;
1210
+ gap: 0.75rem;
1211
+ padding: 0.5rem 1rem;
1212
+ background-color: #3a3a3a;
1213
+ border-radius: 8px;
1214
+ }
1215
+
1216
+ .player-name {
1217
+ color: #e0e0e0;
1218
+ font-weight: 600;
1219
+ }
1220
+
1221
+ .vs-divider {
1222
+ color: #606060;
1223
+ font-weight: 500;
1224
+ }
1225
+
1226
+ .connection-status {
1227
+ padding: 0.5rem 1rem;
1228
+ border-radius: 8px;
1229
+ font-weight: 600;
1230
+ font-size: 0.9rem;
1231
+
1232
+ &.connected {
1233
+ background-color: rgba(74, 222, 128, 0.15);
1234
+ color: #4ade80;
1235
+ }
1236
+
1237
+ &.disconnected {
1238
+ background-color: rgba(239, 68, 68, 0.15);
1239
+ color: #ef4444;
1240
+ }
1241
  }
1242
  }
1243
 
1244
+ // Library Mode Styles
1245
+ &.library-mode {
1246
+ .game-info {
1247
+ display: flex;
1248
+ align-items: center;
1249
+ gap: 1rem;
1250
+ padding: 0.5rem 1rem;
1251
  background-color: #3a3a3a;
1252
+ border-radius: 8px;
1253
+ }
1254
+
1255
+ .game-title {
1256
+ color: #e0e0e0;
1257
+ font-weight: 600;
1258
+ font-size: 1.1rem;
1259
+ }
1260
+
1261
+ .game-date {
1262
+ color: #a0a0a0;
1263
+ font-weight: 500;
1264
+ font-family: monospace;
1265
+ }
1266
+
1267
+ .library-actions {
1268
+ display: flex;
1269
+ gap: 0.5rem;
1270
+ }
1271
+
1272
+ .btn-library {
1273
+ padding: 0.5rem 1rem;
1274
+ border: none;
1275
+ border-radius: 8px;
1276
+ font-weight: 600;
1277
+ font-size: 0.9rem;
1278
+ cursor: pointer;
1279
+ transition: all 0.3s ease;
1280
+
1281
+ &.btn-load {
1282
+ background-color: #2d4059;
1283
+ color: #e0e0e0;
1284
+
1285
+ &:hover {
1286
+ background-color: #3d5069;
1287
+ transform: translateY(-2px);
1288
+ box-shadow: 0 4px 12px rgba(45, 64, 89, 0.4);
1289
+ }
1290
+ }
1291
+
1292
+ &.btn-export {
1293
+ background-color: #e94560;
1294
+ color: #fff;
1295
+
1296
+ &:hover {
1297
+ background-color: #f95670;
1298
+ transform: translateY(-2px);
1299
+ box-shadow: 0 4px 12px rgba(233, 69, 96, 0.4);
1300
+ }
1301
+ }
1302
  }
1303
  }
1304
  }
1305
+ }
1306
 
1307
+ .view-body {
1308
+ display: flex;
1309
+ flex: 1;
1310
+ overflow: hidden;
1311
 
1312
+ .board-container {
 
1313
  flex: 1;
1314
  display: flex;
1315
+ align-items: center;
1316
+ justify-content: center;
1317
+ background-color: #484848;
1318
+ padding: 1rem;
1319
+ position: relative;
1320
 
1321
+ .viewport-wrapper {
1322
+ width: 100%;
1323
+ height: 100%;
1324
+ position: relative;
1325
  border-radius: 8px;
1326
+ overflow: hidden;
1327
+ box-shadow: 0 4px 16px rgba(0, 0, 0, 0.5);
1328
 
1329
+ .viewport-canvas {
1330
+ width: 100%;
1331
+ height: 100%;
1332
+ display: block;
1333
+ background-color: #50505a;
1334
+ }
1335
 
1336
+ .viewport-overlay {
1337
+ position: absolute;
1338
+ top: 0;
1339
+ left: 0;
1340
+ width: 100%;
1341
+ height: 100%;
1342
+ display: flex;
1343
+ align-items: center;
1344
+ justify-content: center;
1345
+ pointer-events: none;
1346
 
1347
+ .loading-text {
1348
+ color: rgba(255, 255, 255, 0.5);
1349
+ font-size: 1.2rem;
1350
+ animation: pulse 2s ease-in-out infinite;
1351
+ }
1352
+ }
1353
 
1354
+ .inspect-tooltip {
1355
+ position: absolute;
1356
+ top: 1rem;
1357
+ left: 1rem;
1358
+ background-color: rgba(255, 241, 176, 0.95);
1359
+ color: #111;
1360
+ padding: 0.5rem 1rem;
1361
+ border-radius: 8px;
1362
+ font-size: 0.9rem;
1363
+ font-weight: 600;
1364
+ pointer-events: none;
1365
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
1366
+ z-index: 100;
1367
 
1368
+ .tooltip-content {
1369
+ display: flex;
1370
+ align-items: center;
1371
+ gap: 0.5rem;
1372
 
1373
+ .tooltip-label {
1374
+ white-space: nowrap;
 
 
 
 
1375
  }
1376
 
1377
+ .tooltip-divider {
1378
+ color: #888;
 
 
 
1379
  }
1380
+ }
1381
+ }
1382
+ }
1383
+ }
1384
 
1385
+ .controls-panel {
1386
+ width: 320px;
1387
+ background-color: #3a3a3a;
1388
+ border-left: 2px solid #606060;
1389
+ display: flex;
1390
+ flex-direction: column;
1391
+ overflow: hidden;
1392
+ box-shadow: -4px 0 16px rgba(0, 0, 0, 0.3);
 
1393
 
1394
+ .panel-section {
1395
+ padding: 0.8rem;
1396
+ border-bottom: 1px solid #505050;
1397
+ position: relative;
1398
 
1399
+ .section-title {
1400
+ font-size: 0.7rem;
1401
+ font-weight: 600;
1402
+ color: #f0bcc5;
1403
+ text-transform: uppercase;
1404
+ letter-spacing: 1px;
1405
+ position: absolute;
1406
+ top: 0;
1407
+ left: 0.8rem;
1408
+ opacity: 0;
1409
+ transition: opacity 0.3s ease-in;
1410
+ }
1411
 
1412
+ &:hover .section-title {
1413
+ opacity: 0.6;
1414
+ }
1415
+ }
1416
 
1417
+ .score-section {
1418
+ // Default style (captured mode) - lighter background
1419
+ .score-display {
1420
+ display: flex;
1421
+ gap: 0.5rem;
1422
+ margin-bottom: 1rem;
1423
 
1424
+ .score-button {
1425
+ flex: 1;
1426
+ display: flex;
1427
+ align-items: center;
1428
+ justify-content: center;
1429
+ gap: 0.5rem;
1430
+ padding: 1rem;
1431
+ border: 2px solid #505050;
1432
+ border-radius: 8px;
1433
+ background-color: #484848;
1434
+ cursor: default;
1435
+ transition: all 0.3s ease;
1436
 
1437
+ &.black {
1438
+ .color-indicator {
1439
+ background-color: #2c2c2c;
1440
+ border: 2px solid #fff;
1441
  }
1442
+ }
1443
 
1444
+ &.white {
1445
+ .color-indicator {
1446
+ background-color: #f0f0f0;
1447
+ border: 2px solid #000;
1448
  }
1449
+ }
1450
 
1451
+ .color-indicator {
1452
+ width: 24px;
1453
+ height: 24px;
1454
+ border-radius: 50%;
 
 
1455
  }
 
 
 
 
1456
 
1457
+ .score {
1458
+ font-size: 1.5rem;
1459
+ font-weight: 700;
1460
+ color: #e0e0e0;
1461
+ }
1462
 
1463
+ &:disabled {
1464
+ opacity: 0.5;
1465
+ }
1466
+ }
1467
  }
1468
 
1469
+ .compute-territory {
1470
+ width: 100%;
1471
+ padding: 0.75rem;
1472
+ background-color: #505050;
1473
+ color: #e0e0e0;
1474
  border: none;
1475
  border-radius: 8px;
1476
  font-weight: 600;
1477
  cursor: pointer;
1478
  transition: all 0.3s ease;
1479
+
1480
+ &:hover:not(:disabled) {
1481
+ background-color: #606060;
1482
+ }
1483
 
1484
  &:disabled {
1485
  opacity: 0.5;
1486
  cursor: not-allowed;
1487
  }
1488
+ }
1489
 
1490
+ // Territory mode - darker background with glow
1491
+ &.show-territory {
1492
+ .score-display .score-button {
1493
+ background-color: #3a3a3a;
1494
+ border-color: #606060;
1495
+ box-shadow: 0 0 12px rgba(233, 69, 96, 0.3);
 
1496
  }
1497
+ }
1498
+ }
1499
 
1500
+ .routine-section {
1501
+ flex: 1;
1502
+ display: flex;
1503
+ flex-direction: column;
1504
+ min-height: 0;
1505
 
1506
+ .routine-content {
1507
+ flex: 1;
1508
+ overflow-y: auto;
1509
+ background-color: #2a2a2a;
1510
+ border-radius: 8px;
1511
+ padding: 0.5rem;
1512
 
1513
+ .move-list {
1514
+ padding: 0;
1515
+ margin: 0;
1516
+
1517
+ .move-row {
1518
+ display: grid;
1519
+ grid-template-columns: 30px 1fr 1fr;
1520
+ gap: 0.25rem;
1521
+ margin-bottom: 0.25rem;
1522
+ align-items: center;
1523
+
1524
+ &.start-row {
1525
+ grid-template-columns: 30px 1fr;
1526
+ padding: 0.5rem;
1527
+ border-radius: 4px;
1528
+ cursor: pointer;
1529
+ transition: all 0.2s ease;
1530
+
1531
+ &:hover {
1532
+ background-color: #484848;
1533
+ }
1534
+
1535
+ &.active {
1536
+ background-color: #505050;
1537
+ border-left: 3px solid #e94560;
1538
+ }
1539
+
1540
+ .open-label {
1541
+ font-weight: 700;
1542
+ color: #e94560;
1543
+ text-transform: uppercase;
1544
+ letter-spacing: 1px;
1545
+ }
1546
+ }
1547
 
1548
+ .round-number {
1549
+ color: #808080;
1550
+ font-size: 0.9em;
1551
+ text-align: right;
1552
+ padding-right: 0.25rem;
1553
+ }
1554
+
1555
+ .move-cell {
1556
+ padding: 0.5rem;
1557
+ border-radius: 4px;
1558
+ cursor: pointer;
1559
+ transition: all 0.2s ease;
1560
+ display: flex;
1561
+ align-items: center;
1562
+ gap: 0.5rem;
1563
+ background-color: #1a1a1a;
1564
+
1565
+ &:hover:not(.empty) {
1566
+ background-color: #484848;
1567
+ }
1568
+
1569
+ &.active {
1570
+ background-color: #505050;
1571
+ border-left: 3px solid #e94560;
1572
+ }
1573
+
1574
+ &.empty {
1575
+ background-color: transparent;
1576
+ cursor: default;
1577
+ }
1578
+
1579
+ .stone-icon {
1580
+ width: 16px;
1581
+ height: 16px;
1582
+ border-radius: 50%;
1583
+ flex-shrink: 0;
1584
+
1585
+ &.black {
1586
+ background-color: #2c2c2c;
1587
+ border: 2px solid #fff;
1588
+ }
1589
+
1590
+ &.white {
1591
+ background-color: #f0f0f0;
1592
+ border: 2px solid #000;
1593
+ }
1594
+ }
1595
+
1596
+ .move-coords {
1597
+ color: #a0a0a0;
1598
+ font-family: monospace;
1599
+ font-size: 0.9em;
1600
+ }
1601
+
1602
+ .move-label {
1603
+ color: #60a5fa;
1604
+ font-weight: 600;
1605
+ font-size: 0.85em;
1606
+ text-transform: lowercase;
1607
+ }
1608
+ }
1609
  }
1610
  }
1611
  }
1612
  }
 
 
 
 
 
 
 
1613
 
1614
+ .controls-section {
1615
+ .control-buttons {
1616
  display: flex;
1617
+ flex-direction: column;
1618
+ gap: 1rem;
1619
 
1620
+ .play-controls,
1621
+ .history-controls {
 
1622
  display: flex;
1623
+ gap: 0.5rem;
 
 
 
 
 
 
 
 
1624
  }
1625
 
1626
+ .btn {
1627
+ padding: 0.75rem 1.5rem;
1628
+ border: none;
1629
  border-radius: 8px;
 
 
 
1630
  font-weight: 600;
1631
+ cursor: pointer;
1632
+ transition: all 0.3s ease;
1633
+ flex: 1;
1634
 
1635
  &:disabled {
1636
  opacity: 0.5;
1637
  cursor: not-allowed;
1638
  }
1639
+
1640
+ &.btn-pass {
1641
+ background-color: #2d4059;
1642
+ color: #e0e0e0;
1643
+
1644
+ &:hover:not(:disabled) {
1645
+ background-color: #3d5069;
1646
+ }
1647
+ }
1648
+
1649
+ &.btn-resign {
1650
+ background-color: #c23b22;
1651
+ color: #fff;
1652
+
1653
+ &:hover:not(:disabled) {
1654
+ background-color: #d44b32;
1655
+ }
1656
+ }
1657
+
1658
+ &.btn-icon {
1659
+ background-color: #505050;
1660
+ color: #e0e0e0;
1661
+ padding: 0.75rem;
1662
+ font-size: 1.2rem;
1663
+
1664
+ &:hover:not(:disabled) {
1665
+ background-color: #606060;
1666
+ }
1667
+ }
1668
  }
1669
  }
1670
+ }
1671
 
1672
+ .settings-section {
1673
+ .settings-content {
1674
+ display: flex;
1675
+ flex-direction: column;
1676
+ gap: 1rem;
 
 
 
 
 
 
1677
 
1678
+ .setting-item {
1679
+ display: flex;
1680
+ align-items: center;
1681
+ justify-content: space-between;
1682
+
1683
+ label {
1684
+ font-weight: 600;
1685
+ color: #a0a0a0;
1686
+ display: flex;
1687
+ align-items: center;
1688
+ gap: 0.3rem;
1689
+
1690
+ .dirty-indicator {
1691
+ color: #e94560;
1692
+ font-size: 1.2rem;
1693
+ font-weight: 700;
1694
+ animation: pulse 2s ease-in-out infinite;
1695
+ }
1696
+ }
1697
+
1698
+ select {
1699
+ padding: 0.5rem;
1700
+ border: 2px solid #505050;
1701
+ border-radius: 8px;
1702
+ background-color: #484848;
1703
+ color: #e0e0e0;
1704
+ cursor: pointer;
1705
+ font-weight: 600;
1706
+
1707
+ &:disabled {
1708
+ opacity: 0.5;
1709
+ cursor: not-allowed;
1710
+ }
1711
+ }
1712
+ }
1713
+
1714
+ .btn-new-game {
1715
+ width: 100%;
1716
+ padding: 0.75rem;
1717
+ background-color: #e94560;
1718
+ color: #fff;
1719
+ border: none;
1720
+ border-radius: 8px;
1721
+ font-weight: 700;
1722
+ cursor: pointer;
1723
+ transition: all 0.3s ease;
1724
+ text-transform: uppercase;
1725
+
1726
+ &:hover {
1727
+ background-color: #f95670;
1728
+ transform: translateY(-2px);
1729
+ box-shadow: 0 4px 12px rgba(233, 69, 96, 0.4);
1730
+ }
1731
  }
1732
  }
1733
  }
1734
  }
1735
  }
1736
  }
1737
+
1738
+ /* TGN Button and Modal Styles */
1739
+ .btn-view-tgn {
1740
+ position: absolute;
1741
+ top: 0;
1742
+ right: 0.8rem;
1743
+ padding: 0.25rem 0.5rem;
1744
+ background-color: transparent;
1745
+ color: #a0a0a0;
1746
+ border: 1px solid #505050;
1747
+ border-radius: 4px;
1748
+ font-size: 0.65rem;
1749
+ font-weight: 600;
1750
+ text-transform: uppercase;
1751
+ letter-spacing: 0.5px;
1752
+ cursor: pointer;
1753
+ transition: all 0.2s ease;
1754
+ opacity: 0;
1755
+
1756
+ &:hover {
1757
+ background-color: #505050;
1758
+ color: #e0e0e0;
1759
+ border-color: #606060;
1760
+ }
1761
+ }
1762
+
1763
+ .routine-section:hover .btn-view-tgn {
1764
+ opacity: 1;
1765
  }
1766
+
1767
+ .tgn-modal {
1768
+ position: fixed;
1769
+ top: 0;
1770
+ left: 0;
1771
+ width: 100vw;
1772
+ height: 100vh;
1773
+ background-color: rgba(0, 0, 0, 0.7);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1774
  display: flex;
1775
+ align-items: center;
1776
+ justify-content: center;
1777
+ z-index: 1000;
1778
+ backdrop-filter: blur(4px);
1779
 
1780
+ .tgn-modal-content {
1781
+ background-color: #3a3a3a;
1782
+ border-radius: 12px;
1783
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5);
1784
+ width: 90%;
1785
+ max-width: 600px;
1786
+ max-height: 80vh;
1787
  display: flex;
1788
+ flex-direction: column;
1789
+ overflow: hidden;
 
 
 
 
 
 
 
 
 
 
1790
 
1791
+ .tgn-modal-header {
1792
+ display: flex;
1793
+ justify-content: space-between;
1794
+ align-items: center;
1795
+ padding: 1rem 1.5rem;
1796
+ background-color: #2a2a2a;
1797
+ border-bottom: 1px solid #505050;
1798
 
1799
+ h3 {
1800
+ margin: 0;
1801
+ font-size: 1.2rem;
1802
+ font-weight: 600;
1803
+ color: #e0e0e0;
1804
  }
1805
 
1806
+ .tgn-status {
1807
+ font-size: 0.85rem;
1808
+ font-weight: 600;
1809
+ padding: 0.4rem 0.8rem;
1810
+ border-radius: 4px;
1811
+ white-space: nowrap;
1812
+ transition: all 0.3s ease;
1813
+
1814
+ &.idle {
1815
+ background-color: #505050;
1816
+ color: #a0a0a0;
1817
+ }
1818
 
1819
+ &.valid {
1820
+ background-color: rgba(74, 222, 128, 0.15);
1821
+ color: #4ade80;
1822
+ border: 1px solid rgba(74, 222, 128, 0.4);
1823
+ }
1824
+
1825
+ &.invalid {
1826
+ background-color: rgba(239, 68, 68, 0.15);
1827
+ color: #ef4444;
1828
+ border: 1px solid rgba(239, 68, 68, 0.4);
1829
+ }
1830
  }
 
1831
 
1832
+ .btn-close {
1833
+ background: none;
1834
+ border: none;
1835
+ color: #a0a0a0;
1836
+ font-size: 1.5rem;
1837
+ cursor: pointer;
1838
+ padding: 0;
1839
+ width: 32px;
1840
+ height: 32px;
1841
+ display: flex;
1842
+ align-items: center;
1843
+ justify-content: center;
1844
+ border-radius: 4px;
1845
+ transition: all 0.2s ease;
1846
 
1847
+ &:hover {
1848
+ background-color: #505050;
1849
+ color: #e0e0e0;
1850
+ }
1851
  }
1852
  }
 
1853
 
1854
+ .tgn-modal-body {
1855
+ flex: 1;
1856
+ padding: 1.5rem;
1857
+ overflow: auto;
1858
 
1859
+ .tgn-textarea {
1860
+ width: 100%;
1861
+ height: 100%;
1862
+ min-height: 300px;
1863
+ background-color: #2a2a2a;
1864
+ color: #e0e0e0;
1865
+ border: 2px solid #505050;
1866
+ border-radius: 8px;
1867
+ padding: 1rem;
1868
+ font-family: "Courier New", Courier, monospace;
1869
+ font-size: 0.9rem;
1870
+ line-height: 1.6;
1871
+ resize: vertical;
1872
+ cursor: text;
1873
+ transition:
1874
+ border-color 0.3s ease,
1875
+ box-shadow 0.3s ease;
1876
+
1877
+ &:focus {
1878
+ outline: none;
1879
+ border-color: #e94560;
1880
+ }
1881
 
1882
+ &.valid {
1883
+ border-color: #4ade80;
1884
+ box-shadow: 0 0 8px rgba(74, 222, 128, 0.2);
1885
+ }
1886
 
1887
+ &.invalid {
1888
+ border-color: #ef4444;
1889
+ box-shadow: 0 0 8px rgba(239, 68, 68, 0.2);
1890
+ }
1891
 
1892
+ &.idle {
1893
+ border-color: #505050;
1894
+ }
1895
  }
1896
  }
 
1897
 
1898
+ .tgn-modal-footer {
1899
+ display: flex;
1900
+ gap: 0.5rem;
1901
+ padding: 1rem 1.5rem;
1902
+ background-color: #2a2a2a;
1903
+ border-top: 1px solid #505050;
1904
 
1905
+ .btn {
1906
+ flex: 1;
1907
+ padding: 0.75rem;
1908
+ border: none;
1909
+ border-radius: 8px;
1910
+ font-weight: 600;
1911
+ cursor: pointer;
1912
+ transition: all 0.3s ease;
 
 
 
 
 
1913
 
1914
+ &:disabled {
1915
+ opacity: 0.5;
1916
+ cursor: not-allowed;
1917
+ }
1918
+
1919
+ &.btn-apply {
1920
+ background-color: #4ade80;
1921
+ color: #000;
1922
 
1923
+ &:hover:not(:disabled) {
1924
+ background-color: #22c55e;
1925
+ transform: translateY(-2px);
1926
+ box-shadow: 0 4px 12px rgba(74, 222, 128, 0.4);
1927
+ }
1928
  }
 
1929
 
1930
+ &.btn-copy {
1931
+ background-color: #e94560;
1932
+ color: #fff;
1933
 
1934
+ &:hover {
1935
+ background-color: #f95670;
1936
+ transform: translateY(-2px);
1937
+ box-shadow: 0 4px 12px rgba(233, 69, 96, 0.4);
1938
+ }
1939
  }
 
1940
 
1941
+ &.btn-close-modal {
1942
+ background-color: #505050;
1943
+ color: #e0e0e0;
1944
 
1945
+ &:hover {
1946
+ background-color: #606060;
1947
+ }
1948
  }
1949
  }
1950
  }
1951
  }
1952
  }
 
1953
 
1954
+ @keyframes pulse {
1955
+ 0%,
1956
+ 100% {
1957
+ opacity: 0.5;
1958
+ }
1959
+ 50% {
1960
+ opacity: 1;
1961
+ }
1962
  }
 
1963
 
1964
+ /* Scrollbar styling */
1965
+ .controls-panel {
1966
+ &::-webkit-scrollbar {
1967
+ width: 8px;
1968
+ }
1969
 
1970
+ &::-webkit-scrollbar-track {
1971
+ background: #2a2a2a;
1972
+ }
1973
 
1974
+ &::-webkit-scrollbar-thumb {
1975
+ background: #505050;
1976
+ border-radius: 4px;
1977
 
1978
+ &:hover {
1979
+ background: #606060;
1980
+ }
1981
  }
1982
  }
 
1983
 
1984
+ .routine-content {
1985
+ &::-webkit-scrollbar {
1986
+ width: 6px;
1987
+ }
1988
 
1989
+ &::-webkit-scrollbar-track {
1990
+ background: #2a2a2a;
1991
+ }
1992
 
1993
+ &::-webkit-scrollbar-thumb {
1994
+ background: #505050;
1995
+ border-radius: 3px;
1996
 
1997
+ &:hover {
1998
+ background: #606060;
1999
+ }
2000
+ }
2001
+ }
2002
+
2003
+ // Animations
2004
+ @keyframes pulse {
2005
+ 0%,
2006
+ 100% {
2007
+ opacity: 1;
2008
+ }
2009
+ 50% {
2010
+ opacity: 0.5;
2011
  }
2012
  }
 
2013
  </style>
trigo-web/app/test_capture.js CHANGED
@@ -1,5 +1,5 @@
1
- const { TrigoGame } = require('../../inc/trigo/trigoGame.js');
2
- const { StoneType } = require('../../inc/trigo/game.js');
3
 
4
  const game = new TrigoGame({ x: 5, y: 5, z: 1 });
5
  game.startGame();
 
1
+ const { TrigoGame } = require("../../inc/trigo/trigoGame.js");
2
+ const { StoneType } = require("../../inc/trigo/game.js");
3
 
4
  const game = new TrigoGame({ x: 5, y: 5, z: 1 });
5
  game.startGame();
trigo-web/app/vite.config.ts CHANGED
@@ -8,7 +8,24 @@ export default defineConfig(({ mode }) => {
8
  const env = loadEnv(mode, process.cwd(), "");
9
 
10
  return {
11
- plugins: [vue()],
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
12
  // Point to parent project's public directory
13
  publicDir: fileURLToPath(new URL("../public", import.meta.url)),
14
  resolve: {
@@ -21,7 +38,14 @@ export default defineConfig(({ mode }) => {
21
  host: env.VITE_HOST || "localhost",
22
  port: parseInt(env.VITE_PORT) || 5173,
23
  strictPort: true,
24
- open: false
 
 
 
 
 
 
 
25
  },
26
  build: {
27
  rollupOptions: {
@@ -39,4 +63,4 @@ export default defineConfig(({ mode }) => {
39
  "process.env": {}
40
  }
41
  };
42
- });
 
8
  const env = loadEnv(mode, process.cwd(), "");
9
 
10
  return {
11
+ plugins: [
12
+ vue(),
13
+ // Plugin to set correct MIME types
14
+ {
15
+ name: "configure-server",
16
+ configureServer(server) {
17
+ server.middlewares.use((req, res, next) => {
18
+ if (req.url && req.url.endsWith(".wasm")) {
19
+ res.setHeader("Content-Type", "application/wasm");
20
+ }
21
+ if (req.url && req.url.endsWith(".mjs")) {
22
+ res.setHeader("Content-Type", "application/javascript");
23
+ }
24
+ next();
25
+ });
26
+ }
27
+ }
28
+ ],
29
  // Point to parent project's public directory
30
  publicDir: fileURLToPath(new URL("../public", import.meta.url)),
31
  resolve: {
 
38
  host: env.VITE_HOST || "localhost",
39
  port: parseInt(env.VITE_PORT) || 5173,
40
  strictPort: true,
41
+ open: false,
42
+ fs: {
43
+ // Allow serving files from node_modules
44
+ allow: ["..", "../.."]
45
+ }
46
+ },
47
+ optimizeDeps: {
48
+ exclude: ["onnxruntime-web"]
49
  },
50
  build: {
51
  rollupOptions: {
 
63
  "process.env": {}
64
  }
65
  };
66
+ });
trigo-web/backend/src/server.ts CHANGED
@@ -11,8 +11,12 @@ const app = express();
11
  const httpServer = createServer(app);
12
  const io = new Server(httpServer, {
13
  cors: {
14
- origin: process.env.CLIENT_URL || "http://localhost:5173",
15
- methods: ["GET", "POST"]
 
 
 
 
16
  }
17
  });
18
 
@@ -73,4 +77,4 @@ httpServer.listen(PORT, HOST, () => {
73
  console.log(`Server running on ${HOST}:${PORT}`);
74
  console.log(`Health check: http://${HOST}:${PORT}/health`);
75
  console.log(`Environment: ${process.env.NODE_ENV || "development"}`);
76
- });
 
11
  const httpServer = createServer(app);
12
  const io = new Server(httpServer, {
13
  cors: {
14
+ origin:
15
+ process.env.NODE_ENV === "production"
16
+ ? process.env.CLIENT_URL || "http://localhost:5173"
17
+ : true, // Allow all origins in development
18
+ methods: ["GET", "POST"],
19
+ credentials: true
20
  }
21
  });
22
 
 
77
  console.log(`Server running on ${HOST}:${PORT}`);
78
  console.log(`Health check: http://${HOST}:${PORT}/health`);
79
  console.log(`Environment: ${process.env.NODE_ENV || "development"}`);
80
+ });
trigo-web/backend/src/services/gameManager.ts CHANGED
@@ -120,11 +120,7 @@ export class GameManager {
120
  }
121
  }
122
 
123
- makeMove(
124
- roomId: string,
125
- playerId: string,
126
- move: { x: number; y: number; z: number }
127
- ): boolean {
128
  const room = this.rooms.get(roomId);
129
  if (!room) return false;
130
 
@@ -277,7 +273,9 @@ export class GameManager {
277
  }
278
 
279
  room.gameState.gameStatus = "finished";
280
- console.log(`Game ${roomId} ended. Black: ${territory.black}, White: ${territory.white}, Winner: ${room.gameState.winner}`);
 
 
281
 
282
  return true;
283
  }
@@ -326,4 +324,4 @@ export class GameManager {
326
  private generateRoomId(): string {
327
  return uuidv4().substring(0, 8).toUpperCase();
328
  }
329
- }
 
120
  }
121
  }
122
 
123
+ makeMove(roomId: string, playerId: string, move: { x: number; y: number; z: number }): boolean {
 
 
 
 
124
  const room = this.rooms.get(roomId);
125
  if (!room) return false;
126
 
 
273
  }
274
 
275
  room.gameState.gameStatus = "finished";
276
+ console.log(
277
+ `Game ${roomId} ended. Black: ${territory.black}, White: ${territory.white}, Winner: ${room.gameState.winner}`
278
+ );
279
 
280
  return true;
281
  }
 
324
  private generateRoomId(): string {
325
  return uuidv4().substring(0, 8).toUpperCase();
326
  }
327
+ }
trigo-web/backend/src/sockets/gameSocket.ts CHANGED
@@ -9,7 +9,9 @@ export function setupSocketHandlers(io: Server, socket: Socket, gameManager: Gam
9
  const { roomId, nickname } = data;
10
 
11
  // Create or join room
12
- const room = roomId ? gameManager.joinRoom(roomId, socket.id, nickname) : gameManager.createRoom(socket.id, nickname);
 
 
13
 
14
  if (room) {
15
  socket.join(room.id);
@@ -152,4 +154,4 @@ export function setupSocketHandlers(io: Server, socket: Socket, gameManager: Gam
152
  });
153
  }
154
  });
155
- }
 
9
  const { roomId, nickname } = data;
10
 
11
  // Create or join room
12
+ const room = roomId
13
+ ? gameManager.joinRoom(roomId, socket.id, nickname)
14
+ : gameManager.createRoom(socket.id, nickname);
15
 
16
  if (room) {
17
  socket.join(room.id);
 
154
  });
155
  }
156
  });
157
+ }
trigo-web/backend/tsconfig.json CHANGED
@@ -19,16 +19,11 @@
19
  "noImplicitAny": false
20
  },
21
  "include": ["src/**/*", "../inc/**/*"],
22
- "exclude": [
23
- "node_modules",
24
- "dist",
25
- "../inc/trigo/parserInit.ts",
26
- "../inc/tgn/tgn.jison.cjs"
27
- ],
28
  "ts-node": {
29
  "esm": false,
30
  "compilerOptions": {
31
  "module": "commonjs"
32
  }
33
  }
34
- }
 
19
  "noImplicitAny": false
20
  },
21
  "include": ["src/**/*", "../inc/**/*"],
22
+ "exclude": ["node_modules", "dist", "../inc/trigo/parserInit.ts", "../inc/tgn/tgn.jison.cjs"],
 
 
 
 
 
23
  "ts-node": {
24
  "esm": false,
25
  "compilerOptions": {
26
  "module": "commonjs"
27
  }
28
  }
29
+ }
trigo-web/inc/modelInferencer.ts ADDED
@@ -0,0 +1,465 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * ONNX Model Inferencer (Frontend/Backend Common)
3
+ *
4
+ * Platform-agnostic inference logic that accepts ONNX session from platform-specific code.
5
+ * No direct dependency on onnxruntime packages - uses dependency injection pattern.
6
+ *
7
+ * Adapted from Node.js test_inference.js for cross-platform use
8
+ * Provides causal language model inference using GPT-2 ONNX model
9
+ */
10
+
11
+ /**
12
+ * Minimal ONNX Tensor interface (platform-agnostic)
13
+ */
14
+ export interface OnnxTensor {
15
+ readonly data: number[] | Float32Array | Int32Array | BigInt64Array | Uint8Array;
16
+ readonly dims: readonly number[];
17
+ readonly type: string;
18
+ }
19
+
20
+ /**
21
+ * Minimal ONNX Session interface (platform-agnostic)
22
+ */
23
+ export interface OnnxSession {
24
+ readonly inputNames: readonly string[];
25
+ readonly outputNames: readonly string[];
26
+ run(feeds: Record<string, OnnxTensor>): Promise<Record<string, OnnxTensor>>;
27
+ }
28
+
29
+ /**
30
+ * Tensor constructor interface (platform-specific)
31
+ */
32
+ export interface TensorConstructor {
33
+ new (
34
+ type: string,
35
+ data: BigInt64Array | Float32Array | Int32Array | Uint8Array,
36
+ dims: number[]
37
+ ): OnnxTensor;
38
+ }
39
+
40
+ /**
41
+ * Configuration for the inferencer
42
+ */
43
+ export interface InferencerConfig {
44
+ vocabSize: number;
45
+ seqLen: number;
46
+ modelPath?: string; // Optional, for reference
47
+ }
48
+
49
+ /**
50
+ * Inference result containing generated tokens and metadata
51
+ */
52
+ export interface InferenceResult {
53
+ tokens: number[];
54
+ text: string;
55
+ logits: Float32Array;
56
+ inferenceTime: number;
57
+ }
58
+
59
+ /**
60
+ * Evaluation mode inputs for tree attention
61
+ */
62
+ export interface EvaluationInputs {
63
+ prefixIds: number[]; // Prefix sequence (causal context)
64
+ evaluatedIds: number[]; // Tokens to evaluate in tree
65
+ evaluatedMask: number[]; // Attention mask [m×m] flattened
66
+ }
67
+
68
+ /**
69
+ * Evaluation mode output
70
+ */
71
+ export interface EvaluationOutput {
72
+ logits: Float32Array; // [m+1, vocab_size] flattened
73
+ numEvaluated: number; // m
74
+ }
75
+
76
+ /**
77
+ * Model Inferencer for Causal Language Model
78
+ * Compatible with both frontend (onnxruntime-web) and backend (onnxruntime-node)
79
+ */
80
+ export class ModelInferencer {
81
+ private session: OnnxSession | null = null;
82
+ private config: InferencerConfig;
83
+ private TensorClass: TensorConstructor;
84
+
85
+ // TGN tokenizer: byte-level (0-255) + PAD(256) + START(257) + END(258)
86
+ private readonly PAD_TOKEN = 256;
87
+ private readonly START_TOKEN = 257;
88
+ private readonly END_TOKEN = 258;
89
+
90
+ constructor(TensorClass: TensorConstructor, config: Partial<InferencerConfig> = {}) {
91
+ this.TensorClass = TensorClass;
92
+ this.config = {
93
+ vocabSize: 259,
94
+ seqLen: 256,
95
+ ...config
96
+ };
97
+ }
98
+
99
+ /**
100
+ * Set the inference session (created by platform-specific code)
101
+ */
102
+ setSession(session: OnnxSession): void {
103
+ this.session = session;
104
+ console.log("[ModelInferencer] ✓ Session set successfully");
105
+ this.printModelInfo();
106
+ }
107
+
108
+ /**
109
+ * Run basic inference test
110
+ */
111
+ async testBasicInference(): Promise<InferenceResult> {
112
+ if (!this.session) {
113
+ throw new Error("Inferencer not initialized. Call setSession() first.");
114
+ }
115
+
116
+ console.log("[ModelInferencer] Running basic inference test...");
117
+
118
+ const batchSize = 1;
119
+ const seqLen = this.config.seqLen;
120
+
121
+ // Create random input
122
+ const inputIds = this.createRandomInput(batchSize, seqLen);
123
+ const inputTensor = new this.TensorClass("int64", inputIds, [batchSize, seqLen]);
124
+
125
+ // Run inference
126
+ const startTime = performance.now();
127
+ const results = await this.session.run({ input_ids: inputTensor });
128
+ const inferenceTime = performance.now() - startTime;
129
+
130
+ // Get logits
131
+ const logits = results.logits;
132
+
133
+ // Validate output
134
+ this.validateOutput(logits, batchSize, seqLen);
135
+
136
+ // Get predictions
137
+ const predictions = this.getPredictions(logits.data as Float32Array, batchSize * seqLen);
138
+
139
+ // Convert tokens to text
140
+ const text = String.fromCharCode(...predictions.slice(0, 100));
141
+
142
+ console.log("[ModelInferencer] Inference completed:");
143
+ console.log(` Input shape: [${inputTensor.dims.join(", ")}]`);
144
+ console.log(` Output shape: [${logits.dims.join(", ")}]`);
145
+ console.log(` Output dtype: ${logits.type}`);
146
+ console.log(` Inference time: ${inferenceTime.toFixed(2)}ms`);
147
+ console.log(` Sample predictions: [${predictions.slice(0, 10).join(", ")}]`);
148
+
149
+ const logitsArray = Array.from(logits.data as Float32Array);
150
+ console.log(
151
+ ` Logits range: [${Math.min(...logitsArray).toFixed(3)}, ${Math.max(...logitsArray).toFixed(3)}]`
152
+ );
153
+
154
+ return {
155
+ tokens: predictions,
156
+ text,
157
+ logits: logits.data as Float32Array,
158
+ inferenceTime
159
+ };
160
+ }
161
+
162
+ /**
163
+ * Generate tokens autoregressively from a prompt
164
+ */
165
+ async generateText(prompt: string, numTokens: number = 10): Promise<InferenceResult> {
166
+ if (!this.session) {
167
+ throw new Error("Inferencer not initialized. Call setSession() first.");
168
+ }
169
+
170
+ console.log(`[ModelInferencer] Generating ${numTokens} tokens from prompt: "${prompt}"`);
171
+
172
+ // Convert prompt to token IDs (byte values)
173
+ const promptTokens = Array.from(prompt).map((c) => c.charCodeAt(0));
174
+ console.log(` Prompt tokens (${promptTokens.length}): [${promptTokens.join(", ")}]`);
175
+
176
+ // Start with prompt tokens
177
+ const sequence = [...promptTokens];
178
+ const times: number[] = [];
179
+
180
+ // Generate tokens
181
+ for (let i = 0; i < numTokens; i++) {
182
+ // Pad sequence to fixed length
183
+ const paddedSequence = this.padSequence(sequence, this.config.seqLen);
184
+
185
+ // Create input tensor
186
+ const inputIds = new BigInt64Array(paddedSequence.map((t) => BigInt(t)));
187
+ const inputTensor = new this.TensorClass("int64", inputIds, [1, this.config.seqLen]);
188
+
189
+ // Run inference
190
+ const startTime = performance.now();
191
+ const results = await this.session.run({ input_ids: inputTensor });
192
+ times.push(performance.now() - startTime);
193
+
194
+ // Get prediction at the last non-padded position
195
+ const logits = results.logits.data as Float32Array;
196
+ const lastPos = sequence.length - 1; // Position before padding
197
+ const offset = lastPos * this.config.vocabSize;
198
+
199
+ // Find token with highest logit
200
+ let maxIdx = 0;
201
+ let maxVal = logits[offset];
202
+ for (let j = 1; j < this.config.vocabSize; j++) {
203
+ if (logits[offset + j] > maxVal) {
204
+ maxVal = logits[offset + j];
205
+ maxIdx = j;
206
+ }
207
+ }
208
+
209
+ sequence.push(maxIdx);
210
+
211
+ // Stop if END token is generated
212
+ if (maxIdx === this.END_TOKEN) {
213
+ console.log(" Generated END token, stopping...");
214
+ break;
215
+ }
216
+ }
217
+
218
+ // Convert generated tokens to text
219
+ const generatedText = String.fromCharCode(...sequence);
220
+
221
+ const avgTime = times.reduce((a, b) => a + b, 0) / times.length;
222
+ console.log(`[ModelInferencer] Generation complete:`);
223
+ console.log(` Generated text: "${generatedText}"`);
224
+ console.log(` Token sequence (${sequence.length}): [${sequence.join(", ")}]`);
225
+ console.log(` Avg inference time: ${avgTime.toFixed(2)}ms`);
226
+ console.log(` Tokens/sec: ${(1000 / avgTime).toFixed(2)}`);
227
+
228
+ return {
229
+ tokens: sequence,
230
+ text: generatedText,
231
+ logits: new Float32Array(), // Not returning full logits for generation
232
+ inferenceTime: avgTime
233
+ };
234
+ }
235
+
236
+ /**
237
+ * Get model information
238
+ */
239
+ getModelInfo(): { inputs: string[]; outputs: string[] } | null {
240
+ if (!this.session) return null;
241
+
242
+ return {
243
+ inputs: [...this.session.inputNames],
244
+ outputs: [...this.session.outputNames]
245
+ };
246
+ }
247
+
248
+ /**
249
+ * Get configuration
250
+ */
251
+ getConfig(): InferencerConfig {
252
+ return this.config;
253
+ }
254
+
255
+ /**
256
+ * Run inference with token array input
257
+ * Returns raw logits as Float32Array
258
+ */
259
+ async runInference(tokens: number[]): Promise<Float32Array> {
260
+ if (!this.session) {
261
+ throw new Error("Inferencer not initialized. Call setSession() first.");
262
+ }
263
+
264
+ const seqLen = this.config.seqLen;
265
+
266
+ // Prepend START_TOKEN to input
267
+ const tokensWithStart = [this.START_TOKEN, ...tokens];
268
+
269
+ // Pad to fixed length
270
+ const paddedTokens = new BigInt64Array(seqLen);
271
+ for (let i = 0; i < seqLen; i++) {
272
+ paddedTokens[i] =
273
+ i < tokensWithStart.length ? BigInt(tokensWithStart[i]) : BigInt(this.PAD_TOKEN);
274
+ }
275
+
276
+ // Create input tensor
277
+ const inputTensor = new this.TensorClass("int64", paddedTokens, [1, seqLen]);
278
+
279
+ // Run inference
280
+ const results = await this.session.run({ input_ids: inputTensor });
281
+ return results.logits.data as Float32Array;
282
+ }
283
+
284
+
285
+ /**
286
+ * Run tree attention inference (evaluation mode)
287
+ * For models exported with --evaluation flag
288
+ * @param inputs - Prefix, evaluated tokens, and attention mask
289
+ * @returns Logits for each evaluated position
290
+ */
291
+ async runEvaluationInference(inputs: EvaluationInputs): Promise<EvaluationOutput> {
292
+ if (!this.session) {
293
+ throw new Error("Inferencer not initialized. Call setSession() first.");
294
+ }
295
+
296
+ const { prefixIds, evaluatedIds, evaluatedMask } = inputs;
297
+ const batchSize = 1;
298
+ const prefixLen = prefixIds.length;
299
+ const m = evaluatedIds.length;
300
+
301
+ // Convert to BigInt64Array for ONNX int64 tensors
302
+ const prefixIdsArray = new BigInt64Array(batchSize * prefixLen);
303
+ for (let i = 0; i < prefixLen; i++) {
304
+ prefixIdsArray[i] = BigInt(prefixIds[i]);
305
+ }
306
+
307
+ const evaluatedIdsArray = new BigInt64Array(batchSize * m);
308
+ for (let i = 0; i < m; i++) {
309
+ evaluatedIdsArray[i] = BigInt(evaluatedIds[i]);
310
+ }
311
+
312
+ // Mask is Float32Array
313
+ const maskArray = new Float32Array(m * m);
314
+ for (let i = 0; i < m * m; i++) {
315
+ maskArray[i] = evaluatedMask[i];
316
+ }
317
+
318
+ // Create ONNX tensors
319
+ const prefixIdsTensor = new this.TensorClass("int64", prefixIdsArray, [
320
+ batchSize,
321
+ prefixLen
322
+ ]);
323
+ const evaluatedIdsTensor = new this.TensorClass("int64", evaluatedIdsArray, [batchSize, m]);
324
+ const evaluatedMaskTensor = new this.TensorClass("float32", maskArray, [1, m, m]);
325
+
326
+ // Run inference
327
+ const results = await this.session.run({
328
+ prefix_ids: prefixIdsTensor,
329
+ evaluated_ids: evaluatedIdsTensor,
330
+ evaluated_mask: evaluatedMaskTensor
331
+ });
332
+
333
+ // Extract logits
334
+ const logits = results.logits.data as Float32Array;
335
+
336
+ // Output shape: [batch, m+1, vocab_size]
337
+ // We return flattened array and num_evaluated for reshaping
338
+ return {
339
+ logits,
340
+ numEvaluated: m
341
+ };
342
+ }
343
+
344
+
345
+ /**
346
+ * Compute softmax for a single position's logits
347
+ * @param logits - Full logits array
348
+ * @param position - Which evaluated position (0 = last prefix, 1-m = evaluated tokens)
349
+ * @returns Probability distribution over vocabulary
350
+ */
351
+ softmax(logits: Float32Array, position: number): Float32Array {
352
+ const vocabSize = this.config.vocabSize;
353
+ const offset = position * vocabSize;
354
+ const probs = new Float32Array(vocabSize);
355
+
356
+ // Find max for numerical stability
357
+ let maxLogit = -Infinity;
358
+ for (let i = 0; i < vocabSize; i++) {
359
+ maxLogit = Math.max(maxLogit, logits[offset + i]);
360
+ }
361
+
362
+ // Compute exp and sum
363
+ let sumExp = 0;
364
+ for (let i = 0; i < vocabSize; i++) {
365
+ probs[i] = Math.exp(logits[offset + i] - maxLogit);
366
+ sumExp += probs[i];
367
+ }
368
+
369
+ // Normalize
370
+ for (let i = 0; i < vocabSize; i++) {
371
+ probs[i] /= sumExp;
372
+ }
373
+
374
+ return probs;
375
+ }
376
+
377
+
378
+ /**
379
+ * Check if inferencer is ready
380
+ */
381
+ isReady(): boolean {
382
+ return this.session !== null;
383
+ }
384
+
385
+ /**
386
+ * Destroy the session and free resources
387
+ */
388
+ destroy(): void {
389
+ this.session = null;
390
+ console.log("[ModelInferencer] Session destroyed");
391
+ }
392
+
393
+ // Private helper methods
394
+
395
+ private printModelInfo(): void {
396
+ if (!this.session) return;
397
+
398
+ console.log("[ModelInferencer] Model Information:");
399
+ console.log(" Inputs:");
400
+ this.session.inputNames.forEach((name, i) => {
401
+ console.log(` [${i}] ${name}`);
402
+ });
403
+ console.log(" Outputs:");
404
+ this.session.outputNames.forEach((name, i) => {
405
+ console.log(` [${i}] ${name}`);
406
+ });
407
+ }
408
+
409
+ private createRandomInput(batchSize: number, seqLen: number): BigInt64Array {
410
+ const size = batchSize * seqLen;
411
+ const data = new BigInt64Array(size);
412
+ for (let i = 0; i < size; i++) {
413
+ data[i] = BigInt(Math.floor(Math.random() * this.config.vocabSize));
414
+ }
415
+ return data;
416
+ }
417
+
418
+ private padSequence(tokens: number[], targetLen: number): number[] {
419
+ const padded = [...tokens];
420
+ while (padded.length < targetLen) {
421
+ padded.push(this.PAD_TOKEN);
422
+ }
423
+ return padded.slice(0, targetLen); // Truncate if too long
424
+ }
425
+
426
+ private validateOutput(logits: OnnxTensor, batchSize: number, seqLen: number): void {
427
+ const expectedShape = [batchSize, seqLen, this.config.vocabSize];
428
+
429
+ if (logits.dims.length !== 3) {
430
+ throw new Error(`Expected 3D output, got ${logits.dims.length}D`);
431
+ }
432
+
433
+ if (
434
+ logits.dims[0] !== expectedShape[0] ||
435
+ logits.dims[1] !== expectedShape[1] ||
436
+ logits.dims[2] !== expectedShape[2]
437
+ ) {
438
+ throw new Error(
439
+ `Shape mismatch! Expected [${expectedShape.join(", ")}], ` +
440
+ `got [${logits.dims.join(", ")}]`
441
+ );
442
+ }
443
+
444
+ if (logits.type !== "float32") {
445
+ throw new Error(`Expected float32 output, got ${logits.type}`);
446
+ }
447
+ }
448
+
449
+ private getPredictions(logitsData: Float32Array, numPositions: number): number[] {
450
+ const predictions: number[] = [];
451
+ for (let i = 0; i < numPositions; i++) {
452
+ let maxIdx = 0;
453
+ let maxVal = logitsData[i * this.config.vocabSize];
454
+ for (let j = 1; j < this.config.vocabSize; j++) {
455
+ const val = logitsData[i * this.config.vocabSize + j];
456
+ if (val > maxVal) {
457
+ maxVal = val;
458
+ maxIdx = j;
459
+ }
460
+ }
461
+ predictions.push(maxIdx);
462
+ }
463
+ return predictions;
464
+ }
465
+ }
trigo-web/inc/tgn/README.md CHANGED
@@ -29,7 +29,7 @@ This will generate `tgn.jison.cjs` from `tgn.jison`.
29
  Import the parser functions from the game module:
30
 
31
  ```typescript
32
- import { TrigoGame, validateTGN, TGNParseError } from '@inc/trigo/game';
33
 
34
  // Parse TGN and create game instance
35
  const tgn = `
@@ -37,24 +37,24 @@ const tgn = `
37
  [Board 5x5x5]
38
 
39
  1. 000 y00
40
- 2. 0y0 pass
41
  `;
42
 
43
  try {
44
- const game = TrigoGame.fromTGN(tgn);
45
- console.log('Game loaded successfully!');
46
  } catch (error) {
47
- if (error instanceof TGNParseError) {
48
- console.error('Parse error at line', error.line);
49
- }
50
  }
51
 
52
  // Validate TGN without parsing
53
  const result = validateTGN(tgn);
54
  if (result.valid) {
55
- console.log('Valid TGN');
56
  } else {
57
- console.error('Invalid TGN:', result.error);
58
  }
59
  ```
60
 
@@ -78,7 +78,7 @@ Example:
78
 
79
  1. 000 y00
80
  2. 0y0 yy0
81
- 3. aaa pass
82
  ```
83
 
84
  ### Coordinate Notation (ab0yz)
@@ -88,12 +88,14 @@ Example:
88
  - `z`, `y`, `x`, ... = positions from opposite edge toward center
89
 
90
  Examples for 5×5×5 board:
 
91
  - `000` = center (2,2,2)
92
  - `aaa` = corner (0,0,0)
93
  - `zzz` = opposite corner (4,4,4)
94
  - `y00` = (4,2,2)
95
 
96
  For 2D boards (e.g., 19×19×1), only two coordinates are used:
 
97
  - `00` = center
98
  - `aa` = corner
99
 
 
29
  Import the parser functions from the game module:
30
 
31
  ```typescript
32
+ import { TrigoGame, validateTGN, TGNParseError } from "@inc/trigo/game";
33
 
34
  // Parse TGN and create game instance
35
  const tgn = `
 
37
  [Board 5x5x5]
38
 
39
  1. 000 y00
40
+ 2. 0y0 Pass
41
  `;
42
 
43
  try {
44
+ const game = TrigoGame.fromTGN(tgn);
45
+ console.log("Game loaded successfully!");
46
  } catch (error) {
47
+ if (error instanceof TGNParseError) {
48
+ console.error("Parse error at line", error.line);
49
+ }
50
  }
51
 
52
  // Validate TGN without parsing
53
  const result = validateTGN(tgn);
54
  if (result.valid) {
55
+ console.log("Valid TGN");
56
  } else {
57
+ console.error("Invalid TGN:", result.error);
58
  }
59
  ```
60
 
 
78
 
79
  1. 000 y00
80
  2. 0y0 yy0
81
+ 3. aaa Pass
82
  ```
83
 
84
  ### Coordinate Notation (ab0yz)
 
88
  - `z`, `y`, `x`, ... = positions from opposite edge toward center
89
 
90
  Examples for 5×5×5 board:
91
+
92
  - `000` = center (2,2,2)
93
  - `aaa` = corner (0,0,0)
94
  - `zzz` = opposite corner (4,4,4)
95
  - `y00` = (4,2,2)
96
 
97
  For 2D boards (e.g., 19×19×1), only two coordinates are used:
98
+
99
  - `00` = center
100
  - `aa` = corner
101
 
trigo-web/inc/tgn/tgn.jison CHANGED
@@ -45,8 +45,8 @@
45
  /* Move notation placeholders - will be expanded later */
46
  [1-9][0-9]* return 'NUMBER'
47
  "." return 'DOT'
48
- "pass" return 'PASS'
49
- "resign" return 'RESIGN'
50
  "points" return 'POINTS'
51
  "stones" return 'STONES'
52
 
 
45
  /* Move notation placeholders - will be expanded later */
46
  [1-9][0-9]* return 'NUMBER'
47
  "." return 'DOT'
48
+ "Pass" return 'PASS'
49
+ "Resign" return 'RESIGN'
50
  "points" return 'POINTS'
51
  "stones" return 'STONES'
52
 
trigo-web/inc/tgn/tgnParser.ts CHANGED
@@ -14,11 +14,10 @@
14
  * Parsed move action - represents a single player's action in a round
15
  */
16
  export interface ParsedMoveAction {
17
- type: 'move' | 'pass' | 'resign';
18
- position?: string; // ab0yz coordinate notation
19
  }
20
 
21
-
22
  /**
23
  * Parsed move round - contains both black and white moves
24
  */
@@ -28,19 +27,17 @@ export interface ParsedMoveRound {
28
  action_white?: ParsedMoveAction;
29
  }
30
 
31
-
32
  /**
33
  * Parsed game result
34
  */
35
  export interface ParsedGameResult {
36
- Result: string; // "black win" | "white win" | "draw" | "unknown"
37
  Conquer?: {
38
  n: number;
39
- unit: string; // "points" | "stones"
40
  };
41
  }
42
 
43
-
44
  /**
45
  * Parsed TGN tags (metadata)
46
  */
@@ -52,7 +49,7 @@ export interface ParsedTags {
52
  Black?: string;
53
  White?: string;
54
  Result?: string;
55
- Board?: number[]; // [x, y, z] or [x, y]
56
  Handicap?: string;
57
  Rules?: string;
58
  TimeControl?: string;
@@ -61,7 +58,6 @@ export interface ParsedTags {
61
  [key: string]: string | number[] | ParsedGameResult | undefined;
62
  }
63
 
64
-
65
  /**
66
  * Parser output structure
67
  */
@@ -71,7 +67,6 @@ export interface TGNParseResult {
71
  success: boolean;
72
  }
73
 
74
-
75
  /**
76
  * Parser error with position information
77
  */
@@ -83,15 +78,13 @@ export class TGNParseError extends Error {
83
  public hash?: any
84
  ) {
85
  super(message);
86
- this.name = 'TGNParseError';
87
  }
88
  }
89
 
90
-
91
  // Will be set by initialization code or build process
92
  let parserModule: any = null;
93
 
94
-
95
  /**
96
  * Set the parser module (called by initialization code)
97
  * This allows the pre-built parser to be used
@@ -100,7 +93,6 @@ export function setParserModule(module: any): void {
100
  parserModule = module;
101
  }
102
 
103
-
104
  /**
105
  * Get the parser module
106
  * Throws error if parser not loaded
@@ -108,14 +100,13 @@ export function setParserModule(module: any): void {
108
  function getParser() {
109
  if (!parserModule) {
110
  throw new Error(
111
- 'TGN parser not loaded. Please ensure the parser has been built.\n' +
112
- 'Run: npm run build:parsers'
113
  );
114
  }
115
  return parserModule;
116
  }
117
 
118
-
119
  /**
120
  * Parse TGN string and return structured data
121
  * Synchronous parsing (no async needed)
@@ -128,7 +119,7 @@ export function parseTGN(tgnString: string): TGNParseResult {
128
  const parser = getParser();
129
 
130
  if (!parser.parse) {
131
- throw new Error('TGN parser parse method not available');
132
  }
133
 
134
  try {
@@ -137,7 +128,7 @@ export function parseTGN(tgnString: string): TGNParseResult {
137
  } catch (error: any) {
138
  // Wrap jison errors with our custom error type
139
  throw new TGNParseError(
140
- error.message || 'Unknown parse error',
141
  error.hash?.line,
142
  error.hash?.loc?.first_column,
143
  error.hash
@@ -145,7 +136,6 @@ export function parseTGN(tgnString: string): TGNParseResult {
145
  }
146
  }
147
 
148
-
149
  /**
150
  * Validate TGN string without fully parsing
151
  * Synchronous validation (no async needed)
@@ -160,7 +150,7 @@ export function validateTGN(tgnString: string): { valid: boolean; error?: string
160
  } catch (error: any) {
161
  return {
162
  valid: false,
163
- error: error.message || 'Unknown validation error'
164
  };
165
  }
166
  }
 
14
  * Parsed move action - represents a single player's action in a round
15
  */
16
  export interface ParsedMoveAction {
17
+ type: "move" | "pass" | "resign";
18
+ position?: string; // ab0yz coordinate notation
19
  }
20
 
 
21
  /**
22
  * Parsed move round - contains both black and white moves
23
  */
 
27
  action_white?: ParsedMoveAction;
28
  }
29
 
 
30
  /**
31
  * Parsed game result
32
  */
33
  export interface ParsedGameResult {
34
+ Result: string; // "black win" | "white win" | "draw" | "unknown"
35
  Conquer?: {
36
  n: number;
37
+ unit: string; // "points" | "stones"
38
  };
39
  }
40
 
 
41
  /**
42
  * Parsed TGN tags (metadata)
43
  */
 
49
  Black?: string;
50
  White?: string;
51
  Result?: string;
52
+ Board?: number[]; // [x, y, z] or [x, y]
53
  Handicap?: string;
54
  Rules?: string;
55
  TimeControl?: string;
 
58
  [key: string]: string | number[] | ParsedGameResult | undefined;
59
  }
60
 
 
61
  /**
62
  * Parser output structure
63
  */
 
67
  success: boolean;
68
  }
69
 
 
70
  /**
71
  * Parser error with position information
72
  */
 
78
  public hash?: any
79
  ) {
80
  super(message);
81
+ this.name = "TGNParseError";
82
  }
83
  }
84
 
 
85
  // Will be set by initialization code or build process
86
  let parserModule: any = null;
87
 
 
88
  /**
89
  * Set the parser module (called by initialization code)
90
  * This allows the pre-built parser to be used
 
93
  parserModule = module;
94
  }
95
 
 
96
  /**
97
  * Get the parser module
98
  * Throws error if parser not loaded
 
100
  function getParser() {
101
  if (!parserModule) {
102
  throw new Error(
103
+ "TGN parser not loaded. Please ensure the parser has been built.\n" +
104
+ "Run: npm run build:parsers"
105
  );
106
  }
107
  return parserModule;
108
  }
109
 
 
110
  /**
111
  * Parse TGN string and return structured data
112
  * Synchronous parsing (no async needed)
 
119
  const parser = getParser();
120
 
121
  if (!parser.parse) {
122
+ throw new Error("TGN parser parse method not available");
123
  }
124
 
125
  try {
 
128
  } catch (error: any) {
129
  // Wrap jison errors with our custom error type
130
  throw new TGNParseError(
131
+ error.message || "Unknown parse error",
132
  error.hash?.line,
133
  error.hash?.loc?.first_column,
134
  error.hash
 
136
  }
137
  }
138
 
 
139
  /**
140
  * Validate TGN string without fully parsing
141
  * Synchronous validation (no async needed)
 
150
  } catch (error: any) {
151
  return {
152
  valid: false,
153
+ error: error.message || "Unknown validation error"
154
  };
155
  }
156
  }
trigo-web/inc/trigo/ab0yz.ts CHANGED
@@ -1,7 +1,6 @@
1
-
2
  // remove ones at tail
3
- const compactShape = (shape: number[]): number[] => shape[shape.length - 1] === 1 ? compactShape(shape.slice(0, shape.length - 1)) : shape;
4
-
5
 
6
  /**
7
  * Encode a position to TGN coordinate string.
@@ -32,7 +31,7 @@ const encodeAb0yz = (pos: number[], boardShape: number[]): string => {
32
 
33
  if (index === center) {
34
  // Center position
35
- result.push('0');
36
  } else if (index < center) {
37
  // Left side: a, b, c, ...
38
  result.push(String.fromCharCode(97 + index)); // 'a' = 97
@@ -43,10 +42,9 @@ const encodeAb0yz = (pos: number[], boardShape: number[]): string => {
43
  }
44
  }
45
 
46
- return result.join('');
47
  };
48
 
49
-
50
  /**
51
  * Decode a TGN coordinate string to position array.
52
  *
@@ -64,7 +62,9 @@ const decodeAb0yz = (code: string, boardShape: number[]): number[] => {
64
  const compactedShape = compactShape(boardShape);
65
 
66
  if (code.length !== compactedShape.length) {
67
- throw new Error(`Invalid TGN coordinate: "${code}" (must be ${compactedShape.length} characters for board shape ${boardShape.join('x')})`);
 
 
68
  }
69
 
70
  const result: number[] = [];
@@ -74,14 +74,15 @@ const decodeAb0yz = (code: string, boardShape: number[]): number[] => {
74
  const size = compactedShape[i];
75
  const center = (size - 1) / 2;
76
 
77
- if (char === '0') {
78
  // Center position
79
  console.assert(Number.isInteger(center));
80
  result.push(center);
81
  } else {
82
  const charCode = char.charCodeAt(0);
83
 
84
- if (charCode >= 97 && charCode <= 122) { // 'a' to 'z'
 
85
  // Calculate distance from 'a' and 'z'
86
  const distFromA = charCode - 97;
87
  const distFromZ = 122 - charCode;
@@ -91,19 +92,25 @@ const decodeAb0yz = (code: string, boardShape: number[]): number[] => {
91
  // Left side: a=0, b=1, c=2, ...
92
  const index = distFromA;
93
  if (index >= center) {
94
- throw new Error(`Invalid TGN coordinate: "${code}" (position ${index} >= center ${center} on axis ${i})`);
 
 
95
  }
96
  result.push(index);
97
  } else {
98
  // Right side: z=size-1, y=size-2, x=size-3, ...
99
  const index = size - 1 - distFromZ;
100
  if (index <= center) {
101
- throw new Error(`Invalid TGN coordinate: "${code}" (position ${index} <= center ${center} on axis ${i})`);
 
 
102
  }
103
  result.push(index);
104
  }
105
  } else {
106
- throw new Error(`Invalid TGN coordinate: "${code}" (character '${char}' at position ${i} must be '0' or a-z)`);
 
 
107
  }
108
  }
109
  }
@@ -116,5 +123,4 @@ const decodeAb0yz = (code: string, boardShape: number[]): number[] => {
116
  return result;
117
  };
118
 
119
-
120
  export { encodeAb0yz, decodeAb0yz };
 
 
1
  // remove ones at tail
2
+ const compactShape = (shape: number[]): number[] =>
3
+ shape[shape.length - 1] === 1 ? compactShape(shape.slice(0, shape.length - 1)) : shape;
4
 
5
  /**
6
  * Encode a position to TGN coordinate string.
 
31
 
32
  if (index === center) {
33
  // Center position
34
+ result.push("0");
35
  } else if (index < center) {
36
  // Left side: a, b, c, ...
37
  result.push(String.fromCharCode(97 + index)); // 'a' = 97
 
42
  }
43
  }
44
 
45
+ return result.join("");
46
  };
47
 
 
48
  /**
49
  * Decode a TGN coordinate string to position array.
50
  *
 
62
  const compactedShape = compactShape(boardShape);
63
 
64
  if (code.length !== compactedShape.length) {
65
+ throw new Error(
66
+ `Invalid TGN coordinate: "${code}" (must be ${compactedShape.length} characters for board shape ${boardShape.join("x")})`
67
+ );
68
  }
69
 
70
  const result: number[] = [];
 
74
  const size = compactedShape[i];
75
  const center = (size - 1) / 2;
76
 
77
+ if (char === "0") {
78
  // Center position
79
  console.assert(Number.isInteger(center));
80
  result.push(center);
81
  } else {
82
  const charCode = char.charCodeAt(0);
83
 
84
+ if (charCode >= 97 && charCode <= 122) {
85
+ // 'a' to 'z'
86
  // Calculate distance from 'a' and 'z'
87
  const distFromA = charCode - 97;
88
  const distFromZ = 122 - charCode;
 
92
  // Left side: a=0, b=1, c=2, ...
93
  const index = distFromA;
94
  if (index >= center) {
95
+ throw new Error(
96
+ `Invalid TGN coordinate: "${code}" (position ${index} >= center ${center} on axis ${i})`
97
+ );
98
  }
99
  result.push(index);
100
  } else {
101
  // Right side: z=size-1, y=size-2, x=size-3, ...
102
  const index = size - 1 - distFromZ;
103
  if (index <= center) {
104
+ throw new Error(
105
+ `Invalid TGN coordinate: "${code}" (position ${index} <= center ${center} on axis ${i})`
106
+ );
107
  }
108
  result.push(index);
109
  }
110
  } else {
111
+ throw new Error(
112
+ `Invalid TGN coordinate: "${code}" (character '${char}' at position ${i} must be '0' or a-z)`
113
+ );
114
  }
115
  }
116
  }
 
123
  return result;
124
  };
125
 
 
126
  export { encodeAb0yz, decodeAb0yz };
trigo-web/inc/trigo/game.ts CHANGED
@@ -15,34 +15,32 @@ import {
15
  findCapturedGroups,
16
  executeCaptures,
17
  calculateTerritory,
 
 
18
  type TerritoryResult
19
  } from "./gameUtils";
20
  import { encodeAb0yz, decodeAb0yz } from "./ab0yz";
21
  import { parseTGN, validateTGN, TGNParseError } from "../tgn/tgnParser";
22
 
23
-
24
  // Re-export StoneType for convenient access with TrigoGame
25
  export { StoneType } from "./gameUtils";
26
 
27
-
28
  /**
29
  * Step Types - Different types of moves in the game
30
  * Equivalent to trigo.Game.StepType in prototype
31
  */
32
  export enum StepType {
33
- DROP = 0, // Place a stone
34
- PASS = 1, // Pass turn
35
  SURRENDER = 2, // Resign/surrender
36
- UNDO = 3 // Undo last move (called "REPENT" in prototype)
37
  }
38
 
39
-
40
  /**
41
  * Game Status enumeration
42
  */
43
  export type GameStatus = "idle" | "playing" | "paused" | "finished";
44
 
45
-
46
  /**
47
  * Game Result information
48
  */
@@ -52,20 +50,18 @@ export interface GameResult {
52
  score?: TerritoryResult;
53
  }
54
 
55
-
56
  /**
57
  * Step - Represents a single move in the game
58
  * Equivalent to step objects in prototype's StepHistory
59
  */
60
  export interface Step {
61
  type: StepType;
62
- position?: Position; // Only for DROP moves
63
- player: Stone; // Which player made this move
64
  capturedPositions?: Position[]; // Stones captured by this move
65
- timestamp: number; // When the move was made
66
  }
67
 
68
-
69
  /**
70
  * Game Callbacks - Event handlers for game state changes
71
  * Equivalent to Callbacks in prototype's trigo.Game constructor
@@ -78,7 +74,6 @@ export interface GameCallbacks {
78
  onTerritoryChange?: (territory: TerritoryResult) => void;
79
  }
80
 
81
-
82
  /**
83
  * TrigoGame - Main game class managing state, history, and logic
84
  *
@@ -114,7 +109,6 @@ export class TrigoGame {
114
  private territoryDirty: boolean = true;
115
  private cachedTerritory: TerritoryResult | null = null;
116
 
117
-
118
  /**
119
  * Constructor
120
  * Equivalent to trigo.Game constructor (lines 75-85)
@@ -131,7 +125,6 @@ export class TrigoGame {
131
  this.passCount = 0;
132
  }
133
 
134
-
135
  /**
136
  * Create an empty board
137
  */
@@ -149,7 +142,6 @@ export class TrigoGame {
149
  return board;
150
  }
151
 
152
-
153
  /**
154
  * Reset the game to initial state
155
  * Equivalent to Game.reset() (lines 153-163)
@@ -167,16 +159,59 @@ export class TrigoGame {
167
  this.passCount = 0;
168
  }
169
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
170
 
171
  /**
172
  * Get current board state (read-only)
173
  */
174
  getBoard(): Stone[][][] {
175
  // Return a deep copy to prevent external modification
176
- return this.board.map(plane => plane.map(row => [...row]));
177
  }
178
 
179
-
180
  /**
181
  * Get stone at specific position
182
  * Equivalent to Game.stone() (lines 95-97)
@@ -185,7 +220,6 @@ export class TrigoGame {
185
  return this.board[pos.x][pos.y][pos.z];
186
  }
187
 
188
-
189
  /**
190
  * Get current player
191
  */
@@ -193,7 +227,6 @@ export class TrigoGame {
193
  return this.currentPlayer;
194
  }
195
 
196
-
197
  /**
198
  * Get current step number
199
  * Equivalent to Game.currentStep() (lines 99-101)
@@ -202,7 +235,6 @@ export class TrigoGame {
202
  return this.currentStepIndex;
203
  }
204
 
205
-
206
  /**
207
  * Get move history
208
  * Equivalent to Game.routine() (lines 103-105)
@@ -211,7 +243,6 @@ export class TrigoGame {
211
  return [...this.stepHistory];
212
  }
213
 
214
-
215
  /**
216
  * Get last move
217
  * Equivalent to Game.lastStep() (lines 107-110)
@@ -223,7 +254,6 @@ export class TrigoGame {
223
  return null;
224
  }
225
 
226
-
227
  /**
228
  * Get board shape
229
  * Equivalent to Game.shape() (lines 87-89)
@@ -232,7 +262,6 @@ export class TrigoGame {
232
  return { ...this.shape };
233
  }
234
 
235
-
236
  /**
237
  * Get game status
238
  */
@@ -240,7 +269,6 @@ export class TrigoGame {
240
  return this.gameStatus;
241
  }
242
 
243
-
244
  /**
245
  * Set game status
246
  */
@@ -248,7 +276,6 @@ export class TrigoGame {
248
  this.gameStatus = status;
249
  }
250
 
251
-
252
  /**
253
  * Get game result
254
  */
@@ -256,7 +283,6 @@ export class TrigoGame {
256
  return this.gameResult;
257
  }
258
 
259
-
260
  /**
261
  * Get consecutive pass count
262
  */
@@ -264,7 +290,6 @@ export class TrigoGame {
264
  return this.passCount;
265
  }
266
 
267
-
268
  /**
269
  * Recalculate consecutive pass count based on current history
270
  * Counts consecutive PASS steps from the end of current history
@@ -282,7 +307,6 @@ export class TrigoGame {
282
  }
283
  }
284
 
285
-
286
  /**
287
  * Start the game
288
  */
@@ -292,7 +316,6 @@ export class TrigoGame {
292
  }
293
  }
294
 
295
-
296
  /**
297
  * Check if game is active
298
  */
@@ -300,7 +323,6 @@ export class TrigoGame {
300
  return this.gameStatus === "playing";
301
  }
302
 
303
-
304
  /**
305
  * Check if a move is valid
306
  * Equivalent to Game.isDropable() and Game.isValidStep() (lines 112-151)
@@ -310,6 +332,59 @@ export class TrigoGame {
310
  return validateMove(pos, playerColor, this.board, this.shape, this.lastCapturedPositions);
311
  }
312
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
313
 
314
  /**
315
  * Reset pass count (called when a stone is placed)
@@ -318,7 +393,6 @@ export class TrigoGame {
318
  this.passCount = 0;
319
  }
320
 
321
-
322
  /**
323
  * Place a stone (drop move)
324
  * Equivalent to Game.drop() and Game.appendStone() (lines 181-273)
@@ -375,7 +449,6 @@ export class TrigoGame {
375
  return true;
376
  }
377
 
378
-
379
  /**
380
  * Pass turn
381
  * Equivalent to PASS step type in prototype
@@ -422,8 +495,12 @@ export class TrigoGame {
422
 
423
  // Trigger win callback
424
  if (this.callbacks.onWin) {
425
- const winnerStone = winner === "black" ? StoneType.BLACK :
426
- winner === "white" ? StoneType.WHITE : StoneType.EMPTY;
 
 
 
 
427
  this.callbacks.onWin(winnerStone);
428
  }
429
  }
@@ -431,13 +508,12 @@ export class TrigoGame {
431
  return true;
432
  }
433
 
434
-
435
  /**
436
  * Surrender/resign
437
  * Equivalent to Game.step() with SURRENDER type (lines 176-178)
438
  */
439
  surrender(): boolean {
440
- const surrenderingPlayer = this.currentPlayer; // Remember who surrendered
441
 
442
  const step: Step = {
443
  type: StepType.SURRENDER,
@@ -464,7 +540,6 @@ export class TrigoGame {
464
  return true;
465
  }
466
 
467
-
468
  /**
469
  * Undo last move
470
  * Equivalent to Game.repent() (lines 197-230)
@@ -481,7 +556,8 @@ export class TrigoGame {
481
  // Revert the move
482
  if (lastStep.type === StepType.DROP && lastStep.position) {
483
  // Remove the placed stone
484
- this.board[lastStep.position.x][lastStep.position.y][lastStep.position.z] = StoneType.EMPTY;
 
485
 
486
  // Restore captured stones
487
  if (lastStep.capturedPositions) {
@@ -519,7 +595,6 @@ export class TrigoGame {
519
  return true;
520
  }
521
 
522
-
523
  /**
524
  * Redo next move (after undo)
525
  *
@@ -536,7 +611,8 @@ export class TrigoGame {
536
  // Re-apply the move
537
  if (nextStep.type === StepType.DROP && nextStep.position) {
538
  // Place the stone
539
- this.board[nextStep.position.x][nextStep.position.y][nextStep.position.z] = nextStep.player;
 
540
 
541
  // Re-execute captures if there were any
542
  if (nextStep.capturedPositions) {
@@ -560,13 +636,15 @@ export class TrigoGame {
560
 
561
  // Trigger callback
562
  if (this.callbacks.onStepAdvance) {
563
- this.callbacks.onStepAdvance(nextStep, this.stepHistory.slice(0, this.currentStepIndex));
 
 
 
564
  }
565
 
566
  return true;
567
  }
568
 
569
-
570
  /**
571
  * Check if redo is available
572
  */
@@ -574,7 +652,6 @@ export class TrigoGame {
574
  return this.currentStepIndex < this.stepHistory.length;
575
  }
576
 
577
-
578
  /**
579
  * Jump to specific step in history
580
  * Rebuilds board state after applying the first 'index' moves
@@ -656,7 +733,6 @@ export class TrigoGame {
656
  return true;
657
  }
658
 
659
-
660
  /**
661
  * Advance to next step
662
  * Equivalent to Game.stepAdvance() (lines 279-287)
@@ -680,7 +756,6 @@ export class TrigoGame {
680
  }
681
  }
682
 
683
-
684
  /**
685
  * Get territory calculation
686
  * Equivalent to Game.blackDomain() and Game.whiteDomain() (lines 232-244)
@@ -695,7 +770,6 @@ export class TrigoGame {
695
  return this.cachedTerritory;
696
  }
697
 
698
-
699
  /**
700
  * Get captured stone counts up to current position in history
701
  * Only counts captures that have been played (up to currentStepIndex)
@@ -720,7 +794,6 @@ export class TrigoGame {
720
  return counts;
721
  }
722
 
723
-
724
  /**
725
  * Serialize game state to JSON
726
  * Equivalent to Game.serialize() (lines 250-252)
@@ -738,7 +811,6 @@ export class TrigoGame {
738
  };
739
  }
740
 
741
-
742
  /**
743
  * Load game state from JSON
744
  */
@@ -780,7 +852,6 @@ export class TrigoGame {
780
  }
781
  }
782
 
783
-
784
  /**
785
  * Get game statistics
786
  */
@@ -818,7 +889,6 @@ export class TrigoGame {
818
  };
819
  }
820
 
821
-
822
  /**
823
  * Save game state to sessionStorage
824
  *
@@ -842,7 +912,6 @@ export class TrigoGame {
842
  return false;
843
  }
844
 
845
-
846
  /**
847
  * Load game state from sessionStorage
848
  *
@@ -871,7 +940,6 @@ export class TrigoGame {
871
  return false;
872
  }
873
 
874
-
875
  /**
876
  * Clear saved game state from sessionStorage
877
  *
@@ -890,7 +958,6 @@ export class TrigoGame {
890
  }
891
  }
892
 
893
-
894
  /**
895
  * Export game to TGN (Trigo Game Notation) format
896
  *
@@ -950,16 +1017,17 @@ export class TrigoGame {
950
  const diff = Math.abs(black - white);
951
  resultStr += `${diff}points`;
952
  } else if (this.gameResult.reason === "resignation") {
953
- resultStr += "resign";
954
  }
955
 
956
  lines.push(`[Result "${resultStr}"]`);
957
  }
958
 
959
  // Add board size (without quotes - parser expects unquoted board shape)
960
- const boardStr = this.shape.z === 1
961
- ? `${this.shape.x}x${this.shape.y}` // 2D board
962
- : `${this.shape.x}x${this.shape.y}x${this.shape.z}`; // 3D board
 
963
  lines.push(`[Board ${boardStr}]`);
964
 
965
  // Add optional metadata
@@ -993,9 +1061,9 @@ export class TrigoGame {
993
  const coord = encodeAb0yz(pos, boardShape);
994
  moveStr += coord;
995
  } else if (step.type === StepType.PASS) {
996
- moveStr += "pass";
997
  } else if (step.type === StepType.SURRENDER) {
998
- moveStr += "resign";
999
  }
1000
 
1001
  moves.push(moveStr);
@@ -1035,7 +1103,6 @@ export class TrigoGame {
1035
  return lines.join("\n");
1036
  }
1037
 
1038
-
1039
  /**
1040
  * Import game from TGN (Trigo Game Notation) format
1041
  *
@@ -1103,7 +1170,6 @@ export class TrigoGame {
1103
  return game;
1104
  }
1105
 
1106
-
1107
  /**
1108
  * Apply a parsed move action to the game
1109
  * Private helper method for fromTGN
@@ -1111,12 +1177,15 @@ export class TrigoGame {
1111
  * @param action Parsed move action from TGN parser
1112
  * @param boardShape Board dimensions for coordinate decoding
1113
  */
1114
- private _applyParsedMove(action: { type: string; position?: string }, boardShape: BoardShape): void {
1115
- if (action.type === 'pass') {
 
 
 
1116
  this.pass();
1117
- } else if (action.type === 'resign') {
1118
  this.surrender();
1119
- } else if (action.type === 'move' && action.position) {
1120
  // Decode ab0yz coordinate to Position
1121
  const coords = decodeAb0yz(action.position, [boardShape.x, boardShape.y, boardShape.z]);
1122
  const position: Position = {
@@ -1131,7 +1200,5 @@ export class TrigoGame {
1131
  }
1132
  }
1133
 
1134
-
1135
  // Re-export parser utilities for convenience
1136
  export { validateTGN, TGNParseError } from "../tgn/tgnParser";
1137
-
 
15
  findCapturedGroups,
16
  executeCaptures,
17
  calculateTerritory,
18
+ isKoViolation,
19
+ isSuicideMove,
20
  type TerritoryResult
21
  } from "./gameUtils";
22
  import { encodeAb0yz, decodeAb0yz } from "./ab0yz";
23
  import { parseTGN, validateTGN, TGNParseError } from "../tgn/tgnParser";
24
 
 
25
  // Re-export StoneType for convenient access with TrigoGame
26
  export { StoneType } from "./gameUtils";
27
 
 
28
  /**
29
  * Step Types - Different types of moves in the game
30
  * Equivalent to trigo.Game.StepType in prototype
31
  */
32
  export enum StepType {
33
+ DROP = 0, // Place a stone
34
+ PASS = 1, // Pass turn
35
  SURRENDER = 2, // Resign/surrender
36
+ UNDO = 3 // Undo last move (called "REPENT" in prototype)
37
  }
38
 
 
39
  /**
40
  * Game Status enumeration
41
  */
42
  export type GameStatus = "idle" | "playing" | "paused" | "finished";
43
 
 
44
  /**
45
  * Game Result information
46
  */
 
50
  score?: TerritoryResult;
51
  }
52
 
 
53
  /**
54
  * Step - Represents a single move in the game
55
  * Equivalent to step objects in prototype's StepHistory
56
  */
57
  export interface Step {
58
  type: StepType;
59
+ position?: Position; // Only for DROP moves
60
+ player: Stone; // Which player made this move
61
  capturedPositions?: Position[]; // Stones captured by this move
62
+ timestamp: number; // When the move was made
63
  }
64
 
 
65
  /**
66
  * Game Callbacks - Event handlers for game state changes
67
  * Equivalent to Callbacks in prototype's trigo.Game constructor
 
74
  onTerritoryChange?: (territory: TerritoryResult) => void;
75
  }
76
 
 
77
  /**
78
  * TrigoGame - Main game class managing state, history, and logic
79
  *
 
109
  private territoryDirty: boolean = true;
110
  private cachedTerritory: TerritoryResult | null = null;
111
 
 
112
  /**
113
  * Constructor
114
  * Equivalent to trigo.Game constructor (lines 75-85)
 
125
  this.passCount = 0;
126
  }
127
 
 
128
  /**
129
  * Create an empty board
130
  */
 
142
  return board;
143
  }
144
 
 
145
  /**
146
  * Reset the game to initial state
147
  * Equivalent to Game.reset() (lines 153-163)
 
159
  this.passCount = 0;
160
  }
161
 
162
+ /**
163
+ * Clone the game state (deep copy)
164
+ * Creates an independent copy with all state preserved
165
+ */
166
+ clone(): TrigoGame {
167
+ const cloned = new TrigoGame(this.shape, {});
168
+
169
+ // Deep copy board
170
+ cloned.board = this.board.map((plane) => plane.map((row) => [...row]));
171
+
172
+ // Copy game state
173
+ cloned.currentPlayer = this.currentPlayer;
174
+ cloned.currentStepIndex = this.currentStepIndex;
175
+ cloned.gameStatus = this.gameStatus;
176
+ cloned.passCount = this.passCount;
177
+
178
+ // Deep copy step history
179
+ cloned.stepHistory = this.stepHistory.map((step) => ({
180
+ ...step,
181
+ position: step.position ? { ...step.position } : undefined,
182
+ capturedPositions: step.capturedPositions
183
+ ? step.capturedPositions.map((pos) => ({ ...pos }))
184
+ : []
185
+ }));
186
+
187
+ // Deep copy last captured positions
188
+ cloned.lastCapturedPositions = this.lastCapturedPositions
189
+ ? this.lastCapturedPositions.map((pos) => ({ ...pos }))
190
+ : null;
191
+
192
+ // Copy game result
193
+ if (this.gameResult) {
194
+ cloned.gameResult = {
195
+ ...this.gameResult,
196
+ score: this.gameResult.score ? { ...this.gameResult.score } : undefined
197
+ };
198
+ }
199
+
200
+ // Territory cache will be recalculated on demand
201
+ cloned.territoryDirty = true;
202
+ cloned.cachedTerritory = null;
203
+
204
+ return cloned;
205
+ }
206
 
207
  /**
208
  * Get current board state (read-only)
209
  */
210
  getBoard(): Stone[][][] {
211
  // Return a deep copy to prevent external modification
212
+ return this.board.map((plane) => plane.map((row) => [...row]));
213
  }
214
 
 
215
  /**
216
  * Get stone at specific position
217
  * Equivalent to Game.stone() (lines 95-97)
 
220
  return this.board[pos.x][pos.y][pos.z];
221
  }
222
 
 
223
  /**
224
  * Get current player
225
  */
 
227
  return this.currentPlayer;
228
  }
229
 
 
230
  /**
231
  * Get current step number
232
  * Equivalent to Game.currentStep() (lines 99-101)
 
235
  return this.currentStepIndex;
236
  }
237
 
 
238
  /**
239
  * Get move history
240
  * Equivalent to Game.routine() (lines 103-105)
 
243
  return [...this.stepHistory];
244
  }
245
 
 
246
  /**
247
  * Get last move
248
  * Equivalent to Game.lastStep() (lines 107-110)
 
254
  return null;
255
  }
256
 
 
257
  /**
258
  * Get board shape
259
  * Equivalent to Game.shape() (lines 87-89)
 
262
  return { ...this.shape };
263
  }
264
 
 
265
  /**
266
  * Get game status
267
  */
 
269
  return this.gameStatus;
270
  }
271
 
 
272
  /**
273
  * Set game status
274
  */
 
276
  this.gameStatus = status;
277
  }
278
 
 
279
  /**
280
  * Get game result
281
  */
 
283
  return this.gameResult;
284
  }
285
 
 
286
  /**
287
  * Get consecutive pass count
288
  */
 
290
  return this.passCount;
291
  }
292
 
 
293
  /**
294
  * Recalculate consecutive pass count based on current history
295
  * Counts consecutive PASS steps from the end of current history
 
307
  }
308
  }
309
 
 
310
  /**
311
  * Start the game
312
  */
 
316
  }
317
  }
318
 
 
319
  /**
320
  * Check if game is active
321
  */
 
323
  return this.gameStatus === "playing";
324
  }
325
 
 
326
  /**
327
  * Check if a move is valid
328
  * Equivalent to Game.isDropable() and Game.isValidStep() (lines 112-151)
 
332
  return validateMove(pos, playerColor, this.board, this.shape, this.lastCapturedPositions);
333
  }
334
 
335
+ /**
336
+ * Get all valid move positions for current player (efficient batch query)
337
+ *
338
+ * This method is optimized to avoid repeated validation checks by:
339
+ * 1. Only checking empty positions
340
+ * 2. Skipping bounds checking (iterator is already within bounds)
341
+ * 3. Using low-level validation functions directly
342
+ * 4. Batching board state access
343
+ *
344
+ * @param player - Optional player color (defaults to current player)
345
+ * @returns Array of all valid move positions
346
+ */
347
+ validMovePositions(player?: Stone): Position[] {
348
+ const playerColor = player || this.currentPlayer;
349
+ const validPositions: Position[] = [];
350
+
351
+ // Iterate through all board positions (bounds are guaranteed)
352
+ for (let x = 0; x < this.shape.x; x++) {
353
+ for (let y = 0; y < this.shape.y; y++) {
354
+ for (let z = 0; z < this.shape.z; z++) {
355
+ // Skip occupied positions (quick filter)
356
+ if (this.board[x][y][z] !== StoneType.EMPTY) {
357
+ continue;
358
+ }
359
+
360
+ const pos: Position = { x, y, z };
361
+
362
+ // Check Ko violation (using low-level function)
363
+ if (
364
+ isKoViolation(
365
+ pos,
366
+ playerColor,
367
+ this.board,
368
+ this.shape,
369
+ this.lastCapturedPositions
370
+ )
371
+ ) {
372
+ continue;
373
+ }
374
+
375
+ // Check suicide rule (using low-level function)
376
+ if (isSuicideMove(pos, playerColor, this.board, this.shape)) {
377
+ continue;
378
+ }
379
+
380
+ // Position is valid
381
+ validPositions.push(pos);
382
+ }
383
+ }
384
+ }
385
+
386
+ return validPositions;
387
+ }
388
 
389
  /**
390
  * Reset pass count (called when a stone is placed)
 
393
  this.passCount = 0;
394
  }
395
 
 
396
  /**
397
  * Place a stone (drop move)
398
  * Equivalent to Game.drop() and Game.appendStone() (lines 181-273)
 
449
  return true;
450
  }
451
 
 
452
  /**
453
  * Pass turn
454
  * Equivalent to PASS step type in prototype
 
495
 
496
  // Trigger win callback
497
  if (this.callbacks.onWin) {
498
+ const winnerStone =
499
+ winner === "black"
500
+ ? StoneType.BLACK
501
+ : winner === "white"
502
+ ? StoneType.WHITE
503
+ : StoneType.EMPTY;
504
  this.callbacks.onWin(winnerStone);
505
  }
506
  }
 
508
  return true;
509
  }
510
 
 
511
  /**
512
  * Surrender/resign
513
  * Equivalent to Game.step() with SURRENDER type (lines 176-178)
514
  */
515
  surrender(): boolean {
516
+ const surrenderingPlayer = this.currentPlayer; // Remember who surrendered
517
 
518
  const step: Step = {
519
  type: StepType.SURRENDER,
 
540
  return true;
541
  }
542
 
 
543
  /**
544
  * Undo last move
545
  * Equivalent to Game.repent() (lines 197-230)
 
556
  // Revert the move
557
  if (lastStep.type === StepType.DROP && lastStep.position) {
558
  // Remove the placed stone
559
+ this.board[lastStep.position.x][lastStep.position.y][lastStep.position.z] =
560
+ StoneType.EMPTY;
561
 
562
  // Restore captured stones
563
  if (lastStep.capturedPositions) {
 
595
  return true;
596
  }
597
 
 
598
  /**
599
  * Redo next move (after undo)
600
  *
 
611
  // Re-apply the move
612
  if (nextStep.type === StepType.DROP && nextStep.position) {
613
  // Place the stone
614
+ this.board[nextStep.position.x][nextStep.position.y][nextStep.position.z] =
615
+ nextStep.player;
616
 
617
  // Re-execute captures if there were any
618
  if (nextStep.capturedPositions) {
 
636
 
637
  // Trigger callback
638
  if (this.callbacks.onStepAdvance) {
639
+ this.callbacks.onStepAdvance(
640
+ nextStep,
641
+ this.stepHistory.slice(0, this.currentStepIndex)
642
+ );
643
  }
644
 
645
  return true;
646
  }
647
 
 
648
  /**
649
  * Check if redo is available
650
  */
 
652
  return this.currentStepIndex < this.stepHistory.length;
653
  }
654
 
 
655
  /**
656
  * Jump to specific step in history
657
  * Rebuilds board state after applying the first 'index' moves
 
733
  return true;
734
  }
735
 
 
736
  /**
737
  * Advance to next step
738
  * Equivalent to Game.stepAdvance() (lines 279-287)
 
756
  }
757
  }
758
 
 
759
  /**
760
  * Get territory calculation
761
  * Equivalent to Game.blackDomain() and Game.whiteDomain() (lines 232-244)
 
770
  return this.cachedTerritory;
771
  }
772
 
 
773
  /**
774
  * Get captured stone counts up to current position in history
775
  * Only counts captures that have been played (up to currentStepIndex)
 
794
  return counts;
795
  }
796
 
 
797
  /**
798
  * Serialize game state to JSON
799
  * Equivalent to Game.serialize() (lines 250-252)
 
811
  };
812
  }
813
 
 
814
  /**
815
  * Load game state from JSON
816
  */
 
852
  }
853
  }
854
 
 
855
  /**
856
  * Get game statistics
857
  */
 
889
  };
890
  }
891
 
 
892
  /**
893
  * Save game state to sessionStorage
894
  *
 
912
  return false;
913
  }
914
 
 
915
  /**
916
  * Load game state from sessionStorage
917
  *
 
940
  return false;
941
  }
942
 
 
943
  /**
944
  * Clear saved game state from sessionStorage
945
  *
 
958
  }
959
  }
960
 
 
961
  /**
962
  * Export game to TGN (Trigo Game Notation) format
963
  *
 
1017
  const diff = Math.abs(black - white);
1018
  resultStr += `${diff}points`;
1019
  } else if (this.gameResult.reason === "resignation") {
1020
+ resultStr += "Resign";
1021
  }
1022
 
1023
  lines.push(`[Result "${resultStr}"]`);
1024
  }
1025
 
1026
  // Add board size (without quotes - parser expects unquoted board shape)
1027
+ const boardStr =
1028
+ this.shape.z === 1
1029
+ ? `${this.shape.x}x${this.shape.y}` // 2D board
1030
+ : `${this.shape.x}x${this.shape.y}x${this.shape.z}`; // 3D board
1031
  lines.push(`[Board ${boardStr}]`);
1032
 
1033
  // Add optional metadata
 
1061
  const coord = encodeAb0yz(pos, boardShape);
1062
  moveStr += coord;
1063
  } else if (step.type === StepType.PASS) {
1064
+ moveStr += "Pass";
1065
  } else if (step.type === StepType.SURRENDER) {
1066
+ moveStr += "Resign";
1067
  }
1068
 
1069
  moves.push(moveStr);
 
1103
  return lines.join("\n");
1104
  }
1105
 
 
1106
  /**
1107
  * Import game from TGN (Trigo Game Notation) format
1108
  *
 
1170
  return game;
1171
  }
1172
 
 
1173
  /**
1174
  * Apply a parsed move action to the game
1175
  * Private helper method for fromTGN
 
1177
  * @param action Parsed move action from TGN parser
1178
  * @param boardShape Board dimensions for coordinate decoding
1179
  */
1180
+ private _applyParsedMove(
1181
+ action: { type: string; position?: string },
1182
+ boardShape: BoardShape
1183
+ ): void {
1184
+ if (action.type === "pass") {
1185
  this.pass();
1186
+ } else if (action.type === "resign") {
1187
  this.surrender();
1188
+ } else if (action.type === "move" && action.position) {
1189
  // Decode ab0yz coordinate to Position
1190
  const coords = decodeAb0yz(action.position, [boardShape.x, boardShape.y, boardShape.z]);
1191
  const position: Position = {
 
1200
  }
1201
  }
1202
 
 
1203
  // Re-export parser utilities for convenience
1204
  export { validateTGN, TGNParseError } from "../tgn/tgnParser";
 
trigo-web/inc/trigo/gameUtils.ts CHANGED
@@ -13,7 +13,6 @@ export const StoneType = {
13
  WHITE: 2 as Stone
14
  };
15
 
16
-
17
  /**
18
  * Get the enemy color of a given stone
19
  *
@@ -25,19 +24,20 @@ export function getEnemyColor(color: Stone): Stone {
25
  return StoneType.EMPTY;
26
  }
27
 
28
-
29
  /**
30
  * Check if a position is within board bounds
31
  */
32
  export function isInBounds(pos: Position, shape: BoardShape): boolean {
33
  return (
34
- pos.x >= 0 && pos.x < shape.x &&
35
- pos.y >= 0 && pos.y < shape.y &&
36
- pos.z >= 0 && pos.z < shape.z
 
 
 
37
  );
38
  }
39
 
40
-
41
  /**
42
  * Get all neighboring positions (up to 6 in 3D: ±x, ±y, ±z)
43
  *
@@ -71,7 +71,6 @@ export function getNeighbors(pos: Position, shape: BoardShape): Position[] {
71
  return neighbors;
72
  }
73
 
74
-
75
  /**
76
  * Compare two positions for equality
77
  */
@@ -79,7 +78,6 @@ export function positionsEqual(p1: Position, p2: Position): boolean {
79
  return p1.x === p2.x && p1.y === p2.y && p1.z === p2.z;
80
  }
81
 
82
-
83
  /**
84
  * Coordinate Set - manages a set of positions (stones in a group or liberties)
85
  */
@@ -123,7 +121,6 @@ export class CoordSet {
123
  }
124
  }
125
 
126
-
127
  /**
128
  * Patch - represents a connected group of same-colored stones
129
  *
@@ -168,15 +165,10 @@ export class Patch {
168
  }
169
  }
170
 
171
-
172
  /**
173
  * Find the connected group of stones at a given position
174
  */
175
- export function findGroup(
176
- pos: Position,
177
- board: Stone[][][],
178
- shape: BoardShape
179
- ): Patch {
180
  const color = board[pos.x][pos.y][pos.z];
181
  const group = new Patch(color);
182
 
@@ -212,7 +204,6 @@ export function findGroup(
212
  return group;
213
  }
214
 
215
-
216
  /**
217
  * Get all neighboring groups (different from current position's color)
218
  */
@@ -247,20 +238,14 @@ export function getNeighborGroups(
247
  return groups;
248
  }
249
 
250
-
251
  /**
252
  * Check if a group would be captured (has no liberties)
253
  */
254
- export function isGroupCaptured(
255
- group: Patch,
256
- board: Stone[][][],
257
- shape: BoardShape
258
- ): boolean {
259
  const liberties = group.getLiberties(board, shape);
260
  return liberties.size() === 0;
261
  }
262
 
263
-
264
  /**
265
  * Find all groups that would be captured by placing a stone at position
266
  *
@@ -297,7 +282,6 @@ export function findCapturedGroups(
297
  return captured;
298
  }
299
 
300
-
301
  /**
302
  * Check if placing a stone at position would result in self-capture (suicide)
303
  *
@@ -328,7 +312,6 @@ export function isSuicideMove(
328
  return liberties.size() === 0;
329
  }
330
 
331
-
332
  /**
333
  * Ko Detection - check if move would recreate previous board state
334
  *
@@ -370,15 +353,11 @@ export function isKoViolation(
370
  return false;
371
  }
372
 
373
-
374
  /**
375
  * Execute captures on the board
376
  * Returns the positions of captured stones
377
  */
378
- export function executeCaptures(
379
- capturedGroups: Patch[],
380
- board: Stone[][][]
381
- ): Position[] {
382
  const capturedPositions: Position[] = [];
383
 
384
  for (const group of capturedGroups) {
@@ -391,7 +370,6 @@ export function executeCaptures(
391
  return capturedPositions;
392
  }
393
 
394
-
395
  /**
396
  * Territory Calculation
397
  *
@@ -408,7 +386,6 @@ export interface TerritoryResult {
408
  neutralTerritory: Position[];
409
  }
410
 
411
-
412
  /**
413
  * Find all connected empty positions starting from a position
414
  */
@@ -445,7 +422,6 @@ function findEmptyRegion(
445
  return region;
446
  }
447
 
448
-
449
  /**
450
  * Determine which player owns an empty region
451
  * Returns BLACK, WHITE, or EMPTY (neutral/dame)
@@ -453,21 +429,17 @@ function findEmptyRegion(
453
  * Equivalent to PatchList.spaceDomain() in prototype (lines 561-585)
454
  * An empty region belongs to a player if ALL bordering stones are that color
455
  */
456
- function determineRegionOwner(
457
- region: CoordSet,
458
- board: Stone[][][],
459
- shape: BoardShape
460
- ): Stone {
461
  let owner: Stone = StoneType.EMPTY;
462
- let solved = false; // Flag to break out when we find mixed colors
463
 
464
  region.forEach((pos) => {
465
- if (solved) return; // Skip if already determined to be neutral
466
 
467
  const neighbors = getNeighbors(pos, shape);
468
 
469
  for (const neighbor of neighbors) {
470
- if (solved) break; // Skip if already determined to be neutral
471
 
472
  const stone = board[neighbor.x][neighbor.y][neighbor.z];
473
 
@@ -478,7 +450,7 @@ function determineRegionOwner(
478
  } else if (owner !== stone) {
479
  // Found a different colored stone - region is neutral
480
  owner = StoneType.EMPTY;
481
- solved = true; // Mark as solved so we stop checking
482
  }
483
  }
484
  }
@@ -487,7 +459,6 @@ function determineRegionOwner(
487
  return owner;
488
  }
489
 
490
-
491
  /**
492
  * Calculate territory for both players
493
  *
@@ -498,10 +469,7 @@ function determineRegionOwner(
498
  * 1. First pass: Add all stones to their player's territory, collect empty regions
499
  * 2. Second pass: Determine ownership of each empty region
500
  */
501
- export function calculateTerritory(
502
- board: Stone[][][],
503
- shape: BoardShape
504
- ): TerritoryResult {
505
  const result: TerritoryResult = {
506
  black: 0,
507
  white: 0,
@@ -556,7 +524,6 @@ export function calculateTerritory(
556
  return result;
557
  }
558
 
559
-
560
  /**
561
  * Validate if a move is legal
562
  *
@@ -568,7 +535,6 @@ export interface MoveValidation {
568
  reason?: string;
569
  }
570
 
571
-
572
  export function validateMove(
573
  pos: Position,
574
  playerColor: Stone,
 
13
  WHITE: 2 as Stone
14
  };
15
 
 
16
  /**
17
  * Get the enemy color of a given stone
18
  *
 
24
  return StoneType.EMPTY;
25
  }
26
 
 
27
  /**
28
  * Check if a position is within board bounds
29
  */
30
  export function isInBounds(pos: Position, shape: BoardShape): boolean {
31
  return (
32
+ pos.x >= 0 &&
33
+ pos.x < shape.x &&
34
+ pos.y >= 0 &&
35
+ pos.y < shape.y &&
36
+ pos.z >= 0 &&
37
+ pos.z < shape.z
38
  );
39
  }
40
 
 
41
  /**
42
  * Get all neighboring positions (up to 6 in 3D: ±x, ±y, ±z)
43
  *
 
71
  return neighbors;
72
  }
73
 
 
74
  /**
75
  * Compare two positions for equality
76
  */
 
78
  return p1.x === p2.x && p1.y === p2.y && p1.z === p2.z;
79
  }
80
 
 
81
  /**
82
  * Coordinate Set - manages a set of positions (stones in a group or liberties)
83
  */
 
121
  }
122
  }
123
 
 
124
  /**
125
  * Patch - represents a connected group of same-colored stones
126
  *
 
165
  }
166
  }
167
 
 
168
  /**
169
  * Find the connected group of stones at a given position
170
  */
171
+ export function findGroup(pos: Position, board: Stone[][][], shape: BoardShape): Patch {
 
 
 
 
172
  const color = board[pos.x][pos.y][pos.z];
173
  const group = new Patch(color);
174
 
 
204
  return group;
205
  }
206
 
 
207
  /**
208
  * Get all neighboring groups (different from current position's color)
209
  */
 
238
  return groups;
239
  }
240
 
 
241
  /**
242
  * Check if a group would be captured (has no liberties)
243
  */
244
+ export function isGroupCaptured(group: Patch, board: Stone[][][], shape: BoardShape): boolean {
 
 
 
 
245
  const liberties = group.getLiberties(board, shape);
246
  return liberties.size() === 0;
247
  }
248
 
 
249
  /**
250
  * Find all groups that would be captured by placing a stone at position
251
  *
 
282
  return captured;
283
  }
284
 
 
285
  /**
286
  * Check if placing a stone at position would result in self-capture (suicide)
287
  *
 
312
  return liberties.size() === 0;
313
  }
314
 
 
315
  /**
316
  * Ko Detection - check if move would recreate previous board state
317
  *
 
353
  return false;
354
  }
355
 
 
356
  /**
357
  * Execute captures on the board
358
  * Returns the positions of captured stones
359
  */
360
+ export function executeCaptures(capturedGroups: Patch[], board: Stone[][][]): Position[] {
 
 
 
361
  const capturedPositions: Position[] = [];
362
 
363
  for (const group of capturedGroups) {
 
370
  return capturedPositions;
371
  }
372
 
 
373
  /**
374
  * Territory Calculation
375
  *
 
386
  neutralTerritory: Position[];
387
  }
388
 
 
389
  /**
390
  * Find all connected empty positions starting from a position
391
  */
 
422
  return region;
423
  }
424
 
 
425
  /**
426
  * Determine which player owns an empty region
427
  * Returns BLACK, WHITE, or EMPTY (neutral/dame)
 
429
  * Equivalent to PatchList.spaceDomain() in prototype (lines 561-585)
430
  * An empty region belongs to a player if ALL bordering stones are that color
431
  */
432
+ function determineRegionOwner(region: CoordSet, board: Stone[][][], shape: BoardShape): Stone {
 
 
 
 
433
  let owner: Stone = StoneType.EMPTY;
434
+ let solved = false; // Flag to break out when we find mixed colors
435
 
436
  region.forEach((pos) => {
437
+ if (solved) return; // Skip if already determined to be neutral
438
 
439
  const neighbors = getNeighbors(pos, shape);
440
 
441
  for (const neighbor of neighbors) {
442
+ if (solved) break; // Skip if already determined to be neutral
443
 
444
  const stone = board[neighbor.x][neighbor.y][neighbor.z];
445
 
 
450
  } else if (owner !== stone) {
451
  // Found a different colored stone - region is neutral
452
  owner = StoneType.EMPTY;
453
+ solved = true; // Mark as solved so we stop checking
454
  }
455
  }
456
  }
 
459
  return owner;
460
  }
461
 
 
462
  /**
463
  * Calculate territory for both players
464
  *
 
469
  * 1. First pass: Add all stones to their player's territory, collect empty regions
470
  * 2. Second pass: Determine ownership of each empty region
471
  */
472
+ export function calculateTerritory(board: Stone[][][], shape: BoardShape): TerritoryResult {
 
 
 
473
  const result: TerritoryResult = {
474
  black: 0,
475
  white: 0,
 
524
  return result;
525
  }
526
 
 
527
  /**
528
  * Validate if a move is legal
529
  *
 
535
  reason?: string;
536
  }
537
 
 
538
  export function validateMove(
539
  pos: Position,
540
  playerColor: Stone,
trigo-web/inc/trigo/index.ts CHANGED
@@ -1,4 +1,3 @@
1
-
2
  export * from "./types";
3
  export * from "./gameUtils";
4
  export * from "./game";
 
 
1
  export * from "./types";
2
  export * from "./gameUtils";
3
  export * from "./game";
trigo-web/inc/trigo/parserInit.ts CHANGED
@@ -16,7 +16,6 @@
16
 
17
  import { setParserModule } from "../tgn/tgnParser";
18
 
19
-
20
  /**
21
  * Check if we're in a browser environment
22
  */
@@ -24,15 +23,17 @@ function isBrowser(): boolean {
24
  return typeof window !== "undefined" && typeof document !== "undefined";
25
  }
26
 
27
-
28
  /**
29
  * Check if we're in a Node.js environment
30
  */
31
  function isNode(): boolean {
32
- return typeof process !== "undefined" && !!(process as any).versions && !!(process as any).versions.node;
 
 
 
 
33
  }
34
 
35
-
36
  /**
37
  * Initialize all parsers for use in the application
38
  * Should be called once at application startup
@@ -55,7 +56,6 @@ export async function initializeParsers(): Promise<void> {
55
  }
56
  }
57
 
58
-
59
  /**
60
  * Initialize parsers for browser environment
61
  * Loads parsers from /lib/ which is served by Vite in dev and included in build
@@ -82,7 +82,9 @@ async function initializeParsersForBrowser(): Promise<void> {
82
 
83
  // We need 'require' to be defined (but can be a dummy)
84
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
85
- const require = function() { throw new Error("require not available in browser"); };
 
 
86
 
87
  // Eval in a scope where module, exports, and require are defined
88
  // eslint-disable-next-line no-eval
@@ -92,7 +94,7 @@ async function initializeParsersForBrowser(): Promise<void> {
92
  // So module.exports.parser is the actual parser instance
93
  const parser = module.exports.parser;
94
 
95
- if (!parser || typeof parser.parse !== 'function') {
96
  throw new Error("Parser loaded but parse method not available");
97
  }
98
 
@@ -103,7 +105,6 @@ async function initializeParsersForBrowser(): Promise<void> {
103
  }
104
  }
105
 
106
-
107
  /**
108
  * Initialize parsers for Node.js environment
109
  * Loads parsers from project's public/lib directory
 
16
 
17
  import { setParserModule } from "../tgn/tgnParser";
18
 
 
19
  /**
20
  * Check if we're in a browser environment
21
  */
 
23
  return typeof window !== "undefined" && typeof document !== "undefined";
24
  }
25
 
 
26
  /**
27
  * Check if we're in a Node.js environment
28
  */
29
  function isNode(): boolean {
30
+ return (
31
+ typeof process !== "undefined" &&
32
+ !!(process as any).versions &&
33
+ !!(process as any).versions.node
34
+ );
35
  }
36
 
 
37
  /**
38
  * Initialize all parsers for use in the application
39
  * Should be called once at application startup
 
56
  }
57
  }
58
 
 
59
  /**
60
  * Initialize parsers for browser environment
61
  * Loads parsers from /lib/ which is served by Vite in dev and included in build
 
82
 
83
  // We need 'require' to be defined (but can be a dummy)
84
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
85
+ const require = function () {
86
+ throw new Error("require not available in browser");
87
+ };
88
 
89
  // Eval in a scope where module, exports, and require are defined
90
  // eslint-disable-next-line no-eval
 
94
  // So module.exports.parser is the actual parser instance
95
  const parser = module.exports.parser;
96
 
97
+ if (!parser || typeof parser.parse !== "function") {
98
  throw new Error("Parser loaded but parse method not available");
99
  }
100
 
 
105
  }
106
  }
107
 
 
108
  /**
109
  * Initialize parsers for Node.js environment
110
  * Loads parsers from project's public/lib directory
trigo-web/inc/trigo/typeAdapters.ts CHANGED
@@ -11,7 +11,6 @@ import type { Stone, Position, Player, Move } from "./types";
11
  import { StoneType } from "./game";
12
  import type { Step } from "./game";
13
 
14
-
15
  /**
16
  * Convert Player string to Stone number
17
  *
@@ -22,7 +21,6 @@ export function playerToStone(player: Player): Stone {
22
  return player === "black" ? StoneType.BLACK : StoneType.WHITE;
23
  }
24
 
25
-
26
  /**
27
  * Convert Stone number to Player string
28
  *
@@ -39,7 +37,6 @@ export function stoneToPlayer(stone: Stone): Player {
39
  return "black";
40
  }
41
 
42
-
43
  /**
44
  * Convert Step (TrigoGame format) to Move (frontend format)
45
  *
@@ -60,14 +57,14 @@ export function stepToMove(step: Step): Move {
60
  }
61
 
62
  // If it's a PASS move, set flag
63
- if (step.type === 1) { // StepType.PASS
 
64
  move.isPass = true;
65
  }
66
 
67
  return move;
68
  }
69
 
70
-
71
  /**
72
  * Convert array of Steps to array of Moves
73
  *
@@ -78,7 +75,6 @@ export function stepsToMoves(steps: Step[]): Move[] {
78
  return steps.map(stepToMove);
79
  }
80
 
81
-
82
  /**
83
  * Convert board (Stone[][][]) to frontend format (number[][][])
84
  *
@@ -89,14 +85,9 @@ export function stepsToMoves(steps: Step[]): Move[] {
89
  * @returns Frontend board format
90
  */
91
  export function boardToNumbers(board: Stone[][][]): number[][][] {
92
- return board.map(plane =>
93
- plane.map(row =>
94
- row.map(cell => cell as number)
95
- )
96
- );
97
  }
98
 
99
-
100
  /**
101
  * Helper: Create Position object from coordinates
102
  *
 
11
  import { StoneType } from "./game";
12
  import type { Step } from "./game";
13
 
 
14
  /**
15
  * Convert Player string to Stone number
16
  *
 
21
  return player === "black" ? StoneType.BLACK : StoneType.WHITE;
22
  }
23
 
 
24
  /**
25
  * Convert Stone number to Player string
26
  *
 
37
  return "black";
38
  }
39
 
 
40
  /**
41
  * Convert Step (TrigoGame format) to Move (frontend format)
42
  *
 
57
  }
58
 
59
  // If it's a PASS move, set flag
60
+ if (step.type === 1) {
61
+ // StepType.PASS
62
  move.isPass = true;
63
  }
64
 
65
  return move;
66
  }
67
 
 
68
  /**
69
  * Convert array of Steps to array of Moves
70
  *
 
75
  return steps.map(stepToMove);
76
  }
77
 
 
78
  /**
79
  * Convert board (Stone[][][]) to frontend format (number[][][])
80
  *
 
85
  * @returns Frontend board format
86
  */
87
  export function boardToNumbers(board: Stone[][][]): number[][][] {
88
+ return board.map((plane) => plane.map((row) => row.map((cell) => cell as number)));
 
 
 
 
89
  }
90
 
 
91
  /**
92
  * Helper: Create Position object from coordinates
93
  *
trigo-web/inc/trigoAgent.ts ADDED
@@ -0,0 +1,262 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Trigo AI Agent - Language Model-based Move Selection (Frontend/Backend Common)
3
+ *
4
+ * Platform-agnostic AI agent that accepts ONNX session from platform-specific code.
5
+ * No direct dependency on onnxruntime packages - uses dependency injection pattern.
6
+ *
7
+ * Uses ONNX language model to score and select moves by:
8
+ * 1. Getting all valid moves for current position
9
+ * 2. Scoring each move by appending it to TGN and computing token probabilities
10
+ * 3. Selecting move with highest probability (argmax)
11
+ */
12
+
13
+ import { ModelInferencer } from "./modelInferencer";
14
+ import { TrigoGame, StoneType } from "./trigo/game";
15
+ import type { Move, Stone } from "./trigo/types";
16
+
17
+ /**
18
+ * Configuration for the AI agent
19
+ */
20
+ export interface TrigoAgentConfig {
21
+ vocabSize?: number;
22
+ seqLen?: number;
23
+ temperature?: number;
24
+ }
25
+
26
+ /**
27
+ * Move score result
28
+ */
29
+ export interface MoveScore {
30
+ move: Move;
31
+ score: number;
32
+ logProb: number;
33
+ }
34
+
35
+ /**
36
+ * Trigo AI Agent for move generation
37
+ * Compatible with both frontend (onnxruntime-web) and backend (onnxruntime-node)
38
+ */
39
+ export class TrigoAgent {
40
+ private inferencer: ModelInferencer;
41
+
42
+ constructor(inferencer: ModelInferencer) {
43
+ this.inferencer = inferencer;
44
+ }
45
+
46
+ /**
47
+ * Check if agent is initialized (checks if inferencer has a session)
48
+ */
49
+ isInitialized(): boolean {
50
+ // Agent is initialized if the inferencer has been set up
51
+ return this.inferencer !== null;
52
+ }
53
+
54
+ /**
55
+ * Convert Stone type to player string
56
+ */
57
+ private stoneToPlayer(stone: Stone): "black" | "white" {
58
+ if (stone === StoneType.BLACK) return "black";
59
+ if (stone === StoneType.WHITE) return "white";
60
+ throw new Error(`Invalid stone type: ${stone}`);
61
+ }
62
+
63
+ /**
64
+ * Convert string to token IDs (byte-level encoding)
65
+ */
66
+ private stringToTokens(text: string): number[] {
67
+ return Array.from(text).map((char) => char.charCodeAt(0));
68
+ }
69
+
70
+ /**
71
+ * Compute softmax probabilities from logits
72
+ */
73
+ private softmax(logits: Float32Array, vocabSize: number): Float32Array {
74
+ const probs = new Float32Array(vocabSize);
75
+ let maxLogit = -Infinity;
76
+
77
+ // Find max for numerical stability
78
+ for (let i = 0; i < vocabSize; i++) {
79
+ if (logits[i] > maxLogit) {
80
+ maxLogit = logits[i];
81
+ }
82
+ }
83
+
84
+ // Compute exp and sum
85
+ let sum = 0;
86
+ for (let i = 0; i < vocabSize; i++) {
87
+ probs[i] = Math.exp(logits[i] - maxLogit);
88
+ sum += probs[i];
89
+ }
90
+
91
+ // Normalize
92
+ for (let i = 0; i < vocabSize; i++) {
93
+ probs[i] /= sum;
94
+ }
95
+
96
+ return probs;
97
+ }
98
+
99
+ /**
100
+ * Score a candidate move by computing token probabilities
101
+ *
102
+ * Clones the game, applies the move, generates new TGN, and computes
103
+ * the probability of the move tokens.
104
+ */
105
+ async scoreMove(game: TrigoGame, move: Move): Promise<number> {
106
+ // Clone the game
107
+ const clonedGame = game.clone();
108
+
109
+ // Apply the move to the cloned game
110
+ let success: boolean;
111
+ if (move.isPass) {
112
+ success = clonedGame.pass();
113
+ } else if (move.x !== undefined && move.y !== undefined && move.z !== undefined) {
114
+ success = clonedGame.drop({ x: move.x, y: move.y, z: move.z });
115
+ } else {
116
+ // Invalid move format
117
+ return -1000;
118
+ }
119
+
120
+ if (!success) {
121
+ // Invalid move, return very low probability
122
+ return -1000;
123
+ }
124
+
125
+ // Generate TGN from both original and cloned game
126
+ const newTGN = clonedGame.toTGN().trim();
127
+
128
+ // Extract the move substring
129
+ // The move should be the new content added after the current TGN
130
+ const moveTokens = this.extractMoveTokens(newTGN);
131
+
132
+ if (moveTokens.length === 0) {
133
+ // Could not extract move, return low probability
134
+ return -100;
135
+ }
136
+
137
+ // Convert new TGN to tokens
138
+ const tokens = this.stringToTokens(newTGN);
139
+
140
+ // Get configuration
141
+ const config = this.inferencer.getConfig();
142
+ const seqLen = config.seqLen;
143
+ const vocabSize = config.vocabSize;
144
+
145
+ // Truncate if too long
146
+ if (tokens.length > seqLen) {
147
+ tokens.splice(0, tokens.length - seqLen);
148
+ }
149
+
150
+ // Run inference (START_TOKEN will be prepended by inferencer)
151
+ const logits = await this.inferencer.runInference(tokens);
152
+
153
+ // Compute probability for the move tokens
154
+ // Note: inferencer prepends START_TOKEN, so positions are offset by +1
155
+ // Token sequence: [START_TOKEN, ...tokens, PAD, PAD, ...]
156
+ // Position in output: token_i is at position i+1 in the padded sequence
157
+
158
+ // Find where move tokens start in original token sequence
159
+ const moveStartInTokens = tokens.length - moveTokens.length;
160
+
161
+ let logProb = 0;
162
+ for (let i = 0; i < moveTokens.length; i++) {
163
+ // Position of this move token in the original tokens array
164
+ const tokenPos = moveStartInTokens + i;
165
+
166
+ // Skip if position is out of bounds
167
+ // Logits at position tokenPos predict the token at tokenPos+1
168
+ if (tokenPos < 0 || tokenPos >= tokens.length) continue;
169
+
170
+ const offset = tokenPos * vocabSize;
171
+ const tokenLogits = logits.slice(offset, offset + vocabSize);
172
+ const probs = this.softmax(tokenLogits, vocabSize);
173
+
174
+ const tokenId = moveTokens[i];
175
+ const prob = probs[tokenId];
176
+
177
+ if (prob > 0) {
178
+ logProb += Math.log(prob);
179
+ } else {
180
+ // If probability is zero, assign very low prob
181
+ logProb += -100;
182
+ }
183
+ }
184
+
185
+ return logProb;
186
+ }
187
+
188
+ /**
189
+ * Extract move tokens from TGN difference
190
+ * Returns the tokens that were added between currentTGN and newTGN
191
+ */
192
+ private extractMoveTokens(tgn: string): number[] {
193
+ const moveCapture = tgn.match(/[Pa-z0]+$/);
194
+
195
+ return this.stringToTokens(moveCapture ? moveCapture[0] : "");
196
+ }
197
+
198
+ /**
199
+ * Select the best move using the language model
200
+ *
201
+ * Scores all valid moves and returns the one with highest probability (argmax).
202
+ */
203
+ async selectBestMove(game: TrigoGame): Promise<Move | null> {
204
+ if (!this.isInitialized()) {
205
+ throw new Error("Agent not initialized. Pass initialized inferencer to constructor.");
206
+ }
207
+
208
+ console.log("[TrigoAgent] Selecting move...");
209
+
210
+ // Get current player as string
211
+ const currentPlayer = this.stoneToPlayer(game.getCurrentPlayer());
212
+
213
+ // Get all valid moves
214
+ const validMoves: Move[] = game.validMovePositions().map((pos) => ({
215
+ x: pos.x,
216
+ y: pos.y,
217
+ z: pos.z,
218
+ player: currentPlayer
219
+ }));
220
+ validMoves.push({ player: currentPlayer, isPass: true }); // Add pass move
221
+
222
+ if (validMoves.length === 0) {
223
+ console.log("[TrigoAgent] No valid moves available");
224
+ return null;
225
+ }
226
+
227
+ console.log(`[TrigoAgent] Evaluating ${validMoves.length} valid moves...`);
228
+
229
+ // Score each move
230
+ const scores: MoveScore[] = [];
231
+ for (const move of validMoves) {
232
+ const logProb = await this.scoreMove(game, move);
233
+
234
+ scores.push({
235
+ move,
236
+ score: Math.exp(logProb), // Convert log prob to probability
237
+ logProb
238
+ });
239
+ }
240
+
241
+ // Find best move (argmax)
242
+ scores.sort((a, b) => b.logProb - a.logProb);
243
+ const bestMove = scores[0];
244
+ console.debug("scores:", scores);
245
+
246
+ console.log("[TrigoAgent] Best move:", bestMove.move, "score:", bestMove.score.toFixed(6));
247
+ console.log("[TrigoAgent] Top 5 moves:");
248
+ for (let i = 0; i < Math.min(5, scores.length); i++) {
249
+ console.log(` ${i + 1}. ${scores[i].move}: ${scores[i].score.toFixed(6)}`);
250
+ }
251
+
252
+ return bestMove.move;
253
+ }
254
+
255
+ /**
256
+ * Clean up resources
257
+ */
258
+ destroy(): void {
259
+ this.inferencer.destroy();
260
+ console.log("[TrigoAgent] Destroyed");
261
+ }
262
+ }
trigo-web/inc/trigoTreeAgent.ts ADDED
@@ -0,0 +1,415 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Trigo Tree Agent - AI agent using tree attention for efficient move evaluation
3
+ *
4
+ * Uses evaluation mode ONNX model to score all valid moves in parallel.
5
+ * Organizes moves as a prefix tree where branches with same head token are merged.
6
+ */
7
+
8
+ import { ModelInferencer } from "./modelInferencer";
9
+ import type { EvaluationInputs } from "./modelInferencer";
10
+ import { TrigoGame, StoneType } from "./trigo/game";
11
+ import type { Move, Stone, Position } from "./trigo/types";
12
+ import { encodeAb0yz } from "./trigo/ab0yz";
13
+
14
+ export interface ScoredMove {
15
+ move: Move;
16
+ score: number; // Log probability
17
+ notation: string; // TGN notation (e.g., "ab0")
18
+ }
19
+
20
+ export class TrigoTreeAgent {
21
+ private inferencer: ModelInferencer;
22
+
23
+ constructor(inferencer: ModelInferencer) {
24
+ this.inferencer = inferencer;
25
+ }
26
+
27
+ /**
28
+ * Convert Stone type to player string
29
+ */
30
+ private stoneToPlayer(stone: Stone): "black" | "white" {
31
+ if (stone === StoneType.BLACK) return "black";
32
+ if (stone === StoneType.WHITE) return "white";
33
+ throw new Error(`Invalid stone type: ${stone}`);
34
+ }
35
+
36
+ /**
37
+ * Encode a position to TGN notation (3 characters for 5×5×5 board)
38
+ */
39
+ private positionToTGN(pos: Position, shape: { x: number; y: number; z: number }): string {
40
+ const posArray = [pos.x, pos.y, pos.z];
41
+ const shapeArray = [shape.x, shape.y, shape.z];
42
+ return encodeAb0yz(posArray, shapeArray);
43
+ }
44
+
45
+ /**
46
+ * Convert string to byte tokens (ASCII encoding)
47
+ */
48
+ private stringToTokens(str: string): number[] {
49
+ const tokens: number[] = [];
50
+ for (let i = 0; i < str.length; i++) {
51
+ tokens.push(str.charCodeAt(i));
52
+ }
53
+ return tokens;
54
+ }
55
+
56
+ /**
57
+ * Build prefix tree from token arrays using recursive merging
58
+ * Merges branches with the same token at EVERY level
59
+ *
60
+ * Algorithm:
61
+ * 1. Group sequences by their first token
62
+ * 2. For each group:
63
+ * - Create one node for the shared first token
64
+ * - Extract remaining tokens (residues)
65
+ * - Recursively build subtree from residues
66
+ * 3. Combine all subtrees and build attention mask
67
+ *
68
+ * Example for ["aa", "ab", "ba", "bb"]:
69
+ * Level 1: Group by first token → 'a': ["a","b"], 'b': ["a","b"]
70
+ * Level 2: Within 'a' group, build subtree for ["a","b"]
71
+ * Within 'b' group, build subtree for ["a","b"]
72
+ * Result: Two branches, each with properly merged second-level nodes
73
+ *
74
+ * @param tokenArrays - Array of token arrays
75
+ * @returns Flattened token array (length m), mask matrix (m×m), and move-to-position mapping
76
+ */
77
+ private buildPrefixTree(tokenArrays: number[][]): {
78
+ evaluatedIds: number[];
79
+ mask: number[];
80
+ moveToLeafPos: number[];
81
+ } {
82
+ type Seq = { moveIndex: number; tokens: number[] };
83
+
84
+ interface Node {
85
+ token: number;
86
+ pos: number;
87
+ parent: number | null;
88
+ children: Node[];
89
+ moveEnds: number[];
90
+ }
91
+
92
+ let nextPos = 0;
93
+
94
+ // --- Build prefix tree through recursive grouping ---
95
+ function build(seqs: Seq[], parent: number | null): Node[] {
96
+ // group by token
97
+ const groups = new Map<number, Seq[]>();
98
+ for (const s of seqs) {
99
+ if (s.tokens.length === 0) continue;
100
+ const t = s.tokens[0];
101
+ if (!groups.has(t)) groups.set(t, []);
102
+ groups.get(t)!.push(s);
103
+ }
104
+
105
+ const levelNodes: Node[] = [];
106
+
107
+ for (const [token, group] of groups) {
108
+ const pos = nextPos++;
109
+ const node: Node = {
110
+ token,
111
+ pos,
112
+ parent,
113
+ children: [],
114
+ moveEnds: []
115
+ };
116
+
117
+ // split residues
118
+ const ends: number[] = [];
119
+ const residues: Seq[] = [];
120
+
121
+ for (const g of group) {
122
+ if (g.tokens.length === 1) ends.push(g.moveIndex);
123
+ else residues.push({ moveIndex: g.moveIndex, tokens: g.tokens.slice(1) });
124
+ }
125
+
126
+ node.moveEnds = ends;
127
+
128
+ // create sub nodes recursively
129
+ if (residues.length > 0) {
130
+ node.children = build(residues, pos);
131
+ }
132
+
133
+ levelNodes.push(node);
134
+ }
135
+ return levelNodes;
136
+ }
137
+
138
+ // Build roots
139
+ const seqs = tokenArrays.map((t, i) => ({ moveIndex: i, tokens: t }));
140
+ const roots = build(seqs, null);
141
+ const total = nextPos;
142
+
143
+ // --- Flatten tree ---
144
+ const evaluatedIds = new Array<number>(total);
145
+ const parent = new Array<number | null>(total).fill(null);
146
+ const moveToLeafPos = new Array<number>(tokenArrays.length).fill(-1);
147
+
148
+ function dfs(n: Node) {
149
+ evaluatedIds[n.pos] = n.token;
150
+ parent[n.pos] = n.parent;
151
+
152
+ for (const m of n.moveEnds) moveToLeafPos[m] = n.pos;
153
+ for (const c of n.children) dfs(c);
154
+ }
155
+ for (const r of roots) dfs(r);
156
+
157
+ // --- Build ancestor mask ---
158
+ const mask = new Array(total * total).fill(0);
159
+ for (let i = 0; i < total; i++) {
160
+ let p = i;
161
+ while (p !== null) {
162
+ mask[i * total + p] = 1;
163
+ p = parent[p]!;
164
+ }
165
+ }
166
+
167
+ return { evaluatedIds, mask, moveToLeafPos };
168
+ }
169
+
170
+ /**
171
+ * Build tree structure for all valid moves
172
+ * Returns prefix tokens and tree structure for batch evaluation
173
+ */
174
+ private buildMoveTree(
175
+ game: TrigoGame,
176
+ moves: Move[]
177
+ ): {
178
+ prefixTokens: number[];
179
+ evaluatedIds: number[];
180
+ mask: number[];
181
+ moveData: Array<{ move: Move; notation: string; leafPos: number; parentPos: number }>;
182
+ } {
183
+ // Get current TGN as prefix
184
+ const currentTGN = game.toTGN().trim();
185
+
186
+ // Build prefix (everything up to next move)
187
+ const lines = currentTGN.split("\n");
188
+ const lastLine = lines[lines.length - 1];
189
+
190
+ let prefix: string;
191
+ if (lastLine.match(/^\d+\./)) {
192
+ // Last line is a move number, include it
193
+ prefix = currentTGN + " ";
194
+ } else if (lastLine.trim() === "") {
195
+ // Empty line, add move number
196
+ const moveMatches = currentTGN.match(/\d+\.\s/g);
197
+ const moveNumber = moveMatches ? moveMatches.length + 1 : 1;
198
+ const isBlackTurn = game.getCurrentPlayer() === StoneType.BLACK;
199
+ if (isBlackTurn) {
200
+ prefix = currentTGN + `${moveNumber}. `;
201
+ } else {
202
+ prefix = currentTGN + " ";
203
+ }
204
+ } else {
205
+ // Last line has moves, add space
206
+ prefix = currentTGN + " ";
207
+ }
208
+
209
+ const prefixTokens = this.stringToTokens(prefix);
210
+
211
+ // Encode each move to tokens (only first 2 tokens)
212
+ const shape = game.getShape();
213
+ const movesWithTokens = moves.map((move) => {
214
+ let notation: string;
215
+ if (move.isPass) {
216
+ notation = "Pass";
217
+ } else if (move.x !== undefined && move.y !== undefined && move.z !== undefined) {
218
+ notation = this.positionToTGN({ x: move.x, y: move.y, z: move.z }, shape);
219
+ } else {
220
+ throw new Error("Invalid move: missing coordinates");
221
+ }
222
+
223
+ // Exclude the last token
224
+ const fullTokens = this.stringToTokens(notation);
225
+ const tokens = fullTokens.slice(0, fullTokens.length - 1);
226
+
227
+ return { move, notation, tokens };
228
+ });
229
+
230
+ // Build prefix tree
231
+ const tokenArrays = movesWithTokens.map((m) => m.tokens);
232
+ const { evaluatedIds, mask, moveToLeafPos } = this.buildPrefixTree(tokenArrays);
233
+
234
+ // Build move data with leaf positions and parent positions
235
+ const moveData = movesWithTokens.map((m, index) => {
236
+ const leafPos = moveToLeafPos[index];
237
+ // Find parent position (root position for this move)
238
+ // Parent is the first token position
239
+ const firstToken = m.tokens[0];
240
+ let parentPos = -1;
241
+ for (let i = 0; i < evaluatedIds.length; i++) {
242
+ if (evaluatedIds[i] === firstToken && i < leafPos) {
243
+ // This is a potential parent
244
+ // Check if it's in the same branch by checking mask
245
+ // If leafPos can see position i, then i might be the parent
246
+ if (mask[leafPos * evaluatedIds.length + i] === 1.0 && i !== leafPos) {
247
+ // Find the closest parent (maximum index less than leafPos that leaf can see)
248
+ if (i > parentPos) {
249
+ parentPos = i;
250
+ }
251
+ }
252
+ }
253
+ }
254
+
255
+ return {
256
+ move: m.move,
257
+ notation: m.notation,
258
+ leafPos,
259
+ parentPos
260
+ };
261
+ });
262
+
263
+ return { prefixTokens, evaluatedIds, mask, moveData };
264
+ }
265
+
266
+ /**
267
+ * Get tree structure for visualization (public method)
268
+ */
269
+ getTreeStructure(
270
+ game: TrigoGame,
271
+ moves: Move[]
272
+ ): {
273
+ evaluatedIds: number[];
274
+ mask: number[];
275
+ moveData: Array<{ move: Move; notation: string; leafPos: number; parentPos: number }>;
276
+ } {
277
+ return this.buildMoveTree(game, moves);
278
+ }
279
+
280
+ /**
281
+ * Select best move using tree attention
282
+ * Evaluates all valid moves in a single inference call
283
+ */
284
+ async selectBestMove(game: TrigoGame): Promise<Move | null> {
285
+ if (!this.inferencer.isReady()) {
286
+ throw new Error("Inferencer not initialized");
287
+ }
288
+
289
+ // Get current player as string
290
+ const currentPlayer = this.stoneToPlayer(game.getCurrentPlayer());
291
+
292
+ // Get all valid moves
293
+ const validMoves: Move[] = game.validMovePositions().map((pos) => ({
294
+ x: pos.x,
295
+ y: pos.y,
296
+ z: pos.z,
297
+ player: currentPlayer
298
+ }));
299
+ validMoves.push({ player: currentPlayer, isPass: true }); // Add pass move
300
+
301
+ if (validMoves.length === 0) {
302
+ return null;
303
+ }
304
+
305
+ // Score all moves using tree attention
306
+ const scoredMoves = await this.scoreMoves(game, validMoves);
307
+
308
+ // Return move with highest score
309
+ if (scoredMoves.length === 0) {
310
+ return null;
311
+ }
312
+
313
+ scoredMoves.sort((a, b) => b.score - a.score);
314
+ return scoredMoves[0].move;
315
+ }
316
+
317
+ /**
318
+ * Score all moves using tree attention (batch evaluation)
319
+ */
320
+ async scoreMoves(game: TrigoGame, moves: Move[]): Promise<ScoredMove[]> {
321
+ if (moves.length === 0) {
322
+ return [];
323
+ }
324
+
325
+ // Build tree structure
326
+ const { prefixTokens, evaluatedIds, mask, moveData } = this.buildMoveTree(game, moves);
327
+
328
+ console.debug(`Tree structure: ${evaluatedIds.length} nodes for ${moveData.length} moves`);
329
+ console.debug(`Evaluated IDs:`, evaluatedIds.map((id) => String.fromCharCode(id)).join(""));
330
+ //console.debug(
331
+ // `Move positions:`,
332
+ // moveData.map((m) => `${m.notation}@${m.leafPos}(parent=${m.parentPos})`)
333
+ //);
334
+
335
+ // Prepare inputs for evaluation
336
+ const inputs: EvaluationInputs = {
337
+ prefixIds: prefixTokens,
338
+ evaluatedIds: evaluatedIds,
339
+ evaluatedMask: mask
340
+ };
341
+
342
+ // Run inference
343
+ const output = await this.inferencer.runEvaluationInference(inputs);
344
+ const { logits, numEvaluated } = output;
345
+
346
+ console.debug(`Inference output: ${numEvaluated} evaluated positions`);
347
+
348
+ // Score each move by accumulating log probabilities for all tokens in the path
349
+ // For each move, traverse the full path from root to leaf and sum log probabilities
350
+ const scoredMoves: ScoredMove[] = [];
351
+
352
+ // Cache softmax results for each output position to avoid recomputation
353
+ const softmaxCache = new Map<number, Float32Array>();
354
+ const getSoftmax = (outputPos: number): Float32Array => {
355
+ if (!softmaxCache.has(outputPos)) {
356
+ softmaxCache.set(outputPos, this.inferencer.softmax(logits, outputPos));
357
+ }
358
+ return softmaxCache.get(outputPos)!;
359
+ };
360
+
361
+ for (const data of moveData) {
362
+ let logProb = 0;
363
+
364
+ // Reconstruct the full path from root to leaf using the mask
365
+ // The mask tells us which positions each position can attend to (ancestors)
366
+ // We need to find all positions from root (or first move token) to leaf
367
+ const leafPos = data.leafPos;
368
+ const path: number[] = [0];
369
+
370
+ // Build path by finding all ancestors that this leaf can see
371
+ // Start from position 0 and find all positions up to leafPos that are in the path
372
+ for (let pos = 0; pos <= leafPos; pos++) {
373
+ // Check if leaf can see this position (it's an ancestor or self)
374
+ if (mask[leafPos * evaluatedIds.length + pos] === 1) {
375
+ path.push(pos + 1);
376
+ }
377
+ }
378
+ //console.debug("path:", data.notation, "->", path);
379
+
380
+ // Now accumulate log probabilities for all transitions in the path
381
+ // For each token in the path, we need P(token[i] | context up to token[i-1])
382
+ // The logits at output position j predict the NEXT token after position j
383
+ // So to get P(token at position i | context), we look at output from parent position
384
+ for (let i = 0; i < path.length; i++) {
385
+ const currentPos = path[i];
386
+ const currentToken = data.notation.charCodeAt(i);
387
+
388
+ // Subsequent tokens: predicted from previous position
389
+ // The output at prevPos predicts the token at currentPos
390
+
391
+ console.assert(currentPos <= numEvaluated, `Output position ${currentPos} exceeds numEvaluated ${numEvaluated}`);
392
+
393
+ if (currentPos <= numEvaluated) {
394
+ const probs = getSoftmax(currentPos);
395
+ const prob = probs[currentToken];
396
+
397
+ if (prob > 0)
398
+ logProb += Math.log(prob);
399
+ else
400
+ logProb += -100;
401
+ }
402
+ else
403
+ logProb += -100;
404
+ }
405
+
406
+ scoredMoves.push({
407
+ move: data.move,
408
+ score: logProb,
409
+ notation: data.notation
410
+ });
411
+ }
412
+
413
+ return scoredMoves;
414
+ }
415
+ }
trigo-web/inc/tsconfig.json CHANGED
@@ -13,4 +13,4 @@
13
  },
14
  "include": ["**/*.ts", "**/*.d.ts"],
15
  "exclude": ["node_modules", "dist"]
16
- }
 
13
  },
14
  "include": ["**/*.ts", "**/*.d.ts"],
15
  "exclude": ["node_modules", "dist"]
16
+ }
trigo-web/package-lock.json CHANGED
@@ -16,14 +16,19 @@
16
  "concurrently": "^7.6.0",
17
  "eslint-config-prettier": "^10.1.8",
18
  "eslint-plugin-prettier": "^5.5.4",
 
19
  "jison": "^0.4.18",
20
  "jsdom": "^27.1.0",
 
 
 
21
  "prettier": "^3.6.2",
22
  "tsx": "^4.20.6",
23
  "typescript": "^5.2.2",
24
  "vite": "^5.4.21",
25
  "vitest": "^4.0.6",
26
  "vue": "^3.3.4",
 
27
  "yargs": "^18.0.0"
28
  }
29
  },
@@ -250,9 +255,9 @@
250
  }
251
  },
252
  "node_modules/@esbuild/aix-ppc64": {
253
- "version": "0.21.5",
254
- "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz",
255
- "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==",
256
  "cpu": [
257
  "ppc64"
258
  ],
@@ -262,13 +267,13 @@
262
  "aix"
263
  ],
264
  "engines": {
265
- "node": ">=12"
266
  }
267
  },
268
  "node_modules/@esbuild/android-arm": {
269
- "version": "0.21.5",
270
- "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz",
271
- "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==",
272
  "cpu": [
273
  "arm"
274
  ],
@@ -278,13 +283,13 @@
278
  "android"
279
  ],
280
  "engines": {
281
- "node": ">=12"
282
  }
283
  },
284
  "node_modules/@esbuild/android-arm64": {
285
- "version": "0.21.5",
286
- "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz",
287
- "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==",
288
  "cpu": [
289
  "arm64"
290
  ],
@@ -294,13 +299,13 @@
294
  "android"
295
  ],
296
  "engines": {
297
- "node": ">=12"
298
  }
299
  },
300
  "node_modules/@esbuild/android-x64": {
301
- "version": "0.21.5",
302
- "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz",
303
- "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==",
304
  "cpu": [
305
  "x64"
306
  ],
@@ -310,13 +315,13 @@
310
  "android"
311
  ],
312
  "engines": {
313
- "node": ">=12"
314
  }
315
  },
316
  "node_modules/@esbuild/darwin-arm64": {
317
- "version": "0.21.5",
318
- "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz",
319
- "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==",
320
  "cpu": [
321
  "arm64"
322
  ],
@@ -326,13 +331,13 @@
326
  "darwin"
327
  ],
328
  "engines": {
329
- "node": ">=12"
330
  }
331
  },
332
  "node_modules/@esbuild/darwin-x64": {
333
- "version": "0.21.5",
334
- "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz",
335
- "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==",
336
  "cpu": [
337
  "x64"
338
  ],
@@ -342,13 +347,13 @@
342
  "darwin"
343
  ],
344
  "engines": {
345
- "node": ">=12"
346
  }
347
  },
348
  "node_modules/@esbuild/freebsd-arm64": {
349
- "version": "0.21.5",
350
- "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz",
351
- "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==",
352
  "cpu": [
353
  "arm64"
354
  ],
@@ -358,13 +363,13 @@
358
  "freebsd"
359
  ],
360
  "engines": {
361
- "node": ">=12"
362
  }
363
  },
364
  "node_modules/@esbuild/freebsd-x64": {
365
- "version": "0.21.5",
366
- "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz",
367
- "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==",
368
  "cpu": [
369
  "x64"
370
  ],
@@ -374,13 +379,13 @@
374
  "freebsd"
375
  ],
376
  "engines": {
377
- "node": ">=12"
378
  }
379
  },
380
  "node_modules/@esbuild/linux-arm": {
381
- "version": "0.21.5",
382
- "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz",
383
- "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==",
384
  "cpu": [
385
  "arm"
386
  ],
@@ -390,13 +395,13 @@
390
  "linux"
391
  ],
392
  "engines": {
393
- "node": ">=12"
394
  }
395
  },
396
  "node_modules/@esbuild/linux-arm64": {
397
- "version": "0.21.5",
398
- "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz",
399
- "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==",
400
  "cpu": [
401
  "arm64"
402
  ],
@@ -406,13 +411,13 @@
406
  "linux"
407
  ],
408
  "engines": {
409
- "node": ">=12"
410
  }
411
  },
412
  "node_modules/@esbuild/linux-ia32": {
413
- "version": "0.21.5",
414
- "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz",
415
- "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==",
416
  "cpu": [
417
  "ia32"
418
  ],
@@ -422,13 +427,13 @@
422
  "linux"
423
  ],
424
  "engines": {
425
- "node": ">=12"
426
  }
427
  },
428
  "node_modules/@esbuild/linux-loong64": {
429
- "version": "0.21.5",
430
- "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz",
431
- "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==",
432
  "cpu": [
433
  "loong64"
434
  ],
@@ -438,13 +443,13 @@
438
  "linux"
439
  ],
440
  "engines": {
441
- "node": ">=12"
442
  }
443
  },
444
  "node_modules/@esbuild/linux-mips64el": {
445
- "version": "0.21.5",
446
- "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz",
447
- "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==",
448
  "cpu": [
449
  "mips64el"
450
  ],
@@ -454,13 +459,13 @@
454
  "linux"
455
  ],
456
  "engines": {
457
- "node": ">=12"
458
  }
459
  },
460
  "node_modules/@esbuild/linux-ppc64": {
461
- "version": "0.21.5",
462
- "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz",
463
- "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==",
464
  "cpu": [
465
  "ppc64"
466
  ],
@@ -470,13 +475,13 @@
470
  "linux"
471
  ],
472
  "engines": {
473
- "node": ">=12"
474
  }
475
  },
476
  "node_modules/@esbuild/linux-riscv64": {
477
- "version": "0.21.5",
478
- "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz",
479
- "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==",
480
  "cpu": [
481
  "riscv64"
482
  ],
@@ -486,13 +491,13 @@
486
  "linux"
487
  ],
488
  "engines": {
489
- "node": ">=12"
490
  }
491
  },
492
  "node_modules/@esbuild/linux-s390x": {
493
- "version": "0.21.5",
494
- "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz",
495
- "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==",
496
  "cpu": [
497
  "s390x"
498
  ],
@@ -502,13 +507,13 @@
502
  "linux"
503
  ],
504
  "engines": {
505
- "node": ">=12"
506
  }
507
  },
508
  "node_modules/@esbuild/linux-x64": {
509
- "version": "0.21.5",
510
- "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz",
511
- "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==",
512
  "cpu": [
513
  "x64"
514
  ],
@@ -518,7 +523,7 @@
518
  "linux"
519
  ],
520
  "engines": {
521
- "node": ">=12"
522
  }
523
  },
524
  "node_modules/@esbuild/netbsd-arm64": {
@@ -538,9 +543,9 @@
538
  }
539
  },
540
  "node_modules/@esbuild/netbsd-x64": {
541
- "version": "0.21.5",
542
- "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz",
543
- "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==",
544
  "cpu": [
545
  "x64"
546
  ],
@@ -550,7 +555,7 @@
550
  "netbsd"
551
  ],
552
  "engines": {
553
- "node": ">=12"
554
  }
555
  },
556
  "node_modules/@esbuild/openbsd-arm64": {
@@ -570,9 +575,9 @@
570
  }
571
  },
572
  "node_modules/@esbuild/openbsd-x64": {
573
- "version": "0.21.5",
574
- "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz",
575
- "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==",
576
  "cpu": [
577
  "x64"
578
  ],
@@ -582,7 +587,7 @@
582
  "openbsd"
583
  ],
584
  "engines": {
585
- "node": ">=12"
586
  }
587
  },
588
  "node_modules/@esbuild/openharmony-arm64": {
@@ -602,9 +607,9 @@
602
  }
603
  },
604
  "node_modules/@esbuild/sunos-x64": {
605
- "version": "0.21.5",
606
- "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz",
607
- "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==",
608
  "cpu": [
609
  "x64"
610
  ],
@@ -614,13 +619,13 @@
614
  "sunos"
615
  ],
616
  "engines": {
617
- "node": ">=12"
618
  }
619
  },
620
  "node_modules/@esbuild/win32-arm64": {
621
- "version": "0.21.5",
622
- "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz",
623
- "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==",
624
  "cpu": [
625
  "arm64"
626
  ],
@@ -630,13 +635,13 @@
630
  "win32"
631
  ],
632
  "engines": {
633
- "node": ">=12"
634
  }
635
  },
636
  "node_modules/@esbuild/win32-ia32": {
637
- "version": "0.21.5",
638
- "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz",
639
- "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==",
640
  "cpu": [
641
  "ia32"
642
  ],
@@ -646,13 +651,13 @@
646
  "win32"
647
  ],
648
  "engines": {
649
- "node": ">=12"
650
  }
651
  },
652
  "node_modules/@esbuild/win32-x64": {
653
- "version": "0.21.5",
654
- "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz",
655
- "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==",
656
  "cpu": [
657
  "x64"
658
  ],
@@ -662,7 +667,7 @@
662
  "win32"
663
  ],
664
  "engines": {
665
- "node": ">=12"
666
  }
667
  },
668
  "node_modules/@eslint-community/eslint-utils": {
@@ -885,6 +890,70 @@
885
  "integrity": "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==",
886
  "dev": true
887
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
888
  "node_modules/@rollup/rollup-android-arm-eabi": {
889
  "version": "4.52.5",
890
  "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.52.5.tgz",
@@ -1342,6 +1411,32 @@
1342
  "url": "https://opencollective.com/vitest"
1343
  }
1344
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1345
  "node_modules/@vue/compiler-core": {
1346
  "version": "3.5.22",
1347
  "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.22.tgz",
@@ -1404,6 +1499,29 @@
1404
  "@vue/shared": "3.5.22"
1405
  }
1406
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1407
  "node_modules/@vue/reactivity": {
1408
  "version": "3.5.22",
1409
  "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.22.tgz",
@@ -1477,6 +1595,15 @@
1477
  "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
1478
  }
1479
  },
 
 
 
 
 
 
 
 
 
1480
  "node_modules/agent-base": {
1481
  "version": "7.1.4",
1482
  "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz",
@@ -1503,6 +1630,12 @@
1503
  "url": "https://github.com/sponsors/epoberezkin"
1504
  }
1505
  },
 
 
 
 
 
 
1506
  "node_modules/amdefine": {
1507
  "version": "1.0.1",
1508
  "resolved": "https://registry.npmjs.org/amdefine/-/amdefine-1.0.1.tgz",
@@ -1513,6 +1646,21 @@
1513
  "node": ">=0.4.2"
1514
  }
1515
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1516
  "node_modules/ansi-regex": {
1517
  "version": "6.2.2",
1518
  "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz",
@@ -1572,6 +1720,13 @@
1572
  "require-from-string": "^2.0.2"
1573
  }
1574
  },
 
 
 
 
 
 
 
1575
  "node_modules/brace-expansion": {
1576
  "version": "1.1.12",
1577
  "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
@@ -1583,10 +1738,22 @@
1583
  "concat-map": "0.0.1"
1584
  }
1585
  },
1586
- "node_modules/callsites": {
1587
- "version": "3.1.0",
1588
- "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
1589
- "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
 
 
 
 
 
 
 
 
 
 
 
 
1590
  "dev": true,
1591
  "peer": true,
1592
  "engines": {
@@ -1618,6 +1785,18 @@
1618
  "url": "https://github.com/chalk/chalk?sponsor=1"
1619
  }
1620
  },
 
 
 
 
 
 
 
 
 
 
 
 
1621
  "node_modules/cjson": {
1622
  "version": "0.3.0",
1623
  "resolved": "https://registry.npmjs.org/cjson/-/cjson-0.3.0.tgz",
@@ -1630,6 +1809,53 @@
1630
  "node": ">= 0.3.0"
1631
  }
1632
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1633
  "node_modules/cliui": {
1634
  "version": "9.0.1",
1635
  "resolved": "https://registry.npmjs.org/cliui/-/cliui-9.0.1.tgz",
@@ -1662,6 +1888,12 @@
1662
  "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
1663
  "dev": true
1664
  },
 
 
 
 
 
 
1665
  "node_modules/colors": {
1666
  "version": "0.5.1",
1667
  "resolved": "https://registry.npmjs.org/colors/-/colors-0.5.1.tgz",
@@ -1671,6 +1903,15 @@
1671
  "node": ">=0.1.90"
1672
  }
1673
  },
 
 
 
 
 
 
 
 
 
1674
  "node_modules/concat-map": {
1675
  "version": "0.0.1",
1676
  "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
@@ -1760,21 +2001,6 @@
1760
  "node": ">=8"
1761
  }
1762
  },
1763
- "node_modules/concurrently/node_modules/supports-color": {
1764
- "version": "8.1.1",
1765
- "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz",
1766
- "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==",
1767
- "dev": true,
1768
- "dependencies": {
1769
- "has-flag": "^4.0.0"
1770
- },
1771
- "engines": {
1772
- "node": ">=10"
1773
- },
1774
- "funding": {
1775
- "url": "https://github.com/chalk/supports-color?sponsor=1"
1776
- }
1777
- },
1778
  "node_modules/concurrently/node_modules/wrap-ansi": {
1779
  "version": "7.0.0",
1780
  "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
@@ -1926,6 +2152,46 @@
1926
  "dev": true,
1927
  "peer": true
1928
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1929
  "node_modules/ebnf-parser": {
1930
  "version": "0.1.10",
1931
  "resolved": "https://registry.npmjs.org/ebnf-parser/-/ebnf-parser-0.1.10.tgz",
@@ -1950,48 +2216,87 @@
1950
  "url": "https://github.com/fb55/entities?sponsor=1"
1951
  }
1952
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1953
  "node_modules/es-module-lexer": {
1954
  "version": "1.7.0",
1955
  "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz",
1956
  "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==",
1957
  "dev": true
1958
  },
 
 
 
 
 
 
1959
  "node_modules/esbuild": {
1960
- "version": "0.21.5",
1961
- "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz",
1962
- "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==",
1963
  "dev": true,
1964
  "hasInstallScript": true,
1965
  "bin": {
1966
  "esbuild": "bin/esbuild"
1967
  },
1968
  "engines": {
1969
- "node": ">=12"
1970
  },
1971
  "optionalDependencies": {
1972
- "@esbuild/aix-ppc64": "0.21.5",
1973
- "@esbuild/android-arm": "0.21.5",
1974
- "@esbuild/android-arm64": "0.21.5",
1975
- "@esbuild/android-x64": "0.21.5",
1976
- "@esbuild/darwin-arm64": "0.21.5",
1977
- "@esbuild/darwin-x64": "0.21.5",
1978
- "@esbuild/freebsd-arm64": "0.21.5",
1979
- "@esbuild/freebsd-x64": "0.21.5",
1980
- "@esbuild/linux-arm": "0.21.5",
1981
- "@esbuild/linux-arm64": "0.21.5",
1982
- "@esbuild/linux-ia32": "0.21.5",
1983
- "@esbuild/linux-loong64": "0.21.5",
1984
- "@esbuild/linux-mips64el": "0.21.5",
1985
- "@esbuild/linux-ppc64": "0.21.5",
1986
- "@esbuild/linux-riscv64": "0.21.5",
1987
- "@esbuild/linux-s390x": "0.21.5",
1988
- "@esbuild/linux-x64": "0.21.5",
1989
- "@esbuild/netbsd-x64": "0.21.5",
1990
- "@esbuild/openbsd-x64": "0.21.5",
1991
- "@esbuild/sunos-x64": "0.21.5",
1992
- "@esbuild/win32-arm64": "0.21.5",
1993
- "@esbuild/win32-ia32": "0.21.5",
1994
- "@esbuild/win32-x64": "0.21.5"
 
 
 
1995
  }
1996
  },
1997
  "node_modules/escalade": {
@@ -2008,7 +2313,6 @@
2008
  "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
2009
  "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
2010
  "dev": true,
2011
- "peer": true,
2012
  "engines": {
2013
  "node": ">=10"
2014
  },
@@ -2273,6 +2577,12 @@
2273
  "node": ">=0.10.0"
2274
  }
2275
  },
 
 
 
 
 
 
2276
  "node_modules/expect-type": {
2277
  "version": "1.2.2",
2278
  "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.2.2.tgz",
@@ -2345,6 +2655,18 @@
2345
  "node": ">=16.0.0"
2346
  }
2347
  },
 
 
 
 
 
 
 
 
 
 
 
 
2348
  "node_modules/find-up": {
2349
  "version": "5.0.0",
2350
  "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
@@ -2376,6 +2698,12 @@
2376
  "node": ">=16"
2377
  }
2378
  },
 
 
 
 
 
 
2379
  "node_modules/flatted": {
2380
  "version": "3.3.3",
2381
  "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz",
@@ -2442,6 +2770,23 @@
2442
  "node": ">=10.13.0"
2443
  }
2444
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2445
  "node_modules/globals": {
2446
  "version": "14.0.0",
2447
  "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz",
@@ -2455,6 +2800,40 @@
2455
  "url": "https://github.com/sponsors/sindresorhus"
2456
  }
2457
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2458
  "node_modules/has-flag": {
2459
  "version": "4.0.0",
2460
  "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
@@ -2464,6 +2843,18 @@
2464
  "node": ">=8"
2465
  }
2466
  },
 
 
 
 
 
 
 
 
 
 
 
 
2467
  "node_modules/html-encoding-sniffer": {
2468
  "version": "4.0.0",
2469
  "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz",
@@ -2502,6 +2893,21 @@
2502
  "node": ">= 14"
2503
  }
2504
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2505
  "node_modules/iconv-lite": {
2506
  "version": "0.6.3",
2507
  "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
@@ -2583,6 +2989,15 @@
2583
  "node": ">=0.10.0"
2584
  }
2585
  },
 
 
 
 
 
 
 
 
 
2586
  "node_modules/is-potential-custom-element-name": {
2587
  "version": "1.0.1",
2588
  "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz",
@@ -2707,6 +3122,12 @@
2707
  "dev": true,
2708
  "peer": true
2709
  },
 
 
 
 
 
 
2710
  "node_modules/jsonlint": {
2711
  "version": "1.6.0",
2712
  "resolved": "https://registry.npmjs.org/jsonlint/-/jsonlint-1.6.0.tgz",
@@ -2771,24 +3192,65 @@
2771
  "integrity": "sha512-DuAEISsr1H4LOpmFLkyMc8YStiRWZCO8hMsoXAXSbgyfvs2WQhSt0+/FBv3ZU/JBFZMGcE+FWzEBSzwUU7U27w==",
2772
  "dev": true
2773
  },
2774
- "node_modules/locate-path": {
2775
- "version": "6.0.0",
2776
- "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
2777
- "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==",
2778
  "dev": true,
2779
- "peer": true,
2780
  "dependencies": {
2781
- "p-locate": "^5.0.0"
 
 
 
 
 
 
 
 
 
2782
  },
2783
  "engines": {
2784
- "node": ">=10"
2785
  },
2786
  "funding": {
2787
- "url": "https://github.com/sponsors/sindresorhus"
2788
  }
2789
  },
2790
- "node_modules/lodash": {
2791
- "version": "4.17.21",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2792
  "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
2793
  "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
2794
  "dev": true
@@ -2800,6 +3262,31 @@
2800
  "dev": true,
2801
  "peer": true
2802
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2803
  "node_modules/lru-cache": {
2804
  "version": "11.2.2",
2805
  "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.2.tgz",
@@ -2818,12 +3305,61 @@
2818
  "@jridgewell/sourcemap-codec": "^1.5.5"
2819
  }
2820
  },
 
 
 
 
 
 
 
 
 
 
 
 
2821
  "node_modules/mdn-data": {
2822
  "version": "2.12.2",
2823
  "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.12.2.tgz",
2824
  "integrity": "sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA==",
2825
  "dev": true
2826
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2827
  "node_modules/minimatch": {
2828
  "version": "3.1.2",
2829
  "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
@@ -2852,6 +3388,24 @@
2852
  "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
2853
  "dev": true
2854
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2855
  "node_modules/nanoid": {
2856
  "version": "3.3.11",
2857
  "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
@@ -2891,6 +3445,67 @@
2891
  "node": "*"
2892
  }
2893
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2894
  "node_modules/optionator": {
2895
  "version": "0.9.4",
2896
  "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
@@ -2966,6 +3581,12 @@
2966
  "url": "https://github.com/inikulin/parse5?sponsor=1"
2967
  }
2968
  },
 
 
 
 
 
 
2969
  "node_modules/path-exists": {
2970
  "version": "4.0.0",
2971
  "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
@@ -3010,6 +3631,24 @@
3010
  "url": "https://github.com/sponsors/jonschlinkert"
3011
  }
3012
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3013
  "node_modules/postcss": {
3014
  "version": "8.5.6",
3015
  "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
@@ -3075,6 +3714,30 @@
3075
  "node": ">=6.0.0"
3076
  }
3077
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3078
  "node_modules/punycode": {
3079
  "version": "2.3.1",
3080
  "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
@@ -3121,6 +3784,45 @@
3121
  "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1"
3122
  }
3123
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3124
  "node_modules/rollup": {
3125
  "version": "4.52.5",
3126
  "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.52.5.tgz",
@@ -3189,6 +3891,39 @@
3189
  "node": ">=v12.22.7"
3190
  }
3191
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3192
  "node_modules/shebang-command": {
3193
  "version": "2.0.0",
3194
  "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
@@ -3230,6 +3965,18 @@
3230
  "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==",
3231
  "dev": true
3232
  },
 
 
 
 
 
 
 
 
 
 
 
 
3233
  "node_modules/sirv": {
3234
  "version": "3.0.2",
3235
  "resolved": "https://registry.npmjs.org/sirv/-/sirv-3.0.2.tgz",
@@ -3244,6 +3991,49 @@
3244
  "node": ">=18"
3245
  }
3246
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3247
  "node_modules/source-map": {
3248
  "version": "0.1.43",
3249
  "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.1.43.tgz",
@@ -3272,6 +4062,12 @@
3272
  "integrity": "sha512-zC8zGoGkmc8J9ndvml8Xksr1Amk9qBujgbF0JAIWO7kXr43w0h/0GJNM/Vustixu+YE8N/MTrQ7N31FvHUACxQ==",
3273
  "dev": true
3274
  },
 
 
 
 
 
 
3275
  "node_modules/stackback": {
3276
  "version": "0.0.2",
3277
  "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz",
@@ -3284,6 +4080,15 @@
3284
  "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==",
3285
  "dev": true
3286
  },
 
 
 
 
 
 
 
 
 
3287
  "node_modules/string-width": {
3288
  "version": "7.2.0",
3289
  "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz",
@@ -3330,15 +4135,18 @@
3330
  }
3331
  },
3332
  "node_modules/supports-color": {
3333
- "version": "7.2.0",
3334
- "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
3335
- "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
3336
  "dev": true,
3337
  "dependencies": {
3338
  "has-flag": "^4.0.0"
3339
  },
3340
  "engines": {
3341
- "node": ">=8"
 
 
 
3342
  }
3343
  },
3344
  "node_modules/symbol-tree": {
@@ -3417,6 +4225,18 @@
3417
  "integrity": "sha512-DieYoGrP78PWKsrXr8MZwtQ7GLCUeLxihtjC1jZsW1DnvSMdKPitJSe8OSYDM2u5H6g3kWJZpePqkp43TfLh0g==",
3418
  "dev": true
3419
  },
 
 
 
 
 
 
 
 
 
 
 
 
3420
  "node_modules/totalist": {
3421
  "version": "3.0.1",
3422
  "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz",
@@ -3484,426 +4304,29 @@
3484
  "fsevents": "~2.3.3"
3485
  }
3486
  },
3487
- "node_modules/tsx/node_modules/@esbuild/aix-ppc64": {
3488
- "version": "0.25.12",
3489
- "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz",
3490
- "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==",
3491
- "cpu": [
3492
- "ppc64"
3493
- ],
3494
  "dev": true,
3495
- "optional": true,
3496
- "os": [
3497
- "aix"
3498
- ],
3499
  "engines": {
3500
- "node": ">=18"
3501
- }
3502
- },
3503
- "node_modules/tsx/node_modules/@esbuild/android-arm": {
3504
- "version": "0.25.12",
3505
- "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz",
3506
- "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==",
3507
- "cpu": [
3508
- "arm"
3509
- ],
3510
- "dev": true,
3511
- "optional": true,
3512
- "os": [
3513
- "android"
3514
- ],
3515
- "engines": {
3516
- "node": ">=18"
3517
- }
3518
- },
3519
- "node_modules/tsx/node_modules/@esbuild/android-arm64": {
3520
- "version": "0.25.12",
3521
- "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz",
3522
- "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==",
3523
- "cpu": [
3524
- "arm64"
3525
- ],
3526
- "dev": true,
3527
- "optional": true,
3528
- "os": [
3529
- "android"
3530
- ],
3531
- "engines": {
3532
- "node": ">=18"
3533
- }
3534
- },
3535
- "node_modules/tsx/node_modules/@esbuild/android-x64": {
3536
- "version": "0.25.12",
3537
- "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz",
3538
- "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==",
3539
- "cpu": [
3540
- "x64"
3541
- ],
3542
- "dev": true,
3543
- "optional": true,
3544
- "os": [
3545
- "android"
3546
- ],
3547
- "engines": {
3548
- "node": ">=18"
3549
- }
3550
- },
3551
- "node_modules/tsx/node_modules/@esbuild/darwin-arm64": {
3552
- "version": "0.25.12",
3553
- "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz",
3554
- "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==",
3555
- "cpu": [
3556
- "arm64"
3557
- ],
3558
- "dev": true,
3559
- "optional": true,
3560
- "os": [
3561
- "darwin"
3562
- ],
3563
- "engines": {
3564
- "node": ">=18"
3565
- }
3566
- },
3567
- "node_modules/tsx/node_modules/@esbuild/darwin-x64": {
3568
- "version": "0.25.12",
3569
- "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz",
3570
- "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==",
3571
- "cpu": [
3572
- "x64"
3573
- ],
3574
- "dev": true,
3575
- "optional": true,
3576
- "os": [
3577
- "darwin"
3578
- ],
3579
- "engines": {
3580
- "node": ">=18"
3581
- }
3582
- },
3583
- "node_modules/tsx/node_modules/@esbuild/freebsd-arm64": {
3584
- "version": "0.25.12",
3585
- "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz",
3586
- "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==",
3587
- "cpu": [
3588
- "arm64"
3589
- ],
3590
- "dev": true,
3591
- "optional": true,
3592
- "os": [
3593
- "freebsd"
3594
- ],
3595
- "engines": {
3596
- "node": ">=18"
3597
- }
3598
- },
3599
- "node_modules/tsx/node_modules/@esbuild/freebsd-x64": {
3600
- "version": "0.25.12",
3601
- "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz",
3602
- "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==",
3603
- "cpu": [
3604
- "x64"
3605
- ],
3606
- "dev": true,
3607
- "optional": true,
3608
- "os": [
3609
- "freebsd"
3610
- ],
3611
- "engines": {
3612
- "node": ">=18"
3613
- }
3614
- },
3615
- "node_modules/tsx/node_modules/@esbuild/linux-arm": {
3616
- "version": "0.25.12",
3617
- "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz",
3618
- "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==",
3619
- "cpu": [
3620
- "arm"
3621
- ],
3622
- "dev": true,
3623
- "optional": true,
3624
- "os": [
3625
- "linux"
3626
- ],
3627
- "engines": {
3628
- "node": ">=18"
3629
- }
3630
- },
3631
- "node_modules/tsx/node_modules/@esbuild/linux-arm64": {
3632
- "version": "0.25.12",
3633
- "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz",
3634
- "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==",
3635
- "cpu": [
3636
- "arm64"
3637
- ],
3638
- "dev": true,
3639
- "optional": true,
3640
- "os": [
3641
- "linux"
3642
- ],
3643
- "engines": {
3644
- "node": ">=18"
3645
- }
3646
- },
3647
- "node_modules/tsx/node_modules/@esbuild/linux-ia32": {
3648
- "version": "0.25.12",
3649
- "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz",
3650
- "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==",
3651
- "cpu": [
3652
- "ia32"
3653
- ],
3654
- "dev": true,
3655
- "optional": true,
3656
- "os": [
3657
- "linux"
3658
- ],
3659
- "engines": {
3660
- "node": ">=18"
3661
- }
3662
- },
3663
- "node_modules/tsx/node_modules/@esbuild/linux-loong64": {
3664
- "version": "0.25.12",
3665
- "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz",
3666
- "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==",
3667
- "cpu": [
3668
- "loong64"
3669
- ],
3670
- "dev": true,
3671
- "optional": true,
3672
- "os": [
3673
- "linux"
3674
- ],
3675
- "engines": {
3676
- "node": ">=18"
3677
- }
3678
- },
3679
- "node_modules/tsx/node_modules/@esbuild/linux-mips64el": {
3680
- "version": "0.25.12",
3681
- "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz",
3682
- "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==",
3683
- "cpu": [
3684
- "mips64el"
3685
- ],
3686
- "dev": true,
3687
- "optional": true,
3688
- "os": [
3689
- "linux"
3690
- ],
3691
- "engines": {
3692
- "node": ">=18"
3693
- }
3694
- },
3695
- "node_modules/tsx/node_modules/@esbuild/linux-ppc64": {
3696
- "version": "0.25.12",
3697
- "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz",
3698
- "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==",
3699
- "cpu": [
3700
- "ppc64"
3701
- ],
3702
- "dev": true,
3703
- "optional": true,
3704
- "os": [
3705
- "linux"
3706
- ],
3707
- "engines": {
3708
- "node": ">=18"
3709
- }
3710
- },
3711
- "node_modules/tsx/node_modules/@esbuild/linux-riscv64": {
3712
- "version": "0.25.12",
3713
- "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz",
3714
- "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==",
3715
- "cpu": [
3716
- "riscv64"
3717
- ],
3718
- "dev": true,
3719
- "optional": true,
3720
- "os": [
3721
- "linux"
3722
- ],
3723
- "engines": {
3724
- "node": ">=18"
3725
- }
3726
- },
3727
- "node_modules/tsx/node_modules/@esbuild/linux-s390x": {
3728
- "version": "0.25.12",
3729
- "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz",
3730
- "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==",
3731
- "cpu": [
3732
- "s390x"
3733
- ],
3734
- "dev": true,
3735
- "optional": true,
3736
- "os": [
3737
- "linux"
3738
- ],
3739
- "engines": {
3740
- "node": ">=18"
3741
- }
3742
- },
3743
- "node_modules/tsx/node_modules/@esbuild/linux-x64": {
3744
- "version": "0.25.12",
3745
- "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz",
3746
- "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==",
3747
- "cpu": [
3748
- "x64"
3749
- ],
3750
- "dev": true,
3751
- "optional": true,
3752
- "os": [
3753
- "linux"
3754
- ],
3755
- "engines": {
3756
- "node": ">=18"
3757
- }
3758
- },
3759
- "node_modules/tsx/node_modules/@esbuild/netbsd-x64": {
3760
- "version": "0.25.12",
3761
- "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz",
3762
- "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==",
3763
- "cpu": [
3764
- "x64"
3765
- ],
3766
- "dev": true,
3767
- "optional": true,
3768
- "os": [
3769
- "netbsd"
3770
- ],
3771
- "engines": {
3772
- "node": ">=18"
3773
- }
3774
- },
3775
- "node_modules/tsx/node_modules/@esbuild/openbsd-x64": {
3776
- "version": "0.25.12",
3777
- "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz",
3778
- "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==",
3779
- "cpu": [
3780
- "x64"
3781
- ],
3782
- "dev": true,
3783
- "optional": true,
3784
- "os": [
3785
- "openbsd"
3786
- ],
3787
- "engines": {
3788
- "node": ">=18"
3789
- }
3790
- },
3791
- "node_modules/tsx/node_modules/@esbuild/sunos-x64": {
3792
- "version": "0.25.12",
3793
- "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz",
3794
- "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==",
3795
- "cpu": [
3796
- "x64"
3797
- ],
3798
- "dev": true,
3799
- "optional": true,
3800
- "os": [
3801
- "sunos"
3802
- ],
3803
- "engines": {
3804
- "node": ">=18"
3805
- }
3806
- },
3807
- "node_modules/tsx/node_modules/@esbuild/win32-arm64": {
3808
- "version": "0.25.12",
3809
- "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz",
3810
- "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==",
3811
- "cpu": [
3812
- "arm64"
3813
- ],
3814
- "dev": true,
3815
- "optional": true,
3816
- "os": [
3817
- "win32"
3818
- ],
3819
- "engines": {
3820
- "node": ">=18"
3821
- }
3822
- },
3823
- "node_modules/tsx/node_modules/@esbuild/win32-ia32": {
3824
- "version": "0.25.12",
3825
- "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz",
3826
- "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==",
3827
- "cpu": [
3828
- "ia32"
3829
- ],
3830
- "dev": true,
3831
- "optional": true,
3832
- "os": [
3833
- "win32"
3834
- ],
3835
- "engines": {
3836
- "node": ">=18"
3837
- }
3838
- },
3839
- "node_modules/tsx/node_modules/@esbuild/win32-x64": {
3840
- "version": "0.25.12",
3841
- "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz",
3842
- "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==",
3843
- "cpu": [
3844
- "x64"
3845
- ],
3846
- "dev": true,
3847
- "optional": true,
3848
- "os": [
3849
- "win32"
3850
- ],
3851
- "engines": {
3852
- "node": ">=18"
3853
  }
3854
  },
3855
- "node_modules/tsx/node_modules/esbuild": {
3856
- "version": "0.25.12",
3857
- "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz",
3858
- "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==",
3859
  "dev": true,
3860
- "hasInstallScript": true,
3861
- "bin": {
3862
- "esbuild": "bin/esbuild"
3863
- },
3864
  "engines": {
3865
- "node": ">=18"
3866
- },
3867
- "optionalDependencies": {
3868
- "@esbuild/aix-ppc64": "0.25.12",
3869
- "@esbuild/android-arm": "0.25.12",
3870
- "@esbuild/android-arm64": "0.25.12",
3871
- "@esbuild/android-x64": "0.25.12",
3872
- "@esbuild/darwin-arm64": "0.25.12",
3873
- "@esbuild/darwin-x64": "0.25.12",
3874
- "@esbuild/freebsd-arm64": "0.25.12",
3875
- "@esbuild/freebsd-x64": "0.25.12",
3876
- "@esbuild/linux-arm": "0.25.12",
3877
- "@esbuild/linux-arm64": "0.25.12",
3878
- "@esbuild/linux-ia32": "0.25.12",
3879
- "@esbuild/linux-loong64": "0.25.12",
3880
- "@esbuild/linux-mips64el": "0.25.12",
3881
- "@esbuild/linux-ppc64": "0.25.12",
3882
- "@esbuild/linux-riscv64": "0.25.12",
3883
- "@esbuild/linux-s390x": "0.25.12",
3884
- "@esbuild/linux-x64": "0.25.12",
3885
- "@esbuild/netbsd-arm64": "0.25.12",
3886
- "@esbuild/netbsd-x64": "0.25.12",
3887
- "@esbuild/openbsd-arm64": "0.25.12",
3888
- "@esbuild/openbsd-x64": "0.25.12",
3889
- "@esbuild/openharmony-arm64": "0.25.12",
3890
- "@esbuild/sunos-x64": "0.25.12",
3891
- "@esbuild/win32-arm64": "0.25.12",
3892
- "@esbuild/win32-ia32": "0.25.12",
3893
- "@esbuild/win32-x64": "0.25.12"
3894
- }
3895
- },
3896
- "node_modules/type-check": {
3897
- "version": "0.4.0",
3898
- "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
3899
- "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==",
3900
- "dev": true,
3901
- "peer": true,
3902
- "dependencies": {
3903
- "prelude-ls": "^1.2.1"
3904
  },
3905
- "engines": {
3906
- "node": ">= 0.8.0"
3907
  }
3908
  },
3909
  "node_modules/typescript": {
@@ -4003,87 +4426,10 @@
4003
  }
4004
  }
4005
  },
4006
- "node_modules/vitest": {
4007
- "version": "4.0.6",
4008
- "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.0.6.tgz",
4009
- "integrity": "sha512-gR7INfiVRwnEOkCk47faros/9McCZMp5LM+OMNWGLaDBSvJxIzwjgNFufkuePBNaesGRnLmNfW+ddbUJRZn0nQ==",
4010
- "dev": true,
4011
- "dependencies": {
4012
- "@vitest/expect": "4.0.6",
4013
- "@vitest/mocker": "4.0.6",
4014
- "@vitest/pretty-format": "4.0.6",
4015
- "@vitest/runner": "4.0.6",
4016
- "@vitest/snapshot": "4.0.6",
4017
- "@vitest/spy": "4.0.6",
4018
- "@vitest/utils": "4.0.6",
4019
- "debug": "^4.4.3",
4020
- "es-module-lexer": "^1.7.0",
4021
- "expect-type": "^1.2.2",
4022
- "magic-string": "^0.30.19",
4023
- "pathe": "^2.0.3",
4024
- "picomatch": "^4.0.3",
4025
- "std-env": "^3.9.0",
4026
- "tinybench": "^2.9.0",
4027
- "tinyexec": "^0.3.2",
4028
- "tinyglobby": "^0.2.15",
4029
- "tinyrainbow": "^3.0.3",
4030
- "vite": "^6.0.0 || ^7.0.0",
4031
- "why-is-node-running": "^2.3.0"
4032
- },
4033
- "bin": {
4034
- "vitest": "vitest.mjs"
4035
- },
4036
- "engines": {
4037
- "node": "^20.0.0 || ^22.0.0 || >=24.0.0"
4038
- },
4039
- "funding": {
4040
- "url": "https://opencollective.com/vitest"
4041
- },
4042
- "peerDependencies": {
4043
- "@edge-runtime/vm": "*",
4044
- "@types/debug": "^4.1.12",
4045
- "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0",
4046
- "@vitest/browser-playwright": "4.0.6",
4047
- "@vitest/browser-preview": "4.0.6",
4048
- "@vitest/browser-webdriverio": "4.0.6",
4049
- "@vitest/ui": "4.0.6",
4050
- "happy-dom": "*",
4051
- "jsdom": "*"
4052
- },
4053
- "peerDependenciesMeta": {
4054
- "@edge-runtime/vm": {
4055
- "optional": true
4056
- },
4057
- "@types/debug": {
4058
- "optional": true
4059
- },
4060
- "@types/node": {
4061
- "optional": true
4062
- },
4063
- "@vitest/browser-playwright": {
4064
- "optional": true
4065
- },
4066
- "@vitest/browser-preview": {
4067
- "optional": true
4068
- },
4069
- "@vitest/browser-webdriverio": {
4070
- "optional": true
4071
- },
4072
- "@vitest/ui": {
4073
- "optional": true
4074
- },
4075
- "happy-dom": {
4076
- "optional": true
4077
- },
4078
- "jsdom": {
4079
- "optional": true
4080
- }
4081
- }
4082
- },
4083
- "node_modules/vitest/node_modules/@esbuild/aix-ppc64": {
4084
- "version": "0.25.12",
4085
- "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz",
4086
- "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==",
4087
  "cpu": [
4088
  "ppc64"
4089
  ],
@@ -4093,13 +4439,13 @@
4093
  "aix"
4094
  ],
4095
  "engines": {
4096
- "node": ">=18"
4097
  }
4098
  },
4099
- "node_modules/vitest/node_modules/@esbuild/android-arm": {
4100
- "version": "0.25.12",
4101
- "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz",
4102
- "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==",
4103
  "cpu": [
4104
  "arm"
4105
  ],
@@ -4109,13 +4455,13 @@
4109
  "android"
4110
  ],
4111
  "engines": {
4112
- "node": ">=18"
4113
  }
4114
  },
4115
- "node_modules/vitest/node_modules/@esbuild/android-arm64": {
4116
- "version": "0.25.12",
4117
- "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz",
4118
- "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==",
4119
  "cpu": [
4120
  "arm64"
4121
  ],
@@ -4125,13 +4471,13 @@
4125
  "android"
4126
  ],
4127
  "engines": {
4128
- "node": ">=18"
4129
  }
4130
  },
4131
- "node_modules/vitest/node_modules/@esbuild/android-x64": {
4132
- "version": "0.25.12",
4133
- "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz",
4134
- "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==",
4135
  "cpu": [
4136
  "x64"
4137
  ],
@@ -4141,13 +4487,13 @@
4141
  "android"
4142
  ],
4143
  "engines": {
4144
- "node": ">=18"
4145
  }
4146
  },
4147
- "node_modules/vitest/node_modules/@esbuild/darwin-arm64": {
4148
- "version": "0.25.12",
4149
- "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz",
4150
- "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==",
4151
  "cpu": [
4152
  "arm64"
4153
  ],
@@ -4157,13 +4503,13 @@
4157
  "darwin"
4158
  ],
4159
  "engines": {
4160
- "node": ">=18"
4161
  }
4162
  },
4163
- "node_modules/vitest/node_modules/@esbuild/darwin-x64": {
4164
- "version": "0.25.12",
4165
- "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz",
4166
- "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==",
4167
  "cpu": [
4168
  "x64"
4169
  ],
@@ -4173,13 +4519,13 @@
4173
  "darwin"
4174
  ],
4175
  "engines": {
4176
- "node": ">=18"
4177
  }
4178
  },
4179
- "node_modules/vitest/node_modules/@esbuild/freebsd-arm64": {
4180
- "version": "0.25.12",
4181
- "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz",
4182
- "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==",
4183
  "cpu": [
4184
  "arm64"
4185
  ],
@@ -4189,13 +4535,13 @@
4189
  "freebsd"
4190
  ],
4191
  "engines": {
4192
- "node": ">=18"
4193
  }
4194
  },
4195
- "node_modules/vitest/node_modules/@esbuild/freebsd-x64": {
4196
- "version": "0.25.12",
4197
- "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz",
4198
- "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==",
4199
  "cpu": [
4200
  "x64"
4201
  ],
@@ -4205,13 +4551,13 @@
4205
  "freebsd"
4206
  ],
4207
  "engines": {
4208
- "node": ">=18"
4209
  }
4210
  },
4211
- "node_modules/vitest/node_modules/@esbuild/linux-arm": {
4212
- "version": "0.25.12",
4213
- "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz",
4214
- "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==",
4215
  "cpu": [
4216
  "arm"
4217
  ],
@@ -4221,13 +4567,13 @@
4221
  "linux"
4222
  ],
4223
  "engines": {
4224
- "node": ">=18"
4225
  }
4226
  },
4227
- "node_modules/vitest/node_modules/@esbuild/linux-arm64": {
4228
- "version": "0.25.12",
4229
- "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz",
4230
- "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==",
4231
  "cpu": [
4232
  "arm64"
4233
  ],
@@ -4237,13 +4583,13 @@
4237
  "linux"
4238
  ],
4239
  "engines": {
4240
- "node": ">=18"
4241
  }
4242
  },
4243
- "node_modules/vitest/node_modules/@esbuild/linux-ia32": {
4244
- "version": "0.25.12",
4245
- "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz",
4246
- "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==",
4247
  "cpu": [
4248
  "ia32"
4249
  ],
@@ -4253,13 +4599,13 @@
4253
  "linux"
4254
  ],
4255
  "engines": {
4256
- "node": ">=18"
4257
  }
4258
  },
4259
- "node_modules/vitest/node_modules/@esbuild/linux-loong64": {
4260
- "version": "0.25.12",
4261
- "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz",
4262
- "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==",
4263
  "cpu": [
4264
  "loong64"
4265
  ],
@@ -4269,13 +4615,13 @@
4269
  "linux"
4270
  ],
4271
  "engines": {
4272
- "node": ">=18"
4273
  }
4274
  },
4275
- "node_modules/vitest/node_modules/@esbuild/linux-mips64el": {
4276
- "version": "0.25.12",
4277
- "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz",
4278
- "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==",
4279
  "cpu": [
4280
  "mips64el"
4281
  ],
@@ -4285,13 +4631,13 @@
4285
  "linux"
4286
  ],
4287
  "engines": {
4288
- "node": ">=18"
4289
  }
4290
  },
4291
- "node_modules/vitest/node_modules/@esbuild/linux-ppc64": {
4292
- "version": "0.25.12",
4293
- "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz",
4294
- "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==",
4295
  "cpu": [
4296
  "ppc64"
4297
  ],
@@ -4301,13 +4647,13 @@
4301
  "linux"
4302
  ],
4303
  "engines": {
4304
- "node": ">=18"
4305
  }
4306
  },
4307
- "node_modules/vitest/node_modules/@esbuild/linux-riscv64": {
4308
- "version": "0.25.12",
4309
- "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz",
4310
- "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==",
4311
  "cpu": [
4312
  "riscv64"
4313
  ],
@@ -4317,13 +4663,13 @@
4317
  "linux"
4318
  ],
4319
  "engines": {
4320
- "node": ">=18"
4321
  }
4322
  },
4323
- "node_modules/vitest/node_modules/@esbuild/linux-s390x": {
4324
- "version": "0.25.12",
4325
- "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz",
4326
- "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==",
4327
  "cpu": [
4328
  "s390x"
4329
  ],
@@ -4333,13 +4679,13 @@
4333
  "linux"
4334
  ],
4335
  "engines": {
4336
- "node": ">=18"
4337
  }
4338
  },
4339
- "node_modules/vitest/node_modules/@esbuild/linux-x64": {
4340
- "version": "0.25.12",
4341
- "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz",
4342
- "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==",
4343
  "cpu": [
4344
  "x64"
4345
  ],
@@ -4349,13 +4695,13 @@
4349
  "linux"
4350
  ],
4351
  "engines": {
4352
- "node": ">=18"
4353
  }
4354
  },
4355
- "node_modules/vitest/node_modules/@esbuild/netbsd-x64": {
4356
- "version": "0.25.12",
4357
- "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz",
4358
- "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==",
4359
  "cpu": [
4360
  "x64"
4361
  ],
@@ -4365,13 +4711,13 @@
4365
  "netbsd"
4366
  ],
4367
  "engines": {
4368
- "node": ">=18"
4369
  }
4370
  },
4371
- "node_modules/vitest/node_modules/@esbuild/openbsd-x64": {
4372
- "version": "0.25.12",
4373
- "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz",
4374
- "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==",
4375
  "cpu": [
4376
  "x64"
4377
  ],
@@ -4381,13 +4727,13 @@
4381
  "openbsd"
4382
  ],
4383
  "engines": {
4384
- "node": ">=18"
4385
  }
4386
  },
4387
- "node_modules/vitest/node_modules/@esbuild/sunos-x64": {
4388
- "version": "0.25.12",
4389
- "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz",
4390
- "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==",
4391
  "cpu": [
4392
  "x64"
4393
  ],
@@ -4397,13 +4743,13 @@
4397
  "sunos"
4398
  ],
4399
  "engines": {
4400
- "node": ">=18"
4401
  }
4402
  },
4403
- "node_modules/vitest/node_modules/@esbuild/win32-arm64": {
4404
- "version": "0.25.12",
4405
- "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz",
4406
- "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==",
4407
  "cpu": [
4408
  "arm64"
4409
  ],
@@ -4413,13 +4759,13 @@
4413
  "win32"
4414
  ],
4415
  "engines": {
4416
- "node": ">=18"
4417
  }
4418
  },
4419
- "node_modules/vitest/node_modules/@esbuild/win32-ia32": {
4420
- "version": "0.25.12",
4421
- "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz",
4422
- "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==",
4423
  "cpu": [
4424
  "ia32"
4425
  ],
@@ -4429,13 +4775,13 @@
4429
  "win32"
4430
  ],
4431
  "engines": {
4432
- "node": ">=18"
4433
  }
4434
  },
4435
- "node_modules/vitest/node_modules/@esbuild/win32-x64": {
4436
- "version": "0.25.12",
4437
- "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz",
4438
- "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==",
4439
  "cpu": [
4440
  "x64"
4441
  ],
@@ -4445,7 +4791,122 @@
4445
  "win32"
4446
  ],
4447
  "engines": {
4448
- "node": ">=18"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4449
  }
4450
  },
4451
  "node_modules/vitest/node_modules/@vitest/mocker": {
@@ -4474,47 +4935,6 @@
4474
  }
4475
  }
4476
  },
4477
- "node_modules/vitest/node_modules/esbuild": {
4478
- "version": "0.25.12",
4479
- "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz",
4480
- "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==",
4481
- "dev": true,
4482
- "hasInstallScript": true,
4483
- "bin": {
4484
- "esbuild": "bin/esbuild"
4485
- },
4486
- "engines": {
4487
- "node": ">=18"
4488
- },
4489
- "optionalDependencies": {
4490
- "@esbuild/aix-ppc64": "0.25.12",
4491
- "@esbuild/android-arm": "0.25.12",
4492
- "@esbuild/android-arm64": "0.25.12",
4493
- "@esbuild/android-x64": "0.25.12",
4494
- "@esbuild/darwin-arm64": "0.25.12",
4495
- "@esbuild/darwin-x64": "0.25.12",
4496
- "@esbuild/freebsd-arm64": "0.25.12",
4497
- "@esbuild/freebsd-x64": "0.25.12",
4498
- "@esbuild/linux-arm": "0.25.12",
4499
- "@esbuild/linux-arm64": "0.25.12",
4500
- "@esbuild/linux-ia32": "0.25.12",
4501
- "@esbuild/linux-loong64": "0.25.12",
4502
- "@esbuild/linux-mips64el": "0.25.12",
4503
- "@esbuild/linux-ppc64": "0.25.12",
4504
- "@esbuild/linux-riscv64": "0.25.12",
4505
- "@esbuild/linux-s390x": "0.25.12",
4506
- "@esbuild/linux-x64": "0.25.12",
4507
- "@esbuild/netbsd-arm64": "0.25.12",
4508
- "@esbuild/netbsd-x64": "0.25.12",
4509
- "@esbuild/openbsd-arm64": "0.25.12",
4510
- "@esbuild/openbsd-x64": "0.25.12",
4511
- "@esbuild/openharmony-arm64": "0.25.12",
4512
- "@esbuild/sunos-x64": "0.25.12",
4513
- "@esbuild/win32-arm64": "0.25.12",
4514
- "@esbuild/win32-ia32": "0.25.12",
4515
- "@esbuild/win32-x64": "0.25.12"
4516
- }
4517
- },
4518
  "node_modules/vitest/node_modules/estree-walker": {
4519
  "version": "3.0.3",
4520
  "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz",
@@ -4598,6 +5018,12 @@
4598
  }
4599
  }
4600
  },
 
 
 
 
 
 
4601
  "node_modules/vue": {
4602
  "version": "3.5.22",
4603
  "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.22.tgz",
@@ -4619,6 +5045,22 @@
4619
  }
4620
  }
4621
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4622
  "node_modules/w3c-xmlserializer": {
4623
  "version": "5.0.0",
4624
  "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz",
@@ -4790,6 +5232,18 @@
4790
  "node": ">=10"
4791
  }
4792
  },
 
 
 
 
 
 
 
 
 
 
 
 
4793
  "node_modules/yargs": {
4794
  "version": "18.0.0",
4795
  "resolved": "https://registry.npmjs.org/yargs/-/yargs-18.0.0.tgz",
 
16
  "concurrently": "^7.6.0",
17
  "eslint-config-prettier": "^10.1.8",
18
  "eslint-plugin-prettier": "^5.5.4",
19
+ "husky": "^9.1.7",
20
  "jison": "^0.4.18",
21
  "jsdom": "^27.1.0",
22
+ "lint-staged": "^16.2.7",
23
+ "onnxruntime-node": "1.23.2",
24
+ "onnxruntime-web": "1.23.2",
25
  "prettier": "^3.6.2",
26
  "tsx": "^4.20.6",
27
  "typescript": "^5.2.2",
28
  "vite": "^5.4.21",
29
  "vitest": "^4.0.6",
30
  "vue": "^3.3.4",
31
+ "vue-tsc": "^3.1.3",
32
  "yargs": "^18.0.0"
33
  }
34
  },
 
255
  }
256
  },
257
  "node_modules/@esbuild/aix-ppc64": {
258
+ "version": "0.25.12",
259
+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz",
260
+ "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==",
261
  "cpu": [
262
  "ppc64"
263
  ],
 
267
  "aix"
268
  ],
269
  "engines": {
270
+ "node": ">=18"
271
  }
272
  },
273
  "node_modules/@esbuild/android-arm": {
274
+ "version": "0.25.12",
275
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz",
276
+ "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==",
277
  "cpu": [
278
  "arm"
279
  ],
 
283
  "android"
284
  ],
285
  "engines": {
286
+ "node": ">=18"
287
  }
288
  },
289
  "node_modules/@esbuild/android-arm64": {
290
+ "version": "0.25.12",
291
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz",
292
+ "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==",
293
  "cpu": [
294
  "arm64"
295
  ],
 
299
  "android"
300
  ],
301
  "engines": {
302
+ "node": ">=18"
303
  }
304
  },
305
  "node_modules/@esbuild/android-x64": {
306
+ "version": "0.25.12",
307
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz",
308
+ "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==",
309
  "cpu": [
310
  "x64"
311
  ],
 
315
  "android"
316
  ],
317
  "engines": {
318
+ "node": ">=18"
319
  }
320
  },
321
  "node_modules/@esbuild/darwin-arm64": {
322
+ "version": "0.25.12",
323
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz",
324
+ "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==",
325
  "cpu": [
326
  "arm64"
327
  ],
 
331
  "darwin"
332
  ],
333
  "engines": {
334
+ "node": ">=18"
335
  }
336
  },
337
  "node_modules/@esbuild/darwin-x64": {
338
+ "version": "0.25.12",
339
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz",
340
+ "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==",
341
  "cpu": [
342
  "x64"
343
  ],
 
347
  "darwin"
348
  ],
349
  "engines": {
350
+ "node": ">=18"
351
  }
352
  },
353
  "node_modules/@esbuild/freebsd-arm64": {
354
+ "version": "0.25.12",
355
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz",
356
+ "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==",
357
  "cpu": [
358
  "arm64"
359
  ],
 
363
  "freebsd"
364
  ],
365
  "engines": {
366
+ "node": ">=18"
367
  }
368
  },
369
  "node_modules/@esbuild/freebsd-x64": {
370
+ "version": "0.25.12",
371
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz",
372
+ "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==",
373
  "cpu": [
374
  "x64"
375
  ],
 
379
  "freebsd"
380
  ],
381
  "engines": {
382
+ "node": ">=18"
383
  }
384
  },
385
  "node_modules/@esbuild/linux-arm": {
386
+ "version": "0.25.12",
387
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz",
388
+ "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==",
389
  "cpu": [
390
  "arm"
391
  ],
 
395
  "linux"
396
  ],
397
  "engines": {
398
+ "node": ">=18"
399
  }
400
  },
401
  "node_modules/@esbuild/linux-arm64": {
402
+ "version": "0.25.12",
403
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz",
404
+ "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==",
405
  "cpu": [
406
  "arm64"
407
  ],
 
411
  "linux"
412
  ],
413
  "engines": {
414
+ "node": ">=18"
415
  }
416
  },
417
  "node_modules/@esbuild/linux-ia32": {
418
+ "version": "0.25.12",
419
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz",
420
+ "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==",
421
  "cpu": [
422
  "ia32"
423
  ],
 
427
  "linux"
428
  ],
429
  "engines": {
430
+ "node": ">=18"
431
  }
432
  },
433
  "node_modules/@esbuild/linux-loong64": {
434
+ "version": "0.25.12",
435
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz",
436
+ "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==",
437
  "cpu": [
438
  "loong64"
439
  ],
 
443
  "linux"
444
  ],
445
  "engines": {
446
+ "node": ">=18"
447
  }
448
  },
449
  "node_modules/@esbuild/linux-mips64el": {
450
+ "version": "0.25.12",
451
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz",
452
+ "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==",
453
  "cpu": [
454
  "mips64el"
455
  ],
 
459
  "linux"
460
  ],
461
  "engines": {
462
+ "node": ">=18"
463
  }
464
  },
465
  "node_modules/@esbuild/linux-ppc64": {
466
+ "version": "0.25.12",
467
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz",
468
+ "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==",
469
  "cpu": [
470
  "ppc64"
471
  ],
 
475
  "linux"
476
  ],
477
  "engines": {
478
+ "node": ">=18"
479
  }
480
  },
481
  "node_modules/@esbuild/linux-riscv64": {
482
+ "version": "0.25.12",
483
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz",
484
+ "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==",
485
  "cpu": [
486
  "riscv64"
487
  ],
 
491
  "linux"
492
  ],
493
  "engines": {
494
+ "node": ">=18"
495
  }
496
  },
497
  "node_modules/@esbuild/linux-s390x": {
498
+ "version": "0.25.12",
499
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz",
500
+ "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==",
501
  "cpu": [
502
  "s390x"
503
  ],
 
507
  "linux"
508
  ],
509
  "engines": {
510
+ "node": ">=18"
511
  }
512
  },
513
  "node_modules/@esbuild/linux-x64": {
514
+ "version": "0.25.12",
515
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz",
516
+ "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==",
517
  "cpu": [
518
  "x64"
519
  ],
 
523
  "linux"
524
  ],
525
  "engines": {
526
+ "node": ">=18"
527
  }
528
  },
529
  "node_modules/@esbuild/netbsd-arm64": {
 
543
  }
544
  },
545
  "node_modules/@esbuild/netbsd-x64": {
546
+ "version": "0.25.12",
547
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz",
548
+ "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==",
549
  "cpu": [
550
  "x64"
551
  ],
 
555
  "netbsd"
556
  ],
557
  "engines": {
558
+ "node": ">=18"
559
  }
560
  },
561
  "node_modules/@esbuild/openbsd-arm64": {
 
575
  }
576
  },
577
  "node_modules/@esbuild/openbsd-x64": {
578
+ "version": "0.25.12",
579
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz",
580
+ "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==",
581
  "cpu": [
582
  "x64"
583
  ],
 
587
  "openbsd"
588
  ],
589
  "engines": {
590
+ "node": ">=18"
591
  }
592
  },
593
  "node_modules/@esbuild/openharmony-arm64": {
 
607
  }
608
  },
609
  "node_modules/@esbuild/sunos-x64": {
610
+ "version": "0.25.12",
611
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz",
612
+ "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==",
613
  "cpu": [
614
  "x64"
615
  ],
 
619
  "sunos"
620
  ],
621
  "engines": {
622
+ "node": ">=18"
623
  }
624
  },
625
  "node_modules/@esbuild/win32-arm64": {
626
+ "version": "0.25.12",
627
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz",
628
+ "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==",
629
  "cpu": [
630
  "arm64"
631
  ],
 
635
  "win32"
636
  ],
637
  "engines": {
638
+ "node": ">=18"
639
  }
640
  },
641
  "node_modules/@esbuild/win32-ia32": {
642
+ "version": "0.25.12",
643
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz",
644
+ "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==",
645
  "cpu": [
646
  "ia32"
647
  ],
 
651
  "win32"
652
  ],
653
  "engines": {
654
+ "node": ">=18"
655
  }
656
  },
657
  "node_modules/@esbuild/win32-x64": {
658
+ "version": "0.25.12",
659
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz",
660
+ "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==",
661
  "cpu": [
662
  "x64"
663
  ],
 
667
  "win32"
668
  ],
669
  "engines": {
670
+ "node": ">=18"
671
  }
672
  },
673
  "node_modules/@eslint-community/eslint-utils": {
 
890
  "integrity": "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==",
891
  "dev": true
892
  },
893
+ "node_modules/@protobufjs/aspromise": {
894
+ "version": "1.1.2",
895
+ "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz",
896
+ "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==",
897
+ "dev": true
898
+ },
899
+ "node_modules/@protobufjs/base64": {
900
+ "version": "1.1.2",
901
+ "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz",
902
+ "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==",
903
+ "dev": true
904
+ },
905
+ "node_modules/@protobufjs/codegen": {
906
+ "version": "2.0.4",
907
+ "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz",
908
+ "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==",
909
+ "dev": true
910
+ },
911
+ "node_modules/@protobufjs/eventemitter": {
912
+ "version": "1.1.0",
913
+ "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz",
914
+ "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==",
915
+ "dev": true
916
+ },
917
+ "node_modules/@protobufjs/fetch": {
918
+ "version": "1.1.0",
919
+ "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz",
920
+ "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==",
921
+ "dev": true,
922
+ "dependencies": {
923
+ "@protobufjs/aspromise": "^1.1.1",
924
+ "@protobufjs/inquire": "^1.1.0"
925
+ }
926
+ },
927
+ "node_modules/@protobufjs/float": {
928
+ "version": "1.0.2",
929
+ "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz",
930
+ "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==",
931
+ "dev": true
932
+ },
933
+ "node_modules/@protobufjs/inquire": {
934
+ "version": "1.1.0",
935
+ "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz",
936
+ "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==",
937
+ "dev": true
938
+ },
939
+ "node_modules/@protobufjs/path": {
940
+ "version": "1.1.2",
941
+ "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz",
942
+ "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==",
943
+ "dev": true
944
+ },
945
+ "node_modules/@protobufjs/pool": {
946
+ "version": "1.1.0",
947
+ "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz",
948
+ "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==",
949
+ "dev": true
950
+ },
951
+ "node_modules/@protobufjs/utf8": {
952
+ "version": "1.1.0",
953
+ "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz",
954
+ "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==",
955
+ "dev": true
956
+ },
957
  "node_modules/@rollup/rollup-android-arm-eabi": {
958
  "version": "4.52.5",
959
  "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.52.5.tgz",
 
1411
  "url": "https://opencollective.com/vitest"
1412
  }
1413
  },
1414
+ "node_modules/@volar/language-core": {
1415
+ "version": "2.4.23",
1416
+ "resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-2.4.23.tgz",
1417
+ "integrity": "sha512-hEEd5ET/oSmBC6pi1j6NaNYRWoAiDhINbT8rmwtINugR39loROSlufGdYMF9TaKGfz+ViGs1Idi3mAhnuPcoGQ==",
1418
+ "dev": true,
1419
+ "dependencies": {
1420
+ "@volar/source-map": "2.4.23"
1421
+ }
1422
+ },
1423
+ "node_modules/@volar/source-map": {
1424
+ "version": "2.4.23",
1425
+ "resolved": "https://registry.npmjs.org/@volar/source-map/-/source-map-2.4.23.tgz",
1426
+ "integrity": "sha512-Z1Uc8IB57Lm6k7q6KIDu/p+JWtf3xsXJqAX/5r18hYOTpJyBn0KXUR8oTJ4WFYOcDzWC9n3IflGgHowx6U6z9Q==",
1427
+ "dev": true
1428
+ },
1429
+ "node_modules/@volar/typescript": {
1430
+ "version": "2.4.23",
1431
+ "resolved": "https://registry.npmjs.org/@volar/typescript/-/typescript-2.4.23.tgz",
1432
+ "integrity": "sha512-lAB5zJghWxVPqfcStmAP1ZqQacMpe90UrP5RJ3arDyrhy4aCUQqmxPPLB2PWDKugvylmO41ljK7vZ+t6INMTag==",
1433
+ "dev": true,
1434
+ "dependencies": {
1435
+ "@volar/language-core": "2.4.23",
1436
+ "path-browserify": "^1.0.1",
1437
+ "vscode-uri": "^3.0.8"
1438
+ }
1439
+ },
1440
  "node_modules/@vue/compiler-core": {
1441
  "version": "3.5.22",
1442
  "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.22.tgz",
 
1499
  "@vue/shared": "3.5.22"
1500
  }
1501
  },
1502
+ "node_modules/@vue/language-core": {
1503
+ "version": "3.1.3",
1504
+ "resolved": "https://registry.npmjs.org/@vue/language-core/-/language-core-3.1.3.tgz",
1505
+ "integrity": "sha512-KpR1F/eGAG9D1RZ0/T6zWJs6dh/pRLfY5WupecyYKJ1fjVmDMgTPw9wXmKv2rBjo4zCJiOSiyB8BDP1OUwpMEA==",
1506
+ "dev": true,
1507
+ "dependencies": {
1508
+ "@volar/language-core": "2.4.23",
1509
+ "@vue/compiler-dom": "^3.5.0",
1510
+ "@vue/shared": "^3.5.0",
1511
+ "alien-signals": "^3.0.0",
1512
+ "muggle-string": "^0.4.1",
1513
+ "path-browserify": "^1.0.1",
1514
+ "picomatch": "^4.0.2"
1515
+ },
1516
+ "peerDependencies": {
1517
+ "typescript": "*"
1518
+ },
1519
+ "peerDependenciesMeta": {
1520
+ "typescript": {
1521
+ "optional": true
1522
+ }
1523
+ }
1524
+ },
1525
  "node_modules/@vue/reactivity": {
1526
  "version": "3.5.22",
1527
  "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.22.tgz",
 
1595
  "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
1596
  }
1597
  },
1598
+ "node_modules/adm-zip": {
1599
+ "version": "0.5.16",
1600
+ "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.16.tgz",
1601
+ "integrity": "sha512-TGw5yVi4saajsSEgz25grObGHEUaDrniwvA2qwSC060KfqGPdglhvPMA2lPIoxs3PQIItj2iag35fONcQqgUaQ==",
1602
+ "dev": true,
1603
+ "engines": {
1604
+ "node": ">=12.0"
1605
+ }
1606
+ },
1607
  "node_modules/agent-base": {
1608
  "version": "7.1.4",
1609
  "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz",
 
1630
  "url": "https://github.com/sponsors/epoberezkin"
1631
  }
1632
  },
1633
+ "node_modules/alien-signals": {
1634
+ "version": "3.1.0",
1635
+ "resolved": "https://registry.npmjs.org/alien-signals/-/alien-signals-3.1.0.tgz",
1636
+ "integrity": "sha512-yufC6VpSy8tK3I0lO67pjumo5JvDQVQyr38+3OHqe6CHl1t2VZekKZ7EKKZSqk0cRmE7U7tfZbpXiKNzuc+ckg==",
1637
+ "dev": true
1638
+ },
1639
  "node_modules/amdefine": {
1640
  "version": "1.0.1",
1641
  "resolved": "https://registry.npmjs.org/amdefine/-/amdefine-1.0.1.tgz",
 
1646
  "node": ">=0.4.2"
1647
  }
1648
  },
1649
+ "node_modules/ansi-escapes": {
1650
+ "version": "7.2.0",
1651
+ "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.2.0.tgz",
1652
+ "integrity": "sha512-g6LhBsl+GBPRWGWsBtutpzBYuIIdBkLEvad5C/va/74Db018+5TZiyA26cZJAr3Rft5lprVqOIPxf5Vid6tqAw==",
1653
+ "dev": true,
1654
+ "dependencies": {
1655
+ "environment": "^1.0.0"
1656
+ },
1657
+ "engines": {
1658
+ "node": ">=18"
1659
+ },
1660
+ "funding": {
1661
+ "url": "https://github.com/sponsors/sindresorhus"
1662
+ }
1663
+ },
1664
  "node_modules/ansi-regex": {
1665
  "version": "6.2.2",
1666
  "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz",
 
1720
  "require-from-string": "^2.0.2"
1721
  }
1722
  },
1723
+ "node_modules/boolean": {
1724
+ "version": "3.2.0",
1725
+ "resolved": "https://registry.npmjs.org/boolean/-/boolean-3.2.0.tgz",
1726
+ "integrity": "sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==",
1727
+ "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.",
1728
+ "dev": true
1729
+ },
1730
  "node_modules/brace-expansion": {
1731
  "version": "1.1.12",
1732
  "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
 
1738
  "concat-map": "0.0.1"
1739
  }
1740
  },
1741
+ "node_modules/braces": {
1742
+ "version": "3.0.3",
1743
+ "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
1744
+ "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
1745
+ "dev": true,
1746
+ "dependencies": {
1747
+ "fill-range": "^7.1.1"
1748
+ },
1749
+ "engines": {
1750
+ "node": ">=8"
1751
+ }
1752
+ },
1753
+ "node_modules/callsites": {
1754
+ "version": "3.1.0",
1755
+ "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
1756
+ "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
1757
  "dev": true,
1758
  "peer": true,
1759
  "engines": {
 
1785
  "url": "https://github.com/chalk/chalk?sponsor=1"
1786
  }
1787
  },
1788
+ "node_modules/chalk/node_modules/supports-color": {
1789
+ "version": "7.2.0",
1790
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
1791
+ "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
1792
+ "dev": true,
1793
+ "dependencies": {
1794
+ "has-flag": "^4.0.0"
1795
+ },
1796
+ "engines": {
1797
+ "node": ">=8"
1798
+ }
1799
+ },
1800
  "node_modules/cjson": {
1801
  "version": "0.3.0",
1802
  "resolved": "https://registry.npmjs.org/cjson/-/cjson-0.3.0.tgz",
 
1809
  "node": ">= 0.3.0"
1810
  }
1811
  },
1812
+ "node_modules/cli-cursor": {
1813
+ "version": "5.0.0",
1814
+ "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz",
1815
+ "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==",
1816
+ "dev": true,
1817
+ "dependencies": {
1818
+ "restore-cursor": "^5.0.0"
1819
+ },
1820
+ "engines": {
1821
+ "node": ">=18"
1822
+ },
1823
+ "funding": {
1824
+ "url": "https://github.com/sponsors/sindresorhus"
1825
+ }
1826
+ },
1827
+ "node_modules/cli-truncate": {
1828
+ "version": "5.1.1",
1829
+ "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-5.1.1.tgz",
1830
+ "integrity": "sha512-SroPvNHxUnk+vIW/dOSfNqdy1sPEFkrTk6TUtqLCnBlo3N7TNYYkzzN7uSD6+jVjrdO4+p8nH7JzH6cIvUem6A==",
1831
+ "dev": true,
1832
+ "dependencies": {
1833
+ "slice-ansi": "^7.1.0",
1834
+ "string-width": "^8.0.0"
1835
+ },
1836
+ "engines": {
1837
+ "node": ">=20"
1838
+ },
1839
+ "funding": {
1840
+ "url": "https://github.com/sponsors/sindresorhus"
1841
+ }
1842
+ },
1843
+ "node_modules/cli-truncate/node_modules/string-width": {
1844
+ "version": "8.1.0",
1845
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.1.0.tgz",
1846
+ "integrity": "sha512-Kxl3KJGb/gxkaUMOjRsQ8IrXiGW75O4E3RPjFIINOVH8AMl2SQ/yWdTzWwF3FevIX9LcMAjJW+GRwAlAbTSXdg==",
1847
+ "dev": true,
1848
+ "dependencies": {
1849
+ "get-east-asian-width": "^1.3.0",
1850
+ "strip-ansi": "^7.1.0"
1851
+ },
1852
+ "engines": {
1853
+ "node": ">=20"
1854
+ },
1855
+ "funding": {
1856
+ "url": "https://github.com/sponsors/sindresorhus"
1857
+ }
1858
+ },
1859
  "node_modules/cliui": {
1860
  "version": "9.0.1",
1861
  "resolved": "https://registry.npmjs.org/cliui/-/cliui-9.0.1.tgz",
 
1888
  "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
1889
  "dev": true
1890
  },
1891
+ "node_modules/colorette": {
1892
+ "version": "2.0.20",
1893
+ "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz",
1894
+ "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==",
1895
+ "dev": true
1896
+ },
1897
  "node_modules/colors": {
1898
  "version": "0.5.1",
1899
  "resolved": "https://registry.npmjs.org/colors/-/colors-0.5.1.tgz",
 
1903
  "node": ">=0.1.90"
1904
  }
1905
  },
1906
+ "node_modules/commander": {
1907
+ "version": "14.0.2",
1908
+ "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.2.tgz",
1909
+ "integrity": "sha512-TywoWNNRbhoD0BXs1P3ZEScW8W5iKrnbithIl0YH+uCmBd0QpPOA8yc82DS3BIE5Ma6FnBVUsJ7wVUDz4dvOWQ==",
1910
+ "dev": true,
1911
+ "engines": {
1912
+ "node": ">=20"
1913
+ }
1914
+ },
1915
  "node_modules/concat-map": {
1916
  "version": "0.0.1",
1917
  "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
 
2001
  "node": ">=8"
2002
  }
2003
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2004
  "node_modules/concurrently/node_modules/wrap-ansi": {
2005
  "version": "7.0.0",
2006
  "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
 
2152
  "dev": true,
2153
  "peer": true
2154
  },
2155
+ "node_modules/define-data-property": {
2156
+ "version": "1.1.4",
2157
+ "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz",
2158
+ "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==",
2159
+ "dev": true,
2160
+ "dependencies": {
2161
+ "es-define-property": "^1.0.0",
2162
+ "es-errors": "^1.3.0",
2163
+ "gopd": "^1.0.1"
2164
+ },
2165
+ "engines": {
2166
+ "node": ">= 0.4"
2167
+ },
2168
+ "funding": {
2169
+ "url": "https://github.com/sponsors/ljharb"
2170
+ }
2171
+ },
2172
+ "node_modules/define-properties": {
2173
+ "version": "1.2.1",
2174
+ "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz",
2175
+ "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==",
2176
+ "dev": true,
2177
+ "dependencies": {
2178
+ "define-data-property": "^1.0.1",
2179
+ "has-property-descriptors": "^1.0.0",
2180
+ "object-keys": "^1.1.1"
2181
+ },
2182
+ "engines": {
2183
+ "node": ">= 0.4"
2184
+ },
2185
+ "funding": {
2186
+ "url": "https://github.com/sponsors/ljharb"
2187
+ }
2188
+ },
2189
+ "node_modules/detect-node": {
2190
+ "version": "2.1.0",
2191
+ "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz",
2192
+ "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==",
2193
+ "dev": true
2194
+ },
2195
  "node_modules/ebnf-parser": {
2196
  "version": "0.1.10",
2197
  "resolved": "https://registry.npmjs.org/ebnf-parser/-/ebnf-parser-0.1.10.tgz",
 
2216
  "url": "https://github.com/fb55/entities?sponsor=1"
2217
  }
2218
  },
2219
+ "node_modules/environment": {
2220
+ "version": "1.1.0",
2221
+ "resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz",
2222
+ "integrity": "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==",
2223
+ "dev": true,
2224
+ "engines": {
2225
+ "node": ">=18"
2226
+ },
2227
+ "funding": {
2228
+ "url": "https://github.com/sponsors/sindresorhus"
2229
+ }
2230
+ },
2231
+ "node_modules/es-define-property": {
2232
+ "version": "1.0.1",
2233
+ "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
2234
+ "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
2235
+ "dev": true,
2236
+ "engines": {
2237
+ "node": ">= 0.4"
2238
+ }
2239
+ },
2240
+ "node_modules/es-errors": {
2241
+ "version": "1.3.0",
2242
+ "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
2243
+ "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
2244
+ "dev": true,
2245
+ "engines": {
2246
+ "node": ">= 0.4"
2247
+ }
2248
+ },
2249
  "node_modules/es-module-lexer": {
2250
  "version": "1.7.0",
2251
  "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz",
2252
  "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==",
2253
  "dev": true
2254
  },
2255
+ "node_modules/es6-error": {
2256
+ "version": "4.1.1",
2257
+ "resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz",
2258
+ "integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==",
2259
+ "dev": true
2260
+ },
2261
  "node_modules/esbuild": {
2262
+ "version": "0.25.12",
2263
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz",
2264
+ "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==",
2265
  "dev": true,
2266
  "hasInstallScript": true,
2267
  "bin": {
2268
  "esbuild": "bin/esbuild"
2269
  },
2270
  "engines": {
2271
+ "node": ">=18"
2272
  },
2273
  "optionalDependencies": {
2274
+ "@esbuild/aix-ppc64": "0.25.12",
2275
+ "@esbuild/android-arm": "0.25.12",
2276
+ "@esbuild/android-arm64": "0.25.12",
2277
+ "@esbuild/android-x64": "0.25.12",
2278
+ "@esbuild/darwin-arm64": "0.25.12",
2279
+ "@esbuild/darwin-x64": "0.25.12",
2280
+ "@esbuild/freebsd-arm64": "0.25.12",
2281
+ "@esbuild/freebsd-x64": "0.25.12",
2282
+ "@esbuild/linux-arm": "0.25.12",
2283
+ "@esbuild/linux-arm64": "0.25.12",
2284
+ "@esbuild/linux-ia32": "0.25.12",
2285
+ "@esbuild/linux-loong64": "0.25.12",
2286
+ "@esbuild/linux-mips64el": "0.25.12",
2287
+ "@esbuild/linux-ppc64": "0.25.12",
2288
+ "@esbuild/linux-riscv64": "0.25.12",
2289
+ "@esbuild/linux-s390x": "0.25.12",
2290
+ "@esbuild/linux-x64": "0.25.12",
2291
+ "@esbuild/netbsd-arm64": "0.25.12",
2292
+ "@esbuild/netbsd-x64": "0.25.12",
2293
+ "@esbuild/openbsd-arm64": "0.25.12",
2294
+ "@esbuild/openbsd-x64": "0.25.12",
2295
+ "@esbuild/openharmony-arm64": "0.25.12",
2296
+ "@esbuild/sunos-x64": "0.25.12",
2297
+ "@esbuild/win32-arm64": "0.25.12",
2298
+ "@esbuild/win32-ia32": "0.25.12",
2299
+ "@esbuild/win32-x64": "0.25.12"
2300
  }
2301
  },
2302
  "node_modules/escalade": {
 
2313
  "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
2314
  "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
2315
  "dev": true,
 
2316
  "engines": {
2317
  "node": ">=10"
2318
  },
 
2577
  "node": ">=0.10.0"
2578
  }
2579
  },
2580
+ "node_modules/eventemitter3": {
2581
+ "version": "5.0.1",
2582
+ "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz",
2583
+ "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==",
2584
+ "dev": true
2585
+ },
2586
  "node_modules/expect-type": {
2587
  "version": "1.2.2",
2588
  "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.2.2.tgz",
 
2655
  "node": ">=16.0.0"
2656
  }
2657
  },
2658
+ "node_modules/fill-range": {
2659
+ "version": "7.1.1",
2660
+ "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
2661
+ "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
2662
+ "dev": true,
2663
+ "dependencies": {
2664
+ "to-regex-range": "^5.0.1"
2665
+ },
2666
+ "engines": {
2667
+ "node": ">=8"
2668
+ }
2669
+ },
2670
  "node_modules/find-up": {
2671
  "version": "5.0.0",
2672
  "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
 
2698
  "node": ">=16"
2699
  }
2700
  },
2701
+ "node_modules/flatbuffers": {
2702
+ "version": "25.9.23",
2703
+ "resolved": "https://registry.npmjs.org/flatbuffers/-/flatbuffers-25.9.23.tgz",
2704
+ "integrity": "sha512-MI1qs7Lo4Syw0EOzUl0xjs2lsoeqFku44KpngfIduHBYvzm8h2+7K8YMQh1JtVVVrUvhLpNwqVi4DERegUJhPQ==",
2705
+ "dev": true
2706
+ },
2707
  "node_modules/flatted": {
2708
  "version": "3.3.3",
2709
  "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz",
 
2770
  "node": ">=10.13.0"
2771
  }
2772
  },
2773
+ "node_modules/global-agent": {
2774
+ "version": "3.0.0",
2775
+ "resolved": "https://registry.npmjs.org/global-agent/-/global-agent-3.0.0.tgz",
2776
+ "integrity": "sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q==",
2777
+ "dev": true,
2778
+ "dependencies": {
2779
+ "boolean": "^3.0.1",
2780
+ "es6-error": "^4.1.1",
2781
+ "matcher": "^3.0.0",
2782
+ "roarr": "^2.15.3",
2783
+ "semver": "^7.3.2",
2784
+ "serialize-error": "^7.0.1"
2785
+ },
2786
+ "engines": {
2787
+ "node": ">=10.0"
2788
+ }
2789
+ },
2790
  "node_modules/globals": {
2791
  "version": "14.0.0",
2792
  "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz",
 
2800
  "url": "https://github.com/sponsors/sindresorhus"
2801
  }
2802
  },
2803
+ "node_modules/globalthis": {
2804
+ "version": "1.0.4",
2805
+ "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz",
2806
+ "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==",
2807
+ "dev": true,
2808
+ "dependencies": {
2809
+ "define-properties": "^1.2.1",
2810
+ "gopd": "^1.0.1"
2811
+ },
2812
+ "engines": {
2813
+ "node": ">= 0.4"
2814
+ },
2815
+ "funding": {
2816
+ "url": "https://github.com/sponsors/ljharb"
2817
+ }
2818
+ },
2819
+ "node_modules/gopd": {
2820
+ "version": "1.2.0",
2821
+ "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
2822
+ "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
2823
+ "dev": true,
2824
+ "engines": {
2825
+ "node": ">= 0.4"
2826
+ },
2827
+ "funding": {
2828
+ "url": "https://github.com/sponsors/ljharb"
2829
+ }
2830
+ },
2831
+ "node_modules/guid-typescript": {
2832
+ "version": "1.0.9",
2833
+ "resolved": "https://registry.npmjs.org/guid-typescript/-/guid-typescript-1.0.9.tgz",
2834
+ "integrity": "sha512-Y8T4vYhEfwJOTbouREvG+3XDsjr8E3kIr7uf+JZ0BYloFsttiHU0WfvANVsR7TxNUJa/WpCnw/Ino/p+DeBhBQ==",
2835
+ "dev": true
2836
+ },
2837
  "node_modules/has-flag": {
2838
  "version": "4.0.0",
2839
  "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
 
2843
  "node": ">=8"
2844
  }
2845
  },
2846
+ "node_modules/has-property-descriptors": {
2847
+ "version": "1.0.2",
2848
+ "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz",
2849
+ "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==",
2850
+ "dev": true,
2851
+ "dependencies": {
2852
+ "es-define-property": "^1.0.0"
2853
+ },
2854
+ "funding": {
2855
+ "url": "https://github.com/sponsors/ljharb"
2856
+ }
2857
+ },
2858
  "node_modules/html-encoding-sniffer": {
2859
  "version": "4.0.0",
2860
  "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz",
 
2893
  "node": ">= 14"
2894
  }
2895
  },
2896
+ "node_modules/husky": {
2897
+ "version": "9.1.7",
2898
+ "resolved": "https://registry.npmjs.org/husky/-/husky-9.1.7.tgz",
2899
+ "integrity": "sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==",
2900
+ "dev": true,
2901
+ "bin": {
2902
+ "husky": "bin.js"
2903
+ },
2904
+ "engines": {
2905
+ "node": ">=18"
2906
+ },
2907
+ "funding": {
2908
+ "url": "https://github.com/sponsors/typicode"
2909
+ }
2910
+ },
2911
  "node_modules/iconv-lite": {
2912
  "version": "0.6.3",
2913
  "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
 
2989
  "node": ">=0.10.0"
2990
  }
2991
  },
2992
+ "node_modules/is-number": {
2993
+ "version": "7.0.0",
2994
+ "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
2995
+ "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
2996
+ "dev": true,
2997
+ "engines": {
2998
+ "node": ">=0.12.0"
2999
+ }
3000
+ },
3001
  "node_modules/is-potential-custom-element-name": {
3002
  "version": "1.0.1",
3003
  "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz",
 
3122
  "dev": true,
3123
  "peer": true
3124
  },
3125
+ "node_modules/json-stringify-safe": {
3126
+ "version": "5.0.1",
3127
+ "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz",
3128
+ "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==",
3129
+ "dev": true
3130
+ },
3131
  "node_modules/jsonlint": {
3132
  "version": "1.6.0",
3133
  "resolved": "https://registry.npmjs.org/jsonlint/-/jsonlint-1.6.0.tgz",
 
3192
  "integrity": "sha512-DuAEISsr1H4LOpmFLkyMc8YStiRWZCO8hMsoXAXSbgyfvs2WQhSt0+/FBv3ZU/JBFZMGcE+FWzEBSzwUU7U27w==",
3193
  "dev": true
3194
  },
3195
+ "node_modules/lint-staged": {
3196
+ "version": "16.2.7",
3197
+ "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-16.2.7.tgz",
3198
+ "integrity": "sha512-lDIj4RnYmK7/kXMya+qJsmkRFkGolciXjrsZ6PC25GdTfWOAWetR0ZbsNXRAj1EHHImRSalc+whZFg56F5DVow==",
3199
  "dev": true,
 
3200
  "dependencies": {
3201
+ "commander": "^14.0.2",
3202
+ "listr2": "^9.0.5",
3203
+ "micromatch": "^4.0.8",
3204
+ "nano-spawn": "^2.0.0",
3205
+ "pidtree": "^0.6.0",
3206
+ "string-argv": "^0.3.2",
3207
+ "yaml": "^2.8.1"
3208
+ },
3209
+ "bin": {
3210
+ "lint-staged": "bin/lint-staged.js"
3211
  },
3212
  "engines": {
3213
+ "node": ">=20.17"
3214
  },
3215
  "funding": {
3216
+ "url": "https://opencollective.com/lint-staged"
3217
  }
3218
  },
3219
+ "node_modules/listr2": {
3220
+ "version": "9.0.5",
3221
+ "resolved": "https://registry.npmjs.org/listr2/-/listr2-9.0.5.tgz",
3222
+ "integrity": "sha512-ME4Fb83LgEgwNw96RKNvKV4VTLuXfoKudAmm2lP8Kk87KaMK0/Xrx/aAkMWmT8mDb+3MlFDspfbCs7adjRxA2g==",
3223
+ "dev": true,
3224
+ "dependencies": {
3225
+ "cli-truncate": "^5.0.0",
3226
+ "colorette": "^2.0.20",
3227
+ "eventemitter3": "^5.0.1",
3228
+ "log-update": "^6.1.0",
3229
+ "rfdc": "^1.4.1",
3230
+ "wrap-ansi": "^9.0.0"
3231
+ },
3232
+ "engines": {
3233
+ "node": ">=20.0.0"
3234
+ }
3235
+ },
3236
+ "node_modules/locate-path": {
3237
+ "version": "6.0.0",
3238
+ "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
3239
+ "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==",
3240
+ "dev": true,
3241
+ "peer": true,
3242
+ "dependencies": {
3243
+ "p-locate": "^5.0.0"
3244
+ },
3245
+ "engines": {
3246
+ "node": ">=10"
3247
+ },
3248
+ "funding": {
3249
+ "url": "https://github.com/sponsors/sindresorhus"
3250
+ }
3251
+ },
3252
+ "node_modules/lodash": {
3253
+ "version": "4.17.21",
3254
  "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
3255
  "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
3256
  "dev": true
 
3262
  "dev": true,
3263
  "peer": true
3264
  },
3265
+ "node_modules/log-update": {
3266
+ "version": "6.1.0",
3267
+ "resolved": "https://registry.npmjs.org/log-update/-/log-update-6.1.0.tgz",
3268
+ "integrity": "sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==",
3269
+ "dev": true,
3270
+ "dependencies": {
3271
+ "ansi-escapes": "^7.0.0",
3272
+ "cli-cursor": "^5.0.0",
3273
+ "slice-ansi": "^7.1.0",
3274
+ "strip-ansi": "^7.1.0",
3275
+ "wrap-ansi": "^9.0.0"
3276
+ },
3277
+ "engines": {
3278
+ "node": ">=18"
3279
+ },
3280
+ "funding": {
3281
+ "url": "https://github.com/sponsors/sindresorhus"
3282
+ }
3283
+ },
3284
+ "node_modules/long": {
3285
+ "version": "5.3.2",
3286
+ "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz",
3287
+ "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==",
3288
+ "dev": true
3289
+ },
3290
  "node_modules/lru-cache": {
3291
  "version": "11.2.2",
3292
  "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.2.tgz",
 
3305
  "@jridgewell/sourcemap-codec": "^1.5.5"
3306
  }
3307
  },
3308
+ "node_modules/matcher": {
3309
+ "version": "3.0.0",
3310
+ "resolved": "https://registry.npmjs.org/matcher/-/matcher-3.0.0.tgz",
3311
+ "integrity": "sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng==",
3312
+ "dev": true,
3313
+ "dependencies": {
3314
+ "escape-string-regexp": "^4.0.0"
3315
+ },
3316
+ "engines": {
3317
+ "node": ">=10"
3318
+ }
3319
+ },
3320
  "node_modules/mdn-data": {
3321
  "version": "2.12.2",
3322
  "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.12.2.tgz",
3323
  "integrity": "sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA==",
3324
  "dev": true
3325
  },
3326
+ "node_modules/micromatch": {
3327
+ "version": "4.0.8",
3328
+ "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
3329
+ "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
3330
+ "dev": true,
3331
+ "dependencies": {
3332
+ "braces": "^3.0.3",
3333
+ "picomatch": "^2.3.1"
3334
+ },
3335
+ "engines": {
3336
+ "node": ">=8.6"
3337
+ }
3338
+ },
3339
+ "node_modules/micromatch/node_modules/picomatch": {
3340
+ "version": "2.3.1",
3341
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
3342
+ "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
3343
+ "dev": true,
3344
+ "engines": {
3345
+ "node": ">=8.6"
3346
+ },
3347
+ "funding": {
3348
+ "url": "https://github.com/sponsors/jonschlinkert"
3349
+ }
3350
+ },
3351
+ "node_modules/mimic-function": {
3352
+ "version": "5.0.1",
3353
+ "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz",
3354
+ "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==",
3355
+ "dev": true,
3356
+ "engines": {
3357
+ "node": ">=18"
3358
+ },
3359
+ "funding": {
3360
+ "url": "https://github.com/sponsors/sindresorhus"
3361
+ }
3362
+ },
3363
  "node_modules/minimatch": {
3364
  "version": "3.1.2",
3365
  "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
 
3388
  "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
3389
  "dev": true
3390
  },
3391
+ "node_modules/muggle-string": {
3392
+ "version": "0.4.1",
3393
+ "resolved": "https://registry.npmjs.org/muggle-string/-/muggle-string-0.4.1.tgz",
3394
+ "integrity": "sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==",
3395
+ "dev": true
3396
+ },
3397
+ "node_modules/nano-spawn": {
3398
+ "version": "2.0.0",
3399
+ "resolved": "https://registry.npmjs.org/nano-spawn/-/nano-spawn-2.0.0.tgz",
3400
+ "integrity": "sha512-tacvGzUY5o2D8CBh2rrwxyNojUsZNU2zjNTzKQrkgGJQTbGAfArVWXSKMBokBeeg6C7OLRGUEyoFlYbfeWQIqw==",
3401
+ "dev": true,
3402
+ "engines": {
3403
+ "node": ">=20.17"
3404
+ },
3405
+ "funding": {
3406
+ "url": "https://github.com/sindresorhus/nano-spawn?sponsor=1"
3407
+ }
3408
+ },
3409
  "node_modules/nanoid": {
3410
  "version": "3.3.11",
3411
  "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
 
3445
  "node": "*"
3446
  }
3447
  },
3448
+ "node_modules/object-keys": {
3449
+ "version": "1.1.1",
3450
+ "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz",
3451
+ "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==",
3452
+ "dev": true,
3453
+ "engines": {
3454
+ "node": ">= 0.4"
3455
+ }
3456
+ },
3457
+ "node_modules/onetime": {
3458
+ "version": "7.0.0",
3459
+ "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz",
3460
+ "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==",
3461
+ "dev": true,
3462
+ "dependencies": {
3463
+ "mimic-function": "^5.0.0"
3464
+ },
3465
+ "engines": {
3466
+ "node": ">=18"
3467
+ },
3468
+ "funding": {
3469
+ "url": "https://github.com/sponsors/sindresorhus"
3470
+ }
3471
+ },
3472
+ "node_modules/onnxruntime-common": {
3473
+ "version": "1.23.2",
3474
+ "resolved": "https://registry.npmjs.org/onnxruntime-common/-/onnxruntime-common-1.23.2.tgz",
3475
+ "integrity": "sha512-5LFsC9Dukzp2WV6kNHYLNzp8sT6V02IubLCbzw2Xd6X5GOlr65gAX6xiJwyi2URJol/s71gaQLC5F2C25AAR2w==",
3476
+ "dev": true
3477
+ },
3478
+ "node_modules/onnxruntime-node": {
3479
+ "version": "1.23.2",
3480
+ "resolved": "https://registry.npmjs.org/onnxruntime-node/-/onnxruntime-node-1.23.2.tgz",
3481
+ "integrity": "sha512-OBTsG0W8ddBVOeVVVychpVBS87A9YV5sa2hJ6lc025T97Le+J4v++PwSC4XFs1C62SWyNdof0Mh4KvnZgtt4aw==",
3482
+ "dev": true,
3483
+ "hasInstallScript": true,
3484
+ "os": [
3485
+ "win32",
3486
+ "darwin",
3487
+ "linux"
3488
+ ],
3489
+ "dependencies": {
3490
+ "adm-zip": "^0.5.16",
3491
+ "global-agent": "^3.0.0",
3492
+ "onnxruntime-common": "1.23.2"
3493
+ }
3494
+ },
3495
+ "node_modules/onnxruntime-web": {
3496
+ "version": "1.23.2",
3497
+ "resolved": "https://registry.npmjs.org/onnxruntime-web/-/onnxruntime-web-1.23.2.tgz",
3498
+ "integrity": "sha512-T09JUtMn+CZLk3mFwqiH0lgQf+4S7+oYHHtk6uhaYAAJI95bTcKi5bOOZYwORXfS/RLZCjDDEXGWIuOCAFlEjg==",
3499
+ "dev": true,
3500
+ "dependencies": {
3501
+ "flatbuffers": "^25.1.24",
3502
+ "guid-typescript": "^1.0.9",
3503
+ "long": "^5.2.3",
3504
+ "onnxruntime-common": "1.23.2",
3505
+ "platform": "^1.3.6",
3506
+ "protobufjs": "^7.2.4"
3507
+ }
3508
+ },
3509
  "node_modules/optionator": {
3510
  "version": "0.9.4",
3511
  "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
 
3581
  "url": "https://github.com/inikulin/parse5?sponsor=1"
3582
  }
3583
  },
3584
+ "node_modules/path-browserify": {
3585
+ "version": "1.0.1",
3586
+ "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz",
3587
+ "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==",
3588
+ "dev": true
3589
+ },
3590
  "node_modules/path-exists": {
3591
  "version": "4.0.0",
3592
  "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
 
3631
  "url": "https://github.com/sponsors/jonschlinkert"
3632
  }
3633
  },
3634
+ "node_modules/pidtree": {
3635
+ "version": "0.6.0",
3636
+ "resolved": "https://registry.npmjs.org/pidtree/-/pidtree-0.6.0.tgz",
3637
+ "integrity": "sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g==",
3638
+ "dev": true,
3639
+ "bin": {
3640
+ "pidtree": "bin/pidtree.js"
3641
+ },
3642
+ "engines": {
3643
+ "node": ">=0.10"
3644
+ }
3645
+ },
3646
+ "node_modules/platform": {
3647
+ "version": "1.3.6",
3648
+ "resolved": "https://registry.npmjs.org/platform/-/platform-1.3.6.tgz",
3649
+ "integrity": "sha512-fnWVljUchTro6RiCFvCXBbNhJc2NijN7oIQxbwsyL0buWJPG85v81ehlHI9fXrJsMNgTofEoWIQeClKpgxFLrg==",
3650
+ "dev": true
3651
+ },
3652
  "node_modules/postcss": {
3653
  "version": "8.5.6",
3654
  "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
 
3714
  "node": ">=6.0.0"
3715
  }
3716
  },
3717
+ "node_modules/protobufjs": {
3718
+ "version": "7.5.4",
3719
+ "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.4.tgz",
3720
+ "integrity": "sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg==",
3721
+ "dev": true,
3722
+ "hasInstallScript": true,
3723
+ "dependencies": {
3724
+ "@protobufjs/aspromise": "^1.1.2",
3725
+ "@protobufjs/base64": "^1.1.2",
3726
+ "@protobufjs/codegen": "^2.0.4",
3727
+ "@protobufjs/eventemitter": "^1.1.0",
3728
+ "@protobufjs/fetch": "^1.1.0",
3729
+ "@protobufjs/float": "^1.0.2",
3730
+ "@protobufjs/inquire": "^1.1.0",
3731
+ "@protobufjs/path": "^1.1.2",
3732
+ "@protobufjs/pool": "^1.1.0",
3733
+ "@protobufjs/utf8": "^1.1.0",
3734
+ "@types/node": ">=13.7.0",
3735
+ "long": "^5.0.0"
3736
+ },
3737
+ "engines": {
3738
+ "node": ">=12.0.0"
3739
+ }
3740
+ },
3741
  "node_modules/punycode": {
3742
  "version": "2.3.1",
3743
  "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
 
3784
  "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1"
3785
  }
3786
  },
3787
+ "node_modules/restore-cursor": {
3788
+ "version": "5.1.0",
3789
+ "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz",
3790
+ "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==",
3791
+ "dev": true,
3792
+ "dependencies": {
3793
+ "onetime": "^7.0.0",
3794
+ "signal-exit": "^4.1.0"
3795
+ },
3796
+ "engines": {
3797
+ "node": ">=18"
3798
+ },
3799
+ "funding": {
3800
+ "url": "https://github.com/sponsors/sindresorhus"
3801
+ }
3802
+ },
3803
+ "node_modules/rfdc": {
3804
+ "version": "1.4.1",
3805
+ "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz",
3806
+ "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==",
3807
+ "dev": true
3808
+ },
3809
+ "node_modules/roarr": {
3810
+ "version": "2.15.4",
3811
+ "resolved": "https://registry.npmjs.org/roarr/-/roarr-2.15.4.tgz",
3812
+ "integrity": "sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A==",
3813
+ "dev": true,
3814
+ "dependencies": {
3815
+ "boolean": "^3.0.1",
3816
+ "detect-node": "^2.0.4",
3817
+ "globalthis": "^1.0.1",
3818
+ "json-stringify-safe": "^5.0.1",
3819
+ "semver-compare": "^1.0.0",
3820
+ "sprintf-js": "^1.1.2"
3821
+ },
3822
+ "engines": {
3823
+ "node": ">=8.0"
3824
+ }
3825
+ },
3826
  "node_modules/rollup": {
3827
  "version": "4.52.5",
3828
  "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.52.5.tgz",
 
3891
  "node": ">=v12.22.7"
3892
  }
3893
  },
3894
+ "node_modules/semver": {
3895
+ "version": "7.7.3",
3896
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
3897
+ "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==",
3898
+ "dev": true,
3899
+ "bin": {
3900
+ "semver": "bin/semver.js"
3901
+ },
3902
+ "engines": {
3903
+ "node": ">=10"
3904
+ }
3905
+ },
3906
+ "node_modules/semver-compare": {
3907
+ "version": "1.0.0",
3908
+ "resolved": "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz",
3909
+ "integrity": "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==",
3910
+ "dev": true
3911
+ },
3912
+ "node_modules/serialize-error": {
3913
+ "version": "7.0.1",
3914
+ "resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-7.0.1.tgz",
3915
+ "integrity": "sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw==",
3916
+ "dev": true,
3917
+ "dependencies": {
3918
+ "type-fest": "^0.13.1"
3919
+ },
3920
+ "engines": {
3921
+ "node": ">=10"
3922
+ },
3923
+ "funding": {
3924
+ "url": "https://github.com/sponsors/sindresorhus"
3925
+ }
3926
+ },
3927
  "node_modules/shebang-command": {
3928
  "version": "2.0.0",
3929
  "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
 
3965
  "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==",
3966
  "dev": true
3967
  },
3968
+ "node_modules/signal-exit": {
3969
+ "version": "4.1.0",
3970
+ "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
3971
+ "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
3972
+ "dev": true,
3973
+ "engines": {
3974
+ "node": ">=14"
3975
+ },
3976
+ "funding": {
3977
+ "url": "https://github.com/sponsors/isaacs"
3978
+ }
3979
+ },
3980
  "node_modules/sirv": {
3981
  "version": "3.0.2",
3982
  "resolved": "https://registry.npmjs.org/sirv/-/sirv-3.0.2.tgz",
 
3991
  "node": ">=18"
3992
  }
3993
  },
3994
+ "node_modules/slice-ansi": {
3995
+ "version": "7.1.2",
3996
+ "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-7.1.2.tgz",
3997
+ "integrity": "sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w==",
3998
+ "dev": true,
3999
+ "dependencies": {
4000
+ "ansi-styles": "^6.2.1",
4001
+ "is-fullwidth-code-point": "^5.0.0"
4002
+ },
4003
+ "engines": {
4004
+ "node": ">=18"
4005
+ },
4006
+ "funding": {
4007
+ "url": "https://github.com/chalk/slice-ansi?sponsor=1"
4008
+ }
4009
+ },
4010
+ "node_modules/slice-ansi/node_modules/ansi-styles": {
4011
+ "version": "6.2.3",
4012
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz",
4013
+ "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==",
4014
+ "dev": true,
4015
+ "engines": {
4016
+ "node": ">=12"
4017
+ },
4018
+ "funding": {
4019
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
4020
+ }
4021
+ },
4022
+ "node_modules/slice-ansi/node_modules/is-fullwidth-code-point": {
4023
+ "version": "5.1.0",
4024
+ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.1.0.tgz",
4025
+ "integrity": "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ==",
4026
+ "dev": true,
4027
+ "dependencies": {
4028
+ "get-east-asian-width": "^1.3.1"
4029
+ },
4030
+ "engines": {
4031
+ "node": ">=18"
4032
+ },
4033
+ "funding": {
4034
+ "url": "https://github.com/sponsors/sindresorhus"
4035
+ }
4036
+ },
4037
  "node_modules/source-map": {
4038
  "version": "0.1.43",
4039
  "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.1.43.tgz",
 
4062
  "integrity": "sha512-zC8zGoGkmc8J9ndvml8Xksr1Amk9qBujgbF0JAIWO7kXr43w0h/0GJNM/Vustixu+YE8N/MTrQ7N31FvHUACxQ==",
4063
  "dev": true
4064
  },
4065
+ "node_modules/sprintf-js": {
4066
+ "version": "1.1.3",
4067
+ "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz",
4068
+ "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==",
4069
+ "dev": true
4070
+ },
4071
  "node_modules/stackback": {
4072
  "version": "0.0.2",
4073
  "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz",
 
4080
  "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==",
4081
  "dev": true
4082
  },
4083
+ "node_modules/string-argv": {
4084
+ "version": "0.3.2",
4085
+ "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.2.tgz",
4086
+ "integrity": "sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==",
4087
+ "dev": true,
4088
+ "engines": {
4089
+ "node": ">=0.6.19"
4090
+ }
4091
+ },
4092
  "node_modules/string-width": {
4093
  "version": "7.2.0",
4094
  "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz",
 
4135
  }
4136
  },
4137
  "node_modules/supports-color": {
4138
+ "version": "8.1.1",
4139
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz",
4140
+ "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==",
4141
  "dev": true,
4142
  "dependencies": {
4143
  "has-flag": "^4.0.0"
4144
  },
4145
  "engines": {
4146
+ "node": ">=10"
4147
+ },
4148
+ "funding": {
4149
+ "url": "https://github.com/chalk/supports-color?sponsor=1"
4150
  }
4151
  },
4152
  "node_modules/symbol-tree": {
 
4225
  "integrity": "sha512-DieYoGrP78PWKsrXr8MZwtQ7GLCUeLxihtjC1jZsW1DnvSMdKPitJSe8OSYDM2u5H6g3kWJZpePqkp43TfLh0g==",
4226
  "dev": true
4227
  },
4228
+ "node_modules/to-regex-range": {
4229
+ "version": "5.0.1",
4230
+ "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
4231
+ "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
4232
+ "dev": true,
4233
+ "dependencies": {
4234
+ "is-number": "^7.0.0"
4235
+ },
4236
+ "engines": {
4237
+ "node": ">=8.0"
4238
+ }
4239
+ },
4240
  "node_modules/totalist": {
4241
  "version": "3.0.1",
4242
  "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz",
 
4304
  "fsevents": "~2.3.3"
4305
  }
4306
  },
4307
+ "node_modules/type-check": {
4308
+ "version": "0.4.0",
4309
+ "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
4310
+ "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==",
 
 
 
4311
  "dev": true,
4312
+ "peer": true,
4313
+ "dependencies": {
4314
+ "prelude-ls": "^1.2.1"
4315
+ },
4316
  "engines": {
4317
+ "node": ">= 0.8.0"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4318
  }
4319
  },
4320
+ "node_modules/type-fest": {
4321
+ "version": "0.13.1",
4322
+ "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.13.1.tgz",
4323
+ "integrity": "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==",
4324
  "dev": true,
 
 
 
 
4325
  "engines": {
4326
+ "node": ">=10"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4327
  },
4328
+ "funding": {
4329
+ "url": "https://github.com/sponsors/sindresorhus"
4330
  }
4331
  },
4332
  "node_modules/typescript": {
 
4426
  }
4427
  }
4428
  },
4429
+ "node_modules/vite/node_modules/@esbuild/aix-ppc64": {
4430
+ "version": "0.21.5",
4431
+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz",
4432
+ "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4433
  "cpu": [
4434
  "ppc64"
4435
  ],
 
4439
  "aix"
4440
  ],
4441
  "engines": {
4442
+ "node": ">=12"
4443
  }
4444
  },
4445
+ "node_modules/vite/node_modules/@esbuild/android-arm": {
4446
+ "version": "0.21.5",
4447
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz",
4448
+ "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==",
4449
  "cpu": [
4450
  "arm"
4451
  ],
 
4455
  "android"
4456
  ],
4457
  "engines": {
4458
+ "node": ">=12"
4459
  }
4460
  },
4461
+ "node_modules/vite/node_modules/@esbuild/android-arm64": {
4462
+ "version": "0.21.5",
4463
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz",
4464
+ "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==",
4465
  "cpu": [
4466
  "arm64"
4467
  ],
 
4471
  "android"
4472
  ],
4473
  "engines": {
4474
+ "node": ">=12"
4475
  }
4476
  },
4477
+ "node_modules/vite/node_modules/@esbuild/android-x64": {
4478
+ "version": "0.21.5",
4479
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz",
4480
+ "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==",
4481
  "cpu": [
4482
  "x64"
4483
  ],
 
4487
  "android"
4488
  ],
4489
  "engines": {
4490
+ "node": ">=12"
4491
  }
4492
  },
4493
+ "node_modules/vite/node_modules/@esbuild/darwin-arm64": {
4494
+ "version": "0.21.5",
4495
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz",
4496
+ "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==",
4497
  "cpu": [
4498
  "arm64"
4499
  ],
 
4503
  "darwin"
4504
  ],
4505
  "engines": {
4506
+ "node": ">=12"
4507
  }
4508
  },
4509
+ "node_modules/vite/node_modules/@esbuild/darwin-x64": {
4510
+ "version": "0.21.5",
4511
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz",
4512
+ "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==",
4513
  "cpu": [
4514
  "x64"
4515
  ],
 
4519
  "darwin"
4520
  ],
4521
  "engines": {
4522
+ "node": ">=12"
4523
  }
4524
  },
4525
+ "node_modules/vite/node_modules/@esbuild/freebsd-arm64": {
4526
+ "version": "0.21.5",
4527
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz",
4528
+ "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==",
4529
  "cpu": [
4530
  "arm64"
4531
  ],
 
4535
  "freebsd"
4536
  ],
4537
  "engines": {
4538
+ "node": ">=12"
4539
  }
4540
  },
4541
+ "node_modules/vite/node_modules/@esbuild/freebsd-x64": {
4542
+ "version": "0.21.5",
4543
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz",
4544
+ "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==",
4545
  "cpu": [
4546
  "x64"
4547
  ],
 
4551
  "freebsd"
4552
  ],
4553
  "engines": {
4554
+ "node": ">=12"
4555
  }
4556
  },
4557
+ "node_modules/vite/node_modules/@esbuild/linux-arm": {
4558
+ "version": "0.21.5",
4559
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz",
4560
+ "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==",
4561
  "cpu": [
4562
  "arm"
4563
  ],
 
4567
  "linux"
4568
  ],
4569
  "engines": {
4570
+ "node": ">=12"
4571
  }
4572
  },
4573
+ "node_modules/vite/node_modules/@esbuild/linux-arm64": {
4574
+ "version": "0.21.5",
4575
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz",
4576
+ "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==",
4577
  "cpu": [
4578
  "arm64"
4579
  ],
 
4583
  "linux"
4584
  ],
4585
  "engines": {
4586
+ "node": ">=12"
4587
  }
4588
  },
4589
+ "node_modules/vite/node_modules/@esbuild/linux-ia32": {
4590
+ "version": "0.21.5",
4591
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz",
4592
+ "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==",
4593
  "cpu": [
4594
  "ia32"
4595
  ],
 
4599
  "linux"
4600
  ],
4601
  "engines": {
4602
+ "node": ">=12"
4603
  }
4604
  },
4605
+ "node_modules/vite/node_modules/@esbuild/linux-loong64": {
4606
+ "version": "0.21.5",
4607
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz",
4608
+ "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==",
4609
  "cpu": [
4610
  "loong64"
4611
  ],
 
4615
  "linux"
4616
  ],
4617
  "engines": {
4618
+ "node": ">=12"
4619
  }
4620
  },
4621
+ "node_modules/vite/node_modules/@esbuild/linux-mips64el": {
4622
+ "version": "0.21.5",
4623
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz",
4624
+ "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==",
4625
  "cpu": [
4626
  "mips64el"
4627
  ],
 
4631
  "linux"
4632
  ],
4633
  "engines": {
4634
+ "node": ">=12"
4635
  }
4636
  },
4637
+ "node_modules/vite/node_modules/@esbuild/linux-ppc64": {
4638
+ "version": "0.21.5",
4639
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz",
4640
+ "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==",
4641
  "cpu": [
4642
  "ppc64"
4643
  ],
 
4647
  "linux"
4648
  ],
4649
  "engines": {
4650
+ "node": ">=12"
4651
  }
4652
  },
4653
+ "node_modules/vite/node_modules/@esbuild/linux-riscv64": {
4654
+ "version": "0.21.5",
4655
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz",
4656
+ "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==",
4657
  "cpu": [
4658
  "riscv64"
4659
  ],
 
4663
  "linux"
4664
  ],
4665
  "engines": {
4666
+ "node": ">=12"
4667
  }
4668
  },
4669
+ "node_modules/vite/node_modules/@esbuild/linux-s390x": {
4670
+ "version": "0.21.5",
4671
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz",
4672
+ "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==",
4673
  "cpu": [
4674
  "s390x"
4675
  ],
 
4679
  "linux"
4680
  ],
4681
  "engines": {
4682
+ "node": ">=12"
4683
  }
4684
  },
4685
+ "node_modules/vite/node_modules/@esbuild/linux-x64": {
4686
+ "version": "0.21.5",
4687
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz",
4688
+ "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==",
4689
  "cpu": [
4690
  "x64"
4691
  ],
 
4695
  "linux"
4696
  ],
4697
  "engines": {
4698
+ "node": ">=12"
4699
  }
4700
  },
4701
+ "node_modules/vite/node_modules/@esbuild/netbsd-x64": {
4702
+ "version": "0.21.5",
4703
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz",
4704
+ "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==",
4705
  "cpu": [
4706
  "x64"
4707
  ],
 
4711
  "netbsd"
4712
  ],
4713
  "engines": {
4714
+ "node": ">=12"
4715
  }
4716
  },
4717
+ "node_modules/vite/node_modules/@esbuild/openbsd-x64": {
4718
+ "version": "0.21.5",
4719
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz",
4720
+ "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==",
4721
  "cpu": [
4722
  "x64"
4723
  ],
 
4727
  "openbsd"
4728
  ],
4729
  "engines": {
4730
+ "node": ">=12"
4731
  }
4732
  },
4733
+ "node_modules/vite/node_modules/@esbuild/sunos-x64": {
4734
+ "version": "0.21.5",
4735
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz",
4736
+ "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==",
4737
  "cpu": [
4738
  "x64"
4739
  ],
 
4743
  "sunos"
4744
  ],
4745
  "engines": {
4746
+ "node": ">=12"
4747
  }
4748
  },
4749
+ "node_modules/vite/node_modules/@esbuild/win32-arm64": {
4750
+ "version": "0.21.5",
4751
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz",
4752
+ "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==",
4753
  "cpu": [
4754
  "arm64"
4755
  ],
 
4759
  "win32"
4760
  ],
4761
  "engines": {
4762
+ "node": ">=12"
4763
  }
4764
  },
4765
+ "node_modules/vite/node_modules/@esbuild/win32-ia32": {
4766
+ "version": "0.21.5",
4767
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz",
4768
+ "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==",
4769
  "cpu": [
4770
  "ia32"
4771
  ],
 
4775
  "win32"
4776
  ],
4777
  "engines": {
4778
+ "node": ">=12"
4779
  }
4780
  },
4781
+ "node_modules/vite/node_modules/@esbuild/win32-x64": {
4782
+ "version": "0.21.5",
4783
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz",
4784
+ "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==",
4785
  "cpu": [
4786
  "x64"
4787
  ],
 
4791
  "win32"
4792
  ],
4793
  "engines": {
4794
+ "node": ">=12"
4795
+ }
4796
+ },
4797
+ "node_modules/vite/node_modules/esbuild": {
4798
+ "version": "0.21.5",
4799
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz",
4800
+ "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==",
4801
+ "dev": true,
4802
+ "hasInstallScript": true,
4803
+ "bin": {
4804
+ "esbuild": "bin/esbuild"
4805
+ },
4806
+ "engines": {
4807
+ "node": ">=12"
4808
+ },
4809
+ "optionalDependencies": {
4810
+ "@esbuild/aix-ppc64": "0.21.5",
4811
+ "@esbuild/android-arm": "0.21.5",
4812
+ "@esbuild/android-arm64": "0.21.5",
4813
+ "@esbuild/android-x64": "0.21.5",
4814
+ "@esbuild/darwin-arm64": "0.21.5",
4815
+ "@esbuild/darwin-x64": "0.21.5",
4816
+ "@esbuild/freebsd-arm64": "0.21.5",
4817
+ "@esbuild/freebsd-x64": "0.21.5",
4818
+ "@esbuild/linux-arm": "0.21.5",
4819
+ "@esbuild/linux-arm64": "0.21.5",
4820
+ "@esbuild/linux-ia32": "0.21.5",
4821
+ "@esbuild/linux-loong64": "0.21.5",
4822
+ "@esbuild/linux-mips64el": "0.21.5",
4823
+ "@esbuild/linux-ppc64": "0.21.5",
4824
+ "@esbuild/linux-riscv64": "0.21.5",
4825
+ "@esbuild/linux-s390x": "0.21.5",
4826
+ "@esbuild/linux-x64": "0.21.5",
4827
+ "@esbuild/netbsd-x64": "0.21.5",
4828
+ "@esbuild/openbsd-x64": "0.21.5",
4829
+ "@esbuild/sunos-x64": "0.21.5",
4830
+ "@esbuild/win32-arm64": "0.21.5",
4831
+ "@esbuild/win32-ia32": "0.21.5",
4832
+ "@esbuild/win32-x64": "0.21.5"
4833
+ }
4834
+ },
4835
+ "node_modules/vitest": {
4836
+ "version": "4.0.6",
4837
+ "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.0.6.tgz",
4838
+ "integrity": "sha512-gR7INfiVRwnEOkCk47faros/9McCZMp5LM+OMNWGLaDBSvJxIzwjgNFufkuePBNaesGRnLmNfW+ddbUJRZn0nQ==",
4839
+ "dev": true,
4840
+ "dependencies": {
4841
+ "@vitest/expect": "4.0.6",
4842
+ "@vitest/mocker": "4.0.6",
4843
+ "@vitest/pretty-format": "4.0.6",
4844
+ "@vitest/runner": "4.0.6",
4845
+ "@vitest/snapshot": "4.0.6",
4846
+ "@vitest/spy": "4.0.6",
4847
+ "@vitest/utils": "4.0.6",
4848
+ "debug": "^4.4.3",
4849
+ "es-module-lexer": "^1.7.0",
4850
+ "expect-type": "^1.2.2",
4851
+ "magic-string": "^0.30.19",
4852
+ "pathe": "^2.0.3",
4853
+ "picomatch": "^4.0.3",
4854
+ "std-env": "^3.9.0",
4855
+ "tinybench": "^2.9.0",
4856
+ "tinyexec": "^0.3.2",
4857
+ "tinyglobby": "^0.2.15",
4858
+ "tinyrainbow": "^3.0.3",
4859
+ "vite": "^6.0.0 || ^7.0.0",
4860
+ "why-is-node-running": "^2.3.0"
4861
+ },
4862
+ "bin": {
4863
+ "vitest": "vitest.mjs"
4864
+ },
4865
+ "engines": {
4866
+ "node": "^20.0.0 || ^22.0.0 || >=24.0.0"
4867
+ },
4868
+ "funding": {
4869
+ "url": "https://opencollective.com/vitest"
4870
+ },
4871
+ "peerDependencies": {
4872
+ "@edge-runtime/vm": "*",
4873
+ "@types/debug": "^4.1.12",
4874
+ "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0",
4875
+ "@vitest/browser-playwright": "4.0.6",
4876
+ "@vitest/browser-preview": "4.0.6",
4877
+ "@vitest/browser-webdriverio": "4.0.6",
4878
+ "@vitest/ui": "4.0.6",
4879
+ "happy-dom": "*",
4880
+ "jsdom": "*"
4881
+ },
4882
+ "peerDependenciesMeta": {
4883
+ "@edge-runtime/vm": {
4884
+ "optional": true
4885
+ },
4886
+ "@types/debug": {
4887
+ "optional": true
4888
+ },
4889
+ "@types/node": {
4890
+ "optional": true
4891
+ },
4892
+ "@vitest/browser-playwright": {
4893
+ "optional": true
4894
+ },
4895
+ "@vitest/browser-preview": {
4896
+ "optional": true
4897
+ },
4898
+ "@vitest/browser-webdriverio": {
4899
+ "optional": true
4900
+ },
4901
+ "@vitest/ui": {
4902
+ "optional": true
4903
+ },
4904
+ "happy-dom": {
4905
+ "optional": true
4906
+ },
4907
+ "jsdom": {
4908
+ "optional": true
4909
+ }
4910
  }
4911
  },
4912
  "node_modules/vitest/node_modules/@vitest/mocker": {
 
4935
  }
4936
  }
4937
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4938
  "node_modules/vitest/node_modules/estree-walker": {
4939
  "version": "3.0.3",
4940
  "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz",
 
5018
  }
5019
  }
5020
  },
5021
+ "node_modules/vscode-uri": {
5022
+ "version": "3.1.0",
5023
+ "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.1.0.tgz",
5024
+ "integrity": "sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==",
5025
+ "dev": true
5026
+ },
5027
  "node_modules/vue": {
5028
  "version": "3.5.22",
5029
  "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.22.tgz",
 
5045
  }
5046
  }
5047
  },
5048
+ "node_modules/vue-tsc": {
5049
+ "version": "3.1.3",
5050
+ "resolved": "https://registry.npmjs.org/vue-tsc/-/vue-tsc-3.1.3.tgz",
5051
+ "integrity": "sha512-StMNfZHwPIXQgY3KxPKM0Jsoc8b46mDV3Fn2UlHCBIwRJApjqrSwqeMYgWf0zpN+g857y74pv7GWuBm+UqQe1w==",
5052
+ "dev": true,
5053
+ "dependencies": {
5054
+ "@volar/typescript": "2.4.23",
5055
+ "@vue/language-core": "3.1.3"
5056
+ },
5057
+ "bin": {
5058
+ "vue-tsc": "bin/vue-tsc.js"
5059
+ },
5060
+ "peerDependencies": {
5061
+ "typescript": ">=5.0.0"
5062
+ }
5063
+ },
5064
  "node_modules/w3c-xmlserializer": {
5065
  "version": "5.0.0",
5066
  "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz",
 
5232
  "node": ">=10"
5233
  }
5234
  },
5235
+ "node_modules/yaml": {
5236
+ "version": "2.8.1",
5237
+ "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.1.tgz",
5238
+ "integrity": "sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==",
5239
+ "dev": true,
5240
+ "bin": {
5241
+ "yaml": "bin.mjs"
5242
+ },
5243
+ "engines": {
5244
+ "node": ">= 14.6"
5245
+ }
5246
+ },
5247
  "node_modules/yargs": {
5248
  "version": "18.0.0",
5249
  "resolved": "https://registry.npmjs.org/yargs/-/yargs-18.0.0.tgz",
trigo-web/package.json CHANGED
@@ -19,7 +19,9 @@
19
  "test": "vitest",
20
  "test:ui": "vitest --ui",
21
  "test:run": "vitest run",
22
- "generate:games": "tsx tools/generateRandomGames.ts"
 
 
23
  },
24
  "keywords": [
25
  "game",
@@ -31,6 +33,9 @@
31
  ],
32
  "author": "",
33
  "license": "MIT",
 
 
 
34
  "devDependencies": {
35
  "@types/node": "^24.10.0",
36
  "@types/yargs": "^17.0.34",
@@ -39,14 +44,19 @@
39
  "concurrently": "^7.6.0",
40
  "eslint-config-prettier": "^10.1.8",
41
  "eslint-plugin-prettier": "^5.5.4",
 
42
  "jison": "^0.4.18",
43
  "jsdom": "^27.1.0",
 
 
 
44
  "prettier": "^3.6.2",
45
  "tsx": "^4.20.6",
46
  "typescript": "^5.2.2",
47
  "vite": "^5.4.21",
48
  "vitest": "^4.0.6",
49
  "vue": "^3.3.4",
 
50
  "yargs": "^18.0.0"
51
  }
52
  }
 
19
  "test": "vitest",
20
  "test:ui": "vitest --ui",
21
  "test:run": "vitest run",
22
+ "generate:games": "tsx tools/generateRandomGames.ts",
23
+ "migrate:tgn": "tsx tools/migrateTGN.ts",
24
+ "prepare": "cd .. && husky"
25
  },
26
  "keywords": [
27
  "game",
 
33
  ],
34
  "author": "",
35
  "license": "MIT",
36
+ "lint-staged": {
37
+ "**/*.{js,ts,vue,json,md,scss,css}": []
38
+ },
39
  "devDependencies": {
40
  "@types/node": "^24.10.0",
41
  "@types/yargs": "^17.0.34",
 
44
  "concurrently": "^7.6.0",
45
  "eslint-config-prettier": "^10.1.8",
46
  "eslint-plugin-prettier": "^5.5.4",
47
+ "husky": "^9.1.7",
48
  "jison": "^0.4.18",
49
  "jsdom": "^27.1.0",
50
+ "lint-staged": "^16.2.7",
51
+ "onnxruntime-node": "1.23.2",
52
+ "onnxruntime-web": "1.23.2",
53
  "prettier": "^3.6.2",
54
  "tsx": "^4.20.6",
55
  "typescript": "^5.2.2",
56
  "vite": "^5.4.21",
57
  "vitest": "^4.0.6",
58
  "vue": "^3.3.4",
59
+ "vue-tsc": "^3.1.3",
60
  "yargs": "^18.0.0"
61
  }
62
  }
trigo-web/public/lib/tgnParser.cjs CHANGED
@@ -759,7 +759,7 @@ case 34:return 'INVALID'
759
  break;
760
  }
761
  },
762
- rules: [/^(?:\s+)/,/^(?:\n)/,/^(?:;[^\n]*)/,/^(?:\{[^}]*\})/,/^(?:\[)/,/^(?:\])/,/^(?:"([^\\\"]|\\.)*")/,/^(?:Event\b)/,/^(?:Site\b)/,/^(?:Date\b)/,/^(?:Round\b)/,/^(?:Black\b)/,/^(?:White\b)/,/^(?:Result\b)/,/^(?:Board\b)/,/^(?:Handicap\b)/,/^(?:Rules\b)/,/^(?:TimeControl\b)/,/^(?:Annotator\b)/,/^(?:Application\b)/,/^(?:B\+)/,/^(?:W\+)/,/^(?:=)/,/^(?:\*)/,/^(?:[1-9][0-9]*)/,/^(?:\.)/,/^(?:pass\b)/,/^(?:resign\b)/,/^(?:points\b)/,/^(?:stones\b)/,/^(?:[x](?=[1-9]))/,/^(?:[a-z0]+)/,/^(?:[A-Z][A-Za-z0-9_]*)/,/^(?:$)/,/^(?:.)/],
763
  conditions: {"INITIAL":{"rules":[0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34],"inclusive":true}}
764
  });
765
  return lexer;
 
759
  break;
760
  }
761
  },
762
+ rules: [/^(?:\s+)/,/^(?:\n)/,/^(?:;[^\n]*)/,/^(?:\{[^}]*\})/,/^(?:\[)/,/^(?:\])/,/^(?:"([^\\\"]|\\.)*")/,/^(?:Event\b)/,/^(?:Site\b)/,/^(?:Date\b)/,/^(?:Round\b)/,/^(?:Black\b)/,/^(?:White\b)/,/^(?:Result\b)/,/^(?:Board\b)/,/^(?:Handicap\b)/,/^(?:Rules\b)/,/^(?:TimeControl\b)/,/^(?:Annotator\b)/,/^(?:Application\b)/,/^(?:B\+)/,/^(?:W\+)/,/^(?:=)/,/^(?:\*)/,/^(?:[1-9][0-9]*)/,/^(?:\.)/,/^(?:Pass\b)/,/^(?:Resign\b)/,/^(?:points\b)/,/^(?:stones\b)/,/^(?:[x](?=[1-9]))/,/^(?:[a-z0]+)/,/^(?:[A-Z][A-Za-z0-9_]*)/,/^(?:$)/,/^(?:.)/],
763
  conditions: {"INITIAL":{"rules":[0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34],"inclusive":true}}
764
  });
765
  return lexer;
trigo-web/tools/README.md CHANGED
@@ -90,8 +90,8 @@ tools/output/
90
  1. 000 y00
91
  2. 0y0 yy0
92
  3. aaa zzz
93
- 4. pass 0az
94
- 5. z0z pass
95
  ```
96
 
97
  ### Performance
 
90
  1. 000 y00
91
  2. 0y0 yy0
92
  3. aaa zzz
93
+ 4. Pass 0az
94
+ 5. z0z Pass
95
  ```
96
 
97
  ### Performance
trigo-web/tools/migrateTGN.ts ADDED
@@ -0,0 +1,291 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env tsx
2
+ /**
3
+ * TGN Migration Tool
4
+ *
5
+ * Converts old TGN files with lowercase "pass" and "resign" to uppercase "Pass" and "Resign"
6
+ * to match the updated TGN specification.
7
+ *
8
+ * Usage:
9
+ * npm run migrate:tgn -- <directory>
10
+ * npm run migrate:tgn -- <directory> --dry-run
11
+ * npm run migrate:tgn -- <directory> --backup
12
+ */
13
+
14
+ import * as fs from "fs";
15
+ import * as path from "path";
16
+ import yargs from "yargs";
17
+ import { hideBin } from "yargs/helpers";
18
+
19
+
20
+ interface MigrationOptions {
21
+ directory: string;
22
+ dryRun: boolean;
23
+ backup: boolean;
24
+ recursive: boolean;
25
+ }
26
+
27
+ interface MigrationStats {
28
+ filesScanned: number;
29
+ filesModified: number;
30
+ passReplacements: number;
31
+ resignReplacements: number;
32
+ errors: string[];
33
+ }
34
+
35
+
36
+ /**
37
+ * Check if a file is a TGN file
38
+ */
39
+ function isTGNFile(filePath: string): boolean {
40
+ return path.extname(filePath).toLowerCase() === ".tgn";
41
+ }
42
+
43
+
44
+ /**
45
+ * Find all TGN files in a directory
46
+ */
47
+ function findTGNFiles(dir: string, recursive: boolean): string[] {
48
+ const files: string[] = [];
49
+
50
+ try {
51
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
52
+
53
+ for (const entry of entries) {
54
+ const fullPath = path.join(dir, entry.name);
55
+
56
+ if (entry.isDirectory() && recursive) {
57
+ files.push(...findTGNFiles(fullPath, recursive));
58
+ } else if (entry.isFile() && isTGNFile(fullPath)) {
59
+ files.push(fullPath);
60
+ }
61
+ }
62
+ } catch (error) {
63
+ console.error(`Error reading directory ${dir}:`, error);
64
+ }
65
+
66
+ return files;
67
+ }
68
+
69
+
70
+ /**
71
+ * Migrate TGN content from lowercase to uppercase
72
+ */
73
+ function migrateTGNContent(content: string): { newContent: string; passCount: number; resignCount: number } {
74
+ let passCount = 0;
75
+ let resignCount = 0;
76
+
77
+ // Replace "pass" with "Pass" (as a standalone word in move notation)
78
+ // Use word boundary to avoid replacing "pass" in other contexts
79
+ const newContent = content
80
+ .replace(/\bpass\b/g, () => {
81
+ passCount++;
82
+ return "Pass";
83
+ })
84
+ .replace(/\bresign\b/g, () => {
85
+ resignCount++;
86
+ return "Resign";
87
+ });
88
+
89
+ return { newContent, passCount, resignCount };
90
+ }
91
+
92
+
93
+ /**
94
+ * Create backup of a file
95
+ */
96
+ function backupFile(filePath: string): string {
97
+ const backupPath = filePath + ".backup";
98
+ fs.copyFileSync(filePath, backupPath);
99
+ return backupPath;
100
+ }
101
+
102
+
103
+ /**
104
+ * Migrate a single TGN file
105
+ */
106
+ function migrateFile(
107
+ filePath: string,
108
+ options: MigrationOptions,
109
+ stats: MigrationStats
110
+ ): void {
111
+ stats.filesScanned++;
112
+
113
+ try {
114
+ // Read file content
115
+ const content = fs.readFileSync(filePath, "utf-8");
116
+
117
+ // Migrate content
118
+ const { newContent, passCount, resignCount } = migrateTGNContent(content);
119
+
120
+ // Check if file needs migration
121
+ if (passCount === 0 && resignCount === 0) {
122
+ return; // File already up to date
123
+ }
124
+
125
+ // Update statistics
126
+ stats.passReplacements += passCount;
127
+ stats.resignReplacements += resignCount;
128
+ stats.filesModified++;
129
+
130
+ // Log changes
131
+ console.log(`\n📄 ${path.basename(filePath)}`);
132
+ if (passCount > 0) {
133
+ console.log(` ✓ Replaced ${passCount} "pass" → "Pass"`);
134
+ }
135
+ if (resignCount > 0) {
136
+ console.log(` ✓ Replaced ${resignCount} "resign" → "Resign"`);
137
+ }
138
+
139
+ // Skip write in dry-run mode
140
+ if (options.dryRun) {
141
+ console.log(" (dry-run: no changes written)");
142
+ return;
143
+ }
144
+
145
+ // Create backup if requested
146
+ if (options.backup) {
147
+ const backupPath = backupFile(filePath);
148
+ console.log(` 📦 Backup created: ${path.basename(backupPath)}`);
149
+ }
150
+
151
+ // Write migrated content
152
+ fs.writeFileSync(filePath, newContent, "utf-8");
153
+ console.log(` ✅ File updated`);
154
+ } catch (error) {
155
+ const errorMsg = `Error processing ${filePath}: ${error}`;
156
+ stats.errors.push(errorMsg);
157
+ console.error(` ❌ ${errorMsg}`);
158
+ }
159
+ }
160
+
161
+
162
+ /**
163
+ * Main migration function
164
+ */
165
+ function migrateTGNFiles(options: MigrationOptions): MigrationStats {
166
+ const stats: MigrationStats = {
167
+ filesScanned: 0,
168
+ filesModified: 0,
169
+ passReplacements: 0,
170
+ resignReplacements: 0,
171
+ errors: []
172
+ };
173
+
174
+ console.log("🔄 TGN Migration Tool");
175
+ console.log("=" .repeat(50));
176
+ console.log(`Directory: ${options.directory}`);
177
+ console.log(`Recursive: ${options.recursive}`);
178
+ console.log(`Dry run: ${options.dryRun}`);
179
+ console.log(`Backup: ${options.backup}`);
180
+ console.log("=" .repeat(50));
181
+
182
+ // Check if directory exists
183
+ if (!fs.existsSync(options.directory)) {
184
+ console.error(`❌ Directory not found: ${options.directory}`);
185
+ process.exit(1);
186
+ }
187
+
188
+ // Find all TGN files
189
+ const files = findTGNFiles(options.directory, options.recursive);
190
+
191
+ if (files.length === 0) {
192
+ console.log("\n⚠️ No TGN files found.");
193
+ return stats;
194
+ }
195
+
196
+ console.log(`\n📁 Found ${files.length} TGN file(s)\n`);
197
+
198
+ // Migrate each file
199
+ for (const file of files) {
200
+ migrateFile(file, options, stats);
201
+ }
202
+
203
+ // Print summary
204
+ console.log("\n" + "=".repeat(50));
205
+ console.log("📊 Migration Summary");
206
+ console.log("=".repeat(50));
207
+ console.log(`Files scanned: ${stats.filesScanned}`);
208
+ console.log(`Files modified: ${stats.filesModified}`);
209
+ console.log(`Total "pass" → "Pass": ${stats.passReplacements}`);
210
+ console.log(`Total "resign" → "Resign": ${stats.resignReplacements}`);
211
+
212
+ if (stats.errors.length > 0) {
213
+ console.log(`\n❌ Errors: ${stats.errors.length}`);
214
+ stats.errors.forEach((error) => console.error(` - ${error}`));
215
+ }
216
+
217
+ if (options.dryRun) {
218
+ console.log("\n⚠️ Dry run mode - no files were modified");
219
+ } else if (stats.filesModified > 0) {
220
+ console.log("\n✅ Migration completed successfully!");
221
+ } else {
222
+ console.log("\n✨ All files are already up to date!");
223
+ }
224
+
225
+ return stats;
226
+ }
227
+
228
+
229
+ /**
230
+ * CLI Entry Point
231
+ */
232
+ async function main() {
233
+ const argv = await yargs(hideBin(process.argv))
234
+ .usage("Usage: $0 <directory> [options]")
235
+ .command("$0 <directory>", "Migrate TGN files to uppercase Pass/Resign format", (yargs) => {
236
+ return yargs.positional("directory", {
237
+ describe: "Directory containing TGN files",
238
+ type: "string",
239
+ demandOption: true
240
+ });
241
+ })
242
+ .option("dry-run", {
243
+ alias: "n",
244
+ type: "boolean",
245
+ default: false,
246
+ description: "Preview changes without modifying files"
247
+ })
248
+ .option("backup", {
249
+ alias: "b",
250
+ type: "boolean",
251
+ default: false,
252
+ description: "Create .backup files before modifying"
253
+ })
254
+ .option("recursive", {
255
+ alias: "r",
256
+ type: "boolean",
257
+ default: false,
258
+ description: "Process subdirectories recursively"
259
+ })
260
+ .example([
261
+ ["$0 output", "Migrate all TGN files in output directory"],
262
+ ["$0 output --dry-run", "Preview changes without modifying files"],
263
+ ["$0 output --backup", "Create backups before modifying"],
264
+ ["$0 output --recursive", "Process all subdirectories"]
265
+ ])
266
+ .help("h")
267
+ .alias("h", "help")
268
+ .strict()
269
+ .parse();
270
+
271
+ const options: MigrationOptions = {
272
+ directory: argv.directory as string,
273
+ dryRun: argv["dry-run"] as boolean,
274
+ backup: argv.backup as boolean,
275
+ recursive: argv.recursive as boolean
276
+ };
277
+
278
+ migrateTGNFiles(options);
279
+ }
280
+
281
+
282
+ // Run the CLI when executed directly
283
+ if (process.argv[1] === new URL(import.meta.url).pathname) {
284
+ main().catch((error) => {
285
+ console.error("Fatal error:", error);
286
+ process.exit(1);
287
+ });
288
+ }
289
+
290
+
291
+ export { migrateTGNFiles, migrateTGNContent, MigrationOptions, MigrationStats };
trigo-web/vitest.config.ts CHANGED
@@ -23,7 +23,7 @@ export default defineConfig({
23
  test: {
24
  globals: true,
25
  environment: 'node', // Changed from jsdom to node since we're testing pure logic
26
- include: ['tests/game/**/*.test.ts'], // Game logic tests
27
  },
28
  resolve: {
29
  alias: {
 
23
  test: {
24
  globals: true,
25
  environment: 'node', // Changed from jsdom to node since we're testing pure logic
26
+ include: ['tests/**/*.test.ts'], // All tests
27
  },
28
  resolve: {
29
  alias: {
trigo-web/yarn.lock CHANGED
@@ -213,6 +213,59 @@
213
  resolved "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz"
214
  integrity sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==
215
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
216
  "@rollup/[email protected]":
217
  version "4.52.5"
218
  resolved "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.52.5.tgz"
@@ -251,7 +304,7 @@
251
  resolved "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz"
252
  integrity sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==
253
 
254
- "@types/node@^18.0.0 || >=20.0.0", "@types/node@^20.0.0 || ^22.0.0 || >=24.0.0", "@types/node@^20.19.0 || >=22.12.0", "@types/node@^24.10.0":
255
  version "24.10.0"
256
  resolved "https://registry.npmjs.org/@types/node/-/node-24.10.0.tgz"
257
  integrity sha512-qzQZRBqkFsYyaSWXuEHc2WR9c0a0CXwiE5FWUvn7ZM+vdy1uZLfCunD38UzhuB7YN/J11ndbDBcTmOdxJo9Q7A==
@@ -346,6 +399,27 @@
346
  "@vitest/pretty-format" "4.0.6"
347
  tinyrainbow "^3.0.3"
348
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
349
  "@vue/[email protected]":
350
  version "3.5.22"
351
  resolved "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.22.tgz"
@@ -357,7 +431,7 @@
357
  estree-walker "^2.0.2"
358
  source-map-js "^1.2.1"
359
 
360
- "@vue/[email protected]":
361
  version "3.5.22"
362
  resolved "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.22.tgz"
363
  integrity sha512-W8RknzUM1BLkypvdz10OVsGxnMAuSIZs9Wdx1vzA3mL5fNMN15rhrSCLiTm6blWeACwUwizzPVqGJgOGBEN/hA==
@@ -388,6 +462,19 @@
388
  "@vue/compiler-dom" "3.5.22"
389
  "@vue/shared" "3.5.22"
390
 
 
 
 
 
 
 
 
 
 
 
 
 
 
391
  "@vue/[email protected]":
392
  version "3.5.22"
393
  resolved "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.22.tgz"
@@ -421,7 +508,7 @@
421
  "@vue/compiler-ssr" "3.5.22"
422
  "@vue/shared" "3.5.22"
423
 
424
- "@vue/[email protected]":
425
  version "3.5.22"
426
  resolved "https://registry.npmjs.org/@vue/shared/-/shared-3.5.22.tgz"
427
  integrity sha512-F4yc6palwq3TT0u+FYf0Ns4Tfl9GRFURDN2gWG7L1ecIaS/4fCIuFOjMTnCyjsu/OK6vaDKLCrGAa+KvvH+h4w==
@@ -436,6 +523,11 @@ acorn-jsx@^5.3.2:
436
  resolved "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz"
437
  integrity sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==
438
 
 
 
 
 
 
439
  agent-base@^7.1.0, agent-base@^7.1.2:
440
  version "7.1.4"
441
  resolved "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz"
@@ -451,11 +543,23 @@ ajv@^6.12.4:
451
  json-schema-traverse "^0.4.1"
452
  uri-js "^4.2.2"
453
 
 
 
 
 
 
454
  amdefine@>=0.0.4:
455
  version "1.0.1"
456
  resolved "https://registry.npmjs.org/amdefine/-/amdefine-1.0.1.tgz"
457
  integrity sha512-S2Hw0TtNkMJhIabBwIojKL9YHO5T0n5eNqWJ7Lrlel/zDbftQpxpapi8tZs3X1HWa+u+QeydGmzzNU0m09+Rcg==
458
 
 
 
 
 
 
 
 
459
  ansi-regex@^5.0.1:
460
  version "5.0.1"
461
  resolved "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz"
@@ -500,6 +604,11 @@ bidi-js@^1.0.3:
500
  dependencies:
501
  require-from-string "^2.0.2"
502
 
 
 
 
 
 
503
  brace-expansion@^1.1.7:
504
  version "1.1.12"
505
  resolved "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz"
@@ -508,6 +617,13 @@ brace-expansion@^1.1.7:
508
  balanced-match "^1.0.0"
509
  concat-map "0.0.1"
510
 
 
 
 
 
 
 
 
511
  callsites@^3.0.0:
512
  version "3.1.0"
513
  resolved "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz"
@@ -533,6 +649,21 @@ [email protected]:
533
  dependencies:
534
  jsonlint "1.6.0"
535
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
536
  cliui@^8.0.1:
537
  version "8.0.1"
538
  resolved "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz"
@@ -563,11 +694,21 @@ color-name@~1.1.4:
563
  resolved "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz"
564
  integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==
565
 
 
 
 
 
 
566
567
  version "0.5.1"
568
  resolved "https://registry.npmjs.org/colors/-/colors-0.5.1.tgz"
569
  integrity sha512-XjsuUwpDeY98+yz959OlUK6m7mLBM+1MEG5oaenfuQnNnrQk1WvtcvFgN3FNDP3f2NmZ211t0mNEfSEN1h0eIg==
570
 
 
 
 
 
 
571
572
  version "0.0.1"
573
  resolved "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz"
@@ -651,6 +792,29 @@ deep-is@^0.1.3:
651
  resolved "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz"
652
  integrity sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==
653
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
654
655
  version "0.1.10"
656
  resolved "https://registry.npmjs.org/ebnf-parser/-/ebnf-parser-0.1.10.tgz"
@@ -676,11 +840,31 @@ entities@^6.0.0:
676
  resolved "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz"
677
  integrity sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==
678
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
679
  es-module-lexer@^1.7.0:
680
  version "1.7.0"
681
  resolved "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz"
682
  integrity sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==
683
 
 
 
 
 
 
684
  esbuild@^0.21.3:
685
  version "0.21.5"
686
  resolved "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz"
@@ -710,39 +894,7 @@ esbuild@^0.21.3:
710
  "@esbuild/win32-ia32" "0.21.5"
711
  "@esbuild/win32-x64" "0.21.5"
712
 
713
- esbuild@^0.25.0:
714
- version "0.25.12"
715
- resolved "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz"
716
- integrity sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==
717
- optionalDependencies:
718
- "@esbuild/aix-ppc64" "0.25.12"
719
- "@esbuild/android-arm" "0.25.12"
720
- "@esbuild/android-arm64" "0.25.12"
721
- "@esbuild/android-x64" "0.25.12"
722
- "@esbuild/darwin-arm64" "0.25.12"
723
- "@esbuild/darwin-x64" "0.25.12"
724
- "@esbuild/freebsd-arm64" "0.25.12"
725
- "@esbuild/freebsd-x64" "0.25.12"
726
- "@esbuild/linux-arm" "0.25.12"
727
- "@esbuild/linux-arm64" "0.25.12"
728
- "@esbuild/linux-ia32" "0.25.12"
729
- "@esbuild/linux-loong64" "0.25.12"
730
- "@esbuild/linux-mips64el" "0.25.12"
731
- "@esbuild/linux-ppc64" "0.25.12"
732
- "@esbuild/linux-riscv64" "0.25.12"
733
- "@esbuild/linux-s390x" "0.25.12"
734
- "@esbuild/linux-x64" "0.25.12"
735
- "@esbuild/netbsd-arm64" "0.25.12"
736
- "@esbuild/netbsd-x64" "0.25.12"
737
- "@esbuild/openbsd-arm64" "0.25.12"
738
- "@esbuild/openbsd-x64" "0.25.12"
739
- "@esbuild/openharmony-arm64" "0.25.12"
740
- "@esbuild/sunos-x64" "0.25.12"
741
- "@esbuild/win32-arm64" "0.25.12"
742
- "@esbuild/win32-ia32" "0.25.12"
743
- "@esbuild/win32-x64" "0.25.12"
744
-
745
- esbuild@~0.25.0:
746
  version "0.25.12"
747
  resolved "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz"
748
  integrity sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==
@@ -926,6 +1078,11 @@ esutils@~1.0.0:
926
  resolved "https://registry.npmjs.org/esutils/-/esutils-1.0.0.tgz"
927
  integrity sha512-x/iYH53X3quDwfHRz4y8rn4XcEwwCJeWsul9pF1zldMbGtgOtMNBEOuYWwB1EQlK2LRa1fev3YAgym/RElp5Cg==
928
 
 
 
 
 
 
929
  expect-type@^1.2.2:
930
  version "1.2.2"
931
  resolved "https://registry.npmjs.org/expect-type/-/expect-type-1.2.2.tgz"
@@ -968,6 +1125,13 @@ file-entry-cache@^8.0.0:
968
  dependencies:
969
  flat-cache "^4.0.0"
970
 
 
 
 
 
 
 
 
971
  find-up@^5.0.0:
972
  version "5.0.0"
973
  resolved "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz"
@@ -984,6 +1148,11 @@ flat-cache@^4.0.0:
984
  flatted "^3.2.9"
985
  keyv "^4.5.4"
986
 
 
 
 
 
 
987
  flatted@^3.2.9, flatted@^3.3.3:
988
  version "3.3.3"
989
  resolved "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz"
@@ -994,7 +1163,7 @@ get-caller-file@^2.0.5:
994
  resolved "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz"
995
  integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==
996
 
997
- get-east-asian-width@^1.0.0:
998
  version "1.4.0"
999
  resolved "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.4.0.tgz"
1000
  integrity sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q==
@@ -1013,16 +1182,53 @@ glob-parent@^6.0.2:
1013
  dependencies:
1014
  is-glob "^4.0.3"
1015
 
 
 
 
 
 
 
 
 
 
 
 
 
1016
  globals@^14.0.0:
1017
  version "14.0.0"
1018
  resolved "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz"
1019
  integrity sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==
1020
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1021
  has-flag@^4.0.0:
1022
  version "4.0.0"
1023
  resolved "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz"
1024
  integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==
1025
 
 
 
 
 
 
 
 
1026
  html-encoding-sniffer@^4.0.0:
1027
  version "4.0.0"
1028
  resolved "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz"
@@ -1046,6 +1252,11 @@ https-proxy-agent@^7.0.6:
1046
  agent-base "^7.1.2"
1047
  debug "4"
1048
 
 
 
 
 
 
1049
1050
  version "0.6.3"
1051
  resolved "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz"
@@ -1081,6 +1292,13 @@ is-fullwidth-code-point@^3.0.0:
1081
  resolved "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz"
1082
  integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==
1083
 
 
 
 
 
 
 
 
1084
  is-glob@^4.0.0, is-glob@^4.0.3:
1085
  version "4.0.3"
1086
  resolved "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz"
@@ -1088,6 +1306,11 @@ is-glob@^4.0.0, is-glob@^4.0.3:
1088
  dependencies:
1089
  is-extglob "^2.1.1"
1090
 
 
 
 
 
 
1091
  is-potential-custom-element-name@^1.0.1:
1092
  version "1.0.1"
1093
  resolved "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz"
@@ -1168,6 +1391,11 @@ json-stable-stringify-without-jsonify@^1.0.1:
1168
  resolved "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz"
1169
  integrity sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==
1170
 
 
 
 
 
 
1171
1172
  version "1.6.0"
1173
  resolved "https://registry.npmjs.org/jsonlint/-/jsonlint-1.6.0.tgz"
@@ -1206,6 +1434,31 @@ lex-parser@~0.1.3, [email protected]:
1206
  resolved "https://registry.npmjs.org/lex-parser/-/lex-parser-0.1.4.tgz"
1207
  integrity sha512-DuAEISsr1H4LOpmFLkyMc8YStiRWZCO8hMsoXAXSbgyfvs2WQhSt0+/FBv3ZU/JBFZMGcE+FWzEBSzwUU7U27w==
1208
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1209
  locate-path@^6.0.0:
1210
  version "6.0.0"
1211
  resolved "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz"
@@ -1223,6 +1476,22 @@ lodash@^4.17.21:
1223
  resolved "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz"
1224
  integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
1225
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1226
  lru-cache@^11.2.1, lru-cache@^11.2.2:
1227
  version "11.2.2"
1228
  resolved "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.2.tgz"
@@ -1235,11 +1504,31 @@ magic-string@^0.30.19:
1235
  dependencies:
1236
  "@jridgewell/sourcemap-codec" "^1.5.5"
1237
 
 
 
 
 
 
 
 
1238
1239
  version "2.12.2"
1240
  resolved "https://registry.npmjs.org/mdn-data/-/mdn-data-2.12.2.tgz"
1241
  integrity sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA==
1242
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1243
  minimatch@^3.1.2:
1244
  version "3.1.2"
1245
  resolved "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz"
@@ -1257,6 +1546,16 @@ ms@^2.1.3:
1257
  resolved "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz"
1258
  integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==
1259
 
 
 
 
 
 
 
 
 
 
 
1260
  nanoid@^3.3.11:
1261
  version "3.3.11"
1262
  resolved "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz"
@@ -1275,6 +1574,44 @@ natural-compare@^1.4.0:
1275
  colors "0.5.x"
1276
  underscore "1.1.x"
1277
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1278
  optionator@^0.9.3:
1279
  version "0.9.4"
1280
  resolved "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz"
@@ -1315,6 +1652,11 @@ parse5@^8.0.0:
1315
  dependencies:
1316
  entities "^6.0.0"
1317
 
 
 
 
 
 
1318
  path-exists@^4.0.0:
1319
  version "4.0.0"
1320
  resolved "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz"
@@ -1335,11 +1677,26 @@ picocolors@^1.1.1:
1335
  resolved "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz"
1336
  integrity sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==
1337
 
1338
- "picomatch@^3 || ^4", picomatch@^4.0.3:
 
 
 
 
 
1339
  version "4.0.3"
1340
  resolved "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz"
1341
  integrity sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==
1342
 
 
 
 
 
 
 
 
 
 
 
1343
  postcss@^8.4.43, postcss@^8.5.6:
1344
  version "8.5.6"
1345
  resolved "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz"
@@ -1366,6 +1723,24 @@ prettier@^3.6.2, prettier@>=3.0.0:
1366
  resolved "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz"
1367
  integrity sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==
1368
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1369
  punycode@^2.1.0, punycode@^2.3.1:
1370
  version "2.3.1"
1371
  resolved "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz"
@@ -1391,6 +1766,31 @@ resolve-pkg-maps@^1.0.0:
1391
  resolved "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz"
1392
  integrity sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==
1393
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1394
  rollup@^4.20.0, rollup@^4.43.0:
1395
  version "4.52.5"
1396
  resolved "https://registry.npmjs.org/rollup/-/rollup-4.52.5.tgz"
@@ -1441,6 +1841,23 @@ saxes@^6.0.0:
1441
  dependencies:
1442
  xmlchars "^2.2.0"
1443
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1444
  shebang-command@^2.0.0:
1445
  version "2.0.0"
1446
  resolved "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz"
@@ -1463,6 +1880,11 @@ siginfo@^2.0.0:
1463
  resolved "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz"
1464
  integrity sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==
1465
 
 
 
 
 
 
1466
  sirv@^3.0.2:
1467
  version "3.0.2"
1468
  resolved "https://registry.npmjs.org/sirv/-/sirv-3.0.2.tgz"
@@ -1472,6 +1894,14 @@ sirv@^3.0.2:
1472
  mrmime "^2.0.0"
1473
  totalist "^3.0.0"
1474
 
 
 
 
 
 
 
 
 
1475
  source-map-js@^1.0.1, source-map-js@^1.2.1:
1476
  version "1.2.1"
1477
  resolved "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz"
@@ -1489,6 +1919,11 @@ spawn-command@^0.0.2-1:
1489
  resolved "https://registry.npmjs.org/spawn-command/-/spawn-command-0.0.2.tgz"
1490
  integrity sha512-zC8zGoGkmc8J9ndvml8Xksr1Amk9qBujgbF0JAIWO7kXr43w0h/0GJNM/Vustixu+YE8N/MTrQ7N31FvHUACxQ==
1491
 
 
 
 
 
 
1492
1493
  version "0.0.2"
1494
  resolved "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz"
@@ -1499,6 +1934,11 @@ std-env@^3.9.0:
1499
  resolved "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz"
1500
  integrity sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==
1501
 
 
 
 
 
 
1502
  string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
1503
  version "4.2.3"
1504
  resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz"
@@ -1517,6 +1957,14 @@ string-width@^7.0.0, string-width@^7.2.0:
1517
  get-east-asian-width "^1.0.0"
1518
  strip-ansi "^7.1.0"
1519
 
 
 
 
 
 
 
 
 
1520
  strip-ansi@^6.0.0, strip-ansi@^6.0.1:
1521
  version "6.0.1"
1522
  resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz"
@@ -1597,6 +2045,13 @@ tldts@^7.0.5:
1597
  dependencies:
1598
  tldts-core "^7.0.17"
1599
 
 
 
 
 
 
 
 
1600
  totalist@^3.0.0:
1601
  version "3.0.1"
1602
  resolved "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz"
@@ -1643,7 +2098,12 @@ type-check@^0.4.0, type-check@~0.4.0:
1643
  dependencies:
1644
  prelude-ls "^1.2.1"
1645
 
1646
- typescript@*, typescript@^5.2.2:
 
 
 
 
 
1647
  version "5.9.3"
1648
  resolved "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz"
1649
  integrity sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==
@@ -1716,6 +2176,19 @@ vitest@^4.0.6, [email protected]:
1716
  vite "^6.0.0 || ^7.0.0"
1717
  why-is-node-running "^2.3.0"
1718
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1719
  vue@^3.2.25, vue@^3.3.4, [email protected]:
1720
  version "3.5.22"
1721
  resolved "https://registry.npmjs.org/vue/-/vue-3.5.22.tgz"
@@ -1817,6 +2290,11 @@ y18n@^5.0.5:
1817
  resolved "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz"
1818
  integrity sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==
1819
 
 
 
 
 
 
1820
  yargs-parser@^21.1.1:
1821
  version "21.1.1"
1822
  resolved "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz"
 
213
  resolved "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz"
214
  integrity sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==
215
 
216
+ "@protobufjs/aspromise@^1.1.1", "@protobufjs/aspromise@^1.1.2":
217
+ version "1.1.2"
218
+ resolved "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz"
219
+ integrity sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==
220
+
221
+ "@protobufjs/base64@^1.1.2":
222
+ version "1.1.2"
223
+ resolved "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz"
224
+ integrity sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==
225
+
226
+ "@protobufjs/codegen@^2.0.4":
227
+ version "2.0.4"
228
+ resolved "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz"
229
+ integrity sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==
230
+
231
+ "@protobufjs/eventemitter@^1.1.0":
232
+ version "1.1.0"
233
+ resolved "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz"
234
+ integrity sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==
235
+
236
+ "@protobufjs/fetch@^1.1.0":
237
+ version "1.1.0"
238
+ resolved "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz"
239
+ integrity sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==
240
+ dependencies:
241
+ "@protobufjs/aspromise" "^1.1.1"
242
+ "@protobufjs/inquire" "^1.1.0"
243
+
244
+ "@protobufjs/float@^1.0.2":
245
+ version "1.0.2"
246
+ resolved "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz"
247
+ integrity sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==
248
+
249
+ "@protobufjs/inquire@^1.1.0":
250
+ version "1.1.0"
251
+ resolved "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz"
252
+ integrity sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==
253
+
254
+ "@protobufjs/path@^1.1.2":
255
+ version "1.1.2"
256
+ resolved "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz"
257
+ integrity sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==
258
+
259
+ "@protobufjs/pool@^1.1.0":
260
+ version "1.1.0"
261
+ resolved "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz"
262
+ integrity sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==
263
+
264
+ "@protobufjs/utf8@^1.1.0":
265
+ version "1.1.0"
266
+ resolved "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz"
267
+ integrity sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==
268
+
269
  "@rollup/[email protected]":
270
  version "4.52.5"
271
  resolved "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.52.5.tgz"
 
304
  resolved "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz"
305
  integrity sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==
306
 
307
+ "@types/node@^18.0.0 || >=20.0.0", "@types/node@^20.0.0 || ^22.0.0 || >=24.0.0", "@types/node@^20.19.0 || >=22.12.0", "@types/node@^24.10.0", "@types/node@>=13.7.0":
308
  version "24.10.0"
309
  resolved "https://registry.npmjs.org/@types/node/-/node-24.10.0.tgz"
310
  integrity sha512-qzQZRBqkFsYyaSWXuEHc2WR9c0a0CXwiE5FWUvn7ZM+vdy1uZLfCunD38UzhuB7YN/J11ndbDBcTmOdxJo9Q7A==
 
399
  "@vitest/pretty-format" "4.0.6"
400
  tinyrainbow "^3.0.3"
401
 
402
+ "@volar/[email protected]":
403
+ version "2.4.23"
404
+ resolved "https://registry.npmjs.org/@volar/language-core/-/language-core-2.4.23.tgz"
405
+ integrity sha512-hEEd5ET/oSmBC6pi1j6NaNYRWoAiDhINbT8rmwtINugR39loROSlufGdYMF9TaKGfz+ViGs1Idi3mAhnuPcoGQ==
406
+ dependencies:
407
+ "@volar/source-map" "2.4.23"
408
+
409
+ "@volar/[email protected]":
410
+ version "2.4.23"
411
+ resolved "https://registry.npmjs.org/@volar/source-map/-/source-map-2.4.23.tgz"
412
+ integrity sha512-Z1Uc8IB57Lm6k7q6KIDu/p+JWtf3xsXJqAX/5r18hYOTpJyBn0KXUR8oTJ4WFYOcDzWC9n3IflGgHowx6U6z9Q==
413
+
414
+ "@volar/[email protected]":
415
+ version "2.4.23"
416
+ resolved "https://registry.npmjs.org/@volar/typescript/-/typescript-2.4.23.tgz"
417
+ integrity sha512-lAB5zJghWxVPqfcStmAP1ZqQacMpe90UrP5RJ3arDyrhy4aCUQqmxPPLB2PWDKugvylmO41ljK7vZ+t6INMTag==
418
+ dependencies:
419
+ "@volar/language-core" "2.4.23"
420
+ path-browserify "^1.0.1"
421
+ vscode-uri "^3.0.8"
422
+
423
  "@vue/[email protected]":
424
  version "3.5.22"
425
  resolved "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.22.tgz"
 
431
  estree-walker "^2.0.2"
432
  source-map-js "^1.2.1"
433
 
434
+ "@vue/compiler-dom@^3.5.0", "@vue/compiler-dom@3.5.22":
435
  version "3.5.22"
436
  resolved "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.22.tgz"
437
  integrity sha512-W8RknzUM1BLkypvdz10OVsGxnMAuSIZs9Wdx1vzA3mL5fNMN15rhrSCLiTm6blWeACwUwizzPVqGJgOGBEN/hA==
 
462
  "@vue/compiler-dom" "3.5.22"
463
  "@vue/shared" "3.5.22"
464
 
465
+ "@vue/[email protected]":
466
+ version "3.1.3"
467
+ resolved "https://registry.npmjs.org/@vue/language-core/-/language-core-3.1.3.tgz"
468
+ integrity sha512-KpR1F/eGAG9D1RZ0/T6zWJs6dh/pRLfY5WupecyYKJ1fjVmDMgTPw9wXmKv2rBjo4zCJiOSiyB8BDP1OUwpMEA==
469
+ dependencies:
470
+ "@volar/language-core" "2.4.23"
471
+ "@vue/compiler-dom" "^3.5.0"
472
+ "@vue/shared" "^3.5.0"
473
+ alien-signals "^3.0.0"
474
+ muggle-string "^0.4.1"
475
+ path-browserify "^1.0.1"
476
+ picomatch "^4.0.2"
477
+
478
  "@vue/[email protected]":
479
  version "3.5.22"
480
  resolved "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.22.tgz"
 
508
  "@vue/compiler-ssr" "3.5.22"
509
  "@vue/shared" "3.5.22"
510
 
511
+ "@vue/shared@^3.5.0", "@vue/shared@3.5.22":
512
  version "3.5.22"
513
  resolved "https://registry.npmjs.org/@vue/shared/-/shared-3.5.22.tgz"
514
  integrity sha512-F4yc6palwq3TT0u+FYf0Ns4Tfl9GRFURDN2gWG7L1ecIaS/4fCIuFOjMTnCyjsu/OK6vaDKLCrGAa+KvvH+h4w==
 
523
  resolved "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz"
524
  integrity sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==
525
 
526
+ adm-zip@^0.5.16:
527
+ version "0.5.16"
528
+ resolved "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.16.tgz"
529
+ integrity sha512-TGw5yVi4saajsSEgz25grObGHEUaDrniwvA2qwSC060KfqGPdglhvPMA2lPIoxs3PQIItj2iag35fONcQqgUaQ==
530
+
531
  agent-base@^7.1.0, agent-base@^7.1.2:
532
  version "7.1.4"
533
  resolved "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz"
 
543
  json-schema-traverse "^0.4.1"
544
  uri-js "^4.2.2"
545
 
546
+ alien-signals@^3.0.0:
547
+ version "3.1.0"
548
+ resolved "https://registry.npmjs.org/alien-signals/-/alien-signals-3.1.0.tgz"
549
+ integrity sha512-yufC6VpSy8tK3I0lO67pjumo5JvDQVQyr38+3OHqe6CHl1t2VZekKZ7EKKZSqk0cRmE7U7tfZbpXiKNzuc+ckg==
550
+
551
  amdefine@>=0.0.4:
552
  version "1.0.1"
553
  resolved "https://registry.npmjs.org/amdefine/-/amdefine-1.0.1.tgz"
554
  integrity sha512-S2Hw0TtNkMJhIabBwIojKL9YHO5T0n5eNqWJ7Lrlel/zDbftQpxpapi8tZs3X1HWa+u+QeydGmzzNU0m09+Rcg==
555
 
556
+ ansi-escapes@^7.0.0:
557
+ version "7.2.0"
558
+ resolved "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.2.0.tgz"
559
+ integrity sha512-g6LhBsl+GBPRWGWsBtutpzBYuIIdBkLEvad5C/va/74Db018+5TZiyA26cZJAr3Rft5lprVqOIPxf5Vid6tqAw==
560
+ dependencies:
561
+ environment "^1.0.0"
562
+
563
  ansi-regex@^5.0.1:
564
  version "5.0.1"
565
  resolved "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz"
 
604
  dependencies:
605
  require-from-string "^2.0.2"
606
 
607
+ boolean@^3.0.1:
608
+ version "3.2.0"
609
+ resolved "https://registry.npmjs.org/boolean/-/boolean-3.2.0.tgz"
610
+ integrity sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==
611
+
612
  brace-expansion@^1.1.7:
613
  version "1.1.12"
614
  resolved "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz"
 
617
  balanced-match "^1.0.0"
618
  concat-map "0.0.1"
619
 
620
+ braces@^3.0.3:
621
+ version "3.0.3"
622
+ resolved "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz"
623
+ integrity sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==
624
+ dependencies:
625
+ fill-range "^7.1.1"
626
+
627
  callsites@^3.0.0:
628
  version "3.1.0"
629
  resolved "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz"
 
649
  dependencies:
650
  jsonlint "1.6.0"
651
 
652
+ cli-cursor@^5.0.0:
653
+ version "5.0.0"
654
+ resolved "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz"
655
+ integrity sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==
656
+ dependencies:
657
+ restore-cursor "^5.0.0"
658
+
659
+ cli-truncate@^5.0.0:
660
+ version "5.1.1"
661
+ resolved "https://registry.npmjs.org/cli-truncate/-/cli-truncate-5.1.1.tgz"
662
+ integrity sha512-SroPvNHxUnk+vIW/dOSfNqdy1sPEFkrTk6TUtqLCnBlo3N7TNYYkzzN7uSD6+jVjrdO4+p8nH7JzH6cIvUem6A==
663
+ dependencies:
664
+ slice-ansi "^7.1.0"
665
+ string-width "^8.0.0"
666
+
667
  cliui@^8.0.1:
668
  version "8.0.1"
669
  resolved "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz"
 
694
  resolved "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz"
695
  integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==
696
 
697
+ colorette@^2.0.20:
698
+ version "2.0.20"
699
+ resolved "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz"
700
+ integrity sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==
701
+
702
703
  version "0.5.1"
704
  resolved "https://registry.npmjs.org/colors/-/colors-0.5.1.tgz"
705
  integrity sha512-XjsuUwpDeY98+yz959OlUK6m7mLBM+1MEG5oaenfuQnNnrQk1WvtcvFgN3FNDP3f2NmZ211t0mNEfSEN1h0eIg==
706
 
707
+ commander@^14.0.2:
708
+ version "14.0.2"
709
+ resolved "https://registry.npmjs.org/commander/-/commander-14.0.2.tgz"
710
+ integrity sha512-TywoWNNRbhoD0BXs1P3ZEScW8W5iKrnbithIl0YH+uCmBd0QpPOA8yc82DS3BIE5Ma6FnBVUsJ7wVUDz4dvOWQ==
711
+
712
713
  version "0.0.1"
714
  resolved "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz"
 
792
  resolved "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz"
793
  integrity sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==
794
 
795
+ define-data-property@^1.0.1:
796
+ version "1.1.4"
797
+ resolved "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz"
798
+ integrity sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==
799
+ dependencies:
800
+ es-define-property "^1.0.0"
801
+ es-errors "^1.3.0"
802
+ gopd "^1.0.1"
803
+
804
+ define-properties@^1.2.1:
805
+ version "1.2.1"
806
+ resolved "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz"
807
+ integrity sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==
808
+ dependencies:
809
+ define-data-property "^1.0.1"
810
+ has-property-descriptors "^1.0.0"
811
+ object-keys "^1.1.1"
812
+
813
+ detect-node@^2.0.4:
814
+ version "2.1.0"
815
+ resolved "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz"
816
+ integrity sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==
817
+
818
819
  version "0.1.10"
820
  resolved "https://registry.npmjs.org/ebnf-parser/-/ebnf-parser-0.1.10.tgz"
 
840
  resolved "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz"
841
  integrity sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==
842
 
843
+ environment@^1.0.0:
844
+ version "1.1.0"
845
+ resolved "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz"
846
+ integrity sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==
847
+
848
+ es-define-property@^1.0.0:
849
+ version "1.0.1"
850
+ resolved "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz"
851
+ integrity sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==
852
+
853
+ es-errors@^1.3.0:
854
+ version "1.3.0"
855
+ resolved "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz"
856
+ integrity sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==
857
+
858
  es-module-lexer@^1.7.0:
859
  version "1.7.0"
860
  resolved "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz"
861
  integrity sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==
862
 
863
+ es6-error@^4.1.1:
864
+ version "4.1.1"
865
+ resolved "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz"
866
+ integrity sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==
867
+
868
  esbuild@^0.21.3:
869
  version "0.21.5"
870
  resolved "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz"
 
894
  "@esbuild/win32-ia32" "0.21.5"
895
  "@esbuild/win32-x64" "0.21.5"
896
 
897
+ esbuild@^0.25.0, esbuild@~0.25.0:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
898
  version "0.25.12"
899
  resolved "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz"
900
  integrity sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==
 
1078
  resolved "https://registry.npmjs.org/esutils/-/esutils-1.0.0.tgz"
1079
  integrity sha512-x/iYH53X3quDwfHRz4y8rn4XcEwwCJeWsul9pF1zldMbGtgOtMNBEOuYWwB1EQlK2LRa1fev3YAgym/RElp5Cg==
1080
 
1081
+ eventemitter3@^5.0.1:
1082
+ version "5.0.1"
1083
+ resolved "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz"
1084
+ integrity sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==
1085
+
1086
  expect-type@^1.2.2:
1087
  version "1.2.2"
1088
  resolved "https://registry.npmjs.org/expect-type/-/expect-type-1.2.2.tgz"
 
1125
  dependencies:
1126
  flat-cache "^4.0.0"
1127
 
1128
+ fill-range@^7.1.1:
1129
+ version "7.1.1"
1130
+ resolved "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz"
1131
+ integrity sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==
1132
+ dependencies:
1133
+ to-regex-range "^5.0.1"
1134
+
1135
  find-up@^5.0.0:
1136
  version "5.0.0"
1137
  resolved "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz"
 
1148
  flatted "^3.2.9"
1149
  keyv "^4.5.4"
1150
 
1151
+ flatbuffers@^25.1.24:
1152
+ version "25.9.23"
1153
+ resolved "https://registry.npmjs.org/flatbuffers/-/flatbuffers-25.9.23.tgz"
1154
+ integrity sha512-MI1qs7Lo4Syw0EOzUl0xjs2lsoeqFku44KpngfIduHBYvzm8h2+7K8YMQh1JtVVVrUvhLpNwqVi4DERegUJhPQ==
1155
+
1156
  flatted@^3.2.9, flatted@^3.3.3:
1157
  version "3.3.3"
1158
  resolved "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz"
 
1163
  resolved "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz"
1164
  integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==
1165
 
1166
+ get-east-asian-width@^1.0.0, get-east-asian-width@^1.3.0, get-east-asian-width@^1.3.1:
1167
  version "1.4.0"
1168
  resolved "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.4.0.tgz"
1169
  integrity sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q==
 
1182
  dependencies:
1183
  is-glob "^4.0.3"
1184
 
1185
+ global-agent@^3.0.0:
1186
+ version "3.0.0"
1187
+ resolved "https://registry.npmjs.org/global-agent/-/global-agent-3.0.0.tgz"
1188
+ integrity sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q==
1189
+ dependencies:
1190
+ boolean "^3.0.1"
1191
+ es6-error "^4.1.1"
1192
+ matcher "^3.0.0"
1193
+ roarr "^2.15.3"
1194
+ semver "^7.3.2"
1195
+ serialize-error "^7.0.1"
1196
+
1197
  globals@^14.0.0:
1198
  version "14.0.0"
1199
  resolved "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz"
1200
  integrity sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==
1201
 
1202
+ globalthis@^1.0.1:
1203
+ version "1.0.4"
1204
+ resolved "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz"
1205
+ integrity sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==
1206
+ dependencies:
1207
+ define-properties "^1.2.1"
1208
+ gopd "^1.0.1"
1209
+
1210
+ gopd@^1.0.1:
1211
+ version "1.2.0"
1212
+ resolved "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz"
1213
+ integrity sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==
1214
+
1215
+ guid-typescript@^1.0.9:
1216
+ version "1.0.9"
1217
+ resolved "https://registry.npmjs.org/guid-typescript/-/guid-typescript-1.0.9.tgz"
1218
+ integrity sha512-Y8T4vYhEfwJOTbouREvG+3XDsjr8E3kIr7uf+JZ0BYloFsttiHU0WfvANVsR7TxNUJa/WpCnw/Ino/p+DeBhBQ==
1219
+
1220
  has-flag@^4.0.0:
1221
  version "4.0.0"
1222
  resolved "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz"
1223
  integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==
1224
 
1225
+ has-property-descriptors@^1.0.0:
1226
+ version "1.0.2"
1227
+ resolved "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz"
1228
+ integrity sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==
1229
+ dependencies:
1230
+ es-define-property "^1.0.0"
1231
+
1232
  html-encoding-sniffer@^4.0.0:
1233
  version "4.0.0"
1234
  resolved "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz"
 
1252
  agent-base "^7.1.2"
1253
  debug "4"
1254
 
1255
+ husky@^9.1.7:
1256
+ version "9.1.7"
1257
+ resolved "https://registry.npmjs.org/husky/-/husky-9.1.7.tgz"
1258
+ integrity sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==
1259
+
1260
1261
  version "0.6.3"
1262
  resolved "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz"
 
1292
  resolved "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz"
1293
  integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==
1294
 
1295
+ is-fullwidth-code-point@^5.0.0:
1296
+ version "5.1.0"
1297
+ resolved "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.1.0.tgz"
1298
+ integrity sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ==
1299
+ dependencies:
1300
+ get-east-asian-width "^1.3.1"
1301
+
1302
  is-glob@^4.0.0, is-glob@^4.0.3:
1303
  version "4.0.3"
1304
  resolved "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz"
 
1306
  dependencies:
1307
  is-extglob "^2.1.1"
1308
 
1309
+ is-number@^7.0.0:
1310
+ version "7.0.0"
1311
+ resolved "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz"
1312
+ integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==
1313
+
1314
  is-potential-custom-element-name@^1.0.1:
1315
  version "1.0.1"
1316
  resolved "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz"
 
1391
  resolved "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz"
1392
  integrity sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==
1393
 
1394
+ json-stringify-safe@^5.0.1:
1395
+ version "5.0.1"
1396
+ resolved "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz"
1397
+ integrity sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==
1398
+
1399
1400
  version "1.6.0"
1401
  resolved "https://registry.npmjs.org/jsonlint/-/jsonlint-1.6.0.tgz"
 
1434
  resolved "https://registry.npmjs.org/lex-parser/-/lex-parser-0.1.4.tgz"
1435
  integrity sha512-DuAEISsr1H4LOpmFLkyMc8YStiRWZCO8hMsoXAXSbgyfvs2WQhSt0+/FBv3ZU/JBFZMGcE+FWzEBSzwUU7U27w==
1436
 
1437
+ lint-staged@^16.2.7:
1438
+ version "16.2.7"
1439
+ resolved "https://registry.npmjs.org/lint-staged/-/lint-staged-16.2.7.tgz"
1440
+ integrity sha512-lDIj4RnYmK7/kXMya+qJsmkRFkGolciXjrsZ6PC25GdTfWOAWetR0ZbsNXRAj1EHHImRSalc+whZFg56F5DVow==
1441
+ dependencies:
1442
+ commander "^14.0.2"
1443
+ listr2 "^9.0.5"
1444
+ micromatch "^4.0.8"
1445
+ nano-spawn "^2.0.0"
1446
+ pidtree "^0.6.0"
1447
+ string-argv "^0.3.2"
1448
+ yaml "^2.8.1"
1449
+
1450
+ listr2@^9.0.5:
1451
+ version "9.0.5"
1452
+ resolved "https://registry.npmjs.org/listr2/-/listr2-9.0.5.tgz"
1453
+ integrity sha512-ME4Fb83LgEgwNw96RKNvKV4VTLuXfoKudAmm2lP8Kk87KaMK0/Xrx/aAkMWmT8mDb+3MlFDspfbCs7adjRxA2g==
1454
+ dependencies:
1455
+ cli-truncate "^5.0.0"
1456
+ colorette "^2.0.20"
1457
+ eventemitter3 "^5.0.1"
1458
+ log-update "^6.1.0"
1459
+ rfdc "^1.4.1"
1460
+ wrap-ansi "^9.0.0"
1461
+
1462
  locate-path@^6.0.0:
1463
  version "6.0.0"
1464
  resolved "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz"
 
1476
  resolved "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz"
1477
  integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
1478
 
1479
+ log-update@^6.1.0:
1480
+ version "6.1.0"
1481
+ resolved "https://registry.npmjs.org/log-update/-/log-update-6.1.0.tgz"
1482
+ integrity sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==
1483
+ dependencies:
1484
+ ansi-escapes "^7.0.0"
1485
+ cli-cursor "^5.0.0"
1486
+ slice-ansi "^7.1.0"
1487
+ strip-ansi "^7.1.0"
1488
+ wrap-ansi "^9.0.0"
1489
+
1490
+ long@^5.0.0, long@^5.2.3:
1491
+ version "5.3.2"
1492
+ resolved "https://registry.npmjs.org/long/-/long-5.3.2.tgz"
1493
+ integrity sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==
1494
+
1495
  lru-cache@^11.2.1, lru-cache@^11.2.2:
1496
  version "11.2.2"
1497
  resolved "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.2.tgz"
 
1504
  dependencies:
1505
  "@jridgewell/sourcemap-codec" "^1.5.5"
1506
 
1507
+ matcher@^3.0.0:
1508
+ version "3.0.0"
1509
+ resolved "https://registry.npmjs.org/matcher/-/matcher-3.0.0.tgz"
1510
+ integrity sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng==
1511
+ dependencies:
1512
+ escape-string-regexp "^4.0.0"
1513
+
1514
1515
  version "2.12.2"
1516
  resolved "https://registry.npmjs.org/mdn-data/-/mdn-data-2.12.2.tgz"
1517
  integrity sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA==
1518
 
1519
+ micromatch@^4.0.8:
1520
+ version "4.0.8"
1521
+ resolved "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz"
1522
+ integrity sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==
1523
+ dependencies:
1524
+ braces "^3.0.3"
1525
+ picomatch "^2.3.1"
1526
+
1527
+ mimic-function@^5.0.0:
1528
+ version "5.0.1"
1529
+ resolved "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz"
1530
+ integrity sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==
1531
+
1532
  minimatch@^3.1.2:
1533
  version "3.1.2"
1534
  resolved "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz"
 
1546
  resolved "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz"
1547
  integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==
1548
 
1549
+ muggle-string@^0.4.1:
1550
+ version "0.4.1"
1551
+ resolved "https://registry.npmjs.org/muggle-string/-/muggle-string-0.4.1.tgz"
1552
+ integrity sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==
1553
+
1554
+ nano-spawn@^2.0.0:
1555
+ version "2.0.0"
1556
+ resolved "https://registry.npmjs.org/nano-spawn/-/nano-spawn-2.0.0.tgz"
1557
+ integrity sha512-tacvGzUY5o2D8CBh2rrwxyNojUsZNU2zjNTzKQrkgGJQTbGAfArVWXSKMBokBeeg6C7OLRGUEyoFlYbfeWQIqw==
1558
+
1559
  nanoid@^3.3.11:
1560
  version "3.3.11"
1561
  resolved "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz"
 
1574
  colors "0.5.x"
1575
  underscore "1.1.x"
1576
 
1577
+ object-keys@^1.1.1:
1578
+ version "1.1.1"
1579
+ resolved "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz"
1580
+ integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==
1581
+
1582
+ onetime@^7.0.0:
1583
+ version "7.0.0"
1584
+ resolved "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz"
1585
+ integrity sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==
1586
+ dependencies:
1587
+ mimic-function "^5.0.0"
1588
+
1589
1590
+ version "1.23.2"
1591
+ resolved "https://registry.npmjs.org/onnxruntime-common/-/onnxruntime-common-1.23.2.tgz"
1592
+ integrity sha512-5LFsC9Dukzp2WV6kNHYLNzp8sT6V02IubLCbzw2Xd6X5GOlr65gAX6xiJwyi2URJol/s71gaQLC5F2C25AAR2w==
1593
+
1594
1595
+ version "1.23.2"
1596
+ resolved "https://registry.npmjs.org/onnxruntime-node/-/onnxruntime-node-1.23.2.tgz"
1597
+ integrity sha512-OBTsG0W8ddBVOeVVVychpVBS87A9YV5sa2hJ6lc025T97Le+J4v++PwSC4XFs1C62SWyNdof0Mh4KvnZgtt4aw==
1598
+ dependencies:
1599
+ adm-zip "^0.5.16"
1600
+ global-agent "^3.0.0"
1601
+ onnxruntime-common "1.23.2"
1602
+
1603
1604
+ version "1.23.2"
1605
+ resolved "https://registry.npmjs.org/onnxruntime-web/-/onnxruntime-web-1.23.2.tgz"
1606
+ integrity sha512-T09JUtMn+CZLk3mFwqiH0lgQf+4S7+oYHHtk6uhaYAAJI95bTcKi5bOOZYwORXfS/RLZCjDDEXGWIuOCAFlEjg==
1607
+ dependencies:
1608
+ flatbuffers "^25.1.24"
1609
+ guid-typescript "^1.0.9"
1610
+ long "^5.2.3"
1611
+ onnxruntime-common "1.23.2"
1612
+ platform "^1.3.6"
1613
+ protobufjs "^7.2.4"
1614
+
1615
  optionator@^0.9.3:
1616
  version "0.9.4"
1617
  resolved "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz"
 
1652
  dependencies:
1653
  entities "^6.0.0"
1654
 
1655
+ path-browserify@^1.0.1:
1656
+ version "1.0.1"
1657
+ resolved "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz"
1658
+ integrity sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==
1659
+
1660
  path-exists@^4.0.0:
1661
  version "4.0.0"
1662
  resolved "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz"
 
1677
  resolved "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz"
1678
  integrity sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==
1679
 
1680
+ picomatch@^2.3.1:
1681
+ version "2.3.1"
1682
+ resolved "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz"
1683
+ integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==
1684
+
1685
+ "picomatch@^3 || ^4", picomatch@^4.0.2, picomatch@^4.0.3:
1686
  version "4.0.3"
1687
  resolved "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz"
1688
  integrity sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==
1689
 
1690
+ pidtree@^0.6.0:
1691
+ version "0.6.0"
1692
+ resolved "https://registry.npmjs.org/pidtree/-/pidtree-0.6.0.tgz"
1693
+ integrity sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g==
1694
+
1695
+ platform@^1.3.6:
1696
+ version "1.3.6"
1697
+ resolved "https://registry.npmjs.org/platform/-/platform-1.3.6.tgz"
1698
+ integrity sha512-fnWVljUchTro6RiCFvCXBbNhJc2NijN7oIQxbwsyL0buWJPG85v81ehlHI9fXrJsMNgTofEoWIQeClKpgxFLrg==
1699
+
1700
  postcss@^8.4.43, postcss@^8.5.6:
1701
  version "8.5.6"
1702
  resolved "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz"
 
1723
  resolved "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz"
1724
  integrity sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==
1725
 
1726
+ protobufjs@^7.2.4:
1727
+ version "7.5.4"
1728
+ resolved "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.4.tgz"
1729
+ integrity sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg==
1730
+ dependencies:
1731
+ "@protobufjs/aspromise" "^1.1.2"
1732
+ "@protobufjs/base64" "^1.1.2"
1733
+ "@protobufjs/codegen" "^2.0.4"
1734
+ "@protobufjs/eventemitter" "^1.1.0"
1735
+ "@protobufjs/fetch" "^1.1.0"
1736
+ "@protobufjs/float" "^1.0.2"
1737
+ "@protobufjs/inquire" "^1.1.0"
1738
+ "@protobufjs/path" "^1.1.2"
1739
+ "@protobufjs/pool" "^1.1.0"
1740
+ "@protobufjs/utf8" "^1.1.0"
1741
+ "@types/node" ">=13.7.0"
1742
+ long "^5.0.0"
1743
+
1744
  punycode@^2.1.0, punycode@^2.3.1:
1745
  version "2.3.1"
1746
  resolved "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz"
 
1766
  resolved "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz"
1767
  integrity sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==
1768
 
1769
+ restore-cursor@^5.0.0:
1770
+ version "5.1.0"
1771
+ resolved "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz"
1772
+ integrity sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==
1773
+ dependencies:
1774
+ onetime "^7.0.0"
1775
+ signal-exit "^4.1.0"
1776
+
1777
+ rfdc@^1.4.1:
1778
+ version "1.4.1"
1779
+ resolved "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz"
1780
+ integrity sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==
1781
+
1782
+ roarr@^2.15.3:
1783
+ version "2.15.4"
1784
+ resolved "https://registry.npmjs.org/roarr/-/roarr-2.15.4.tgz"
1785
+ integrity sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A==
1786
+ dependencies:
1787
+ boolean "^3.0.1"
1788
+ detect-node "^2.0.4"
1789
+ globalthis "^1.0.1"
1790
+ json-stringify-safe "^5.0.1"
1791
+ semver-compare "^1.0.0"
1792
+ sprintf-js "^1.1.2"
1793
+
1794
  rollup@^4.20.0, rollup@^4.43.0:
1795
  version "4.52.5"
1796
  resolved "https://registry.npmjs.org/rollup/-/rollup-4.52.5.tgz"
 
1841
  dependencies:
1842
  xmlchars "^2.2.0"
1843
 
1844
+ semver-compare@^1.0.0:
1845
+ version "1.0.0"
1846
+ resolved "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz"
1847
+ integrity sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==
1848
+
1849
+ semver@^7.3.2:
1850
+ version "7.7.3"
1851
+ resolved "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz"
1852
+ integrity sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==
1853
+
1854
+ serialize-error@^7.0.1:
1855
+ version "7.0.1"
1856
+ resolved "https://registry.npmjs.org/serialize-error/-/serialize-error-7.0.1.tgz"
1857
+ integrity sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw==
1858
+ dependencies:
1859
+ type-fest "^0.13.1"
1860
+
1861
  shebang-command@^2.0.0:
1862
  version "2.0.0"
1863
  resolved "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz"
 
1880
  resolved "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz"
1881
  integrity sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==
1882
 
1883
+ signal-exit@^4.1.0:
1884
+ version "4.1.0"
1885
+ resolved "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz"
1886
+ integrity sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==
1887
+
1888
  sirv@^3.0.2:
1889
  version "3.0.2"
1890
  resolved "https://registry.npmjs.org/sirv/-/sirv-3.0.2.tgz"
 
1894
  mrmime "^2.0.0"
1895
  totalist "^3.0.0"
1896
 
1897
+ slice-ansi@^7.1.0:
1898
+ version "7.1.2"
1899
+ resolved "https://registry.npmjs.org/slice-ansi/-/slice-ansi-7.1.2.tgz"
1900
+ integrity sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w==
1901
+ dependencies:
1902
+ ansi-styles "^6.2.1"
1903
+ is-fullwidth-code-point "^5.0.0"
1904
+
1905
  source-map-js@^1.0.1, source-map-js@^1.2.1:
1906
  version "1.2.1"
1907
  resolved "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz"
 
1919
  resolved "https://registry.npmjs.org/spawn-command/-/spawn-command-0.0.2.tgz"
1920
  integrity sha512-zC8zGoGkmc8J9ndvml8Xksr1Amk9qBujgbF0JAIWO7kXr43w0h/0GJNM/Vustixu+YE8N/MTrQ7N31FvHUACxQ==
1921
 
1922
+ sprintf-js@^1.1.2:
1923
+ version "1.1.3"
1924
+ resolved "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz"
1925
+ integrity sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==
1926
+
1927
1928
  version "0.0.2"
1929
  resolved "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz"
 
1934
  resolved "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz"
1935
  integrity sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==
1936
 
1937
+ string-argv@^0.3.2:
1938
+ version "0.3.2"
1939
+ resolved "https://registry.npmjs.org/string-argv/-/string-argv-0.3.2.tgz"
1940
+ integrity sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==
1941
+
1942
  string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
1943
  version "4.2.3"
1944
  resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz"
 
1957
  get-east-asian-width "^1.0.0"
1958
  strip-ansi "^7.1.0"
1959
 
1960
+ string-width@^8.0.0:
1961
+ version "8.1.0"
1962
+ resolved "https://registry.npmjs.org/string-width/-/string-width-8.1.0.tgz"
1963
+ integrity sha512-Kxl3KJGb/gxkaUMOjRsQ8IrXiGW75O4E3RPjFIINOVH8AMl2SQ/yWdTzWwF3FevIX9LcMAjJW+GRwAlAbTSXdg==
1964
+ dependencies:
1965
+ get-east-asian-width "^1.3.0"
1966
+ strip-ansi "^7.1.0"
1967
+
1968
  strip-ansi@^6.0.0, strip-ansi@^6.0.1:
1969
  version "6.0.1"
1970
  resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz"
 
2045
  dependencies:
2046
  tldts-core "^7.0.17"
2047
 
2048
+ to-regex-range@^5.0.1:
2049
+ version "5.0.1"
2050
+ resolved "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz"
2051
+ integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==
2052
+ dependencies:
2053
+ is-number "^7.0.0"
2054
+
2055
  totalist@^3.0.0:
2056
  version "3.0.1"
2057
  resolved "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz"
 
2098
  dependencies:
2099
  prelude-ls "^1.2.1"
2100
 
2101
+ type-fest@^0.13.1:
2102
+ version "0.13.1"
2103
+ resolved "https://registry.npmjs.org/type-fest/-/type-fest-0.13.1.tgz"
2104
+ integrity sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==
2105
+
2106
+ typescript@*, typescript@^5.2.2, typescript@>=5.0.0:
2107
  version "5.9.3"
2108
  resolved "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz"
2109
  integrity sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==
 
2176
  vite "^6.0.0 || ^7.0.0"
2177
  why-is-node-running "^2.3.0"
2178
 
2179
+ vscode-uri@^3.0.8:
2180
+ version "3.1.0"
2181
+ resolved "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.1.0.tgz"
2182
+ integrity sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==
2183
+
2184
+ vue-tsc@^3.1.3:
2185
+ version "3.1.3"
2186
+ resolved "https://registry.npmjs.org/vue-tsc/-/vue-tsc-3.1.3.tgz"
2187
+ integrity sha512-StMNfZHwPIXQgY3KxPKM0Jsoc8b46mDV3Fn2UlHCBIwRJApjqrSwqeMYgWf0zpN+g857y74pv7GWuBm+UqQe1w==
2188
+ dependencies:
2189
+ "@volar/typescript" "2.4.23"
2190
+ "@vue/language-core" "3.1.3"
2191
+
2192
  vue@^3.2.25, vue@^3.3.4, [email protected]:
2193
  version "3.5.22"
2194
  resolved "https://registry.npmjs.org/vue/-/vue-3.5.22.tgz"
 
2290
  resolved "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz"
2291
  integrity sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==
2292
 
2293
+ yaml@^2.4.2, yaml@^2.8.1:
2294
+ version "2.8.1"
2295
+ resolved "https://registry.npmjs.org/yaml/-/yaml-2.8.1.tgz"
2296
+ integrity sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==
2297
+
2298
  yargs-parser@^21.1.1:
2299
  version "21.1.1"
2300
  resolved "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz"