Spaces:
Build error
Build error
Commit
·
15f353f
1
Parent(s):
0b679f2
Update trigo-web with VS People multiplayer mode
Browse filesChanges include:
- VS People mode: real-time multiplayer via Socket.io
- Room management with URL hash sharing
- Turn-based gameplay with validation
- Pass and stone placement synchronization
- Session storage fix for multiplayer mode
- Updated ONNX model paths
- Various bug fixes and improvements
Co-Authored-By: Claude <noreply@anthropic.com>
This view is limited to 50 files because it contains too many changes.
See raw diff
- trigo-web/.claude/agents/agentlog-updater.md +77 -0
- trigo-web/.env +76 -0
- trigo-web/.env.local.example +43 -0
- trigo-web/app/.env +8 -3
- trigo-web/app/package-lock.json +689 -118
- trigo-web/app/package.json +3 -0
- trigo-web/app/src/components/InlineNicknameEditor.vue +225 -0
- trigo-web/app/src/components/mcts/MCTSBoardHeatmap.vue +434 -0
- trigo-web/app/src/components/mcts/MCTSDataLoader.vue +283 -0
- trigo-web/app/src/components/mcts/MCTSMoveNavigation.vue +148 -0
- trigo-web/app/src/components/mcts/MCTSStatisticsPanel.vue +221 -0
- trigo-web/app/src/components/mcts/MCTSTreeVisualization.vue +407 -0
- trigo-web/app/src/composables/useRoomHash.ts +55 -0
- trigo-web/app/src/composables/useSocket.ts +206 -7
- trigo-web/app/src/composables/useTrigoAgent.ts +15 -10
- trigo-web/app/src/data/defaultNicknames.ts +140 -0
- trigo-web/app/src/router/index.ts +12 -0
- trigo-web/app/src/stores/gameStore.ts +70 -0
- trigo-web/app/src/stores/mctsStore.ts +179 -0
- trigo-web/app/src/stores/playerStore.ts +211 -0
- trigo-web/app/src/styles/test-pages.scss +554 -0
- trigo-web/app/src/types/mcts.ts +52 -0
- trigo-web/app/src/utils/mctsColorScale.ts +184 -0
- trigo-web/app/src/utils/mctsDataParser.ts +174 -0
- trigo-web/app/src/utils/storage.ts +3 -0
- trigo-web/app/src/views/MCTSAnalysisView.vue +176 -0
- trigo-web/app/src/views/OnnxTestView.vue +3 -3
- trigo-web/app/src/views/SocketTestView.vue +932 -0
- trigo-web/app/src/views/TrigoAgentTestView.vue +3 -3
- trigo-web/app/src/views/TrigoTreeTestView.vue +15 -4
- trigo-web/app/src/views/TrigoView.vue +771 -134
- trigo-web/app/vite.config.ts +4 -2
- trigo-web/backend/.env +9 -3
- trigo-web/backend/.env.local +2 -0
- trigo-web/backend/package-lock.json +498 -0
- trigo-web/backend/package.json +3 -1
- trigo-web/backend/src/server.ts +53 -10
- trigo-web/backend/src/services/gameManager.ts +113 -5
- trigo-web/backend/src/sockets/gameSocket.ts +350 -44
- trigo-web/inc/config.ts +181 -0
- trigo-web/inc/mctsAgent.ts +855 -0
- trigo-web/inc/modelInferencer.ts +60 -5
- trigo-web/inc/trigo/game.ts +149 -23
- trigo-web/inc/trigoAgent.ts +3 -1
- trigo-web/inc/trigoEvaluationAgent.ts +266 -0
- trigo-web/inc/trigoTreeAgent.ts +198 -77
- trigo-web/package-lock.json +0 -0
- trigo-web/package.json +4 -1
- trigo-web/public/onnx/{GPT2CausalLM_ep0015_evaluation.onnx → 20251220-trigo-value-llama-l6-h64-251220-value0.02/LlamaCausalLM_ep0036_evaluation.onnx} +2 -2
- trigo-web/public/onnx/20251220-trigo-value-llama-l6-h64-251220-value0.02/LlamaCausalLM_ep0036_tree.onnx +3 -0
trigo-web/.claude/agents/agentlog-updater.md
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
name: agentlog-updater
|
| 3 |
+
description: Use this agent when a mini-milestone has been accomplished in the Trigo project and the development history needs to be documented in agentlog.md. This includes after completing features, fixing significant bugs, making architectural changes, or finishing logical chunks of work. Examples:\n\n<example>\nContext: User has just finished implementing a new feature for multiplayer room management.\nuser: "I've finished adding the room creation and joining functionality"\nassistant: "Let me document this milestone in agentlog.md using the agentlog-updater agent."\n<commentary>\nSince a development milestone has been reached, use the agentlog-updater agent to properly document it in the project's development history.\n</commentary>\n</example>\n\n<example>\nContext: User mentions completing work on 3D board rendering improvements.\nuser: "完成了3D棋盘渲染的优化" (Chinese: "Completed optimization of 3D board rendering")\nassistant: "I'll use the agentlog-updater agent to document this milestone in agentlog.md with proper English translation and formatting."\n<commentary>\nA milestone has been reached and needs documentation. The agent will handle translation and proper formatting according to project standards.\n</commentary>\n</example>\n\n<example>\nContext: After a series of bug fixes and code refactoring.\nuser: "Can you update the agentlog with what we just did?"\nassistant: "I'll launch the agentlog-updater agent to review our recent conversation and document the mini-milestone in agentlog.md."\n<commentary>\nUser explicitly requests agentlog update, triggering the specialized agent for this task.\n</commentary>\n</example>
|
| 4 |
+
tools: Glob, Grep, Read, WebFetch, TodoWrite, WebSearch, BashOutput, Edit, Write, NotebookEdit
|
| 5 |
+
model: haiku
|
| 6 |
+
color: green
|
| 7 |
+
---
|
| 8 |
+
|
| 9 |
+
You are the Agentlog Documentation Specialist, an expert technical writer specialized in maintaining clear, concise development histories for software projects. Your singular focus is documenting mini-milestones in the Trigo project's agentlog.md file.
|
| 10 |
+
|
| 11 |
+
## Your Core Responsibilities
|
| 12 |
+
|
| 13 |
+
1. **Analyze Recent Conversations**: Review the conversation history to identify the completed mini-milestone, extracting key accomplishments, decisions made, and technical details.
|
| 14 |
+
|
| 15 |
+
2. **Format User Requests Properly**:
|
| 16 |
+
- Start user requests with `> ` prefix
|
| 17 |
+
- Translate non-English text to English while preserving technical terms
|
| 18 |
+
- Fix any typos or grammar errors
|
| 19 |
+
- Keep the request concise but complete (1-2 sentences typically)
|
| 20 |
+
- Maintain the user's intent and technical accuracy
|
| 21 |
+
|
| 22 |
+
3. **Create Structured Agent Responses**:
|
| 23 |
+
- Enclose all agent response content within `<details>` and `</details>` tags
|
| 24 |
+
- Add a `<summary>` tag with a concise, descriptive title (5-10 words)
|
| 25 |
+
- Within the details block, document:
|
| 26 |
+
* What was accomplished
|
| 27 |
+
* Key technical decisions or approaches taken
|
| 28 |
+
* Any significant challenges or learnings
|
| 29 |
+
* Files or components affected
|
| 30 |
+
- Use clear, professional technical writing
|
| 31 |
+
- Format code references with backticks
|
| 32 |
+
- Use bullet points for lists
|
| 33 |
+
- Keep paragraphs focused and scannable
|
| 34 |
+
|
| 35 |
+
4. **Maintain Consistency**:
|
| 36 |
+
- Study the existing agentlog.md format before writing
|
| 37 |
+
- Match the tone, style, and level of detail of previous entries
|
| 38 |
+
- Use the same heading structure and formatting conventions
|
| 39 |
+
- Ensure chronological ordering
|
| 40 |
+
|
| 41 |
+
5. **Write in English Always**: All documentation must be in English, regardless of the original conversation language. Translate accurately while preserving technical precision.
|
| 42 |
+
|
| 43 |
+
## Quality Standards
|
| 44 |
+
|
| 45 |
+
- **Conciseness**: Every word should add value. Remove redundancy.
|
| 46 |
+
- **Clarity**: Technical details should be understandable to future developers
|
| 47 |
+
- **Completeness**: Capture the essence of what was accomplished without overwhelming detail
|
| 48 |
+
- **Accuracy**: Preserve technical terms, file paths, and technical decisions exactly
|
| 49 |
+
- **Professionalism**: Use formal technical writing style appropriate for project documentation
|
| 50 |
+
|
| 51 |
+
## Your Workflow
|
| 52 |
+
|
| 53 |
+
1. Read the entire recent conversation to understand the milestone
|
| 54 |
+
2. Review existing agentlog.md entries to match style and format
|
| 55 |
+
3. Extract the user's core request, translating and cleaning as needed
|
| 56 |
+
4. Synthesize the agent's work into a well-structured details block
|
| 57 |
+
5. Present the formatted entry for review before committing
|
| 58 |
+
6. Update agentlog.md with the new entry in chronological order
|
| 59 |
+
|
| 60 |
+
## Format Template
|
| 61 |
+
|
| 62 |
+
```markdown
|
| 63 |
+
> [User's request in English, concise, typos fixed]
|
| 64 |
+
|
| 65 |
+
<details>
|
| 66 |
+
<summary>[Concise title of what was accomplished]</summary>
|
| 67 |
+
|
| 68 |
+
[Well-structured documentation of the work, including:
|
| 69 |
+
- What was done
|
| 70 |
+
- Key technical details
|
| 71 |
+
- Files/components affected
|
| 72 |
+
- Any important decisions or learnings]
|
| 73 |
+
|
| 74 |
+
</details>
|
| 75 |
+
```
|
| 76 |
+
|
| 77 |
+
You are meticulous about formatting, translation accuracy, and maintaining documentation consistency. Every entry you create should be a valuable reference for understanding the project's evolution.
|
trigo-web/.env
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Unified Environment Configuration for Trigo Web
|
| 2 |
+
# This file is used by frontend (Vite), backend (Express), and tools (Node scripts)
|
| 3 |
+
#
|
| 4 |
+
# LOCAL OVERRIDES:
|
| 5 |
+
# Create .env.local (not committed to git) to override any values below
|
| 6 |
+
# Example: cp .env.local.example .env.local
|
| 7 |
+
#
|
| 8 |
+
# Loading order: .env → .env.local (overrides)
|
| 9 |
+
#
|
| 10 |
+
# See .env.local.example for common override scenarios
|
| 11 |
+
|
| 12 |
+
# ============================================================================
|
| 13 |
+
# Frontend Configuration (Vite - requires VITE_ prefix)
|
| 14 |
+
# ============================================================================
|
| 15 |
+
|
| 16 |
+
# Backend Server URL
|
| 17 |
+
VITE_SERVER_URL=http://localhost:8157
|
| 18 |
+
|
| 19 |
+
# Vite Dev Server Configuration
|
| 20 |
+
VITE_HOST=0.0.0.0
|
| 21 |
+
VITE_PORT=5173
|
| 22 |
+
|
| 23 |
+
# ONNX Model Paths (relative to /public directory)
|
| 24 |
+
# Evaluation mode model - predicts position value
|
| 25 |
+
VITE_ONNX_EVALUATION_MODEL=/onnx/20251230-trigo-value-llama-l6-h64-it2_251221-value0.01-pretrain/LlamaCausalLM_ep0036_evaluation.onnx
|
| 26 |
+
|
| 27 |
+
# Tree mode model - generates move trees
|
| 28 |
+
VITE_ONNX_TREE_MODEL=/onnx/20251230-trigo-value-llama-l6-h64-it2_251221-value0.01-pretrain/LlamaCausalLM_ep0036_tree.onnx
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
# ============================================================================
|
| 32 |
+
# Backend Configuration (Express Server)
|
| 33 |
+
# ============================================================================
|
| 34 |
+
|
| 35 |
+
# Server port (HTTP and Socket.io)
|
| 36 |
+
PORT=3000
|
| 37 |
+
|
| 38 |
+
# Frontend URL (used for CORS)
|
| 39 |
+
CLIENT_URL=http://localhost:5173
|
| 40 |
+
|
| 41 |
+
# Environment mode
|
| 42 |
+
NODE_ENV=development
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
# ============================================================================
|
| 46 |
+
# Tools Configuration (Node scripts - tools/ directory)
|
| 47 |
+
# ============================================================================
|
| 48 |
+
|
| 49 |
+
# ONNX Model Paths (relative to project root)
|
| 50 |
+
# Evaluation mode model - predicts position value
|
| 51 |
+
ONNX_EVALUATION_MODEL=./public/onnx/20251204-trigo-value-gpt2-l6-h64-251125-lr500/GPT2CausalLM_ep0019_evaluation.onnx
|
| 52 |
+
|
| 53 |
+
# Tree mode model - generates move trees
|
| 54 |
+
ONNX_TREE_MODEL=./public/onnx/20251204-trigo-value-gpt2-l6-h64-251125-lr500/GPT2CausalLM_ep0019_tree.onnx
|
| 55 |
+
|
| 56 |
+
# ONNX Runtime Performance Configuration
|
| 57 |
+
# See docs/onnx-threading-configuration.md for detailed tuning guide
|
| 58 |
+
|
| 59 |
+
# Intra-operator parallelism (threads within a single operator)
|
| 60 |
+
# Recommended: 4 for most systems, higher for large models
|
| 61 |
+
# ONNX_INTRA_OP_NUM_THREADS=4
|
| 62 |
+
#ONNX_INTRA_OP_NUM_THREADS=28
|
| 63 |
+
|
| 64 |
+
# Inter-operator parallelism (threads across operators)
|
| 65 |
+
# Recommended: 1-2 for sequential models, higher for complex graphs
|
| 66 |
+
# ONNX_INTER_OP_NUM_THREADS=2
|
| 67 |
+
ONNX_INTER_OP_NUM_THREADS=28
|
| 68 |
+
|
| 69 |
+
# Graph optimization level
|
| 70 |
+
# Options: "disabled", "basic", "extended", "all" (default: "all")
|
| 71 |
+
# ONNX_GRAPH_OPTIMIZATION_LEVEL=all
|
| 72 |
+
|
| 73 |
+
# Memory optimization settings
|
| 74 |
+
# ONNX_ENABLE_CPU_MEM_ARENA=true
|
| 75 |
+
# ONNX_ENABLE_MEM_PATTERN=true
|
| 76 |
+
|
trigo-web/.env.local.example
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Local Environment Overrides
|
| 2 |
+
#
|
| 3 |
+
# This file is for local development overrides and is NOT committed to git.
|
| 4 |
+
# Copy this file to .env.local and customize as needed.
|
| 5 |
+
#
|
| 6 |
+
# Usage:
|
| 7 |
+
# cp .env.local.example .env.local
|
| 8 |
+
# # Edit .env.local with your local settings
|
| 9 |
+
#
|
| 10 |
+
# Any variables set here will override the values in .env
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
# ==============================================================================
|
| 14 |
+
# Example: Override ONNX Threading Configuration
|
| 15 |
+
# ==============================================================================
|
| 16 |
+
|
| 17 |
+
# Uncomment and adjust for your CPU:
|
| 18 |
+
# ONNX_INTRA_OP_NUM_THREADS=8
|
| 19 |
+
# ONNX_INTER_OP_NUM_THREADS=4
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
# ==============================================================================
|
| 23 |
+
# Example: Use Different Model Versions for Testing
|
| 24 |
+
# ==============================================================================
|
| 25 |
+
|
| 26 |
+
# ONNX_EVALUATION_MODEL=./public/onnx/experimental/model_v2_evaluation.onnx
|
| 27 |
+
# ONNX_TREE_MODEL=./public/onnx/experimental/model_v2_tree.onnx
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
# ==============================================================================
|
| 31 |
+
# Example: Frontend Development Settings
|
| 32 |
+
# ==============================================================================
|
| 33 |
+
|
| 34 |
+
# VITE_PORT=3000
|
| 35 |
+
# VITE_SERVER_URL=http://192.168.1.100:3000
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
# ==============================================================================
|
| 39 |
+
# Example: Backend Development Settings
|
| 40 |
+
# ==============================================================================
|
| 41 |
+
|
| 42 |
+
# PORT=4000
|
| 43 |
+
# CLIENT_URL=http://localhost:3000
|
trigo-web/app/.env
CHANGED
|
@@ -1,3 +1,8 @@
|
|
| 1 |
-
|
| 2 |
-
|
| 3 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# ============================================================================
|
| 2 |
+
# Frontend Configuration
|
| 3 |
+
# ============================================================================
|
| 4 |
+
# This file is DEPRECATED - all configuration is now in the root .env file
|
| 5 |
+
# Vite is configured to load from ../. env (repository root)
|
| 6 |
+
#
|
| 7 |
+
# To configure the frontend, edit: ../. env
|
| 8 |
+
# ============================================================================
|
trigo-web/app/package-lock.json
CHANGED
|
@@ -8,8 +8,8 @@
|
|
| 8 |
"name": "trigo-app",
|
| 9 |
"version": "0.0.0",
|
| 10 |
"dependencies": {
|
| 11 |
-
"
|
| 12 |
-
"
|
| 13 |
"pinia": "^2.1.6",
|
| 14 |
"socket.io-client": "^4.5.2",
|
| 15 |
"three": "^0.156.1",
|
|
@@ -17,6 +17,7 @@
|
|
| 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",
|
|
@@ -742,60 +743,6 @@
|
|
| 742 |
"url": "https://opencollective.com/parcel"
|
| 743 |
}
|
| 744 |
},
|
| 745 |
-
"node_modules/@protobufjs/aspromise": {
|
| 746 |
-
"version": "1.1.2",
|
| 747 |
-
"resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz",
|
| 748 |
-
"integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ=="
|
| 749 |
-
},
|
| 750 |
-
"node_modules/@protobufjs/base64": {
|
| 751 |
-
"version": "1.1.2",
|
| 752 |
-
"resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz",
|
| 753 |
-
"integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg=="
|
| 754 |
-
},
|
| 755 |
-
"node_modules/@protobufjs/codegen": {
|
| 756 |
-
"version": "2.0.4",
|
| 757 |
-
"resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz",
|
| 758 |
-
"integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg=="
|
| 759 |
-
},
|
| 760 |
-
"node_modules/@protobufjs/eventemitter": {
|
| 761 |
-
"version": "1.1.0",
|
| 762 |
-
"resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz",
|
| 763 |
-
"integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q=="
|
| 764 |
-
},
|
| 765 |
-
"node_modules/@protobufjs/fetch": {
|
| 766 |
-
"version": "1.1.0",
|
| 767 |
-
"resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz",
|
| 768 |
-
"integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==",
|
| 769 |
-
"dependencies": {
|
| 770 |
-
"@protobufjs/aspromise": "^1.1.1",
|
| 771 |
-
"@protobufjs/inquire": "^1.1.0"
|
| 772 |
-
}
|
| 773 |
-
},
|
| 774 |
-
"node_modules/@protobufjs/float": {
|
| 775 |
-
"version": "1.0.2",
|
| 776 |
-
"resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz",
|
| 777 |
-
"integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ=="
|
| 778 |
-
},
|
| 779 |
-
"node_modules/@protobufjs/inquire": {
|
| 780 |
-
"version": "1.1.0",
|
| 781 |
-
"resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz",
|
| 782 |
-
"integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q=="
|
| 783 |
-
},
|
| 784 |
-
"node_modules/@protobufjs/path": {
|
| 785 |
-
"version": "1.1.2",
|
| 786 |
-
"resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz",
|
| 787 |
-
"integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA=="
|
| 788 |
-
},
|
| 789 |
-
"node_modules/@protobufjs/pool": {
|
| 790 |
-
"version": "1.1.0",
|
| 791 |
-
"resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz",
|
| 792 |
-
"integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw=="
|
| 793 |
-
},
|
| 794 |
-
"node_modules/@protobufjs/utf8": {
|
| 795 |
-
"version": "1.1.0",
|
| 796 |
-
"resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz",
|
| 797 |
-
"integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw=="
|
| 798 |
-
},
|
| 799 |
"node_modules/@rollup/rollup-android-arm-eabi": {
|
| 800 |
"version": "4.52.5",
|
| 801 |
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.52.5.tgz",
|
|
@@ -1087,16 +1034,278 @@
|
|
| 1087 |
"resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz",
|
| 1088 |
"integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA=="
|
| 1089 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1090 |
"node_modules/@types/estree": {
|
| 1091 |
"version": "1.0.8",
|
| 1092 |
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
|
| 1093 |
"integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
|
| 1094 |
"dev": true
|
| 1095 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1096 |
"node_modules/@types/node": {
|
| 1097 |
"version": "24.10.1",
|
| 1098 |
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.1.tgz",
|
| 1099 |
"integrity": "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==",
|
|
|
|
|
|
|
|
|
|
| 1100 |
"dependencies": {
|
| 1101 |
"undici-types": "~7.16.0"
|
| 1102 |
}
|
|
@@ -1356,11 +1565,389 @@
|
|
| 1356 |
"integrity": "sha512-twmVoizEW7ylZSN32OgKdXRmo1qg+wT5/6C3xu5b9QsWzSFAhHLn2xd8ro0diCsKfCj1RdaTP/nrcW+vAoQPIw==",
|
| 1357 |
"dev": true
|
| 1358 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1359 |
"node_modules/csstype": {
|
| 1360 |
"version": "3.1.3",
|
| 1361 |
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
|
| 1362 |
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="
|
| 1363 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1364 |
"node_modules/de-indent": {
|
| 1365 |
"version": "1.0.2",
|
| 1366 |
"resolved": "https://registry.npmjs.org/de-indent/-/de-indent-1.0.2.tgz",
|
|
@@ -1383,6 +1970,14 @@
|
|
| 1383 |
}
|
| 1384 |
}
|
| 1385 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1386 |
"node_modules/detect-libc": {
|
| 1387 |
"version": "1.0.3",
|
| 1388 |
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz",
|
|
@@ -1489,11 +2084,6 @@
|
|
| 1489 |
"node": ">=8"
|
| 1490 |
}
|
| 1491 |
},
|
| 1492 |
-
"node_modules/flatbuffers": {
|
| 1493 |
-
"version": "25.9.23",
|
| 1494 |
-
"resolved": "https://registry.npmjs.org/flatbuffers/-/flatbuffers-25.9.23.tgz",
|
| 1495 |
-
"integrity": "sha512-MI1qs7Lo4Syw0EOzUl0xjs2lsoeqFku44KpngfIduHBYvzm8h2+7K8YMQh1JtVVVrUvhLpNwqVi4DERegUJhPQ=="
|
| 1496 |
-
},
|
| 1497 |
"node_modules/fsevents": {
|
| 1498 |
"version": "2.3.3",
|
| 1499 |
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
|
@@ -1508,11 +2098,6 @@
|
|
| 1508 |
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
| 1509 |
}
|
| 1510 |
},
|
| 1511 |
-
"node_modules/guid-typescript": {
|
| 1512 |
-
"version": "1.0.9",
|
| 1513 |
-
"resolved": "https://registry.npmjs.org/guid-typescript/-/guid-typescript-1.0.9.tgz",
|
| 1514 |
-
"integrity": "sha512-Y8T4vYhEfwJOTbouREvG+3XDsjr8E3kIr7uf+JZ0BYloFsttiHU0WfvANVsR7TxNUJa/WpCnw/Ino/p+DeBhBQ=="
|
| 1515 |
-
},
|
| 1516 |
"node_modules/has-flag": {
|
| 1517 |
"version": "4.0.0",
|
| 1518 |
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
|
|
@@ -1531,12 +2116,31 @@
|
|
| 1531 |
"he": "bin/he"
|
| 1532 |
}
|
| 1533 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1534 |
"node_modules/immutable": {
|
| 1535 |
"version": "5.1.4",
|
| 1536 |
"resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.4.tgz",
|
| 1537 |
"integrity": "sha512-p6u1bG3YSnINT5RQmx/yRZBpenIl30kVxkTLDyHLIMk0gict704Q9n+thfDI7lTRm9vXdDYutVzXhzcThxTnXA==",
|
| 1538 |
"dev": true
|
| 1539 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1540 |
"node_modules/is-extglob": {
|
| 1541 |
"version": "2.1.1",
|
| 1542 |
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
|
|
@@ -1570,11 +2174,6 @@
|
|
| 1570 |
"node": ">=0.12.0"
|
| 1571 |
}
|
| 1572 |
},
|
| 1573 |
-
"node_modules/long": {
|
| 1574 |
-
"version": "5.3.2",
|
| 1575 |
-
"resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz",
|
| 1576 |
-
"integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA=="
|
| 1577 |
-
},
|
| 1578 |
"node_modules/magic-string": {
|
| 1579 |
"version": "0.30.19",
|
| 1580 |
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.19.tgz",
|
|
@@ -1653,24 +2252,6 @@
|
|
| 1653 |
"dev": true,
|
| 1654 |
"optional": true
|
| 1655 |
},
|
| 1656 |
-
"node_modules/onnxruntime-common": {
|
| 1657 |
-
"version": "1.23.2",
|
| 1658 |
-
"resolved": "https://registry.npmjs.org/onnxruntime-common/-/onnxruntime-common-1.23.2.tgz",
|
| 1659 |
-
"integrity": "sha512-5LFsC9Dukzp2WV6kNHYLNzp8sT6V02IubLCbzw2Xd6X5GOlr65gAX6xiJwyi2URJol/s71gaQLC5F2C25AAR2w=="
|
| 1660 |
-
},
|
| 1661 |
-
"node_modules/onnxruntime-web": {
|
| 1662 |
-
"version": "1.23.2",
|
| 1663 |
-
"resolved": "https://registry.npmjs.org/onnxruntime-web/-/onnxruntime-web-1.23.2.tgz",
|
| 1664 |
-
"integrity": "sha512-T09JUtMn+CZLk3mFwqiH0lgQf+4S7+oYHHtk6uhaYAAJI95bTcKi5bOOZYwORXfS/RLZCjDDEXGWIuOCAFlEjg==",
|
| 1665 |
-
"dependencies": {
|
| 1666 |
-
"flatbuffers": "^25.1.24",
|
| 1667 |
-
"guid-typescript": "^1.0.9",
|
| 1668 |
-
"long": "^5.2.3",
|
| 1669 |
-
"onnxruntime-common": "1.23.2",
|
| 1670 |
-
"platform": "^1.3.6",
|
| 1671 |
-
"protobufjs": "^7.2.4"
|
| 1672 |
-
}
|
| 1673 |
-
},
|
| 1674 |
"node_modules/path-browserify": {
|
| 1675 |
"version": "1.0.1",
|
| 1676 |
"resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz",
|
|
@@ -1716,11 +2297,6 @@
|
|
| 1716 |
}
|
| 1717 |
}
|
| 1718 |
},
|
| 1719 |
-
"node_modules/platform": {
|
| 1720 |
-
"version": "1.3.6",
|
| 1721 |
-
"resolved": "https://registry.npmjs.org/platform/-/platform-1.3.6.tgz",
|
| 1722 |
-
"integrity": "sha512-fnWVljUchTro6RiCFvCXBbNhJc2NijN7oIQxbwsyL0buWJPG85v81ehlHI9fXrJsMNgTofEoWIQeClKpgxFLrg=="
|
| 1723 |
-
},
|
| 1724 |
"node_modules/postcss": {
|
| 1725 |
"version": "8.5.6",
|
| 1726 |
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
|
|
@@ -1748,29 +2324,6 @@
|
|
| 1748 |
"node": "^10 || ^12 || >=14"
|
| 1749 |
}
|
| 1750 |
},
|
| 1751 |
-
"node_modules/protobufjs": {
|
| 1752 |
-
"version": "7.5.4",
|
| 1753 |
-
"resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.4.tgz",
|
| 1754 |
-
"integrity": "sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg==",
|
| 1755 |
-
"hasInstallScript": true,
|
| 1756 |
-
"dependencies": {
|
| 1757 |
-
"@protobufjs/aspromise": "^1.1.2",
|
| 1758 |
-
"@protobufjs/base64": "^1.1.2",
|
| 1759 |
-
"@protobufjs/codegen": "^2.0.4",
|
| 1760 |
-
"@protobufjs/eventemitter": "^1.1.0",
|
| 1761 |
-
"@protobufjs/fetch": "^1.1.0",
|
| 1762 |
-
"@protobufjs/float": "^1.0.2",
|
| 1763 |
-
"@protobufjs/inquire": "^1.1.0",
|
| 1764 |
-
"@protobufjs/path": "^1.1.2",
|
| 1765 |
-
"@protobufjs/pool": "^1.1.0",
|
| 1766 |
-
"@protobufjs/utf8": "^1.1.0",
|
| 1767 |
-
"@types/node": ">=13.7.0",
|
| 1768 |
-
"long": "^5.0.0"
|
| 1769 |
-
},
|
| 1770 |
-
"engines": {
|
| 1771 |
-
"node": ">=12.0.0"
|
| 1772 |
-
}
|
| 1773 |
-
},
|
| 1774 |
"node_modules/readdirp": {
|
| 1775 |
"version": "4.1.2",
|
| 1776 |
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz",
|
|
@@ -1785,6 +2338,11 @@
|
|
| 1785 |
"url": "https://paulmillr.com/funding/"
|
| 1786 |
}
|
| 1787 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1788 |
"node_modules/rollup": {
|
| 1789 |
"version": "4.52.5",
|
| 1790 |
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.52.5.tgz",
|
|
@@ -1826,6 +2384,11 @@
|
|
| 1826 |
"fsevents": "~2.3.2"
|
| 1827 |
}
|
| 1828 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1829 |
"node_modules/rxjs": {
|
| 1830 |
"version": "7.8.2",
|
| 1831 |
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz",
|
|
@@ -1835,6 +2398,11 @@
|
|
| 1835 |
"tslib": "^2.1.0"
|
| 1836 |
}
|
| 1837 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1838 |
"node_modules/sass": {
|
| 1839 |
"version": "1.93.2",
|
| 1840 |
"resolved": "https://registry.npmjs.org/sass/-/sass-1.93.2.tgz",
|
|
@@ -2296,7 +2864,10 @@
|
|
| 2296 |
"node_modules/undici-types": {
|
| 2297 |
"version": "7.16.0",
|
| 2298 |
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz",
|
| 2299 |
-
"integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="
|
|
|
|
|
|
|
|
|
|
| 2300 |
},
|
| 2301 |
"node_modules/varint": {
|
| 2302 |
"version": "6.0.0",
|
|
|
|
| 8 |
"name": "trigo-app",
|
| 9 |
"version": "0.0.0",
|
| 10 |
"dependencies": {
|
| 11 |
+
"d3": "^7.9.0",
|
| 12 |
+
"d3-scale-chromatic": "^3.1.0",
|
| 13 |
"pinia": "^2.1.6",
|
| 14 |
"socket.io-client": "^4.5.2",
|
| 15 |
"three": "^0.156.1",
|
|
|
|
| 17 |
"vue-router": "^4.2.4"
|
| 18 |
},
|
| 19 |
"devDependencies": {
|
| 20 |
+
"@types/d3": "^7.4.3",
|
| 21 |
"@types/three": "^0.156.0",
|
| 22 |
"@vitejs/plugin-vue": "^5.2.4",
|
| 23 |
"sass-embedded": "^1.93.2",
|
|
|
|
| 743 |
"url": "https://opencollective.com/parcel"
|
| 744 |
}
|
| 745 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 746 |
"node_modules/@rollup/rollup-android-arm-eabi": {
|
| 747 |
"version": "4.52.5",
|
| 748 |
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.52.5.tgz",
|
|
|
|
| 1034 |
"resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz",
|
| 1035 |
"integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA=="
|
| 1036 |
},
|
| 1037 |
+
"node_modules/@types/d3": {
|
| 1038 |
+
"version": "7.4.3",
|
| 1039 |
+
"resolved": "https://registry.npmjs.org/@types/d3/-/d3-7.4.3.tgz",
|
| 1040 |
+
"integrity": "sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww==",
|
| 1041 |
+
"dev": true,
|
| 1042 |
+
"dependencies": {
|
| 1043 |
+
"@types/d3-array": "*",
|
| 1044 |
+
"@types/d3-axis": "*",
|
| 1045 |
+
"@types/d3-brush": "*",
|
| 1046 |
+
"@types/d3-chord": "*",
|
| 1047 |
+
"@types/d3-color": "*",
|
| 1048 |
+
"@types/d3-contour": "*",
|
| 1049 |
+
"@types/d3-delaunay": "*",
|
| 1050 |
+
"@types/d3-dispatch": "*",
|
| 1051 |
+
"@types/d3-drag": "*",
|
| 1052 |
+
"@types/d3-dsv": "*",
|
| 1053 |
+
"@types/d3-ease": "*",
|
| 1054 |
+
"@types/d3-fetch": "*",
|
| 1055 |
+
"@types/d3-force": "*",
|
| 1056 |
+
"@types/d3-format": "*",
|
| 1057 |
+
"@types/d3-geo": "*",
|
| 1058 |
+
"@types/d3-hierarchy": "*",
|
| 1059 |
+
"@types/d3-interpolate": "*",
|
| 1060 |
+
"@types/d3-path": "*",
|
| 1061 |
+
"@types/d3-polygon": "*",
|
| 1062 |
+
"@types/d3-quadtree": "*",
|
| 1063 |
+
"@types/d3-random": "*",
|
| 1064 |
+
"@types/d3-scale": "*",
|
| 1065 |
+
"@types/d3-scale-chromatic": "*",
|
| 1066 |
+
"@types/d3-selection": "*",
|
| 1067 |
+
"@types/d3-shape": "*",
|
| 1068 |
+
"@types/d3-time": "*",
|
| 1069 |
+
"@types/d3-time-format": "*",
|
| 1070 |
+
"@types/d3-timer": "*",
|
| 1071 |
+
"@types/d3-transition": "*",
|
| 1072 |
+
"@types/d3-zoom": "*"
|
| 1073 |
+
}
|
| 1074 |
+
},
|
| 1075 |
+
"node_modules/@types/d3-array": {
|
| 1076 |
+
"version": "3.2.2",
|
| 1077 |
+
"resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz",
|
| 1078 |
+
"integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==",
|
| 1079 |
+
"dev": true
|
| 1080 |
+
},
|
| 1081 |
+
"node_modules/@types/d3-axis": {
|
| 1082 |
+
"version": "3.0.6",
|
| 1083 |
+
"resolved": "https://registry.npmjs.org/@types/d3-axis/-/d3-axis-3.0.6.tgz",
|
| 1084 |
+
"integrity": "sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw==",
|
| 1085 |
+
"dev": true,
|
| 1086 |
+
"dependencies": {
|
| 1087 |
+
"@types/d3-selection": "*"
|
| 1088 |
+
}
|
| 1089 |
+
},
|
| 1090 |
+
"node_modules/@types/d3-brush": {
|
| 1091 |
+
"version": "3.0.6",
|
| 1092 |
+
"resolved": "https://registry.npmjs.org/@types/d3-brush/-/d3-brush-3.0.6.tgz",
|
| 1093 |
+
"integrity": "sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A==",
|
| 1094 |
+
"dev": true,
|
| 1095 |
+
"dependencies": {
|
| 1096 |
+
"@types/d3-selection": "*"
|
| 1097 |
+
}
|
| 1098 |
+
},
|
| 1099 |
+
"node_modules/@types/d3-chord": {
|
| 1100 |
+
"version": "3.0.6",
|
| 1101 |
+
"resolved": "https://registry.npmjs.org/@types/d3-chord/-/d3-chord-3.0.6.tgz",
|
| 1102 |
+
"integrity": "sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg==",
|
| 1103 |
+
"dev": true
|
| 1104 |
+
},
|
| 1105 |
+
"node_modules/@types/d3-color": {
|
| 1106 |
+
"version": "3.1.3",
|
| 1107 |
+
"resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz",
|
| 1108 |
+
"integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==",
|
| 1109 |
+
"dev": true
|
| 1110 |
+
},
|
| 1111 |
+
"node_modules/@types/d3-contour": {
|
| 1112 |
+
"version": "3.0.6",
|
| 1113 |
+
"resolved": "https://registry.npmjs.org/@types/d3-contour/-/d3-contour-3.0.6.tgz",
|
| 1114 |
+
"integrity": "sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg==",
|
| 1115 |
+
"dev": true,
|
| 1116 |
+
"dependencies": {
|
| 1117 |
+
"@types/d3-array": "*",
|
| 1118 |
+
"@types/geojson": "*"
|
| 1119 |
+
}
|
| 1120 |
+
},
|
| 1121 |
+
"node_modules/@types/d3-delaunay": {
|
| 1122 |
+
"version": "6.0.4",
|
| 1123 |
+
"resolved": "https://registry.npmjs.org/@types/d3-delaunay/-/d3-delaunay-6.0.4.tgz",
|
| 1124 |
+
"integrity": "sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw==",
|
| 1125 |
+
"dev": true
|
| 1126 |
+
},
|
| 1127 |
+
"node_modules/@types/d3-dispatch": {
|
| 1128 |
+
"version": "3.0.7",
|
| 1129 |
+
"resolved": "https://registry.npmjs.org/@types/d3-dispatch/-/d3-dispatch-3.0.7.tgz",
|
| 1130 |
+
"integrity": "sha512-5o9OIAdKkhN1QItV2oqaE5KMIiXAvDWBDPrD85e58Qlz1c1kI/J0NcqbEG88CoTwJrYe7ntUCVfeUl2UJKbWgA==",
|
| 1131 |
+
"dev": true
|
| 1132 |
+
},
|
| 1133 |
+
"node_modules/@types/d3-drag": {
|
| 1134 |
+
"version": "3.0.7",
|
| 1135 |
+
"resolved": "https://registry.npmjs.org/@types/d3-drag/-/d3-drag-3.0.7.tgz",
|
| 1136 |
+
"integrity": "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==",
|
| 1137 |
+
"dev": true,
|
| 1138 |
+
"dependencies": {
|
| 1139 |
+
"@types/d3-selection": "*"
|
| 1140 |
+
}
|
| 1141 |
+
},
|
| 1142 |
+
"node_modules/@types/d3-dsv": {
|
| 1143 |
+
"version": "3.0.7",
|
| 1144 |
+
"resolved": "https://registry.npmjs.org/@types/d3-dsv/-/d3-dsv-3.0.7.tgz",
|
| 1145 |
+
"integrity": "sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g==",
|
| 1146 |
+
"dev": true
|
| 1147 |
+
},
|
| 1148 |
+
"node_modules/@types/d3-ease": {
|
| 1149 |
+
"version": "3.0.2",
|
| 1150 |
+
"resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz",
|
| 1151 |
+
"integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==",
|
| 1152 |
+
"dev": true
|
| 1153 |
+
},
|
| 1154 |
+
"node_modules/@types/d3-fetch": {
|
| 1155 |
+
"version": "3.0.7",
|
| 1156 |
+
"resolved": "https://registry.npmjs.org/@types/d3-fetch/-/d3-fetch-3.0.7.tgz",
|
| 1157 |
+
"integrity": "sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA==",
|
| 1158 |
+
"dev": true,
|
| 1159 |
+
"dependencies": {
|
| 1160 |
+
"@types/d3-dsv": "*"
|
| 1161 |
+
}
|
| 1162 |
+
},
|
| 1163 |
+
"node_modules/@types/d3-force": {
|
| 1164 |
+
"version": "3.0.10",
|
| 1165 |
+
"resolved": "https://registry.npmjs.org/@types/d3-force/-/d3-force-3.0.10.tgz",
|
| 1166 |
+
"integrity": "sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw==",
|
| 1167 |
+
"dev": true
|
| 1168 |
+
},
|
| 1169 |
+
"node_modules/@types/d3-format": {
|
| 1170 |
+
"version": "3.0.4",
|
| 1171 |
+
"resolved": "https://registry.npmjs.org/@types/d3-format/-/d3-format-3.0.4.tgz",
|
| 1172 |
+
"integrity": "sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g==",
|
| 1173 |
+
"dev": true
|
| 1174 |
+
},
|
| 1175 |
+
"node_modules/@types/d3-geo": {
|
| 1176 |
+
"version": "3.1.0",
|
| 1177 |
+
"resolved": "https://registry.npmjs.org/@types/d3-geo/-/d3-geo-3.1.0.tgz",
|
| 1178 |
+
"integrity": "sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ==",
|
| 1179 |
+
"dev": true,
|
| 1180 |
+
"dependencies": {
|
| 1181 |
+
"@types/geojson": "*"
|
| 1182 |
+
}
|
| 1183 |
+
},
|
| 1184 |
+
"node_modules/@types/d3-hierarchy": {
|
| 1185 |
+
"version": "3.1.7",
|
| 1186 |
+
"resolved": "https://registry.npmjs.org/@types/d3-hierarchy/-/d3-hierarchy-3.1.7.tgz",
|
| 1187 |
+
"integrity": "sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg==",
|
| 1188 |
+
"dev": true
|
| 1189 |
+
},
|
| 1190 |
+
"node_modules/@types/d3-interpolate": {
|
| 1191 |
+
"version": "3.0.4",
|
| 1192 |
+
"resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz",
|
| 1193 |
+
"integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==",
|
| 1194 |
+
"dev": true,
|
| 1195 |
+
"dependencies": {
|
| 1196 |
+
"@types/d3-color": "*"
|
| 1197 |
+
}
|
| 1198 |
+
},
|
| 1199 |
+
"node_modules/@types/d3-path": {
|
| 1200 |
+
"version": "3.1.1",
|
| 1201 |
+
"resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz",
|
| 1202 |
+
"integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==",
|
| 1203 |
+
"dev": true
|
| 1204 |
+
},
|
| 1205 |
+
"node_modules/@types/d3-polygon": {
|
| 1206 |
+
"version": "3.0.2",
|
| 1207 |
+
"resolved": "https://registry.npmjs.org/@types/d3-polygon/-/d3-polygon-3.0.2.tgz",
|
| 1208 |
+
"integrity": "sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA==",
|
| 1209 |
+
"dev": true
|
| 1210 |
+
},
|
| 1211 |
+
"node_modules/@types/d3-quadtree": {
|
| 1212 |
+
"version": "3.0.6",
|
| 1213 |
+
"resolved": "https://registry.npmjs.org/@types/d3-quadtree/-/d3-quadtree-3.0.6.tgz",
|
| 1214 |
+
"integrity": "sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg==",
|
| 1215 |
+
"dev": true
|
| 1216 |
+
},
|
| 1217 |
+
"node_modules/@types/d3-random": {
|
| 1218 |
+
"version": "3.0.3",
|
| 1219 |
+
"resolved": "https://registry.npmjs.org/@types/d3-random/-/d3-random-3.0.3.tgz",
|
| 1220 |
+
"integrity": "sha512-Imagg1vJ3y76Y2ea0871wpabqp613+8/r0mCLEBfdtqC7xMSfj9idOnmBYyMoULfHePJyxMAw3nWhJxzc+LFwQ==",
|
| 1221 |
+
"dev": true
|
| 1222 |
+
},
|
| 1223 |
+
"node_modules/@types/d3-scale": {
|
| 1224 |
+
"version": "4.0.9",
|
| 1225 |
+
"resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz",
|
| 1226 |
+
"integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==",
|
| 1227 |
+
"dev": true,
|
| 1228 |
+
"dependencies": {
|
| 1229 |
+
"@types/d3-time": "*"
|
| 1230 |
+
}
|
| 1231 |
+
},
|
| 1232 |
+
"node_modules/@types/d3-scale-chromatic": {
|
| 1233 |
+
"version": "3.1.0",
|
| 1234 |
+
"resolved": "https://registry.npmjs.org/@types/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz",
|
| 1235 |
+
"integrity": "sha512-iWMJgwkK7yTRmWqRB5plb1kadXyQ5Sj8V/zYlFGMUBbIPKQScw+Dku9cAAMgJG+z5GYDoMjWGLVOvjghDEFnKQ==",
|
| 1236 |
+
"dev": true
|
| 1237 |
+
},
|
| 1238 |
+
"node_modules/@types/d3-selection": {
|
| 1239 |
+
"version": "3.0.11",
|
| 1240 |
+
"resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.11.tgz",
|
| 1241 |
+
"integrity": "sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==",
|
| 1242 |
+
"dev": true
|
| 1243 |
+
},
|
| 1244 |
+
"node_modules/@types/d3-shape": {
|
| 1245 |
+
"version": "3.1.7",
|
| 1246 |
+
"resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.7.tgz",
|
| 1247 |
+
"integrity": "sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg==",
|
| 1248 |
+
"dev": true,
|
| 1249 |
+
"dependencies": {
|
| 1250 |
+
"@types/d3-path": "*"
|
| 1251 |
+
}
|
| 1252 |
+
},
|
| 1253 |
+
"node_modules/@types/d3-time": {
|
| 1254 |
+
"version": "3.0.4",
|
| 1255 |
+
"resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz",
|
| 1256 |
+
"integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==",
|
| 1257 |
+
"dev": true
|
| 1258 |
+
},
|
| 1259 |
+
"node_modules/@types/d3-time-format": {
|
| 1260 |
+
"version": "4.0.3",
|
| 1261 |
+
"resolved": "https://registry.npmjs.org/@types/d3-time-format/-/d3-time-format-4.0.3.tgz",
|
| 1262 |
+
"integrity": "sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg==",
|
| 1263 |
+
"dev": true
|
| 1264 |
+
},
|
| 1265 |
+
"node_modules/@types/d3-timer": {
|
| 1266 |
+
"version": "3.0.2",
|
| 1267 |
+
"resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz",
|
| 1268 |
+
"integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==",
|
| 1269 |
+
"dev": true
|
| 1270 |
+
},
|
| 1271 |
+
"node_modules/@types/d3-transition": {
|
| 1272 |
+
"version": "3.0.9",
|
| 1273 |
+
"resolved": "https://registry.npmjs.org/@types/d3-transition/-/d3-transition-3.0.9.tgz",
|
| 1274 |
+
"integrity": "sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==",
|
| 1275 |
+
"dev": true,
|
| 1276 |
+
"dependencies": {
|
| 1277 |
+
"@types/d3-selection": "*"
|
| 1278 |
+
}
|
| 1279 |
+
},
|
| 1280 |
+
"node_modules/@types/d3-zoom": {
|
| 1281 |
+
"version": "3.0.8",
|
| 1282 |
+
"resolved": "https://registry.npmjs.org/@types/d3-zoom/-/d3-zoom-3.0.8.tgz",
|
| 1283 |
+
"integrity": "sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==",
|
| 1284 |
+
"dev": true,
|
| 1285 |
+
"dependencies": {
|
| 1286 |
+
"@types/d3-interpolate": "*",
|
| 1287 |
+
"@types/d3-selection": "*"
|
| 1288 |
+
}
|
| 1289 |
+
},
|
| 1290 |
"node_modules/@types/estree": {
|
| 1291 |
"version": "1.0.8",
|
| 1292 |
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
|
| 1293 |
"integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
|
| 1294 |
"dev": true
|
| 1295 |
},
|
| 1296 |
+
"node_modules/@types/geojson": {
|
| 1297 |
+
"version": "7946.0.16",
|
| 1298 |
+
"resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz",
|
| 1299 |
+
"integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==",
|
| 1300 |
+
"dev": true
|
| 1301 |
+
},
|
| 1302 |
"node_modules/@types/node": {
|
| 1303 |
"version": "24.10.1",
|
| 1304 |
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.1.tgz",
|
| 1305 |
"integrity": "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==",
|
| 1306 |
+
"dev": true,
|
| 1307 |
+
"optional": true,
|
| 1308 |
+
"peer": true,
|
| 1309 |
"dependencies": {
|
| 1310 |
"undici-types": "~7.16.0"
|
| 1311 |
}
|
|
|
|
| 1565 |
"integrity": "sha512-twmVoizEW7ylZSN32OgKdXRmo1qg+wT5/6C3xu5b9QsWzSFAhHLn2xd8ro0diCsKfCj1RdaTP/nrcW+vAoQPIw==",
|
| 1566 |
"dev": true
|
| 1567 |
},
|
| 1568 |
+
"node_modules/commander": {
|
| 1569 |
+
"version": "7.2.0",
|
| 1570 |
+
"resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz",
|
| 1571 |
+
"integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==",
|
| 1572 |
+
"engines": {
|
| 1573 |
+
"node": ">= 10"
|
| 1574 |
+
}
|
| 1575 |
+
},
|
| 1576 |
"node_modules/csstype": {
|
| 1577 |
"version": "3.1.3",
|
| 1578 |
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
|
| 1579 |
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="
|
| 1580 |
},
|
| 1581 |
+
"node_modules/d3": {
|
| 1582 |
+
"version": "7.9.0",
|
| 1583 |
+
"resolved": "https://registry.npmjs.org/d3/-/d3-7.9.0.tgz",
|
| 1584 |
+
"integrity": "sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA==",
|
| 1585 |
+
"dependencies": {
|
| 1586 |
+
"d3-array": "3",
|
| 1587 |
+
"d3-axis": "3",
|
| 1588 |
+
"d3-brush": "3",
|
| 1589 |
+
"d3-chord": "3",
|
| 1590 |
+
"d3-color": "3",
|
| 1591 |
+
"d3-contour": "4",
|
| 1592 |
+
"d3-delaunay": "6",
|
| 1593 |
+
"d3-dispatch": "3",
|
| 1594 |
+
"d3-drag": "3",
|
| 1595 |
+
"d3-dsv": "3",
|
| 1596 |
+
"d3-ease": "3",
|
| 1597 |
+
"d3-fetch": "3",
|
| 1598 |
+
"d3-force": "3",
|
| 1599 |
+
"d3-format": "3",
|
| 1600 |
+
"d3-geo": "3",
|
| 1601 |
+
"d3-hierarchy": "3",
|
| 1602 |
+
"d3-interpolate": "3",
|
| 1603 |
+
"d3-path": "3",
|
| 1604 |
+
"d3-polygon": "3",
|
| 1605 |
+
"d3-quadtree": "3",
|
| 1606 |
+
"d3-random": "3",
|
| 1607 |
+
"d3-scale": "4",
|
| 1608 |
+
"d3-scale-chromatic": "3",
|
| 1609 |
+
"d3-selection": "3",
|
| 1610 |
+
"d3-shape": "3",
|
| 1611 |
+
"d3-time": "3",
|
| 1612 |
+
"d3-time-format": "4",
|
| 1613 |
+
"d3-timer": "3",
|
| 1614 |
+
"d3-transition": "3",
|
| 1615 |
+
"d3-zoom": "3"
|
| 1616 |
+
},
|
| 1617 |
+
"engines": {
|
| 1618 |
+
"node": ">=12"
|
| 1619 |
+
}
|
| 1620 |
+
},
|
| 1621 |
+
"node_modules/d3-array": {
|
| 1622 |
+
"version": "3.2.4",
|
| 1623 |
+
"resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz",
|
| 1624 |
+
"integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==",
|
| 1625 |
+
"dependencies": {
|
| 1626 |
+
"internmap": "1 - 2"
|
| 1627 |
+
},
|
| 1628 |
+
"engines": {
|
| 1629 |
+
"node": ">=12"
|
| 1630 |
+
}
|
| 1631 |
+
},
|
| 1632 |
+
"node_modules/d3-axis": {
|
| 1633 |
+
"version": "3.0.0",
|
| 1634 |
+
"resolved": "https://registry.npmjs.org/d3-axis/-/d3-axis-3.0.0.tgz",
|
| 1635 |
+
"integrity": "sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==",
|
| 1636 |
+
"engines": {
|
| 1637 |
+
"node": ">=12"
|
| 1638 |
+
}
|
| 1639 |
+
},
|
| 1640 |
+
"node_modules/d3-brush": {
|
| 1641 |
+
"version": "3.0.0",
|
| 1642 |
+
"resolved": "https://registry.npmjs.org/d3-brush/-/d3-brush-3.0.0.tgz",
|
| 1643 |
+
"integrity": "sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==",
|
| 1644 |
+
"dependencies": {
|
| 1645 |
+
"d3-dispatch": "1 - 3",
|
| 1646 |
+
"d3-drag": "2 - 3",
|
| 1647 |
+
"d3-interpolate": "1 - 3",
|
| 1648 |
+
"d3-selection": "3",
|
| 1649 |
+
"d3-transition": "3"
|
| 1650 |
+
},
|
| 1651 |
+
"engines": {
|
| 1652 |
+
"node": ">=12"
|
| 1653 |
+
}
|
| 1654 |
+
},
|
| 1655 |
+
"node_modules/d3-chord": {
|
| 1656 |
+
"version": "3.0.1",
|
| 1657 |
+
"resolved": "https://registry.npmjs.org/d3-chord/-/d3-chord-3.0.1.tgz",
|
| 1658 |
+
"integrity": "sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==",
|
| 1659 |
+
"dependencies": {
|
| 1660 |
+
"d3-path": "1 - 3"
|
| 1661 |
+
},
|
| 1662 |
+
"engines": {
|
| 1663 |
+
"node": ">=12"
|
| 1664 |
+
}
|
| 1665 |
+
},
|
| 1666 |
+
"node_modules/d3-color": {
|
| 1667 |
+
"version": "3.1.0",
|
| 1668 |
+
"resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz",
|
| 1669 |
+
"integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==",
|
| 1670 |
+
"engines": {
|
| 1671 |
+
"node": ">=12"
|
| 1672 |
+
}
|
| 1673 |
+
},
|
| 1674 |
+
"node_modules/d3-contour": {
|
| 1675 |
+
"version": "4.0.2",
|
| 1676 |
+
"resolved": "https://registry.npmjs.org/d3-contour/-/d3-contour-4.0.2.tgz",
|
| 1677 |
+
"integrity": "sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA==",
|
| 1678 |
+
"dependencies": {
|
| 1679 |
+
"d3-array": "^3.2.0"
|
| 1680 |
+
},
|
| 1681 |
+
"engines": {
|
| 1682 |
+
"node": ">=12"
|
| 1683 |
+
}
|
| 1684 |
+
},
|
| 1685 |
+
"node_modules/d3-delaunay": {
|
| 1686 |
+
"version": "6.0.4",
|
| 1687 |
+
"resolved": "https://registry.npmjs.org/d3-delaunay/-/d3-delaunay-6.0.4.tgz",
|
| 1688 |
+
"integrity": "sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==",
|
| 1689 |
+
"dependencies": {
|
| 1690 |
+
"delaunator": "5"
|
| 1691 |
+
},
|
| 1692 |
+
"engines": {
|
| 1693 |
+
"node": ">=12"
|
| 1694 |
+
}
|
| 1695 |
+
},
|
| 1696 |
+
"node_modules/d3-dispatch": {
|
| 1697 |
+
"version": "3.0.1",
|
| 1698 |
+
"resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz",
|
| 1699 |
+
"integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==",
|
| 1700 |
+
"engines": {
|
| 1701 |
+
"node": ">=12"
|
| 1702 |
+
}
|
| 1703 |
+
},
|
| 1704 |
+
"node_modules/d3-drag": {
|
| 1705 |
+
"version": "3.0.0",
|
| 1706 |
+
"resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz",
|
| 1707 |
+
"integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==",
|
| 1708 |
+
"dependencies": {
|
| 1709 |
+
"d3-dispatch": "1 - 3",
|
| 1710 |
+
"d3-selection": "3"
|
| 1711 |
+
},
|
| 1712 |
+
"engines": {
|
| 1713 |
+
"node": ">=12"
|
| 1714 |
+
}
|
| 1715 |
+
},
|
| 1716 |
+
"node_modules/d3-dsv": {
|
| 1717 |
+
"version": "3.0.1",
|
| 1718 |
+
"resolved": "https://registry.npmjs.org/d3-dsv/-/d3-dsv-3.0.1.tgz",
|
| 1719 |
+
"integrity": "sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==",
|
| 1720 |
+
"dependencies": {
|
| 1721 |
+
"commander": "7",
|
| 1722 |
+
"iconv-lite": "0.6",
|
| 1723 |
+
"rw": "1"
|
| 1724 |
+
},
|
| 1725 |
+
"bin": {
|
| 1726 |
+
"csv2json": "bin/dsv2json.js",
|
| 1727 |
+
"csv2tsv": "bin/dsv2dsv.js",
|
| 1728 |
+
"dsv2dsv": "bin/dsv2dsv.js",
|
| 1729 |
+
"dsv2json": "bin/dsv2json.js",
|
| 1730 |
+
"json2csv": "bin/json2dsv.js",
|
| 1731 |
+
"json2dsv": "bin/json2dsv.js",
|
| 1732 |
+
"json2tsv": "bin/json2dsv.js",
|
| 1733 |
+
"tsv2csv": "bin/dsv2dsv.js",
|
| 1734 |
+
"tsv2json": "bin/dsv2json.js"
|
| 1735 |
+
},
|
| 1736 |
+
"engines": {
|
| 1737 |
+
"node": ">=12"
|
| 1738 |
+
}
|
| 1739 |
+
},
|
| 1740 |
+
"node_modules/d3-ease": {
|
| 1741 |
+
"version": "3.0.1",
|
| 1742 |
+
"resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz",
|
| 1743 |
+
"integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==",
|
| 1744 |
+
"engines": {
|
| 1745 |
+
"node": ">=12"
|
| 1746 |
+
}
|
| 1747 |
+
},
|
| 1748 |
+
"node_modules/d3-fetch": {
|
| 1749 |
+
"version": "3.0.1",
|
| 1750 |
+
"resolved": "https://registry.npmjs.org/d3-fetch/-/d3-fetch-3.0.1.tgz",
|
| 1751 |
+
"integrity": "sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==",
|
| 1752 |
+
"dependencies": {
|
| 1753 |
+
"d3-dsv": "1 - 3"
|
| 1754 |
+
},
|
| 1755 |
+
"engines": {
|
| 1756 |
+
"node": ">=12"
|
| 1757 |
+
}
|
| 1758 |
+
},
|
| 1759 |
+
"node_modules/d3-force": {
|
| 1760 |
+
"version": "3.0.0",
|
| 1761 |
+
"resolved": "https://registry.npmjs.org/d3-force/-/d3-force-3.0.0.tgz",
|
| 1762 |
+
"integrity": "sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==",
|
| 1763 |
+
"dependencies": {
|
| 1764 |
+
"d3-dispatch": "1 - 3",
|
| 1765 |
+
"d3-quadtree": "1 - 3",
|
| 1766 |
+
"d3-timer": "1 - 3"
|
| 1767 |
+
},
|
| 1768 |
+
"engines": {
|
| 1769 |
+
"node": ">=12"
|
| 1770 |
+
}
|
| 1771 |
+
},
|
| 1772 |
+
"node_modules/d3-format": {
|
| 1773 |
+
"version": "3.1.0",
|
| 1774 |
+
"resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz",
|
| 1775 |
+
"integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==",
|
| 1776 |
+
"engines": {
|
| 1777 |
+
"node": ">=12"
|
| 1778 |
+
}
|
| 1779 |
+
},
|
| 1780 |
+
"node_modules/d3-geo": {
|
| 1781 |
+
"version": "3.1.1",
|
| 1782 |
+
"resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-3.1.1.tgz",
|
| 1783 |
+
"integrity": "sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q==",
|
| 1784 |
+
"dependencies": {
|
| 1785 |
+
"d3-array": "2.5.0 - 3"
|
| 1786 |
+
},
|
| 1787 |
+
"engines": {
|
| 1788 |
+
"node": ">=12"
|
| 1789 |
+
}
|
| 1790 |
+
},
|
| 1791 |
+
"node_modules/d3-hierarchy": {
|
| 1792 |
+
"version": "3.1.2",
|
| 1793 |
+
"resolved": "https://registry.npmjs.org/d3-hierarchy/-/d3-hierarchy-3.1.2.tgz",
|
| 1794 |
+
"integrity": "sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==",
|
| 1795 |
+
"engines": {
|
| 1796 |
+
"node": ">=12"
|
| 1797 |
+
}
|
| 1798 |
+
},
|
| 1799 |
+
"node_modules/d3-interpolate": {
|
| 1800 |
+
"version": "3.0.1",
|
| 1801 |
+
"resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz",
|
| 1802 |
+
"integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==",
|
| 1803 |
+
"dependencies": {
|
| 1804 |
+
"d3-color": "1 - 3"
|
| 1805 |
+
},
|
| 1806 |
+
"engines": {
|
| 1807 |
+
"node": ">=12"
|
| 1808 |
+
}
|
| 1809 |
+
},
|
| 1810 |
+
"node_modules/d3-path": {
|
| 1811 |
+
"version": "3.1.0",
|
| 1812 |
+
"resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz",
|
| 1813 |
+
"integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==",
|
| 1814 |
+
"engines": {
|
| 1815 |
+
"node": ">=12"
|
| 1816 |
+
}
|
| 1817 |
+
},
|
| 1818 |
+
"node_modules/d3-polygon": {
|
| 1819 |
+
"version": "3.0.1",
|
| 1820 |
+
"resolved": "https://registry.npmjs.org/d3-polygon/-/d3-polygon-3.0.1.tgz",
|
| 1821 |
+
"integrity": "sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==",
|
| 1822 |
+
"engines": {
|
| 1823 |
+
"node": ">=12"
|
| 1824 |
+
}
|
| 1825 |
+
},
|
| 1826 |
+
"node_modules/d3-quadtree": {
|
| 1827 |
+
"version": "3.0.1",
|
| 1828 |
+
"resolved": "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-3.0.1.tgz",
|
| 1829 |
+
"integrity": "sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==",
|
| 1830 |
+
"engines": {
|
| 1831 |
+
"node": ">=12"
|
| 1832 |
+
}
|
| 1833 |
+
},
|
| 1834 |
+
"node_modules/d3-random": {
|
| 1835 |
+
"version": "3.0.1",
|
| 1836 |
+
"resolved": "https://registry.npmjs.org/d3-random/-/d3-random-3.0.1.tgz",
|
| 1837 |
+
"integrity": "sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==",
|
| 1838 |
+
"engines": {
|
| 1839 |
+
"node": ">=12"
|
| 1840 |
+
}
|
| 1841 |
+
},
|
| 1842 |
+
"node_modules/d3-scale": {
|
| 1843 |
+
"version": "4.0.2",
|
| 1844 |
+
"resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz",
|
| 1845 |
+
"integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==",
|
| 1846 |
+
"dependencies": {
|
| 1847 |
+
"d3-array": "2.10.0 - 3",
|
| 1848 |
+
"d3-format": "1 - 3",
|
| 1849 |
+
"d3-interpolate": "1.2.0 - 3",
|
| 1850 |
+
"d3-time": "2.1.1 - 3",
|
| 1851 |
+
"d3-time-format": "2 - 4"
|
| 1852 |
+
},
|
| 1853 |
+
"engines": {
|
| 1854 |
+
"node": ">=12"
|
| 1855 |
+
}
|
| 1856 |
+
},
|
| 1857 |
+
"node_modules/d3-scale-chromatic": {
|
| 1858 |
+
"version": "3.1.0",
|
| 1859 |
+
"resolved": "https://registry.npmjs.org/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz",
|
| 1860 |
+
"integrity": "sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ==",
|
| 1861 |
+
"dependencies": {
|
| 1862 |
+
"d3-color": "1 - 3",
|
| 1863 |
+
"d3-interpolate": "1 - 3"
|
| 1864 |
+
},
|
| 1865 |
+
"engines": {
|
| 1866 |
+
"node": ">=12"
|
| 1867 |
+
}
|
| 1868 |
+
},
|
| 1869 |
+
"node_modules/d3-selection": {
|
| 1870 |
+
"version": "3.0.0",
|
| 1871 |
+
"resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz",
|
| 1872 |
+
"integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==",
|
| 1873 |
+
"engines": {
|
| 1874 |
+
"node": ">=12"
|
| 1875 |
+
}
|
| 1876 |
+
},
|
| 1877 |
+
"node_modules/d3-shape": {
|
| 1878 |
+
"version": "3.2.0",
|
| 1879 |
+
"resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz",
|
| 1880 |
+
"integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==",
|
| 1881 |
+
"dependencies": {
|
| 1882 |
+
"d3-path": "^3.1.0"
|
| 1883 |
+
},
|
| 1884 |
+
"engines": {
|
| 1885 |
+
"node": ">=12"
|
| 1886 |
+
}
|
| 1887 |
+
},
|
| 1888 |
+
"node_modules/d3-time": {
|
| 1889 |
+
"version": "3.1.0",
|
| 1890 |
+
"resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz",
|
| 1891 |
+
"integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==",
|
| 1892 |
+
"dependencies": {
|
| 1893 |
+
"d3-array": "2 - 3"
|
| 1894 |
+
},
|
| 1895 |
+
"engines": {
|
| 1896 |
+
"node": ">=12"
|
| 1897 |
+
}
|
| 1898 |
+
},
|
| 1899 |
+
"node_modules/d3-time-format": {
|
| 1900 |
+
"version": "4.1.0",
|
| 1901 |
+
"resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz",
|
| 1902 |
+
"integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==",
|
| 1903 |
+
"dependencies": {
|
| 1904 |
+
"d3-time": "1 - 3"
|
| 1905 |
+
},
|
| 1906 |
+
"engines": {
|
| 1907 |
+
"node": ">=12"
|
| 1908 |
+
}
|
| 1909 |
+
},
|
| 1910 |
+
"node_modules/d3-timer": {
|
| 1911 |
+
"version": "3.0.1",
|
| 1912 |
+
"resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz",
|
| 1913 |
+
"integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==",
|
| 1914 |
+
"engines": {
|
| 1915 |
+
"node": ">=12"
|
| 1916 |
+
}
|
| 1917 |
+
},
|
| 1918 |
+
"node_modules/d3-transition": {
|
| 1919 |
+
"version": "3.0.1",
|
| 1920 |
+
"resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz",
|
| 1921 |
+
"integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==",
|
| 1922 |
+
"dependencies": {
|
| 1923 |
+
"d3-color": "1 - 3",
|
| 1924 |
+
"d3-dispatch": "1 - 3",
|
| 1925 |
+
"d3-ease": "1 - 3",
|
| 1926 |
+
"d3-interpolate": "1 - 3",
|
| 1927 |
+
"d3-timer": "1 - 3"
|
| 1928 |
+
},
|
| 1929 |
+
"engines": {
|
| 1930 |
+
"node": ">=12"
|
| 1931 |
+
},
|
| 1932 |
+
"peerDependencies": {
|
| 1933 |
+
"d3-selection": "2 - 3"
|
| 1934 |
+
}
|
| 1935 |
+
},
|
| 1936 |
+
"node_modules/d3-zoom": {
|
| 1937 |
+
"version": "3.0.0",
|
| 1938 |
+
"resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz",
|
| 1939 |
+
"integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==",
|
| 1940 |
+
"dependencies": {
|
| 1941 |
+
"d3-dispatch": "1 - 3",
|
| 1942 |
+
"d3-drag": "2 - 3",
|
| 1943 |
+
"d3-interpolate": "1 - 3",
|
| 1944 |
+
"d3-selection": "2 - 3",
|
| 1945 |
+
"d3-transition": "2 - 3"
|
| 1946 |
+
},
|
| 1947 |
+
"engines": {
|
| 1948 |
+
"node": ">=12"
|
| 1949 |
+
}
|
| 1950 |
+
},
|
| 1951 |
"node_modules/de-indent": {
|
| 1952 |
"version": "1.0.2",
|
| 1953 |
"resolved": "https://registry.npmjs.org/de-indent/-/de-indent-1.0.2.tgz",
|
|
|
|
| 1970 |
}
|
| 1971 |
}
|
| 1972 |
},
|
| 1973 |
+
"node_modules/delaunator": {
|
| 1974 |
+
"version": "5.0.1",
|
| 1975 |
+
"resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.0.1.tgz",
|
| 1976 |
+
"integrity": "sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw==",
|
| 1977 |
+
"dependencies": {
|
| 1978 |
+
"robust-predicates": "^3.0.2"
|
| 1979 |
+
}
|
| 1980 |
+
},
|
| 1981 |
"node_modules/detect-libc": {
|
| 1982 |
"version": "1.0.3",
|
| 1983 |
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz",
|
|
|
|
| 2084 |
"node": ">=8"
|
| 2085 |
}
|
| 2086 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2087 |
"node_modules/fsevents": {
|
| 2088 |
"version": "2.3.3",
|
| 2089 |
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
|
|
|
| 2098 |
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
| 2099 |
}
|
| 2100 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2101 |
"node_modules/has-flag": {
|
| 2102 |
"version": "4.0.0",
|
| 2103 |
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
|
|
|
|
| 2116 |
"he": "bin/he"
|
| 2117 |
}
|
| 2118 |
},
|
| 2119 |
+
"node_modules/iconv-lite": {
|
| 2120 |
+
"version": "0.6.3",
|
| 2121 |
+
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
|
| 2122 |
+
"integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
|
| 2123 |
+
"dependencies": {
|
| 2124 |
+
"safer-buffer": ">= 2.1.2 < 3.0.0"
|
| 2125 |
+
},
|
| 2126 |
+
"engines": {
|
| 2127 |
+
"node": ">=0.10.0"
|
| 2128 |
+
}
|
| 2129 |
+
},
|
| 2130 |
"node_modules/immutable": {
|
| 2131 |
"version": "5.1.4",
|
| 2132 |
"resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.4.tgz",
|
| 2133 |
"integrity": "sha512-p6u1bG3YSnINT5RQmx/yRZBpenIl30kVxkTLDyHLIMk0gict704Q9n+thfDI7lTRm9vXdDYutVzXhzcThxTnXA==",
|
| 2134 |
"dev": true
|
| 2135 |
},
|
| 2136 |
+
"node_modules/internmap": {
|
| 2137 |
+
"version": "2.0.3",
|
| 2138 |
+
"resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz",
|
| 2139 |
+
"integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==",
|
| 2140 |
+
"engines": {
|
| 2141 |
+
"node": ">=12"
|
| 2142 |
+
}
|
| 2143 |
+
},
|
| 2144 |
"node_modules/is-extglob": {
|
| 2145 |
"version": "2.1.1",
|
| 2146 |
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
|
|
|
|
| 2174 |
"node": ">=0.12.0"
|
| 2175 |
}
|
| 2176 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2177 |
"node_modules/magic-string": {
|
| 2178 |
"version": "0.30.19",
|
| 2179 |
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.19.tgz",
|
|
|
|
| 2252 |
"dev": true,
|
| 2253 |
"optional": true
|
| 2254 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2255 |
"node_modules/path-browserify": {
|
| 2256 |
"version": "1.0.1",
|
| 2257 |
"resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz",
|
|
|
|
| 2297 |
}
|
| 2298 |
}
|
| 2299 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2300 |
"node_modules/postcss": {
|
| 2301 |
"version": "8.5.6",
|
| 2302 |
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
|
|
|
|
| 2324 |
"node": "^10 || ^12 || >=14"
|
| 2325 |
}
|
| 2326 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2327 |
"node_modules/readdirp": {
|
| 2328 |
"version": "4.1.2",
|
| 2329 |
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz",
|
|
|
|
| 2338 |
"url": "https://paulmillr.com/funding/"
|
| 2339 |
}
|
| 2340 |
},
|
| 2341 |
+
"node_modules/robust-predicates": {
|
| 2342 |
+
"version": "3.0.2",
|
| 2343 |
+
"resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.2.tgz",
|
| 2344 |
+
"integrity": "sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg=="
|
| 2345 |
+
},
|
| 2346 |
"node_modules/rollup": {
|
| 2347 |
"version": "4.52.5",
|
| 2348 |
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.52.5.tgz",
|
|
|
|
| 2384 |
"fsevents": "~2.3.2"
|
| 2385 |
}
|
| 2386 |
},
|
| 2387 |
+
"node_modules/rw": {
|
| 2388 |
+
"version": "1.3.3",
|
| 2389 |
+
"resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz",
|
| 2390 |
+
"integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ=="
|
| 2391 |
+
},
|
| 2392 |
"node_modules/rxjs": {
|
| 2393 |
"version": "7.8.2",
|
| 2394 |
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz",
|
|
|
|
| 2398 |
"tslib": "^2.1.0"
|
| 2399 |
}
|
| 2400 |
},
|
| 2401 |
+
"node_modules/safer-buffer": {
|
| 2402 |
+
"version": "2.1.2",
|
| 2403 |
+
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
|
| 2404 |
+
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
|
| 2405 |
+
},
|
| 2406 |
"node_modules/sass": {
|
| 2407 |
"version": "1.93.2",
|
| 2408 |
"resolved": "https://registry.npmjs.org/sass/-/sass-1.93.2.tgz",
|
|
|
|
| 2864 |
"node_modules/undici-types": {
|
| 2865 |
"version": "7.16.0",
|
| 2866 |
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz",
|
| 2867 |
+
"integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==",
|
| 2868 |
+
"dev": true,
|
| 2869 |
+
"optional": true,
|
| 2870 |
+
"peer": true
|
| 2871 |
},
|
| 2872 |
"node_modules/varint": {
|
| 2873 |
"version": "6.0.0",
|
trigo-web/app/package.json
CHANGED
|
@@ -10,6 +10,8 @@
|
|
| 10 |
"preview": "vite preview"
|
| 11 |
},
|
| 12 |
"dependencies": {
|
|
|
|
|
|
|
| 13 |
"pinia": "^2.1.6",
|
| 14 |
"socket.io-client": "^4.5.2",
|
| 15 |
"three": "^0.156.1",
|
|
@@ -18,6 +20,7 @@
|
|
| 18 |
"onnxruntime-web": "^1.23.2"
|
| 19 |
},
|
| 20 |
"devDependencies": {
|
|
|
|
| 21 |
"@types/three": "^0.156.0",
|
| 22 |
"@vitejs/plugin-vue": "^5.2.4",
|
| 23 |
"sass-embedded": "^1.93.2",
|
|
|
|
| 10 |
"preview": "vite preview"
|
| 11 |
},
|
| 12 |
"dependencies": {
|
| 13 |
+
"d3": "^7.9.0",
|
| 14 |
+
"d3-scale-chromatic": "^3.1.0",
|
| 15 |
"pinia": "^2.1.6",
|
| 16 |
"socket.io-client": "^4.5.2",
|
| 17 |
"three": "^0.156.1",
|
|
|
|
| 20 |
"onnxruntime-web": "^1.23.2"
|
| 21 |
},
|
| 22 |
"devDependencies": {
|
| 23 |
+
"@types/d3": "^7.4.3",
|
| 24 |
"@types/three": "^0.156.0",
|
| 25 |
"@vitejs/plugin-vue": "^5.2.4",
|
| 26 |
"sass-embedded": "^1.93.2",
|
trigo-web/app/src/components/InlineNicknameEditor.vue
ADDED
|
@@ -0,0 +1,225 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<template>
|
| 2 |
+
<div class="inline-nickname-editor" :class="{ editable, editing, [playerColor || 'neutral']: true }">
|
| 3 |
+
<!-- Display mode -->
|
| 4 |
+
<div
|
| 5 |
+
v-if="!editing"
|
| 6 |
+
class="nickname-display"
|
| 7 |
+
:title="editable ? 'Click to edit nickname' : ''"
|
| 8 |
+
@click="startEdit"
|
| 9 |
+
>
|
| 10 |
+
<span class="stone-icon" :class="playerColor"></span>
|
| 11 |
+
<span class="nickname-text">{{ displayNickname }}</span>
|
| 12 |
+
<span v-if="editable" class="edit-icon">✏️</span>
|
| 13 |
+
</div>
|
| 14 |
+
|
| 15 |
+
<!-- Edit mode -->
|
| 16 |
+
<div v-else class="nickname-edit">
|
| 17 |
+
<span class="stone-icon" :class="playerColor"></span>
|
| 18 |
+
<input
|
| 19 |
+
ref="inputRef"
|
| 20 |
+
v-model="editValue"
|
| 21 |
+
type="text"
|
| 22 |
+
class="nickname-input"
|
| 23 |
+
:class="{ error: hasError }"
|
| 24 |
+
:maxlength="20"
|
| 25 |
+
:title="inputTooltip"
|
| 26 |
+
@keydown.enter="confirmEdit"
|
| 27 |
+
@keydown.esc="cancelEdit"
|
| 28 |
+
@blur="confirmEdit"
|
| 29 |
+
/>
|
| 30 |
+
<span v-if="hasError" class="error-icon" :title="errorMessage">⚠️</span>
|
| 31 |
+
</div>
|
| 32 |
+
|
| 33 |
+
<!-- Character count and help text when editing -->
|
| 34 |
+
<div v-if="editing" class="edit-info">
|
| 35 |
+
<span class="help-text">Enter to save • Esc to cancel</span>
|
| 36 |
+
<span class="char-count">{{ editValue.length }}/20</span>
|
| 37 |
+
</div>
|
| 38 |
+
</div>
|
| 39 |
+
</template>
|
| 40 |
+
|
| 41 |
+
<script setup lang="ts">
|
| 42 |
+
import { ref, computed, watch, nextTick } from "vue";
|
| 43 |
+
|
| 44 |
+
const props = defineProps<{
|
| 45 |
+
nickname: string;
|
| 46 |
+
editable: boolean;
|
| 47 |
+
playerColor: "black" | "white" | null;
|
| 48 |
+
}>();
|
| 49 |
+
|
| 50 |
+
const emit = defineEmits<{
|
| 51 |
+
(e: "update", nickname: string): void;
|
| 52 |
+
}>();
|
| 53 |
+
|
| 54 |
+
const editing = ref(false);
|
| 55 |
+
const editValue = ref(props.nickname);
|
| 56 |
+
const errorMessage = ref("");
|
| 57 |
+
const inputRef = ref<HTMLInputElement | null>(null);
|
| 58 |
+
|
| 59 |
+
const displayNickname = computed(() => props.nickname || "Guest");
|
| 60 |
+
const hasError = computed(() => errorMessage.value !== "");
|
| 61 |
+
const inputTooltip = computed(() =>
|
| 62 |
+
"3-20 characters, letters/numbers/spaces only"
|
| 63 |
+
);
|
| 64 |
+
|
| 65 |
+
function validateNickname(nickname: string): { valid: boolean; error: string } {
|
| 66 |
+
const trimmed = nickname.trim();
|
| 67 |
+
|
| 68 |
+
if (trimmed.length < 3) return { valid: false, error: "Must be 3+ chars" };
|
| 69 |
+
if (trimmed.length > 20) return { valid: false, error: "Max 20 chars" };
|
| 70 |
+
if (!/^[a-zA-Z0-9 ]+$/.test(trimmed)) return { valid: false, error: "Letters, numbers, spaces only" };
|
| 71 |
+
if (trimmed !== nickname) return { valid: false, error: "No leading/trailing spaces" };
|
| 72 |
+
|
| 73 |
+
return { valid: true, error: "" };
|
| 74 |
+
}
|
| 75 |
+
|
| 76 |
+
function startEdit() {
|
| 77 |
+
if (!props.editable) return;
|
| 78 |
+
|
| 79 |
+
editing.value = true;
|
| 80 |
+
editValue.value = props.nickname;
|
| 81 |
+
errorMessage.value = "";
|
| 82 |
+
|
| 83 |
+
nextTick(() => {
|
| 84 |
+
inputRef.value?.focus();
|
| 85 |
+
inputRef.value?.select();
|
| 86 |
+
});
|
| 87 |
+
}
|
| 88 |
+
|
| 89 |
+
function confirmEdit() {
|
| 90 |
+
if (!editing.value) return;
|
| 91 |
+
|
| 92 |
+
const trimmed = editValue.value.trim();
|
| 93 |
+
|
| 94 |
+
if (trimmed === props.nickname) {
|
| 95 |
+
cancelEdit();
|
| 96 |
+
return;
|
| 97 |
+
}
|
| 98 |
+
|
| 99 |
+
const validation = validateNickname(trimmed);
|
| 100 |
+
if (!validation.valid) {
|
| 101 |
+
errorMessage.value = validation.error;
|
| 102 |
+
return;
|
| 103 |
+
}
|
| 104 |
+
|
| 105 |
+
editing.value = false;
|
| 106 |
+
errorMessage.value = "";
|
| 107 |
+
emit("update", trimmed);
|
| 108 |
+
}
|
| 109 |
+
|
| 110 |
+
function cancelEdit() {
|
| 111 |
+
editing.value = false;
|
| 112 |
+
editValue.value = props.nickname;
|
| 113 |
+
errorMessage.value = "";
|
| 114 |
+
}
|
| 115 |
+
|
| 116 |
+
watch(
|
| 117 |
+
() => props.nickname,
|
| 118 |
+
(newNickname) => {
|
| 119 |
+
if (!editing.value) {
|
| 120 |
+
editValue.value = newNickname;
|
| 121 |
+
}
|
| 122 |
+
}
|
| 123 |
+
);
|
| 124 |
+
</script>
|
| 125 |
+
|
| 126 |
+
<style lang="scss" scoped>
|
| 127 |
+
.inline-nickname-editor {
|
| 128 |
+
display: flex;
|
| 129 |
+
flex-direction: column;
|
| 130 |
+
gap: 0.25rem;
|
| 131 |
+
|
| 132 |
+
&.editable .nickname-display {
|
| 133 |
+
cursor: pointer;
|
| 134 |
+
transition: background-color 0.2s ease;
|
| 135 |
+
|
| 136 |
+
&:hover {
|
| 137 |
+
background-color: rgba(255, 255, 255, 0.05);
|
| 138 |
+
}
|
| 139 |
+
}
|
| 140 |
+
|
| 141 |
+
.nickname-display,
|
| 142 |
+
.nickname-edit {
|
| 143 |
+
display: flex;
|
| 144 |
+
align-items: center;
|
| 145 |
+
gap: 0.5rem;
|
| 146 |
+
padding: 0.5rem;
|
| 147 |
+
border-radius: 6px;
|
| 148 |
+
background-color: rgba(0, 0, 0, 0.2);
|
| 149 |
+
}
|
| 150 |
+
|
| 151 |
+
.stone-icon {
|
| 152 |
+
width: 20px;
|
| 153 |
+
height: 20px;
|
| 154 |
+
border-radius: 50%;
|
| 155 |
+
flex-shrink: 0;
|
| 156 |
+
|
| 157 |
+
&.black {
|
| 158 |
+
background-color: #2c2c2c;
|
| 159 |
+
border: 2px solid #fff;
|
| 160 |
+
}
|
| 161 |
+
|
| 162 |
+
&.white {
|
| 163 |
+
background-color: #f0f0f0;
|
| 164 |
+
border: 2px solid #000;
|
| 165 |
+
}
|
| 166 |
+
}
|
| 167 |
+
|
| 168 |
+
.nickname-text {
|
| 169 |
+
font-weight: 600;
|
| 170 |
+
color: #e0e0e0;
|
| 171 |
+
}
|
| 172 |
+
|
| 173 |
+
.edit-icon {
|
| 174 |
+
opacity: 0;
|
| 175 |
+
transition: opacity 0.2s ease;
|
| 176 |
+
font-size: 0.8rem;
|
| 177 |
+
}
|
| 178 |
+
|
| 179 |
+
&.editable:hover .edit-icon {
|
| 180 |
+
opacity: 0.6;
|
| 181 |
+
}
|
| 182 |
+
|
| 183 |
+
.nickname-input {
|
| 184 |
+
flex: 1;
|
| 185 |
+
background-color: rgba(255, 255, 255, 0.1);
|
| 186 |
+
border: 2px solid #60a5fa;
|
| 187 |
+
border-radius: 4px;
|
| 188 |
+
padding: 0.25rem 0.5rem;
|
| 189 |
+
color: #e0e0e0;
|
| 190 |
+
font-weight: 600;
|
| 191 |
+
font-size: 0.95rem;
|
| 192 |
+
outline: none;
|
| 193 |
+
|
| 194 |
+
&.error {
|
| 195 |
+
border-color: #ef4444;
|
| 196 |
+
}
|
| 197 |
+
|
| 198 |
+
&:focus {
|
| 199 |
+
background-color: rgba(255, 255, 255, 0.15);
|
| 200 |
+
}
|
| 201 |
+
}
|
| 202 |
+
|
| 203 |
+
.error-icon {
|
| 204 |
+
font-size: 1rem;
|
| 205 |
+
cursor: help;
|
| 206 |
+
}
|
| 207 |
+
|
| 208 |
+
.edit-info {
|
| 209 |
+
display: flex;
|
| 210 |
+
justify-content: space-between;
|
| 211 |
+
align-items: center;
|
| 212 |
+
padding: 0 0.5rem;
|
| 213 |
+
font-size: 0.75rem;
|
| 214 |
+
}
|
| 215 |
+
|
| 216 |
+
.help-text {
|
| 217 |
+
color: #60a5fa;
|
| 218 |
+
font-style: italic;
|
| 219 |
+
}
|
| 220 |
+
|
| 221 |
+
.char-count {
|
| 222 |
+
color: #9ca3af;
|
| 223 |
+
}
|
| 224 |
+
}
|
| 225 |
+
</style>
|
trigo-web/app/src/components/mcts/MCTSBoardHeatmap.vue
ADDED
|
@@ -0,0 +1,434 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<template>
|
| 2 |
+
<div class="board-heatmap">
|
| 3 |
+
<div v-if="!hasData" class="no-data">
|
| 4 |
+
<p>No data loaded. Please upload files to begin.</p>
|
| 5 |
+
</div>
|
| 6 |
+
|
| 7 |
+
<div v-else class="heatmap-wrapper">
|
| 8 |
+
<!-- SVG Board -->
|
| 9 |
+
<svg
|
| 10 |
+
:width="svgWidth"
|
| 11 |
+
:height="svgHeight"
|
| 12 |
+
@mouseleave="onMouseLeave"
|
| 13 |
+
class="board-svg"
|
| 14 |
+
>
|
| 15 |
+
<!-- Grid lines -->
|
| 16 |
+
<g class="grid-lines">
|
| 17 |
+
<!-- Vertical lines -->
|
| 18 |
+
<line
|
| 19 |
+
v-for="i in shape.x + 1"
|
| 20 |
+
:key="'v' + i"
|
| 21 |
+
:x1="(i - 1) * cellSize + margin"
|
| 22 |
+
:y1="margin"
|
| 23 |
+
:x2="(i - 1) * cellSize + margin"
|
| 24 |
+
:y2="boardHeight + margin"
|
| 25 |
+
stroke="#505050"
|
| 26 |
+
stroke-width="1"
|
| 27 |
+
/>
|
| 28 |
+
<!-- Horizontal lines -->
|
| 29 |
+
<line
|
| 30 |
+
v-for="j in shape.y + 1"
|
| 31 |
+
:key="'h' + j"
|
| 32 |
+
:x1="margin"
|
| 33 |
+
:y1="(j - 1) * cellSize + margin"
|
| 34 |
+
:x2="boardWidth + margin"
|
| 35 |
+
:y2="(j - 1) * cellSize + margin"
|
| 36 |
+
stroke="#505050"
|
| 37 |
+
stroke-width="1"
|
| 38 |
+
/>
|
| 39 |
+
</g>
|
| 40 |
+
|
| 41 |
+
<!-- Heatmap cells -->
|
| 42 |
+
<g class="heatmap-cells">
|
| 43 |
+
<rect
|
| 44 |
+
v-for="stat in statistics"
|
| 45 |
+
:key="stat.actionKey"
|
| 46 |
+
v-show="stat.position"
|
| 47 |
+
:x="stat.position ? stat.position.x * cellSize + margin : 0"
|
| 48 |
+
:y="stat.position ? stat.position.y * cellSize + margin : 0"
|
| 49 |
+
:width="cellSize"
|
| 50 |
+
:height="cellSize"
|
| 51 |
+
:fill="getColor(stat)"
|
| 52 |
+
:opacity="0.7"
|
| 53 |
+
:class="{ selected: isSelected(stat.actionKey) }"
|
| 54 |
+
@mouseenter="onCellHover(stat)"
|
| 55 |
+
@click="onCellClick(stat)"
|
| 56 |
+
class="heatmap-cell"
|
| 57 |
+
/>
|
| 58 |
+
</g>
|
| 59 |
+
|
| 60 |
+
<!-- Stones (current game state) -->
|
| 61 |
+
<g class="stones">
|
| 62 |
+
<circle
|
| 63 |
+
v-for="(stone, idx) in currentStones"
|
| 64 |
+
:key="idx"
|
| 65 |
+
:cx="(stone.x + 0.5) * cellSize + margin"
|
| 66 |
+
:cy="(stone.y + 0.5) * cellSize + margin"
|
| 67 |
+
:r="cellSize * 0.35"
|
| 68 |
+
:fill="stone.color === 1 ? '#070707' : '#f0f0f0'"
|
| 69 |
+
:stroke="stone.color === 1 ? '#202020' : '#d0d0d0'"
|
| 70 |
+
stroke-width="1"
|
| 71 |
+
class="stone"
|
| 72 |
+
/>
|
| 73 |
+
</g>
|
| 74 |
+
|
| 75 |
+
<!-- Last move marker -->
|
| 76 |
+
<g class="last-move-marker" v-if="lastMovePosition">
|
| 77 |
+
<circle
|
| 78 |
+
:cx="(lastMovePosition.x + 0.5) * cellSize + margin"
|
| 79 |
+
:cy="(lastMovePosition.y + 0.5) * cellSize + margin"
|
| 80 |
+
:r="cellSize * 0.15"
|
| 81 |
+
fill="#e94560"
|
| 82 |
+
class="last-move-circle"
|
| 83 |
+
/>
|
| 84 |
+
</g>
|
| 85 |
+
|
| 86 |
+
<!-- Coordinate labels -->
|
| 87 |
+
<g class="coord-labels">
|
| 88 |
+
<!-- Column labels (A, B, C...) -->
|
| 89 |
+
<text
|
| 90 |
+
v-for="i in shape.x"
|
| 91 |
+
:key="'col-' + i"
|
| 92 |
+
:x="(i - 0.5) * cellSize + margin"
|
| 93 |
+
:y="svgHeight - margin / 3"
|
| 94 |
+
text-anchor="middle"
|
| 95 |
+
class="coord-label"
|
| 96 |
+
>
|
| 97 |
+
{{ String.fromCharCode(64 + i) }}
|
| 98 |
+
</text>
|
| 99 |
+
<!-- Row labels (1, 2, 3...) -->
|
| 100 |
+
<text
|
| 101 |
+
v-for="j in shape.y"
|
| 102 |
+
:key="'row-' + j"
|
| 103 |
+
:x="margin / 3"
|
| 104 |
+
:y="(j - 0.5) * cellSize + margin"
|
| 105 |
+
text-anchor="middle"
|
| 106 |
+
dominant-baseline="middle"
|
| 107 |
+
class="coord-label"
|
| 108 |
+
>
|
| 109 |
+
{{ j }}
|
| 110 |
+
</text>
|
| 111 |
+
</g>
|
| 112 |
+
</svg>
|
| 113 |
+
|
| 114 |
+
<!-- Legend -->
|
| 115 |
+
<div class="legend">
|
| 116 |
+
<div class="legend-title">{{ legendTitle }}</div>
|
| 117 |
+
<div class="legend-gradient">
|
| 118 |
+
<div
|
| 119 |
+
v-for="stop in legendStops"
|
| 120 |
+
:key="stop.value"
|
| 121 |
+
:style="{ backgroundColor: stop.color }"
|
| 122 |
+
class="legend-stop"
|
| 123 |
+
></div>
|
| 124 |
+
</div>
|
| 125 |
+
<div class="legend-labels">
|
| 126 |
+
<span class="legend-label-min">{{ minLabel }}</span>
|
| 127 |
+
<span class="legend-label-max">{{ maxLabel }}</span>
|
| 128 |
+
</div>
|
| 129 |
+
</div>
|
| 130 |
+
|
| 131 |
+
<!-- Tooltip -->
|
| 132 |
+
<div
|
| 133 |
+
v-if="hoveredCell"
|
| 134 |
+
class="tooltip"
|
| 135 |
+
:style="{ left: tooltipX + 'px', top: tooltipY + 'px' }"
|
| 136 |
+
>
|
| 137 |
+
<div class="tooltip-position">{{ formatPosition(hoveredCell.position) }}</div>
|
| 138 |
+
<div class="tooltip-stat">N: {{ hoveredCell.N }}</div>
|
| 139 |
+
<div class="tooltip-stat">π: {{ (hoveredCell.pi * 100).toFixed(2) }}%</div>
|
| 140 |
+
</div>
|
| 141 |
+
</div>
|
| 142 |
+
</div>
|
| 143 |
+
</template>
|
| 144 |
+
|
| 145 |
+
<script setup lang="ts">
|
| 146 |
+
import { ref, computed, watch } from "vue";
|
| 147 |
+
import { useMCTSStore } from "@/stores/mctsStore";
|
| 148 |
+
import type { MCTSMoveStatistic } from "@/types/mcts";
|
| 149 |
+
import { getColorForStatistic, generateLegendStops, getStatisticLabel } from "@/utils/mctsColorScale";
|
| 150 |
+
import { formatPosition } from "@/utils/mctsDataParser";
|
| 151 |
+
import { StepType } from "../../../../inc/trigo/game";
|
| 152 |
+
|
| 153 |
+
const mctsStore = useMCTSStore();
|
| 154 |
+
|
| 155 |
+
// Constants
|
| 156 |
+
const cellSize = 50;
|
| 157 |
+
const margin = 40;
|
| 158 |
+
|
| 159 |
+
// Computed
|
| 160 |
+
const hasData = computed(() => mctsStore.hasData);
|
| 161 |
+
|
| 162 |
+
const currentMoveData = computed(() => mctsStore.currentMoveData);
|
| 163 |
+
|
| 164 |
+
const statistics = computed(() => mctsStore.currentStatistics);
|
| 165 |
+
|
| 166 |
+
const shape = computed(() => {
|
| 167 |
+
if (!currentMoveData.value) return { x: 5, y: 5, z: 1 };
|
| 168 |
+
return currentMoveData.value.gameState.getShape();
|
| 169 |
+
});
|
| 170 |
+
|
| 171 |
+
const boardWidth = computed(() => shape.value.x * cellSize);
|
| 172 |
+
const boardHeight = computed(() => shape.value.y * cellSize);
|
| 173 |
+
const svgWidth = computed(() => boardWidth.value + margin * 2);
|
| 174 |
+
const svgHeight = computed(() => boardHeight.value + margin * 2);
|
| 175 |
+
|
| 176 |
+
const currentStones = computed(() => {
|
| 177 |
+
if (!currentMoveData.value) return [];
|
| 178 |
+
|
| 179 |
+
const board = currentMoveData.value.gameState.getBoard();
|
| 180 |
+
const stones: Array<{ x: number; y: number; color: number }> = [];
|
| 181 |
+
|
| 182 |
+
for (let x = 0; x < shape.value.x; x++) {
|
| 183 |
+
for (let y = 0; y < shape.value.y; y++) {
|
| 184 |
+
const color = board[x][y][0]; // z=0 for 2D boards
|
| 185 |
+
if (color !== 0) {
|
| 186 |
+
stones.push({ x, y, color });
|
| 187 |
+
}
|
| 188 |
+
}
|
| 189 |
+
}
|
| 190 |
+
|
| 191 |
+
return stones;
|
| 192 |
+
});
|
| 193 |
+
|
| 194 |
+
// Get last move position for highlighting
|
| 195 |
+
const lastMovePosition = computed(() => {
|
| 196 |
+
if (!currentMoveData.value || currentMoveData.value.moveNumber === 0) return null;
|
| 197 |
+
|
| 198 |
+
const history = currentMoveData.value.gameState.getHistory();
|
| 199 |
+
if (history.length === 0) return null;
|
| 200 |
+
|
| 201 |
+
const lastMove = history[history.length - 1];
|
| 202 |
+
|
| 203 |
+
// Skip pass moves or moves without position
|
| 204 |
+
if (lastMove.type !== StepType.DROP || !lastMove.position) return null;
|
| 205 |
+
|
| 206 |
+
// Return position for 2D boards (z=0)
|
| 207 |
+
return {
|
| 208 |
+
x: lastMove.position.x,
|
| 209 |
+
y: lastMove.position.y,
|
| 210 |
+
z: 0
|
| 211 |
+
};
|
| 212 |
+
});
|
| 213 |
+
|
| 214 |
+
// Color scale
|
| 215 |
+
const minValue = computed(() => {
|
| 216 |
+
if (statistics.value.length === 0) return 0;
|
| 217 |
+
const values = statistics.value.map((s) => {
|
| 218 |
+
return mctsStore.selectedStatistic === "N" ? s.N : s.pi;
|
| 219 |
+
});
|
| 220 |
+
return Math.min(...values);
|
| 221 |
+
});
|
| 222 |
+
|
| 223 |
+
const maxValue = computed(() => {
|
| 224 |
+
if (statistics.value.length === 0) return 1;
|
| 225 |
+
const values = statistics.value.map((s) => {
|
| 226 |
+
return mctsStore.selectedStatistic === "N" ? s.N : s.pi;
|
| 227 |
+
});
|
| 228 |
+
return Math.max(...values);
|
| 229 |
+
});
|
| 230 |
+
|
| 231 |
+
// Legend
|
| 232 |
+
const legendTitle = computed(() => getStatisticLabel(mctsStore.selectedStatistic));
|
| 233 |
+
|
| 234 |
+
const legendStops = computed(() => {
|
| 235 |
+
return generateLegendStops(mctsStore.selectedStatistic, minValue.value, maxValue.value, 5);
|
| 236 |
+
});
|
| 237 |
+
|
| 238 |
+
const minLabel = computed(() => {
|
| 239 |
+
if (mctsStore.selectedStatistic === "pi") {
|
| 240 |
+
return `${(minValue.value * 100).toFixed(1)}%`;
|
| 241 |
+
}
|
| 242 |
+
return Math.round(minValue.value).toString();
|
| 243 |
+
});
|
| 244 |
+
|
| 245 |
+
const maxLabel = computed(() => {
|
| 246 |
+
if (mctsStore.selectedStatistic === "pi") {
|
| 247 |
+
return `${(maxValue.value * 100).toFixed(1)}%`;
|
| 248 |
+
}
|
| 249 |
+
return Math.round(maxValue.value).toString();
|
| 250 |
+
});
|
| 251 |
+
|
| 252 |
+
// Hover state
|
| 253 |
+
const hoveredCell = ref<MCTSMoveStatistic | null>(null);
|
| 254 |
+
const tooltipX = ref(0);
|
| 255 |
+
const tooltipY = ref(0);
|
| 256 |
+
|
| 257 |
+
// Functions
|
| 258 |
+
function getColor(stat: MCTSMoveStatistic): string {
|
| 259 |
+
const value = mctsStore.selectedStatistic === "N" ? stat.N : stat.pi;
|
| 260 |
+
return getColorForStatistic(
|
| 261 |
+
value,
|
| 262 |
+
mctsStore.selectedStatistic,
|
| 263 |
+
minValue.value,
|
| 264 |
+
maxValue.value,
|
| 265 |
+
mctsStore.colorScale
|
| 266 |
+
);
|
| 267 |
+
}
|
| 268 |
+
|
| 269 |
+
function isSelected(actionKey: string): boolean {
|
| 270 |
+
return mctsStore.selectedActionKey === actionKey;
|
| 271 |
+
}
|
| 272 |
+
|
| 273 |
+
function onCellHover(stat: MCTSMoveStatistic) {
|
| 274 |
+
hoveredCell.value = stat;
|
| 275 |
+
// Tooltip position will be updated via mouse position
|
| 276 |
+
}
|
| 277 |
+
|
| 278 |
+
function onCellClick(stat: MCTSMoveStatistic) {
|
| 279 |
+
mctsStore.selectAction(stat.actionKey);
|
| 280 |
+
}
|
| 281 |
+
|
| 282 |
+
function onMouseLeave() {
|
| 283 |
+
hoveredCell.value = null;
|
| 284 |
+
}
|
| 285 |
+
|
| 286 |
+
// Track mouse position for tooltip
|
| 287 |
+
function onMouseMove(event: MouseEvent) {
|
| 288 |
+
tooltipX.value = event.clientX + 10;
|
| 289 |
+
tooltipY.value = event.clientY + 10;
|
| 290 |
+
}
|
| 291 |
+
|
| 292 |
+
// Set up mouse tracking
|
| 293 |
+
if (typeof window !== "undefined") {
|
| 294 |
+
window.addEventListener("mousemove", onMouseMove);
|
| 295 |
+
}
|
| 296 |
+
</script>
|
| 297 |
+
|
| 298 |
+
<style scoped lang="scss">
|
| 299 |
+
.board-heatmap {
|
| 300 |
+
width: 100%;
|
| 301 |
+
height: 100%;
|
| 302 |
+
display: flex;
|
| 303 |
+
flex-direction: column;
|
| 304 |
+
align-items: center;
|
| 305 |
+
justify-content: center;
|
| 306 |
+
}
|
| 307 |
+
|
| 308 |
+
.no-data {
|
| 309 |
+
display: flex;
|
| 310 |
+
justify-content: center;
|
| 311 |
+
align-items: center;
|
| 312 |
+
height: 100%;
|
| 313 |
+
color: #606060;
|
| 314 |
+
font-size: 14px;
|
| 315 |
+
}
|
| 316 |
+
|
| 317 |
+
.heatmap-wrapper {
|
| 318 |
+
display: flex;
|
| 319 |
+
flex-direction: column;
|
| 320 |
+
align-items: center;
|
| 321 |
+
gap: 20px;
|
| 322 |
+
max-width: 100%;
|
| 323 |
+
max-height: 100%;
|
| 324 |
+
}
|
| 325 |
+
|
| 326 |
+
.board-svg {
|
| 327 |
+
border-radius: 4px;
|
| 328 |
+
}
|
| 329 |
+
|
| 330 |
+
.heatmap-cell {
|
| 331 |
+
cursor: pointer;
|
| 332 |
+
transition: opacity 0.2s;
|
| 333 |
+
|
| 334 |
+
&:hover {
|
| 335 |
+
opacity: 0.9 !important;
|
| 336 |
+
stroke: #4a90e2;
|
| 337 |
+
stroke-width: 2;
|
| 338 |
+
}
|
| 339 |
+
|
| 340 |
+
&.selected {
|
| 341 |
+
stroke: #e94560;
|
| 342 |
+
stroke-width: 3;
|
| 343 |
+
}
|
| 344 |
+
}
|
| 345 |
+
|
| 346 |
+
.stone {
|
| 347 |
+
pointer-events: none;
|
| 348 |
+
}
|
| 349 |
+
|
| 350 |
+
.last-move-circle {
|
| 351 |
+
pointer-events: none;
|
| 352 |
+
animation: pulse 1.5s ease-in-out infinite;
|
| 353 |
+
}
|
| 354 |
+
|
| 355 |
+
@keyframes pulse {
|
| 356 |
+
0%, 100% {
|
| 357 |
+
opacity: 1;
|
| 358 |
+
}
|
| 359 |
+
50% {
|
| 360 |
+
opacity: 0.4;
|
| 361 |
+
}
|
| 362 |
+
}
|
| 363 |
+
|
| 364 |
+
.coord-label {
|
| 365 |
+
fill: #808080;
|
| 366 |
+
font-size: 11px;
|
| 367 |
+
user-select: none;
|
| 368 |
+
}
|
| 369 |
+
|
| 370 |
+
/* ============================================================================
|
| 371 |
+
Legend
|
| 372 |
+
============================================================================ */
|
| 373 |
+
|
| 374 |
+
.legend {
|
| 375 |
+
display: flex;
|
| 376 |
+
flex-direction: column;
|
| 377 |
+
gap: 8px;
|
| 378 |
+
width: 300px;
|
| 379 |
+
}
|
| 380 |
+
|
| 381 |
+
.legend-title {
|
| 382 |
+
font-size: 13px;
|
| 383 |
+
font-weight: 500;
|
| 384 |
+
color: #c0c0c0;
|
| 385 |
+
text-align: center;
|
| 386 |
+
}
|
| 387 |
+
|
| 388 |
+
.legend-gradient {
|
| 389 |
+
display: flex;
|
| 390 |
+
height: 20px;
|
| 391 |
+
border-radius: 3px;
|
| 392 |
+
overflow: hidden;
|
| 393 |
+
border: 1px solid #404040;
|
| 394 |
+
}
|
| 395 |
+
|
| 396 |
+
.legend-stop {
|
| 397 |
+
flex: 1;
|
| 398 |
+
}
|
| 399 |
+
|
| 400 |
+
.legend-labels {
|
| 401 |
+
display: flex;
|
| 402 |
+
justify-content: space-between;
|
| 403 |
+
font-size: 11px;
|
| 404 |
+
color: #808080;
|
| 405 |
+
}
|
| 406 |
+
|
| 407 |
+
/* ============================================================================
|
| 408 |
+
Tooltip
|
| 409 |
+
============================================================================ */
|
| 410 |
+
|
| 411 |
+
.tooltip {
|
| 412 |
+
position: fixed;
|
| 413 |
+
background-color: rgba(0, 0, 0, 0.9);
|
| 414 |
+
border: 1px solid #4a90e2;
|
| 415 |
+
border-radius: 4px;
|
| 416 |
+
padding: 8px 12px;
|
| 417 |
+
font-size: 12px;
|
| 418 |
+
color: #e0e0e0;
|
| 419 |
+
pointer-events: none;
|
| 420 |
+
z-index: 1000;
|
| 421 |
+
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3);
|
| 422 |
+
}
|
| 423 |
+
|
| 424 |
+
.tooltip-position {
|
| 425 |
+
font-weight: 600;
|
| 426 |
+
color: #4a90e2;
|
| 427 |
+
margin-bottom: 4px;
|
| 428 |
+
}
|
| 429 |
+
|
| 430 |
+
.tooltip-stat {
|
| 431 |
+
color: #c0c0c0;
|
| 432 |
+
line-height: 1.4;
|
| 433 |
+
}
|
| 434 |
+
</style>
|
trigo-web/app/src/components/mcts/MCTSDataLoader.vue
ADDED
|
@@ -0,0 +1,283 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<template>
|
| 2 |
+
<div class="mcts-data-loader">
|
| 3 |
+
<div class="file-inputs">
|
| 4 |
+
<!-- TGN File Input -->
|
| 5 |
+
<div class="file-input-group">
|
| 6 |
+
<label for="tgn-file" class="file-label">
|
| 7 |
+
TGN Game File
|
| 8 |
+
<span class="file-status" :class="{ ready: tgnFile }">
|
| 9 |
+
{{ tgnFile ? `✓ ${tgnFile.name}` : "Not selected" }}
|
| 10 |
+
</span>
|
| 11 |
+
</label>
|
| 12 |
+
<input
|
| 13 |
+
id="tgn-file"
|
| 14 |
+
type="file"
|
| 15 |
+
accept=".tgn"
|
| 16 |
+
@change="onTGNFileChange"
|
| 17 |
+
class="file-input"
|
| 18 |
+
/>
|
| 19 |
+
</div>
|
| 20 |
+
|
| 21 |
+
<!-- Visit Counts JSON Input -->
|
| 22 |
+
<div class="file-input-group">
|
| 23 |
+
<label for="json-file" class="file-label">
|
| 24 |
+
Visit Counts JSON
|
| 25 |
+
<span class="file-status" :class="{ ready: jsonFile }">
|
| 26 |
+
{{ jsonFile ? `✓ ${jsonFile.name}` : "Not selected" }}
|
| 27 |
+
</span>
|
| 28 |
+
</label>
|
| 29 |
+
<input
|
| 30 |
+
id="json-file"
|
| 31 |
+
type="file"
|
| 32 |
+
accept=".json"
|
| 33 |
+
@change="onJSONFileChange"
|
| 34 |
+
class="file-input"
|
| 35 |
+
/>
|
| 36 |
+
</div>
|
| 37 |
+
</div>
|
| 38 |
+
|
| 39 |
+
<!-- Load Button -->
|
| 40 |
+
<button
|
| 41 |
+
@click="loadData"
|
| 42 |
+
:disabled="!canLoad || loading"
|
| 43 |
+
class="btn-load"
|
| 44 |
+
:class="{ loading }"
|
| 45 |
+
>
|
| 46 |
+
{{ loading ? "Loading..." : "Load Data" }}
|
| 47 |
+
</button>
|
| 48 |
+
|
| 49 |
+
<!-- Status Messages -->
|
| 50 |
+
<div v-if="loading" class="status-message loading">
|
| 51 |
+
<div class="spinner"></div>
|
| 52 |
+
<span>Parsing data...</span>
|
| 53 |
+
</div>
|
| 54 |
+
|
| 55 |
+
<div v-if="error" class="status-message error">
|
| 56 |
+
<strong>Error:</strong> {{ error }}
|
| 57 |
+
</div>
|
| 58 |
+
|
| 59 |
+
<div v-if="success" class="status-message success">
|
| 60 |
+
<strong>Success:</strong> Loaded {{ movesCount }} moves from a {{ boardShape }} board
|
| 61 |
+
</div>
|
| 62 |
+
</div>
|
| 63 |
+
</template>
|
| 64 |
+
|
| 65 |
+
<script setup lang="ts">
|
| 66 |
+
import { ref, computed } from "vue";
|
| 67 |
+
import { useMCTSStore } from "@/stores/mctsStore";
|
| 68 |
+
import { parseMCTSData } from "@/utils/mctsDataParser";
|
| 69 |
+
|
| 70 |
+
const mctsStore = useMCTSStore();
|
| 71 |
+
|
| 72 |
+
// File state
|
| 73 |
+
const tgnFile = ref<File | null>(null);
|
| 74 |
+
const jsonFile = ref<File | null>(null);
|
| 75 |
+
|
| 76 |
+
// Loading state
|
| 77 |
+
const loading = ref(false);
|
| 78 |
+
const error = ref<string | null>(null);
|
| 79 |
+
const success = ref(false);
|
| 80 |
+
|
| 81 |
+
// Success data
|
| 82 |
+
const movesCount = ref(0);
|
| 83 |
+
const boardShape = ref("");
|
| 84 |
+
|
| 85 |
+
// Computed
|
| 86 |
+
const canLoad = computed(() => tgnFile.value !== null && jsonFile.value !== null);
|
| 87 |
+
|
| 88 |
+
// Handlers
|
| 89 |
+
function onTGNFileChange(event: Event) {
|
| 90 |
+
const target = event.target as HTMLInputElement;
|
| 91 |
+
tgnFile.value = target.files?.[0] || null;
|
| 92 |
+
clearMessages();
|
| 93 |
+
}
|
| 94 |
+
|
| 95 |
+
function onJSONFileChange(event: Event) {
|
| 96 |
+
const target = event.target as HTMLInputElement;
|
| 97 |
+
jsonFile.value = target.files?.[0] || null;
|
| 98 |
+
clearMessages();
|
| 99 |
+
}
|
| 100 |
+
|
| 101 |
+
function clearMessages() {
|
| 102 |
+
error.value = null;
|
| 103 |
+
success.value = false;
|
| 104 |
+
}
|
| 105 |
+
|
| 106 |
+
async function loadData() {
|
| 107 |
+
if (!canLoad.value) return;
|
| 108 |
+
|
| 109 |
+
loading.value = true;
|
| 110 |
+
clearMessages();
|
| 111 |
+
|
| 112 |
+
try {
|
| 113 |
+
// Read files
|
| 114 |
+
const tgnContent = await tgnFile.value!.text();
|
| 115 |
+
const jsonContent = await jsonFile.value!.text();
|
| 116 |
+
|
| 117 |
+
// Parse data
|
| 118 |
+
const data = await parseMCTSData(tgnContent, jsonContent);
|
| 119 |
+
|
| 120 |
+
// Store data
|
| 121 |
+
mctsStore.setGameHistory(data);
|
| 122 |
+
|
| 123 |
+
// Show success
|
| 124 |
+
success.value = true;
|
| 125 |
+
movesCount.value = data.length;
|
| 126 |
+
const shape = data[0].gameState.getShape();
|
| 127 |
+
boardShape.value = `${shape.x}×${shape.y}×${shape.z}`;
|
| 128 |
+
|
| 129 |
+
console.log(`[MCTS Loader] Successfully loaded ${data.length} moves`);
|
| 130 |
+
} catch (err) {
|
| 131 |
+
error.value = err instanceof Error ? err.message : String(err);
|
| 132 |
+
console.error("[MCTS Loader] Error loading data:", err);
|
| 133 |
+
} finally {
|
| 134 |
+
loading.value = false;
|
| 135 |
+
}
|
| 136 |
+
}
|
| 137 |
+
</script>
|
| 138 |
+
|
| 139 |
+
<style scoped lang="scss">
|
| 140 |
+
.mcts-data-loader {
|
| 141 |
+
display: flex;
|
| 142 |
+
flex-direction: column;
|
| 143 |
+
gap: 15px;
|
| 144 |
+
}
|
| 145 |
+
|
| 146 |
+
.file-inputs {
|
| 147 |
+
display: flex;
|
| 148 |
+
flex-direction: column;
|
| 149 |
+
gap: 12px;
|
| 150 |
+
}
|
| 151 |
+
|
| 152 |
+
.file-input-group {
|
| 153 |
+
display: flex;
|
| 154 |
+
flex-direction: column;
|
| 155 |
+
gap: 6px;
|
| 156 |
+
}
|
| 157 |
+
|
| 158 |
+
.file-label {
|
| 159 |
+
font-size: 13px;
|
| 160 |
+
font-weight: 500;
|
| 161 |
+
color: #c0c0c0;
|
| 162 |
+
display: flex;
|
| 163 |
+
justify-content: space-between;
|
| 164 |
+
align-items: center;
|
| 165 |
+
}
|
| 166 |
+
|
| 167 |
+
.file-status {
|
| 168 |
+
font-size: 11px;
|
| 169 |
+
color: #808080;
|
| 170 |
+
font-weight: normal;
|
| 171 |
+
|
| 172 |
+
&.ready {
|
| 173 |
+
color: #4a90e2;
|
| 174 |
+
}
|
| 175 |
+
}
|
| 176 |
+
|
| 177 |
+
.file-input {
|
| 178 |
+
padding: 8px;
|
| 179 |
+
background-color: #1a1a1a;
|
| 180 |
+
border: 1px solid #404040;
|
| 181 |
+
border-radius: 4px;
|
| 182 |
+
color: #e0e0e0;
|
| 183 |
+
font-size: 13px;
|
| 184 |
+
cursor: pointer;
|
| 185 |
+
|
| 186 |
+
&:hover {
|
| 187 |
+
border-color: #4a90e2;
|
| 188 |
+
}
|
| 189 |
+
|
| 190 |
+
&:focus {
|
| 191 |
+
outline: none;
|
| 192 |
+
border-color: #4a90e2;
|
| 193 |
+
}
|
| 194 |
+
|
| 195 |
+
&::file-selector-button {
|
| 196 |
+
padding: 6px 12px;
|
| 197 |
+
background-color: #2a2a2a;
|
| 198 |
+
border: 1px solid #404040;
|
| 199 |
+
border-radius: 3px;
|
| 200 |
+
color: #e0e0e0;
|
| 201 |
+
font-size: 12px;
|
| 202 |
+
cursor: pointer;
|
| 203 |
+
margin-right: 10px;
|
| 204 |
+
|
| 205 |
+
&:hover {
|
| 206 |
+
background-color: #3a3a3a;
|
| 207 |
+
border-color: #4a90e2;
|
| 208 |
+
}
|
| 209 |
+
}
|
| 210 |
+
}
|
| 211 |
+
|
| 212 |
+
.btn-load {
|
| 213 |
+
padding: 12px 20px;
|
| 214 |
+
background-color: #4a90e2;
|
| 215 |
+
border: none;
|
| 216 |
+
border-radius: 4px;
|
| 217 |
+
color: #ffffff;
|
| 218 |
+
font-size: 14px;
|
| 219 |
+
font-weight: 500;
|
| 220 |
+
cursor: pointer;
|
| 221 |
+
transition: background-color 0.2s, opacity 0.2s;
|
| 222 |
+
|
| 223 |
+
&:hover:not(:disabled) {
|
| 224 |
+
background-color: #357abd;
|
| 225 |
+
}
|
| 226 |
+
|
| 227 |
+
&:active:not(:disabled) {
|
| 228 |
+
background-color: #2a6ba8;
|
| 229 |
+
}
|
| 230 |
+
|
| 231 |
+
&:disabled {
|
| 232 |
+
opacity: 0.5;
|
| 233 |
+
cursor: not-allowed;
|
| 234 |
+
}
|
| 235 |
+
|
| 236 |
+
&.loading {
|
| 237 |
+
opacity: 0.7;
|
| 238 |
+
}
|
| 239 |
+
}
|
| 240 |
+
|
| 241 |
+
.status-message {
|
| 242 |
+
padding: 10px;
|
| 243 |
+
border-radius: 4px;
|
| 244 |
+
font-size: 13px;
|
| 245 |
+
display: flex;
|
| 246 |
+
align-items: center;
|
| 247 |
+
gap: 10px;
|
| 248 |
+
|
| 249 |
+
&.loading {
|
| 250 |
+
background-color: #2a3a4a;
|
| 251 |
+
color: #4a90e2;
|
| 252 |
+
}
|
| 253 |
+
|
| 254 |
+
&.error {
|
| 255 |
+
background-color: #4a2a2a;
|
| 256 |
+
color: #ff6b6b;
|
| 257 |
+
}
|
| 258 |
+
|
| 259 |
+
&.success {
|
| 260 |
+
background-color: #2a4a2a;
|
| 261 |
+
color: #6bff6b;
|
| 262 |
+
}
|
| 263 |
+
|
| 264 |
+
strong {
|
| 265 |
+
font-weight: 600;
|
| 266 |
+
}
|
| 267 |
+
}
|
| 268 |
+
|
| 269 |
+
.spinner {
|
| 270 |
+
width: 16px;
|
| 271 |
+
height: 16px;
|
| 272 |
+
border: 2px solid #4a90e2;
|
| 273 |
+
border-top-color: transparent;
|
| 274 |
+
border-radius: 50%;
|
| 275 |
+
animation: spin 0.8s linear infinite;
|
| 276 |
+
}
|
| 277 |
+
|
| 278 |
+
@keyframes spin {
|
| 279 |
+
to {
|
| 280 |
+
transform: rotate(360deg);
|
| 281 |
+
}
|
| 282 |
+
}
|
| 283 |
+
</style>
|
trigo-web/app/src/components/mcts/MCTSMoveNavigation.vue
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<template>
|
| 2 |
+
<div class="mcts-move-navigation">
|
| 3 |
+
<div class="nav-buttons">
|
| 4 |
+
<button @click="mctsStore.firstMove()" :disabled="isFirst" class="nav-btn">⏮ First</button>
|
| 5 |
+
<button @click="mctsStore.prevMove()" :disabled="isFirst" class="nav-btn">◀ Prev</button>
|
| 6 |
+
<button @click="mctsStore.nextMove()" :disabled="isLast" class="nav-btn">Next ▶</button>
|
| 7 |
+
<button @click="mctsStore.lastMove()" :disabled="isLast" class="nav-btn">Last ⏭</button>
|
| 8 |
+
</div>
|
| 9 |
+
|
| 10 |
+
<div class="move-slider">
|
| 11 |
+
<input
|
| 12 |
+
type="range"
|
| 13 |
+
:min="0"
|
| 14 |
+
:max="maxMove"
|
| 15 |
+
:value="currentMove"
|
| 16 |
+
@input="onSliderChange"
|
| 17 |
+
class="slider"
|
| 18 |
+
/>
|
| 19 |
+
<span class="move-label">Move {{ currentMove + 1 }} / {{ maxMove + 1 }}</span>
|
| 20 |
+
</div>
|
| 21 |
+
|
| 22 |
+
<div class="move-info">
|
| 23 |
+
<span>Player: <strong>{{ currentPlayer }}</strong></span>
|
| 24 |
+
</div>
|
| 25 |
+
</div>
|
| 26 |
+
</template>
|
| 27 |
+
|
| 28 |
+
<script setup lang="ts">
|
| 29 |
+
import { computed } from "vue";
|
| 30 |
+
import { useMCTSStore } from "@/stores/mctsStore";
|
| 31 |
+
|
| 32 |
+
const mctsStore = useMCTSStore();
|
| 33 |
+
|
| 34 |
+
const currentMove = computed(() => mctsStore.currentMoveIndex);
|
| 35 |
+
const maxMove = computed(() => mctsStore.maxMoveIndex);
|
| 36 |
+
const isFirst = computed(() => currentMove.value === 0);
|
| 37 |
+
const isLast = computed(() => currentMove.value === maxMove.value);
|
| 38 |
+
|
| 39 |
+
const currentPlayer = computed(() => {
|
| 40 |
+
const moveData = mctsStore.currentMoveData;
|
| 41 |
+
return moveData ? moveData.player : "-";
|
| 42 |
+
});
|
| 43 |
+
|
| 44 |
+
function onSliderChange(event: Event) {
|
| 45 |
+
const target = event.target as HTMLInputElement;
|
| 46 |
+
mctsStore.setCurrentMoveIndex(parseInt(target.value, 10));
|
| 47 |
+
}
|
| 48 |
+
</script>
|
| 49 |
+
|
| 50 |
+
<style scoped lang="scss">
|
| 51 |
+
.mcts-move-navigation {
|
| 52 |
+
display: flex;
|
| 53 |
+
flex-direction: column;
|
| 54 |
+
gap: 15px;
|
| 55 |
+
}
|
| 56 |
+
|
| 57 |
+
.nav-buttons {
|
| 58 |
+
display: grid;
|
| 59 |
+
grid-template-columns: 1fr 1fr;
|
| 60 |
+
gap: 8px;
|
| 61 |
+
}
|
| 62 |
+
|
| 63 |
+
.nav-btn {
|
| 64 |
+
padding: 8px 12px;
|
| 65 |
+
background-color: #3a3a3a;
|
| 66 |
+
border: 1px solid #505050;
|
| 67 |
+
border-radius: 4px;
|
| 68 |
+
color: #e0e0e0;
|
| 69 |
+
font-size: 12px;
|
| 70 |
+
cursor: pointer;
|
| 71 |
+
transition: all 0.2s;
|
| 72 |
+
|
| 73 |
+
&:hover:not(:disabled) {
|
| 74 |
+
background-color: #4a4a4a;
|
| 75 |
+
border-color: #4a90e2;
|
| 76 |
+
}
|
| 77 |
+
|
| 78 |
+
&:active:not(:disabled) {
|
| 79 |
+
background-color: #2a2a2a;
|
| 80 |
+
}
|
| 81 |
+
|
| 82 |
+
&:disabled {
|
| 83 |
+
opacity: 0.4;
|
| 84 |
+
cursor: not-allowed;
|
| 85 |
+
}
|
| 86 |
+
}
|
| 87 |
+
|
| 88 |
+
.move-slider {
|
| 89 |
+
display: flex;
|
| 90 |
+
flex-direction: column;
|
| 91 |
+
gap: 8px;
|
| 92 |
+
}
|
| 93 |
+
|
| 94 |
+
.slider {
|
| 95 |
+
width: 100%;
|
| 96 |
+
height: 6px;
|
| 97 |
+
border-radius: 3px;
|
| 98 |
+
background-color: #3a3a3a;
|
| 99 |
+
outline: none;
|
| 100 |
+
-webkit-appearance: none;
|
| 101 |
+
|
| 102 |
+
&::-webkit-slider-thumb {
|
| 103 |
+
-webkit-appearance: none;
|
| 104 |
+
appearance: none;
|
| 105 |
+
width: 16px;
|
| 106 |
+
height: 16px;
|
| 107 |
+
border-radius: 50%;
|
| 108 |
+
background-color: #4a90e2;
|
| 109 |
+
cursor: pointer;
|
| 110 |
+
|
| 111 |
+
&:hover {
|
| 112 |
+
background-color: #357abd;
|
| 113 |
+
}
|
| 114 |
+
}
|
| 115 |
+
|
| 116 |
+
&::-moz-range-thumb {
|
| 117 |
+
width: 16px;
|
| 118 |
+
height: 16px;
|
| 119 |
+
border-radius: 50%;
|
| 120 |
+
background-color: #4a90e2;
|
| 121 |
+
cursor: pointer;
|
| 122 |
+
border: none;
|
| 123 |
+
|
| 124 |
+
&:hover {
|
| 125 |
+
background-color: #357abd;
|
| 126 |
+
}
|
| 127 |
+
}
|
| 128 |
+
}
|
| 129 |
+
|
| 130 |
+
.move-label {
|
| 131 |
+
font-size: 13px;
|
| 132 |
+
color: #c0c0c0;
|
| 133 |
+
text-align: center;
|
| 134 |
+
}
|
| 135 |
+
|
| 136 |
+
.move-info {
|
| 137 |
+
padding: 8px;
|
| 138 |
+
background-color: #1a1a1a;
|
| 139 |
+
border-radius: 4px;
|
| 140 |
+
font-size: 12px;
|
| 141 |
+
color: #a0a0a0;
|
| 142 |
+
|
| 143 |
+
strong {
|
| 144 |
+
color: #e0e0e0;
|
| 145 |
+
text-transform: capitalize;
|
| 146 |
+
}
|
| 147 |
+
}
|
| 148 |
+
</style>
|
trigo-web/app/src/components/mcts/MCTSStatisticsPanel.vue
ADDED
|
@@ -0,0 +1,221 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<template>
|
| 2 |
+
<div class="mcts-statistics-panel">
|
| 3 |
+
<div v-if="!hasData" class="no-data">No data</div>
|
| 4 |
+
|
| 5 |
+
<div v-else class="stats-content">
|
| 6 |
+
<div class="stats-summary">
|
| 7 |
+
<div class="stat-item">
|
| 8 |
+
<span class="stat-label">Total visits:</span>
|
| 9 |
+
<span class="stat-value">{{ totalVisits }}</span>
|
| 10 |
+
</div>
|
| 11 |
+
</div>
|
| 12 |
+
|
| 13 |
+
<div class="stats-options">
|
| 14 |
+
<label class="option-label">Display:</label>
|
| 15 |
+
<div class="radio-group">
|
| 16 |
+
<label class="radio-option">
|
| 17 |
+
<input
|
| 18 |
+
type="radio"
|
| 19 |
+
value="N"
|
| 20 |
+
:checked="selectedStat === 'N'"
|
| 21 |
+
@change="onStatChange"
|
| 22 |
+
/>
|
| 23 |
+
<span>Visit Count (N)</span>
|
| 24 |
+
</label>
|
| 25 |
+
<label class="radio-option">
|
| 26 |
+
<input
|
| 27 |
+
type="radio"
|
| 28 |
+
value="pi"
|
| 29 |
+
:checked="selectedStat === 'pi'"
|
| 30 |
+
@change="onStatChange"
|
| 31 |
+
/>
|
| 32 |
+
<span>Policy (π)</span>
|
| 33 |
+
</label>
|
| 34 |
+
</div>
|
| 35 |
+
</div>
|
| 36 |
+
|
| 37 |
+
<div class="top-moves-table">
|
| 38 |
+
<table>
|
| 39 |
+
<thead>
|
| 40 |
+
<tr>
|
| 41 |
+
<th>#</th>
|
| 42 |
+
<th>Pos</th>
|
| 43 |
+
<th>N</th>
|
| 44 |
+
<th>%</th>
|
| 45 |
+
<th>π</th>
|
| 46 |
+
</tr>
|
| 47 |
+
</thead>
|
| 48 |
+
<tbody>
|
| 49 |
+
<tr
|
| 50 |
+
v-for="(move, idx) in topMoves"
|
| 51 |
+
:key="move.actionKey"
|
| 52 |
+
:class="{ selected: isSelected(move.actionKey) }"
|
| 53 |
+
@click="onMoveClick(move.actionKey)"
|
| 54 |
+
class="move-row"
|
| 55 |
+
>
|
| 56 |
+
<td>{{ idx + 1 }}</td>
|
| 57 |
+
<td>{{ formatPosition(move.position) }}</td>
|
| 58 |
+
<td>{{ move.N }}</td>
|
| 59 |
+
<td>{{ ((move.N / totalVisits) * 100).toFixed(1) }}%</td>
|
| 60 |
+
<td>{{ move.pi.toFixed(3) }}</td>
|
| 61 |
+
</tr>
|
| 62 |
+
</tbody>
|
| 63 |
+
</table>
|
| 64 |
+
</div>
|
| 65 |
+
</div>
|
| 66 |
+
</div>
|
| 67 |
+
</template>
|
| 68 |
+
|
| 69 |
+
<script setup lang="ts">
|
| 70 |
+
import { computed } from "vue";
|
| 71 |
+
import { useMCTSStore } from "@/stores/mctsStore";
|
| 72 |
+
import { formatPosition } from "@/utils/mctsDataParser";
|
| 73 |
+
import type { MCTSStatisticType } from "@/types/mcts";
|
| 74 |
+
|
| 75 |
+
const mctsStore = useMCTSStore();
|
| 76 |
+
|
| 77 |
+
const hasData = computed(() => mctsStore.hasData);
|
| 78 |
+
const totalVisits = computed(() => mctsStore.totalVisits);
|
| 79 |
+
const topMoves = computed(() => mctsStore.topMoves);
|
| 80 |
+
const selectedStat = computed(() => mctsStore.selectedStatistic);
|
| 81 |
+
|
| 82 |
+
function isSelected(actionKey: string): boolean {
|
| 83 |
+
return mctsStore.selectedActionKey === actionKey;
|
| 84 |
+
}
|
| 85 |
+
|
| 86 |
+
function onMoveClick(actionKey: string) {
|
| 87 |
+
mctsStore.selectAction(actionKey);
|
| 88 |
+
}
|
| 89 |
+
|
| 90 |
+
function onStatChange(event: Event) {
|
| 91 |
+
const target = event.target as HTMLInputElement;
|
| 92 |
+
mctsStore.setSelectedStatistic(target.value as MCTSStatisticType);
|
| 93 |
+
}
|
| 94 |
+
</script>
|
| 95 |
+
|
| 96 |
+
<style scoped lang="scss">
|
| 97 |
+
.mcts-statistics-panel {
|
| 98 |
+
display: flex;
|
| 99 |
+
flex-direction: column;
|
| 100 |
+
}
|
| 101 |
+
|
| 102 |
+
.no-data {
|
| 103 |
+
text-align: center;
|
| 104 |
+
color: #606060;
|
| 105 |
+
font-size: 12px;
|
| 106 |
+
padding: 20px;
|
| 107 |
+
}
|
| 108 |
+
|
| 109 |
+
.stats-content {
|
| 110 |
+
display: flex;
|
| 111 |
+
flex-direction: column;
|
| 112 |
+
gap: 15px;
|
| 113 |
+
}
|
| 114 |
+
|
| 115 |
+
.stats-summary {
|
| 116 |
+
padding: 10px;
|
| 117 |
+
background-color: #1a1a1a;
|
| 118 |
+
border-radius: 4px;
|
| 119 |
+
}
|
| 120 |
+
|
| 121 |
+
.stat-item {
|
| 122 |
+
display: flex;
|
| 123 |
+
justify-content: space-between;
|
| 124 |
+
font-size: 13px;
|
| 125 |
+
}
|
| 126 |
+
|
| 127 |
+
.stat-label {
|
| 128 |
+
color: #a0a0a0;
|
| 129 |
+
}
|
| 130 |
+
|
| 131 |
+
.stat-value {
|
| 132 |
+
color: #e0e0e0;
|
| 133 |
+
font-weight: 500;
|
| 134 |
+
}
|
| 135 |
+
|
| 136 |
+
.stats-options {
|
| 137 |
+
display: flex;
|
| 138 |
+
flex-direction: column;
|
| 139 |
+
gap: 8px;
|
| 140 |
+
}
|
| 141 |
+
|
| 142 |
+
.option-label {
|
| 143 |
+
font-size: 12px;
|
| 144 |
+
color: #a0a0a0;
|
| 145 |
+
font-weight: 500;
|
| 146 |
+
}
|
| 147 |
+
|
| 148 |
+
.radio-group {
|
| 149 |
+
display: flex;
|
| 150 |
+
flex-direction: column;
|
| 151 |
+
gap: 6px;
|
| 152 |
+
}
|
| 153 |
+
|
| 154 |
+
.radio-option {
|
| 155 |
+
display: flex;
|
| 156 |
+
align-items: center;
|
| 157 |
+
gap: 8px;
|
| 158 |
+
font-size: 12px;
|
| 159 |
+
color: #c0c0c0;
|
| 160 |
+
cursor: pointer;
|
| 161 |
+
|
| 162 |
+
input[type="radio"] {
|
| 163 |
+
cursor: pointer;
|
| 164 |
+
}
|
| 165 |
+
}
|
| 166 |
+
|
| 167 |
+
.top-moves-table {
|
| 168 |
+
max-height: 300px;
|
| 169 |
+
|
| 170 |
+
table {
|
| 171 |
+
width: 100%;
|
| 172 |
+
border-collapse: collapse;
|
| 173 |
+
font-size: 12px;
|
| 174 |
+
|
| 175 |
+
thead {
|
| 176 |
+
position: sticky;
|
| 177 |
+
top: 0;
|
| 178 |
+
background-color: #2a2a2a;
|
| 179 |
+
z-index: 1;
|
| 180 |
+
|
| 181 |
+
th {
|
| 182 |
+
padding: 8px 6px;
|
| 183 |
+
text-align: left;
|
| 184 |
+
color: #4a90e2;
|
| 185 |
+
font-weight: 600;
|
| 186 |
+
border-bottom: 1px solid #404040;
|
| 187 |
+
}
|
| 188 |
+
}
|
| 189 |
+
|
| 190 |
+
tbody {
|
| 191 |
+
tr {
|
| 192 |
+
cursor: pointer;
|
| 193 |
+
transition: background-color 0.2s;
|
| 194 |
+
|
| 195 |
+
&:hover {
|
| 196 |
+
background-color: #3a3a3a;
|
| 197 |
+
}
|
| 198 |
+
|
| 199 |
+
&.selected {
|
| 200 |
+
background-color: #4a3030;
|
| 201 |
+
|
| 202 |
+
td {
|
| 203 |
+
color: #e94560;
|
| 204 |
+
font-weight: 500;
|
| 205 |
+
}
|
| 206 |
+
}
|
| 207 |
+
|
| 208 |
+
td {
|
| 209 |
+
padding: 8px 6px;
|
| 210 |
+
color: #c0c0c0;
|
| 211 |
+
border-bottom: 1px solid #303030;
|
| 212 |
+
|
| 213 |
+
&:first-child {
|
| 214 |
+
color: #808080;
|
| 215 |
+
}
|
| 216 |
+
}
|
| 217 |
+
}
|
| 218 |
+
}
|
| 219 |
+
}
|
| 220 |
+
}
|
| 221 |
+
</style>
|
trigo-web/app/src/components/mcts/MCTSTreeVisualization.vue
ADDED
|
@@ -0,0 +1,407 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<template>
|
| 2 |
+
<div class="tree-visualization">
|
| 3 |
+
<div v-if="!hasData" class="no-data">
|
| 4 |
+
<p>No data loaded. Please upload files to begin.</p>
|
| 5 |
+
<p class="note">Note: Only root-level statistics available from saved data</p>
|
| 6 |
+
</div>
|
| 7 |
+
|
| 8 |
+
<div v-else class="tree-content">
|
| 9 |
+
<svg
|
| 10 |
+
:width="svgWidth"
|
| 11 |
+
:height="svgHeight"
|
| 12 |
+
:viewBox="`0 0 ${svgWidth} ${svgHeight}`"
|
| 13 |
+
preserveAspectRatio="xMidYMid meet"
|
| 14 |
+
class="tree-svg"
|
| 15 |
+
ref="svgRef"
|
| 16 |
+
>
|
| 17 |
+
<g :transform="`translate(${centerX}, ${rootY})`">
|
| 18 |
+
<!-- Root node -->
|
| 19 |
+
<g class="root-node">
|
| 20 |
+
<circle
|
| 21 |
+
:r="rootRadius"
|
| 22 |
+
fill="#4a90e2"
|
| 23 |
+
stroke="#357abd"
|
| 24 |
+
stroke-width="2"
|
| 25 |
+
class="node-circle"
|
| 26 |
+
/>
|
| 27 |
+
<text y="5" text-anchor="middle" class="node-label">Root</text>
|
| 28 |
+
<text y="20" text-anchor="middle" class="node-value">
|
| 29 |
+
{{ totalVisits }} visits
|
| 30 |
+
</text>
|
| 31 |
+
</g>
|
| 32 |
+
|
| 33 |
+
<!-- Links to children -->
|
| 34 |
+
<g class="links">
|
| 35 |
+
<path
|
| 36 |
+
v-for="(child, idx) in visibleChildren"
|
| 37 |
+
:key="child.actionKey"
|
| 38 |
+
:d="getLinkPath(idx)"
|
| 39 |
+
:stroke="getChildColor(child)"
|
| 40 |
+
:stroke-width="getStrokeWidth(child)"
|
| 41 |
+
fill="none"
|
| 42 |
+
opacity="0.6"
|
| 43 |
+
class="tree-link"
|
| 44 |
+
/>
|
| 45 |
+
</g>
|
| 46 |
+
|
| 47 |
+
<!-- Child nodes -->
|
| 48 |
+
<g class="child-nodes">
|
| 49 |
+
<g
|
| 50 |
+
v-for="(child, idx) in visibleChildren"
|
| 51 |
+
:key="child.actionKey"
|
| 52 |
+
:transform="getChildTransform(idx)"
|
| 53 |
+
@mouseenter="onChildHover(child)"
|
| 54 |
+
@mouseleave="onChildLeave"
|
| 55 |
+
@click="onChildClick(child)"
|
| 56 |
+
class="child-node"
|
| 57 |
+
:class="{ selected: isSelected(child.actionKey) }"
|
| 58 |
+
>
|
| 59 |
+
<circle
|
| 60 |
+
:r="getChildRadius(child)"
|
| 61 |
+
:fill="getChildColor(child)"
|
| 62 |
+
:stroke="isSelected(child.actionKey) ? '#e94560' : '#2a2a2a'"
|
| 63 |
+
:stroke-width="isSelected(child.actionKey) ? 3 : 1"
|
| 64 |
+
class="node-circle"
|
| 65 |
+
/>
|
| 66 |
+
<text y="-15" text-anchor="middle" class="node-label">
|
| 67 |
+
{{ formatPosition(child.position) }}
|
| 68 |
+
</text>
|
| 69 |
+
<text y="0" text-anchor="middle" class="node-value">
|
| 70 |
+
{{ child.N }}
|
| 71 |
+
</text>
|
| 72 |
+
<text y="12" text-anchor="middle" class="node-percent">
|
| 73 |
+
{{ (child.pi * 100).toFixed(1) }}%
|
| 74 |
+
</text>
|
| 75 |
+
</g>
|
| 76 |
+
</g>
|
| 77 |
+
</g>
|
| 78 |
+
</svg>
|
| 79 |
+
|
| 80 |
+
<!-- Settings -->
|
| 81 |
+
<div class="tree-settings">
|
| 82 |
+
<label class="setting-label">
|
| 83 |
+
Show top:
|
| 84 |
+
<input
|
| 85 |
+
type="range"
|
| 86 |
+
min="5"
|
| 87 |
+
max="20"
|
| 88 |
+
:value="topN"
|
| 89 |
+
@input="onTopNChange"
|
| 90 |
+
class="setting-slider"
|
| 91 |
+
/>
|
| 92 |
+
<span class="setting-value">{{ topN }} moves</span>
|
| 93 |
+
</label>
|
| 94 |
+
</div>
|
| 95 |
+
|
| 96 |
+
<!-- Tooltip -->
|
| 97 |
+
<div
|
| 98 |
+
v-if="hoveredChild"
|
| 99 |
+
class="tooltip"
|
| 100 |
+
:style="{ left: tooltipX + 'px', top: tooltipY + 'px' }"
|
| 101 |
+
>
|
| 102 |
+
<div class="tooltip-pos">{{ formatPosition(hoveredChild.position) }}</div>
|
| 103 |
+
<div class="tooltip-line">Visit count: {{ hoveredChild.N }}</div>
|
| 104 |
+
<div class="tooltip-line">
|
| 105 |
+
Percentage: {{ ((hoveredChild.N / totalVisits) * 100).toFixed(1) }}%
|
| 106 |
+
</div>
|
| 107 |
+
<div class="tooltip-line">Policy π: {{ hoveredChild.pi.toFixed(3) }}</div>
|
| 108 |
+
</div>
|
| 109 |
+
</div>
|
| 110 |
+
</div>
|
| 111 |
+
</template>
|
| 112 |
+
|
| 113 |
+
<script setup lang="ts">
|
| 114 |
+
import { ref, computed } from "vue";
|
| 115 |
+
import { useMCTSStore } from "@/stores/mctsStore";
|
| 116 |
+
import type { MCTSMoveStatistic } from "@/types/mcts";
|
| 117 |
+
import { formatPosition } from "@/utils/mctsDataParser";
|
| 118 |
+
import { getColorForStatistic } from "@/utils/mctsColorScale";
|
| 119 |
+
|
| 120 |
+
const mctsStore = useMCTSStore();
|
| 121 |
+
|
| 122 |
+
// Layout constants
|
| 123 |
+
const rootRadius = 30;
|
| 124 |
+
const rootY = 50; // Root node Y position
|
| 125 |
+
const childY = 150; // Child nodes Y position
|
| 126 |
+
const padding = 20; // Padding around content
|
| 127 |
+
|
| 128 |
+
// Settings
|
| 129 |
+
const topN = ref(10);
|
| 130 |
+
|
| 131 |
+
// Computed
|
| 132 |
+
const hasData = computed(() => mctsStore.hasData);
|
| 133 |
+
const totalVisits = computed(() => mctsStore.totalVisits);
|
| 134 |
+
const currentStatistics = computed(() => mctsStore.currentStatistics);
|
| 135 |
+
|
| 136 |
+
const visibleChildren = computed(() => {
|
| 137 |
+
return currentStatistics.value
|
| 138 |
+
.filter((stat: MCTSMoveStatistic) => stat.position !== null) // Exclude pass for tree view
|
| 139 |
+
.sort((a: MCTSMoveStatistic, b: MCTSMoveStatistic) => b.N - a.N)
|
| 140 |
+
.slice(0, topN.value);
|
| 141 |
+
});
|
| 142 |
+
|
| 143 |
+
// Calculate max child radius for accurate bounds
|
| 144 |
+
const maxChildRadius = computed(() => {
|
| 145 |
+
if (visibleChildren.value.length === 0) return 15;
|
| 146 |
+
const maxN = Math.max(...visibleChildren.value.map((c: MCTSMoveStatistic) => c.N));
|
| 147 |
+
const minRadius = 15;
|
| 148 |
+
const maxRadius = 35;
|
| 149 |
+
return maxRadius; // Use max possible radius for bounds calculation
|
| 150 |
+
});
|
| 151 |
+
|
| 152 |
+
// Calculate horizontal spread
|
| 153 |
+
const horizontalSpread = computed(() => {
|
| 154 |
+
const count = visibleChildren.value.length;
|
| 155 |
+
if (count === 0) return 100;
|
| 156 |
+
return Math.min(count * 80, 800);
|
| 157 |
+
});
|
| 158 |
+
|
| 159 |
+
// Calculate SVG dimensions based on content
|
| 160 |
+
const svgWidth = computed(() => {
|
| 161 |
+
const calculatedWidth = horizontalSpread.value + padding * 2 + maxChildRadius.value * 2;
|
| 162 |
+
return Math.max(calculatedWidth, 300); // Minimum width of 300px
|
| 163 |
+
});
|
| 164 |
+
|
| 165 |
+
const svgHeight = computed(() => {
|
| 166 |
+
// Root at rootY with rootRadius, children at childY with maxChildRadius
|
| 167 |
+
const calculatedHeight = childY + maxChildRadius.value + padding * 2;
|
| 168 |
+
return Math.max(calculatedHeight, 250); // Minimum height of 250px
|
| 169 |
+
});
|
| 170 |
+
|
| 171 |
+
const centerX = computed(() => svgWidth.value / 2);
|
| 172 |
+
|
| 173 |
+
// Hover state
|
| 174 |
+
const hoveredChild = ref<MCTSMoveStatistic | null>(null);
|
| 175 |
+
const tooltipX = ref(0);
|
| 176 |
+
const tooltipY = ref(0);
|
| 177 |
+
|
| 178 |
+
// Functions
|
| 179 |
+
function getChildTransform(index: number): string {
|
| 180 |
+
const count = visibleChildren.value.length;
|
| 181 |
+
const spread = horizontalSpread.value;
|
| 182 |
+
const startX = -spread / 2;
|
| 183 |
+
const x = startX + (spread / (count - 1 || 1)) * index;
|
| 184 |
+
return `translate(${x}, ${childY})`;
|
| 185 |
+
}
|
| 186 |
+
|
| 187 |
+
function getLinkPath(index: number): string {
|
| 188 |
+
const count = visibleChildren.value.length;
|
| 189 |
+
const spread = horizontalSpread.value;
|
| 190 |
+
const startX = -spread / 2;
|
| 191 |
+
const x = startX + (spread / (count - 1 || 1)) * index;
|
| 192 |
+
|
| 193 |
+
// Curved path from root to child
|
| 194 |
+
return `M 0,${rootRadius} Q 0,${childY / 2} ${x},${childY - getChildRadius(visibleChildren.value[index])}`;
|
| 195 |
+
}
|
| 196 |
+
|
| 197 |
+
function getChildRadius(child: MCTSMoveStatistic): number {
|
| 198 |
+
// Size based on visit count
|
| 199 |
+
const maxN = Math.max(...visibleChildren.value.map((c: MCTSMoveStatistic) => c.N));
|
| 200 |
+
const minRadius = 15;
|
| 201 |
+
const maxRadius = 35;
|
| 202 |
+
return minRadius + ((child.N / maxN) * (maxRadius - minRadius));
|
| 203 |
+
}
|
| 204 |
+
|
| 205 |
+
function getChildColor(child: MCTSMoveStatistic): string {
|
| 206 |
+
const maxN = Math.max(...visibleChildren.value.map((c: MCTSMoveStatistic) => c.N));
|
| 207 |
+
return getColorForStatistic(child.N, "N", 0, maxN, mctsStore.colorScale);
|
| 208 |
+
}
|
| 209 |
+
|
| 210 |
+
function getStrokeWidth(child: MCTSMoveStatistic): number {
|
| 211 |
+
// Thicker lines for more visited nodes
|
| 212 |
+
const maxN = Math.max(...visibleChildren.value.map((c: MCTSMoveStatistic) => c.N));
|
| 213 |
+
return 1 + (child.N / maxN) * 4;
|
| 214 |
+
}
|
| 215 |
+
|
| 216 |
+
function isSelected(actionKey: string): boolean {
|
| 217 |
+
return mctsStore.selectedActionKey === actionKey;
|
| 218 |
+
}
|
| 219 |
+
|
| 220 |
+
function onChildHover(child: MCTSMoveStatistic) {
|
| 221 |
+
hoveredChild.value = child;
|
| 222 |
+
}
|
| 223 |
+
|
| 224 |
+
function onChildLeave() {
|
| 225 |
+
hoveredChild.value = null;
|
| 226 |
+
}
|
| 227 |
+
|
| 228 |
+
function onChildClick(child: MCTSMoveStatistic) {
|
| 229 |
+
mctsStore.selectAction(child.actionKey);
|
| 230 |
+
}
|
| 231 |
+
|
| 232 |
+
function onTopNChange(event: Event) {
|
| 233 |
+
const target = event.target as HTMLInputElement;
|
| 234 |
+
topN.value = parseInt(target.value, 10);
|
| 235 |
+
mctsStore.setTopNFilter(topN.value);
|
| 236 |
+
}
|
| 237 |
+
|
| 238 |
+
// Track mouse for tooltip
|
| 239 |
+
function onMouseMove(event: MouseEvent) {
|
| 240 |
+
tooltipX.value = event.clientX + 10;
|
| 241 |
+
tooltipY.value = event.clientY + 10;
|
| 242 |
+
}
|
| 243 |
+
|
| 244 |
+
if (typeof window !== "undefined") {
|
| 245 |
+
window.addEventListener("mousemove", onMouseMove);
|
| 246 |
+
}
|
| 247 |
+
</script>
|
| 248 |
+
|
| 249 |
+
<style scoped lang="scss">
|
| 250 |
+
.tree-visualization {
|
| 251 |
+
width: 100%;
|
| 252 |
+
display: flex;
|
| 253 |
+
flex-direction: column;
|
| 254 |
+
align-items: center;
|
| 255 |
+
justify-content: flex-start;
|
| 256 |
+
}
|
| 257 |
+
|
| 258 |
+
.no-data {
|
| 259 |
+
display: flex;
|
| 260 |
+
flex-direction: column;
|
| 261 |
+
justify-content: center;
|
| 262 |
+
align-items: center;
|
| 263 |
+
height: 100%;
|
| 264 |
+
gap: 10px;
|
| 265 |
+
|
| 266 |
+
p {
|
| 267 |
+
color: #606060;
|
| 268 |
+
font-size: 14px;
|
| 269 |
+
margin: 0;
|
| 270 |
+
}
|
| 271 |
+
|
| 272 |
+
.note {
|
| 273 |
+
font-size: 11px;
|
| 274 |
+
color: #808080;
|
| 275 |
+
font-style: italic;
|
| 276 |
+
}
|
| 277 |
+
}
|
| 278 |
+
|
| 279 |
+
.tree-content {
|
| 280 |
+
display: flex;
|
| 281 |
+
flex-direction: column;
|
| 282 |
+
gap: 15px;
|
| 283 |
+
width: 100%;
|
| 284 |
+
align-items: center;
|
| 285 |
+
justify-content: flex-start;
|
| 286 |
+
}
|
| 287 |
+
|
| 288 |
+
.tree-svg {
|
| 289 |
+
display: block;
|
| 290 |
+
max-width: 100%;
|
| 291 |
+
height: auto;
|
| 292 |
+
}
|
| 293 |
+
|
| 294 |
+
.node-circle {
|
| 295 |
+
cursor: pointer;
|
| 296 |
+
transition: all 0.2s;
|
| 297 |
+
}
|
| 298 |
+
|
| 299 |
+
.child-node {
|
| 300 |
+
cursor: pointer;
|
| 301 |
+
|
| 302 |
+
&:hover .node-circle {
|
| 303 |
+
filter: brightness(1.2);
|
| 304 |
+
}
|
| 305 |
+
|
| 306 |
+
&.selected .node-circle {
|
| 307 |
+
filter: brightness(1.3);
|
| 308 |
+
}
|
| 309 |
+
}
|
| 310 |
+
|
| 311 |
+
.node-label {
|
| 312 |
+
fill: #e0e0e0;
|
| 313 |
+
font-size: 11px;
|
| 314 |
+
font-weight: 600;
|
| 315 |
+
pointer-events: none;
|
| 316 |
+
}
|
| 317 |
+
|
| 318 |
+
.node-value {
|
| 319 |
+
fill: #e0e0e0;
|
| 320 |
+
font-size: 10px;
|
| 321 |
+
pointer-events: none;
|
| 322 |
+
}
|
| 323 |
+
|
| 324 |
+
.node-percent {
|
| 325 |
+
fill: #a0a0a0;
|
| 326 |
+
font-size: 9px;
|
| 327 |
+
pointer-events: none;
|
| 328 |
+
}
|
| 329 |
+
|
| 330 |
+
.tree-link {
|
| 331 |
+
pointer-events: none;
|
| 332 |
+
}
|
| 333 |
+
|
| 334 |
+
/* Tree Settings */
|
| 335 |
+
.tree-settings {
|
| 336 |
+
padding: 10px 15px;
|
| 337 |
+
background-color: #1a1a1a;
|
| 338 |
+
border-radius: 4px;
|
| 339 |
+
display: flex;
|
| 340 |
+
justify-content: center;
|
| 341 |
+
}
|
| 342 |
+
|
| 343 |
+
.setting-label {
|
| 344 |
+
display: flex;
|
| 345 |
+
align-items: center;
|
| 346 |
+
gap: 10px;
|
| 347 |
+
font-size: 12px;
|
| 348 |
+
color: #c0c0c0;
|
| 349 |
+
}
|
| 350 |
+
|
| 351 |
+
.setting-slider {
|
| 352 |
+
width: 150px;
|
| 353 |
+
height: 4px;
|
| 354 |
+
border-radius: 2px;
|
| 355 |
+
background-color: #3a3a3a;
|
| 356 |
+
outline: none;
|
| 357 |
+
-webkit-appearance: none;
|
| 358 |
+
|
| 359 |
+
&::-webkit-slider-thumb {
|
| 360 |
+
-webkit-appearance: none;
|
| 361 |
+
width: 12px;
|
| 362 |
+
height: 12px;
|
| 363 |
+
border-radius: 50%;
|
| 364 |
+
background-color: #4a90e2;
|
| 365 |
+
cursor: pointer;
|
| 366 |
+
}
|
| 367 |
+
|
| 368 |
+
&::-moz-range-thumb {
|
| 369 |
+
width: 12px;
|
| 370 |
+
height: 12px;
|
| 371 |
+
border-radius: 50%;
|
| 372 |
+
background-color: #4a90e2;
|
| 373 |
+
cursor: pointer;
|
| 374 |
+
border: none;
|
| 375 |
+
}
|
| 376 |
+
}
|
| 377 |
+
|
| 378 |
+
.setting-value {
|
| 379 |
+
font-weight: 500;
|
| 380 |
+
color: #e0e0e0;
|
| 381 |
+
}
|
| 382 |
+
|
| 383 |
+
/* Tooltip */
|
| 384 |
+
.tooltip {
|
| 385 |
+
position: fixed;
|
| 386 |
+
background-color: rgba(0, 0, 0, 0.9);
|
| 387 |
+
border: 1px solid #4a90e2;
|
| 388 |
+
border-radius: 4px;
|
| 389 |
+
padding: 8px 12px;
|
| 390 |
+
font-size: 11px;
|
| 391 |
+
color: #e0e0e0;
|
| 392 |
+
pointer-events: none;
|
| 393 |
+
z-index: 1000;
|
| 394 |
+
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3);
|
| 395 |
+
}
|
| 396 |
+
|
| 397 |
+
.tooltip-pos {
|
| 398 |
+
font-weight: 600;
|
| 399 |
+
color: #4a90e2;
|
| 400 |
+
margin-bottom: 4px;
|
| 401 |
+
}
|
| 402 |
+
|
| 403 |
+
.tooltip-line {
|
| 404 |
+
color: #c0c0c0;
|
| 405 |
+
line-height: 1.4;
|
| 406 |
+
}
|
| 407 |
+
</style>
|
trigo-web/app/src/composables/useRoomHash.ts
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Composable for managing room ID in URL hash
|
| 3 |
+
* Enables shareable room links for VS People multiplayer mode
|
| 4 |
+
*/
|
| 5 |
+
|
| 6 |
+
export function useRoomHash() {
|
| 7 |
+
/**
|
| 8 |
+
* Extract room ID from URL hash
|
| 9 |
+
* @returns Room ID string or null if no hash present
|
| 10 |
+
*/
|
| 11 |
+
function getRoomIdFromHash(): string | null {
|
| 12 |
+
const hash = window.location.hash.substring(1); // Remove '#' prefix
|
| 13 |
+
return hash || null;
|
| 14 |
+
}
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
/**
|
| 18 |
+
* Update URL hash with room ID
|
| 19 |
+
* Uses replaceState to avoid adding browser history entry
|
| 20 |
+
* @param roomId - 8-character uppercase alphanumeric room ID
|
| 21 |
+
*/
|
| 22 |
+
function updateHash(roomId: string): void {
|
| 23 |
+
const newUrl = `${window.location.pathname}#${roomId}`;
|
| 24 |
+
window.history.replaceState(null, "", newUrl);
|
| 25 |
+
}
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
/**
|
| 29 |
+
* Remove hash from URL
|
| 30 |
+
* Clears room ID from address bar
|
| 31 |
+
*/
|
| 32 |
+
function clearHash(): void {
|
| 33 |
+
const newUrl = window.location.pathname;
|
| 34 |
+
window.history.replaceState(null, "", newUrl);
|
| 35 |
+
}
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
/**
|
| 39 |
+
* Validate room ID format
|
| 40 |
+
* Must match backend generation: 8 uppercase alphanumeric characters
|
| 41 |
+
* @param roomId - Room ID to validate
|
| 42 |
+
* @returns true if valid format, false otherwise
|
| 43 |
+
*/
|
| 44 |
+
function isValidRoomId(roomId: string): boolean {
|
| 45 |
+
return /^[A-Z0-9]{8}$/.test(roomId);
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
return {
|
| 50 |
+
getRoomIdFromHash,
|
| 51 |
+
updateHash,
|
| 52 |
+
clearHash,
|
| 53 |
+
isValidRoomId
|
| 54 |
+
};
|
| 55 |
+
}
|
trigo-web/app/src/composables/useSocket.ts
CHANGED
|
@@ -4,9 +4,11 @@ import { io, Socket } from "socket.io-client";
|
|
| 4 |
// Singleton socket instance
|
| 5 |
let socketInstance: Socket | null = null;
|
| 6 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7 |
export function useSocket() {
|
| 8 |
-
const connected = ref(false);
|
| 9 |
-
const error = ref<string | null>(null);
|
| 10 |
|
| 11 |
// Get or create socket instance
|
| 12 |
const getSocket = (): Socket => {
|
|
@@ -16,10 +18,8 @@ export function useSocket() {
|
|
| 16 |
|
| 17 |
let serverUrl: string;
|
| 18 |
if (isDev) {
|
| 19 |
-
// Development: Use
|
| 20 |
-
|
| 21 |
-
const currentHost = window.location.hostname;
|
| 22 |
-
serverUrl = `http://${currentHost}:3000`;
|
| 23 |
} else {
|
| 24 |
// Production: same origin
|
| 25 |
serverUrl = window.location.origin;
|
|
@@ -50,6 +50,9 @@ export function useSocket() {
|
|
| 50 |
error.value = err.message;
|
| 51 |
console.error("[Socket.io] Connection error:", err.message);
|
| 52 |
});
|
|
|
|
|
|
|
|
|
|
| 53 |
}
|
| 54 |
|
| 55 |
return socketInstance;
|
|
@@ -85,6 +88,171 @@ export function useSocket() {
|
|
| 85 |
});
|
| 86 |
};
|
| 87 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 88 |
// Clean up on unmount
|
| 89 |
onUnmounted(() => {
|
| 90 |
// Note: We don't disconnect the singleton instance
|
|
@@ -95,7 +263,38 @@ export function useSocket() {
|
|
| 95 |
socket: getSocket(),
|
| 96 |
connected,
|
| 97 |
error,
|
| 98 |
-
sendEcho
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 99 |
};
|
| 100 |
}
|
| 101 |
|
|
|
|
| 4 |
// Singleton socket instance
|
| 5 |
let socketInstance: Socket | null = null;
|
| 6 |
|
| 7 |
+
// Singleton reactive refs (shared across all useSocket() calls)
|
| 8 |
+
const connected = ref(false);
|
| 9 |
+
const error = ref<string | null>(null);
|
| 10 |
+
|
| 11 |
export function useSocket() {
|
|
|
|
|
|
|
| 12 |
|
| 13 |
// Get or create socket instance
|
| 14 |
const getSocket = (): Socket => {
|
|
|
|
| 18 |
|
| 19 |
let serverUrl: string;
|
| 20 |
if (isDev) {
|
| 21 |
+
// Development: Use VITE_SERVER_URL from .env or .env.local
|
| 22 |
+
serverUrl = import.meta.env.VITE_SERVER_URL || "http://localhost:3000";
|
|
|
|
|
|
|
| 23 |
} else {
|
| 24 |
// Production: same origin
|
| 25 |
serverUrl = window.location.origin;
|
|
|
|
| 50 |
error.value = err.message;
|
| 51 |
console.error("[Socket.io] Connection error:", err.message);
|
| 52 |
});
|
| 53 |
+
} else {
|
| 54 |
+
// Socket already exists, sync the connected state
|
| 55 |
+
connected.value = socketInstance.connected;
|
| 56 |
}
|
| 57 |
|
| 58 |
return socketInstance;
|
|
|
|
| 88 |
});
|
| 89 |
};
|
| 90 |
|
| 91 |
+
// Join a room
|
| 92 |
+
const joinRoom = (
|
| 93 |
+
data: { roomId?: string; nickname: string },
|
| 94 |
+
callback: (response: any) => void
|
| 95 |
+
): void => {
|
| 96 |
+
const socket = getSocket();
|
| 97 |
+
console.log("[useSocket] joinRoom called:", {
|
| 98 |
+
roomId: data.roomId,
|
| 99 |
+
nickname: data.nickname,
|
| 100 |
+
socketConnected: socket.connected,
|
| 101 |
+
socketId: socket.id
|
| 102 |
+
});
|
| 103 |
+
socket.emit("joinRoom", data, callback);
|
| 104 |
+
console.log("[useSocket] joinRoom event emitted");
|
| 105 |
+
};
|
| 106 |
+
|
| 107 |
+
// Leave current room
|
| 108 |
+
const leaveRoom = (): void => {
|
| 109 |
+
const socket = getSocket();
|
| 110 |
+
socket.emit("leaveRoom");
|
| 111 |
+
};
|
| 112 |
+
|
| 113 |
+
// Change player nickname
|
| 114 |
+
const changeNickname = (nickname: string, callback?: (response: any) => void): void => {
|
| 115 |
+
const socket = getSocket();
|
| 116 |
+
socket.emit("changeNickname", { nickname }, callback);
|
| 117 |
+
};
|
| 118 |
+
|
| 119 |
+
|
| 120 |
+
// Game actions
|
| 121 |
+
|
| 122 |
+
// Make a move (place stone)
|
| 123 |
+
const makeMove = (x: number, y: number, z: number): void => {
|
| 124 |
+
const socket = getSocket();
|
| 125 |
+
socket.emit("makeMove", { x, y, z });
|
| 126 |
+
};
|
| 127 |
+
|
| 128 |
+
// Pass turn
|
| 129 |
+
const pass = (): void => {
|
| 130 |
+
const socket = getSocket();
|
| 131 |
+
socket.emit("pass");
|
| 132 |
+
};
|
| 133 |
+
|
| 134 |
+
// Resign game
|
| 135 |
+
const resign = (): void => {
|
| 136 |
+
const socket = getSocket();
|
| 137 |
+
socket.emit("resign");
|
| 138 |
+
};
|
| 139 |
+
|
| 140 |
+
// Undo move
|
| 141 |
+
const undoMove = (callback?: (response: any) => void): void => {
|
| 142 |
+
const socket = getSocket();
|
| 143 |
+
socket.emit("undoMove", callback);
|
| 144 |
+
};
|
| 145 |
+
|
| 146 |
+
// Redo move
|
| 147 |
+
const redoMove = (callback?: (response: any) => void): void => {
|
| 148 |
+
const socket = getSocket();
|
| 149 |
+
socket.emit("redoMove", callback);
|
| 150 |
+
};
|
| 151 |
+
|
| 152 |
+
// Reset game
|
| 153 |
+
const resetGame = (
|
| 154 |
+
options?: { boardShape?: { x: number; y: number; z: number }; playerColors?: { [playerId: string]: "black" | "white" } },
|
| 155 |
+
callback?: (response: any) => void
|
| 156 |
+
): void => {
|
| 157 |
+
const socket = getSocket();
|
| 158 |
+
socket.emit("resetGame", options, callback);
|
| 159 |
+
};
|
| 160 |
+
|
| 161 |
+
|
| 162 |
+
// Event listeners
|
| 163 |
+
const onPlayerJoined = (handler: (data: { playerId: string; nickname: string }) => void): void => {
|
| 164 |
+
const socket = getSocket();
|
| 165 |
+
socket.on("playerJoined", handler);
|
| 166 |
+
};
|
| 167 |
+
|
| 168 |
+
const onPlayerLeft = (handler: (data: { playerId: string }) => void): void => {
|
| 169 |
+
const socket = getSocket();
|
| 170 |
+
socket.on("playerLeft", handler);
|
| 171 |
+
};
|
| 172 |
+
|
| 173 |
+
const onNicknameChanged = (
|
| 174 |
+
handler: (data: { playerId: string; nickname: string; oldNickname: string }) => void
|
| 175 |
+
): void => {
|
| 176 |
+
const socket = getSocket();
|
| 177 |
+
socket.on("nicknameChanged", handler);
|
| 178 |
+
};
|
| 179 |
+
|
| 180 |
+
const onRoomJoined = (handler: (data: any) => void): void => {
|
| 181 |
+
const socket = getSocket();
|
| 182 |
+
socket.on("roomJoined", handler);
|
| 183 |
+
};
|
| 184 |
+
|
| 185 |
+
const onGameUpdate = (handler: (data: any) => void): void => {
|
| 186 |
+
const socket = getSocket();
|
| 187 |
+
socket.on("gameUpdate", handler);
|
| 188 |
+
};
|
| 189 |
+
|
| 190 |
+
const onGameEnded = (handler: (data: any) => void): void => {
|
| 191 |
+
const socket = getSocket();
|
| 192 |
+
socket.on("gameEnded", handler);
|
| 193 |
+
};
|
| 194 |
+
|
| 195 |
+
const onGameReset = (handler: (data: any) => void): void => {
|
| 196 |
+
const socket = getSocket();
|
| 197 |
+
socket.on("gameReset", handler);
|
| 198 |
+
};
|
| 199 |
+
|
| 200 |
+
const onPlayerDisconnected = (handler: (data: { playerId: string }) => void): void => {
|
| 201 |
+
const socket = getSocket();
|
| 202 |
+
socket.on("playerDisconnected", handler);
|
| 203 |
+
};
|
| 204 |
+
|
| 205 |
+
const onError = (handler: (data: { message: string }) => void): void => {
|
| 206 |
+
const socket = getSocket();
|
| 207 |
+
socket.on("error", handler);
|
| 208 |
+
};
|
| 209 |
+
|
| 210 |
+
// Remove event listeners
|
| 211 |
+
const offPlayerJoined = (handler?: any): void => {
|
| 212 |
+
const socket = getSocket();
|
| 213 |
+
socket.off("playerJoined", handler);
|
| 214 |
+
};
|
| 215 |
+
|
| 216 |
+
const offPlayerLeft = (handler?: any): void => {
|
| 217 |
+
const socket = getSocket();
|
| 218 |
+
socket.off("playerLeft", handler);
|
| 219 |
+
};
|
| 220 |
+
|
| 221 |
+
const offNicknameChanged = (handler?: any): void => {
|
| 222 |
+
const socket = getSocket();
|
| 223 |
+
socket.off("nicknameChanged", handler);
|
| 224 |
+
};
|
| 225 |
+
|
| 226 |
+
const offRoomJoined = (handler?: any): void => {
|
| 227 |
+
const socket = getSocket();
|
| 228 |
+
socket.off("roomJoined", handler);
|
| 229 |
+
};
|
| 230 |
+
|
| 231 |
+
const offGameUpdate = (handler?: any): void => {
|
| 232 |
+
const socket = getSocket();
|
| 233 |
+
socket.off("gameUpdate", handler);
|
| 234 |
+
};
|
| 235 |
+
|
| 236 |
+
const offGameEnded = (handler?: any): void => {
|
| 237 |
+
const socket = getSocket();
|
| 238 |
+
socket.off("gameEnded", handler);
|
| 239 |
+
};
|
| 240 |
+
|
| 241 |
+
const offGameReset = (handler?: any): void => {
|
| 242 |
+
const socket = getSocket();
|
| 243 |
+
socket.off("gameReset", handler);
|
| 244 |
+
};
|
| 245 |
+
|
| 246 |
+
const offPlayerDisconnected = (handler?: any): void => {
|
| 247 |
+
const socket = getSocket();
|
| 248 |
+
socket.off("playerDisconnected", handler);
|
| 249 |
+
};
|
| 250 |
+
|
| 251 |
+
const offError = (handler?: any): void => {
|
| 252 |
+
const socket = getSocket();
|
| 253 |
+
socket.off("error", handler);
|
| 254 |
+
};
|
| 255 |
+
|
| 256 |
// Clean up on unmount
|
| 257 |
onUnmounted(() => {
|
| 258 |
// Note: We don't disconnect the singleton instance
|
|
|
|
| 263 |
socket: getSocket(),
|
| 264 |
connected,
|
| 265 |
error,
|
| 266 |
+
sendEcho,
|
| 267 |
+
// Room management
|
| 268 |
+
joinRoom,
|
| 269 |
+
leaveRoom,
|
| 270 |
+
changeNickname,
|
| 271 |
+
// Game actions
|
| 272 |
+
makeMove,
|
| 273 |
+
pass,
|
| 274 |
+
resign,
|
| 275 |
+
undoMove,
|
| 276 |
+
redoMove,
|
| 277 |
+
resetGame,
|
| 278 |
+
// Event listeners
|
| 279 |
+
onPlayerJoined,
|
| 280 |
+
onPlayerLeft,
|
| 281 |
+
onNicknameChanged,
|
| 282 |
+
onRoomJoined,
|
| 283 |
+
onGameUpdate,
|
| 284 |
+
onGameEnded,
|
| 285 |
+
onGameReset,
|
| 286 |
+
onPlayerDisconnected,
|
| 287 |
+
onError,
|
| 288 |
+
// Event cleanup
|
| 289 |
+
offPlayerJoined,
|
| 290 |
+
offPlayerLeft,
|
| 291 |
+
offNicknameChanged,
|
| 292 |
+
offRoomJoined,
|
| 293 |
+
offGameUpdate,
|
| 294 |
+
offGameEnded,
|
| 295 |
+
offGameReset,
|
| 296 |
+
offPlayerDisconnected,
|
| 297 |
+
offError
|
| 298 |
};
|
| 299 |
}
|
| 300 |
|
trigo-web/app/src/composables/useTrigoAgent.ts
CHANGED
|
@@ -14,7 +14,7 @@ import { ref, onUnmounted } from "vue";
|
|
| 14 |
import { TrigoTreeAgent } from "@inc/trigoTreeAgent";
|
| 15 |
import { OnnxInferencer } from "@/services/onnxInferencer";
|
| 16 |
import type { TrigoGame } from "@inc/trigo/game";
|
| 17 |
-
import type {
|
| 18 |
|
| 19 |
/**
|
| 20 |
* Composable for using the Trigo AI agent in Vue components
|
|
@@ -49,9 +49,9 @@ export function useTrigoAgent() {
|
|
| 49 |
|
| 50 |
// Create OnnxInferencer with evaluation model
|
| 51 |
inferencer = new OnnxInferencer({
|
| 52 |
-
modelPath:
|
| 53 |
-
vocabSize:
|
| 54 |
-
seqLen:
|
| 55 |
});
|
| 56 |
|
| 57 |
// Initialize ONNX model
|
|
@@ -75,10 +75,10 @@ export function useTrigoAgent() {
|
|
| 75 |
* Generate the next move for the AI player
|
| 76 |
*
|
| 77 |
* @param game - Current game instance
|
| 78 |
-
* @returns The selected move
|
| 79 |
* @throws Error if agent is not initialized
|
| 80 |
*/
|
| 81 |
-
const generateMove = async (game: TrigoGame): Promise<
|
| 82 |
if (!agent) {
|
| 83 |
throw new Error("AI agent not initialized. Call initialize() first.");
|
| 84 |
}
|
|
@@ -106,14 +106,19 @@ export function useTrigoAgent() {
|
|
| 106 |
lastMoveTime.value = elapsed;
|
| 107 |
console.log(`[useTrigoAgent] ✓ Move generated in ${elapsed.toFixed(2)}ms`);
|
| 108 |
|
| 109 |
-
//
|
| 110 |
-
if (!move
|
| 111 |
-
console.log("[useTrigoAgent]
|
| 112 |
return null;
|
| 113 |
}
|
| 114 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 115 |
if (move.x !== undefined && move.y !== undefined && move.z !== undefined) {
|
| 116 |
-
return
|
| 117 |
}
|
| 118 |
|
| 119 |
console.warn("[useTrigoAgent] Move has undefined coordinates:", move);
|
|
|
|
| 14 |
import { TrigoTreeAgent } from "@inc/trigoTreeAgent";
|
| 15 |
import { OnnxInferencer } from "@/services/onnxInferencer";
|
| 16 |
import type { TrigoGame } from "@inc/trigo/game";
|
| 17 |
+
import type { Move } from "@inc/trigo/types";
|
| 18 |
|
| 19 |
/**
|
| 20 |
* Composable for using the Trigo AI agent in Vue components
|
|
|
|
| 49 |
|
| 50 |
// Create OnnxInferencer with evaluation model
|
| 51 |
inferencer = new OnnxInferencer({
|
| 52 |
+
modelPath: import.meta.env.VITE_ONNX_TREE_MODEL,
|
| 53 |
+
vocabSize: 128,
|
| 54 |
+
seqLen: 256
|
| 55 |
});
|
| 56 |
|
| 57 |
// Initialize ONNX model
|
|
|
|
| 75 |
* Generate the next move for the AI player
|
| 76 |
*
|
| 77 |
* @param game - Current game instance
|
| 78 |
+
* @returns The selected move (can be a pass move with isPass: true), or null if no valid moves
|
| 79 |
* @throws Error if agent is not initialized
|
| 80 |
*/
|
| 81 |
+
const generateMove = async (game: TrigoGame): Promise<Move | null> => {
|
| 82 |
if (!agent) {
|
| 83 |
throw new Error("AI agent not initialized. Call initialize() first.");
|
| 84 |
}
|
|
|
|
| 106 |
lastMoveTime.value = elapsed;
|
| 107 |
console.log(`[useTrigoAgent] ✓ Move generated in ${elapsed.toFixed(2)}ms`);
|
| 108 |
|
| 109 |
+
// Return the Move object directly (can be pass move or normal move)
|
| 110 |
+
if (!move) {
|
| 111 |
+
console.log("[useTrigoAgent] No valid move available");
|
| 112 |
return null;
|
| 113 |
}
|
| 114 |
|
| 115 |
+
if (move.isPass) {
|
| 116 |
+
console.log("[useTrigoAgent] AI chose to pass");
|
| 117 |
+
return move; // Return the pass move object
|
| 118 |
+
}
|
| 119 |
+
|
| 120 |
if (move.x !== undefined && move.y !== undefined && move.z !== undefined) {
|
| 121 |
+
return move; // Return the normal move object
|
| 122 |
}
|
| 123 |
|
| 124 |
console.warn("[useTrigoAgent] Move has undefined coordinates:", move);
|
trigo-web/app/src/data/defaultNicknames.ts
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Default Nicknames for Trigo Players
|
| 3 |
+
*
|
| 4 |
+
* A curated list of common first names used for random nickname generation.
|
| 5 |
+
* When a user first visits the VS People mode, they're assigned a random
|
| 6 |
+
* nickname from this list. The nickname persists in localStorage and can
|
| 7 |
+
* be changed at any time.
|
| 8 |
+
*/
|
| 9 |
+
|
| 10 |
+
export const DEFAULT_NICKNAMES: string[] = [
|
| 11 |
+
// Male names (50)
|
| 12 |
+
"John",
|
| 13 |
+
"James",
|
| 14 |
+
"Michael",
|
| 15 |
+
"David",
|
| 16 |
+
"William",
|
| 17 |
+
"Richard",
|
| 18 |
+
"Joseph",
|
| 19 |
+
"Thomas",
|
| 20 |
+
"Charles",
|
| 21 |
+
"Daniel",
|
| 22 |
+
"Matthew",
|
| 23 |
+
"Anthony",
|
| 24 |
+
"Mark",
|
| 25 |
+
"Donald",
|
| 26 |
+
"Steven",
|
| 27 |
+
"Paul",
|
| 28 |
+
"Andrew",
|
| 29 |
+
"Joshua",
|
| 30 |
+
"Kenneth",
|
| 31 |
+
"Kevin",
|
| 32 |
+
"Brian",
|
| 33 |
+
"George",
|
| 34 |
+
"Edward",
|
| 35 |
+
"Ronald",
|
| 36 |
+
"Timothy",
|
| 37 |
+
"Jason",
|
| 38 |
+
"Jeffrey",
|
| 39 |
+
"Ryan",
|
| 40 |
+
"Jacob",
|
| 41 |
+
"Gary",
|
| 42 |
+
"Nicholas",
|
| 43 |
+
"Eric",
|
| 44 |
+
"Jonathan",
|
| 45 |
+
"Stephen",
|
| 46 |
+
"Larry",
|
| 47 |
+
"Justin",
|
| 48 |
+
"Scott",
|
| 49 |
+
"Brandon",
|
| 50 |
+
"Benjamin",
|
| 51 |
+
"Samuel",
|
| 52 |
+
"Raymond",
|
| 53 |
+
"Gregory",
|
| 54 |
+
"Frank",
|
| 55 |
+
"Alexander",
|
| 56 |
+
"Patrick",
|
| 57 |
+
"Jack",
|
| 58 |
+
"Dennis",
|
| 59 |
+
"Jerry",
|
| 60 |
+
"Tyler",
|
| 61 |
+
"Aaron",
|
| 62 |
+
|
| 63 |
+
// Female names (50)
|
| 64 |
+
"Mary",
|
| 65 |
+
"Patricia",
|
| 66 |
+
"Jennifer",
|
| 67 |
+
"Linda",
|
| 68 |
+
"Barbara",
|
| 69 |
+
"Elizabeth",
|
| 70 |
+
"Susan",
|
| 71 |
+
"Jessica",
|
| 72 |
+
"Sarah",
|
| 73 |
+
"Karen",
|
| 74 |
+
"Nancy",
|
| 75 |
+
"Lisa",
|
| 76 |
+
"Betty",
|
| 77 |
+
"Margaret",
|
| 78 |
+
"Sandra",
|
| 79 |
+
"Ashley",
|
| 80 |
+
"Kimberly",
|
| 81 |
+
"Emily",
|
| 82 |
+
"Donna",
|
| 83 |
+
"Michelle",
|
| 84 |
+
"Dorothy",
|
| 85 |
+
"Carol",
|
| 86 |
+
"Amanda",
|
| 87 |
+
"Melissa",
|
| 88 |
+
"Deborah",
|
| 89 |
+
"Stephanie",
|
| 90 |
+
"Rebecca",
|
| 91 |
+
"Sharon",
|
| 92 |
+
"Laura",
|
| 93 |
+
"Cynthia",
|
| 94 |
+
"Kathleen",
|
| 95 |
+
"Amy",
|
| 96 |
+
"Angela",
|
| 97 |
+
"Shirley",
|
| 98 |
+
"Anna",
|
| 99 |
+
"Brenda",
|
| 100 |
+
"Pamela",
|
| 101 |
+
"Emma",
|
| 102 |
+
"Nicole",
|
| 103 |
+
"Helen",
|
| 104 |
+
"Samantha",
|
| 105 |
+
"Katherine",
|
| 106 |
+
"Christine",
|
| 107 |
+
"Debra",
|
| 108 |
+
"Rachel",
|
| 109 |
+
"Catherine",
|
| 110 |
+
"Carolyn",
|
| 111 |
+
"Janet",
|
| 112 |
+
"Ruth",
|
| 113 |
+
"Maria",
|
| 114 |
+
|
| 115 |
+
// Neutral/Additional (15)
|
| 116 |
+
"Alex",
|
| 117 |
+
"Jordan",
|
| 118 |
+
"Taylor",
|
| 119 |
+
"Morgan",
|
| 120 |
+
"Casey",
|
| 121 |
+
"Riley",
|
| 122 |
+
"Jamie",
|
| 123 |
+
"Avery",
|
| 124 |
+
"Quinn",
|
| 125 |
+
"Sage",
|
| 126 |
+
"Robin",
|
| 127 |
+
"Charlie",
|
| 128 |
+
"Blake",
|
| 129 |
+
"Cameron",
|
| 130 |
+
"Hayden"
|
| 131 |
+
];
|
| 132 |
+
|
| 133 |
+
/**
|
| 134 |
+
* Get a random nickname from the default list
|
| 135 |
+
* @returns A randomly selected name from DEFAULT_NICKNAMES
|
| 136 |
+
*/
|
| 137 |
+
export function getRandomNickname(): string {
|
| 138 |
+
const index = Math.floor(Math.random() * DEFAULT_NICKNAMES.length);
|
| 139 |
+
return DEFAULT_NICKNAMES[index];
|
| 140 |
+
}
|
trigo-web/app/src/router/index.ts
CHANGED
|
@@ -3,6 +3,8 @@ import TrigoView from "@/views/TrigoView.vue";
|
|
| 3 |
import OnnxTestView from "@/views/OnnxTestView.vue";
|
| 4 |
import TrigoAgentTestView from "@/views/TrigoAgentTestView.vue";
|
| 5 |
import TrigoTreeTestView from "@/views/TrigoTreeTestView.vue";
|
|
|
|
|
|
|
| 6 |
|
| 7 |
const router = createRouter({
|
| 8 |
history: createWebHistory(import.meta.env.BASE_URL),
|
|
@@ -45,6 +47,16 @@ const router = createRouter({
|
|
| 45 |
path: "/tree-test",
|
| 46 |
name: "tree-test",
|
| 47 |
component: TrigoTreeTestView
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 48 |
}
|
| 49 |
]
|
| 50 |
});
|
|
|
|
| 3 |
import OnnxTestView from "@/views/OnnxTestView.vue";
|
| 4 |
import TrigoAgentTestView from "@/views/TrigoAgentTestView.vue";
|
| 5 |
import TrigoTreeTestView from "@/views/TrigoTreeTestView.vue";
|
| 6 |
+
import SocketTestView from "@/views/SocketTestView.vue";
|
| 7 |
+
import MCTSAnalysisView from "@/views/MCTSAnalysisView.vue";
|
| 8 |
|
| 9 |
const router = createRouter({
|
| 10 |
history: createWebHistory(import.meta.env.BASE_URL),
|
|
|
|
| 47 |
path: "/tree-test",
|
| 48 |
name: "tree-test",
|
| 49 |
component: TrigoTreeTestView
|
| 50 |
+
},
|
| 51 |
+
{
|
| 52 |
+
path: "/socket-test",
|
| 53 |
+
name: "socket-test",
|
| 54 |
+
component: SocketTestView
|
| 55 |
+
},
|
| 56 |
+
{
|
| 57 |
+
path: "/mcts-analysis",
|
| 58 |
+
name: "mcts-analysis",
|
| 59 |
+
component: MCTSAnalysisView
|
| 60 |
}
|
| 61 |
]
|
| 62 |
});
|
trigo-web/app/src/stores/gameStore.ts
CHANGED
|
@@ -297,6 +297,76 @@ export const useGameStore = defineStore("game", {
|
|
| 297 |
} catch (error) {
|
| 298 |
console.error("Failed to clear session storage:", error);
|
| 299 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 300 |
}
|
| 301 |
}
|
| 302 |
});
|
|
|
|
| 297 |
} catch (error) {
|
| 298 |
console.error("Failed to clear session storage:", error);
|
| 299 |
}
|
| 300 |
+
},
|
| 301 |
+
|
| 302 |
+
|
| 303 |
+
/**
|
| 304 |
+
* Set current player (for multiplayer sync)
|
| 305 |
+
*/
|
| 306 |
+
setCurrentPlayer(player: Player): void {
|
| 307 |
+
// This is handled by the underlying game state
|
| 308 |
+
// The current player is determined by the move history
|
| 309 |
+
// For proper sync, use loadFromTGN instead
|
| 310 |
+
console.log(`[gameStore] setCurrentPlayer called with: ${player}`);
|
| 311 |
+
},
|
| 312 |
+
|
| 313 |
+
|
| 314 |
+
/**
|
| 315 |
+
* Load game state from TGN string (for multiplayer sync)
|
| 316 |
+
*/
|
| 317 |
+
loadFromTGN(tgn: string): boolean {
|
| 318 |
+
try {
|
| 319 |
+
// Create a new game from TGN
|
| 320 |
+
const { TrigoGame } = require("../../../inc/trigo/game");
|
| 321 |
+
const newGame = TrigoGame.fromTGN(tgn);
|
| 322 |
+
|
| 323 |
+
// Create new frontend game with same shape
|
| 324 |
+
const shape = newGame.getShape();
|
| 325 |
+
this.game = new TrigoGameFrontend(shape);
|
| 326 |
+
|
| 327 |
+
// Replay all moves to sync state
|
| 328 |
+
const history = newGame.getStepHistory();
|
| 329 |
+
this.game.startGame();
|
| 330 |
+
|
| 331 |
+
for (const step of history) {
|
| 332 |
+
if (step.type === "drop" && step.position) {
|
| 333 |
+
this.game.makeMove(step.position.x, step.position.y, step.position.z);
|
| 334 |
+
} else if (step.type === "pass") {
|
| 335 |
+
this.game.pass();
|
| 336 |
+
}
|
| 337 |
+
}
|
| 338 |
+
|
| 339 |
+
this.saveToSessionStorage();
|
| 340 |
+
return true;
|
| 341 |
+
} catch (error) {
|
| 342 |
+
console.error("[gameStore] Failed to load from TGN:", error);
|
| 343 |
+
return false;
|
| 344 |
+
}
|
| 345 |
+
},
|
| 346 |
+
|
| 347 |
+
|
| 348 |
+
/**
|
| 349 |
+
* Get all stones on the board (for viewport sync)
|
| 350 |
+
*/
|
| 351 |
+
getAllStones(): Array<{ x: number; y: number; z: number; color: Player }> {
|
| 352 |
+
const stones: Array<{ x: number; y: number; z: number; color: Player }> = [];
|
| 353 |
+
const board = this.game.getBoard();
|
| 354 |
+
const shape = this.game.getShape();
|
| 355 |
+
|
| 356 |
+
for (let x = 0; x < shape.x; x++) {
|
| 357 |
+
for (let y = 0; y < shape.y; y++) {
|
| 358 |
+
for (let z = 0; z < shape.z; z++) {
|
| 359 |
+
const stone = board[x][y][z];
|
| 360 |
+
if (stone === 1) {
|
| 361 |
+
stones.push({ x, y, z, color: "black" });
|
| 362 |
+
} else if (stone === 2) {
|
| 363 |
+
stones.push({ x, y, z, color: "white" });
|
| 364 |
+
}
|
| 365 |
+
}
|
| 366 |
+
}
|
| 367 |
+
}
|
| 368 |
+
|
| 369 |
+
return stones;
|
| 370 |
}
|
| 371 |
}
|
| 372 |
});
|
trigo-web/app/src/stores/mctsStore.ts
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Pinia store for MCTS visualization state
|
| 3 |
+
*/
|
| 4 |
+
|
| 5 |
+
import { defineStore } from "pinia";
|
| 6 |
+
import { ref, computed } from "vue";
|
| 7 |
+
import type {
|
| 8 |
+
MCTSMoveData,
|
| 9 |
+
MCTSMoveStatistic,
|
| 10 |
+
MCTSStatisticType,
|
| 11 |
+
ColorScaleType
|
| 12 |
+
} from "@/types/mcts";
|
| 13 |
+
import type { Position } from "../../../inc/trigo/types";
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
export const useMCTSStore = defineStore("mcts", () => {
|
| 17 |
+
// ============================================================================
|
| 18 |
+
// State
|
| 19 |
+
// ============================================================================
|
| 20 |
+
|
| 21 |
+
// Game data
|
| 22 |
+
const gameHistory = ref<MCTSMoveData[]>([]);
|
| 23 |
+
const currentMoveIndex = ref<number>(0);
|
| 24 |
+
|
| 25 |
+
// Visualization settings
|
| 26 |
+
const selectedStatistic = ref<MCTSStatisticType>("N");
|
| 27 |
+
const colorScale = ref<ColorScaleType>("linear");
|
| 28 |
+
const topNFilter = ref<number>(10); // Show top N moves in tree/table
|
| 29 |
+
|
| 30 |
+
// Selection state
|
| 31 |
+
const selectedActionKey = ref<string | null>(null);
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
// ============================================================================
|
| 35 |
+
// Getters
|
| 36 |
+
// ============================================================================
|
| 37 |
+
|
| 38 |
+
const hasData = computed(() => gameHistory.value.length > 0);
|
| 39 |
+
|
| 40 |
+
const currentMoveData = computed((): MCTSMoveData | null => {
|
| 41 |
+
if (!hasData.value) return null;
|
| 42 |
+
return gameHistory.value[currentMoveIndex.value] || null;
|
| 43 |
+
});
|
| 44 |
+
|
| 45 |
+
const maxMoveIndex = computed(() => {
|
| 46 |
+
return Math.max(0, gameHistory.value.length - 1);
|
| 47 |
+
});
|
| 48 |
+
|
| 49 |
+
const currentStatistics = computed((): MCTSMoveStatistic[] => {
|
| 50 |
+
return currentMoveData.value?.statistics || [];
|
| 51 |
+
});
|
| 52 |
+
|
| 53 |
+
const topMoves = computed((): MCTSMoveStatistic[] => {
|
| 54 |
+
const stats = currentStatistics.value;
|
| 55 |
+
return stats
|
| 56 |
+
.slice()
|
| 57 |
+
.sort((a, b) => b.N - a.N)
|
| 58 |
+
.slice(0, topNFilter.value);
|
| 59 |
+
});
|
| 60 |
+
|
| 61 |
+
const totalVisits = computed((): number => {
|
| 62 |
+
return currentStatistics.value.reduce((sum, stat) => sum + stat.N, 0);
|
| 63 |
+
});
|
| 64 |
+
|
| 65 |
+
const selectedMove = computed((): MCTSMoveStatistic | null => {
|
| 66 |
+
if (!selectedActionKey.value) return null;
|
| 67 |
+
return (
|
| 68 |
+
currentStatistics.value.find(
|
| 69 |
+
(stat) => stat.actionKey === selectedActionKey.value
|
| 70 |
+
) || null
|
| 71 |
+
);
|
| 72 |
+
});
|
| 73 |
+
|
| 74 |
+
|
| 75 |
+
// ============================================================================
|
| 76 |
+
// Actions
|
| 77 |
+
// ============================================================================
|
| 78 |
+
|
| 79 |
+
function setGameHistory(history: MCTSMoveData[]) {
|
| 80 |
+
gameHistory.value = history;
|
| 81 |
+
currentMoveIndex.value = 0;
|
| 82 |
+
selectedActionKey.value = null;
|
| 83 |
+
}
|
| 84 |
+
|
| 85 |
+
function clearGameHistory() {
|
| 86 |
+
gameHistory.value = [];
|
| 87 |
+
currentMoveIndex.value = 0;
|
| 88 |
+
selectedActionKey.value = null;
|
| 89 |
+
}
|
| 90 |
+
|
| 91 |
+
function setCurrentMoveIndex(index: number) {
|
| 92 |
+
if (index < 0 || index > maxMoveIndex.value) return;
|
| 93 |
+
currentMoveIndex.value = index;
|
| 94 |
+
// Clear selection when moving to different move
|
| 95 |
+
selectedActionKey.value = null;
|
| 96 |
+
}
|
| 97 |
+
|
| 98 |
+
function nextMove() {
|
| 99 |
+
if (currentMoveIndex.value < maxMoveIndex.value) {
|
| 100 |
+
setCurrentMoveIndex(currentMoveIndex.value + 1);
|
| 101 |
+
}
|
| 102 |
+
}
|
| 103 |
+
|
| 104 |
+
function prevMove() {
|
| 105 |
+
if (currentMoveIndex.value > 0) {
|
| 106 |
+
setCurrentMoveIndex(currentMoveIndex.value - 1);
|
| 107 |
+
}
|
| 108 |
+
}
|
| 109 |
+
|
| 110 |
+
function firstMove() {
|
| 111 |
+
setCurrentMoveIndex(0);
|
| 112 |
+
}
|
| 113 |
+
|
| 114 |
+
function lastMove() {
|
| 115 |
+
setCurrentMoveIndex(maxMoveIndex.value);
|
| 116 |
+
}
|
| 117 |
+
|
| 118 |
+
function setSelectedStatistic(stat: MCTSStatisticType) {
|
| 119 |
+
selectedStatistic.value = stat;
|
| 120 |
+
}
|
| 121 |
+
|
| 122 |
+
function setColorScale(scale: ColorScaleType) {
|
| 123 |
+
colorScale.value = scale;
|
| 124 |
+
}
|
| 125 |
+
|
| 126 |
+
function setTopNFilter(n: number) {
|
| 127 |
+
topNFilter.value = Math.max(3, Math.min(50, n));
|
| 128 |
+
}
|
| 129 |
+
|
| 130 |
+
function selectAction(actionKey: string | null) {
|
| 131 |
+
selectedActionKey.value = actionKey;
|
| 132 |
+
}
|
| 133 |
+
|
| 134 |
+
function selectPosition(position: Position | null) {
|
| 135 |
+
if (!position) {
|
| 136 |
+
selectedActionKey.value = "pass";
|
| 137 |
+
return;
|
| 138 |
+
}
|
| 139 |
+
selectedActionKey.value = `${position.x},${position.y},${position.z}`;
|
| 140 |
+
}
|
| 141 |
+
|
| 142 |
+
|
| 143 |
+
// ============================================================================
|
| 144 |
+
// Return
|
| 145 |
+
// ============================================================================
|
| 146 |
+
|
| 147 |
+
return {
|
| 148 |
+
// State
|
| 149 |
+
gameHistory,
|
| 150 |
+
currentMoveIndex,
|
| 151 |
+
selectedStatistic,
|
| 152 |
+
colorScale,
|
| 153 |
+
topNFilter,
|
| 154 |
+
selectedActionKey,
|
| 155 |
+
|
| 156 |
+
// Getters
|
| 157 |
+
hasData,
|
| 158 |
+
currentMoveData,
|
| 159 |
+
maxMoveIndex,
|
| 160 |
+
currentStatistics,
|
| 161 |
+
topMoves,
|
| 162 |
+
totalVisits,
|
| 163 |
+
selectedMove,
|
| 164 |
+
|
| 165 |
+
// Actions
|
| 166 |
+
setGameHistory,
|
| 167 |
+
clearGameHistory,
|
| 168 |
+
setCurrentMoveIndex,
|
| 169 |
+
nextMove,
|
| 170 |
+
prevMove,
|
| 171 |
+
firstMove,
|
| 172 |
+
lastMove,
|
| 173 |
+
setSelectedStatistic,
|
| 174 |
+
setColorScale,
|
| 175 |
+
setTopNFilter,
|
| 176 |
+
selectAction,
|
| 177 |
+
selectPosition
|
| 178 |
+
};
|
| 179 |
+
});
|
trigo-web/app/src/stores/playerStore.ts
ADDED
|
@@ -0,0 +1,211 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Player Store
|
| 3 |
+
*
|
| 4 |
+
* Manages player state for multiplayer mode, including nickname, connection status,
|
| 5 |
+
* and opponent information.
|
| 6 |
+
*/
|
| 7 |
+
|
| 8 |
+
import { defineStore } from "pinia";
|
| 9 |
+
import { StorageKey, localStorageManager } from "@/utils/storage";
|
| 10 |
+
import { getRandomNickname } from "@/data/defaultNicknames";
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
/**
|
| 14 |
+
* Player state interface
|
| 15 |
+
*/
|
| 16 |
+
export interface PlayerState {
|
| 17 |
+
nickname: string; // Local player nickname
|
| 18 |
+
playerId: string | null; // Socket ID
|
| 19 |
+
playerColor: "black" | "white" | null; // Player's stone color in current game
|
| 20 |
+
roomId: string | null; // Current room ID
|
| 21 |
+
opponentNickname: string | null; // Opponent's nickname
|
| 22 |
+
connectionStatus: "disconnected" | "connected" | "in-room"; // Connection state
|
| 23 |
+
}
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
/**
|
| 27 |
+
* Nickname validation result
|
| 28 |
+
*/
|
| 29 |
+
interface ValidationResult {
|
| 30 |
+
valid: boolean;
|
| 31 |
+
error?: string;
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
/**
|
| 36 |
+
* Validate a nickname against the rules:
|
| 37 |
+
* - Length: 3-20 characters
|
| 38 |
+
* - Characters: alphanumeric + spaces only
|
| 39 |
+
* - No leading/trailing whitespace
|
| 40 |
+
*/
|
| 41 |
+
function validateNickname(nickname: string): ValidationResult {
|
| 42 |
+
if (!nickname || typeof nickname !== "string") {
|
| 43 |
+
return { valid: false, error: "Nickname is required" };
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
+
if (nickname.length < 3) {
|
| 47 |
+
return { valid: false, error: "Nickname must be at least 3 characters" };
|
| 48 |
+
}
|
| 49 |
+
|
| 50 |
+
if (nickname.length > 20) {
|
| 51 |
+
return { valid: false, error: "Nickname must be 20 characters or less" };
|
| 52 |
+
}
|
| 53 |
+
|
| 54 |
+
if (!/^[a-zA-Z0-9 ]+$/.test(nickname)) {
|
| 55 |
+
return { valid: false, error: "Only letters, numbers, and spaces allowed" };
|
| 56 |
+
}
|
| 57 |
+
|
| 58 |
+
if (nickname.trim() !== nickname) {
|
| 59 |
+
return { valid: false, error: "No leading or trailing spaces allowed" };
|
| 60 |
+
}
|
| 61 |
+
|
| 62 |
+
return { valid: true };
|
| 63 |
+
}
|
| 64 |
+
|
| 65 |
+
|
| 66 |
+
/**
|
| 67 |
+
* Load nickname from localStorage or generate a random one
|
| 68 |
+
*/
|
| 69 |
+
function loadNickname(): string {
|
| 70 |
+
const stored = localStorageManager.getString(StorageKey.PLAYER_NICKNAME);
|
| 71 |
+
if (stored) {
|
| 72 |
+
// Validate stored nickname
|
| 73 |
+
const validation = validateNickname(stored);
|
| 74 |
+
if (validation.valid) {
|
| 75 |
+
return stored;
|
| 76 |
+
}
|
| 77 |
+
}
|
| 78 |
+
// Generate random nickname if none exists or invalid
|
| 79 |
+
return getRandomNickname();
|
| 80 |
+
}
|
| 81 |
+
|
| 82 |
+
|
| 83 |
+
/**
|
| 84 |
+
* Save nickname to localStorage
|
| 85 |
+
*/
|
| 86 |
+
function saveNickname(nickname: string): void {
|
| 87 |
+
localStorageManager.setString(StorageKey.PLAYER_NICKNAME, nickname);
|
| 88 |
+
}
|
| 89 |
+
|
| 90 |
+
|
| 91 |
+
/**
|
| 92 |
+
* Player store
|
| 93 |
+
*/
|
| 94 |
+
export const usePlayerStore = defineStore("player", {
|
| 95 |
+
state: (): PlayerState => ({
|
| 96 |
+
nickname: loadNickname(),
|
| 97 |
+
playerId: null,
|
| 98 |
+
playerColor: null,
|
| 99 |
+
roomId: null,
|
| 100 |
+
opponentNickname: null,
|
| 101 |
+
connectionStatus: "disconnected"
|
| 102 |
+
}),
|
| 103 |
+
|
| 104 |
+
getters: {
|
| 105 |
+
/**
|
| 106 |
+
* Is player currently in a room?
|
| 107 |
+
*/
|
| 108 |
+
isInRoom: (state): boolean => state.connectionStatus === "in-room",
|
| 109 |
+
|
| 110 |
+
/**
|
| 111 |
+
* Does player have an opponent in the room?
|
| 112 |
+
*/
|
| 113 |
+
hasOpponent: (state): boolean => state.opponentNickname !== null,
|
| 114 |
+
|
| 115 |
+
/**
|
| 116 |
+
* Display name with fallback to "Guest"
|
| 117 |
+
*/
|
| 118 |
+
displayName: (state): string => state.nickname || "Guest",
|
| 119 |
+
|
| 120 |
+
/**
|
| 121 |
+
* Is player connected to socket server?
|
| 122 |
+
*/
|
| 123 |
+
isConnected: (state): boolean => state.connectionStatus !== "disconnected"
|
| 124 |
+
},
|
| 125 |
+
|
| 126 |
+
actions: {
|
| 127 |
+
/**
|
| 128 |
+
* Set the player's nickname
|
| 129 |
+
* @param nickname - New nickname to set
|
| 130 |
+
* @returns true if successful, false if validation failed
|
| 131 |
+
*/
|
| 132 |
+
setNickname(nickname: string): boolean {
|
| 133 |
+
const validation = validateNickname(nickname);
|
| 134 |
+
if (!validation.valid) {
|
| 135 |
+
console.warn(`[PlayerStore] Invalid nickname: ${validation.error}`);
|
| 136 |
+
return false;
|
| 137 |
+
}
|
| 138 |
+
|
| 139 |
+
this.nickname = nickname;
|
| 140 |
+
saveNickname(nickname);
|
| 141 |
+
return true;
|
| 142 |
+
},
|
| 143 |
+
|
| 144 |
+
/**
|
| 145 |
+
* Set the player's socket ID
|
| 146 |
+
* @param id - Socket ID from server
|
| 147 |
+
*/
|
| 148 |
+
setPlayerId(id: string): void {
|
| 149 |
+
this.playerId = id;
|
| 150 |
+
this.connectionStatus = "connected";
|
| 151 |
+
},
|
| 152 |
+
|
| 153 |
+
/**
|
| 154 |
+
* Join a room with the given ID and assigned color
|
| 155 |
+
* @param roomId - Room ID
|
| 156 |
+
* @param color - Assigned player color
|
| 157 |
+
*/
|
| 158 |
+
joinRoom(roomId: string, color: "black" | "white"): void {
|
| 159 |
+
this.roomId = roomId;
|
| 160 |
+
this.playerColor = color;
|
| 161 |
+
this.connectionStatus = "in-room";
|
| 162 |
+
},
|
| 163 |
+
|
| 164 |
+
/**
|
| 165 |
+
* Set the opponent's nickname
|
| 166 |
+
* @param nickname - Opponent's nickname
|
| 167 |
+
*/
|
| 168 |
+
setOpponentNickname(nickname: string): void {
|
| 169 |
+
this.opponentNickname = nickname;
|
| 170 |
+
},
|
| 171 |
+
|
| 172 |
+
/**
|
| 173 |
+
* Leave the current room
|
| 174 |
+
*/
|
| 175 |
+
leaveRoom(): void {
|
| 176 |
+
this.roomId = null;
|
| 177 |
+
this.playerColor = null;
|
| 178 |
+
this.opponentNickname = null;
|
| 179 |
+
this.connectionStatus = "connected";
|
| 180 |
+
},
|
| 181 |
+
|
| 182 |
+
/**
|
| 183 |
+
* Disconnect from the server
|
| 184 |
+
*/
|
| 185 |
+
disconnect(): void {
|
| 186 |
+
this.playerId = null;
|
| 187 |
+
this.roomId = null;
|
| 188 |
+
this.playerColor = null;
|
| 189 |
+
this.opponentNickname = null;
|
| 190 |
+
this.connectionStatus = "disconnected";
|
| 191 |
+
},
|
| 192 |
+
|
| 193 |
+
/**
|
| 194 |
+
* Reset player state to initial state
|
| 195 |
+
* Note: Nickname is preserved in localStorage
|
| 196 |
+
*/
|
| 197 |
+
reset(): void {
|
| 198 |
+
this.playerId = null;
|
| 199 |
+
this.playerColor = null;
|
| 200 |
+
this.roomId = null;
|
| 201 |
+
this.opponentNickname = null;
|
| 202 |
+
this.connectionStatus = "disconnected";
|
| 203 |
+
}
|
| 204 |
+
}
|
| 205 |
+
});
|
| 206 |
+
|
| 207 |
+
|
| 208 |
+
/**
|
| 209 |
+
* Export validation function for use in other components
|
| 210 |
+
*/
|
| 211 |
+
export { validateNickname };
|
trigo-web/app/src/styles/test-pages.scss
ADDED
|
@@ -0,0 +1,554 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Shared styles for test/analysis pages
|
| 3 |
+
*
|
| 4 |
+
* Provides consistent styling for:
|
| 5 |
+
* - MCTSAnalysisView
|
| 6 |
+
* - TrigoTreeTestView
|
| 7 |
+
* - TrigoAgentTestView
|
| 8 |
+
* - OnnxTestView
|
| 9 |
+
* - SocketTestView
|
| 10 |
+
*/
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
// ============================================================================
|
| 14 |
+
// Theme Variables
|
| 15 |
+
// ============================================================================
|
| 16 |
+
|
| 17 |
+
// Light theme colors
|
| 18 |
+
$light-bg-primary: #f5f5f5;
|
| 19 |
+
$light-bg-secondary: white;
|
| 20 |
+
$light-bg-tertiary: #f8f9fa;
|
| 21 |
+
$light-text-primary: #333;
|
| 22 |
+
$light-text-secondary: #666;
|
| 23 |
+
$light-border: #e1e4e8;
|
| 24 |
+
$light-shadow: rgba(0, 0, 0, 0.1);
|
| 25 |
+
|
| 26 |
+
// Dark theme colors
|
| 27 |
+
$dark-bg-primary: #1a1a1a;
|
| 28 |
+
$dark-bg-secondary: #2a2a2a;
|
| 29 |
+
$dark-bg-tertiary: #3a3a3a;
|
| 30 |
+
$dark-text-primary: #e0e0e0;
|
| 31 |
+
$dark-text-secondary: #a0a0a0;
|
| 32 |
+
$dark-border: #404040;
|
| 33 |
+
$dark-shadow: rgba(0, 0, 0, 0.3);
|
| 34 |
+
|
| 35 |
+
// Status colors (shared)
|
| 36 |
+
$color-success: #28a745;
|
| 37 |
+
$color-warning: #ffc107;
|
| 38 |
+
$color-error: #dc3545;
|
| 39 |
+
$color-info: #007bff;
|
| 40 |
+
$color-primary: #4a90e2;
|
| 41 |
+
$color-accent: #e94560;
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
// ============================================================================
|
| 45 |
+
// Custom Scrollbar Mixin
|
| 46 |
+
// ============================================================================
|
| 47 |
+
|
| 48 |
+
@mixin custom-scrollbar($theme: 'dark') {
|
| 49 |
+
@if $theme == 'dark' {
|
| 50 |
+
&::-webkit-scrollbar {
|
| 51 |
+
width: 8px;
|
| 52 |
+
height: 8px;
|
| 53 |
+
}
|
| 54 |
+
|
| 55 |
+
&::-webkit-scrollbar-track {
|
| 56 |
+
background: $dark-bg-primary;
|
| 57 |
+
border-radius: 4px;
|
| 58 |
+
}
|
| 59 |
+
|
| 60 |
+
&::-webkit-scrollbar-thumb {
|
| 61 |
+
background: #4a4a4a;
|
| 62 |
+
border-radius: 4px;
|
| 63 |
+
|
| 64 |
+
&:hover {
|
| 65 |
+
background: #5a5a5a;
|
| 66 |
+
}
|
| 67 |
+
}
|
| 68 |
+
} @else {
|
| 69 |
+
&::-webkit-scrollbar {
|
| 70 |
+
width: 8px;
|
| 71 |
+
height: 8px;
|
| 72 |
+
}
|
| 73 |
+
|
| 74 |
+
&::-webkit-scrollbar-track {
|
| 75 |
+
background: $light-bg-tertiary;
|
| 76 |
+
border-radius: 4px;
|
| 77 |
+
}
|
| 78 |
+
|
| 79 |
+
&::-webkit-scrollbar-thumb {
|
| 80 |
+
background: #c0c0c0;
|
| 81 |
+
border-radius: 4px;
|
| 82 |
+
|
| 83 |
+
&:hover {
|
| 84 |
+
background: #a0a0a0;
|
| 85 |
+
}
|
| 86 |
+
}
|
| 87 |
+
}
|
| 88 |
+
}
|
| 89 |
+
|
| 90 |
+
|
| 91 |
+
// ============================================================================
|
| 92 |
+
// Base Container Styles
|
| 93 |
+
// ============================================================================
|
| 94 |
+
|
| 95 |
+
@mixin test-page-container($theme: 'light') {
|
| 96 |
+
width: 100%;
|
| 97 |
+
height: 100vh;
|
| 98 |
+
overflow-y: auto;
|
| 99 |
+
overflow-x: hidden;
|
| 100 |
+
|
| 101 |
+
@if $theme == 'dark' {
|
| 102 |
+
background-color: $dark-bg-primary;
|
| 103 |
+
color: $dark-text-primary;
|
| 104 |
+
} @else {
|
| 105 |
+
background-color: $light-bg-primary;
|
| 106 |
+
color: $light-text-primary;
|
| 107 |
+
}
|
| 108 |
+
|
| 109 |
+
@include custom-scrollbar($theme);
|
| 110 |
+
}
|
| 111 |
+
|
| 112 |
+
|
| 113 |
+
// ============================================================================
|
| 114 |
+
// Header Styles
|
| 115 |
+
// ============================================================================
|
| 116 |
+
|
| 117 |
+
@mixin test-page-header() {
|
| 118 |
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
| 119 |
+
color: white;
|
| 120 |
+
padding: 2rem;
|
| 121 |
+
text-align: center;
|
| 122 |
+
|
| 123 |
+
h1 {
|
| 124 |
+
margin: 0 0 0.5rem 0;
|
| 125 |
+
font-size: 2rem;
|
| 126 |
+
font-weight: 700;
|
| 127 |
+
}
|
| 128 |
+
|
| 129 |
+
.subtitle {
|
| 130 |
+
margin: 0;
|
| 131 |
+
opacity: 0.9;
|
| 132 |
+
font-size: 1.1rem;
|
| 133 |
+
}
|
| 134 |
+
}
|
| 135 |
+
|
| 136 |
+
|
| 137 |
+
// ============================================================================
|
| 138 |
+
// Section Styles
|
| 139 |
+
// ============================================================================
|
| 140 |
+
|
| 141 |
+
@mixin test-section($theme: 'light') {
|
| 142 |
+
border-radius: 8px;
|
| 143 |
+
padding: 20px;
|
| 144 |
+
margin-bottom: 20px;
|
| 145 |
+
|
| 146 |
+
@if $theme == 'dark' {
|
| 147 |
+
background-color: $dark-bg-secondary;
|
| 148 |
+
border: 1px solid $dark-border;
|
| 149 |
+
} @else {
|
| 150 |
+
background-color: $light-bg-secondary;
|
| 151 |
+
border: 1px solid $light-border;
|
| 152 |
+
box-shadow: 0 2px 8px $light-shadow;
|
| 153 |
+
}
|
| 154 |
+
|
| 155 |
+
h2, h3 {
|
| 156 |
+
margin-top: 0;
|
| 157 |
+
margin-bottom: 15px;
|
| 158 |
+
font-weight: 600;
|
| 159 |
+
|
| 160 |
+
@if $theme == 'dark' {
|
| 161 |
+
color: $color-primary;
|
| 162 |
+
} @else {
|
| 163 |
+
color: $light-text-primary;
|
| 164 |
+
}
|
| 165 |
+
}
|
| 166 |
+
|
| 167 |
+
h2 {
|
| 168 |
+
font-size: 20px;
|
| 169 |
+
}
|
| 170 |
+
|
| 171 |
+
h3 {
|
| 172 |
+
font-size: 16px;
|
| 173 |
+
}
|
| 174 |
+
|
| 175 |
+
p {
|
| 176 |
+
margin: 0 0 1rem 0;
|
| 177 |
+
|
| 178 |
+
@if $theme == 'dark' {
|
| 179 |
+
color: $dark-text-secondary;
|
| 180 |
+
} @else {
|
| 181 |
+
color: $light-text-secondary;
|
| 182 |
+
}
|
| 183 |
+
}
|
| 184 |
+
}
|
| 185 |
+
|
| 186 |
+
|
| 187 |
+
// ============================================================================
|
| 188 |
+
// Panel Styles (for sidebars/control panels)
|
| 189 |
+
// ============================================================================
|
| 190 |
+
|
| 191 |
+
@mixin control-panel($theme: 'dark') {
|
| 192 |
+
border-radius: 8px;
|
| 193 |
+
padding: 20px;
|
| 194 |
+
overflow-y: auto;
|
| 195 |
+
max-height: calc(100vh - 80px);
|
| 196 |
+
|
| 197 |
+
@if $theme == 'dark' {
|
| 198 |
+
background-color: $dark-bg-secondary;
|
| 199 |
+
} @else {
|
| 200 |
+
background-color: $light-bg-secondary;
|
| 201 |
+
border: 1px solid $light-border;
|
| 202 |
+
}
|
| 203 |
+
|
| 204 |
+
@include custom-scrollbar($theme);
|
| 205 |
+
|
| 206 |
+
h2 {
|
| 207 |
+
margin: 0 0 20px 0;
|
| 208 |
+
font-size: 20px;
|
| 209 |
+
padding-bottom: 10px;
|
| 210 |
+
|
| 211 |
+
@if $theme == 'dark' {
|
| 212 |
+
color: $color-accent;
|
| 213 |
+
border-bottom: 1px solid $dark-border;
|
| 214 |
+
} @else {
|
| 215 |
+
color: $light-text-primary;
|
| 216 |
+
border-bottom: 1px solid $light-border;
|
| 217 |
+
}
|
| 218 |
+
}
|
| 219 |
+
|
| 220 |
+
h3 {
|
| 221 |
+
margin: 0 0 15px 0;
|
| 222 |
+
font-size: 16px;
|
| 223 |
+
color: $color-primary;
|
| 224 |
+
}
|
| 225 |
+
}
|
| 226 |
+
|
| 227 |
+
|
| 228 |
+
// ============================================================================
|
| 229 |
+
// Button Styles
|
| 230 |
+
// ============================================================================
|
| 231 |
+
|
| 232 |
+
@mixin test-button($variant: 'primary') {
|
| 233 |
+
padding: 0.75rem 1.5rem;
|
| 234 |
+
border: none;
|
| 235 |
+
border-radius: 8px;
|
| 236 |
+
font-weight: 600;
|
| 237 |
+
font-size: 1rem;
|
| 238 |
+
cursor: pointer;
|
| 239 |
+
transition: all 0.3s ease;
|
| 240 |
+
|
| 241 |
+
&:disabled {
|
| 242 |
+
opacity: 0.5;
|
| 243 |
+
cursor: not-allowed;
|
| 244 |
+
}
|
| 245 |
+
|
| 246 |
+
@if $variant == 'primary' {
|
| 247 |
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
| 248 |
+
color: white;
|
| 249 |
+
|
| 250 |
+
&:hover:not(:disabled) {
|
| 251 |
+
transform: translateY(-2px);
|
| 252 |
+
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
|
| 253 |
+
}
|
| 254 |
+
} @else if $variant == 'secondary' {
|
| 255 |
+
background: #6c757d;
|
| 256 |
+
color: white;
|
| 257 |
+
|
| 258 |
+
&:hover:not(:disabled) {
|
| 259 |
+
background: #5a6268;
|
| 260 |
+
}
|
| 261 |
+
} @else if $variant == 'success' {
|
| 262 |
+
background: $color-success;
|
| 263 |
+
color: white;
|
| 264 |
+
|
| 265 |
+
&:hover:not(:disabled) {
|
| 266 |
+
background: darken($color-success, 10%);
|
| 267 |
+
transform: translateY(-2px);
|
| 268 |
+
box-shadow: 0 4px 12px rgba($color-success, 0.4);
|
| 269 |
+
}
|
| 270 |
+
} @else if $variant == 'test' {
|
| 271 |
+
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
|
| 272 |
+
color: white;
|
| 273 |
+
|
| 274 |
+
&:hover:not(:disabled) {
|
| 275 |
+
transform: translateY(-2px);
|
| 276 |
+
box-shadow: 0 4px 12px rgba(245, 87, 108, 0.4);
|
| 277 |
+
}
|
| 278 |
+
}
|
| 279 |
+
}
|
| 280 |
+
|
| 281 |
+
|
| 282 |
+
// ============================================================================
|
| 283 |
+
// Status Display
|
| 284 |
+
// ============================================================================
|
| 285 |
+
|
| 286 |
+
@mixin status-display($theme: 'light') {
|
| 287 |
+
border-radius: 8px;
|
| 288 |
+
padding: 1rem;
|
| 289 |
+
margin-bottom: 1rem;
|
| 290 |
+
|
| 291 |
+
@if $theme == 'dark' {
|
| 292 |
+
background-color: $dark-bg-primary;
|
| 293 |
+
} @else {
|
| 294 |
+
background-color: $light-bg-tertiary;
|
| 295 |
+
}
|
| 296 |
+
|
| 297 |
+
.status-item {
|
| 298 |
+
display: flex;
|
| 299 |
+
justify-content: space-between;
|
| 300 |
+
padding: 0.5rem 0;
|
| 301 |
+
|
| 302 |
+
@if $theme == 'dark' {
|
| 303 |
+
border-bottom: 1px solid $dark-border;
|
| 304 |
+
} @else {
|
| 305 |
+
border-bottom: 1px solid #e9ecef;
|
| 306 |
+
}
|
| 307 |
+
|
| 308 |
+
&:last-child {
|
| 309 |
+
border-bottom: none;
|
| 310 |
+
}
|
| 311 |
+
|
| 312 |
+
.status-label {
|
| 313 |
+
font-weight: 600;
|
| 314 |
+
|
| 315 |
+
@if $theme == 'dark' {
|
| 316 |
+
color: $dark-text-secondary;
|
| 317 |
+
} @else {
|
| 318 |
+
color: $light-text-secondary;
|
| 319 |
+
}
|
| 320 |
+
}
|
| 321 |
+
|
| 322 |
+
.status-value {
|
| 323 |
+
@if $theme == 'dark' {
|
| 324 |
+
color: $dark-text-primary;
|
| 325 |
+
} @else {
|
| 326 |
+
color: $light-text-primary;
|
| 327 |
+
}
|
| 328 |
+
}
|
| 329 |
+
|
| 330 |
+
&.ready .status-value {
|
| 331 |
+
color: $color-success;
|
| 332 |
+
font-weight: 600;
|
| 333 |
+
}
|
| 334 |
+
|
| 335 |
+
&.loading .status-value {
|
| 336 |
+
color: $color-warning;
|
| 337 |
+
font-weight: 600;
|
| 338 |
+
}
|
| 339 |
+
|
| 340 |
+
&.error .status-value {
|
| 341 |
+
color: $color-error;
|
| 342 |
+
font-weight: 600;
|
| 343 |
+
}
|
| 344 |
+
}
|
| 345 |
+
}
|
| 346 |
+
|
| 347 |
+
|
| 348 |
+
// ============================================================================
|
| 349 |
+
// Error/Success Messages
|
| 350 |
+
// ============================================================================
|
| 351 |
+
|
| 352 |
+
@mixin error-message() {
|
| 353 |
+
background: #fff3cd;
|
| 354 |
+
border: 1px solid #ffeaa7;
|
| 355 |
+
border-radius: 6px;
|
| 356 |
+
padding: 0.75rem;
|
| 357 |
+
color: #856404;
|
| 358 |
+
margin-top: 1rem;
|
| 359 |
+
}
|
| 360 |
+
|
| 361 |
+
@mixin success-message() {
|
| 362 |
+
background: #d4edda;
|
| 363 |
+
border: 1px solid #c3e6cb;
|
| 364 |
+
border-radius: 6px;
|
| 365 |
+
padding: 0.75rem;
|
| 366 |
+
color: #155724;
|
| 367 |
+
font-weight: 600;
|
| 368 |
+
margin-top: 1rem;
|
| 369 |
+
}
|
| 370 |
+
|
| 371 |
+
|
| 372 |
+
// ============================================================================
|
| 373 |
+
// Table Styles
|
| 374 |
+
// ============================================================================
|
| 375 |
+
|
| 376 |
+
@mixin data-table($theme: 'light') {
|
| 377 |
+
width: 100%;
|
| 378 |
+
border-collapse: collapse;
|
| 379 |
+
font-size: 12px;
|
| 380 |
+
|
| 381 |
+
thead {
|
| 382 |
+
position: sticky;
|
| 383 |
+
top: 0;
|
| 384 |
+
z-index: 1;
|
| 385 |
+
|
| 386 |
+
@if $theme == 'dark' {
|
| 387 |
+
background-color: $dark-bg-secondary;
|
| 388 |
+
} @else {
|
| 389 |
+
background-color: $light-bg-tertiary;
|
| 390 |
+
}
|
| 391 |
+
|
| 392 |
+
th {
|
| 393 |
+
padding: 8px 6px;
|
| 394 |
+
text-align: left;
|
| 395 |
+
font-weight: 600;
|
| 396 |
+
|
| 397 |
+
@if $theme == 'dark' {
|
| 398 |
+
color: $color-primary;
|
| 399 |
+
border-bottom: 1px solid $dark-border;
|
| 400 |
+
} @else {
|
| 401 |
+
color: $light-text-secondary;
|
| 402 |
+
border-bottom: 2px solid #dee2e6;
|
| 403 |
+
}
|
| 404 |
+
}
|
| 405 |
+
}
|
| 406 |
+
|
| 407 |
+
tbody {
|
| 408 |
+
tr {
|
| 409 |
+
cursor: pointer;
|
| 410 |
+
transition: background-color 0.2s;
|
| 411 |
+
|
| 412 |
+
@if $theme == 'dark' {
|
| 413 |
+
&:hover {
|
| 414 |
+
background-color: $dark-bg-tertiary;
|
| 415 |
+
}
|
| 416 |
+
|
| 417 |
+
&.selected {
|
| 418 |
+
background-color: #4a3030;
|
| 419 |
+
|
| 420 |
+
td {
|
| 421 |
+
color: $color-accent;
|
| 422 |
+
font-weight: 500;
|
| 423 |
+
}
|
| 424 |
+
}
|
| 425 |
+
} @else {
|
| 426 |
+
&:hover {
|
| 427 |
+
background-color: $light-bg-tertiary;
|
| 428 |
+
}
|
| 429 |
+
|
| 430 |
+
&.selected {
|
| 431 |
+
background-color: #e7f5ff;
|
| 432 |
+
|
| 433 |
+
td {
|
| 434 |
+
color: $color-info;
|
| 435 |
+
font-weight: 500;
|
| 436 |
+
}
|
| 437 |
+
}
|
| 438 |
+
}
|
| 439 |
+
}
|
| 440 |
+
|
| 441 |
+
td {
|
| 442 |
+
padding: 8px 6px;
|
| 443 |
+
|
| 444 |
+
@if $theme == 'dark' {
|
| 445 |
+
color: #c0c0c0;
|
| 446 |
+
border-bottom: 1px solid #303030;
|
| 447 |
+
} @else {
|
| 448 |
+
color: $light-text-primary;
|
| 449 |
+
border-bottom: 1px solid #dee2e6;
|
| 450 |
+
}
|
| 451 |
+
}
|
| 452 |
+
}
|
| 453 |
+
}
|
| 454 |
+
|
| 455 |
+
|
| 456 |
+
// ============================================================================
|
| 457 |
+
// Input Styles
|
| 458 |
+
// ============================================================================
|
| 459 |
+
|
| 460 |
+
@mixin input-field($theme: 'light') {
|
| 461 |
+
width: 100%;
|
| 462 |
+
padding: 0.75rem;
|
| 463 |
+
border-radius: 8px;
|
| 464 |
+
font-size: 1rem;
|
| 465 |
+
transition: border-color 0.3s ease;
|
| 466 |
+
|
| 467 |
+
@if $theme == 'dark' {
|
| 468 |
+
background-color: $dark-bg-primary;
|
| 469 |
+
border: 2px solid $dark-border;
|
| 470 |
+
color: $dark-text-primary;
|
| 471 |
+
|
| 472 |
+
&:focus {
|
| 473 |
+
outline: none;
|
| 474 |
+
border-color: $color-primary;
|
| 475 |
+
}
|
| 476 |
+
|
| 477 |
+
&:disabled {
|
| 478 |
+
background-color: $dark-bg-tertiary;
|
| 479 |
+
cursor: not-allowed;
|
| 480 |
+
}
|
| 481 |
+
} @else {
|
| 482 |
+
background-color: white;
|
| 483 |
+
border: 2px solid #e9ecef;
|
| 484 |
+
color: $light-text-primary;
|
| 485 |
+
|
| 486 |
+
&:focus {
|
| 487 |
+
outline: none;
|
| 488 |
+
border-color: #667eea;
|
| 489 |
+
}
|
| 490 |
+
|
| 491 |
+
&:disabled {
|
| 492 |
+
background-color: $light-bg-tertiary;
|
| 493 |
+
cursor: not-allowed;
|
| 494 |
+
}
|
| 495 |
+
}
|
| 496 |
+
}
|
| 497 |
+
|
| 498 |
+
|
| 499 |
+
// ============================================================================
|
| 500 |
+
// Code/Debug Display
|
| 501 |
+
// ============================================================================
|
| 502 |
+
|
| 503 |
+
@mixin code-display($theme: 'light') {
|
| 504 |
+
border-radius: 8px;
|
| 505 |
+
padding: 1rem;
|
| 506 |
+
font-family: "Courier New", monospace;
|
| 507 |
+
font-size: 0.9rem;
|
| 508 |
+
overflow-x: auto;
|
| 509 |
+
white-space: pre-wrap;
|
| 510 |
+
word-break: break-word;
|
| 511 |
+
margin: 0;
|
| 512 |
+
|
| 513 |
+
@if $theme == 'dark' {
|
| 514 |
+
background-color: $dark-bg-primary;
|
| 515 |
+
color: $dark-text-primary;
|
| 516 |
+
border: 1px solid $dark-border;
|
| 517 |
+
} @else {
|
| 518 |
+
background-color: white;
|
| 519 |
+
color: $light-text-primary;
|
| 520 |
+
border: 1px solid $light-border;
|
| 521 |
+
}
|
| 522 |
+
}
|
| 523 |
+
|
| 524 |
+
|
| 525 |
+
// ============================================================================
|
| 526 |
+
// Utility Classes
|
| 527 |
+
// ============================================================================
|
| 528 |
+
|
| 529 |
+
.no-data-message {
|
| 530 |
+
text-align: center;
|
| 531 |
+
padding: 2rem;
|
| 532 |
+
font-size: 14px;
|
| 533 |
+
font-style: italic;
|
| 534 |
+
}
|
| 535 |
+
|
| 536 |
+
.panel-section {
|
| 537 |
+
margin-bottom: 30px;
|
| 538 |
+
|
| 539 |
+
&:last-child {
|
| 540 |
+
margin-bottom: 0;
|
| 541 |
+
}
|
| 542 |
+
}
|
| 543 |
+
|
| 544 |
+
.button-group {
|
| 545 |
+
display: flex;
|
| 546 |
+
gap: 0.5rem;
|
| 547 |
+
flex-wrap: wrap;
|
| 548 |
+
}
|
| 549 |
+
|
| 550 |
+
.info-text {
|
| 551 |
+
font-size: 13px;
|
| 552 |
+
line-height: 1.5;
|
| 553 |
+
margin: 0 0 15px 0;
|
| 554 |
+
}
|
trigo-web/app/src/types/mcts.ts
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* TypeScript type definitions for MCTS visualization
|
| 3 |
+
*/
|
| 4 |
+
|
| 5 |
+
import type { TrigoGame } from "../../../inc/trigo/game";
|
| 6 |
+
import type { Position } from "../../../inc/trigo/types";
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
/**
|
| 10 |
+
* Statistics for a single move (position) at a given game state
|
| 11 |
+
*/
|
| 12 |
+
export interface MCTSMoveStatistic {
|
| 13 |
+
position: Position | null; // null for pass move
|
| 14 |
+
actionKey: string; // "x,y,z" or "pass"
|
| 15 |
+
N: number; // Visit count
|
| 16 |
+
pi: number; // Search policy (normalized visit count)
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
/**
|
| 21 |
+
* Complete MCTS data for a single move in the game
|
| 22 |
+
*/
|
| 23 |
+
export interface MCTSMoveData {
|
| 24 |
+
moveNumber: number; // Move index (0-based)
|
| 25 |
+
player: "black" | "white"; // Player to move
|
| 26 |
+
gameState: TrigoGame; // Game state at this move
|
| 27 |
+
statistics: MCTSMoveStatistic[]; // Statistics for all legal moves
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
/**
|
| 32 |
+
* Statistic type for visualization selection
|
| 33 |
+
*/
|
| 34 |
+
export type MCTSStatisticType = "N" | "pi";
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
/**
|
| 38 |
+
* Color scale type for heatmap
|
| 39 |
+
*/
|
| 40 |
+
export type ColorScaleType = "linear" | "log";
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
/**
|
| 44 |
+
* Tree node for D3 visualization
|
| 45 |
+
*/
|
| 46 |
+
export interface MCTSTreeNode {
|
| 47 |
+
id: string;
|
| 48 |
+
label: string;
|
| 49 |
+
N: number;
|
| 50 |
+
pi: number;
|
| 51 |
+
children: MCTSTreeNode[];
|
| 52 |
+
}
|
trigo-web/app/src/utils/mctsColorScale.ts
ADDED
|
@@ -0,0 +1,184 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Color scale utilities for MCTS visualization
|
| 3 |
+
*
|
| 4 |
+
* Uses D3 color scales to map statistics to colors
|
| 5 |
+
*/
|
| 6 |
+
|
| 7 |
+
import * as d3 from "d3";
|
| 8 |
+
import * as d3Chromatic from "d3-scale-chromatic";
|
| 9 |
+
import type { MCTSStatisticType, ColorScaleType } from "@/types/mcts";
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
/**
|
| 13 |
+
* Get color for a statistic value
|
| 14 |
+
*
|
| 15 |
+
* @param value - The statistic value
|
| 16 |
+
* @param statistic - Which statistic (N or pi)
|
| 17 |
+
* @param min - Minimum value in the range
|
| 18 |
+
* @param max - Maximum value in the range
|
| 19 |
+
* @param scale - Linear or logarithmic scale
|
| 20 |
+
* @returns CSS color string
|
| 21 |
+
*/
|
| 22 |
+
export function getColorForStatistic(
|
| 23 |
+
value: number,
|
| 24 |
+
statistic: MCTSStatisticType,
|
| 25 |
+
min: number,
|
| 26 |
+
max: number,
|
| 27 |
+
scale: ColorScaleType = "linear"
|
| 28 |
+
): string {
|
| 29 |
+
// Handle edge cases
|
| 30 |
+
if (!Number.isFinite(value) || max === min) {
|
| 31 |
+
return "#808080"; // Gray for undefined/invalid
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
// Normalize value to [0, 1]
|
| 35 |
+
let normalized: number;
|
| 36 |
+
|
| 37 |
+
if (scale === "log") {
|
| 38 |
+
// Logarithmic scale (useful for visit counts with wide range)
|
| 39 |
+
const logMin = Math.log(Math.max(min, 1));
|
| 40 |
+
const logMax = Math.log(Math.max(max, 1));
|
| 41 |
+
const logValue = Math.log(Math.max(value, 1));
|
| 42 |
+
normalized = (logValue - logMin) / (logMax - logMin);
|
| 43 |
+
} else {
|
| 44 |
+
// Linear scale
|
| 45 |
+
normalized = (value - min) / (max - min);
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
// Clamp to [0, 1]
|
| 49 |
+
normalized = Math.max(0, Math.min(1, normalized));
|
| 50 |
+
|
| 51 |
+
// Apply color scale based on statistic type
|
| 52 |
+
switch (statistic) {
|
| 53 |
+
case "N":
|
| 54 |
+
// Visit counts: Blue scale (light to dark)
|
| 55 |
+
return d3Chromatic.interpolateBlues(normalized * 0.9 + 0.1);
|
| 56 |
+
|
| 57 |
+
case "pi":
|
| 58 |
+
// Search policy: Yellow-Orange-Red scale
|
| 59 |
+
return d3Chromatic.interpolateYlOrRd(normalized * 0.9 + 0.1);
|
| 60 |
+
|
| 61 |
+
default:
|
| 62 |
+
return "#808080";
|
| 63 |
+
}
|
| 64 |
+
}
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
/**
|
| 68 |
+
* Create a D3 color scale for a statistic
|
| 69 |
+
*
|
| 70 |
+
* @param statistic - Which statistic
|
| 71 |
+
* @param min - Minimum value
|
| 72 |
+
* @param max - Maximum value
|
| 73 |
+
* @param scaleType - Linear or logarithmic
|
| 74 |
+
* @returns D3 scale function
|
| 75 |
+
*/
|
| 76 |
+
export function createColorScale(
|
| 77 |
+
statistic: MCTSStatisticType,
|
| 78 |
+
min: number,
|
| 79 |
+
max: number,
|
| 80 |
+
scaleType: ColorScaleType = "linear"
|
| 81 |
+
) {
|
| 82 |
+
// Create domain based on scale type
|
| 83 |
+
let domain: number[];
|
| 84 |
+
|
| 85 |
+
if (scaleType === "log") {
|
| 86 |
+
const logMin = Math.log(Math.max(min, 1));
|
| 87 |
+
const logMax = Math.log(Math.max(max, 1));
|
| 88 |
+
domain = [logMin, logMax];
|
| 89 |
+
} else {
|
| 90 |
+
domain = [min, max];
|
| 91 |
+
}
|
| 92 |
+
|
| 93 |
+
// Create range based on statistic
|
| 94 |
+
let range: string[];
|
| 95 |
+
|
| 96 |
+
switch (statistic) {
|
| 97 |
+
case "N":
|
| 98 |
+
// Blues
|
| 99 |
+
range = [d3Chromatic.interpolateBlues(0.1), d3Chromatic.interpolateBlues(0.9)];
|
| 100 |
+
break;
|
| 101 |
+
|
| 102 |
+
case "pi":
|
| 103 |
+
// Yellow-Orange-Red
|
| 104 |
+
range = [d3Chromatic.interpolateYlOrRd(0.1), d3Chromatic.interpolateYlOrRd(0.9)];
|
| 105 |
+
break;
|
| 106 |
+
|
| 107 |
+
default:
|
| 108 |
+
range = ["#e0e0e0", "#404040"];
|
| 109 |
+
}
|
| 110 |
+
|
| 111 |
+
// Create scale
|
| 112 |
+
if (scaleType === "log") {
|
| 113 |
+
return d3.scaleLinear().domain(domain).range(range);
|
| 114 |
+
} else {
|
| 115 |
+
return d3.scaleLinear().domain(domain).range(range);
|
| 116 |
+
}
|
| 117 |
+
}
|
| 118 |
+
|
| 119 |
+
|
| 120 |
+
/**
|
| 121 |
+
* Generate legend color stops for display
|
| 122 |
+
*
|
| 123 |
+
* @param statistic - Which statistic
|
| 124 |
+
* @param min - Minimum value
|
| 125 |
+
* @param max - Maximum value
|
| 126 |
+
* @param steps - Number of color stops (default: 10)
|
| 127 |
+
* @returns Array of {value, color} objects
|
| 128 |
+
*/
|
| 129 |
+
export function generateLegendStops(
|
| 130 |
+
statistic: MCTSStatisticType,
|
| 131 |
+
min: number,
|
| 132 |
+
max: number,
|
| 133 |
+
steps: number = 10
|
| 134 |
+
): Array<{ value: number; color: string; label: string }> {
|
| 135 |
+
const stops: Array<{ value: number; color: string; label: string }> = [];
|
| 136 |
+
|
| 137 |
+
for (let i = 0; i <= steps; i++) {
|
| 138 |
+
const t = i / steps;
|
| 139 |
+
const value = min + (max - min) * t;
|
| 140 |
+
const color = getColorForStatistic(value, statistic, min, max, "linear");
|
| 141 |
+
|
| 142 |
+
// Format label based on statistic type
|
| 143 |
+
let label: string;
|
| 144 |
+
if (statistic === "pi") {
|
| 145 |
+
label = `${(value * 100).toFixed(1)}%`;
|
| 146 |
+
} else {
|
| 147 |
+
label = value < 10 ? value.toFixed(1) : Math.round(value).toString();
|
| 148 |
+
}
|
| 149 |
+
|
| 150 |
+
stops.push({ value, color, label });
|
| 151 |
+
}
|
| 152 |
+
|
| 153 |
+
return stops;
|
| 154 |
+
}
|
| 155 |
+
|
| 156 |
+
|
| 157 |
+
/**
|
| 158 |
+
* Get appropriate label for a statistic
|
| 159 |
+
*/
|
| 160 |
+
export function getStatisticLabel(statistic: MCTSStatisticType): string {
|
| 161 |
+
switch (statistic) {
|
| 162 |
+
case "N":
|
| 163 |
+
return "Visit Count";
|
| 164 |
+
case "pi":
|
| 165 |
+
return "Search Policy π";
|
| 166 |
+
default:
|
| 167 |
+
return "Unknown";
|
| 168 |
+
}
|
| 169 |
+
}
|
| 170 |
+
|
| 171 |
+
|
| 172 |
+
/**
|
| 173 |
+
* Get descriptive text for a statistic
|
| 174 |
+
*/
|
| 175 |
+
export function getStatisticDescription(statistic: MCTSStatisticType): string {
|
| 176 |
+
switch (statistic) {
|
| 177 |
+
case "N":
|
| 178 |
+
return "Number of times MCTS explored this move";
|
| 179 |
+
case "pi":
|
| 180 |
+
return "Final probability of selecting this move after search";
|
| 181 |
+
default:
|
| 182 |
+
return "";
|
| 183 |
+
}
|
| 184 |
+
}
|
trigo-web/app/src/utils/mctsDataParser.ts
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* MCTS Data Parser
|
| 3 |
+
*
|
| 4 |
+
* Parses TGN game files and visit count JSON files to reconstruct
|
| 5 |
+
* move-by-move MCTS statistics.
|
| 6 |
+
*/
|
| 7 |
+
|
| 8 |
+
import { TrigoGame } from "../../../inc/trigo/game";
|
| 9 |
+
import type { MCTSMoveData, MCTSMoveStatistic } from "../types/mcts";
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
/**
|
| 13 |
+
* Parse MCTS data from TGN and visit counts JSON
|
| 14 |
+
*
|
| 15 |
+
* @param tgnContent - TGN game notation string
|
| 16 |
+
* @param visitCountsJson - JSON string containing visit counts array
|
| 17 |
+
* @returns Array of MCTS move data for each move in the game
|
| 18 |
+
* @throws Error if parsing fails or data is invalid
|
| 19 |
+
*/
|
| 20 |
+
export async function parseMCTSData(
|
| 21 |
+
tgnContent: string,
|
| 22 |
+
visitCountsJson: string
|
| 23 |
+
): Promise<MCTSMoveData[]> {
|
| 24 |
+
// 1. Parse TGN to get game
|
| 25 |
+
let game: TrigoGame;
|
| 26 |
+
try {
|
| 27 |
+
game = TrigoGame.fromTGN(tgnContent);
|
| 28 |
+
} catch (error) {
|
| 29 |
+
throw new Error(`Failed to parse TGN file: ${error instanceof Error ? error.message : String(error)}`);
|
| 30 |
+
}
|
| 31 |
+
|
| 32 |
+
// 2. Validate board shape (must be 2D)
|
| 33 |
+
const shape = game.getShape();
|
| 34 |
+
if (shape.z !== 1) {
|
| 35 |
+
throw new Error(
|
| 36 |
+
`Only 2D boards (z=1) are supported. This game has shape ${shape.x}×${shape.y}×${shape.z}`
|
| 37 |
+
);
|
| 38 |
+
}
|
| 39 |
+
|
| 40 |
+
// 3. Parse visit counts JSON
|
| 41 |
+
let visitCounts: number[][];
|
| 42 |
+
try {
|
| 43 |
+
visitCounts = JSON.parse(visitCountsJson);
|
| 44 |
+
|
| 45 |
+
if (!Array.isArray(visitCounts)) {
|
| 46 |
+
throw new Error("Visit counts must be an array");
|
| 47 |
+
}
|
| 48 |
+
|
| 49 |
+
if (visitCounts.length === 0) {
|
| 50 |
+
throw new Error("Visit counts array is empty");
|
| 51 |
+
}
|
| 52 |
+
|
| 53 |
+
// Validate that each element is an array of numbers
|
| 54 |
+
for (let i = 0; i < visitCounts.length; i++) {
|
| 55 |
+
if (!Array.isArray(visitCounts[i])) {
|
| 56 |
+
throw new Error(`Visit counts at index ${i} is not an array`);
|
| 57 |
+
}
|
| 58 |
+
}
|
| 59 |
+
} catch (error) {
|
| 60 |
+
throw new Error(`Failed to parse visit counts JSON: ${error instanceof Error ? error.message : String(error)}`);
|
| 61 |
+
}
|
| 62 |
+
|
| 63 |
+
// 4. Replay game move-by-move and map visit counts to positions
|
| 64 |
+
const result: MCTSMoveData[] = [];
|
| 65 |
+
const replayGame = new TrigoGame(shape, {});
|
| 66 |
+
const stepHistory = game.getHistory();
|
| 67 |
+
|
| 68 |
+
// Ensure we have visit counts for each move
|
| 69 |
+
if (visitCounts.length !== stepHistory.length) {
|
| 70 |
+
console.warn(
|
| 71 |
+
`Visit counts length (${visitCounts.length}) doesn't match step history length (${stepHistory.length}). Using minimum.`
|
| 72 |
+
);
|
| 73 |
+
}
|
| 74 |
+
|
| 75 |
+
const minLength = Math.min(visitCounts.length, stepHistory.length);
|
| 76 |
+
|
| 77 |
+
for (let i = 0; i < minLength; i++) {
|
| 78 |
+
// Get current player
|
| 79 |
+
const currentPlayer = replayGame.getCurrentPlayer() === 1 ? "black" : "white";
|
| 80 |
+
|
| 81 |
+
// Get all valid positions at this state
|
| 82 |
+
const validPos = replayGame.validMovePositions();
|
| 83 |
+
|
| 84 |
+
// Map visit counts to positions
|
| 85 |
+
const statistics: MCTSMoveStatistic[] = validPos.map((pos, idx) => ({
|
| 86 |
+
position: pos,
|
| 87 |
+
actionKey: `${pos.x},${pos.y},${pos.z}`,
|
| 88 |
+
N: visitCounts[i][idx] || 0,
|
| 89 |
+
pi: 0 // Will be calculated after
|
| 90 |
+
}));
|
| 91 |
+
|
| 92 |
+
// Add pass move (typically the last element in visit counts)
|
| 93 |
+
const passN = visitCounts[i][validPos.length] || 0;
|
| 94 |
+
statistics.push({
|
| 95 |
+
position: null,
|
| 96 |
+
actionKey: "pass",
|
| 97 |
+
N: passN,
|
| 98 |
+
pi: 0
|
| 99 |
+
});
|
| 100 |
+
|
| 101 |
+
// Calculate total visits
|
| 102 |
+
const totalN = statistics.reduce((sum, stat) => sum + stat.N, 0);
|
| 103 |
+
|
| 104 |
+
// Normalize to get search policy π
|
| 105 |
+
if (totalN > 0) {
|
| 106 |
+
statistics.forEach((stat) => {
|
| 107 |
+
stat.pi = stat.N / totalN;
|
| 108 |
+
});
|
| 109 |
+
}
|
| 110 |
+
|
| 111 |
+
// Store move data
|
| 112 |
+
result.push({
|
| 113 |
+
moveNumber: i,
|
| 114 |
+
player: currentPlayer,
|
| 115 |
+
gameState: replayGame.clone(),
|
| 116 |
+
statistics
|
| 117 |
+
});
|
| 118 |
+
|
| 119 |
+
// Apply move to advance game state
|
| 120 |
+
const step = stepHistory[i];
|
| 121 |
+
if (step.type === 1) { // StepType.PASS
|
| 122 |
+
replayGame.pass();
|
| 123 |
+
} else if (step.position) {
|
| 124 |
+
replayGame.drop(step.position);
|
| 125 |
+
}
|
| 126 |
+
}
|
| 127 |
+
|
| 128 |
+
if (result.length === 0) {
|
| 129 |
+
throw new Error("No MCTS data could be parsed");
|
| 130 |
+
}
|
| 131 |
+
|
| 132 |
+
console.log(`[MCTS Parser] Successfully parsed ${result.length} moves`);
|
| 133 |
+
return result;
|
| 134 |
+
}
|
| 135 |
+
|
| 136 |
+
|
| 137 |
+
/**
|
| 138 |
+
* Validate that visit counts data has the expected structure
|
| 139 |
+
*/
|
| 140 |
+
export function validateVisitCounts(visitCounts: any): visitCounts is number[][] {
|
| 141 |
+
if (!Array.isArray(visitCounts)) {
|
| 142 |
+
return false;
|
| 143 |
+
}
|
| 144 |
+
|
| 145 |
+
for (const counts of visitCounts) {
|
| 146 |
+
if (!Array.isArray(counts)) {
|
| 147 |
+
return false;
|
| 148 |
+
}
|
| 149 |
+
|
| 150 |
+
for (const count of counts) {
|
| 151 |
+
if (typeof count !== "number" || !Number.isFinite(count) || count < 0) {
|
| 152 |
+
return false;
|
| 153 |
+
}
|
| 154 |
+
}
|
| 155 |
+
}
|
| 156 |
+
|
| 157 |
+
return true;
|
| 158 |
+
}
|
| 159 |
+
|
| 160 |
+
|
| 161 |
+
/**
|
| 162 |
+
* Format position for display
|
| 163 |
+
*/
|
| 164 |
+
export function formatPosition(position: { x: number; y: number; z?: number } | null): string {
|
| 165 |
+
if (!position) {
|
| 166 |
+
return "Pass";
|
| 167 |
+
}
|
| 168 |
+
|
| 169 |
+
// Convert to chess-like notation: column letter + row number
|
| 170 |
+
const col = String.fromCharCode(65 + position.x); // A, B, C, ...
|
| 171 |
+
const row = position.y + 1; // 1, 2, 3, ...
|
| 172 |
+
|
| 173 |
+
return `${col}${row}`;
|
| 174 |
+
}
|
trigo-web/app/src/utils/storage.ts
CHANGED
|
@@ -32,6 +32,9 @@ export enum StorageKey {
|
|
| 32 |
// AI settings
|
| 33 |
AI_PLAYER_COLOR = "trigoAIPlayerColor",
|
| 34 |
|
|
|
|
|
|
|
|
|
|
| 35 |
// User preferences (examples for future use)
|
| 36 |
// THEME = "trigoTheme",
|
| 37 |
// LANGUAGE = "trigoLanguage",
|
|
|
|
| 32 |
// AI settings
|
| 33 |
AI_PLAYER_COLOR = "trigoAIPlayerColor",
|
| 34 |
|
| 35 |
+
// Player settings
|
| 36 |
+
PLAYER_NICKNAME = "trigoPlayerNickname",
|
| 37 |
+
|
| 38 |
// User preferences (examples for future use)
|
| 39 |
// THEME = "trigoTheme",
|
| 40 |
// LANGUAGE = "trigoLanguage",
|
trigo-web/app/src/views/MCTSAnalysisView.vue
ADDED
|
@@ -0,0 +1,176 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<template>
|
| 2 |
+
<div class="mcts-analysis-container">
|
| 3 |
+
<h1>MCTS Analysis</h1>
|
| 4 |
+
|
| 5 |
+
<div class="mcts-grid">
|
| 6 |
+
<!-- Left sidebar: Control Panel -->
|
| 7 |
+
<div class="control-panel">
|
| 8 |
+
<h2>Controls</h2>
|
| 9 |
+
|
| 10 |
+
<!-- Data Loader Section -->
|
| 11 |
+
<section class="panel-section">
|
| 12 |
+
<h3>Data Source</h3>
|
| 13 |
+
<p class="info-text">
|
| 14 |
+
Upload a TGN game file and its corresponding visit counts JSON to analyze MCTS
|
| 15 |
+
behavior. Only 2D boards are supported.
|
| 16 |
+
</p>
|
| 17 |
+
<MCTSDataLoader />
|
| 18 |
+
</section>
|
| 19 |
+
|
| 20 |
+
<!-- Move Navigation Section -->
|
| 21 |
+
<section class="panel-section">
|
| 22 |
+
<h3>Navigation</h3>
|
| 23 |
+
<MCTSMoveNavigation v-if="hasData" />
|
| 24 |
+
<div v-else class="nav-placeholder">
|
| 25 |
+
<p>Load data to enable navigation</p>
|
| 26 |
+
</div>
|
| 27 |
+
</section>
|
| 28 |
+
|
| 29 |
+
<!-- Visualization Settings Section -->
|
| 30 |
+
<section class="panel-section">
|
| 31 |
+
<h3>Statistics</h3>
|
| 32 |
+
<MCTSStatisticsPanel />
|
| 33 |
+
</section>
|
| 34 |
+
</div>
|
| 35 |
+
|
| 36 |
+
<!-- Right content area -->
|
| 37 |
+
<div class="content-area">
|
| 38 |
+
<!-- Tree Visualization -->
|
| 39 |
+
<div class="visualization-panel tree-panel">
|
| 40 |
+
<h3>Search Tree</h3>
|
| 41 |
+
<MCTSTreeVisualization />
|
| 42 |
+
</div>
|
| 43 |
+
|
| 44 |
+
<!-- Board Heatmap -->
|
| 45 |
+
<div class="visualization-panel heatmap-panel">
|
| 46 |
+
<h3>Board Heatmap</h3>
|
| 47 |
+
<MCTSBoardHeatmap />
|
| 48 |
+
</div>
|
| 49 |
+
</div>
|
| 50 |
+
</div>
|
| 51 |
+
</div>
|
| 52 |
+
</template>
|
| 53 |
+
|
| 54 |
+
<script setup lang="ts">
|
| 55 |
+
import { ref, computed, onMounted } from "vue";
|
| 56 |
+
import { useMCTSStore } from "@/stores/mctsStore";
|
| 57 |
+
import MCTSDataLoader from "@/components/mcts/MCTSDataLoader.vue";
|
| 58 |
+
import MCTSMoveNavigation from "@/components/mcts/MCTSMoveNavigation.vue";
|
| 59 |
+
import MCTSStatisticsPanel from "@/components/mcts/MCTSStatisticsPanel.vue";
|
| 60 |
+
import MCTSBoardHeatmap from "@/components/mcts/MCTSBoardHeatmap.vue";
|
| 61 |
+
import MCTSTreeVisualization from "@/components/mcts/MCTSTreeVisualization.vue";
|
| 62 |
+
|
| 63 |
+
const mctsStore = useMCTSStore();
|
| 64 |
+
|
| 65 |
+
const hasData = computed(() => mctsStore.hasData);
|
| 66 |
+
|
| 67 |
+
onMounted(() => {
|
| 68 |
+
console.log("[MCTS Analysis] View mounted");
|
| 69 |
+
});
|
| 70 |
+
</script>
|
| 71 |
+
|
| 72 |
+
<style scoped lang="scss">
|
| 73 |
+
@use "@/styles/test-pages.scss" as *;
|
| 74 |
+
|
| 75 |
+
.mcts-analysis-container {
|
| 76 |
+
@include test-page-container('dark');
|
| 77 |
+
padding: 20px;
|
| 78 |
+
|
| 79 |
+
h1 {
|
| 80 |
+
margin: 0 0 20px 0;
|
| 81 |
+
font-size: 28px;
|
| 82 |
+
color: #e94560;
|
| 83 |
+
}
|
| 84 |
+
}
|
| 85 |
+
|
| 86 |
+
.mcts-grid {
|
| 87 |
+
display: grid;
|
| 88 |
+
grid-template-columns: 320px 1fr;
|
| 89 |
+
gap: 20px;
|
| 90 |
+
min-height: calc(100vh - 80px);
|
| 91 |
+
}
|
| 92 |
+
|
| 93 |
+
/* ============================================================================
|
| 94 |
+
Control Panel (Left Sidebar)
|
| 95 |
+
============================================================================ */
|
| 96 |
+
|
| 97 |
+
.control-panel {
|
| 98 |
+
@include control-panel('dark');
|
| 99 |
+
}
|
| 100 |
+
|
| 101 |
+
.panel-section {
|
| 102 |
+
@extend .panel-section;
|
| 103 |
+
}
|
| 104 |
+
|
| 105 |
+
.info-text {
|
| 106 |
+
@extend .info-text;
|
| 107 |
+
color: #a0a0a0;
|
| 108 |
+
}
|
| 109 |
+
|
| 110 |
+
.nav-placeholder {
|
| 111 |
+
padding: 15px;
|
| 112 |
+
background-color: #1a1a1a;
|
| 113 |
+
border-radius: 4px;
|
| 114 |
+
border: 1px dashed #404040;
|
| 115 |
+
text-align: center;
|
| 116 |
+
color: #606060;
|
| 117 |
+
font-size: 12px;
|
| 118 |
+
}
|
| 119 |
+
|
| 120 |
+
/* ============================================================================
|
| 121 |
+
Content Area (Right Side)
|
| 122 |
+
============================================================================ */
|
| 123 |
+
|
| 124 |
+
.content-area {
|
| 125 |
+
display: grid;
|
| 126 |
+
grid-template-rows: auto 1fr;
|
| 127 |
+
gap: 20px;
|
| 128 |
+
overflow: hidden;
|
| 129 |
+
}
|
| 130 |
+
|
| 131 |
+
.visualization-panel {
|
| 132 |
+
@include test-section('dark');
|
| 133 |
+
display: flex;
|
| 134 |
+
flex-direction: column;
|
| 135 |
+
min-height: 0;
|
| 136 |
+
padding: 20px;
|
| 137 |
+
|
| 138 |
+
h3 {
|
| 139 |
+
margin: 0 0 15px 0;
|
| 140 |
+
font-size: 18px;
|
| 141 |
+
color: #4a90e2;
|
| 142 |
+
flex-shrink: 0;
|
| 143 |
+
}
|
| 144 |
+
}
|
| 145 |
+
|
| 146 |
+
.tree-panel {
|
| 147 |
+
overflow: visible;
|
| 148 |
+
}
|
| 149 |
+
|
| 150 |
+
/* ============================================================================
|
| 151 |
+
Responsive
|
| 152 |
+
============================================================================ */
|
| 153 |
+
|
| 154 |
+
@media (max-width: 1200px) {
|
| 155 |
+
.mcts-grid {
|
| 156 |
+
grid-template-columns: 280px 1fr;
|
| 157 |
+
}
|
| 158 |
+
}
|
| 159 |
+
|
| 160 |
+
@media (max-width: 900px) {
|
| 161 |
+
.mcts-grid {
|
| 162 |
+
grid-template-columns: 1fr;
|
| 163 |
+
grid-template-rows: auto 1fr;
|
| 164 |
+
min-height: auto;
|
| 165 |
+
}
|
| 166 |
+
|
| 167 |
+
.control-panel {
|
| 168 |
+
max-height: 400px;
|
| 169 |
+
}
|
| 170 |
+
|
| 171 |
+
.content-area {
|
| 172 |
+
min-height: 800px;
|
| 173 |
+
max-height: none;
|
| 174 |
+
}
|
| 175 |
+
}
|
| 176 |
+
</style>
|
trigo-web/app/src/views/OnnxTestView.vue
CHANGED
|
@@ -179,9 +179,9 @@
|
|
| 179 |
console.log("=".repeat(80));
|
| 180 |
|
| 181 |
inferencer = new OnnxInferencer({
|
| 182 |
-
modelPath:
|
| 183 |
-
vocabSize:
|
| 184 |
-
seqLen:
|
| 185 |
executionProviders: ["wasm"] // Use WebAssembly for compatibility
|
| 186 |
});
|
| 187 |
|
|
|
|
| 179 |
console.log("=".repeat(80));
|
| 180 |
|
| 181 |
inferencer = new OnnxInferencer({
|
| 182 |
+
modelPath: import.meta.env.VITE_ONNX_TREE_MODEL,
|
| 183 |
+
vocabSize: 128,
|
| 184 |
+
seqLen: 256,
|
| 185 |
executionProviders: ["wasm"] // Use WebAssembly for compatibility
|
| 186 |
});
|
| 187 |
|
trigo-web/app/src/views/SocketTestView.vue
ADDED
|
@@ -0,0 +1,932 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<template>
|
| 2 |
+
<div class="socket-test-view">
|
| 3 |
+
<div class="test-header">
|
| 4 |
+
<h1>Socket API Test Page</h1>
|
| 5 |
+
<p class="subtitle">Testing Backend Game Socket Handlers</p>
|
| 6 |
+
</div>
|
| 7 |
+
|
| 8 |
+
<div class="test-body">
|
| 9 |
+
<!-- Left Column: Controls and Status -->
|
| 10 |
+
<div class="left-column">
|
| 11 |
+
<!-- Connection Status -->
|
| 12 |
+
<div class="test-section">
|
| 13 |
+
<h2>Connection Status</h2>
|
| 14 |
+
<div class="status-info">
|
| 15 |
+
<div class="status-item" :class="{ ready: connected, error: error }">
|
| 16 |
+
<span class="status-label">Socket:</span>
|
| 17 |
+
<span class="status-value">
|
| 18 |
+
{{ connected ? "✓ Connected" : "⚪ Disconnected" }}
|
| 19 |
+
</span>
|
| 20 |
+
</div>
|
| 21 |
+
<div v-if="socket" class="status-item">
|
| 22 |
+
<span class="status-label">Socket ID:</span>
|
| 23 |
+
<span class="status-value code">{{ socket.id }}</span>
|
| 24 |
+
</div>
|
| 25 |
+
<div v-if="error" class="error-message">❌ Error: {{ error }}</div>
|
| 26 |
+
</div>
|
| 27 |
+
</div>
|
| 28 |
+
|
| 29 |
+
<!-- Room Management -->
|
| 30 |
+
<div class="test-section">
|
| 31 |
+
<h2>Room Management</h2>
|
| 32 |
+
<div class="room-info">
|
| 33 |
+
<div class="info-item">
|
| 34 |
+
<span class="label">Current Room:</span>
|
| 35 |
+
<span class="value code">{{ currentRoomId || "Not in room" }}</span>
|
| 36 |
+
</div>
|
| 37 |
+
<div v-if="playerColor" class="info-item">
|
| 38 |
+
<span class="label">Player Color:</span>
|
| 39 |
+
<span class="value" :class="playerColor">{{ playerColor }}</span>
|
| 40 |
+
</div>
|
| 41 |
+
<div v-if="currentRoomId" class="info-item">
|
| 42 |
+
<span class="label">Room Admin:</span>
|
| 43 |
+
<span class="value">{{ isAdmin ? "You (Admin)" : "Other player" }}</span>
|
| 44 |
+
</div>
|
| 45 |
+
</div>
|
| 46 |
+
|
| 47 |
+
<div class="room-controls">
|
| 48 |
+
<div class="control-group">
|
| 49 |
+
<input
|
| 50 |
+
v-model="nickname"
|
| 51 |
+
type="text"
|
| 52 |
+
placeholder="Nickname"
|
| 53 |
+
class="input-text"
|
| 54 |
+
/>
|
| 55 |
+
<input
|
| 56 |
+
v-model="roomIdInput"
|
| 57 |
+
type="text"
|
| 58 |
+
placeholder="Room ID (leave empty to create)"
|
| 59 |
+
class="input-text"
|
| 60 |
+
/>
|
| 61 |
+
</div>
|
| 62 |
+
<div class="button-group">
|
| 63 |
+
<button
|
| 64 |
+
class="btn btn-primary"
|
| 65 |
+
@click="testJoinRoom"
|
| 66 |
+
:disabled="!connected || currentRoomId !== null"
|
| 67 |
+
>
|
| 68 |
+
Join/Create Room
|
| 69 |
+
</button>
|
| 70 |
+
<button
|
| 71 |
+
class="btn btn-secondary"
|
| 72 |
+
@click="testLeaveRoom"
|
| 73 |
+
:disabled="!currentRoomId"
|
| 74 |
+
>
|
| 75 |
+
Leave Room
|
| 76 |
+
</button>
|
| 77 |
+
</div>
|
| 78 |
+
</div>
|
| 79 |
+
</div>
|
| 80 |
+
|
| 81 |
+
<!-- Game State Display -->
|
| 82 |
+
<div class="test-section">
|
| 83 |
+
<h2>Game State</h2>
|
| 84 |
+
<div class="game-info">
|
| 85 |
+
<div class="info-item">
|
| 86 |
+
<span class="label">Game Status:</span>
|
| 87 |
+
<span class="value">{{ gameState.gameStatus || "N/A" }}</span>
|
| 88 |
+
</div>
|
| 89 |
+
<div class="info-item">
|
| 90 |
+
<span class="label">Current Player:</span>
|
| 91 |
+
<span class="value" :class="gameState.currentPlayer">
|
| 92 |
+
{{ gameState.currentPlayer || "N/A" }}
|
| 93 |
+
</span>
|
| 94 |
+
</div>
|
| 95 |
+
<div class="info-item">
|
| 96 |
+
<span class="label">Current Index:</span>
|
| 97 |
+
<span class="value">{{ gameState.currentMoveIndex }}</span>
|
| 98 |
+
</div>
|
| 99 |
+
<div class="info-item">
|
| 100 |
+
<span class="label">Captured (Black):</span>
|
| 101 |
+
<span class="value">{{ gameState.capturedStones.black }}</span>
|
| 102 |
+
</div>
|
| 103 |
+
<div class="info-item">
|
| 104 |
+
<span class="label">Captured (White):</span>
|
| 105 |
+
<span class="value">{{ gameState.capturedStones.white }}</span>
|
| 106 |
+
</div>
|
| 107 |
+
</div>
|
| 108 |
+
|
| 109 |
+
<!-- TGN Display -->
|
| 110 |
+
<div v-if="gameState.tgn" class="tgn-display">
|
| 111 |
+
<h3>Current TGN:</h3>
|
| 112 |
+
<pre class="tgn-content">{{ gameState.tgn }}</pre>
|
| 113 |
+
</div>
|
| 114 |
+
</div>
|
| 115 |
+
|
| 116 |
+
<!-- Game Actions -->
|
| 117 |
+
<div class="test-section">
|
| 118 |
+
<h2>Game Actions</h2>
|
| 119 |
+
<div class="game-controls">
|
| 120 |
+
<div class="control-group">
|
| 121 |
+
<input
|
| 122 |
+
v-model.number="moveX"
|
| 123 |
+
type="number"
|
| 124 |
+
placeholder="X"
|
| 125 |
+
class="input-number"
|
| 126 |
+
min="0"
|
| 127 |
+
max="4"
|
| 128 |
+
/>
|
| 129 |
+
<input
|
| 130 |
+
v-model.number="moveY"
|
| 131 |
+
type="number"
|
| 132 |
+
placeholder="Y"
|
| 133 |
+
class="input-number"
|
| 134 |
+
min="0"
|
| 135 |
+
max="4"
|
| 136 |
+
/>
|
| 137 |
+
<input
|
| 138 |
+
v-model.number="moveZ"
|
| 139 |
+
type="number"
|
| 140 |
+
placeholder="Z"
|
| 141 |
+
class="input-number"
|
| 142 |
+
min="0"
|
| 143 |
+
max="4"
|
| 144 |
+
/>
|
| 145 |
+
<button
|
| 146 |
+
class="btn btn-primary"
|
| 147 |
+
@click="testMakeMove"
|
| 148 |
+
:disabled="!currentRoomId"
|
| 149 |
+
>
|
| 150 |
+
Make Move
|
| 151 |
+
</button>
|
| 152 |
+
</div>
|
| 153 |
+
<div class="button-group">
|
| 154 |
+
<button
|
| 155 |
+
class="btn btn-secondary"
|
| 156 |
+
@click="testPass"
|
| 157 |
+
:disabled="!currentRoomId"
|
| 158 |
+
>
|
| 159 |
+
Pass
|
| 160 |
+
</button>
|
| 161 |
+
<button
|
| 162 |
+
class="btn btn-warning"
|
| 163 |
+
@click="testResign"
|
| 164 |
+
:disabled="!currentRoomId"
|
| 165 |
+
>
|
| 166 |
+
Resign
|
| 167 |
+
</button>
|
| 168 |
+
</div>
|
| 169 |
+
</div>
|
| 170 |
+
</div>
|
| 171 |
+
|
| 172 |
+
<!-- Admin Actions -->
|
| 173 |
+
<div class="test-section admin-section">
|
| 174 |
+
<h2>Admin Actions (Undo/Redo/Reset)</h2>
|
| 175 |
+
<p class="section-desc">Test the new admin APIs</p>
|
| 176 |
+
<div v-if="!isAdmin && currentRoomId" class="admin-warning">
|
| 177 |
+
⚠️ Only the room admin can reset the game
|
| 178 |
+
</div>
|
| 179 |
+
<div class="button-group">
|
| 180 |
+
<button
|
| 181 |
+
class="btn btn-admin"
|
| 182 |
+
@click="testUndo"
|
| 183 |
+
:disabled="!currentRoomId || gameState.currentMoveIndex === 0"
|
| 184 |
+
>
|
| 185 |
+
Undo Move
|
| 186 |
+
</button>
|
| 187 |
+
<button
|
| 188 |
+
class="btn btn-admin"
|
| 189 |
+
@click="testRedo"
|
| 190 |
+
:disabled="!currentRoomId"
|
| 191 |
+
>
|
| 192 |
+
Redo Move
|
| 193 |
+
</button>
|
| 194 |
+
</div>
|
| 195 |
+
<div class="reset-controls">
|
| 196 |
+
<div class="control-group">
|
| 197 |
+
<label class="input-label">Board Shape:</label>
|
| 198 |
+
<input
|
| 199 |
+
v-model.number="boardShapeX"
|
| 200 |
+
type="number"
|
| 201 |
+
placeholder="X"
|
| 202 |
+
class="input-number"
|
| 203 |
+
min="3"
|
| 204 |
+
max="9"
|
| 205 |
+
/>
|
| 206 |
+
<input
|
| 207 |
+
v-model.number="boardShapeY"
|
| 208 |
+
type="number"
|
| 209 |
+
placeholder="Y"
|
| 210 |
+
class="input-number"
|
| 211 |
+
min="3"
|
| 212 |
+
max="9"
|
| 213 |
+
/>
|
| 214 |
+
<input
|
| 215 |
+
v-model.number="boardShapeZ"
|
| 216 |
+
type="number"
|
| 217 |
+
placeholder="Z"
|
| 218 |
+
class="input-number"
|
| 219 |
+
min="3"
|
| 220 |
+
max="9"
|
| 221 |
+
/>
|
| 222 |
+
</div>
|
| 223 |
+
<div v-if="roomPlayers.length === 2" class="control-group">
|
| 224 |
+
<label class="checkbox-label">
|
| 225 |
+
<input type="checkbox" v-model="swapColors" />
|
| 226 |
+
Swap Player Colors (Black ↔ White)
|
| 227 |
+
</label>
|
| 228 |
+
</div>
|
| 229 |
+
<button class="btn btn-danger" @click="testReset" :disabled="!currentRoomId || !isAdmin">
|
| 230 |
+
Reset Game (Admin Only)
|
| 231 |
+
</button>
|
| 232 |
+
</div>
|
| 233 |
+
</div>
|
| 234 |
+
|
| 235 |
+
</div>
|
| 236 |
+
<!-- End Left Column -->
|
| 237 |
+
|
| 238 |
+
<!-- Right Column: Event Log -->
|
| 239 |
+
<div class="right-column">
|
| 240 |
+
<!-- Event Log -->
|
| 241 |
+
<div class="test-section event-log-section">
|
| 242 |
+
<h2>Event Log</h2>
|
| 243 |
+
<div class="event-log-controls">
|
| 244 |
+
<button class="btn btn-secondary" @click="clearEventLog">Clear Log</button>
|
| 245 |
+
<label class="checkbox-label">
|
| 246 |
+
<input type="checkbox" v-model="autoScroll" />
|
| 247 |
+
Auto-scroll
|
| 248 |
+
</label>
|
| 249 |
+
</div>
|
| 250 |
+
<div class="event-log" ref="eventLogRef">
|
| 251 |
+
<div v-if="eventLog.length === 0" class="empty-state">No events yet</div>
|
| 252 |
+
<div
|
| 253 |
+
v-for="(event, index) in eventLog"
|
| 254 |
+
:key="index"
|
| 255 |
+
class="event-item"
|
| 256 |
+
:class="event.type"
|
| 257 |
+
>
|
| 258 |
+
<span class="event-timestamp">{{ event.timestamp }}</span>
|
| 259 |
+
<span class="event-name">{{ event.name }}</span>
|
| 260 |
+
<span class="event-type-badge">{{ event.type }}</span>
|
| 261 |
+
<pre class="event-data">{{ JSON.stringify(event.data, null, 2) }}</pre>
|
| 262 |
+
</div>
|
| 263 |
+
</div>
|
| 264 |
+
</div>
|
| 265 |
+
</div>
|
| 266 |
+
<!-- End Right Column -->
|
| 267 |
+
|
| 268 |
+
</div>
|
| 269 |
+
</div>
|
| 270 |
+
</template>
|
| 271 |
+
|
| 272 |
+
<script setup lang="ts">
|
| 273 |
+
import { ref, reactive, onMounted, onUnmounted, nextTick, watch } from "vue";
|
| 274 |
+
import { useSocket } from "@/composables/useSocket";
|
| 275 |
+
|
| 276 |
+
// Socket composable
|
| 277 |
+
const { socket, connected, error } = useSocket();
|
| 278 |
+
|
| 279 |
+
// Room state
|
| 280 |
+
const currentRoomId = ref<string | null>(null);
|
| 281 |
+
const playerColor = ref<string | null>(null);
|
| 282 |
+
const nickname = ref("TestUser");
|
| 283 |
+
const roomIdInput = ref("");
|
| 284 |
+
const isAdmin = ref(false);
|
| 285 |
+
const roomPlayers = ref<string[]>([]);
|
| 286 |
+
const playerColorAssignments = reactive<{ [playerId: string]: "black" | "white" }>({});
|
| 287 |
+
const swapColors = ref(false);
|
| 288 |
+
|
| 289 |
+
// Game state
|
| 290 |
+
const gameState = reactive({
|
| 291 |
+
currentPlayer: null as string | null,
|
| 292 |
+
currentMoveIndex: 0,
|
| 293 |
+
capturedStones: { black: 0, white: 0 },
|
| 294 |
+
gameStatus: null as string | null,
|
| 295 |
+
tgn: "" as string
|
| 296 |
+
});
|
| 297 |
+
|
| 298 |
+
// Move inputs
|
| 299 |
+
const moveX = ref(0);
|
| 300 |
+
const moveY = ref(0);
|
| 301 |
+
const moveZ = ref(0);
|
| 302 |
+
|
| 303 |
+
// Board shape inputs for reset
|
| 304 |
+
const boardShapeX = ref(5);
|
| 305 |
+
const boardShapeY = ref(5);
|
| 306 |
+
const boardShapeZ = ref(5);
|
| 307 |
+
|
| 308 |
+
// Event log
|
| 309 |
+
const eventLog = ref<
|
| 310 |
+
Array<{
|
| 311 |
+
timestamp: string;
|
| 312 |
+
name: string;
|
| 313 |
+
type: "sent" | "received" | "error";
|
| 314 |
+
data: any;
|
| 315 |
+
}>
|
| 316 |
+
>([]);
|
| 317 |
+
const eventLogRef = ref<HTMLElement | null>(null);
|
| 318 |
+
const autoScroll = ref(true);
|
| 319 |
+
|
| 320 |
+
// Helper to log events
|
| 321 |
+
function logEvent(name: string, data: any, type: "sent" | "received" | "error" = "received") {
|
| 322 |
+
const timestamp = new Date().toLocaleTimeString();
|
| 323 |
+
eventLog.value.push({ timestamp, name, type, data });
|
| 324 |
+
|
| 325 |
+
if (autoScroll.value) {
|
| 326 |
+
nextTick(() => {
|
| 327 |
+
if (eventLogRef.value) {
|
| 328 |
+
eventLogRef.value.scrollTop = eventLogRef.value.scrollHeight;
|
| 329 |
+
}
|
| 330 |
+
});
|
| 331 |
+
}
|
| 332 |
+
}
|
| 333 |
+
|
| 334 |
+
function clearEventLog() {
|
| 335 |
+
eventLog.value = [];
|
| 336 |
+
}
|
| 337 |
+
|
| 338 |
+
// Room management
|
| 339 |
+
function testJoinRoom() {
|
| 340 |
+
const data = {
|
| 341 |
+
roomId: roomIdInput.value || undefined,
|
| 342 |
+
nickname: nickname.value
|
| 343 |
+
};
|
| 344 |
+
logEvent("joinRoom", data, "sent");
|
| 345 |
+
|
| 346 |
+
socket.emit("joinRoom", data, (response: any) => {
|
| 347 |
+
logEvent("joinRoom:response", response, response.success ? "received" : "error");
|
| 348 |
+
|
| 349 |
+
if (response.success) {
|
| 350 |
+
currentRoomId.value = response.roomId;
|
| 351 |
+
playerColor.value = response.playerColor;
|
| 352 |
+
isAdmin.value = response.isAdmin || false;
|
| 353 |
+
|
| 354 |
+
// Initialize room players from response
|
| 355 |
+
if (response.players) {
|
| 356 |
+
roomPlayers.value = Object.keys(response.players);
|
| 357 |
+
// Initialize player color assignments
|
| 358 |
+
for (const [playerId, playerInfo] of Object.entries(response.players) as any) {
|
| 359 |
+
playerColorAssignments[playerId] = playerInfo.color;
|
| 360 |
+
}
|
| 361 |
+
}
|
| 362 |
+
|
| 363 |
+
// Update game state
|
| 364 |
+
if (response.gameState) {
|
| 365 |
+
Object.assign(gameState, response.gameState);
|
| 366 |
+
|
| 367 |
+
// Update board shape inputs
|
| 368 |
+
if (response.gameState.boardShape) {
|
| 369 |
+
boardShapeX.value = response.gameState.boardShape.x;
|
| 370 |
+
boardShapeY.value = response.gameState.boardShape.y;
|
| 371 |
+
boardShapeZ.value = response.gameState.boardShape.z;
|
| 372 |
+
}
|
| 373 |
+
}
|
| 374 |
+
}
|
| 375 |
+
});
|
| 376 |
+
}
|
| 377 |
+
|
| 378 |
+
function testLeaveRoom() {
|
| 379 |
+
logEvent("leaveRoom", {}, "sent");
|
| 380 |
+
socket.emit("leaveRoom");
|
| 381 |
+
|
| 382 |
+
// Reset local state
|
| 383 |
+
currentRoomId.value = null;
|
| 384 |
+
playerColor.value = null;
|
| 385 |
+
Object.assign(gameState, {
|
| 386 |
+
currentPlayer: null,
|
| 387 |
+
currentMoveIndex: 0,
|
| 388 |
+
capturedStones: { black: 0, white: 0 },
|
| 389 |
+
gameStatus: null,
|
| 390 |
+
tgn: ""
|
| 391 |
+
});
|
| 392 |
+
}
|
| 393 |
+
|
| 394 |
+
// Game actions
|
| 395 |
+
function testMakeMove() {
|
| 396 |
+
const data = { x: moveX.value, y: moveY.value, z: moveZ.value };
|
| 397 |
+
logEvent("makeMove", data, "sent");
|
| 398 |
+
socket.emit("makeMove", data);
|
| 399 |
+
}
|
| 400 |
+
|
| 401 |
+
function testPass() {
|
| 402 |
+
logEvent("pass", {}, "sent");
|
| 403 |
+
socket.emit("pass");
|
| 404 |
+
}
|
| 405 |
+
|
| 406 |
+
function testResign() {
|
| 407 |
+
if (!confirm("Are you sure you want to resign?")) return;
|
| 408 |
+
logEvent("resign", {}, "sent");
|
| 409 |
+
socket.emit("resign");
|
| 410 |
+
}
|
| 411 |
+
|
| 412 |
+
// Admin actions
|
| 413 |
+
function testUndo() {
|
| 414 |
+
logEvent("undoMove", {}, "sent");
|
| 415 |
+
socket.emit("undoMove", (response: any) => {
|
| 416 |
+
logEvent("undoMove:response", response, response.success ? "received" : "error");
|
| 417 |
+
});
|
| 418 |
+
}
|
| 419 |
+
|
| 420 |
+
function testRedo() {
|
| 421 |
+
logEvent("redoMove", {}, "sent");
|
| 422 |
+
socket.emit("redoMove", (response: any) => {
|
| 423 |
+
logEvent("redoMove:response", response, response.success ? "received" : "error");
|
| 424 |
+
});
|
| 425 |
+
}
|
| 426 |
+
|
| 427 |
+
function testReset() {
|
| 428 |
+
if (!confirm("Reset entire game?")) return;
|
| 429 |
+
const data: any = {
|
| 430 |
+
boardShape: {
|
| 431 |
+
x: boardShapeX.value,
|
| 432 |
+
y: boardShapeY.value,
|
| 433 |
+
z: boardShapeZ.value
|
| 434 |
+
}
|
| 435 |
+
};
|
| 436 |
+
|
| 437 |
+
// Apply color swap if checkbox is checked
|
| 438 |
+
if (swapColors.value && roomPlayers.value.length === 2) {
|
| 439 |
+
const [player1, player2] = roomPlayers.value;
|
| 440 |
+
const color1 = playerColorAssignments[player1];
|
| 441 |
+
const color2 = playerColorAssignments[player2];
|
| 442 |
+
|
| 443 |
+
// Swap the colors
|
| 444 |
+
data.playerColors = {
|
| 445 |
+
[player1]: color2,
|
| 446 |
+
[player2]: color1
|
| 447 |
+
};
|
| 448 |
+
}
|
| 449 |
+
|
| 450 |
+
logEvent("resetGame", data, "sent");
|
| 451 |
+
socket.emit("resetGame", data, (response: any) => {
|
| 452 |
+
logEvent("resetGame:response", response, response.success ? "received" : "error");
|
| 453 |
+
});
|
| 454 |
+
}
|
| 455 |
+
|
| 456 |
+
// Socket event listeners
|
| 457 |
+
onMounted(() => {
|
| 458 |
+
// Listen to gameUpdate
|
| 459 |
+
socket.on("gameUpdate", (data: any) => {
|
| 460 |
+
logEvent("gameUpdate", data, "received");
|
| 461 |
+
|
| 462 |
+
// Update local game state
|
| 463 |
+
if (data.currentPlayer !== undefined) gameState.currentPlayer = data.currentPlayer;
|
| 464 |
+
if (data.currentMoveIndex !== undefined) gameState.currentMoveIndex = data.currentMoveIndex;
|
| 465 |
+
if (data.capturedStones !== undefined) gameState.capturedStones = data.capturedStones;
|
| 466 |
+
if (data.tgn !== undefined) gameState.tgn = data.tgn;
|
| 467 |
+
});
|
| 468 |
+
|
| 469 |
+
// Listen to gameReset
|
| 470 |
+
socket.on("gameReset", (data: any) => {
|
| 471 |
+
logEvent("gameReset", data, "received");
|
| 472 |
+
|
| 473 |
+
// Reset local game state
|
| 474 |
+
gameState.currentPlayer = data.currentPlayer;
|
| 475 |
+
gameState.currentMoveIndex = 0;
|
| 476 |
+
gameState.capturedStones = { black: 0, white: 0 };
|
| 477 |
+
gameState.tgn = data.tgn || "";
|
| 478 |
+
|
| 479 |
+
// Update board shape inputs
|
| 480 |
+
if (data.boardShape) {
|
| 481 |
+
boardShapeX.value = data.boardShape.x;
|
| 482 |
+
boardShapeY.value = data.boardShape.y;
|
| 483 |
+
boardShapeZ.value = data.boardShape.z;
|
| 484 |
+
}
|
| 485 |
+
|
| 486 |
+
// Update player list and colors if provided
|
| 487 |
+
if (data.players) {
|
| 488 |
+
roomPlayers.value = Object.keys(data.players);
|
| 489 |
+
// Initialize player color assignments
|
| 490 |
+
for (const [playerId, playerInfo] of Object.entries(data.players) as any) {
|
| 491 |
+
playerColorAssignments[playerId] = playerInfo.color;
|
| 492 |
+
}
|
| 493 |
+
|
| 494 |
+
// Update current player's color display
|
| 495 |
+
if (data.players[socket.id]) {
|
| 496 |
+
playerColor.value = data.players[socket.id].color;
|
| 497 |
+
}
|
| 498 |
+
}
|
| 499 |
+
});
|
| 500 |
+
|
| 501 |
+
// Listen to gameEnded
|
| 502 |
+
socket.on("gameEnded", (data: any) => {
|
| 503 |
+
logEvent("gameEnded", data, "received");
|
| 504 |
+
gameState.gameStatus = "finished";
|
| 505 |
+
});
|
| 506 |
+
|
| 507 |
+
// Listen to playerJoined
|
| 508 |
+
socket.on("playerJoined", (data: any) => {
|
| 509 |
+
logEvent("playerJoined", data, "received");
|
| 510 |
+
|
| 511 |
+
// Add the new player to the room players list
|
| 512 |
+
if (data.playerId && !roomPlayers.value.includes(data.playerId)) {
|
| 513 |
+
roomPlayers.value.push(data.playerId);
|
| 514 |
+
// Default the new player to white (second player)
|
| 515 |
+
playerColorAssignments[data.playerId] = "white";
|
| 516 |
+
}
|
| 517 |
+
});
|
| 518 |
+
|
| 519 |
+
// Listen to playerLeft
|
| 520 |
+
socket.on("playerLeft", (data: any) => {
|
| 521 |
+
logEvent("playerLeft", data, "received");
|
| 522 |
+
});
|
| 523 |
+
|
| 524 |
+
// Listen to nicknameChanged
|
| 525 |
+
socket.on("nicknameChanged", (data: any) => {
|
| 526 |
+
logEvent("nicknameChanged", data, "received");
|
| 527 |
+
});
|
| 528 |
+
|
| 529 |
+
// Listen to error events
|
| 530 |
+
socket.on("error", (data: any) => {
|
| 531 |
+
logEvent("error", data, "error");
|
| 532 |
+
});
|
| 533 |
+
});
|
| 534 |
+
|
| 535 |
+
onUnmounted(() => {
|
| 536 |
+
// Clean up listeners
|
| 537 |
+
socket.off("gameUpdate");
|
| 538 |
+
socket.off("gameReset");
|
| 539 |
+
socket.off("gameEnded");
|
| 540 |
+
socket.off("playerJoined");
|
| 541 |
+
socket.off("playerLeft");
|
| 542 |
+
socket.off("nicknameChanged");
|
| 543 |
+
socket.off("error");
|
| 544 |
+
});
|
| 545 |
+
</script>
|
| 546 |
+
|
| 547 |
+
<style scoped>
|
| 548 |
+
.socket-test-view {
|
| 549 |
+
max-width: 1200px;
|
| 550 |
+
margin: 0 auto;
|
| 551 |
+
padding: 20px;
|
| 552 |
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
| 553 |
+
}
|
| 554 |
+
|
| 555 |
+
.test-header {
|
| 556 |
+
text-align: center;
|
| 557 |
+
margin-bottom: 30px;
|
| 558 |
+
padding-bottom: 20px;
|
| 559 |
+
border-bottom: 2px solid #e0e0e0;
|
| 560 |
+
}
|
| 561 |
+
|
| 562 |
+
.test-header h1 {
|
| 563 |
+
margin: 0 0 10px 0;
|
| 564 |
+
font-size: 2em;
|
| 565 |
+
color: #333;
|
| 566 |
+
}
|
| 567 |
+
|
| 568 |
+
.subtitle {
|
| 569 |
+
margin: 0;
|
| 570 |
+
color: #666;
|
| 571 |
+
font-size: 1.1em;
|
| 572 |
+
}
|
| 573 |
+
|
| 574 |
+
.test-body {
|
| 575 |
+
display: grid;
|
| 576 |
+
grid-template-columns: 1fr 1fr;
|
| 577 |
+
gap: 20px;
|
| 578 |
+
overflow: auto;
|
| 579 |
+
max-height: calc(100vh - 200px);
|
| 580 |
+
}
|
| 581 |
+
|
| 582 |
+
.left-column {
|
| 583 |
+
display: flex;
|
| 584 |
+
flex-direction: column;
|
| 585 |
+
gap: 20px;
|
| 586 |
+
overflow-y: auto;
|
| 587 |
+
}
|
| 588 |
+
|
| 589 |
+
.right-column {
|
| 590 |
+
display: flex;
|
| 591 |
+
flex-direction: column;
|
| 592 |
+
min-height: 0;
|
| 593 |
+
}
|
| 594 |
+
|
| 595 |
+
.event-log-section {
|
| 596 |
+
display: flex;
|
| 597 |
+
flex-direction: column;
|
| 598 |
+
height: 100%;
|
| 599 |
+
min-height: 0;
|
| 600 |
+
}
|
| 601 |
+
|
| 602 |
+
.test-section {
|
| 603 |
+
background: #f9f9f9;
|
| 604 |
+
border: 1px solid #ddd;
|
| 605 |
+
border-radius: 8px;
|
| 606 |
+
padding: 20px;
|
| 607 |
+
}
|
| 608 |
+
|
| 609 |
+
.test-section h2 {
|
| 610 |
+
margin: 0 0 15px 0;
|
| 611 |
+
font-size: 1.5em;
|
| 612 |
+
color: #333;
|
| 613 |
+
}
|
| 614 |
+
|
| 615 |
+
.section-desc {
|
| 616 |
+
margin: -10px 0 15px 0;
|
| 617 |
+
color: #666;
|
| 618 |
+
font-size: 0.9em;
|
| 619 |
+
}
|
| 620 |
+
|
| 621 |
+
.admin-section {
|
| 622 |
+
background: #fff8e1;
|
| 623 |
+
border-color: #ffd54f;
|
| 624 |
+
}
|
| 625 |
+
|
| 626 |
+
/* Status */
|
| 627 |
+
.status-info,
|
| 628 |
+
.room-info,
|
| 629 |
+
.game-info {
|
| 630 |
+
display: flex;
|
| 631 |
+
flex-direction: column;
|
| 632 |
+
gap: 10px;
|
| 633 |
+
margin-bottom: 15px;
|
| 634 |
+
}
|
| 635 |
+
|
| 636 |
+
.status-item,
|
| 637 |
+
.info-item {
|
| 638 |
+
display: flex;
|
| 639 |
+
gap: 10px;
|
| 640 |
+
align-items: center;
|
| 641 |
+
padding: 8px;
|
| 642 |
+
background: white;
|
| 643 |
+
border-radius: 4px;
|
| 644 |
+
}
|
| 645 |
+
|
| 646 |
+
.status-item.ready {
|
| 647 |
+
background: #e8f5e9;
|
| 648 |
+
}
|
| 649 |
+
|
| 650 |
+
.status-item.error {
|
| 651 |
+
background: #ffebee;
|
| 652 |
+
}
|
| 653 |
+
|
| 654 |
+
.status-label,
|
| 655 |
+
.label {
|
| 656 |
+
font-weight: 600;
|
| 657 |
+
min-width: 120px;
|
| 658 |
+
}
|
| 659 |
+
|
| 660 |
+
.status-value,
|
| 661 |
+
.value {
|
| 662 |
+
flex: 1;
|
| 663 |
+
}
|
| 664 |
+
|
| 665 |
+
.value.black {
|
| 666 |
+
color: #000;
|
| 667 |
+
font-weight: bold;
|
| 668 |
+
}
|
| 669 |
+
|
| 670 |
+
.value.white {
|
| 671 |
+
color: #999;
|
| 672 |
+
font-weight: bold;
|
| 673 |
+
}
|
| 674 |
+
|
| 675 |
+
.code {
|
| 676 |
+
font-family: "Courier New", monospace;
|
| 677 |
+
background: #f0f0f0;
|
| 678 |
+
padding: 2px 6px;
|
| 679 |
+
border-radius: 3px;
|
| 680 |
+
font-size: 0.9em;
|
| 681 |
+
}
|
| 682 |
+
|
| 683 |
+
.error-message {
|
| 684 |
+
padding: 10px;
|
| 685 |
+
background: #ffebee;
|
| 686 |
+
border: 1px solid #ef5350;
|
| 687 |
+
border-radius: 4px;
|
| 688 |
+
color: #c62828;
|
| 689 |
+
}
|
| 690 |
+
|
| 691 |
+
/* Controls */
|
| 692 |
+
.room-controls,
|
| 693 |
+
.game-controls {
|
| 694 |
+
display: flex;
|
| 695 |
+
flex-direction: column;
|
| 696 |
+
gap: 10px;
|
| 697 |
+
}
|
| 698 |
+
|
| 699 |
+
.control-group,
|
| 700 |
+
.button-group {
|
| 701 |
+
display: flex;
|
| 702 |
+
gap: 10px;
|
| 703 |
+
flex-wrap: wrap;
|
| 704 |
+
align-items: center;
|
| 705 |
+
}
|
| 706 |
+
|
| 707 |
+
.reset-controls {
|
| 708 |
+
display: flex;
|
| 709 |
+
flex-direction: column;
|
| 710 |
+
gap: 10px;
|
| 711 |
+
margin-top: 10px;
|
| 712 |
+
}
|
| 713 |
+
|
| 714 |
+
.input-label {
|
| 715 |
+
font-weight: 600;
|
| 716 |
+
margin-right: 5px;
|
| 717 |
+
}
|
| 718 |
+
|
| 719 |
+
.input-text,
|
| 720 |
+
.input-number,
|
| 721 |
+
.input-select {
|
| 722 |
+
padding: 8px 12px;
|
| 723 |
+
border: 1px solid #ddd;
|
| 724 |
+
border-radius: 4px;
|
| 725 |
+
font-size: 1em;
|
| 726 |
+
flex: 1;
|
| 727 |
+
min-width: 150px;
|
| 728 |
+
}
|
| 729 |
+
|
| 730 |
+
.input-number {
|
| 731 |
+
min-width: 80px;
|
| 732 |
+
max-width: 100px;
|
| 733 |
+
}
|
| 734 |
+
|
| 735 |
+
.input-select {
|
| 736 |
+
min-width: 200px;
|
| 737 |
+
cursor: pointer;
|
| 738 |
+
background: white;
|
| 739 |
+
}
|
| 740 |
+
|
| 741 |
+
.admin-warning {
|
| 742 |
+
padding: 10px;
|
| 743 |
+
background: #fff8e1;
|
| 744 |
+
border: 1px solid #ffd54f;
|
| 745 |
+
border-radius: 4px;
|
| 746 |
+
color: #f57c00;
|
| 747 |
+
font-weight: 500;
|
| 748 |
+
margin-bottom: 10px;
|
| 749 |
+
}
|
| 750 |
+
|
| 751 |
+
.btn {
|
| 752 |
+
padding: 10px 20px;
|
| 753 |
+
border: none;
|
| 754 |
+
border-radius: 4px;
|
| 755 |
+
font-size: 1em;
|
| 756 |
+
cursor: pointer;
|
| 757 |
+
transition: background 0.2s;
|
| 758 |
+
font-weight: 500;
|
| 759 |
+
}
|
| 760 |
+
|
| 761 |
+
.btn:disabled {
|
| 762 |
+
opacity: 0.5;
|
| 763 |
+
cursor: not-allowed;
|
| 764 |
+
}
|
| 765 |
+
|
| 766 |
+
.btn-primary {
|
| 767 |
+
background: #2196f3;
|
| 768 |
+
color: white;
|
| 769 |
+
}
|
| 770 |
+
|
| 771 |
+
.btn-primary:hover:not(:disabled) {
|
| 772 |
+
background: #1976d2;
|
| 773 |
+
}
|
| 774 |
+
|
| 775 |
+
.btn-secondary {
|
| 776 |
+
background: #757575;
|
| 777 |
+
color: white;
|
| 778 |
+
}
|
| 779 |
+
|
| 780 |
+
.btn-secondary:hover:not(:disabled) {
|
| 781 |
+
background: #616161;
|
| 782 |
+
}
|
| 783 |
+
|
| 784 |
+
.btn-warning {
|
| 785 |
+
background: #ff9800;
|
| 786 |
+
color: white;
|
| 787 |
+
}
|
| 788 |
+
|
| 789 |
+
.btn-warning:hover:not(:disabled) {
|
| 790 |
+
background: #f57c00;
|
| 791 |
+
}
|
| 792 |
+
|
| 793 |
+
.btn-danger {
|
| 794 |
+
background: #f44336;
|
| 795 |
+
color: white;
|
| 796 |
+
}
|
| 797 |
+
|
| 798 |
+
.btn-danger:hover:not(:disabled) {
|
| 799 |
+
background: #d32f2f;
|
| 800 |
+
}
|
| 801 |
+
|
| 802 |
+
.btn-admin {
|
| 803 |
+
background: #9c27b0;
|
| 804 |
+
color: white;
|
| 805 |
+
}
|
| 806 |
+
|
| 807 |
+
.btn-admin:hover:not(:disabled) {
|
| 808 |
+
background: #7b1fa2;
|
| 809 |
+
}
|
| 810 |
+
|
| 811 |
+
.checkbox-label {
|
| 812 |
+
display: flex;
|
| 813 |
+
align-items: center;
|
| 814 |
+
gap: 5px;
|
| 815 |
+
cursor: pointer;
|
| 816 |
+
}
|
| 817 |
+
|
| 818 |
+
/* Event Log */
|
| 819 |
+
.event-log-controls {
|
| 820 |
+
display: flex;
|
| 821 |
+
gap: 10px;
|
| 822 |
+
align-items: center;
|
| 823 |
+
margin-bottom: 10px;
|
| 824 |
+
}
|
| 825 |
+
|
| 826 |
+
.event-log {
|
| 827 |
+
flex: 1;
|
| 828 |
+
overflow-y: auto;
|
| 829 |
+
background: white;
|
| 830 |
+
border: 1px solid #ddd;
|
| 831 |
+
border-radius: 4px;
|
| 832 |
+
padding: 10px;
|
| 833 |
+
min-height: 0;
|
| 834 |
+
}
|
| 835 |
+
|
| 836 |
+
.empty-state {
|
| 837 |
+
text-align: center;
|
| 838 |
+
color: #999;
|
| 839 |
+
padding: 20px;
|
| 840 |
+
}
|
| 841 |
+
|
| 842 |
+
/* TGN Display */
|
| 843 |
+
.tgn-display {
|
| 844 |
+
margin-top: 15px;
|
| 845 |
+
}
|
| 846 |
+
|
| 847 |
+
.tgn-display h3 {
|
| 848 |
+
margin: 0 0 10px 0;
|
| 849 |
+
font-size: 1.1em;
|
| 850 |
+
color: #333;
|
| 851 |
+
}
|
| 852 |
+
|
| 853 |
+
.tgn-content {
|
| 854 |
+
background: white;
|
| 855 |
+
border: 1px solid #ddd;
|
| 856 |
+
border-radius: 4px;
|
| 857 |
+
padding: 10px;
|
| 858 |
+
margin: 0;
|
| 859 |
+
font-family: "Courier New", monospace;
|
| 860 |
+
font-size: 0.9em;
|
| 861 |
+
overflow-x: auto;
|
| 862 |
+
white-space: pre-wrap;
|
| 863 |
+
word-wrap: break-word;
|
| 864 |
+
max-height: 200px;
|
| 865 |
+
overflow-y: auto;
|
| 866 |
+
}
|
| 867 |
+
|
| 868 |
+
.event-item {
|
| 869 |
+
margin-bottom: 10px;
|
| 870 |
+
padding: 10px;
|
| 871 |
+
border-radius: 4px;
|
| 872 |
+
border-left: 4px solid #2196f3;
|
| 873 |
+
background: #f5f5f5;
|
| 874 |
+
}
|
| 875 |
+
|
| 876 |
+
.event-item.sent {
|
| 877 |
+
border-left-color: #4caf50;
|
| 878 |
+
background: #e8f5e9;
|
| 879 |
+
}
|
| 880 |
+
|
| 881 |
+
.event-item.error {
|
| 882 |
+
border-left-color: #f44336;
|
| 883 |
+
background: #ffebee;
|
| 884 |
+
}
|
| 885 |
+
|
| 886 |
+
.event-timestamp {
|
| 887 |
+
font-size: 0.85em;
|
| 888 |
+
color: #666;
|
| 889 |
+
margin-right: 10px;
|
| 890 |
+
}
|
| 891 |
+
|
| 892 |
+
.event-name {
|
| 893 |
+
font-weight: 600;
|
| 894 |
+
margin-right: 10px;
|
| 895 |
+
}
|
| 896 |
+
|
| 897 |
+
.event-type-badge {
|
| 898 |
+
display: inline-block;
|
| 899 |
+
padding: 2px 8px;
|
| 900 |
+
border-radius: 12px;
|
| 901 |
+
font-size: 0.75em;
|
| 902 |
+
font-weight: 600;
|
| 903 |
+
text-transform: uppercase;
|
| 904 |
+
}
|
| 905 |
+
|
| 906 |
+
.event-item.sent .event-type-badge {
|
| 907 |
+
background: #4caf50;
|
| 908 |
+
color: white;
|
| 909 |
+
}
|
| 910 |
+
|
| 911 |
+
.event-item.received .event-type-badge {
|
| 912 |
+
background: #2196f3;
|
| 913 |
+
color: white;
|
| 914 |
+
}
|
| 915 |
+
|
| 916 |
+
.event-item.error .event-type-badge {
|
| 917 |
+
background: #f44336;
|
| 918 |
+
color: white;
|
| 919 |
+
}
|
| 920 |
+
|
| 921 |
+
.event-data {
|
| 922 |
+
margin: 10px 0 0 0;
|
| 923 |
+
padding: 10px;
|
| 924 |
+
background: white;
|
| 925 |
+
border: 1px solid #ddd;
|
| 926 |
+
border-radius: 4px;
|
| 927 |
+
font-size: 0.85em;
|
| 928 |
+
overflow-x: auto;
|
| 929 |
+
white-space: pre-wrap;
|
| 930 |
+
word-wrap: break-word;
|
| 931 |
+
}
|
| 932 |
+
</style>
|
trigo-web/app/src/views/TrigoAgentTestView.vue
CHANGED
|
@@ -218,9 +218,9 @@
|
|
| 218 |
|
| 219 |
// Create and initialize inferencer
|
| 220 |
inferencer = new OnnxInferencer({
|
| 221 |
-
modelPath:
|
| 222 |
-
vocabSize:
|
| 223 |
-
seqLen:
|
| 224 |
});
|
| 225 |
|
| 226 |
await inferencer.initialize();
|
|
|
|
| 218 |
|
| 219 |
// Create and initialize inferencer
|
| 220 |
inferencer = new OnnxInferencer({
|
| 221 |
+
modelPath: import.meta.env.VITE_ONNX_TREE_MODEL,
|
| 222 |
+
vocabSize: 128,
|
| 223 |
+
seqLen: 256
|
| 224 |
});
|
| 225 |
|
| 226 |
await inferencer.initialize();
|
trigo-web/app/src/views/TrigoTreeTestView.vue
CHANGED
|
@@ -158,7 +158,7 @@
|
|
| 158 |
>
|
| 159 |
<span class="move-notation">{{ move.notation }}</span>
|
| 160 |
<span class="move-positions"
|
| 161 |
-
>
|
| 162 |
>
|
| 163 |
</div>
|
| 164 |
</div>
|
|
@@ -264,7 +264,7 @@
|
|
| 264 |
import type { Move } from "../../../inc/trigo/types";
|
| 265 |
|
| 266 |
// Configuration
|
| 267 |
-
const modelPath =
|
| 268 |
const vocabSize = 259;
|
| 269 |
|
| 270 |
// State
|
|
@@ -285,7 +285,8 @@
|
|
| 285 |
const treeVisualization = ref<{
|
| 286 |
evaluatedIds: number[];
|
| 287 |
mask: number[];
|
| 288 |
-
|
|
|
|
| 289 |
} | null>(null);
|
| 290 |
|
| 291 |
// Computed properties
|
|
@@ -329,6 +330,16 @@
|
|
| 329 |
// Compute sum of exp scores
|
| 330 |
const sumExp = expScores.reduce((sum, exp) => sum + exp, 0);
|
| 331 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 332 |
// Return moves with normalized probabilities
|
| 333 |
return scoredMoves.value.map((move, i) => ({
|
| 334 |
...move,
|
|
@@ -416,9 +427,9 @@
|
|
| 416 |
treeVisualization.value = {
|
| 417 |
evaluatedIds: treeStructure.evaluatedIds,
|
| 418 |
mask: treeStructure.mask,
|
|
|
|
| 419 |
moveData: treeStructure.moveData.map((m) => ({
|
| 420 |
notation: m.notation,
|
| 421 |
-
parentPos: m.parentPos,
|
| 422 |
leafPos: m.leafPos
|
| 423 |
}))
|
| 424 |
};
|
|
|
|
| 158 |
>
|
| 159 |
<span class="move-notation">{{ move.notation }}</span>
|
| 160 |
<span class="move-positions"
|
| 161 |
+
>leaf={{ move.leafPos }}, parent={{ treeVisualization.parent[move.leafPos] }}</span
|
| 162 |
>
|
| 163 |
</div>
|
| 164 |
</div>
|
|
|
|
| 264 |
import type { Move } from "../../../inc/trigo/types";
|
| 265 |
|
| 266 |
// Configuration
|
| 267 |
+
const modelPath = import.meta.env.VITE_ONNX_TREE_MODEL;
|
| 268 |
const vocabSize = 259;
|
| 269 |
|
| 270 |
// State
|
|
|
|
| 285 |
const treeVisualization = ref<{
|
| 286 |
evaluatedIds: number[];
|
| 287 |
mask: number[];
|
| 288 |
+
parent: Array<number | null>;
|
| 289 |
+
moveData: Array<{ notation: string; leafPos: number }>;
|
| 290 |
} | null>(null);
|
| 291 |
|
| 292 |
// Computed properties
|
|
|
|
| 330 |
// Compute sum of exp scores
|
| 331 |
const sumExp = expScores.reduce((sum, exp) => sum + exp, 0);
|
| 332 |
|
| 333 |
+
// Handle edge case where all probabilities underflow to 0
|
| 334 |
+
if (sumExp === 0 || !isFinite(sumExp)) {
|
| 335 |
+
console.warn("Probability sum is 0 or non-finite, using uniform distribution");
|
| 336 |
+
const uniformProb = 1.0 / scoredMoves.value.length;
|
| 337 |
+
return scoredMoves.value.map((move) => ({
|
| 338 |
+
...move,
|
| 339 |
+
probability: uniformProb
|
| 340 |
+
}));
|
| 341 |
+
}
|
| 342 |
+
|
| 343 |
// Return moves with normalized probabilities
|
| 344 |
return scoredMoves.value.map((move, i) => ({
|
| 345 |
...move,
|
|
|
|
| 427 |
treeVisualization.value = {
|
| 428 |
evaluatedIds: treeStructure.evaluatedIds,
|
| 429 |
mask: treeStructure.mask,
|
| 430 |
+
parent: treeStructure.parent,
|
| 431 |
moveData: treeStructure.moveData.map((m) => ({
|
| 432 |
notation: m.notation,
|
|
|
|
| 433 |
leafPos: m.leafPos
|
| 434 |
}))
|
| 435 |
};
|
trigo-web/app/src/views/TrigoView.vue
CHANGED
|
@@ -41,14 +41,43 @@
|
|
| 41 |
<div v-else-if="gameMode === 'vs-people'" class="view-status people-mode">
|
| 42 |
<div class="room-info">
|
| 43 |
<span class="room-label">Room:</span>
|
| 44 |
-
<span class="room-code">
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 45 |
</div>
|
|
|
|
| 46 |
<div class="players-info">
|
| 47 |
-
<
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 48 |
<span class="vs-divider">vs</span>
|
| 49 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 50 |
</div>
|
| 51 |
-
|
|
|
|
|
|
|
|
|
|
| 52 |
</div>
|
| 53 |
|
| 54 |
<!-- Library Mode: Game Info & Controls -->
|
|
@@ -62,6 +91,60 @@
|
|
| 62 |
<button class="btn-library btn-export" title="Export TGN">📤 Export</button>
|
| 63 |
</div>
|
| 64 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 65 |
</div>
|
| 66 |
|
| 67 |
<div class="view-body">
|
|
@@ -213,36 +296,6 @@
|
|
| 213 |
</div>
|
| 214 |
</div>
|
| 215 |
</div>
|
| 216 |
-
|
| 217 |
-
<!-- Game Settings -->
|
| 218 |
-
<div class="panel-section settings-section">
|
| 219 |
-
<h3 class="section-title">Settings</h3>
|
| 220 |
-
<div class="settings-content">
|
| 221 |
-
<div class="setting-item">
|
| 222 |
-
<label for="board-shape">
|
| 223 |
-
Board Shape:
|
| 224 |
-
<span
|
| 225 |
-
v-if="isBoardShapeDirty"
|
| 226 |
-
class="dirty-indicator"
|
| 227 |
-
title="Board shape will change on next game"
|
| 228 |
-
>*</span
|
| 229 |
-
>
|
| 230 |
-
</label>
|
| 231 |
-
<select id="board-shape" v-model="selectedBoardShape">
|
| 232 |
-
<option value="3*3*3">3×3×3</option>
|
| 233 |
-
<option value="5*5*5">5×5×5</option>
|
| 234 |
-
<option value="7*7*7">7×7×7</option>
|
| 235 |
-
<option value="9*9*1">9×9×1 (2D)</option>
|
| 236 |
-
<option value="13*13*1">13×13×1 (2D)</option>
|
| 237 |
-
<option value="19*19*1">19×19×1 (2D)</option>
|
| 238 |
-
<option value="9*9*2">9×9×2</option>
|
| 239 |
-
</select>
|
| 240 |
-
</div>
|
| 241 |
-
<button class="btn btn-primary btn-new-game" @click="newGame">
|
| 242 |
-
{{ gameStarted ? "Reset Game" : "Start Game" }}
|
| 243 |
-
</button>
|
| 244 |
-
</div>
|
| 245 |
-
</div>
|
| 246 |
</div>
|
| 247 |
</div>
|
| 248 |
|
|
@@ -281,7 +334,11 @@
|
|
| 281 |
import { useRoute } from "vue-router";
|
| 282 |
import { TrigoViewport } from "@/services/trigoViewport";
|
| 283 |
import { useGameStore } from "@/stores/gameStore";
|
|
|
|
| 284 |
import { useTrigoAgent } from "@/composables/useTrigoAgent";
|
|
|
|
|
|
|
|
|
|
| 285 |
import { storeToRefs } from "pinia";
|
| 286 |
import type { BoardShape } from "../../../inc/trigo";
|
| 287 |
import { Stone, validateMove, StoneType, validateTGN } from "../../../inc/trigo";
|
|
@@ -293,6 +350,31 @@
|
|
| 293 |
const route = useRoute();
|
| 294 |
const gameMode = computed(() => (route.meta.mode as string) || "single");
|
| 295 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 296 |
// Helper functions for board shape parsing
|
| 297 |
const parseBoardShape = (shapeStr: string): BoardShape => {
|
| 298 |
const parts = shapeStr
|
|
@@ -312,21 +394,6 @@
|
|
| 312 |
return encodeAb0yz([move.x, move.y, move.z], [shape.x, shape.y, shape.z]);
|
| 313 |
};
|
| 314 |
|
| 315 |
-
// Use game store
|
| 316 |
-
const gameStore = useGameStore();
|
| 317 |
-
const {
|
| 318 |
-
currentPlayer,
|
| 319 |
-
moveHistory,
|
| 320 |
-
currentMoveIndex,
|
| 321 |
-
capturedStones,
|
| 322 |
-
gameStatus,
|
| 323 |
-
boardShape,
|
| 324 |
-
moveCount,
|
| 325 |
-
isGameActive,
|
| 326 |
-
passCount
|
| 327 |
-
} = storeToRefs(gameStore);
|
| 328 |
-
|
| 329 |
-
|
| 330 |
// AI Agent (for VS AI mode)
|
| 331 |
const aiAgent = useTrigoAgent();
|
| 332 |
const { isReady: aiReady, isThinking: aiThinking, error: aiError, lastMoveTime } = aiAgent;
|
|
@@ -340,6 +407,19 @@
|
|
| 340 |
const aiPlayerColor = ref<"black" | "white">(loadAIColor());
|
| 341 |
const humanPlayerColor = computed(() => (aiPlayerColor.value === "white" ? "black" : "white"));
|
| 342 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 343 |
// Local state
|
| 344 |
const hoveredPosition = ref<string | null>(null);
|
| 345 |
const blackScore = ref(0);
|
|
@@ -497,29 +577,39 @@
|
|
| 497 |
|
| 498 |
// Apply AI move if valid
|
| 499 |
if (aiMove) {
|
| 500 |
-
|
| 501 |
-
|
| 502 |
-
|
| 503 |
-
|
| 504 |
-
|
| 505 |
-
|
| 506 |
-
|
| 507 |
-
|
| 508 |
-
viewport.addStone(aiMove.x, aiMove.y, aiMove.z, stoneColor);
|
| 509 |
-
|
| 510 |
-
// Remove captured stones
|
| 511 |
-
if (result.capturedPositions && result.capturedPositions.length > 0) {
|
| 512 |
-
result.capturedPositions.forEach((pos: any) => {
|
| 513 |
-
viewport.removeStone(pos.x, pos.y, pos.z);
|
| 514 |
-
});
|
| 515 |
-
console.log(`[TrigoView] AI captured ${result.capturedPositions.length} stone(s)`);
|
| 516 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 517 |
|
| 518 |
-
|
|
|
|
| 519 |
}
|
| 520 |
} else {
|
| 521 |
-
console.log("[TrigoView] AI
|
| 522 |
-
// Handle AI pass if needed
|
| 523 |
}
|
| 524 |
} catch (err) {
|
| 525 |
console.error("[TrigoView] Failed to generate AI move:", err);
|
|
@@ -527,11 +617,39 @@
|
|
| 527 |
};
|
| 528 |
|
| 529 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 530 |
// Handle stone placement
|
| 531 |
const handleStoneClick = async (x: number, y: number, z: number) => {
|
| 532 |
if (!gameStarted.value) return;
|
| 533 |
|
| 534 |
-
//
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 535 |
const result = gameStore.makeMove(x, y, z);
|
| 536 |
|
| 537 |
if (result.success && viewport) {
|
|
@@ -656,7 +774,40 @@
|
|
| 656 |
}
|
| 657 |
};
|
| 658 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 659 |
const pass = () => {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 660 |
const previousPlayer = currentPlayer.value;
|
| 661 |
const success = gameStore.pass();
|
| 662 |
|
|
@@ -840,6 +991,265 @@
|
|
| 840 |
}
|
| 841 |
};
|
| 842 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 843 |
// Watch for current player changes to update viewport preview
|
| 844 |
watch(currentPlayer, (newPlayer) => {
|
| 845 |
if (viewport) {
|
|
@@ -862,6 +1272,20 @@
|
|
| 862 |
});
|
| 863 |
});
|
| 864 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 865 |
// Watch TGN modal to populate editable content when opened
|
| 866 |
watch(showTGNModal, (isVisible) => {
|
| 867 |
if (isVisible) {
|
|
@@ -876,8 +1300,12 @@
|
|
| 876 |
onMounted(() => {
|
| 877 |
console.log("TrigoDemo component mounted");
|
| 878 |
|
| 879 |
-
//
|
| 880 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 881 |
|
| 882 |
// If not restored from storage, initialize new game
|
| 883 |
if (!restoredFromStorage) {
|
|
@@ -947,6 +1375,171 @@
|
|
| 947 |
});
|
| 948 |
}
|
| 949 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 950 |
// Add keyboard shortcuts
|
| 951 |
window.addEventListener("keydown", handleKeyPress);
|
| 952 |
});
|
|
@@ -978,6 +1571,22 @@
|
|
| 978 |
// Remove keyboard shortcuts
|
| 979 |
window.removeEventListener("keydown", handleKeyPress);
|
| 980 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 981 |
// Disconnect ResizeObserver
|
| 982 |
if (resizeObserver) {
|
| 983 |
resizeObserver.disconnect();
|
|
@@ -1081,7 +1690,7 @@
|
|
| 1081 |
|
| 1082 |
.view-header {
|
| 1083 |
display: flex;
|
| 1084 |
-
justify-content:
|
| 1085 |
align-items: center;
|
| 1086 |
padding: 1rem 2rem;
|
| 1087 |
background: linear-gradient(135deg, #505050 0%, #454545 100%);
|
|
@@ -1225,6 +1834,21 @@
|
|
| 1225 |
font-size: 1.1rem;
|
| 1226 |
}
|
| 1227 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1228 |
.players-info {
|
| 1229 |
display: flex;
|
| 1230 |
align-items: center;
|
|
@@ -1234,11 +1858,24 @@
|
|
| 1234 |
border-radius: 8px;
|
| 1235 |
}
|
| 1236 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1237 |
.player-name {
|
| 1238 |
color: #e0e0e0;
|
| 1239 |
font-weight: 600;
|
| 1240 |
}
|
| 1241 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1242 |
.vs-divider {
|
| 1243 |
color: #606060;
|
| 1244 |
font-weight: 500;
|
|
@@ -1325,6 +1962,68 @@
|
|
| 1325 |
}
|
| 1326 |
}
|
| 1327 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1328 |
.view-body {
|
| 1329 |
display: flex;
|
| 1330 |
flex: 1;
|
|
@@ -1690,68 +2389,6 @@
|
|
| 1690 |
}
|
| 1691 |
}
|
| 1692 |
|
| 1693 |
-
.settings-section {
|
| 1694 |
-
.settings-content {
|
| 1695 |
-
display: flex;
|
| 1696 |
-
flex-direction: column;
|
| 1697 |
-
gap: 1rem;
|
| 1698 |
-
|
| 1699 |
-
.setting-item {
|
| 1700 |
-
display: flex;
|
| 1701 |
-
align-items: center;
|
| 1702 |
-
justify-content: space-between;
|
| 1703 |
-
|
| 1704 |
-
label {
|
| 1705 |
-
font-weight: 600;
|
| 1706 |
-
color: #a0a0a0;
|
| 1707 |
-
display: flex;
|
| 1708 |
-
align-items: center;
|
| 1709 |
-
gap: 0.3rem;
|
| 1710 |
-
|
| 1711 |
-
.dirty-indicator {
|
| 1712 |
-
color: #e94560;
|
| 1713 |
-
font-size: 1.2rem;
|
| 1714 |
-
font-weight: 700;
|
| 1715 |
-
animation: pulse 2s ease-in-out infinite;
|
| 1716 |
-
}
|
| 1717 |
-
}
|
| 1718 |
-
|
| 1719 |
-
select {
|
| 1720 |
-
padding: 0.5rem;
|
| 1721 |
-
border: 2px solid #505050;
|
| 1722 |
-
border-radius: 8px;
|
| 1723 |
-
background-color: #484848;
|
| 1724 |
-
color: #e0e0e0;
|
| 1725 |
-
cursor: pointer;
|
| 1726 |
-
font-weight: 600;
|
| 1727 |
-
|
| 1728 |
-
&:disabled {
|
| 1729 |
-
opacity: 0.5;
|
| 1730 |
-
cursor: not-allowed;
|
| 1731 |
-
}
|
| 1732 |
-
}
|
| 1733 |
-
}
|
| 1734 |
-
|
| 1735 |
-
.btn-new-game {
|
| 1736 |
-
width: 100%;
|
| 1737 |
-
padding: 0.75rem;
|
| 1738 |
-
background-color: #e94560;
|
| 1739 |
-
color: #fff;
|
| 1740 |
-
border: none;
|
| 1741 |
-
border-radius: 8px;
|
| 1742 |
-
font-weight: 700;
|
| 1743 |
-
cursor: pointer;
|
| 1744 |
-
transition: all 0.3s ease;
|
| 1745 |
-
text-transform: uppercase;
|
| 1746 |
-
|
| 1747 |
-
&:hover {
|
| 1748 |
-
background-color: #f95670;
|
| 1749 |
-
transform: translateY(-2px);
|
| 1750 |
-
box-shadow: 0 4px 12px rgba(233, 69, 96, 0.4);
|
| 1751 |
-
}
|
| 1752 |
-
}
|
| 1753 |
-
}
|
| 1754 |
-
}
|
| 1755 |
}
|
| 1756 |
}
|
| 1757 |
}
|
|
|
|
| 41 |
<div v-else-if="gameMode === 'vs-people'" class="view-status people-mode">
|
| 42 |
<div class="room-info">
|
| 43 |
<span class="room-label">Room:</span>
|
| 44 |
+
<span class="room-code">{{ playerStore.roomId || "---" }}</span>
|
| 45 |
+
<button
|
| 46 |
+
v-if="playerStore.roomId"
|
| 47 |
+
class="btn-copy-room"
|
| 48 |
+
@click="copyRoomCode"
|
| 49 |
+
title="Copy room code"
|
| 50 |
+
>
|
| 51 |
+
📋
|
| 52 |
+
</button>
|
| 53 |
</div>
|
| 54 |
+
|
| 55 |
<div class="players-info">
|
| 56 |
+
<div class="player-display" :class="{ 'on-turn': isLocalPlayerTurn }">
|
| 57 |
+
<inline-nickname-editor
|
| 58 |
+
:nickname="playerStore.nickname"
|
| 59 |
+
:editable="true"
|
| 60 |
+
:player-color="playerStore.playerColor"
|
| 61 |
+
@update="updateLocalNickname"
|
| 62 |
+
/>
|
| 63 |
+
</div>
|
| 64 |
+
|
| 65 |
<span class="vs-divider">vs</span>
|
| 66 |
+
|
| 67 |
+
<div class="player-display" :class="{ 'on-turn': !isLocalPlayerTurn }">
|
| 68 |
+
<inline-nickname-editor
|
| 69 |
+
v-if="playerStore.opponentNickname"
|
| 70 |
+
:nickname="playerStore.opponentNickname"
|
| 71 |
+
:editable="false"
|
| 72 |
+
:player-color="opponentPlayerColor"
|
| 73 |
+
/>
|
| 74 |
+
<span v-else class="waiting-text">Waiting...</span>
|
| 75 |
+
</div>
|
| 76 |
</div>
|
| 77 |
+
|
| 78 |
+
<span class="connection-status" :class="connectionStatusClass">
|
| 79 |
+
{{ connectionStatusIcon }} {{ connectionStatusText }}
|
| 80 |
+
</span>
|
| 81 |
</div>
|
| 82 |
|
| 83 |
<!-- Library Mode: Game Info & Controls -->
|
|
|
|
| 91 |
<button class="btn-library btn-export" title="Export TGN">📤 Export</button>
|
| 92 |
</div>
|
| 93 |
</div>
|
| 94 |
+
|
| 95 |
+
<!-- Header Controls (right side) -->
|
| 96 |
+
<div class="header-controls" v-if="gameMode === 'single'">
|
| 97 |
+
<select v-model="selectedBoardShape" class="board-shape-select">
|
| 98 |
+
<option value="3x3x3">3×3×3</option>
|
| 99 |
+
<option value="5x5x5">5×5×5</option>
|
| 100 |
+
<option value="7x7x7">7×7×7</option>
|
| 101 |
+
<option value="9x9x1">9×9×1 (2D)</option>
|
| 102 |
+
<option value="9x9x9">9×9×9</option>
|
| 103 |
+
<option value="5x5x1">5×5×1 (2D)</option>
|
| 104 |
+
<option value="7x7x1">7×7×1 (2D)</option>
|
| 105 |
+
</select>
|
| 106 |
+
<button class="btn-reset" @click="newGame">
|
| 107 |
+
{{ gameStarted ? "Reset" : "Start" }}
|
| 108 |
+
</button>
|
| 109 |
+
</div>
|
| 110 |
+
|
| 111 |
+
<div class="header-controls" v-else-if="gameMode === 'vs-ai'">
|
| 112 |
+
<select v-model="selectedBoardShape" class="board-shape-select">
|
| 113 |
+
<option value="3x3x3">3×3×3</option>
|
| 114 |
+
<option value="5x5x5">5×5×5</option>
|
| 115 |
+
<option value="7x7x7">7×7×7</option>
|
| 116 |
+
<option value="9x9x1">9×9×1 (2D)</option>
|
| 117 |
+
<option value="9x9x9">9×9×9</option>
|
| 118 |
+
<option value="5x5x1">5×5×1 (2D)</option>
|
| 119 |
+
<option value="7x7x1">7×7×1 (2D)</option>
|
| 120 |
+
</select>
|
| 121 |
+
<button class="btn-reset" @click="newGame">
|
| 122 |
+
{{ gameStarted ? "Reset" : "Start" }}
|
| 123 |
+
</button>
|
| 124 |
+
</div>
|
| 125 |
+
|
| 126 |
+
<div class="header-controls" v-else-if="gameMode === 'vs-people'">
|
| 127 |
+
<select
|
| 128 |
+
v-model="preferredColor"
|
| 129 |
+
class="color-preference-select"
|
| 130 |
+
:disabled="gameStarted || connectionStatus === 'in-room'"
|
| 131 |
+
>
|
| 132 |
+
<option value="black">Play as Black</option>
|
| 133 |
+
<option value="white">Play as White</option>
|
| 134 |
+
</select>
|
| 135 |
+
|
| 136 |
+
<select v-model="selectedBoardShape" class="board-shape-select">
|
| 137 |
+
<option value="3x3x3">3×3×3</option>
|
| 138 |
+
<option value="5x5x5">5×5×5</option>
|
| 139 |
+
<option value="7x7x7">7×7×7</option>
|
| 140 |
+
<option value="9x9x1">9×9×1 (2D)</option>
|
| 141 |
+
<option value="9x9x9">9×9×9</option>
|
| 142 |
+
<option value="5x5x1">5×5×1 (2D)</option>
|
| 143 |
+
<option value="7x7x1">7×7×1 (2D)</option>
|
| 144 |
+
</select>
|
| 145 |
+
|
| 146 |
+
<button class="btn-reset" @click="resetMultiplayerGame">Reset</button>
|
| 147 |
+
</div>
|
| 148 |
</div>
|
| 149 |
|
| 150 |
<div class="view-body">
|
|
|
|
| 296 |
</div>
|
| 297 |
</div>
|
| 298 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 299 |
</div>
|
| 300 |
</div>
|
| 301 |
|
|
|
|
| 334 |
import { useRoute } from "vue-router";
|
| 335 |
import { TrigoViewport } from "@/services/trigoViewport";
|
| 336 |
import { useGameStore } from "@/stores/gameStore";
|
| 337 |
+
import { usePlayerStore } from "@/stores/playerStore";
|
| 338 |
import { useTrigoAgent } from "@/composables/useTrigoAgent";
|
| 339 |
+
import { useSocket } from "@/composables/useSocket";
|
| 340 |
+
import { useRoomHash } from "@/composables/useRoomHash";
|
| 341 |
+
import InlineNicknameEditor from "@/components/InlineNicknameEditor.vue";
|
| 342 |
import { storeToRefs } from "pinia";
|
| 343 |
import type { BoardShape } from "../../../inc/trigo";
|
| 344 |
import { Stone, validateMove, StoneType, validateTGN } from "../../../inc/trigo";
|
|
|
|
| 350 |
const route = useRoute();
|
| 351 |
const gameMode = computed(() => (route.meta.mode as string) || "single");
|
| 352 |
|
| 353 |
+
// Game store
|
| 354 |
+
const gameStore = useGameStore();
|
| 355 |
+
const {
|
| 356 |
+
game,
|
| 357 |
+
currentPlayer,
|
| 358 |
+
moveHistory,
|
| 359 |
+
currentMoveIndex,
|
| 360 |
+
capturedStones,
|
| 361 |
+
gameStatus,
|
| 362 |
+
boardShape,
|
| 363 |
+
moveCount,
|
| 364 |
+
isGameActive,
|
| 365 |
+
passCount
|
| 366 |
+
} = storeToRefs(gameStore);
|
| 367 |
+
|
| 368 |
+
// Player store (for multiplayer)
|
| 369 |
+
const playerStore = usePlayerStore();
|
| 370 |
+
|
| 371 |
+
// Socket.io (for multiplayer)
|
| 372 |
+
const socketApi = useSocket();
|
| 373 |
+
|
| 374 |
+
// Room hash management (for VS People mode)
|
| 375 |
+
const { getRoomIdFromHash, updateHash, clearHash, isValidRoomId } = useRoomHash();
|
| 376 |
+
const isJoiningRoom = ref(false);
|
| 377 |
+
|
| 378 |
// Helper functions for board shape parsing
|
| 379 |
const parseBoardShape = (shapeStr: string): BoardShape => {
|
| 380 |
const parts = shapeStr
|
|
|
|
| 394 |
return encodeAb0yz([move.x, move.y, move.z], [shape.x, shape.y, shape.z]);
|
| 395 |
};
|
| 396 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 397 |
// AI Agent (for VS AI mode)
|
| 398 |
const aiAgent = useTrigoAgent();
|
| 399 |
const { isReady: aiReady, isThinking: aiThinking, error: aiError, lastMoveTime } = aiAgent;
|
|
|
|
| 407 |
const aiPlayerColor = ref<"black" | "white">(loadAIColor());
|
| 408 |
const humanPlayerColor = computed(() => (aiPlayerColor.value === "white" ? "black" : "white"));
|
| 409 |
|
| 410 |
+
// VS People color preference with storage persistence
|
| 411 |
+
const loadPreferredColor = (): "black" | "white" => {
|
| 412 |
+
const stored = storage.getString("vs-people-color-preference");
|
| 413 |
+
return stored === "black" || stored === "white" ? stored : "black";
|
| 414 |
+
};
|
| 415 |
+
|
| 416 |
+
const preferredColor = ref<"black" | "white">(loadPreferredColor());
|
| 417 |
+
|
| 418 |
+
// Watch for color preference changes and persist to storage
|
| 419 |
+
watch(preferredColor, (newColor) => {
|
| 420 |
+
storage.setString("vs-people-color-preference", newColor);
|
| 421 |
+
});
|
| 422 |
+
|
| 423 |
// Local state
|
| 424 |
const hoveredPosition = ref<string | null>(null);
|
| 425 |
const blackScore = ref(0);
|
|
|
|
| 577 |
|
| 578 |
// Apply AI move if valid
|
| 579 |
if (aiMove) {
|
| 580 |
+
// Check if AI chose to pass
|
| 581 |
+
if (aiMove.isPass) {
|
| 582 |
+
console.log("[TrigoView] AI chose to pass");
|
| 583 |
+
const result = gameStore.pass();
|
| 584 |
+
if (result.success) {
|
| 585 |
+
console.log(`[TrigoView] AI pass applied successfully (${lastMoveTime.value.toFixed(0)}ms)`);
|
| 586 |
+
} else {
|
| 587 |
+
console.error("[TrigoView] Failed to apply AI pass");
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 588 |
}
|
| 589 |
+
} else {
|
| 590 |
+
console.log(`[TrigoView] AI suggests move at (${aiMove.x}, ${aiMove.y}, ${aiMove.z})`);
|
| 591 |
+
|
| 592 |
+
// Make move in store
|
| 593 |
+
const result = gameStore.makeMove(aiMove.x, aiMove.y, aiMove.z);
|
| 594 |
+
|
| 595 |
+
if (result.success && viewport) {
|
| 596 |
+
// Add AI stone to viewport
|
| 597 |
+
const stoneColor = gameStore.opponentPlayer;
|
| 598 |
+
viewport.addStone(aiMove.x, aiMove.y, aiMove.z, stoneColor);
|
| 599 |
+
|
| 600 |
+
// Remove captured stones
|
| 601 |
+
if (result.capturedPositions && result.capturedPositions.length > 0) {
|
| 602 |
+
result.capturedPositions.forEach((pos: any) => {
|
| 603 |
+
viewport.removeStone(pos.x, pos.y, pos.z);
|
| 604 |
+
});
|
| 605 |
+
console.log(`[TrigoView] AI captured ${result.capturedPositions.length} stone(s)`);
|
| 606 |
+
}
|
| 607 |
|
| 608 |
+
console.log(`[TrigoView] AI move applied successfully (${lastMoveTime.value.toFixed(0)}ms)`);
|
| 609 |
+
}
|
| 610 |
}
|
| 611 |
} else {
|
| 612 |
+
console.log("[TrigoView] AI returned no move");
|
|
|
|
| 613 |
}
|
| 614 |
} catch (err) {
|
| 615 |
console.error("[TrigoView] Failed to generate AI move:", err);
|
|
|
|
| 617 |
};
|
| 618 |
|
| 619 |
|
| 620 |
+
// Sync viewport with current game state (for undo/redo/reset)
|
| 621 |
+
const syncViewportWithGame = () => {
|
| 622 |
+
if (!viewport) return;
|
| 623 |
+
|
| 624 |
+
// Get all stones from game store and add to viewport
|
| 625 |
+
const stones = gameStore.getAllStones();
|
| 626 |
+
for (const stone of stones) {
|
| 627 |
+
viewport.addStone(stone.x, stone.y, stone.z, stone.color);
|
| 628 |
+
}
|
| 629 |
+
|
| 630 |
+
console.log(`[TrigoView] Synced viewport with ${stones.length} stones`);
|
| 631 |
+
};
|
| 632 |
+
|
| 633 |
+
|
| 634 |
// Handle stone placement
|
| 635 |
const handleStoneClick = async (x: number, y: number, z: number) => {
|
| 636 |
if (!gameStarted.value) return;
|
| 637 |
|
| 638 |
+
// VS People mode: validate turn and emit socket event
|
| 639 |
+
if (gameMode.value === "vs-people") {
|
| 640 |
+
// Check if it's the local player's turn
|
| 641 |
+
if (!isLocalPlayerTurn.value) {
|
| 642 |
+
console.log("[TrigoView] Not your turn, ignoring click");
|
| 643 |
+
return;
|
| 644 |
+
}
|
| 645 |
+
|
| 646 |
+
// Emit move to server (server will validate and broadcast)
|
| 647 |
+
console.log(`[TrigoView] Emitting makeMove: (${x}, ${y}, ${z})`);
|
| 648 |
+
socketApi.makeMove(x, y, z);
|
| 649 |
+
return;
|
| 650 |
+
}
|
| 651 |
+
|
| 652 |
+
// Single player and VS AI mode: make move locally
|
| 653 |
const result = gameStore.makeMove(x, y, z);
|
| 654 |
|
| 655 |
if (result.success && viewport) {
|
|
|
|
| 774 |
}
|
| 775 |
};
|
| 776 |
|
| 777 |
+
// Reset game for multiplayer (VS People mode)
|
| 778 |
+
const resetMultiplayerGame = () => {
|
| 779 |
+
if (!confirm("Reset the game? This will clear all moves.")) return;
|
| 780 |
+
|
| 781 |
+
const shape = parseBoardShape(selectedBoardShape.value);
|
| 782 |
+
const swapColors = playerStore.playerColor !== preferredColor.value;
|
| 783 |
+
|
| 784 |
+
socketApi.socket.emit("resetGame", {
|
| 785 |
+
boardShape: shape,
|
| 786 |
+
swapColors: swapColors
|
| 787 |
+
}, (response: any) => {
|
| 788 |
+
if (response.success) {
|
| 789 |
+
console.log("Multiplayer game reset successfully");
|
| 790 |
+
// Local reset is handled by gameReset event listener
|
| 791 |
+
} else {
|
| 792 |
+
console.error("Reset failed:", response.error);
|
| 793 |
+
alert(`Failed to reset game: ${response.error || "Unknown error"}`);
|
| 794 |
+
}
|
| 795 |
+
});
|
| 796 |
+
};
|
| 797 |
+
|
| 798 |
const pass = () => {
|
| 799 |
+
// VS People mode: validate turn and emit socket event
|
| 800 |
+
if (gameMode.value === "vs-people") {
|
| 801 |
+
if (!isLocalPlayerTurn.value) {
|
| 802 |
+
console.log("[TrigoView] Not your turn, cannot pass");
|
| 803 |
+
return;
|
| 804 |
+
}
|
| 805 |
+
console.log("[TrigoView] Emitting pass");
|
| 806 |
+
socketApi.pass();
|
| 807 |
+
return;
|
| 808 |
+
}
|
| 809 |
+
|
| 810 |
+
// Single player and VS AI mode: pass locally
|
| 811 |
const previousPlayer = currentPlayer.value;
|
| 812 |
const success = gameStore.pass();
|
| 813 |
|
|
|
|
| 991 |
}
|
| 992 |
};
|
| 993 |
|
| 994 |
+
// ===== VS People Mode: Nickname Management =====
|
| 995 |
+
|
| 996 |
+
// Computed properties for multiplayer
|
| 997 |
+
const opponentPlayerColor = computed(() => {
|
| 998 |
+
if (!playerStore.playerColor) return null;
|
| 999 |
+
return playerStore.playerColor === "black" ? "white" : "black";
|
| 1000 |
+
});
|
| 1001 |
+
|
| 1002 |
+
const isLocalPlayerTurn = computed(() => {
|
| 1003 |
+
return currentPlayer.value === playerStore.playerColor;
|
| 1004 |
+
});
|
| 1005 |
+
|
| 1006 |
+
const connectionStatusClass = computed(() => {
|
| 1007 |
+
return playerStore.connectionStatus === "in-room" ? "connected" : "disconnected";
|
| 1008 |
+
});
|
| 1009 |
+
|
| 1010 |
+
const connectionStatusText = computed(() => {
|
| 1011 |
+
if (playerStore.connectionStatus === "in-room") return "Connected";
|
| 1012 |
+
if (playerStore.connectionStatus === "connected") return "Joining room...";
|
| 1013 |
+
return "Disconnected";
|
| 1014 |
+
});
|
| 1015 |
+
|
| 1016 |
+
const connectionStatusIcon = computed(() => {
|
| 1017 |
+
if (playerStore.connectionStatus === "in-room") return "🟢";
|
| 1018 |
+
if (playerStore.connectionStatus === "connected") return "🟡"; // Yellow while joining
|
| 1019 |
+
return "🔴";
|
| 1020 |
+
});
|
| 1021 |
+
|
| 1022 |
+
// Update local player nickname
|
| 1023 |
+
const updateLocalNickname = (newNickname: string) => {
|
| 1024 |
+
const success = playerStore.setNickname(newNickname);
|
| 1025 |
+
|
| 1026 |
+
if (!success) {
|
| 1027 |
+
console.error("Failed to update nickname");
|
| 1028 |
+
return;
|
| 1029 |
+
}
|
| 1030 |
+
|
| 1031 |
+
// If in a room, broadcast nickname change to opponents
|
| 1032 |
+
if (playerStore.isInRoom) {
|
| 1033 |
+
socketApi.changeNickname(newNickname, (response: any) => {
|
| 1034 |
+
if (!response.success) {
|
| 1035 |
+
console.error("Failed to broadcast nickname change:", response.error);
|
| 1036 |
+
alert(`Failed to change nickname: ${response.error}`);
|
| 1037 |
+
}
|
| 1038 |
+
});
|
| 1039 |
+
}
|
| 1040 |
+
};
|
| 1041 |
+
|
| 1042 |
+
// Copy room code to clipboard
|
| 1043 |
+
const copyRoomCode = () => {
|
| 1044 |
+
if (!playerStore.roomId) return;
|
| 1045 |
+
|
| 1046 |
+
navigator.clipboard
|
| 1047 |
+
.writeText(playerStore.roomId)
|
| 1048 |
+
.then(() => alert("Room code copied to clipboard!"))
|
| 1049 |
+
.catch((err) => console.error("Failed to copy room code:", err));
|
| 1050 |
+
};
|
| 1051 |
+
|
| 1052 |
+
|
| 1053 |
+
// ===== Room Management Functions (VS People Mode) =====
|
| 1054 |
+
|
| 1055 |
+
/**
|
| 1056 |
+
* Wait for socket connection with timeout
|
| 1057 |
+
* Prevents race condition where hash is read before socket connects
|
| 1058 |
+
*/
|
| 1059 |
+
async function waitForSocketConnection(timeout = 5000): Promise<void> {
|
| 1060 |
+
return new Promise((resolve, reject) => {
|
| 1061 |
+
if (socketApi.socket.connected) {
|
| 1062 |
+
resolve();
|
| 1063 |
+
return;
|
| 1064 |
+
}
|
| 1065 |
+
|
| 1066 |
+
const checkInterval = setInterval(() => {
|
| 1067 |
+
if (socketApi.socket.connected) {
|
| 1068 |
+
clearInterval(checkInterval);
|
| 1069 |
+
clearTimeout(timeoutHandle);
|
| 1070 |
+
resolve();
|
| 1071 |
+
}
|
| 1072 |
+
}, 100);
|
| 1073 |
+
|
| 1074 |
+
const timeoutHandle = setTimeout(() => {
|
| 1075 |
+
clearInterval(checkInterval);
|
| 1076 |
+
reject(new Error("Socket connection timeout"));
|
| 1077 |
+
}, timeout);
|
| 1078 |
+
});
|
| 1079 |
+
}
|
| 1080 |
+
|
| 1081 |
+
|
| 1082 |
+
/**
|
| 1083 |
+
* Initialize multiplayer room based on URL hash
|
| 1084 |
+
* - No hash: create new room
|
| 1085 |
+
* - Valid hash: join existing room
|
| 1086 |
+
* - Invalid hash: redirect to create new room
|
| 1087 |
+
*/
|
| 1088 |
+
async function initializeMultiplayerRoom() {
|
| 1089 |
+
console.log("[TrigoView] ========== INITIALIZE MULTIPLAYER ROOM ==========");
|
| 1090 |
+
console.log("[TrigoView] isJoiningRoom:", isJoiningRoom.value);
|
| 1091 |
+
console.log("[TrigoView] Socket connected:", socketApi.socket.connected);
|
| 1092 |
+
console.log("[TrigoView] Socket ID:", socketApi.socket.id);
|
| 1093 |
+
|
| 1094 |
+
if (isJoiningRoom.value) {
|
| 1095 |
+
console.log("[TrigoView] Already joining, skipping");
|
| 1096 |
+
return; // Prevent duplicate joins
|
| 1097 |
+
}
|
| 1098 |
+
|
| 1099 |
+
const hashRoomId = getRoomIdFromHash();
|
| 1100 |
+
console.log("[TrigoView] hashRoomId from URL:", hashRoomId);
|
| 1101 |
+
|
| 1102 |
+
if (!hashRoomId) {
|
| 1103 |
+
// No hash: create new room
|
| 1104 |
+
console.log("[TrigoView] No hash found, creating new room");
|
| 1105 |
+
await createAndJoinRoom();
|
| 1106 |
+
} else if (isValidRoomId(hashRoomId)) {
|
| 1107 |
+
// Valid hash: join existing room
|
| 1108 |
+
console.log("[TrigoView] Valid hash found, joining room:", hashRoomId);
|
| 1109 |
+
await joinRoomByHash(hashRoomId);
|
| 1110 |
+
} else {
|
| 1111 |
+
// Invalid hash: redirect to create new room
|
| 1112 |
+
console.warn(`[TrigoView] Invalid room ID in hash: ${hashRoomId}`);
|
| 1113 |
+
clearHash();
|
| 1114 |
+
await createAndJoinRoom();
|
| 1115 |
+
}
|
| 1116 |
+
}
|
| 1117 |
+
|
| 1118 |
+
|
| 1119 |
+
/**
|
| 1120 |
+
* Create a new room and join it
|
| 1121 |
+
*/
|
| 1122 |
+
async function createAndJoinRoom() {
|
| 1123 |
+
console.log("[TrigoView] createAndJoinRoom called, isJoiningRoom:", isJoiningRoom.value);
|
| 1124 |
+
if (isJoiningRoom.value) {
|
| 1125 |
+
console.log("[TrigoView] Already joining, returning");
|
| 1126 |
+
return;
|
| 1127 |
+
}
|
| 1128 |
+
isJoiningRoom.value = true;
|
| 1129 |
+
console.log("[TrigoView] Set isJoiningRoom to true");
|
| 1130 |
+
|
| 1131 |
+
try {
|
| 1132 |
+
console.log("[TrigoView] Waiting for socket connection...");
|
| 1133 |
+
await waitForSocketConnection();
|
| 1134 |
+
console.log("[TrigoView] Socket connection ready, calling joinRoom");
|
| 1135 |
+
|
| 1136 |
+
socketApi.joinRoom({ nickname: playerStore.nickname }, (response: any) => {
|
| 1137 |
+
console.log("[TrigoView] joinRoom callback received:", response);
|
| 1138 |
+
isJoiningRoom.value = false;
|
| 1139 |
+
|
| 1140 |
+
if (response.success !== false && response.roomId) {
|
| 1141 |
+
playerStore.joinRoom(response.roomId, response.playerColor);
|
| 1142 |
+
updateHash(response.roomId);
|
| 1143 |
+
console.log(`[TrigoView] Room created: ${response.roomId}`);
|
| 1144 |
+
} else {
|
| 1145 |
+
console.error("[TrigoView] Failed to create room:", response.error);
|
| 1146 |
+
alert("Failed to create room. Please try again.");
|
| 1147 |
+
}
|
| 1148 |
+
});
|
| 1149 |
+
} catch (err) {
|
| 1150 |
+
isJoiningRoom.value = false;
|
| 1151 |
+
console.error("[TrigoView] Socket connection failed:", err);
|
| 1152 |
+
alert("Connection failed. Please check your internet and try again.");
|
| 1153 |
+
}
|
| 1154 |
+
}
|
| 1155 |
+
|
| 1156 |
+
|
| 1157 |
+
/**
|
| 1158 |
+
* Join an existing room by room ID from hash
|
| 1159 |
+
*/
|
| 1160 |
+
async function joinRoomByHash(roomId: string) {
|
| 1161 |
+
if (isJoiningRoom.value) return;
|
| 1162 |
+
isJoiningRoom.value = true;
|
| 1163 |
+
|
| 1164 |
+
try {
|
| 1165 |
+
await waitForSocketConnection();
|
| 1166 |
+
|
| 1167 |
+
socketApi.joinRoom({ roomId, nickname: playerStore.nickname }, (response: any) => {
|
| 1168 |
+
isJoiningRoom.value = false;
|
| 1169 |
+
|
| 1170 |
+
if (response.success !== false && response.roomId) {
|
| 1171 |
+
playerStore.joinRoom(response.roomId, response.playerColor);
|
| 1172 |
+
console.log(`[TrigoView] Room joined: ${response.roomId}`);
|
| 1173 |
+
|
| 1174 |
+
// Extract opponent from players list
|
| 1175 |
+
if (response.players && socketApi.socket.id) {
|
| 1176 |
+
for (const [playerId, player] of Object.entries(response.players) as [string, any][]) {
|
| 1177 |
+
if (playerId !== socketApi.socket.id) {
|
| 1178 |
+
playerStore.setOpponentNickname(player.nickname);
|
| 1179 |
+
console.log(`[TrigoView] Opponent found: ${player.nickname}`);
|
| 1180 |
+
break;
|
| 1181 |
+
}
|
| 1182 |
+
}
|
| 1183 |
+
}
|
| 1184 |
+
|
| 1185 |
+
// Start game if both players are present
|
| 1186 |
+
const playerCount = response.players ? Object.keys(response.players).length : 0;
|
| 1187 |
+
if (playerCount >= 2 && !gameStarted.value) {
|
| 1188 |
+
gameStore.startGame();
|
| 1189 |
+
if (viewport) {
|
| 1190 |
+
viewport.setGameActive(true);
|
| 1191 |
+
}
|
| 1192 |
+
console.log("[TrigoView] Game started - both players present");
|
| 1193 |
+
}
|
| 1194 |
+
} else {
|
| 1195 |
+
console.warn(`[TrigoView] Failed to join room:`, response.error);
|
| 1196 |
+
handleJoinFailure(response.error || "Failed to join room");
|
| 1197 |
+
}
|
| 1198 |
+
});
|
| 1199 |
+
} catch (err) {
|
| 1200 |
+
isJoiningRoom.value = false;
|
| 1201 |
+
console.error("[TrigoView] Socket connection failed:", err);
|
| 1202 |
+
alert("Connection failed. Please check your internet and try again.");
|
| 1203 |
+
}
|
| 1204 |
+
}
|
| 1205 |
+
|
| 1206 |
+
|
| 1207 |
+
/**
|
| 1208 |
+
* Handle room join failure
|
| 1209 |
+
* Clears invalid hash and creates new room
|
| 1210 |
+
*/
|
| 1211 |
+
function handleJoinFailure(error: string) {
|
| 1212 |
+
clearHash();
|
| 1213 |
+
|
| 1214 |
+
if (error.includes("not found")) {
|
| 1215 |
+
alert("Room not found. Creating a new room...");
|
| 1216 |
+
} else if (error.includes("full")) {
|
| 1217 |
+
alert("This room is full. Creating a new room...");
|
| 1218 |
+
} else {
|
| 1219 |
+
alert(`Failed to join room: ${error}. Creating a new room...`);
|
| 1220 |
+
}
|
| 1221 |
+
|
| 1222 |
+
createAndJoinRoom();
|
| 1223 |
+
}
|
| 1224 |
+
|
| 1225 |
+
|
| 1226 |
+
/**
|
| 1227 |
+
* Handle browser navigation (back/forward) and manual hash edits
|
| 1228 |
+
*/
|
| 1229 |
+
function handleHashChange() {
|
| 1230 |
+
if (gameMode.value !== "vs-people") return;
|
| 1231 |
+
|
| 1232 |
+
const currentHash = getRoomIdFromHash();
|
| 1233 |
+
const currentRoom = playerStore.roomId;
|
| 1234 |
+
|
| 1235 |
+
if (currentHash !== currentRoom) {
|
| 1236 |
+
if (currentHash && isValidRoomId(currentHash)) {
|
| 1237 |
+
// User navigated to different room hash
|
| 1238 |
+
if (currentRoom) socketApi.leaveRoom();
|
| 1239 |
+
joinRoomByHash(currentHash);
|
| 1240 |
+
} else {
|
| 1241 |
+
// Invalid hash or removed hash
|
| 1242 |
+
if (currentRoom) {
|
| 1243 |
+
updateHash(currentRoom); // Restore correct hash
|
| 1244 |
+
} else {
|
| 1245 |
+
createAndJoinRoom(); // Create new room
|
| 1246 |
+
}
|
| 1247 |
+
}
|
| 1248 |
+
}
|
| 1249 |
+
}
|
| 1250 |
+
|
| 1251 |
+
// ===== Watchers =====
|
| 1252 |
+
|
| 1253 |
// Watch for current player changes to update viewport preview
|
| 1254 |
watch(currentPlayer, (newPlayer) => {
|
| 1255 |
if (viewport) {
|
|
|
|
| 1272 |
});
|
| 1273 |
});
|
| 1274 |
|
| 1275 |
+
// Watch playerStore.roomId to sync URL hash (VS People mode)
|
| 1276 |
+
watch(
|
| 1277 |
+
() => playerStore.roomId,
|
| 1278 |
+
(newRoomId) => {
|
| 1279 |
+
if (gameMode.value !== "vs-people") return;
|
| 1280 |
+
|
| 1281 |
+
if (newRoomId) {
|
| 1282 |
+
updateHash(newRoomId);
|
| 1283 |
+
} else {
|
| 1284 |
+
clearHash();
|
| 1285 |
+
}
|
| 1286 |
+
}
|
| 1287 |
+
);
|
| 1288 |
+
|
| 1289 |
// Watch TGN modal to populate editable content when opened
|
| 1290 |
watch(showTGNModal, (isVisible) => {
|
| 1291 |
if (isVisible) {
|
|
|
|
| 1300 |
onMounted(() => {
|
| 1301 |
console.log("TrigoDemo component mounted");
|
| 1302 |
|
| 1303 |
+
// In VS People mode, always start fresh (server maintains authoritative state)
|
| 1304 |
+
// In other modes, try to restore from session storage
|
| 1305 |
+
let restoredFromStorage = false;
|
| 1306 |
+
if (gameMode.value !== "vs-people") {
|
| 1307 |
+
restoredFromStorage = gameStore.loadFromSessionStorage();
|
| 1308 |
+
}
|
| 1309 |
|
| 1310 |
// If not restored from storage, initialize new game
|
| 1311 |
if (!restoredFromStorage) {
|
|
|
|
| 1375 |
});
|
| 1376 |
}
|
| 1377 |
|
| 1378 |
+
// Setup socket listeners for VS People mode
|
| 1379 |
+
if (gameMode.value === "vs-people") {
|
| 1380 |
+
console.log("[TrigoView] Setting up socket listeners for VS People mode...");
|
| 1381 |
+
|
| 1382 |
+
// Listen for nickname changes
|
| 1383 |
+
socketApi.onNicknameChanged((data) => {
|
| 1384 |
+
console.log(
|
| 1385 |
+
`[TrigoView] Player ${data.playerId} changed nickname: ${data.oldNickname} -> ${data.nickname}`
|
| 1386 |
+
);
|
| 1387 |
+
|
| 1388 |
+
// If it's not us, update opponent nickname
|
| 1389 |
+
if (data.playerId !== socketApi.socket.id) {
|
| 1390 |
+
playerStore.setOpponentNickname(data.nickname);
|
| 1391 |
+
}
|
| 1392 |
+
});
|
| 1393 |
+
|
| 1394 |
+
// Listen for game updates (moves, passes, undo, redo)
|
| 1395 |
+
socketApi.onGameUpdate((data) => {
|
| 1396 |
+
console.log("[TrigoView] Game update received:", data);
|
| 1397 |
+
|
| 1398 |
+
// Update current player from server
|
| 1399 |
+
if (data.currentPlayer) {
|
| 1400 |
+
gameStore.setCurrentPlayer(data.currentPlayer);
|
| 1401 |
+
}
|
| 1402 |
+
|
| 1403 |
+
// Handle different actions
|
| 1404 |
+
if (data.action === "move" && data.lastMove) {
|
| 1405 |
+
const { x, y, z } = data.lastMove;
|
| 1406 |
+
// Apply move to local game state
|
| 1407 |
+
const result = gameStore.makeMove(x, y, z);
|
| 1408 |
+
|
| 1409 |
+
if (result.success && viewport) {
|
| 1410 |
+
// Add stone to viewport
|
| 1411 |
+
const stoneColor = gameStore.opponentPlayer;
|
| 1412 |
+
viewport.addStone(x, y, z, stoneColor);
|
| 1413 |
+
|
| 1414 |
+
// Remove captured stones
|
| 1415 |
+
if (data.capturedPositions && data.capturedPositions.length > 0) {
|
| 1416 |
+
data.capturedPositions.forEach((pos: { x: number; y: number; z: number }) => {
|
| 1417 |
+
viewport.removeStone(pos.x, pos.y, pos.z);
|
| 1418 |
+
});
|
| 1419 |
+
console.log(`[TrigoView] Captured ${data.capturedPositions.length} stone(s)`);
|
| 1420 |
+
}
|
| 1421 |
+
|
| 1422 |
+
// Hide territory visualization
|
| 1423 |
+
viewport.hideDomainCubes();
|
| 1424 |
+
showTerritoryMode.value = false;
|
| 1425 |
+
}
|
| 1426 |
+
} else if (data.action === "pass") {
|
| 1427 |
+
gameStore.pass();
|
| 1428 |
+
console.log("[TrigoView] Pass received from server");
|
| 1429 |
+
} else if (data.action === "undo" || data.action === "redo") {
|
| 1430 |
+
// For undo/redo, reload game from TGN
|
| 1431 |
+
if (data.tgn && viewport) {
|
| 1432 |
+
gameStore.loadFromTGN(data.tgn);
|
| 1433 |
+
// Resync viewport with game state
|
| 1434 |
+
viewport.clearBoard();
|
| 1435 |
+
syncViewportWithGame();
|
| 1436 |
+
}
|
| 1437 |
+
}
|
| 1438 |
+
});
|
| 1439 |
+
|
| 1440 |
+
// Listen for player joined
|
| 1441 |
+
socketApi.onPlayerJoined((data) => {
|
| 1442 |
+
console.log("[TrigoView] Player joined:", data);
|
| 1443 |
+
playerStore.setOpponentNickname(data.nickname);
|
| 1444 |
+
|
| 1445 |
+
// Start game when second player joins
|
| 1446 |
+
if (!gameStarted.value) {
|
| 1447 |
+
gameStore.startGame();
|
| 1448 |
+
if (viewport) {
|
| 1449 |
+
viewport.setGameActive(true);
|
| 1450 |
+
}
|
| 1451 |
+
console.log("[TrigoView] Game started - opponent joined");
|
| 1452 |
+
}
|
| 1453 |
+
});
|
| 1454 |
+
|
| 1455 |
+
// Listen for player left
|
| 1456 |
+
socketApi.onPlayerLeft((data) => {
|
| 1457 |
+
console.log("[TrigoView] Player left:", data);
|
| 1458 |
+
playerStore.setOpponentNickname(null);
|
| 1459 |
+
});
|
| 1460 |
+
|
| 1461 |
+
// Listen for player disconnected
|
| 1462 |
+
socketApi.onPlayerDisconnected((data) => {
|
| 1463 |
+
console.log("[TrigoView] Player disconnected:", data);
|
| 1464 |
+
if (data.playerId !== socketApi.socket.id) {
|
| 1465 |
+
playerStore.setOpponentNickname(null);
|
| 1466 |
+
}
|
| 1467 |
+
});
|
| 1468 |
+
|
| 1469 |
+
// Listen for game ended
|
| 1470 |
+
socketApi.onGameEnded((data) => {
|
| 1471 |
+
console.log("[TrigoView] Game ended:", data);
|
| 1472 |
+
alert(`Game ended! Winner: ${data.winner || "None"}\nReason: ${data.reason}`);
|
| 1473 |
+
});
|
| 1474 |
+
|
| 1475 |
+
// Listen for game reset
|
| 1476 |
+
socketApi.onGameReset((data) => {
|
| 1477 |
+
console.log("[TrigoView] Game reset:", data);
|
| 1478 |
+
// Reload game state
|
| 1479 |
+
if (data.tgn) {
|
| 1480 |
+
gameStore.loadFromTGN(data.tgn);
|
| 1481 |
+
} else {
|
| 1482 |
+
gameStore.resetGame(boardShape.value);
|
| 1483 |
+
}
|
| 1484 |
+
if (viewport) {
|
| 1485 |
+
viewport.clearBoard();
|
| 1486 |
+
}
|
| 1487 |
+
// Update player colors if changed
|
| 1488 |
+
if (data.players && socketApi.socket.id) {
|
| 1489 |
+
const myPlayer = data.players[socketApi.socket.id];
|
| 1490 |
+
if (myPlayer) {
|
| 1491 |
+
playerStore.playerColor = myPlayer.color;
|
| 1492 |
+
}
|
| 1493 |
+
}
|
| 1494 |
+
});
|
| 1495 |
+
|
| 1496 |
+
// Listen for errors
|
| 1497 |
+
socketApi.onError((data) => {
|
| 1498 |
+
console.error("[TrigoView] Socket error:", data.message);
|
| 1499 |
+
});
|
| 1500 |
+
|
| 1501 |
+
// Set player ID when socket connects
|
| 1502 |
+
if (socketApi.socket.id) {
|
| 1503 |
+
playerStore.setPlayerId(socketApi.socket.id);
|
| 1504 |
+
}
|
| 1505 |
+
|
| 1506 |
+
// Listen for browser navigation (back/forward) and manual hash edits
|
| 1507 |
+
window.addEventListener("hashchange", handleHashChange);
|
| 1508 |
+
|
| 1509 |
+
// Handle socket connection and reconnection
|
| 1510 |
+
socketApi.socket.on("connect", () => {
|
| 1511 |
+
console.log("[TrigoView] Socket connected in VS People mode");
|
| 1512 |
+
|
| 1513 |
+
// Update player ID and connection status
|
| 1514 |
+
if (socketApi.socket.id) {
|
| 1515 |
+
playerStore.setPlayerId(socketApi.socket.id);
|
| 1516 |
+
}
|
| 1517 |
+
|
| 1518 |
+
// Initialize room based on URL hash (only if not already in a room)
|
| 1519 |
+
if (!playerStore.roomId && !isJoiningRoom.value) {
|
| 1520 |
+
console.log("[TrigoView] Initializing room after socket connection");
|
| 1521 |
+
initializeMultiplayerRoom();
|
| 1522 |
+
}
|
| 1523 |
+
});
|
| 1524 |
+
|
| 1525 |
+
// Handle socket disconnection
|
| 1526 |
+
socketApi.socket.on("disconnect", () => {
|
| 1527 |
+
console.log("[TrigoView] Socket disconnected in VS People mode");
|
| 1528 |
+
// Don't reset roomId - we want to rejoin on reconnect
|
| 1529 |
+
if (playerStore.roomId) {
|
| 1530 |
+
playerStore.connectionStatus = "connected"; // Still have room info, just disconnected
|
| 1531 |
+
} else {
|
| 1532 |
+
playerStore.disconnect();
|
| 1533 |
+
}
|
| 1534 |
+
});
|
| 1535 |
+
|
| 1536 |
+
// If socket is already connected, initialize room immediately
|
| 1537 |
+
if (socketApi.socket.connected) {
|
| 1538 |
+
console.log("[TrigoView] Socket already connected, initializing room");
|
| 1539 |
+
initializeMultiplayerRoom();
|
| 1540 |
+
}
|
| 1541 |
+
}
|
| 1542 |
+
|
| 1543 |
// Add keyboard shortcuts
|
| 1544 |
window.addEventListener("keydown", handleKeyPress);
|
| 1545 |
});
|
|
|
|
| 1571 |
// Remove keyboard shortcuts
|
| 1572 |
window.removeEventListener("keydown", handleKeyPress);
|
| 1573 |
|
| 1574 |
+
// Cleanup socket listeners for VS People mode
|
| 1575 |
+
if (gameMode.value === "vs-people") {
|
| 1576 |
+
console.log("[TrigoView] Cleaning up socket listeners...");
|
| 1577 |
+
socketApi.offNicknameChanged();
|
| 1578 |
+
socketApi.offGameUpdate();
|
| 1579 |
+
socketApi.offPlayerJoined();
|
| 1580 |
+
socketApi.offPlayerLeft();
|
| 1581 |
+
socketApi.offPlayerDisconnected();
|
| 1582 |
+
socketApi.offGameEnded();
|
| 1583 |
+
socketApi.offGameReset();
|
| 1584 |
+
socketApi.offError();
|
| 1585 |
+
|
| 1586 |
+
// Remove hashchange listener
|
| 1587 |
+
window.removeEventListener("hashchange", handleHashChange);
|
| 1588 |
+
}
|
| 1589 |
+
|
| 1590 |
// Disconnect ResizeObserver
|
| 1591 |
if (resizeObserver) {
|
| 1592 |
resizeObserver.disconnect();
|
|
|
|
| 1690 |
|
| 1691 |
.view-header {
|
| 1692 |
display: flex;
|
| 1693 |
+
justify-content: space-between;
|
| 1694 |
align-items: center;
|
| 1695 |
padding: 1rem 2rem;
|
| 1696 |
background: linear-gradient(135deg, #505050 0%, #454545 100%);
|
|
|
|
| 1834 |
font-size: 1.1rem;
|
| 1835 |
}
|
| 1836 |
|
| 1837 |
+
.btn-copy-room {
|
| 1838 |
+
padding: 0.25rem 0.5rem;
|
| 1839 |
+
background-color: transparent;
|
| 1840 |
+
border: 1px solid #606060;
|
| 1841 |
+
border-radius: 4px;
|
| 1842 |
+
cursor: pointer;
|
| 1843 |
+
font-size: 0.9rem;
|
| 1844 |
+
transition: all 0.2s ease;
|
| 1845 |
+
|
| 1846 |
+
&:hover {
|
| 1847 |
+
background-color: #505050;
|
| 1848 |
+
border-color: #808080;
|
| 1849 |
+
}
|
| 1850 |
+
}
|
| 1851 |
+
|
| 1852 |
.players-info {
|
| 1853 |
display: flex;
|
| 1854 |
align-items: center;
|
|
|
|
| 1858 |
border-radius: 8px;
|
| 1859 |
}
|
| 1860 |
|
| 1861 |
+
.player-display {
|
| 1862 |
+
transition: all 0.3s ease;
|
| 1863 |
+
|
| 1864 |
+
&.on-turn {
|
| 1865 |
+
filter: brightness(1.3);
|
| 1866 |
+
}
|
| 1867 |
+
}
|
| 1868 |
+
|
| 1869 |
.player-name {
|
| 1870 |
color: #e0e0e0;
|
| 1871 |
font-weight: 600;
|
| 1872 |
}
|
| 1873 |
|
| 1874 |
+
.waiting-text {
|
| 1875 |
+
color: #9ca3af;
|
| 1876 |
+
font-style: italic;
|
| 1877 |
+
}
|
| 1878 |
+
|
| 1879 |
.vs-divider {
|
| 1880 |
color: #606060;
|
| 1881 |
font-weight: 500;
|
|
|
|
| 1962 |
}
|
| 1963 |
}
|
| 1964 |
|
| 1965 |
+
.header-controls {
|
| 1966 |
+
display: flex;
|
| 1967 |
+
align-items: center;
|
| 1968 |
+
gap: 0.75rem;
|
| 1969 |
+
margin-left: auto;
|
| 1970 |
+
|
| 1971 |
+
.board-shape-select,
|
| 1972 |
+
.color-preference-select {
|
| 1973 |
+
background: rgba(0, 0, 0, 0.3);
|
| 1974 |
+
border: 1px solid rgba(255, 255, 255, 0.3);
|
| 1975 |
+
border-radius: 4px;
|
| 1976 |
+
color: #fff;
|
| 1977 |
+
padding: 0.4rem 0.6rem;
|
| 1978 |
+
font-size: 0.9rem;
|
| 1979 |
+
cursor: pointer;
|
| 1980 |
+
transition: background 0.2s;
|
| 1981 |
+
|
| 1982 |
+
&:hover {
|
| 1983 |
+
background: rgba(0, 0, 0, 0.4);
|
| 1984 |
+
}
|
| 1985 |
+
|
| 1986 |
+
&:disabled {
|
| 1987 |
+
opacity: 0.5;
|
| 1988 |
+
cursor: not-allowed;
|
| 1989 |
+
}
|
| 1990 |
+
|
| 1991 |
+
// Ensure dropdown options are readable
|
| 1992 |
+
option {
|
| 1993 |
+
background: #3a3a3a;
|
| 1994 |
+
color: #e0e0e0;
|
| 1995 |
+
}
|
| 1996 |
+
}
|
| 1997 |
+
|
| 1998 |
+
.color-preference-select {
|
| 1999 |
+
min-width: 130px;
|
| 2000 |
+
}
|
| 2001 |
+
|
| 2002 |
+
.board-shape-select {
|
| 2003 |
+
min-width: 90px;
|
| 2004 |
+
}
|
| 2005 |
+
|
| 2006 |
+
.btn-reset {
|
| 2007 |
+
background: #e94560;
|
| 2008 |
+
color: white;
|
| 2009 |
+
border: none;
|
| 2010 |
+
border-radius: 4px;
|
| 2011 |
+
padding: 0.4rem 1rem;
|
| 2012 |
+
font-size: 0.9rem;
|
| 2013 |
+
font-weight: 600;
|
| 2014 |
+
cursor: pointer;
|
| 2015 |
+
transition: background 0.2s;
|
| 2016 |
+
|
| 2017 |
+
&:hover {
|
| 2018 |
+
background: #d63850;
|
| 2019 |
+
}
|
| 2020 |
+
|
| 2021 |
+
&:active {
|
| 2022 |
+
background: #c02040;
|
| 2023 |
+
}
|
| 2024 |
+
}
|
| 2025 |
+
}
|
| 2026 |
+
|
| 2027 |
.view-body {
|
| 2028 |
display: flex;
|
| 2029 |
flex: 1;
|
|
|
|
| 2389 |
}
|
| 2390 |
}
|
| 2391 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2392 |
}
|
| 2393 |
}
|
| 2394 |
}
|
trigo-web/app/vite.config.ts
CHANGED
|
@@ -4,10 +4,12 @@ import { fileURLToPath, URL } from "node:url";
|
|
| 4 |
|
| 5 |
// https://vitejs.dev/config/
|
| 6 |
export default defineConfig(({ mode }) => {
|
| 7 |
-
// Load env file
|
| 8 |
-
const env = loadEnv(mode,
|
| 9 |
|
| 10 |
return {
|
|
|
|
|
|
|
| 11 |
plugins: [
|
| 12 |
vue(),
|
| 13 |
// Plugin to set correct MIME types
|
|
|
|
| 4 |
|
| 5 |
// https://vitejs.dev/config/
|
| 6 |
export default defineConfig(({ mode }) => {
|
| 7 |
+
// Load env file from repository root (parent directory)
|
| 8 |
+
const env = loadEnv(mode, fileURLToPath(new URL("..", import.meta.url)), "");
|
| 9 |
|
| 10 |
return {
|
| 11 |
+
// Load .env files from repository root
|
| 12 |
+
envDir: "..",
|
| 13 |
plugins: [
|
| 14 |
vue(),
|
| 15 |
// Plugin to set correct MIME types
|
trigo-web/backend/.env
CHANGED
|
@@ -1,3 +1,9 @@
|
|
| 1 |
-
|
| 2 |
-
|
| 3 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# ============================================================================
|
| 2 |
+
# Backend Server Configuration
|
| 3 |
+
# ============================================================================
|
| 4 |
+
# This file is DEPRECATED - all configuration is now in the root .env file
|
| 5 |
+
# Backend should load environment variables from ../. env (repository root)
|
| 6 |
+
#
|
| 7 |
+
# To configure the backend, edit: ../. env
|
| 8 |
+
# ============================================================================
|
| 9 |
+
|
trigo-web/backend/.env.local
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
PORT=8157
|
trigo-web/backend/package-lock.json
CHANGED
|
@@ -22,6 +22,7 @@
|
|
| 22 |
"@types/node": "^20.5.0",
|
| 23 |
"nodemon": "^3.0.1",
|
| 24 |
"ts-node": "^10.9.1",
|
|
|
|
| 25 |
"typescript": "^5.2.2"
|
| 26 |
}
|
| 27 |
},
|
|
@@ -37,6 +38,422 @@
|
|
| 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",
|
|
@@ -613,6 +1030,47 @@
|
|
| 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",
|
|
@@ -773,6 +1231,18 @@
|
|
| 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",
|
|
@@ -1188,6 +1658,15 @@
|
|
| 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",
|
|
@@ -1552,6 +2031,25 @@
|
|
| 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",
|
|
|
|
| 22 |
"@types/node": "^20.5.0",
|
| 23 |
"nodemon": "^3.0.1",
|
| 24 |
"ts-node": "^10.9.1",
|
| 25 |
+
"tsx": "^4.20.6",
|
| 26 |
"typescript": "^5.2.2"
|
| 27 |
}
|
| 28 |
},
|
|
|
|
| 38 |
"node": ">=12"
|
| 39 |
}
|
| 40 |
},
|
| 41 |
+
"node_modules/@esbuild/aix-ppc64": {
|
| 42 |
+
"version": "0.25.12",
|
| 43 |
+
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz",
|
| 44 |
+
"integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==",
|
| 45 |
+
"cpu": [
|
| 46 |
+
"ppc64"
|
| 47 |
+
],
|
| 48 |
+
"dev": true,
|
| 49 |
+
"optional": true,
|
| 50 |
+
"os": [
|
| 51 |
+
"aix"
|
| 52 |
+
],
|
| 53 |
+
"engines": {
|
| 54 |
+
"node": ">=18"
|
| 55 |
+
}
|
| 56 |
+
},
|
| 57 |
+
"node_modules/@esbuild/android-arm": {
|
| 58 |
+
"version": "0.25.12",
|
| 59 |
+
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz",
|
| 60 |
+
"integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==",
|
| 61 |
+
"cpu": [
|
| 62 |
+
"arm"
|
| 63 |
+
],
|
| 64 |
+
"dev": true,
|
| 65 |
+
"optional": true,
|
| 66 |
+
"os": [
|
| 67 |
+
"android"
|
| 68 |
+
],
|
| 69 |
+
"engines": {
|
| 70 |
+
"node": ">=18"
|
| 71 |
+
}
|
| 72 |
+
},
|
| 73 |
+
"node_modules/@esbuild/android-arm64": {
|
| 74 |
+
"version": "0.25.12",
|
| 75 |
+
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz",
|
| 76 |
+
"integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==",
|
| 77 |
+
"cpu": [
|
| 78 |
+
"arm64"
|
| 79 |
+
],
|
| 80 |
+
"dev": true,
|
| 81 |
+
"optional": true,
|
| 82 |
+
"os": [
|
| 83 |
+
"android"
|
| 84 |
+
],
|
| 85 |
+
"engines": {
|
| 86 |
+
"node": ">=18"
|
| 87 |
+
}
|
| 88 |
+
},
|
| 89 |
+
"node_modules/@esbuild/android-x64": {
|
| 90 |
+
"version": "0.25.12",
|
| 91 |
+
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz",
|
| 92 |
+
"integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==",
|
| 93 |
+
"cpu": [
|
| 94 |
+
"x64"
|
| 95 |
+
],
|
| 96 |
+
"dev": true,
|
| 97 |
+
"optional": true,
|
| 98 |
+
"os": [
|
| 99 |
+
"android"
|
| 100 |
+
],
|
| 101 |
+
"engines": {
|
| 102 |
+
"node": ">=18"
|
| 103 |
+
}
|
| 104 |
+
},
|
| 105 |
+
"node_modules/@esbuild/darwin-arm64": {
|
| 106 |
+
"version": "0.25.12",
|
| 107 |
+
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz",
|
| 108 |
+
"integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==",
|
| 109 |
+
"cpu": [
|
| 110 |
+
"arm64"
|
| 111 |
+
],
|
| 112 |
+
"dev": true,
|
| 113 |
+
"optional": true,
|
| 114 |
+
"os": [
|
| 115 |
+
"darwin"
|
| 116 |
+
],
|
| 117 |
+
"engines": {
|
| 118 |
+
"node": ">=18"
|
| 119 |
+
}
|
| 120 |
+
},
|
| 121 |
+
"node_modules/@esbuild/darwin-x64": {
|
| 122 |
+
"version": "0.25.12",
|
| 123 |
+
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz",
|
| 124 |
+
"integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==",
|
| 125 |
+
"cpu": [
|
| 126 |
+
"x64"
|
| 127 |
+
],
|
| 128 |
+
"dev": true,
|
| 129 |
+
"optional": true,
|
| 130 |
+
"os": [
|
| 131 |
+
"darwin"
|
| 132 |
+
],
|
| 133 |
+
"engines": {
|
| 134 |
+
"node": ">=18"
|
| 135 |
+
}
|
| 136 |
+
},
|
| 137 |
+
"node_modules/@esbuild/freebsd-arm64": {
|
| 138 |
+
"version": "0.25.12",
|
| 139 |
+
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz",
|
| 140 |
+
"integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==",
|
| 141 |
+
"cpu": [
|
| 142 |
+
"arm64"
|
| 143 |
+
],
|
| 144 |
+
"dev": true,
|
| 145 |
+
"optional": true,
|
| 146 |
+
"os": [
|
| 147 |
+
"freebsd"
|
| 148 |
+
],
|
| 149 |
+
"engines": {
|
| 150 |
+
"node": ">=18"
|
| 151 |
+
}
|
| 152 |
+
},
|
| 153 |
+
"node_modules/@esbuild/freebsd-x64": {
|
| 154 |
+
"version": "0.25.12",
|
| 155 |
+
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz",
|
| 156 |
+
"integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==",
|
| 157 |
+
"cpu": [
|
| 158 |
+
"x64"
|
| 159 |
+
],
|
| 160 |
+
"dev": true,
|
| 161 |
+
"optional": true,
|
| 162 |
+
"os": [
|
| 163 |
+
"freebsd"
|
| 164 |
+
],
|
| 165 |
+
"engines": {
|
| 166 |
+
"node": ">=18"
|
| 167 |
+
}
|
| 168 |
+
},
|
| 169 |
+
"node_modules/@esbuild/linux-arm": {
|
| 170 |
+
"version": "0.25.12",
|
| 171 |
+
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz",
|
| 172 |
+
"integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==",
|
| 173 |
+
"cpu": [
|
| 174 |
+
"arm"
|
| 175 |
+
],
|
| 176 |
+
"dev": true,
|
| 177 |
+
"optional": true,
|
| 178 |
+
"os": [
|
| 179 |
+
"linux"
|
| 180 |
+
],
|
| 181 |
+
"engines": {
|
| 182 |
+
"node": ">=18"
|
| 183 |
+
}
|
| 184 |
+
},
|
| 185 |
+
"node_modules/@esbuild/linux-arm64": {
|
| 186 |
+
"version": "0.25.12",
|
| 187 |
+
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz",
|
| 188 |
+
"integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==",
|
| 189 |
+
"cpu": [
|
| 190 |
+
"arm64"
|
| 191 |
+
],
|
| 192 |
+
"dev": true,
|
| 193 |
+
"optional": true,
|
| 194 |
+
"os": [
|
| 195 |
+
"linux"
|
| 196 |
+
],
|
| 197 |
+
"engines": {
|
| 198 |
+
"node": ">=18"
|
| 199 |
+
}
|
| 200 |
+
},
|
| 201 |
+
"node_modules/@esbuild/linux-ia32": {
|
| 202 |
+
"version": "0.25.12",
|
| 203 |
+
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz",
|
| 204 |
+
"integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==",
|
| 205 |
+
"cpu": [
|
| 206 |
+
"ia32"
|
| 207 |
+
],
|
| 208 |
+
"dev": true,
|
| 209 |
+
"optional": true,
|
| 210 |
+
"os": [
|
| 211 |
+
"linux"
|
| 212 |
+
],
|
| 213 |
+
"engines": {
|
| 214 |
+
"node": ">=18"
|
| 215 |
+
}
|
| 216 |
+
},
|
| 217 |
+
"node_modules/@esbuild/linux-loong64": {
|
| 218 |
+
"version": "0.25.12",
|
| 219 |
+
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz",
|
| 220 |
+
"integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==",
|
| 221 |
+
"cpu": [
|
| 222 |
+
"loong64"
|
| 223 |
+
],
|
| 224 |
+
"dev": true,
|
| 225 |
+
"optional": true,
|
| 226 |
+
"os": [
|
| 227 |
+
"linux"
|
| 228 |
+
],
|
| 229 |
+
"engines": {
|
| 230 |
+
"node": ">=18"
|
| 231 |
+
}
|
| 232 |
+
},
|
| 233 |
+
"node_modules/@esbuild/linux-mips64el": {
|
| 234 |
+
"version": "0.25.12",
|
| 235 |
+
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz",
|
| 236 |
+
"integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==",
|
| 237 |
+
"cpu": [
|
| 238 |
+
"mips64el"
|
| 239 |
+
],
|
| 240 |
+
"dev": true,
|
| 241 |
+
"optional": true,
|
| 242 |
+
"os": [
|
| 243 |
+
"linux"
|
| 244 |
+
],
|
| 245 |
+
"engines": {
|
| 246 |
+
"node": ">=18"
|
| 247 |
+
}
|
| 248 |
+
},
|
| 249 |
+
"node_modules/@esbuild/linux-ppc64": {
|
| 250 |
+
"version": "0.25.12",
|
| 251 |
+
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz",
|
| 252 |
+
"integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==",
|
| 253 |
+
"cpu": [
|
| 254 |
+
"ppc64"
|
| 255 |
+
],
|
| 256 |
+
"dev": true,
|
| 257 |
+
"optional": true,
|
| 258 |
+
"os": [
|
| 259 |
+
"linux"
|
| 260 |
+
],
|
| 261 |
+
"engines": {
|
| 262 |
+
"node": ">=18"
|
| 263 |
+
}
|
| 264 |
+
},
|
| 265 |
+
"node_modules/@esbuild/linux-riscv64": {
|
| 266 |
+
"version": "0.25.12",
|
| 267 |
+
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz",
|
| 268 |
+
"integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==",
|
| 269 |
+
"cpu": [
|
| 270 |
+
"riscv64"
|
| 271 |
+
],
|
| 272 |
+
"dev": true,
|
| 273 |
+
"optional": true,
|
| 274 |
+
"os": [
|
| 275 |
+
"linux"
|
| 276 |
+
],
|
| 277 |
+
"engines": {
|
| 278 |
+
"node": ">=18"
|
| 279 |
+
}
|
| 280 |
+
},
|
| 281 |
+
"node_modules/@esbuild/linux-s390x": {
|
| 282 |
+
"version": "0.25.12",
|
| 283 |
+
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz",
|
| 284 |
+
"integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==",
|
| 285 |
+
"cpu": [
|
| 286 |
+
"s390x"
|
| 287 |
+
],
|
| 288 |
+
"dev": true,
|
| 289 |
+
"optional": true,
|
| 290 |
+
"os": [
|
| 291 |
+
"linux"
|
| 292 |
+
],
|
| 293 |
+
"engines": {
|
| 294 |
+
"node": ">=18"
|
| 295 |
+
}
|
| 296 |
+
},
|
| 297 |
+
"node_modules/@esbuild/linux-x64": {
|
| 298 |
+
"version": "0.25.12",
|
| 299 |
+
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz",
|
| 300 |
+
"integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==",
|
| 301 |
+
"cpu": [
|
| 302 |
+
"x64"
|
| 303 |
+
],
|
| 304 |
+
"dev": true,
|
| 305 |
+
"optional": true,
|
| 306 |
+
"os": [
|
| 307 |
+
"linux"
|
| 308 |
+
],
|
| 309 |
+
"engines": {
|
| 310 |
+
"node": ">=18"
|
| 311 |
+
}
|
| 312 |
+
},
|
| 313 |
+
"node_modules/@esbuild/netbsd-arm64": {
|
| 314 |
+
"version": "0.25.12",
|
| 315 |
+
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz",
|
| 316 |
+
"integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==",
|
| 317 |
+
"cpu": [
|
| 318 |
+
"arm64"
|
| 319 |
+
],
|
| 320 |
+
"dev": true,
|
| 321 |
+
"optional": true,
|
| 322 |
+
"os": [
|
| 323 |
+
"netbsd"
|
| 324 |
+
],
|
| 325 |
+
"engines": {
|
| 326 |
+
"node": ">=18"
|
| 327 |
+
}
|
| 328 |
+
},
|
| 329 |
+
"node_modules/@esbuild/netbsd-x64": {
|
| 330 |
+
"version": "0.25.12",
|
| 331 |
+
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz",
|
| 332 |
+
"integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==",
|
| 333 |
+
"cpu": [
|
| 334 |
+
"x64"
|
| 335 |
+
],
|
| 336 |
+
"dev": true,
|
| 337 |
+
"optional": true,
|
| 338 |
+
"os": [
|
| 339 |
+
"netbsd"
|
| 340 |
+
],
|
| 341 |
+
"engines": {
|
| 342 |
+
"node": ">=18"
|
| 343 |
+
}
|
| 344 |
+
},
|
| 345 |
+
"node_modules/@esbuild/openbsd-arm64": {
|
| 346 |
+
"version": "0.25.12",
|
| 347 |
+
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz",
|
| 348 |
+
"integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==",
|
| 349 |
+
"cpu": [
|
| 350 |
+
"arm64"
|
| 351 |
+
],
|
| 352 |
+
"dev": true,
|
| 353 |
+
"optional": true,
|
| 354 |
+
"os": [
|
| 355 |
+
"openbsd"
|
| 356 |
+
],
|
| 357 |
+
"engines": {
|
| 358 |
+
"node": ">=18"
|
| 359 |
+
}
|
| 360 |
+
},
|
| 361 |
+
"node_modules/@esbuild/openbsd-x64": {
|
| 362 |
+
"version": "0.25.12",
|
| 363 |
+
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz",
|
| 364 |
+
"integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==",
|
| 365 |
+
"cpu": [
|
| 366 |
+
"x64"
|
| 367 |
+
],
|
| 368 |
+
"dev": true,
|
| 369 |
+
"optional": true,
|
| 370 |
+
"os": [
|
| 371 |
+
"openbsd"
|
| 372 |
+
],
|
| 373 |
+
"engines": {
|
| 374 |
+
"node": ">=18"
|
| 375 |
+
}
|
| 376 |
+
},
|
| 377 |
+
"node_modules/@esbuild/openharmony-arm64": {
|
| 378 |
+
"version": "0.25.12",
|
| 379 |
+
"resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz",
|
| 380 |
+
"integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==",
|
| 381 |
+
"cpu": [
|
| 382 |
+
"arm64"
|
| 383 |
+
],
|
| 384 |
+
"dev": true,
|
| 385 |
+
"optional": true,
|
| 386 |
+
"os": [
|
| 387 |
+
"openharmony"
|
| 388 |
+
],
|
| 389 |
+
"engines": {
|
| 390 |
+
"node": ">=18"
|
| 391 |
+
}
|
| 392 |
+
},
|
| 393 |
+
"node_modules/@esbuild/sunos-x64": {
|
| 394 |
+
"version": "0.25.12",
|
| 395 |
+
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz",
|
| 396 |
+
"integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==",
|
| 397 |
+
"cpu": [
|
| 398 |
+
"x64"
|
| 399 |
+
],
|
| 400 |
+
"dev": true,
|
| 401 |
+
"optional": true,
|
| 402 |
+
"os": [
|
| 403 |
+
"sunos"
|
| 404 |
+
],
|
| 405 |
+
"engines": {
|
| 406 |
+
"node": ">=18"
|
| 407 |
+
}
|
| 408 |
+
},
|
| 409 |
+
"node_modules/@esbuild/win32-arm64": {
|
| 410 |
+
"version": "0.25.12",
|
| 411 |
+
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz",
|
| 412 |
+
"integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==",
|
| 413 |
+
"cpu": [
|
| 414 |
+
"arm64"
|
| 415 |
+
],
|
| 416 |
+
"dev": true,
|
| 417 |
+
"optional": true,
|
| 418 |
+
"os": [
|
| 419 |
+
"win32"
|
| 420 |
+
],
|
| 421 |
+
"engines": {
|
| 422 |
+
"node": ">=18"
|
| 423 |
+
}
|
| 424 |
+
},
|
| 425 |
+
"node_modules/@esbuild/win32-ia32": {
|
| 426 |
+
"version": "0.25.12",
|
| 427 |
+
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz",
|
| 428 |
+
"integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==",
|
| 429 |
+
"cpu": [
|
| 430 |
+
"ia32"
|
| 431 |
+
],
|
| 432 |
+
"dev": true,
|
| 433 |
+
"optional": true,
|
| 434 |
+
"os": [
|
| 435 |
+
"win32"
|
| 436 |
+
],
|
| 437 |
+
"engines": {
|
| 438 |
+
"node": ">=18"
|
| 439 |
+
}
|
| 440 |
+
},
|
| 441 |
+
"node_modules/@esbuild/win32-x64": {
|
| 442 |
+
"version": "0.25.12",
|
| 443 |
+
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz",
|
| 444 |
+
"integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==",
|
| 445 |
+
"cpu": [
|
| 446 |
+
"x64"
|
| 447 |
+
],
|
| 448 |
+
"dev": true,
|
| 449 |
+
"optional": true,
|
| 450 |
+
"os": [
|
| 451 |
+
"win32"
|
| 452 |
+
],
|
| 453 |
+
"engines": {
|
| 454 |
+
"node": ">=18"
|
| 455 |
+
}
|
| 456 |
+
},
|
| 457 |
"node_modules/@jridgewell/resolve-uri": {
|
| 458 |
"version": "3.1.2",
|
| 459 |
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
|
|
|
|
| 1030 |
"node": ">= 0.4"
|
| 1031 |
}
|
| 1032 |
},
|
| 1033 |
+
"node_modules/esbuild": {
|
| 1034 |
+
"version": "0.25.12",
|
| 1035 |
+
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz",
|
| 1036 |
+
"integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==",
|
| 1037 |
+
"dev": true,
|
| 1038 |
+
"hasInstallScript": true,
|
| 1039 |
+
"bin": {
|
| 1040 |
+
"esbuild": "bin/esbuild"
|
| 1041 |
+
},
|
| 1042 |
+
"engines": {
|
| 1043 |
+
"node": ">=18"
|
| 1044 |
+
},
|
| 1045 |
+
"optionalDependencies": {
|
| 1046 |
+
"@esbuild/aix-ppc64": "0.25.12",
|
| 1047 |
+
"@esbuild/android-arm": "0.25.12",
|
| 1048 |
+
"@esbuild/android-arm64": "0.25.12",
|
| 1049 |
+
"@esbuild/android-x64": "0.25.12",
|
| 1050 |
+
"@esbuild/darwin-arm64": "0.25.12",
|
| 1051 |
+
"@esbuild/darwin-x64": "0.25.12",
|
| 1052 |
+
"@esbuild/freebsd-arm64": "0.25.12",
|
| 1053 |
+
"@esbuild/freebsd-x64": "0.25.12",
|
| 1054 |
+
"@esbuild/linux-arm": "0.25.12",
|
| 1055 |
+
"@esbuild/linux-arm64": "0.25.12",
|
| 1056 |
+
"@esbuild/linux-ia32": "0.25.12",
|
| 1057 |
+
"@esbuild/linux-loong64": "0.25.12",
|
| 1058 |
+
"@esbuild/linux-mips64el": "0.25.12",
|
| 1059 |
+
"@esbuild/linux-ppc64": "0.25.12",
|
| 1060 |
+
"@esbuild/linux-riscv64": "0.25.12",
|
| 1061 |
+
"@esbuild/linux-s390x": "0.25.12",
|
| 1062 |
+
"@esbuild/linux-x64": "0.25.12",
|
| 1063 |
+
"@esbuild/netbsd-arm64": "0.25.12",
|
| 1064 |
+
"@esbuild/netbsd-x64": "0.25.12",
|
| 1065 |
+
"@esbuild/openbsd-arm64": "0.25.12",
|
| 1066 |
+
"@esbuild/openbsd-x64": "0.25.12",
|
| 1067 |
+
"@esbuild/openharmony-arm64": "0.25.12",
|
| 1068 |
+
"@esbuild/sunos-x64": "0.25.12",
|
| 1069 |
+
"@esbuild/win32-arm64": "0.25.12",
|
| 1070 |
+
"@esbuild/win32-ia32": "0.25.12",
|
| 1071 |
+
"@esbuild/win32-x64": "0.25.12"
|
| 1072 |
+
}
|
| 1073 |
+
},
|
| 1074 |
"node_modules/escape-html": {
|
| 1075 |
"version": "1.0.3",
|
| 1076 |
"resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
|
|
|
|
| 1231 |
"node": ">= 0.4"
|
| 1232 |
}
|
| 1233 |
},
|
| 1234 |
+
"node_modules/get-tsconfig": {
|
| 1235 |
+
"version": "4.13.0",
|
| 1236 |
+
"resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.0.tgz",
|
| 1237 |
+
"integrity": "sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ==",
|
| 1238 |
+
"dev": true,
|
| 1239 |
+
"dependencies": {
|
| 1240 |
+
"resolve-pkg-maps": "^1.0.0"
|
| 1241 |
+
},
|
| 1242 |
+
"funding": {
|
| 1243 |
+
"url": "https://github.com/privatenumber/get-tsconfig?sponsor=1"
|
| 1244 |
+
}
|
| 1245 |
+
},
|
| 1246 |
"node_modules/glob-parent": {
|
| 1247 |
"version": "5.1.2",
|
| 1248 |
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
|
|
|
|
| 1658 |
"node": ">=8.10.0"
|
| 1659 |
}
|
| 1660 |
},
|
| 1661 |
+
"node_modules/resolve-pkg-maps": {
|
| 1662 |
+
"version": "1.0.0",
|
| 1663 |
+
"resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz",
|
| 1664 |
+
"integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==",
|
| 1665 |
+
"dev": true,
|
| 1666 |
+
"funding": {
|
| 1667 |
+
"url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1"
|
| 1668 |
+
}
|
| 1669 |
+
},
|
| 1670 |
"node_modules/safe-buffer": {
|
| 1671 |
"version": "5.2.1",
|
| 1672 |
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
|
|
|
|
| 2031 |
}
|
| 2032 |
}
|
| 2033 |
},
|
| 2034 |
+
"node_modules/tsx": {
|
| 2035 |
+
"version": "4.20.6",
|
| 2036 |
+
"resolved": "https://registry.npmjs.org/tsx/-/tsx-4.20.6.tgz",
|
| 2037 |
+
"integrity": "sha512-ytQKuwgmrrkDTFP4LjR0ToE2nqgy886GpvRSpU0JAnrdBYppuY5rLkRUYPU1yCryb24SsKBTL/hlDQAEFVwtZg==",
|
| 2038 |
+
"dev": true,
|
| 2039 |
+
"dependencies": {
|
| 2040 |
+
"esbuild": "~0.25.0",
|
| 2041 |
+
"get-tsconfig": "^4.7.5"
|
| 2042 |
+
},
|
| 2043 |
+
"bin": {
|
| 2044 |
+
"tsx": "dist/cli.mjs"
|
| 2045 |
+
},
|
| 2046 |
+
"engines": {
|
| 2047 |
+
"node": ">=18.0.0"
|
| 2048 |
+
},
|
| 2049 |
+
"optionalDependencies": {
|
| 2050 |
+
"fsevents": "~2.3.3"
|
| 2051 |
+
}
|
| 2052 |
+
},
|
| 2053 |
"node_modules/type-is": {
|
| 2054 |
"version": "1.6.18",
|
| 2055 |
"resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
|
trigo-web/backend/package.json
CHANGED
|
@@ -1,10 +1,11 @@
|
|
| 1 |
{
|
| 2 |
"name": "trigo-backend",
|
| 3 |
"version": "1.0.0",
|
|
|
|
| 4 |
"description": "Backend server for Trigo game",
|
| 5 |
"main": "dist/backend/src/server.js",
|
| 6 |
"scripts": {
|
| 7 |
-
"dev": "nodemon --watch src --exec
|
| 8 |
"build": "tsc",
|
| 9 |
"start": "node dist/backend/src/server.js",
|
| 10 |
"test": "echo \"Error: no test specified\" && exit 1"
|
|
@@ -31,6 +32,7 @@
|
|
| 31 |
"@types/node": "^20.5.0",
|
| 32 |
"nodemon": "^3.0.1",
|
| 33 |
"ts-node": "^10.9.1",
|
|
|
|
| 34 |
"typescript": "^5.2.2"
|
| 35 |
}
|
| 36 |
}
|
|
|
|
| 1 |
{
|
| 2 |
"name": "trigo-backend",
|
| 3 |
"version": "1.0.0",
|
| 4 |
+
"type": "module",
|
| 5 |
"description": "Backend server for Trigo game",
|
| 6 |
"main": "dist/backend/src/server.js",
|
| 7 |
"scripts": {
|
| 8 |
+
"dev": "nodemon --watch src --exec tsx src/server.ts",
|
| 9 |
"build": "tsc",
|
| 10 |
"start": "node dist/backend/src/server.js",
|
| 11 |
"test": "echo \"Error: no test specified\" && exit 1"
|
|
|
|
| 32 |
"@types/node": "^20.5.0",
|
| 33 |
"nodemon": "^3.0.1",
|
| 34 |
"ts-node": "^10.9.1",
|
| 35 |
+
"tsx": "^4.20.6",
|
| 36 |
"typescript": "^5.2.2"
|
| 37 |
}
|
| 38 |
}
|
trigo-web/backend/src/server.ts
CHANGED
|
@@ -1,11 +1,42 @@
|
|
| 1 |
-
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7 |
|
| 8 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 9 |
|
| 10 |
const app = express();
|
| 11 |
const httpServer = createServer(app);
|
|
@@ -20,8 +51,17 @@ const io = new Server(httpServer, {
|
|
| 20 |
}
|
| 21 |
});
|
| 22 |
|
| 23 |
-
|
| 24 |
-
const
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 25 |
|
| 26 |
// Middleware
|
| 27 |
app.use(cors());
|
|
@@ -50,7 +90,10 @@ app.get("/health", (_req: any, res: any) => {
|
|
| 50 |
io.on("connection", (socket: any) => {
|
| 51 |
console.log(`New client connected: ${socket.id}`);
|
| 52 |
|
| 53 |
-
//
|
|
|
|
|
|
|
|
|
|
| 54 |
socket.on("echo", (data: any, callback: any) => {
|
| 55 |
const timestamp = new Date().toISOString();
|
| 56 |
const responseMessage = `Hello from server! Received: "${data.message}" at ${timestamp}`;
|
|
|
|
| 1 |
+
import express from "express";
|
| 2 |
+
import { createServer } from "http";
|
| 3 |
+
import { Server } from "socket.io";
|
| 4 |
+
import cors from "cors";
|
| 5 |
+
import dotenv from "dotenv";
|
| 6 |
+
import path from "path";
|
| 7 |
+
import fs from "fs";
|
| 8 |
+
import { fileURLToPath } from "url";
|
| 9 |
+
import { GameManager } from "./services/gameManager";
|
| 10 |
+
import { setupSocketHandlers } from "./sockets/gameSocket";
|
| 11 |
|
| 12 |
+
// Get __dirname equivalent in ES modules
|
| 13 |
+
const __filename = fileURLToPath(import.meta.url);
|
| 14 |
+
const __dirname = path.dirname(__filename);
|
| 15 |
+
|
| 16 |
+
// Load environment variables
|
| 17 |
+
// Priority: .env.local > .env (load .env first, then .env.local overwrites)
|
| 18 |
+
// When running via ts-node: __dirname = backend/src/, so go up 1 level
|
| 19 |
+
// When running compiled: __dirname = dist/backend/src/, so go up 3 levels
|
| 20 |
+
const isDev = __dirname.includes("/src") && !__dirname.includes("/dist");
|
| 21 |
+
const levelsUp = isDev ? "../" : "../../../";
|
| 22 |
+
const envPath = path.join(__dirname, levelsUp, ".env");
|
| 23 |
+
const envLocalPath = path.join(__dirname, levelsUp, ".env.local");
|
| 24 |
+
|
| 25 |
+
// Load .env first (base configuration)
|
| 26 |
+
if (fs.existsSync(envPath)) {
|
| 27 |
+
dotenv.config({ path: envPath });
|
| 28 |
+
console.log("[Config] Loaded .env");
|
| 29 |
+
} else {
|
| 30 |
+
console.log(`[Config] .env not found at: ${envPath}`);
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
// Load .env.local second (overrides .env for local development)
|
| 34 |
+
if (fs.existsSync(envLocalPath)) {
|
| 35 |
+
dotenv.config({ path: envLocalPath, override: true });
|
| 36 |
+
console.log("[Config] Loaded .env.local (overriding .env)");
|
| 37 |
+
} else {
|
| 38 |
+
console.log(`[Config] .env.local not found at: ${envLocalPath}`);
|
| 39 |
+
}
|
| 40 |
|
| 41 |
const app = express();
|
| 42 |
const httpServer = createServer(app);
|
|
|
|
| 51 |
}
|
| 52 |
});
|
| 53 |
|
| 54 |
+
// Create GameManager instance
|
| 55 |
+
const gameManager = new GameManager();
|
| 56 |
+
|
| 57 |
+
const PORT = parseInt(process.env.PORT || "3000", 10);
|
| 58 |
+
const HOST = process.env.HOST || "0.0.0.0";
|
| 59 |
+
|
| 60 |
+
console.log(`[Config] Server Configuration:`);
|
| 61 |
+
console.log(`[Config] PORT: ${PORT}`);
|
| 62 |
+
console.log(`[Config] HOST: ${HOST}`);
|
| 63 |
+
console.log(`[Config] NODE_ENV: ${process.env.NODE_ENV || "development"}`);
|
| 64 |
+
console.log(`[Config] CLIENT_URL: ${process.env.CLIENT_URL || "not set"}`);
|
| 65 |
|
| 66 |
// Middleware
|
| 67 |
app.use(cors());
|
|
|
|
| 90 |
io.on("connection", (socket: any) => {
|
| 91 |
console.log(`New client connected: ${socket.id}`);
|
| 92 |
|
| 93 |
+
// Setup game-related socket handlers
|
| 94 |
+
setupSocketHandlers(io, socket, gameManager);
|
| 95 |
+
|
| 96 |
+
// Echo test handler (for testing)
|
| 97 |
socket.on("echo", (data: any, callback: any) => {
|
| 98 |
const timestamp = new Date().toISOString();
|
| 99 |
const responseMessage = `Hello from server! Received: "${data.message}" at ${timestamp}`;
|
trigo-web/backend/src/services/gameManager.ts
CHANGED
|
@@ -16,6 +16,7 @@ export interface GameState {
|
|
| 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
|
|
@@ -32,17 +33,21 @@ export class GameManager {
|
|
| 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:
|
| 46 |
connected: true
|
| 47 |
}
|
| 48 |
},
|
|
@@ -72,7 +77,7 @@ export class GameManager {
|
|
| 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;
|
|
@@ -83,11 +88,22 @@ export class GameManager {
|
|
| 83 |
return null; // Room is full
|
| 84 |
}
|
| 85 |
|
| 86 |
-
//
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 87 |
room.players[playerId] = {
|
| 88 |
id: playerId,
|
| 89 |
nickname,
|
| 90 |
-
color:
|
| 91 |
connected: true
|
| 92 |
};
|
| 93 |
|
|
@@ -207,6 +223,98 @@ export class GameManager {
|
|
| 207 |
return room.game.undo();
|
| 208 |
}
|
| 209 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 210 |
/**
|
| 211 |
* Get game board state for a room
|
| 212 |
*/
|
|
|
|
| 16 |
|
| 17 |
export interface GameRoom {
|
| 18 |
id: string;
|
| 19 |
+
adminId: string; // Room creator who has admin permissions
|
| 20 |
players: { [playerId: string]: Player };
|
| 21 |
game: TrigoGame; // The actual game instance
|
| 22 |
gameState: GameState; // Game status metadata
|
|
|
|
| 33 |
console.log("GameManager initialized");
|
| 34 |
}
|
| 35 |
|
| 36 |
+
createRoom(playerId: string, nickname: string, boardShape?: BoardShape, preferredColor?: "black" | "white"): GameRoom | null {
|
| 37 |
const roomId = this.generateRoomId();
|
| 38 |
const shape = boardShape || this.defaultBoardShape;
|
| 39 |
|
| 40 |
+
// Use preferred color if specified, default to black
|
| 41 |
+
const playerColor = preferredColor || "black";
|
| 42 |
+
|
| 43 |
const room: GameRoom = {
|
| 44 |
id: roomId,
|
| 45 |
+
adminId: playerId, // Room creator is admin
|
| 46 |
players: {
|
| 47 |
[playerId]: {
|
| 48 |
id: playerId,
|
| 49 |
nickname,
|
| 50 |
+
color: playerColor,
|
| 51 |
connected: true
|
| 52 |
}
|
| 53 |
},
|
|
|
|
| 77 |
return room;
|
| 78 |
}
|
| 79 |
|
| 80 |
+
joinRoom(roomId: string, playerId: string, nickname: string, preferredColor?: "black" | "white"): GameRoom | null {
|
| 81 |
const room = this.rooms.get(roomId);
|
| 82 |
if (!room) {
|
| 83 |
return null;
|
|
|
|
| 88 |
return null; // Room is full
|
| 89 |
}
|
| 90 |
|
| 91 |
+
// Try to assign preferred color if specified
|
| 92 |
+
const firstPlayer = Object.values(room.players)[0];
|
| 93 |
+
let assignedColor: "black" | "white";
|
| 94 |
+
|
| 95 |
+
if (preferredColor && preferredColor !== firstPlayer.color) {
|
| 96 |
+
// Preferred color is available
|
| 97 |
+
assignedColor = preferredColor;
|
| 98 |
+
} else {
|
| 99 |
+
// Assign opposite of first player's color
|
| 100 |
+
assignedColor = firstPlayer.color === "black" ? "white" : "black";
|
| 101 |
+
}
|
| 102 |
+
|
| 103 |
room.players[playerId] = {
|
| 104 |
id: playerId,
|
| 105 |
nickname,
|
| 106 |
+
color: assignedColor,
|
| 107 |
connected: true
|
| 108 |
};
|
| 109 |
|
|
|
|
| 223 |
return room.game.undo();
|
| 224 |
}
|
| 225 |
|
| 226 |
+
|
| 227 |
+
/**
|
| 228 |
+
* Redo the last undone move (forward in history)
|
| 229 |
+
*/
|
| 230 |
+
redoMove(roomId: string, playerId: string): boolean {
|
| 231 |
+
const room = this.rooms.get(roomId);
|
| 232 |
+
if (!room) return false;
|
| 233 |
+
|
| 234 |
+
const player = room.players[playerId];
|
| 235 |
+
if (!player) return false;
|
| 236 |
+
|
| 237 |
+
if (room.gameState.gameStatus !== "playing") {
|
| 238 |
+
return false;
|
| 239 |
+
}
|
| 240 |
+
|
| 241 |
+
return room.game.redo();
|
| 242 |
+
}
|
| 243 |
+
|
| 244 |
+
|
| 245 |
+
/**
|
| 246 |
+
* Reset the game to initial state (new game in same room)
|
| 247 |
+
* Only admin can reset the game
|
| 248 |
+
*/
|
| 249 |
+
resetGame(
|
| 250 |
+
roomId: string,
|
| 251 |
+
adminId: string,
|
| 252 |
+
options?: {
|
| 253 |
+
boardShape?: BoardShape;
|
| 254 |
+
playerColors?: { [playerId: string]: "black" | "white" };
|
| 255 |
+
}
|
| 256 |
+
): { success: boolean; error?: string } {
|
| 257 |
+
const room = this.rooms.get(roomId);
|
| 258 |
+
if (!room) return { success: false, error: "Room not found" };
|
| 259 |
+
|
| 260 |
+
// Check admin permission
|
| 261 |
+
if (room.adminId !== adminId) {
|
| 262 |
+
return { success: false, error: "Only room admin can reset the game" };
|
| 263 |
+
}
|
| 264 |
+
|
| 265 |
+
const boardShape = options?.boardShape;
|
| 266 |
+
const playerColors = options?.playerColors;
|
| 267 |
+
|
| 268 |
+
// Apply player color assignments if provided
|
| 269 |
+
if (playerColors) {
|
| 270 |
+
const playerIds = Object.keys(room.players);
|
| 271 |
+
for (const playerId of playerIds) {
|
| 272 |
+
if (playerColors[playerId]) {
|
| 273 |
+
room.players[playerId].color = playerColors[playerId];
|
| 274 |
+
}
|
| 275 |
+
}
|
| 276 |
+
console.log(`Player colors assigned:`, playerColors);
|
| 277 |
+
}
|
| 278 |
+
|
| 279 |
+
// If board shape is provided and different from current, create a new game
|
| 280 |
+
if (boardShape) {
|
| 281 |
+
const currentShape = room.game.getShape();
|
| 282 |
+
if (
|
| 283 |
+
boardShape.x !== currentShape.x ||
|
| 284 |
+
boardShape.y !== currentShape.y ||
|
| 285 |
+
boardShape.z !== currentShape.z
|
| 286 |
+
) {
|
| 287 |
+
// Create new game with new board shape
|
| 288 |
+
room.game = new TrigoGame(boardShape, {
|
| 289 |
+
onStepAdvance: (_step, history) => {
|
| 290 |
+
console.log(`Step ${history.length}: Player made move`);
|
| 291 |
+
},
|
| 292 |
+
onCapture: (captured) => {
|
| 293 |
+
console.log(`Captured ${captured.length} stones`);
|
| 294 |
+
},
|
| 295 |
+
onWin: (winner) => {
|
| 296 |
+
console.log(`Game won by ${winner}`);
|
| 297 |
+
}
|
| 298 |
+
});
|
| 299 |
+
console.log(`Game ${roomId} reset with new board shape: ${boardShape.x}x${boardShape.y}x${boardShape.z}`);
|
| 300 |
+
} else {
|
| 301 |
+
// Same shape, just reset
|
| 302 |
+
room.game.reset();
|
| 303 |
+
console.log(`Game ${roomId} reset to initial state`);
|
| 304 |
+
}
|
| 305 |
+
} else {
|
| 306 |
+
// No shape provided, just reset
|
| 307 |
+
room.game.reset();
|
| 308 |
+
console.log(`Game ${roomId} reset to initial state`);
|
| 309 |
+
}
|
| 310 |
+
|
| 311 |
+
room.gameState.gameStatus = "playing";
|
| 312 |
+
room.gameState.winner = null;
|
| 313 |
+
room.startedAt = new Date();
|
| 314 |
+
|
| 315 |
+
return { success: true };
|
| 316 |
+
}
|
| 317 |
+
|
| 318 |
/**
|
| 319 |
* Get game board state for a room
|
| 320 |
*/
|
trigo-web/backend/src/sockets/gameSocket.ts
CHANGED
|
@@ -5,52 +5,141 @@ export function setupSocketHandlers(io: Server, socket: Socket, gameManager: Gam
|
|
| 5 |
console.log(`Setting up socket handlers for ${socket.id}`);
|
| 6 |
|
| 7 |
// Join room
|
| 8 |
-
socket.on(
|
| 9 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 10 |
|
| 11 |
-
|
| 12 |
-
const room = roomId
|
| 13 |
-
? gameManager.joinRoom(roomId, socket.id, nickname)
|
| 14 |
-
: gameManager.createRoom(socket.id, nickname);
|
| 15 |
|
| 16 |
-
|
| 17 |
-
|
|
|
|
| 18 |
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
const stats = gameManager.getGameStats(room.id);
|
| 23 |
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 40 |
}
|
| 41 |
-
});
|
| 42 |
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
playerId: socket.id,
|
| 46 |
-
nickname: nickname
|
| 47 |
-
});
|
| 48 |
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 52 |
}
|
| 53 |
-
|
| 54 |
|
| 55 |
// Leave room
|
| 56 |
socket.on("leaveRoom", () => {
|
|
@@ -71,23 +160,23 @@ export function setupSocketHandlers(io: Server, socket: Socket, gameManager: Gam
|
|
| 71 |
const room = gameManager.getPlayerRoom(socket.id);
|
| 72 |
if (room && gameManager.makeMove(room.id, socket.id, data)) {
|
| 73 |
// Get updated game data
|
| 74 |
-
const board = gameManager.getGameBoard(room.id);
|
| 75 |
const currentPlayer = gameManager.getCurrentPlayer(room.id);
|
| 76 |
const stats = gameManager.getGameStats(room.id);
|
| 77 |
const lastStep = room.game.getLastStep();
|
|
|
|
| 78 |
|
| 79 |
// Broadcast game update to all players in the room
|
| 80 |
io.to(room.id).emit("gameUpdate", {
|
| 81 |
-
board,
|
| 82 |
currentPlayer,
|
|
|
|
| 83 |
lastMove: data,
|
| 84 |
capturedStones: {
|
| 85 |
black: stats?.capturedByBlack || 0,
|
| 86 |
white: stats?.capturedByWhite || 0
|
| 87 |
},
|
| 88 |
capturedPositions: lastStep?.capturedPositions,
|
| 89 |
-
|
| 90 |
-
|
| 91 |
});
|
| 92 |
} else {
|
| 93 |
socket.emit("error", { message: "Invalid move" });
|
|
@@ -99,12 +188,13 @@ export function setupSocketHandlers(io: Server, socket: Socket, gameManager: Gam
|
|
| 99 |
const room = gameManager.getPlayerRoom(socket.id);
|
| 100 |
if (room && gameManager.passTurn(room.id, socket.id)) {
|
| 101 |
const currentPlayer = gameManager.getCurrentPlayer(room.id);
|
|
|
|
| 102 |
|
| 103 |
io.to(room.id).emit("gameUpdate", {
|
| 104 |
currentPlayer,
|
| 105 |
action: "pass",
|
| 106 |
-
|
| 107 |
-
|
| 108 |
});
|
| 109 |
|
| 110 |
// Check for consecutive passes (game end)
|
|
@@ -130,6 +220,150 @@ export function setupSocketHandlers(io: Server, socket: Socket, gameManager: Gam
|
|
| 130 |
}
|
| 131 |
});
|
| 132 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 133 |
// Chat messages
|
| 134 |
socket.on("chatMessage", (data: { content: string }) => {
|
| 135 |
const room = gameManager.getPlayerRoom(socket.id);
|
|
@@ -143,6 +377,48 @@ export function setupSocketHandlers(io: Server, socket: Socket, gameManager: Gam
|
|
| 143 |
}
|
| 144 |
});
|
| 145 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 146 |
// Handle disconnection
|
| 147 |
socket.on("disconnect", () => {
|
| 148 |
console.log(`Client disconnected: ${socket.id}`);
|
|
@@ -155,3 +431,33 @@ export function setupSocketHandlers(io: Server, socket: Socket, gameManager: Gam
|
|
| 155 |
}
|
| 156 |
});
|
| 157 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5 |
console.log(`Setting up socket handlers for ${socket.id}`);
|
| 6 |
|
| 7 |
// Join room
|
| 8 |
+
socket.on(
|
| 9 |
+
"joinRoom",
|
| 10 |
+
(data: { roomId?: string; nickname: string; preferredColor?: "black" | "white" }, callback?: (response: any) => void) => {
|
| 11 |
+
console.log("[gameSocket] joinRoom event received:", {
|
| 12 |
+
roomId: data.roomId,
|
| 13 |
+
nickname: data.nickname,
|
| 14 |
+
preferredColor: data.preferredColor,
|
| 15 |
+
hasCallback: !!callback,
|
| 16 |
+
socketId: socket.id
|
| 17 |
+
});
|
| 18 |
|
| 19 |
+
const { roomId, nickname, preferredColor } = data;
|
|
|
|
|
|
|
|
|
|
| 20 |
|
| 21 |
+
// Try to create or join room
|
| 22 |
+
try {
|
| 23 |
+
let room;
|
| 24 |
|
| 25 |
+
if (roomId) {
|
| 26 |
+
// Joining existing room
|
| 27 |
+
const existingRoom = gameManager.getRoom(roomId);
|
|
|
|
| 28 |
|
| 29 |
+
if (!existingRoom) {
|
| 30 |
+
// Room doesn't exist
|
| 31 |
+
if (callback) {
|
| 32 |
+
callback({
|
| 33 |
+
success: false,
|
| 34 |
+
error: "Room not found",
|
| 35 |
+
errorCode: "ROOM_NOT_FOUND"
|
| 36 |
+
});
|
| 37 |
+
}
|
| 38 |
+
return;
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
const playerCount = Object.keys(existingRoom.players).length;
|
| 42 |
+
if (playerCount >= 2) {
|
| 43 |
+
// Room is full
|
| 44 |
+
if (callback) {
|
| 45 |
+
callback({
|
| 46 |
+
success: false,
|
| 47 |
+
error: "Room is full",
|
| 48 |
+
errorCode: "ROOM_FULL"
|
| 49 |
+
});
|
| 50 |
+
}
|
| 51 |
+
return;
|
| 52 |
+
}
|
| 53 |
+
|
| 54 |
+
room = gameManager.joinRoom(roomId, socket.id, nickname, preferredColor);
|
| 55 |
+
} else {
|
| 56 |
+
// Creating new room
|
| 57 |
+
room = gameManager.createRoom(socket.id, nickname, undefined, preferredColor);
|
| 58 |
}
|
|
|
|
| 59 |
|
| 60 |
+
if (room) {
|
| 61 |
+
socket.join(room.id);
|
|
|
|
|
|
|
|
|
|
| 62 |
|
| 63 |
+
// Get complete game data for frontend
|
| 64 |
+
const currentPlayer = gameManager.getCurrentPlayer(room.id);
|
| 65 |
+
const stats = gameManager.getGameStats(room.id);
|
| 66 |
+
const tgn = room.game.toTGN();
|
| 67 |
+
|
| 68 |
+
// Get player list with colors
|
| 69 |
+
const players: { [playerId: string]: { nickname: string; color: "black" | "white" } } = {};
|
| 70 |
+
for (const [pid, player] of Object.entries(room.players)) {
|
| 71 |
+
players[pid] = {
|
| 72 |
+
nickname: player.nickname,
|
| 73 |
+
color: player.color
|
| 74 |
+
};
|
| 75 |
+
}
|
| 76 |
+
|
| 77 |
+
const response = {
|
| 78 |
+
success: true,
|
| 79 |
+
roomId: room.id,
|
| 80 |
+
playerId: socket.id,
|
| 81 |
+
playerColor: room.players[socket.id]?.color,
|
| 82 |
+
isAdmin: room.adminId === socket.id,
|
| 83 |
+
adminId: room.adminId,
|
| 84 |
+
players, // Include all players in room
|
| 85 |
+
gameState: {
|
| 86 |
+
boardShape: room.game.getShape(),
|
| 87 |
+
currentPlayer,
|
| 88 |
+
currentMoveIndex: room.game.getCurrentStep(),
|
| 89 |
+
capturedStones: {
|
| 90 |
+
black: stats?.capturedByBlack || 0,
|
| 91 |
+
white: stats?.capturedByWhite || 0
|
| 92 |
+
},
|
| 93 |
+
gameStatus: room.gameState.gameStatus,
|
| 94 |
+
winner: room.gameState.winner,
|
| 95 |
+
tgn
|
| 96 |
+
}
|
| 97 |
+
};
|
| 98 |
+
|
| 99 |
+
// Send response via callback
|
| 100 |
+
if (callback) {
|
| 101 |
+
console.log("[gameSocket] Sending response via callback:", {
|
| 102 |
+
roomId: response.roomId,
|
| 103 |
+
playerColor: response.playerColor
|
| 104 |
+
});
|
| 105 |
+
callback(response);
|
| 106 |
+
} else {
|
| 107 |
+
// Fallback to event emit for backward compatibility
|
| 108 |
+
console.log("[gameSocket] No callback, using roomJoined emit");
|
| 109 |
+
socket.emit("roomJoined", response);
|
| 110 |
+
}
|
| 111 |
+
|
| 112 |
+
// Notify other players
|
| 113 |
+
socket.to(room.id).emit("playerJoined", {
|
| 114 |
+
playerId: socket.id,
|
| 115 |
+
nickname: nickname
|
| 116 |
+
});
|
| 117 |
+
|
| 118 |
+
console.log(
|
| 119 |
+
`Player ${socket.id} ${roomId ? "joined" : "created"} room ${room.id}`
|
| 120 |
+
);
|
| 121 |
+
} else {
|
| 122 |
+
// Generic failure
|
| 123 |
+
if (callback) {
|
| 124 |
+
callback({
|
| 125 |
+
success: false,
|
| 126 |
+
error: "Failed to join or create room",
|
| 127 |
+
errorCode: "UNKNOWN_ERROR"
|
| 128 |
+
});
|
| 129 |
+
}
|
| 130 |
+
}
|
| 131 |
+
} catch (error) {
|
| 132 |
+
console.error(`Error in joinRoom handler:`, error);
|
| 133 |
+
if (callback) {
|
| 134 |
+
callback({
|
| 135 |
+
success: false,
|
| 136 |
+
error: "Server error",
|
| 137 |
+
errorCode: "SERVER_ERROR"
|
| 138 |
+
});
|
| 139 |
+
}
|
| 140 |
+
}
|
| 141 |
}
|
| 142 |
+
);
|
| 143 |
|
| 144 |
// Leave room
|
| 145 |
socket.on("leaveRoom", () => {
|
|
|
|
| 160 |
const room = gameManager.getPlayerRoom(socket.id);
|
| 161 |
if (room && gameManager.makeMove(room.id, socket.id, data)) {
|
| 162 |
// Get updated game data
|
|
|
|
| 163 |
const currentPlayer = gameManager.getCurrentPlayer(room.id);
|
| 164 |
const stats = gameManager.getGameStats(room.id);
|
| 165 |
const lastStep = room.game.getLastStep();
|
| 166 |
+
const tgn = room.game.toTGN();
|
| 167 |
|
| 168 |
// Broadcast game update to all players in the room
|
| 169 |
io.to(room.id).emit("gameUpdate", {
|
|
|
|
| 170 |
currentPlayer,
|
| 171 |
+
action: "move",
|
| 172 |
lastMove: data,
|
| 173 |
capturedStones: {
|
| 174 |
black: stats?.capturedByBlack || 0,
|
| 175 |
white: stats?.capturedByWhite || 0
|
| 176 |
},
|
| 177 |
capturedPositions: lastStep?.capturedPositions,
|
| 178 |
+
currentMoveIndex: room.game.getCurrentStep(),
|
| 179 |
+
tgn
|
| 180 |
});
|
| 181 |
} else {
|
| 182 |
socket.emit("error", { message: "Invalid move" });
|
|
|
|
| 188 |
const room = gameManager.getPlayerRoom(socket.id);
|
| 189 |
if (room && gameManager.passTurn(room.id, socket.id)) {
|
| 190 |
const currentPlayer = gameManager.getCurrentPlayer(room.id);
|
| 191 |
+
const tgn = room.game.toTGN();
|
| 192 |
|
| 193 |
io.to(room.id).emit("gameUpdate", {
|
| 194 |
currentPlayer,
|
| 195 |
action: "pass",
|
| 196 |
+
currentMoveIndex: room.game.getCurrentStep(),
|
| 197 |
+
tgn
|
| 198 |
});
|
| 199 |
|
| 200 |
// Check for consecutive passes (game end)
|
|
|
|
| 220 |
}
|
| 221 |
});
|
| 222 |
|
| 223 |
+
|
| 224 |
+
// Undo move
|
| 225 |
+
socket.on("undoMove", (callback?: (response: any) => void) => {
|
| 226 |
+
const room = gameManager.getPlayerRoom(socket.id);
|
| 227 |
+
|
| 228 |
+
if (!room) {
|
| 229 |
+
if (callback) callback({ success: false, error: "Not in a room", errorCode: "NOT_IN_ROOM" });
|
| 230 |
+
return;
|
| 231 |
+
}
|
| 232 |
+
|
| 233 |
+
if (room.gameState.gameStatus !== "playing") {
|
| 234 |
+
if (callback) callback({ success: false, error: "Game not active", errorCode: "GAME_NOT_ACTIVE" });
|
| 235 |
+
return;
|
| 236 |
+
}
|
| 237 |
+
|
| 238 |
+
const success = gameManager.undoMove(room.id, socket.id);
|
| 239 |
+
|
| 240 |
+
if (success) {
|
| 241 |
+
const currentPlayer = gameManager.getCurrentPlayer(room.id);
|
| 242 |
+
const stats = gameManager.getGameStats(room.id);
|
| 243 |
+
const tgn = room.game.toTGN();
|
| 244 |
+
|
| 245 |
+
io.to(room.id).emit("gameUpdate", {
|
| 246 |
+
currentPlayer,
|
| 247 |
+
action: "undo",
|
| 248 |
+
currentMoveIndex: room.game.getCurrentStep(),
|
| 249 |
+
capturedStones: {
|
| 250 |
+
black: stats?.capturedByBlack || 0,
|
| 251 |
+
white: stats?.capturedByWhite || 0
|
| 252 |
+
},
|
| 253 |
+
tgn
|
| 254 |
+
});
|
| 255 |
+
|
| 256 |
+
if (callback) callback({ success: true });
|
| 257 |
+
} else {
|
| 258 |
+
if (callback) callback({ success: false, error: "Cannot undo", errorCode: "UNDO_FAILED" });
|
| 259 |
+
}
|
| 260 |
+
});
|
| 261 |
+
|
| 262 |
+
|
| 263 |
+
// Redo move
|
| 264 |
+
socket.on("redoMove", (callback?: (response: any) => void) => {
|
| 265 |
+
const room = gameManager.getPlayerRoom(socket.id);
|
| 266 |
+
|
| 267 |
+
if (!room) {
|
| 268 |
+
if (callback) callback({ success: false, error: "Not in a room", errorCode: "NOT_IN_ROOM" });
|
| 269 |
+
return;
|
| 270 |
+
}
|
| 271 |
+
|
| 272 |
+
if (room.gameState.gameStatus !== "playing") {
|
| 273 |
+
if (callback) callback({ success: false, error: "Game not active", errorCode: "GAME_NOT_ACTIVE" });
|
| 274 |
+
return;
|
| 275 |
+
}
|
| 276 |
+
|
| 277 |
+
if (!room.game.canRedo()) {
|
| 278 |
+
if (callback) callback({ success: false, error: "Nothing to redo", errorCode: "NOTHING_TO_REDO" });
|
| 279 |
+
return;
|
| 280 |
+
}
|
| 281 |
+
|
| 282 |
+
const success = gameManager.redoMove(room.id, socket.id);
|
| 283 |
+
|
| 284 |
+
if (success) {
|
| 285 |
+
const currentPlayer = gameManager.getCurrentPlayer(room.id);
|
| 286 |
+
const stats = gameManager.getGameStats(room.id);
|
| 287 |
+
const lastStep = room.game.getLastStep();
|
| 288 |
+
const tgn = room.game.toTGN();
|
| 289 |
+
|
| 290 |
+
io.to(room.id).emit("gameUpdate", {
|
| 291 |
+
currentPlayer,
|
| 292 |
+
action: "redo",
|
| 293 |
+
lastMove: lastStep?.position,
|
| 294 |
+
capturedStones: {
|
| 295 |
+
black: stats?.capturedByBlack || 0,
|
| 296 |
+
white: stats?.capturedByWhite || 0
|
| 297 |
+
},
|
| 298 |
+
capturedPositions: lastStep?.capturedPositions,
|
| 299 |
+
currentMoveIndex: room.game.getCurrentStep(),
|
| 300 |
+
tgn
|
| 301 |
+
});
|
| 302 |
+
|
| 303 |
+
if (callback) callback({ success: true });
|
| 304 |
+
} else {
|
| 305 |
+
if (callback) callback({ success: false, error: "Redo failed", errorCode: "REDO_FAILED" });
|
| 306 |
+
}
|
| 307 |
+
});
|
| 308 |
+
|
| 309 |
+
|
| 310 |
+
// Reset game
|
| 311 |
+
socket.on("resetGame", (
|
| 312 |
+
data?: {
|
| 313 |
+
boardShape?: { x: number; y: number; z: number };
|
| 314 |
+
playerColors?: { [playerId: string]: "black" | "white" };
|
| 315 |
+
} | ((response: any) => void),
|
| 316 |
+
callback?: (response: any) => void
|
| 317 |
+
) => {
|
| 318 |
+
const room = gameManager.getPlayerRoom(socket.id);
|
| 319 |
+
|
| 320 |
+
if (!room) {
|
| 321 |
+
const cb = typeof data === 'function' ? data : callback;
|
| 322 |
+
if (cb) cb({ success: false, error: "Not in a room", errorCode: "NOT_IN_ROOM" });
|
| 323 |
+
return;
|
| 324 |
+
}
|
| 325 |
+
|
| 326 |
+
// Handle both old signature (no data) and new signature (with options)
|
| 327 |
+
const options = typeof data === 'object' && data !== null ? {
|
| 328 |
+
boardShape: data.boardShape,
|
| 329 |
+
playerColors: data.playerColors
|
| 330 |
+
} : undefined;
|
| 331 |
+
const responseCb = typeof data === 'function' ? data : callback;
|
| 332 |
+
|
| 333 |
+
const result = gameManager.resetGame(room.id, socket.id, options);
|
| 334 |
+
|
| 335 |
+
if (result.success) {
|
| 336 |
+
const currentPlayer = gameManager.getCurrentPlayer(room.id);
|
| 337 |
+
const tgn = room.game.toTGN();
|
| 338 |
+
|
| 339 |
+
// Get updated player info with new colors
|
| 340 |
+
const players: { [playerId: string]: { nickname: string; color: "black" | "white" } } = {};
|
| 341 |
+
for (const [pid, player] of Object.entries(room.players)) {
|
| 342 |
+
players[pid] = {
|
| 343 |
+
nickname: player.nickname,
|
| 344 |
+
color: player.color
|
| 345 |
+
};
|
| 346 |
+
}
|
| 347 |
+
|
| 348 |
+
io.to(room.id).emit("gameReset", {
|
| 349 |
+
currentPlayer,
|
| 350 |
+
boardShape: room.game.getShape(),
|
| 351 |
+
currentMoveIndex: 0,
|
| 352 |
+
capturedStones: { black: 0, white: 0 },
|
| 353 |
+
players,
|
| 354 |
+
tgn
|
| 355 |
+
});
|
| 356 |
+
|
| 357 |
+
if (responseCb) responseCb({ success: true });
|
| 358 |
+
} else {
|
| 359 |
+
if (responseCb) responseCb({
|
| 360 |
+
success: false,
|
| 361 |
+
error: result.error || "Reset failed",
|
| 362 |
+
errorCode: result.error === "Only room admin can reset the game" ? "NOT_ADMIN" : "RESET_FAILED"
|
| 363 |
+
});
|
| 364 |
+
}
|
| 365 |
+
});
|
| 366 |
+
|
| 367 |
// Chat messages
|
| 368 |
socket.on("chatMessage", (data: { content: string }) => {
|
| 369 |
const room = gameManager.getPlayerRoom(socket.id);
|
|
|
|
| 377 |
}
|
| 378 |
});
|
| 379 |
|
| 380 |
+
// Change nickname
|
| 381 |
+
socket.on(
|
| 382 |
+
"changeNickname",
|
| 383 |
+
(data: { nickname: string }, callback?: (response: any) => void) => {
|
| 384 |
+
const room = gameManager.getPlayerRoom(socket.id);
|
| 385 |
+
|
| 386 |
+
if (!room) {
|
| 387 |
+
const error = { success: false, error: "Not in a room" };
|
| 388 |
+
if (callback) callback(error);
|
| 389 |
+
return;
|
| 390 |
+
}
|
| 391 |
+
|
| 392 |
+
// Validate nickname
|
| 393 |
+
const validation = validateNickname(data.nickname);
|
| 394 |
+
if (!validation.valid) {
|
| 395 |
+
const error = { success: false, error: validation.error };
|
| 396 |
+
if (callback) callback(error);
|
| 397 |
+
return;
|
| 398 |
+
}
|
| 399 |
+
|
| 400 |
+
// Update player nickname
|
| 401 |
+
const player = room.players[socket.id];
|
| 402 |
+
if (player) {
|
| 403 |
+
const oldNickname = player.nickname;
|
| 404 |
+
player.nickname = data.nickname;
|
| 405 |
+
|
| 406 |
+
// Broadcast to all players in room
|
| 407 |
+
io.to(room.id).emit("nicknameChanged", {
|
| 408 |
+
playerId: socket.id,
|
| 409 |
+
nickname: data.nickname,
|
| 410 |
+
oldNickname: oldNickname
|
| 411 |
+
});
|
| 412 |
+
|
| 413 |
+
console.log(`Player ${socket.id} changed nickname: ${oldNickname} -> ${data.nickname}`);
|
| 414 |
+
|
| 415 |
+
if (callback) {
|
| 416 |
+
callback({ success: true, nickname: data.nickname });
|
| 417 |
+
}
|
| 418 |
+
}
|
| 419 |
+
}
|
| 420 |
+
);
|
| 421 |
+
|
| 422 |
// Handle disconnection
|
| 423 |
socket.on("disconnect", () => {
|
| 424 |
console.log(`Client disconnected: ${socket.id}`);
|
|
|
|
| 431 |
}
|
| 432 |
});
|
| 433 |
}
|
| 434 |
+
|
| 435 |
+
|
| 436 |
+
/**
|
| 437 |
+
* Validate nickname according to the rules:
|
| 438 |
+
* - Length: 3-20 characters
|
| 439 |
+
* - Characters: alphanumeric + spaces only
|
| 440 |
+
* - No leading/trailing whitespace
|
| 441 |
+
*/
|
| 442 |
+
function validateNickname(nickname: string): { valid: boolean; error?: string } {
|
| 443 |
+
if (!nickname || typeof nickname !== "string") {
|
| 444 |
+
return { valid: false, error: "Invalid nickname" };
|
| 445 |
+
}
|
| 446 |
+
|
| 447 |
+
const trimmed = nickname.trim();
|
| 448 |
+
|
| 449 |
+
if (trimmed.length < 3) {
|
| 450 |
+
return { valid: false, error: "Nickname must be at least 3 characters" };
|
| 451 |
+
}
|
| 452 |
+
if (trimmed.length > 20) {
|
| 453 |
+
return { valid: false, error: "Nickname must be 20 characters or less" };
|
| 454 |
+
}
|
| 455 |
+
if (!/^[a-zA-Z0-9 ]+$/.test(trimmed)) {
|
| 456 |
+
return { valid: false, error: "Only letters, numbers, and spaces allowed" };
|
| 457 |
+
}
|
| 458 |
+
if (trimmed !== nickname) {
|
| 459 |
+
return { valid: false, error: "No leading or trailing spaces allowed" };
|
| 460 |
+
}
|
| 461 |
+
|
| 462 |
+
return { valid: true };
|
| 463 |
+
}
|
trigo-web/inc/config.ts
ADDED
|
@@ -0,0 +1,181 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Configuration Utilities
|
| 3 |
+
*
|
| 4 |
+
* Provides centralized access to environment variables for ONNX model paths
|
| 5 |
+
* and other configuration values.
|
| 6 |
+
*
|
| 7 |
+
* Works across different contexts:
|
| 8 |
+
* - Frontend (Vite): Uses import.meta.env.VITE_* variables
|
| 9 |
+
* - Backend/Tools (Node): Uses process.env.* variables with dotenv
|
| 10 |
+
*/
|
| 11 |
+
|
| 12 |
+
import * as path from "path";
|
| 13 |
+
import { fileURLToPath } from "url";
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
// Environment detection
|
| 17 |
+
const isNode = typeof process !== "undefined" && process.versions?.node;
|
| 18 |
+
const isBrowser = typeof window !== "undefined";
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
/**
|
| 22 |
+
* ONNX Session Options
|
| 23 |
+
*/
|
| 24 |
+
export interface OnnxSessionOptions {
|
| 25 |
+
executionProviders: string[];
|
| 26 |
+
intraOpNumThreads?: number;
|
| 27 |
+
interOpNumThreads?: number;
|
| 28 |
+
graphOptimizationLevel?: "disabled" | "basic" | "extended" | "all";
|
| 29 |
+
enableCpuMemArena?: boolean;
|
| 30 |
+
enableMemPattern?: boolean;
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
/**
|
| 35 |
+
* Get ONNX model paths
|
| 36 |
+
* Returns paths appropriate for the current environment
|
| 37 |
+
*/
|
| 38 |
+
export function getOnnxModelPaths(): {
|
| 39 |
+
evaluationModel: string;
|
| 40 |
+
treeModel: string;
|
| 41 |
+
} {
|
| 42 |
+
// Frontend (Vite environment)
|
| 43 |
+
if (isBrowser && typeof import.meta.env !== "undefined") {
|
| 44 |
+
const evaluationModel = import.meta.env.VITE_ONNX_EVALUATION_MODEL;
|
| 45 |
+
const treeModel = import.meta.env.VITE_ONNX_TREE_MODEL;
|
| 46 |
+
|
| 47 |
+
if (!evaluationModel || !treeModel) {
|
| 48 |
+
throw new Error("ONNX model paths not configured. Check VITE_ONNX_EVALUATION_MODEL and VITE_ONNX_TREE_MODEL in .env");
|
| 49 |
+
}
|
| 50 |
+
|
| 51 |
+
return { evaluationModel, treeModel };
|
| 52 |
+
}
|
| 53 |
+
|
| 54 |
+
// Backend/Tools (Node environment)
|
| 55 |
+
if (isNode) {
|
| 56 |
+
const evaluationModel = process.env.ONNX_EVALUATION_MODEL;
|
| 57 |
+
const treeModel = process.env.ONNX_TREE_MODEL;
|
| 58 |
+
|
| 59 |
+
if (!evaluationModel || !treeModel) {
|
| 60 |
+
throw new Error("ONNX model paths not configured. Check ONNX_EVALUATION_MODEL and ONNX_TREE_MODEL in .env");
|
| 61 |
+
}
|
| 62 |
+
|
| 63 |
+
return { evaluationModel, treeModel };
|
| 64 |
+
}
|
| 65 |
+
|
| 66 |
+
// Should not reach here
|
| 67 |
+
throw new Error("Unknown environment - cannot determine model paths");
|
| 68 |
+
}
|
| 69 |
+
|
| 70 |
+
|
| 71 |
+
/**
|
| 72 |
+
* Get ONNX session options from environment variables
|
| 73 |
+
* Returns session options with threading and optimization configuration
|
| 74 |
+
*/
|
| 75 |
+
export function getOnnxSessionOptions(): OnnxSessionOptions {
|
| 76 |
+
// Default values
|
| 77 |
+
const options: OnnxSessionOptions = {
|
| 78 |
+
executionProviders: ["cpu"],
|
| 79 |
+
graphOptimizationLevel: "all",
|
| 80 |
+
enableCpuMemArena: true,
|
| 81 |
+
enableMemPattern: true
|
| 82 |
+
};
|
| 83 |
+
|
| 84 |
+
// Frontend (Vite environment)
|
| 85 |
+
if (isBrowser && typeof import.meta.env !== "undefined") {
|
| 86 |
+
const intraThreadsEnv = import.meta.env.VITE_ONNX_INTRA_OP_NUM_THREADS;
|
| 87 |
+
const interThreadsEnv = import.meta.env.VITE_ONNX_INTER_OP_NUM_THREADS;
|
| 88 |
+
const graphOptEnv = import.meta.env.VITE_ONNX_GRAPH_OPTIMIZATION_LEVEL;
|
| 89 |
+
|
| 90 |
+
if (intraThreadsEnv) options.intraOpNumThreads = parseInt(intraThreadsEnv, 10);
|
| 91 |
+
if (interThreadsEnv) options.interOpNumThreads = parseInt(interThreadsEnv, 10);
|
| 92 |
+
if (graphOptEnv) options.graphOptimizationLevel = graphOptEnv as any;
|
| 93 |
+
}
|
| 94 |
+
|
| 95 |
+
// Backend/Tools (Node environment)
|
| 96 |
+
if (isNode) {
|
| 97 |
+
const intraThreadsEnv = process.env.ONNX_INTRA_OP_NUM_THREADS;
|
| 98 |
+
const interThreadsEnv = process.env.ONNX_INTER_OP_NUM_THREADS;
|
| 99 |
+
const graphOptEnv = process.env.ONNX_GRAPH_OPTIMIZATION_LEVEL;
|
| 100 |
+
const cpuMemArenaEnv = process.env.ONNX_ENABLE_CPU_MEM_ARENA;
|
| 101 |
+
const memPatternEnv = process.env.ONNX_ENABLE_MEM_PATTERN;
|
| 102 |
+
|
| 103 |
+
if (intraThreadsEnv) options.intraOpNumThreads = parseInt(intraThreadsEnv, 10);
|
| 104 |
+
if (interThreadsEnv) options.interOpNumThreads = parseInt(interThreadsEnv, 10);
|
| 105 |
+
if (graphOptEnv) options.graphOptimizationLevel = graphOptEnv as any;
|
| 106 |
+
if (cpuMemArenaEnv) options.enableCpuMemArena = cpuMemArenaEnv === "true";
|
| 107 |
+
if (memPatternEnv) options.enableMemPattern = memPatternEnv === "true";
|
| 108 |
+
}
|
| 109 |
+
|
| 110 |
+
return options;
|
| 111 |
+
}
|
| 112 |
+
|
| 113 |
+
|
| 114 |
+
/**
|
| 115 |
+
* Get absolute path to model file (Node.js only)
|
| 116 |
+
* Resolves relative paths to absolute paths from project root
|
| 117 |
+
*/
|
| 118 |
+
export function getAbsoluteModelPath(relativePath: string): string {
|
| 119 |
+
if (!isNode) {
|
| 120 |
+
return relativePath; // Browser environment - return as-is
|
| 121 |
+
}
|
| 122 |
+
|
| 123 |
+
// If already absolute, return as-is
|
| 124 |
+
if (path.isAbsolute(relativePath)) {
|
| 125 |
+
return relativePath;
|
| 126 |
+
}
|
| 127 |
+
|
| 128 |
+
// Resolve relative to project root
|
| 129 |
+
const __filename = fileURLToPath(import.meta.url);
|
| 130 |
+
const __dirname = path.dirname(__filename);
|
| 131 |
+
const rootDir = path.resolve(__dirname, "..");
|
| 132 |
+
return path.resolve(rootDir, relativePath);
|
| 133 |
+
}
|
| 134 |
+
|
| 135 |
+
|
| 136 |
+
/**
|
| 137 |
+
* Load environment variables from .env file (Node.js only)
|
| 138 |
+
* Call this at the start of tool scripts
|
| 139 |
+
*
|
| 140 |
+
* Loading order:
|
| 141 |
+
* 1. .env (base configuration, committed to git)
|
| 142 |
+
* 2. .env.local (local overrides, not committed to git)
|
| 143 |
+
*/
|
| 144 |
+
export async function loadEnvConfig(): Promise<void> {
|
| 145 |
+
if (!isNode) {
|
| 146 |
+
console.warn("[Config] loadEnvConfig() called in non-Node environment");
|
| 147 |
+
return;
|
| 148 |
+
}
|
| 149 |
+
|
| 150 |
+
try {
|
| 151 |
+
// Dynamically import dotenv (ESM-compatible)
|
| 152 |
+
const dotenv = (await import("dotenv")).default;
|
| 153 |
+
const __filename = fileURLToPath(import.meta.url);
|
| 154 |
+
const __dirname = path.dirname(__filename);
|
| 155 |
+
const rootDir = path.resolve(__dirname, "..");
|
| 156 |
+
|
| 157 |
+
// Load base .env file
|
| 158 |
+
const baseResult = dotenv.config({ path: path.join(rootDir, ".env") });
|
| 159 |
+
|
| 160 |
+
if (baseResult.error) {
|
| 161 |
+
console.warn("[Config] Failed to load .env:", baseResult.error.message);
|
| 162 |
+
} else {
|
| 163 |
+
console.log("[Config] ✓ Base environment variables loaded from .env");
|
| 164 |
+
}
|
| 165 |
+
|
| 166 |
+
// Load .env.local for local overrides (if exists)
|
| 167 |
+
// Use override: true to allow .env.local to override .env values
|
| 168 |
+
const localResult = dotenv.config({
|
| 169 |
+
path: path.join(rootDir, ".env.local"),
|
| 170 |
+
override: true
|
| 171 |
+
});
|
| 172 |
+
|
| 173 |
+
if (!localResult.error) {
|
| 174 |
+
console.log("[Config] ✓ Local overrides loaded from .env.local");
|
| 175 |
+
}
|
| 176 |
+
// Silently ignore if .env.local doesn't exist (optional file)
|
| 177 |
+
|
| 178 |
+
} catch (error) {
|
| 179 |
+
console.warn("[Config] dotenv not available:", error);
|
| 180 |
+
}
|
| 181 |
+
}
|
trigo-web/inc/mctsAgent.ts
ADDED
|
@@ -0,0 +1,855 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Monte Carlo Tree Search (MCTS) Agent for Trigo
|
| 3 |
+
*
|
| 4 |
+
* Implements AlphaGo Zero-style MCTS with:
|
| 5 |
+
* - PUCT (Polynomial Upper Confidence Trees) selection
|
| 6 |
+
* - Neural network guidance for policy and value
|
| 7 |
+
* - Visit count statistics for training data generation
|
| 8 |
+
*
|
| 9 |
+
* Based on: Silver et al., "Mastering the Game of Go without Human Knowledge"
|
| 10 |
+
*/
|
| 11 |
+
|
| 12 |
+
import { TrigoGame } from "./trigo/game";
|
| 13 |
+
import type { Move } from "./trigo/types";
|
| 14 |
+
import { TrigoTreeAgent } from "./trigoTreeAgent";
|
| 15 |
+
import { TrigoEvaluationAgent } from "./trigoEvaluationAgent";
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
/**
|
| 19 |
+
* MCTS Configuration
|
| 20 |
+
*/
|
| 21 |
+
export interface MCTSConfig {
|
| 22 |
+
numSimulations: number; // Number of MCTS simulations per move (default: 600)
|
| 23 |
+
cPuct: number; // PUCT exploration constant (default: 1.0)
|
| 24 |
+
temperature: number; // Selection temperature for first 30 moves (default: 1.0)
|
| 25 |
+
dirichletAlpha: number; // Dirichlet noise alpha parameter (default: 0.03)
|
| 26 |
+
dirichletEpsilon: number; // Dirichlet noise mixing weight (default: 0.25)
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
/**
|
| 31 |
+
* MCTS Tree Node
|
| 32 |
+
* Stores search statistics for all legal actions from a given game state
|
| 33 |
+
*
|
| 34 |
+
* Memory optimization: Only root node stores the full game state.
|
| 35 |
+
* Non-root nodes only store the action that led to them.
|
| 36 |
+
* During simulation, a working state is cloned once and mutated along the path.
|
| 37 |
+
*/
|
| 38 |
+
interface MCTSNode {
|
| 39 |
+
state: TrigoGame | null; // Game state (only stored at root node for memory efficiency)
|
| 40 |
+
parent: MCTSNode | null; // Parent node (null for root)
|
| 41 |
+
action: Move | null; // Action that led to this node (null for root)
|
| 42 |
+
|
| 43 |
+
// MCTS statistics per action (action key -> value)
|
| 44 |
+
N: Map<string, number>; // Visit counts N(s,a)
|
| 45 |
+
W: Map<string, number>; // Total action-value W(s,a)
|
| 46 |
+
Q: Map<string, number>; // Mean action-value Q(s,a) = W(s,a) / N(s,a)
|
| 47 |
+
P: Map<string, number>; // Prior probabilities P(s,a) from policy network
|
| 48 |
+
|
| 49 |
+
children: Map<string, MCTSNode>; // Child nodes (action key -> child node)
|
| 50 |
+
expanded: boolean; // Whether this node has been expanded
|
| 51 |
+
terminalValue: number | null; // Cached terminal value (null if not terminal or not computed)
|
| 52 |
+
|
| 53 |
+
// Terminal propagation optimization (GPT-5.1 suggestions)
|
| 54 |
+
depth: number; // Distance from root (0 for root)
|
| 55 |
+
playerToMove: number; // Player to move at this node (1=Black, 2=White)
|
| 56 |
+
}
|
| 57 |
+
|
| 58 |
+
|
| 59 |
+
/**
|
| 60 |
+
* MCTS Agent
|
| 61 |
+
* Combines tree search with neural network evaluation
|
| 62 |
+
*/
|
| 63 |
+
export class MCTSAgent {
|
| 64 |
+
private treeAgent: TrigoTreeAgent; // For policy priors
|
| 65 |
+
private evaluationAgent: TrigoEvaluationAgent; // For value evaluation
|
| 66 |
+
private config: MCTSConfig;
|
| 67 |
+
public debugMode: boolean = false; // Enable debug logging
|
| 68 |
+
|
| 69 |
+
|
| 70 |
+
constructor(
|
| 71 |
+
treeAgent: TrigoTreeAgent,
|
| 72 |
+
evaluationAgent: TrigoEvaluationAgent,
|
| 73 |
+
config: Partial<MCTSConfig> = {}
|
| 74 |
+
) {
|
| 75 |
+
this.treeAgent = treeAgent;
|
| 76 |
+
this.evaluationAgent = evaluationAgent;
|
| 77 |
+
|
| 78 |
+
// Default configuration (AlphaGo Zero-inspired)
|
| 79 |
+
this.config = {
|
| 80 |
+
numSimulations: config.numSimulations ?? 600,
|
| 81 |
+
cPuct: config.cPuct ?? 1.0,
|
| 82 |
+
temperature: config.temperature ?? 1.0,
|
| 83 |
+
dirichletAlpha: config.dirichletAlpha ?? 0.03,
|
| 84 |
+
dirichletEpsilon: config.dirichletEpsilon ?? 0.25
|
| 85 |
+
};
|
| 86 |
+
}
|
| 87 |
+
|
| 88 |
+
|
| 89 |
+
/**
|
| 90 |
+
* Select best move using MCTS
|
| 91 |
+
*
|
| 92 |
+
* @param game Current game state
|
| 93 |
+
* @param moveNumber Move number (for temperature schedule)
|
| 94 |
+
* @returns Selected move with visit count statistics
|
| 95 |
+
*/
|
| 96 |
+
async selectMove(game: TrigoGame, moveNumber: number): Promise<{
|
| 97 |
+
move: Move;
|
| 98 |
+
visitCounts: Map<string, number>;
|
| 99 |
+
searchPolicy: Map<string, number>; // Normalized visit counts π(a|s)
|
| 100 |
+
rootValue: number;
|
| 101 |
+
}> {
|
| 102 |
+
// Create root node
|
| 103 |
+
const root = this.createNode(game, null, null);
|
| 104 |
+
|
| 105 |
+
// Check if root is already terminal (game over)
|
| 106 |
+
const terminalResult = this.checkTerminal(game);
|
| 107 |
+
if (terminalResult !== null) {
|
| 108 |
+
const currentPlayer = game.getCurrentPlayer();
|
| 109 |
+
return {
|
| 110 |
+
move: { player: currentPlayer === 1 ? "black" : "white", isPass: true },
|
| 111 |
+
visitCounts: new Map(),
|
| 112 |
+
searchPolicy: new Map(),
|
| 113 |
+
rootValue: terminalResult
|
| 114 |
+
};
|
| 115 |
+
}
|
| 116 |
+
|
| 117 |
+
// Run MCTS simulations
|
| 118 |
+
for (let i = 0; i < this.config.numSimulations; i++) {
|
| 119 |
+
await this.runSimulation(root, i);
|
| 120 |
+
}
|
| 121 |
+
|
| 122 |
+
// Temperature schedule: τ=1 for first 30 moves, τ→0 afterward
|
| 123 |
+
const temperature = moveNumber < 30 ? this.config.temperature : 0.01;
|
| 124 |
+
|
| 125 |
+
// Select move based on visit counts
|
| 126 |
+
const move = this.selectPlayAction(root, temperature);
|
| 127 |
+
|
| 128 |
+
// Set correct player for returned move
|
| 129 |
+
const currentPlayer = game.getCurrentPlayer();
|
| 130 |
+
move.player = currentPlayer === 1 ? "black" : "white";
|
| 131 |
+
|
| 132 |
+
// Compute search policy (normalized visit counts)
|
| 133 |
+
const searchPolicy = this.computeSearchPolicy(root, temperature);
|
| 134 |
+
|
| 135 |
+
// Get root value estimate (average Q-value weighted by visit counts)
|
| 136 |
+
const rootValue = this.getRootValue(root);
|
| 137 |
+
|
| 138 |
+
return {
|
| 139 |
+
move,
|
| 140 |
+
visitCounts: new Map(root.N),
|
| 141 |
+
searchPolicy,
|
| 142 |
+
rootValue
|
| 143 |
+
};
|
| 144 |
+
}
|
| 145 |
+
|
| 146 |
+
|
| 147 |
+
/**
|
| 148 |
+
* Run a single MCTS simulation
|
| 149 |
+
* Select -> Expand & Evaluate -> Backup
|
| 150 |
+
*
|
| 151 |
+
* Memory optimization: Clone state once at start, mutate along path.
|
| 152 |
+
* This reduces memory from O(nodes) to O(simulations).
|
| 153 |
+
*/
|
| 154 |
+
private async runSimulation(root: MCTSNode, simIndex?: number): Promise<void> {
|
| 155 |
+
// Invariant: root node must always have a non-null state
|
| 156 |
+
if (!root.state) {
|
| 157 |
+
throw new Error("runSimulation: root node must have a non-null state");
|
| 158 |
+
}
|
| 159 |
+
|
| 160 |
+
// Clone root state once for this simulation
|
| 161 |
+
const workingState = root.state.clone();
|
| 162 |
+
|
| 163 |
+
// 1. Selection: Traverse tree using PUCT until reaching unexpanded node
|
| 164 |
+
const { node, path } = this.select(root, workingState);
|
| 165 |
+
|
| 166 |
+
// 2. Expand and Evaluate: Get value from neural network
|
| 167 |
+
const value = await this.expandAndEvaluate(node, workingState);
|
| 168 |
+
|
| 169 |
+
// Debug logging
|
| 170 |
+
if (this.debugMode && simIndex !== undefined && simIndex < 10) {
|
| 171 |
+
const pathStr = path.map(p => p.actionKey).join(" → ");
|
| 172 |
+
const terminalStr = node.terminalValue !== null ? " [TERMINAL]" : "";
|
| 173 |
+
console.log(`Sim ${simIndex + 1}: ${pathStr || "(root)"} → value=${value.toFixed(4)}${terminalStr}`);
|
| 174 |
+
}
|
| 175 |
+
|
| 176 |
+
// 3. Backup: Propagate value up the tree
|
| 177 |
+
this.backup(path, value);
|
| 178 |
+
}
|
| 179 |
+
|
| 180 |
+
|
| 181 |
+
/**
|
| 182 |
+
* Selection phase: Traverse tree using PUCT
|
| 183 |
+
*
|
| 184 |
+
* @param root Root node to start selection from
|
| 185 |
+
* @param workingState Mutable game state that gets updated along the path
|
| 186 |
+
* @returns Leaf node and path taken
|
| 187 |
+
*/
|
| 188 |
+
private select(root: MCTSNode, workingState: TrigoGame): {
|
| 189 |
+
node: MCTSNode;
|
| 190 |
+
path: Array<{ node: MCTSNode; actionKey: string }>;
|
| 191 |
+
} {
|
| 192 |
+
const path: Array<{ node: MCTSNode; actionKey: string }> = [];
|
| 193 |
+
let node = root;
|
| 194 |
+
|
| 195 |
+
// Traverse until we reach an unexpanded node
|
| 196 |
+
while (node.expanded) {
|
| 197 |
+
// GPT-5.1 recommendation: Stop at terminal nodes immediately
|
| 198 |
+
// Terminal nodes should not be expanded or evaluated further
|
| 199 |
+
if (node.terminalValue !== null) {
|
| 200 |
+
break; // Return terminal node, use its cached value
|
| 201 |
+
}
|
| 202 |
+
|
| 203 |
+
// Get all legal actions
|
| 204 |
+
const actionKeys = Array.from(node.P.keys());
|
| 205 |
+
|
| 206 |
+
// Terminal node check: if expanded but no actions, this is a terminal node
|
| 207 |
+
if (actionKeys.length === 0) {
|
| 208 |
+
break; // Return this terminal node as leaf
|
| 209 |
+
}
|
| 210 |
+
|
| 211 |
+
// Select action with best PUCT value
|
| 212 |
+
// Both players select HIGHEST PUCT value:
|
| 213 |
+
// - Black: PUCT = -Q + U, max PUCT = max(-Q) = min(Q) ✓
|
| 214 |
+
// - White: PUCT = Q + U, max PUCT = max(Q) ✓
|
| 215 |
+
const currentPlayer = workingState.getCurrentPlayer();
|
| 216 |
+
const isWhite = currentPlayer === 2;
|
| 217 |
+
|
| 218 |
+
let bestActionKey = actionKeys[0];
|
| 219 |
+
let bestPuct = this.calculatePUCT(node, bestActionKey, isWhite);
|
| 220 |
+
|
| 221 |
+
for (let i = 1; i < actionKeys.length; i++) {
|
| 222 |
+
const actionKey = actionKeys[i];
|
| 223 |
+
const puct = this.calculatePUCT(node, actionKey, isWhite);
|
| 224 |
+
|
| 225 |
+
if (puct > bestPuct) {
|
| 226 |
+
bestPuct = puct;
|
| 227 |
+
bestActionKey = actionKey;
|
| 228 |
+
}
|
| 229 |
+
}
|
| 230 |
+
|
| 231 |
+
// Record path
|
| 232 |
+
path.push({ node, actionKey: bestActionKey });
|
| 233 |
+
|
| 234 |
+
// Apply action to working state (instead of cloning)
|
| 235 |
+
const action = this.decodeAction(bestActionKey);
|
| 236 |
+
if (action.isPass) {
|
| 237 |
+
workingState.pass();
|
| 238 |
+
} else if (action.x !== undefined && action.y !== undefined && action.z !== undefined) {
|
| 239 |
+
workingState.drop({ x: action.x, y: action.y, z: action.z });
|
| 240 |
+
}
|
| 241 |
+
|
| 242 |
+
// Move to child (create if doesn't exist)
|
| 243 |
+
if (!node.children.has(bestActionKey)) {
|
| 244 |
+
// Create child node WITHOUT storing state (memory optimization)
|
| 245 |
+
const childNode = this.createNode(null, node, action);
|
| 246 |
+
node.children.set(bestActionKey, childNode);
|
| 247 |
+
}
|
| 248 |
+
|
| 249 |
+
node = node.children.get(bestActionKey)!;
|
| 250 |
+
}
|
| 251 |
+
|
| 252 |
+
return { node, path };
|
| 253 |
+
}
|
| 254 |
+
|
| 255 |
+
|
| 256 |
+
/**
|
| 257 |
+
* Expand and evaluate leaf node using neural networks
|
| 258 |
+
*
|
| 259 |
+
* @param node Leaf node to expand
|
| 260 |
+
* @param workingState Current game state at this node (passed from simulation)
|
| 261 |
+
* @returns Value estimate from evaluation network
|
| 262 |
+
*/
|
| 263 |
+
private async expandAndEvaluate(node: MCTSNode, workingState: TrigoGame): Promise<number> {
|
| 264 |
+
// Check if terminal value is already cached
|
| 265 |
+
if (node.terminalValue !== null) {
|
| 266 |
+
return node.terminalValue;
|
| 267 |
+
}
|
| 268 |
+
|
| 269 |
+
// Check if game is over (terminal state)
|
| 270 |
+
const terminalValue = this.checkTerminal(workingState);
|
| 271 |
+
if (terminalValue !== null) {
|
| 272 |
+
// Mark terminal node as expanded with empty action set to prevent revisits
|
| 273 |
+
// Cache the terminal value to avoid repeated checks
|
| 274 |
+
node.expanded = true;
|
| 275 |
+
node.terminalValue = terminalValue;
|
| 276 |
+
node.P = new Map(); // No actions available (terminal)
|
| 277 |
+
node.N = new Map();
|
| 278 |
+
node.W = new Map();
|
| 279 |
+
node.Q = new Map();
|
| 280 |
+
node.children = new Map();
|
| 281 |
+
|
| 282 |
+
return terminalValue;
|
| 283 |
+
}
|
| 284 |
+
|
| 285 |
+
// Non-terminal state: expand with policy network and evaluate
|
| 286 |
+
// Get all valid moves
|
| 287 |
+
const currentPlayer = workingState.getCurrentPlayer() === 1 ? "black" : "white";
|
| 288 |
+
const validPositions = workingState.validMovePositions();
|
| 289 |
+
const moves: Move[] = validPositions.map(pos => ({
|
| 290 |
+
x: pos.x,
|
| 291 |
+
y: pos.y,
|
| 292 |
+
z: pos.z,
|
| 293 |
+
player: currentPlayer
|
| 294 |
+
}));
|
| 295 |
+
moves.push({ player: currentPlayer, isPass: true });
|
| 296 |
+
|
| 297 |
+
// Get policy priors from tree agent
|
| 298 |
+
const scoredMoves = await this.treeAgent.scoreMoves(workingState, moves);
|
| 299 |
+
|
| 300 |
+
// Convert log probabilities to probabilities and normalize (stable softmax)
|
| 301 |
+
const maxScore = Math.max(...scoredMoves.map(m => m.score));
|
| 302 |
+
const expScores = scoredMoves.map(m => Math.exp(m.score - maxScore));
|
| 303 |
+
const sumExp = expScores.reduce((sum, exp) => sum + exp, 0);
|
| 304 |
+
|
| 305 |
+
// Initialize priors P(s,a)
|
| 306 |
+
node.P = new Map();
|
| 307 |
+
node.N = new Map();
|
| 308 |
+
node.W = new Map();
|
| 309 |
+
node.Q = new Map();
|
| 310 |
+
|
| 311 |
+
// Handle edge case: if all scores are -Infinity or sumExp is 0/NaN
|
| 312 |
+
const useFallback = !isFinite(sumExp) || sumExp < 1e-10;
|
| 313 |
+
|
| 314 |
+
for (let i = 0; i < scoredMoves.length; i++) {
|
| 315 |
+
const actionKey = this.encodeAction(scoredMoves[i].move);
|
| 316 |
+
|
| 317 |
+
// Use uniform distribution as fallback if normalization fails
|
| 318 |
+
const prior = useFallback ? (1.0 / scoredMoves.length) : (expScores[i] / sumExp);
|
| 319 |
+
|
| 320 |
+
node.P.set(actionKey, prior);
|
| 321 |
+
node.N.set(actionKey, 0);
|
| 322 |
+
node.W.set(actionKey, 0);
|
| 323 |
+
node.Q.set(actionKey, 0);
|
| 324 |
+
}
|
| 325 |
+
|
| 326 |
+
// Add Dirichlet noise at root
|
| 327 |
+
if (node.parent === null) {
|
| 328 |
+
this.addDirichletNoise(node.P);
|
| 329 |
+
}
|
| 330 |
+
|
| 331 |
+
// Mark as expanded
|
| 332 |
+
node.expanded = true;
|
| 333 |
+
|
| 334 |
+
// Get value estimate from evaluation agent
|
| 335 |
+
const evaluation = await this.evaluationAgent.evaluatePosition(workingState);
|
| 336 |
+
|
| 337 |
+
// Return value directly (value model returns white-positive by design)
|
| 338 |
+
return evaluation.value;
|
| 339 |
+
}
|
| 340 |
+
|
| 341 |
+
|
| 342 |
+
/**
|
| 343 |
+
* Backup phase: Propagate value up the tree
|
| 344 |
+
*
|
| 345 |
+
* White-positive minimax propagation:
|
| 346 |
+
* - All Q-values represent White's advantage (positive = White winning)
|
| 347 |
+
* - When all children are terminal, mark parent as terminal with minimax value:
|
| 348 |
+
* * White's turn: terminal_value = max(children terminal values)
|
| 349 |
+
* * Black's turn: terminal_value = min(children terminal values)
|
| 350 |
+
*
|
| 351 |
+
* Improvements (based on GPT-5.1 review):
|
| 352 |
+
* - Uses stored playerToMove instead of computing from depth
|
| 353 |
+
* - Uses stored depth instead of recomputing via parent walk
|
| 354 |
+
*
|
| 355 |
+
* @param path Path from root to leaf
|
| 356 |
+
* @param value Value to propagate (white-positive: positive = white winning)
|
| 357 |
+
*/
|
| 358 |
+
private backup(path: Array<{ node: MCTSNode; actionKey: string }>, value: number): void {
|
| 359 |
+
// Propagate value up the tree (white-positive throughout)
|
| 360 |
+
// No sign flipping needed - Q values are always white-positive
|
| 361 |
+
for (let i = path.length - 1; i >= 0; i--) {
|
| 362 |
+
const { node, actionKey } = path[i];
|
| 363 |
+
|
| 364 |
+
// Update statistics
|
| 365 |
+
const n = node.N.get(actionKey) ?? 0;
|
| 366 |
+
const w = node.W.get(actionKey) ?? 0;
|
| 367 |
+
|
| 368 |
+
node.N.set(actionKey, n + 1);
|
| 369 |
+
node.W.set(actionKey, w + value);
|
| 370 |
+
node.Q.set(actionKey, (w + value) / (n + 1));
|
| 371 |
+
|
| 372 |
+
// ========== Terminal State Propagation ==========
|
| 373 |
+
// Check if this node should be marked as terminal
|
| 374 |
+
// Condition: node is fully expanded AND all children are terminal AND node itself not yet marked
|
| 375 |
+
if (node.expanded && node.terminalValue === null) {
|
| 376 |
+
const actionKeys = Array.from(node.P.keys());
|
| 377 |
+
|
| 378 |
+
// Skip propagation if no actions (already a terminal leaf, or error state)
|
| 379 |
+
if (actionKeys.length === 0) {
|
| 380 |
+
continue;
|
| 381 |
+
}
|
| 382 |
+
|
| 383 |
+
// Check if ALL children are terminal
|
| 384 |
+
let allChildrenTerminal = true;
|
| 385 |
+
const childTerminalValues: number[] = [];
|
| 386 |
+
|
| 387 |
+
for (const key of actionKeys) {
|
| 388 |
+
const child = node.children.get(key);
|
| 389 |
+
|
| 390 |
+
// If child doesn't exist yet, not all children explored
|
| 391 |
+
if (!child) {
|
| 392 |
+
allChildrenTerminal = false;
|
| 393 |
+
break;
|
| 394 |
+
}
|
| 395 |
+
|
| 396 |
+
// If child is not terminal, not all children terminal
|
| 397 |
+
if (child.terminalValue === null) {
|
| 398 |
+
allChildrenTerminal = false;
|
| 399 |
+
break;
|
| 400 |
+
}
|
| 401 |
+
|
| 402 |
+
// Child is terminal, collect its value
|
| 403 |
+
childTerminalValues.push(child.terminalValue);
|
| 404 |
+
}
|
| 405 |
+
|
| 406 |
+
// If all children are terminal, mark current node as terminal with minimax value
|
| 407 |
+
if (allChildrenTerminal && childTerminalValues.length > 0) {
|
| 408 |
+
// Use stored playerToMove instead of computing from depth (GPT-5.1 suggestion)
|
| 409 |
+
const isWhiteTurn = node.playerToMove === 2; // 2 = White, 1 = Black
|
| 410 |
+
|
| 411 |
+
// Apply minimax: choose best child value from current player's perspective
|
| 412 |
+
let terminalValue: number;
|
| 413 |
+
|
| 414 |
+
if (isWhiteTurn) {
|
| 415 |
+
// White maximizes Q-value (white-positive)
|
| 416 |
+
terminalValue = Math.max(...childTerminalValues);
|
| 417 |
+
} else {
|
| 418 |
+
// Black minimizes Q-value (white-positive)
|
| 419 |
+
terminalValue = Math.min(...childTerminalValues);
|
| 420 |
+
}
|
| 421 |
+
|
| 422 |
+
// Mark this node as terminal with the minimax value
|
| 423 |
+
node.terminalValue = terminalValue;
|
| 424 |
+
|
| 425 |
+
// Debug logging for terminal propagation
|
| 426 |
+
if (this.debugMode) {
|
| 427 |
+
const playerName = isWhiteTurn ? 'White' : 'Black';
|
| 428 |
+
console.log(
|
| 429 |
+
`[Terminal Propagation] Node at depth ${node.depth} (${playerName}) marked terminal: ` +
|
| 430 |
+
`value=${terminalValue.toFixed(4)}, children=[${childTerminalValues.map(v => v.toFixed(2)).join(', ')}]`
|
| 431 |
+
);
|
| 432 |
+
}
|
| 433 |
+
}
|
| 434 |
+
}
|
| 435 |
+
// ================================================
|
| 436 |
+
}
|
| 437 |
+
}
|
| 438 |
+
|
| 439 |
+
|
| 440 |
+
/**
|
| 441 |
+
* Calculate PUCT value for action selection
|
| 442 |
+
*
|
| 443 |
+
* PUCT = Q(s,a) + U(s,a) [for White, who maximizes]
|
| 444 |
+
* PUCT = -Q(s,a) + U(s,a) [for Black, who minimizes]
|
| 445 |
+
* where U(s,a) = c_puct * P(s,a) * sqrt(Σ_b N(s,b)) / (1 + N(s,a))
|
| 446 |
+
*
|
| 447 |
+
* @param node Current node
|
| 448 |
+
* @param actionKey Action to evaluate
|
| 449 |
+
* @param isWhite Whether current player is White
|
| 450 |
+
* @returns PUCT value
|
| 451 |
+
*/
|
| 452 |
+
private calculatePUCT(node: MCTSNode, actionKey: string, isWhite: boolean): number {
|
| 453 |
+
const Q = node.Q.get(actionKey) ?? 0;
|
| 454 |
+
const N = node.N.get(actionKey) ?? 0;
|
| 455 |
+
const P = node.P.get(actionKey) ?? 0;
|
| 456 |
+
|
| 457 |
+
// Sum of all visit counts at this node
|
| 458 |
+
const totalN = Array.from(node.N.values()).reduce((sum, n) => sum + n, 0);
|
| 459 |
+
|
| 460 |
+
// Exploration term: U(s,a) = c_puct * P(s,a) * sqrt(Σ_b N(s,b) + 1) / (1 + N(s,a))
|
| 461 |
+
// +1 in sqrt to avoid zero exploration when node first expanded
|
| 462 |
+
const U = this.config.cPuct * P * Math.sqrt(totalN + 1) / (1 + N);
|
| 463 |
+
|
| 464 |
+
// Black minimizes Q (flips sign), White maximizes Q
|
| 465 |
+
return (isWhite ? Q : -Q) + U;
|
| 466 |
+
}
|
| 467 |
+
|
| 468 |
+
|
| 469 |
+
/**
|
| 470 |
+
* Select action to play based on visit counts
|
| 471 |
+
* Uses temperature to control exploration vs exploitation
|
| 472 |
+
*
|
| 473 |
+
* @param node Root node
|
| 474 |
+
* @param temperature Selection temperature (τ=1 for exploration, τ→0 for greedy)
|
| 475 |
+
* @returns Selected move
|
| 476 |
+
*/
|
| 477 |
+
private selectPlayAction(node: MCTSNode, temperature: number): Move {
|
| 478 |
+
const actionKeys = Array.from(node.N.keys());
|
| 479 |
+
|
| 480 |
+
// Edge case: no actions available (unexpanded root or terminal state)
|
| 481 |
+
if (actionKeys.length === 0) {
|
| 482 |
+
// Fallback to priors if available
|
| 483 |
+
const priorKeys = Array.from(node.P.keys());
|
| 484 |
+
if (priorKeys.length > 0) {
|
| 485 |
+
// Sample from prior distribution
|
| 486 |
+
const priors = priorKeys.map(key => node.P.get(key) ?? 0);
|
| 487 |
+
const sumP = priors.reduce((sum, p) => sum + p, 0);
|
| 488 |
+
if (sumP > 0) {
|
| 489 |
+
let rand = Math.random() * sumP;
|
| 490 |
+
for (let i = 0; i < priorKeys.length; i++) {
|
| 491 |
+
rand -= priors[i];
|
| 492 |
+
if (rand <= 0) {
|
| 493 |
+
return this.decodeAction(priorKeys[i]);
|
| 494 |
+
}
|
| 495 |
+
}
|
| 496 |
+
return this.decodeAction(priorKeys[priorKeys.length - 1]);
|
| 497 |
+
}
|
| 498 |
+
// Uniform fallback
|
| 499 |
+
const randomIndex = Math.floor(Math.random() * priorKeys.length);
|
| 500 |
+
return this.decodeAction(priorKeys[randomIndex]);
|
| 501 |
+
}
|
| 502 |
+
// No actions at all - return Pass as last resort
|
| 503 |
+
return { player: "black", isPass: true };
|
| 504 |
+
}
|
| 505 |
+
|
| 506 |
+
if (temperature < 0.01) {
|
| 507 |
+
// Greedy: Select action with highest visit count
|
| 508 |
+
let bestActionKey = actionKeys[0];
|
| 509 |
+
let bestN = node.N.get(bestActionKey) ?? 0;
|
| 510 |
+
|
| 511 |
+
for (let i = 1; i < actionKeys.length; i++) {
|
| 512 |
+
const actionKey = actionKeys[i];
|
| 513 |
+
const n = node.N.get(actionKey) ?? 0;
|
| 514 |
+
if (n > bestN) {
|
| 515 |
+
bestN = n;
|
| 516 |
+
bestActionKey = actionKey;
|
| 517 |
+
}
|
| 518 |
+
}
|
| 519 |
+
|
| 520 |
+
return this.decodeAction(bestActionKey);
|
| 521 |
+
} else {
|
| 522 |
+
// Temperature-based sampling: π(a|s) ∝ N(s,a)^(1/τ)
|
| 523 |
+
const nValues = actionKeys.map(key => node.N.get(key) ?? 0);
|
| 524 |
+
const nPowered = nValues.map(n => Math.pow(n, 1 / temperature));
|
| 525 |
+
const sumN = nPowered.reduce((sum, n) => sum + n, 0);
|
| 526 |
+
|
| 527 |
+
// Handle edge case: if all visits are 0 or sum is invalid
|
| 528 |
+
if (!isFinite(sumN) || sumN <= 0) {
|
| 529 |
+
// Fallback to uniform random selection (or use priors)
|
| 530 |
+
const randomIndex = Math.floor(Math.random() * actionKeys.length);
|
| 531 |
+
return this.decodeAction(actionKeys[randomIndex]);
|
| 532 |
+
}
|
| 533 |
+
|
| 534 |
+
// Sample from distribution
|
| 535 |
+
let rand = Math.random() * sumN;
|
| 536 |
+
for (let i = 0; i < actionKeys.length; i++) {
|
| 537 |
+
rand -= nPowered[i];
|
| 538 |
+
if (rand <= 0) {
|
| 539 |
+
return this.decodeAction(actionKeys[i]);
|
| 540 |
+
}
|
| 541 |
+
}
|
| 542 |
+
|
| 543 |
+
// Fallback (shouldn't reach here due to floating point precision)
|
| 544 |
+
return this.decodeAction(actionKeys[actionKeys.length - 1]);
|
| 545 |
+
}
|
| 546 |
+
}
|
| 547 |
+
|
| 548 |
+
|
| 549 |
+
/**
|
| 550 |
+
* Compute search policy from visit counts
|
| 551 |
+
* π(a|s) = N(s,a)^(1/τ) / Σ_b N(s,b)^(1/τ)
|
| 552 |
+
*
|
| 553 |
+
* @param node Root node
|
| 554 |
+
* @param temperature Selection temperature
|
| 555 |
+
* @returns Normalized policy distribution
|
| 556 |
+
*/
|
| 557 |
+
private computeSearchPolicy(node: MCTSNode, temperature: number): Map<string, number> {
|
| 558 |
+
const policy = new Map<string, number>();
|
| 559 |
+
const actionKeys = Array.from(node.N.keys());
|
| 560 |
+
|
| 561 |
+
// Compute π(a|s) ∝ N(s,a)^(1/τ)
|
| 562 |
+
const nPowered = actionKeys.map(key => Math.pow(node.N.get(key) ?? 0, 1 / temperature));
|
| 563 |
+
const sumN = nPowered.reduce((sum, n) => sum + n, 0);
|
| 564 |
+
|
| 565 |
+
// Handle edge case: if all visits are 0 or sum is invalid
|
| 566 |
+
if (!isFinite(sumN) || sumN <= 0) {
|
| 567 |
+
// Fallback to uniform distribution
|
| 568 |
+
const uniform = 1 / actionKeys.length;
|
| 569 |
+
for (const key of actionKeys) {
|
| 570 |
+
policy.set(key, uniform);
|
| 571 |
+
}
|
| 572 |
+
return policy;
|
| 573 |
+
}
|
| 574 |
+
|
| 575 |
+
for (let i = 0; i < actionKeys.length; i++) {
|
| 576 |
+
const actionKey = actionKeys[i];
|
| 577 |
+
policy.set(actionKey, nPowered[i] / sumN);
|
| 578 |
+
}
|
| 579 |
+
|
| 580 |
+
return policy;
|
| 581 |
+
}
|
| 582 |
+
|
| 583 |
+
|
| 584 |
+
/**
|
| 585 |
+
* Get root value estimate (weighted average of Q-values)
|
| 586 |
+
*/
|
| 587 |
+
private getRootValue(node: MCTSNode): number {
|
| 588 |
+
const actionKeys = Array.from(node.N.keys());
|
| 589 |
+
const totalN = Array.from(node.N.values()).reduce((sum, n) => sum + n, 0);
|
| 590 |
+
|
| 591 |
+
if (totalN === 0) {
|
| 592 |
+
return 0;
|
| 593 |
+
}
|
| 594 |
+
|
| 595 |
+
let weightedSum = 0;
|
| 596 |
+
for (const actionKey of actionKeys) {
|
| 597 |
+
const q = node.Q.get(actionKey) ?? 0;
|
| 598 |
+
const n = node.N.get(actionKey) ?? 0;
|
| 599 |
+
weightedSum += q * n;
|
| 600 |
+
}
|
| 601 |
+
|
| 602 |
+
return weightedSum / totalN;
|
| 603 |
+
}
|
| 604 |
+
|
| 605 |
+
|
| 606 |
+
/**
|
| 607 |
+
* Add Dirichlet noise to prior probabilities at root
|
| 608 |
+
* P(s,a) = (1 - ε) * p_a + ε * η_a
|
| 609 |
+
* where η ~ Dir(α)
|
| 610 |
+
*
|
| 611 |
+
* Note: Pass move is excluded from noise to prevent exploration of
|
| 612 |
+
* clearly suboptimal opening passes.
|
| 613 |
+
*/
|
| 614 |
+
private addDirichletNoise(priors: Map<string, number>): void {
|
| 615 |
+
// Exclude Pass from Dirichlet noise - it should not be explored at root
|
| 616 |
+
const actionKeys = Array.from(priors.keys()).filter(key => key !== "pass");
|
| 617 |
+
const alpha = this.config.dirichletAlpha;
|
| 618 |
+
const epsilon = this.config.dirichletEpsilon;
|
| 619 |
+
|
| 620 |
+
// If only Pass is available, no noise to add
|
| 621 |
+
if (actionKeys.length === 0) {
|
| 622 |
+
return;
|
| 623 |
+
}
|
| 624 |
+
|
| 625 |
+
// Generate Dirichlet noise (simplified using Gamma distribution)
|
| 626 |
+
const noise: number[] = [];
|
| 627 |
+
let noiseSum = 0;
|
| 628 |
+
|
| 629 |
+
for (let i = 0; i < actionKeys.length; i++) {
|
| 630 |
+
// Gamma(α, 1) approximation using rejection sampling
|
| 631 |
+
const sample = this.sampleGamma(alpha);
|
| 632 |
+
noise.push(sample);
|
| 633 |
+
noiseSum += sample;
|
| 634 |
+
}
|
| 635 |
+
|
| 636 |
+
// Handle edge case: if all Gamma samples are 0 (extremely unlikely but possible)
|
| 637 |
+
if (!isFinite(noiseSum) || noiseSum <= 0) {
|
| 638 |
+
// Fallback: use uniform noise (no mixing, keep original priors)
|
| 639 |
+
return;
|
| 640 |
+
}
|
| 641 |
+
|
| 642 |
+
// Normalize and mix with priors (only for non-Pass actions)
|
| 643 |
+
for (let i = 0; i < actionKeys.length; i++) {
|
| 644 |
+
const actionKey = actionKeys[i];
|
| 645 |
+
const prior = priors.get(actionKey) ?? 0;
|
| 646 |
+
const noiseFraction = noise[i] / noiseSum;
|
| 647 |
+
priors.set(actionKey, (1 - epsilon) * prior + epsilon * noiseFraction);
|
| 648 |
+
}
|
| 649 |
+
}
|
| 650 |
+
|
| 651 |
+
|
| 652 |
+
/**
|
| 653 |
+
* Sample from Gamma distribution using Marsaglia and Tsang method (2000)
|
| 654 |
+
* Used for Dirichlet noise generation
|
| 655 |
+
*/
|
| 656 |
+
private sampleGamma(alpha: number): number {
|
| 657 |
+
if (alpha <= 0) {
|
| 658 |
+
throw new Error("Gamma distribution alpha must be > 0");
|
| 659 |
+
}
|
| 660 |
+
|
| 661 |
+
// For alpha < 1, use transformation: sample Gamma(alpha+1) then multiply by U^(1/alpha)
|
| 662 |
+
if (alpha < 1) {
|
| 663 |
+
const u = Math.random();
|
| 664 |
+
const g = this.sampleGamma(alpha + 1);
|
| 665 |
+
return g * Math.pow(u, 1 / alpha);
|
| 666 |
+
}
|
| 667 |
+
|
| 668 |
+
// For alpha >= 1, use Marsaglia and Tsang's method
|
| 669 |
+
const d = alpha - 1/3;
|
| 670 |
+
const c = 1 / Math.sqrt(9 * d);
|
| 671 |
+
|
| 672 |
+
while (true) {
|
| 673 |
+
let x, v;
|
| 674 |
+
do {
|
| 675 |
+
x = this.randomNormal();
|
| 676 |
+
v = 1 + c * x;
|
| 677 |
+
} while (v <= 0);
|
| 678 |
+
|
| 679 |
+
v = v * v * v;
|
| 680 |
+
const u = Math.random();
|
| 681 |
+
|
| 682 |
+
// Fast acceptance check
|
| 683 |
+
if (u < 1 - 0.0331 * x * x * x * x) {
|
| 684 |
+
return d * v;
|
| 685 |
+
}
|
| 686 |
+
|
| 687 |
+
// Fallback acceptance check
|
| 688 |
+
if (Math.log(u) < 0.5 * x * x + d * (1 - v + Math.log(v))) {
|
| 689 |
+
return d * v;
|
| 690 |
+
}
|
| 691 |
+
}
|
| 692 |
+
}
|
| 693 |
+
|
| 694 |
+
|
| 695 |
+
/**
|
| 696 |
+
* Sample from standard normal distribution (Box-Muller transform)
|
| 697 |
+
*/
|
| 698 |
+
private randomNormal(): number {
|
| 699 |
+
const u1 = Math.random();
|
| 700 |
+
const u2 = Math.random();
|
| 701 |
+
return Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2);
|
| 702 |
+
}
|
| 703 |
+
|
| 704 |
+
|
| 705 |
+
/**
|
| 706 |
+
* Check if game state is terminal and return value if so
|
| 707 |
+
*
|
| 708 |
+
* Terminal conditions (checked in order of cost):
|
| 709 |
+
* 1. Game already finished (double-pass or resignation) - CHEAPEST
|
| 710 |
+
* 2. Board coverage > 50% AND naturally terminal (calls isNaturallyTerminal) - EXPENSIVE
|
| 711 |
+
*
|
| 712 |
+
* NOTE: The coverage check (> 50%) is an optimization to avoid expensive
|
| 713 |
+
* territory calculations on sparse boards where natural termination is unlikely.
|
| 714 |
+
*
|
| 715 |
+
* @param state Game state to check
|
| 716 |
+
* @returns Terminal value (white-positive) if terminal, null otherwise
|
| 717 |
+
*/
|
| 718 |
+
private checkTerminal(state: TrigoGame): number | null {
|
| 719 |
+
// 1. Check if game is already finished (double-pass, resignation, etc.)
|
| 720 |
+
// This is the cheapest check - just reading a status flag
|
| 721 |
+
if (state.getGameStatus() === "finished") {
|
| 722 |
+
const territory = state.getTerritory();
|
| 723 |
+
return this.calculateTerminalValue(territory);
|
| 724 |
+
}
|
| 725 |
+
|
| 726 |
+
// 2. Check for "natural" game end (all territory claimed, no capturing moves)
|
| 727 |
+
// Optimization: Only check if board is reasonably full (> 50% coverage)
|
| 728 |
+
// because natural termination is unlikely on sparse boards
|
| 729 |
+
const board = state.getBoard();
|
| 730 |
+
const shape = state.getShape();
|
| 731 |
+
const totalPositions = shape.x * shape.y * shape.z;
|
| 732 |
+
|
| 733 |
+
// Count stones (cheap)
|
| 734 |
+
let stoneCount = 0;
|
| 735 |
+
|
| 736 |
+
for (let x = 0; x < shape.x; x++) {
|
| 737 |
+
for (let y = 0; y < shape.y; y++) {
|
| 738 |
+
for (let z = 0; z < shape.z; z++) {
|
| 739 |
+
const stone = board[x][y][z];
|
| 740 |
+
if (stone === 1 || stone === 2) { // StoneType.BLACK or WHITE
|
| 741 |
+
stoneCount++;
|
| 742 |
+
}
|
| 743 |
+
}
|
| 744 |
+
}
|
| 745 |
+
}
|
| 746 |
+
|
| 747 |
+
const coverageRatio = stoneCount / totalPositions;
|
| 748 |
+
|
| 749 |
+
// Only check for natural termination if board is reasonably full
|
| 750 |
+
if (coverageRatio > 0.5) {
|
| 751 |
+
if (state.isNaturallyTerminal()) {
|
| 752 |
+
const territory = state.getTerritory();
|
| 753 |
+
return this.calculateTerminalValue(territory);
|
| 754 |
+
}
|
| 755 |
+
}
|
| 756 |
+
|
| 757 |
+
|
| 758 |
+
return null; // Not terminal
|
| 759 |
+
}
|
| 760 |
+
|
| 761 |
+
|
| 762 |
+
/**
|
| 763 |
+
* Calculate terminal value from territory scores
|
| 764 |
+
* Uses logarithmic scaling matching the training code
|
| 765 |
+
*
|
| 766 |
+
* @param territory Territory counts from game
|
| 767 |
+
* @returns Value (white-positive: positive = white winning)
|
| 768 |
+
*/
|
| 769 |
+
private calculateTerminalValue(territory: { black: number; white: number; neutral: number }): number {
|
| 770 |
+
const scoreDiff = territory.white - territory.black;
|
| 771 |
+
|
| 772 |
+
if (Math.abs(scoreDiff) < 1e-6) {
|
| 773 |
+
// Draw/tie case
|
| 774 |
+
return 0.0;
|
| 775 |
+
}
|
| 776 |
+
|
| 777 |
+
// Match training formula from valueCausalLoss.py:_expand_value_targets
|
| 778 |
+
// target = sign(score) * (1 + log(|score|)) * territory_value_factor
|
| 779 |
+
// The log term incentivizes winning by larger margins (logarithmically)
|
| 780 |
+
const territory_value_factor = 1.0; // Default from training config
|
| 781 |
+
const signScore = Math.sign(scoreDiff);
|
| 782 |
+
return signScore * (1 + Math.log(Math.abs(scoreDiff))) * territory_value_factor;
|
| 783 |
+
}
|
| 784 |
+
|
| 785 |
+
|
| 786 |
+
/**
|
| 787 |
+
* Create a new MCTS node
|
| 788 |
+
*
|
| 789 |
+
* @param state Game state (only provided for root node, null for others to save memory)
|
| 790 |
+
* @param parent Parent node
|
| 791 |
+
* @param action Action that led to this node
|
| 792 |
+
* @param playerToMove Player to move at this node (derived from state if available)
|
| 793 |
+
*/
|
| 794 |
+
private createNode(state: TrigoGame | null, parent: MCTSNode | null, action: Move | null, playerToMove?: number): MCTSNode {
|
| 795 |
+
// Determine player to move
|
| 796 |
+
let player: number;
|
| 797 |
+
if (playerToMove !== undefined) {
|
| 798 |
+
player = playerToMove;
|
| 799 |
+
} else if (state) {
|
| 800 |
+
// Most reliable: derive from actual game state
|
| 801 |
+
player = state.getCurrentPlayer();
|
| 802 |
+
} else if (parent) {
|
| 803 |
+
// NOTE: Fallback assumes strictly alternating turns (no passes keeping same player)
|
| 804 |
+
// For standard Go-like games with strict alternation, this is safe.
|
| 805 |
+
player = parent.playerToMove === 1 ? 2 : 1;
|
| 806 |
+
} else {
|
| 807 |
+
// Default to Black for root if no info
|
| 808 |
+
player = 1;
|
| 809 |
+
}
|
| 810 |
+
|
| 811 |
+
return {
|
| 812 |
+
state,
|
| 813 |
+
parent,
|
| 814 |
+
action,
|
| 815 |
+
N: new Map(),
|
| 816 |
+
W: new Map(),
|
| 817 |
+
Q: new Map(),
|
| 818 |
+
P: new Map(),
|
| 819 |
+
children: new Map(),
|
| 820 |
+
expanded: false,
|
| 821 |
+
terminalValue: null,
|
| 822 |
+
depth: parent ? parent.depth + 1 : 0,
|
| 823 |
+
playerToMove: player
|
| 824 |
+
};
|
| 825 |
+
}
|
| 826 |
+
|
| 827 |
+
|
| 828 |
+
/**
|
| 829 |
+
* Encode move to string key for storage in maps
|
| 830 |
+
* Note: Only encodes position, player info is handled separately
|
| 831 |
+
*/
|
| 832 |
+
private encodeAction(move: Move): string {
|
| 833 |
+
if (move.isPass) {
|
| 834 |
+
return "pass";
|
| 835 |
+
}
|
| 836 |
+
return `${move.x},${move.y},${move.z}`;
|
| 837 |
+
}
|
| 838 |
+
|
| 839 |
+
|
| 840 |
+
/**
|
| 841 |
+
* Decode string key back to move
|
| 842 |
+
* Note: Returns move with placeholder player - caller must set correct player
|
| 843 |
+
* based on game state before using the move externally
|
| 844 |
+
*/
|
| 845 |
+
private decodeAction(key: string): Move {
|
| 846 |
+
if (key === "pass") {
|
| 847 |
+
// Player is placeholder - will be set by caller (selectMove sets it from game state)
|
| 848 |
+
return { player: "black", isPass: true };
|
| 849 |
+
}
|
| 850 |
+
|
| 851 |
+
const [x, y, z] = key.split(",").map(Number);
|
| 852 |
+
// Player is placeholder - will be set by caller (selectMove sets it from game state)
|
| 853 |
+
return { player: "black", x, y, z };
|
| 854 |
+
}
|
| 855 |
+
}
|
trigo-web/inc/modelInferencer.ts
CHANGED
|
@@ -6,6 +6,15 @@
|
|
| 6 |
*
|
| 7 |
* Adapted from Node.js test_inference.js for cross-platform use
|
| 8 |
* Provides causal language model inference using GPT-2 ONNX model
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 9 |
*/
|
| 10 |
|
| 11 |
/**
|
|
@@ -82,15 +91,20 @@ export class ModelInferencer {
|
|
| 82 |
private config: InferencerConfig;
|
| 83 |
private TensorClass: TensorConstructor;
|
| 84 |
|
| 85 |
-
// TGN tokenizer:
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 89 |
|
| 90 |
constructor(TensorClass: TensorConstructor, config: Partial<InferencerConfig> = {}) {
|
| 91 |
this.TensorClass = TensorClass;
|
| 92 |
this.config = {
|
| 93 |
-
vocabSize:
|
| 94 |
seqLen: 256,
|
| 95 |
...config
|
| 96 |
};
|
|
@@ -342,6 +356,47 @@ export class ModelInferencer {
|
|
| 342 |
}
|
| 343 |
|
| 344 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 345 |
/**
|
| 346 |
* Compute softmax for a single position's logits
|
| 347 |
* @param logits - Full logits array
|
|
|
|
| 6 |
*
|
| 7 |
* Adapted from Node.js test_inference.js for cross-platform use
|
| 8 |
* Provides causal language model inference using GPT-2 ONNX model
|
| 9 |
+
*
|
| 10 |
+
* Vocabulary Design (128 tokens):
|
| 11 |
+
* 0-3: Special tokens (PAD=0, START=1, END=2, VALUE=3)
|
| 12 |
+
* 4-7: Reserved for future use
|
| 13 |
+
* 10: LF (newline) for multi-line game records
|
| 14 |
+
* 32-127: ASCII printable characters (direct identity mapping)
|
| 15 |
+
*
|
| 16 |
+
* This design uses direct identity mapping: token_id = ascii_value
|
| 17 |
+
* No complex formulas needed - simple and efficient.
|
| 18 |
*/
|
| 19 |
|
| 20 |
/**
|
|
|
|
| 91 |
private config: InferencerConfig;
|
| 92 |
private TensorClass: TensorConstructor;
|
| 93 |
|
| 94 |
+
// TGN tokenizer: Compact 128-token vocabulary with direct ASCII mapping
|
| 95 |
+
// 0-3: Special tokens (PAD, START, END, VALUE)
|
| 96 |
+
// 4-7: Reserved for future use
|
| 97 |
+
// 10: Newline (LF)
|
| 98 |
+
// 32-127: ASCII printable characters (direct identity mapping)
|
| 99 |
+
private readonly PAD_TOKEN = 0;
|
| 100 |
+
private readonly START_TOKEN = 1;
|
| 101 |
+
private readonly END_TOKEN = 2;
|
| 102 |
+
private readonly VALUE_TOKEN = 3;
|
| 103 |
|
| 104 |
constructor(TensorClass: TensorConstructor, config: Partial<InferencerConfig> = {}) {
|
| 105 |
this.TensorClass = TensorClass;
|
| 106 |
this.config = {
|
| 107 |
+
vocabSize: config.vocabSize || 128, // Allow override via config
|
| 108 |
seqLen: 256,
|
| 109 |
...config
|
| 110 |
};
|
|
|
|
| 356 |
}
|
| 357 |
|
| 358 |
|
| 359 |
+
/**
|
| 360 |
+
* Run value prediction inference (for evaluation mode models)
|
| 361 |
+
* For models exported with --evaluation-mode flag
|
| 362 |
+
* @param tokens - Token IDs (already includes START/END tokens and padding)
|
| 363 |
+
* @returns Predicted game outcome value in range [-1, 1]
|
| 364 |
+
*/
|
| 365 |
+
async runValuePrediction(tokens: number[]): Promise<number> {
|
| 366 |
+
if (!this.session) {
|
| 367 |
+
throw new Error("Inferencer not initialized. Call setSession() first.");
|
| 368 |
+
}
|
| 369 |
+
|
| 370 |
+
const seqLen = tokens.length;
|
| 371 |
+
|
| 372 |
+
// Convert to BigInt64Array for ONNX int64 tensors
|
| 373 |
+
const inputIds = new BigInt64Array(seqLen);
|
| 374 |
+
for (let i = 0; i < seqLen; i++) {
|
| 375 |
+
inputIds[i] = BigInt(tokens[i]);
|
| 376 |
+
}
|
| 377 |
+
|
| 378 |
+
// Create input tensor [1, seq_len]
|
| 379 |
+
const inputTensor = new this.TensorClass("int64", inputIds, [1, seqLen]);
|
| 380 |
+
|
| 381 |
+
// Run inference
|
| 382 |
+
const results = await this.session.run({
|
| 383 |
+
input_ids: inputTensor
|
| 384 |
+
});
|
| 385 |
+
|
| 386 |
+
// Extract value
|
| 387 |
+
// Output shape: [batch_size] = [1]
|
| 388 |
+
// For evaluation models, output name is "values" not "logits"
|
| 389 |
+
const values = results.values;
|
| 390 |
+
if (!values) {
|
| 391 |
+
throw new Error("Evaluation model did not return 'values' output. Check model export.");
|
| 392 |
+
}
|
| 393 |
+
|
| 394 |
+
const predictedValue = values.data[0] as number;
|
| 395 |
+
|
| 396 |
+
return predictedValue;
|
| 397 |
+
}
|
| 398 |
+
|
| 399 |
+
|
| 400 |
/**
|
| 401 |
* Compute softmax for a single position's logits
|
| 402 |
* @param logits - Full logits array
|
trigo-web/inc/trigo/game.ts
CHANGED
|
@@ -105,9 +105,10 @@ export class TrigoGame {
|
|
| 105 |
// Last captured stones for Ko rule detection
|
| 106 |
private lastCapturedPositions: Position[] | null = null;
|
| 107 |
|
| 108 |
-
//
|
| 109 |
-
|
| 110 |
private cachedTerritory: TerritoryResult | null = null;
|
|
|
|
| 111 |
|
| 112 |
/**
|
| 113 |
* Constructor
|
|
@@ -152,13 +153,22 @@ export class TrigoGame {
|
|
| 152 |
this.stepHistory = [];
|
| 153 |
this.currentStepIndex = 0;
|
| 154 |
this.lastCapturedPositions = null;
|
| 155 |
-
this.
|
| 156 |
-
this.cachedTerritory = null;
|
| 157 |
this.gameStatus = "idle";
|
| 158 |
this.gameResult = undefined;
|
| 159 |
this.passCount = 0;
|
| 160 |
}
|
| 161 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 162 |
/**
|
| 163 |
* Clone the game state (deep copy)
|
| 164 |
* Creates an independent copy with all state preserved
|
|
@@ -197,9 +207,8 @@ export class TrigoGame {
|
|
| 197 |
};
|
| 198 |
}
|
| 199 |
|
| 200 |
-
//
|
| 201 |
-
cloned.
|
| 202 |
-
cloned.cachedTerritory = null;
|
| 203 |
|
| 204 |
return cloned;
|
| 205 |
}
|
|
@@ -386,6 +395,115 @@ export class TrigoGame {
|
|
| 386 |
return validPositions;
|
| 387 |
}
|
| 388 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 389 |
/**
|
| 390 |
* Reset pass count (called when a stone is placed)
|
| 391 |
*/
|
|
@@ -419,8 +537,8 @@ export class TrigoGame {
|
|
| 419 |
// Store captured positions for Ko rule
|
| 420 |
this.lastCapturedPositions = capturedPositions.length > 0 ? capturedPositions : null;
|
| 421 |
|
| 422 |
-
//
|
| 423 |
-
this.
|
| 424 |
|
| 425 |
// Reset pass count when a stone is placed
|
| 426 |
this.resetPassCount();
|
|
@@ -442,7 +560,7 @@ export class TrigoGame {
|
|
| 442 |
this.callbacks.onCapture(capturedPositions);
|
| 443 |
}
|
| 444 |
|
| 445 |
-
if (this.
|
| 446 |
this.callbacks.onTerritoryChange(this.getTerritory());
|
| 447 |
}
|
| 448 |
|
|
@@ -584,8 +702,8 @@ export class TrigoGame {
|
|
| 584 |
this.lastCapturedPositions = null;
|
| 585 |
}
|
| 586 |
|
| 587 |
-
//
|
| 588 |
-
this.
|
| 589 |
|
| 590 |
// Trigger callback
|
| 591 |
if (this.callbacks.onStepBack) {
|
|
@@ -631,8 +749,8 @@ export class TrigoGame {
|
|
| 631 |
this.currentStepIndex++;
|
| 632 |
this.currentPlayer = getEnemyColor(nextStep.player); // Switch to next player
|
| 633 |
|
| 634 |
-
//
|
| 635 |
-
this.
|
| 636 |
|
| 637 |
// Trigger callback
|
| 638 |
if (this.callbacks.onStepAdvance) {
|
|
@@ -718,8 +836,8 @@ export class TrigoGame {
|
|
| 718 |
// Recalculate pass count based on new history position
|
| 719 |
this.recalculatePassCount();
|
| 720 |
|
| 721 |
-
//
|
| 722 |
-
this.
|
| 723 |
|
| 724 |
// Trigger callback based on direction
|
| 725 |
if (index < oldStepIndex && this.callbacks.onStepBack) {
|
|
@@ -763,10 +881,9 @@ export class TrigoGame {
|
|
| 763 |
* Returns cached result if territory hasn't changed
|
| 764 |
*/
|
| 765 |
getTerritory(): TerritoryResult {
|
| 766 |
-
if (
|
| 767 |
this.cachedTerritory = calculateTerritory(this.board, this.shape);
|
| 768 |
-
|
| 769 |
-
}
|
| 770 |
return this.cachedTerritory;
|
| 771 |
}
|
| 772 |
|
|
@@ -842,8 +959,7 @@ export class TrigoGame {
|
|
| 842 |
this.lastCapturedPositions = null;
|
| 843 |
}
|
| 844 |
|
| 845 |
-
this.
|
| 846 |
-
this.cachedTerritory = null;
|
| 847 |
|
| 848 |
return true;
|
| 849 |
} catch (error) {
|
|
@@ -987,7 +1103,7 @@ export class TrigoGame {
|
|
| 987 |
timeControl?: string;
|
| 988 |
application?: string;
|
| 989 |
[key: string]: string | undefined;
|
| 990 |
-
}): string {
|
| 991 |
const lines: string[] = [];
|
| 992 |
|
| 993 |
// Add metadata tags
|
|
@@ -1020,7 +1136,7 @@ export class TrigoGame {
|
|
| 1020 |
resultStr += "Resign";
|
| 1021 |
}
|
| 1022 |
|
| 1023 |
-
lines.push(`[Result "${resultStr}"]`);
|
| 1024 |
}
|
| 1025 |
|
| 1026 |
// Add board size (without quotes - parser expects unquoted board shape)
|
|
@@ -1097,6 +1213,16 @@ export class TrigoGame {
|
|
| 1097 |
lines.push(currentLine);
|
| 1098 |
}
|
| 1099 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1100 |
// Add empty line at the end
|
| 1101 |
lines.push("");
|
| 1102 |
|
|
|
|
| 105 |
// Last captured stones for Ko rule detection
|
| 106 |
private lastCapturedPositions: Position[] | null = null;
|
| 107 |
|
| 108 |
+
// Static analysis cache (territory, capturing moves)
|
| 109 |
+
// Invalidated on any board state change
|
| 110 |
private cachedTerritory: TerritoryResult | null = null;
|
| 111 |
+
private cachedCapturingMove: Map<Stone, boolean> = new Map();
|
| 112 |
|
| 113 |
/**
|
| 114 |
* Constructor
|
|
|
|
| 153 |
this.stepHistory = [];
|
| 154 |
this.currentStepIndex = 0;
|
| 155 |
this.lastCapturedPositions = null;
|
| 156 |
+
this.invalidateAnalysisCache();
|
|
|
|
| 157 |
this.gameStatus = "idle";
|
| 158 |
this.gameResult = undefined;
|
| 159 |
this.passCount = 0;
|
| 160 |
}
|
| 161 |
|
| 162 |
+
|
| 163 |
+
/**
|
| 164 |
+
* Invalidate all static analysis caches
|
| 165 |
+
* Called when board state changes
|
| 166 |
+
*/
|
| 167 |
+
private invalidateAnalysisCache(): void {
|
| 168 |
+
this.cachedTerritory = null;
|
| 169 |
+
this.cachedCapturingMove.clear();
|
| 170 |
+
}
|
| 171 |
+
|
| 172 |
/**
|
| 173 |
* Clone the game state (deep copy)
|
| 174 |
* Creates an independent copy with all state preserved
|
|
|
|
| 207 |
};
|
| 208 |
}
|
| 209 |
|
| 210 |
+
// Analysis cache will be recalculated on demand
|
| 211 |
+
cloned.invalidateAnalysisCache();
|
|
|
|
| 212 |
|
| 213 |
return cloned;
|
| 214 |
}
|
|
|
|
| 395 |
return validPositions;
|
| 396 |
}
|
| 397 |
|
| 398 |
+
|
| 399 |
+
/**
|
| 400 |
+
* Check if any valid move can capture enemy stones
|
| 401 |
+
* Used by MCTS to determine if a position is truly terminal
|
| 402 |
+
*
|
| 403 |
+
* Results are cached and invalidated when board state changes.
|
| 404 |
+
*
|
| 405 |
+
* @param player - Optional player color (defaults to current player)
|
| 406 |
+
* @returns true if at least one valid move would capture stones
|
| 407 |
+
*/
|
| 408 |
+
hasCapturingMove(player?: Stone): boolean {
|
| 409 |
+
const playerColor = player ?? this.currentPlayer;
|
| 410 |
+
|
| 411 |
+
// Check cache first
|
| 412 |
+
if (this.cachedCapturingMove.has(playerColor)) {
|
| 413 |
+
return this.cachedCapturingMove.get(playerColor)!;
|
| 414 |
+
}
|
| 415 |
+
|
| 416 |
+
// Compute and cache the result
|
| 417 |
+
const result = this.computeHasCapturingMove(playerColor);
|
| 418 |
+
this.cachedCapturingMove.set(playerColor, result);
|
| 419 |
+
|
| 420 |
+
return result;
|
| 421 |
+
}
|
| 422 |
+
|
| 423 |
+
|
| 424 |
+
/**
|
| 425 |
+
* Internal: Compute whether a player has any capturing move
|
| 426 |
+
*/
|
| 427 |
+
private computeHasCapturingMove(playerColor: Stone): boolean {
|
| 428 |
+
// Iterate through all board positions
|
| 429 |
+
for (let x = 0; x < this.shape.x; x++) {
|
| 430 |
+
for (let y = 0; y < this.shape.y; y++) {
|
| 431 |
+
for (let z = 0; z < this.shape.z; z++) {
|
| 432 |
+
// Skip occupied positions
|
| 433 |
+
if (this.board[x][y][z] !== StoneType.EMPTY) {
|
| 434 |
+
continue;
|
| 435 |
+
}
|
| 436 |
+
|
| 437 |
+
const pos: Position = { x, y, z };
|
| 438 |
+
|
| 439 |
+
// Skip invalid moves (Ko, suicide)
|
| 440 |
+
if (
|
| 441 |
+
isKoViolation(
|
| 442 |
+
pos,
|
| 443 |
+
playerColor,
|
| 444 |
+
this.board,
|
| 445 |
+
this.shape,
|
| 446 |
+
this.lastCapturedPositions
|
| 447 |
+
)
|
| 448 |
+
) {
|
| 449 |
+
continue;
|
| 450 |
+
}
|
| 451 |
+
|
| 452 |
+
if (isSuicideMove(pos, playerColor, this.board, this.shape)) {
|
| 453 |
+
continue;
|
| 454 |
+
}
|
| 455 |
+
|
| 456 |
+
// Check if this move would capture any stones
|
| 457 |
+
const capturedGroups = findCapturedGroups(pos, playerColor, this.board, this.shape);
|
| 458 |
+
if (capturedGroups.length > 0) {
|
| 459 |
+
return true; // Found a capturing move
|
| 460 |
+
}
|
| 461 |
+
}
|
| 462 |
+
}
|
| 463 |
+
}
|
| 464 |
+
|
| 465 |
+
return false; // No capturing moves available
|
| 466 |
+
}
|
| 467 |
+
|
| 468 |
+
|
| 469 |
+
/**
|
| 470 |
+
* Check if the game has reached a natural terminal state
|
| 471 |
+
*
|
| 472 |
+
* A game is naturally terminal when:
|
| 473 |
+
* - All territory is claimed (neutral === 0)
|
| 474 |
+
* - AND neither player has any capturing moves available
|
| 475 |
+
*
|
| 476 |
+
* This is different from the "finished" status which requires double pass.
|
| 477 |
+
* Natural termination means the game state is completely settled and
|
| 478 |
+
* no further moves can meaningfully change the outcome.
|
| 479 |
+
*
|
| 480 |
+
* NOTE: This method is expensive due to territory calculation and capture move checking.
|
| 481 |
+
* Use coverage ratio check as a pre-filter when calling frequently.
|
| 482 |
+
*
|
| 483 |
+
* Used by:
|
| 484 |
+
* - MCTS agent (terminal detection for tree search)
|
| 485 |
+
* - Model battle (early stopping when settled)
|
| 486 |
+
* - Self-play games (early stopping when settled)
|
| 487 |
+
*
|
| 488 |
+
* @returns true if the game is naturally terminal, false otherwise
|
| 489 |
+
*/
|
| 490 |
+
isNaturallyTerminal(): boolean {
|
| 491 |
+
// Check if all territory is claimed (uses cached territory)
|
| 492 |
+
const territory = this.getTerritory();
|
| 493 |
+
if (territory.neutral !== 0) {
|
| 494 |
+
return false; // Still has neutral territory - not terminal
|
| 495 |
+
}
|
| 496 |
+
|
| 497 |
+
// All territory claimed - check if capturing moves are possible
|
| 498 |
+
// IMPORTANT: Check even if one player has no stones - they might still capture!
|
| 499 |
+
const blackCanCapture = this.hasCapturingMove(StoneType.BLACK);
|
| 500 |
+
const whiteCanCapture = this.hasCapturingMove(StoneType.WHITE);
|
| 501 |
+
|
| 502 |
+
// Terminal only if neither player can capture
|
| 503 |
+
return !blackCanCapture && !whiteCanCapture;
|
| 504 |
+
}
|
| 505 |
+
|
| 506 |
+
|
| 507 |
/**
|
| 508 |
* Reset pass count (called when a stone is placed)
|
| 509 |
*/
|
|
|
|
| 537 |
// Store captured positions for Ko rule
|
| 538 |
this.lastCapturedPositions = capturedPositions.length > 0 ? capturedPositions : null;
|
| 539 |
|
| 540 |
+
// Invalidate analysis cache
|
| 541 |
+
this.invalidateAnalysisCache();
|
| 542 |
|
| 543 |
// Reset pass count when a stone is placed
|
| 544 |
this.resetPassCount();
|
|
|
|
| 560 |
this.callbacks.onCapture(capturedPositions);
|
| 561 |
}
|
| 562 |
|
| 563 |
+
if (this.callbacks.onTerritoryChange) {
|
| 564 |
this.callbacks.onTerritoryChange(this.getTerritory());
|
| 565 |
}
|
| 566 |
|
|
|
|
| 702 |
this.lastCapturedPositions = null;
|
| 703 |
}
|
| 704 |
|
| 705 |
+
// Invalidate analysis cache
|
| 706 |
+
this.invalidateAnalysisCache();
|
| 707 |
|
| 708 |
// Trigger callback
|
| 709 |
if (this.callbacks.onStepBack) {
|
|
|
|
| 749 |
this.currentStepIndex++;
|
| 750 |
this.currentPlayer = getEnemyColor(nextStep.player); // Switch to next player
|
| 751 |
|
| 752 |
+
// Invalidate analysis cache
|
| 753 |
+
this.invalidateAnalysisCache();
|
| 754 |
|
| 755 |
// Trigger callback
|
| 756 |
if (this.callbacks.onStepAdvance) {
|
|
|
|
| 836 |
// Recalculate pass count based on new history position
|
| 837 |
this.recalculatePassCount();
|
| 838 |
|
| 839 |
+
// Invalidate analysis cache
|
| 840 |
+
this.invalidateAnalysisCache();
|
| 841 |
|
| 842 |
// Trigger callback based on direction
|
| 843 |
if (index < oldStepIndex && this.callbacks.onStepBack) {
|
|
|
|
| 881 |
* Returns cached result if territory hasn't changed
|
| 882 |
*/
|
| 883 |
getTerritory(): TerritoryResult {
|
| 884 |
+
if (!this.cachedTerritory)
|
| 885 |
this.cachedTerritory = calculateTerritory(this.board, this.shape);
|
| 886 |
+
|
|
|
|
| 887 |
return this.cachedTerritory;
|
| 888 |
}
|
| 889 |
|
|
|
|
| 959 |
this.lastCapturedPositions = null;
|
| 960 |
}
|
| 961 |
|
| 962 |
+
this.invalidateAnalysisCache();
|
|
|
|
| 963 |
|
| 964 |
return true;
|
| 965 |
} catch (error) {
|
|
|
|
| 1103 |
timeControl?: string;
|
| 1104 |
application?: string;
|
| 1105 |
[key: string]: string | undefined;
|
| 1106 |
+
}, {markResult}: {markResult?: boolean} = {}): string {
|
| 1107 |
const lines: string[] = [];
|
| 1108 |
|
| 1109 |
// Add metadata tags
|
|
|
|
| 1136 |
resultStr += "Resign";
|
| 1137 |
}
|
| 1138 |
|
| 1139 |
+
//lines.push(`[Result "${resultStr}"]`);
|
| 1140 |
}
|
| 1141 |
|
| 1142 |
// Add board size (without quotes - parser expects unquoted board shape)
|
|
|
|
| 1213 |
lines.push(currentLine);
|
| 1214 |
}
|
| 1215 |
|
| 1216 |
+
// Add score result if game has finished
|
| 1217 |
+
if (markResult) {
|
| 1218 |
+
// Calculate territory to get final score
|
| 1219 |
+
const territory = this.getTerritory();
|
| 1220 |
+
const scoreDiff = territory.black - territory.white;
|
| 1221 |
+
|
| 1222 |
+
// Add score comment: negative means black wins, positive means white wins
|
| 1223 |
+
lines.push(`; ${scoreDiff > 0 ? '-' : scoreDiff < 0 ? '+' : ''}${Math.abs(scoreDiff)}`);
|
| 1224 |
+
}
|
| 1225 |
+
|
| 1226 |
// Add empty line at the end
|
| 1227 |
lines.push("");
|
| 1228 |
|
trigo-web/inc/trigoAgent.ts
CHANGED
|
@@ -61,7 +61,9 @@ export class TrigoAgent {
|
|
| 61 |
}
|
| 62 |
|
| 63 |
/**
|
| 64 |
-
* Convert string to token IDs (byte-level encoding)
|
|
|
|
|
|
|
| 65 |
*/
|
| 66 |
private stringToTokens(text: string): number[] {
|
| 67 |
return Array.from(text).map((char) => char.charCodeAt(0));
|
|
|
|
| 61 |
}
|
| 62 |
|
| 63 |
/**
|
| 64 |
+
* Convert string to token IDs (byte-level encoding with ASCII direct mapping)
|
| 65 |
+
* For characters 32-127, token_id = ascii_value
|
| 66 |
+
* Special tokens (0-3) and newline (10) are handled by the tokenizer
|
| 67 |
*/
|
| 68 |
private stringToTokens(text: string): number[] {
|
| 69 |
return Array.from(text).map((char) => char.charCodeAt(0));
|
trigo-web/inc/trigoEvaluationAgent.ts
ADDED
|
@@ -0,0 +1,266 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Trigo Evaluation Agent - AI agent using value prediction for position evaluation
|
| 3 |
+
*
|
| 4 |
+
* Uses evaluation mode ONNX model to predict game outcomes.
|
| 5 |
+
* The model takes a TGN sequence and returns a value in [-1, 1]:
|
| 6 |
+
* - Positive values favor White (second player)
|
| 7 |
+
* - Negative values favor Black (first player)
|
| 8 |
+
* - Values near ±1 indicate strong advantage
|
| 9 |
+
* - Values near 0 indicate balanced position
|
| 10 |
+
*/
|
| 11 |
+
|
| 12 |
+
import { ModelInferencer } from "./modelInferencer";
|
| 13 |
+
import { TrigoGame, StoneType } from "./trigo/game";
|
| 14 |
+
import type { Move, Stone, Position } from "./trigo/types";
|
| 15 |
+
import { encodeAb0yz } from "./trigo/ab0yz";
|
| 16 |
+
|
| 17 |
+
export interface ValuedMove {
|
| 18 |
+
move: Move;
|
| 19 |
+
value: number; // Position value in [-1, 1] after this move
|
| 20 |
+
notation: string; // TGN notation
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
export interface PositionEvaluation {
|
| 24 |
+
value: number; // Current position value
|
| 25 |
+
interpretation: string; // Human-readable interpretation
|
| 26 |
+
}
|
| 27 |
+
|
| 28 |
+
export class TrigoEvaluationAgent {
|
| 29 |
+
private inferencer: ModelInferencer;
|
| 30 |
+
private readonly START_TOKEN = 1;
|
| 31 |
+
private readonly END_TOKEN = 2;
|
| 32 |
+
private readonly VALUE_TOKEN = 3;
|
| 33 |
+
|
| 34 |
+
constructor(inferencer: ModelInferencer) {
|
| 35 |
+
this.inferencer = inferencer;
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
/**
|
| 39 |
+
* Check if agent is initialized (checks if inferencer has a session)
|
| 40 |
+
*/
|
| 41 |
+
isInitialized(): boolean {
|
| 42 |
+
return this.inferencer !== null && this.inferencer.isReady();
|
| 43 |
+
}
|
| 44 |
+
|
| 45 |
+
/**
|
| 46 |
+
* Convert Stone type to player string
|
| 47 |
+
*/
|
| 48 |
+
private stoneToPlayer(stone: Stone): "black" | "white" {
|
| 49 |
+
if (stone === StoneType.BLACK) return "black";
|
| 50 |
+
if (stone === StoneType.WHITE) return "white";
|
| 51 |
+
throw new Error(`Invalid stone type: ${stone}`);
|
| 52 |
+
}
|
| 53 |
+
|
| 54 |
+
/**
|
| 55 |
+
* Encode a position to TGN notation
|
| 56 |
+
*/
|
| 57 |
+
private positionToTGN(pos: Position, shape: { x: number; y: number; z: number }): string {
|
| 58 |
+
const posArray = [pos.x, pos.y, pos.z];
|
| 59 |
+
const shapeArray = [shape.x, shape.y, shape.z];
|
| 60 |
+
return encodeAb0yz(posArray, shapeArray);
|
| 61 |
+
}
|
| 62 |
+
|
| 63 |
+
/**
|
| 64 |
+
* Convert string to byte tokens (ASCII encoding)
|
| 65 |
+
*/
|
| 66 |
+
private stringToTokens(str: string): number[] {
|
| 67 |
+
const tokens: number[] = [];
|
| 68 |
+
for (let i = 0; i < str.length; i++) {
|
| 69 |
+
const charCode = str.charCodeAt(i);
|
| 70 |
+
// Only accept valid ASCII tokens (0-127)
|
| 71 |
+
if (charCode < 128) {
|
| 72 |
+
tokens.push(charCode);
|
| 73 |
+
}
|
| 74 |
+
}
|
| 75 |
+
return tokens;
|
| 76 |
+
}
|
| 77 |
+
|
| 78 |
+
/**
|
| 79 |
+
* Tokenize TGN text with special tokens
|
| 80 |
+
* Adds START and END tokens for proper sequence formatting
|
| 81 |
+
*/
|
| 82 |
+
private tokenizeTGN(tgnText: string, maxLength: number = 256): number[] {
|
| 83 |
+
// Convert TGN to tokens
|
| 84 |
+
const contentTokens = this.stringToTokens(tgnText);
|
| 85 |
+
|
| 86 |
+
// Add special tokens: START + content + END
|
| 87 |
+
const tokens = [this.START_TOKEN, ...contentTokens, this.END_TOKEN];
|
| 88 |
+
|
| 89 |
+
// Truncate if needed (preserve END token)
|
| 90 |
+
if (tokens.length > maxLength) {
|
| 91 |
+
return [...tokens.slice(0, maxLength - 1), this.END_TOKEN];
|
| 92 |
+
}
|
| 93 |
+
|
| 94 |
+
// Pad with PAD tokens (0) to fixed length
|
| 95 |
+
while (tokens.length < maxLength) {
|
| 96 |
+
tokens.push(0); // PAD_TOKEN
|
| 97 |
+
}
|
| 98 |
+
|
| 99 |
+
return tokens;
|
| 100 |
+
}
|
| 101 |
+
|
| 102 |
+
/**
|
| 103 |
+
* Evaluate a position by predicting the game outcome value
|
| 104 |
+
* @param game - The game to evaluate
|
| 105 |
+
* @returns Position evaluation with value and interpretation
|
| 106 |
+
*/
|
| 107 |
+
async evaluatePosition(game: TrigoGame): Promise<PositionEvaluation> {
|
| 108 |
+
if (!this.isInitialized()) {
|
| 109 |
+
throw new Error("Agent not initialized. Inferencer must have a session.");
|
| 110 |
+
}
|
| 111 |
+
|
| 112 |
+
// Get current TGN
|
| 113 |
+
const tgn = game.toTGN().trim();
|
| 114 |
+
|
| 115 |
+
// Tokenize
|
| 116 |
+
const config = this.inferencer.getConfig();
|
| 117 |
+
const tokens = this.tokenizeTGN(tgn, config.seqLen);
|
| 118 |
+
|
| 119 |
+
// Run value prediction inference
|
| 120 |
+
const value = await this.inferencer.runValuePrediction(tokens);
|
| 121 |
+
|
| 122 |
+
return {
|
| 123 |
+
value,
|
| 124 |
+
interpretation: this.interpretValue(value)
|
| 125 |
+
};
|
| 126 |
+
}
|
| 127 |
+
|
| 128 |
+
/**
|
| 129 |
+
* Interpret a position value to human-readable text
|
| 130 |
+
*/
|
| 131 |
+
private interpretValue(value: number): string {
|
| 132 |
+
if (value > 0.5) {
|
| 133 |
+
return `Strong advantage for White (+${value.toFixed(3)})`;
|
| 134 |
+
} else if (value > 0.1) {
|
| 135 |
+
return `Slight advantage for White (+${value.toFixed(3)})`;
|
| 136 |
+
} else if (value > -0.1) {
|
| 137 |
+
return `Balanced position (${value.toFixed(3)})`;
|
| 138 |
+
} else if (value > -0.5) {
|
| 139 |
+
return `Slight advantage for Black (${value.toFixed(3)})`;
|
| 140 |
+
} else {
|
| 141 |
+
return `Strong advantage for Black (${value.toFixed(3)})`;
|
| 142 |
+
}
|
| 143 |
+
}
|
| 144 |
+
|
| 145 |
+
/**
|
| 146 |
+
* Evaluate all valid moves and return them sorted by value
|
| 147 |
+
* @param game - Current game state
|
| 148 |
+
* @returns Array of moves with their position values
|
| 149 |
+
*/
|
| 150 |
+
async evaluateMoves(game: TrigoGame): Promise<ValuedMove[]> {
|
| 151 |
+
if (!this.isInitialized()) {
|
| 152 |
+
throw new Error("Agent not initialized");
|
| 153 |
+
}
|
| 154 |
+
|
| 155 |
+
const currentPlayer = this.stoneToPlayer(game.getCurrentPlayer());
|
| 156 |
+
|
| 157 |
+
// Get all valid moves
|
| 158 |
+
const validMoves: Move[] = game.validMovePositions().map((pos) => ({
|
| 159 |
+
x: pos.x,
|
| 160 |
+
y: pos.y,
|
| 161 |
+
z: pos.z,
|
| 162 |
+
player: currentPlayer
|
| 163 |
+
}));
|
| 164 |
+
validMoves.push({ player: currentPlayer, isPass: true }); // Add pass move
|
| 165 |
+
|
| 166 |
+
if (validMoves.length === 0) {
|
| 167 |
+
return [];
|
| 168 |
+
}
|
| 169 |
+
|
| 170 |
+
console.log(`[TrigoEvaluationAgent] Evaluating ${validMoves.length} moves...`);
|
| 171 |
+
|
| 172 |
+
// Evaluate each move by simulating it and evaluating the resulting position
|
| 173 |
+
const valuedMoves: ValuedMove[] = [];
|
| 174 |
+
|
| 175 |
+
for (const move of validMoves) {
|
| 176 |
+
// Clone game and apply move
|
| 177 |
+
const testGame = game.clone();
|
| 178 |
+
let success: boolean;
|
| 179 |
+
|
| 180 |
+
if (move.isPass) {
|
| 181 |
+
success = testGame.pass();
|
| 182 |
+
} else if (move.x !== undefined && move.y !== undefined && move.z !== undefined) {
|
| 183 |
+
success = testGame.drop({ x: move.x, y: move.y, z: move.z });
|
| 184 |
+
} else {
|
| 185 |
+
continue; // Invalid move format
|
| 186 |
+
}
|
| 187 |
+
|
| 188 |
+
if (!success) {
|
| 189 |
+
continue; // Move failed
|
| 190 |
+
}
|
| 191 |
+
|
| 192 |
+
// Evaluate the resulting position
|
| 193 |
+
const evaluation = await this.evaluatePosition(testGame);
|
| 194 |
+
|
| 195 |
+
// Get move notation
|
| 196 |
+
const notation = move.isPass
|
| 197 |
+
? "P"
|
| 198 |
+
: this.positionToTGN(
|
| 199 |
+
{ x: move.x!, y: move.y!, z: move.z! },
|
| 200 |
+
testGame.getShape()
|
| 201 |
+
);
|
| 202 |
+
|
| 203 |
+
// Model returns white-positive values (positive = white advantage)
|
| 204 |
+
// Adjust to current player perspective: positive = advantage for current player
|
| 205 |
+
const adjustedValue = currentPlayer === "white" ? evaluation.value : -evaluation.value;
|
| 206 |
+
|
| 207 |
+
valuedMoves.push({
|
| 208 |
+
move,
|
| 209 |
+
value: adjustedValue,
|
| 210 |
+
notation
|
| 211 |
+
});
|
| 212 |
+
}
|
| 213 |
+
|
| 214 |
+
// Sort by value (highest first - best for current player)
|
| 215 |
+
valuedMoves.sort((a, b) => b.value - a.value);
|
| 216 |
+
|
| 217 |
+
return valuedMoves;
|
| 218 |
+
}
|
| 219 |
+
|
| 220 |
+
/**
|
| 221 |
+
* Select the best move based on position evaluation
|
| 222 |
+
* @param game - Current game state
|
| 223 |
+
* @returns Best move or null if no moves available
|
| 224 |
+
*/
|
| 225 |
+
async selectBestMove(game: TrigoGame): Promise<Move | null> {
|
| 226 |
+
const valuedMoves = await this.evaluateMoves(game);
|
| 227 |
+
|
| 228 |
+
if (valuedMoves.length === 0) {
|
| 229 |
+
return null;
|
| 230 |
+
}
|
| 231 |
+
|
| 232 |
+
console.log(`[TrigoEvaluationAgent] Best move: ${valuedMoves[0].notation} (value: ${valuedMoves[0].value.toFixed(4)})`);
|
| 233 |
+
console.log(`[TrigoEvaluationAgent] Top 3 moves:`);
|
| 234 |
+
for (let i = 0; i < Math.min(3, valuedMoves.length); i++) {
|
| 235 |
+
console.log(` ${i + 1}. ${valuedMoves[i].notation}: ${valuedMoves[i].value.toFixed(4)}`);
|
| 236 |
+
}
|
| 237 |
+
|
| 238 |
+
return valuedMoves[0].move;
|
| 239 |
+
}
|
| 240 |
+
|
| 241 |
+
/**
|
| 242 |
+
* Get detailed evaluation report for current position
|
| 243 |
+
*/
|
| 244 |
+
async getEvaluationReport(game: TrigoGame): Promise<{
|
| 245 |
+
currentValue: PositionEvaluation;
|
| 246 |
+
topMoves: ValuedMove[];
|
| 247 |
+
moveCount: number;
|
| 248 |
+
}> {
|
| 249 |
+
const currentEval = await this.evaluatePosition(game);
|
| 250 |
+
const valuedMoves = await this.evaluateMoves(game);
|
| 251 |
+
|
| 252 |
+
return {
|
| 253 |
+
currentValue: currentEval,
|
| 254 |
+
topMoves: valuedMoves.slice(0, 5), // Top 5 moves
|
| 255 |
+
moveCount: valuedMoves.length
|
| 256 |
+
};
|
| 257 |
+
}
|
| 258 |
+
|
| 259 |
+
/**
|
| 260 |
+
* Clean up resources
|
| 261 |
+
*/
|
| 262 |
+
destroy(): void {
|
| 263 |
+
// Agent doesn't own the inferencer, so just clear reference
|
| 264 |
+
console.log("[TrigoEvaluationAgent] Destroyed");
|
| 265 |
+
}
|
| 266 |
+
}
|
trigo-web/inc/trigoTreeAgent.ts
CHANGED
|
@@ -20,6 +20,9 @@ export interface ScoredMove {
|
|
| 20 |
export class TrigoTreeAgent {
|
| 21 |
private inferencer: ModelInferencer;
|
| 22 |
|
|
|
|
|
|
|
|
|
|
| 23 |
constructor(inferencer: ModelInferencer) {
|
| 24 |
this.inferencer = inferencer;
|
| 25 |
}
|
|
@@ -78,6 +81,7 @@ export class TrigoTreeAgent {
|
|
| 78 |
evaluatedIds: number[];
|
| 79 |
mask: number[];
|
| 80 |
moveToLeafPos: number[];
|
|
|
|
| 81 |
} {
|
| 82 |
type Seq = { moveIndex: number; tokens: number[] };
|
| 83 |
|
|
@@ -154,6 +158,9 @@ export class TrigoTreeAgent {
|
|
| 154 |
}
|
| 155 |
for (const r of roots) dfs(r);
|
| 156 |
|
|
|
|
|
|
|
|
|
|
| 157 |
// --- Build ancestor mask ---
|
| 158 |
const mask = new Array(total * total).fill(0);
|
| 159 |
for (let i = 0; i < total; i++) {
|
|
@@ -164,7 +171,7 @@ export class TrigoTreeAgent {
|
|
| 164 |
}
|
| 165 |
}
|
| 166 |
|
| 167 |
-
return { evaluatedIds, mask, moveToLeafPos };
|
| 168 |
}
|
| 169 |
|
| 170 |
/**
|
|
@@ -178,7 +185,8 @@ export class TrigoTreeAgent {
|
|
| 178 |
prefixTokens: number[];
|
| 179 |
evaluatedIds: number[];
|
| 180 |
mask: number[];
|
| 181 |
-
|
|
|
|
| 182 |
} {
|
| 183 |
// Get current TGN as prefix
|
| 184 |
const currentTGN = game.toTGN().trim();
|
|
@@ -206,7 +214,7 @@ export class TrigoTreeAgent {
|
|
| 206 |
prefix = currentTGN + " ";
|
| 207 |
}
|
| 208 |
|
| 209 |
-
const prefixTokens = this.stringToTokens(prefix);
|
| 210 |
|
| 211 |
// Encode each move to tokens (only first 2 tokens)
|
| 212 |
const shape = game.getShape();
|
|
@@ -221,6 +229,8 @@ export class TrigoTreeAgent {
|
|
| 221 |
}
|
| 222 |
|
| 223 |
// Exclude the last token
|
|
|
|
|
|
|
| 224 |
const fullTokens = this.stringToTokens(notation);
|
| 225 |
const tokens = fullTokens.slice(0, fullTokens.length - 1);
|
| 226 |
|
|
@@ -229,38 +239,19 @@ export class TrigoTreeAgent {
|
|
| 229 |
|
| 230 |
// Build prefix tree
|
| 231 |
const tokenArrays = movesWithTokens.map((m) => m.tokens);
|
| 232 |
-
const { evaluatedIds, mask, moveToLeafPos } = this.buildPrefixTree(tokenArrays);
|
| 233 |
|
| 234 |
-
// Build move data with leaf positions
|
| 235 |
const moveData = movesWithTokens.map((m, index) => {
|
| 236 |
const leafPos = moveToLeafPos[index];
|
| 237 |
-
// Find parent position (root position for this move)
|
| 238 |
-
// Parent is the first token position
|
| 239 |
-
const firstToken = m.tokens[0];
|
| 240 |
-
let parentPos = -1;
|
| 241 |
-
for (let i = 0; i < evaluatedIds.length; i++) {
|
| 242 |
-
if (evaluatedIds[i] === firstToken && i < leafPos) {
|
| 243 |
-
// This is a potential parent
|
| 244 |
-
// Check if it's in the same branch by checking mask
|
| 245 |
-
// If leafPos can see position i, then i might be the parent
|
| 246 |
-
if (mask[leafPos * evaluatedIds.length + i] === 1.0 && i !== leafPos) {
|
| 247 |
-
// Find the closest parent (maximum index less than leafPos that leaf can see)
|
| 248 |
-
if (i > parentPos) {
|
| 249 |
-
parentPos = i;
|
| 250 |
-
}
|
| 251 |
-
}
|
| 252 |
-
}
|
| 253 |
-
}
|
| 254 |
-
|
| 255 |
return {
|
| 256 |
move: m.move,
|
| 257 |
notation: m.notation,
|
| 258 |
-
leafPos
|
| 259 |
-
parentPos
|
| 260 |
};
|
| 261 |
});
|
| 262 |
|
| 263 |
-
return { prefixTokens, evaluatedIds, mask, moveData };
|
| 264 |
}
|
| 265 |
|
| 266 |
/**
|
|
@@ -272,16 +263,19 @@ export class TrigoTreeAgent {
|
|
| 272 |
): {
|
| 273 |
evaluatedIds: number[];
|
| 274 |
mask: number[];
|
| 275 |
-
|
|
|
|
| 276 |
} {
|
| 277 |
return this.buildMoveTree(game, moves);
|
| 278 |
}
|
| 279 |
|
| 280 |
/**
|
| 281 |
-
* Select
|
| 282 |
-
*
|
|
|
|
|
|
|
| 283 |
*/
|
| 284 |
-
async
|
| 285 |
if (!this.inferencer.isReady()) {
|
| 286 |
throw new Error("Inferencer not initialized");
|
| 287 |
}
|
|
@@ -289,29 +283,76 @@ export class TrigoTreeAgent {
|
|
| 289 |
// Get current player as string
|
| 290 |
const currentPlayer = this.stoneToPlayer(game.getCurrentPlayer());
|
| 291 |
|
| 292 |
-
// Get all valid moves
|
| 293 |
const validMoves: Move[] = game.validMovePositions().map((pos) => ({
|
| 294 |
x: pos.x,
|
| 295 |
y: pos.y,
|
| 296 |
z: pos.z,
|
| 297 |
player: currentPlayer
|
| 298 |
}));
|
| 299 |
-
validMoves.push({ player: currentPlayer, isPass: true }); // Add pass move
|
| 300 |
|
|
|
|
| 301 |
if (validMoves.length === 0) {
|
| 302 |
-
return
|
| 303 |
}
|
| 304 |
|
| 305 |
-
// Score
|
| 306 |
const scoredMoves = await this.scoreMoves(game, validMoves);
|
| 307 |
|
| 308 |
-
//
|
| 309 |
if (scoredMoves.length === 0) {
|
| 310 |
-
return
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 311 |
}
|
| 312 |
|
| 313 |
-
|
| 314 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 315 |
}
|
| 316 |
|
| 317 |
/**
|
|
@@ -323,13 +364,13 @@ export class TrigoTreeAgent {
|
|
| 323 |
}
|
| 324 |
|
| 325 |
// Build tree structure
|
| 326 |
-
const { prefixTokens, evaluatedIds, mask, moveData } = this.buildMoveTree(game, moves);
|
| 327 |
|
| 328 |
-
console.debug(`Tree structure: ${evaluatedIds.length} nodes for ${moveData.length} moves`);
|
| 329 |
-
console.debug(`Evaluated IDs:`, evaluatedIds.map((id) => String.fromCharCode(id)).join(""));
|
| 330 |
//console.debug(
|
| 331 |
// `Move positions:`,
|
| 332 |
-
// moveData.map((m) => `${m.notation}@${m.leafPos}
|
| 333 |
//);
|
| 334 |
|
| 335 |
// Prepare inputs for evaluation
|
|
@@ -343,10 +384,14 @@ export class TrigoTreeAgent {
|
|
| 343 |
const output = await this.inferencer.runEvaluationInference(inputs);
|
| 344 |
const { logits, numEvaluated } = output;
|
| 345 |
|
| 346 |
-
console.debug(`Inference output: ${numEvaluated} evaluated positions`);
|
|
|
|
|
|
|
|
|
|
|
|
|
| 347 |
|
| 348 |
-
// Score each move by accumulating log probabilities
|
| 349 |
-
// For each move,
|
| 350 |
const scoredMoves: ScoredMove[] = [];
|
| 351 |
|
| 352 |
// Cache softmax results for each output position to avoid recomputation
|
|
@@ -361,46 +406,122 @@ export class TrigoTreeAgent {
|
|
| 361 |
for (const data of moveData) {
|
| 362 |
let logProb = 0;
|
| 363 |
|
| 364 |
-
//
|
| 365 |
-
//
|
| 366 |
-
|
| 367 |
-
|
| 368 |
-
|
| 369 |
-
|
| 370 |
-
|
| 371 |
-
|
| 372 |
-
|
| 373 |
-
|
| 374 |
-
|
| 375 |
-
|
|
|
|
| 376 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 377 |
}
|
| 378 |
-
//console.debug("path:", data.notation, "->", path);
|
| 379 |
|
| 380 |
-
//
|
| 381 |
-
|
| 382 |
-
|
| 383 |
-
|
| 384 |
-
|
| 385 |
-
|
| 386 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 387 |
|
| 388 |
-
//
|
| 389 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 390 |
|
| 391 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 392 |
|
| 393 |
-
|
| 394 |
-
|
| 395 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 396 |
|
| 397 |
-
|
| 398 |
-
|
| 399 |
-
|
| 400 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 401 |
}
|
| 402 |
-
else
|
| 403 |
-
logProb += -100;
|
| 404 |
}
|
| 405 |
|
| 406 |
scoredMoves.push({
|
|
|
|
| 20 |
export class TrigoTreeAgent {
|
| 21 |
private inferencer: ModelInferencer;
|
| 22 |
|
| 23 |
+
// Special token constants (must match TGN tokenizer)
|
| 24 |
+
private readonly START_TOKEN = 1;
|
| 25 |
+
|
| 26 |
constructor(inferencer: ModelInferencer) {
|
| 27 |
this.inferencer = inferencer;
|
| 28 |
}
|
|
|
|
| 81 |
evaluatedIds: number[];
|
| 82 |
mask: number[];
|
| 83 |
moveToLeafPos: number[];
|
| 84 |
+
parent: Array<number | null>;
|
| 85 |
} {
|
| 86 |
type Seq = { moveIndex: number; tokens: number[] };
|
| 87 |
|
|
|
|
| 158 |
}
|
| 159 |
for (const r of roots) dfs(r);
|
| 160 |
|
| 161 |
+
// NOTE: moveToLeafPos[i] = -1 means the move has empty tokens (e.g., single-char notation)
|
| 162 |
+
// In this case, we use prefix logits directly for scoring (valid behavior)
|
| 163 |
+
|
| 164 |
// --- Build ancestor mask ---
|
| 165 |
const mask = new Array(total * total).fill(0);
|
| 166 |
for (let i = 0; i < total; i++) {
|
|
|
|
| 171 |
}
|
| 172 |
}
|
| 173 |
|
| 174 |
+
return { evaluatedIds, mask, moveToLeafPos, parent };
|
| 175 |
}
|
| 176 |
|
| 177 |
/**
|
|
|
|
| 185 |
prefixTokens: number[];
|
| 186 |
evaluatedIds: number[];
|
| 187 |
mask: number[];
|
| 188 |
+
parent: Array<number | null>;
|
| 189 |
+
moveData: Array<{ move: Move; notation: string; leafPos: number }>;
|
| 190 |
} {
|
| 191 |
// Get current TGN as prefix
|
| 192 |
const currentTGN = game.toTGN().trim();
|
|
|
|
| 214 |
prefix = currentTGN + " ";
|
| 215 |
}
|
| 216 |
|
| 217 |
+
const prefixTokens = [this.START_TOKEN, ...this.stringToTokens(prefix)];
|
| 218 |
|
| 219 |
// Encode each move to tokens (only first 2 tokens)
|
| 220 |
const shape = game.getShape();
|
|
|
|
| 229 |
}
|
| 230 |
|
| 231 |
// Exclude the last token
|
| 232 |
+
// For single-char notations, this results in empty tokens array,
|
| 233 |
+
// which means we use prefix logits directly for scoring
|
| 234 |
const fullTokens = this.stringToTokens(notation);
|
| 235 |
const tokens = fullTokens.slice(0, fullTokens.length - 1);
|
| 236 |
|
|
|
|
| 239 |
|
| 240 |
// Build prefix tree
|
| 241 |
const tokenArrays = movesWithTokens.map((m) => m.tokens);
|
| 242 |
+
const { evaluatedIds, mask, moveToLeafPos, parent } = this.buildPrefixTree(tokenArrays);
|
| 243 |
|
| 244 |
+
// Build move data with leaf positions only
|
| 245 |
const moveData = movesWithTokens.map((m, index) => {
|
| 246 |
const leafPos = moveToLeafPos[index];
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 247 |
return {
|
| 248 |
move: m.move,
|
| 249 |
notation: m.notation,
|
| 250 |
+
leafPos
|
|
|
|
| 251 |
};
|
| 252 |
});
|
| 253 |
|
| 254 |
+
return { prefixTokens, evaluatedIds, mask, parent, moveData };
|
| 255 |
}
|
| 256 |
|
| 257 |
/**
|
|
|
|
| 263 |
): {
|
| 264 |
evaluatedIds: number[];
|
| 265 |
mask: number[];
|
| 266 |
+
parent: Array<number | null>;
|
| 267 |
+
moveData: Array<{ move: Move; notation: string; leafPos: number }>;
|
| 268 |
} {
|
| 269 |
return this.buildMoveTree(game, moves);
|
| 270 |
}
|
| 271 |
|
| 272 |
/**
|
| 273 |
+
* Select move using tree attention with optional temperature sampling
|
| 274 |
+
* @param game Current game state
|
| 275 |
+
* @param temperature Sampling temperature (0 = greedy, higher = more random)
|
| 276 |
+
* @returns Selected move (position or Pass if no valid positions)
|
| 277 |
*/
|
| 278 |
+
async selectMove(game: TrigoGame, temperature: number = 0): Promise<Move> {
|
| 279 |
if (!this.inferencer.isReady()) {
|
| 280 |
throw new Error("Inferencer not initialized");
|
| 281 |
}
|
|
|
|
| 283 |
// Get current player as string
|
| 284 |
const currentPlayer = this.stoneToPlayer(game.getCurrentPlayer());
|
| 285 |
|
| 286 |
+
// Get all valid position moves (excluding Pass)
|
| 287 |
const validMoves: Move[] = game.validMovePositions().map((pos) => ({
|
| 288 |
x: pos.x,
|
| 289 |
y: pos.y,
|
| 290 |
z: pos.z,
|
| 291 |
player: currentPlayer
|
| 292 |
}));
|
|
|
|
| 293 |
|
| 294 |
+
// If no position moves available, return Pass directly
|
| 295 |
if (validMoves.length === 0) {
|
| 296 |
+
return { player: currentPlayer, isPass: true };
|
| 297 |
}
|
| 298 |
|
| 299 |
+
// Score only position moves (Pass excluded from inference)
|
| 300 |
const scoredMoves = await this.scoreMoves(game, validMoves);
|
| 301 |
|
| 302 |
+
// Fallback to Pass if scoring fails
|
| 303 |
if (scoredMoves.length === 0) {
|
| 304 |
+
return { player: currentPlayer, isPass: true };
|
| 305 |
+
}
|
| 306 |
+
|
| 307 |
+
// Select move based on temperature
|
| 308 |
+
if (temperature <= 0.01) {
|
| 309 |
+
// Greedy selection (use reduce to avoid mutating scoredMoves)
|
| 310 |
+
const best = scoredMoves.reduce((a, b) => (b.score > a.score ? b : a));
|
| 311 |
+
return best.move;
|
| 312 |
+
}
|
| 313 |
+
|
| 314 |
+
// Temperature sampling
|
| 315 |
+
return this.sampleMove(scoredMoves, temperature);
|
| 316 |
+
}
|
| 317 |
+
|
| 318 |
+
/**
|
| 319 |
+
* Select best move using tree attention (greedy, temperature=0)
|
| 320 |
+
* Evaluates all valid moves in a single inference call
|
| 321 |
+
* Pass is excluded from model prediction - returned directly if no positions available
|
| 322 |
+
*/
|
| 323 |
+
async selectBestMove(game: TrigoGame): Promise<Move> {
|
| 324 |
+
return this.selectMove(game, 0);
|
| 325 |
+
}
|
| 326 |
+
|
| 327 |
+
/**
|
| 328 |
+
* Sample a move from scored moves using temperature
|
| 329 |
+
*/
|
| 330 |
+
private sampleMove(scoredMoves: ScoredMove[], temperature: number): Move {
|
| 331 |
+
// Apply temperature scaling to log probabilities
|
| 332 |
+
const adjustedScores = scoredMoves.map((m) => m.score / temperature);
|
| 333 |
+
const maxScore = Math.max(...adjustedScores);
|
| 334 |
+
const expScores = adjustedScores.map((score) => Math.exp(score - maxScore));
|
| 335 |
+
const sumExp = expScores.reduce((sum, exp) => sum + exp, 0);
|
| 336 |
+
|
| 337 |
+
if (sumExp === 0 || !isFinite(sumExp)) {
|
| 338 |
+
// Fallback to uniform random
|
| 339 |
+
const idx = Math.floor(Math.random() * scoredMoves.length);
|
| 340 |
+
return scoredMoves[idx].move;
|
| 341 |
}
|
| 342 |
|
| 343 |
+
const probabilities = expScores.map((exp) => exp / sumExp);
|
| 344 |
+
|
| 345 |
+
// Weighted random sampling
|
| 346 |
+
const random = Math.random();
|
| 347 |
+
let cumulative = 0;
|
| 348 |
+
for (let i = 0; i < scoredMoves.length; i++) {
|
| 349 |
+
cumulative += probabilities[i];
|
| 350 |
+
if (random <= cumulative) {
|
| 351 |
+
return scoredMoves[i].move;
|
| 352 |
+
}
|
| 353 |
+
}
|
| 354 |
+
|
| 355 |
+
return scoredMoves[scoredMoves.length - 1].move;
|
| 356 |
}
|
| 357 |
|
| 358 |
/**
|
|
|
|
| 364 |
}
|
| 365 |
|
| 366 |
// Build tree structure
|
| 367 |
+
const { prefixTokens, evaluatedIds, mask, parent, moveData } = this.buildMoveTree(game, moves);
|
| 368 |
|
| 369 |
+
//console.debug(`Tree structure: ${evaluatedIds.length} nodes for ${moveData.length} moves`);
|
| 370 |
+
//console.debug(`Evaluated IDs:`, evaluatedIds.map((id) => String.fromCharCode(id)).join(""));
|
| 371 |
//console.debug(
|
| 372 |
// `Move positions:`,
|
| 373 |
+
// moveData.map((m) => `${m.notation}@${m.leafPos}`)
|
| 374 |
//);
|
| 375 |
|
| 376 |
// Prepare inputs for evaluation
|
|
|
|
| 384 |
const output = await this.inferencer.runEvaluationInference(inputs);
|
| 385 |
const { logits, numEvaluated } = output;
|
| 386 |
|
| 387 |
+
//console.debug(`Inference output: ${numEvaluated} evaluated positions`);
|
| 388 |
+
//process.stdout.write(".");
|
| 389 |
+
|
| 390 |
+
// Minimum probability threshold to avoid log(0) while preserving small probabilities
|
| 391 |
+
const MIN_PROB = 1e-10; // log(1e-10) ≈ -23
|
| 392 |
|
| 393 |
+
// Score each move by accumulating log probabilities along the path
|
| 394 |
+
// For each move, build the path from root to leaf using parent array
|
| 395 |
const scoredMoves: ScoredMove[] = [];
|
| 396 |
|
| 397 |
// Cache softmax results for each output position to avoid recomputation
|
|
|
|
| 406 |
for (const data of moveData) {
|
| 407 |
let logProb = 0;
|
| 408 |
|
| 409 |
+
// Special case: leafPos = -1 means empty tokens (single-char notation)
|
| 410 |
+
// Use prefix logits directly to predict the single character
|
| 411 |
+
if (data.leafPos === -1) {
|
| 412 |
+
const notationTokens = this.stringToTokens(data.notation);
|
| 413 |
+
if (notationTokens.length === 1) {
|
| 414 |
+
// Single-char notation: use prefix output (logits[0]) to predict it
|
| 415 |
+
const token = notationTokens[0];
|
| 416 |
+
const probs = getSoftmax(0); // Prefix output
|
| 417 |
+
const prob = Math.max(probs[token], MIN_PROB);
|
| 418 |
+
logProb = Math.log(prob);
|
| 419 |
+
} else {
|
| 420 |
+
console.error(`Unexpected: leafPos=-1 but notation length=${notationTokens.length}`);
|
| 421 |
+
logProb = Math.log(MIN_PROB);
|
| 422 |
}
|
| 423 |
+
|
| 424 |
+
scoredMoves.push({
|
| 425 |
+
move: data.move,
|
| 426 |
+
score: logProb,
|
| 427 |
+
notation: data.notation
|
| 428 |
+
});
|
| 429 |
+
continue; // Skip the normal path processing
|
| 430 |
}
|
|
|
|
| 431 |
|
| 432 |
+
// Build path from leaf to root using parent array, then reverse
|
| 433 |
+
const pathReverse: number[] = [];
|
| 434 |
+
let pos: number | null = data.leafPos;
|
| 435 |
+
const visited = new Set<number>();
|
| 436 |
+
|
| 437 |
+
// Safety checks: prevent infinite loops and invalid indices
|
| 438 |
+
while (pos !== null && pos !== undefined) {
|
| 439 |
+
// Check for cycles
|
| 440 |
+
if (visited.has(pos)) {
|
| 441 |
+
console.error(`Cycle detected in parent array at position ${pos}`);
|
| 442 |
+
break;
|
| 443 |
+
}
|
| 444 |
|
| 445 |
+
// Check for valid index
|
| 446 |
+
if (pos < 0 || pos >= parent.length) {
|
| 447 |
+
console.error(`Invalid position ${pos}, parent array length: ${parent.length}`);
|
| 448 |
+
break;
|
| 449 |
+
}
|
| 450 |
+
|
| 451 |
+
visited.add(pos);
|
| 452 |
+
pathReverse.push(pos);
|
| 453 |
+
pos = parent[pos];
|
| 454 |
+
|
| 455 |
+
// Safety limit to prevent runaway loops
|
| 456 |
+
if (pathReverse.length > 10000) {
|
| 457 |
+
console.error(`Path too long (>10000), possible infinite loop. leafPos: ${data.leafPos}`);
|
| 458 |
+
break;
|
| 459 |
+
}
|
| 460 |
+
}
|
| 461 |
|
| 462 |
+
// Reverse to get root→leaf path (indices in evaluatedIds array)
|
| 463 |
+
const path = pathReverse.reverse();
|
| 464 |
+
|
| 465 |
+
// Now accumulate log probabilities for each transition in path
|
| 466 |
+
// TreeLM returns logits[0..m] where:
|
| 467 |
+
// logits[0] = output at prefix last position (n-1) → predicts evaluatedIds[0]
|
| 468 |
+
// logits[i] = output at position (n-1+i) → predicts evaluatedIds[i]
|
| 469 |
+
//
|
| 470 |
+
// For a parent→child transition:
|
| 471 |
+
// Parent: evaluatedIds[parentIdx] at input position (n+parentIdx)
|
| 472 |
+
// Parent output: at position (n+parentIdx), which is logits[parentIdx+1]
|
| 473 |
+
// Child token: evaluatedIds[childIdx]
|
| 474 |
+
// Probability: softmax(logits[parentIdx+1])[evaluatedIds[childIdx]]
|
| 475 |
+
|
| 476 |
+
// Special case: root token (predicted from prefix last position)
|
| 477 |
+
if (path.length > 0) {
|
| 478 |
+
const rootPos = path[0];
|
| 479 |
+
const rootToken = evaluatedIds[rootPos];
|
| 480 |
+
|
| 481 |
+
// Root is predicted by prefix last position output (logits[0])
|
| 482 |
+
const probs = getSoftmax(0);
|
| 483 |
+
const prob = Math.max(probs[rootToken], MIN_PROB); // Clip to minimum
|
| 484 |
+
logProb += Math.log(prob);
|
| 485 |
+
}
|
| 486 |
|
| 487 |
+
// Subsequent transitions: parent→child in tree
|
| 488 |
+
for (let i = 1; i < path.length; i++) {
|
| 489 |
+
const parentPos = path[i - 1]; // evaluatedIds index
|
| 490 |
+
const childPos = path[i]; // evaluatedIds index
|
| 491 |
+
const childToken = evaluatedIds[childPos];
|
| 492 |
+
|
| 493 |
+
// Parent output is at logits[parentPos+1]
|
| 494 |
+
const logitsIndex = parentPos + 1;
|
| 495 |
+
|
| 496 |
+
// Check bounds: logitsIndex must be <= numEvaluated
|
| 497 |
+
// (logits has length numEvaluated+1, indices 0 to numEvaluated)
|
| 498 |
+
if (logitsIndex <= numEvaluated) {
|
| 499 |
+
const probs = getSoftmax(logitsIndex);
|
| 500 |
+
const prob = Math.max(probs[childToken], MIN_PROB); // Clip to minimum
|
| 501 |
+
logProb += Math.log(prob);
|
| 502 |
+
} else {
|
| 503 |
+
// Parent position out of bounds
|
| 504 |
+
logProb += Math.log(MIN_PROB);
|
| 505 |
+
}
|
| 506 |
+
}
|
| 507 |
|
| 508 |
+
// CRITICAL: Add probability for the LAST token (excluded from tree)
|
| 509 |
+
// The last character of the move notation was excluded from evaluatedIds
|
| 510 |
+
// We need to predict it using the leaf node's output
|
| 511 |
+
if (path.length > 0) {
|
| 512 |
+
const leafPos = path[path.length - 1]; // Last position in path
|
| 513 |
+
const lastToken = this.stringToTokens(data.notation).pop()!; // Last char of notation
|
| 514 |
+
|
| 515 |
+
// Leaf output is at logits[leafPos+1]
|
| 516 |
+
const logitsIndex = leafPos + 1;
|
| 517 |
+
|
| 518 |
+
if (logitsIndex <= numEvaluated) {
|
| 519 |
+
const probs = getSoftmax(logitsIndex);
|
| 520 |
+
const prob = Math.max(probs[lastToken], MIN_PROB); // Clip to minimum
|
| 521 |
+
logProb += Math.log(prob);
|
| 522 |
+
} else {
|
| 523 |
+
logProb += Math.log(MIN_PROB);
|
| 524 |
}
|
|
|
|
|
|
|
| 525 |
}
|
| 526 |
|
| 527 |
scoredMoves.push({
|
trigo-web/package-lock.json
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
trigo-web/package.json
CHANGED
|
@@ -48,7 +48,7 @@
|
|
| 48 |
"jison": "^0.4.18",
|
| 49 |
"jsdom": "^27.1.0",
|
| 50 |
"lint-staged": "^16.2.7",
|
| 51 |
-
"onnxruntime-node": "1.23.2",
|
| 52 |
"onnxruntime-web": "1.23.2",
|
| 53 |
"prettier": "^3.6.2",
|
| 54 |
"tsx": "^4.20.6",
|
|
@@ -58,5 +58,8 @@
|
|
| 58 |
"vue": "^3.3.4",
|
| 59 |
"vue-tsc": "^3.1.3",
|
| 60 |
"yargs": "^18.0.0"
|
|
|
|
|
|
|
|
|
|
| 61 |
}
|
| 62 |
}
|
|
|
|
| 48 |
"jison": "^0.4.18",
|
| 49 |
"jsdom": "^27.1.0",
|
| 50 |
"lint-staged": "^16.2.7",
|
| 51 |
+
"onnxruntime-node": "^1.23.2",
|
| 52 |
"onnxruntime-web": "1.23.2",
|
| 53 |
"prettier": "^3.6.2",
|
| 54 |
"tsx": "^4.20.6",
|
|
|
|
| 58 |
"vue": "^3.3.4",
|
| 59 |
"vue-tsc": "^3.1.3",
|
| 60 |
"yargs": "^18.0.0"
|
| 61 |
+
},
|
| 62 |
+
"dependencies": {
|
| 63 |
+
"dotenv": "^17.2.3"
|
| 64 |
}
|
| 65 |
}
|
trigo-web/public/onnx/{GPT2CausalLM_ep0015_evaluation.onnx → 20251220-trigo-value-llama-l6-h64-251220-value0.02/LlamaCausalLM_ep0036_evaluation.onnx}
RENAMED
|
@@ -1,3 +1,3 @@
|
|
| 1 |
version https://git-lfs.github.com/spec/v1
|
| 2 |
-
oid sha256:
|
| 3 |
-
size
|
|
|
|
| 1 |
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:f28ac6d8655eec8189887baff3677159e0c9b4dbc4c415884de9159b6d0ee621
|
| 3 |
+
size 1387656
|
trigo-web/public/onnx/20251220-trigo-value-llama-l6-h64-251220-value0.02/LlamaCausalLM_ep0036_tree.onnx
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:84f0441fa4688cd3b3b02195237a3bce8ff9380f1ba57db6fd8fe1fa9ffcec01
|
| 3 |
+
size 1378983
|