Tauri(2.5.1)+Leptos(0.8.2)开发自用桌面小程序_tauri-2048
在之前工作(Tauri(2.5.1)+Leptos(0.7.8)开发桌面应用--简单的工作进度管理-CSDN博客)的基础上,添加一个休闲数字游戏2048。具体效果如下:
使用leptos-router新建一个标签页,用于2048游戏界面。
1. src/main.rs
mod app;use app::*;use leptos::prelude::*;//打开trunk serve --open 以开始开发您的应用程序。 Trunk 服务器将在文件更改时重新加载您的应用程序,从而使开发相对无缝。fn main() { console_error_panic_hook::set_once(); //浏览器中运行 WASM 代码发生 panic 时可以获得一个实际的 Rust 堆栈跟踪,其中包括 Rust 源代码中的一行。 mount_to_body(|| { view! { } })}
2. src/app.rs
#[warn(unused_imports)]use leptos::prelude::*;use leptos_router::components::{Route, Router, Routes};use leptos_router::path;mod acidinput;mod schedule;mod game2048;mod game5;use acidinput::*;use schedule::*;use game2048::*;use game5::*;#[component]pub fn App() -> impl IntoView { view! { // / just has an un-nested \"Home\" <Route path=path!(\"/\") view= || view! {} /> <Route path=path!(\"/acidinput\") view=|| view! {} /> <Route path=path!(\"/game2048\") view=|| view! {} /> <Route path=path!(\"/game5\") view=|| view! {} /> }}
3. src/app/game2048.rs
use leptos::*;use leptos::prelude::*;use leptos::component;use leptos::view;/// 定义移动方向的枚举#[derive(Clone, Copy, PartialEq)]pub enum Direction { Up, // 向上移动 Down, // 向下移动 Left, // 向左移动 Right, // 向右移动}/// 游戏状态结构体#[derive(Clone)]pub struct Game { pub grid: [[u32; 4]; 4], // 4x4游戏网格 pub score: u32, // 当前得分 pub game_over: bool, // 游戏是否结束 pub win: bool, // 是否获胜(达到2048)}impl Game { /// 生成[min, max)范围内的随机数 fn random_range(&self, min: usize, max: usize) -> usize { use rand::random; min + (random::() * (max - min) as f64).floor() as usize } /// 以给定概率返回true fn random_bool(&self, probability: f64) -> bool { use rand::random; random::() Self { let mut game = Game { grid: [[0; 4]; 4], // 初始化4x4空网格 score: 0, // 初始分数为0 game_over: false, // 游戏未结束 win: false, // 未获胜 }; game.add_tile(); // 添加第一个方块 game.add_tile(); // 添加第二个方块 game } /// 在随机空位置添加新方块(90%概率为2,10%概率为4) pub fn add_tile(&mut self) { let mut empty_positions = Vec::new(); // 收集所有空位置 for (i, row) in self.grid.iter().enumerate() { for (j, &cell) in row.iter().enumerate() { if cell == 0 { empty_positions.push((i, j)); } } } // 如果有空位置,随机选择一个添加新方块 if !empty_positions.is_empty() { let (i, j) = empty_positions[self.random_range(0, empty_positions.len())]; self.grid[i][j] = if self.random_bool(0.9) { 2 } else { 4 }; } } /// 根据方向移动方块 pub fn move_tiles(&mut self, direction: Direction) { let mut moved = false; // 标记是否有方块移动 let mut grid = self.grid; match direction { Direction::Left => { // 向左移动每行 for row in &mut grid { moved |= self.slide_row(row); } } Direction::Right => { // 向右移动: 先反转行,滑动后再反转回来 for row in &mut grid { row.reverse(); moved |= self.slide_row(row); row.reverse(); } } Direction::Up => { // 向上移动: 先转置网格,滑动每行后再转置回来 self.transpose(&mut grid); for row in &mut grid { moved |= self.slide_row(row); } self.transpose(&mut grid); } Direction::Down => { // 向下移动: 转置网格,反转每行,滑动后再反转并转置回来 self.transpose(&mut grid); for row in &mut grid { row.reverse(); moved |= self.slide_row(row); row.reverse(); } self.transpose(&mut grid); } } // 如果有方块移动,更新网格并添加新方块 if moved { self.grid = grid; self.add_tile(); self.check_game_over(); // 检查游戏是否结束 } } /// 滑动单行方块并合并相同数字 fn slide_row(&mut self, row: &mut [u32; 4]) -> bool { let mut moved = false; // 标记是否有移动 let mut merged = [false; 4]; // 标记已合并的方块 // 第一步: 将所有方块向左滑动(消除空格) for _ in 0..3 { for i in 0..3 { if row[i] == 0 && row[i + 1] != 0 { row[i] = row[i + 1]; row[i + 1] = 0; moved = true; } } } // 第二步: 合并相邻相同数字 for i in 0..3 { if row[i] != 0 && row[i] == row[i + 1] && !merged[i] { row[i] *= 2; // 合并方块 self.score += row[i]; // 增加分数 if row[i] == 2048 { self.win = true; // 达到2048,获胜 } row[i + 1] = 0; // 清空合并后的位置 merged[i] = true; // 标记已合并 moved = true; } } // 第三步: 再次滑动消除合并后产生的空格 for _ in 0..3 { for i in 0..3 { if row[i] == 0 && row[i + 1] != 0 { row[i] = row[i + 1]; row[i + 1] = 0; moved = true; } } } moved // 返回是否有移动发生 } /// 转置4x4网格(行列互换) fn transpose(&self, grid: &mut [[u32; 4]; 4]) { for i in 0..4 { for j in i + 1..4 { let temp = grid[i][j]; grid[i][j] = grid[j][i]; grid[j][i] = temp; } } } /// 检查游戏是否结束(无空格且无法合并) fn check_game_over(&mut self) { if self.win { return; // 已经获胜,不需要检查 } // 检查是否有空格 for row in &self.grid { for &cell in row { if cell == 0 { return; // 有空位,游戏继续 } } } // 检查是否有可合并的相邻方块 for i in 0..4 { for j in 0..4 { let cell = self.grid[i][j]; if (j < 3 && cell == self.grid[i][j + 1]) || (i impl IntoView { // 创建游戏状态信号 let (game, set_game) = signal(Game::new()); // 监听键盘事件 window_event_listener(ev::keydown, move |ev| { if game.get().game_over || game.get().win { return; // 游戏结束或已获胜,不处理输入 } // 根据按键确定移动方向 let direction = match &ev.key()[..] { \"ArrowUp\" => Some(Direction::Up), \"ArrowDown\" => Some(Direction::Down), \"ArrowLeft\" => Some(Direction::Left), \"ArrowRight\" => Some(Direction::Right), _ => None, }; // 如果有有效方向,移动方块 if let Some(dir) = direction { set_game.update(|g| g.move_tiles(dir)); } }); // 渲染方块内容(空方块显示空字符串) fn render_tile(value: u32) -> String { if value == 0 { \"\".to_string() } else { value.to_string() } } // 根据方块值返回对应的CSS颜色类 fn tile_color(value: u32) -> &\'static str { match value { 0 => \"bg-gray-300\", 2 => \"bg-yellow-100\", 4 => \"bg-yellow-200\", 8 => \"bg-orange-200\", 16 => \"bg-orange-300\", 32 => \"bg-red-300\", 64 => \"bg-red-400\", 128 => \"bg-amber-400\", 256 => \"bg-amber-500\", 512 => \"bg-amber-600\", 1024 => \"bg-yellow-700 text-white\", 2048 => \"bg-yellow-800 text-white\", _ => \"bg-purple-500 text-white\", } } // 重置游戏函数 let reset = move |_| { set_game.update(|g| { *g = Game::new(); g.win = false; }); }; // 游戏界面视图 view! { \"2048小游戏\"
\"得分\" {move || game.get().score} {move || { game.get().grid.iter().flat_map(|row| { row.iter().map(|&value| { view! { {render_tile(value)} } }) }).collect::<Vec>() }} {/* 获胜提示 */} \"You Win! Final Score: \" {move || game.get().score} {/* 游戏结束提示 */} \"Game Over! Final Score: \" {move || game.get().score} }}
4. cargo.toml
[package]name = \"schedule-app\"version = \"0.1.0\"description = \"A Work-schedule\"authors = [\"you\"]edition = \"2021\"[lib]name = \"acid_index_lib\"crate-type = [\"staticlib\", \"cdylib\", \"rlib\"][build-dependencies]tauri-build = { version = \"2.2.0\", features = [] }[dependencies]tauri = { version = \"2.5.1\", features = [\"tray-icon\", \"devtools\"] }wasm-bindgen = \"0.2.100\"serde-wasm-bindgen = \"0.6.5\"tauri-utils=\"2.4.0\"tauri-plugin=\"2.2.0\"tauri-plugin-opener = \"2\"serde = { version = \"1.0.219\", features = [\"derive\"] }serde_json = \"1\"sqlx = { version = \"0.8.6\", features = [\"sqlite\", \"runtime-tokio\"] }tokio = { version =\"1\", features = [\"full\"] }futures = \"0.3.31\"log = \"0.4.22\"chrono = \"0.4.39\"plotters = { version = \"0.3.7\"}plotters-iced = \"0.11\"iced = { version = \"0.13.1\", features = [\"canvas\", \"tokio\"] }base64 = \"0.22.1\"image = \"0.25.5\"uuid = { version = \"1.8.0\", features = [\"v4\"] }video-rs = { version = \"0.10\", features = [\"ndarray\"] }ndarray = \"0.16\"