k-l-lambda Claude commited on
Commit
634af17
·
1 Parent(s): 33b3f87

Add debug logging for socket.io room membership

Browse files

Track socket joins and broadcasts to diagnose why playerJoined
events are not being received by other players in the same room.

Co-Authored-By: Claude <noreply@anthropic.com>

trigo-web/.env CHANGED
@@ -14,6 +14,7 @@
14
  # ============================================================================
15
 
16
  # Backend Server URL
 
17
 
18
  # Vite Dev Server Configuration
19
  VITE_HOST=0.0.0.0
@@ -38,7 +39,7 @@ PORT=3000
38
  CLIENT_URL=http://localhost:5173
39
 
40
  # Environment mode
41
- NODE_ENV=production
42
 
43
 
44
  # ============================================================================
 
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
 
39
  CLIENT_URL=http://localhost:5173
40
 
41
  # Environment mode
42
+ NODE_ENV=development
43
 
44
 
45
  # ============================================================================
trigo-web/app/package.json CHANGED
@@ -1,32 +1,30 @@
1
  {
2
- "name": "trigo-app",
3
- "private": true,
4
- "version": "0.0.0",
5
- "type": "module",
6
- "scripts": {
7
- "dev": "vite",
8
- "dev:host": "vite --host",
9
- "build": "vite build",
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",
18
- "vue": "^3.3.4",
19
- "vue-router": "^4.2.4",
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",
27
- "typescript": "^5.2.2",
28
- "vite": "^5.4.21",
29
- "vue-tsc": "^2.2.12",
30
- "@types/node": "^24.10.1"
31
- }
32
  }
 
1
  {
2
+ "name": "trigo-app",
3
+ "private": true,
4
+ "version": "0.0.0",
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "vite",
8
+ "dev:host": "vite --host",
9
+ "build": "vue-tsc --noEmit && vite build",
10
+ "preview": "vite preview"
11
+ },
12
+ "dependencies": {
13
+ "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",
18
+ "vue": "^3.3.4",
19
+ "vue-router": "^4.2.4"
20
+ },
21
+ "devDependencies": {
22
+ "@types/d3": "^7.4.3",
23
+ "@types/three": "^0.156.0",
24
+ "@vitejs/plugin-vue": "^5.2.4",
25
+ "sass-embedded": "^1.93.2",
26
+ "typescript": "^5.2.2",
27
+ "vite": "^5.4.21",
28
+ "vue-tsc": "^2.2.12"
29
+ }
 
 
30
  }
trigo-web/app/tsconfig.json DELETED
@@ -1,31 +0,0 @@
1
- {
2
- "compilerOptions": {
3
- "target": "ES2020",
4
- "useDefineForClassFields": true,
5
- "module": "ESNext",
6
- "lib": ["ES2020", "DOM", "DOM.Iterable"],
7
- "skipLibCheck": true,
8
-
9
- /* Bundler mode */
10
- "moduleResolution": "bundler",
11
- "allowImportingTsExtensions": true,
12
- "resolveJsonModule": true,
13
- "isolatedModules": true,
14
- "noEmit": true,
15
- "jsx": "preserve",
16
-
17
- /* Linting */
18
- "strict": true,
19
- "noUnusedLocals": true,
20
- "noUnusedParameters": true,
21
- "noFallthroughCasesInSwitch": true,
22
-
23
- /* Path aliases */
24
- "baseUrl": ".",
25
- "paths": {
26
- "@/*": ["./src/*"],
27
- "@inc/*": ["../inc/*"]
28
- }
29
- },
30
- "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"]
31
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
trigo-web/app/tsconfig.node.json DELETED
@@ -1,10 +0,0 @@
1
- {
2
- "compilerOptions": {
3
- "composite": true,
4
- "skipLibCheck": true,
5
- "module": "ESNext",
6
- "moduleResolution": "bundler",
7
- "allowSyntheticDefaultImports": true
8
- },
9
- "include": ["vite.config.ts"]
10
- }
 
 
 
 
 
 
 
 
 
 
 
trigo-web/backend/.env.local ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+
2
+ PORT=8157
trigo-web/backend/package.json CHANGED
@@ -3,11 +3,11 @@
3
  "version": "1.0.0",
4
  "type": "module",
5
  "description": "Backend server for Trigo game",
6
- "main": "dist/server.js",
7
  "scripts": {
8
  "dev": "nodemon --watch src --exec tsx src/server.ts",
9
  "build": "tsc",
10
- "start": "node dist/server.js",
11
  "test": "echo \"Error: no test specified\" && exit 1"
12
  },
13
  "keywords": [
 
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"
12
  },
