trigo / trigo-web /app /src /views /TrigoAgentTestView.vue
k-l-lambda's picture
updated
502af73
<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>