【Rust】基于Rust + WebAssembly;实现人机记忆井字棋游戏(人机对战)

文章目录

    • 项目介绍
    • 技术栈
    • 项目实现
      • [1. 项目结构](#1. 项目结构)
      • [2. Cargo.toml 配置](#2. Cargo.toml 配置)
      • [3. Rust 游戏逻辑 (`src/lib.rs`)](#3. Rust 游戏逻辑 (src/lib.rs))
        • [3.1 数据结构定义](#3.1 数据结构定义)
        • [3.2 游戏核心方法](#3.2 游戏核心方法)
        • [3.3 Minimax 算法实现](#3.3 Minimax 算法实现)
      • [4. 前端实现](#4. 前端实现)
        • [4.1 HTML 结构 (`index.html`)](#4.1 HTML 结构 (index.html))
        • [4.2 JavaScript 交互 (`index.js`)](#4.2 JavaScript 交互 (index.js))
        • [4.3 样式设计 (`style.css`)](#4.3 样式设计 (style.css))
    • 项目运行
      • [方法 1:使用一键启动脚本](#方法 1:使用一键启动脚本)
      • [方法 2:手动编译运行](#方法 2:手动编译运行)
        • [步骤 1:安装 wasm-pack](#步骤 1:安装 wasm-pack)
        • [步骤 2:编译 WebAssembly](#步骤 2:编译 WebAssembly)
        • [步骤 3:启动本地服务器](#步骤 3:启动本地服务器)
        • [步骤 4:打开浏览器](#步骤 4:打开浏览器)
      • 游戏玩法
    • 项目总结

项目介绍

本项目使用 Rust 和 WebAssembly 技术实现了一个在线人机对战井字棋游戏。玩家可以在网页中与采用 Minimax 算法的 AI 进行对战。项目展示了如何使用 Rust 编写高性能的游戏逻辑,并通过 WebAssembly 在浏览器中运行。

核心特性:

  • 流畅的网页游戏体验
  • 智能 AI 对手(Minimax 算法,永不失败)
  • 现代化响应式 UI 设计
  • WebAssembly 带来的高性能
  • 完全响应式布局,支持移动端

适用场景:

  • 学习 Rust + WebAssembly 开发
  • 理解游戏 AI 算法(Minimax)
  • 练习前后端交互
  • WebAssembly 入门项目

技术栈

技术 版本 用途
Rust 2021 edition 游戏逻辑实现
WebAssembly - 编译目标平台
wasm-bindgen 0.2 Rust/JavaScript 互操作
serde 1.0 数据序列化
HTML5 - 页面结构
CSS3 - 样式和动画
JavaScript (ES6+) - 前端交互逻辑

项目实现

1. 项目结构

plain 复制代码
tic-tac-toe-wasm/
├── src/
│   └── lib.rs              # Rust 游戏核心逻辑
├── pkg/                    # 编译生成的 WebAssembly 文件
│   ├── tic_tac_toe_wasm.js
│   ├── tic_tac_toe_wasm_bg.wasm
│   └── ...
├── index.html              # 游戏主页面
├── style.css               # 样式文件
├── index.js                # JavaScript 交互代码
├── Cargo.toml              # Rust 项目配置
├── run.bat                 # 一键启动脚本

2. Cargo.toml 配置

toml 复制代码
[package]
name = "tic-tac-toe-wasm"
version = "0.1.0"
edition = "2021"

[package.metadata.wasm-pack.profile.release]
wasm-opt = false

[lib]
crate-type = ["cdylib", "rlib"]

[dependencies]
wasm-bindgen = "0.2"
serde = { version = "1.0", features = ["derive"] }
serde-wasm-bindgen = "0.6"

[dev-dependencies]
wasm-bindgen-test = "0.3"

配置说明:

  • crate-type = ["cdylib", "rlib"]:生成动态库供 WebAssembly 使用
  • wasm-bindgen:Rust 与 JavaScript 互操作的桥梁
  • serde:用于数据序列化,便于 Rust 和 JS 之间传递复杂数据

3. Rust 游戏逻辑 (src/lib.rs)

3.1 数据结构定义
rust 复制代码
use wasm_bindgen::prelude::*;
use serde::{Deserialize, Serialize};

#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
pub enum Player {
    X,  // 玩家
    O,  // AI
}

#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
pub enum Cell {
    Empty,
    Filled(Player),
}

#[wasm_bindgen]
#[derive(Clone, Serialize, Deserialize)]
pub struct Game {
    board: Vec<Cell>,                // 3x3 棋盘(9个格子)
    current_player: Player,          // 当前玩家
    game_over: bool,                 // 游戏是否结束
    winner: Option<Player>,          // 获胜者
}
  • 使用枚举类型清晰表示玩家和格子状态
  • #[wasm_bindgen] 宏标记需要暴露给 JavaScript 的类型
  • #[derive(Serialize, Deserialize)] 支持数据序列化
3.2 游戏核心方法
rust 复制代码
use wasm_bindgen::prelude::*;
use serde::{Deserialize, Serialize};

#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
pub enum Player {
    X,
    O,
}

#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
pub enum Cell {
    Empty,
    Filled(Player),
}

#[wasm_bindgen]
#[derive(Clone, Serialize, Deserialize)]
pub struct Game {
    board: Vec<Cell>,
    current_player: Player,
    game_over: bool,
    winner: Option<Player>,
}

#[wasm_bindgen]
impl Game {
    #[wasm_bindgen(constructor)]
    pub fn new() -> Game {
        Game {
            board: vec![Cell::Empty; 9],
            current_player: Player::X,
            game_over: false,
            winner: None,
        }
    }

    pub fn make_move(&mut self, position: usize) -> bool {
        if self.game_over || position >= 9 || self.board[position] != Cell::Empty {
            return false;
        }

        self.board[position] = Cell::Filled(self.current_player);
        
        if let Some(winner) = self.check_winner() {
            self.game_over = true;
            self.winner = Some(winner);
        } else if self.is_board_full() {
            self.game_over = true;
        } else {
            self.current_player = match self.current_player {
                Player::X => Player::O,
                Player::O => Player::X,
            };
        }

        true
    }

    pub fn ai_move(&mut self) {
        if self.game_over {
            return;
        }

        // 使用 Minimax 算法找最佳移动
        if let Some(best_move) = self.find_best_move() {
            self.make_move(best_move);
        }
    }

    pub fn reset(&mut self) {
        self.board = vec![Cell::Empty; 9];
        self.current_player = Player::X;
        self.game_over = false;
        self.winner = None;
    }

    pub fn is_game_over(&self) -> bool {
        self.game_over
    }

    pub fn get_winner(&self) -> JsValue {
        match self.winner {
            Some(Player::X) => JsValue::from_str("X"),
            Some(Player::O) => JsValue::from_str("O"),
            None => JsValue::NULL,
        }
    }

    pub fn get_board(&self) -> Vec<String> {
        self.board.iter().map(|cell| {
            match cell {
                Cell::Empty => "".to_string(),
                Cell::Filled(Player::X) => "X".to_string(),
                Cell::Filled(Player::O) => "O".to_string(),
            }
        }).collect()
    }

    pub fn is_draw(&self) -> bool {
        self.game_over && self.winner.is_none()
    }
}
  • 清晰的游戏状态管理
  • 完善的边界条件检查
  • 使用 JsValue 与 JavaScript 交互
  • 通过 serde-wasm-bindgen 传递复杂数据
3.3 Minimax 算法实现
rust 复制代码
impl Game {
    fn check_winner(&self) -> Option<Player> {
        let winning_patterns = [
            [0, 1, 2], [3, 4, 5], [6, 7, 8], // 横
            [0, 3, 6], [1, 4, 7], [2, 5, 8], // 竖
            [0, 4, 8], [2, 4, 6],            // 斜
        ];

        for pattern in &winning_patterns {
            if let Cell::Filled(player) = self.board[pattern[0]] {
                if self.board[pattern[1]] == Cell::Filled(player)
                    && self.board[pattern[2]] == Cell::Filled(player)
                {
                    return Some(player);
                }
            }
        }

        None
    }

    fn is_board_full(&self) -> bool {
        self.board.iter().all(|cell| *cell != Cell::Empty)
    }

    fn find_best_move(&self) -> Option<usize> {
        let mut best_score = i32::MIN;
        let mut best_move = None;

        for i in 0..9 {
            if self.board[i] == Cell::Empty {
                let mut temp_game = self.clone();
                temp_game.board[i] = Cell::Filled(Player::O);
                
                let score = temp_game.minimax(0, false);
                
                if score > best_score {
                    best_score = score;
                    best_move = Some(i);
                }
            }
        }

        best_move
    }

    fn minimax(&self, depth: i32, is_maximizing: bool) -> i32 {
        if let Some(winner) = self.check_winner() {
            return match winner {
                Player::O => 10 - depth,  // AI 胜利
                Player::X => depth - 10,  // 玩家胜利
            };
        }

        if self.is_board_full() {
            return 0; // 平局
        }

        if is_maximizing {
            let mut best_score = i32::MIN;
            for i in 0..9 {
                if self.board[i] == Cell::Empty {
                    let mut temp_game = self.clone();
                    temp_game.board[i] = Cell::Filled(Player::O);
                    let score = temp_game.minimax(depth + 1, false);
                    best_score = best_score.max(score);
                }
            }
            best_score
        } else {
            let mut best_score = i32::MAX;
            for i in 0..9 {
                if self.board[i] == Cell::Empty {
                    let mut temp_game = self.clone();
                    temp_game.board[i] = Cell::Filled(Player::X);
                    let score = temp_game.minimax(depth + 1, true);
                    best_score = best_score.min(score);
                }
            }
            best_score
        }
    }
}

Minimax 算法原理:

  1. 递归搜索:遍历所有可能的游戏状态树
  2. 极大极小:AI 最大化自己分数,假设玩家会最小化 AI 分数
  3. 剪枝优化:使用深度惩罚,优先选择更快获胜的路径
  4. 完美策略:在井字棋这种简单游戏中,AI 永远不会输

4. 前端实现

4.1 HTML 结构 (index.html)
html 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>井字棋 - 人机对战</title>
    <link rel="stylesheet" href="style.css">
</head>
<body>
    <div class="container">
        <h1>井字棋游戏</h1>
        <div class="game-info">
            <div class="status" id="status">轮到你了 (X)</div>
            <button class="btn-reset" id="resetBtn">重新开始</button>
        </div>
        
        <div class="board" id="board">
            <div class="cell" data-index="0"></div>
            <div class="cell" data-index="1"></div>
            <div class="cell" data-index="2"></div>
            <div class="cell" data-index="3"></div>
            <div class="cell" data-index="4"></div>
            <div class="cell" data-index="5"></div>
            <div class="cell" data-index="6"></div>
            <div class="cell" data-index="7"></div>
            <div class="cell" data-index="8"></div>
        </div>

        <div class="info">
            <p> 游戏规则:</p>
            <ul>
                <li>你是 <strong>X</strong>,AI 是 <strong>O</strong></li>
                <li>三个相同标记连成一线即获胜</li>
                <li>AI 使用 Minimax 算法,非常聪明哦!</li>
            </ul>
        </div>
    </div>

    <script type="module" src="./index.js"></script>
</body>
</html>
4.2 JavaScript 交互 (index.js)
javascript 复制代码
import init, { Game } from './pkg/tic_tac_toe_wasm.js';

let game;
let isAIThinking = false;

async function run() {
    await init();
    
    game = new Game();
    
    setupEventListeners();
    updateBoard();
    updateStatus();
}

function setupEventListeners() {
    const cells = document.querySelectorAll('.cell');
    cells.forEach(cell => {
        cell.addEventListener('click', handleCellClick);
    });
    
    document.getElementById('resetBtn').addEventListener('click', resetGame);
}

async function handleCellClick(e) {
    if (isAIThinking || game.is_game_over()) {
        return;
    }
    
    const index = parseInt(e.target.dataset.index);
    
    // 玩家下棋
    if (game.make_move(index)) {
        updateBoard();
        updateStatus();
        
        // 检查游戏是否结束
        if (game.is_game_over()) {
            showGameOver();
            return;
        }
        
        // AI 下棋
        isAIThinking = true;
        updateStatus('AI 正在思考...');
        
        // 延迟一下让 AI 看起来在思考
        setTimeout(() => {
            game.ai_move();
            updateBoard();
            isAIThinking = false;
            updateStatus();
            
            if (game.is_game_over()) {
                showGameOver();
            }
        }, 500);
    }
}

function updateBoard() {
    const board = game.get_board();
    const cells = document.querySelectorAll('.cell');
    
    cells.forEach((cell, index) => {
        cell.textContent = board[index];
        cell.classList.remove('x', 'o', 'filled');
        
        if (board[index]) {
            cell.classList.add('filled');
            cell.classList.add(board[index].toLowerCase());
        }
    });
}

function updateStatus(customMessage) {
    const statusEl = document.getElementById('status');
    
    if (customMessage) {
        statusEl.textContent = customMessage;
        return;
    }
    
    if (game.is_game_over()) {
        const winner = game.get_winner();
        if (winner === null) {
            statusEl.textContent = '平局!';
            statusEl.style.color = '#ffa500';
        } else if (winner === 'X') {
            statusEl.textContent = ' 你赢了!';
            statusEl.style.color = '#4caf50';
        } else {
            statusEl.textContent = ' AI 赢了!';
            statusEl.style.color = '#f44336';
        }
    } else {
        statusEl.textContent = '轮到你了 (X)';
        statusEl.style.color = '#667eea';
    }
}

function showGameOver() {
    const board = document.getElementById('board');
    board.classList.add('game-over');
    updateStatus();
}

function resetGame() {
    game.reset();
    const board = document.getElementById('board');
    board.classList.remove('game-over');
    updateBoard();
    updateStatus();
    isAIThinking = false;
}

run().catch(err => console.error('初始化失败:', err));

交互逻辑:

  • 使用 ES6 模块导入 WebAssembly
  • 异步初始化 WASM 模块
  • 实时更新界面状态
  • AI 思考时添加延迟效果,提升用户体验
4.3 样式设计 (style.css)
css 复制代码
* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

body {
  font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
  min-height: 100vh;
  display: flex;
  justify-content: center;
  align-items: center;
  padding: 20px;
}

.container {
  background: white;
  border-radius: 20px;
  padding: 40px;
  box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
  max-width: 500px;
  width: 100%;
}

h1 {
  text-align: center;
  color: #333;
  margin-bottom: 30px;
  font-size: 2.5em;
}

.game-info {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 30px;
}

.status {
  font-size: 1.3em;
  font-weight: bold;
  color: #667eea;
}

.btn-reset {
  background: #667eea;
  color: white;
  border: none;
  padding: 12px 24px;
  border-radius: 8px;
  font-size: 1em;
  cursor: pointer;
  transition: all 0.3s;
  font-weight: bold;
}

.btn-reset:hover {
  background: #764ba2;
  transform: translateY(-2px);
  box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
}

.btn-reset:active {
  transform: translateY(0);
}

.board {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 10px;
  margin-bottom: 30px;
  aspect-ratio: 1;
}

.cell {
  background: #f0f0f0;
  border: none;
  border-radius: 10px;
  font-size: 3em;
  font-weight: bold;
  cursor: pointer;
  transition: all 0.3s;
  display: flex;
  justify-content: center;
  align-items: center;
  color: #333;
}

.cell:hover:not(.filled) {
  background: #e0e0e0;
  transform: scale(1.05);
}

.cell.filled {
  cursor: not-allowed;
}

.cell.x {
  color: #667eea;
  animation: fadeIn 0.3s;
}

.cell.o {
  color: #764ba2;
  animation: fadeIn 0.3s;
}

@keyframes fadeIn {
  from {
    opacity: 0;
    transform: scale(0.5);
  }
  to {
    opacity: 1;
    transform: scale(1);
  }
}

.info {
  background: #f8f9fa;
  padding: 20px;
  border-radius: 10px;
  border-left: 4px solid #667eea;
}

.info p {
  font-size: 1.1em;
  font-weight: bold;
  color: #333;
  margin-bottom: 10px;
}

.info ul {
  list-style: none;
  padding-left: 0;
}

.info li {
  padding: 8px 0;
  color: #666;
  position: relative;
  padding-left: 25px;
}

.info li::before {
  content: "✓";
  position: absolute;
  left: 0;
  color: #667eea;
  font-weight: bold;
}

.game-over {
  pointer-events: none;
}

.win-line {
  animation: winPulse 0.6s infinite;
}

@keyframes winPulse {
  0%, 100% {
    background: #ffd700;
  }
  50% {
    background: #ffed4e;
  }
}

@media (max-width: 600px) {
  .container {
    padding: 20px;
  }

  h1 {
    font-size: 2em;
  }

  .cell {
    font-size: 2em;
  }

  .game-info {
    flex-direction: column;
    gap: 15px;
  }
}

关键样式特性:

  • 渐变背景
  • 卡片式设计
  • 悬停动画效果
  • 响应式布局
  • 获胜动画

项目运行

方法 1:使用一键启动脚本

双击 run.bat 文件,脚本会自动:

  1. 检查并安装 wasm-pack
  2. 编译 WebAssembly
  3. 启动本地服务器
  4. 打开浏览器

方法 2:手动编译运行

步骤 1:安装 wasm-pack
bash 复制代码
cargo install wasm-pack
步骤 2:编译 WebAssembly
bash 复制代码
cd tic-tac-toe-wasm
wasm-pack build --target web

编译成功后会在 pkg/ 目录生成:

  • tic_tac_toe_wasm.js - JavaScript 绑定
  • tic_tac_toe_wasm_bg.wasm - WebAssembly 二进制文件
  • tic_tac_toe_wasm.d.ts - TypeScript 类型定义
步骤 3:启动本地服务器
bash 复制代码
# 使用 Python
python -m http.server 8000

# 或使用 Node.js
npx http-server -p 8000
步骤 4:打开浏览器

访问 http://localhost:8000

游戏玩法

  1. 打开游戏后,你是 X,AI 是 O
  2. 点击任意空格子下棋
  3. AI 会自动应对(短暂延迟后)
  4. 三个相同符号连成一线即获胜
  5. 点击"重新开始"按钮开始新游戏

项目总结

本项目完整展示了如何使用 Rust 和 WebAssembly 构建一个交互式网页游戏。通过实现经典的井字棋游戏和 Minimax AI 算法,展示Rust 在 Web 领域的应用潜力,WebAssembly 带来的性能优势,理解游戏 AI 的基本原理,实现前后端协作的最佳实践。

WebAssembly 为 Web 开发带来了新的可能性,特别是在需要高性能计算的场景下。Rust 的内存安全性和性能优势,使其成为 WebAssembly 开发的理想选择。

想了解更多关于Rust语言的知识及应用,可前往旋武开源社区(https://xuanwu.openatom.cn/),了解更多资讯~

相关推荐
万事可爱^6 小时前
GitHub爆火开源项目——RustScan深度拆解
c语言·开发语言·rust·开源·github·rustscan
受之以蒙12 小时前
具身智能的“任督二脉”:用 Rust ndarray 打通数据闭环的最后一公里
人工智能·笔记·rust
有梦想的攻城狮12 小时前
初识Rust语言
java·开发语言·rust
JosieBook19 小时前
【Rust】基于Rust 设计开发nginx运行日志高效分析工具
服务器·网络·rust
四问四不知19 小时前
Rust语言入门
开发语言·rust
JosieBook19 小时前
【Rust】 基于Rust 从零构建一个本地 RSS 阅读器
开发语言·后端·rust
云边有个稻草人19 小时前
部分移动(Partial Move)的使用场景:Rust 所有权拆分的精细化实践
开发语言·算法·rust
咸甜适中1 天前
rust语言,将JSON中的所有值以字符串形式存储到sqlite数据库中(逐行注释)
数据库·rust·sqlite·json
在人间负债^1 天前
Rust 实战项目:TODO 管理器
开发语言·后端·rust