13
  "keywords": [
trigo-web/backend/src/server.ts CHANGED
@@ -69,7 +69,7 @@ app.use(express.json());
69
 
70
  // Serve static files from frontend build (for production)
71
  if (process.env.NODE_ENV === "production") {
72
- const frontendPath = path.join(__dirname, "../../app/dist");
73
  app.use(express.static(frontendPath));
74
 
75
  // Serve index.html for all routes (SPA support)
 
69
 
70
  // Serve static files from frontend build (for production)
71
  if (process.env.NODE_ENV === "production") {
72
+ const frontendPath = path.join(__dirname, "../../../../app/dist");
73
  app.use(express.static(frontendPath));
74
 
75
  // Serve index.html for all routes (SPA support)
trigo-web/backend/src/sockets/gameSocket.ts CHANGED
@@ -83,6 +83,11 @@ export function setupSocketHandlers(io: Server, socket: Socket, gameManager: Gam
83
  if (room) {
84
  socket.join(room.id);
85
 
 
 
 
 
 
86
  // Get complete game data for frontend
87
  const currentPlayer = gameManager.getCurrentPlayer(room.id);
88
  const stats = gameManager.getGameStats(room.id);
@@ -133,10 +138,12 @@ export function setupSocketHandlers(io: Server, socket: Socket, gameManager: Gam
133
  }
134
 
135
  // Notify other players
 
136
  socket.to(room.id).emit("playerJoined", {
137
  playerId: socket.id,
138
  nickname: nickname
139
  });
 
140
 
141
  // Broadcast room update to all sockets (for room list)
142
  const roomSummary = getRoomSummary(room);
 
83
  if (room) {
84
  socket.join(room.id);
85
 
86
+ // Debug: Log socket.io room membership
87
+ const roomSockets = io.sockets.adapter.rooms.get(room.id);
88
+ console.log(`[gameSocket] Socket ${socket.id} joined room ${room.id}`);
89
+ console.log(`[gameSocket] Room ${room.id} now has sockets:`, roomSockets ? Array.from(roomSockets) : []);
90
+
91
  // Get complete game data for frontend
92
  const currentPlayer = gameManager.getCurrentPlayer(room.id);
93
  const stats = gameManager.getGameStats(room.id);
 
138
  }
139
 
140
  // Notify other players
141
+ console.log(`[gameSocket] Broadcasting playerJoined to room ${room.id} (excluding ${socket.id})`);
142
  socket.to(room.id).emit("playerJoined", {
143
  playerId: socket.id,
144
  nickname: nickname
145
  });
146
+ console.log(`[gameSocket] playerJoined broadcast sent`);
147
 
148
  // Broadcast room update to all sockets (for room list)
149
  const roomSummary = getRoomSummary(room);
trigo-web/package.json CHANGED
@@ -1,64 +1,65 @@
1
  {
2
- "name": "trigo-web",
3
- "version": "1.0.0",
4
- "type": "module",
5
- "description": "3D Go board game with Vue3 and Node.js",
6
- "scripts": {
7
- "dev": "concurrently \"npm run dev:backend\" \"npm run dev:app\"",
8
- "dev:app": "cd app && npm run dev",
9
- "dev:backend": "cd backend && npm run dev",
10
- "build": "npm run build:app && npm run build:backend",
11
- "build:app": "cd app && npm run build",
12
- "build:backend": "cd backend && npm run build",
13
- "build:parsers": "npm run build:parser:tgn",
14
- "build:parser:tgn": "tsx tools/buildJisonParser.ts",
15
- "install:all": "npm install && cd app && npm install && cd ../backend && npm install",
16
- "start:prod": "cd backend && npm start",
17
- "format": "prettier --write \"**/*.{js,ts,vue,json,md,scss,css}\"",
18
- "format:check": "prettier --check \"**/*.{js,ts,vue,json,md,scss,css}\"",
19
- "test": "vitest",
20
- "test:ui": "vitest --ui",
21
- "test:run": "vitest run",
22
- "generate:games": "tsx tools/generateRandomGames.ts",
23
- "migrate:tgn": "tsx tools/migrateTGN.ts"
24
- },
25
- "keywords": [
26
- "game",
27
- "go",
28
- "3d",
29
- "vue",
30
- "nodejs",
31
- "websocket"
32
- ],
33
- "author": "",
34
- "license": "MIT",
35
- "lint-staged": {
36
- "**/*.{js,ts,vue,json,md,scss,css}": []
37
- },
38
- "devDependencies": {
39
- "@types/node": "^24.10.0",
40
- "@types/yargs": "^17.0.34",
41
- "@vitejs/plugin-vue": "^5.2.4",
42
- "@vitest/ui": "^4.0.6",
43
- "concurrently": "^7.6.0",
44
- "eslint-config-prettier": "^10.1.8",
45
- "eslint-plugin-prettier": "^5.5.4",
46
- "husky": "^9.1.7",
47
- "jison": "^0.4.18",
48
- "jsdom": "^27.1.0",
49
- "lint-staged": "^16.2.7",
50
- "onnxruntime-node": "^1.23.2",
51
- "onnxruntime-web": "1.23.2",
52
- "prettier": "^3.6.2",
53
- "tsx": "^4.20.6",
54
- "typescript": "^5.2.2",
55
- "vite": "^5.4.21",
56
- "vitest": "^4.0.6",
57
- "vue": "^3.3.4",
58
- "vue-tsc": "^3.1.3",
59
- "yargs": "^18.0.0"
60
- },
61
- "dependencies": {
62
- "dotenv": "^17.2.3"
63
- }
 
64
  }
 
1
  {
2
+ "name": "trigo-web",
3
+ "version": "1.0.0",
4
+ "type": "module",
5
+ "description": "3D Go board game with Vue3 and Node.js",
6
+ "scripts": {
7
+ "dev": "concurrently \"npm run dev:backend\" \"npm run dev:app\"",
8
+ "dev:app": "cd app && npm run dev",
9
+ "dev:backend": "cd backend && npm run dev",
10
+ "build": "npm run build:app && npm run build:backend",
11
+ "build:app": "cd app && npm run build",
12
+ "build:backend": "cd backend && npm run build",
13
+ "build:parsers": "npm run build:parser:tgn",
14
+ "build:parser:tgn": "tsx tools/buildJisonParser.ts",
15
+ "install:all": "npm install && cd app && npm install && cd ../backend && npm install",
16
+ "start:prod": "cd backend && npm start",
17
+ "format": "prettier --write \"**/*.{js,ts,vue,json,md,scss,css}\"",
18
+ "format:check": "prettier --check \"**/*.{js,ts,vue,json,md,scss,css}\"",
19
+ "test": "vitest",
20
+ "test:ui": "vitest --ui",
21
+ "test:run": "vitest run",
22
+ "generate:games": "tsx tools/generateRandomGames.ts",
23
+ "migrate:tgn": "tsx tools/migrateTGN.ts",
24
+ "prepare": "cd .. && husky"
25
+ },
26
+ "keywords": [
27
+ "game",
28
+ "go",
29
+ "3d",
30
+ "vue",
31
+ "nodejs",
32
+ "websocket"
33
+ ],
34
+ "author": "",
35
+ "license": "MIT",
36
+ "lint-staged": {
37
+ "**/*.{js,ts,vue,json,md,scss,css}": []
38
+ },
39
+ "devDependencies": {
40
+ "@types/node": "^24.10.0",
41
+ "@types/yargs": "^17.0.34",
42
+ "@vitejs/plugin-vue": "^5.2.4",
43
+ "@vitest/ui": "^4.0.6",
44
+ "concurrently": "^7.6.0",
45
+ "eslint-config-prettier": "^10.1.8",
46
+ "eslint-plugin-prettier": "^5.5.4",
47
+ "husky": "^9.1.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",
55
+ "typescript": "^5.2.2",
56
+ "vite": "^5.4.21",
57
+ "vitest": "^4.0.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/tests/game/debug_capture.test.ts ADDED
@@ -0,0 +1,44 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { describe, it } from "vitest";
2
+ import { TrigoGame, StoneType } from "@inc/trigo/game";
3
+
4
+ describe("Debug Capture", () => {
5
+ it("debug 2D capture scenario", () => {
6
+ const game = new TrigoGame({ x: 5, y: 5, z: 1 });
7
+ game.startGame();
8
+
9
+ console.log("\n=== Testing 2D Capture ===");
10
+
11
+ // Check White stone at (3,2,0) neighbors
12
+ game.drop({ x: 2, y: 2, z: 0 }); // Black
13
+ game.drop({ x: 3, y: 2, z: 0 }); // White (target)
14
+ console.log("White placed at (3,2,0)");
15
+
16
+ game.drop({ x: 4, y: 2, z: 0 }); // Black (right of white)
17
+ console.log("After Black at (4,2,0) - right of white");
18
+
19
+ game.drop({ x: 3, y: 1, z: 0 }); // White elsewhere
20
+ game.drop({ x: 3, y: 3, z: 0 }); // Black (bottom of white)
21
+ console.log("After Black at (3,3,0) - bottom of white");
22
+
23
+ game.drop({ x: 1, y: 1, z: 0 }); // White elsewhere
24
+
25
+ console.log("\nBefore final move:");
26
+ console.log(" board[2][2][0] (left):", game.getBoard()[2][2][0], "BLACK=1");
27
+ console.log(" board[3][2][0] (white):", game.getBoard()[3][2][0], "WHITE=2");
28
+ console.log(" board[4][2][0] (right):", game.getBoard()[4][2][0], "BLACK=1");
29
+ console.log(" board[3][1][0] (top):", game.getBoard()[3][1][0], "WHITE=2");
30
+ console.log(" board[3][3][0] (bottom):", game.getBoard()[3][3][0], "BLACK=1");
31
+ console.log(" board[2][1][0] (top-left):", game.getBoard()[2][1][0], "EMPTY=0");
32
+
33
+ game.drop({ x: 2, y: 1, z: 0 }); // Black at (2,1,0) - top of white?
34
+
35
+ console.log("\nAfter final Black move at (2,1,0):");
36
+ console.log(
37
+ " board[3][2][0]:",
38
+ game.getBoard()[3][2][0],
39
+ "(should be EMPTY=0 if captured)"
40
+ );
41
+ console.log(" Last step capturedPositions:", game.getLastStep()?.capturedPositions);
42
+ console.log(" Captured counts:", game.getCapturedCounts());
43
+ });
44
+ });
trigo-web/tests/game/debug_redo.test.ts ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { describe, it } from "vitest";
2
+ import { TrigoGame, StoneType } from "@inc/trigo/game";
3
+
4
+ describe("Debug Redo After Jump", () => {
5
+ it("trace redo behavior", () => {
6
+ const game = new TrigoGame({ x: 5, y: 5, z: 1 });
7
+ game.startGame();
8
+
9
+ console.log("\n=== Initial state ===");
10
+ console.log(`getCurrentStep: ${game.getCurrentStep()}`);
11
+ console.log(`history length: ${game.getHistory().length}`);
12
+ console.log(`Expected: step 0 means NO moves applied`);
13
+
14
+ console.log("\n=== After first drop (2,2,0) ===");
15
+ game.drop({ x: 2, y: 2, z: 0 });
16
+ console.log(`getCurrentStep: ${game.getCurrentStep()}`);
17
+ console.log(`history length: ${game.getHistory().length}`);
18
+ console.log(`board[2][2][0]: ${game.getBoard()[2][2][0]} (BLACK=1)`);
19
+
20
+ console.log("\n=== After second drop (3,2,0) ===");
21
+ game.drop({ x: 3, y: 2, z: 0 });
22
+ console.log(`getCurrentStep: ${game.getCurrentStep()}`);
23
+ console.log(`history length: ${game.getHistory().length}`);
24
+ console.log(`board[2][2][0]: ${game.getBoard()[2][2][0]}`);
25
+ console.log(`board[3][2][0]: ${game.getBoard()[3][2][0]} (WHITE=2)`);
26
+
27
+ console.log("\n=== After jumpToStep(0) ===");
28
+ game.jumpToStep(0);
29
+ console.log(`getCurrentStep: ${game.getCurrentStep()}`);
30
+ console.log(`history length: ${game.getHistory().length}`);
31
+ console.log(`board[2][2][0]: ${game.getBoard()[2][2][0]}`);
32
+ console.log(`board[3][2][0]: ${game.getBoard()[3][2][0]} (should be 0=EMPTY)`);
33
+ console.log(`canRedo: ${game.canRedo()}`);
34
+
35
+ console.log("\n=== After redo() ===");
36
+ const redoResult = game.redo();
37
+ console.log(`redo returned: ${redoResult}`);
38
+ console.log(`getCurrentStep: ${game.getCurrentStep()}`);
39
+ console.log(`board[2][2][0]: ${game.getBoard()[2][2][0]}`);
40
+ console.log(`board[3][2][0]: ${game.getBoard()[3][2][0]} (expected WHITE=2)`);
41
+ });
42
+ });
trigo-web/tests/game/trigoGame.core.test.ts ADDED
@@ -0,0 +1,300 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * TrigoGame Core Functionality Tests
3
+ *
4
+ * Tests basic game operations: initialization, moves, pass, surrender
5
+ */
6
+
7
+ import { describe, it, expect, beforeEach } from "vitest";
8
+ import { TrigoGame, StoneType } from "@inc/trigo/game";
9
+ import type { BoardShape } from "@inc/trigo/types";
10
+
11
+ describe("TrigoGame - Core Functionality", () => {
12
+ let game: TrigoGame;
13
+ const defaultShape: BoardShape = { x: 5, y: 5, z: 5 };
14
+
15
+ beforeEach(() => {
16
+ game = new TrigoGame(defaultShape);
17
+ });
18
+
19
+ describe("Initialization", () => {
20
+ it("should initialize with empty board", () => {
21
+ const board = game.getBoard();
22
+ expect(board).toBeDefined();
23
+ expect(board.length).toBe(5);
24
+ expect(board[0].length).toBe(5);
25
+ expect(board[0][0].length).toBe(5);
26
+ });
27
+
28
+ it("should start with black player", () => {
29
+ expect(game.getCurrentPlayer()).toBe(StoneType.BLACK);
30
+ });
31
+
32
+ it("should have initial game status as idle", () => {
33
+ expect(game.getGameStatus()).toBe("idle");
34
+ });
35
+
36
+ it("should have no moves initially", () => {
37
+ expect(game.getCurrentStep()).toBe(0);
38
+ expect(game.getHistory().length).toBe(0);
39
+ });
40
+
41
+ it("should initialize with correct board shape", () => {
42
+ const shape = game.getShape();
43
+ expect(shape).toEqual(defaultShape);
44
+ });
45
+ });
46
+
47
+ describe("Start Game", () => {
48
+ it("should change status from idle to playing", () => {
49
+ game.startGame();
50
+ expect(game.getGameStatus()).toBe("playing");
51
+ });
52
+
53
+ it("should not start if already playing", () => {
54
+ game.startGame();
55
+ expect(game.getGameStatus()).toBe("playing");
56
+ game.startGame(); // Try starting again
57
+ expect(game.getGameStatus()).toBe("playing");
58
+ });
59
+ });
60
+
61
+ describe("Drop Stone", () => {
62
+ beforeEach(() => {
63
+ game.startGame();
64
+ });
65
+
66
+ it("should place a black stone at position", () => {
67
+ const success = game.drop({ x: 2, y: 2, z: 2 });
68
+ expect(success).toBe(true);
69
+
70
+ const board = game.getBoard();
71
+ expect(board[2][2][2]).toBe(StoneType.BLACK);
72
+ });
73
+
74
+ it("should switch to white player after black moves", () => {
75
+ game.drop({ x: 2, y: 2, z: 2 });
76
+ expect(game.getCurrentPlayer()).toBe(StoneType.WHITE);
77
+ });
78
+
79
+ it("should increment step count", () => {
80
+ expect(game.getCurrentStep()).toBe(0);
81
+ game.drop({ x: 2, y: 2, z: 2 });
82
+ expect(game.getCurrentStep()).toBe(1);
83
+ });
84
+
85
+ it("should add move to history", () => {
86
+ game.drop({ x: 2, y: 2, z: 2 });
87
+ const history = game.getHistory();
88
+ expect(history.length).toBe(1);
89
+ expect(history[0].position).toEqual({ x: 2, y: 2, z: 2 });
90
+ expect(history[0].player).toBe(StoneType.BLACK);
91
+ });
92
+
93
+ it("should not place stone on occupied position", () => {
94
+ game.drop({ x: 2, y: 2, z: 2 });
95
+ const success = game.drop({ x: 2, y: 2, z: 2 });
96
+ expect(success).toBe(false);
97
+ });
98
+
99
+ it("should not place stone out of bounds", () => {
100
+ const success = game.drop({ x: 10, y: 10, z: 10 });
101
+ expect(success).toBe(false);
102
+ });
103
+
104
+ it("should allow alternating moves", () => {
105
+ game.drop({ x: 2, y: 2, z: 2 }); // Black
106
+ game.drop({ x: 3, y: 2, z: 2 }); // White
107
+ game.drop({ x: 2, y: 3, z: 2 }); // Black
108
+
109
+ const board = game.getBoard();
110
+ expect(board[2][2][2]).toBe(StoneType.BLACK);
111
+ expect(board[3][2][2]).toBe(StoneType.WHITE);
112
+ expect(board[2][3][2]).toBe(StoneType.BLACK);
113
+ expect(game.getCurrentPlayer()).toBe(StoneType.WHITE);
114
+ });
115
+
116
+ it("should reset pass count when stone is placed", () => {
117
+ game.pass(); // Black passes
118
+ expect(game.getPassCount()).toBe(1);
119
+ game.drop({ x: 2, y: 2, z: 2 }); // White places stone
120
+ expect(game.getPassCount()).toBe(0);
121
+ });
122
+ });
123
+
124
+ describe("Pass", () => {
125
+ beforeEach(() => {
126
+ game.startGame();
127
+ });
128
+
129
+ it("should allow player to pass", () => {
130
+ const success = game.pass();
131
+ expect(success).toBe(true);
132
+ });
133
+
134
+ it("should switch player after pass", () => {
135
+ expect(game.getCurrentPlayer()).toBe(StoneType.BLACK);
136
+ game.pass();
137
+ expect(game.getCurrentPlayer()).toBe(StoneType.WHITE);
138
+ });
139
+
140
+ it("should increment pass count", () => {
141
+ expect(game.getPassCount()).toBe(0);
142
+ game.pass();
143
+ expect(game.getPassCount()).toBe(1);
144
+ });
145
+
146
+ it("should add pass to history", () => {
147
+ game.pass();
148
+ const history = game.getHistory();
149
+ expect(history.length).toBe(1);
150
+ expect(history[0].type).toBe(1); // StepType.PASS
151
+ });
152
+
153
+ it("should end game after two consecutive passes", () => {
154
+ game.pass(); // Black passes
155
+ game.pass(); // White passes
156
+
157
+ expect(game.getGameStatus()).toBe("finished");
158
+ expect(game.getGameResult()).toBeDefined();
159
+ expect(game.getGameResult()?.reason).toBe("double-pass");
160
+ });
161
+
162
+ it("should not end game if pass not consecutive", () => {
163
+ game.pass(); // Black passes
164
+ game.drop({ x: 2, y: 2, z: 2 }); // White places stone
165
+ game.pass(); // Black passes again
166
+
167
+ expect(game.getGameStatus()).toBe("playing");
168
+ });
169
+ });
170
+
171
+ describe("Surrender", () => {
172
+ beforeEach(() => {
173
+ game.startGame();
174
+ });
175
+
176
+ it("should allow player to surrender", () => {
177
+ const success = game.surrender();
178
+ expect(success).toBe(true);
179
+ });
180
+
181
+ it("should end the game", () => {
182
+ game.surrender();
183
+ expect(game.getGameStatus()).toBe("finished");
184
+ });
185
+
186
+ it("should set winner as opponent", () => {
187
+ // Black surrenders
188
+ expect(game.getCurrentPlayer()).toBe(StoneType.BLACK);
189
+ game.surrender();
190
+
191
+ const result = game.getGameResult();
192
+ expect(result).toBeDefined();
193
+ expect(result?.winner).toBe("white");
194
+ expect(result?.reason).toBe("resignation");
195
+ });
196
+
197
+ it("should add surrender to history", () => {
198
+ game.surrender();
199
+ const history = game.getHistory();
200
+ expect(history.length).toBe(1);
201
+ expect(history[0].type).toBe(2); // StepType.SURRENDER
202
+ });
203
+ });
204
+
205
+ describe("Reset", () => {
206
+ it("should reset game to initial state", () => {
207
+ game.startGame();
208
+ game.drop({ x: 2, y: 2, z: 2 });
209
+ game.drop({ x: 3, y: 2, z: 2 });
210
+ game.pass();
211
+
212
+ game.reset();
213
+
214
+ expect(game.getCurrentPlayer()).toBe(StoneType.BLACK);
215
+ expect(game.getCurrentStep()).toBe(0);
216
+ expect(game.getHistory().length).toBe(0);
217
+ expect(game.getGameStatus()).toBe("idle");
218
+ expect(game.getPassCount()).toBe(0);
219
+
220
+ // Board should be empty
221
+ const board = game.getBoard();
222
+ for (let x = 0; x < 5; x++) {
223
+ for (let y = 0; y < 5; y++) {
224
+ for (let z = 0; z < 5; z++) {
225
+ expect(board[x][y][z]).toBe(StoneType.EMPTY);
226
+ }
227
+ }
228
+ }
229
+ });
230
+ });
231
+
232
+ describe("Getters", () => {
233
+ beforeEach(() => {
234
+ game.startGame();
235
+ });
236
+
237
+ it("should get stone at position", () => {
238
+ game.drop({ x: 2, y: 2, z: 2 });
239
+ const stone = game.getStone({ x: 2, y: 2, z: 2 });
240
+ expect(stone).toBe(StoneType.BLACK);
241
+ });
242
+
243
+ it("should return EMPTY for empty position", () => {
244
+ const stone = game.getStone({ x: 2, y: 2, z: 2 });
245
+ expect(stone).toBe(StoneType.EMPTY);
246
+ });
247
+
248
+ it("should get last step", () => {
249
+ game.drop({ x: 2, y: 2, z: 2 });
250
+ const lastStep = game.getLastStep();
251
+ expect(lastStep).toBeDefined();
252
+ expect(lastStep?.position).toEqual({ x: 2, y: 2, z: 2 });
253
+ });
254
+
255
+ it("should return null for last step when no moves", () => {
256
+ const lastStep = game.getLastStep();
257
+ expect(lastStep).toBeNull();
258
+ });
259
+
260
+ it("should get captured counts", () => {
261
+ const counts = game.getCapturedCounts();
262
+ expect(counts).toEqual({ black: 0, white: 0 });
263
+ });
264
+
265
+ it("should get game stats", () => {
266
+ game.drop({ x: 2, y: 2, z: 2 }); // Black
267
+ game.drop({ x: 3, y: 2, z: 2 }); // White
268
+
269
+ const stats = game.getStats();
270
+ expect(stats.totalMoves).toBe(2);
271
+ expect(stats.blackMoves).toBe(1);
272
+ expect(stats.whiteMoves).toBe(1);
273
+ expect(stats.territory).toBeDefined();
274
+ });
275
+ });
276
+
277
+ describe("Validation", () => {
278
+ beforeEach(() => {
279
+ game.startGame();
280
+ });
281
+
282
+ it("should validate valid move", () => {
283
+ const result = game.isValidMove({ x: 2, y: 2, z: 2 });
284
+ expect(result.valid).toBe(true);
285
+ });
286
+
287
+ it("should invalidate out of bounds move", () => {
288
+ const result = game.isValidMove({ x: 10, y: 10, z: 10 });
289
+ expect(result.valid).toBe(false);
290
+ expect(result.reason).toContain("out of bounds");
291
+ });
292
+
293
+ it("should invalidate occupied position", () => {
294
+ game.drop({ x: 2, y: 2, z: 2 });
295
+ const result = game.isValidMove({ x: 2, y: 2, z: 2 });
296
+ expect(result.valid).toBe(false);
297
+ expect(result.reason).toContain("occupied");
298
+ });
299
+ });
300
+ });
trigo-web/tests/game/trigoGame.fromTGN.test.ts ADDED
@@ -0,0 +1,319 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Tests for TrigoGame.fromTGN() - TGN Import Functionality
3
+ */
4
+
5
+ import { describe, it, expect, beforeAll } from "vitest";
6
+ import { TrigoGame, StoneType, validateTGN, TGNParseError } from "@inc/trigo/game";
7
+ import { initializeParsers } from "@inc/trigo/parserInit";
8
+
9
+ describe("TrigoGame.fromTGN() - TGN Import", () => {
10
+ // Initialize parsers before running tests
11
+ beforeAll(async () => {
12
+ await initializeParsers();
13
+ });
14
+
15
+ describe("Basic TGN Parsing", () => {
16
+ it("should parse empty TGN with default board", () => {
17
+ const tgn = ``;
18
+ const game = TrigoGame.fromTGN(tgn);
19
+
20
+ expect(game.getShape()).toEqual({ x: 5, y: 5, z: 5 });
21
+ expect(game.getGameStatus()).toBe("playing");
22
+ expect(game.getHistory()).toHaveLength(0);
23
+ });
24
+
25
+ it("should parse TGN with only metadata", () => {
26
+ const tgn = `
27
+ [Event "Test Game"]
28
+ [Black "Alice"]
29
+ [White "Bob"]
30
+ [Board 5x5x5]
31
+ `;
32
+ const game = TrigoGame.fromTGN(tgn);
33
+
34
+ expect(game.getShape()).toEqual({ x: 5, y: 5, z: 5 });
35
+ expect(game.getGameStatus()).toBe("playing");
36
+ });
37
+
38
+ it("should parse TGN with 3x3x3 board", () => {
39
+ const tgn = `[Board 3x3x3]`;
40
+ const game = TrigoGame.fromTGN(tgn);
41
+
42
+ expect(game.getShape()).toEqual({ x: 3, y: 3, z: 3 });
43
+ });
44
+
45
+ it("should parse TGN with 2D board (19x19)", () => {
46
+ const tgn = `[Board 19x19]`;
47
+ const game = TrigoGame.fromTGN(tgn);
48
+
49
+ expect(game.getShape()).toEqual({ x: 19, y: 19, z: 1 });
50
+ });
51
+ });
52
+
53
+ describe("Move Parsing", () => {
54
+ it("should parse and replay simple move sequence", () => {
55
+ const tgn = `
56
+ [Board 5x5x5]
57
+
58
+ 1. 000 y00
59
+ `;
60
+ const game = TrigoGame.fromTGN(tgn);
61
+
62
+ expect(game.getHistory()).toHaveLength(2);
63
+
64
+ // Check first move (black at center)
65
+ const board = game.getBoard();
66
+ expect(board[2][2][2]).toBe(StoneType.BLACK); // 000
67
+
68
+ // Check second move (white at y=3, 0=2, 0=2)
69
+ expect(board[3][2][2]).toBe(StoneType.WHITE); // y00 = [3,2,2]
70
+ });
71
+
72
+ it("should parse multiple rounds", () => {
73
+ const tgn = `
74
+ [Board 5x5x5]
75
+
76
+ 1. 000 y00
77
+ 2. 0aa zyy
78
+ `;
79
+ const game = TrigoGame.fromTGN(tgn);
80
+
81
+ expect(game.getHistory()).toHaveLength(4);
82
+
83
+ const board = game.getBoard();
84
+ expect(board[2][2][2]).toBe(StoneType.BLACK); // 000 = [2,2,2]
85
+ expect(board[3][2][2]).toBe(StoneType.WHITE); // y00 = [3,2,2]
86
+ expect(board[2][0][0]).toBe(StoneType.BLACK); // 0aa = [2,0,0]
87
+ expect(board[4][3][3]).toBe(StoneType.WHITE); // zyy = [4,3,3]
88
+ });
89
+
90
+ it("should parse pass move", () => {
91
+ const tgn = `
92
+ [Board 5x5x5]
93
+
94
+ 1. 000 Pass
95
+ `;
96
+ const game = TrigoGame.fromTGN(tgn);
97
+
98
+ expect(game.getHistory()).toHaveLength(2);
99
+
100
+ const history = game.getHistory();
101
+ expect(history[0].position).toEqual({ x: 2, y: 2, z: 2 });
102
+ expect(history[1].position).toBeUndefined();
103
+ });
104
+
105
+ it("should parse resign move", () => {
106
+ const tgn = `
107
+ [Board 5x5x5]
108
+
109
+ 1. 000 Resign
110
+ `;
111
+ const game = TrigoGame.fromTGN(tgn);
112
+
113
+ expect(game.getHistory()).toHaveLength(2);
114
+ expect(game.getGameStatus()).toBe("finished");
115
+ });
116
+
117
+ it("should handle incomplete round (black only)", () => {
118
+ const tgn = `
119
+ [Board 5x5x5]
120
+
121
+ 1. 000 y00
122
+ 2. 0aa
123
+ `;
124
+ const game = TrigoGame.fromTGN(tgn);
125
+
126
+ expect(game.getHistory()).toHaveLength(3);
127
+
128
+ const board = game.getBoard();
129
+ expect(board[2][2][2]).toBe(StoneType.BLACK); // 000 = [2,2,2]
130
+ expect(board[3][2][2]).toBe(StoneType.WHITE); // y00 = [3,2,2]
131
+ expect(board[2][0][0]).toBe(StoneType.BLACK); // 0aa = [2,0,0]
132
+ });
133
+ });
134
+
135
+ describe("2D Board Games", () => {
136
+ it("should parse 2D game correctly", () => {
137
+ const tgn = `
138
+ [Board 9x9]
139
+
140
+ 1. 00 y0
141
+ 2. 0a zy
142
+ `;
143
+ const game = TrigoGame.fromTGN(tgn);
144
+
145
+ expect(game.getShape()).toEqual({ x: 9, y: 9, z: 1 });
146
+ expect(game.getHistory()).toHaveLength(4);
147
+
148
+ const board = game.getBoard();
149
+ expect(board[4][4][0]).toBe(StoneType.BLACK); // 00 = [4,4,0] (center of 9x9)
150
+ expect(board[7][4][0]).toBe(StoneType.WHITE); // y0 = [7,4,0] (y=7 for 9x9)
151
+ });
152
+ });
153
+
154
+ describe("Complete Game Examples", () => {
155
+ it("should parse complete game with metadata and moves", () => {
156
+ const tgn = `
157
+ [Event "World Championship"]
158
+ [Site "Tokyo"]
159
+ [Date "2025.10.31"]
160
+ [Black "Alice"]
161
+ [White "Bob"]
162
+ [Board 5x5x5]
163
+ [Rules "Chinese"]
164
+ [Application "Trigo v1.0"]
165
+
166
+ 1. 000 y00
167
+ 2. 0y0 yy0
168
+ 3. aaa Pass
169
+ `;
170
+ const game = TrigoGame.fromTGN(tgn);
171
+
172
+ expect(game.getShape()).toEqual({ x: 5, y: 5, z: 5 });
173
+ expect(game.getHistory()).toHaveLength(6);
174
+
175
+ const board = game.getBoard();
176
+ expect(board[2][2][2]).toBe(StoneType.BLACK); // 000 = [2,2,2]
177
+ expect(board[3][2][2]).toBe(StoneType.WHITE); // y00 = [3,2,2]
178
+ expect(board[2][3][2]).toBe(StoneType.BLACK); // 0y0 = [2,3,2]
179
+ expect(board[3][3][2]).toBe(StoneType.WHITE); // yy0 = [3,3,2]
180
+ expect(board[0][0][0]).toBe(StoneType.BLACK); // aaa = [0,0,0]
181
+ });
182
+
183
+ it("should handle game with early resignation", () => {
184
+ const tgn = `
185
+ [Board 5x5x5]
186
+
187
+ 1. 000 y00
188
+ 2. Resign
189
+ `;
190
+ const game = TrigoGame.fromTGN(tgn);
191
+
192
+ expect(game.getHistory()).toHaveLength(3);
193
+ expect(game.getGameStatus()).toBe("finished");
194
+
195
+ const result = game.getGameResult();
196
+ expect(result?.winner).toBe("white");
197
+ expect(result?.reason).toBe("resignation");
198
+ });
199
+ });
200
+
201
+ describe("TGN Validation", () => {
202
+ it("should validate correct TGN", () => {
203
+ const tgn = `
204
+ [Board 5x5x5]
205
+
206
+ 1. 000 y00
207
+ `;
208
+ const result = validateTGN(tgn);
209
+
210
+ expect(result.valid).toBe(true);
211
+ expect(result.error).toBeUndefined();
212
+ });
213
+
214
+ it("should detect invalid TGN", () => {
215
+ const tgn = `[Board invalid]`;
216
+ const result = validateTGN(tgn);
217
+
218
+ expect(result.valid).toBe(false);
219
+ expect(result.error).toBeDefined();
220
+ });
221
+
222
+ it("should throw TGNParseError on invalid input", () => {
223
+ const tgn = `[Invalid Tag Without Value`;
224
+
225
+ expect(() => TrigoGame.fromTGN(tgn)).toThrow();
226
+ });
227
+ });
228
+
229
+ describe("Roundtrip Testing", () => {
230
+ it("should roundtrip: export to TGN and reimport", () => {
231
+ // Create original game
232
+ const game1 = new TrigoGame({ x: 5, y: 5, z: 5 });
233
+ game1.startGame();
234
+ game1.drop({ x: 2, y: 2, z: 2 }); // 000
235
+ game1.drop({ x: 4, y: 2, z: 2 }); // y00
236
+ game1.drop({ x: 2, y: 4, z: 2 }); // 0y0
237
+ game1.pass();
238
+
239
+ // Export to TGN
240
+ const tgn = game1.toTGN({
241
+ event: "Roundtrip Test",
242
+ black: "Player 1",
243
+ white: "Player 2"
244
+ });
245
+
246
+ // Import from TGN
247
+ const game2 = TrigoGame.fromTGN(tgn);
248
+
249
+ // Verify board state matches
250
+ const board1 = game1.getBoard();
251
+ const board2 = game2.getBoard();
252
+
253
+ for (let x = 0; x < 5; x++) {
254
+ for (let y = 0; y < 5; y++) {
255
+ for (let z = 0; z < 5; z++) {
256
+ expect(board2[x][y][z]).toBe(board1[x][y][z]);
257
+ }
258
+ }
259
+ }
260
+
261
+ // Verify step history length
262
+ expect(game2.getHistory()).toHaveLength(game1.getHistory().length);
263
+ });
264
+
265
+ it("should roundtrip complex game", () => {
266
+ // Create a more complex game
267
+ const game1 = new TrigoGame({ x: 3, y: 3, z: 3 });
268
+ game1.startGame();
269
+
270
+ // Play several moves
271
+ game1.drop({ x: 1, y: 1, z: 1 }); // Center
272
+ game1.drop({ x: 0, y: 0, z: 0 });
273
+ game1.drop({ x: 2, y: 2, z: 2 });
274
+ game1.drop({ x: 0, y: 2, z: 0 });
275
+ game1.pass();
276
+ game1.drop({ x: 1, y: 0, z: 1 });
277
+
278
+ const tgn = game1.toTGN({
279
+ application: "Test Suite"
280
+ });
281
+
282
+ const game2 = TrigoGame.fromTGN(tgn);
283
+
284
+ // Boards should be identical
285
+ const board1 = game1.getBoard();
286
+ const board2 = game2.getBoard();
287
+
288
+ for (let x = 0; x < 3; x++) {
289
+ for (let y = 0; y < 3; y++) {
290
+ for (let z = 0; z < 3; z++) {
291
+ expect(board2[x][y][z]).toBe(board1[x][y][z]);
292
+ }
293
+ }
294
+ }
295
+ });
296
+ });
297
+
298
+ describe("Error Handling", () => {
299
+ it("should handle invalid coordinates gracefully", () => {
300
+ const tgn = `
301
+ [Board 5x5x5]
302
+
303
+ 1. xyz 000
304
+ `;
305
+ // Should throw an error during parsing (now synchronous)
306
+ expect(() => {
307
+ TrigoGame.fromTGN(tgn);
308
+ }).toThrow();
309
+ });
310
+
311
+ it("should handle invalid board shape", () => {
312
+ const tgn = `[Board notaboard]`;
313
+
314
+ // Parser should reject this
315
+ const result = validateTGN(tgn);
316
+ expect(result.valid).toBe(false);
317
+ });
318
+ });
319
+ });
trigo-web/tests/game/trigoGame.history.test.ts ADDED
@@ -0,0 +1,301 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * TrigoGame History Management Tests
3
+ *
4
+ * Tests undo, redo, and jump-to-step functionality
5
+ */
6
+
7
+ import { describe, it, expect, beforeEach } from 'vitest'
8
+ import { TrigoGame, StoneType } from '@inc/trigo/game'
9
+ import type { BoardShape } from '@inc/trigo/types'
10
+
11
+
12
+ describe('TrigoGame - History Management', () => {
13
+ let game: TrigoGame
14
+ const defaultShape: BoardShape = { x: 5, y: 5, z: 1 } // Use 2D board for capture tests
15
+
16
+ beforeEach(() => {
17
+ game = new TrigoGame(defaultShape)
18
+ game.startGame()
19
+ })
20
+
21
+
22
+ describe('Undo', () => {
23
+ it('should undo last move', () => {
24
+ game.drop({ x: 2, y: 2, z: 0 })
25
+ expect(game.getCurrentStep()).toBe(1)
26
+
27
+ const success = game.undo()
28
+ expect(success).toBe(true)
29
+ expect(game.getCurrentStep()).toBe(0)
30
+
31
+ const board = game.getBoard()
32
+ expect(board[2][2][0]).toBe(StoneType.EMPTY)
33
+ })
34
+
35
+ it('should restore previous player after undo', () => {
36
+ game.drop({ x: 2, y: 2, z: 0 }) // Black moves
37
+ expect(game.getCurrentPlayer()).toBe(StoneType.WHITE)
38
+
39
+ game.undo()
40
+ expect(game.getCurrentPlayer()).toBe(StoneType.BLACK)
41
+ })
42
+
43
+ it('should not undo when at start', () => {
44
+ const success = game.undo()
45
+ expect(success).toBe(false)
46
+ })
47
+
48
+ it('should undo multiple moves', () => {
49
+ game.drop({ x: 2, y: 2, z: 0 }) // Black
50
+ game.drop({ x: 3, y: 2, z: 0 }) // White
51
+ game.drop({ x: 2, y: 3, z: 0 }) // Black
52
+
53
+ expect(game.getCurrentStep()).toBe(3)
54
+
55
+ game.undo() // Undo black's move
56
+ expect(game.getCurrentStep()).toBe(2)
57
+ expect(game.getCurrentPlayer()).toBe(StoneType.BLACK)
58
+
59
+ game.undo() // Undo white's move
60
+ expect(game.getCurrentStep()).toBe(1)
61
+ expect(game.getCurrentPlayer()).toBe(StoneType.WHITE)
62
+
63
+ game.undo() // Undo black's move
64
+ expect(game.getCurrentStep()).toBe(0)
65
+ expect(game.getCurrentPlayer()).toBe(StoneType.BLACK)
66
+ })
67
+
68
+ it('should undo pass move', () => {
69
+ game.pass()
70
+ expect(game.getCurrentStep()).toBe(1)
71
+ expect(game.getPassCount()).toBe(1)
72
+
73
+ game.undo()
74
+ expect(game.getCurrentStep()).toBe(0)
75
+ expect(game.getPassCount()).toBe(0)
76
+ })
77
+
78
+ it('should restore captured stones on undo', () => {
79
+ // Create a capture scenario: surround white stone with black
80
+ game.drop({ x: 2, y: 2, z: 0 }) // Black
81
+ game.drop({ x: 3, y: 2, z: 0 }) // White (target)
82
+ game.drop({ x: 4, y: 2, z: 0 }) // Black (surrounds white)
83
+ game.drop({ x: 0, y: 0, z: 0 }) // White elsewhere
84
+ game.drop({ x: 3, y: 3, z: 0 }) // Black surrounds
85
+ game.drop({ x: 0, y: 1, z: 0 }) // White elsewhere
86
+ game.drop({ x: 3, y: 1, z: 0 }) // Black surrounds
87
+
88
+ // White stone should be captured
89
+ let board = game.getBoard()
90
+ expect(board[3][2][0]).toBe(StoneType.EMPTY)
91
+
92
+ // Undo last move
93
+ game.undo()
94
+
95
+ // White stone should be restored
96
+ board = game.getBoard()
97
+ expect(board[3][2][0]).toBe(StoneType.WHITE)
98
+ })
99
+ })
100
+
101
+
102
+ describe('Redo', () => {
103
+ it('should redo undone move', () => {
104
+ game.drop({ x: 2, y: 2, z: 0 })
105
+ game.undo()
106
+
107
+ const success = game.redo()
108
+ expect(success).toBe(true)
109
+ expect(game.getCurrentStep()).toBe(1)
110
+
111
+ const board = game.getBoard()
112
+ expect(board[2][2][0]).toBe(StoneType.BLACK)
113
+ })
114
+
115
+ it('should not redo when at end of history', () => {
116
+ game.drop({ x: 2, y: 2, z: 0 })
117
+ const success = game.redo()
118
+ expect(success).toBe(false)
119
+ })
120
+
121
+ it('should redo multiple moves', () => {
122
+ game.drop({ x: 2, y: 2, z: 0 }) // Black
123
+ game.drop({ x: 3, y: 2, z: 0 }) // White
124
+ game.drop({ x: 2, y: 3, z: 0 }) // Black
125
+
126
+ game.undo()
127
+ game.undo()
128
+ game.undo()
129
+
130
+ expect(game.getCurrentStep()).toBe(0)
131
+
132
+ game.redo()
133
+ expect(game.getCurrentStep()).toBe(1)
134
+ expect(game.getCurrentPlayer()).toBe(StoneType.WHITE)
135
+
136
+ game.redo()
137
+ expect(game.getCurrentStep()).toBe(2)
138
+ expect(game.getCurrentPlayer()).toBe(StoneType.BLACK)
139
+
140
+ game.redo()
141
+ expect(game.getCurrentStep()).toBe(3)
142
+ expect(game.getCurrentPlayer()).toBe(StoneType.WHITE)
143
+ })
144
+
145
+ it('should check canRedo correctly', () => {
146
+ expect(game.canRedo()).toBe(false)
147
+
148
+ game.drop({ x: 2, y: 2, z: 0 })
149
+ expect(game.canRedo()).toBe(false)
150
+
151
+ game.undo()
152
+ expect(game.canRedo()).toBe(true)
153
+
154
+ game.redo()
155
+ expect(game.canRedo()).toBe(false)
156
+ })
157
+
158
+ it('should truncate history when making new move after undo', () => {
159
+ game.drop({ x: 2, y: 2, z: 0 }) // Move 1
160
+ game.drop({ x: 3, y: 2, z: 0 }) // Move 2
161
+ game.drop({ x: 2, y: 3, z: 0 }) // Move 3
162
+
163
+ game.undo() // Back to move 2
164
+ game.undo() // Back to move 1
165
+
166
+ // Make a new move - should truncate moves 2 and 3
167
+ game.drop({ x: 4, y: 4, z: 0 })
168
+
169
+ expect(game.getCurrentStep()).toBe(2)
170
+ expect(game.canRedo()).toBe(false)
171
+
172
+ const history = game.getHistory()
173
+ expect(history.length).toBe(2)
174
+ })
175
+ })
176
+
177
+
178
+ describe('Jump to Step', () => {
179
+ beforeEach(() => {
180
+ // Set up a game with several moves
181
+ game.drop({ x: 2, y: 2, z: 0 }) // Move 0
182
+ game.drop({ x: 3, y: 2, z: 0 }) // Move 1
183
+ game.drop({ x: 2, y: 3, z: 0 }) // Move 2
184
+ game.drop({ x: 3, y: 3, z: 0 }) // Move 3
185
+ })
186
+
187
+ it('should jump to specific step in history', () => {
188
+ expect(game.getCurrentStep()).toBe(4)
189
+
190
+ const success = game.jumpToStep(1)
191
+ expect(success).toBe(true)
192
+ expect(game.getCurrentStep()).toBe(1)
193
+ })
194
+
195
+ it('should jump to initial state with index 0', () => {
196
+ const success = game.jumpToStep(0)
197
+ expect(success).toBe(true)
198
+ expect(game.getCurrentStep()).toBe(0)
199
+
200
+ // Board should be empty
201
+ const board = game.getBoard()
202
+ expect(board[2][2][0]).toBe(StoneType.EMPTY)
203
+ expect(board[3][2][0]).toBe(StoneType.EMPTY)
204
+
205
+ // Current player should be BLACK (ready to make first move)
206
+ expect(game.getCurrentPlayer()).toBe(StoneType.BLACK)
207
+ })
208
+
209
+ it('should rebuild board correctly at target step', () => {
210
+ game.jumpToStep(2) // After 2 moves
211
+
212
+ const board = game.getBoard()
213
+ expect(board[2][2][0]).toBe(StoneType.BLACK)
214
+ expect(board[3][2][0]).toBe(StoneType.WHITE)
215
+ expect(board[2][3][0]).toBe(StoneType.EMPTY) // Not placed yet
216
+ })
217
+
218
+ it('should set correct player after jump', () => {
219
+ game.jumpToStep(2) // After 2 moves (black, white)
220
+ expect(game.getCurrentPlayer()).toBe(StoneType.BLACK)
221
+
222
+ game.jumpToStep(3) // After 3 moves
223
+ expect(game.getCurrentPlayer()).toBe(StoneType.WHITE)
224
+ })
225
+
226
+ it('should not jump to invalid index', () => {
227
+ const successNegative = game.jumpToStep(-5)
228
+ expect(successNegative).toBe(false)
229
+
230
+ const successTooLarge = game.jumpToStep(100)
231
+ expect(successTooLarge).toBe(false)
232
+ })
233
+
234
+ it('should do nothing when jumping to current step', () => {
235
+ const currentStep = game.getCurrentStep()
236
+ const success = game.jumpToStep(currentStep)
237
+ expect(success).toBe(false)
238
+ expect(game.getCurrentStep()).toBe(currentStep)
239
+ })
240
+
241
+ it('should allow jumping forward and backward', () => {
242
+ game.jumpToStep(1)
243
+ expect(game.getCurrentStep()).toBe(1)
244
+
245
+ game.jumpToStep(3)
246
+ expect(game.getCurrentStep()).toBe(3)
247
+
248
+ game.jumpToStep(0)
249
+ expect(game.getCurrentStep()).toBe(0)
250
+ })
251
+ })
252
+
253
+
254
+ describe('Complex History Scenarios', () => {
255
+ it('should handle undo after jump', () => {
256
+ game.drop({ x: 2, y: 2, z: 0 }) // Move 1
257
+ game.drop({ x: 3, y: 2, z: 0 }) // Move 2
258
+
259
+ game.jumpToStep(1) // Jump to to step 1 (after move 1)
260
+ expect(game.getCurrentStep()).toBe(1)
261
+
262
+ game.undo() // Should go back to start
263
+
264
+ expect(game.getCurrentStep()).toBe(0)
265
+ const board = game.getBoard()
266
+ expect(board[2][2][0]).toBe(StoneType.EMPTY)
267
+ })
268
+
269
+ it('should handle redo after jump', () => {
270
+ game.drop({ x: 2, y: 2, z: 0 }) // Move 1
271
+ game.drop({ x: 3, y: 2, z: 0 }) // Move 2
272
+
273
+ game.jumpToStep(1) // Jump to to step 1 (after move 1)
274
+ game.redo() // Should move forward to move 2
275
+
276
+ expect(game.getCurrentStep()).toBe(2)
277
+ const board = game.getBoard()
278
+ expect(board[3][2][0]).toBe(StoneType.WHITE)
279
+ })
280
+
281
+ it('should maintain history integrity across operations', () => {
282
+ game.drop({ x: 2, y: 2, z: 0 })
283
+ game.drop({ x: 3, y: 2, z: 0 })
284
+ game.drop({ x: 2, y: 3, z: 0 })
285
+
286
+ const originalHistory = game.getHistory()
287
+ expect(originalHistory.length).toBe(3)
288
+
289
+ // Jump back and forth
290
+ game.jumpToStep(0)
291
+ game.jumpToStep(2)
292
+ game.undo()
293
+ game.redo()
294
+
295
+ // History should remain unchanged
296
+ const finalHistory = game.getHistory()
297
+ expect(finalHistory.length).toBe(3)
298
+ expect(finalHistory).toEqual(originalHistory)
299
+ })
300
+ })
301
+ })
trigo-web/tests/game/trigoGame.parserInit.test.ts ADDED
@@ -0,0 +1,160 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Test for TGN Parser Initialization and Functionality
3
+ */
4
+
5
+ import { describe, it, expect, beforeAll } from "vitest";
6
+ import { TrigoGame, StoneType, validateTGN } from "@inc/trigo/game";
7
+ import { initializeParsers } from "@inc/trigo/parserInit";
8
+
9
+
10
+ describe("TGN Parser - Full Integration (with synchronous API)", () => {
11
+
12
+ // Initialize parsers before running tests
13
+ beforeAll(async () => {
14
+ await initializeParsers();
15
+ });
16
+
17
+
18
+ describe("Parser Initialization", () => {
19
+
20
+ it("should initialize parsers successfully", async () => {
21
+ // If we get here without error, initialization succeeded
22
+ expect(true).toBe(true);
23
+ });
24
+
25
+ });
26
+
27
+
28
+ describe("Synchronous TGN Parsing", () => {
29
+
30
+ it("should validate TGN synchronously (no await)", () => {
31
+ const tgn = `[Board 5x5x5]
32
+ 1. 000 y00`;
33
+
34
+ // This should NOT be an async function - should work synchronously
35
+ const result = validateTGN(tgn);
36
+
37
+ expect(result.valid).toBe(true);
38
+ expect(result.error).toBeUndefined();
39
+ });
40
+
41
+
42
+ it("should detect invalid TGN synchronously", () => {
43
+ const tgn = `[Board invalid]`;
44
+
45
+ // Synchronous validation
46
+ const result = validateTGN(tgn);
47
+
48
+ expect(result.valid).toBe(false);
49
+ expect(result.error).toBeDefined();
50
+ });
51
+
52
+ });
53
+
54
+
55
+ describe("Synchronous Game Import (fromTGN)", () => {
56
+
57
+ it("should import TGN synchronously (no await)", () => {
58
+ const tgn = `[Event "Test Game"]
59
+ [Board 5x5x5]
60
+ [Black "Alice"]
61
+ [White "Bob"]
62
+
63
+ 1. 000 y00
64
+ 2. 0y0 yy0`;
65
+
66
+ // This should NOT be an async function - should work synchronously
67
+ const game = TrigoGame.fromTGN(tgn);
68
+
69
+ expect(game.getShape()).toEqual({ x: 5, y: 5, z: 5 });
70
+ expect(game.getHistory()).toHaveLength(4);
71
+ expect(game.getGameStatus()).toBe("playing");
72
+ });
73
+
74
+
75
+ it("should replay moves correctly after import", () => {
76
+ const tgn = `[Board 5x5x5]
77
+ 1. 000 y00
78
+ 2. 0y0 yy0`;
79
+
80
+ const game = TrigoGame.fromTGN(tgn);
81
+ const board = game.getBoard();
82
+
83
+ expect(board[2][2][2]).toBe(StoneType.BLACK); // 000
84
+ expect(board[3][2][2]).toBe(StoneType.WHITE); // y00
85
+ expect(board[2][3][2]).toBe(StoneType.BLACK); // 0y0
86
+ expect(board[3][3][2]).toBe(StoneType.WHITE); // yy0
87
+ });
88
+
89
+ });
90
+
91
+
92
+ describe("Round-trip Testing", () => {
93
+
94
+ it("should export and re-import TGN without loss", () => {
95
+ // Create original game
96
+ const game1 = new TrigoGame({ x: 5, y: 5, z: 5 });
97
+ game1.startGame();
98
+ game1.drop({ x: 2, y: 2, z: 2 });
99
+ game1.drop({ x: 3, y: 2, z: 2 });
100
+ game1.drop({ x: 2, y: 3, z: 2 });
101
+
102
+ // Export to TGN
103
+ const tgn = game1.toTGN({
104
+ event: "Test",
105
+ black: "Alice",
106
+ white: "Bob"
107
+ });
108
+
109
+ // Re-import - using synchronous fromTGN (no await!)
110
+ const game2 = TrigoGame.fromTGN(tgn);
111
+
112
+ // Verify boards match
113
+ const board1 = game1.getBoard();
114
+ const board2 = game2.getBoard();
115
+
116
+ for (let x = 0; x < 5; x++) {
117
+ for (let y = 0; y < 5; y++) {
118
+ for (let z = 0; z < 5; z++) {
119
+ expect(board2[x][y][z]).toBe(board1[x][y][z]);
120
+ }
121
+ }
122
+ }
123
+
124
+ // Verify move counts match
125
+ expect(game2.getHistory()).toHaveLength(game1.getHistory().length);
126
+ });
127
+
128
+ });
129
+
130
+
131
+ describe("Lotus Architecture Compliance", () => {
132
+
133
+ it("should use pre-built parser from public/lib", () => {
134
+ // This test verifies that the parser was loaded successfully
135
+ // If it throws an error, the parser module was not initialized
136
+ const tgn = `[Board 5x5x5]`;
137
+ const result = validateTGN(tgn);
138
+ expect(result.valid).toBe(true);
139
+ });
140
+
141
+
142
+ it("should support both browser and Node.js equally", () => {
143
+ // The same synchronous API works in both environments
144
+ // Browser: imports from /lib/tgnParser.js (served by Vite)
145
+ // Node.js: imports from public/lib/tgnParser.js
146
+
147
+ const tgn = `[Board 5x5x5]
148
+ 1. 000 y00`;
149
+
150
+ // No environment checks needed - just use the API
151
+ const result = validateTGN(tgn);
152
+ const game = TrigoGame.fromTGN(tgn);
153
+
154
+ expect(result.valid).toBe(true);
155
+ expect(game.getShape()).toEqual({ x: 5, y: 5, z: 5 });
156
+ });
157
+
158
+ });
159
+
160
+ });
trigo-web/tests/game/trigoGame.rules.test.ts ADDED
@@ -0,0 +1,356 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * TrigoGame Rules Tests
3
+ *
4
+ * Tests capture logic, Ko rule, suicide rule, and territory calculation
5
+ */
6
+
7
+ import { describe, it, expect, beforeEach } from 'vitest'
8
+ import { TrigoGame, StoneType } from '@inc/trigo/game'
9
+ import type { BoardShape } from '@inc/trigo/types'
10
+
11
+
12
+ describe('TrigoGame - Game Rules', () => {
13
+ let game: TrigoGame
14
+ const defaultShape: BoardShape = { x: 5, y: 5, z: 1 } // 2D board (single z layer)
15
+
16
+ beforeEach(() => {
17
+ game = new TrigoGame(defaultShape)
18
+ game.startGame()
19
+ })
20
+
21
+
22
+ describe('Capture Logic', () => {
23
+ it('should capture a surrounded stone (2D case)', () => {
24
+ // Surround a white stone with black on 2D board (z=0)
25
+ // B . B
26
+ // B W B
27
+ // . B .
28
+
29
+ game.drop({ x: 2, y: 2, z: 0 }) // Black left
30
+ game.drop({ x: 3, y: 2, z: 0 }) // White (target)
31
+ game.drop({ x: 4, y: 2, z: 0 }) // Black right
32
+ game.drop({ x: 0, y: 0, z: 0 }) // White elsewhere
33
+ game.drop({ x: 3, y: 3, z: 0 }) // Black bottom
34
+ game.drop({ x: 0, y: 1, z: 0 }) // White elsewhere
35
+ game.drop({ x: 3, y: 1, z: 0 }) // Black top - completes capture
36
+
37
+ // White stone at (3,2,0) should be captured
38
+ const board = game.getBoard()
39
+ expect(board[3][2][0]).toBe(StoneType.EMPTY)
40
+ })
41
+
42
+ it('should track captured stones in history', () => {
43
+ // Simple capture scenario on 2D board
44
+ game.drop({ x: 2, y: 2, z: 0 }) // Black left
45
+ game.drop({ x: 3, y: 2, z: 0 }) // White (target)
46
+ game.drop({ x: 4, y: 2, z: 0 }) // Black right
47
+ game.drop({ x: 0, y: 0, z: 0 }) // White elsewhere
48
+ game.drop({ x: 3, y: 3, z: 0 }) // Black bottom
49
+ game.drop({ x: 0, y: 1, z: 0 }) // White elsewhere
50
+ game.drop({ x: 3, y: 1, z: 0 }) // Black top - captures
51
+
52
+ const lastStep = game.getLastStep()
53
+ expect(lastStep).toBeDefined()
54
+ expect(lastStep?.capturedPositions).toBeDefined()
55
+ expect(lastStep?.capturedPositions?.length).toBeGreaterThan(0)
56
+ })
57
+
58
+ it('should update captured counts', () => {
59
+ // Capture one white stone on 2D board
60
+ game.drop({ x: 2, y: 2, z: 0 }) // Black left
61
+ game.drop({ x: 3, y: 2, z: 0 }) // White (target)
62
+ game.drop({ x: 4, y: 2, z: 0 }) // Black right
63
+ game.drop({ x: 0, y: 0, z: 0 }) // White elsewhere
64
+ game.drop({ x: 3, y: 3, z: 0 }) // Black bottom
65
+ game.drop({ x: 0, y: 1, z: 0 }) // White elsewhere
66
+ game.drop({ x: 3, y: 1, z: 0 }) // Black top - captures
67
+
68
+ const counts = game.getCapturedCounts()
69
+ expect(counts.white).toBeGreaterThan(0)
70
+ })
71
+
72
+ it('should not capture stones with liberties', () => {
73
+ // Place stones but leave an escape route
74
+ game.drop({ x: 2, y: 2, z: 0 }) // Black
75
+ game.drop({ x: 3, y: 2, z: 0 }) // White
76
+ game.drop({ x: 3, y: 3, z: 0 }) // Black
77
+ game.drop({ x: 4, y: 2, z: 0 }) // White still has liberties
78
+
79
+ const board = game.getBoard()
80
+ expect(board[3][2][0]).toBe(StoneType.WHITE) // Not captured
81
+ })
82
+ })
83
+
84
+
85
+ describe('Ko Rule (打劫)', () => {
86
+ // Ko Rule (from Wikipedia):
87
+ // "A special rule that prevents immediate repetition of position, by a short 'loop'
88
+ // in which a single stone is captured, and another single stone immediately taken back.
89
+ // The immediate recapture is forbidden, for ONE TURN only."
90
+ //
91
+ // Example Ko situation:
92
+ // Initial: After BLACK captures: After WHITE recaptures (ILLEGAL):
93
+ // . B . . B . . B .
94
+ // B W B --> B . B --> B W B (same as initial!)
95
+ // . W . . W . . W .
96
+ //
97
+ // The Ko rule implementation checks:
98
+ // 1. Previous move captured exactly ONE stone
99
+ // 2. Current move would capture exactly ONE stone
100
+ // 3. Player is placing at the previously captured position
101
+ // → If all true, it's a Ko violation (immediate recapture forbidden)
102
+
103
+ it('should detect Ko using TGN example: bb ab ca ba aa', () => {
104
+ // Real Ko example in TGN notation:
105
+ // 1. bb ab (BLACK at (1,1), WHITE at (0,1))
106
+ // 2. ca ba (BLACK at (2,0), WHITE at (1,0))
107
+ // 3. aa (BLACK at (0,0) - captures WHITE at ba)
108
+ // Then WHITE playing at ba would be Ko violation
109
+ //
110
+ // Board after moves 1-2: After move 3 (BLACK aa):
111
+ // a b c a b c
112
+ // a . W B (y=0) a B . B (y=0, WHITE captured)
113
+ // b W B . (y=1) b W B . (y=1)
114
+
115
+ game.drop({ x: 1, y: 1, z: 0 }) // Move 1: BLACK bb
116
+ game.drop({ x: 0, y: 1, z: 0 }) // Move 1: WHITE ab
117
+ game.drop({ x: 2, y: 0, z: 0 }) // Move 2: BLACK ca
118
+ game.drop({ x: 1, y: 0, z: 0 }) // Move 2: WHITE ba
119
+
120
+ // Before capture, verify WHITE at ba (1,0) is surrounded
121
+ let board = game.getBoard()
122
+ expect(board[1][0][0]).toBe(StoneType.WHITE) // WHITE at ba
123
+
124
+ // Move 3: BLACK aa captures WHITE at ba
125
+ game.drop({ x: 0, y: 0, z: 0 }) // BLACK aa
126
+
127
+ // Verify capture happened
128
+ board = game.getBoard()
129
+ expect(board[1][0][0]).toBe(StoneType.EMPTY) // ba is now empty
130
+ expect(board[0][0][0]).toBe(StoneType.BLACK) // aa has BLACK
131
+
132
+ const lastStep = game.getLastStep()
133
+ expect(lastStep?.capturedPositions).toBeDefined()
134
+ expect(lastStep?.capturedPositions?.length).toBe(1)
135
+ expect(lastStep?.capturedPositions?.[0]).toEqual({ x: 1, y: 0, z: 0 })
136
+
137
+ // Now if WHITE immediately plays at ba, it would capture BLACK at aa
138
+ // and recreate the original position - this is Ko!
139
+ const result = game.isValidMove({ x: 1, y: 0, z: 0 })
140
+
141
+ expect(result.valid).toBe(false)
142
+ expect(result.reason).toContain('Ko')
143
+ })
144
+
145
+ it('should allow recapture after intervening moves (Ko resolved)', () => {
146
+ // Same Ko setup as above
147
+ game.drop({ x: 1, y: 1, z: 0 }) // BLACK bb
148
+ game.drop({ x: 0, y: 1, z: 0 }) // WHITE ab
149
+ game.drop({ x: 2, y: 0, z: 0 }) // BLACK ca
150
+ game.drop({ x: 1, y: 0, z: 0 }) // WHITE ba
151
+ game.drop({ x: 0, y: 0, z: 0 }) // BLACK aa - captures WHITE at ba
152
+
153
+ // Per Ko rule: "immediate recapture is forbidden, for ONE TURN only"
154
+ // WHITE must play elsewhere first (Ko threat)
155
+ game.drop({ x: 3, y: 3, z: 0 }) // WHITE plays elsewhere
156
+ game.drop({ x: 4, y: 4, z: 0 }) // BLACK responds elsewhere
157
+
158
+ // After intervening moves, Ko is resolved
159
+ // WHITE can now play at ba
160
+ const result = game.isValidMove({ x: 1, y: 0, z: 0 })
161
+
162
+ expect(result.valid).toBe(true)
163
+ expect(result.reason).toBeUndefined()
164
+ })
165
+
166
+ it('should verify Ko rule logic after single-stone capture', () => {
167
+ // Additional test using the simple capture pattern
168
+ game.drop({ x: 2, y: 2, z: 0 }) // BLACK
169
+ game.drop({ x: 3, y: 2, z: 0 }) // WHITE (will be captured)
170
+ game.drop({ x: 4, y: 2, z: 0 }) // BLACK right
171
+ game.drop({ x: 0, y: 0, z: 0 }) // WHITE elsewhere
172
+ game.drop({ x: 3, y: 3, z: 0 }) // BLACK bottom
173
+ game.drop({ x: 0, y: 1, z: 0 }) // WHITE elsewhere
174
+ game.drop({ x: 3, y: 1, z: 0 }) // BLACK top - captures WHITE at (3,2,0)
175
+
176
+ // Verify capture happened and was recorded
177
+ const board = game.getBoard()
178
+ expect(board[3][2][0]).toBe(StoneType.EMPTY)
179
+
180
+ const lastStep = game.getLastStep()
181
+ expect(lastStep?.capturedPositions).toBeDefined()
182
+ expect(lastStep?.capturedPositions?.length).toBe(1)
183
+ expect(lastStep?.capturedPositions?.[0]).toEqual({ x: 3, y: 2, z: 0 })
184
+
185
+ // Move validation at the captured position should run Ko checks
186
+ // In this specific case, WHITE playing at (3,2,0) would be suicide (not Ko)
187
+ // because WHITE cannot capture any BLACK stones from that position
188
+ const result = game.isValidMove({ x: 3, y: 2, z: 0 })
189
+ expect(result.valid).toBe(false)
190
+ // Will be 'suicide' in this case, but Ko logic is still checked first
191
+ })
192
+ })
193
+
194
+
195
+ describe('Suicide Rule', () => {
196
+ it('should prevent suicide move (placing stone with no liberties)', () => {
197
+ // Surround a position completely with opponent's stones on 2D board
198
+ game.drop({ x: 1, y: 2, z: 0 }) // Black elsewhere
199
+ game.drop({ x: 2, y: 2, z: 0 }) // White
200
+ game.drop({ x: 1, y: 3, z: 0 }) // Black elsewhere
201
+ game.drop({ x: 4, y: 2, z: 0 }) // White
202
+ game.drop({ x: 1, y: 1, z: 0 }) // Black elsewhere
203
+ game.drop({ x: 3, y: 3, z: 0 }) // White
204
+ game.drop({ x: 1, y: 0, z: 0 }) // Black elsewhere
205
+ game.drop({ x: 3, y: 1, z: 0 }) // White
206
+
207
+ // Position (3,2,0) is now surrounded by white stones (4 neighbors on 2D)
208
+ // Black trying to play there would be suicide
209
+ const result = game.isValidMove({ x: 3, y: 2, z: 0 })
210
+
211
+ if (result.valid === false) {
212
+ expect(result.reason).toContain('suicide')
213
+ }
214
+ })
215
+
216
+ it('should allow move that captures opponent (not suicide)', () => {
217
+ // Set up a position where placing a stone captures opponent
218
+ // thereby creating liberties for itself
219
+ game.drop({ x: 2, y: 2, z: 0 }) // Black
220
+ game.drop({ x: 3, y: 2, z: 0 }) // White (will be captured)
221
+ game.drop({ x: 4, y: 2, z: 0 }) // Black
222
+ game.drop({ x: 3, y: 1, z: 0 }) // White elsewhere
223
+ game.drop({ x: 3, y: 3, z: 0 }) // Black
224
+ game.drop({ x: 1, y: 1, z: 0 }) // White elsewhere
225
+
226
+ // Black placing at (2,1,0) captures white, so it's not suicide
227
+ const result = game.isValidMove({ x: 2, y: 1, z: 0 })
228
+ expect(result.valid).toBe(true)
229
+ })
230
+ })
231
+
232
+
233
+ describe('Territory Calculation', () => {
234
+ it('should calculate territory for empty board', () => {
235
+ const territory = game.getTerritory()
236
+ expect(territory).toBeDefined()
237
+ expect(territory.black).toBe(0)
238
+ expect(territory.white).toBe(0)
239
+ expect(territory.neutral).toBeGreaterThan(0)
240
+ })
241
+
242
+ it('should calculate territory with some stones', () => {
243
+ // Place a few stones
244
+ game.drop({ x: 0, y: 0, z: 0 }) // Black
245
+ game.drop({ x: 4, y: 4, z: 4 }) // White
246
+ game.drop({ x: 0, y: 0, z: 1 }) // Black
247
+ game.drop({ x: 4, y: 4, z: 3 }) // White
248
+
249
+ const territory = game.getTerritory()
250
+ expect(territory).toBeDefined()
251
+ // With stones placed, some territory should be claimed
252
+ expect(territory.black + territory.white + territory.neutral).toBeGreaterThan(0)
253
+ })
254
+
255
+ it('should cache territory calculation', () => {
256
+ const territory1 = game.getTerritory()
257
+ const territory2 = game.getTerritory()
258
+
259
+ // Should return same result (cached)
260
+ expect(territory1).toEqual(territory2)
261
+ })
262
+
263
+ it('should invalidate territory cache after move', () => {
264
+ const territory1 = game.getTerritory()
265
+
266
+ game.drop({ x: 2, y: 2, z: 2 })
267
+
268
+ const territory2 = game.getTerritory()
269
+
270
+ // Territory may have changed
271
+ // At minimum, it should recalculate (not use stale cache)
272
+ expect(territory2).toBeDefined()
273
+ })
274
+ })
275
+
276
+
277
+ describe('Game End Conditions', () => {
278
+ it('should end game and calculate winner on double pass', () => {
279
+ // Set up a game state
280
+ game.drop({ x: 0, y: 0, z: 0 }) // Black
281
+ game.drop({ x: 4, y: 4, z: 4 }) // White
282
+
283
+ // Double pass
284
+ game.pass() // Black
285
+ game.pass() // White
286
+
287
+ expect(game.getGameStatus()).toBe('finished')
288
+ const result = game.getGameResult()
289
+ expect(result).toBeDefined()
290
+ expect(result?.winner).toBeDefined()
291
+ expect(result?.reason).toBe('double-pass')
292
+ expect(result?.score).toBeDefined()
293
+ })
294
+
295
+ it('should declare winner based on territory', () => {
296
+ // Create a situation where black has more territory
297
+ game.drop({ x: 0, y: 0, z: 0 }) // Black
298
+ game.drop({ x: 0, y: 0, z: 1 }) // White
299
+ game.drop({ x: 1, y: 0, z: 0 }) // Black
300
+ game.drop({ x: 0, y: 1, z: 1 }) // White
301
+ game.drop({ x: 0, y: 1, z: 0 }) // Black
302
+ game.drop({ x: 1, y: 0, z: 1 }) // White
303
+ game.drop({ x: 1, y: 1, z: 0 }) // Black
304
+
305
+ game.pass()
306
+ game.pass()
307
+
308
+ const result = game.getGameResult()
309
+ expect(result?.winner).toBeDefined()
310
+ // Winner determined by territory + captured stones
311
+ })
312
+
313
+ it('should handle draw situation', () => {
314
+ // Empty board - should be a draw
315
+ game.pass()
316
+ game.pass()
317
+
318
+ const result = game.getGameResult()
319
+ expect(result).toBeDefined()
320
+ // In perfectly equal situation, could be draw
321
+ })
322
+ })
323
+
324
+
325
+ describe('Complex Scenarios', () => {
326
+ it('should handle multiple captures in one move', () => {
327
+ // This would require a specific board setup
328
+ // where one move captures multiple groups
329
+ // For now, test that the system handles it gracefully
330
+
331
+ game.drop({ x: 2, y: 2, z: 2 })
332
+ const counts = game.getCapturedCounts()
333
+ expect(counts).toBeDefined()
334
+ })
335
+
336
+ it('should maintain game integrity across complex moves', () => {
337
+ // Play a realistic game sequence
338
+ const moves = [
339
+ { x: 2, y: 2, z: 0 },
340
+ { x: 3, y: 2, z: 0 },
341
+ { x: 2, y: 3, z: 0 },
342
+ { x: 3, y: 3, z: 0 },
343
+ { x: 1, y: 2, z: 0 },
344
+ ]
345
+
346
+ for (const move of moves) {
347
+ const success = game.drop(move)
348
+ expect(success).toBe(true)
349
+ }
350
+
351
+ // Game should be in valid state
352
+ expect(game.getCurrentStep()).toBe(moves.length)
353
+ expect(game.getHistory().length).toBe(moves.length)
354
+ })
355
+ })
356
+ })
trigo-web/tests/game/trigoGame.state.test.ts ADDED
@@ -0,0 +1,406 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * TrigoGame State Management and Persistence Tests
3
+ *
4
+ * Tests game status, serialization, and session storage
5
+ */
6
+
7
+ import { describe, it, expect, beforeEach, vi } from 'vitest'
8
+ import { TrigoGame, StoneType } from '@inc/trigo/game'
9
+ import type { BoardShape } from '@inc/trigo/types'
10
+
11
+
12
+ describe('TrigoGame - State Management', () => {
13
+ let game: TrigoGame
14
+ const defaultShape: BoardShape = { x: 5, y: 5, z: 5 }
15
+
16
+ beforeEach(() => {
17
+ game = new TrigoGame(defaultShape)
18
+ })
19
+
20
+
21
+ describe('Game Status', () => {
22
+ it('should start with idle status', () => {
23
+ expect(game.getGameStatus()).toBe('idle')
24
+ })
25
+
26
+ it('should change to playing when started', () => {
27
+ game.startGame()
28
+ expect(game.getGameStatus()).toBe('playing')
29
+ })
30
+
31
+ it('should change to finished on surrender', () => {
32
+ game.startGame()
33
+ game.surrender()
34
+ expect(game.getGameStatus()).toBe('finished')
35
+ })
36
+
37
+ it('should change to finished on double pass', () => {
38
+ game.startGame()
39
+ game.pass()
40
+ game.pass()
41
+ expect(game.getGameStatus()).toBe('finished')
42
+ })
43
+
44
+ it('should allow manual status change', () => {
45
+ game.setGameStatus('paused')
46
+ expect(game.getGameStatus()).toBe('paused')
47
+ })
48
+
49
+ it('should check if game is active', () => {
50
+ expect(game.isGameActive()).toBe(false)
51
+ game.startGame()
52
+ expect(game.isGameActive()).toBe(true)
53
+ game.surrender()
54
+ expect(game.isGameActive()).toBe(false)
55
+ })
56
+ })
57
+
58
+
59
+ describe('Game Result', () => {
60
+ beforeEach(() => {
61
+ game.startGame()
62
+ })
63
+
64
+ it('should have no result initially', () => {
65
+ expect(game.getGameResult()).toBeUndefined()
66
+ })
67
+
68
+ it('should set result on resignation', () => {
69
+ game.surrender()
70
+ const result = game.getGameResult()
71
+
72
+ expect(result).toBeDefined()
73
+ expect(result?.winner).toBe('white')
74
+ expect(result?.reason).toBe('resignation')
75
+ })
76
+
77
+ it('should set result on double pass', () => {
78
+ game.drop({ x: 0, y: 0, z: 0 }) // Black
79
+ game.pass() // White
80
+ game.pass() // Black
81
+
82
+ const result = game.getGameResult()
83
+ expect(result).toBeDefined()
84
+ expect(result?.reason).toBe('double-pass')
85
+ expect(result?.score).toBeDefined()
86
+ })
87
+
88
+ it('should include territory score in result', () => {
89
+ game.pass()
90
+ game.pass()
91
+
92
+ const result = game.getGameResult()
93
+ expect(result?.score).toBeDefined()
94
+ expect(result?.score?.black).toBeDefined()
95
+ expect(result?.score?.white).toBeDefined()
96
+ })
97
+ })
98
+
99
+
100
+ describe('Pass Count', () => {
101
+ beforeEach(() => {
102
+ game.startGame()
103
+ })
104
+
105
+ it('should start with zero passes', () => {
106
+ expect(game.getPassCount()).toBe(0)
107
+ })
108
+
109
+ it('should increment on pass', () => {
110
+ game.pass()
111
+ expect(game.getPassCount()).toBe(1)
112
+
113
+ game.pass()
114
+ expect(game.getPassCount()).toBe(2)
115
+ })
116
+
117
+ it('should reset on stone placement', () => {
118
+ game.pass()
119
+ game.drop({ x: 2, y: 2, z: 2 })
120
+ expect(game.getPassCount()).toBe(0)
121
+ })
122
+
123
+ it('should maintain correct count across undo', () => {
124
+ game.pass()
125
+ expect(game.getPassCount()).toBe(1)
126
+
127
+ game.undo()
128
+ expect(game.getPassCount()).toBe(0)
129
+ })
130
+ })
131
+
132
+
133
+ describe('Callbacks', () => {
134
+ it('should trigger onStepAdvance callback', () => {
135
+ const onStepAdvance = vi.fn()
136
+ const gameWithCallbacks = new TrigoGame(defaultShape, { onStepAdvance })
137
+
138
+ gameWithCallbacks.startGame()
139
+ gameWithCallbacks.drop({ x: 2, y: 2, z: 2 })
140
+
141
+ expect(onStepAdvance).toHaveBeenCalledOnce()
142
+ })
143
+
144
+ it('should trigger onStepBack callback on undo', () => {
145
+ const onStepBack = vi.fn()
146
+ const gameWithCallbacks = new TrigoGame(defaultShape, { onStepBack })
147
+
148
+ gameWithCallbacks.startGame()
149
+ gameWithCallbacks.drop({ x: 2, y: 2, z: 2 })
150
+ gameWithCallbacks.undo()
151
+
152
+ expect(onStepBack).toHaveBeenCalledOnce()
153
+ })
154
+
155
+ it('should trigger onCapture callback', () => {
156
+ const onCapture = vi.fn()
157
+ const gameWithCallbacks = new TrigoGame(defaultShape, { onCapture })
158
+
159
+ gameWithCallbacks.startGame()
160
+
161
+ // Create capture scenario
162
+ gameWithCallbacks.drop({ x: 2, y: 2, z: 2 }) // Black
163
+ gameWithCallbacks.drop({ x: 3, y: 2, z: 2 }) // White (target)
164
+ gameWithCallbacks.drop({ x: 4, y: 2, z: 2 }) // Black
165
+ gameWithCallbacks.drop({ x: 3, y: 1, z: 2 }) // White
166
+ gameWithCallbacks.drop({ x: 3, y: 3, z: 2 }) // Black
167
+ gameWithCallbacks.drop({ x: 3, y: 1, z: 1 }) // White
168
+ gameWithCallbacks.drop({ x: 3, y: 2, z: 1 }) // Black
169
+ gameWithCallbacks.drop({ x: 3, y: 1, z: 3 }) // White
170
+ gameWithCallbacks.drop({ x: 3, y: 2, z: 3 }) // Black - captures
171
+
172
+ // onCapture should have been called if capture occurred
173
+ if (onCapture.mock.calls.length > 0) {
174
+ expect(onCapture).toHaveBeenCalled()
175
+ }
176
+ })
177
+
178
+ it('should trigger onWin callback on game end', () => {
179
+ const onWin = vi.fn()
180
+ const gameWithCallbacks = new TrigoGame(defaultShape, { onWin })
181
+
182
+ gameWithCallbacks.startGame()
183
+ gameWithCallbacks.surrender()
184
+
185
+ expect(onWin).toHaveBeenCalledOnce()
186
+ })
187
+ })
188
+ })
189
+
190
+
191
+ describe('TrigoGame - Serialization', () => {
192
+ let game: TrigoGame
193
+ const defaultShape: BoardShape = { x: 5, y: 5, z: 5 }
194
+
195
+ beforeEach(() => {
196
+ game = new TrigoGame(defaultShape)
197
+ game.startGame()
198
+ })
199
+
200
+
201
+ describe('JSON Serialization', () => {
202
+ it('should serialize game state', () => {
203
+ game.drop({ x: 2, y: 2, z: 2 })
204
+ game.drop({ x: 3, y: 2, z: 2 })
205
+
206
+ const json = game.toJSON()
207
+
208
+ expect(json).toBeDefined()
209
+ expect(json).toHaveProperty('shape')
210
+ expect(json).toHaveProperty('currentPlayer')
211
+ expect(json).toHaveProperty('currentStepIndex')
212
+ expect(json).toHaveProperty('history')
213
+ expect(json).toHaveProperty('board')
214
+ expect(json).toHaveProperty('gameStatus')
215
+ expect(json).toHaveProperty('passCount')
216
+ })
217
+
218
+ it('should deserialize game state', () => {
219
+ // Set up a game state
220
+ game.drop({ x: 2, y: 2, z: 2 })
221
+ game.drop({ x: 3, y: 2, z: 2 })
222
+ game.pass()
223
+
224
+ const json = game.toJSON()
225
+
226
+ // Create new game and load state
227
+ const newGame = new TrigoGame()
228
+ const success = newGame.fromJSON(json)
229
+
230
+ expect(success).toBe(true)
231
+ expect(newGame.getCurrentStep()).toBe(game.getCurrentStep())
232
+ expect(newGame.getCurrentPlayer()).toBe(game.getCurrentPlayer())
233
+ expect(newGame.getGameStatus()).toBe(game.getGameStatus())
234
+ expect(newGame.getPassCount()).toBe(game.getPassCount())
235
+ })
236
+
237
+ it('should preserve board state through serialization', () => {
238
+ game.drop({ x: 2, y: 2, z: 2 })
239
+ game.drop({ x: 3, y: 2, z: 2 })
240
+
241
+ const originalBoard = game.getBoard()
242
+ const json = game.toJSON()
243
+
244
+ const newGame = new TrigoGame()
245
+ newGame.fromJSON(json)
246
+ const restoredBoard = newGame.getBoard()
247
+
248
+ // Check key positions
249
+ expect(restoredBoard[2][2][2]).toBe(originalBoard[2][2][2])
250
+ expect(restoredBoard[3][2][2]).toBe(originalBoard[3][2][2])
251
+ })
252
+
253
+ it('should preserve history through serialization', () => {
254
+ game.drop({ x: 2, y: 2, z: 2 })
255
+ game.drop({ x: 3, y: 2, z: 2 })
256
+
257
+ const originalHistory = game.getHistory()
258
+ const json = game.toJSON()
259
+
260
+ const newGame = new TrigoGame()
261
+ newGame.fromJSON(json)
262
+ const restoredHistory = newGame.getHistory()
263
+
264
+ expect(restoredHistory.length).toBe(originalHistory.length)
265
+ })
266
+
267
+ it('should handle malformed JSON gracefully', () => {
268
+ const newGame = new TrigoGame()
269
+ const success = newGame.fromJSON({ invalid: 'data' })
270
+
271
+ // Should return false for invalid data
272
+ expect(success).toBe(false)
273
+ })
274
+ })
275
+
276
+
277
+ describe('Session Storage', () => {
278
+ // Mock sessionStorage for testing
279
+ const sessionStorageMock = (() => {
280
+ let store: Record<string, string> = {}
281
+
282
+ return {
283
+ getItem: (key: string) => store[key] || null,
284
+ setItem: (key: string, value: string) => { store[key] = value },
285
+ removeItem: (key: string) => { delete store[key] },
286
+ clear: () => { store = {} }
287
+ }
288
+ })()
289
+
290
+ beforeEach(() => {
291
+ // Set up globalThis.sessionStorage mock
292
+ if (typeof globalThis !== 'undefined') {
293
+ (globalThis as any).sessionStorage = sessionStorageMock
294
+ sessionStorageMock.clear()
295
+ }
296
+ })
297
+
298
+ it('should save to session storage', () => {
299
+ game.drop({ x: 2, y: 2, z: 2 })
300
+
301
+ const success = game.saveToSessionStorage('testKey')
302
+ expect(success).toBe(true)
303
+
304
+ const saved = sessionStorageMock.getItem('testKey')
305
+ expect(saved).toBeDefined()
306
+ expect(saved).not.toBeNull()
307
+ })
308
+
309
+ it('should load from session storage', () => {
310
+ // Save game state
311
+ game.drop({ x: 2, y: 2, z: 2 })
312
+ game.drop({ x: 3, y: 2, z: 2 })
313
+ game.saveToSessionStorage('testKey')
314
+
315
+ // Create new game and load
316
+ const newGame = new TrigoGame()
317
+ const success = newGame.loadFromSessionStorage('testKey')
318
+
319
+ expect(success).toBe(true)
320
+ expect(newGame.getCurrentStep()).toBe(2)
321
+
322
+ const board = newGame.getBoard()
323
+ expect(board[2][2][2]).toBe(StoneType.BLACK)
324
+ expect(board[3][2][2]).toBe(StoneType.WHITE)
325
+ })
326
+
327
+ it('should clear session storage', () => {
328
+ game.saveToSessionStorage('testKey')
329
+
330
+ game.clearSessionStorage('testKey')
331
+
332
+ const saved = sessionStorageMock.getItem('testKey')
333
+ expect(saved).toBeNull()
334
+ })
335
+
336
+ it('should return false when loading non-existent data', () => {
337
+ const newGame = new TrigoGame()
338
+ const success = newGame.loadFromSessionStorage('nonExistentKey')
339
+
340
+ expect(success).toBe(false)
341
+ })
342
+
343
+ it('should use default key when not specified', () => {
344
+ game.drop({ x: 2, y: 2, z: 2 })
345
+ game.saveToSessionStorage() // No key specified
346
+
347
+ const saved = sessionStorageMock.getItem('trigoGameState')
348
+ expect(saved).toBeDefined()
349
+ })
350
+ })
351
+ })
352
+
353
+
354
+ describe('TrigoGame - Statistics', () => {
355
+ let game: TrigoGame
356
+ const defaultShape: BoardShape = { x: 5, y: 5, z: 5 }
357
+
358
+ beforeEach(() => {
359
+ game = new TrigoGame(defaultShape)
360
+ game.startGame()
361
+ })
362
+
363
+
364
+ describe('getStats', () => {
365
+ it('should return initial stats', () => {
366
+ const stats = game.getStats()
367
+
368
+ expect(stats.totalMoves).toBe(0)
369
+ expect(stats.blackMoves).toBe(0)
370
+ expect(stats.whiteMoves).toBe(0)
371
+ expect(stats.capturedByBlack).toBe(0)
372
+ expect(stats.capturedByWhite).toBe(0)
373
+ expect(stats.territory).toBeDefined()
374
+ })
375
+
376
+ it('should count moves correctly', () => {
377
+ game.drop({ x: 2, y: 2, z: 2 }) // Black
378
+ game.drop({ x: 3, y: 2, z: 2 }) // White
379
+ game.drop({ x: 2, y: 3, z: 2 }) // Black
380
+
381
+ const stats = game.getStats()
382
+ expect(stats.totalMoves).toBe(3)
383
+ expect(stats.blackMoves).toBe(2)
384
+ expect(stats.whiteMoves).toBe(1)
385
+ })
386
+
387
+ it('should not count pass moves in move counts', () => {
388
+ game.drop({ x: 2, y: 2, z: 2 }) // Black
389
+ game.pass() // White
390
+ game.drop({ x: 2, y: 3, z: 2 }) // Black
391
+
392
+ const stats = game.getStats()
393
+ expect(stats.blackMoves).toBe(2)
394
+ expect(stats.whiteMoves).toBe(0)
395
+ })
396
+
397
+ it('should include territory calculation', () => {
398
+ game.drop({ x: 0, y: 0, z: 0 })
399
+
400
+ const stats = game.getStats()
401
+ expect(stats.territory.black).toBeDefined()
402
+ expect(stats.territory.white).toBeDefined()
403
+ expect(stats.territory.neutral).toBeDefined()
404
+ })
405
+ })
406
+ })
trigo-web/tests/game/trigoGame.tgn.test.ts ADDED
@@ -0,0 +1,229 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * TrigoGame TGN Export Tests
3
+ *
4
+ * Tests the TGN (Trigo Game Notation) export functionality
5
+ */
6
+
7
+ import { describe, it, expect, beforeEach } from 'vitest'
8
+ import { TrigoGame, StoneType } from '@inc/trigo/game'
9
+ import type { BoardShape } from '@inc/trigo/types'
10
+
11
+
12
+ describe('TrigoGame - TGN Export', () => {
13
+ let game: TrigoGame
14
+ const defaultShape: BoardShape = { x: 5, y: 5, z: 5 }
15
+
16
+
17
+ beforeEach(() => {
18
+ game = new TrigoGame(defaultShape)
19
+ game.startGame()
20
+ })
21
+
22
+
23
+ describe('Basic TGN Export', () => {
24
+ it('should export empty game with board size', () => {
25
+ const tgn = game.toTGN()
26
+
27
+ expect(tgn).toContain('[Board 5x5x5]')
28
+ expect(tgn.trim()).not.toBe('')
29
+ })
30
+
31
+ it('should export 2D board format correctly', () => {
32
+ const game2d = new TrigoGame({ x: 19, y: 19, z: 1 })
33
+ game2d.startGame()
34
+ const tgn = game2d.toTGN()
35
+
36
+ expect(tgn).toContain('[Board 19x19]')
37
+ })
38
+
39
+ it('should export game with metadata', () => {
40
+ const tgn = game.toTGN({
41
+ event: 'World Championship',
42
+ site: 'Tokyo',
43
+ date: '2025.10.31',
44
+ black: 'Alice',
45
+ white: 'Bob'
46
+ })
47
+
48
+ expect(tgn).toContain('[Event "World Championship"]')
49
+ expect(tgn).toContain('[Site "Tokyo"]')
50
+ expect(tgn).toContain('[Date "2025.10.31"]')
51
+ expect(tgn).toContain('[Black "Alice"]')
52
+ expect(tgn).toContain('[White "Bob"]')
53
+ })
54
+
55
+ it('should include optional metadata tags', () => {
56
+ const tgn = game.toTGN({
57
+ rules: 'Chinese',
58
+ timeControl: '30+10',
59
+ application: 'Trigo v1.0'
60
+ })
61
+
62
+ expect(tgn).toContain('[Rules "Chinese"]')
63
+ expect(tgn).toContain('[TimeControl "30+10"]')
64
+ expect(tgn).toContain('[Application "Trigo v1.0"]')
65
+ })
66
+ })
67
+
68
+
69
+ describe('Move Sequence Export', () => {
70
+ it('should export simple move sequence', () => {
71
+ // Play some moves
72
+ game.drop({ x: 2, y: 2, z: 2 }) // Black at center - "000"
73
+ game.drop({ x: 3, y: 2, z: 2 }) // White - "y00"
74
+ game.drop({ x: 2, y: 3, z: 2 }) // Black - "0y0"
75
+
76
+ const tgn = game.toTGN()
77
+
78
+ expect(tgn).toContain('1. 000 y00')
79
+ expect(tgn).toContain('2. 0y0')
80
+ })
81
+
82
+ it('should export moves with correct coordinates', () => {
83
+ // Test corner positions
84
+ game.drop({ x: 0, y: 0, z: 0 }) // Black - "aaa"
85
+ game.drop({ x: 4, y: 4, z: 4 }) // White - "zzz"
86
+
87
+ const tgn = game.toTGN()
88
+
89
+ expect(tgn).toContain('1. aaa zzz')
90
+ })
91
+
92
+ it('should export pass moves', () => {
93
+ game.drop({ x: 2, y: 2, z: 2 }) // Black
94
+ game.pass() // White passes
95
+ game.drop({ x: 3, y: 2, z: 2 }) // Black
96
+
97
+ const tgn = game.toTGN()
98
+
99
+ expect(tgn).toContain('1. 000 Pass')
100
+ expect(tgn).toContain('2. y00')
101
+ })
102
+
103
+ it('should export multiple rounds correctly', () => {
104
+ // Play 4 moves (2 rounds)
105
+ game.drop({ x: 0, y: 0, z: 0 }) // 1. Black
106
+ game.drop({ x: 1, y: 0, z: 0 }) // 1. White
107
+ game.drop({ x: 0, y: 1, z: 0 }) // 2. Black
108
+ game.drop({ x: 1, y: 1, z: 0 }) // 2. White
109
+
110
+ const tgn = game.toTGN()
111
+
112
+ expect(tgn).toContain('1. aaa baa')
113
+ expect(tgn).toContain('2. aba bba')
114
+ })
115
+ })
116
+
117
+
118
+ describe('Game Result Export', () => {
119
+ it('should export resignation result', () => {
120
+ game.drop({ x: 2, y: 2, z: 2 })
121
+ game.surrender() // White surrenders
122
+
123
+ const tgn = game.toTGN()
124
+
125
+ expect(tgn).toContain('[Result "B+Resign"]')
126
+ })
127
+
128
+ it('should export double pass result', () => {
129
+ game.drop({ x: 2, y: 2, z: 2 }) // Black
130
+ game.drop({ x: 3, y: 2, z: 2 }) // White
131
+ game.pass() // Black pass
132
+ game.pass() // White pass - game ends
133
+
134
+ const tgn = game.toTGN()
135
+
136
+ // Result should be present (winner determined by territory)
137
+ expect(tgn).toContain('[Result "')
138
+ })
139
+ })
140
+
141
+
142
+ describe('Complete Game Example', () => {
143
+ it('should export a complete game with all elements', () => {
144
+ const tgn = game.toTGN({
145
+ event: 'Test Game',
146
+ site: 'Local',
147
+ date: '2025.11.03',
148
+ black: 'Player 1',
149
+ white: 'Player 2',
150
+ rules: 'Chinese',
151
+ application: 'Trigo Test'
152
+ })
153
+
154
+ // Should have metadata section
155
+ expect(tgn).toContain('[Event "Test Game"]')
156
+ expect(tgn).toContain('[Site "Local"]')
157
+ expect(tgn).toContain('[Date "2025.11.03"]')
158
+ expect(tgn).toContain('[Black "Player 1"]')
159
+ expect(tgn).toContain('[White "Player 2"]')
160
+ expect(tgn).toContain('[Board 5x5x5]')
161
+ expect(tgn).toContain('[Rules "Chinese"]')
162
+ expect(tgn).toContain('[Application "Trigo Test"]')
163
+
164
+ // Should have empty line after metadata
165
+ const lines = tgn.split('\n')
166
+ const boardLineIndex = lines.findIndex(l => l.includes('[Board'))
167
+ const applicationLineIndex = lines.findIndex(l => l.includes('[Application'))
168
+ // Empty line after last metadata tag
169
+ expect(lines[applicationLineIndex + 1]).toBe('')
170
+ })
171
+
172
+ it('should format output with proper line breaks', () => {
173
+ game.drop({ x: 2, y: 2, z: 2 })
174
+ game.drop({ x: 3, y: 2, z: 2 })
175
+
176
+ const tgn = game.toTGN()
177
+ const lines = tgn.split('\n')
178
+
179
+ // Should end with empty line
180
+ expect(lines[lines.length - 1]).toBe('')
181
+
182
+ // Should have moves on separate lines from metadata
183
+ const moveLines = lines.filter(l => l.match(/^\d+\./))
184
+ expect(moveLines.length).toBeGreaterThan(0)
185
+ })
186
+ })
187
+
188
+
189
+ describe('Edge Cases', () => {
190
+ it('should handle game with no moves', () => {
191
+ const tgn = game.toTGN()
192
+
193
+ expect(tgn).toContain('[Board 5x5x5]')
194
+ // Should not have any move numbers
195
+ expect(tgn).not.toMatch(/\d+\./)
196
+ })
197
+
198
+ it('should handle incomplete rounds', () => {
199
+ game.drop({ x: 2, y: 2, z: 2 }) // Only black's move
200
+
201
+ const tgn = game.toTGN()
202
+
203
+ expect(tgn).toContain('1. 000')
204
+ // Should not have white's move on this line
205
+ const lines = tgn.split('\n')
206
+ const moveLine = lines.find(l => l.startsWith('1.'))
207
+ expect(moveLine).not.toContain(' z') // No white move
208
+ })
209
+
210
+ it('should export TGN that can be read back', () => {
211
+ // Play a simple game
212
+ game.drop({ x: 2, y: 2, z: 2 })
213
+ game.drop({ x: 3, y: 2, z: 2 })
214
+ game.drop({ x: 2, y: 3, z: 2 })
215
+ game.drop({ x: 3, y: 3, z: 2 })
216
+
217
+ const tgn = game.toTGN({
218
+ black: 'Alice',
219
+ white: 'Bob'
220
+ })
221
+
222
+ // TGN should be valid format
223
+ expect(tgn).toContain('[Black "Alice"]')
224
+ expect(tgn).toContain('[White "Bob"]')
225
+ expect(tgn).toContain('1. 000 y00')
226
+ expect(tgn).toContain('2. 0y0 yy0')
227
+ })
228
+ })
229
+ })
trigo-web/tests/game/verify_capture.test.ts ADDED
@@ -0,0 +1,79 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { describe, it } from 'vitest'
2
+ import { TrigoGame, StoneType } from '@inc/trigo/game'
3
+
4
+ describe('Verify Capture Logic', () => {
5
+ it('step by step capture verification', () => {
6
+ const game = new TrigoGame({ x: 5, y: 5, z: 1 })
7
+ game.startGame()
8
+
9
+ console.log("\n=== Step-by-step Capture Verification ===")
10
+ console.log("Target: White at (3, 2, 0)")
11
+ console.log("Need to surround with Black at:")
12
+ console.log(" Left: (2, 2, 0)")
13
+ console.log(" Right: (4, 2, 0)")
14
+ console.log(" Top: (3, 1, 0)")
15
+ console.log(" Bottom: (3, 3, 0)")
16
+
17
+ // Move 1: Black left
18
+ game.drop({ x: 2, y: 2, z: 0 })
19
+ console.log("\nMove 1: Black at (2, 2, 0) - LEFT of white")
20
+
21
+ // Move 2: White target
22
+ game.drop({ x: 3, y: 2, z: 0 })
23
+ console.log("Move 2: White at (3, 2, 0) - TARGET")
24
+
25
+ // Move 3: Black right
26
+ game.drop({ x: 4, y: 2, z: 0 })
27
+ console.log("Move 3: Black at (4, 2, 0) - RIGHT of white")
28
+
29
+ // Move 4: White elsewhere
30
+ game.drop({ x: 0, y: 0, z: 0 })
31
+ console.log("Move 4: White at (0, 0, 0) - elsewhere")
32
+
33
+ // Move 5: Black bottom
34
+ game.drop({ x: 3, y: 3, z: 0 })
35
+ console.log("Move 5: Black at (3, 3, 0) - BOTTOM of white")
36
+
37
+ // Move 6: White elsewhere
38
+ game.drop({ x: 0, y: 1, z: 0 })
39
+ console.log("Move 6: White at (0, 1, 0) - elsewhere")
40
+
41
+ console.log("\nBefore final move - White's neighbors:")
42
+ const board = game.getBoard()
43
+ console.log(` Left (2, 2, 0): ${board[2][2][0]} (${board[2][2][0] === StoneType.BLACK ? 'BLACK' : 'other'})`)
44
+ console.log(` Right (4, 2, 0): ${board[4][2][0]} (${board[4][2][0] === StoneType.BLACK ? 'BLACK' : 'other'})`)
45
+ console.log(` Top (3, 1, 0): ${board[3][1][0]} (${board[3][1][0] === StoneType.EMPTY ? 'EMPTY' : 'occupied'})`)
46
+ console.log(` Bottom (3, 3, 0): ${board[3][3][0]} (${board[3][3][0] === StoneType.BLACK ? 'BLACK' : 'other'})`)
47
+ console.log(` Target (3, 2, 0): ${board[3][2][0]} (WHITE=${StoneType.WHITE})`)
48
+
49
+ // Move 7: Black top - should capture
50
+ console.log("\nMove 7: Black at (3, 1, 0) - TOP of white (should capture)")
51
+ game.drop({ x: 3, y: 1, z: 0 })
52
+
53
+ console.log("\n=== After final move ===")
54
+ const finalBoard = game.getBoard()
55
+ console.log(`Target (3, 2, 0): ${finalBoard[3][2][0]} (should be 0=EMPTY if captured)`)
56
+ console.log(`Neighbors after capture:`)
57
+ console.log(` Left (2, 2, 0): ${finalBoard[2][2][0]}`)
58
+ console.log(` Right (4, 2, 0): ${finalBoard[4][2][0]}`)
59
+ console.log(` Top (3, 1, 0): ${finalBoard[3][1][0]}`)
60
+ console.log(` Bottom (3, 3, 0): ${finalBoard[3][3][0]}`)
61
+
62
+ const lastStep = game.getLastStep()
63
+ console.log(`\nCapture info:`)
64
+ console.log(` capturedPositions: ${JSON.stringify(lastStep?.capturedPositions)}`)
65
+ console.log(` captured count: ${game.getCapturedCounts().white}`)
66
+
67
+ // Verification
68
+ if (finalBoard[3][2][0] === StoneType.EMPTY) {
69
+ console.log("\n✅ SUCCESS: White was captured!")
70
+ } else {
71
+ console.log("\n❌ FAILED: White was NOT captured")
72
+ console.log("All neighbors should be BLACK:")
73
+ console.log(` Left: ${finalBoard[2][2][0] === StoneType.BLACK ? '✅' : '❌'}`)
74
+ console.log(` Right: ${finalBoard[4][2][0] === StoneType.BLACK ? '✅' : '❌'}`)
75
+ console.log(` Top: ${finalBoard[3][1][0] === StoneType.BLACK ? '✅' : '❌'}`)
76
+ console.log(` Bottom: ${finalBoard[3][3][0] === StoneType.BLACK ? '✅' : '❌'}`)
77
+ }
78
+ })
79
+ })
trigo-web/tests/mctsTerminalPropagation.test.ts ADDED
@@ -0,0 +1,257 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Unit Tests for MCTS Terminal State Propagation
3
+ *
4
+ * Tests the minimax propagation logic where nodes with all terminal children
5
+ * are themselves marked as terminal.
6
+ *
7
+ * Based on GPT-5.1 review recommendations.
8
+ */
9
+
10
+ import { describe, it, expect, beforeEach } from "vitest";
11
+ import { TrigoGame } from "../inc/trigo/game.js";
12
+ import { MCTSAgent } from "../inc/mctsAgent.js";
13
+ import { TrigoTreeAgent } from "../inc/trigoTreeAgent.js";
14
+ import { TrigoEvaluationAgent } from "../inc/trigoEvaluationAgent.js";
15
+ import { ModelInferencer } from "../inc/modelInferencer.js";
16
+ import type { Move } from "../inc/trigo/types.js";
17
+
18
+
19
+ /**
20
+ * Mock Tree Agent that returns uniform priors
21
+ */
22
+ class MockTreeAgent extends TrigoTreeAgent {
23
+ constructor() {
24
+ super(null as any);
25
+ }
26
+
27
+ async scoreMoves(game: TrigoGame, moves: Move[]): Promise<Array<{ move: Move; score: number }>> {
28
+ // Return uniform log probabilities
29
+ const logProb = Math.log(1.0 / moves.length);
30
+ return moves.map(move => ({ move, score: logProb }));
31
+ }
32
+ }
33
+
34
+
35
+ /**
36
+ * Mock Evaluation Agent that returns fixed values
37
+ */
38
+ class MockEvaluationAgent extends TrigoEvaluationAgent {
39
+ private mockValue: number;
40
+
41
+ constructor(value: number = 0.0) {
42
+ super(null as any);
43
+ this.mockValue = value;
44
+ }
45
+
46
+ setMockValue(value: number): void {
47
+ this.mockValue = value;
48
+ }
49
+
50
+ async evaluatePosition(game: TrigoGame): Promise<{ value: number; interpretation: string }> {
51
+ return {
52
+ value: this.mockValue,
53
+ interpretation: this.mockValue > 0 ? "White advantage" : "Black advantage"
54
+ };
55
+ }
56
+ }
57
+
58
+
59
+ /**
60
+ * Helper to access private MCTSNode properties via type assertion
61
+ */
62
+ interface MCTSNode {
63
+ state: TrigoGame | null;
64
+ parent: MCTSNode | null;
65
+ action: Move | null;
66
+ N: Map<string, number>;
67
+ W: Map<string, number>;
68
+ Q: Map<string, number>;
69
+ P: Map<string, number>;
70
+ children: Map<string, MCTSNode>;
71
+ expanded: boolean;
72
+ terminalValue: number | null;
73
+ depth: number;
74
+ playerToMove: number;
75
+ }
76
+
77
+
78
+ describe("MCTS Terminal Propagation", () => {
79
+ let game: TrigoGame;
80
+ let treeAgent: MockTreeAgent;
81
+ let evalAgent: MockEvaluationAgent;
82
+ let mctsAgent: MCTSAgent;
83
+
84
+ beforeEach(() => {
85
+ treeAgent = new MockTreeAgent();
86
+ evalAgent = new MockEvaluationAgent(0.0);
87
+ mctsAgent = new MCTSAgent(treeAgent, evalAgent, {
88
+ numSimulations: 10,
89
+ cPuct: 1.0,
90
+ temperature: 1.0,
91
+ dirichletAlpha: 0.03,
92
+ dirichletEpsilon: 0.0 // Disable Dirichlet noise for testing
93
+ });
94
+ });
95
+
96
+
97
+ describe("Test 1: Single-Step Endgame", () => {
98
+ it("should propagate terminal value when Black chooses between terminal children", async () => {
99
+ // Setup: 5x1x1 board with 2 empty positions (a and b)
100
+ // Black to move, can play at position 0 (index 2) or position 1 (index 1)
101
+ game = new TrigoGame({ x: 5, y: 1, z: 1 });
102
+ game.startGame();
103
+
104
+ // Simulate near-terminal game: Black at z(4), y(3); White at a(0), b(1)
105
+ // Remaining: position 2 (center '0')
106
+ game.drop({ x: 4, y: 0, z: 0 }); // Black at z
107
+ game.drop({ x: 0, y: 0, z: 0 }); // White at a
108
+ game.drop({ x: 3, y: 0, z: 0 }); // Black at y
109
+ game.drop({ x: 1, y: 0, z: 0 }); // White at b
110
+
111
+ // Now Black to move at position 2 (center)
112
+ // This will end the game
113
+ const result = await mctsAgent.selectMove(game, 5);
114
+
115
+ // Verify move was selected
116
+ expect(result.move).toBeDefined();
117
+
118
+ // Check that terminal propagation happened
119
+ // (Terminal value should be set on nodes)
120
+ expect(result.rootValue).toBeDefined();
121
+ });
122
+ });
123
+
124
+
125
+ describe("Test 2: Two-Step Propagation", () => {
126
+ it("should propagate terminal values up two levels", async () => {
127
+ // Setup: Near-terminal position
128
+ game = new TrigoGame({ x: 3, y: 1, z: 1 });
129
+ game.startGame();
130
+
131
+ // Black at position 0, White at position 2
132
+ // Remaining: position 1
133
+ game.drop({ x: 0, y: 0, z: 0 }); // Black
134
+ game.drop({ x: 2, y: 0, z: 0 }); // White
135
+
136
+ // Now Black to move - only one move possible
137
+ const result = await mctsAgent.selectMove(game, 3);
138
+
139
+ // After enough simulations, should mark nodes as terminal
140
+ expect(result.move).toBeDefined();
141
+ });
142
+ });
143
+
144
+
145
+ describe("Test 3: White vs Black Turn Correctness", () => {
146
+ it("should use max for White's turn and min for Black's turn", async () => {
147
+ // This is a conceptual test - we verify via the implementation
148
+ // White should maximize Q-values (white-positive)
149
+ // Black should minimize Q-values (white-positive)
150
+
151
+ // Setup simple 3x1x1 board
152
+ game = new TrigoGame({ x: 3, y: 1, z: 1 });
153
+ game.startGame();
154
+
155
+ // Run a few simulations
156
+ const result = await mctsAgent.selectMove(game, 1);
157
+
158
+ // Verify basic functionality works
159
+ expect(result.move).toBeDefined();
160
+ expect(result.visitCounts.size).toBeGreaterThan(0);
161
+ });
162
+ });
163
+
164
+
165
+ describe("Test 4: Terminal Leaf with No Actions", () => {
166
+ it("should handle terminal nodes with empty action sets", async () => {
167
+ // Create a finished game
168
+ game = new TrigoGame({ x: 3, y: 1, z: 1 });
169
+ game.startGame();
170
+
171
+ // Fill the board completely
172
+ game.drop({ x: 0, y: 0, z: 0 }); // Black
173
+ game.drop({ x: 1, y: 0, z: 0 }); // White
174
+ game.drop({ x: 2, y: 0, z: 0 }); // Black
175
+
176
+ // Game is now terminal (no more moves)
177
+ // Double-pass to finish
178
+ game.pass();
179
+ game.pass();
180
+
181
+ // Should return immediately without error
182
+ const result = await mctsAgent.selectMove(game, 4);
183
+
184
+ expect(result.move.isPass).toBe(true);
185
+ expect(result.rootValue).toBeDefined();
186
+ });
187
+ });
188
+
189
+
190
+ describe("Test 5: Selection Skips Terminal Nodes", () => {
191
+ it("should not expand terminal nodes during selection", async () => {
192
+ // Setup near-terminal position
193
+ game = new TrigoGame({ x: 5, y: 1, z: 1 });
194
+ game.startGame();
195
+
196
+ // Play several moves to approach terminal state
197
+ game.drop({ x: 0, y: 0, z: 0 }); // Black at a
198
+ game.drop({ x: 4, y: 0, z: 0 }); // White at z
199
+ game.drop({ x: 1, y: 0, z: 0 }); // Black at b
200
+ game.drop({ x: 3, y: 0, z: 0 }); // White at y
201
+
202
+ // One position left (center)
203
+ const result = await mctsAgent.selectMove(game, 5);
204
+
205
+ expect(result.move).toBeDefined();
206
+
207
+ // Should have visit counts for available moves
208
+ expect(result.visitCounts.size).toBeGreaterThan(0);
209
+ });
210
+ });
211
+
212
+
213
+ describe("Test 6: White-Positive Minimax Verification", () => {
214
+ it("should correctly apply minimax with white-positive values", async () => {
215
+ // Test scenario from verification doc:
216
+ // White to move with children [+5, +1, -2] should choose +5
217
+ // Black to move with children [+5, +1, -2] should choose -2
218
+
219
+ game = new TrigoGame({ x: 3, y: 1, z: 1 });
220
+ game.startGame();
221
+
222
+ // Run MCTS
223
+ const result = await mctsAgent.selectMove(game, 1);
224
+
225
+ // Basic verification
226
+ expect(result.move).toBeDefined();
227
+ expect(result.rootValue).toBeDefined();
228
+ });
229
+ });
230
+
231
+
232
+ describe("Test 7: 5x1x1 Board Complete Game", () => {
233
+ it("should handle 5x1x1 board terminal propagation correctly", async () => {
234
+ // Test terminal propagation on a 5x1x1 board
235
+ // Focus on verifying the mechanics work, not exact game outcome
236
+
237
+ game = new TrigoGame({ x: 5, y: 1, z: 1 });
238
+ game.startGame();
239
+
240
+ // Play a few moves
241
+ game.drop({ x: 2, y: 0, z: 0 }); // Black center
242
+ game.drop({ x: 0, y: 0, z: 0 }); // White a
243
+ game.drop({ x: 4, y: 0, z: 0 }); // Black z
244
+
245
+ // Now run MCTS to find best move for White
246
+ // Should handle near-terminal position correctly
247
+ const result = await mctsAgent.selectMove(game, 4);
248
+
249
+ expect(result.move).toBeDefined();
250
+ expect(result.visitCounts.size).toBeGreaterThan(0);
251
+ expect(result.rootValue).toBeDefined();
252
+
253
+ // The key test: terminal propagation should work without errors
254
+ // Even if we don't have terminal nodes yet, the mechanism should be ready
255
+ });
256
+ });
257
+ });
trigo-web/tests/testMCTSSingleStep.ts ADDED
@@ -0,0 +1,159 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Test MCTS consistency between TypeScript and C++
3
+ *
4
+ * Compare policy priors, visits, and Q-values
5
+ * on empty 5x5 board for comparison with C++ implementation
6
+ */
7
+
8
+ import { TrigoTreeAgent } from "../inc/trigoTreeAgent.js";
9
+ import { TrigoEvaluationAgent } from "../inc/trigoEvaluationAgent.js";
10
+ import { TrigoGame } from "../inc/trigo/game.js";
11
+ import { Stone } from "../inc/trigo/types.js";
12
+ import { encodeAb0yz } from "../inc/trigo/ab0yz.js";
13
+ import { ModelInferencer } from "../inc/modelInferencer.js";
14
+ import * as ort from "onnxruntime-node";
15
+
16
+
17
+ async function testMCTSSingleStep() {
18
+ console.log("============================================================================");
19
+ console.log("MCTS Single Step Consistency Test (TypeScript)");
20
+ console.log("============================================================================");
21
+ console.log();
22
+
23
+ // Load models - use tree/evaluation model pair
24
+ const modelDir = "/home/camus/work/trigo/trigo-web/public/onnx/20251204-trigo-value-gpt2-l6-h64-251125-lr500";
25
+ const treeModelPath = `${modelDir}/GPT2CausalLM_ep0019_tree.onnx`;
26
+ const evalModelPath = `${modelDir}/GPT2CausalLM_ep0019_evaluation.onnx`;
27
+
28
+ console.log("Loading models...");
29
+ console.log(` Tree model: ${treeModelPath}`);
30
+ console.log(` Eval model: ${evalModelPath}`);
31
+ console.log();
32
+
33
+ // Create ONNX sessions
34
+ const sessionOptions: ort.InferenceSession.SessionOptions = {
35
+ executionProviders: ["cpu"]
36
+ };
37
+
38
+ const treeSession = await ort.InferenceSession.create(treeModelPath, sessionOptions);
39
+ const evalSession = await ort.InferenceSession.create(evalModelPath, sessionOptions);
40
+
41
+ // Create inferencers with proper initialization
42
+ const treeInferencer = new ModelInferencer(ort.Tensor as any, {
43
+ vocabSize: 128,
44
+ seqLen: 256
45
+ });
46
+ treeInferencer.setSession(treeSession as any);
47
+
48
+ const evalInferencer = new ModelInferencer(ort.Tensor as any, {
49
+ vocabSize: 128,
50
+ seqLen: 256
51
+ });
52
+ evalInferencer.setSession(evalSession as any);
53
+
54
+ console.log("[ModelInferencer] ✓ Session set successfully");
55
+ console.log();
56
+
57
+ // Create agents
58
+ const treeAgent = new TrigoTreeAgent(treeInferencer);
59
+ const evaluationAgent = new TrigoEvaluationAgent(evalInferencer);
60
+
61
+ // Setup game: 5x5 board, empty
62
+ const shape = { x: 5, y: 5, z: 1 };
63
+ const game = new TrigoGame(shape);
64
+ game.startGame();
65
+
66
+ console.log("Game Configuration:");
67
+ console.log(" Board: 5×5×1");
68
+ console.log(" Position: Empty board (Move 1)");
69
+ console.log(` Current player: ${game.getCurrentPlayer() === Stone.Black ? "Black" : "White"}`);
70
+ const validMoves = game.validMovePositions(game.getCurrentPlayer());
71
+ console.log(` Valid moves: ${validMoves.length}`);
72
+ console.log();
73
+
74
+ // Step 1: Get policy priors directly from tree agent
75
+ console.log("Step 1: Getting policy priors from tree agent...");
76
+ const currentPlayer = game.getCurrentPlayer() === Stone.Black ? "black" : "white";
77
+ const moves = validMoves.map(pos => ({
78
+ x: pos.x,
79
+ y: pos.y,
80
+ z: pos.z,
81
+ player: currentPlayer as "black" | "white"
82
+ }));
83
+ moves.push({ player: currentPlayer as "black" | "white", isPass: true });
84
+
85
+ const scoredMoves = await treeAgent.scoreMoves(game, moves);
86
+
87
+ // Sort by score descending
88
+ scoredMoves.sort((a, b) => b.score - a.score);
89
+
90
+ // Compute priors via exp-normalize
91
+ const maxScore = Math.max(...scoredMoves.map(m => m.score));
92
+ const expScores = scoredMoves.map(m => Math.exp(m.score - maxScore));
93
+ const sumExp = expScores.reduce((sum, exp) => sum + exp, 0);
94
+
95
+ console.log();
96
+ console.log("Top 5 moves by policy prior (log score):");
97
+ console.log("| Rank | Move | Position | Log Score | Prior |");
98
+ console.log("|------|------|----------|-----------|-------|");
99
+
100
+ for (let i = 0; i < Math.min(5, scoredMoves.length); i++) {
101
+ const m = scoredMoves[i];
102
+ let moveStr: string;
103
+ let posStr: string;
104
+
105
+ if (m.move.isPass) {
106
+ moveStr = "Pass";
107
+ posStr = "N/A";
108
+ } else {
109
+ moveStr = encodeAb0yz([m.move.x!, m.move.y!, m.move.z!], [shape.x, shape.y, shape.z]);
110
+ posStr = `(${m.move.x},${m.move.y},${m.move.z})`;
111
+ }
112
+
113
+ const prior = expScores[i] / sumExp;
114
+ console.log(`| ${i + 1} | ${moveStr} | ${posStr} | ${m.score.toFixed(6)} | ${prior.toFixed(6)} |`);
115
+ }
116
+ console.log();
117
+
118
+ // Step 2: Get value estimate directly
119
+ console.log("Step 2: Getting value estimate...");
120
+ const evaluation = await evaluationAgent.evaluatePosition(game);
121
+ console.log(` Value: ${evaluation.value.toFixed(6)} (${evaluation.interpretation})`);
122
+ console.log();
123
+
124
+ // Summary for comparison
125
+ console.log("============================================================================");
126
+ console.log("Summary for C++ Comparison:");
127
+ console.log("============================================================================");
128
+ console.log();
129
+ console.log("Policy Priors (top 5):");
130
+ for (let i = 0; i < Math.min(5, scoredMoves.length); i++) {
131
+ const m = scoredMoves[i];
132
+ let moveStr: string;
133
+
134
+ if (m.move.isPass) {
135
+ moveStr = "Pass";
136
+ } else {
137
+ moveStr = encodeAb0yz([m.move.x!, m.move.y!, m.move.z!], [shape.x, shape.y, shape.z]);
138
+ }
139
+
140
+ const prior = expScores[i] / sumExp;
141
+ console.log(` ${i + 1}. ${moveStr} log_score=${m.score.toFixed(6)} prior=${prior.toFixed(6)}`);
142
+ }
143
+ console.log();
144
+ console.log(`Value estimate: ${evaluation.value.toFixed(6)}`);
145
+ console.log();
146
+
147
+ console.log("Expected C++ results (from test_mcts_single_step):");
148
+ console.log(" 1. az log_score=-2.855756 prior=0.170112");
149
+ console.log(" 2. aa log_score=-2.880302 prior=0.165987");
150
+ console.log(" 3. yz log_score=-3.696474 prior=0.073386");
151
+ console.log(" 4. ya log_score=-3.754188 prior=0.069271");
152
+ console.log(" 5. bz log_score=-3.799809 prior=0.066182");
153
+ console.log();
154
+ console.log("Note: C++ uses prefix-cached model, TypeScript uses tree/evaluation models");
155
+ console.log(" Different models may produce different policy distributions.");
156
+ }
157
+
158
+
159
+ testMCTSSingleStep().catch(console.error);
trigo-web/tests/testMCTSWithVisits.ts ADDED
@@ -0,0 +1,212 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Test MCTS with visits and Q-values - TypeScript vs C++
3
+ *
4
+ * Run actual MCTS simulations and compare:
5
+ * - Policy priors
6
+ * - Visit counts
7
+ * - Q-values
8
+ */
9
+
10
+ import { MCTSAgent } from "../inc/mctsAgent.js";
11
+ import { TrigoTreeAgent } from "../inc/trigoTreeAgent.js";
12
+ import { TrigoEvaluationAgent } from "../inc/trigoEvaluationAgent.js";
13
+ import { TrigoGame } from "../inc/trigo/game.js";
14
+ import { Stone } from "../inc/trigo/types.js";
15
+ import { encodeAb0yz } from "../inc/trigo/ab0yz.js";
16
+ import { ModelInferencer } from "../inc/modelInferencer.js";
17
+ import * as ort from "onnxruntime-node";
18
+
19
+
20
+ async function testMCTSWithVisits() {
21
+ console.log("============================================================================");
22
+ console.log("MCTS with Visits and Q-values Test (TypeScript)");
23
+ console.log("============================================================================");
24
+ console.log();
25
+
26
+ // Load models
27
+ const modelDir = "/home/camus/work/trigo/trigo-web/public/onnx/20251204-trigo-value-gpt2-l6-h64-251125-lr500";
28
+ const treeModelPath = `${modelDir}/GPT2CausalLM_ep0019_tree.onnx`;
29
+ const evalModelPath = `${modelDir}/GPT2CausalLM_ep0019_evaluation.onnx`;
30
+
31
+ console.log("Loading models...");
32
+
33
+ const sessionOptions: ort.InferenceSession.SessionOptions = {
34
+ executionProviders: ["cpu"]
35
+ };
36
+
37
+ const treeSession = await ort.InferenceSession.create(treeModelPath, sessionOptions);
38
+ const evalSession = await ort.InferenceSession.create(evalModelPath, sessionOptions);
39
+
40
+ const treeInferencer = new ModelInferencer(ort.Tensor as any, {
41
+ vocabSize: 128,
42
+ seqLen: 256
43
+ });
44
+ treeInferencer.setSession(treeSession as any);
45
+
46
+ const evalInferencer = new ModelInferencer(ort.Tensor as any, {
47
+ vocabSize: 128,
48
+ seqLen: 256
49
+ });
50
+ evalInferencer.setSession(evalSession as any);
51
+
52
+ console.log("✓ Models loaded");
53
+ console.log();
54
+
55
+ const treeAgent = new TrigoTreeAgent(treeInferencer);
56
+ const evaluationAgent = new TrigoEvaluationAgent(evalInferencer);
57
+
58
+ // Setup game
59
+ const shape = { x: 5, y: 5, z: 1 };
60
+ const game = new TrigoGame(shape);
61
+ game.startGame();
62
+
63
+ console.log("Game Configuration:");
64
+ console.log(` Board: ${shape.x}×${shape.y}×${shape.z}`);
65
+ console.log(` Current player: ${game.getCurrentPlayer() === Stone.Black ? "Black" : "White"}`);
66
+ console.log(` Valid moves: ${game.validMovePositions(game.getCurrentPlayer()).length}`);
67
+ console.log();
68
+
69
+ // MCTS configuration - match C++ settings
70
+ const numSimulations = 10; // Use 10 simulations to get meaningful visits
71
+ console.log("MCTS Configuration:");
72
+ console.log(` Simulations: ${numSimulations}`);
73
+ console.log(" c_puct: 1.0");
74
+ console.log(" Temperature: 0.01 (near-greedy)");
75
+ console.log(" Dirichlet noise: DISABLED (epsilon=0)");
76
+ console.log();
77
+
78
+ // Create MCTS agent with NO Dirichlet noise for reproducibility
79
+ const mctsAgent = new MCTSAgent(treeAgent, evaluationAgent, {
80
+ numSimulations: numSimulations,
81
+ cPuct: 1.0,
82
+ temperature: 0.01,
83
+ dirichletAlpha: 0.03,
84
+ dirichletEpsilon: 0.0 // NO noise
85
+ });
86
+
87
+ console.log("Running MCTS search...");
88
+ console.log("------------------------------------------------------------");
89
+
90
+ const startTime = Date.now();
91
+ const result = await mctsAgent.selectMove(game, 0);
92
+ const elapsedTime = Date.now() - startTime;
93
+
94
+ console.log("------------------------------------------------------------");
95
+ console.log();
96
+
97
+ // We need to access the internal node to get Q-values
98
+ // Since MCTSAgent doesn't expose them directly, we'll run scoreMoves separately
99
+ // to get policy priors, then show what MCTS selected
100
+
101
+ // Get policy priors separately
102
+ const currentPlayer = game.getCurrentPlayer() === Stone.Black ? "black" : "white";
103
+ const validPositions = game.validMovePositions(game.getCurrentPlayer());
104
+ const moves = validPositions.map(pos => ({
105
+ x: pos.x,
106
+ y: pos.y,
107
+ z: pos.z,
108
+ player: currentPlayer as "black" | "white"
109
+ }));
110
+ moves.push({ player: currentPlayer as "black" | "white", isPass: true });
111
+
112
+ const scoredMoves = await treeAgent.scoreMoves(game, moves);
113
+ scoredMoves.sort((a, b) => b.score - a.score);
114
+
115
+ // Compute priors
116
+ const maxScore = Math.max(...scoredMoves.map(m => m.score));
117
+ const expScores = scoredMoves.map(m => Math.exp(m.score - maxScore));
118
+ const sumExp = expScores.reduce((sum, exp) => sum + exp, 0);
119
+
120
+ // Build a map from action key to prior
121
+ const priorMap = new Map<string, number>();
122
+ for (let i = 0; i < scoredMoves.length; i++) {
123
+ const m = scoredMoves[i];
124
+ const actionKey = m.move.isPass ? "pass" : `${m.move.x},${m.move.y},${m.move.z}`;
125
+ priorMap.set(actionKey, expScores[i] / sumExp);
126
+ }
127
+
128
+ // Collect results with visits
129
+ const results: Array<{
130
+ move: string;
131
+ position: string;
132
+ visits: number;
133
+ prior: number;
134
+ }> = [];
135
+
136
+ for (const [actionKey, visits] of result.visitCounts.entries()) {
137
+ let moveNotation: string;
138
+ let posStr: string;
139
+
140
+ if (actionKey === "pass") {
141
+ moveNotation = "Pass";
142
+ posStr = "N/A";
143
+ } else {
144
+ const [x, y, z] = actionKey.split(",").map(Number);
145
+ moveNotation = encodeAb0yz([x, y, z], [shape.x, shape.y, shape.z]);
146
+ posStr = `(${x},${y},${z})`;
147
+ }
148
+
149
+ const prior = priorMap.get(actionKey) ?? 0;
150
+
151
+ results.push({
152
+ move: moveNotation,
153
+ position: posStr,
154
+ visits: visits,
155
+ prior: prior
156
+ });
157
+ }
158
+
159
+ // Sort by visits descending, then by prior
160
+ results.sort((a, b) => {
161
+ if (b.visits !== a.visits) return b.visits - a.visits;
162
+ return b.prior - a.prior;
163
+ });
164
+
165
+ // Print results
166
+ console.log("MCTS Results (sorted by visits):");
167
+ console.log("| Rank | Move | Position | Visits | Prior | Notes |");
168
+ console.log("|------|------|----------|--------|-------|-------|");
169
+
170
+ for (let i = 0; i < Math.min(10, results.length); i++) {
171
+ const r = results[i];
172
+ const marker = r.visits > 0 ? "Explored" : "";
173
+ console.log(`| ${i + 1} | ${r.move} | ${r.position} | ${r.visits} | ${r.prior.toFixed(6)} | ${marker} |`);
174
+ }
175
+ console.log();
176
+
177
+ // Print selected move
178
+ console.log("Selected Move:");
179
+ if (result.move.isPass) {
180
+ console.log(" Move: Pass");
181
+ } else {
182
+ const notation = encodeAb0yz([result.move.x!, result.move.y!, result.move.z!], [shape.x, shape.y, shape.z]);
183
+ console.log(` Move: ${notation} (${result.move.x},${result.move.y},${result.move.z})`);
184
+ }
185
+ console.log(` Root value: ${result.rootValue.toFixed(6)}`);
186
+ console.log();
187
+
188
+ // Summary statistics
189
+ let totalVisits = 0;
190
+ let exploredMoves = 0;
191
+ for (const [_, visits] of result.visitCounts.entries()) {
192
+ totalVisits += visits;
193
+ if (visits > 0) exploredMoves++;
194
+ }
195
+
196
+ console.log("Statistics:");
197
+ console.log(` Total visits: ${totalVisits}`);
198
+ console.log(` Explored moves: ${exploredMoves} / ${result.visitCounts.size}`);
199
+ console.log(` Elapsed time: ${elapsedTime}ms`);
200
+ console.log();
201
+
202
+ console.log("============================================================================");
203
+ console.log("C++ Comparison (from test_mcts_single_step with 1 simulation):");
204
+ console.log(" Selected: ya (3,0,0)");
205
+ console.log(" Visits: 1");
206
+ console.log(" Q-value: -0.082214");
207
+ console.log(" Prior: 0.069271");
208
+ console.log("============================================================================");
209
+ }
210
+
211
+
212
+ testMCTSWithVisits().catch(console.error);
trigo-web/tests/tgn/19x19.tgn ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+
2
+ [Board 19x19]
3
+
4
+ 1. dd wx
5
+ 2. dw wd
6
+ 3. xv
trigo-web/tests/tgn/meta.tgn ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ [Event "World 3D Go Championship 2025"]
3
+ [Site "Tokyo, Japan"]
4
+ [Date "2025.10.31"]
5
+ [Black "Alice Chen"]
6
+ [White "Bob Smith"]
7
+ [Result B+ 5stones]
8
+ [Board 5x5x5]
9
+ [Rules "Chinese"]
10
+ [TimeControl "30+10"]
11
+ [Application "Trigo v1.0"]