@@ -14,8 +14,9 @@ import {
1414 generateMoveNotation ,
1515} from '../models/chess'
1616import type { GameSetupConfig , AIStyle } from '../components/GameSetup.vue'
17- import { getBestAIMove , getPromotionChoice , type AIDifficulty } from '../models/ai'
17+ import { getPromotionChoice , type AIDifficulty } from '../models/ai'
1818import 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
0 commit comments