Spaces:
Running
Running
| <template> | |
| <div class="agent-test-view"> | |
| <div class="test-header"> | |
| <h1>Trigo AI Agent Test</h1> | |
| <p class="subtitle">Testing ONNX-based Move Generation</p> | |
| </div> | |
| <div class="test-body"> | |
| <!-- Agent Status --> | |
| <div class="test-section"> | |
| <h2>Agent Status</h2> | |
| <div class="status-info"> | |
| <div | |
| class="status-item" | |
| :class="{ ready: isInitialized, loading: isInitializing }" | |
| > | |
| <span class="status-label">Status:</span> | |
| <span class="status-value"> | |
| {{ | |
| isInitialized | |
| ? "✓ Ready" | |
| : isInitializing | |
| ? "⏳ Initializing..." | |
| : "⚪ Not initialized" | |
| }} | |
| </span> | |
| </div> | |
| <div v-if="error" class="error-message">❌ Error: {{ error }}</div> | |
| </div> | |
| <button | |
| class="btn btn-primary" | |
| @click="initialize" | |
| :disabled="isInitializing || isInitialized" | |
| > | |
| {{ isInitialized ? "Initialized" : "Initialize Agent" }} | |
| </button> | |
| </div> | |
| <!-- Game State --> | |
| <div class="test-section"> | |
| <h2>Test Game State</h2> | |
| <div class="game-info"> | |
| <div class="info-item"> | |
| <span class="label">Board Shape:</span> | |
| <span class="value" | |
| >{{ boardShape.x }}×{{ boardShape.y }}×{{ boardShape.z }}</span | |
| > | |
| </div> | |
| <div class="info-item"> | |
| <span class="label">Current Player:</span> | |
| <span class="value" :class="currentPlayer">{{ currentPlayer }}</span> | |
| </div> | |
| <div class="info-item"> | |
| <span class="label">Move Count:</span> | |
| <span class="value">{{ moveHistory.length }}</span> | |
| </div> | |
| <div class="info-item"> | |
| <span class="label">Valid Moves:</span> | |
| <span class="value">{{ validMovesCount }}</span> | |
| </div> | |
| </div> | |
| <div class="game-controls"> | |
| <button class="btn btn-secondary" @click="resetGame">Reset Game</button> | |
| <button | |
| class="btn btn-secondary" | |
| @click="makeRandomMove" | |
| :disabled="!gameStarted" | |
| > | |
| Make Random Move | |
| </button> | |
| </div> | |
| <!-- Current TGN --> | |
| <div class="tgn-display"> | |
| <h3>Current TGN:</h3> | |
| <pre class="tgn-content">{{ currentTGN }}</pre> | |
| </div> | |
| </div> | |
| <!-- Move Generation Test --> | |
| <div class="test-section"> | |
| <h2>Test: Generate AI Move</h2> | |
| <p>Generate the best move for the current position using the AI agent</p> | |
| <button | |
| class="btn btn-test" | |
| @click="testGenerateMove" | |
| :disabled="!isInitialized || isGenerating || !gameStarted" | |
| > | |
| {{ isGenerating ? "Generating..." : "Generate Best Move" }} | |
| </button> | |
| <div v-if="moveResult" class="test-result"> | |
| <h3>Generated Move:</h3> | |
| <div class="result-item"> | |
| <span class="label">Position:</span> | |
| <span class="value code">{{ moveResult.position }}</span> | |
| </div> | |
| <div class="result-item"> | |
| <span class="label">TGN Notation:</span> | |
| <span class="value code">{{ moveResult.notation }}</span> | |
| </div> | |
| <div class="result-item"> | |
| <span class="label">Generation Time:</span> | |
| <span class="value">{{ moveResult.time.toFixed(2) }}ms</span> | |
| </div> | |
| <div class="result-item"> | |
| <span class="label">Valid Moves Evaluated:</span> | |
| <span class="value">{{ moveResult.movesEvaluated }}</span> | |
| </div> | |
| <button class="btn btn-apply" @click="applyMove">Apply Move to Board</button> | |
| </div> | |
| </div> | |
| <!-- Move History --> | |
| <div class="test-section"> | |
| <h2>Move History</h2> | |
| <div class="move-history"> | |
| <div v-if="moveHistory.length === 0" class="empty-state">No moves yet</div> | |
| <div v-else class="move-list"> | |
| <div | |
| v-for="(move, index) in moveHistory" | |
| :key="index" | |
| class="move-item" | |
| :class="move.player" | |
| > | |
| <span class="move-number">{{ index + 1 }}.</span> | |
| <span class="move-player">{{ move.player }}</span> | |
| <span class="move-notation">{{ move.notation }}</span> | |
| <span class="move-pos">{{ move.position }}</span> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Console Output --> | |
| <div class="test-section"> | |
| <h2>Console Output</h2> | |
| <p>Check the browser console (F12) for detailed logs</p> | |
| <button class="btn btn-secondary" @click="clearConsole">Clear Console</button> | |
| </div> | |
| </div> | |
| </div> | |
| </template> | |
| <script setup lang="ts"> | |
| import "onnxruntime-web"; | |
| import { ref, computed, shallowRef } from "vue"; | |
| import { OnnxInferencer } from "@/services/onnxInferencer"; | |
| import { TrigoAgent } from "@inc/trigoAgent"; | |
| import { TrigoGame } from "@inc/trigo/game"; | |
| import type { Position } from "@inc/trigo/types"; | |
| import { encodeAb0yz } from "@inc/trigo/ab0yz"; | |
| // Inferencer instance | |
| let inferencer: OnnxInferencer | null = null; | |
| // Agent instance | |
| let agent: TrigoAgent | null = null; | |
| // Game instance (use shallowRef for reactivity) | |
| const game = shallowRef<TrigoGame | null>(null); | |
| // State | |
| const isInitializing = ref(false); | |
| const isInitialized = ref(false); | |
| const isGenerating = ref(false); | |
| const error = ref<string | null>(null); | |
| // Game state | |
| const boardShape = ref({ x: 5, y: 5, z: 5 }); | |
| const currentPlayer = ref<"black" | "white">("black"); | |
| const moveHistory = ref< | |
| Array<{ | |
| player: "black" | "white"; | |
| notation: string; | |
| position: string; | |
| }> | |
| >([]); | |
| const gameStarted = ref(false); | |
| // Move generation result | |
| const moveResult = ref<{ | |
| position: string; | |
| notation: string; | |
| time: number; | |
| movesEvaluated: number; | |
| rawPosition?: Position; | |
| } | null>(null); | |
| // Computed | |
| const validMovesCount = computed(() => { | |
| console.log("validMovesCount:", game); | |
| if (!game.value) return 0; | |
| return getValidMovesCount(); | |
| }); | |
| const currentTGN = computed(() => { | |
| if (!game.value) return "[Board 5x5] *"; | |
| return game.value.toTGN(); | |
| }); | |
| /** | |
| * Initialize the AI agent | |
| */ | |
| const initialize = async () => { | |
| isInitializing.value = true; | |
| error.value = null; | |
| try { | |
| console.log("=".repeat(80)); | |
| console.log("[TrigoAgentTest] Initializing AI agent..."); | |
| console.log("=".repeat(80)); | |
| // Create and initialize inferencer | |
| inferencer = new OnnxInferencer({ | |
| modelPath: "/onnx/GPT2CausalLM_ep0015_int8_seq2048_int8.onnx", | |
| vocabSize: 259, | |
| seqLen: 2048 | |
| }); | |
| await inferencer.initialize(); | |
| // Create agent with inferencer | |
| agent = new TrigoAgent(inferencer); | |
| isInitialized.value = true; | |
| // Initialize game | |
| resetGame(); | |
| console.log("[TrigoAgentTest] ✓ Agent initialized successfully!"); | |
| console.log("=".repeat(80)); | |
| } catch (err) { | |
| error.value = err instanceof Error ? err.message : "Unknown error"; | |
| console.error("[TrigoAgentTest] Initialization failed:", err); | |
| } finally { | |
| isInitializing.value = false; | |
| } | |
| }; | |
| /** | |
| * Reset the game to initial state | |
| */ | |
| const resetGame = () => { | |
| console.log("[TrigoAgentTest] Resetting game.value..."); | |
| game.value = new TrigoGame(boardShape.value); | |
| currentPlayer.value = "black"; | |
| moveHistory.value = []; | |
| gameStarted.value = true; | |
| moveResult.value = null; | |
| console.log("[TrigoAgentTest] ✓ Game reset"); | |
| }; | |
| /** | |
| * Get count of valid moves (using efficient batch query) | |
| */ | |
| const getValidMovesCount = (): number => { | |
| console.debug("getValidMovesCount:", game); | |
| if (!game.value) return 0; | |
| return game.value.validMovePositions().length; | |
| }; | |
| /** | |
| * Make a random move (for testing, using efficient batch query) | |
| */ | |
| const makeRandomMove = () => { | |
| if (!game.value) return; | |
| // Get all valid moves efficiently | |
| const validMoves = game.value.validMovePositions(); | |
| if (validMoves.length === 0) { | |
| console.log("[TrigoAgentTest] No valid moves available"); | |
| return; | |
| } | |
| // Pick random move | |
| const move = validMoves[Math.floor(Math.random() * validMoves.length)]; | |
| const shape = game.value.getShape(); | |
| const notation = encodeAb0yz([move.x, move.y, move.z], [shape.x, shape.y, shape.z]); | |
| // Make move | |
| game.value.drop(move); | |
| // Record in history | |
| moveHistory.value.push({ | |
| player: currentPlayer.value, | |
| notation, | |
| position: `(${move.x}, ${move.y}, ${move.z})` | |
| }); | |
| // Switch player | |
| currentPlayer.value = currentPlayer.value === "black" ? "white" : "black"; | |
| console.log(`[TrigoAgentTest] Random move: ${notation} at ${move.x},${move.y},${move.z}`); | |
| }; | |
| /** | |
| * Test: Generate best move using AI | |
| */ | |
| const testGenerateMove = async () => { | |
| if (!agent || !game) return; | |
| isGenerating.value = true; | |
| error.value = null; | |
| moveResult.value = null; | |
| try { | |
| console.log("\n" + "=".repeat(80)); | |
| console.log("[TrigoAgentTest] Generating AI move..."); | |
| console.log(`[TrigoAgentTest] Current TGN: ${game.value.toTGN()}`); | |
| console.log(`[TrigoAgentTest] Valid moves to evaluate: ${validMovesCount.value}`); | |
| console.log("=".repeat(80)); | |
| const startTime = performance.now(); | |
| const position = await agent.selectBestMove(game.value); | |
| const elapsed = performance.now() - startTime; | |
| if (!position) { | |
| error.value = "No valid moves found"; | |
| console.error("[TrigoAgentTest] No valid moves found"); | |
| return; | |
| } | |
| const notation = encodeAb0yz( | |
| [position.x, position.y, position.z], | |
| [boardShape.value.x, boardShape.value.y, boardShape.value.z] | |
| ); | |
| moveResult.value = { | |
| position: `(${position.x}, ${position.y}, ${position.z})`, | |
| notation, | |
| time: elapsed, | |
| movesEvaluated: validMovesCount.value, | |
| rawPosition: position | |
| }; | |
| console.log("[TrigoAgentTest] ✓ Move generated successfully!"); | |
| console.log( | |
| `[TrigoAgentTest] Best move: ${notation} at ${position.x},${position.y},${position.z}` | |
| ); | |
| console.log(`[TrigoAgentTest] Generation time: ${elapsed.toFixed(2)}ms`); | |
| console.log("=".repeat(80)); | |
| } catch (err) { | |
| error.value = err instanceof Error ? err.message : "Generation failed"; | |
| console.error("[TrigoAgentTest] Move generation failed:", err); | |
| } finally { | |
| isGenerating.value = false; | |
| } | |
| }; | |
| /** | |
| * Apply the generated move to the board | |
| */ | |
| const applyMove = () => { | |
| if (!moveResult.value || !moveResult.value.rawPosition || !game) return; | |
| const pos = moveResult.value.rawPosition; | |
| // Make move | |
| game.value.drop(pos); | |
| // Record in history | |
| moveHistory.value.push({ | |
| player: currentPlayer.value, | |
| notation: moveResult.value.notation, | |
| position: moveResult.value.position | |
| }); | |
| // Switch player | |
| currentPlayer.value = currentPlayer.value === "black" ? "white" : "black"; | |
| // Clear result | |
| moveResult.value = null; | |
| console.log(`[TrigoAgentTest] Move applied`); | |
| }; | |
| /** | |
| * Clear browser console | |
| */ | |
| const clearConsole = () => { | |
| console.clear(); | |
| console.log("[TrigoAgentTest] Console cleared"); | |
| }; | |
| </script> | |
| <style lang="scss" scoped> | |
| .agent-test-view { | |
| display: flex; | |
| flex-direction: column; | |
| height: 100%; | |
| background-color: #f5f5f5; | |
| overflow: auto; | |
| } | |
| .test-header { | |
| background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); | |
| color: white; | |
| padding: 2rem; | |
| text-align: center; | |
| h1 { | |
| margin: 0 0 0.5rem 0; | |
| font-size: 2rem; | |
| font-weight: 700; | |
| } | |
| .subtitle { | |
| margin: 0; | |
| opacity: 0.9; | |
| font-size: 1.1rem; | |
| } | |
| } | |
| .test-body { | |
| flex: 1; | |
| padding: 2rem; | |
| max-width: 1200px; | |
| margin: 0 auto; | |
| width: 100%; | |
| } | |
| .test-section { | |
| background: white; | |
| border-radius: 12px; | |
| padding: 1.5rem; | |
| margin-bottom: 1.5rem; | |
| box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); | |
| h2 { | |
| margin: 0 0 0.5rem 0; | |
| color: #333; | |
| font-size: 1.3rem; | |
| font-weight: 600; | |
| } | |
| p { | |
| margin: 0 0 1rem 0; | |
| color: #666; | |
| } | |
| } | |
| .status-info { | |
| background: #f8f9fa; | |
| border-radius: 8px; | |
| padding: 1rem; | |
| margin-bottom: 1rem; | |
| .status-item { | |
| display: flex; | |
| justify-content: space-between; | |
| padding: 0.5rem 0; | |
| &.ready .status-value { | |
| color: #28a745; | |
| font-weight: 600; | |
| } | |
| &.loading .status-value { | |
| color: #ffc107; | |
| font-weight: 600; | |
| } | |
| .status-label { | |
| font-weight: 600; | |
| color: #666; | |
| } | |
| .status-value { | |
| color: #333; | |
| } | |
| } | |
| } | |
| .error-message { | |
| background: #fff3cd; | |
| border: 1px solid #ffeaa7; | |
| border-radius: 6px; | |
| padding: 0.75rem; | |
| color: #856404; | |
| margin-top: 1rem; | |
| } | |
| .game-info { | |
| background: #f8f9fa; | |
| border-radius: 8px; | |
| padding: 1rem; | |
| margin-bottom: 1rem; | |
| .info-item { | |
| display: flex; | |
| justify-content: space-between; | |
| padding: 0.5rem 0; | |
| border-bottom: 1px solid #e9ecef; | |
| &:last-child { | |
| border-bottom: none; | |
| } | |
| .label { | |
| font-weight: 600; | |
| color: #666; | |
| } | |
| .value { | |
| color: #333; | |
| font-weight: 500; | |
| &.black { | |
| color: #2c2c2c; | |
| } | |
| &.white { | |
| color: #666; | |
| } | |
| } | |
| } | |
| } | |
| .game-controls { | |
| display: flex; | |
| gap: 0.5rem; | |
| margin-bottom: 1rem; | |
| } | |
| .tgn-display { | |
| h3 { | |
| margin: 0 0 0.5rem 0; | |
| color: #333; | |
| font-size: 1rem; | |
| } | |
| .tgn-content { | |
| background: #2a2a2a; | |
| color: #e0e0e0; | |
| padding: 1rem; | |
| border-radius: 8px; | |
| font-family: "Courier New", monospace; | |
| font-size: 0.9rem; | |
| line-height: 1.6; | |
| overflow-x: auto; | |
| white-space: pre-wrap; | |
| word-break: break-all; | |
| } | |
| } | |
| .btn { | |
| padding: 0.75rem 1.5rem; | |
| border: none; | |
| border-radius: 8px; | |
| font-weight: 600; | |
| font-size: 1rem; | |
| cursor: pointer; | |
| transition: all 0.3s ease; | |
| &:disabled { | |
| opacity: 0.5; | |
| cursor: not-allowed; | |
| } | |
| &.btn-primary { | |
| background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); | |
| color: white; | |
| &:hover:not(:disabled) { | |
| transform: translateY(-2px); | |
| box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4); | |
| } | |
| } | |
| &.btn-test { | |
| background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%); | |
| color: white; | |
| &:hover:not(:disabled) { | |
| transform: translateY(-2px); | |
| box-shadow: 0 4px 12px rgba(245, 87, 108, 0.4); | |
| } | |
| } | |
| &.btn-secondary { | |
| background: #6c757d; | |
| color: white; | |
| &:hover:not(:disabled) { | |
| background: #5a6268; | |
| } | |
| } | |
| &.btn-apply { | |
| background: #28a745; | |
| color: white; | |
| margin-top: 1rem; | |
| &:hover:not(:disabled) { | |
| background: #218838; | |
| transform: translateY(-2px); | |
| box-shadow: 0 4px 12px rgba(40, 167, 69, 0.4); | |
| } | |
| } | |
| } | |
| .test-result { | |
| background: #f8f9fa; | |
| border-radius: 8px; | |
| padding: 1rem; | |
| margin-top: 1rem; | |
| h3 { | |
| margin: 0 0 1rem 0; | |
| color: #333; | |
| font-size: 1.1rem; | |
| } | |
| .result-item { | |
| display: flex; | |
| justify-content: space-between; | |
| padding: 0.5rem 0; | |
| border-bottom: 1px solid #e9ecef; | |
| &:last-of-type { | |
| border-bottom: none; | |
| } | |
| .label { | |
| font-weight: 600; | |
| color: #666; | |
| } | |
| .value { | |
| color: #333; | |
| &.code { | |
| font-family: "Courier New", monospace; | |
| background: #fff; | |
| padding: 0.25rem 0.5rem; | |
| border-radius: 4px; | |
| font-size: 0.9rem; | |
| } | |
| } | |
| } | |
| } | |
| .move-history { | |
| background: #f8f9fa; | |
| border-radius: 8px; | |
| padding: 1rem; | |
| max-height: 400px; | |
| overflow-y: auto; | |
| .empty-state { | |
| text-align: center; | |
| color: #999; | |
| padding: 2rem; | |
| font-style: italic; | |
| } | |
| .move-list { | |
| display: flex; | |
| flex-direction: column; | |
| gap: 0.5rem; | |
| } | |
| .move-item { | |
| display: grid; | |
| grid-template-columns: 40px 80px 100px 1fr; | |
| gap: 0.5rem; | |
| padding: 0.5rem; | |
| background: white; | |
| border-radius: 6px; | |
| border-left: 3px solid; | |
| &.black { | |
| border-color: #2c2c2c; | |
| } | |
| &.white { | |
| border-color: #999; | |
| } | |
| .move-number { | |
| color: #999; | |
| font-weight: 600; | |
| } | |
| .move-player { | |
| font-weight: 600; | |
| text-transform: capitalize; | |
| } | |
| .move-notation { | |
| font-family: "Courier New", monospace; | |
| font-weight: 600; | |
| } | |
| .move-pos { | |
| color: #666; | |
| font-family: "Courier New", monospace; | |
| font-size: 0.9rem; | |
| } | |
| } | |
| } | |
| </style> | |