文章目录
-
- 项目介绍
- 技术栈
- 项目实现
-
- [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))
- [4.1 HTML 结构 (`index.html`)](#4.1 HTML 结构 (
- 项目运行
-
- [方法 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 算法原理:
- 递归搜索:遍历所有可能的游戏状态树
- 极大极小:AI 最大化自己分数,假设玩家会最小化 AI 分数
- 剪枝优化:使用深度惩罚,优先选择更快获胜的路径
- 完美策略:在井字棋这种简单游戏中,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 文件,脚本会自动:
- 检查并安装 wasm-pack
- 编译 WebAssembly
- 启动本地服务器
- 打开浏览器



方法 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:打开浏览器


游戏玩法
- 打开游戏后,你是 X,AI 是 O
- 点击任意空格子下棋
- AI 会自动应对(短暂延迟后)
- 三个相同符号连成一线即获胜
- 点击"重新开始"按钮开始新游戏
项目总结
本项目完整展示了如何使用 Rust 和 WebAssembly 构建一个交互式网页游戏。通过实现经典的井字棋游戏和 Minimax AI 算法,展示Rust 在 Web 领域的应用潜力,WebAssembly 带来的性能优势,理解游戏 AI 的基本原理,实现前后端协作的最佳实践。
WebAssembly 为 Web 开发带来了新的可能性,特别是在需要高性能计算的场景下。Rust 的内存安全性和性能优势,使其成为 WebAssembly 开发的理想选择。
想了解更多关于Rust语言的知识及应用,可前往旋武开源社区(https://xuanwu.openatom.cn/),了解更多资讯~