From dddfd035c67afcbf5f7e02fa93f94c0a3a4d3801 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E8=88=AA=E5=AE=87?= <3364451258@qq.com> Date: Sun, 31 May 2026 13:46:45 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=8E=A5=E5=85=A5=20LLM=20AI=20?= =?UTF-8?q?=E5=BC=95=E6=93=8E=E5=88=B0=20GUI=EF=BC=8CGameConfig=20?= =?UTF-8?q?=E6=94=AF=E6=8C=81=20useLlm=20=E5=88=87=E6=8D=A2=20AI=20?= =?UTF-8?q?=E7=B1=BB=E5=9E=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.7 --- core/src/types.rs | 12 ++++++++++++ gui/src/commands.rs | 34 ++++++++++++++++++++-------------- src/core/types.ts | 4 ++++ 3 files changed, 36 insertions(+), 14 deletions(-) diff --git a/core/src/types.rs b/core/src/types.rs index 714d04d..b9dca3e 100644 --- a/core/src/types.rs +++ b/core/src/types.rs @@ -121,6 +121,14 @@ pub struct GameConfig { pub is_server: bool, #[serde(default)] pub remote_address: String, + #[serde(default)] + pub use_llm: bool, + #[serde(default)] + pub llm_endpoint: String, + #[serde(default)] + pub llm_api_key: String, + #[serde(default)] + pub llm_model: String, } impl Default for GameConfig { @@ -134,6 +142,10 @@ impl Default for GameConfig { player_color: Color::Black, is_server: false, remote_address: String::new(), + use_llm: false, + llm_endpoint: String::new(), + llm_api_key: String::new(), + llm_model: String::new(), } } } diff --git a/gui/src/commands.rs b/gui/src/commands.rs index 8a002b1..a666858 100644 --- a/gui/src/commands.rs +++ b/gui/src/commands.rs @@ -1,5 +1,6 @@ use gobang_core::ai::search::AlphaBetaAi; use gobang_core::ai::AiEngine; +use gobang_core::llm::LlmAi; use gobang_core::board::Board; use gobang_core::rules; use gobang_core::types::*; @@ -11,7 +12,7 @@ pub struct AppState { pub board: Mutex>, pub game_mode: Mutex, pub config: Mutex, - pub ai_engine: Mutex>, + pub ai_engine: Mutex>>, pub current_color: Mutex, pub game_over: Mutex, } @@ -42,7 +43,15 @@ pub fn new_game(mode: GameMode, config: GameConfig, state: State) -> R // 初始化 AI (如果是人机模式) if is_vs_ai { - let ai = AlphaBetaAi::new(config.ai_difficulty as usize); + let ai: Box = if config.use_llm { + Box::new(LlmAi::new( + &config.llm_endpoint, + &config.llm_api_key, + &config.llm_model, + )) + } else { + Box::new(AlphaBetaAi::new(config.ai_difficulty as usize)) + }; *state.ai_engine.lock().map_err(|e| e.to_string())? = Some(ai); } @@ -121,24 +130,21 @@ pub fn undo(steps: u32, state: State) -> Result<(), String> { #[tauri::command] pub fn ai_move(state: State) -> Result, String> { - let (board_clone, color, ai_clone) = { + // 预先提取棋盘和当前颜色,释放 board/color 锁 + let (board_clone, color) = { let board_opt = state.board.lock().map_err(|e| e.to_string())?; let board = board_opt.as_ref().ok_or("游戏未开始")?.clone(); let color = *state.current_color.lock().map_err(|e| e.to_string())?; - let ai_guard = state.ai_engine.lock().map_err(|e| e.to_string())?; - let ai = ai_guard.as_ref().ok_or("AI 未初始化")?.clone(); - (board, color, ai) + (board, color) }; - let (tx, rx) = std::sync::mpsc::channel(); - std::thread::spawn(move || { - let result = ai_clone.best_move(&board_clone, color); - let _ = tx.send(result); - }); + // 持有 ai_engine 锁同步调用 best_move(阻塞 IPC 命令,但只影响 AI 走棋) + let ai_guard = state.ai_engine.lock().map_err(|e| e.to_string())?; + let ai = ai_guard.as_ref().ok_or("AI 未初始化")?; + let result = ai.best_move(&board_clone, color); + drop(ai_guard); - rx.recv_timeout(std::time::Duration::from_secs(30)) - .map_err(|_| "AI 计算超时".to_string()) - .map(|r| r.map(|p| (p.x, p.y))) + Ok(result.map(|p| (p.x, p.y))) } #[tauri::command] diff --git a/src/core/types.ts b/src/core/types.ts index 1a7c9d5..a8b4d8c 100644 --- a/src/core/types.ts +++ b/src/core/types.ts @@ -20,6 +20,10 @@ export interface GameConfig { playerColor: Color; isServer: boolean; remoteAddress: string; + useLlm?: boolean; + llmEndpoint?: string; + llmApiKey?: string; + llmModel?: string; } export interface MoveResult {