Spaces:
Running
Running
Commit
·
466436b
1
Parent(s):
e992c03
the first deploy
Browse filesThis view is limited to 50 files because it contains too many changes.
See raw diff
- .dockerignore +43 -0
- Dockerfile +42 -0
- deploy.sh +61 -0
- trigo-web/.prettierignore +9 -0
- trigo-web/.prettierrc +17 -0
- trigo-web/README.md +227 -0
- trigo-web/app/.env +3 -0
- trigo-web/app/.env.local.example +11 -0
- trigo-web/app/.gitignore +28 -0
- trigo-web/app/index.html +13 -0
- trigo-web/app/package-lock.json +0 -0
- trigo-web/app/package.json +27 -0
- trigo-web/app/src/App.vue +27 -0
- trigo-web/app/src/assets/logo.png +0 -0
- trigo-web/app/src/main.ts +23 -0
- trigo-web/app/src/router/index.ts +17 -0
- trigo-web/app/src/services/trigoViewport.ts +1679 -0
- trigo-web/app/src/stores/gameStore.ts +310 -0
- trigo-web/app/src/utils/TrigoGameFrontend.ts +258 -0
- trigo-web/app/src/views/TrigoView.vue +1604 -0
- trigo-web/app/test_capture.js +23 -0
- trigo-web/app/vite.config.ts +42 -0
- trigo-web/backend/.env +3 -0
- trigo-web/backend/package-lock.json +1663 -0
- trigo-web/backend/package.json +36 -0
- trigo-web/backend/src/server.ts +59 -0
- trigo-web/backend/src/services/gameManager.ts +329 -0
- trigo-web/backend/src/sockets/gameSocket.ts +155 -0
- trigo-web/backend/tsconfig.json +34 -0
- trigo-web/inc/tgn/README.md +109 -0
- trigo-web/inc/tgn/tgn.jison +224 -0
- trigo-web/inc/tgn/tgn.jison.cjs +791 -0
- trigo-web/inc/tgn/tgnParser.ts +166 -0
- trigo-web/inc/trigo/ab0yz.ts +120 -0
- trigo-web/inc/trigo/game.ts +1137 -0
- trigo-web/inc/trigo/gameUtils.ts +600 -0
- trigo-web/inc/trigo/index.ts +5 -0
- trigo-web/inc/trigo/parserInit.ts +135 -0
- trigo-web/inc/trigo/typeAdapters.ts +110 -0
- trigo-web/inc/trigo/types.ts +48 -0
- trigo-web/inc/tsconfig.json +16 -0
- trigo-web/package-lock.json +0 -0
- trigo-web/package.json +52 -0
- trigo-web/public/lib/tgnParser.cjs +791 -0
- trigo-web/tools/README.md +152 -0
- trigo-web/tools/buildJisonParser.ts +74 -0
- trigo-web/tools/generateRandomGames.ts +465 -0
- trigo-web/tools/output/.gitignore +1 -0
- trigo-web/tsconfig.json +20 -0
- 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 |
+
|