Skip to content

Commit bd03b71

Browse files
committed
update
1 parent 7f86317 commit bd03b71

2 files changed

Lines changed: 126 additions & 16 deletions

File tree

src/composables/useGameState.ts

Lines changed: 70 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,9 @@ import {
1414
generateMoveNotation,
1515
} from '../models/chess'
1616
import type { GameSetupConfig, AIStyle } from '../components/GameSetup.vue'
17-
import { getBestAIMove, getPromotionChoice, type AIDifficulty } from '../models/ai'
17+
import { getPromotionChoice, type AIDifficulty } from '../models/ai'
1818
import type { AIDetailedMove } from '../models/ai'
19+
import AIWorker from '../workers/ai-worker?worker'
1920

2021
// ============================================================
2122
// 音频资源(模块级,避免重复创建 Audio 对象)
@@ -107,6 +108,7 @@ export function useGameState(
107108
const aiStyle = ref<AIStyle>('balanced')
108109
const isAIThinking = ref(false)
109110
let aiMoveTimer: number | null = null
111+
let aiWorker: Worker | null = null
110112

111113
// ---- Premove 状态 ----
112114
const premove = ref<{ from: { row: number; col: number }; to: { row: number; col: number } } | null>(null)
@@ -259,6 +261,10 @@ export function useGameState(
259261
window.clearTimeout(aiMoveTimer)
260262
aiMoveTimer = null
261263
}
264+
if (aiWorker !== null) {
265+
aiWorker.terminate()
266+
aiWorker = null
267+
}
262268
isAIThinking.value = false
263269
}
264270

@@ -1331,25 +1337,73 @@ export function useGameState(
13311337
return
13321338
}
13331339

1334-
const bestMove = getBestAIMove(
1335-
board.value,
1336-
aiColor,
1337-
aiDifficulty.value,
1338-
aiStyle.value,
1339-
lastMove.value,
1340-
)
1340+
// 创建新的 Web Worker 用于 AI 计算,避免阻塞 UI 线程
1341+
// 终止可能残留的旧 Worker(防御性编程)
1342+
if (aiWorker !== null) {
1343+
aiWorker.terminate()
1344+
aiWorker = null
1345+
}
1346+
1347+
try {
1348+
aiWorker = new AIWorker()
1349+
} catch (err) {
1350+
console.error('Failed to create AI Worker:', err)
1351+
isAIThinking.value = false
1352+
return
1353+
}
13411354

1342-
isAIThinking.value = false
1355+
aiWorker.onmessage = (e: MessageEvent<{ type: string; move: AIDetailedMove | null }>) => {
1356+
aiWorker = null
1357+
isAIThinking.value = false
13431358

1344-
if (bestMove) {
1345-
executeAIMoveOnBoard(bestMove)
1346-
// AI 走棋完成后,尝试执行玩家预设的 premove
1347-
if (!isGameOver.value && gameMode.value === 'ai') {
1348-
void nextTick(() => {
1349-
tryExecutePremove()
1350-
})
1359+
if (e.data.type === 'bestMove' && e.data.move) {
1360+
executeAIMoveOnBoard(e.data.move)
1361+
// AI 走棋完成后,尝试执行玩家预设的 premove
1362+
if (!isGameOver.value && gameMode.value === 'ai') {
1363+
void nextTick(() => {
1364+
tryExecutePremove()
1365+
})
1366+
}
13511367
}
13521368
}
1369+
1370+
aiWorker.onmessageerror = () => {
1371+
console.error('AI Worker: message could not be deserialized')
1372+
aiWorker?.terminate()
1373+
aiWorker = null
1374+
isAIThinking.value = false
1375+
}
1376+
1377+
aiWorker.onerror = (err) => {
1378+
console.error('AI Worker error:', err)
1379+
aiWorker = null
1380+
isAIThinking.value = false
1381+
}
1382+
1383+
// JSON 序列化确保完全剥离 Vue 响应式 Proxy,避免 postMessage 的 DataCloneError
1384+
try {
1385+
const plainBoard = JSON.parse(JSON.stringify(board.value)) as Board
1386+
const plainLastMove = lastMove.value
1387+
? (JSON.parse(JSON.stringify(lastMove.value)) as {
1388+
from: { row: number; col: number }
1389+
to: { row: number; col: number }
1390+
})
1391+
: null
1392+
1393+
aiWorker.postMessage({
1394+
type: 'findBestMove',
1395+
board: plainBoard,
1396+
color: aiColor,
1397+
difficulty: aiDifficulty.value,
1398+
style: aiStyle.value,
1399+
lastMove: plainLastMove,
1400+
})
1401+
} catch (err) {
1402+
console.error('Failed to post message to AI Worker:', err)
1403+
aiWorker?.terminate()
1404+
aiWorker = null
1405+
isAIThinking.value = false
1406+
}
13531407
}, thinkDelay)
13541408
}
13551409

src/workers/ai-worker.ts

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import { getBestAIMove } from '../models/ai'
2+
import type { Board, Color, Square } from '../models/chess'
3+
4+
// ============================================================
5+
// 消息类型
6+
// ============================================================
7+
interface WorkerRequest {
8+
type: 'findBestMove'
9+
board: Board
10+
color: Color
11+
difficulty: number
12+
style: string
13+
lastMove: { from: Square; to: Square } | null
14+
}
15+
16+
interface WorkerResponse {
17+
type: 'bestMove'
18+
move: Record<string, unknown> | null
19+
}
20+
21+
// 全局错误处理,确保任何异常都回传主线程
22+
self.addEventListener('error', (evt) => {
23+
console.error('Worker global error:', evt.message)
24+
})
25+
26+
self.addEventListener('unhandledrejection', (evt) => {
27+
console.error('Worker unhandled rejection:', evt.reason)
28+
})
29+
30+
// ============================================================
31+
// Worker 入口
32+
// ============================================================
33+
self.onmessage = (e: MessageEvent<WorkerRequest>) => {
34+
if (e.data.type !== 'findBestMove') return
35+
36+
const { board, color, difficulty, style, lastMove } = e.data
37+
38+
try {
39+
const move = getBestAIMove(
40+
board as Board,
41+
color as Color,
42+
difficulty as 1 | 2 | 3 | 4 | 5,
43+
style as 'balanced' | 'aggressive' | 'defensive' | 'unpredictable',
44+
lastMove,
45+
)
46+
47+
const response: WorkerResponse = {
48+
type: 'bestMove',
49+
move: move as unknown as Record<string, unknown> | null,
50+
}
51+
self.postMessage(response)
52+
} catch (err) {
53+
console.error('Worker computation error:', err)
54+
self.postMessage({ type: 'bestMove', move: null })
55+
}
56+
}

0 commit comments

Comments
 (0)