k-l-lambda commited on
Commit
466436b
·
1 Parent(s): e992c03

the first deploy

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .dockerignore +43 -0
  2. Dockerfile +42 -0
  3. deploy.sh +61 -0
  4. trigo-web/.prettierignore +9 -0
  5. trigo-web/.prettierrc +17 -0
  6. trigo-web/README.md +227 -0
  7. trigo-web/app/.env +3 -0
  8. trigo-web/app/.env.local.example +11 -0
  9. trigo-web/app/.gitignore +28 -0
  10. trigo-web/app/index.html +13 -0
  11. trigo-web/app/package-lock.json +0 -0
  12. trigo-web/app/package.json +27 -0
  13. trigo-web/app/src/App.vue +27 -0
  14. trigo-web/app/src/assets/logo.png +0 -0
  15. trigo-web/app/src/main.ts +23 -0
  16. trigo-web/app/src/router/index.ts +17 -0
  17. trigo-web/app/src/services/trigoViewport.ts +1679 -0
  18. trigo-web/app/src/stores/gameStore.ts +310 -0
  19. trigo-web/app/src/utils/TrigoGameFrontend.ts +258 -0
  20. trigo-web/app/src/views/TrigoView.vue +1604 -0
  21. trigo-web/app/test_capture.js +23 -0
  22. trigo-web/app/vite.config.ts +42 -0
  23. trigo-web/backend/.env +3 -0
  24. trigo-web/backend/package-lock.json +1663 -0
  25. trigo-web/backend/package.json +36 -0
  26. trigo-web/backend/src/server.ts +59 -0
  27. trigo-web/backend/src/services/gameManager.ts +329 -0
  28. trigo-web/backend/src/sockets/gameSocket.ts +155 -0
  29. trigo-web/backend/tsconfig.json +34 -0
  30. trigo-web/inc/tgn/README.md +109 -0
  31. trigo-web/inc/tgn/tgn.jison +224 -0
  32. trigo-web/inc/tgn/tgn.jison.cjs +791 -0
  33. trigo-web/inc/tgn/tgnParser.ts +166 -0
  34. trigo-web/inc/trigo/ab0yz.ts +120 -0
  35. trigo-web/inc/trigo/game.ts +1137 -0
  36. trigo-web/inc/trigo/gameUtils.ts +600 -0
  37. trigo-web/inc/trigo/index.ts +5 -0
  38. trigo-web/inc/trigo/parserInit.ts +135 -0
  39. trigo-web/inc/trigo/typeAdapters.ts +110 -0
  40. trigo-web/inc/trigo/types.ts +48 -0
  41. trigo-web/inc/tsconfig.json +16 -0
  42. trigo-web/package-lock.json +0 -0
  43. trigo-web/package.json +52 -0
  44. trigo-web/public/lib/tgnParser.cjs +791 -0
  45. trigo-web/tools/README.md +152 -0
  46. trigo-web/tools/buildJisonParser.ts +74 -0
  47. trigo-web/tools/generateRandomGames.ts +465 -0
  48. trigo-web/tools/output/.gitignore +1 -0
  49. trigo-web/tsconfig.json +20 -0
  50. trigo-web/vitest.config.ts +41 -0
.dockerignore ADDED
@@ -0,0 +1,43 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Git
2
+ .git
3
+ .gitignore
4
+ .gitattributes
5
+
6
+ # Documentation
7
+ README.md
8
+ *.md
9
+
10
+ # Node modules (will be installed during build)
11
+ node_modules
12
+ **/node_modules
13
+
14
+ # Build outputs (will be generated during build)
15
+ dist
16
+ **/dist
17
+
18
+ # Test files
19
+ tests
20
+ **/tests
21
+ *.test.ts
22
+ *.test.js
23
+
24
+ # Development files
25
+ .vscode
26
+ .idea
27
+ *.log
28
+ *.lock
29
+
30
+ # Environment files
31
+ .env
32
+ .env.local
33
+ .env.*.local
34
+
35
+ # Temporary files
36
+ *.tmp
37
+ *.temp
38
+ tmp/
39
+ temp/
40
+
41
+ # OS files
42
+ .DS_Store
43
+ Thumbs.db
Dockerfile ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM node:20-slim
2
+
3
+ # Set noninteractive installation
4
+ ENV DEBIAN_FRONTEND=noninteractive
5
+
6
+ # Install build dependencies
7
+ RUN apt-get update && apt-get install -y \
8
+ curl \
9
+ git \
10
+ && rm -rf /var/lib/apt/lists/*
11
+
12
+ # Create app directory
13
+ WORKDIR /app
14
+
15
+ # Copy entire trigo-web project
16
+ COPY trigo-web/ ./
17
+
18
+ # Install all dependencies (root, app, backend)
19
+ RUN npm install && \
20
+ cd app && npm install && \
21
+ cd ../backend && npm install && \
22
+ cd ..
23
+
24
+ # Build jison parsers (required for the game)
25
+ RUN npm run build:parsers
26
+
27
+ # Build frontend (generates dist folder)
28
+ RUN cd app && npm run build
29
+
30
+ # Build backend TypeScript
31
+ RUN cd backend && npm run build
32
+
33
+ # Set environment variables for Hugging Face Spaces
34
+ ENV PORT=7860
35
+ ENV HOST=0.0.0.0
36
+ ENV NODE_ENV=production
37
+
38
+ # Expose port 7860 (required by Hugging Face Spaces)
39
+ EXPOSE 7860
40
+
41
+ # Start backend server (which will also serve frontend static files)
42
+ CMD ["npm", "run", "start:prod"]
deploy.sh ADDED
@@ -0,0 +1,61 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/bin/bash
2
+
3
+ # Trigo Hugging Face Space Deployment Script
4
+ # This script copies the trigo-web project to the HF Space repo and prepares it for deployment
5
+
6
+ set -e
7
+
8
+ echo "🚀 Trigo HF Space Deployment Script"
9
+ echo "===================================="
10
+ echo ""
11
+
12
+ # Define paths
13
+ PROJECT_ROOT="/home/camus/work/trigo"
14
+ TRIGO_WEB="$PROJECT_ROOT/trigo-web"
15
+ HF_SPACE="$PROJECT_ROOT/third_party/trigo-hfspace"
16
+
17
+ # Check if directories exist
18
+ if [ ! -d "$TRIGO_WEB" ]; then
19
+ echo "❌ Error: trigo-web directory not found at $TRIGO_WEB"
20
+ exit 1
21
+ fi
22
+
23
+ if [ ! -d "$HF_SPACE" ]; then
24
+ echo "❌ Error: HF Space directory not found at $HF_SPACE"
25
+ exit 1
26
+ fi
27
+
28
+ echo "📂 Project directory: $TRIGO_WEB"
29
+ echo "📂 HF Space directory: $HF_SPACE"
30
+ echo ""
31
+
32
+ # Clean up old trigo-web in HF space if it exists
33
+ if [ -d "$HF_SPACE/trigo-web" ]; then
34
+ echo "🧹 Cleaning up old trigo-web directory..."
35
+ rm -rf "$HF_SPACE/trigo-web"
36
+ fi
37
+
38
+ # Copy trigo-web to HF space
39
+ echo "📦 Copying trigo-web to HF Space repository..."
40
+ cp -r "$TRIGO_WEB" "$HF_SPACE/"
41
+
42
+ # Remove node_modules and dist folders (will be built in Docker)
43
+ echo "🧹 Removing node_modules and dist folders (will be rebuilt in Docker)..."
44
+ find "$HF_SPACE/trigo-web" -type d -name "node_modules" -exec rm -rf {} + 2>/dev/null || true
45
+ find "$HF_SPACE/trigo-web" -type d -name "dist" -exec rm -rf {} + 2>/dev/null || true
46
+
47
+ # Remove test files
48
+ echo "🧹 Removing test files..."
49
+ rm -rf "$HF_SPACE/trigo-web/tests" 2>/dev/null || true
50
+
51
+ echo ""
52
+ echo "✅ Files prepared successfully!"
53
+ echo ""
54
+ echo "📋 Next steps:"
55
+ echo " 1. cd $HF_SPACE"
56
+ echo " 2. git add ."
57
+ echo " 3. git commit -m 'Deploy Trigo game'"
58
+ echo " 4. git push"
59
+ echo ""
60
+ echo "🌐 After pushing, HF will automatically build and deploy your app."
61
+ echo ""
trigo-web/.prettierignore ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ node_modules
2
+ dist
3
+ build
4
+ coverage
5
+ .vscode
6
+ .idea
7
+ *.log
8
+ *.min.js
9
+ *.min.css
trigo-web/.prettierrc ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "semi": true,
3
+ "trailingComma": "none",
4
+ "singleQuote": false,
5
+ "printWidth": 100,
6
+ "tabWidth": 4,
7
+ "useTabs": true,
8
+ "bracketSpacing": true,
9
+ "arrowParens": "always",
10
+ "endOfLine": "lf",
11
+ "vueIndentScriptAndStyle": true,
12
+ "quoteProps": "as-needed",
13
+ "bracketSameLine": false,
14
+ "jsxSingleQuote": false,
15
+ "proseWrap": "preserve",
16
+ "htmlWhitespaceSensitivity": "css"
17
+ }
trigo-web/README.md ADDED
@@ -0,0 +1,227 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Trigo - 3D Go Board Game
2
+
3
+ A modern web implementation of Go played on a three-dimensional board, built with Vue 3, TypeScript, Node.js, and Three.js.
4
+
5
+ ## Features
6
+
7
+ - **3D Board**: Play Go on a 5x5x5 three-dimensional board
8
+ - **Multiplayer**: Real-time online gameplay via WebSocket
9
+ - **Single Player**: Play against AI opponents
10
+ - **Game Replay**: Review and analyze completed games
11
+ - **Modern Tech Stack**: Vue 3, TypeScript, Three.js for WebGL rendering
12
+
13
+ ## Project Structure
14
+
15
+ ```
16
+ trigo-web/
17
+ ├── app/ # Frontend application (Vue 3)
18
+ │ ├── src/
19
+ │ │ ├── components/ # Vue components
20
+ │ │ ├── views/ # Page views
21
+ │ │ ├── game/ # Game logic and 3D rendering
22
+ │ │ ├── store/ # Pinia state management
23
+ │ │ └── services/ # API and WebSocket services
24
+ │ └── package.json
25
+ ├── backend/ # Backend server (Node.js)
26
+ │ ├── src/
27
+ │ │ ├── controllers/
28
+ │ │ ├── services/ # Game logic and room management
29
+ │ │ ├── sockets/ # Socket.io handlers
30
+ │ │ └── server.ts # Main server entry
31
+ │ └── package.json
32
+ ├── inc/ # Shared code between frontend and backend
33
+ │ ├── trigo/ # Core game logic and types
34
+ │ │ ├── types.ts # TypeScript interfaces
35
+ │ │ ├── gameUtils.ts # Game utility functions (capture, Ko, territory)
36
+ │ │ ├── game.ts # TrigoGame class - main game state management
37
+ │ │ └── ab0yz.ts # TGN coordinate encoding
38
+ ├── tests/ # Test files
39
+ │ └── game/ # Game logic tests (TrigoGame)
40
+ ├── vitest.config.ts # Vitest test configuration
41
+ ├── tsconfig.json # TypeScript configuration
42
+ └── package.json # Root package.json with dev scripts
43
+ ```
44
+
45
+ ## Getting Started
46
+
47
+ ### Prerequisites
48
+
49
+ - Node.js (v18+ recommended)
50
+ - npm or yarn
51
+
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
+ ```
64
+
65
+ This will install dependencies for the root, frontend, and backend.
66
+
67
+ ### Development
68
+
69
+ Run both frontend and backend in development mode:
70
+
71
+ ```bash
72
+ npm run dev
73
+ ```
74
+
75
+ Or run them separately:
76
+
77
+ ```bash
78
+ # Frontend only (runs on http://localhost:5173)
79
+ npm run dev:app
80
+
81
+ # Backend only (runs on http://localhost:3000)
82
+ npm run dev:backend
83
+ ```
84
+
85
+ ### Building for Production
86
+
87
+ Build both frontend and backend:
88
+
89
+ ```bash
90
+ npm run build
91
+ ```
92
+
93
+ ### Testing
94
+
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
121
+ - Game rules (18 tests) - Capture, Ko rule, suicide, territory calculation
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
128
+ - `trigoGame.state.test.ts` - State persistence and callbacks
129
+
130
+ ### Code Formatting
131
+
132
+ Format all code with Prettier:
133
+
134
+ ```bash
135
+ npm run format
136
+ ```
137
+
138
+ Check formatting without making changes:
139
+
140
+ ```bash
141
+ npm run format:check
142
+ ```
143
+
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
150
+ - **Pinia** - State management
151
+ - **Vue Router** - Client-side routing
152
+ - **Socket.io Client** - WebSocket communication
153
+ - **Vite** - Fast build tool
154
+ - **SASS** - CSS preprocessor
155
+
156
+ ### Backend
157
+ - **Node.js** - JavaScript runtime
158
+ - **Express** - Web framework
159
+ - **Socket.io** - Real-time bidirectional communication
160
+ - **TypeScript** - Type-safe JavaScript
161
+ - **UUID** - Unique ID generation
162
+
163
+ ## Game Rules
164
+
165
+ Trigo extends the traditional Go game into three dimensions:
166
+
167
+ 1. The game is played on a 5x5x5 cubic board
168
+ 2. Players take turns placing stones (black and white)
169
+ 3. Stones are captured when completely surrounded in 3D space
170
+ 4. The goal is to control more territory than your opponent
171
+
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
178
+
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
185
+ - `pass` - Pass turn
186
+ - `resign` - Resign from game
187
+ - `chatMessage` - Send chat message
188
+
189
+ #### Server → Client
190
+ - `roomJoined` - Successfully joined room
191
+ - `gameUpdate` - Game state update
192
+ - `playerJoined` - Another player joined
193
+ - `playerLeft` - Player left room
194
+ - `chatMessage` - Incoming chat message
195
+ - `gameEnded` - Game finished
196
+
197
+ ## Development Guidelines
198
+
199
+ ### Code Style
200
+ - Uses Prettier for consistent formatting
201
+ - Tab indentation (following prototype style)
202
+ - Double quotes for strings
203
+ - No trailing commas
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`
210
+ 4. Commit with descriptive messages
211
+ 5. Create pull request
212
+
213
+ ## Based On
214
+
215
+ This project is a modern reimplementation of the original Trigo prototype found in `third_party/klstrigo/`, updating the technology stack while preserving the core game mechanics and 3D gameplay experience.
216
+
217
+ ## License
218
+
219
+ MIT License - See LICENSE file for details
220
+
221
+ ## Contributing
222
+
223
+ Contributions are welcome! Please read the contributing guidelines before submitting pull requests.
224
+
225
+ ## Support
226
+
227
+ For issues, questions, or suggestions, please open an issue on GitHub.
trigo-web/app/.env ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ VITE_SERVER_URL=http://localhost:3000
2
+ VITE_HOST=0.0.0.0
3
+ VITE_PORT=5173
trigo-web/app/.env.local.example ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Local environment variables (not committed to git)
2
+ # Override these values for your local development setup
3
+
4
+ # Example: To expose the dev server to your network:
5
+ # VITE_HOST=0.0.0.0
6
+
7
+ # Example: To use a different port:
8
+ # VITE_PORT=3001
9
+
10
+ # Example: To connect to a different backend:
11
+ # VITE_SERVER_URL=http://192.168.1.100:3000
trigo-web/app/.gitignore ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Logs
2
+ logs
3
+ *.log
4
+ npm-debug.log*
5
+ yarn-debug.log*
6
+ yarn-error.log*
7
+ pnpm-debug.log*
8
+ lerna-debug.log*
9
+
10
+ node_modules
11
+ dist
12
+ dist-ssr
13
+ *.local
14
+
15
+ # Editor directories and files
16
+ .vscode/*
17
+ !.vscode/extensions.json
18
+ .idea
19
+ .DS_Store
20
+ *.suo
21
+ *.ntvs*
22
+ *.njsproj
23
+ *.sln
24
+ *.sw?
25
+
26
+ # Environment files
27
+ .env.local
28
+ .env.*.local
trigo-web/app/index.html ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <link rel="icon" type="image/svg+xml" href="/vite.svg" />
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
+ <title>Trigo - 3D Go Game</title>
8
+ </head>
9
+ <body>
10
+ <div id="app"></div>
11
+ <script type="module" src="/src/main.ts"></script>
12
+ </body>
13
+ </html>
trigo-web/app/package-lock.json ADDED
The diff for this file is too large to render. See raw diff
 
trigo-web/app/package.json ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "trigo-app",
3
+ "private": true,
4
+ "version": "0.0.0",
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "vite",
8
+ "dev:host": "vite --host",
9
+ "build": "vue-tsc --noEmit && vite build",
10
+ "preview": "vite preview"
11
+ },
12
+ "dependencies": {
13
+ "pinia": "^2.1.6",
14
+ "socket.io-client": "^4.5.2",
15
+ "three": "^0.156.1",
16
+ "vue": "^3.3.4",
17
+ "vue-router": "^4.2.4"
18
+ },
19
+ "devDependencies": {
20
+ "@types/three": "^0.156.0",
21
+ "@vitejs/plugin-vue": "^5.2.4",
22
+ "sass-embedded": "^1.93.2",
23
+ "typescript": "^5.2.2",
24
+ "vite": "^5.4.21",
25
+ "vue-tsc": "^2.2.12"
26
+ }
27
+ }
trigo-web/app/src/App.vue ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ * {
11
+ margin: 0;
12
+ padding: 0;
13
+ box-sizing: border-box;
14
+ }
15
+
16
+ html,
17
+ body {
18
+ height: 100%;
19
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial,
20
+ sans-serif;
21
+ }
22
+
23
+ #app {
24
+ height: 100vh;
25
+ background: #f5f5f5;
26
+ }
27
+ </style>
trigo-web/app/src/assets/logo.png ADDED
trigo-web/app/src/main.ts ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { createApp } from "vue";
2
+ import { createPinia } from "pinia";
3
+
4
+ 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
+
12
+ 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
+ });
trigo-web/app/src/router/index.ts ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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;
trigo-web/app/src/services/trigoViewport.ts ADDED
@@ -0,0 +1,1679 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as THREE from "three";
2
+ import { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js";
3
+ import type { BoardShape } from "../../../inc/trigo";
4
+
5
+ // Color constants
6
+ const COLORS = {
7
+ // Scene colors
8
+ SCENE_BACKGROUND: 0x505055,
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,
25
+ STONE_WHITE: 0xf0f0f0,
26
+
27
+ // Stone specular highlights
28
+ STONE_BLACK_SPECULAR: 0x445577,
29
+ STONE_WHITE_SPECULAR: 0xeeeedd,
30
+
31
+ // Lighting 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
39
+ const OPACITY = {
40
+ // Chess frame opacities
41
+ FRAME_CREST: 0.64,
42
+ FRAME_SURFACE: 0.12,
43
+ FRAME_INTERIOR: 0.04,
44
+
45
+ // Grid and point opacities
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)
68
+ STONE_RADIUS: 0.28,
69
+ INTERSECTION_POINT_RADIUS: 0.16,
70
+ JOINT_RADIUS: 0.12,
71
+ JOINT_LENGTH: 0.47,
72
+ DOMAIN_CUBE_SIZE: 0.6,
73
+
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,
111
+ EMISSIVE_COLOR: [0.03, 0.32, 0.6],
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";
122
+ mesh?: THREE.Mesh;
123
+ }
124
+
125
+ export interface ViewportCallbacks {
126
+ onStoneClick?: (x: number, y: number, z: number) => void;
127
+ onPositionHover?: (x: number | null, y: number | null, z: number | null) => void;
128
+ isPositionDroppable?: (x: number, y: number, z: number) => boolean;
129
+ onInspectGroup?: (groupSize: number, liberties: number) => void;
130
+ }
131
+
132
+ export class TrigoViewport {
133
+ private canvas: HTMLCanvasElement;
134
+ private scene: THREE.Scene;
135
+ private camera: THREE.PerspectiveCamera;
136
+ private renderer: THREE.WebGLRenderer;
137
+ private controls!: OrbitControls;
138
+ private raycaster: THREE.Raycaster;
139
+ private mouse: THREE.Vector2;
140
+
141
+ private boardShape: BoardShape;
142
+ private gridSpacing: number = 2;
143
+ private gridGroup: THREE.Group;
144
+ private stonesGroup: THREE.Group;
145
+ private jointsGroup: THREE.Group;
146
+ private intersectionPoints: THREE.Group;
147
+ private domainCubesGroup: THREE.Group;
148
+ private highlightedPoint: THREE.Mesh | null = null;
149
+ private highlightedAxisPoints: THREE.Mesh[] = [];
150
+ private previewStone: THREE.Mesh | null = null;
151
+ private lastPlacedStone: { x: number; y: number; z: number } | null = null;
152
+ private hoveredPosition: { x: number; y: number; z: number } | null = null;
153
+
154
+ private stones: Map<string, Stone> = new Map();
155
+ private joints: Map<string, { X?: THREE.Mesh; Y?: THREE.Mesh; Z?: THREE.Mesh }> = new Map();
156
+ private domainCubes: Map<string, THREE.Mesh> = new Map();
157
+ private callbacks: ViewportCallbacks;
158
+
159
+ private animationId: number | null = null;
160
+ private isDestroyed: boolean = false;
161
+ private currentPlayerColor: "black" | "white" = "black";
162
+ private isGameActive: boolean = false;
163
+ private lastCameraDistance: number = 0;
164
+
165
+ // Mouse drag detection
166
+ private isMouseDown: boolean = false;
167
+ private mouseDownPosition: { x: number; y: number } | null = null;
168
+ private hasDragged: boolean = false;
169
+ private dragThreshold: number = 5; // pixels
170
+
171
+ // Inspect mode for analyzing stone groups
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
179
+ private blackDomainVisible: boolean = false;
180
+ private whiteDomainVisible: boolean = false;
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;
188
+
189
+ // Initialize Three.js components
190
+ this.scene = new THREE.Scene();
191
+ this.camera = new THREE.PerspectiveCamera(
192
+ CAMERA.FOV,
193
+ canvas.clientWidth / canvas.clientHeight,
194
+ CAMERA.NEAR,
195
+ CAMERA.FAR
196
+ );
197
+ this.renderer = new THREE.WebGLRenderer({
198
+ canvas,
199
+ antialias: true,
200
+ alpha: true
201
+ });
202
+ this.raycaster = new THREE.Raycaster();
203
+ this.mouse = new THREE.Vector2();
204
+
205
+ // Groups for organizing scene objects
206
+ this.gridGroup = new THREE.Group();
207
+ this.stonesGroup = new THREE.Group();
208
+ this.jointsGroup = new THREE.Group();
209
+ this.intersectionPoints = new THREE.Group();
210
+ this.domainCubesGroup = new THREE.Group();
211
+
212
+ this.initialize();
213
+ }
214
+
215
+ private initialize(): void {
216
+ // Setup renderer - use getBoundingClientRect for accurate CSS size
217
+ const rect = this.canvas.getBoundingClientRect();
218
+ this.renderer.setSize(rect.width, rect.height, false);
219
+ this.renderer.setPixelRatio(window.devicePixelRatio);
220
+ this.renderer.setClearColor(COLORS.SCENE_CLEAR, 1);
221
+
222
+ // Setup camera - use max dimension for distance calculation
223
+ const maxDim = Math.max(this.boardShape.x, this.boardShape.y, this.boardShape.z);
224
+ const distance = maxDim * this.gridSpacing * CAMERA.DISTANCE_MULTIPLIER;
225
+ this.camera.position.set(distance, distance * CAMERA.HEIGHT_RATIO, distance);
226
+ this.camera.lookAt(0, 0, 0);
227
+
228
+ // Setup controls
229
+ this.controls = new OrbitControls(this.camera, this.canvas);
230
+ this.controls.enableDamping = true;
231
+ this.controls.dampingFactor = 0.05;
232
+ this.controls.minDistance = 5;
233
+ this.controls.maxDistance = 100;
234
+ this.controls.maxPolarAngle = Math.PI / 2 + Math.PI / 4;
235
+ this.controls.enablePan = false; // Disable camera panning with right mouse button
236
+
237
+ // Setup scene
238
+ this.scene.background = new THREE.Color(COLORS.SCENE_BACKGROUND);
239
+ this.setupFog();
240
+ this.setupLighting();
241
+ this.createGrid();
242
+ this.createIntersectionPoints();
243
+ this.createJoints();
244
+ this.createDomainCubes();
245
+ this.createPreviewStone();
246
+
247
+ // Add groups to scene
248
+ this.scene.add(this.gridGroup);
249
+ this.scene.add(this.stonesGroup);
250
+ this.scene.add(this.jointsGroup);
251
+ this.scene.add(this.intersectionPoints);
252
+ this.scene.add(this.domainCubesGroup);
253
+
254
+ // Event listeners
255
+ this.canvas.addEventListener("mousemove", this.onMouseMove.bind(this));
256
+ this.canvas.addEventListener("mousedown", this.onMouseDown.bind(this));
257
+ this.canvas.addEventListener("mouseup", this.onMouseUp.bind(this));
258
+ this.canvas.addEventListener("click", this.onClick.bind(this));
259
+ window.addEventListener("resize", this.onWindowResize.bind(this));
260
+ window.addEventListener("keydown", this.onKeyDown.bind(this));
261
+ window.addEventListener("keyup", this.onKeyUp.bind(this));
262
+
263
+ // Start animation loop
264
+ this.animate();
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
277
+ this.scene.add(this.previewStone);
278
+ }
279
+
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 {
288
+ // Use scene background color for fog
289
+ this.scene.fog = new THREE.Fog(COLORS.SCENE_BACKGROUND, 0, 1);
290
+
291
+ // Update fog parameters based on current camera position
292
+ this.updateFog(true);
293
+ }
294
+
295
+ private highlightAxisPoints(gridX: number, gridY: number, gridZ: number): void {
296
+ // Clear previous axis highlights
297
+ this.clearAxisHighlights();
298
+
299
+ // Highlight points along the same axes
300
+ this.intersectionPoints.children.forEach((child) => {
301
+ const point = child as THREE.Mesh;
302
+ const { gridX: px, gridY: py, gridZ: pz } = point.userData;
303
+
304
+ // Check if point is on the same X, Y, or Z axis
305
+ const alignedXAxis = px === gridX;
306
+ const alignedYAxis = py === gridY;
307
+ const alignedZAxis = pz === gridZ;
308
+ const aligned = Number(alignedXAxis) + Number(alignedYAxis) + Number(alignedZAxis);
309
+
310
+ // Highlight if on one axis
311
+ if (aligned == 2) {
312
+ const material = point.material as THREE.MeshBasicMaterial;
313
+ material.color.set(COLORS.POINT_AXIS_ALIGNED);
314
+ material.opacity = OPACITY.POINT_AXIS_ALIGNED;
315
+ this.highlightedAxisPoints.push(point);
316
+ }
317
+ });
318
+ }
319
+
320
+ private clearAxisHighlights(): void {
321
+ // Reset all previously highlighted axis points
322
+ this.highlightedAxisPoints.forEach((point) => {
323
+ const material = point.material as THREE.MeshBasicMaterial;
324
+ material.color.set(COLORS.POINT_DEFAULT);
325
+ material.opacity = OPACITY.POINT_DEFAULT;
326
+ });
327
+ this.highlightedAxisPoints = [];
328
+ }
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
+ }
351
+
352
+ private createGrid(): void {
353
+ const { x: sizeX, y: sizeY, z: sizeZ } = this.boardShape;
354
+ const spacing = this.gridSpacing;
355
+ const offsetX = ((sizeX - 1) * spacing) / 2;
356
+ const offsetY = ((sizeY - 1) * spacing) / 2;
357
+ const offsetZ = ((sizeZ - 1) * spacing) / 2;
358
+
359
+ // Chess frame materials - three-tiered system from prototype
360
+ // Crest: edges/corners (most visible)
361
+ const crestMaterial = new THREE.LineBasicMaterial({
362
+ color: COLORS.FRAME_CREST,
363
+ opacity: OPACITY.FRAME_CREST,
364
+ transparent: true
365
+ });
366
+
367
+ // Surface: edges on faces (medium visibility)
368
+ const surfaceMaterial = new THREE.LineBasicMaterial({
369
+ color: COLORS.FRAME_SURFACE,
370
+ opacity: OPACITY.FRAME_SURFACE,
371
+ transparent: true
372
+ });
373
+
374
+ // Interior: inner lines (least visible)
375
+ const interiorMaterial = new THREE.LineBasicMaterial({
376
+ color: COLORS.FRAME_INTERIOR,
377
+ opacity: OPACITY.FRAME_INTERIOR,
378
+ transparent: true
379
+ });
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 = [];
396
+ for (let x = 0; x < sizeX; x++) {
397
+ points.push(
398
+ new THREE.Vector3(
399
+ x * spacing - offsetX,
400
+ y * spacing - offsetY,
401
+ z * spacing - offsetZ
402
+ )
403
+ );
404
+ }
405
+ const geometry = new THREE.BufferGeometry().setFromPoints(points);
406
+ const line = new THREE.Line(geometry, material);
407
+ this.gridGroup.add(line);
408
+ }
409
+ }
410
+
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 = [];
419
+ for (let y = 0; y < sizeY; y++) {
420
+ points.push(
421
+ new THREE.Vector3(
422
+ x * spacing - offsetX,
423
+ y * spacing - offsetY,
424
+ z * spacing - offsetZ
425
+ )
426
+ );
427
+ }
428
+ const geometry = new THREE.BufferGeometry().setFromPoints(points);
429
+ const line = new THREE.Line(geometry, material);
430
+ this.gridGroup.add(line);
431
+ }
432
+ }
433
+
434
+ // Z-axis lines (parallel to Z)
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 = [];
443
+ for (let z = 0; z < sizeZ; z++) {
444
+ points.push(
445
+ new THREE.Vector3(
446
+ x * spacing - offsetX,
447
+ y * spacing - offsetY,
448
+ z * spacing - offsetZ
449
+ )
450
+ );
451
+ }
452
+ const geometry = new THREE.BufferGeometry().setFromPoints(points);
453
+ const line = new THREE.Line(geometry, material);
454
+ this.gridGroup.add(line);
455
+ }
456
+ }
457
+ }
458
+
459
+ // Add axes helper for orientation
460
+ const maxOffset = Math.max(offsetX, offsetY, offsetZ);
461
+
462
+ if (sizeZ >= 3) {
463
+ // Show all three axes for 3D boards
464
+ const axesHelper = new THREE.AxesHelper(maxOffset * 1.2);
465
+ this.gridGroup.add(axesHelper);
466
+ } else {
467
+ // Show only X and Y axes for 2D boards (hide Z axis)
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);
491
+ }
492
+ }
493
+
494
+ private createIntersectionPoints(): void {
495
+ const { x: sizeX, y: sizeY, z: sizeZ } = this.boardShape;
496
+ const spacing = this.gridSpacing;
497
+ const offsetX = ((sizeX - 1) * spacing) / 2;
498
+ const offsetY = ((sizeY - 1) * spacing) / 2;
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++) {
506
+ for (let z = 0; z < sizeZ; z++) {
507
+ // Create a unique material for each point so they can be styled independently
508
+ const pointMaterial = new THREE.MeshBasicMaterial({
509
+ color: COLORS.POINT_DEFAULT,
510
+ opacity: OPACITY.POINT_DEFAULT,
511
+ transparent: true
512
+ });
513
+ const point = new THREE.Mesh(pointGeometry, pointMaterial);
514
+ point.position.set(
515
+ x * spacing - offsetX,
516
+ y * spacing - offsetY,
517
+ z * spacing - offsetZ
518
+ );
519
+ point.userData = { gridX: x, gridY: y, gridZ: z };
520
+ this.intersectionPoints.add(point);
521
+ }
522
+ }
523
+ }
524
+ }
525
+
526
+
527
+ private createJoints(): void {
528
+ const { x: sizeX, y: sizeY, z: sizeZ } = this.boardShape;
529
+ const spacing = this.gridSpacing;
530
+ const offsetX = ((sizeX - 1) * spacing) / 2;
531
+ const offsetY = ((sizeY - 1) * spacing) / 2;
532
+ const offsetZ = ((sizeZ - 1) * spacing) / 2;
533
+
534
+ // Joint dimensions from prototype: scale (0.06, 0.47, 0.06)
535
+ const jointRadius = SIZES.JOINT_RADIUS;
536
+ const jointLength = SIZES.JOINT_LENGTH * spacing; // Scale by grid spacing
537
+
538
+ // Create joints for each grid position
539
+ for (let x = 0; x < sizeX; x++) {
540
+ for (let y = 0; y < sizeY; y++) {
541
+ for (let z = 0; z < sizeZ; z++) {
542
+ const key = this.getStoneKey(x, y, z);
543
+ const jointNodes: { X?: THREE.Mesh; Y?: THREE.Mesh; Z?: THREE.Mesh } = {};
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,
551
+ specular: COLORS.STONE_BLACK_SPECULAR
552
+ });
553
+ const joint = new THREE.Mesh(geometry, material);
554
+
555
+ // Position at midpoint between stones, rotate to align with X-axis
556
+ joint.position.set(
557
+ (x + 0.5) * spacing - offsetX,
558
+ y * spacing - offsetY,
559
+ z * spacing - offsetZ
560
+ );
561
+ joint.rotation.set(0, 0, Math.PI / 2); // Rotate to X-axis
562
+
563
+ joint.visible = false; // Hidden by default
564
+ this.jointsGroup.add(joint);
565
+ jointNodes.X = joint;
566
+ }
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,
574
+ specular: COLORS.STONE_BLACK_SPECULAR
575
+ });
576
+ const joint = new THREE.Mesh(geometry, material);
577
+
578
+ // Position at midpoint between stones (Y-axis is already aligned)
579
+ joint.position.set(
580
+ x * spacing - offsetX,
581
+ (y + 0.5) * spacing - offsetY,
582
+ z * spacing - offsetZ
583
+ );
584
+ // No rotation needed for Y-axis (cylinder default orientation)
585
+
586
+ joint.visible = false; // Hidden by default
587
+ this.jointsGroup.add(joint);
588
+ jointNodes.Y = joint;
589
+ }
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,
597
+ specular: COLORS.STONE_BLACK_SPECULAR
598
+ });
599
+ const joint = new THREE.Mesh(geometry, material);
600
+
601
+ // Position at midpoint between stones, rotate to align with Z-axis
602
+ joint.position.set(
603
+ x * spacing - offsetX,
604
+ y * spacing - offsetY,
605
+ (z + 0.5) * spacing - offsetZ
606
+ );
607
+ joint.rotation.set(Math.PI / 2, 0, 0); // Rotate to Z-axis
608
+
609
+ joint.visible = false; // Hidden by default
610
+ this.jointsGroup.add(joint);
611
+ jointNodes.Z = joint;
612
+ }
613
+
614
+ this.joints.set(key, jointNodes);
615
+ }
616
+ }
617
+ }
618
+ }
619
+
620
+
621
+ private createDomainCubes(): void {
622
+ const { x: sizeX, y: sizeY, z: sizeZ } = this.boardShape;
623
+ const spacing = this.gridSpacing;
624
+ const offsetX = ((sizeX - 1) * spacing) / 2;
625
+ const offsetY = ((sizeY - 1) * spacing) / 2;
626
+ const offsetZ = ((sizeZ - 1) * spacing) / 2;
627
+
628
+ // Domain cube size from prototype: scale 0.6
629
+ const cubeSize = SIZES.DOMAIN_CUBE_SIZE * spacing;
630
+
631
+ // Create domain cubes for each grid position
632
+ for (let x = 0; x < sizeX; x++) {
633
+ for (let y = 0; y < sizeY; y++) {
634
+ for (let z = 0; z < sizeZ; z++) {
635
+ const key = this.getStoneKey(x, y, z);
636
+
637
+ // Create cube geometry
638
+ const geometry = new THREE.BoxGeometry(cubeSize, cubeSize, cubeSize);
639
+
640
+ // Create material (will be updated dynamically based on domain type)
641
+ const material = new THREE.MeshBasicMaterial({
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);
649
+
650
+ // Position at grid intersection
651
+ cube.position.set(
652
+ x * spacing - offsetX,
653
+ y * spacing - offsetY,
654
+ z * spacing - offsetZ
655
+ );
656
+
657
+ cube.visible = false; // Hidden by default
658
+ this.domainCubesGroup.add(cube);
659
+ this.domainCubes.set(key, cube);
660
+ }
661
+ }
662
+ }
663
+ }
664
+
665
+ private getStoneKey(x: number, y: number, z: number): string {
666
+ return `${x},${y},${z}`;
667
+ }
668
+
669
+ public addStone(x: number, y: number, z: number, color: "black" | "white"): void {
670
+ const key = this.getStoneKey(x, y, z);
671
+ if (this.stones.has(key)) {
672
+ console.warn(`Stone already exists at (${x}, ${y}, ${z})`);
673
+ return;
674
+ }
675
+
676
+ // Hide preview stone immediately when adding a new stone
677
+ this.hidePreviewStone();
678
+
679
+ const spacing = this.gridSpacing;
680
+ const offsetX = ((this.boardShape.x - 1) * spacing) / 2;
681
+ const offsetY = ((this.boardShape.y - 1) * spacing) / 2;
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,
689
+ specular: color === "black" ? COLORS.STONE_BLACK_SPECULAR : COLORS.STONE_WHITE_SPECULAR,
690
+ emissive: 0x000000, // Will be animated for last placed stone
691
+ emissiveIntensity: 0
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 },
703
+ color,
704
+ mesh: stoneMesh
705
+ };
706
+
707
+ this.stones.set(key, stone);
708
+ this.stonesGroup.add(stoneMesh);
709
+
710
+ // Clear emissive from previous last placed stone
711
+ if (this.lastPlacedStone) {
712
+ const prevKey = this.getStoneKey(
713
+ this.lastPlacedStone.x,
714
+ this.lastPlacedStone.y,
715
+ this.lastPlacedStone.z
716
+ );
717
+ const prevStone = this.stones.get(prevKey);
718
+ if (prevStone && prevStone.mesh) {
719
+ const prevMaterial = prevStone.mesh.material as THREE.MeshPhongMaterial;
720
+ prevMaterial.emissive.set(0x000000);
721
+ prevMaterial.emissiveIntensity = 0;
722
+ }
723
+ }
724
+
725
+ // Track this as the last placed stone
726
+ this.lastPlacedStone = { x, y, z };
727
+
728
+ // Update joints to show connections
729
+ this.refreshJoints();
730
+ }
731
+
732
+ public removeStone(x: number, y: number, z: number): void {
733
+ const key = this.getStoneKey(x, y, z);
734
+ const stone = this.stones.get(key);
735
+
736
+ if (stone && stone.mesh) {
737
+ this.stonesGroup.remove(stone.mesh);
738
+ stone.mesh.geometry.dispose();
739
+ if (stone.mesh.material instanceof THREE.Material) {
740
+ stone.mesh.material.dispose();
741
+ }
742
+ this.stones.delete(key);
743
+
744
+ // Update joints after removing stone
745
+ this.refreshJoints();
746
+ }
747
+ }
748
+
749
+ public clearBoard(): void {
750
+ // Remove all stones
751
+ this.stones.forEach((stone) => {
752
+ if (stone.mesh) {
753
+ this.stonesGroup.remove(stone.mesh);
754
+ stone.mesh.geometry.dispose();
755
+ if (stone.mesh.material instanceof THREE.Material) {
756
+ stone.mesh.material.dispose();
757
+ }
758
+ }
759
+ });
760
+ this.stones.clear();
761
+ this.lastPlacedStone = null;
762
+
763
+ // Hide all joints
764
+ this.refreshJoints();
765
+ }
766
+
767
+ public hasStone(x: number, y: number, z: number): boolean {
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
+
775
+ for (let x = 0; x < sizeX; x++) {
776
+ for (let y = 0; y < sizeY; y++) {
777
+ for (let z = 0; z < sizeZ; z++) {
778
+ const key = this.getStoneKey(x, y, z);
779
+ const jointNodes = this.joints.get(key);
780
+
781
+ if (!jointNodes) continue;
782
+
783
+ const centerStone = this.stones.get(key);
784
+
785
+ // X-axis joint: check if current and (x+1) have same color
786
+ if (jointNodes.X) {
787
+ if (centerStone && x + 1 < sizeX) {
788
+ const nextKey = this.getStoneKey(x + 1, y, z);
789
+ const nextStone = this.stones.get(nextKey);
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;
799
+ } else {
800
+ jointNodes.X.visible = false;
801
+ }
802
+ } else {
803
+ jointNodes.X.visible = false;
804
+ }
805
+ }
806
+
807
+ // Y-axis joint: check if current and (y+1) have same color
808
+ if (jointNodes.Y) {
809
+ if (centerStone && y + 1 < sizeY) {
810
+ const nextKey = this.getStoneKey(x, y + 1, z);
811
+ const nextStone = this.stones.get(nextKey);
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;
821
+ } else {
822
+ jointNodes.Y.visible = false;
823
+ }
824
+ } else {
825
+ jointNodes.Y.visible = false;
826
+ }
827
+ }
828
+
829
+ // Z-axis joint: check if current and (z+1) have same color
830
+ if (jointNodes.Z) {
831
+ if (centerStone && z + 1 < sizeZ) {
832
+ const nextKey = this.getStoneKey(x, y, z + 1);
833
+ const nextStone = this.stones.get(nextKey);
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;
843
+ } else {
844
+ jointNodes.Z.visible = false;
845
+ }
846
+ } else {
847
+ jointNodes.Z.visible = false;
848
+ }
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;
856
+
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) {
864
+ const leftKey = this.getStoneKey(x - 1, y, z);
865
+ const leftStone = this.stones.get(leftKey);
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;
876
+ }
877
+ }
878
+ }
879
+
880
+ // Check -Y direction (bottom neighbor)
881
+ if (y > 0) {
882
+ const bottomKey = this.getStoneKey(x, y - 1, z);
883
+ const bottomStone = this.stones.get(bottomKey);
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;
894
+ }
895
+ }
896
+ }
897
+
898
+ // Check -Z direction (back neighbor)
899
+ if (z > 0) {
900
+ const backKey = this.getStoneKey(x, y, z - 1);
901
+ const backStone = this.stones.get(backKey);
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;
912
+ }
913
+ }
914
+ }
915
+
916
+ // Check +X direction (right neighbor)
917
+ if (x + 1 < sizeX && jointNodes.X) {
918
+ const rightKey = this.getStoneKey(x + 1, y, z);
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;
928
+ }
929
+ }
930
+
931
+ // Check +Y direction (top neighbor)
932
+ if (y + 1 < sizeY && jointNodes.Y) {
933
+ const topKey = this.getStoneKey(x, y + 1, z);
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;
943
+ }
944
+ }
945
+
946
+ // Check +Z direction (front neighbor)
947
+ if (z + 1 < sizeZ && jointNodes.Z) {
948
+ const frontKey = this.getStoneKey(x, y, z + 1);
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;
958
+ }
959
+ }
960
+ }
961
+ }
962
+ }
963
+ }
964
+ }
965
+
966
+
967
+ public setCurrentPlayer(color: "black" | "white"): void {
968
+ this.currentPlayerColor = color;
969
+ this.updatePreviewStoneColor();
970
+ }
971
+
972
+ public setGameActive(active: boolean): void {
973
+ this.isGameActive = active;
974
+ }
975
+
976
+
977
+ public hidePreviewStone(): void {
978
+ if (this.previewStone) {
979
+ this.previewStone.visible = false;
980
+ }
981
+ this.hoveredPosition = null;
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;
993
+ prevMaterial.emissive.set(0x000000);
994
+ prevMaterial.emissiveIntensity = 0;
995
+ }
996
+ }
997
+
998
+ // Set new last placed stone
999
+ if (x !== null && y !== null && z !== null) {
1000
+ this.lastPlacedStone = { x, y, z };
1001
+ } else {
1002
+ this.lastPlacedStone = null;
1003
+ }
1004
+ }
1005
+
1006
+
1007
+ // Domain visibility methods (for territory display)
1008
+ public setBlackDomainVisible(visible: boolean): void {
1009
+ if (this.blackDomainVisible !== visible) {
1010
+ this.blackDomainVisible = visible;
1011
+ this.refreshDomainVisualization();
1012
+ }
1013
+ }
1014
+
1015
+ public setWhiteDomainVisible(visible: boolean): void {
1016
+ if (this.whiteDomainVisible !== visible) {
1017
+ this.whiteDomainVisible = visible;
1018
+ this.refreshDomainVisualization();
1019
+ }
1020
+ }
1021
+
1022
+ public setDomainData(blackDomain: Set<string> | null, whiteDomain: Set<string> | null): void {
1023
+ this.blackDomain = blackDomain;
1024
+ this.whiteDomain = whiteDomain;
1025
+ this.refreshDomainVisualization();
1026
+ }
1027
+
1028
+ private refreshDomainVisualization(): void {
1029
+ // In inspect mode, air patch takes priority over domain visualization
1030
+ if (this.inspectMode && this.airPatch) {
1031
+ this.updateDomainCubesVisualization(null, null);
1032
+ } else {
1033
+ // Normal mode: show domains based on visibility flags
1034
+ const black = this.blackDomainVisible ? this.blackDomain : null;
1035
+ const white = this.whiteDomainVisible ? this.whiteDomain : null;
1036
+ this.updateDomainCubesVisualization(black, white);
1037
+ }
1038
+ }
1039
+
1040
+ public showDomainCubes(blackDomain: Set<string> | null, whiteDomain: Set<string> | null): void {
1041
+ this.setDomainData(blackDomain, whiteDomain);
1042
+ this.setBlackDomainVisible(true);
1043
+ this.setWhiteDomainVisible(true);
1044
+ }
1045
+
1046
+ public hideDomainCubes(): void {
1047
+ this.setBlackDomainVisible(false);
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
+
1057
+ // Clear existing grid, points, and joints
1058
+ this.gridGroup.clear();
1059
+ this.intersectionPoints.clear();
1060
+ this.jointsGroup.clear();
1061
+ this.joints.clear();
1062
+ this.domainCubesGroup.clear();
1063
+ this.domainCubes.clear();
1064
+ this.clearBoard();
1065
+
1066
+ // Recreate grid, points, and joints
1067
+ this.createGrid();
1068
+ this.createIntersectionPoints();
1069
+ this.createJoints();
1070
+ this.createDomainCubes();
1071
+
1072
+ // Update fog for new board size
1073
+ this.setupFog();
1074
+
1075
+ // Adjust camera position
1076
+ const maxDim = Math.max(shape.x, shape.y, shape.z);
1077
+ const distance = maxDim * this.gridSpacing * CAMERA.DISTANCE_MULTIPLIER;
1078
+ this.camera.position.set(distance, distance * CAMERA.HEIGHT_RATIO, distance);
1079
+ this.camera.lookAt(0, 0, 0);
1080
+ }
1081
+
1082
+ private onMouseDown(event: MouseEvent): void {
1083
+ // Handle middle button (button 1) for inspect mode
1084
+ if (event.button === 1) {
1085
+ event.preventDefault();
1086
+ this.inspectMode = true;
1087
+ this.updateHighlightedGroup(event);
1088
+ this.updateStoneOpacity();
1089
+ return;
1090
+ }
1091
+
1092
+ // Handle left button (button 0)
1093
+ if (event.button === 0) {
1094
+ this.isMouseDown = true;
1095
+ this.mouseDownPosition = { x: event.clientX, y: event.clientY };
1096
+ this.hasDragged = false;
1097
+
1098
+ // Exit inspect mode on left click
1099
+ if (this.inspectMode && !this.ctrlKeyDown) {
1100
+ this.inspectMode = false;
1101
+ this.highlightedGroup = null;
1102
+ this.airPatch = null;
1103
+ this.updateStoneOpacity();
1104
+ // Clear tooltip by calling callback with 0, 0
1105
+ if (this.callbacks.onInspectGroup) {
1106
+ this.callbacks.onInspectGroup(0, 0);
1107
+ }
1108
+ }
1109
+ }
1110
+ }
1111
+
1112
+ private onMouseUp(event: MouseEvent): void {
1113
+ // Handle middle button release
1114
+ if (event.button === 1) {
1115
+ event.preventDefault();
1116
+ if (this.inspectMode && !this.ctrlKeyDown) {
1117
+ this.inspectMode = false;
1118
+ this.highlightedGroup = null;
1119
+ this.airPatch = null;
1120
+ this.updateStoneOpacity();
1121
+ // Clear tooltip by calling callback with 0, 0
1122
+ if (this.callbacks.onInspectGroup) {
1123
+ this.callbacks.onInspectGroup(0, 0);
1124
+ }
1125
+ }
1126
+ return;
1127
+ }
1128
+
1129
+ // Handle left button release
1130
+ if (event.button === 0) {
1131
+ this.isMouseDown = false;
1132
+ this.mouseDownPosition = null;
1133
+ // Note: hasDragged will be reset on next mousedown
1134
+ }
1135
+ }
1136
+
1137
+ private onMouseMove(event: MouseEvent): void {
1138
+ // Store last mouse event for Ctrl key inspection
1139
+ this.lastMouseEvent = event;
1140
+
1141
+ // If Ctrl is held and inspect mode is active, update highlighted group
1142
+ if (this.ctrlKeyDown && this.inspectMode) {
1143
+ this.updateHighlightedGroup(event);
1144
+ this.updateStoneOpacity();
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;
1152
+ }
1153
+ this.hoveredPosition = null;
1154
+ this.refreshJoints();
1155
+ // Clear highlights when mouse is outside
1156
+ if (this.highlightedPoint) {
1157
+ const material = this.highlightedPoint.material as THREE.MeshBasicMaterial;
1158
+ material.color.set(COLORS.POINT_DEFAULT);
1159
+ material.opacity = OPACITY.POINT_DEFAULT;
1160
+ this.highlightedPoint = null;
1161
+ }
1162
+ this.clearAxisHighlights();
1163
+ return;
1164
+ }
1165
+
1166
+ // Check if we're dragging
1167
+ if (this.isMouseDown && this.mouseDownPosition) {
1168
+ const dx = event.clientX - this.mouseDownPosition.x;
1169
+ const dy = event.clientY - this.mouseDownPosition.y;
1170
+ const distance = Math.sqrt(dx * dx + dy * dy);
1171
+
1172
+ if (distance > this.dragThreshold) {
1173
+ this.hasDragged = true;
1174
+ }
1175
+ }
1176
+
1177
+ // Hide preview stone if dragging
1178
+ if (this.isMouseDown || this.hasDragged) {
1179
+ if (this.previewStone) {
1180
+ this.previewStone.visible = false;
1181
+ }
1182
+ this.hoveredPosition = null;
1183
+ this.refreshJoints();
1184
+ // Clear highlights when dragging
1185
+ if (this.highlightedPoint) {
1186
+ const material = this.highlightedPoint.material as THREE.MeshBasicMaterial;
1187
+ material.color.set(COLORS.POINT_DEFAULT);
1188
+ material.opacity = OPACITY.POINT_DEFAULT;
1189
+ this.highlightedPoint = null;
1190
+ }
1191
+ this.clearAxisHighlights();
1192
+ return; // Don't process hover logic when dragging
1193
+ }
1194
+
1195
+ const rect = this.canvas.getBoundingClientRect();
1196
+ this.mouse.x = ((event.clientX - rect.left) / rect.width) * 2 - 1;
1197
+ this.mouse.y = -((event.clientY - rect.top) / rect.height) * 2 + 1;
1198
+
1199
+ // Raycast to find intersection points
1200
+ this.raycaster.setFromCamera(this.mouse, this.camera);
1201
+ const intersects = this.raycaster.intersectObjects(this.intersectionPoints.children);
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
+
1210
+ // Clear previous axis highlights
1211
+ this.clearAxisHighlights();
1212
+
1213
+ if (intersects.length > 0) {
1214
+ const intersect = intersects[0];
1215
+ const point = intersect.object as THREE.Mesh;
1216
+ const { gridX, gridY, gridZ } = point.userData;
1217
+
1218
+ // Check if there's already a stone at this position
1219
+ if (!this.hasStone(gridX, gridY, gridZ)) {
1220
+ // Check if position is droppable using game logic validation
1221
+ const isDroppable =
1222
+ this.isGameActive &&
1223
+ (!this.callbacks.isPositionDroppable ||
1224
+ this.callbacks.isPositionDroppable(gridX, gridY, gridZ));
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
+
1232
+ // Highlight axis-aligned points
1233
+ this.highlightAxisPoints(gridX, gridY, gridZ);
1234
+
1235
+ // Show preview stone only at droppable positions
1236
+ if (this.previewStone && isDroppable) {
1237
+ const spacing = this.gridSpacing;
1238
+ const offsetX = ((this.boardShape.x - 1) * spacing) / 2;
1239
+ const offsetY = ((this.boardShape.y - 1) * spacing) / 2;
1240
+ const offsetZ = ((this.boardShape.z - 1) * spacing) / 2;
1241
+
1242
+ this.previewStone.position.set(
1243
+ gridX * spacing - offsetX,
1244
+ gridY * spacing - offsetY,
1245
+ gridZ * spacing - offsetZ
1246
+ );
1247
+ this.previewStone.visible = true;
1248
+
1249
+ // Update hovered position and refresh joints for preview
1250
+ this.hoveredPosition = { x: gridX, y: gridY, z: gridZ };
1251
+ this.refreshJoints();
1252
+ } else if (this.previewStone) {
1253
+ // Hide preview stone if position is not droppable
1254
+ this.previewStone.visible = false;
1255
+ this.hoveredPosition = null;
1256
+ this.refreshJoints();
1257
+ }
1258
+
1259
+ if (this.callbacks.onPositionHover) {
1260
+ this.callbacks.onPositionHover(gridX, gridY, gridZ);
1261
+ }
1262
+ } else {
1263
+ // Hide preview stone if position is occupied
1264
+ if (this.previewStone) {
1265
+ this.previewStone.visible = false;
1266
+ }
1267
+ this.hoveredPosition = null;
1268
+ this.refreshJoints();
1269
+ if (this.callbacks.onPositionHover) {
1270
+ this.callbacks.onPositionHover(null, null, null);
1271
+ }
1272
+ }
1273
+ } else {
1274
+ // Hide preview stone when not hovering over grid
1275
+ if (this.previewStone) {
1276
+ this.previewStone.visible = false;
1277
+ }
1278
+ this.hoveredPosition = null;
1279
+ this.refreshJoints();
1280
+ if (this.callbacks.onPositionHover) {
1281
+ this.callbacks.onPositionHover(null, null, null);
1282
+ }
1283
+ }
1284
+
1285
+ // Only show pointer cursor when game is active and hovering over valid position
1286
+ const canPlaceStone = intersects.length > 0 && this.isGameActive;
1287
+ this.canvas.style.cursor = canPlaceStone ? "pointer" : "default";
1288
+ }
1289
+
1290
+ private onClick(event: MouseEvent): void {
1291
+ // Don't place stone if we've been dragging
1292
+ if (this.hasDragged) {
1293
+ this.hasDragged = false; // Reset for next interaction
1294
+ return;
1295
+ }
1296
+
1297
+ const rect = this.canvas.getBoundingClientRect();
1298
+ this.mouse.x = ((event.clientX - rect.left) / rect.width) * 2 - 1;
1299
+ this.mouse.y = -((event.clientY - rect.top) / rect.height) * 2 + 1;
1300
+
1301
+ this.raycaster.setFromCamera(this.mouse, this.camera);
1302
+ const intersects = this.raycaster.intersectObjects(this.intersectionPoints.children);
1303
+
1304
+ if (intersects.length > 0) {
1305
+ const intersect = intersects[0];
1306
+ const point = intersect.object as THREE.Mesh;
1307
+ const { gridX, gridY, gridZ } = point.userData;
1308
+
1309
+ if (!this.hasStone(gridX, gridY, gridZ)) {
1310
+ if (this.callbacks.onStoneClick) {
1311
+ this.callbacks.onStoneClick(gridX, gridY, gridZ);
1312
+ }
1313
+ }
1314
+ }
1315
+ }
1316
+
1317
+ private onWindowResize(): void {
1318
+ if (this.isDestroyed) return;
1319
+
1320
+ // Use getBoundingClientRect to get actual CSS size
1321
+ const rect = this.canvas.getBoundingClientRect();
1322
+ const width = rect.width;
1323
+ const height = rect.height;
1324
+
1325
+ this.camera.aspect = width / height;
1326
+ this.camera.updateProjectionMatrix();
1327
+ // Use false as third parameter to prevent updating canvas style
1328
+ this.renderer.setSize(width, height, false);
1329
+ }
1330
+
1331
+ private animate(): void {
1332
+ if (this.isDestroyed) return;
1333
+
1334
+ this.animationId = requestAnimationFrame(() => this.animate());
1335
+
1336
+ // Update fog based on camera distance
1337
+ this.updateFog();
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
+
1345
+ this.controls.update();
1346
+ this.renderer.render(this.scene, this.camera);
1347
+ }
1348
+
1349
+ private updateFog(forceUpdate: boolean = false): void {
1350
+ if (!this.scene.fog) return;
1351
+
1352
+ // Get current camera distance from origin
1353
+ const cameraDistance = this.camera.position.length();
1354
+
1355
+ // Only update if camera distance changed (unless forced)
1356
+ if (!forceUpdate && Math.abs(cameraDistance - this.lastCameraDistance) < 0.01) return;
1357
+
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
1365
+ const fog = this.scene.fog as THREE.Fog;
1366
+ fog.near = Math.max(FOG.MIN_NEAR, cameraDistance - boardDiagonal * FOG.NEAR_FACTOR);
1367
+ fog.far = cameraDistance + boardDiagonal * FOG.FAR_FACTOR;
1368
+ }
1369
+
1370
+ private updateLastStoneHighlight(): void {
1371
+ if (!this.lastPlacedStone) return;
1372
+
1373
+ // Flicker function similar to prototype: sine wave animation
1374
+ const time = Date.now();
1375
+ const flicker = Math.sin(time * SHINING.FLICKER_SPEED) / 2 + 0.5;
1376
+
1377
+ // Get the last placed stone
1378
+ const key = this.getStoneKey(
1379
+ this.lastPlacedStone.x,
1380
+ this.lastPlacedStone.y,
1381
+ this.lastPlacedStone.z
1382
+ );
1383
+ const stone = this.stones.get(key);
1384
+
1385
+ if (stone && stone.mesh) {
1386
+ const material = stone.mesh.material as THREE.MeshPhongMaterial;
1387
+
1388
+ // Cyan/blue glow color (matching prototype)
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;
1397
+ material.emissiveIntensity = intensity;
1398
+ }
1399
+ }
1400
+
1401
+ private updateHighlightedGroup(event: MouseEvent): void {
1402
+ // Find the stone group under the mouse cursor
1403
+ const rect = this.canvas.getBoundingClientRect();
1404
+ const mouse = new THREE.Vector2();
1405
+ mouse.x = ((event.clientX - rect.left) / rect.width) * 2 - 1;
1406
+ mouse.y = -((event.clientY - rect.top) / rect.height) * 2 + 1;
1407
+
1408
+ // Raycast to find clicked stone
1409
+ this.raycaster.setFromCamera(mouse, this.camera);
1410
+ const intersects = this.raycaster.intersectObjects(this.stonesGroup.children);
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) {
1418
+ clickedPosition = stone.position;
1419
+ break;
1420
+ }
1421
+ }
1422
+
1423
+ if (clickedPosition) {
1424
+ // Find the connected group using flood fill
1425
+ this.highlightedGroup = this.findConnectedGroup(clickedPosition);
1426
+
1427
+ // Calculate liberties for the group
1428
+ const libertiesResult = this.calculateLiberties(this.highlightedGroup);
1429
+ this.airPatch = libertiesResult.positions;
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 {
1437
+ this.highlightedGroup = null;
1438
+ this.airPatch = null;
1439
+ // Clear inspect info
1440
+ if (this.callbacks.onInspectGroup) {
1441
+ this.callbacks.onInspectGroup(0, 0);
1442
+ }
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);
1450
+
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) {
1458
+ const pos = queue.shift()!;
1459
+ const key = this.getStoneKey(pos.x, pos.y, pos.z);
1460
+
1461
+ if (visited.has(key)) continue;
1462
+ visited.add(key);
1463
+
1464
+ const stone = this.stones.get(key);
1465
+ if (!stone || stone.color !== color) continue;
1466
+
1467
+ group.add(key);
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);
1487
+ }
1488
+ }
1489
+ }
1490
+ }
1491
+
1492
+ return group;
1493
+ }
1494
+
1495
+ private calculateLiberties(group: Set<string>): { count: number; positions: Set<string> } {
1496
+ const liberties = new Set<string>();
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)) {
1521
+ liberties.add(neighborKey);
1522
+ }
1523
+ }
1524
+ }
1525
+ }
1526
+
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) => {
1534
+ if (!stone.mesh) return;
1535
+ const material = stone.mesh.material as THREE.MeshPhongMaterial;
1536
+
1537
+ if (this.inspectMode && this.highlightedGroup) {
1538
+ // Dim stones not in highlighted group
1539
+ if (this.highlightedGroup.has(key)) {
1540
+ material.opacity = 1.0;
1541
+ material.transparent = false;
1542
+ } else {
1543
+ material.opacity = OPACITY.DIMMED;
1544
+ material.transparent = true;
1545
+ }
1546
+ } else {
1547
+ // Normal opacity
1548
+ material.opacity = 1.0;
1549
+ material.transparent = false;
1550
+ }
1551
+ });
1552
+
1553
+ // Update domain cubes first (respects visibility flags and inspect mode)
1554
+ this.refreshDomainVisualization();
1555
+
1556
+ // Update intersection point colors to show air patch (liberties)
1557
+ // This must come after domain cubes so it can check if cubes are visible
1558
+ this.updateAirPatchVisualization();
1559
+ }
1560
+
1561
+
1562
+ private updateAirPatchVisualization(): void {
1563
+ // Reset all intersection points to default color
1564
+ this.intersectionPoints.children.forEach((child) => {
1565
+ const point = child as THREE.Mesh;
1566
+ const material = point.material as THREE.MeshBasicMaterial;
1567
+ const { gridX, gridY, gridZ } = point.userData;
1568
+ const key = this.getStoneKey(gridX, gridY, gridZ);
1569
+
1570
+ // Check if domain cube is visible at this position
1571
+ const domainCube = this.domainCubes.get(key);
1572
+ const hasDomainCube = domainCube && domainCube.visible;
1573
+
1574
+ // Check if this position is a liberty (air patch)
1575
+ if (this.inspectMode && this.airPatch && this.airPatch.has(key)) {
1576
+ if (hasDomainCube) {
1577
+ // Hide intersection point when domain cube is visible
1578
+ point.visible = false;
1579
+ } else {
1580
+ // Show liberty highlight when no domain cube
1581
+ point.visible = true;
1582
+ material.color.set(COLORS.POINT_AIR_PATCH);
1583
+ material.opacity = OPACITY.POINT_AIR_PATCH;
1584
+ }
1585
+ } else if (!this.stones.has(key)) {
1586
+ // Empty position not in air patch - reset to default
1587
+ point.visible = true;
1588
+ material.color.set(COLORS.POINT_DEFAULT);
1589
+ material.opacity = OPACITY.POINT_DEFAULT;
1590
+ }
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;
1599
+
1600
+ // Priority: Air patch > Black domain > White domain > Hidden
1601
+ if (this.inspectMode && this.airPatch && this.airPatch.has(key)) {
1602
+ // Show air patch (liberty) with green color
1603
+ material.color.set(COLORS.POINT_AIR_PATCH);
1604
+ material.opacity = OPACITY.POINT_AIR_PATCH;
1605
+ cube.visible = true;
1606
+ } else if (blackDomain && blackDomain.has(key)) {
1607
+ // Show black territory
1608
+ material.color.set(COLORS.STONE_BLACK);
1609
+ material.opacity = OPACITY.DOMAIN_BLACK;
1610
+ cube.visible = true;
1611
+ } else if (whiteDomain && whiteDomain.has(key)) {
1612
+ // Show white territory
1613
+ material.color.set(COLORS.STONE_WHITE);
1614
+ material.opacity = OPACITY.DOMAIN_WHITE;
1615
+ cube.visible = true;
1616
+ } else {
1617
+ // Hide cube
1618
+ cube.visible = false;
1619
+ }
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) {
1627
+ this.ctrlKeyDown = true;
1628
+ this.inspectMode = true;
1629
+ // Update highlighted group based on last mouse position if available
1630
+ if (this.lastMouseEvent) {
1631
+ this.updateHighlightedGroup(this.lastMouseEvent);
1632
+ }
1633
+ this.updateStoneOpacity();
1634
+ }
1635
+ }
1636
+
1637
+ private onKeyUp(event: KeyboardEvent): void {
1638
+ // Ctrl key release
1639
+ if (!event.ctrlKey && !event.metaKey) {
1640
+ this.ctrlKeyDown = false;
1641
+ if (this.inspectMode) {
1642
+ this.inspectMode = false;
1643
+ this.highlightedGroup = null;
1644
+ this.airPatch = null;
1645
+ this.updateStoneOpacity();
1646
+ // Clear tooltip by calling callback with 0, 0
1647
+ if (this.callbacks.onInspectGroup) {
1648
+ this.callbacks.onInspectGroup(0, 0);
1649
+ }
1650
+ }
1651
+ }
1652
+ }
1653
+
1654
+
1655
+ public destroy(): void {
1656
+ this.isDestroyed = true;
1657
+
1658
+ // Cancel animation
1659
+ if (this.animationId !== null) {
1660
+ cancelAnimationFrame(this.animationId);
1661
+ }
1662
+
1663
+ // Remove event listeners
1664
+ this.canvas.removeEventListener("mousemove", this.onMouseMove.bind(this));
1665
+ this.canvas.removeEventListener("mousedown", this.onMouseDown.bind(this));
1666
+ this.canvas.removeEventListener("mouseup", this.onMouseUp.bind(this));
1667
+ this.canvas.removeEventListener("click", this.onClick.bind(this));
1668
+ window.removeEventListener("resize", this.onWindowResize.bind(this));
1669
+ window.removeEventListener("keydown", this.onKeyDown.bind(this));
1670
+ window.removeEventListener("keyup", this.onKeyUp.bind(this));
1671
+
1672
+ // Dispose of Three.js resources
1673
+ this.clearBoard();
1674
+ this.gridGroup.clear();
1675
+ this.intersectionPoints.clear();
1676
+ this.renderer.dispose();
1677
+ this.controls.dispose();
1678
+ }
1679
+ }
trigo-web/app/src/stores/gameStore.ts ADDED
@@ -0,0 +1,310 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { defineStore } from "pinia";
2
+ import { TrigoGameFrontend } from "../utils/TrigoGameFrontend";
3
+ import type {
4
+ Stone,
5
+ BoardShape,
6
+ Player,
7
+ Move,
8
+ Position,
9
+ GameConfig,
10
+ TerritoryResult
11
+ } from "../../../inc/trigo";
12
+
13
+
14
+ /**
15
+ * Game Store State
16
+ *
17
+ * Refactored to use TrigoGameFrontend as the single source of truth
18
+ */
19
+ export interface GameState {
20
+ game: TrigoGameFrontend;
21
+ config: GameConfig;
22
+ }
23
+
24
+
25
+ /**
26
+ * Pinia store for game state management
27
+ *
28
+ * Now delegates all game logic to TrigoGameFrontend
29
+ */
30
+ export const useGameStore = defineStore("game", {
31
+ state: (): GameState => ({
32
+ game: new TrigoGameFrontend({ x: 5, y: 5, z: 5 }),
33
+ config: {
34
+ boardShape: { x: 5, y: 5, z: 5 },
35
+ allowUndo: true
36
+ }
37
+ }),
38
+
39
+
40
+ getters: {
41
+ // Board state
42
+ board: (state): Stone[][][] => state.game.getBoard(),
43
+ boardShape: (state): BoardShape => state.game.getShape(),
44
+
45
+ // Current player
46
+ currentPlayer: (state): Player => state.game.getCurrentPlayerString(),
47
+ currentPlayerColor: (state): Player => state.game.getCurrentPlayerString(),
48
+ opponentPlayer: (state): Player => state.game.getOpponentPlayer(),
49
+
50
+ // Move history
51
+ moveHistory: (state): Move[] => state.game.getMoveHistory(),
52
+ moveCount: (state): number => state.game.getMoveCount(),
53
+ currentMoveIndex: (state): number => state.game.getCurrentMoveIndex(),
54
+ currentMoves: (state): Move[] => {
55
+ const history = state.game.getMoveHistory();
56
+ const currentIndex = state.game.getCurrentMoveIndex();
57
+ return history.slice(0, currentIndex);
58
+ },
59
+
60
+ // Game status
61
+ gameStatus: (state) => state.game.getStatus(),
62
+ gameResult: (state) => state.game.getResult(),
63
+ isGameActive: (state): boolean => state.game.isGameActive(),
64
+
65
+ // Captured stones
66
+ capturedStones: (state): { black: number; white: number } => {
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
+
74
+ // Pass tracking
75
+ passCount: (state): number => state.game.getConsecutivePassCount(),
76
+
77
+ // Undo/Redo capabilities
78
+ canUndo: (state): boolean => {
79
+ return state.config.allowUndo && state.game.canUndo();
80
+ },
81
+ canRedo: (state): boolean => {
82
+ return state.config.allowUndo && state.game.canRedo();
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
101
+ */
102
+ initializeGame(boardShape: BoardShape = { x: 5, y: 5, z: 5 }): void {
103
+ this.game = new TrigoGameFrontend(boardShape);
104
+ this.config.boardShape = boardShape;
105
+ this.saveToSessionStorage();
106
+ },
107
+
108
+
109
+ /**
110
+ * Start the game
111
+ */
112
+ startGame(): void {
113
+ this.game.startGame();
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");
125
+ return { success: false };
126
+ }
127
+
128
+ // Attempt to make the move
129
+ const success = this.game.makeMove(x, y, z);
130
+
131
+ if (success) {
132
+ // Get captured positions from last step
133
+ const lastStep = this.game.getLastStep();
134
+ const capturedPositions = lastStep?.capturedPositions;
135
+
136
+ // Save to storage
137
+ this.saveToSessionStorage();
138
+
139
+ return { success: true, capturedPositions };
140
+ }
141
+
142
+ return { success: false };
143
+ },
144
+
145
+
146
+ /**
147
+ * Pass turn
148
+ */
149
+ pass(): boolean {
150
+ if (!this.game.isGameActive()) {
151
+ return false;
152
+ }
153
+
154
+ const success = this.game.pass();
155
+ if (success) {
156
+ this.saveToSessionStorage();
157
+ }
158
+ return success;
159
+ },
160
+
161
+
162
+ /**
163
+ * Resign/surrender
164
+ */
165
+ resign(): boolean {
166
+ if (!this.game.isGameActive()) {
167
+ return false;
168
+ }
169
+
170
+ const success = this.game.surrender();
171
+ if (success) {
172
+ this.saveToSessionStorage();
173
+ }
174
+ return success;
175
+ },
176
+
177
+
178
+ /**
179
+ * Undo last move
180
+ */
181
+ undoMove(): boolean {
182
+ if (!this.canUndo) {
183
+ return false;
184
+ }
185
+
186
+ const success = this.game.undoMove();
187
+ if (success) {
188
+ this.saveToSessionStorage();
189
+ }
190
+ return success;
191
+ },
192
+
193
+
194
+ /**
195
+ * Redo next move
196
+ */
197
+ redoMove(): boolean {
198
+ if (!this.canRedo) {
199
+ return false;
200
+ }
201
+
202
+ const success = this.game.redoMove();
203
+ if (success) {
204
+ this.saveToSessionStorage();
205
+ }
206
+ return success;
207
+ },
208
+
209
+
210
+ /**
211
+ * Jump to specific move in history
212
+ */
213
+ jumpToMove(index: number): boolean {
214
+ const success = this.game.jumpToMove(index);
215
+ if (success) {
216
+ this.saveToSessionStorage();
217
+ }
218
+ return success;
219
+ },
220
+
221
+
222
+ /**
223
+ * Reset game to initial state
224
+ */
225
+ resetGame(): void {
226
+ this.game.reset();
227
+ this.saveToSessionStorage();
228
+ },
229
+
230
+
231
+ /**
232
+ * Change board shape (only when game is idle)
233
+ */
234
+ setBoardShape(shape: BoardShape): boolean {
235
+ if (this.game.getStatus() !== "idle") {
236
+ console.warn("Cannot change board shape while game is active");
237
+ return false;
238
+ }
239
+
240
+ this.initializeGame(shape);
241
+ return true;
242
+ },
243
+
244
+
245
+ /**
246
+ * Calculate territory
247
+ */
248
+ computeTerritory(): TerritoryResult {
249
+ return this.game.computeTerritory();
250
+ },
251
+
252
+
253
+ /**
254
+ * Get neighboring positions (6 directions in 3D)
255
+ */
256
+ getNeighbors(x: number, y: number, z: number): Array<{ x: number; y: number; z: number }> {
257
+ const neighbors = [
258
+ { x: x + 1, y, z },
259
+ { x: x - 1, y, z },
260
+ { x, y: y + 1, z },
261
+ { x, y: y - 1, z },
262
+ { x, y, z: z + 1 },
263
+ { x, y, z: z - 1 }
264
+ ];
265
+
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
+ */
273
+ saveToSessionStorage(): void {
274
+ try {
275
+ this.game.saveToSessionStorage("trigoGameState");
276
+ } catch (error) {
277
+ console.error("Failed to save game state:", error);
278
+ }
279
+ },
280
+
281
+
282
+ /**
283
+ * Load game state from session storage
284
+ */
285
+ loadFromSessionStorage(): boolean {
286
+ try {
287
+ const success = this.game.loadFromSessionStorage("trigoGameState");
288
+ if (success) {
289
+ console.log("Game state restored from session storage");
290
+ }
291
+ return success;
292
+ } catch (error) {
293
+ console.error("Failed to load game state:", error);
294
+ return false;
295
+ }
296
+ },
297
+
298
+
299
+ /**
300
+ * Clear saved game state
301
+ */
302
+ clearSessionStorage(): void {
303
+ try {
304
+ this.game.clearSessionStorage("trigoGameState");
305
+ } catch (error) {
306
+ console.error("Failed to clear session storage:", error);
307
+ }
308
+ }
309
+ }
310
+ });
trigo-web/app/src/utils/TrigoGameFrontend.ts ADDED
@@ -0,0 +1,258 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * TrigoGameFrontend - Frontend wrapper for TrigoGame
3
+ *
4
+ * Provides convenient string-based interfaces for frontend use
5
+ * Wraps TrigoGame's number-based types with string types
6
+ */
7
+
8
+ import {
9
+ TrigoGame,
10
+ type BoardShape,
11
+ type Move,
12
+ type Player,
13
+ type TerritoryResult,
14
+ stoneToPlayer,
15
+ stepsToMoves,
16
+ makePosition,
17
+ type GameStatus,
18
+ type GameResult
19
+ } from "../../../inc/trigo";
20
+
21
+
22
+ /**
23
+ * TrigoGameFrontend - Extended TrigoGame with frontend-friendly interfaces
24
+ */
25
+ export class TrigoGameFrontend extends TrigoGame {
26
+ /**
27
+ * Make a move with coordinates (convenience method)
28
+ *
29
+ * @param x X coordinate
30
+ * @param y Y coordinate
31
+ * @param z Z coordinate
32
+ * @returns true if move was successful
33
+ */
34
+ makeMove(x: number, y: number, z: number): boolean {
35
+ return this.drop(makePosition(x, y, z));
36
+ }
37
+
38
+
39
+ /**
40
+ * Get current player as string
41
+ *
42
+ * @returns "black" | "white"
43
+ */
44
+ getCurrentPlayerString(): Player {
45
+ return stoneToPlayer(this.getCurrentPlayer());
46
+ }
47
+
48
+
49
+ /**
50
+ * Get move history in frontend format
51
+ *
52
+ * @returns Array of Move objects
53
+ */
54
+ getMoveHistory(): Move[] {
55
+ return stepsToMoves(this.getHistory());
56
+ }
57
+
58
+
59
+ /**
60
+ * Get stone at position, returns number format for frontend
61
+ *
62
+ * @param x X coordinate
63
+ * @param y Y coordinate
64
+ * @param z Z coordinate
65
+ * @returns 0 = Empty, 1 = Black, 2 = White
66
+ */
67
+ getStoneAt(x: number, y: number, z: number): number {
68
+ return this.getStone(makePosition(x, y, z)) as number;
69
+ }
70
+
71
+
72
+ /**
73
+ * Check if position is valid
74
+ *
75
+ * @param x X coordinate
76
+ * @param y Y coordinate
77
+ * @param z Z coordinate
78
+ * @returns true if position is on the board
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
+ *
96
+ * @param x X coordinate
97
+ * @param y Y coordinate
98
+ * @param z Z coordinate
99
+ * @returns true if position is empty
100
+ */
101
+ isEmpty(x: number, y: number, z: number): boolean {
102
+ if (!this.isValidPosition(x, y, z)) {
103
+ return false;
104
+ }
105
+ return this.getStoneAt(x, y, z) === 0;
106
+ }
107
+
108
+
109
+ /**
110
+ * Check if a move is valid at position
111
+ *
112
+ * @param x X coordinate
113
+ * @param y Y coordinate
114
+ * @param z Z coordinate
115
+ * @returns Object with valid flag and optional reason
116
+ */
117
+ isValidMoveAt(x: number, y: number, z: number): { valid: boolean; reason?: string } {
118
+ return this.isValidMove(makePosition(x, y, z));
119
+ }
120
+
121
+
122
+ /**
123
+ * Get opponent player
124
+ *
125
+ * @returns Opponent player color
126
+ */
127
+ getOpponentPlayer(): Player {
128
+ const current = this.getCurrentPlayerString();
129
+ return current === "black" ? "white" : "black";
130
+ }
131
+
132
+
133
+ /**
134
+ * Check if undo is available
135
+ *
136
+ * @returns true if can undo
137
+ */
138
+ canUndo(): boolean {
139
+ return this.getCurrentStep() > 0;
140
+ }
141
+
142
+
143
+ /**
144
+ * Undo move (convenience wrapper)
145
+ *
146
+ * @returns true if undo was successful
147
+ */
148
+ undoMove(): boolean {
149
+ return this.undo();
150
+ }
151
+
152
+
153
+ /**
154
+ * Redo move (convenience wrapper)
155
+ *
156
+ * @returns true if redo was successful
157
+ */
158
+ redoMove(): boolean {
159
+ return this.redo();
160
+ }
161
+
162
+
163
+ /**
164
+ * Jump to specific move in history
165
+ *
166
+ * @param index Move index (-1 for initial state)
167
+ * @returns true if jump was successful
168
+ */
169
+ jumpToMove(index: number): boolean {
170
+ return this.jumpToStep(index);
171
+ }
172
+
173
+
174
+ /**
175
+ * Get total move count
176
+ *
177
+ * @returns Number of moves played
178
+ */
179
+ getMoveCount(): number {
180
+ return this.getCurrentStep();
181
+ }
182
+
183
+
184
+ /**
185
+ * Get current move index
186
+ *
187
+ * @returns Current step index
188
+ */
189
+ getCurrentMoveIndex(): number {
190
+ return this.getCurrentStep();
191
+ }
192
+
193
+
194
+ /**
195
+ * Get board shape
196
+ *
197
+ * @returns Board dimensions
198
+ */
199
+ getBoardShape(): BoardShape {
200
+ return this.getShape();
201
+ }
202
+
203
+
204
+ /**
205
+ * Initialize game with board shape
206
+ *
207
+ * @param shape Board dimensions
208
+ */
209
+ initializeGame(shape?: BoardShape): void {
210
+ if (shape) {
211
+ // Would need to recreate the game instance with new shape
212
+ // For now, just reset
213
+ this.reset();
214
+ } else {
215
+ this.reset();
216
+ }
217
+ }
218
+
219
+
220
+ /**
221
+ * Compute territory (convenience wrapper)
222
+ *
223
+ * @returns Territory calculation result
224
+ */
225
+ computeTerritory(): TerritoryResult {
226
+ return this.getTerritory();
227
+ }
228
+
229
+
230
+ /**
231
+ * Get game status
232
+ *
233
+ * @returns Game status
234
+ */
235
+ getStatus(): GameStatus {
236
+ return this.getGameStatus();
237
+ }
238
+
239
+
240
+ /**
241
+ * Get game result
242
+ *
243
+ * @returns Game result if available
244
+ */
245
+ getResult(): GameResult | undefined {
246
+ return this.getGameResult();
247
+ }
248
+
249
+
250
+ /**
251
+ * Get consecutive pass count
252
+ *
253
+ * @returns Number of consecutive passes
254
+ */
255
+ getConsecutivePassCount(): number {
256
+ return this.getPassCount();
257
+ }
258
+ }
trigo-web/app/src/views/TrigoView.vue ADDED
@@ -0,0 +1,1604 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <template>
2
+ <div class="trigo-view">
3
+ <div class="view-header">
4
+ <div class="logo-container" title="K.L. Trigo">
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">
16
+ <!-- 3D Board Canvas Area (Left) -->
17
+ <div class="board-container">
18
+ <div class="viewport-wrapper">
19
+ <canvas ref="viewportCanvas" class="viewport-canvas"></canvas>
20
+ <div class="viewport-overlay" v-if="isLoading">
21
+ <p class="loading-text">Loading 3D Board...</p>
22
+ </div>
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>
32
+ </div>
33
+
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>
53
+
54
+ <!-- Move History -->
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>
62
+ <div class="routine-content" ref="moveHistoryContainer">
63
+ <div class="move-list">
64
+ <!-- START placeholder -->
65
+ <div
66
+ :class="{ active: currentMoveIndex === 0 }"
67
+ @click="jumpToMove(0)"
68
+ class="move-row start-row"
69
+ >
70
+ <span class="round-number"></span>
71
+ <span class="move-label open-label">OPEN</span>
72
+ </div>
73
+ <!-- Move history items by round -->
74
+ <div
75
+ v-for="(round, roundIndex) in moveRounds"
76
+ :key="roundIndex"
77
+ class="move-row"
78
+ >
79
+ <span class="round-number">{{ roundIndex + 1 }}.</span>
80
+ <div
81
+ class="move-cell black-move"
82
+ :class="{ active: round.blackIndex === currentMoveIndex }"
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
90
+ v-if="round.white"
91
+ class="move-cell white-move"
92
+ :class="{ active: round.whiteIndex === currentMoveIndex }"
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>
100
+ </div>
101
+ </div>
102
+ </div>
103
+ </div>
104
+
105
+ <!-- Game Controls -->
106
+ <div class="panel-section controls-section">
107
+ <h3 class="section-title">Controls</h3>
108
+ <div class="control-buttons">
109
+ <div class="play-controls">
110
+ <button class="btn btn-pass" @click="pass" :disabled="!gameStarted">
111
+ Pass
112
+ </button>
113
+ <button class="btn btn-resign" @click="resign" :disabled="!gameStarted">
114
+ Resign
115
+ </button>
116
+ </div>
117
+ <div class="history-controls">
118
+ <button
119
+ class="btn btn-icon btn-prev"
120
+ @click="previousMove"
121
+ :disabled="currentMoveIndex <= 0"
122
+ title="Previous Move"
123
+ >
124
+
125
+ </button>
126
+ <button
127
+ class="btn btn-icon btn-next"
128
+ @click="nextMove"
129
+ :disabled="currentMoveIndex >= moveHistory.length"
130
+ title="Next Move"
131
+ >
132
+
133
+ </button>
134
+ </div>
135
+ </div>
136
+ </div>
137
+
138
+ <!-- Game Settings -->
139
+ <div class="panel-section settings-section">
140
+ <h3 class="section-title">Settings</h3>
141
+ <div class="settings-content">
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>
149
+ <option value="5*5*5">5×5×5</option>
150
+ <option value="7*7*7">7×7×7</option>
151
+ <option value="9*9*1">9×9×1 (2D)</option>
152
+ <option value="13*13*1">13×13×1 (2D)</option>
153
+ <option value="19*19*1">19×19×1 (2D)</option>
154
+ <option value="9*9*2">9×9×2</option>
155
+ </select>
156
+ </div>
157
+ <button class="btn btn-primary btn-new-game" @click="newGame">
158
+ {{ gameStarted ? "Reset Game" : "Start Game" }}
159
+ </button>
160
+ </div>
161
+ </div>
162
+ </div>
163
+ </div>
164
+
165
+ <!-- TGN Modal -->
166
+ <div class="tgn-modal" v-if="showTGNModal" @click.self="showTGNModal = false">
167
+ <div class="tgn-modal-content">
168
+ <div class="tgn-modal-header">
169
+ <h3>Game Notation (TGN)</h3>
170
+ <div class="tgn-status" :class="tgnValidationClass">
171
+ {{ tgnValidationMessage }}
172
+ </div>
173
+ <button class="btn-close" @click="showTGNModal = false">×</button>
174
+ </div>
175
+ <div class="tgn-modal-body">
176
+ <textarea
177
+ v-model="editableTGNContent"
178
+ :class="['tgn-textarea', tgnValidationClass]"
179
+ @input="onTGNEdit"
180
+ placeholder="Enter TGN game notation..."
181
+ ></textarea>
182
+ </div>
183
+ <div class="tgn-modal-footer">
184
+ <button class="btn btn-apply" @click="applyTGN" :disabled="!tgnIsValid">
185
+ Apply TGN
186
+ </button>
187
+ <button class="btn btn-copy" @click="copyTGN">Copy to Clipboard</button>
188
+ <button class="btn btn-close-modal" @click="showTGNModal = false">Close</button>
189
+ </div>
190
+ </div>
191
+ </div>
192
+ </div>
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
+
206
+ // Helper functions for board shape parsing
207
+ const parseBoardShape = (shapeStr: string): BoardShape => {
208
+ const parts = shapeStr.split(/[^\d]+/).filter(Boolean).map(Number);
209
+ return { x: parts[0] || 5, y: parts[1] || 5, z: parts[2] || 5 };
210
+ };
211
+
212
+ const printBoardShape = (shape: BoardShape): string => {
213
+ return `${shape.x}*${shape.y}*${shape.z}`;
214
+ };
215
+
216
+ // Format move coordinates using TGN notation
217
+ const formatMoveCoords = (move: { x: number; y: number; z: number }): string => {
218
+ const shape = boardShape.value;
219
+ return encodeAb0yz([move.x, move.y, move.z], [shape.x, shape.y, shape.z]);
220
+ };
221
+
222
+ // Use game store
223
+ const gameStore = useGameStore();
224
+ const {
225
+ currentPlayer,
226
+ moveHistory,
227
+ currentMoveIndex,
228
+ capturedStones,
229
+ gameStatus,
230
+ boardShape,
231
+ moveCount,
232
+ isGameActive,
233
+ passCount
234
+ } = storeToRefs(gameStore);
235
+
236
+ // Local state
237
+ const hoveredPosition = ref<string | null>(null);
238
+ const blackScore = ref(0);
239
+ const whiteScore = ref(0);
240
+ const isLoading = ref(true);
241
+ const selectedBoardShape = ref<string>(printBoardShape(boardShape.value));
242
+ const showTerritoryMode = ref(false);
243
+ const showTGNModal = ref(false);
244
+ const inspectInfo = ref({
245
+ visible: false,
246
+ groupSize: 0,
247
+ liberties: 0
248
+ });
249
+
250
+ // TGN Editor state
251
+ const editableTGNContent = ref<string>('');
252
+ const tgnValidationState = ref<'idle' | 'valid' | 'invalid'>('idle');
253
+ const tgnValidationError = ref<string>('');
254
+ let tgnValidationTimeout: ReturnType<typeof setTimeout> | null = null;
255
+
256
+ // Canvas reference and viewport
257
+ const viewportCanvas = ref<HTMLCanvasElement | null>(null);
258
+ const moveHistoryContainer = ref<HTMLDivElement | null>(null);
259
+ let viewport: TrigoViewport | null = null;
260
+
261
+ // Computed properties
262
+ const gameStarted = computed(() => isGameActive.value);
263
+
264
+ // Group moves into rounds (pairs of black and white moves)
265
+ const moveRounds = computed(() => {
266
+ const rounds: Array<{
267
+ black: any;
268
+ white: any | null;
269
+ blackIndex: number;
270
+ whiteIndex: number | null;
271
+ }> = [];
272
+
273
+ for (let i = 0; i < moveHistory.value.length; i += 2) {
274
+ const blackMove = moveHistory.value[i];
275
+ const whiteMove = moveHistory.value[i + 1] || null;
276
+
277
+ rounds.push({
278
+ black: blackMove,
279
+ white: whiteMove,
280
+ blackIndex: i + 1, // Convert array index to "moves applied" count
281
+ whiteIndex: whiteMove ? i + 2 : null // Convert array index to "moves applied" count
282
+ });
283
+ }
284
+
285
+ return rounds;
286
+ });
287
+
288
+ // Check if selected board shape differs from current game board shape
289
+ const isBoardShapeDirty = computed(() => {
290
+ const selectedShape = parseBoardShape(selectedBoardShape.value);
291
+ const currentShape = boardShape.value;
292
+ return selectedShape.x !== currentShape.x ||
293
+ selectedShape.y !== currentShape.y ||
294
+ selectedShape.z !== currentShape.z;
295
+ });
296
+
297
+ // Generate TGN content
298
+ const tgnContent = computed(() => {
299
+ return gameStore.game?.toTGN({
300
+ application: 'Trigo Demo v1.0',
301
+ date: new Date().toISOString().split('T')[0].replace(/-/g, '.')
302
+ }) || '';
303
+ });
304
+
305
+ // TGN validation computed properties
306
+ const tgnIsValid = computed(() => tgnValidationState.value === 'valid');
307
+
308
+ const tgnValidationClass = computed(() => {
309
+ if (tgnValidationState.value === 'idle') return 'idle';
310
+ if (tgnValidationState.value === 'valid') return 'valid';
311
+ return 'invalid';
312
+ });
313
+
314
+ const tgnValidationMessage = computed(() => {
315
+ if (tgnValidationState.value === 'idle') return 'Ready to validate';
316
+ if (tgnValidationState.value === 'valid') return '✓ Valid TGN';
317
+ return `✗ ${tgnValidationError.value}`;
318
+ });
319
+
320
+ // Debounced TGN validation (synchronous)
321
+ const onTGNEdit = () => {
322
+ // Clear existing timeout
323
+ if (tgnValidationTimeout) {
324
+ clearTimeout(tgnValidationTimeout);
325
+ }
326
+
327
+ // Set validation state to idle while waiting for debounce
328
+ tgnValidationState.value = 'idle';
329
+
330
+ // Set new debounce timeout (300ms)
331
+ tgnValidationTimeout = setTimeout(() => {
332
+ const result = validateTGN(editableTGNContent.value);
333
+
334
+ if (result.valid) {
335
+ tgnValidationState.value = 'valid';
336
+ tgnValidationError.value = '';
337
+ } else {
338
+ tgnValidationState.value = 'invalid';
339
+ tgnValidationError.value = result.error || 'Invalid TGN format';
340
+ }
341
+ }, 300);
342
+ };
343
+
344
+ // Apply TGN and update game state (synchronous)
345
+ const applyTGN = () => {
346
+ if (!tgnIsValid.value) return;
347
+
348
+ try {
349
+ const newGame = TrigoGameFrontend.fromTGN(editableTGNContent.value);
350
+
351
+ // Update game store with the new TrigoGameFrontend instance
352
+ gameStore.game = newGame;
353
+
354
+ // Save to session storage
355
+ gameStore.saveToSessionStorage();
356
+
357
+ // Update viewport with new board state
358
+ // The getters will automatically compute the new values from gameStore.game
359
+ if (viewport) {
360
+ viewport.setBoardShape(newGame.getShape());
361
+ syncViewportWithStore();
362
+ }
363
+
364
+ // Close modal
365
+ showTGNModal.value = false;
366
+ } catch (err) {
367
+ console.error('Failed to apply TGN:', err);
368
+ tgnValidationState.value = 'invalid';
369
+ tgnValidationError.value = err instanceof Error ? err.message : 'Failed to apply TGN';
370
+ }
371
+ };
372
+
373
+ // Handle stone placement
374
+ const handleStoneClick = (x: number, y: number, z: number) => {
375
+ if (!gameStarted.value)
376
+ return;
377
+
378
+ // Make move in store
379
+ const result = gameStore.makeMove(x, y, z);
380
+
381
+ if (result.success && viewport) {
382
+ // Add stone to viewport (store already switched player, so use opponent)
383
+ const stoneColor = gameStore.opponentPlayer;
384
+ viewport.addStone(x, y, z, stoneColor);
385
+
386
+ // Remove captured stones from viewport
387
+ if (result.capturedPositions && result.capturedPositions.length > 0) {
388
+ result.capturedPositions.forEach(pos => {
389
+ viewport.removeStone(pos.x, pos.y, pos.z);
390
+ });
391
+ console.log(`Captured ${result.capturedPositions.length} stone(s)`);
392
+ }
393
+
394
+ // Hide domain visualization and switch back to captured display after move
395
+ viewport.hideDomainCubes();
396
+ showTerritoryMode.value = false;
397
+ }
398
+ };
399
+
400
+ // Handle position hover
401
+ const handlePositionHover = (x: number | null, y: number | null, z: number | null) => {
402
+ if (x !== null && y !== null && z !== null) {
403
+ hoveredPosition.value = `(${x}, ${y}, ${z})`;
404
+ } else {
405
+ hoveredPosition.value = null;
406
+ }
407
+ };
408
+
409
+ // Check if a position is droppable (validates with game rules)
410
+ const isPositionDroppable = (x: number, y: number, z: number): boolean => {
411
+ const pos = { x, y, z };
412
+ const playerColor = currentPlayer.value === "black" ? StoneType.BLACK : StoneType.WHITE;
413
+
414
+ const validation = validateMove(
415
+ pos,
416
+ playerColor,
417
+ gameStore.board,
418
+ boardShape.value,
419
+ gameStore.lastCapturedPositions
420
+ );
421
+
422
+ return validation.valid;
423
+ };
424
+
425
+ // Handle inspect mode callback
426
+ const handleInspectGroup = (groupSize: number, liberties: number) => {
427
+ if (groupSize > 0) {
428
+ inspectInfo.value = {
429
+ visible: true,
430
+ groupSize,
431
+ liberties
432
+ };
433
+ } else {
434
+ inspectInfo.value = {
435
+ visible: false,
436
+ groupSize: 0,
437
+ liberties: 0
438
+ };
439
+ }
440
+ };
441
+
442
+
443
+ // Game control methods
444
+ const newGame = () => {
445
+ // Parse selected board shape
446
+ const shape = parseBoardShape(selectedBoardShape.value);
447
+
448
+ // Initialize game in store
449
+ gameStore.initializeGame(shape);
450
+ gameStore.startGame();
451
+
452
+ // Update viewport with new board shape
453
+ if (viewport) {
454
+ viewport.setBoardShape(shape);
455
+ viewport.clearBoard();
456
+ viewport.setGameActive(true);
457
+
458
+ // Exit territory mode if active
459
+ viewport.hideDomainCubes();
460
+ }
461
+
462
+ // Reset scores and territory mode
463
+ blackScore.value = 0;
464
+ whiteScore.value = 0;
465
+ showTerritoryMode.value = false;
466
+
467
+ console.log(
468
+ `Starting new game with board shape ${shape.x}×${shape.y}×${shape.z}`
469
+ );
470
+ };
471
+
472
+ const pass = () => {
473
+ const previousPlayer = currentPlayer.value;
474
+ const success = gameStore.pass();
475
+
476
+ if (success) {
477
+ // Check if game ended due to double pass
478
+ //if (gameStore.gameResult?.reason === "double-pass") {
479
+ // showGameResult();
480
+ //} else {
481
+ console.log(`${previousPlayer} passed (Pass count: ${gameStore.passCount})`);
482
+ //}
483
+ }
484
+ };
485
+
486
+
487
+ const resign = () => {
488
+ // Confirm resignation
489
+ const confirmed = confirm(
490
+ `Are you sure ${currentPlayer.value} wants to resign?\n\nThis will end the game immediately.`
491
+ );
492
+
493
+ if (!confirmed) return;
494
+
495
+ //const success = gameStore.resign();
496
+
497
+ //if (success) {
498
+ // showGameResult();
499
+ //}
500
+ };
501
+
502
+
503
+ const showGameResult = () => {
504
+ const result = gameStore.gameResult;
505
+ if (!result) return;
506
+
507
+ // Don't set game as inactive - allow continued analysis
508
+ // if (viewport) {
509
+ // viewport.setGameActive(false);
510
+ // }
511
+
512
+ let message = "";
513
+ if (result.reason === "resignation") {
514
+ message = `${result.winner === "black" ? "Black" : "White"} wins by resignation!\n\nGame continues for analysis.`;
515
+ } else if (result.reason === "double-pass") {
516
+ // Calculate final scores for double pass
517
+ const territory = gameStore.computeTerritory();
518
+ const blackTotal = territory.black + capturedStones.value.black;
519
+ const whiteTotal = territory.white + capturedStones.value.white;
520
+
521
+ blackScore.value = blackTotal;
522
+ whiteScore.value = whiteTotal;
523
+
524
+ if (blackTotal > whiteTotal) {
525
+ message = `Black wins by ${blackTotal - whiteTotal} points!\n\nBlack: ${blackTotal} points\nWhite: ${whiteTotal} points\n\nGame continues for analysis.`;
526
+ } else if (whiteTotal > blackTotal) {
527
+ message = `White wins by ${whiteTotal - blackTotal} points!\n\nWhite: ${whiteTotal} points\nBlack: ${blackTotal} points\n\nGame continues for analysis.`;
528
+ } else {
529
+ message = `Game is a draw!\n\nBoth players: ${blackTotal} points\n\nGame continues for analysis.`;
530
+ }
531
+ }
532
+
533
+ setTimeout(() => {
534
+ alert(message);
535
+ }, 100);
536
+ };
537
+
538
+
539
+ const computeTerritory = () => {
540
+ if (!gameStarted.value) return;
541
+
542
+ // Toggle territory mode
543
+ if (showTerritoryMode.value) {
544
+ // Exit territory mode
545
+ if (viewport) {
546
+ viewport.hideDomainCubes();
547
+ }
548
+ showTerritoryMode.value = false;
549
+ // Reset scores to captured stones count
550
+ blackScore.value = 0;
551
+ whiteScore.value = 0;
552
+ return;
553
+ }
554
+
555
+ // Enter territory mode - Use store's territory calculation
556
+ const territory = gameStore.computeTerritory();
557
+ blackScore.value = territory.black + capturedStones.value.black;
558
+ whiteScore.value = territory.white + capturedStones.value.white;
559
+
560
+ // Switch to territory display mode
561
+ showTerritoryMode.value = true;
562
+
563
+ // Convert territory arrays to Sets of position keys for viewport
564
+ if (viewport) {
565
+ const blackDomain = new Set<string>();
566
+ const whiteDomain = new Set<string>();
567
+
568
+ // Use the calculated territory positions from the territory result
569
+ territory.blackTerritory.forEach(pos => {
570
+ const key = `${pos.x},${pos.y},${pos.z}`;
571
+ blackDomain.add(key);
572
+ });
573
+
574
+ territory.whiteTerritory.forEach(pos => {
575
+ const key = `${pos.x},${pos.y},${pos.z}`;
576
+ whiteDomain.add(key);
577
+ });
578
+
579
+ // Set domain data and show both domains
580
+ viewport.setDomainData(blackDomain, whiteDomain);
581
+ viewport.setBlackDomainVisible(true);
582
+ viewport.setWhiteDomainVisible(true);
583
+ }
584
+
585
+ console.log("Territory computed:", territory);
586
+ };
587
+
588
+ const previousMove = () => {
589
+ const success = gameStore.undoMove();
590
+
591
+ if (success && viewport) {
592
+ // Rebuild viewport from current board state
593
+ syncViewportWithStore();
594
+ }
595
+ };
596
+
597
+ const nextMove = () => {
598
+ const success = gameStore.redoMove();
599
+
600
+ if (success && viewport) {
601
+ // Rebuild viewport from current board state
602
+ syncViewportWithStore();
603
+ }
604
+ };
605
+
606
+ const jumpToMove = (index: number) => {
607
+ const success = gameStore.jumpToMove(index);
608
+
609
+ if (success && viewport) {
610
+ // Rebuild viewport from current board state
611
+ syncViewportWithStore();
612
+
613
+ // Exit territory mode when navigating move history
614
+ viewport.hideDomainCubes();
615
+ showTerritoryMode.value = false;
616
+ // Reset scores to 0 when exiting territory mode
617
+ blackScore.value = 0;
618
+ whiteScore.value = 0;
619
+ }
620
+ };
621
+
622
+ // Sync viewport with store's board state
623
+ const syncViewportWithStore = () => {
624
+ if (!viewport) return;
625
+
626
+ // Clear viewport
627
+ viewport.clearBoard();
628
+
629
+ // Read the actual board state from the store (which has captures applied)
630
+ const board = gameStore.board;
631
+ const shape = boardShape.value;
632
+
633
+ // Add all stones that exist on the board
634
+ for (let x = 0; x < shape.x; x++) {
635
+ for (let y = 0; y < shape.y; y++) {
636
+ for (let z = 0; z < shape.z; z++) {
637
+ const stone = board[x][y][z];
638
+ if (stone === Stone.Black) {
639
+ viewport.addStone(x, y, z, "black");
640
+ } else if (stone === Stone.White) {
641
+ viewport.addStone(x, y, z, "white");
642
+ }
643
+ }
644
+ }
645
+ }
646
+
647
+ // Set the last placed stone highlight based on current move index
648
+ // currentMoveIndex represents the number of moves applied (not array index)
649
+ // So the last applied move is at array index (currentMoveIndex - 1)
650
+ if (currentMoveIndex.value > 0 && currentMoveIndex.value <= moveHistory.value.length) {
651
+ const lastMove = moveHistory.value[currentMoveIndex.value - 1];
652
+ viewport.setLastPlacedStone(lastMove.x, lastMove.y, lastMove.z);
653
+ } else {
654
+ // No moves applied or at START position
655
+ viewport.setLastPlacedStone(null, null, null);
656
+ }
657
+ };
658
+
659
+ // Watch for current player changes to update viewport preview
660
+ watch(currentPlayer, (newPlayer) => {
661
+ if (viewport) {
662
+ viewport.setCurrentPlayer(newPlayer);
663
+ }
664
+ });
665
+
666
+ // Watch currentMoveIndex to auto-scroll move history to keep current move visible
667
+ watch(currentMoveIndex, () => {
668
+ // Use nextTick to ensure DOM is updated before scrolling
669
+ nextTick(() => {
670
+ if (moveHistoryContainer.value) {
671
+ // Find the active move element
672
+ const activeElement = moveHistoryContainer.value.querySelector('.active');
673
+ if (activeElement) {
674
+ // Scroll the active element into view smoothly
675
+ activeElement.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
676
+ }
677
+ }
678
+ });
679
+ });
680
+
681
+ // Watch TGN modal to populate editable content when opened
682
+ watch(showTGNModal, (isVisible) => {
683
+ if (isVisible) {
684
+ // Populate with current game's TGN when modal opens
685
+ editableTGNContent.value = tgnContent.value;
686
+ tgnValidationState.value = 'valid'; // Current TGN is always valid
687
+ tgnValidationError.value = '';
688
+ }
689
+ });
690
+
691
+
692
+ // Lifecycle hooks
693
+ onMounted(() => {
694
+ console.log("TrigoDemo component mounted");
695
+
696
+ // Try to restore game state from session storage
697
+ const restoredFromStorage = gameStore.loadFromSessionStorage();
698
+
699
+ // If not restored from storage, initialize new game
700
+ if (!restoredFromStorage) {
701
+ // Parse initial board shape
702
+ const shape = parseBoardShape(selectedBoardShape.value);
703
+
704
+ // Initialize game store
705
+ gameStore.initializeGame(shape);
706
+ } else {
707
+ // Update selected board shape to match restored state
708
+ selectedBoardShape.value = printBoardShape(boardShape.value);
709
+ console.log("Restored game state from session storage");
710
+ }
711
+
712
+ // Initialize Three.js viewport with current board shape
713
+ if (viewportCanvas.value) {
714
+ viewport = new TrigoViewport(viewportCanvas.value, boardShape.value, {
715
+ onStoneClick: handleStoneClick,
716
+ onPositionHover: handlePositionHover,
717
+ isPositionDroppable: isPositionDroppable,
718
+ onInspectGroup: handleInspectGroup
719
+ });
720
+ console.log("TrigoViewport initialized");
721
+
722
+ // Hide loading overlay after viewport is initialized
723
+ isLoading.value = false;
724
+
725
+ // If game was restored, sync viewport with restored board state
726
+ if (restoredFromStorage) {
727
+ syncViewportWithStore();
728
+ viewport.setGameActive(isGameActive.value);
729
+ }
730
+ }
731
+
732
+ // Start game automatically if not restored or was playing
733
+ if (!restoredFromStorage || gameStore.gameStatus === "idle") {
734
+ gameStore.startGame();
735
+ if (viewport) {
736
+ viewport.setGameActive(true);
737
+ }
738
+ }
739
+
740
+ // Add keyboard shortcuts
741
+ window.addEventListener("keydown", handleKeyPress);
742
+ });
743
+
744
+
745
+ onUnmounted(() => {
746
+ console.log("TrigoDemo component unmounted");
747
+
748
+ // Remove keyboard shortcuts
749
+ window.removeEventListener("keydown", handleKeyPress);
750
+
751
+ // Cleanup Three.js resources
752
+ if (viewport) {
753
+ viewport.destroy();
754
+ viewport = null;
755
+ }
756
+ });
757
+
758
+
759
+ // Keyboard shortcuts handler
760
+ const handleKeyPress = (event: KeyboardEvent) => {
761
+ // Ignore if typing in an input field
762
+ if (event.target instanceof HTMLInputElement || event.target instanceof HTMLTextAreaElement) {
763
+ return;
764
+ }
765
+
766
+ switch (event.key.toLowerCase()) {
767
+ case "p": // Pass
768
+ if (gameStarted.value) {
769
+ pass();
770
+ }
771
+ break;
772
+ case "n": // New game
773
+ newGame();
774
+ break;
775
+ case "r": // Resign
776
+ if (gameStarted.value) {
777
+ resign();
778
+ }
779
+ break;
780
+ case "arrowleft": // Previous move
781
+ previousMove();
782
+ event.preventDefault();
783
+ break;
784
+ case "arrowright": // Next move
785
+ nextMove();
786
+ event.preventDefault();
787
+ break;
788
+ case "t": // Compute territory
789
+ if (gameStarted.value) {
790
+ computeTerritory();
791
+ }
792
+ break;
793
+ }
794
+ };
795
+
796
+
797
+ // TGN Modal Methods
798
+ const selectAllTGN = (event: Event) => {
799
+ const textarea = event.target as HTMLTextAreaElement;
800
+ textarea.select();
801
+ };
802
+
803
+ const copyTGN = async () => {
804
+ try {
805
+ // Try modern clipboard API first
806
+ if (navigator.clipboard && navigator.clipboard.writeText) {
807
+ await navigator.clipboard.writeText(tgnContent.value);
808
+ alert('TGN copied to clipboard!');
809
+ } else {
810
+ // Fallback for older browsers or non-secure contexts
811
+ const textarea = document.createElement('textarea');
812
+ textarea.value = tgnContent.value;
813
+ textarea.style.position = 'fixed';
814
+ textarea.style.opacity = '0';
815
+ document.body.appendChild(textarea);
816
+ textarea.select();
817
+
818
+ try {
819
+ document.execCommand('copy');
820
+ alert('TGN copied to clipboard!');
821
+ } catch (fallbackErr) {
822
+ console.error('Fallback copy failed:', fallbackErr);
823
+ alert('Failed to copy TGN. Please select and copy manually.');
824
+ } finally {
825
+ document.body.removeChild(textarea);
826
+ }
827
+ }
828
+ } catch (err) {
829
+ console.error('Failed to copy TGN:', err);
830
+ alert('Failed to copy TGN. Please select and copy manually.');
831
+ }
832
+ };
833
+ </script>
834
+
835
+ <style lang="scss" scoped>
836
+ .trigo-view {
837
+ display: flex;
838
+ flex-direction: column;
839
+ height: 100%;
840
+ background-color: #404040;
841
+ color: #e0e0e0;
842
+ overflow: hidden;
843
+
844
+ .view-header {
845
+ display: flex;
846
+ justify-content: space-between;
847
+ align-items: center;
848
+ padding: 1rem 2rem;
849
+ background: linear-gradient(135deg, #505050 0%, #454545 100%);
850
+ border-bottom: 2px solid #606060;
851
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
852
+
853
+ .logo-container {
854
+ display: flex;
855
+ align-items: center;
856
+ gap: 0.75rem;
857
+
858
+ .app-logo {
859
+ width: 40px;
860
+ height: 40px;
861
+ object-fit: contain;
862
+ }
863
+
864
+ .app-title {
865
+ font-size: 1.5rem;
866
+ font-weight: 700;
867
+ color: #e94560;
868
+ }
869
+ }
870
+
871
+ .view-title {
872
+ font-size: 1.5rem;
873
+ font-weight: 700;
874
+ margin: 0;
875
+ color: #e94560;
876
+ }
877
+
878
+ .view-status {
879
+ display: flex;
880
+ gap: 2rem;
881
+ align-items: center;
882
+
883
+ .turn-indicator {
884
+ padding: 0.5rem 1rem;
885
+ border-radius: 8px;
886
+ font-weight: 600;
887
+ transition: all 0.3s ease;
888
+
889
+ &.black {
890
+ background-color: #2c2c2c;
891
+ color: #fff;
892
+ box-shadow: 0 0 10px rgba(255, 255, 255, 0.3);
893
+ }
894
+
895
+ &.white {
896
+ background-color: #f0f0f0;
897
+ color: #000;
898
+ box-shadow: 0 0 10px rgba(240, 240, 240, 0.3);
899
+ }
900
+ }
901
+
902
+ .move-count {
903
+ font-size: 1rem;
904
+ color: #a0a0a0;
905
+ }
906
+ }
907
+ }
908
+
909
+ .view-body {
910
+ display: flex;
911
+ flex: 1;
912
+ overflow: hidden;
913
+
914
+ .board-container {
915
+ flex: 1;
916
+ display: flex;
917
+ align-items: center;
918
+ justify-content: center;
919
+ background-color: #484848;
920
+ padding: 1rem;
921
+ position: relative;
922
+
923
+ .viewport-wrapper {
924
+ width: 100%;
925
+ height: 100%;
926
+ position: relative;
927
+ border-radius: 8px;
928
+ overflow: hidden;
929
+ box-shadow: 0 4px 16px rgba(0, 0, 0, 0.5);
930
+
931
+ .viewport-canvas {
932
+ width: 100%;
933
+ height: 100%;
934
+ display: block;
935
+ background-color: #50505a;
936
+ }
937
+
938
+ .viewport-overlay {
939
+ position: absolute;
940
+ top: 0;
941
+ left: 0;
942
+ width: 100%;
943
+ height: 100%;
944
+ display: flex;
945
+ align-items: center;
946
+ justify-content: center;
947
+ pointer-events: none;
948
+
949
+ .loading-text {
950
+ color: rgba(255, 255, 255, 0.5);
951
+ font-size: 1.2rem;
952
+ animation: pulse 2s ease-in-out infinite;
953
+ }
954
+ }
955
+
956
+ .inspect-tooltip {
957
+ position: absolute;
958
+ top: 1rem;
959
+ left: 1rem;
960
+ background-color: rgba(255, 241, 176, 0.95);
961
+ color: #111;
962
+ padding: 0.5rem 1rem;
963
+ border-radius: 8px;
964
+ font-size: 0.9rem;
965
+ font-weight: 600;
966
+ pointer-events: none;
967
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
968
+ z-index: 100;
969
+
970
+ .tooltip-content {
971
+ display: flex;
972
+ align-items: center;
973
+ gap: 0.5rem;
974
+
975
+ .tooltip-label {
976
+ white-space: nowrap;
977
+ }
978
+
979
+ .tooltip-divider {
980
+ color: #888;
981
+ }
982
+ }
983
+ }
984
+ }
985
+ }
986
+
987
+ .controls-panel {
988
+ width: 320px;
989
+ background-color: #3a3a3a;
990
+ border-left: 2px solid #606060;
991
+ display: flex;
992
+ flex-direction: column;
993
+ overflow: hidden;
994
+ box-shadow: -4px 0 16px rgba(0, 0, 0, 0.3);
995
+
996
+ .panel-section {
997
+ padding: 0.8rem;
998
+ border-bottom: 1px solid #505050;
999
+ position: relative;
1000
+
1001
+ .section-title {
1002
+ font-size: 0.7rem;
1003
+ font-weight: 600;
1004
+ color: #f0bcc5;
1005
+ text-transform: uppercase;
1006
+ letter-spacing: 1px;
1007
+ position: absolute;
1008
+ top: 0;
1009
+ left: 0.8rem;
1010
+ opacity: 0;
1011
+ transition: opacity 0.3s ease-in;
1012
+ }
1013
+
1014
+ &:hover .section-title {
1015
+ opacity: 0.6;
1016
+ }
1017
+ }
1018
+
1019
+ .score-section {
1020
+ // Default style (captured mode) - lighter background
1021
+ .score-display {
1022
+ display: flex;
1023
+ gap: 0.5rem;
1024
+ margin-bottom: 1rem;
1025
+
1026
+ .score-button {
1027
+ flex: 1;
1028
+ display: flex;
1029
+ align-items: center;
1030
+ justify-content: center;
1031
+ gap: 0.5rem;
1032
+ padding: 1rem;
1033
+ border: 2px solid #505050;
1034
+ border-radius: 8px;
1035
+ background-color: #484848;
1036
+ cursor: default;
1037
+ transition: all 0.3s ease;
1038
+
1039
+ &.black {
1040
+ .color-indicator {
1041
+ background-color: #2c2c2c;
1042
+ border: 2px solid #fff;
1043
+ }
1044
+ }
1045
+
1046
+ &.white {
1047
+ .color-indicator {
1048
+ background-color: #f0f0f0;
1049
+ border: 2px solid #000;
1050
+ }
1051
+ }
1052
+
1053
+ .color-indicator {
1054
+ width: 24px;
1055
+ height: 24px;
1056
+ border-radius: 50%;
1057
+ }
1058
+
1059
+ .score {
1060
+ font-size: 1.5rem;
1061
+ font-weight: 700;
1062
+ color: #e0e0e0;
1063
+ }
1064
+
1065
+ &:disabled {
1066
+ opacity: 0.5;
1067
+ }
1068
+ }
1069
+ }
1070
+
1071
+ .compute-territory {
1072
+ width: 100%;
1073
+ padding: 0.75rem;
1074
+ background-color: #505050;
1075
+ color: #e0e0e0;
1076
+ border: none;
1077
+ border-radius: 8px;
1078
+ font-weight: 600;
1079
+ cursor: pointer;
1080
+ transition: all 0.3s ease;
1081
+
1082
+ &:hover:not(:disabled) {
1083
+ background-color: #606060;
1084
+ }
1085
+
1086
+ &:disabled {
1087
+ opacity: 0.5;
1088
+ cursor: not-allowed;
1089
+ }
1090
+ }
1091
+
1092
+ // Territory mode - darker background with glow
1093
+ &.show-territory {
1094
+ .score-display .score-button {
1095
+ background-color: #3a3a3a;
1096
+ border-color: #606060;
1097
+ box-shadow: 0 0 12px rgba(233, 69, 96, 0.3);
1098
+ }
1099
+ }
1100
+ }
1101
+
1102
+
1103
+
1104
+ .routine-section {
1105
+ flex: 1;
1106
+ display: flex;
1107
+ flex-direction: column;
1108
+ min-height: 0;
1109
+
1110
+ .routine-content {
1111
+ flex: 1;
1112
+ overflow-y: auto;
1113
+ background-color: #2a2a2a;
1114
+ border-radius: 8px;
1115
+ padding: 0.5rem;
1116
+
1117
+ .move-list {
1118
+ padding: 0;
1119
+ margin: 0;
1120
+
1121
+ .move-row {
1122
+ display: grid;
1123
+ grid-template-columns: 30px 1fr 1fr;
1124
+ gap: 0.25rem;
1125
+ margin-bottom: 0.25rem;
1126
+ align-items: center;
1127
+
1128
+ &.start-row {
1129
+ grid-template-columns: 30px 1fr;
1130
+ padding: 0.5rem;
1131
+ border-radius: 4px;
1132
+ cursor: pointer;
1133
+ transition: all 0.2s ease;
1134
+
1135
+ &:hover {
1136
+ background-color: #484848;
1137
+ }
1138
+
1139
+ &.active {
1140
+ background-color: #505050;
1141
+ border-left: 3px solid #e94560;
1142
+ }
1143
+
1144
+ .open-label {
1145
+ font-weight: 700;
1146
+ color: #e94560;
1147
+ text-transform: uppercase;
1148
+ letter-spacing: 1px;
1149
+ }
1150
+ }
1151
+
1152
+ .round-number {
1153
+ color: #808080;
1154
+ font-size: 0.9em;
1155
+ text-align: right;
1156
+ padding-right: 0.25rem;
1157
+ }
1158
+
1159
+ .move-cell {
1160
+ padding: 0.5rem;
1161
+ border-radius: 4px;
1162
+ cursor: pointer;
1163
+ transition: all 0.2s ease;
1164
+ display: flex;
1165
+ align-items: center;
1166
+ gap: 0.5rem;
1167
+ background-color: #1a1a1a;
1168
+
1169
+ &:hover:not(.empty) {
1170
+ background-color: #484848;
1171
+ }
1172
+
1173
+ &.active {
1174
+ background-color: #505050;
1175
+ border-left: 3px solid #e94560;
1176
+ }
1177
+
1178
+ &.empty {
1179
+ background-color: transparent;
1180
+ cursor: default;
1181
+ }
1182
+
1183
+ .stone-icon {
1184
+ width: 16px;
1185
+ height: 16px;
1186
+ border-radius: 50%;
1187
+ flex-shrink: 0;
1188
+
1189
+ &.black {
1190
+ background-color: #2c2c2c;
1191
+ border: 2px solid #fff;
1192
+ }
1193
+
1194
+ &.white {
1195
+ background-color: #f0f0f0;
1196
+ border: 2px solid #000;
1197
+ }
1198
+ }
1199
+
1200
+ .move-coords {
1201
+ color: #a0a0a0;
1202
+ font-family: monospace;
1203
+ font-size: 0.9em;
1204
+ }
1205
+
1206
+ .move-label {
1207
+ color: #60a5fa;
1208
+ font-weight: 600;
1209
+ font-size: 0.85em;
1210
+ text-transform: lowercase;
1211
+ }
1212
+ }
1213
+ }
1214
+ }
1215
+ }
1216
+ }
1217
+
1218
+ .controls-section {
1219
+ .control-buttons {
1220
+ display: flex;
1221
+ flex-direction: column;
1222
+ gap: 1rem;
1223
+
1224
+ .play-controls,
1225
+ .history-controls {
1226
+ display: flex;
1227
+ gap: 0.5rem;
1228
+ }
1229
+
1230
+ .btn {
1231
+ padding: 0.75rem 1.5rem;
1232
+ border: none;
1233
+ border-radius: 8px;
1234
+ font-weight: 600;
1235
+ cursor: pointer;
1236
+ transition: all 0.3s ease;
1237
+ flex: 1;
1238
+
1239
+ &:disabled {
1240
+ opacity: 0.5;
1241
+ cursor: not-allowed;
1242
+ }
1243
+
1244
+ &.btn-pass {
1245
+ background-color: #2d4059;
1246
+ color: #e0e0e0;
1247
+
1248
+ &:hover:not(:disabled) {
1249
+ background-color: #3d5069;
1250
+ }
1251
+ }
1252
+
1253
+ &.btn-resign {
1254
+ background-color: #c23b22;
1255
+ color: #fff;
1256
+
1257
+ &:hover:not(:disabled) {
1258
+ background-color: #d44b32;
1259
+ }
1260
+ }
1261
+
1262
+ &.btn-icon {
1263
+ background-color: #505050;
1264
+ color: #e0e0e0;
1265
+ padding: 0.75rem;
1266
+ font-size: 1.2rem;
1267
+
1268
+ &:hover:not(:disabled) {
1269
+ background-color: #606060;
1270
+ }
1271
+ }
1272
+ }
1273
+ }
1274
+ }
1275
+
1276
+ .settings-section {
1277
+ .settings-content {
1278
+ display: flex;
1279
+ flex-direction: column;
1280
+ gap: 1rem;
1281
+
1282
+ .setting-item {
1283
+ display: flex;
1284
+ align-items: center;
1285
+ justify-content: space-between;
1286
+
1287
+ label {
1288
+ font-weight: 600;
1289
+ color: #a0a0a0;
1290
+ display: flex;
1291
+ align-items: center;
1292
+ gap: 0.3rem;
1293
+
1294
+ .dirty-indicator {
1295
+ color: #e94560;
1296
+ font-size: 1.2rem;
1297
+ font-weight: 700;
1298
+ animation: pulse 2s ease-in-out infinite;
1299
+ }
1300
+ }
1301
+
1302
+ select {
1303
+ padding: 0.5rem;
1304
+ border: 2px solid #505050;
1305
+ border-radius: 8px;
1306
+ background-color: #484848;
1307
+ color: #e0e0e0;
1308
+ cursor: pointer;
1309
+ font-weight: 600;
1310
+
1311
+ &:disabled {
1312
+ opacity: 0.5;
1313
+ cursor: not-allowed;
1314
+ }
1315
+ }
1316
+ }
1317
+
1318
+ .btn-new-game {
1319
+ width: 100%;
1320
+ padding: 0.75rem;
1321
+ background-color: #e94560;
1322
+ color: #fff;
1323
+ border: none;
1324
+ border-radius: 8px;
1325
+ font-weight: 700;
1326
+ cursor: pointer;
1327
+ transition: all 0.3s ease;
1328
+ text-transform: uppercase;
1329
+
1330
+ &:hover {
1331
+ background-color: #f95670;
1332
+ transform: translateY(-2px);
1333
+ box-shadow: 0 4px 12px rgba(233, 69, 96, 0.4);
1334
+ }
1335
+ }
1336
+ }
1337
+ }
1338
+ }
1339
+ }
1340
+ }
1341
+
1342
+ /* TGN Button and Modal Styles */
1343
+ .btn-view-tgn {
1344
+ position: absolute;
1345
+ top: 0;
1346
+ right: 0.8rem;
1347
+ padding: 0.25rem 0.5rem;
1348
+ background-color: transparent;
1349
+ color: #a0a0a0;
1350
+ border: 1px solid #505050;
1351
+ border-radius: 4px;
1352
+ font-size: 0.65rem;
1353
+ font-weight: 600;
1354
+ text-transform: uppercase;
1355
+ letter-spacing: 0.5px;
1356
+ cursor: pointer;
1357
+ transition: all 0.2s ease;
1358
+ opacity: 0;
1359
+
1360
+ &:hover {
1361
+ background-color: #505050;
1362
+ color: #e0e0e0;
1363
+ border-color: #606060;
1364
+ }
1365
+ }
1366
+
1367
+ .routine-section:hover .btn-view-tgn {
1368
+ opacity: 1;
1369
+ }
1370
+
1371
+ .tgn-modal {
1372
+ position: fixed;
1373
+ top: 0;
1374
+ left: 0;
1375
+ width: 100vw;
1376
+ height: 100vh;
1377
+ background-color: rgba(0, 0, 0, 0.7);
1378
+ display: flex;
1379
+ align-items: center;
1380
+ justify-content: center;
1381
+ z-index: 1000;
1382
+ backdrop-filter: blur(4px);
1383
+
1384
+ .tgn-modal-content {
1385
+ background-color: #3a3a3a;
1386
+ border-radius: 12px;
1387
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5);
1388
+ width: 90%;
1389
+ max-width: 600px;
1390
+ max-height: 80vh;
1391
+ display: flex;
1392
+ flex-direction: column;
1393
+ overflow: hidden;
1394
+
1395
+ .tgn-modal-header {
1396
+ display: flex;
1397
+ justify-content: space-between;
1398
+ align-items: center;
1399
+ padding: 1rem 1.5rem;
1400
+ background-color: #2a2a2a;
1401
+ border-bottom: 1px solid #505050;
1402
+
1403
+ h3 {
1404
+ margin: 0;
1405
+ font-size: 1.2rem;
1406
+ font-weight: 600;
1407
+ color: #e0e0e0;
1408
+ }
1409
+
1410
+ .tgn-status {
1411
+ font-size: 0.85rem;
1412
+ font-weight: 600;
1413
+ padding: 0.4rem 0.8rem;
1414
+ border-radius: 4px;
1415
+ white-space: nowrap;
1416
+ transition: all 0.3s ease;
1417
+
1418
+ &.idle {
1419
+ background-color: #505050;
1420
+ color: #a0a0a0;
1421
+ }
1422
+
1423
+ &.valid {
1424
+ background-color: rgba(74, 222, 128, 0.15);
1425
+ color: #4ade80;
1426
+ border: 1px solid rgba(74, 222, 128, 0.4);
1427
+ }
1428
+
1429
+ &.invalid {
1430
+ background-color: rgba(239, 68, 68, 0.15);
1431
+ color: #ef4444;
1432
+ border: 1px solid rgba(239, 68, 68, 0.4);
1433
+ }
1434
+ }
1435
+
1436
+ .btn-close {
1437
+ background: none;
1438
+ border: none;
1439
+ color: #a0a0a0;
1440
+ font-size: 1.5rem;
1441
+ cursor: pointer;
1442
+ padding: 0;
1443
+ width: 32px;
1444
+ height: 32px;
1445
+ display: flex;
1446
+ align-items: center;
1447
+ justify-content: center;
1448
+ border-radius: 4px;
1449
+ transition: all 0.2s ease;
1450
+
1451
+ &:hover {
1452
+ background-color: #505050;
1453
+ color: #e0e0e0;
1454
+ }
1455
+ }
1456
+ }
1457
+
1458
+ .tgn-modal-body {
1459
+ flex: 1;
1460
+ padding: 1.5rem;
1461
+ overflow: auto;
1462
+
1463
+ .tgn-textarea {
1464
+ width: 100%;
1465
+ height: 100%;
1466
+ min-height: 300px;
1467
+ background-color: #2a2a2a;
1468
+ color: #e0e0e0;
1469
+ border: 2px solid #505050;
1470
+ border-radius: 8px;
1471
+ padding: 1rem;
1472
+ font-family: 'Courier New', Courier, monospace;
1473
+ font-size: 0.9rem;
1474
+ line-height: 1.6;
1475
+ resize: vertical;
1476
+ cursor: text;
1477
+ transition: border-color 0.3s ease, box-shadow 0.3s ease;
1478
+
1479
+ &:focus {
1480
+ outline: none;
1481
+ border-color: #e94560;
1482
+ }
1483
+
1484
+ &.valid {
1485
+ border-color: #4ade80;
1486
+ box-shadow: 0 0 8px rgba(74, 222, 128, 0.2);
1487
+ }
1488
+
1489
+ &.invalid {
1490
+ border-color: #ef4444;
1491
+ box-shadow: 0 0 8px rgba(239, 68, 68, 0.2);
1492
+ }
1493
+
1494
+ &.idle {
1495
+ border-color: #505050;
1496
+ }
1497
+ }
1498
+ }
1499
+
1500
+ .tgn-modal-footer {
1501
+ display: flex;
1502
+ gap: 0.5rem;
1503
+ padding: 1rem 1.5rem;
1504
+ background-color: #2a2a2a;
1505
+ border-top: 1px solid #505050;
1506
+
1507
+ .btn {
1508
+ flex: 1;
1509
+ padding: 0.75rem;
1510
+ border: none;
1511
+ border-radius: 8px;
1512
+ font-weight: 600;
1513
+ cursor: pointer;
1514
+ transition: all 0.3s ease;
1515
+
1516
+ &:disabled {
1517
+ opacity: 0.5;
1518
+ cursor: not-allowed;
1519
+ }
1520
+
1521
+ &.btn-apply {
1522
+ background-color: #4ade80;
1523
+ color: #000;
1524
+
1525
+ &:hover:not(:disabled) {
1526
+ background-color: #22c55e;
1527
+ transform: translateY(-2px);
1528
+ box-shadow: 0 4px 12px rgba(74, 222, 128, 0.4);
1529
+ }
1530
+ }
1531
+
1532
+ &.btn-copy {
1533
+ background-color: #e94560;
1534
+ color: #fff;
1535
+
1536
+ &:hover {
1537
+ background-color: #f95670;
1538
+ transform: translateY(-2px);
1539
+ box-shadow: 0 4px 12px rgba(233, 69, 96, 0.4);
1540
+ }
1541
+ }
1542
+
1543
+ &.btn-close-modal {
1544
+ background-color: #505050;
1545
+ color: #e0e0e0;
1546
+
1547
+ &:hover {
1548
+ background-color: #606060;
1549
+ }
1550
+ }
1551
+ }
1552
+ }
1553
+ }
1554
+ }
1555
+
1556
+ @keyframes pulse {
1557
+ 0%,
1558
+ 100% {
1559
+ opacity: 0.5;
1560
+ }
1561
+ 50% {
1562
+ opacity: 1;
1563
+ }
1564
+ }
1565
+
1566
+ /* Scrollbar styling */
1567
+ .controls-panel {
1568
+ &::-webkit-scrollbar {
1569
+ width: 8px;
1570
+ }
1571
+
1572
+ &::-webkit-scrollbar-track {
1573
+ background: #2a2a2a;
1574
+ }
1575
+
1576
+ &::-webkit-scrollbar-thumb {
1577
+ background: #505050;
1578
+ border-radius: 4px;
1579
+
1580
+ &:hover {
1581
+ background: #606060;
1582
+ }
1583
+ }
1584
+ }
1585
+
1586
+ .routine-content {
1587
+ &::-webkit-scrollbar {
1588
+ width: 6px;
1589
+ }
1590
+
1591
+ &::-webkit-scrollbar-track {
1592
+ background: #2a2a2a;
1593
+ }
1594
+
1595
+ &::-webkit-scrollbar-thumb {
1596
+ background: #505050;
1597
+ border-radius: 3px;
1598
+
1599
+ &:hover {
1600
+ background: #606060;
1601
+ }
1602
+ }
1603
+ }
1604
+ </style>
trigo-web/app/test_capture.js ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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();
6
+
7
+ console.log("=== Testing 2D Capture ===");
8
+ game.drop({ x: 2, y: 2, z: 0 }); // Black
9
+ console.log("After move 1, board[3][2][0]:", game.getBoard()[3][2][0]);
10
+ game.drop({ x: 3, y: 2, z: 0 }); // White (target)
11
+ console.log("After move 2, board[3][2][0]:", game.getBoard()[3][2][0]);
12
+ game.drop({ x: 4, y: 2, z: 0 }); // Black
13
+ console.log("After move 3, board[3][2][0]:", game.getBoard()[3][2][0]);
14
+ game.drop({ x: 3, y: 1, z: 0 }); // White elsewhere
15
+ game.drop({ x: 3, y: 3, z: 0 }); // Black
16
+ console.log("After move 5, board[3][2][0]:", game.getBoard()[3][2][0]);
17
+ game.drop({ x: 1, y: 1, z: 0 }); // White elsewhere
18
+ game.drop({ x: 2, y: 1, z: 0 }); // Black - should capture
19
+
20
+ console.log("\nAfter final move:");
21
+ console.log("board[3][2][0]:", game.getBoard()[3][2][0], "(should be 0/EMPTY if captured)");
22
+ console.log("Last step capturedPositions:", game.getLastStep()?.capturedPositions);
23
+ console.log("Captured counts:", game.getCapturedCounts());
trigo-web/app/vite.config.ts ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { defineConfig, loadEnv } from "vite";
2
+ import vue from "@vitejs/plugin-vue";
3
+ import { fileURLToPath, URL } from "node:url";
4
+
5
+ // https://vitejs.dev/config/
6
+ export default defineConfig(({ mode }) => {
7
+ // Load env file based on `mode` in the current working directory.
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: {
15
+ alias: {
16
+ "@": fileURLToPath(new URL("./src", import.meta.url)),
17
+ "@inc": fileURLToPath(new URL("../inc", import.meta.url))
18
+ }
19
+ },
20
+ server: {
21
+ host: env.VITE_HOST || "localhost",
22
+ port: parseInt(env.VITE_PORT) || 5173,
23
+ strictPort: true,
24
+ open: false
25
+ },
26
+ build: {
27
+ rollupOptions: {
28
+ external: ["/lib/tgnParser.cjs"]
29
+ }
30
+ },
31
+ css: {
32
+ preprocessorOptions: {
33
+ scss: {
34
+ api: "modern" // Use modern Sass API
35
+ }
36
+ }
37
+ },
38
+ define: {
39
+ "process.env": {}
40
+ }
41
+ };
42
+ });
trigo-web/backend/.env ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ PORT=3000
2
+ CLIENT_URL=http://localhost:5173
3
+ NODE_ENV=development
trigo-web/backend/package-lock.json ADDED
@@ -0,0 +1,1663 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "trigo-backend",
3
+ "version": "1.0.0",
4
+ "lockfileVersion": 3,
5
+ "requires": true,
6
+ "packages": {
7
+ "": {
8
+ "name": "trigo-backend",
9
+ "version": "1.0.0",
10
+ "license": "MIT",
11
+ "dependencies": {
12
+ "@types/uuid": "^11.0.0",
13
+ "cors": "^2.8.5",
14
+ "dotenv": "^16.6.1",
15
+ "express": "^4.21.2",
16
+ "socket.io": "^4.8.1",
17
+ "uuid": "^13.0.0"
18
+ },
19
+ "devDependencies": {
20
+ "@types/cors": "^2.8.13",
21
+ "@types/express": "^4.17.17",
22
+ "@types/node": "^20.5.0",
23
+ "nodemon": "^3.0.1",
24
+ "ts-node": "^10.9.1",
25
+ "typescript": "^5.2.2"
26
+ }
27
+ },
28
+ "node_modules/@cspotcode/source-map-support": {
29
+ "version": "0.8.1",
30
+ "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz",
31
+ "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==",
32
+ "dev": true,
33
+ "dependencies": {
34
+ "@jridgewell/trace-mapping": "0.3.9"
35
+ },
36
+ "engines": {
37
+ "node": ">=12"
38
+ }
39
+ },
40
+ "node_modules/@jridgewell/resolve-uri": {
41
+ "version": "3.1.2",
42
+ "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
43
+ "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
44
+ "dev": true,
45
+ "engines": {
46
+ "node": ">=6.0.0"
47
+ }
48
+ },
49
+ "node_modules/@jridgewell/sourcemap-codec": {
50
+ "version": "1.5.5",
51
+ "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
52
+ "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
53
+ "dev": true
54
+ },
55
+ "node_modules/@jridgewell/trace-mapping": {
56
+ "version": "0.3.9",
57
+ "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz",
58
+ "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==",
59
+ "dev": true,
60
+ "dependencies": {
61
+ "@jridgewell/resolve-uri": "^3.0.3",
62
+ "@jridgewell/sourcemap-codec": "^1.4.10"
63
+ }
64
+ },
65
+ "node_modules/@socket.io/component-emitter": {
66
+ "version": "3.1.2",
67
+ "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz",
68
+ "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA=="
69
+ },
70
+ "node_modules/@tsconfig/node10": {
71
+ "version": "1.0.11",
72
+ "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz",
73
+ "integrity": "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==",
74
+ "dev": true
75
+ },
76
+ "node_modules/@tsconfig/node12": {
77
+ "version": "1.0.11",
78
+ "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz",
79
+ "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==",
80
+ "dev": true
81
+ },
82
+ "node_modules/@tsconfig/node14": {
83
+ "version": "1.0.3",
84
+ "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz",
85
+ "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==",
86
+ "dev": true
87
+ },
88
+ "node_modules/@tsconfig/node16": {
89
+ "version": "1.0.4",
90
+ "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz",
91
+ "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==",
92
+ "dev": true
93
+ },
94
+ "node_modules/@types/body-parser": {
95
+ "version": "1.19.6",
96
+ "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz",
97
+ "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==",
98
+ "dev": true,
99
+ "dependencies": {
100
+ "@types/connect": "*",
101
+ "@types/node": "*"
102
+ }
103
+ },
104
+ "node_modules/@types/connect": {
105
+ "version": "3.4.38",
106
+ "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz",
107
+ "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==",
108
+ "dev": true,
109
+ "dependencies": {
110
+ "@types/node": "*"
111
+ }
112
+ },
113
+ "node_modules/@types/cors": {
114
+ "version": "2.8.19",
115
+ "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz",
116
+ "integrity": "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==",
117
+ "dependencies": {
118
+ "@types/node": "*"
119
+ }
120
+ },
121
+ "node_modules/@types/express": {
122
+ "version": "4.17.23",
123
+ "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.23.tgz",
124
+ "integrity": "sha512-Crp6WY9aTYP3qPi2wGDo9iUe/rceX01UMhnF1jmwDcKCFM6cx7YhGP/Mpr3y9AASpfHixIG0E6azCcL5OcDHsQ==",
125
+ "dev": true,
126
+ "dependencies": {
127
+ "@types/body-parser": "*",
128
+ "@types/express-serve-static-core": "^4.17.33",
129
+ "@types/qs": "*",
130
+ "@types/serve-static": "*"
131
+ }
132
+ },
133
+ "node_modules/@types/express-serve-static-core": {
134
+ "version": "4.19.7",
135
+ "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.7.tgz",
136
+ "integrity": "sha512-FvPtiIf1LfhzsaIXhv/PHan/2FeQBbtBDtfX2QfvPxdUelMDEckK08SM6nqo1MIZY3RUlfA+HV8+hFUSio78qg==",
137
+ "dev": true,
138
+ "dependencies": {
139
+ "@types/node": "*",
140
+ "@types/qs": "*",
141
+ "@types/range-parser": "*",
142
+ "@types/send": "*"
143
+ }
144
+ },
145
+ "node_modules/@types/http-errors": {
146
+ "version": "2.0.5",
147
+ "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz",
148
+ "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==",
149
+ "dev": true
150
+ },
151
+ "node_modules/@types/mime": {
152
+ "version": "1.3.5",
153
+ "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz",
154
+ "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==",
155
+ "dev": true
156
+ },
157
+ "node_modules/@types/node": {
158
+ "version": "20.19.23",
159
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.23.tgz",
160
+ "integrity": "sha512-yIdlVVVHXpmqRhtyovZAcSy0MiPcYWGkoO4CGe/+jpP0hmNuihm4XhHbADpK++MsiLHP5MVlv+bcgdF99kSiFQ==",
161
+ "dependencies": {
162
+ "undici-types": "~6.21.0"
163
+ }
164
+ },
165
+ "node_modules/@types/qs": {
166
+ "version": "6.14.0",
167
+ "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz",
168
+ "integrity": "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==",
169
+ "dev": true
170
+ },
171
+ "node_modules/@types/range-parser": {
172
+ "version": "1.2.7",
173
+ "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz",
174
+ "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==",
175
+ "dev": true
176
+ },
177
+ "node_modules/@types/send": {
178
+ "version": "1.2.0",
179
+ "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.0.tgz",
180
+ "integrity": "sha512-zBF6vZJn1IaMpg3xUF25VK3gd3l8zwE0ZLRX7dsQyQi+jp4E8mMDJNGDYnYse+bQhYwWERTxVwHpi3dMOq7RKQ==",
181
+ "dev": true,
182
+ "dependencies": {
183
+ "@types/node": "*"
184
+ }
185
+ },
186
+ "node_modules/@types/serve-static": {
187
+ "version": "1.15.9",
188
+ "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.9.tgz",
189
+ "integrity": "sha512-dOTIuqpWLyl3BBXU3maNQsS4A3zuuoYRNIvYSxxhebPfXg2mzWQEPne/nlJ37yOse6uGgR386uTpdsx4D0QZWA==",
190
+ "dev": true,
191
+ "dependencies": {
192
+ "@types/http-errors": "*",
193
+ "@types/node": "*",
194
+ "@types/send": "<1"
195
+ }
196
+ },
197
+ "node_modules/@types/serve-static/node_modules/@types/send": {
198
+ "version": "0.17.5",
199
+ "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.5.tgz",
200
+ "integrity": "sha512-z6F2D3cOStZvuk2SaP6YrwkNO65iTZcwA2ZkSABegdkAh/lf+Aa/YQndZVfmEXT5vgAp6zv06VQ3ejSVjAny4w==",
201
+ "dev": true,
202
+ "dependencies": {
203
+ "@types/mime": "^1",
204
+ "@types/node": "*"
205
+ }
206
+ },
207
+ "node_modules/@types/uuid": {
208
+ "version": "11.0.0",
209
+ "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-11.0.0.tgz",
210
+ "integrity": "sha512-HVyk8nj2m+jcFRNazzqyVKiZezyhDKrGUA3jlEcg/nZ6Ms+qHwocba1Y/AaVaznJTAM9xpdFSh+ptbNrhOGvZA==",
211
+ "deprecated": "This is a stub types definition. uuid provides its own type definitions, so you do not need this installed.",
212
+ "dependencies": {
213
+ "uuid": "*"
214
+ }
215
+ },
216
+ "node_modules/accepts": {
217
+ "version": "1.3.8",
218
+ "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
219
+ "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==",
220
+ "dependencies": {
221
+ "mime-types": "~2.1.34",
222
+ "negotiator": "0.6.3"
223
+ },
224
+ "engines": {
225
+ "node": ">= 0.6"
226
+ }
227
+ },
228
+ "node_modules/acorn": {
229
+ "version": "8.15.0",
230
+ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
231
+ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
232
+ "dev": true,
233
+ "bin": {
234
+ "acorn": "bin/acorn"
235
+ },
236
+ "engines": {
237
+ "node": ">=0.4.0"
238
+ }
239
+ },
240
+ "node_modules/acorn-walk": {
241
+ "version": "8.3.4",
242
+ "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz",
243
+ "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==",
244
+ "dev": true,
245
+ "dependencies": {
246
+ "acorn": "^8.11.0"
247
+ },
248
+ "engines": {
249
+ "node": ">=0.4.0"
250
+ }
251
+ },
252
+ "node_modules/anymatch": {
253
+ "version": "3.1.3",
254
+ "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
255
+ "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
256
+ "dev": true,
257
+ "dependencies": {
258
+ "normalize-path": "^3.0.0",
259
+ "picomatch": "^2.0.4"
260
+ },
261
+ "engines": {
262
+ "node": ">= 8"
263
+ }
264
+ },
265
+ "node_modules/arg": {
266
+ "version": "4.1.3",
267
+ "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz",
268
+ "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==",
269
+ "dev": true
270
+ },
271
+ "node_modules/array-flatten": {
272
+ "version": "1.1.1",
273
+ "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
274
+ "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg=="
275
+ },
276
+ "node_modules/balanced-match": {
277
+ "version": "1.0.2",
278
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
279
+ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
280
+ "dev": true
281
+ },
282
+ "node_modules/base64id": {
283
+ "version": "2.0.0",
284
+ "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz",
285
+ "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==",
286
+ "engines": {
287
+ "node": "^4.5.0 || >= 5.9"
288
+ }
289
+ },
290
+ "node_modules/binary-extensions": {
291
+ "version": "2.3.0",
292
+ "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
293
+ "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==",
294
+ "dev": true,
295
+ "engines": {
296
+ "node": ">=8"
297
+ },
298
+ "funding": {
299
+ "url": "https://github.com/sponsors/sindresorhus"
300
+ }
301
+ },
302
+ "node_modules/body-parser": {
303
+ "version": "1.20.3",
304
+ "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz",
305
+ "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==",
306
+ "dependencies": {
307
+ "bytes": "3.1.2",
308
+ "content-type": "~1.0.5",
309
+ "debug": "2.6.9",
310
+ "depd": "2.0.0",
311
+ "destroy": "1.2.0",
312
+ "http-errors": "2.0.0",
313
+ "iconv-lite": "0.4.24",
314
+ "on-finished": "2.4.1",
315
+ "qs": "6.13.0",
316
+ "raw-body": "2.5.2",
317
+ "type-is": "~1.6.18",
318
+ "unpipe": "1.0.0"
319
+ },
320
+ "engines": {
321
+ "node": ">= 0.8",
322
+ "npm": "1.2.8000 || >= 1.4.16"
323
+ }
324
+ },
325
+ "node_modules/brace-expansion": {
326
+ "version": "1.1.12",
327
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
328
+ "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
329
+ "dev": true,
330
+ "dependencies": {
331
+ "balanced-match": "^1.0.0",
332
+ "concat-map": "0.0.1"
333
+ }
334
+ },
335
+ "node_modules/braces": {
336
+ "version": "3.0.3",
337
+ "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
338
+ "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
339
+ "dev": true,
340
+ "dependencies": {
341
+ "fill-range": "^7.1.1"
342
+ },
343
+ "engines": {
344
+ "node": ">=8"
345
+ }
346
+ },
347
+ "node_modules/bytes": {
348
+ "version": "3.1.2",
349
+ "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
350
+ "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
351
+ "engines": {
352
+ "node": ">= 0.8"
353
+ }
354
+ },
355
+ "node_modules/call-bind-apply-helpers": {
356
+ "version": "1.0.2",
357
+ "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
358
+ "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
359
+ "dependencies": {
360
+ "es-errors": "^1.3.0",
361
+ "function-bind": "^1.1.2"
362
+ },
363
+ "engines": {
364
+ "node": ">= 0.4"
365
+ }
366
+ },
367
+ "node_modules/call-bound": {
368
+ "version": "1.0.4",
369
+ "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
370
+ "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
371
+ "dependencies": {
372
+ "call-bind-apply-helpers": "^1.0.2",
373
+ "get-intrinsic": "^1.3.0"
374
+ },
375
+ "engines": {
376
+ "node": ">= 0.4"
377
+ },
378
+ "funding": {
379
+ "url": "https://github.com/sponsors/ljharb"
380
+ }
381
+ },
382
+ "node_modules/chokidar": {
383
+ "version": "3.6.0",
384
+ "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
385
+ "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
386
+ "dev": true,
387
+ "dependencies": {
388
+ "anymatch": "~3.1.2",
389
+ "braces": "~3.0.2",
390
+ "glob-parent": "~5.1.2",
391
+ "is-binary-path": "~2.1.0",
392
+ "is-glob": "~4.0.1",
393
+ "normalize-path": "~3.0.0",
394
+ "readdirp": "~3.6.0"
395
+ },
396
+ "engines": {
397
+ "node": ">= 8.10.0"
398
+ },
399
+ "funding": {
400
+ "url": "https://paulmillr.com/funding/"
401
+ },
402
+ "optionalDependencies": {
403
+ "fsevents": "~2.3.2"
404
+ }
405
+ },
406
+ "node_modules/concat-map": {
407
+ "version": "0.0.1",
408
+ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
409
+ "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
410
+ "dev": true
411
+ },
412
+ "node_modules/content-disposition": {
413
+ "version": "0.5.4",
414
+ "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
415
+ "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==",
416
+ "dependencies": {
417
+ "safe-buffer": "5.2.1"
418
+ },
419
+ "engines": {
420
+ "node": ">= 0.6"
421
+ }
422
+ },
423
+ "node_modules/content-type": {
424
+ "version": "1.0.5",
425
+ "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
426
+ "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
427
+ "engines": {
428
+ "node": ">= 0.6"
429
+ }
430
+ },
431
+ "node_modules/cookie": {
432
+ "version": "0.7.1",
433
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz",
434
+ "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==",
435
+ "engines": {
436
+ "node": ">= 0.6"
437
+ }
438
+ },
439
+ "node_modules/cookie-signature": {
440
+ "version": "1.0.6",
441
+ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
442
+ "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ=="
443
+ },
444
+ "node_modules/cors": {
445
+ "version": "2.8.5",
446
+ "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz",
447
+ "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==",
448
+ "dependencies": {
449
+ "object-assign": "^4",
450
+ "vary": "^1"
451
+ },
452
+ "engines": {
453
+ "node": ">= 0.10"
454
+ }
455
+ },
456
+ "node_modules/create-require": {
457
+ "version": "1.1.1",
458
+ "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz",
459
+ "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==",
460
+ "dev": true
461
+ },
462
+ "node_modules/debug": {
463
+ "version": "2.6.9",
464
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
465
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
466
+ "dependencies": {
467
+ "ms": "2.0.0"
468
+ }
469
+ },
470
+ "node_modules/depd": {
471
+ "version": "2.0.0",
472
+ "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
473
+ "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
474
+ "engines": {
475
+ "node": ">= 0.8"
476
+ }
477
+ },
478
+ "node_modules/destroy": {
479
+ "version": "1.2.0",
480
+ "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz",
481
+ "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==",
482
+ "engines": {
483
+ "node": ">= 0.8",
484
+ "npm": "1.2.8000 || >= 1.4.16"
485
+ }
486
+ },
487
+ "node_modules/diff": {
488
+ "version": "4.0.2",
489
+ "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz",
490
+ "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==",
491
+ "dev": true,
492
+ "engines": {
493
+ "node": ">=0.3.1"
494
+ }
495
+ },
496
+ "node_modules/dotenv": {
497
+ "version": "16.6.1",
498
+ "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz",
499
+ "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==",
500
+ "engines": {
501
+ "node": ">=12"
502
+ },
503
+ "funding": {
504
+ "url": "https://dotenvx.com"
505
+ }
506
+ },
507
+ "node_modules/dunder-proto": {
508
+ "version": "1.0.1",
509
+ "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
510
+ "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
511
+ "dependencies": {
512
+ "call-bind-apply-helpers": "^1.0.1",
513
+ "es-errors": "^1.3.0",
514
+ "gopd": "^1.2.0"
515
+ },
516
+ "engines": {
517
+ "node": ">= 0.4"
518
+ }
519
+ },
520
+ "node_modules/ee-first": {
521
+ "version": "1.1.1",
522
+ "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
523
+ "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="
524
+ },
525
+ "node_modules/encodeurl": {
526
+ "version": "2.0.0",
527
+ "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
528
+ "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
529
+ "engines": {
530
+ "node": ">= 0.8"
531
+ }
532
+ },
533
+ "node_modules/engine.io": {
534
+ "version": "6.6.4",
535
+ "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.4.tgz",
536
+ "integrity": "sha512-ZCkIjSYNDyGn0R6ewHDtXgns/Zre/NT6Agvq1/WobF7JXgFff4SeDroKiCO3fNJreU9YG429Sc81o4w5ok/W5g==",
537
+ "dependencies": {
538
+ "@types/cors": "^2.8.12",
539
+ "@types/node": ">=10.0.0",
540
+ "accepts": "~1.3.4",
541
+ "base64id": "2.0.0",
542
+ "cookie": "~0.7.2",
543
+ "cors": "~2.8.5",
544
+ "debug": "~4.3.1",
545
+ "engine.io-parser": "~5.2.1",
546
+ "ws": "~8.17.1"
547
+ },
548
+ "engines": {
549
+ "node": ">=10.2.0"
550
+ }
551
+ },
552
+ "node_modules/engine.io-parser": {
553
+ "version": "5.2.3",
554
+ "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz",
555
+ "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==",
556
+ "engines": {
557
+ "node": ">=10.0.0"
558
+ }
559
+ },
560
+ "node_modules/engine.io/node_modules/cookie": {
561
+ "version": "0.7.2",
562
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
563
+ "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
564
+ "engines": {
565
+ "node": ">= 0.6"
566
+ }
567
+ },
568
+ "node_modules/engine.io/node_modules/debug": {
569
+ "version": "4.3.7",
570
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
571
+ "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
572
+ "dependencies": {
573
+ "ms": "^2.1.3"
574
+ },
575
+ "engines": {
576
+ "node": ">=6.0"
577
+ },
578
+ "peerDependenciesMeta": {
579
+ "supports-color": {
580
+ "optional": true
581
+ }
582
+ }
583
+ },
584
+ "node_modules/engine.io/node_modules/ms": {
585
+ "version": "2.1.3",
586
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
587
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
588
+ },
589
+ "node_modules/es-define-property": {
590
+ "version": "1.0.1",
591
+ "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
592
+ "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
593
+ "engines": {
594
+ "node": ">= 0.4"
595
+ }
596
+ },
597
+ "node_modules/es-errors": {
598
+ "version": "1.3.0",
599
+ "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
600
+ "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
601
+ "engines": {
602
+ "node": ">= 0.4"
603
+ }
604
+ },
605
+ "node_modules/es-object-atoms": {
606
+ "version": "1.1.1",
607
+ "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
608
+ "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
609
+ "dependencies": {
610
+ "es-errors": "^1.3.0"
611
+ },
612
+ "engines": {
613
+ "node": ">= 0.4"
614
+ }
615
+ },
616
+ "node_modules/escape-html": {
617
+ "version": "1.0.3",
618
+ "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
619
+ "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="
620
+ },
621
+ "node_modules/etag": {
622
+ "version": "1.8.1",
623
+ "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
624
+ "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
625
+ "engines": {
626
+ "node": ">= 0.6"
627
+ }
628
+ },
629
+ "node_modules/express": {
630
+ "version": "4.21.2",
631
+ "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz",
632
+ "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==",
633
+ "dependencies": {
634
+ "accepts": "~1.3.8",
635
+ "array-flatten": "1.1.1",
636
+ "body-parser": "1.20.3",
637
+ "content-disposition": "0.5.4",
638
+ "content-type": "~1.0.4",
639
+ "cookie": "0.7.1",
640
+ "cookie-signature": "1.0.6",
641
+ "debug": "2.6.9",
642
+ "depd": "2.0.0",
643
+ "encodeurl": "~2.0.0",
644
+ "escape-html": "~1.0.3",
645
+ "etag": "~1.8.1",
646
+ "finalhandler": "1.3.1",
647
+ "fresh": "0.5.2",
648
+ "http-errors": "2.0.0",
649
+ "merge-descriptors": "1.0.3",
650
+ "methods": "~1.1.2",
651
+ "on-finished": "2.4.1",
652
+ "parseurl": "~1.3.3",
653
+ "path-to-regexp": "0.1.12",
654
+ "proxy-addr": "~2.0.7",
655
+ "qs": "6.13.0",
656
+ "range-parser": "~1.2.1",
657
+ "safe-buffer": "5.2.1",
658
+ "send": "0.19.0",
659
+ "serve-static": "1.16.2",
660
+ "setprototypeof": "1.2.0",
661
+ "statuses": "2.0.1",
662
+ "type-is": "~1.6.18",
663
+ "utils-merge": "1.0.1",
664
+ "vary": "~1.1.2"
665
+ },
666
+ "engines": {
667
+ "node": ">= 0.10.0"
668
+ },
669
+ "funding": {
670
+ "type": "opencollective",
671
+ "url": "https://opencollective.com/express"
672
+ }
673
+ },
674
+ "node_modules/fill-range": {
675
+ "version": "7.1.1",
676
+ "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
677
+ "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
678
+ "dev": true,
679
+ "dependencies": {
680
+ "to-regex-range": "^5.0.1"
681
+ },
682
+ "engines": {
683
+ "node": ">=8"
684
+ }
685
+ },
686
+ "node_modules/finalhandler": {
687
+ "version": "1.3.1",
688
+ "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz",
689
+ "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==",
690
+ "dependencies": {
691
+ "debug": "2.6.9",
692
+ "encodeurl": "~2.0.0",
693
+ "escape-html": "~1.0.3",
694
+ "on-finished": "2.4.1",
695
+ "parseurl": "~1.3.3",
696
+ "statuses": "2.0.1",
697
+ "unpipe": "~1.0.0"
698
+ },
699
+ "engines": {
700
+ "node": ">= 0.8"
701
+ }
702
+ },
703
+ "node_modules/forwarded": {
704
+ "version": "0.2.0",
705
+ "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
706
+ "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
707
+ "engines": {
708
+ "node": ">= 0.6"
709
+ }
710
+ },
711
+ "node_modules/fresh": {
712
+ "version": "0.5.2",
713
+ "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
714
+ "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==",
715
+ "engines": {
716
+ "node": ">= 0.6"
717
+ }
718
+ },
719
+ "node_modules/fsevents": {
720
+ "version": "2.3.3",
721
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
722
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
723
+ "dev": true,
724
+ "hasInstallScript": true,
725
+ "optional": true,
726
+ "os": [
727
+ "darwin"
728
+ ],
729
+ "engines": {
730
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
731
+ }
732
+ },
733
+ "node_modules/function-bind": {
734
+ "version": "1.1.2",
735
+ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
736
+ "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
737
+ "funding": {
738
+ "url": "https://github.com/sponsors/ljharb"
739
+ }
740
+ },
741
+ "node_modules/get-intrinsic": {
742
+ "version": "1.3.0",
743
+ "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
744
+ "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
745
+ "dependencies": {
746
+ "call-bind-apply-helpers": "^1.0.2",
747
+ "es-define-property": "^1.0.1",
748
+ "es-errors": "^1.3.0",
749
+ "es-object-atoms": "^1.1.1",
750
+ "function-bind": "^1.1.2",
751
+ "get-proto": "^1.0.1",
752
+ "gopd": "^1.2.0",
753
+ "has-symbols": "^1.1.0",
754
+ "hasown": "^2.0.2",
755
+ "math-intrinsics": "^1.1.0"
756
+ },
757
+ "engines": {
758
+ "node": ">= 0.4"
759
+ },
760
+ "funding": {
761
+ "url": "https://github.com/sponsors/ljharb"
762
+ }
763
+ },
764
+ "node_modules/get-proto": {
765
+ "version": "1.0.1",
766
+ "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
767
+ "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
768
+ "dependencies": {
769
+ "dunder-proto": "^1.0.1",
770
+ "es-object-atoms": "^1.0.0"
771
+ },
772
+ "engines": {
773
+ "node": ">= 0.4"
774
+ }
775
+ },
776
+ "node_modules/glob-parent": {
777
+ "version": "5.1.2",
778
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
779
+ "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
780
+ "dev": true,
781
+ "dependencies": {
782
+ "is-glob": "^4.0.1"
783
+ },
784
+ "engines": {
785
+ "node": ">= 6"
786
+ }
787
+ },
788
+ "node_modules/gopd": {
789
+ "version": "1.2.0",
790
+ "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
791
+ "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
792
+ "engines": {
793
+ "node": ">= 0.4"
794
+ },
795
+ "funding": {
796
+ "url": "https://github.com/sponsors/ljharb"
797
+ }
798
+ },
799
+ "node_modules/has-flag": {
800
+ "version": "3.0.0",
801
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
802
+ "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==",
803
+ "dev": true,
804
+ "engines": {
805
+ "node": ">=4"
806
+ }
807
+ },
808
+ "node_modules/has-symbols": {
809
+ "version": "1.1.0",
810
+ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
811
+ "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
812
+ "engines": {
813
+ "node": ">= 0.4"
814
+ },
815
+ "funding": {
816
+ "url": "https://github.com/sponsors/ljharb"
817
+ }
818
+ },
819
+ "node_modules/hasown": {
820
+ "version": "2.0.2",
821
+ "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
822
+ "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
823
+ "dependencies": {
824
+ "function-bind": "^1.1.2"
825
+ },
826
+ "engines": {
827
+ "node": ">= 0.4"
828
+ }
829
+ },
830
+ "node_modules/http-errors": {
831
+ "version": "2.0.0",
832
+ "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz",
833
+ "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==",
834
+ "dependencies": {
835
+ "depd": "2.0.0",
836
+ "inherits": "2.0.4",
837
+ "setprototypeof": "1.2.0",
838
+ "statuses": "2.0.1",
839
+ "toidentifier": "1.0.1"
840
+ },
841
+ "engines": {
842
+ "node": ">= 0.8"
843
+ }
844
+ },
845
+ "node_modules/iconv-lite": {
846
+ "version": "0.4.24",
847
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
848
+ "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
849
+ "dependencies": {
850
+ "safer-buffer": ">= 2.1.2 < 3"
851
+ },
852
+ "engines": {
853
+ "node": ">=0.10.0"
854
+ }
855
+ },
856
+ "node_modules/ignore-by-default": {
857
+ "version": "1.0.1",
858
+ "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz",
859
+ "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==",
860
+ "dev": true
861
+ },
862
+ "node_modules/inherits": {
863
+ "version": "2.0.4",
864
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
865
+ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
866
+ },
867
+ "node_modules/ipaddr.js": {
868
+ "version": "1.9.1",
869
+ "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
870
+ "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
871
+ "engines": {
872
+ "node": ">= 0.10"
873
+ }
874
+ },
875
+ "node_modules/is-binary-path": {
876
+ "version": "2.1.0",
877
+ "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
878
+ "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
879
+ "dev": true,
880
+ "dependencies": {
881
+ "binary-extensions": "^2.0.0"
882
+ },
883
+ "engines": {
884
+ "node": ">=8"
885
+ }
886
+ },
887
+ "node_modules/is-extglob": {
888
+ "version": "2.1.1",
889
+ "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
890
+ "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
891
+ "dev": true,
892
+ "engines": {
893
+ "node": ">=0.10.0"
894
+ }
895
+ },
896
+ "node_modules/is-glob": {
897
+ "version": "4.0.3",
898
+ "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
899
+ "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
900
+ "dev": true,
901
+ "dependencies": {
902
+ "is-extglob": "^2.1.1"
903
+ },
904
+ "engines": {
905
+ "node": ">=0.10.0"
906
+ }
907
+ },
908
+ "node_modules/is-number": {
909
+ "version": "7.0.0",
910
+ "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
911
+ "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
912
+ "dev": true,
913
+ "engines": {
914
+ "node": ">=0.12.0"
915
+ }
916
+ },
917
+ "node_modules/make-error": {
918
+ "version": "1.3.6",
919
+ "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz",
920
+ "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==",
921
+ "dev": true
922
+ },
923
+ "node_modules/math-intrinsics": {
924
+ "version": "1.1.0",
925
+ "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
926
+ "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
927
+ "engines": {
928
+ "node": ">= 0.4"
929
+ }
930
+ },
931
+ "node_modules/media-typer": {
932
+ "version": "0.3.0",
933
+ "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
934
+ "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==",
935
+ "engines": {
936
+ "node": ">= 0.6"
937
+ }
938
+ },
939
+ "node_modules/merge-descriptors": {
940
+ "version": "1.0.3",
941
+ "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz",
942
+ "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==",
943
+ "funding": {
944
+ "url": "https://github.com/sponsors/sindresorhus"
945
+ }
946
+ },
947
+ "node_modules/methods": {
948
+ "version": "1.1.2",
949
+ "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
950
+ "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==",
951
+ "engines": {
952
+ "node": ">= 0.6"
953
+ }
954
+ },
955
+ "node_modules/mime": {
956
+ "version": "1.6.0",
957
+ "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
958
+ "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
959
+ "bin": {
960
+ "mime": "cli.js"
961
+ },
962
+ "engines": {
963
+ "node": ">=4"
964
+ }
965
+ },
966
+ "node_modules/mime-db": {
967
+ "version": "1.52.0",
968
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
969
+ "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
970
+ "engines": {
971
+ "node": ">= 0.6"
972
+ }
973
+ },
974
+ "node_modules/mime-types": {
975
+ "version": "2.1.35",
976
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
977
+ "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
978
+ "dependencies": {
979
+ "mime-db": "1.52.0"
980
+ },
981
+ "engines": {
982
+ "node": ">= 0.6"
983
+ }
984
+ },
985
+ "node_modules/minimatch": {
986
+ "version": "3.1.2",
987
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
988
+ "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
989
+ "dev": true,
990
+ "dependencies": {
991
+ "brace-expansion": "^1.1.7"
992
+ },
993
+ "engines": {
994
+ "node": "*"
995
+ }
996
+ },
997
+ "node_modules/ms": {
998
+ "version": "2.0.0",
999
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
1000
+ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="
1001
+ },
1002
+ "node_modules/negotiator": {
1003
+ "version": "0.6.3",
1004
+ "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
1005
+ "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==",
1006
+ "engines": {
1007
+ "node": ">= 0.6"
1008
+ }
1009
+ },
1010
+ "node_modules/nodemon": {
1011
+ "version": "3.1.10",
1012
+ "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.10.tgz",
1013
+ "integrity": "sha512-WDjw3pJ0/0jMFmyNDp3gvY2YizjLmmOUQo6DEBY+JgdvW/yQ9mEeSw6H5ythl5Ny2ytb7f9C2nIbjSxMNzbJXw==",
1014
+ "dev": true,
1015
+ "dependencies": {
1016
+ "chokidar": "^3.5.2",
1017
+ "debug": "^4",
1018
+ "ignore-by-default": "^1.0.1",
1019
+ "minimatch": "^3.1.2",
1020
+ "pstree.remy": "^1.1.8",
1021
+ "semver": "^7.5.3",
1022
+ "simple-update-notifier": "^2.0.0",
1023
+ "supports-color": "^5.5.0",
1024
+ "touch": "^3.1.0",
1025
+ "undefsafe": "^2.0.5"
1026
+ },
1027
+ "bin": {
1028
+ "nodemon": "bin/nodemon.js"
1029
+ },
1030
+ "engines": {
1031
+ "node": ">=10"
1032
+ },
1033
+ "funding": {
1034
+ "type": "opencollective",
1035
+ "url": "https://opencollective.com/nodemon"
1036
+ }
1037
+ },
1038
+ "node_modules/nodemon/node_modules/debug": {
1039
+ "version": "4.4.3",
1040
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
1041
+ "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
1042
+ "dev": true,
1043
+ "dependencies": {
1044
+ "ms": "^2.1.3"
1045
+ },
1046
+ "engines": {
1047
+ "node": ">=6.0"
1048
+ },
1049
+ "peerDependenciesMeta": {
1050
+ "supports-color": {
1051
+ "optional": true
1052
+ }
1053
+ }
1054
+ },
1055
+ "node_modules/nodemon/node_modules/ms": {
1056
+ "version": "2.1.3",
1057
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
1058
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
1059
+ "dev": true
1060
+ },
1061
+ "node_modules/normalize-path": {
1062
+ "version": "3.0.0",
1063
+ "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
1064
+ "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
1065
+ "dev": true,
1066
+ "engines": {
1067
+ "node": ">=0.10.0"
1068
+ }
1069
+ },
1070
+ "node_modules/object-assign": {
1071
+ "version": "4.1.1",
1072
+ "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
1073
+ "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
1074
+ "engines": {
1075
+ "node": ">=0.10.0"
1076
+ }
1077
+ },
1078
+ "node_modules/object-inspect": {
1079
+ "version": "1.13.4",
1080
+ "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
1081
+ "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
1082
+ "engines": {
1083
+ "node": ">= 0.4"
1084
+ },
1085
+ "funding": {
1086
+ "url": "https://github.com/sponsors/ljharb"
1087
+ }
1088
+ },
1089
+ "node_modules/on-finished": {
1090
+ "version": "2.4.1",
1091
+ "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
1092
+ "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
1093
+ "dependencies": {
1094
+ "ee-first": "1.1.1"
1095
+ },
1096
+ "engines": {
1097
+ "node": ">= 0.8"
1098
+ }
1099
+ },
1100
+ "node_modules/parseurl": {
1101
+ "version": "1.3.3",
1102
+ "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
1103
+ "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
1104
+ "engines": {
1105
+ "node": ">= 0.8"
1106
+ }
1107
+ },
1108
+ "node_modules/path-to-regexp": {
1109
+ "version": "0.1.12",
1110
+ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz",
1111
+ "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ=="
1112
+ },
1113
+ "node_modules/picomatch": {
1114
+ "version": "2.3.1",
1115
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
1116
+ "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
1117
+ "dev": true,
1118
+ "engines": {
1119
+ "node": ">=8.6"
1120
+ },
1121
+ "funding": {
1122
+ "url": "https://github.com/sponsors/jonschlinkert"
1123
+ }
1124
+ },
1125
+ "node_modules/proxy-addr": {
1126
+ "version": "2.0.7",
1127
+ "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
1128
+ "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
1129
+ "dependencies": {
1130
+ "forwarded": "0.2.0",
1131
+ "ipaddr.js": "1.9.1"
1132
+ },
1133
+ "engines": {
1134
+ "node": ">= 0.10"
1135
+ }
1136
+ },
1137
+ "node_modules/pstree.remy": {
1138
+ "version": "1.1.8",
1139
+ "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz",
1140
+ "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==",
1141
+ "dev": true
1142
+ },
1143
+ "node_modules/qs": {
1144
+ "version": "6.13.0",
1145
+ "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz",
1146
+ "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==",
1147
+ "dependencies": {
1148
+ "side-channel": "^1.0.6"
1149
+ },
1150
+ "engines": {
1151
+ "node": ">=0.6"
1152
+ },
1153
+ "funding": {
1154
+ "url": "https://github.com/sponsors/ljharb"
1155
+ }
1156
+ },
1157
+ "node_modules/range-parser": {
1158
+ "version": "1.2.1",
1159
+ "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
1160
+ "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
1161
+ "engines": {
1162
+ "node": ">= 0.6"
1163
+ }
1164
+ },
1165
+ "node_modules/raw-body": {
1166
+ "version": "2.5.2",
1167
+ "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz",
1168
+ "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==",
1169
+ "dependencies": {
1170
+ "bytes": "3.1.2",
1171
+ "http-errors": "2.0.0",
1172
+ "iconv-lite": "0.4.24",
1173
+ "unpipe": "1.0.0"
1174
+ },
1175
+ "engines": {
1176
+ "node": ">= 0.8"
1177
+ }
1178
+ },
1179
+ "node_modules/readdirp": {
1180
+ "version": "3.6.0",
1181
+ "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
1182
+ "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
1183
+ "dev": true,
1184
+ "dependencies": {
1185
+ "picomatch": "^2.2.1"
1186
+ },
1187
+ "engines": {
1188
+ "node": ">=8.10.0"
1189
+ }
1190
+ },
1191
+ "node_modules/safe-buffer": {
1192
+ "version": "5.2.1",
1193
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
1194
+ "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
1195
+ "funding": [
1196
+ {
1197
+ "type": "github",
1198
+ "url": "https://github.com/sponsors/feross"
1199
+ },
1200
+ {
1201
+ "type": "patreon",
1202
+ "url": "https://www.patreon.com/feross"
1203
+ },
1204
+ {
1205
+ "type": "consulting",
1206
+ "url": "https://feross.org/support"
1207
+ }
1208
+ ]
1209
+ },
1210
+ "node_modules/safer-buffer": {
1211
+ "version": "2.1.2",
1212
+ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
1213
+ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
1214
+ },
1215
+ "node_modules/semver": {
1216
+ "version": "7.7.3",
1217
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
1218
+ "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==",
1219
+ "dev": true,
1220
+ "bin": {
1221
+ "semver": "bin/semver.js"
1222
+ },
1223
+ "engines": {
1224
+ "node": ">=10"
1225
+ }
1226
+ },
1227
+ "node_modules/send": {
1228
+ "version": "0.19.0",
1229
+ "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz",
1230
+ "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==",
1231
+ "dependencies": {
1232
+ "debug": "2.6.9",
1233
+ "depd": "2.0.0",
1234
+ "destroy": "1.2.0",
1235
+ "encodeurl": "~1.0.2",
1236
+ "escape-html": "~1.0.3",
1237
+ "etag": "~1.8.1",
1238
+ "fresh": "0.5.2",
1239
+ "http-errors": "2.0.0",
1240
+ "mime": "1.6.0",
1241
+ "ms": "2.1.3",
1242
+ "on-finished": "2.4.1",
1243
+ "range-parser": "~1.2.1",
1244
+ "statuses": "2.0.1"
1245
+ },
1246
+ "engines": {
1247
+ "node": ">= 0.8.0"
1248
+ }
1249
+ },
1250
+ "node_modules/send/node_modules/encodeurl": {
1251
+ "version": "1.0.2",
1252
+ "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
1253
+ "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==",
1254
+ "engines": {
1255
+ "node": ">= 0.8"
1256
+ }
1257
+ },
1258
+ "node_modules/send/node_modules/ms": {
1259
+ "version": "2.1.3",
1260
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
1261
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
1262
+ },
1263
+ "node_modules/serve-static": {
1264
+ "version": "1.16.2",
1265
+ "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz",
1266
+ "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==",
1267
+ "dependencies": {
1268
+ "encodeurl": "~2.0.0",
1269
+ "escape-html": "~1.0.3",
1270
+ "parseurl": "~1.3.3",
1271
+ "send": "0.19.0"
1272
+ },
1273
+ "engines": {
1274
+ "node": ">= 0.8.0"
1275
+ }
1276
+ },
1277
+ "node_modules/setprototypeof": {
1278
+ "version": "1.2.0",
1279
+ "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
1280
+ "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="
1281
+ },
1282
+ "node_modules/side-channel": {
1283
+ "version": "1.1.0",
1284
+ "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
1285
+ "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==",
1286
+ "dependencies": {
1287
+ "es-errors": "^1.3.0",
1288
+ "object-inspect": "^1.13.3",
1289
+ "side-channel-list": "^1.0.0",
1290
+ "side-channel-map": "^1.0.1",
1291
+ "side-channel-weakmap": "^1.0.2"
1292
+ },
1293
+ "engines": {
1294
+ "node": ">= 0.4"
1295
+ },
1296
+ "funding": {
1297
+ "url": "https://github.com/sponsors/ljharb"
1298
+ }
1299
+ },
1300
+ "node_modules/side-channel-list": {
1301
+ "version": "1.0.0",
1302
+ "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz",
1303
+ "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==",
1304
+ "dependencies": {
1305
+ "es-errors": "^1.3.0",
1306
+ "object-inspect": "^1.13.3"
1307
+ },
1308
+ "engines": {
1309
+ "node": ">= 0.4"
1310
+ },
1311
+ "funding": {
1312
+ "url": "https://github.com/sponsors/ljharb"
1313
+ }
1314
+ },
1315
+ "node_modules/side-channel-map": {
1316
+ "version": "1.0.1",
1317
+ "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
1318
+ "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
1319
+ "dependencies": {
1320
+ "call-bound": "^1.0.2",
1321
+ "es-errors": "^1.3.0",
1322
+ "get-intrinsic": "^1.2.5",
1323
+ "object-inspect": "^1.13.3"
1324
+ },
1325
+ "engines": {
1326
+ "node": ">= 0.4"
1327
+ },
1328
+ "funding": {
1329
+ "url": "https://github.com/sponsors/ljharb"
1330
+ }
1331
+ },
1332
+ "node_modules/side-channel-weakmap": {
1333
+ "version": "1.0.2",
1334
+ "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
1335
+ "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
1336
+ "dependencies": {
1337
+ "call-bound": "^1.0.2",
1338
+ "es-errors": "^1.3.0",
1339
+ "get-intrinsic": "^1.2.5",
1340
+ "object-inspect": "^1.13.3",
1341
+ "side-channel-map": "^1.0.1"
1342
+ },
1343
+ "engines": {
1344
+ "node": ">= 0.4"
1345
+ },
1346
+ "funding": {
1347
+ "url": "https://github.com/sponsors/ljharb"
1348
+ }
1349
+ },
1350
+ "node_modules/simple-update-notifier": {
1351
+ "version": "2.0.0",
1352
+ "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz",
1353
+ "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==",
1354
+ "dev": true,
1355
+ "dependencies": {
1356
+ "semver": "^7.5.3"
1357
+ },
1358
+ "engines": {
1359
+ "node": ">=10"
1360
+ }
1361
+ },
1362
+ "node_modules/socket.io": {
1363
+ "version": "4.8.1",
1364
+ "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.1.tgz",
1365
+ "integrity": "sha512-oZ7iUCxph8WYRHHcjBEc9unw3adt5CmSNlppj/5Q4k2RIrhl8Z5yY2Xr4j9zj0+wzVZ0bxmYoGSzKJnRl6A4yg==",
1366
+ "dependencies": {
1367
+ "accepts": "~1.3.4",
1368
+ "base64id": "~2.0.0",
1369
+ "cors": "~2.8.5",
1370
+ "debug": "~4.3.2",
1371
+ "engine.io": "~6.6.0",
1372
+ "socket.io-adapter": "~2.5.2",
1373
+ "socket.io-parser": "~4.2.4"
1374
+ },
1375
+ "engines": {
1376
+ "node": ">=10.2.0"
1377
+ }
1378
+ },
1379
+ "node_modules/socket.io-adapter": {
1380
+ "version": "2.5.5",
1381
+ "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.5.tgz",
1382
+ "integrity": "sha512-eLDQas5dzPgOWCk9GuuJC2lBqItuhKI4uxGgo9aIV7MYbk2h9Q6uULEh8WBzThoI7l+qU9Ast9fVUmkqPP9wYg==",
1383
+ "dependencies": {
1384
+ "debug": "~4.3.4",
1385
+ "ws": "~8.17.1"
1386
+ }
1387
+ },
1388
+ "node_modules/socket.io-adapter/node_modules/debug": {
1389
+ "version": "4.3.7",
1390
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
1391
+ "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
1392
+ "dependencies": {
1393
+ "ms": "^2.1.3"
1394
+ },
1395
+ "engines": {
1396
+ "node": ">=6.0"
1397
+ },
1398
+ "peerDependenciesMeta": {
1399
+ "supports-color": {
1400
+ "optional": true
1401
+ }
1402
+ }
1403
+ },
1404
+ "node_modules/socket.io-adapter/node_modules/ms": {
1405
+ "version": "2.1.3",
1406
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
1407
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
1408
+ },
1409
+ "node_modules/socket.io-parser": {
1410
+ "version": "4.2.4",
1411
+ "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz",
1412
+ "integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==",
1413
+ "dependencies": {
1414
+ "@socket.io/component-emitter": "~3.1.0",
1415
+ "debug": "~4.3.1"
1416
+ },
1417
+ "engines": {
1418
+ "node": ">=10.0.0"
1419
+ }
1420
+ },
1421
+ "node_modules/socket.io-parser/node_modules/debug": {
1422
+ "version": "4.3.7",
1423
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
1424
+ "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
1425
+ "dependencies": {
1426
+ "ms": "^2.1.3"
1427
+ },
1428
+ "engines": {
1429
+ "node": ">=6.0"
1430
+ },
1431
+ "peerDependenciesMeta": {
1432
+ "supports-color": {
1433
+ "optional": true
1434
+ }
1435
+ }
1436
+ },
1437
+ "node_modules/socket.io-parser/node_modules/ms": {
1438
+ "version": "2.1.3",
1439
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
1440
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
1441
+ },
1442
+ "node_modules/socket.io/node_modules/debug": {
1443
+ "version": "4.3.7",
1444
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
1445
+ "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
1446
+ "dependencies": {
1447
+ "ms": "^2.1.3"
1448
+ },
1449
+ "engines": {
1450
+ "node": ">=6.0"
1451
+ },
1452
+ "peerDependenciesMeta": {
1453
+ "supports-color": {
1454
+ "optional": true
1455
+ }
1456
+ }
1457
+ },
1458
+ "node_modules/socket.io/node_modules/ms": {
1459
+ "version": "2.1.3",
1460
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
1461
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
1462
+ },
1463
+ "node_modules/statuses": {
1464
+ "version": "2.0.1",
1465
+ "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
1466
+ "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==",
1467
+ "engines": {
1468
+ "node": ">= 0.8"
1469
+ }
1470
+ },
1471
+ "node_modules/supports-color": {
1472
+ "version": "5.5.0",
1473
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
1474
+ "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
1475
+ "dev": true,
1476
+ "dependencies": {
1477
+ "has-flag": "^3.0.0"
1478
+ },
1479
+ "engines": {
1480
+ "node": ">=4"
1481
+ }
1482
+ },
1483
+ "node_modules/to-regex-range": {
1484
+ "version": "5.0.1",
1485
+ "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
1486
+ "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
1487
+ "dev": true,
1488
+ "dependencies": {
1489
+ "is-number": "^7.0.0"
1490
+ },
1491
+ "engines": {
1492
+ "node": ">=8.0"
1493
+ }
1494
+ },
1495
+ "node_modules/toidentifier": {
1496
+ "version": "1.0.1",
1497
+ "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
1498
+ "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
1499
+ "engines": {
1500
+ "node": ">=0.6"
1501
+ }
1502
+ },
1503
+ "node_modules/touch": {
1504
+ "version": "3.1.1",
1505
+ "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz",
1506
+ "integrity": "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==",
1507
+ "dev": true,
1508
+ "bin": {
1509
+ "nodetouch": "bin/nodetouch.js"
1510
+ }
1511
+ },
1512
+ "node_modules/ts-node": {
1513
+ "version": "10.9.2",
1514
+ "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz",
1515
+ "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==",
1516
+ "dev": true,
1517
+ "dependencies": {
1518
+ "@cspotcode/source-map-support": "^0.8.0",
1519
+ "@tsconfig/node10": "^1.0.7",
1520
+ "@tsconfig/node12": "^1.0.7",
1521
+ "@tsconfig/node14": "^1.0.0",
1522
+ "@tsconfig/node16": "^1.0.2",
1523
+ "acorn": "^8.4.1",
1524
+ "acorn-walk": "^8.1.1",
1525
+ "arg": "^4.1.0",
1526
+ "create-require": "^1.1.0",
1527
+ "diff": "^4.0.1",
1528
+ "make-error": "^1.1.1",
1529
+ "v8-compile-cache-lib": "^3.0.1",
1530
+ "yn": "3.1.1"
1531
+ },
1532
+ "bin": {
1533
+ "ts-node": "dist/bin.js",
1534
+ "ts-node-cwd": "dist/bin-cwd.js",
1535
+ "ts-node-esm": "dist/bin-esm.js",
1536
+ "ts-node-script": "dist/bin-script.js",
1537
+ "ts-node-transpile-only": "dist/bin-transpile.js",
1538
+ "ts-script": "dist/bin-script-deprecated.js"
1539
+ },
1540
+ "peerDependencies": {
1541
+ "@swc/core": ">=1.2.50",
1542
+ "@swc/wasm": ">=1.2.50",
1543
+ "@types/node": "*",
1544
+ "typescript": ">=2.7"
1545
+ },
1546
+ "peerDependenciesMeta": {
1547
+ "@swc/core": {
1548
+ "optional": true
1549
+ },
1550
+ "@swc/wasm": {
1551
+ "optional": true
1552
+ }
1553
+ }
1554
+ },
1555
+ "node_modules/type-is": {
1556
+ "version": "1.6.18",
1557
+ "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
1558
+ "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
1559
+ "dependencies": {
1560
+ "media-typer": "0.3.0",
1561
+ "mime-types": "~2.1.24"
1562
+ },
1563
+ "engines": {
1564
+ "node": ">= 0.6"
1565
+ }
1566
+ },
1567
+ "node_modules/typescript": {
1568
+ "version": "5.9.3",
1569
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
1570
+ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
1571
+ "dev": true,
1572
+ "bin": {
1573
+ "tsc": "bin/tsc",
1574
+ "tsserver": "bin/tsserver"
1575
+ },
1576
+ "engines": {
1577
+ "node": ">=14.17"
1578
+ }
1579
+ },
1580
+ "node_modules/undefsafe": {
1581
+ "version": "2.0.5",
1582
+ "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz",
1583
+ "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==",
1584
+ "dev": true
1585
+ },
1586
+ "node_modules/undici-types": {
1587
+ "version": "6.21.0",
1588
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
1589
+ "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="
1590
+ },
1591
+ "node_modules/unpipe": {
1592
+ "version": "1.0.0",
1593
+ "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
1594
+ "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
1595
+ "engines": {
1596
+ "node": ">= 0.8"
1597
+ }
1598
+ },
1599
+ "node_modules/utils-merge": {
1600
+ "version": "1.0.1",
1601
+ "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
1602
+ "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==",
1603
+ "engines": {
1604
+ "node": ">= 0.4.0"
1605
+ }
1606
+ },
1607
+ "node_modules/uuid": {
1608
+ "version": "13.0.0",
1609
+ "resolved": "https://registry.npmjs.org/uuid/-/uuid-13.0.0.tgz",
1610
+ "integrity": "sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w==",
1611
+ "funding": [
1612
+ "https://github.com/sponsors/broofa",
1613
+ "https://github.com/sponsors/ctavan"
1614
+ ],
1615
+ "bin": {
1616
+ "uuid": "dist-node/bin/uuid"
1617
+ }
1618
+ },
1619
+ "node_modules/v8-compile-cache-lib": {
1620
+ "version": "3.0.1",
1621
+ "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz",
1622
+ "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==",
1623
+ "dev": true
1624
+ },
1625
+ "node_modules/vary": {
1626
+ "version": "1.1.2",
1627
+ "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
1628
+ "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
1629
+ "engines": {
1630
+ "node": ">= 0.8"
1631
+ }
1632
+ },
1633
+ "node_modules/ws": {
1634
+ "version": "8.17.1",
1635
+ "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz",
1636
+ "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==",
1637
+ "engines": {
1638
+ "node": ">=10.0.0"
1639
+ },
1640
+ "peerDependencies": {
1641
+ "bufferutil": "^4.0.1",
1642
+ "utf-8-validate": ">=5.0.2"
1643
+ },
1644
+ "peerDependenciesMeta": {
1645
+ "bufferutil": {
1646
+ "optional": true
1647
+ },
1648
+ "utf-8-validate": {
1649
+ "optional": true
1650
+ }
1651
+ }
1652
+ },
1653
+ "node_modules/yn": {
1654
+ "version": "3.1.1",
1655
+ "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz",
1656
+ "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==",
1657
+ "dev": true,
1658
+ "engines": {
1659
+ "node": ">=6"
1660
+ }
1661
+ }
1662
+ }
1663
+ }
trigo-web/backend/package.json ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "trigo-backend",
3
+ "version": "1.0.0",
4
+ "description": "Backend server for Trigo game",
5
+ "main": "dist/server.js",
6
+ "scripts": {
7
+ "dev": "nodemon --watch src --exec ts-node src/server.ts",
8
+ "build": "tsc",
9
+ "start": "node dist/server.js",
10
+ "test": "echo \"Error: no test specified\" && exit 1"
11
+ },
12
+ "keywords": [
13
+ "game",
14
+ "go",
15
+ "websocket",
16
+ "nodejs"
17
+ ],
18
+ "author": "",
19
+ "license": "MIT",
20
+ "dependencies": {
21
+ "@types/uuid": "^11.0.0",
22
+ "cors": "^2.8.5",
23
+ "dotenv": "^16.6.1",
24
+ "express": "^4.21.2",
25
+ "socket.io": "^4.8.1",
26
+ "uuid": "^13.0.0"
27
+ },
28
+ "devDependencies": {
29
+ "@types/cors": "^2.8.13",
30
+ "@types/express": "^4.17.17",
31
+ "@types/node": "^20.5.0",
32
+ "nodemon": "^3.0.1",
33
+ "ts-node": "^10.9.1",
34
+ "typescript": "^5.2.2"
35
+ }
36
+ }
trigo-web/backend/src/server.ts ADDED
@@ -0,0 +1,59 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const express = require("express");
2
+ const { createServer } = require("http");
3
+ const { Server } = require("socket.io");
4
+ const cors = require("cors");
5
+ const dotenv = require("dotenv");
6
+ const path = require("path");
7
+
8
+ dotenv.config();
9
+
10
+ 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
+
19
+ const PORT = process.env.PORT || 3000;
20
+ const HOST = process.env.HOST || "localhost";
21
+
22
+ // Middleware
23
+ app.use(cors());
24
+ app.use(express.json());
25
+
26
+ // Serve static files from frontend build (for production)
27
+ if (process.env.NODE_ENV === "production") {
28
+ const frontendPath = path.join(__dirname, "../../app/dist");
29
+ app.use(express.static(frontendPath));
30
+
31
+ // Serve index.html for all routes (SPA support)
32
+ app.get("*", (req: any, res: any, next: any) => {
33
+ if (req.path.startsWith("/health") || req.path.startsWith("/socket.io")) {
34
+ return next();
35
+ }
36
+ res.sendFile(path.join(frontendPath, "index.html"));
37
+ });
38
+ }
39
+
40
+ // Health check endpoint
41
+ app.get("/health", (_req: any, res: any) => {
42
+ res.json({ status: "ok", timestamp: new Date().toISOString() });
43
+ });
44
+
45
+ // Socket.io connection handling
46
+ io.on("connection", (socket: any) => {
47
+ console.log(`New client connected: ${socket.id}`);
48
+
49
+ socket.on("disconnect", () => {
50
+ console.log(`Client disconnected: ${socket.id}`);
51
+ });
52
+ });
53
+
54
+ // Start server
55
+ httpServer.listen(PORT, HOST, () => {
56
+ console.log(`Server running on ${HOST}:${PORT}`);
57
+ console.log(`Health check: http://${HOST}:${PORT}/health`);
58
+ console.log(`Environment: ${process.env.NODE_ENV || "development"}`);
59
+ });
trigo-web/backend/src/services/gameManager.ts ADDED
@@ -0,0 +1,329 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { v4 as uuidv4 } from "uuid";
2
+ import type { BoardShape, Position } from "../../../inc/trigo";
3
+ import { TrigoGame, StepType, StoneType } from "../../../inc/trigo/game";
4
+
5
+ export interface Player {
6
+ id: string;
7
+ nickname: string;
8
+ color: "black" | "white";
9
+ connected: boolean;
10
+ }
11
+
12
+ export interface GameState {
13
+ gameStatus: "waiting" | "playing" | "finished";
14
+ winner: "black" | "white" | null;
15
+ }
16
+
17
+ export interface GameRoom {
18
+ id: string;
19
+ players: { [playerId: string]: Player };
20
+ game: TrigoGame; // The actual game instance
21
+ gameState: GameState; // Game status metadata
22
+ createdAt: Date;
23
+ startedAt: Date | null;
24
+ }
25
+
26
+ export class GameManager {
27
+ private rooms: Map<string, GameRoom> = new Map();
28
+ private playerRoomMap: Map<string, string> = new Map();
29
+ private defaultBoardShape: BoardShape = { x: 5, y: 5, z: 5 }; // Default 5x5x5 board
30
+
31
+ constructor() {
32
+ console.log("GameManager initialized");
33
+ }
34
+
35
+ createRoom(playerId: string, nickname: string, boardShape?: BoardShape): GameRoom | null {
36
+ const roomId = this.generateRoomId();
37
+ const shape = boardShape || this.defaultBoardShape;
38
+
39
+ const room: GameRoom = {
40
+ id: roomId,
41
+ players: {
42
+ [playerId]: {
43
+ id: playerId,
44
+ nickname,
45
+ color: "black",
46
+ connected: true
47
+ }
48
+ },
49
+ game: new TrigoGame(shape, {
50
+ onStepAdvance: (_step, history) => {
51
+ console.log(`Step ${history.length}: Player made move`);
52
+ },
53
+ onCapture: (captured) => {
54
+ console.log(`Captured ${captured.length} stones`);
55
+ },
56
+ onWin: (winner) => {
57
+ console.log(`Game won by ${winner}`);
58
+ }
59
+ }),
60
+ gameState: {
61
+ gameStatus: "waiting",
62
+ winner: null
63
+ },
64
+ createdAt: new Date(),
65
+ startedAt: null
66
+ };
67
+
68
+ this.rooms.set(roomId, room);
69
+ this.playerRoomMap.set(playerId, roomId);
70
+
71
+ console.log(`Room ${roomId} created by ${playerId}`);
72
+ return room;
73
+ }
74
+
75
+ joinRoom(roomId: string, playerId: string, nickname: string): GameRoom | null {
76
+ const room = this.rooms.get(roomId);
77
+ if (!room) {
78
+ return null;
79
+ }
80
+
81
+ const playerCount = Object.keys(room.players).length;
82
+ if (playerCount >= 2) {
83
+ return null; // Room is full
84
+ }
85
+
86
+ // Assign white color to the second player
87
+ room.players[playerId] = {
88
+ id: playerId,
89
+ nickname,
90
+ color: "white",
91
+ connected: true
92
+ };
93
+
94
+ this.playerRoomMap.set(playerId, roomId);
95
+
96
+ // Start the game when second player joins
97
+ if (playerCount === 1) {
98
+ room.gameState.gameStatus = "playing";
99
+ room.startedAt = new Date();
100
+ }
101
+
102
+ return room;
103
+ }
104
+
105
+ leaveRoom(roomId: string, playerId: string): void {
106
+ const room = this.rooms.get(roomId);
107
+ if (!room) return;
108
+
109
+ if (room.players[playerId]) {
110
+ room.players[playerId].connected = false;
111
+ }
112
+
113
+ this.playerRoomMap.delete(playerId);
114
+
115
+ // Check if room should be deleted
116
+ const connectedPlayers = Object.values(room.players).filter((p) => p.connected);
117
+ if (connectedPlayers.length === 0) {
118
+ this.rooms.delete(roomId);
119
+ console.log(`Room ${roomId} deleted - no players remaining`);
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
+
131
+ const player = room.players[playerId];
132
+ if (!player) return false;
133
+
134
+ // Check game status
135
+ if (room.gameState.gameStatus !== "playing") {
136
+ return false;
137
+ }
138
+
139
+ // Convert player color to Stone type
140
+ const expectedPlayer = player.color === "black" ? StoneType.BLACK : StoneType.WHITE;
141
+ const currentPlayer = room.game.getCurrentPlayer();
142
+
143
+ // Check if it's the player's turn
144
+ if (currentPlayer !== expectedPlayer) {
145
+ return false;
146
+ }
147
+
148
+ // Attempt to make the move using TrigoGame
149
+ const position: Position = { x: move.x, y: move.y, z: move.z };
150
+ const success = room.game.drop(position);
151
+
152
+ return success;
153
+ }
154
+
155
+ passTurn(roomId: string, playerId: string): boolean {
156
+ const room = this.rooms.get(roomId);
157
+ if (!room) return false;
158
+
159
+ const player = room.players[playerId];
160
+ if (!player) return false;
161
+
162
+ // Check game status
163
+ if (room.gameState.gameStatus !== "playing") {
164
+ return false;
165
+ }
166
+
167
+ // Convert player color to Stone type
168
+ const expectedPlayer = player.color === "black" ? StoneType.BLACK : StoneType.WHITE;
169
+ const currentPlayer = room.game.getCurrentPlayer();
170
+
171
+ // Check if it's the player's turn
172
+ if (currentPlayer !== expectedPlayer) {
173
+ return false;
174
+ }
175
+
176
+ return room.game.pass();
177
+ }
178
+
179
+ resign(roomId: string, playerId: string): boolean {
180
+ const room = this.rooms.get(roomId);
181
+ if (!room) return false;
182
+
183
+ const player = room.players[playerId];
184
+ if (!player) return false;
185
+
186
+ // Surrender the game
187
+ room.game.surrender();
188
+
189
+ // Update room state
190
+ room.gameState.gameStatus = "finished";
191
+ room.gameState.winner = player.color === "black" ? "white" : "black";
192
+
193
+ return true;
194
+ }
195
+
196
+ /**
197
+ * Undo the last move (悔棋)
198
+ */
199
+ undoMove(roomId: string, playerId: string): boolean {
200
+ const room = this.rooms.get(roomId);
201
+ if (!room) return false;
202
+
203
+ const player = room.players[playerId];
204
+ if (!player) return false;
205
+
206
+ // Check game status
207
+ if (room.gameState.gameStatus !== "playing") {
208
+ return false;
209
+ }
210
+
211
+ return room.game.undo();
212
+ }
213
+
214
+ /**
215
+ * Get game board state for a room
216
+ */
217
+ getGameBoard(roomId: string): number[][][] | null {
218
+ const room = this.rooms.get(roomId);
219
+ if (!room) return null;
220
+
221
+ return room.game.getBoard();
222
+ }
223
+
224
+ /**
225
+ * Get game statistics for a room
226
+ */
227
+ getGameStats(roomId: string) {
228
+ const room = this.rooms.get(roomId);
229
+ if (!room) return null;
230
+
231
+ return room.game.getStats();
232
+ }
233
+
234
+ /**
235
+ * Get current player for a room
236
+ */
237
+ getCurrentPlayer(roomId: string): "black" | "white" | null {
238
+ const room = this.rooms.get(roomId);
239
+ if (!room) return null;
240
+
241
+ const currentStone = room.game.getCurrentPlayer();
242
+ return currentStone === StoneType.BLACK ? "black" : "white";
243
+ }
244
+
245
+ /**
246
+ * Calculate and get territory for a room
247
+ */
248
+ getTerritory(roomId: string) {
249
+ const room = this.rooms.get(roomId);
250
+ if (!room) return null;
251
+
252
+ return room.game.getTerritory();
253
+ }
254
+
255
+ /**
256
+ * End the game and determine winner based on territory
257
+ */
258
+ endGameByTerritory(roomId: string): boolean {
259
+ const room = this.rooms.get(roomId);
260
+ if (!room) return false;
261
+
262
+ if (room.gameState.gameStatus !== "playing") {
263
+ return false;
264
+ }
265
+
266
+ // Calculate final territory
267
+ const territory = room.game.getTerritory();
268
+
269
+ // Determine winner
270
+ if (territory.black > territory.white) {
271
+ room.gameState.winner = "black";
272
+ } else if (territory.white > territory.black) {
273
+ room.gameState.winner = "white";
274
+ } else {
275
+ // Draw - could set winner to null or handle differently
276
+ room.gameState.winner = null;
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
+ }
284
+
285
+ /**
286
+ * Check if both players passed consecutively (game should end)
287
+ * Returns true if game was ended
288
+ */
289
+ checkConsecutivePasses(roomId: string): boolean {
290
+ const room = this.rooms.get(roomId);
291
+ if (!room) return false;
292
+
293
+ const history = room.game.getHistory();
294
+ if (history.length < 2) return false;
295
+
296
+ // Get last two moves
297
+ const lastMove = history[history.length - 1];
298
+ const secondLastMove = history[history.length - 2];
299
+
300
+ // Check if both were passes
301
+ if (lastMove.type === StepType.PASS && secondLastMove.type === StepType.PASS) {
302
+ // Two consecutive passes - end the game
303
+ this.endGameByTerritory(roomId);
304
+ return true;
305
+ }
306
+
307
+ return false;
308
+ }
309
+
310
+ getRoom(roomId: string): GameRoom | undefined {
311
+ return this.rooms.get(roomId);
312
+ }
313
+
314
+ getPlayerRoom(playerId: string): GameRoom | undefined {
315
+ const roomId = this.playerRoomMap.get(playerId);
316
+ if (!roomId) return undefined;
317
+ return this.rooms.get(roomId);
318
+ }
319
+
320
+ getActiveRooms(): GameRoom[] {
321
+ return Array.from(this.rooms.values()).filter(
322
+ (room) => room.gameState.gameStatus !== "finished"
323
+ );
324
+ }
325
+
326
+ private generateRoomId(): string {
327
+ return uuidv4().substring(0, 8).toUpperCase();
328
+ }
329
+ }
trigo-web/backend/src/sockets/gameSocket.ts ADDED
@@ -0,0 +1,155 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Server, Socket } from "socket.io";
2
+ import { GameManager } from "../services/gameManager";
3
+
4
+ export function setupSocketHandlers(io: Server, socket: Socket, gameManager: GameManager) {
5
+ console.log(`Setting up socket handlers for ${socket.id}`);
6
+
7
+ // Join room
8
+ socket.on("joinRoom", (data: { roomId?: string; nickname: string }) => {
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);
16
+
17
+ // Get complete game data for frontend
18
+ const board = gameManager.getGameBoard(room.id);
19
+ const currentPlayer = gameManager.getCurrentPlayer(room.id);
20
+ const stats = gameManager.getGameStats(room.id);
21
+
22
+ socket.emit("roomJoined", {
23
+ roomId: room.id,
24
+ playerId: socket.id,
25
+ playerColor: room.players[socket.id]?.color,
26
+ gameState: {
27
+ board,
28
+ boardShape: room.game.getShape(),
29
+ currentPlayer,
30
+ moveHistory: room.game.getHistory(),
31
+ currentMoveIndex: room.game.getCurrentStep(),
32
+ capturedStones: {
33
+ black: stats?.capturedByBlack || 0,
34
+ white: stats?.capturedByWhite || 0
35
+ },
36
+ gameStatus: room.gameState.gameStatus,
37
+ winner: room.gameState.winner
38
+ }
39
+ });
40
+
41
+ // Notify other players
42
+ socket.to(room.id).emit("playerJoined", {
43
+ playerId: socket.id,
44
+ nickname: nickname
45
+ });
46
+
47
+ console.log(`Player ${socket.id} joined room ${room.id}`);
48
+ } else {
49
+ socket.emit("error", { message: "Failed to join room" });
50
+ }
51
+ });
52
+
53
+ // Leave room
54
+ socket.on("leaveRoom", () => {
55
+ const room = gameManager.getPlayerRoom(socket.id);
56
+ if (room) {
57
+ socket.leave(room.id);
58
+ gameManager.leaveRoom(room.id, socket.id);
59
+
60
+ // Notify others
61
+ socket.to(room.id).emit("playerLeft", {
62
+ playerId: socket.id
63
+ });
64
+ }
65
+ });
66
+
67
+ // Game moves
68
+ socket.on("makeMove", (data: { x: number; y: number; z: number }) => {
69
+ const room = gameManager.getPlayerRoom(socket.id);
70
+ if (room && gameManager.makeMove(room.id, socket.id, data)) {
71
+ // Get updated game data
72
+ const board = gameManager.getGameBoard(room.id);
73
+ const currentPlayer = gameManager.getCurrentPlayer(room.id);
74
+ const stats = gameManager.getGameStats(room.id);
75
+ const lastStep = room.game.getLastStep();
76
+
77
+ // Broadcast game update to all players in the room
78
+ io.to(room.id).emit("gameUpdate", {
79
+ board,
80
+ currentPlayer,
81
+ lastMove: data,
82
+ capturedStones: {
83
+ black: stats?.capturedByBlack || 0,
84
+ white: stats?.capturedByWhite || 0
85
+ },
86
+ capturedPositions: lastStep?.capturedPositions,
87
+ moveHistory: room.game.getHistory(),
88
+ currentMoveIndex: room.game.getCurrentStep()
89
+ });
90
+ } else {
91
+ socket.emit("error", { message: "Invalid move" });
92
+ }
93
+ });
94
+
95
+ // Pass turn
96
+ socket.on("pass", () => {
97
+ const room = gameManager.getPlayerRoom(socket.id);
98
+ if (room && gameManager.passTurn(room.id, socket.id)) {
99
+ const currentPlayer = gameManager.getCurrentPlayer(room.id);
100
+
101
+ io.to(room.id).emit("gameUpdate", {
102
+ currentPlayer,
103
+ action: "pass",
104
+ moveHistory: room.game.getHistory(),
105
+ currentMoveIndex: room.game.getCurrentStep()
106
+ });
107
+
108
+ // Check for consecutive passes (game end)
109
+ if (gameManager.checkConsecutivePasses(room.id)) {
110
+ const territory = gameManager.getTerritory(room.id);
111
+ io.to(room.id).emit("gameEnded", {
112
+ winner: room.gameState.winner,
113
+ reason: "double-pass",
114
+ territory
115
+ });
116
+ }
117
+ }
118
+ });
119
+
120
+ // Resign
121
+ socket.on("resign", () => {
122
+ const room = gameManager.getPlayerRoom(socket.id);
123
+ if (room && gameManager.resign(room.id, socket.id)) {
124
+ io.to(room.id).emit("gameEnded", {
125
+ winner: room.gameState.winner,
126
+ reason: "resignation"
127
+ });
128
+ }
129
+ });
130
+
131
+ // Chat messages
132
+ socket.on("chatMessage", (data: { content: string }) => {
133
+ const room = gameManager.getPlayerRoom(socket.id);
134
+ if (room) {
135
+ const player = room.players[socket.id];
136
+ io.to(room.id).emit("chatMessage", {
137
+ author: player?.nickname || "Unknown",
138
+ content: data.content,
139
+ playerId: socket.id
140
+ });
141
+ }
142
+ });
143
+
144
+ // Handle disconnection
145
+ socket.on("disconnect", () => {
146
+ console.log(`Client disconnected: ${socket.id}`);
147
+ const room = gameManager.getPlayerRoom(socket.id);
148
+ if (room) {
149
+ gameManager.leaveRoom(room.id, socket.id);
150
+ socket.to(room.id).emit("playerDisconnected", {
151
+ playerId: socket.id
152
+ });
153
+ }
154
+ });
155
+ }
trigo-web/backend/tsconfig.json ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2020",
4
+ "module": "commonjs",
5
+ "lib": ["ES2020"],
6
+ "outDir": "./dist",
7
+ "rootDir": "../",
8
+ "strict": false,
9
+ "esModuleInterop": true,
10
+ "skipLibCheck": true,
11
+ "forceConsistentCasingInFileNames": true,
12
+ "moduleResolution": "node",
13
+ "resolveJsonModule": true,
14
+ "declaration": true,
15
+ "declarationMap": true,
16
+ "sourceMap": true,
17
+ "types": ["node"],
18
+ "allowJs": true,
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
+ }
trigo-web/inc/tgn/README.md ADDED
@@ -0,0 +1,109 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # TGN (Trigo Game Notation) Parser
2
+
3
+ This directory contains the TGN parser implementation for reading and writing Trigo game notation files.
4
+
5
+ ## Files
6
+
7
+ - **`tgn.jison`** - Grammar definition for TGN format (similar to PGN for chess)
8
+ - **`tgnParser.ts`** - TypeScript wrapper providing type-safe API for the parser
9
+ - **`tgn.jison.cjs`** - Generated parser (auto-generated, gitignored)
10
+
11
+ ## Building the Parser
12
+
13
+ The parser is generated from the jison grammar file. To rebuild:
14
+
15
+ ```bash
16
+ npm run build:parsers
17
+ ```
18
+
19
+ Or specifically for TGN:
20
+
21
+ ```bash
22
+ npm run build:parser:tgn
23
+ ```
24
+
25
+ This will generate `tgn.jison.cjs` from `tgn.jison`.
26
+
27
+ ## Usage
28
+
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 = `
36
+ [Event "Test Game"]
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
+
61
+ ## TGN Format
62
+
63
+ TGN format consists of:
64
+
65
+ 1. **Metadata tags** - Key-value pairs in square brackets
66
+ 2. **Move sequence** - Numbered rounds with moves in ab0yz coordinate notation
67
+
68
+ Example:
69
+
70
+ ```tgn
71
+ [Event "World Championship"]
72
+ [Site "Tokyo"]
73
+ [Date "2025.10.31"]
74
+ [Black "Alice"]
75
+ [White "Bob"]
76
+ [Board 5x5x5]
77
+ [Rules "Chinese"]
78
+
79
+ 1. 000 y00
80
+ 2. 0y0 yy0
81
+ 3. aaa pass
82
+ ```
83
+
84
+ ### Coordinate Notation (ab0yz)
85
+
86
+ - `0` = center position
87
+ - `a`, `b`, `c`, ... = positions from one edge toward center
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
+
100
+ ## Development
101
+
102
+ The parser is built using [Jison](https://github.com/zaach/jison), a JavaScript parser generator similar to Bison/Yacc.
103
+
104
+ **Note:** Generated files (`*.jison.cjs`, `*.jison.js`) are gitignored and must be built locally or in CI/CD.
105
+
106
+ ## Related Documentation
107
+
108
+ - [TGN Format Specification](../../docs/tgn-format-spec.md)
109
+ - [Parser Development Guide](../../docs/README.tgn.md)
trigo-web/inc/tgn/tgn.jison ADDED
@@ -0,0 +1,224 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* Trigo Game Notation Parser
2
+ * Similar as PGN (Portable Game Notation) format
3
+ */
4
+
5
+ /* ========== Lexical Grammar ========== */
6
+ %lex
7
+
8
+ %%
9
+
10
+ \s+ /* skip whitespace */
11
+ \n /* skip newlines */
12
+ ";"[^\n]* /* skip line comments */
13
+ "{"[^}]*"}" /* skip block comments */
14
+
15
+ /* Brackets */
16
+ "[" return '['
17
+ "]" return ']'
18
+
19
+ /* String literals */
20
+ \"([^\\\"]|\\.)*\" return 'STRING'
21
+
22
+ /* Tag names - Common PGN tags */
23
+ "Event" return 'TAG_EVENT'
24
+ "Site" return 'TAG_SITE'
25
+ "Date" return 'TAG_DATE'
26
+ "Round" return 'TAG_ROUND'
27
+ "Black" return 'TAG_BLACK'
28
+ "White" return 'TAG_WHITE'
29
+ "Result" return 'TAG_RESULT'
30
+
31
+ /* Trigo-specific tags */
32
+ "Board" return 'TAG_BOARD'
33
+ "Handicap" return 'TAG_HANDICAP'
34
+ "Rules" return 'TAG_RULES'
35
+ "TimeControl" return 'TAG_TIMECONTROL'
36
+ "Annotator" return 'TAG_ANNOTATOR'
37
+ "Application" return 'TAG_APPLICATION'
38
+
39
+ /* Game result symbols */
40
+ "B+" return 'RESULT_BLACK'
41
+ "W+" return 'RESULT_WHITE'
42
+ "=" return '=' /* draw */
43
+ "*" return '*' /* unknown */
44
+
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
+
53
+ [x](?=[1-9]) return 'TIMES'
54
+
55
+ /* Coordinates for moves - will be refined */
56
+ [a-z0]+ return 'COORDINATE'
57
+
58
+ /* Generic tag name for extensibility */
59
+ [A-Z][A-Za-z0-9_]* return 'TAG_NAME'
60
+
61
+ /* End of file */
62
+ <<EOF>> return 'EOF'
63
+
64
+ /* Catch-all for unexpected characters */
65
+ . return 'INVALID'
66
+
67
+ /lex
68
+
69
+ /* ========== Grammar Rules ========== */
70
+
71
+ %%
72
+
73
+ game
74
+ : tag_section move_section EOF
75
+ {
76
+ return {
77
+ tags: $1,
78
+ moves: $2,
79
+ success: true
80
+ };
81
+ }
82
+ | tag_section EOF
83
+ {
84
+ return {
85
+ tags: $1,
86
+ moves: null,
87
+ success: true
88
+ };
89
+ }
90
+ | move_section EOF
91
+ {
92
+ return {
93
+ tags: {},
94
+ moves: $1,
95
+ success: true
96
+ };
97
+ }
98
+ | EOF
99
+ {
100
+ return {
101
+ tags: {},
102
+ moves: null,
103
+ success: true
104
+ };
105
+ }
106
+ ;
107
+
108
+ /* ========== Tag Section (Metadata) ========== */
109
+
110
+ tag_section
111
+ : tag_pair -> $1
112
+ | tag_section tag_pair -> Object.assign({}, $1, $2)
113
+ ;
114
+
115
+ tag_pair
116
+ : "[" tag_name STRING "]"
117
+ {
118
+ const tagName = $2;
119
+ const tagValue = $3.slice(1, -1); // Remove quotes
120
+ $$ = { [tagName]: tagValue };
121
+ }
122
+ | "[" TAG_RESULT game_result "]" -> $3
123
+ | "[" TAG_BOARD board_shape "]" -> ({[$2]: $3})
124
+ ;
125
+
126
+ tag_name
127
+ : TAG_EVENT
128
+ | TAG_SITE
129
+ | TAG_DATE
130
+ | TAG_ROUND
131
+ | TAG_BLACK
132
+ | TAG_WHITE
133
+ | TAG_HANDICAP
134
+ | TAG_RULES
135
+ | TAG_TIMECONTROL
136
+ | TAG_ANNOTATOR
137
+ | TAG_APPLICATION
138
+ | TAG_NAME { $$ = yytext; }
139
+ ;
140
+
141
+ /* ========== Movetext Section (Game Body) ========== */
142
+ /* PLACEHOLDER: Will be implemented in future iterations */
143
+
144
+ move_section
145
+ : move_sequence
146
+ //| move_sequence game_termination
147
+ ;
148
+
149
+ move_sequence
150
+ : move_sequence_intact -> $1
151
+ | move_sequence_truncated -> $1
152
+ ;
153
+
154
+ move_sequence_intact
155
+ : /* empty */ -> []
156
+ | move_sequence_intact move_round -> $1.concat([$2])
157
+ ;
158
+
159
+ move_sequence_truncated
160
+ : move_sequence_intact move_round_half -> $1.concat([$2])
161
+ ;
162
+
163
+ move_round
164
+ : number DOT move_action move_action -> ({ round: $1, action_black: $3, action_white: $4 })
165
+ ;
166
+
167
+ move_round_half
168
+ : number DOT move_action -> ({ round: $1, action_black: $3 })
169
+ ;
170
+
171
+ move_action
172
+ : PASS -> ({ type: 'pass' })
173
+ | RESIGN -> ({ type: 'resign' })
174
+ | COORDINATE
175
+ {
176
+ // Placeholder: Parse coordinate notation
177
+ $$ = {
178
+ type: 'move',
179
+ position: yytext
180
+ };
181
+ }
182
+ ;
183
+
184
+ game_result
185
+ : win -> ({Result: $1})
186
+ | win conquer -> ({Result: $1, Conquer: $2})
187
+ | "=" -> ({Result: "draw"})
188
+ | "*" -> ({Result: "unknown"})
189
+ ;
190
+
191
+ win
192
+ : RESULT_BLACK -> "black win"
193
+ | RESULT_WHITE -> "white win"
194
+ ;
195
+
196
+ conquer
197
+ : number conquer_unit -> ({n: $1, unit: $2})
198
+ ;
199
+
200
+ conquer_unit
201
+ : POINTS
202
+ | STONES
203
+ ;
204
+
205
+ board_shape
206
+ : number -> [$1]
207
+ | board_shape TIMES number -> $1.concat($3)
208
+ ;
209
+
210
+ number
211
+ : NUMBER -> parseInt($1)
212
+ ;
213
+
214
+ %%
215
+
216
+ /* ========== Additional JavaScript Code ========== */
217
+
218
+ // Parser configuration
219
+ parser.yy = {
220
+ // Helper functions can be added here
221
+ parseError: function(str, hash) {
222
+ throw new Error('Parse error: ' + str);
223
+ }
224
+ };
trigo-web/inc/tgn/tgn.jison.cjs ADDED
@@ -0,0 +1,791 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* parser generated by jison 0.4.18 */
2
+ /*
3
+ Returns a Parser object of the following structure:
4
+
5
+ Parser: {
6
+ yy: {}
7
+ }
8
+
9
+ Parser.prototype: {
10
+ yy: {},
11
+ trace: function(),
12
+ symbols_: {associative list: name ==> number},
13
+ terminals_: {associative list: number ==> name},
14
+ productions_: [...],
15
+ performAction: function anonymous(yytext, yyleng, yylineno, yy, yystate, $$, _$),
16
+ table: [...],
17
+ defaultActions: {...},
18
+ parseError: function(str, hash),
19
+ parse: function(input),
20
+
21
+ lexer: {
22
+ EOF: 1,
23
+ parseError: function(str, hash),
24
+ setInput: function(input),
25
+ input: function(),
26
+ unput: function(str),
27
+ more: function(),
28
+ less: function(n),
29
+ pastInput: function(),
30
+ upcomingInput: function(),
31
+ showPosition: function(),
32
+ test_match: function(regex_match_array, rule_index),
33
+ next: function(),
34
+ lex: function(),
35
+ begin: function(condition),
36
+ popState: function(),
37
+ _currentRules: function(),
38
+ topState: function(),
39
+ pushState: function(condition),
40
+
41
+ options: {
42
+ ranges: boolean (optional: true ==> token location info will include a .range[] member)
43
+ flex: boolean (optional: true ==> flex-like lexing behaviour where the rules are tested exhaustively to find the longest match)
44
+ backtrack_lexer: boolean (optional: true ==> lexer regexes are tested in order and for each matching regex the action code is invoked; the lexer terminates the scan when a token is returned by the action code)
45
+ },
46
+
47
+ performAction: function(yy, yy_, $avoiding_name_collisions, YY_START),
48
+ rules: [...],
49
+ conditions: {associative list: name ==> set},
50
+ }
51
+ }
52
+
53
+
54
+ token location info (@$, _$, etc.): {
55
+ first_line: n,
56
+ last_line: n,
57
+ first_column: n,
58
+ last_column: n,
59
+ range: [start_number, end_number] (where the numbers are indexes into the input string, regular zero-based)
60
+ }
61
+
62
+
63
+ the parseError function receives a 'hash' object with these members for lexer and parser errors: {
64
+ text: (matched text)
65
+ token: (the produced terminal token, if any)
66
+ line: (yylineno)
67
+ }
68
+ while parser (grammar) errors will also provide these members, i.e. parser errors deliver a superset of attributes: {
69
+ loc: (yylloc)
70
+ expected: (string describing the set of expected tokens)
71
+ recoverable: (boolean: TRUE when the parser has a error recovery rule available for this particular error)
72
+ }
73
+ */
74
+ var tgn = (function(){
75
+ var o=function(k,v,o,l){for(o=o||{},l=k.length;l--;o[k[l]]=v);return o},$V0=[1,7],$V1=[2,25],$V2=[6,8,49],$V3=[1,32],$V4=[6,49],$V5=[11,49],$V6=[11,48],$V7=[1,51],$V8=[1,52],$V9=[1,53],$Va=[6,36,37,38,49];
76
+ var parser = {trace: function trace () { },
77
+ yy: {},
78
+ symbols_: {"error":2,"game":3,"tag_section":4,"move_section":5,"EOF":6,"tag_pair":7,"[":8,"tag_name":9,"STRING":10,"]":11,"TAG_RESULT":12,"game_result":13,"TAG_BOARD":14,"board_shape":15,"TAG_EVENT":16,"TAG_SITE":17,"TAG_DATE":18,"TAG_ROUND":19,"TAG_BLACK":20,"TAG_WHITE":21,"TAG_HANDICAP":22,"TAG_RULES":23,"TAG_TIMECONTROL":24,"TAG_ANNOTATOR":25,"TAG_APPLICATION":26,"TAG_NAME":27,"move_sequence":28,"move_sequence_intact":29,"move_sequence_truncated":30,"move_round":31,"move_round_half":32,"number":33,"DOT":34,"move_action":35,"PASS":36,"RESIGN":37,"COORDINATE":38,"win":39,"conquer":40,"=":41,"*":42,"RESULT_BLACK":43,"RESULT_WHITE":44,"conquer_unit":45,"POINTS":46,"STONES":47,"TIMES":48,"NUMBER":49,"$accept":0,"$end":1},
79
+ terminals_: {2:"error",6:"EOF",8:"[",10:"STRING",11:"]",12:"TAG_RESULT",14:"TAG_BOARD",16:"TAG_EVENT",17:"TAG_SITE",18:"TAG_DATE",19:"TAG_ROUND",20:"TAG_BLACK",21:"TAG_WHITE",22:"TAG_HANDICAP",23:"TAG_RULES",24:"TAG_TIMECONTROL",25:"TAG_ANNOTATOR",26:"TAG_APPLICATION",27:"TAG_NAME",34:"DOT",36:"PASS",37:"RESIGN",38:"COORDINATE",41:"=",42:"*",43:"RESULT_BLACK",44:"RESULT_WHITE",46:"POINTS",47:"STONES",48:"TIMES",49:"NUMBER"},
80
+ productions_: [0,[3,3],[3,2],[3,2],[3,1],[4,1],[4,2],[7,4],[7,4],[7,4],[9,1],[9,1],[9,1],[9,1],[9,1],[9,1],[9,1],[9,1],[9,1],[9,1],[9,1],[9,1],[5,1],[28,1],[28,1],[29,0],[29,2],[30,2],[31,4],[32,3],[35,1],[35,1],[35,1],[13,1],[13,2],[13,1],[13,1],[39,1],[39,1],[40,2],[45,1],[45,1],[15,1],[15,3],[33,1]],
81
+ performAction: function anonymous(yytext, yyleng, yylineno, yy, yystate /* action[1] */, $$ /* vstack */, _$ /* lstack */) {
82
+ /* this == yyval */
83
+
84
+ var $0 = $$.length - 1;
85
+ switch (yystate) {
86
+ case 1:
87
+
88
+ return {
89
+ tags: $$[$0-2],
90
+ moves: $$[$0-1],
91
+ success: true
92
+ };
93
+
94
+ break;
95
+ case 2:
96
+
97
+ return {
98
+ tags: $$[$0-1],
99
+ moves: null,
100
+ success: true
101
+ };
102
+
103
+ break;
104
+ case 3:
105
+
106
+ return {
107
+ tags: {},
108
+ moves: $$[$0-1],
109
+ success: true
110
+ };
111
+
112
+ break;
113
+ case 4:
114
+
115
+ return {
116
+ tags: {},
117
+ moves: null,
118
+ success: true
119
+ };
120
+
121
+ break;
122
+ case 5: case 23: case 24:
123
+ this.$ = $$[$0];
124
+ break;
125
+ case 6:
126
+ this.$ = Object.assign({}, $$[$0-1], $$[$0]);
127
+ break;
128
+ case 7:
129
+
130
+ const tagName = $$[$0-2];
131
+ const tagValue = $$[$0-1].slice(1, -1); // Remove quotes
132
+ this.$ = { [tagName]: tagValue };
133
+
134
+ break;
135
+ case 8:
136
+ this.$ = $$[$0-1];
137
+ break;
138
+ case 9:
139
+ this.$ = ({[$$[$0-2]]: $$[$0-1]});
140
+ break;
141
+ case 21:
142
+ this.$ = yytext;
143
+ break;
144
+ case 25:
145
+ this.$ = [];
146
+ break;
147
+ case 26: case 27:
148
+ this.$ = $$[$0-1].concat([$$[$0]]);
149
+ break;
150
+ case 28:
151
+ this.$ = ({ round: $$[$0-3], action_black: $$[$0-1], action_white: $$[$0] });
152
+ break;
153
+ case 29:
154
+ this.$ = ({ round: $$[$0-2], action_black: $$[$0] });
155
+ break;
156
+ case 30:
157
+ this.$ = ({ type: 'pass' });
158
+ break;
159
+ case 31:
160
+ this.$ = ({ type: 'resign' });
161
+ break;
162
+ case 32:
163
+
164
+ // Placeholder: Parse coordinate notation
165
+ this.$ = {
166
+ type: 'move',
167
+ position: yytext
168
+ };
169
+
170
+ break;
171
+ case 33:
172
+ this.$ = ({Result: $$[$0]});
173
+ break;
174
+ case 34:
175
+ this.$ = ({Result: $$[$0-1], Conquer: $$[$0]});
176
+ break;
177
+ case 35:
178
+ this.$ = ({Result: "draw"});
179
+ break;
180
+ case 36:
181
+ this.$ = ({Result: "unknown"});
182
+ break;
183
+ case 37:
184
+ this.$ = "black win";
185
+ break;
186
+ case 38:
187
+ this.$ = "white win";
188
+ break;
189
+ case 39:
190
+ this.$ = ({n: $$[$0-1], unit: $$[$0]});
191
+ break;
192
+ case 42:
193
+ this.$ = [$$[$0]];
194
+ break;
195
+ case 43:
196
+ this.$ = $$[$0-2].concat($$[$0]);
197
+ break;
198
+ case 44:
199
+ this.$ = parseInt($$[$0]);
200
+ break;
201
+ }
202
+ },
203
+ table: [{3:1,4:2,5:3,6:[1,4],7:5,8:$V0,28:6,29:8,30:9,49:$V1},{1:[3]},{5:10,6:[1,11],7:12,8:$V0,28:6,29:8,30:9,49:$V1},{6:[1,13]},{1:[2,4]},o($V2,[2,5]),{6:[2,22]},{9:14,12:[1,15],14:[1,16],16:[1,17],17:[1,18],18:[1,19],19:[1,20],20:[1,21],21:[1,22],22:[1,23],23:[1,24],24:[1,25],25:[1,26],26:[1,27],27:[1,28]},{6:[2,23],31:29,32:30,33:31,49:$V3},{6:[2,24]},{6:[1,33]},{1:[2,2]},o($V2,[2,6]),{1:[2,3]},{10:[1,34]},{13:35,39:36,41:[1,37],42:[1,38],43:[1,39],44:[1,40]},{15:41,33:42,49:$V3},{10:[2,10]},{10:[2,11]},{10:[2,12]},{10:[2,13]},{10:[2,14]},{10:[2,15]},{10:[2,16]},{10:[2,17]},{10:[2,18]},{10:[2,19]},{10:[2,20]},{10:[2,21]},o($V4,[2,26]),{6:[2,27]},{34:[1,43]},o([11,34,46,47,48],[2,44]),{1:[2,1]},{11:[1,44]},{11:[1,45]},{11:[2,33],33:47,40:46,49:$V3},{11:[2,35]},{11:[2,36]},o($V5,[2,37]),o($V5,[2,38]),{11:[1,48],48:[1,49]},o($V6,[2,42]),{35:50,36:$V7,37:$V8,38:$V9},o($V2,[2,7]),o($V2,[2,8]),{11:[2,34]},{45:54,46:[1,55],47:[1,56]},o($V2,[2,9]),{33:57,49:$V3},{6:[2,29],35:58,36:$V7,37:$V8,38:$V9},o($Va,[2,30]),o($Va,[2,31]),o($Va,[2,32]),{11:[2,39]},{11:[2,40]},{11:[2,41]},o($V6,[2,43]),o($V4,[2,28])],
204
+ defaultActions: {4:[2,4],6:[2,22],9:[2,24],11:[2,2],13:[2,3],17:[2,10],18:[2,11],19:[2,12],20:[2,13],21:[2,14],22:[2,15],23:[2,16],24:[2,17],25:[2,18],26:[2,19],27:[2,20],28:[2,21],30:[2,27],33:[2,1],37:[2,35],38:[2,36],46:[2,34],54:[2,39],55:[2,40],56:[2,41]},
205
+ parseError: function parseError (str, hash) {
206
+ if (hash.recoverable) {
207
+ this.trace(str);
208
+ } else {
209
+ var error = new Error(str);
210
+ error.hash = hash;
211
+ throw error;
212
+ }
213
+ },
214
+ parse: function parse(input) {
215
+ var self = this, stack = [0], tstack = [], vstack = [null], lstack = [], table = this.table, yytext = '', yylineno = 0, yyleng = 0, recovering = 0, TERROR = 2, EOF = 1;
216
+ var args = lstack.slice.call(arguments, 1);
217
+ var lexer = Object.create(this.lexer);
218
+ var sharedState = { yy: {} };
219
+ for (var k in this.yy) {
220
+ if (Object.prototype.hasOwnProperty.call(this.yy, k)) {
221
+ sharedState.yy[k] = this.yy[k];
222
+ }
223
+ }
224
+ lexer.setInput(input, sharedState.yy);
225
+ sharedState.yy.lexer = lexer;
226
+ sharedState.yy.parser = this;
227
+ if (typeof lexer.yylloc == 'undefined') {
228
+ lexer.yylloc = {};
229
+ }
230
+ var yyloc = lexer.yylloc;
231
+ lstack.push(yyloc);
232
+ var ranges = lexer.options && lexer.options.ranges;
233
+ if (typeof sharedState.yy.parseError === 'function') {
234
+ this.parseError = sharedState.yy.parseError;
235
+ } else {
236
+ this.parseError = Object.getPrototypeOf(this).parseError;
237
+ }
238
+ function popStack(n) {
239
+ stack.length = stack.length - 2 * n;
240
+ vstack.length = vstack.length - n;
241
+ lstack.length = lstack.length - n;
242
+ }
243
+ _token_stack:
244
+ var lex = function () {
245
+ var token;
246
+ token = lexer.lex() || EOF;
247
+ if (typeof token !== 'number') {
248
+ token = self.symbols_[token] || token;
249
+ }
250
+ return token;
251
+ };
252
+ var symbol, preErrorSymbol, state, action, a, r, yyval = {}, p, len, newState, expected;
253
+ while (true) {
254
+ state = stack[stack.length - 1];
255
+ if (this.defaultActions[state]) {
256
+ action = this.defaultActions[state];
257
+ } else {
258
+ if (symbol === null || typeof symbol == 'undefined') {
259
+ symbol = lex();
260
+ }
261
+ action = table[state] && table[state][symbol];
262
+ }
263
+ if (typeof action === 'undefined' || !action.length || !action[0]) {
264
+ var errStr = '';
265
+ expected = [];
266
+ for (p in table[state]) {
267
+ if (this.terminals_[p] && p > TERROR) {
268
+ expected.push('\'' + this.terminals_[p] + '\'');
269
+ }
270
+ }
271
+ if (lexer.showPosition) {
272
+ errStr = 'Parse error on line ' + (yylineno + 1) + ':\n' + lexer.showPosition() + '\nExpecting ' + expected.join(', ') + ', got \'' + (this.terminals_[symbol] || symbol) + '\'';
273
+ } else {
274
+ errStr = 'Parse error on line ' + (yylineno + 1) + ': Unexpected ' + (symbol == EOF ? 'end of input' : '\'' + (this.terminals_[symbol] || symbol) + '\'');
275
+ }
276
+ this.parseError(errStr, {
277
+ text: lexer.match,
278
+ token: this.terminals_[symbol] || symbol,
279
+ line: lexer.yylineno,
280
+ loc: yyloc,
281
+ expected: expected
282
+ });
283
+ }
284
+ if (action[0] instanceof Array && action.length > 1) {
285
+ throw new Error('Parse Error: multiple actions possible at state: ' + state + ', token: ' + symbol);
286
+ }
287
+ switch (action[0]) {
288
+ case 1:
289
+ stack.push(symbol);
290
+ vstack.push(lexer.yytext);
291
+ lstack.push(lexer.yylloc);
292
+ stack.push(action[1]);
293
+ symbol = null;
294
+ if (!preErrorSymbol) {
295
+ yyleng = lexer.yyleng;
296
+ yytext = lexer.yytext;
297
+ yylineno = lexer.yylineno;
298
+ yyloc = lexer.yylloc;
299
+ if (recovering > 0) {
300
+ recovering--;
301
+ }
302
+ } else {
303
+ symbol = preErrorSymbol;
304
+ preErrorSymbol = null;
305
+ }
306
+ break;
307
+ case 2:
308
+ len = this.productions_[action[1]][1];
309
+ yyval.$ = vstack[vstack.length - len];
310
+ yyval._$ = {
311
+ first_line: lstack[lstack.length - (len || 1)].first_line,
312
+ last_line: lstack[lstack.length - 1].last_line,
313
+ first_column: lstack[lstack.length - (len || 1)].first_column,
314
+ last_column: lstack[lstack.length - 1].last_column
315
+ };
316
+ if (ranges) {
317
+ yyval._$.range = [
318
+ lstack[lstack.length - (len || 1)].range[0],
319
+ lstack[lstack.length - 1].range[1]
320
+ ];
321
+ }
322
+ r = this.performAction.apply(yyval, [
323
+ yytext,
324
+ yyleng,
325
+ yylineno,
326
+ sharedState.yy,
327
+ action[1],
328
+ vstack,
329
+ lstack
330
+ ].concat(args));
331
+ if (typeof r !== 'undefined') {
332
+ return r;
333
+ }
334
+ if (len) {
335
+ stack = stack.slice(0, -1 * len * 2);
336
+ vstack = vstack.slice(0, -1 * len);
337
+ lstack = lstack.slice(0, -1 * len);
338
+ }
339
+ stack.push(this.productions_[action[1]][0]);
340
+ vstack.push(yyval.$);
341
+ lstack.push(yyval._$);
342
+ newState = table[stack[stack.length - 2]][stack[stack.length - 1]];
343
+ stack.push(newState);
344
+ break;
345
+ case 3:
346
+ return true;
347
+ }
348
+ }
349
+ return true;
350
+ }};
351
+
352
+
353
+ /* ========== Additional JavaScript Code ========== */
354
+
355
+ // Parser configuration
356
+ parser.yy = {
357
+ // Helper functions can be added here
358
+ parseError: function(str, hash) {
359
+ throw new Error('Parse error: ' + str);
360
+ }
361
+ };
362
+ /* generated by jison-lex 0.3.4 */
363
+ var lexer = (function(){
364
+ var lexer = ({
365
+
366
+ EOF:1,
367
+
368
+ parseError:function parseError(str, hash) {
369
+ if (this.yy.parser) {
370
+ this.yy.parser.parseError(str, hash);
371
+ } else {
372
+ throw new Error(str);
373
+ }
374
+ },
375
+
376
+ // resets the lexer, sets new input
377
+ setInput:function (input, yy) {
378
+ this.yy = yy || this.yy || {};
379
+ this._input = input;
380
+ this._more = this._backtrack = this.done = false;
381
+ this.yylineno = this.yyleng = 0;
382
+ this.yytext = this.matched = this.match = '';
383
+ this.conditionStack = ['INITIAL'];
384
+ this.yylloc = {
385
+ first_line: 1,
386
+ first_column: 0,
387
+ last_line: 1,
388
+ last_column: 0
389
+ };
390
+ if (this.options.ranges) {
391
+ this.yylloc.range = [0,0];
392
+ }
393
+ this.offset = 0;
394
+ return this;
395
+ },
396
+
397
+ // consumes and returns one char from the input
398
+ input:function () {
399
+ var ch = this._input[0];
400
+ this.yytext += ch;
401
+ this.yyleng++;
402
+ this.offset++;
403
+ this.match += ch;
404
+ this.matched += ch;
405
+ var lines = ch.match(/(?:\r\n?|\n).*/g);
406
+ if (lines) {
407
+ this.yylineno++;
408
+ this.yylloc.last_line++;
409
+ } else {
410
+ this.yylloc.last_column++;
411
+ }
412
+ if (this.options.ranges) {
413
+ this.yylloc.range[1]++;
414
+ }
415
+
416
+ this._input = this._input.slice(1);
417
+ return ch;
418
+ },
419
+
420
+ // unshifts one char (or a string) into the input
421
+ unput:function (ch) {
422
+ var len = ch.length;
423
+ var lines = ch.split(/(?:\r\n?|\n)/g);
424
+
425
+ this._input = ch + this._input;
426
+ this.yytext = this.yytext.substr(0, this.yytext.length - len);
427
+ //this.yyleng -= len;
428
+ this.offset -= len;
429
+ var oldLines = this.match.split(/(?:\r\n?|\n)/g);
430
+ this.match = this.match.substr(0, this.match.length - 1);
431
+ this.matched = this.matched.substr(0, this.matched.length - 1);
432
+
433
+ if (lines.length - 1) {
434
+ this.yylineno -= lines.length - 1;
435
+ }
436
+ var r = this.yylloc.range;
437
+
438
+ this.yylloc = {
439
+ first_line: this.yylloc.first_line,
440
+ last_line: this.yylineno + 1,
441
+ first_column: this.yylloc.first_column,
442
+ last_column: lines ?
443
+ (lines.length === oldLines.length ? this.yylloc.first_column : 0)
444
+ + oldLines[oldLines.length - lines.length].length - lines[0].length :
445
+ this.yylloc.first_column - len
446
+ };
447
+
448
+ if (this.options.ranges) {
449
+ this.yylloc.range = [r[0], r[0] + this.yyleng - len];
450
+ }
451
+ this.yyleng = this.yytext.length;
452
+ return this;
453
+ },
454
+
455
+ // When called from action, caches matched text and appends it on next action
456
+ more:function () {
457
+ this._more = true;
458
+ return this;
459
+ },
460
+
461
+ // When called from action, signals the lexer that this rule fails to match the input, so the next matching rule (regex) should be tested instead.
462
+ reject:function () {
463
+ if (this.options.backtrack_lexer) {
464
+ this._backtrack = true;
465
+ } else {
466
+ return this.parseError('Lexical error on line ' + (this.yylineno + 1) + '. You can only invoke reject() in the lexer when the lexer is of the backtracking persuasion (options.backtrack_lexer = true).\n' + this.showPosition(), {
467
+ text: "",
468
+ token: null,
469
+ line: this.yylineno
470
+ });
471
+
472
+ }
473
+ return this;
474
+ },
475
+
476
+ // retain first n characters of the match
477
+ less:function (n) {
478
+ this.unput(this.match.slice(n));
479
+ },
480
+
481
+ // displays already matched input, i.e. for error messages
482
+ pastInput:function () {
483
+ var past = this.matched.substr(0, this.matched.length - this.match.length);
484
+ return (past.length > 20 ? '...':'') + past.substr(-20).replace(/\n/g, "");
485
+ },
486
+
487
+ // displays upcoming input, i.e. for error messages
488
+ upcomingInput:function () {
489
+ var next = this.match;
490
+ if (next.length < 20) {
491
+ next += this._input.substr(0, 20-next.length);
492
+ }
493
+ return (next.substr(0,20) + (next.length > 20 ? '...' : '')).replace(/\n/g, "");
494
+ },
495
+
496
+ // displays the character position where the lexing error occurred, i.e. for error messages
497
+ showPosition:function () {
498
+ var pre = this.pastInput();
499
+ var c = new Array(pre.length + 1).join("-");
500
+ return pre + this.upcomingInput() + "\n" + c + "^";
501
+ },
502
+
503
+ // test the lexed token: return FALSE when not a match, otherwise return token
504
+ test_match:function(match, indexed_rule) {
505
+ var token,
506
+ lines,
507
+ backup;
508
+
509
+ if (this.options.backtrack_lexer) {
510
+ // save context
511
+ backup = {
512
+ yylineno: this.yylineno,
513
+ yylloc: {
514
+ first_line: this.yylloc.first_line,
515
+ last_line: this.last_line,
516
+ first_column: this.yylloc.first_column,
517
+ last_column: this.yylloc.last_column
518
+ },
519
+ yytext: this.yytext,
520
+ match: this.match,
521
+ matches: this.matches,
522
+ matched: this.matched,
523
+ yyleng: this.yyleng,
524
+ offset: this.offset,
525
+ _more: this._more,
526
+ _input: this._input,
527
+ yy: this.yy,
528
+ conditionStack: this.conditionStack.slice(0),
529
+ done: this.done
530
+ };
531
+ if (this.options.ranges) {
532
+ backup.yylloc.range = this.yylloc.range.slice(0);
533
+ }
534
+ }
535
+
536
+ lines = match[0].match(/(?:\r\n?|\n).*/g);
537
+ if (lines) {
538
+ this.yylineno += lines.length;
539
+ }
540
+ this.yylloc = {
541
+ first_line: this.yylloc.last_line,
542
+ last_line: this.yylineno + 1,
543
+ first_column: this.yylloc.last_column,
544
+ last_column: lines ?
545
+ lines[lines.length - 1].length - lines[lines.length - 1].match(/\r?\n?/)[0].length :
546
+ this.yylloc.last_column + match[0].length
547
+ };
548
+ this.yytext += match[0];
549
+ this.match += match[0];
550
+ this.matches = match;
551
+ this.yyleng = this.yytext.length;
552
+ if (this.options.ranges) {
553
+ this.yylloc.range = [this.offset, this.offset += this.yyleng];
554
+ }
555
+ this._more = false;
556
+ this._backtrack = false;
557
+ this._input = this._input.slice(match[0].length);
558
+ this.matched += match[0];
559
+ token = this.performAction.call(this, this.yy, this, indexed_rule, this.conditionStack[this.conditionStack.length - 1]);
560
+ if (this.done && this._input) {
561
+ this.done = false;
562
+ }
563
+ if (token) {
564
+ return token;
565
+ } else if (this._backtrack) {
566
+ // recover context
567
+ for (var k in backup) {
568
+ this[k] = backup[k];
569
+ }
570
+ return false; // rule action called reject() implying the next rule should be tested instead.
571
+ }
572
+ return false;
573
+ },
574
+
575
+ // return next match in input
576
+ next:function () {
577
+ if (this.done) {
578
+ return this.EOF;
579
+ }
580
+ if (!this._input) {
581
+ this.done = true;
582
+ }
583
+
584
+ var token,
585
+ match,
586
+ tempMatch,
587
+ index;
588
+ if (!this._more) {
589
+ this.yytext = '';
590
+ this.match = '';
591
+ }
592
+ var rules = this._currentRules();
593
+ for (var i = 0; i < rules.length; i++) {
594
+ tempMatch = this._input.match(this.rules[rules[i]]);
595
+ if (tempMatch && (!match || tempMatch[0].length > match[0].length)) {
596
+ match = tempMatch;
597
+ index = i;
598
+ if (this.options.backtrack_lexer) {
599
+ token = this.test_match(tempMatch, rules[i]);
600
+ if (token !== false) {
601
+ return token;
602
+ } else if (this._backtrack) {
603
+ match = false;
604
+ continue; // rule action called reject() implying a rule MISmatch.
605
+ } else {
606
+ // else: this is a lexer rule which consumes input without producing a token (e.g. whitespace)
607
+ return false;
608
+ }
609
+ } else if (!this.options.flex) {
610
+ break;
611
+ }
612
+ }
613
+ }
614
+ if (match) {
615
+ token = this.test_match(match, rules[index]);
616
+ if (token !== false) {
617
+ return token;
618
+ }
619
+ // else: this is a lexer rule which consumes input without producing a token (e.g. whitespace)
620
+ return false;
621
+ }
622
+ if (this._input === "") {
623
+ return this.EOF;
624
+ } else {
625
+ return this.parseError('Lexical error on line ' + (this.yylineno + 1) + '. Unrecognized text.\n' + this.showPosition(), {
626
+ text: "",
627
+ token: null,
628
+ line: this.yylineno
629
+ });
630
+ }
631
+ },
632
+
633
+ // return next match that has a token
634
+ lex:function lex () {
635
+ var r = this.next();
636
+ if (r) {
637
+ return r;
638
+ } else {
639
+ return this.lex();
640
+ }
641
+ },
642
+
643
+ // activates a new lexer condition state (pushes the new lexer condition state onto the condition stack)
644
+ begin:function begin (condition) {
645
+ this.conditionStack.push(condition);
646
+ },
647
+
648
+ // pop the previously active lexer condition state off the condition stack
649
+ popState:function popState () {
650
+ var n = this.conditionStack.length - 1;
651
+ if (n > 0) {
652
+ return this.conditionStack.pop();
653
+ } else {
654
+ return this.conditionStack[0];
655
+ }
656
+ },
657
+
658
+ // produce the lexer rule set which is active for the currently active lexer condition state
659
+ _currentRules:function _currentRules () {
660
+ if (this.conditionStack.length && this.conditionStack[this.conditionStack.length - 1]) {
661
+ return this.conditions[this.conditionStack[this.conditionStack.length - 1]].rules;
662
+ } else {
663
+ return this.conditions["INITIAL"].rules;
664
+ }
665
+ },
666
+
667
+ // return the currently active lexer condition state; when an index argument is provided it produces the N-th previous condition state, if available
668
+ topState:function topState (n) {
669
+ n = this.conditionStack.length - 1 - Math.abs(n || 0);
670
+ if (n >= 0) {
671
+ return this.conditionStack[n];
672
+ } else {
673
+ return "INITIAL";
674
+ }
675
+ },
676
+
677
+ // alias for begin(condition)
678
+ pushState:function pushState (condition) {
679
+ this.begin(condition);
680
+ },
681
+
682
+ // return the number of states currently on the stack
683
+ stateStackSize:function stateStackSize() {
684
+ return this.conditionStack.length;
685
+ },
686
+ options: {},
687
+ performAction: function anonymous(yy,yy_,$avoiding_name_collisions,YY_START) {
688
+ var YYSTATE=YY_START;
689
+ switch($avoiding_name_collisions) {
690
+ case 0:/* skip whitespace */
691
+ break;
692
+ case 1:/* skip newlines */
693
+ break;
694
+ case 2:/* skip line comments */
695
+ break;
696
+ case 3:/* skip block comments */
697
+ break;
698
+ case 4:return 8
699
+ break;
700
+ case 5:return 11
701
+ break;
702
+ case 6:return 10
703
+ break;
704
+ case 7:return 16
705
+ break;
706
+ case 8:return 17
707
+ break;
708
+ case 9:return 18
709
+ break;
710
+ case 10:return 19
711
+ break;
712
+ case 11:return 20
713
+ break;
714
+ case 12:return 21
715
+ break;
716
+ case 13:return 12
717
+ break;
718
+ case 14:return 14
719
+ break;
720
+ case 15:return 22
721
+ break;
722
+ case 16:return 23
723
+ break;
724
+ case 17:return 24
725
+ break;
726
+ case 18:return 25
727
+ break;
728
+ case 19:return 26
729
+ break;
730
+ case 20:return 43
731
+ break;
732
+ case 21:return 44
733
+ break;
734
+ case 22:return 41 /* draw */
735
+ break;
736
+ case 23:return 42 /* unknown */
737
+ break;
738
+ case 24:return 49
739
+ break;
740
+ case 25:return 34
741
+ break;
742
+ case 26:return 36
743
+ break;
744
+ case 27:return 37
745
+ break;
746
+ case 28:return 46
747
+ break;
748
+ case 29:return 47
749
+ break;
750
+ case 30:return 48
751
+ break;
752
+ case 31:return 38
753
+ break;
754
+ case 32:return 27
755
+ break;
756
+ case 33:return 6
757
+ break;
758
+ 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;
766
+ })();
767
+ parser.lexer = lexer;
768
+ function Parser () {
769
+ this.yy = {};
770
+ }
771
+ Parser.prototype = parser;parser.Parser = Parser;
772
+ return new Parser;
773
+ })();
774
+
775
+
776
+ if (typeof require !== 'undefined' && typeof exports !== 'undefined') {
777
+ exports.parser = tgn;
778
+ exports.Parser = tgn.Parser;
779
+ exports.parse = function () { return tgn.parse.apply(tgn, arguments); };
780
+ exports.main = function commonjsMain (args) {
781
+ if (!args[1]) {
782
+ console.log('Usage: '+args[0]+' FILE');
783
+ process.exit(1);
784
+ }
785
+ var source = require('fs').readFileSync(require('path').normalize(args[1]), "utf8");
786
+ return exports.parser.parse(source);
787
+ };
788
+ if (typeof module !== 'undefined' && require.main === module) {
789
+ exports.main(process.argv.slice(1));
790
+ }
791
+ }
trigo-web/inc/tgn/tgnParser.ts ADDED
@@ -0,0 +1,166 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * TGN Parser TypeScript Wrapper
3
+ *
4
+ * Wraps the jison-generated parser with TypeScript types
5
+ *
6
+ * Based on lotus project architecture:
7
+ * - Use jison npm package for grammar compilation at build time
8
+ * - Generate parser to .js file in build phase
9
+ * - Use synchronous parsing (no async needed)
10
+ * - Works in both browser and Node.js environments
11
+ */
12
+
13
+ /**
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
+ */
25
+ export interface ParsedMoveRound {
26
+ round: number;
27
+ action_black: ParsedMoveAction;
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
+ */
47
+ export interface ParsedTags {
48
+ Event?: string;
49
+ Site?: string;
50
+ Date?: string;
51
+ Round?: string;
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;
59
+ Annotator?: string;
60
+ Application?: string;
61
+ [key: string]: string | number[] | ParsedGameResult | undefined;
62
+ }
63
+
64
+
65
+ /**
66
+ * Parser output structure
67
+ */
68
+ export interface TGNParseResult {
69
+ tags: ParsedTags;
70
+ moves: ParsedMoveRound[] | null;
71
+ success: boolean;
72
+ }
73
+
74
+
75
+ /**
76
+ * Parser error with position information
77
+ */
78
+ export class TGNParseError extends Error {
79
+ constructor(
80
+ message: string,
81
+ public line?: number,
82
+ public column?: number,
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
98
+ */
99
+ 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
107
+ */
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)
122
+ *
123
+ * @param tgnString - TGN formatted game notation
124
+ * @returns Parsed game data with tags and moves
125
+ * @throws TGNParseError if parsing fails
126
+ */
127
+ 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 {
135
+ const result = parser.parse(tgnString);
136
+ return result as 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
144
+ );
145
+ }
146
+ }
147
+
148
+
149
+ /**
150
+ * Validate TGN string without fully parsing
151
+ * Synchronous validation (no async needed)
152
+ *
153
+ * @param tgnString - TGN formatted game notation
154
+ * @returns Object with valid flag and error message if invalid
155
+ */
156
+ export function validateTGN(tgnString: string): { valid: boolean; error?: string } {
157
+ try {
158
+ parseTGN(tgnString);
159
+ return { valid: true };
160
+ } catch (error: any) {
161
+ return {
162
+ valid: false,
163
+ error: error.message || 'Unknown validation error'
164
+ };
165
+ }
166
+ }
trigo-web/inc/trigo/ab0yz.ts ADDED
@@ -0,0 +1,120 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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.
8
+ *
9
+ * Coordinate system:
10
+ * - '0' represents the center position on an axis
11
+ * - 'a', 'b', 'c', ... from one edge toward center
12
+ * - 'z', 'y', 'x', ... from opposite edge toward center
13
+ *
14
+ * @param pos - Position array [x, y, z, ...] with 0-based indices
15
+ * @param boardShape - Board dimensions [sizeX, sizeY, sizeZ, ...]
16
+ * @returns TGN coordinate string (e.g., "000", "aa0", "bzz", "aa", "0")
17
+ *
18
+ * @example
19
+ * encodeAb0yz([2, 2, 2], [5, 5, 5]) // "000" - center of 5x5x5 board
20
+ * encodeAb0yz([0, 0, 2], [5, 5, 5]) // "aa0" - corner
21
+ * encodeAb0yz([4, 2, 2], [5, 5, 5]) // "z00"
22
+ * encodeAb0yz([0, 0, 0], [19, 19, 1]) // "aa" - 2D board (trailing 1 ignored)
23
+ */
24
+ const encodeAb0yz = (pos: number[], boardShape: number[]): string => {
25
+ const compactedShape = compactShape(boardShape);
26
+ const result: string[] = [];
27
+
28
+ for (let i = 0; i < compactedShape.length; i++) {
29
+ const size = compactedShape[i];
30
+ const center = (size - 1) / 2;
31
+ const index = pos[i];
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
39
+ } else {
40
+ // Right side: z, y, x, ...
41
+ const offset = size - 1 - index;
42
+ result.push(String.fromCharCode(122 - offset)); // 'z' = 122
43
+ }
44
+ }
45
+
46
+ return result.join('');
47
+ };
48
+
49
+
50
+ /**
51
+ * Decode a TGN coordinate string to position array.
52
+ *
53
+ * @param code - TGN coordinate string (e.g., "000", "aa0", "bzz", "aa", "0")
54
+ * @param boardShape - Board dimensions [sizeX, sizeY, sizeZ, ...]
55
+ * @returns Position array [x, y, z, ...] with 0-based indices
56
+ *
57
+ * @example
58
+ * decodeAb0yz("000", [5, 5, 5]) // [2, 2, 2] - center
59
+ * decodeAb0yz("aa0", [5, 5, 5]) // [0, 0, 2]
60
+ * decodeAb0yz("z00", [5, 5, 5]) // [4, 2, 2]
61
+ * decodeAb0yz("aa", [19, 19, 1]) // [0, 0, 0] - 2D board
62
+ */
63
+ 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[] = [];
71
+
72
+ for (let i = 0; i < compactedShape.length; i++) {
73
+ const char = code[i];
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;
88
+
89
+ // Determine if it's left side (closer to 'a') or right side (closer to 'z')
90
+ if (distFromA < distFromZ) {
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
+ }
110
+
111
+ // Fill remaining dimensions with 0 if boardShape has trailing 1s
112
+ while (result.length < boardShape.length) {
113
+ result.push(0);
114
+ }
115
+
116
+ return result;
117
+ };
118
+
119
+
120
+ export { encodeAb0yz, decodeAb0yz };
trigo-web/inc/trigo/game.ts ADDED
@@ -0,0 +1,1137 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * TrigoGame Class - Main Game State Management
3
+ *
4
+ * Modern reimplementation of prototype's trigo.Game
5
+ * Integrates game state, move history, and game logic in a single class
6
+ *
7
+ * Equivalent to: third_party/klstrigo/Source/static/js/trigo.game.js:75-601
8
+ */
9
+
10
+ import type { Position, Stone, BoardShape } from "./types";
11
+ import {
12
+ StoneType,
13
+ getEnemyColor,
14
+ validateMove,
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
+ */
49
+ export interface GameResult {
50
+ winner: "black" | "white" | "draw";
51
+ reason: "resignation" | "timeout" | "completion" | "double-pass";
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
72
+ */
73
+ export interface GameCallbacks {
74
+ onStepAdvance?: (step: Step, history: Step[]) => void;
75
+ onStepBack?: (step: Step, history: Step[]) => void;
76
+ onCapture?: (capturedPositions: Position[]) => void;
77
+ onWin?: (winner: Stone) => void;
78
+ onTerritoryChange?: (territory: TerritoryResult) => void;
79
+ }
80
+
81
+
82
+ /**
83
+ * TrigoGame - Main game class managing state, history, and logic
84
+ *
85
+ * Equivalent to trigo.Game in prototype (lines 75-395)
86
+ *
87
+ * Key features:
88
+ * - Maintains game board state
89
+ * - Tracks complete move history
90
+ * - Implements Go rules (capture, Ko, suicide)
91
+ * - Supports undo/redo functionality
92
+ * - Territory calculation
93
+ */
94
+ export class TrigoGame {
95
+ // Game configuration
96
+ private shape: BoardShape;
97
+ private callbacks: GameCallbacks;
98
+
99
+ // Game state
100
+ private board: Stone[][][];
101
+ private currentPlayer: Stone;
102
+ private stepHistory: Step[];
103
+ private currentStepIndex: number;
104
+
105
+ // Game status management
106
+ private gameStatus: GameStatus;
107
+ private gameResult?: GameResult;
108
+ private passCount: number;
109
+
110
+ // Last captured stones for Ko rule detection
111
+ private lastCapturedPositions: Position[] | null = null;
112
+
113
+ // Territory cache
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)
121
+ */
122
+ constructor(shape: BoardShape = { x: 5, y: 5, z: 5 }, callbacks: GameCallbacks = {}) {
123
+ this.shape = shape;
124
+ this.callbacks = callbacks;
125
+ this.board = this.createEmptyBoard();
126
+ this.currentPlayer = StoneType.BLACK;
127
+ this.stepHistory = [];
128
+ this.currentStepIndex = 0;
129
+ this.gameStatus = "idle";
130
+ this.gameResult = undefined;
131
+ this.passCount = 0;
132
+ }
133
+
134
+
135
+ /**
136
+ * Create an empty board
137
+ */
138
+ private createEmptyBoard(): Stone[][][] {
139
+ const board: Stone[][][] = [];
140
+ for (let x = 0; x < this.shape.x; x++) {
141
+ board[x] = [];
142
+ for (let y = 0; y < this.shape.y; y++) {
143
+ board[x][y] = [];
144
+ for (let z = 0; z < this.shape.z; z++) {
145
+ board[x][y][z] = StoneType.EMPTY;
146
+ }
147
+ }
148
+ }
149
+ return board;
150
+ }
151
+
152
+
153
+ /**
154
+ * Reset the game to initial state
155
+ * Equivalent to Game.reset() (lines 153-163)
156
+ */
157
+ reset(): void {
158
+ this.board = this.createEmptyBoard();
159
+ this.currentPlayer = StoneType.BLACK;
160
+ this.stepHistory = [];
161
+ this.currentStepIndex = 0;
162
+ this.lastCapturedPositions = null;
163
+ this.territoryDirty = true;
164
+ this.cachedTerritory = null;
165
+ this.gameStatus = "idle";
166
+ this.gameResult = undefined;
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)
183
+ */
184
+ getStone(pos: Position): Stone {
185
+ return this.board[pos.x][pos.y][pos.z];
186
+ }
187
+
188
+
189
+ /**
190
+ * Get current player
191
+ */
192
+ getCurrentPlayer(): Stone {
193
+ return this.currentPlayer;
194
+ }
195
+
196
+
197
+ /**
198
+ * Get current step number
199
+ * Equivalent to Game.currentStep() (lines 99-101)
200
+ */
201
+ getCurrentStep(): number {
202
+ return this.currentStepIndex;
203
+ }
204
+
205
+
206
+ /**
207
+ * Get move history
208
+ * Equivalent to Game.routine() (lines 103-105)
209
+ */
210
+ getHistory(): Step[] {
211
+ return [...this.stepHistory];
212
+ }
213
+
214
+
215
+ /**
216
+ * Get last move
217
+ * Equivalent to Game.lastStep() (lines 107-110)
218
+ */
219
+ getLastStep(): Step | null {
220
+ if (this.currentStepIndex > 0) {
221
+ return this.stepHistory[this.currentStepIndex - 1];
222
+ }
223
+ return null;
224
+ }
225
+
226
+
227
+ /**
228
+ * Get board shape
229
+ * Equivalent to Game.shape() (lines 87-89)
230
+ */
231
+ getShape(): BoardShape {
232
+ return { ...this.shape };
233
+ }
234
+
235
+
236
+ /**
237
+ * Get game status
238
+ */
239
+ getGameStatus(): GameStatus {
240
+ return this.gameStatus;
241
+ }
242
+
243
+
244
+ /**
245
+ * Set game status
246
+ */
247
+ setGameStatus(status: GameStatus): void {
248
+ this.gameStatus = status;
249
+ }
250
+
251
+
252
+ /**
253
+ * Get game result
254
+ */
255
+ getGameResult(): GameResult | undefined {
256
+ return this.gameResult;
257
+ }
258
+
259
+
260
+ /**
261
+ * Get consecutive pass count
262
+ */
263
+ getPassCount(): number {
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
271
+ */
272
+ private recalculatePassCount(): void {
273
+ this.passCount = 0;
274
+
275
+ // Count backwards from current position to find consecutive passes
276
+ for (let i = this.currentStepIndex - 1; i >= 0; i--) {
277
+ if (this.stepHistory[i].type === StepType.PASS) {
278
+ this.passCount++;
279
+ } else {
280
+ break; // Stop at first non-pass move
281
+ }
282
+ }
283
+ }
284
+
285
+
286
+ /**
287
+ * Start the game
288
+ */
289
+ startGame(): void {
290
+ if (this.gameStatus === "idle") {
291
+ this.gameStatus = "playing";
292
+ }
293
+ }
294
+
295
+
296
+ /**
297
+ * Check if game is active
298
+ */
299
+ isGameActive(): boolean {
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)
307
+ */
308
+ isValidMove(pos: Position, player?: Stone): { valid: boolean; reason?: string } {
309
+ const playerColor = player || this.currentPlayer;
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)
316
+ */
317
+ private resetPassCount(): void {
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)
325
+ *
326
+ * @returns true if move was successful, false otherwise
327
+ */
328
+ drop(pos: Position): boolean {
329
+ // Validate the move
330
+ const validation = this.isValidMove(pos);
331
+ if (!validation.valid) {
332
+ console.warn(`Invalid move at (${pos.x}, ${pos.y}, ${pos.z}): ${validation.reason}`);
333
+ return false;
334
+ }
335
+
336
+ // Find captured groups BEFORE placing the stone
337
+ const capturedGroups = findCapturedGroups(pos, this.currentPlayer, this.board, this.shape);
338
+
339
+ // Place the stone on the board
340
+ this.board[pos.x][pos.y][pos.z] = this.currentPlayer;
341
+
342
+ // Execute captures
343
+ const capturedPositions = executeCaptures(capturedGroups, this.board);
344
+
345
+ // Store captured positions for Ko rule
346
+ this.lastCapturedPositions = capturedPositions.length > 0 ? capturedPositions : null;
347
+
348
+ // Mark territory as dirty
349
+ this.territoryDirty = true;
350
+
351
+ // Reset pass count when a stone is placed
352
+ this.resetPassCount();
353
+
354
+ // Create step record
355
+ const step: Step = {
356
+ type: StepType.DROP,
357
+ position: pos,
358
+ player: this.currentPlayer,
359
+ capturedPositions: capturedPositions.length > 0 ? capturedPositions : undefined,
360
+ timestamp: Date.now()
361
+ };
362
+
363
+ // Advance to next step
364
+ this.advanceStep(step);
365
+
366
+ // Trigger callbacks
367
+ if (capturedPositions.length > 0 && this.callbacks.onCapture) {
368
+ this.callbacks.onCapture(capturedPositions);
369
+ }
370
+
371
+ if (this.territoryDirty && this.callbacks.onTerritoryChange) {
372
+ this.callbacks.onTerritoryChange(this.getTerritory());
373
+ }
374
+
375
+ return true;
376
+ }
377
+
378
+
379
+ /**
380
+ * Pass turn
381
+ * Equivalent to PASS step type in prototype
382
+ */
383
+ pass(): boolean {
384
+ const step: Step = {
385
+ type: StepType.PASS,
386
+ player: this.currentPlayer,
387
+ timestamp: Date.now()
388
+ };
389
+
390
+ this.lastCapturedPositions = null;
391
+
392
+ // Increment pass count
393
+ this.passCount++;
394
+
395
+ // Advance step
396
+ this.advanceStep(step);
397
+
398
+ // Check for double pass (game end condition)
399
+ if (this.passCount >= 2) {
400
+ // Calculate territory to determine winner
401
+ const territory = this.getTerritory();
402
+ const capturedCounts = this.getCapturedCounts();
403
+ const blackTotal = territory.black + capturedCounts.white; // black's territory + white stones captured
404
+ const whiteTotal = territory.white + capturedCounts.black; // white's territory + black stones captured
405
+
406
+ let winner: "black" | "white" | "draw";
407
+ if (blackTotal > whiteTotal) {
408
+ winner = "black";
409
+ } else if (whiteTotal > blackTotal) {
410
+ winner = "white";
411
+ } else {
412
+ winner = "draw";
413
+ }
414
+
415
+ this.gameResult = {
416
+ winner,
417
+ reason: "double-pass",
418
+ score: territory
419
+ };
420
+
421
+ this.gameStatus = "finished";
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
+ }
430
+
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,
444
+ player: this.currentPlayer,
445
+ timestamp: Date.now()
446
+ };
447
+
448
+ this.advanceStep(step);
449
+
450
+ // Set game result - opponent of surrendering player wins
451
+ const winner = surrenderingPlayer === StoneType.BLACK ? "white" : "black";
452
+ this.gameResult = {
453
+ winner,
454
+ reason: "resignation"
455
+ };
456
+ this.gameStatus = "finished";
457
+
458
+ // Trigger win callback for the opponent
459
+ const winnerStone = getEnemyColor(surrenderingPlayer);
460
+ if (this.callbacks.onWin) {
461
+ this.callbacks.onWin(winnerStone);
462
+ }
463
+
464
+ return true;
465
+ }
466
+
467
+
468
+ /**
469
+ * Undo last move
470
+ * Equivalent to Game.repent() (lines 197-230)
471
+ *
472
+ * @returns true if undo was successful, false if no moves to undo
473
+ */
474
+ undo(): boolean {
475
+ if (this.currentStepIndex === 0 || this.stepHistory.length === 0) {
476
+ return false;
477
+ }
478
+
479
+ const lastStep = this.stepHistory[this.currentStepIndex - 1];
480
+
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) {
488
+ const enemyColor = getEnemyColor(lastStep.player);
489
+ for (const pos of lastStep.capturedPositions) {
490
+ this.board[pos.x][pos.y][pos.z] = enemyColor;
491
+ }
492
+ }
493
+ }
494
+
495
+ // Move back in history
496
+ this.currentStepIndex--;
497
+ this.currentPlayer = lastStep.player; // Restore player who made that move
498
+
499
+ // Recalculate pass count based on new history position
500
+ this.recalculatePassCount();
501
+
502
+ // Update last captured positions for Ko rule
503
+ // Need to check the step before this one
504
+ if (this.currentStepIndex > 0) {
505
+ const previousStep = this.stepHistory[this.currentStepIndex - 1];
506
+ this.lastCapturedPositions = previousStep.capturedPositions || null;
507
+ } else {
508
+ this.lastCapturedPositions = null;
509
+ }
510
+
511
+ // Mark territory as dirty
512
+ this.territoryDirty = true;
513
+
514
+ // Trigger callback
515
+ if (this.callbacks.onStepBack) {
516
+ this.callbacks.onStepBack(lastStep, this.stepHistory.slice(0, this.currentStepIndex));
517
+ }
518
+
519
+ return true;
520
+ }
521
+
522
+
523
+ /**
524
+ * Redo next move (after undo)
525
+ *
526
+ * @returns true if redo was successful, false if no moves to redo
527
+ */
528
+ redo(): boolean {
529
+ // Check if we can redo (not at the end of history)
530
+ if (this.currentStepIndex >= this.stepHistory.length) {
531
+ return false;
532
+ }
533
+
534
+ const nextStep = this.stepHistory[this.currentStepIndex];
535
+
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) {
543
+ for (const pos of nextStep.capturedPositions) {
544
+ this.board[pos.x][pos.y][pos.z] = StoneType.EMPTY;
545
+ }
546
+ }
547
+
548
+ // Update last captured positions
549
+ this.lastCapturedPositions = nextStep.capturedPositions || null;
550
+ } else if (nextStep.type === StepType.PASS) {
551
+ this.lastCapturedPositions = null;
552
+ }
553
+
554
+ // Move forward in history
555
+ this.currentStepIndex++;
556
+ this.currentPlayer = getEnemyColor(nextStep.player); // Switch to next player
557
+
558
+ // Mark territory as dirty
559
+ this.territoryDirty = true;
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
+ */
573
+ canRedo(): boolean {
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
581
+ *
582
+ * @param index Number of moves to apply from history (0 for initial state, 1 for after first move, etc.)
583
+ * @returns true if jump was successful
584
+ */
585
+ jumpToStep(index: number): boolean {
586
+ // Validate index: allow 0 (initial state) up to stepHistory.length (all moves applied)
587
+ if (index < 0 || index > this.stepHistory.length) {
588
+ return false;
589
+ }
590
+
591
+ // If already at target index, return false (no change made)
592
+ if (index === this.currentStepIndex) {
593
+ return false;
594
+ }
595
+
596
+ // Rebuild board from scratch
597
+ this.board = this.createEmptyBoard();
598
+ this.lastCapturedPositions = null;
599
+
600
+ // Replay all moves up to (but not including) target index
601
+ // After this loop, we'll have applied 'index' number of moves
602
+ for (let i = 0; i < index; i++) {
603
+ const step = this.stepHistory[i];
604
+
605
+ if (step.type === StepType.DROP && step.position) {
606
+ const pos = step.position;
607
+
608
+ // Place the stone
609
+ this.board[pos.x][pos.y][pos.z] = step.player;
610
+
611
+ // Re-execute captures
612
+ if (step.capturedPositions) {
613
+ for (const capturedPos of step.capturedPositions) {
614
+ this.board[capturedPos.x][capturedPos.y][capturedPos.z] = StoneType.EMPTY;
615
+ }
616
+ }
617
+ }
618
+ }
619
+
620
+ // Set last captured positions from the last applied move (if any)
621
+ if (index > 0) {
622
+ const lastAppliedStep = this.stepHistory[index - 1];
623
+ if (lastAppliedStep.type === StepType.DROP) {
624
+ this.lastCapturedPositions = lastAppliedStep.capturedPositions || null;
625
+ } else if (lastAppliedStep.type === StepType.PASS) {
626
+ this.lastCapturedPositions = null;
627
+ }
628
+ } else {
629
+ this.lastCapturedPositions = null;
630
+ }
631
+
632
+ // Update current index
633
+ const oldStepIndex = this.currentStepIndex;
634
+ this.currentStepIndex = index;
635
+
636
+ // Set current player based on number of moves played
637
+ // currentStepIndex represents the number of moves applied
638
+ const movesPlayed = index;
639
+ this.currentPlayer = movesPlayed % 2 === 0 ? StoneType.BLACK : StoneType.WHITE;
640
+
641
+ // Recalculate pass count based on new history position
642
+ this.recalculatePassCount();
643
+
644
+ // Mark territory as dirty
645
+ this.territoryDirty = true;
646
+
647
+ // Trigger callback based on direction
648
+ if (index < oldStepIndex && this.callbacks.onStepBack) {
649
+ const currentStep = this.stepHistory[index];
650
+ this.callbacks.onStepBack(currentStep, this.stepHistory.slice(0, index + 1));
651
+ } else if (index > oldStepIndex && this.callbacks.onStepAdvance) {
652
+ const currentStep = this.stepHistory[index];
653
+ this.callbacks.onStepAdvance(currentStep, this.stepHistory.slice(0, index + 1));
654
+ }
655
+
656
+ return true;
657
+ }
658
+
659
+
660
+ /**
661
+ * Advance to next step
662
+ * Equivalent to Game.stepAdvance() (lines 279-287)
663
+ */
664
+ private advanceStep(step: Step): void {
665
+ // If we're not at the end of history, truncate future steps
666
+ if (this.currentStepIndex < this.stepHistory.length) {
667
+ this.stepHistory = this.stepHistory.slice(0, this.currentStepIndex);
668
+ }
669
+
670
+ // Add the new step
671
+ this.stepHistory.push(step);
672
+ this.currentStepIndex++;
673
+
674
+ // Switch player
675
+ this.currentPlayer = getEnemyColor(this.currentPlayer);
676
+
677
+ // Trigger callback
678
+ if (this.callbacks.onStepAdvance) {
679
+ this.callbacks.onStepAdvance(step, this.stepHistory);
680
+ }
681
+ }
682
+
683
+
684
+ /**
685
+ * Get territory calculation
686
+ * Equivalent to Game.blackDomain() and Game.whiteDomain() (lines 232-244)
687
+ *
688
+ * Returns cached result if territory hasn't changed
689
+ */
690
+ getTerritory(): TerritoryResult {
691
+ if (this.territoryDirty || !this.cachedTerritory) {
692
+ this.cachedTerritory = calculateTerritory(this.board, this.shape);
693
+ this.territoryDirty = false;
694
+ }
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)
702
+ */
703
+ getCapturedCounts(): { black: number; white: number } {
704
+ const counts = { black: 0, white: 0 };
705
+
706
+ // Only count captures up to current step index
707
+ for (let i = 0; i < this.currentStepIndex; i++) {
708
+ const step = this.stepHistory[i];
709
+ if (step.capturedPositions && step.capturedPositions.length > 0) {
710
+ // Captured stones belong to the enemy of the player who made the move
711
+ const enemyColor = getEnemyColor(step.player);
712
+ if (enemyColor === StoneType.BLACK) {
713
+ counts.black += step.capturedPositions.length;
714
+ } else if (enemyColor === StoneType.WHITE) {
715
+ counts.white += step.capturedPositions.length;
716
+ }
717
+ }
718
+ }
719
+
720
+ return counts;
721
+ }
722
+
723
+
724
+ /**
725
+ * Serialize game state to JSON
726
+ * Equivalent to Game.serialize() (lines 250-252)
727
+ */
728
+ toJSON(): object {
729
+ return {
730
+ shape: this.shape,
731
+ currentPlayer: this.currentPlayer,
732
+ currentStepIndex: this.currentStepIndex,
733
+ history: this.stepHistory,
734
+ board: this.board,
735
+ gameStatus: this.gameStatus,
736
+ gameResult: this.gameResult,
737
+ passCount: this.passCount
738
+ };
739
+ }
740
+
741
+
742
+ /**
743
+ * Load game state from JSON
744
+ */
745
+ fromJSON(data: any): boolean {
746
+ try {
747
+ // Validate required fields
748
+ if (!data || typeof data !== "object") {
749
+ return false;
750
+ }
751
+
752
+ if (!data.shape || !data.board || !Array.isArray(data.history)) {
753
+ return false;
754
+ }
755
+
756
+ this.shape = data.shape;
757
+ this.currentPlayer = data.currentPlayer;
758
+ this.currentStepIndex = data.currentStepIndex;
759
+ this.stepHistory = data.history || [];
760
+ this.board = data.board;
761
+ this.gameStatus = data.gameStatus || "idle";
762
+ this.gameResult = data.gameResult;
763
+ this.passCount = data.passCount || 0;
764
+
765
+ // Recalculate last captured positions
766
+ if (this.currentStepIndex > 0) {
767
+ const lastStep = this.stepHistory[this.currentStepIndex - 1];
768
+ this.lastCapturedPositions = lastStep.capturedPositions || null;
769
+ } else {
770
+ this.lastCapturedPositions = null;
771
+ }
772
+
773
+ this.territoryDirty = true;
774
+ this.cachedTerritory = null;
775
+
776
+ return true;
777
+ } catch (error) {
778
+ console.error("Failed to load game state:", error);
779
+ return false;
780
+ }
781
+ }
782
+
783
+
784
+ /**
785
+ * Get game statistics
786
+ */
787
+ getStats(): {
788
+ totalMoves: number;
789
+ blackMoves: number;
790
+ whiteMoves: number;
791
+ capturedByBlack: number;
792
+ capturedByWhite: number;
793
+ territory: TerritoryResult;
794
+ } {
795
+ const captured = this.getCapturedCounts();
796
+ const territory = this.getTerritory();
797
+
798
+ let blackMoves = 0;
799
+ let whiteMoves = 0;
800
+
801
+ for (const step of this.stepHistory.slice(0, this.currentStepIndex)) {
802
+ if (step.type === StepType.DROP) {
803
+ if (step.player === StoneType.BLACK) {
804
+ blackMoves++;
805
+ } else if (step.player === StoneType.WHITE) {
806
+ whiteMoves++;
807
+ }
808
+ }
809
+ }
810
+
811
+ return {
812
+ totalMoves: this.currentStepIndex,
813
+ blackMoves,
814
+ whiteMoves,
815
+ capturedByBlack: captured.white, // Black captures white stones
816
+ capturedByWhite: captured.black, // White captures black stones
817
+ territory
818
+ };
819
+ }
820
+
821
+
822
+ /**
823
+ * Save game state to sessionStorage
824
+ *
825
+ * @param key Storage key (default: "trigoGameState")
826
+ * @returns true if save was successful
827
+ */
828
+ saveToSessionStorage(key: string = "trigoGameState"): boolean {
829
+ // Check if sessionStorage is available (browser environment)
830
+ if (typeof globalThis !== "undefined" && (globalThis as any).sessionStorage) {
831
+ try {
832
+ const gameState = this.toJSON();
833
+ (globalThis as any).sessionStorage.setItem(key, JSON.stringify(gameState));
834
+ return true;
835
+ } catch (error) {
836
+ console.error("Failed to save game state to sessionStorage:", error);
837
+ return false;
838
+ }
839
+ }
840
+
841
+ console.warn("sessionStorage is not available");
842
+ return false;
843
+ }
844
+
845
+
846
+ /**
847
+ * Load game state from sessionStorage
848
+ *
849
+ * @param key Storage key (default: "trigoGameState")
850
+ * @returns true if load was successful
851
+ */
852
+ loadFromSessionStorage(key: string = "trigoGameState"): boolean {
853
+ // Check if sessionStorage is available (browser environment)
854
+ if (typeof globalThis !== "undefined" && (globalThis as any).sessionStorage) {
855
+ try {
856
+ const savedState = (globalThis as any).sessionStorage.getItem(key);
857
+ if (!savedState) {
858
+ console.log("No saved game state found");
859
+ return false;
860
+ }
861
+
862
+ const data = JSON.parse(savedState);
863
+ return this.fromJSON(data);
864
+ } catch (error) {
865
+ console.error("Failed to load game state from sessionStorage:", error);
866
+ return false;
867
+ }
868
+ }
869
+
870
+ console.warn("sessionStorage is not available");
871
+ return false;
872
+ }
873
+
874
+
875
+ /**
876
+ * Clear saved game state from sessionStorage
877
+ *
878
+ * @param key Storage key (default: "trigoGameState")
879
+ */
880
+ clearSessionStorage(key: string = "trigoGameState"): void {
881
+ // Check if sessionStorage is available (browser environment)
882
+ if (typeof globalThis !== "undefined" && (globalThis as any).sessionStorage) {
883
+ try {
884
+ (globalThis as any).sessionStorage.removeItem(key);
885
+ } catch (error) {
886
+ console.error("Failed to clear sessionStorage:", error);
887
+ }
888
+ } else {
889
+ console.warn("sessionStorage is not available");
890
+ }
891
+ }
892
+
893
+
894
+ /**
895
+ * Export game to TGN (Trigo Game Notation) format
896
+ *
897
+ * TGN format is similar to PGN (Portable Game Notation) for chess.
898
+ * It includes metadata tags and move sequence using ab0yz coordinate notation.
899
+ *
900
+ * @param metadata Optional metadata for the game (Event, Site, Date, Players, etc.)
901
+ * @returns TGN-formatted string
902
+ *
903
+ * @example
904
+ * const tgn = game.toTGN({
905
+ * event: "World Championship",
906
+ * site: "Tokyo",
907
+ * date: "2025.10.31",
908
+ * black: "Alice",
909
+ * white: "Bob"
910
+ * });
911
+ */
912
+ toTGN(metadata?: {
913
+ event?: string;
914
+ site?: string;
915
+ date?: string;
916
+ round?: string;
917
+ black?: string;
918
+ white?: string;
919
+ rules?: string;
920
+ timeControl?: string;
921
+ application?: string;
922
+ [key: string]: string | undefined;
923
+ }): string {
924
+ const lines: string[] = [];
925
+
926
+ // Add metadata tags
927
+ if (metadata) {
928
+ if (metadata.event) lines.push(`[Event "${metadata.event}"]`);
929
+ if (metadata.site) lines.push(`[Site "${metadata.site}"]`);
930
+ if (metadata.date) lines.push(`[Date "${metadata.date}"]`);
931
+ if (metadata.round) lines.push(`[Round "${metadata.round}"]`);
932
+ if (metadata.black) lines.push(`[Black "${metadata.black}"]`);
933
+ if (metadata.white) lines.push(`[White "${metadata.white}"]`);
934
+ }
935
+
936
+ // Add result if game is finished
937
+ if (this.gameStatus === "finished" && this.gameResult) {
938
+ let resultStr = "";
939
+ if (this.gameResult.winner === "black") {
940
+ resultStr = "B+";
941
+ } else if (this.gameResult.winner === "white") {
942
+ resultStr = "W+";
943
+ } else {
944
+ resultStr = "=";
945
+ }
946
+
947
+ // Add score information if available
948
+ if (this.gameResult.score) {
949
+ const { black, white } = this.gameResult.score;
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
966
+ if (metadata) {
967
+ if (metadata.rules) lines.push(`[Rules "${metadata.rules}"]`);
968
+ if (metadata.timeControl) lines.push(`[TimeControl "${metadata.timeControl}"]`);
969
+ if (metadata.application) lines.push(`[Application "${metadata.application}"]`);
970
+ }
971
+
972
+ // Add empty line after metadata
973
+ lines.push("");
974
+
975
+ // Generate move sequence
976
+ const moves: string[] = [];
977
+ let moveNumber = 1;
978
+
979
+ for (let i = 0; i < this.stepHistory.length; i++) {
980
+ const step = this.stepHistory[i];
981
+ let moveStr = "";
982
+
983
+ // Add move number at the start of each round (black's move)
984
+ if (step.player === StoneType.BLACK) {
985
+ moveStr = `${moveNumber}. `;
986
+ }
987
+
988
+ // Format the move
989
+ if (step.type === StepType.DROP && step.position) {
990
+ // Convert position to TGN coordinate
991
+ const pos = [step.position.x, step.position.y, step.position.z];
992
+ const boardShape = [this.shape.x, this.shape.y, this.shape.z];
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);
1002
+
1003
+ // Increment move number after white's move
1004
+ if (step.player === StoneType.WHITE) {
1005
+ moveNumber++;
1006
+ }
1007
+ }
1008
+
1009
+ // Format moves with proper line breaks
1010
+ // Group moves in pairs (black, white) on the same line
1011
+ let currentLine = "";
1012
+ for (let i = 0; i < moves.length; i++) {
1013
+ const move = moves[i];
1014
+
1015
+ if (move.match(/^\d+\./)) {
1016
+ // Start of a new round (black's move)
1017
+ if (currentLine) {
1018
+ lines.push(currentLine);
1019
+ }
1020
+ currentLine = move;
1021
+ } else {
1022
+ // White's move - add to current line
1023
+ currentLine += " " + move;
1024
+ }
1025
+ }
1026
+
1027
+ // Add the last line if it exists
1028
+ if (currentLine) {
1029
+ lines.push(currentLine);
1030
+ }
1031
+
1032
+ // Add empty line at the end
1033
+ lines.push("");
1034
+
1035
+ return lines.join("\n");
1036
+ }
1037
+
1038
+
1039
+ /**
1040
+ * Import game from TGN (Trigo Game Notation) format
1041
+ *
1042
+ * Static factory method that parses a TGN string and creates a new TrigoGame instance
1043
+ * with the board configuration and moves from the TGN file.
1044
+ *
1045
+ * Synchronous operation - requires parser to be loaded via setParserModule()
1046
+ *
1047
+ * @param tgnString TGN-formatted game notation string
1048
+ * @param callbacks Optional game callbacks
1049
+ * @returns New TrigoGame instance with the imported game state
1050
+ * @throws TGNParseError if the TGN string is invalid
1051
+ *
1052
+ * @example
1053
+ * const tgnString = `
1054
+ * [Event "World Championship"]
1055
+ * [Board "5x5x5"]
1056
+ * [Black "Alice"]
1057
+ * [White "Bob"]
1058
+ *
1059
+ * 1. 000 y00
1060
+ * 2. 0y0 pass
1061
+ * `;
1062
+ * const game = TrigoGame.fromTGN(tgnString);
1063
+ */
1064
+ static fromTGN(tgnString: string, callbacks?: GameCallbacks): TrigoGame {
1065
+ // Parse the TGN string (synchronous)
1066
+ const parsed = parseTGN(tgnString);
1067
+
1068
+ // Extract board shape from tags
1069
+ let boardShape: BoardShape;
1070
+ if (parsed.tags.Board && Array.isArray(parsed.tags.Board)) {
1071
+ const shape = parsed.tags.Board;
1072
+ // Ensure we have at least 2 dimensions, default to 1 for z if not 3D
1073
+ boardShape = {
1074
+ x: shape[0] || 5,
1075
+ y: shape[1] || 5,
1076
+ z: shape[2] || 1
1077
+ };
1078
+ } else {
1079
+ // Default to 5x5x5 if no board shape specified
1080
+ boardShape = { x: 5, y: 5, z: 5 };
1081
+ }
1082
+
1083
+ // Create new game instance using 'this' constructor
1084
+ // This allows subclasses to automatically get their own type
1085
+ const game = new this(boardShape, callbacks);
1086
+ game.startGame();
1087
+
1088
+ // Replay all moves from the parsed data
1089
+ if (parsed.moves && parsed.moves.length > 0) {
1090
+ for (const round of parsed.moves) {
1091
+ // Play black's move
1092
+ if (round.action_black) {
1093
+ game._applyParsedMove(round.action_black, boardShape);
1094
+ }
1095
+
1096
+ // Play white's move if it exists
1097
+ if (round.action_white) {
1098
+ game._applyParsedMove(round.action_white, boardShape);
1099
+ }
1100
+ }
1101
+ }
1102
+
1103
+ return game;
1104
+ }
1105
+
1106
+
1107
+ /**
1108
+ * Apply a parsed move action to the game
1109
+ * Private helper method for fromTGN
1110
+ *
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 = {
1123
+ x: coords[0],
1124
+ y: coords[1],
1125
+ z: coords[2]
1126
+ };
1127
+
1128
+ // Make the move
1129
+ this.drop(position);
1130
+ }
1131
+ }
1132
+ }
1133
+
1134
+
1135
+ // Re-export parser utilities for convenience
1136
+ export { validateTGN, TGNParseError } from "../tgn/tgnParser";
1137
+
trigo-web/inc/trigo/gameUtils.ts ADDED
@@ -0,0 +1,600 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Game Logic Service - Core Go Rules Implementation
3
+ *
4
+ * Implements capture detection, Ko rule, and territory counting
5
+ * Ported from prototype trigo.game.js to modern TypeScript
6
+ */
7
+
8
+ import type { Position, Stone, BoardShape } from "./types";
9
+
10
+ export const StoneType = {
11
+ EMPTY: 0 as Stone,
12
+ BLACK: 1 as Stone,
13
+ WHITE: 2 as Stone
14
+ };
15
+
16
+
17
+ /**
18
+ * Get the enemy color of a given stone
19
+ *
20
+ * Equivalent to trigo.enemyColor() in prototype
21
+ */
22
+ export function getEnemyColor(color: Stone): Stone {
23
+ if (color === StoneType.BLACK) return StoneType.WHITE;
24
+ if (color === StoneType.WHITE) return StoneType.BLACK;
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
+ *
44
+ * Equivalent to StoneArray.stoneNeighbors() in prototype
45
+ */
46
+ export function getNeighbors(pos: Position, shape: BoardShape): Position[] {
47
+ const neighbors: Position[] = [];
48
+
49
+ // Check all 6 directions in 3D space
50
+ const directions = [
51
+ { x: 1, y: 0, z: 0 },
52
+ { x: -1, y: 0, z: 0 },
53
+ { x: 0, y: 1, z: 0 },
54
+ { x: 0, y: -1, z: 0 },
55
+ { x: 0, y: 0, z: 1 },
56
+ { x: 0, y: 0, z: -1 }
57
+ ];
58
+
59
+ for (const dir of directions) {
60
+ const neighbor: Position = {
61
+ x: pos.x + dir.x,
62
+ y: pos.y + dir.y,
63
+ z: pos.z + dir.z
64
+ };
65
+
66
+ if (isInBounds(neighbor, shape)) {
67
+ neighbors.push(neighbor);
68
+ }
69
+ }
70
+
71
+ return neighbors;
72
+ }
73
+
74
+
75
+ /**
76
+ * Compare two positions for equality
77
+ */
78
+ 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
+ */
86
+ export class CoordSet {
87
+ private positions: Position[] = [];
88
+
89
+ has(pos: Position): boolean {
90
+ return this.positions.some((p) => positionsEqual(p, pos));
91
+ }
92
+
93
+ insert(pos: Position): boolean {
94
+ if (!this.has(pos)) {
95
+ this.positions.push(pos);
96
+ return true;
97
+ }
98
+ return false;
99
+ }
100
+
101
+ remove(pos: Position): void {
102
+ this.positions = this.positions.filter((p) => !positionsEqual(p, pos));
103
+ }
104
+
105
+ size(): number {
106
+ return this.positions.length;
107
+ }
108
+
109
+ empty(): boolean {
110
+ return this.positions.length === 0;
111
+ }
112
+
113
+ forEach(callback: (pos: Position, index: number) => void): void {
114
+ this.positions.forEach(callback);
115
+ }
116
+
117
+ toArray(): Position[] {
118
+ return [...this.positions];
119
+ }
120
+
121
+ clear(): void {
122
+ this.positions = [];
123
+ }
124
+ }
125
+
126
+
127
+ /**
128
+ * Patch - represents a connected group of same-colored stones
129
+ *
130
+ * Note: Called "Patch" in the original prototype (trigo.game.js)
131
+ * Equivalent to trigo.Game.Patch which extends trigo.CoordSet
132
+ */
133
+ export class Patch {
134
+ positions: CoordSet = new CoordSet();
135
+ color: Stone = StoneType.EMPTY;
136
+
137
+ constructor(color: Stone = StoneType.EMPTY) {
138
+ this.color = color;
139
+ }
140
+
141
+ addStone(pos: Position): void {
142
+ this.positions.insert(pos);
143
+ }
144
+
145
+ size(): number {
146
+ return this.positions.size();
147
+ }
148
+
149
+ /**
150
+ * Get all liberties (empty adjacent positions) for this group
151
+ *
152
+ * Equivalent to StoneArray.patchAir() in prototype
153
+ * Returns a CoordSet of empty positions adjacent to this patch
154
+ */
155
+ getLiberties(board: Stone[][][], shape: BoardShape): CoordSet {
156
+ const liberties = new CoordSet();
157
+
158
+ this.positions.forEach((stonePos) => {
159
+ const neighbors = getNeighbors(stonePos, shape);
160
+ for (const neighbor of neighbors) {
161
+ if (board[neighbor.x][neighbor.y][neighbor.z] === StoneType.EMPTY) {
162
+ liberties.insert(neighbor);
163
+ }
164
+ }
165
+ });
166
+
167
+ return liberties;
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
+
183
+ if (color === StoneType.EMPTY) {
184
+ return group;
185
+ }
186
+
187
+ // Flood fill to find all connected stones of the same color
188
+ const visited = new CoordSet();
189
+ const stack: Position[] = [pos];
190
+
191
+ while (stack.length > 0) {
192
+ const current = stack.pop()!;
193
+
194
+ if (visited.has(current)) {
195
+ continue;
196
+ }
197
+
198
+ visited.insert(current);
199
+
200
+ if (board[current.x][current.y][current.z] === color) {
201
+ group.addStone(current);
202
+
203
+ const neighbors = getNeighbors(current, shape);
204
+ for (const neighbor of neighbors) {
205
+ if (!visited.has(neighbor)) {
206
+ stack.push(neighbor);
207
+ }
208
+ }
209
+ }
210
+ }
211
+
212
+ return group;
213
+ }
214
+
215
+
216
+ /**
217
+ * Get all neighboring groups (different from current position's color)
218
+ */
219
+ export function getNeighborGroups(
220
+ pos: Position,
221
+ board: Stone[][][],
222
+ shape: BoardShape,
223
+ excludeEmpty: boolean = false
224
+ ): Patch[] {
225
+ const neighbors = getNeighbors(pos, shape);
226
+ const groups: Patch[] = [];
227
+ const processedPositions = new CoordSet();
228
+
229
+ for (const neighbor of neighbors) {
230
+ if (processedPositions.has(neighbor)) {
231
+ continue;
232
+ }
233
+
234
+ const stone = board[neighbor.x][neighbor.y][neighbor.z];
235
+
236
+ if (excludeEmpty && stone === StoneType.EMPTY) {
237
+ continue;
238
+ }
239
+
240
+ if (stone !== StoneType.EMPTY) {
241
+ const group = findGroup(neighbor, board, shape);
242
+ group.positions.forEach((p) => processedPositions.insert(p));
243
+ groups.push(group);
244
+ }
245
+ }
246
+
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
+ *
267
+ * Equivalent to Game.killedPatches() in prototype
268
+ *
269
+ * Note: Prototype checks patchAir <= 1 BEFORE placement.
270
+ * We create a temp board with stone placed, then check for 0 liberties AFTER.
271
+ * Both approaches produce the same result.
272
+ */
273
+ export function findCapturedGroups(
274
+ pos: Position,
275
+ playerColor: Stone,
276
+ board: Stone[][][],
277
+ shape: BoardShape
278
+ ): Patch[] {
279
+ const enemyColor = getEnemyColor(playerColor);
280
+ const captured: Patch[] = [];
281
+
282
+ // Create a temporary board with the new stone placed
283
+ const tempBoard = board.map((plane) => plane.map((row) => [...row]));
284
+ tempBoard[pos.x][pos.y][pos.z] = playerColor;
285
+
286
+ // Check all neighboring enemy groups
287
+ const neighborGroups = getNeighborGroups(pos, tempBoard, shape, true);
288
+
289
+ for (const group of neighborGroups) {
290
+ if (group.color === enemyColor) {
291
+ if (isGroupCaptured(group, tempBoard, shape)) {
292
+ captured.push(group);
293
+ }
294
+ }
295
+ }
296
+
297
+ return captured;
298
+ }
299
+
300
+
301
+ /**
302
+ * Check if placing a stone at position would result in self-capture (suicide)
303
+ *
304
+ * Equivalent to Game.isDeadStone() in prototype
305
+ *
306
+ * Exception: Move is allowed if it captures enemy stones first
307
+ */
308
+ export function isSuicideMove(
309
+ pos: Position,
310
+ playerColor: Stone,
311
+ board: Stone[][][],
312
+ shape: BoardShape
313
+ ): boolean {
314
+ // Create temporary board with the new stone
315
+ const tempBoard = board.map((plane) => plane.map((row) => [...row]));
316
+ tempBoard[pos.x][pos.y][pos.z] = playerColor;
317
+
318
+ // If this move captures enemy stones, it's not suicide
319
+ const capturedGroups = findCapturedGroups(pos, playerColor, board, shape);
320
+ if (capturedGroups.length > 0) {
321
+ return false;
322
+ }
323
+
324
+ // Check if the placed stone's group has any liberties
325
+ const placedGroup = findGroup(pos, tempBoard, shape);
326
+ const liberties = placedGroup.getLiberties(tempBoard, shape);
327
+
328
+ return liberties.size() === 0;
329
+ }
330
+
331
+
332
+ /**
333
+ * Ko Detection - check if move would recreate previous board state
334
+ *
335
+ * Equivalent to "rob" check in Game.isDropable() (lines 127-132)
336
+ * Note: Ko rule is called "rob" (打劫 dǎjié) in the prototype
337
+ *
338
+ * Ko rule: Cannot immediately recapture a single stone if it would
339
+ * return the board to the previous position
340
+ */
341
+ export function isKoViolation(
342
+ pos: Position,
343
+ playerColor: Stone,
344
+ board: Stone[][][],
345
+ shape: BoardShape,
346
+ lastCapturedPositions: Position[] | null
347
+ ): boolean {
348
+ // Ko only applies when:
349
+ // 1. We would capture exactly one stone
350
+ // 2. The previous move also captured exactly one stone
351
+ // 3. We're placing at the position of the previously captured stone
352
+
353
+ if (!lastCapturedPositions || lastCapturedPositions.length !== 1) {
354
+ return false;
355
+ }
356
+
357
+ const capturedGroups = findCapturedGroups(pos, playerColor, board, shape);
358
+
359
+ // Check if this move would capture exactly one stone
360
+ if (capturedGroups.length !== 1 || capturedGroups[0].size() !== 1) {
361
+ return false;
362
+ }
363
+
364
+ // Check if we're placing at the position that was just captured
365
+ const previouslyCaptured = lastCapturedPositions[0];
366
+ if (positionsEqual(pos, previouslyCaptured)) {
367
+ return true;
368
+ }
369
+
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) {
385
+ group.positions.forEach((pos) => {
386
+ board[pos.x][pos.y][pos.z] = StoneType.EMPTY;
387
+ capturedPositions.push(pos);
388
+ });
389
+ }
390
+
391
+ return capturedPositions;
392
+ }
393
+
394
+
395
+ /**
396
+ * Territory Calculation
397
+ *
398
+ * Determines which empty regions belong to which player
399
+ * An empty region belongs to a player if it's completely surrounded
400
+ * by that player's stones
401
+ */
402
+ export interface TerritoryResult {
403
+ black: number;
404
+ white: number;
405
+ neutral: number;
406
+ blackTerritory: Position[];
407
+ whiteTerritory: Position[];
408
+ neutralTerritory: Position[];
409
+ }
410
+
411
+
412
+ /**
413
+ * Find all connected empty positions starting from a position
414
+ */
415
+ function findEmptyRegion(
416
+ startPos: Position,
417
+ board: Stone[][][],
418
+ shape: BoardShape,
419
+ visited: CoordSet
420
+ ): CoordSet {
421
+ const region = new CoordSet();
422
+ const stack: Position[] = [startPos];
423
+
424
+ while (stack.length > 0) {
425
+ const pos = stack.pop()!;
426
+
427
+ if (visited.has(pos)) {
428
+ continue;
429
+ }
430
+
431
+ visited.insert(pos);
432
+
433
+ if (board[pos.x][pos.y][pos.z] === StoneType.EMPTY) {
434
+ region.insert(pos);
435
+
436
+ const neighbors = getNeighbors(pos, shape);
437
+ for (const neighbor of neighbors) {
438
+ if (!visited.has(neighbor)) {
439
+ stack.push(neighbor);
440
+ }
441
+ }
442
+ }
443
+ }
444
+
445
+ return region;
446
+ }
447
+
448
+
449
+ /**
450
+ * Determine which player owns an empty region
451
+ * Returns BLACK, WHITE, or EMPTY (neutral/dame)
452
+ *
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
+
474
+ if (stone !== StoneType.EMPTY) {
475
+ if (owner === StoneType.EMPTY) {
476
+ // First colored stone we've found
477
+ owner = stone;
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
+ }
485
+ });
486
+
487
+ return owner;
488
+ }
489
+
490
+
491
+ /**
492
+ * Calculate territory for both players
493
+ *
494
+ * Equivalent to Game.calculateDomainSize() in prototype (lines 305-343)
495
+ * Uses PatchList.spaceDomain() logic to determine region ownership
496
+ *
497
+ * Algorithm:
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,
508
+ neutral: 0,
509
+ blackTerritory: [],
510
+ whiteTerritory: [],
511
+ neutralTerritory: []
512
+ };
513
+
514
+ const visited = new CoordSet();
515
+ const emptyRegions: CoordSet[] = [];
516
+
517
+ // FIRST PASS: Count all stones and find all empty regions
518
+ for (let x = 0; x < shape.x; x++) {
519
+ for (let y = 0; y < shape.y; y++) {
520
+ for (let z = 0; z < shape.z; z++) {
521
+ const pos: Position = { x, y, z };
522
+ const stone = board[x][y][z];
523
+
524
+ if (stone === StoneType.BLACK) {
525
+ result.black++;
526
+ result.blackTerritory.push(pos);
527
+ } else if (stone === StoneType.WHITE) {
528
+ result.white++;
529
+ result.whiteTerritory.push(pos);
530
+ } else if (!visited.has(pos)) {
531
+ // Found an empty position - explore the region and save it
532
+ const region = findEmptyRegion(pos, board, shape, visited);
533
+ emptyRegions.push(region);
534
+ }
535
+ }
536
+ }
537
+ }
538
+
539
+ // SECOND PASS: Determine ownership of each empty region
540
+ for (const region of emptyRegions) {
541
+ const owner = determineRegionOwner(region, board, shape);
542
+ const regionArray = region.toArray();
543
+
544
+ if (owner === StoneType.BLACK) {
545
+ result.black += region.size();
546
+ result.blackTerritory.push(...regionArray);
547
+ } else if (owner === StoneType.WHITE) {
548
+ result.white += region.size();
549
+ result.whiteTerritory.push(...regionArray);
550
+ } else {
551
+ result.neutral += region.size();
552
+ result.neutralTerritory.push(...regionArray);
553
+ }
554
+ }
555
+
556
+ return result;
557
+ }
558
+
559
+
560
+ /**
561
+ * Validate if a move is legal
562
+ *
563
+ * Equivalent to Game.isDropable() in prototype
564
+ * Checks: bounds, occupation, Ko rule ("rob"), suicide rule
565
+ */
566
+ export interface MoveValidation {
567
+ valid: boolean;
568
+ reason?: string;
569
+ }
570
+
571
+
572
+ export function validateMove(
573
+ pos: Position,
574
+ playerColor: Stone,
575
+ board: Stone[][][],
576
+ shape: BoardShape,
577
+ lastCapturedPositions: Position[] | null = null
578
+ ): MoveValidation {
579
+ // Check bounds
580
+ if (!isInBounds(pos, shape)) {
581
+ return { valid: false, reason: "Position out of bounds" };
582
+ }
583
+
584
+ // Check if position is empty
585
+ if (board[pos.x][pos.y][pos.z] !== StoneType.EMPTY) {
586
+ return { valid: false, reason: "Position already occupied" };
587
+ }
588
+
589
+ // Check for Ko violation
590
+ if (isKoViolation(pos, playerColor, board, shape, lastCapturedPositions)) {
591
+ return { valid: false, reason: "Ko rule violation" };
592
+ }
593
+
594
+ // Check for suicide (self-capture)
595
+ if (isSuicideMove(pos, playerColor, board, shape)) {
596
+ return { valid: false, reason: "suicide move not allowed" };
597
+ }
598
+
599
+ return { valid: true };
600
+ }
trigo-web/inc/trigo/index.ts ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+
2
+ export * from "./types";
3
+ export * from "./gameUtils";
4
+ export * from "./game";
5
+ export * from "./typeAdapters";
trigo-web/inc/trigo/parserInit.ts ADDED
@@ -0,0 +1,135 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Parser Initialization Module
3
+ *
4
+ * Loads pre-built parsers from public/lib and injects them into parser wrappers
5
+ * This allows the same synchronous API to work in both browser and Node.js
6
+ *
7
+ * Usage in Node.js tests:
8
+ * ```
9
+ * import { initializeParsers } from "@inc/trigo/parserInit"
10
+ * await initializeParsers()
11
+ * ```
12
+ *
13
+ * Usage in Vue app:
14
+ * Add to main.ts before using any game functionality
15
+ */
16
+
17
+ import { setParserModule } from "../tgn/tgnParser";
18
+
19
+
20
+ /**
21
+ * Check if we're in a browser environment
22
+ */
23
+ 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
39
+ *
40
+ * In browser: Dynamically imports from /lib/
41
+ * In Node.js: Loads from project's public/lib directory
42
+ */
43
+ export async function initializeParsers(): Promise<void> {
44
+ try {
45
+ if (isBrowser()) {
46
+ await initializeParsersForBrowser();
47
+ } else if (isNode()) {
48
+ await initializeParsersForNode();
49
+ } else {
50
+ throw new Error("Unable to determine runtime environment");
51
+ }
52
+ } catch (error) {
53
+ console.error("❌ Failed to initialize parsers:", error);
54
+ throw error;
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
62
+ */
63
+ async function initializeParsersForBrowser(): Promise<void> {
64
+ try {
65
+ // In browser, fetch the parser from the public directory
66
+ // Vite serves /public as / so /lib/tgnParser.cjs maps to public/lib/tgnParser.cjs
67
+ const libPath = "/" + "lib/tgnParser.cjs";
68
+
69
+ const response = await fetch(libPath);
70
+ if (!response.ok) {
71
+ throw new Error(`Failed to fetch parser: ${response.status} ${response.statusText}`);
72
+ }
73
+
74
+ const code = await response.text();
75
+
76
+ // Create a CommonJS module environment
77
+ // The jison parser checks: if (typeof require !== 'undefined' && typeof exports !== 'undefined')
78
+ // So we need to make these available as variables in the eval scope
79
+ const module: any = { exports: {} };
80
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
81
+ const exports = module.exports;
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
89
+ eval(code);
90
+
91
+ // The jison parser exports: exports.parser = tgnParser
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
+
99
+ setParserModule(parser);
100
+ } catch (error) {
101
+ console.error("✗ Failed to load TGN parser:", error);
102
+ throw error;
103
+ }
104
+ }
105
+
106
+
107
+ /**
108
+ * Initialize parsers for Node.js environment
109
+ * Loads parsers from project's public/lib directory
110
+ */
111
+ async function initializeParsersForNode(): Promise<void> {
112
+ try {
113
+ // Import Node.js modules dynamically
114
+ const pathModule = await import("path");
115
+ const urlModule = await import("url");
116
+
117
+ const path = pathModule.default;
118
+ const { fileURLToPath, pathToFileURL } = urlModule;
119
+
120
+ // Get project root directory
121
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
122
+ const projectRoot = path.resolve(__dirname, "../..");
123
+ const libPath = path.resolve(projectRoot, "public/lib");
124
+
125
+ // Load TGN parser
126
+ const tgnParserPath = path.resolve(libPath, "tgnParser.cjs");
127
+ const tgnParserModule = await import(/* @vite-ignore */ pathToFileURL(tgnParserPath).href);
128
+ const tgnParser = (tgnParserModule as any).parser;
129
+
130
+ setParserModule(tgnParser);
131
+ } catch (error) {
132
+ console.error("✗ Failed to load TGN parser:", error);
133
+ throw error;
134
+ }
135
+ }
trigo-web/inc/trigo/typeAdapters.ts ADDED
@@ -0,0 +1,110 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Type Adapters - Convert between frontend string types and TrigoGame number types
3
+ *
4
+ * Frontend gameStore uses string types like "black"|"white" for better readability
5
+ * TrigoGame uses number enum (StoneType) for better performance and compatibility with legacy code
6
+ *
7
+ * This module provides conversion functions between these two systems
8
+ */
9
+
10
+ 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
+ *
18
+ * @param player "black" | "white"
19
+ * @returns StoneType.BLACK | StoneType.WHITE
20
+ */
21
+ 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
+ *
29
+ * @param stone StoneType enum value
30
+ * @returns "black" | "white"
31
+ */
32
+ export function stoneToPlayer(stone: Stone): Player {
33
+ if (stone === StoneType.BLACK) {
34
+ return "black";
35
+ } else if (stone === StoneType.WHITE) {
36
+ return "white";
37
+ }
38
+ // Default to black if EMPTY or invalid
39
+ return "black";
40
+ }
41
+
42
+
43
+ /**
44
+ * Convert Step (TrigoGame format) to Move (frontend format)
45
+ *
46
+ * @param step Step from TrigoGame history
47
+ * @returns Move for frontend gameStore
48
+ */
49
+ export function stepToMove(step: Step): Move {
50
+ const move: Move = {
51
+ player: stoneToPlayer(step.player),
52
+ timestamp: new Date(step.timestamp)
53
+ };
54
+
55
+ // If it's a DROP move, include position
56
+ if (step.position) {
57
+ move.x = step.position.x;
58
+ move.y = step.position.y;
59
+ move.z = step.position.z;
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
+ *
74
+ * @param steps Array of Steps from TrigoGame
75
+ * @returns Array of Moves for frontend
76
+ */
77
+ export function stepsToMoves(steps: Step[]): Move[] {
78
+ return steps.map(stepToMove);
79
+ }
80
+
81
+
82
+ /**
83
+ * Convert board (Stone[][][]) to frontend format (number[][][])
84
+ *
85
+ * Frontend may prefer explicit number format
86
+ * 0 = Empty, 1 = Black, 2 = White
87
+ *
88
+ * @param board TrigoGame board
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
+ *
103
+ * @param x X coordinate
104
+ * @param y Y coordinate
105
+ * @param z Z coordinate
106
+ * @returns Position object
107
+ */
108
+ export function makePosition(x: number, y: number, z: number): Position {
109
+ return { x, y, z };
110
+ }
trigo-web/inc/trigo/types.ts ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export interface Position {
2
+ x: number;
3
+ y: number;
4
+ z: number;
5
+ }
6
+
7
+ export interface Move {
8
+ x?: number;
9
+ y?: number;
10
+ z?: number;
11
+ player: "black" | "white";
12
+ timestamp?: Date;
13
+ isPass?: boolean;
14
+ }
15
+
16
+ export interface BoardShape {
17
+ x: number;
18
+ y: number;
19
+ z: number;
20
+ }
21
+
22
+ export interface GameConfig {
23
+ boardShape: BoardShape;
24
+ timeLimit?: number;
25
+ allowUndo: boolean;
26
+ }
27
+
28
+ export enum Stone {
29
+ Empty = 0,
30
+ Black = 1,
31
+ White = 2
32
+ }
33
+
34
+ export type Player = "black" | "white";
35
+
36
+ export interface GameRecord {
37
+ id: string;
38
+ moves: Move[];
39
+ result?: {
40
+ winner: Player | "draw";
41
+ reason: "resignation" | "timeout" | "completion" | "double-pass";
42
+ };
43
+ createdAt: Date;
44
+ players: {
45
+ black: string;
46
+ white: string;
47
+ };
48
+ }
trigo-web/inc/tsconfig.json ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2020",
4
+ "module": "ESNext",
5
+ "lib": ["ES2020", "DOM"],
6
+ "strict": true,
7
+ "esModuleInterop": true,
8
+ "skipLibCheck": true,
9
+ "forceConsistentCasingInFileNames": true,
10
+ "moduleResolution": "node",
11
+ "resolveJsonModule": true,
12
+ "noEmit": true
13
+ },
14
+ "include": ["**/*.ts", "**/*.d.ts"],
15
+ "exclude": ["node_modules", "dist"]
16
+ }
trigo-web/package-lock.json ADDED
The diff for this file is too large to render. See raw diff
 
trigo-web/package.json ADDED
@@ -0,0 +1,52 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "trigo-web",
3
+ "version": "1.0.0",
4
+ "type": "module",
5
+ "description": "3D Go board game with Vue3 and Node.js",
6
+ "scripts": {
7
+ "dev": "concurrently \"npm run dev:backend\" \"npm run dev:app\"",
8
+ "dev:app": "cd app && npm run dev",
9
+ "dev:backend": "cd backend && npm run dev",
10
+ "build": "npm run build:app && npm run build:backend",
11
+ "build:app": "cd app && npm run build",
12
+ "build:backend": "cd backend && npm run build",
13
+ "build:parsers": "npm run build:parser:tgn",
14
+ "build:parser:tgn": "tsx tools/buildJisonParser.ts",
15
+ "install:all": "npm install && cd app && npm install && cd ../backend && npm install",
16
+ "start:prod": "cd backend && npm start",
17
+ "format": "prettier --write \"**/*.{js,ts,vue,json,md,scss,css}\"",
18
+ "format:check": "prettier --check \"**/*.{js,ts,vue,json,md,scss,css}\"",
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",
26
+ "go",
27
+ "3d",
28
+ "vue",
29
+ "nodejs",
30
+ "websocket"
31
+ ],
32
+ "author": "",
33
+ "license": "MIT",
34
+ "devDependencies": {
35
+ "@types/node": "^24.10.0",
36
+ "@types/yargs": "^17.0.34",
37
+ "@vitejs/plugin-vue": "^5.2.4",
38
+ "@vitest/ui": "^4.0.6",
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
+ }
trigo-web/public/lib/tgnParser.cjs ADDED
@@ -0,0 +1,791 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* parser generated by jison 0.4.18 */
2
+ /*
3
+ Returns a Parser object of the following structure:
4
+
5
+ Parser: {
6
+ yy: {}
7
+ }
8
+
9
+ Parser.prototype: {
10
+ yy: {},
11
+ trace: function(),
12
+ symbols_: {associative list: name ==> number},
13
+ terminals_: {associative list: number ==> name},
14
+ productions_: [...],
15
+ performAction: function anonymous(yytext, yyleng, yylineno, yy, yystate, $$, _$),
16
+ table: [...],
17
+ defaultActions: {...},
18
+ parseError: function(str, hash),
19
+ parse: function(input),
20
+
21
+ lexer: {
22
+ EOF: 1,
23
+ parseError: function(str, hash),
24
+ setInput: function(input),
25
+ input: function(),
26
+ unput: function(str),
27
+ more: function(),
28
+ less: function(n),
29
+ pastInput: function(),
30
+ upcomingInput: function(),
31
+ showPosition: function(),
32
+ test_match: function(regex_match_array, rule_index),
33
+ next: function(),
34
+ lex: function(),
35
+ begin: function(condition),
36
+ popState: function(),
37
+ _currentRules: function(),
38
+ topState: function(),
39
+ pushState: function(condition),
40
+
41
+ options: {
42
+ ranges: boolean (optional: true ==> token location info will include a .range[] member)
43
+ flex: boolean (optional: true ==> flex-like lexing behaviour where the rules are tested exhaustively to find the longest match)
44
+ backtrack_lexer: boolean (optional: true ==> lexer regexes are tested in order and for each matching regex the action code is invoked; the lexer terminates the scan when a token is returned by the action code)
45
+ },
46
+
47
+ performAction: function(yy, yy_, $avoiding_name_collisions, YY_START),
48
+ rules: [...],
49
+ conditions: {associative list: name ==> set},
50
+ }
51
+ }
52
+
53
+
54
+ token location info (@$, _$, etc.): {
55
+ first_line: n,
56
+ last_line: n,
57
+ first_column: n,
58
+ last_column: n,
59
+ range: [start_number, end_number] (where the numbers are indexes into the input string, regular zero-based)
60
+ }
61
+
62
+
63
+ the parseError function receives a 'hash' object with these members for lexer and parser errors: {
64
+ text: (matched text)
65
+ token: (the produced terminal token, if any)
66
+ line: (yylineno)
67
+ }
68
+ while parser (grammar) errors will also provide these members, i.e. parser errors deliver a superset of attributes: {
69
+ loc: (yylloc)
70
+ expected: (string describing the set of expected tokens)
71
+ recoverable: (boolean: TRUE when the parser has a error recovery rule available for this particular error)
72
+ }
73
+ */
74
+ var tgnParser = (function(){
75
+ var o=function(k,v,o,l){for(o=o||{},l=k.length;l--;o[k[l]]=v);return o},$V0=[1,7],$V1=[2,25],$V2=[6,8,49],$V3=[1,32],$V4=[6,49],$V5=[11,49],$V6=[11,48],$V7=[1,51],$V8=[1,52],$V9=[1,53],$Va=[6,36,37,38,49];
76
+ var parser = {trace: function trace () { },
77
+ yy: {},
78
+ symbols_: {"error":2,"game":3,"tag_section":4,"move_section":5,"EOF":6,"tag_pair":7,"[":8,"tag_name":9,"STRING":10,"]":11,"TAG_RESULT":12,"game_result":13,"TAG_BOARD":14,"board_shape":15,"TAG_EVENT":16,"TAG_SITE":17,"TAG_DATE":18,"TAG_ROUND":19,"TAG_BLACK":20,"TAG_WHITE":21,"TAG_HANDICAP":22,"TAG_RULES":23,"TAG_TIMECONTROL":24,"TAG_ANNOTATOR":25,"TAG_APPLICATION":26,"TAG_NAME":27,"move_sequence":28,"move_sequence_intact":29,"move_sequence_truncated":30,"move_round":31,"move_round_half":32,"number":33,"DOT":34,"move_action":35,"PASS":36,"RESIGN":37,"COORDINATE":38,"win":39,"conquer":40,"=":41,"*":42,"RESULT_BLACK":43,"RESULT_WHITE":44,"conquer_unit":45,"POINTS":46,"STONES":47,"TIMES":48,"NUMBER":49,"$accept":0,"$end":1},
79
+ terminals_: {2:"error",6:"EOF",8:"[",10:"STRING",11:"]",12:"TAG_RESULT",14:"TAG_BOARD",16:"TAG_EVENT",17:"TAG_SITE",18:"TAG_DATE",19:"TAG_ROUND",20:"TAG_BLACK",21:"TAG_WHITE",22:"TAG_HANDICAP",23:"TAG_RULES",24:"TAG_TIMECONTROL",25:"TAG_ANNOTATOR",26:"TAG_APPLICATION",27:"TAG_NAME",34:"DOT",36:"PASS",37:"RESIGN",38:"COORDINATE",41:"=",42:"*",43:"RESULT_BLACK",44:"RESULT_WHITE",46:"POINTS",47:"STONES",48:"TIMES",49:"NUMBER"},
80
+ productions_: [0,[3,3],[3,2],[3,2],[3,1],[4,1],[4,2],[7,4],[7,4],[7,4],[9,1],[9,1],[9,1],[9,1],[9,1],[9,1],[9,1],[9,1],[9,1],[9,1],[9,1],[9,1],[5,1],[28,1],[28,1],[29,0],[29,2],[30,2],[31,4],[32,3],[35,1],[35,1],[35,1],[13,1],[13,2],[13,1],[13,1],[39,1],[39,1],[40,2],[45,1],[45,1],[15,1],[15,3],[33,1]],
81
+ performAction: function anonymous(yytext, yyleng, yylineno, yy, yystate /* action[1] */, $$ /* vstack */, _$ /* lstack */) {
82
+ /* this == yyval */
83
+
84
+ var $0 = $$.length - 1;
85
+ switch (yystate) {
86
+ case 1:
87
+
88
+ return {
89
+ tags: $$[$0-2],
90
+ moves: $$[$0-1],
91
+ success: true
92
+ };
93
+
94
+ break;
95
+ case 2:
96
+
97
+ return {
98
+ tags: $$[$0-1],
99
+ moves: null,
100
+ success: true
101
+ };
102
+
103
+ break;
104
+ case 3:
105
+
106
+ return {
107
+ tags: {},
108
+ moves: $$[$0-1],
109
+ success: true
110
+ };
111
+
112
+ break;
113
+ case 4:
114
+
115
+ return {
116
+ tags: {},
117
+ moves: null,
118
+ success: true
119
+ };
120
+
121
+ break;
122
+ case 5: case 23: case 24:
123
+ this.$ = $$[$0];
124
+ break;
125
+ case 6:
126
+ this.$ = Object.assign({}, $$[$0-1], $$[$0]);
127
+ break;
128
+ case 7:
129
+
130
+ const tagName = $$[$0-2];
131
+ const tagValue = $$[$0-1].slice(1, -1); // Remove quotes
132
+ this.$ = { [tagName]: tagValue };
133
+
134
+ break;
135
+ case 8:
136
+ this.$ = $$[$0-1];
137
+ break;
138
+ case 9:
139
+ this.$ = ({[$$[$0-2]]: $$[$0-1]});
140
+ break;
141
+ case 21:
142
+ this.$ = yytext;
143
+ break;
144
+ case 25:
145
+ this.$ = [];
146
+ break;
147
+ case 26: case 27:
148
+ this.$ = $$[$0-1].concat([$$[$0]]);
149
+ break;
150
+ case 28:
151
+ this.$ = ({ round: $$[$0-3], action_black: $$[$0-1], action_white: $$[$0] });
152
+ break;
153
+ case 29:
154
+ this.$ = ({ round: $$[$0-2], action_black: $$[$0] });
155
+ break;
156
+ case 30:
157
+ this.$ = ({ type: 'pass' });
158
+ break;
159
+ case 31:
160
+ this.$ = ({ type: 'resign' });
161
+ break;
162
+ case 32:
163
+
164
+ // Placeholder: Parse coordinate notation
165
+ this.$ = {
166
+ type: 'move',
167
+ position: yytext
168
+ };
169
+
170
+ break;
171
+ case 33:
172
+ this.$ = ({Result: $$[$0]});
173
+ break;
174
+ case 34:
175
+ this.$ = ({Result: $$[$0-1], Conquer: $$[$0]});
176
+ break;
177
+ case 35:
178
+ this.$ = ({Result: "draw"});
179
+ break;
180
+ case 36:
181
+ this.$ = ({Result: "unknown"});
182
+ break;
183
+ case 37:
184
+ this.$ = "black win";
185
+ break;
186
+ case 38:
187
+ this.$ = "white win";
188
+ break;
189
+ case 39:
190
+ this.$ = ({n: $$[$0-1], unit: $$[$0]});
191
+ break;
192
+ case 42:
193
+ this.$ = [$$[$0]];
194
+ break;
195
+ case 43:
196
+ this.$ = $$[$0-2].concat($$[$0]);
197
+ break;
198
+ case 44:
199
+ this.$ = parseInt($$[$0]);
200
+ break;
201
+ }
202
+ },
203
+ table: [{3:1,4:2,5:3,6:[1,4],7:5,8:$V0,28:6,29:8,30:9,49:$V1},{1:[3]},{5:10,6:[1,11],7:12,8:$V0,28:6,29:8,30:9,49:$V1},{6:[1,13]},{1:[2,4]},o($V2,[2,5]),{6:[2,22]},{9:14,12:[1,15],14:[1,16],16:[1,17],17:[1,18],18:[1,19],19:[1,20],20:[1,21],21:[1,22],22:[1,23],23:[1,24],24:[1,25],25:[1,26],26:[1,27],27:[1,28]},{6:[2,23],31:29,32:30,33:31,49:$V3},{6:[2,24]},{6:[1,33]},{1:[2,2]},o($V2,[2,6]),{1:[2,3]},{10:[1,34]},{13:35,39:36,41:[1,37],42:[1,38],43:[1,39],44:[1,40]},{15:41,33:42,49:$V3},{10:[2,10]},{10:[2,11]},{10:[2,12]},{10:[2,13]},{10:[2,14]},{10:[2,15]},{10:[2,16]},{10:[2,17]},{10:[2,18]},{10:[2,19]},{10:[2,20]},{10:[2,21]},o($V4,[2,26]),{6:[2,27]},{34:[1,43]},o([11,34,46,47,48],[2,44]),{1:[2,1]},{11:[1,44]},{11:[1,45]},{11:[2,33],33:47,40:46,49:$V3},{11:[2,35]},{11:[2,36]},o($V5,[2,37]),o($V5,[2,38]),{11:[1,48],48:[1,49]},o($V6,[2,42]),{35:50,36:$V7,37:$V8,38:$V9},o($V2,[2,7]),o($V2,[2,8]),{11:[2,34]},{45:54,46:[1,55],47:[1,56]},o($V2,[2,9]),{33:57,49:$V3},{6:[2,29],35:58,36:$V7,37:$V8,38:$V9},o($Va,[2,30]),o($Va,[2,31]),o($Va,[2,32]),{11:[2,39]},{11:[2,40]},{11:[2,41]},o($V6,[2,43]),o($V4,[2,28])],
204
+ defaultActions: {4:[2,4],6:[2,22],9:[2,24],11:[2,2],13:[2,3],17:[2,10],18:[2,11],19:[2,12],20:[2,13],21:[2,14],22:[2,15],23:[2,16],24:[2,17],25:[2,18],26:[2,19],27:[2,20],28:[2,21],30:[2,27],33:[2,1],37:[2,35],38:[2,36],46:[2,34],54:[2,39],55:[2,40],56:[2,41]},
205
+ parseError: function parseError (str, hash) {
206
+ if (hash.recoverable) {
207
+ this.trace(str);
208
+ } else {
209
+ var error = new Error(str);
210
+ error.hash = hash;
211
+ throw error;
212
+ }
213
+ },
214
+ parse: function parse(input) {
215
+ var self = this, stack = [0], tstack = [], vstack = [null], lstack = [], table = this.table, yytext = '', yylineno = 0, yyleng = 0, recovering = 0, TERROR = 2, EOF = 1;
216
+ var args = lstack.slice.call(arguments, 1);
217
+ var lexer = Object.create(this.lexer);
218
+ var sharedState = { yy: {} };
219
+ for (var k in this.yy) {
220
+ if (Object.prototype.hasOwnProperty.call(this.yy, k)) {
221
+ sharedState.yy[k] = this.yy[k];
222
+ }
223
+ }
224
+ lexer.setInput(input, sharedState.yy);
225
+ sharedState.yy.lexer = lexer;
226
+ sharedState.yy.parser = this;
227
+ if (typeof lexer.yylloc == 'undefined') {
228
+ lexer.yylloc = {};
229
+ }
230
+ var yyloc = lexer.yylloc;
231
+ lstack.push(yyloc);
232
+ var ranges = lexer.options && lexer.options.ranges;
233
+ if (typeof sharedState.yy.parseError === 'function') {
234
+ this.parseError = sharedState.yy.parseError;
235
+ } else {
236
+ this.parseError = Object.getPrototypeOf(this).parseError;
237
+ }
238
+ function popStack(n) {
239
+ stack.length = stack.length - 2 * n;
240
+ vstack.length = vstack.length - n;
241
+ lstack.length = lstack.length - n;
242
+ }
243
+ _token_stack:
244
+ var lex = function () {
245
+ var token;
246
+ token = lexer.lex() || EOF;
247
+ if (typeof token !== 'number') {
248
+ token = self.symbols_[token] || token;
249
+ }
250
+ return token;
251
+ };
252
+ var symbol, preErrorSymbol, state, action, a, r, yyval = {}, p, len, newState, expected;
253
+ while (true) {
254
+ state = stack[stack.length - 1];
255
+ if (this.defaultActions[state]) {
256
+ action = this.defaultActions[state];
257
+ } else {
258
+ if (symbol === null || typeof symbol == 'undefined') {
259
+ symbol = lex();
260
+ }
261
+ action = table[state] && table[state][symbol];
262
+ }
263
+ if (typeof action === 'undefined' || !action.length || !action[0]) {
264
+ var errStr = '';
265
+ expected = [];
266
+ for (p in table[state]) {
267
+ if (this.terminals_[p] && p > TERROR) {
268
+ expected.push('\'' + this.terminals_[p] + '\'');
269
+ }
270
+ }
271
+ if (lexer.showPosition) {
272
+ errStr = 'Parse error on line ' + (yylineno + 1) + ':\n' + lexer.showPosition() + '\nExpecting ' + expected.join(', ') + ', got \'' + (this.terminals_[symbol] || symbol) + '\'';
273
+ } else {
274
+ errStr = 'Parse error on line ' + (yylineno + 1) + ': Unexpected ' + (symbol == EOF ? 'end of input' : '\'' + (this.terminals_[symbol] || symbol) + '\'');
275
+ }
276
+ this.parseError(errStr, {
277
+ text: lexer.match,
278
+ token: this.terminals_[symbol] || symbol,
279
+ line: lexer.yylineno,
280
+ loc: yyloc,
281
+ expected: expected
282
+ });
283
+ }
284
+ if (action[0] instanceof Array && action.length > 1) {
285
+ throw new Error('Parse Error: multiple actions possible at state: ' + state + ', token: ' + symbol);
286
+ }
287
+ switch (action[0]) {
288
+ case 1:
289
+ stack.push(symbol);
290
+ vstack.push(lexer.yytext);
291
+ lstack.push(lexer.yylloc);
292
+ stack.push(action[1]);
293
+ symbol = null;
294
+ if (!preErrorSymbol) {
295
+ yyleng = lexer.yyleng;
296
+ yytext = lexer.yytext;
297
+ yylineno = lexer.yylineno;
298
+ yyloc = lexer.yylloc;
299
+ if (recovering > 0) {
300
+ recovering--;
301
+ }
302
+ } else {
303
+ symbol = preErrorSymbol;
304
+ preErrorSymbol = null;
305
+ }
306
+ break;
307
+ case 2:
308
+ len = this.productions_[action[1]][1];
309
+ yyval.$ = vstack[vstack.length - len];
310
+ yyval._$ = {
311
+ first_line: lstack[lstack.length - (len || 1)].first_line,
312
+ last_line: lstack[lstack.length - 1].last_line,
313
+ first_column: lstack[lstack.length - (len || 1)].first_column,
314
+ last_column: lstack[lstack.length - 1].last_column
315
+ };
316
+ if (ranges) {
317
+ yyval._$.range = [
318
+ lstack[lstack.length - (len || 1)].range[0],
319
+ lstack[lstack.length - 1].range[1]
320
+ ];
321
+ }
322
+ r = this.performAction.apply(yyval, [
323
+ yytext,
324
+ yyleng,
325
+ yylineno,
326
+ sharedState.yy,
327
+ action[1],
328
+ vstack,
329
+ lstack
330
+ ].concat(args));
331
+ if (typeof r !== 'undefined') {
332
+ return r;
333
+ }
334
+ if (len) {
335
+ stack = stack.slice(0, -1 * len * 2);
336
+ vstack = vstack.slice(0, -1 * len);
337
+ lstack = lstack.slice(0, -1 * len);
338
+ }
339
+ stack.push(this.productions_[action[1]][0]);
340
+ vstack.push(yyval.$);
341
+ lstack.push(yyval._$);
342
+ newState = table[stack[stack.length - 2]][stack[stack.length - 1]];
343
+ stack.push(newState);
344
+ break;
345
+ case 3:
346
+ return true;
347
+ }
348
+ }
349
+ return true;
350
+ }};
351
+
352
+
353
+ /* ========== Additional JavaScript Code ========== */
354
+
355
+ // Parser configuration
356
+ parser.yy = {
357
+ // Helper functions can be added here
358
+ parseError: function(str, hash) {
359
+ throw new Error('Parse error: ' + str);
360
+ }
361
+ };
362
+ /* generated by jison-lex 0.3.4 */
363
+ var lexer = (function(){
364
+ var lexer = ({
365
+
366
+ EOF:1,
367
+
368
+ parseError:function parseError(str, hash) {
369
+ if (this.yy.parser) {
370
+ this.yy.parser.parseError(str, hash);
371
+ } else {
372
+ throw new Error(str);
373
+ }
374
+ },
375
+
376
+ // resets the lexer, sets new input
377
+ setInput:function (input, yy) {
378
+ this.yy = yy || this.yy || {};
379
+ this._input = input;
380
+ this._more = this._backtrack = this.done = false;
381
+ this.yylineno = this.yyleng = 0;
382
+ this.yytext = this.matched = this.match = '';
383
+ this.conditionStack = ['INITIAL'];
384
+ this.yylloc = {
385
+ first_line: 1,
386
+ first_column: 0,
387
+ last_line: 1,
388
+ last_column: 0
389
+ };
390
+ if (this.options.ranges) {
391
+ this.yylloc.range = [0,0];
392
+ }
393
+ this.offset = 0;
394
+ return this;
395
+ },
396
+
397
+ // consumes and returns one char from the input
398
+ input:function () {
399
+ var ch = this._input[0];
400
+ this.yytext += ch;
401
+ this.yyleng++;
402
+ this.offset++;
403
+ this.match += ch;
404
+ this.matched += ch;
405
+ var lines = ch.match(/(?:\r\n?|\n).*/g);
406
+ if (lines) {
407
+ this.yylineno++;
408
+ this.yylloc.last_line++;
409
+ } else {
410
+ this.yylloc.last_column++;
411
+ }
412
+ if (this.options.ranges) {
413
+ this.yylloc.range[1]++;
414
+ }
415
+
416
+ this._input = this._input.slice(1);
417
+ return ch;
418
+ },
419
+
420
+ // unshifts one char (or a string) into the input
421
+ unput:function (ch) {
422
+ var len = ch.length;
423
+ var lines = ch.split(/(?:\r\n?|\n)/g);
424
+
425
+ this._input = ch + this._input;
426
+ this.yytext = this.yytext.substr(0, this.yytext.length - len);
427
+ //this.yyleng -= len;
428
+ this.offset -= len;
429
+ var oldLines = this.match.split(/(?:\r\n?|\n)/g);
430
+ this.match = this.match.substr(0, this.match.length - 1);
431
+ this.matched = this.matched.substr(0, this.matched.length - 1);
432
+
433
+ if (lines.length - 1) {
434
+ this.yylineno -= lines.length - 1;
435
+ }
436
+ var r = this.yylloc.range;
437
+
438
+ this.yylloc = {
439
+ first_line: this.yylloc.first_line,
440
+ last_line: this.yylineno + 1,
441
+ first_column: this.yylloc.first_column,
442
+ last_column: lines ?
443
+ (lines.length === oldLines.length ? this.yylloc.first_column : 0)
444
+ + oldLines[oldLines.length - lines.length].length - lines[0].length :
445
+ this.yylloc.first_column - len
446
+ };
447
+
448
+ if (this.options.ranges) {
449
+ this.yylloc.range = [r[0], r[0] + this.yyleng - len];
450
+ }
451
+ this.yyleng = this.yytext.length;
452
+ return this;
453
+ },
454
+
455
+ // When called from action, caches matched text and appends it on next action
456
+ more:function () {
457
+ this._more = true;
458
+ return this;
459
+ },
460
+
461
+ // When called from action, signals the lexer that this rule fails to match the input, so the next matching rule (regex) should be tested instead.
462
+ reject:function () {
463
+ if (this.options.backtrack_lexer) {
464
+ this._backtrack = true;
465
+ } else {
466
+ return this.parseError('Lexical error on line ' + (this.yylineno + 1) + '. You can only invoke reject() in the lexer when the lexer is of the backtracking persuasion (options.backtrack_lexer = true).\n' + this.showPosition(), {
467
+ text: "",
468
+ token: null,
469
+ line: this.yylineno
470
+ });
471
+
472
+ }
473
+ return this;
474
+ },
475
+
476
+ // retain first n characters of the match
477
+ less:function (n) {
478
+ this.unput(this.match.slice(n));
479
+ },
480
+
481
+ // displays already matched input, i.e. for error messages
482
+ pastInput:function () {
483
+ var past = this.matched.substr(0, this.matched.length - this.match.length);
484
+ return (past.length > 20 ? '...':'') + past.substr(-20).replace(/\n/g, "");
485
+ },
486
+
487
+ // displays upcoming input, i.e. for error messages
488
+ upcomingInput:function () {
489
+ var next = this.match;
490
+ if (next.length < 20) {
491
+ next += this._input.substr(0, 20-next.length);
492
+ }
493
+ return (next.substr(0,20) + (next.length > 20 ? '...' : '')).replace(/\n/g, "");
494
+ },
495
+
496
+ // displays the character position where the lexing error occurred, i.e. for error messages
497
+ showPosition:function () {
498
+ var pre = this.pastInput();
499
+ var c = new Array(pre.length + 1).join("-");
500
+ return pre + this.upcomingInput() + "\n" + c + "^";
501
+ },
502
+
503
+ // test the lexed token: return FALSE when not a match, otherwise return token
504
+ test_match:function(match, indexed_rule) {
505
+ var token,
506
+ lines,
507
+ backup;
508
+
509
+ if (this.options.backtrack_lexer) {
510
+ // save context
511
+ backup = {
512
+ yylineno: this.yylineno,
513
+ yylloc: {
514
+ first_line: this.yylloc.first_line,
515
+ last_line: this.last_line,
516
+ first_column: this.yylloc.first_column,
517
+ last_column: this.yylloc.last_column
518
+ },
519
+ yytext: this.yytext,
520
+ match: this.match,
521
+ matches: this.matches,
522
+ matched: this.matched,
523
+ yyleng: this.yyleng,
524
+ offset: this.offset,
525
+ _more: this._more,
526
+ _input: this._input,
527
+ yy: this.yy,
528
+ conditionStack: this.conditionStack.slice(0),
529
+ done: this.done
530
+ };
531
+ if (this.options.ranges) {
532
+ backup.yylloc.range = this.yylloc.range.slice(0);
533
+ }
534
+ }
535
+
536
+ lines = match[0].match(/(?:\r\n?|\n).*/g);
537
+ if (lines) {
538
+ this.yylineno += lines.length;
539
+ }
540
+ this.yylloc = {
541
+ first_line: this.yylloc.last_line,
542
+ last_line: this.yylineno + 1,
543
+ first_column: this.yylloc.last_column,
544
+ last_column: lines ?
545
+ lines[lines.length - 1].length - lines[lines.length - 1].match(/\r?\n?/)[0].length :
546
+ this.yylloc.last_column + match[0].length
547
+ };
548
+ this.yytext += match[0];
549
+ this.match += match[0];
550
+ this.matches = match;
551
+ this.yyleng = this.yytext.length;
552
+ if (this.options.ranges) {
553
+ this.yylloc.range = [this.offset, this.offset += this.yyleng];
554
+ }
555
+ this._more = false;
556
+ this._backtrack = false;
557
+ this._input = this._input.slice(match[0].length);
558
+ this.matched += match[0];
559
+ token = this.performAction.call(this, this.yy, this, indexed_rule, this.conditionStack[this.conditionStack.length - 1]);
560
+ if (this.done && this._input) {
561
+ this.done = false;
562
+ }
563
+ if (token) {
564
+ return token;
565
+ } else if (this._backtrack) {
566
+ // recover context
567
+ for (var k in backup) {
568
+ this[k] = backup[k];
569
+ }
570
+ return false; // rule action called reject() implying the next rule should be tested instead.
571
+ }
572
+ return false;
573
+ },
574
+
575
+ // return next match in input
576
+ next:function () {
577
+ if (this.done) {
578
+ return this.EOF;
579
+ }
580
+ if (!this._input) {
581
+ this.done = true;
582
+ }
583
+
584
+ var token,
585
+ match,
586
+ tempMatch,
587
+ index;
588
+ if (!this._more) {
589
+ this.yytext = '';
590
+ this.match = '';
591
+ }
592
+ var rules = this._currentRules();
593
+ for (var i = 0; i < rules.length; i++) {
594
+ tempMatch = this._input.match(this.rules[rules[i]]);
595
+ if (tempMatch && (!match || tempMatch[0].length > match[0].length)) {
596
+ match = tempMatch;
597
+ index = i;
598
+ if (this.options.backtrack_lexer) {
599
+ token = this.test_match(tempMatch, rules[i]);
600
+ if (token !== false) {
601
+ return token;
602
+ } else if (this._backtrack) {
603
+ match = false;
604
+ continue; // rule action called reject() implying a rule MISmatch.
605
+ } else {
606
+ // else: this is a lexer rule which consumes input without producing a token (e.g. whitespace)
607
+ return false;
608
+ }
609
+ } else if (!this.options.flex) {
610
+ break;
611
+ }
612
+ }
613
+ }
614
+ if (match) {
615
+ token = this.test_match(match, rules[index]);
616
+ if (token !== false) {
617
+ return token;
618
+ }
619
+ // else: this is a lexer rule which consumes input without producing a token (e.g. whitespace)
620
+ return false;
621
+ }
622
+ if (this._input === "") {
623
+ return this.EOF;
624
+ } else {
625
+ return this.parseError('Lexical error on line ' + (this.yylineno + 1) + '. Unrecognized text.\n' + this.showPosition(), {
626
+ text: "",
627
+ token: null,
628
+ line: this.yylineno
629
+ });
630
+ }
631
+ },
632
+
633
+ // return next match that has a token
634
+ lex:function lex () {
635
+ var r = this.next();
636
+ if (r) {
637
+ return r;
638
+ } else {
639
+ return this.lex();
640
+ }
641
+ },
642
+
643
+ // activates a new lexer condition state (pushes the new lexer condition state onto the condition stack)
644
+ begin:function begin (condition) {
645
+ this.conditionStack.push(condition);
646
+ },
647
+
648
+ // pop the previously active lexer condition state off the condition stack
649
+ popState:function popState () {
650
+ var n = this.conditionStack.length - 1;
651
+ if (n > 0) {
652
+ return this.conditionStack.pop();
653
+ } else {
654
+ return this.conditionStack[0];
655
+ }
656
+ },
657
+
658
+ // produce the lexer rule set which is active for the currently active lexer condition state
659
+ _currentRules:function _currentRules () {
660
+ if (this.conditionStack.length && this.conditionStack[this.conditionStack.length - 1]) {
661
+ return this.conditions[this.conditionStack[this.conditionStack.length - 1]].rules;
662
+ } else {
663
+ return this.conditions["INITIAL"].rules;
664
+ }
665
+ },
666
+
667
+ // return the currently active lexer condition state; when an index argument is provided it produces the N-th previous condition state, if available
668
+ topState:function topState (n) {
669
+ n = this.conditionStack.length - 1 - Math.abs(n || 0);
670
+ if (n >= 0) {
671
+ return this.conditionStack[n];
672
+ } else {
673
+ return "INITIAL";
674
+ }
675
+ },
676
+
677
+ // alias for begin(condition)
678
+ pushState:function pushState (condition) {
679
+ this.begin(condition);
680
+ },
681
+
682
+ // return the number of states currently on the stack
683
+ stateStackSize:function stateStackSize() {
684
+ return this.conditionStack.length;
685
+ },
686
+ options: {},
687
+ performAction: function anonymous(yy,yy_,$avoiding_name_collisions,YY_START) {
688
+ var YYSTATE=YY_START;
689
+ switch($avoiding_name_collisions) {
690
+ case 0:/* skip whitespace */
691
+ break;
692
+ case 1:/* skip newlines */
693
+ break;
694
+ case 2:/* skip line comments */
695
+ break;
696
+ case 3:/* skip block comments */
697
+ break;
698
+ case 4:return 8
699
+ break;
700
+ case 5:return 11
701
+ break;
702
+ case 6:return 10
703
+ break;
704
+ case 7:return 16
705
+ break;
706
+ case 8:return 17
707
+ break;
708
+ case 9:return 18
709
+ break;
710
+ case 10:return 19
711
+ break;
712
+ case 11:return 20
713
+ break;
714
+ case 12:return 21
715
+ break;
716
+ case 13:return 12
717
+ break;
718
+ case 14:return 14
719
+ break;
720
+ case 15:return 22
721
+ break;
722
+ case 16:return 23
723
+ break;
724
+ case 17:return 24
725
+ break;
726
+ case 18:return 25
727
+ break;
728
+ case 19:return 26
729
+ break;
730
+ case 20:return 43
731
+ break;
732
+ case 21:return 44
733
+ break;
734
+ case 22:return 41 /* draw */
735
+ break;
736
+ case 23:return 42 /* unknown */
737
+ break;
738
+ case 24:return 49
739
+ break;
740
+ case 25:return 34
741
+ break;
742
+ case 26:return 36
743
+ break;
744
+ case 27:return 37
745
+ break;
746
+ case 28:return 46
747
+ break;
748
+ case 29:return 47
749
+ break;
750
+ case 30:return 48
751
+ break;
752
+ case 31:return 38
753
+ break;
754
+ case 32:return 27
755
+ break;
756
+ case 33:return 6
757
+ break;
758
+ 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;
766
+ })();
767
+ parser.lexer = lexer;
768
+ function Parser () {
769
+ this.yy = {};
770
+ }
771
+ Parser.prototype = parser;parser.Parser = Parser;
772
+ return new Parser;
773
+ })();
774
+
775
+
776
+ if (typeof require !== 'undefined' && typeof exports !== 'undefined') {
777
+ exports.parser = tgnParser;
778
+ exports.Parser = tgnParser.Parser;
779
+ exports.parse = function () { return tgnParser.parse.apply(tgnParser, arguments); };
780
+ exports.main = function commonjsMain (args) {
781
+ if (!args[1]) {
782
+ console.log('Usage: '+args[0]+' FILE');
783
+ process.exit(1);
784
+ }
785
+ var source = require('fs').readFileSync(require('path').normalize(args[1]), "utf8");
786
+ return exports.parser.parse(source);
787
+ };
788
+ if (typeof module !== 'undefined' && require.main === module) {
789
+ exports.main(process.argv.slice(1));
790
+ }
791
+ }
trigo-web/tools/README.md ADDED
@@ -0,0 +1,152 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Trigo Tools
2
+
3
+ Development tools for the Trigo project.
4
+
5
+ ## Random Game Generator
6
+
7
+ Generate random Trigo games in TGN (Trigo Game Notation) format for testing, development, and analysis.
8
+
9
+ ### Usage
10
+
11
+ ```bash
12
+ # Generate 10 games with default settings (play until territory settled)
13
+ npm run generate:games
14
+
15
+ # Generate 100 games with custom move range
16
+ npm run generate:games -- --count 100 --moves 20-80
17
+
18
+ # Generate games with exactly 30 moves each
19
+ npm run generate:games -- --moves 30 --count 50
20
+
21
+ # Generate games on 3x3x3 board (plays until settled)
22
+ npm run generate:games -- --board "3*3*3" --count 50
23
+
24
+ # Generate 2D games (9x9x1 board)
25
+ npm run generate:games -- --board "9*9*1" --count 20
26
+
27
+ # Higher pass probability
28
+ npm run generate:games -- --pass-chance 0.3
29
+
30
+ # Custom output directory
31
+ npm run generate:games -- --output "my_games" --count 50
32
+ ```
33
+
34
+ ### Options
35
+
36
+ | Option | Short | Default | Description |
37
+ |--------|-------|---------|-------------|
38
+ | `--count` | `-c` | 10 | Number of games to generate |
39
+ | `--moves` | `-m` | (settled) | Moves per game as "MIN-MAX" or single number. Default: play until neutral territory = 0 |
40
+ | `--pass-chance` | | 0 | Probability of passing (0.0-1.0) |
41
+ | `--board` | `-b` | "random" | Board size as "X*Y*Z" or "random" |
42
+ | `--output` | `-o` | "output" | Output directory (relative to tools/) |
43
+ | `--help` | `-h` | - | Show help message |
44
+
45
+ ### How It Works
46
+
47
+ **Default Mode (no --moves specified):**
48
+ 1. Creates a new Trigo game with the specified board shape
49
+ 2. Plays random valid moves until the board reaches 90% coverage
50
+ 3. After 90% coverage, checks territory after each move
51
+ 4. Stops when neutral territory reaches 0 (game is fully settled)
52
+ 5. Results in complete, finished games with all territory claimed
53
+
54
+ **Custom Move Mode (--moves specified):**
55
+ 1. Creates a new Trigo game with the specified board shape
56
+ 2. Randomly selects valid moves for the specified number of moves
57
+ 3. Respects Go rules: Ko, suicide prevention, capture
58
+ 4. Games may end early on double-pass
59
+ 5. Exports each game to TGN format with metadata
60
+
61
+ ### Output Format
62
+
63
+ Generated files are named using SHA-256 hash of the TGN content: `game_<HASH>.tgn`
64
+
65
+ This ensures:
66
+ - **Content-based naming**: Same game content = same filename
67
+ - **Duplicate detection**: Identical games won't overwrite each other
68
+ - **Deterministic**: Reproducible filenames independent of generation time
69
+
70
+ Example output:
71
+ ```
72
+ tools/output/
73
+ ├── game_a3f5c8d912e4b6f1.tgn
74
+ ├── game_7b2d9e1a4c8f3605.tgn
75
+ └── game_9e4b6f1a3f5c8d91.tgn
76
+ ```
77
+
78
+ ### TGN Format Example
79
+
80
+ ```tgn
81
+ [Event "Random Generated Game"]
82
+ [Site "Batch Generator"]
83
+ [Date "2025.11.03"]
84
+ [Black "Random Black"]
85
+ [White "Random White"]
86
+ [Board "5x5x5"]
87
+ [Rules "Chinese"]
88
+ [Application "Trigo Random Generator v1.0 (5×5×5)"]
89
+
90
+ 1. 000 y00
91
+ 2. 0y0 yy0
92
+ 3. aaa zzz
93
+ 4. pass 0az
94
+ 5. z0z pass
95
+ ```
96
+
97
+ ### Performance
98
+
99
+ - **Small boards (3×3×3)**: ~250 games/second
100
+ - **Standard boards (5×5×5)**: ~75 games/second
101
+ - **Large boards (9×9×1)**: ~33 games/second
102
+
103
+ Generating 1000 games on a 5×5×5 board takes approximately 13 seconds.
104
+
105
+ ### Use Cases
106
+
107
+ 1. **Testing TGN parser** - Create diverse test files for import functionality
108
+ 2. **Performance testing** - Generate large datasets for board rendering tests
109
+ 3. **AI training** - Create training data for future AI opponent
110
+ 4. **Game analysis** - Study patterns in random gameplay
111
+ 5. **Format validation** - Verify TGN export works correctly across scenarios
112
+
113
+ ### Board Sizes
114
+
115
+ **3D Boards:**
116
+ - `3*3*3` - Tiny cube (27 positions)
117
+ - `5*5*5` - Standard cube (125 positions)
118
+ - `7*7*7` - Large cube (343 positions)
119
+ - `9*9*2` - Wide board (162 positions)
120
+
121
+ **2D Boards (Traditional Go):**
122
+ - `9*9*1` - Small board (81 positions)
123
+ - `13*13*1` - Medium board (169 positions)
124
+ - `19*19*1` - Standard board (361 positions)
125
+
126
+ ### Implementation Details
127
+
128
+ **File:** `tools/generateRandomGames.ts`
129
+
130
+ **Key Functions:**
131
+ - `generateRandomGame()` - Simulates a complete random game
132
+ - `getValidMoves()` - Finds all legal moves for current player
133
+ - `selectRandomMove()` - Randomly picks from valid moves
134
+ - `generateBatch()` - Generates multiple games in parallel
135
+
136
+ **Safety Features:**
137
+ - Validates all moves using game rules (Ko, suicide, occupation)
138
+ - Handles cases where no valid moves exist (must pass)
139
+ - Respects double-pass game end condition
140
+ - Progress indicator for large batches
141
+ - Error handling and validation
142
+
143
+ ### Dependencies
144
+
145
+ - **tsx** - TypeScript execution
146
+ - **yargs** - Advanced command-line argument parsing with validation
147
+ - **TrigoGame** - Core game logic from `inc/trigo/game.ts`
148
+ - **Node.js fs/path** - File system operations
149
+
150
+ ---
151
+
152
+ For more information about TGN format, see `/tests/game/trigoGame.tgn.test.ts`
trigo-web/tools/buildJisonParser.ts ADDED
@@ -0,0 +1,74 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Build script for Jison parser generation
3
+ *
4
+ * Generates pre-compiled parser to public/lib/ directory for use in browser
5
+ * Following lotus project architecture:
6
+ * - Use jison npm package CLI to compile grammar
7
+ * - Pre-build parsers to static .js files
8
+ * - Serve from public directory
9
+ * - Works in both browser and Node.js
10
+ */
11
+
12
+ import { execSync } from "child_process";
13
+ import fs from "fs";
14
+ import path from "path";
15
+ import { fileURLToPath } from "url";
16
+
17
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
18
+ const projectRoot = path.resolve(__dirname, "..");
19
+
20
+
21
+ /**
22
+ * Build a single parser from grammar file
23
+ * @param grammarPath - Path to .jison grammar file
24
+ * @param targetPath - Where to write the compiled parser
25
+ */
26
+ function buildParser(grammarPath: string, targetPath: string): void {
27
+ console.log(`Building parser: ${grammarPath} → ${targetPath}`);
28
+
29
+ // Ensure output directory exists
30
+ const targetDir = path.dirname(targetPath);
31
+ if (!fs.existsSync(targetDir)) {
32
+ fs.mkdirSync(targetDir, { recursive: true });
33
+ console.log(`Created directory: ${targetDir}`);
34
+ }
35
+
36
+ // Run jison CLI to generate parser
37
+ // Using -m commonjs for CommonJS module format (works in Node.js and Vite)
38
+ // Outputs to .cjs file which will be treated as CommonJS
39
+ try {
40
+ execSync(`npx jison "${grammarPath}" -o "${targetPath.replace(".js", ".cjs")}" -m commonjs`, {
41
+ cwd: projectRoot,
42
+ stdio: "inherit"
43
+ });
44
+ console.log(`✓ Parser written to: ${targetPath.replace(".js", ".cjs")}`);
45
+ } catch (error) {
46
+ console.error(`✗ Failed to build parser:`, error);
47
+ throw error;
48
+ }
49
+ }
50
+
51
+
52
+ /**
53
+ * Main build function
54
+ */
55
+ function main(): void {
56
+ try {
57
+ console.log("🔨 Building Jison parsers...\n");
58
+
59
+ // Build TGN parser to public/lib/
60
+ const tgnGrammarPath = path.resolve(projectRoot, "inc/tgn/tgn.jison");
61
+ const tgnOutputPath = path.resolve(projectRoot, "public/lib/tgnParser.js");
62
+
63
+ buildParser(tgnGrammarPath, tgnOutputPath);
64
+
65
+ console.log("\n✅ Done building parsers!");
66
+ } catch (error) {
67
+ console.error("❌ Error building parsers:", error);
68
+ process.exit(1);
69
+ }
70
+ }
71
+
72
+
73
+ // Run main function
74
+ main();
trigo-web/tools/generateRandomGames.ts ADDED
@@ -0,0 +1,465 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Random Trigo Game Generator
4
+ *
5
+ * Generates random Trigo games and exports them as TGN files.
6
+ * Useful for testing, development, and creating training data.
7
+ *
8
+ * Usage:
9
+ * npm run generate:games
10
+ * npm run generate:games -- --count 100 --min-moves 20 --max-moves 80
11
+ * npm run generate:games -- --board "3*3*3" --count 50
12
+ */
13
+
14
+ import { TrigoGame, StoneType } from "../inc/trigo/game.js";
15
+ import type { BoardShape, Position } from "../inc/trigo/types.js";
16
+ import { calculateTerritory } from "../inc/trigo/gameUtils.js";
17
+ import * as fs from "fs";
18
+ import * as path from "path";
19
+ import * as crypto from "crypto";
20
+ import { fileURLToPath } from "url";
21
+ import yargs from "yargs";
22
+ import { hideBin } from "yargs/helpers";
23
+
24
+
25
+ const __filename = fileURLToPath(import.meta.url);
26
+ const __dirname = path.dirname(__filename);
27
+
28
+
29
+ type BoardShapeTuple = [number, number, number];
30
+
31
+
32
+ const arangeShape = (min: BoardShapeTuple, max: BoardShapeTuple): BoardShapeTuple[] => {
33
+ // traverse all shapes in the range between min & max (boundary includes)
34
+ const result: BoardShapeTuple[] = [];
35
+
36
+ for (let x = min[0]; x <= max[0]; x++) {
37
+ for (let y = min[1]; y <= max[1]; y++) {
38
+ for (let z = min[2]; z <= max[2]; z++) {
39
+ result.push([x, y, z]);
40
+ }
41
+ }
42
+ }
43
+
44
+ return result;
45
+ };
46
+
47
+
48
+ const CANDIDATE_BOARD_SHAPES = [
49
+ ...arangeShape([2, 1, 1], [19, 19, 1]),
50
+ ...arangeShape([2, 2, 2], [9, 9, 9]),
51
+ ];
52
+
53
+
54
+ /**
55
+ * Generator configuration options
56
+ */
57
+ interface GeneratorOptions {
58
+ minMoves: number;
59
+ maxMoves: number;
60
+ passChance: number;
61
+ boardShape?: BoardShape;
62
+ outputDir: string;
63
+ moveToEndGame: boolean; // true if using default, false if user-specified
64
+ }
65
+
66
+
67
+ /**
68
+ * Parse board shape string (e.g., "5*5*5" or "9*9*1")
69
+ * Special value "random" selects randomly from CANDIDATE_BOARD_SHAPES
70
+ */
71
+ function parseBoardShape(shapeStr: string): BoardShape {
72
+ // Handle random selection
73
+ if (shapeStr.toLowerCase() === "random") {
74
+ const randomIndex = Math.floor(Math.random() * CANDIDATE_BOARD_SHAPES.length);
75
+ const [x, y, z] = CANDIDATE_BOARD_SHAPES[randomIndex];
76
+ console.log(` [Random board selected: ${x}×${y}×${z}]`);
77
+ return { x, y, z };
78
+ }
79
+
80
+ // Parse explicit board shape
81
+ const parts = shapeStr.split(/[^0-9]+/).filter(Boolean).map(Number);
82
+ if (parts.length !== 3) {
83
+ throw new Error(`Invalid board shape: ${shapeStr}. Expected format: "X*Y*Z" or "random"`);
84
+ }
85
+ return { x: parts[0], y: parts[1], z: parts[2] };
86
+ }
87
+
88
+
89
+ /**
90
+ * Get all empty positions on the board
91
+ */
92
+ function getAllEmptyPositions(game: TrigoGame): Position[] {
93
+ const board = game.getBoard();
94
+ const shape = game.getShape();
95
+ const emptyPositions: Position[] = [];
96
+
97
+ for (let x = 0; x < shape.x; x++) {
98
+ for (let y = 0; y < shape.y; y++) {
99
+ for (let z = 0; z < shape.z; z++) {
100
+ if (board[x][y][z] === StoneType.EMPTY) {
101
+ emptyPositions.push({ x, y, z });
102
+ }
103
+ }
104
+ }
105
+ }
106
+
107
+ return emptyPositions;
108
+ }
109
+
110
+
111
+ /**
112
+ * Get all valid moves for the current player
113
+ */
114
+ function getValidMoves(game: TrigoGame): Position[] {
115
+ const emptyPositions = getAllEmptyPositions(game);
116
+ return emptyPositions.filter((pos) => game.isValidMove(pos).valid);
117
+ }
118
+
119
+
120
+ /**
121
+ * Select a random move from available positions
122
+ */
123
+ function selectRandomMove(validMoves: Position[]): Position {
124
+ const randomIndex = Math.floor(Math.random() * validMoves.length);
125
+ return validMoves[randomIndex];
126
+ }
127
+
128
+
129
+ /**
130
+ * Generate a single random game
131
+ */
132
+ function generateRandomGame(options: GeneratorOptions): string {
133
+ // Select board shape (random if undefined)
134
+ const boardShape = options.boardShape || selectRandomBoardShape();
135
+
136
+ const game = new TrigoGame(boardShape);
137
+ game.startGame();
138
+
139
+ const totalPositions = boardShape.x * boardShape.y * boardShape.z;
140
+ const coverageThreshold = Math.floor(totalPositions * 0.9); // 90% coverage
141
+
142
+ let moveCount = 0;
143
+ let consecutivePasses = 0;
144
+
145
+ if (options.moveToEndGame) {
146
+ // Default mode: Play until neutral territory is zero
147
+ // Start checking territory after 90% coverage
148
+ let territoryCheckStarted = false;
149
+
150
+ while (game.getGameStatus() === "playing") {
151
+ // Check if we should start territory checking
152
+ if (!territoryCheckStarted && moveCount >= coverageThreshold) {
153
+ territoryCheckStarted = true;
154
+ }
155
+
156
+ // Random chance to pass (if configured)
157
+ if (options.passChance > 0 && Math.random() < options.passChance) {
158
+ game.pass();
159
+ consecutivePasses++;
160
+ moveCount++;
161
+
162
+ // Game ends on double pass
163
+ if (consecutivePasses >= 2) {
164
+ break;
165
+ }
166
+ continue;
167
+ }
168
+
169
+ // Try to make a move
170
+ const validMoves = getValidMoves(game);
171
+
172
+ if (validMoves.length === 0) {
173
+ // No valid moves available, must pass
174
+ game.pass();
175
+ if (options.passChance <= 0)
176
+ break;
177
+
178
+ consecutivePasses++;
179
+ moveCount++;
180
+
181
+ if (consecutivePasses >= 2)
182
+ break;
183
+ } else {
184
+ // Make a random valid move
185
+ const move = selectRandomMove(validMoves);
186
+ const success = game.drop(move);
187
+
188
+ if (success) {
189
+ consecutivePasses = 0;
190
+ moveCount++;
191
+
192
+ // Check territory after 90% coverage
193
+ if (territoryCheckStarted) {
194
+ const board = game.getBoard();
195
+ const territory = calculateTerritory(board, boardShape);
196
+
197
+ // Stop if neutral territory is zero (game is settled)
198
+ if (territory.neutral === 0) {
199
+ break;
200
+ }
201
+ }
202
+ }
203
+ }
204
+ }
205
+ } else {
206
+ // User-specified move count: Play for target number of moves
207
+ const targetMoves =
208
+ Math.floor(Math.random() * (options.maxMoves - options.minMoves)) + options.minMoves;
209
+
210
+ while (moveCount < targetMoves && game.getGameStatus() === "playing") {
211
+ // Random chance to pass
212
+ if (Math.random() < options.passChance) {
213
+ game.pass();
214
+ consecutivePasses++;
215
+ moveCount++;
216
+
217
+ // Game ends on double pass
218
+ if (consecutivePasses >= 2) {
219
+ break;
220
+ }
221
+ continue;
222
+ }
223
+
224
+ // Try to make a move
225
+ const validMoves = getValidMoves(game);
226
+
227
+ if (validMoves.length === 0) {
228
+ // No valid moves available, must pass
229
+ game.pass();
230
+ consecutivePasses++;
231
+ moveCount++;
232
+
233
+ if (consecutivePasses >= 2) {
234
+ break;
235
+ }
236
+ } else {
237
+ // Make a random valid move
238
+ const move = selectRandomMove(validMoves);
239
+ const success = game.drop(move);
240
+
241
+ if (success) {
242
+ consecutivePasses = 0;
243
+ moveCount++;
244
+ }
245
+ }
246
+ }
247
+ }
248
+
249
+ // Generate TGN
250
+ const tgn = game.toTGN();
251
+
252
+ return tgn;
253
+ }
254
+
255
+
256
+ /**
257
+ * Select a random board shape from CANDIDATE_BOARD_SHAPES
258
+ */
259
+ function selectRandomBoardShape(): BoardShape {
260
+ const randomIndex = Math.floor(Math.random() * CANDIDATE_BOARD_SHAPES.length);
261
+ const [x, y, z] = CANDIDATE_BOARD_SHAPES[randomIndex];
262
+ return { x, y, z };
263
+ }
264
+
265
+
266
+ /**
267
+ * Generate a batch of random games
268
+ */
269
+ function generateBatch(count: number, options: GeneratorOptions): void {
270
+ // Create output directory if it doesn't exist
271
+ const outputPath = path.resolve(__dirname, options.outputDir);
272
+ if (!fs.existsSync(outputPath)) {
273
+ fs.mkdirSync(outputPath, { recursive: true });
274
+ console.log(`Created output directory: ${outputPath}`);
275
+ }
276
+
277
+ console.log(`\nGenerating ${count} random games...`);
278
+ if (options.boardShape) {
279
+ console.log(`Board: ${options.boardShape.x}×${options.boardShape.y}×${options.boardShape.z}`);
280
+ } else {
281
+ console.log(`Board: Random selection from ${CANDIDATE_BOARD_SHAPES.length} candidates`);
282
+ }
283
+
284
+ if (options.moveToEndGame) {
285
+ console.log(`Mode: Play until neutral territory = 0 (check after 90% coverage)`);
286
+ } else {
287
+ console.log(`Moves: ${options.minMoves}-${options.maxMoves}`);
288
+ }
289
+ console.log(`Pass chance: ${(options.passChance * 100).toFixed(0)}%`);
290
+ console.log(`Output: ${outputPath}\n`);
291
+
292
+ const startTime = Date.now();
293
+
294
+ process.stdout.write(".".repeat(count));
295
+ process.stdout.write("\r");
296
+
297
+ for (let i = 0; i < count; i++) {
298
+ try {
299
+ const tgn = generateRandomGame(options);
300
+
301
+ // Generate filename based on content hash
302
+ const hash = crypto.createHash('sha256').update(tgn).digest('hex');
303
+ const filename = `game_${hash.substring(0, 16)}.tgn`;
304
+ const filepath = path.join(outputPath, filename);
305
+
306
+ // Write TGN file
307
+ fs.writeFileSync(filepath, tgn, "utf-8");
308
+
309
+ process.stdout.write("+");
310
+ } catch (error) {
311
+ console.error(`\nError generating game ${i}:`, error);
312
+ }
313
+ }
314
+
315
+ const elapsedTime = ((Date.now() - startTime) / 1000).toFixed(2);
316
+ console.log(`\n\nGeneration complete!`);
317
+ console.log(`Time: ${elapsedTime}s`);
318
+ console.log(`Rate: ${(count / parseFloat(elapsedTime)).toFixed(2)} games/second`);
319
+ console.log(`Output: ${outputPath}`);
320
+ }
321
+
322
+
323
+ /**
324
+ * Parse moves range string (e.g., "10-50" or "20")
325
+ * Returns [min, max] tuple
326
+ */
327
+ function parseMovesRange(movesStr: string): [number, number] {
328
+ // Check if it's a range (e.g., "10-50")
329
+ if (movesStr.includes("-")) {
330
+ const parts = movesStr.split("-").map(s => s.trim());
331
+ if (parts.length !== 2) {
332
+ throw new Error(`Invalid moves range: ${movesStr}. Expected format: "MIN-MAX" or "N"`);
333
+ }
334
+ const min = parseInt(parts[0], 10);
335
+ const max = parseInt(parts[1], 10);
336
+
337
+ if (isNaN(min) || isNaN(max)) {
338
+ throw new Error(`Invalid moves range: ${movesStr}. Values must be numbers`);
339
+ }
340
+ if (min < 0 || max < 0) {
341
+ throw new Error("moves must be non-negative");
342
+ }
343
+ if (max < min) {
344
+ throw new Error("max moves must be greater than or equal to min moves");
345
+ }
346
+
347
+ return [min, max];
348
+ }
349
+
350
+ // Single number means exact move count (min = max)
351
+ const moves = parseInt(movesStr, 10);
352
+ if (isNaN(moves)) {
353
+ throw new Error(`Invalid moves value: ${movesStr}. Must be a number or range like "10-50"`);
354
+ }
355
+ if (moves < 0) {
356
+ throw new Error("moves must be non-negative");
357
+ }
358
+
359
+ return [moves, moves];
360
+ }
361
+
362
+
363
+ /**
364
+ * Parse command line arguments using yargs
365
+ */
366
+ function parseArgs(): { count: number; options: GeneratorOptions } {
367
+ const argv = yargs(hideBin(process.argv))
368
+ .scriptName("npm run generate:games --")
369
+ .usage("Usage: $0 [options]")
370
+ .option("count", {
371
+ alias: "c",
372
+ type: "number",
373
+ default: 10,
374
+ description: "Number of games to generate",
375
+ coerce: (value) => {
376
+ if (value <= 0) {
377
+ throw new Error("count must be greater than 0");
378
+ }
379
+ return value;
380
+ }
381
+ })
382
+ .option("moves", {
383
+ alias: "m",
384
+ type: "string",
385
+ description: "Moves per game as \"MIN-MAX\" or single number (default: play until settled)",
386
+ })
387
+ .option("pass-chance", {
388
+ type: "number",
389
+ default: 0,
390
+ description: "Probability of passing (0.0-1.0)",
391
+ coerce: (value) => {
392
+ if (value < 0 || value > 1) {
393
+ throw new Error("pass-chance must be between 0.0 and 1.0");
394
+ }
395
+ return value;
396
+ }
397
+ })
398
+ .option("board", {
399
+ alias: "b",
400
+ type: "string",
401
+ default: "random",
402
+ description: 'Board size as "X*Y*Z" or "random" for random selection',
403
+ })
404
+ .option("output", {
405
+ alias: "o",
406
+ type: "string",
407
+ default: "output",
408
+ description: "Output directory (relative to tools/)"
409
+ })
410
+ .example([
411
+ ["$0", "Generate 10 games until territory settled"],
412
+ ["$0 --count 100 --moves 20-80", "Generate 100 games with 20-80 moves"],
413
+ ["$0 --board '5*5*5' --count 50", "Generate 50 games on 5×5×5 board"],
414
+ ["$0 --moves 30 --count 20", "Generate 20 games with exactly 30 moves"],
415
+ ["$0 --pass-chance 0.3 --output test_games", "Generate games with 30% pass chance"]
416
+ ])
417
+ .help("h")
418
+ .alias("h", "help")
419
+ .version(false)
420
+ .strict()
421
+ .parseSync();
422
+
423
+ // Check if moves was user-specified or default
424
+ const moveToEndGame = !argv.moves;
425
+
426
+ // Parse board shape: undefined for random mode, otherwise parse the string
427
+ const boardShape = argv.board.toLowerCase() === "random"
428
+ ? undefined
429
+ : parseBoardShape(argv.board);
430
+
431
+ // Parse moves range (use dummy range for default mode, will be ignored)
432
+ const [minMoves, maxMoves] = moveToEndGame ? [0, 0] : parseMovesRange(argv.moves as string);
433
+
434
+ return {
435
+ count: argv.count,
436
+ options: {
437
+ minMoves,
438
+ maxMoves,
439
+ passChance: argv["pass-chance"],
440
+ boardShape,
441
+ outputDir: argv.output,
442
+ moveToEndGame
443
+ }
444
+ };
445
+ }
446
+
447
+
448
+ /**
449
+ * Main entry point
450
+ */
451
+ function main(): void {
452
+ try {
453
+ console.log("=== Trigo Random Game Generator ===\n");
454
+
455
+ const { count, options } = parseArgs();
456
+ generateBatch(count, options);
457
+ } catch (error) {
458
+ console.error("\nError:", error instanceof Error ? error.message : error);
459
+ process.exit(1);
460
+ }
461
+ }
462
+
463
+
464
+ // Run the generator
465
+ main();
trigo-web/tools/output/.gitignore ADDED
@@ -0,0 +1 @@
 
 
1
+ *.tgn
trigo-web/tsconfig.json ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2020",
4
+ "module": "ESNext",
5
+ "lib": ["ES2020", "DOM"],
6
+ "strict": true,
7
+ "esModuleInterop": true,
8
+ "skipLibCheck": true,
9
+ "forceConsistentCasingInFileNames": true,
10
+ "moduleResolution": "bundler",
11
+ "resolveJsonModule": true,
12
+ "types": ["vitest/globals", "node"],
13
+ "baseUrl": ".",
14
+ "paths": {
15
+ "@inc/*": ["./inc/*"]
16
+ }
17
+ },
18
+ "include": ["tests/**/*.ts", "inc/**/*.ts", "tools/**/*.ts"],
19
+ "exclude": ["node_modules", "dist", "app", "backend"]
20
+ }
trigo-web/vitest.config.ts ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { defineConfig, Plugin } from 'vitest/config'
2
+ import vue from '@vitejs/plugin-vue'
3
+ import { fileURLToPath } from 'node:url'
4
+
5
+ // Plugin to handle /lib/tgnParser.cjs imports in Node.js environment
6
+ const libParserPlugin: Plugin = {
7
+ name: 'lib-parser-bypass',
8
+ enforce: 'pre',
9
+ transform(code, id) {
10
+ // Only transform parserInit.ts
11
+ if (id.includes('parserInit')) {
12
+ // Replace import calls of /lib/tgnParser.cjs with a marker that won't be analyzed
13
+ return code.replace(
14
+ /await import\(.*?\/lib\/tgnParser\.cjs/g,
15
+ 'await import("" + "/lib/tgnParser.cjs'
16
+ )
17
+ }
18
+ }
19
+ }
20
+
21
+ export default defineConfig({
22
+ plugins: [libParserPlugin, vue()],
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: {
30
+ '@': fileURLToPath(new URL('./app/src', import.meta.url)),
31
+ '@inc': fileURLToPath(new URL('./inc', import.meta.url))
32
+ }
33
+ },
34
+ ssr: {
35
+ external: ['/lib/tgnParser.cjs']
36
+ }
37
+ })
38
+
39
+
40
+
41
+