1. 设计思路
1.1 选择有限的宇宙
康威生命游戏的世界是无限二维网格,但由于计算机内存和性能有限,我们通常采用以下三种有限宇宙策略:
- 动态扩展宇宙:仅存储“活跃区域”,并按需扩展(可能会无限增长)。
- 固定大小无边界:边界处的细胞无法继续扩展,会被“消灭”。
- 固定大小的环绕宇宙(Toroidal Universe)✅(我们采用此方案)
环绕宇宙允许**滑翔机(Glider)**无限运行,而不会被边界阻止:
- 顶部边界的细胞与底部边界相连
- 左侧边界的细胞与右侧边界相连
2. Rust 代码实现
2.1 定义 Cell
结构
#[wasm_bindgen]
#[repr(u8)]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Cell {
Dead = 0,
Alive = 1,
}
解析:
#[repr(u8)]
使Cell
占用 1 字节,减少内存浪费。Dead = 0, Alive = 1
便于使用整数加法计算活细胞数量。
2.2 定义 Universe
结构
#[wasm_bindgen]
pub struct Universe {
width: u32,
height: u32,
cells: Vec<Cell>,
}
解析:
width
、height
:宇宙的宽度和高度。cells: Vec<Cell>
:存储所有细胞状态(0=死,1=活)。
2.3 计算网格索引
impl Universe {
fn get_index(&self, row: u32, column: u32) -> usize {
(row * self.width + column) as usize
}
}
- 计算二维网格在一维数组中的索引,方便访问细胞状态。
2.4 计算活邻居数量
impl Universe {
fn live_neighbor_count(&self, row: u32, column: u32) -> u8 {
let mut count = 0;
for delta_row in [self.height - 1, 0, 1].iter().cloned() {
for delta_col in [self.width - 1, 0, 1].iter().cloned() {
if delta_row == 0 && delta_col == 0 {
continue;
}
let neighbor_row = (row + delta_row) % self.height;
let neighbor_col = (column + delta_col) % self.width;
let idx = self.get_index(neighbor_row, neighbor_col);
count += self.cells[idx] as u8;
}
}
count
}
}
解析:
- 使用
modulo
实现环绕宇宙,保证边界细胞能正确访问邻居。 - 避免
if
语句,减少特殊情况,提高性能。
2.5 更新细胞状态
#[wasm_bindgen]
impl Universe {
pub fn tick(&mut self) {
let mut next = self.cells.clone();
for row in 0..self.height {
for col in 0..self.width {
let idx = self.get_index(row, col);
let cell = self.cells[idx];
let live_neighbors = self.live_neighbor_count(row, col);
let next_cell = match (cell, live_neighbors) {
(Cell::Alive, x) if x < 2 => Cell::Dead,
(Cell::Alive, 2) | (Cell::Alive, 3) => Cell::Alive,
(Cell::Alive, x) if x > 3 => Cell::Dead,
(Cell::Dead, 3) => Cell::Alive,
(otherwise, _) => otherwise,
};
next[idx] = next_cell;
}
}
self.cells = next;
}
}
解析:
- 翻译生命游戏规则:
- 少于 2 个活邻居 → 死亡
- 2 或 3 个活邻居 → 存活
- 超过 3 个活邻居 → 死亡
- 死细胞有 3 个活邻居 → 复活
3. Rust WebAssembly 单元测试
3.1 增加 set_width
和 set_height
#[wasm_bindgen]
impl Universe {
pub fn set_width(&mut self, width: u32) {
self.width = width;
self.cells = (0..width * self.height).map(|_i| Cell::Dead).collect();
}
pub fn set_height(&mut self, height: u32) {
self.height = height;
self.cells = (0..self.width * height).map(|_i| Cell::Dead).collect();
}
}
- 调整宇宙大小时,所有细胞重置为 Dead。
3.2 增加 get_cells
和 set_cells
impl Universe {
pub fn get_cells(&self) -> &[Cell] {
&self.cells
}
pub fn set_cells(&mut self, cells: &[(u32, u32)]) {
for (row, col) in cells.iter().cloned() {
let idx = self.get_index(row, col);
self.cells[idx] = Cell::Alive;
}
}
}
get_cells
→ 获取宇宙的当前状态。set_cells
→ 手动设置特定细胞为 Alive,方便测试。
3.3 定义 Spaceship 初始状态
#[cfg(test)]
pub fn input_spaceship() -> Universe {
let mut universe = Universe::new();
universe.set_width(6);
universe.set_height(6);
universe.set_cells(&[(1,2), (2,3), (3,1), (3,2), (3,3)]);
universe
}
#[cfg(test)]
pub fn expected_spaceship() -> Universe {
let mut universe = Universe::new();
universe.set_width(6);
universe.set_height(6);
universe.set_cells(&[(2,1), (2,3), (3,2), (3,3), (4,2)]);
universe
}
input_spaceship()
→ 初始 glider 形态。expected_spaceship()
→ 经过一次tick()
之后的正确形态。
3.4 编写 test_tick
单元测试
#[wasm_bindgen_test]
pub fn test_tick() {
let mut input_universe = input_spaceship();
let expected_universe = expected_spaceship();
input_universe.tick();
assert_eq!(&input_universe.get_cells(), &expected_universe.get_cells());
}
测试步骤:
- 创建
input_spaceship
宇宙 - 创建
expected_spaceship
宇宙 - 执行
tick()
- 对比
get_cells()
结果
4. 运行测试
在 wasm-game-of-life
目录执行:
wasm-pack test --firefox --headless
- 也可用 Chrome、Safari 进行测试:
wasm-pack test --chrome --headless wasm-pack test --safari --headless
5. 总结
- Rust + WebAssembly 实现康威生命游戏
- 编写单元测试,确保
tick()
逻辑正确 - 支持不同尺寸的宇宙
- 可在不同浏览器环境中运行
这就是一个完整、优化的 WebAssembly 生命游戏!