From 36f6b15b8e2ca8c3b90a55b8c1127d37fde685e4 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 00:18:07 +0800 Subject: [PATCH] =?UTF-8?q?feat(gui):=20Tauri=20IPC=20=E5=91=BD=E4=BB=A4?= =?UTF-8?q?=20=E2=80=94=20new=5Fgame/place=5Fpiece/undo/ai=5Fmove/get=5Fbo?= =?UTF-8?q?ard/get=5Fgame=5Fstate?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.7 --- gui/src/commands.rs | 162 ++++++++++++++++++++++++++++++++++++++++++++ gui/src/lib.rs | 14 ++++ 2 files changed, 176 insertions(+) create mode 100644 gui/src/commands.rs diff --git a/gui/src/commands.rs b/gui/src/commands.rs new file mode 100644 index 0000000..9e714e3 --- /dev/null +++ b/gui/src/commands.rs @@ -0,0 +1,162 @@ +use gobang_core::ai::search::AlphaBetaAi; +use gobang_core::ai::AiEngine; +use gobang_core::board::Board; +use gobang_core::rules; +use gobang_core::types::*; +use std::sync::Mutex; +use tauri::State; + +/// 应用全局状态 +pub struct AppState { + pub board: Mutex>, + pub game_mode: Mutex, + pub config: Mutex, + pub ai_engine: Mutex>, + pub current_color: Mutex, + pub game_over: Mutex, +} + +impl Default for AppState { + fn default() -> Self { + Self { + board: Mutex::new(None), + game_mode: Mutex::new(GameMode::Local), + config: Mutex::new(GameConfig::default()), + ai_engine: Mutex::new(None), + current_color: Mutex::new(Color::Black), + game_over: Mutex::new(true), + } + } +} + +#[tauri::command] +pub fn new_game(mode: GameMode, config: GameConfig, state: State) -> Result<(), String> { + let is_vs_ai = mode == GameMode::VsAi; + let board = Board::new(config.board_size); + *state.board.lock().map_err(|e| e.to_string())? = Some(board); + *state.game_mode.lock().map_err(|e| e.to_string())? = mode; + *state.config.lock().map_err(|e| e.to_string())? = config.clone(); + *state.current_color.lock().map_err(|e| e.to_string())? = config.player_color; + *state.game_over.lock().map_err(|e| e.to_string())? = false; + + // 初始化 AI (如果是人机模式) + if is_vs_ai { + let ai = AlphaBetaAi::new(config.ai_difficulty as usize); + *state.ai_engine.lock().map_err(|e| e.to_string())? = Some(ai); + } + + Ok(()) +} + +#[tauri::command] +pub fn place_piece(x: usize, y: usize, state: State) -> Result { + // 检查游戏是否结束 + { + let game_over = state.game_over.lock().map_err(|e| e.to_string())?; + if *game_over { + return Err("游戏已结束".into()); + } + } + + let color = *state.current_color.lock().map_err(|e| e.to_string())?; + let pos = Position::new(x, y); + + // 在作用域内验证并落子,确保 board 锁在写入前释放 + let (new_board, is_win) = { + let board_opt = state.board.lock().map_err(|e| e.to_string())?; + let board = board_opt.as_ref().ok_or("游戏未开始")?; + let config = state.config.lock().map_err(|e| e.to_string())?; + + // 禁手检查 + if config.use_forbidden_rules && rules::is_forbidden(board, pos, color) { + return Err("禁手位置,不能落子".into()); + } + + let new_board = board.place(pos, color).map_err(|e| e.to_string())?; + let is_win = new_board.check_win(pos); + (new_board, is_win) + }; + + // 更新游戏状态(前面作用域内的锁已全部释放) + *state.board.lock().map_err(|e| e.to_string())? = Some(new_board); + *state.current_color.lock().map_err(|e| e.to_string())? = color.opponent(); + *state.game_over.lock().map_err(|e| e.to_string())? = is_win; + + Ok(MoveResult { + position: pos, + is_win, + is_forbidden: false, + }) +} + +#[tauri::command] +pub fn undo(steps: u32, state: State) -> Result<(), String> { + let mut board_opt = state.board.lock().map_err(|e| e.to_string())?; + let mut board = board_opt.clone().ok_or("游戏未开始")?; + + for _ in 0..steps * 2 { + board = board.undo().map_err(|e| e.to_string())?; + } + + *board_opt = Some(board); + Ok(()) +} + +#[tauri::command] +pub fn get_board(state: State) -> Result>, String> { + let board_opt = state.board.lock().map_err(|e| e.to_string())?; + let board = board_opt.as_ref().ok_or("游戏未开始")?; + + let mut result = vec![vec![0i32; board.size]; board.size]; + for x in 0..board.size { + for y in 0..board.size { + result[x][y] = match board.get(Position::new(x, y)) { + CellState::Empty => 0, + CellState::Occupied(Color::Black) => 1, + CellState::Occupied(Color::White) => 2, + }; + } + } + Ok(result) +} + +#[tauri::command] +pub fn ai_move(state: State) -> Result, String> { + let board_opt = state.board.lock().map_err(|e| e.to_string())?; + let board = board_opt.as_ref().ok_or("游戏未开始")?; + let color = *state.current_color.lock().map_err(|e| e.to_string())?; + let ai = state.ai_engine.lock().map_err(|e| e.to_string())?; + let ai = ai.as_ref().ok_or("AI 未初始化")?; + + Ok(ai.best_move(board, color).map(|p| (p.x, p.y))) +} + +#[tauri::command] +pub fn get_game_state(state: State) -> Result { + let board_opt = state.board.lock().map_err(|e| e.to_string())?; + let color = *state.current_color.lock().map_err(|e| e.to_string())?; + let game_over = *state.game_over.lock().map_err(|e| e.to_string())?; + let board = board_opt.as_ref(); + + let cells: Vec> = board + .map(|b| { + (0..b.size) + .map(|x| { + (0..b.size) + .map(move |y| match b.get(Position::new(x, y)) { + CellState::Empty => 0, + CellState::Occupied(Color::Black) => 1, + CellState::Occupied(Color::White) => 2, + }) + .collect() + }) + .collect() + }) + .unwrap_or_default(); + + Ok(serde_json::json!({ + "board": cells, + "current_color": match color { Color::Black => "Black", Color::White => "White" }, + "game_over": game_over, + })) +} diff --git a/gui/src/lib.rs b/gui/src/lib.rs index 9e30d77..7e857a6 100644 --- a/gui/src/lib.rs +++ b/gui/src/lib.rs @@ -1,6 +1,20 @@ +mod commands; + +use commands::AppState; + +#[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { tauri::Builder::default() .plugin(tauri_plugin_opener::init()) + .manage(AppState::default()) + .invoke_handler(tauri::generate_handler![ + commands::new_game, + commands::place_piece, + commands::undo, + commands::get_board, + commands::ai_move, + commands::get_game_state, + ]) .run(tauri::generate_context!()) .expect("error while running tauri application"); }