feat(gui): Tauri IPC 命令 — new_game/place_piece/undo/ai_move/get_board/get_game_state

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-31 00:18:07 +08:00
parent a17fba8ff5
commit 36f6b15b8e
2 changed files with 176 additions and 0 deletions
+162
View File
@@ -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<Option<Board>>,
pub game_mode: Mutex<GameMode>,
pub config: Mutex<GameConfig>,
pub ai_engine: Mutex<Option<AlphaBetaAi>>,
pub current_color: Mutex<Color>,
pub game_over: Mutex<bool>,
}
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<AppState>) -> 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<AppState>) -> Result<MoveResult, String> {
// 检查游戏是否结束
{
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<AppState>) -> 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<AppState>) -> Result<Vec<Vec<i32>>, 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<AppState>) -> Result<Option<(usize, usize)>, 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<AppState>) -> Result<serde_json::Value, String> {
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<Vec<i32>> = 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,
}))
}
+14
View File
@@ -1,6 +1,20 @@
mod commands;
use commands::AppState;
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() { pub fn run() {
tauri::Builder::default() tauri::Builder::default()
.plugin(tauri_plugin_opener::init()) .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!()) .run(tauri::generate_context!())
.expect("error while running tauri application"); .expect("error while running tauri application");
} }