使用Rust制作康威生命游戏的实现代码

目录
  • 前言
  • 安装准备
  • 初始项目
  • Cargo.toml
  • 初始web项目
  • 游戏规则
  • 游戏设计
  • Rust实现
  • 测试
  • 调试

前言

之前学了几遍,后来忘记了,通过制作该游戏再复习复习。

安装准备

cargo install cargo-generate

初始项目

初始rust项目

使用wasm的项目模板:

cargo generate --git https://github.com/rustwasm/wasm-pack-template
  • 提示输入project名wasm-game-of-life
  • 在lib.rs中可以看见如下内容:
mod utils;
use wasm_bindgen::prelude::*;
// When the `wee_alloc` feature is enabled, use `wee_alloc` as the global
// allocator.
#[cfg(feature = "wee_alloc")]
#[global_allocator]
static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT;

#[wasm_bindgen]
extern {
    fn alert(s: &str);
}

#[wasm_bindgen]
pub fn greet() {
    alert("Hello, wasm-game-of-life!");
}
  • 它导入 window.alertJavaScript 函数,并导出greet的Rust 函数。

Cargo.toml

  • Cargo.toml预置了[lib]和[dependencies]。解释一下crate-type中f=“https://users.rust-lang.org/t/what-is-the-difference-between-dylib-and-cdylib/28847”>cdylib和rlib的作用:
  • cdylib:顾名思义,是C的动态链接库的意思,可以被C和C++程序链接使用
  • rlib:Rust静态链接库,用于静态连接其他crates
  • 依赖中使用的:
  • wasm-bindgen可以将Rust编写的函数和结构体暴露到JS中或者把JS的方法引入到Rust中使用
  • console_error_panic_hook提供了Wasm输出Rust Panic的能力
  • wee_alloc是一个轻量的Wasm内存分配器,但是会比默认分配器慢一些。

初始web项目

npm init wasm-app www
  • 看到生成的pkg.json:
{
  "name": "create-wasm-app",
  "version": "0.1.0",
  "description": "create an app to consume rust-generated wasm packages",
  "main": "index.js",
  "bin": {
    "create-wasm-app": ".bin/create-wasm-app.js"
  },
  "scripts": {
    "build": "webpack --config webpack.config.js",
    "start": "webpack-dev-server"
  },
  • html里导入boostrap.js,boostrap.js里导入index.js。 index.js里面导入了其已经制作好的一个包:
import * as wasm from "hello-wasm-pack";
wasm.greet();
  • 我们修改pkg.json,导入自己的包(该包需要使用wasm-pack build生成)
  "wasm-game-of-life": "file:../pkg"
  • 将index.js更换下:
import * as wasm from "wasm-game-of-life";
wasm.greet();
  • 使用npm i 安装依赖。
  • 使用npm run start 启动页面,打开http://localhost:8080/即可看见alert。

游戏规则

  • Conway’s Game of Life是英国数学家约翰·何顿·康威在1970年发明的放置类无玩家参与的游戏
  • 百度百科
  • https://baike.baidu.com/item/%E5%BA%B7%E5%A8%81%E7%94%9F%E5%91%BD%E6%B8%B8%E6%88%8F/22668799?fr=aladdin主要规则如下:
  • 1、任何少于两个活邻居的活细胞都会死亡,就像是由于人口不足造成的。
  • 2、任何有两三个活邻居的活细胞都可以活到下一代。
  • 3、任何有超过三个活邻居的活细胞都会死亡,就像人口过剩一样。
  • 4、任何只有三个活邻居的死细胞都会变成活细胞,就像通过繁殖一样。

游戏设计

  • 为啥说这个呢,因为2种语言去做这个东西会考虑哪个东西在哪个里面去实现。
  • rust推荐大型、长寿命的数据结构被实现为 Rust 类型,这些类型存在于 WebAssembly 线性内存中,并作为不透明的句柄暴露给 JavaScript。JavaScript 调用导出的 WebAssembly 函数,这些函数采用这些不透明的句柄、转换它们的数据、执行繁重的计算、查询数据并最终返回一个可复制的结果。通过只返回计算结果,我们避免了在 JavaScript 垃圾收集堆和 WebAssembly 线性内存之间来回复制和/或序列化所有内容。
  • 这个游戏中,会将universe的显示效果暴露给js渲染,其余计算在rust去实现。
  • 由于宇宙是n*n的,所以我们可以用一维数组去表示它,比如4x4的宇宙就是这样:

  • 将数组每个row换下来就是需要的4x4的显示了。因为这种表现形式,所以我们需要对数组索引和行列进行转换,公式为:
index(row, column, universe) = row * width(universe) + column
  • 就比如我要知道4行4列是索引几,根据公式就是3*4 + 3。
  • 每个单元格有一个字节,其中0表示死亡,1表示存活。

Rust实现

首先我们需要定义每个单元格:

#[wasm_bindgen]
#[repr(u8)]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Cell {
    Dead = 0,
    Alive = 1,
}

枚举类型,0是死亡,1是存活,#[repr(u8)]表示一个单元格1字节。复习下:

长度 有符号 无符号
8-bit i8 u8
16-bit i16 u16
32-bit i32 u32
64-bit i64 u64
128-bit i128 u128
arch isize usize

接下来定义宇宙:

#[wasm_bindgen]
pub struct Universe {
    width: u32,
    height: u32,
    cells: Vec<Cell>,
}
  • 宇宙是长宽和一个动态数组。
  • 我们对universe实现一些方法便于操作:
#[wasm_bindgen]
impl Universe {
    fn get_index(&self, row: u32, column: u32) -> usize {
        (row * self.width + column) as usize
    }
}
  • get_index就是上面公式做索引。
  • 从前面游戏规则上可知,我们需要对每个单元格求出周围格子的存活数量,于是加上这个函数:
  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;
                println!("{},{}-s-", neighbor_row, neighbor_col);
                let idx = self.get_index(neighbor_row, neighbor_col);
                count += self.cells[idx] as u8;
            }
        }
        count
    }
  • 解释下这个函数,其中迭代height-1 , 0 , 1 以及 width-1,0,1就是求传入row与col的周围的格子里存活数量。当迭代到0,0时,这个格子代表其自身,所以直接忽略。
  • 比如64x64的宇宙,查询2,2周围的格子就是:

1,1
1,2
1,3
2,1
2,3
3,1
3,2
3,3

  • 边界处理靠取余,这样也能避免无符号向下溢出,所以0,0的周围格子就是:

63,63
63,0-
63,1
0,63
0,1
1,63
1,0
1,1

  • 再从当前宇宙中获取格子的状态,如果是0,那么加上也不会增加,这样最终返回的就是周围格子的存活数量了。
  • 下面根据规则迭代每个细胞状态,暴露出来:
 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) {
                    // Rule 1: Any live cell with fewer than two live neighbours
                    // dies, as if caused by underpopulation.
                    (Cell::Alive, x) if x < 2 => Cell::Dead,
                    // Rule 2: Any live cell with two or three live neighbours
                    // lives on to the next generation.
                    (Cell::Alive, 2) | (Cell::Alive, 3) => Cell::Alive,
                    // Rule 3: Any live cell with more than three live
                    // neighbours dies, as if by overpopulation.
                    (Cell::Alive, x) if x > 3 => Cell::Dead,
                    // Rule 4: Any dead cell with exactly three live neighbours
                    // becomes a live cell, as if by reproduction.
                    (Cell::Dead, 3) => Cell::Alive,
                    // All other cells remain in the same state.
                    (otherwise, _) => otherwise,
                };
                next[idx] = next_cell;
            }
        }
        self.cells = next;
    }
  • 最后需要对universe实现输出功能,先将其输出成文本,实现display方法:
impl fmt::Display for Universe {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        for line in self.cells.as_slice().chunks(self.width as usize) {
            for &cell in line {
                let symbol = if cell == Cell::Dead { '' } else { '' };
                write!(f, "{}", symbol)?;
            }
            write!(f, "\n")?;
        }
        Ok(())
    }
}

最后进行暴露初始化和渲染方法:

pub fn new() -> Universe {
        let width = 64;
        let height = 64;

        let cells = (0..width * height)
            .map(|i| {
                if i % 2 == 0 || i % 7 == 0 {
                    Cell::Alive
                } else {
                    Cell::Dead
                }
            })
            .collect();

        Universe {
            width,
            height,
            cells,
        }
    }

    pub fn render(&self) -> String {
        self.to_string()
    }
  • 使用wasm-pack build打包
  • 使用js渲染,修改html加入标签:
 <pre id="game-of-life-canvas"></pre>

index.js加入下面代码:

import { Universe } from "wasm-game-of-life";
const pre = document.getElementById("game-of-life-canvas");
const universe = Universe.new();
const renderLoop = () => {
	pre.textContent = universe.render();
	universe.tick();
	requestAnimationFrame(renderLoop);
};
renderLoop();
  • 即可看见效果。
  • 下面使用canvas进行渲染,将universe中暴露其属性:
    pub fn width(&self) -> u32 {
        self.width
    }

    pub fn height(&self) -> u32 {
        self.height
    }

    pub fn cells(&self) -> *const Cell {
        self.cells.as_ptr()
    }
  • html中替换为canvas:
<canvas id="game-of-life-canvas"></canvas>

修改js:

import { Universe, Cell } from "wasm-game-of-life";
import { memory } from "wasm-game-of-life/wasm_game_of_life_bg";
const CELL_SIZE = 5; // px
const GRID_COLOR = "#CCCCCC";
const DEAD_COLOR = "#FFFFFF";
const ALIVE_COLOR = "#000000";
const universe = Universe.new();
const width = universe.width();
const height = universe.height();

// Give the canvas room for all of our cells and a 1px border
// around each of them.
const canvas = document.getElementById("game-of-life-canvas");
canvas.height = (CELL_SIZE + 1) * height + 1;
canvas.width = (CELL_SIZE + 1) * width + 1;

const ctx = canvas.getContext("2d");
const drawGrid = () => {
	ctx.beginPath();
	ctx.strokeStyle = GRID_COLOR;
	// Vertical lines.
	for (let i = 0; i <= width; i++) {
		ctx.moveTo(i * (CELL_SIZE + 1) + 1, 0);
		ctx.lineTo(i * (CELL_SIZE + 1) + 1, (CELL_SIZE + 1) * height + 1);
	}
	// Horizontal lines.
	for (let j = 0; j <= height; j++) {
		ctx.moveTo(0, j * (CELL_SIZE + 1) + 1);
		ctx.lineTo((CELL_SIZE + 1) * width + 1, j * (CELL_SIZE + 1) + 1);
	}
	ctx.stroke();
};
const getIndex = (row, column) => {
	return row * width + column;
};

const drawCells = () => {
	const cellsPtr = universe.cells();
	const cells = new Uint8Array(memory.buffer, cellsPtr, width * height);
	ctx.beginPath();
	for (let row = 0; row < height; row++) {
		for (let col = 0; col < width; col++) {
			const idx = getIndex(row, col);
			ctx.fillStyle = cells[idx] === Cell.Dead ? DEAD_COLOR : ALIVE_COLOR;
			ctx.fillRect(
				col * (CELL_SIZE + 1) + 1,
				row * (CELL_SIZE + 1) + 1,
				CELL_SIZE,
				CELL_SIZE
			);
		}
	}
	ctx.stroke();
};
const renderLoop = () => {
	universe.tick();
	drawGrid();
	drawCells();
	requestAnimationFrame(renderLoop);
};
renderLoop();

即可看见效果:

测试

  • 一般代码需要写单元测试,看一下rust的测试怎么写。
  • 首先,对Universe增加2个实现,可以将元组转换为universe的cell:
impl Universe {
    /// Get the dead and alive values of the entire universe.
    pub fn get_cells(&self) -> &[Cell] {
        &self.cells
    }

    /// Set cells to be alive in a universe by passing the row and column
    /// of each cell as an array.
    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;
        }
    }
}

新增重置的方法:

  /// Set the width of the universe.
    ///
    /// Resets all cells to the dead state.
    pub fn set_width(&mut self, width: u32) {
        self.width = width;
        self.cells = (0..width * self.height).map(|_i| Cell::Dead).collect();
    }

    /// Set the height of the universe.
    ///
    /// Resets all cells to the dead state.
    pub fn set_height(&mut self, height: u32) {
        self.height = height;
        self.cells = (0..self.width * height).map(|_i| Cell::Dead).collect();
    }
  • 下面编写测试,测试在tests文件夹下的web.rs中。
  • 增加以下代码:
#![cfg(target_arch = "wasm32")]

extern crate wasm_bindgen_test;
use std::assert_eq;

use wasm_bindgen_test::*;
extern crate wasm_game_of_life;
use wasm_game_of_life::Universe;
wasm_bindgen_test_configure!(run_in_browser);

#[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
}
#[wasm_bindgen_test]
pub fn test_tick() {
    // Let's create a smaller Universe with a small spaceship to test!
    let mut input_universe = input_spaceship();

    // This is what our spaceship should look like
    // after one tick in our universe.
    let expected_universe = expected_spaceship();

    // Call `tick` and then see if the cells in the `Universe`s are the same.
    input_universe.tick();
    assert_eq!(&input_universe.get_cells(), &expected_universe.get_cells());
}
  • 然后使用wasm-pack test --firefox --headless即可运行测试结果。如果安装浏览器失败,可以使用谷歌,或者去掉无头属性,直接网页上看测试结果。

调试

  • 我们知道,web上使用console.log去输出调试内容,rust的代码如何在web中调试呢?
  • 这里需要安装下web-sys
[dependencies.web-sys]
version = "0.3"
features = [
  "console",
]
  • 导入外部websys,制作自定义宏:
extern crate web_sys;
// A macro to provide `println!(..)`-style syntax for `console.log` logging.
macro_rules! log {
    ( $( $t:tt )* ) => {
        web_sys::console::log_1(&format!( $( $t )* ).into());
    }
}

format宏与其他几个输出区别在于其使用write,不输出到标准输出中:

format!: write formatted text to String
print!: same as format! but the text is printed to the console (io::stdout).
println!: same as print! but a newline is appended.
eprint!: same as format! but the text is printed to the standard error (io::stderr).
eprintln!: same as eprint!but a newline is appended.

然后就可以在需要的地方console了,比如neighbours那:

 let live_neighbors = self.live_neighbor_count(row, col);
                log!(
                    "cell[{}, {}] is initially {:?} and has {} live neighbors",
                    row,
                    col,
                    cell,
                    live_neighbors
                );
                let next_cell = match (cell, live_neighbors) {
                    // Rule 1: Any live cell with fewer than two live neighbours
                    // dies, as if caused by underpopulation.
                    (Cell::Alive, x) if x < 2 => Cell::Dead,
                    // Rule 2: Any live cell with two or three live neighbours
                    // lives on to the next generation.
                    (Cell::Alive, 2) | (Cell::Alive, 3) => Cell::Alive,
                    // Rule 3: Any live cell with more than three live
                    // neighbours dies, as if by overpopulation.
                    (Cell::Alive, x) if x > 3 => Cell::Dead,
                    // Rule 4: Any dead cell with exactly three live neighbours
                    // becomes a live cell, as if by reproduction.
                    (Cell::Dead, 3) => Cell::Alive,
                    // All other cells remain in the same state.
                    (otherwise, _) => otherwise,
                };
                log!("    it becomes {:?}", next_cell);
                next[idx] = next_cell;

打开web,即可看见console的内容。

到此这篇关于使用Rust制作康威生命游戏的文章就介绍到这了,更多相关Rust康威生命游戏内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • 深入讲解下Rust模块使用方式

    目录 前言 模块声明&使用 方法一:直接在根文件下声明add.rs 方法二:声明add文件夹,文件夹下包含mod.rs 方法三:add.rs和add文件夹同时存在 同模块相邻文件引用 不同模块引用 小结 前言 本文适用于刚开始学习rust的同学,用于帮助理解rust模块间是如何相互引用的.本文尽量用极少的代码来演示,即便之前没有了解过rust也可以混个眼熟.用的时候可以有个印象. 如果你之前没有了解过rust,只需要知道:Cargo-依赖管理工具,类似于npm,Cargo 使用文件放置约定,即文

  • 详解Rust中的workspace

    目录 一.目录结构 二.子crata中的Cargo.toml声明 三.代码引用 java项目中用maven管理代码时,如果遇到大型工程,一般会拆分成不同的模块,比如spring-mvc中,通常会按model, view, controller建3个模块,然后根据一定的依赖关系进行引用.这个概念在Rust中是通用的,只不过maven换成了cargo,而模块变成了crate,看下面的例子. 一.目录结构 .├── Cargo.toml├── controller│   ├── Cargo.toml│

  • 使用Rust制作康威生命游戏的实现代码

    目录 前言 安装准备 初始项目 Cargo.toml 初始web项目 游戏规则 游戏设计 Rust实现 测试 调试 前言 之前学了几遍,后来忘记了,通过制作该游戏再复习复习. 安装准备 wasm-pack : https://rustwasm.github.io/wasm-pack/installer/ cargo-generate: cargo install cargo-generate 初始项目 初始rust项目 使用wasm的项目模板: cargo generate --git http

  • c# 实现康威生命游戏(细胞自动机)的示例

    规则(来自百度百科,康威生命游戏词条) 游戏开始时,每个细胞随机地设定为"生"或"死"之一的某个状态.然后,根据某种规则,计算出下一代每个细胞的状态,画出下一代细胞的生死分布图. 应该规定什么样的迭代规则呢?需要一个简单的,但又反映生命之间既协同又竞争的生存定律.为简单起见,最基本的考虑是假设每一个细胞都遵循完全一样的生存定律:再进一步,把细胞之间的相互影响只限制在最靠近该细胞的8个邻居中. 也就是说,每个细胞迭代后的状态由该细胞及周围8个细胞状态所决定.作了这些限

  • Python实战之实现康威生命游戏

    前言 康威生命游戏设计并不难,我的思路就是借助pygame进行外观的展示,最近一段时间的游戏项目都是使用pygame进行的,做起来比较顺利.内部代码的实现也比较简单根据他的规则我们需要的是多次的计算和判断,再刷新数组. 一.康威生命游戏规则 当周围仅有1个或没有存活细胞时, 原来的存活细胞进入死亡状态.(模拟生命数量稀少)当周围有2个或3个存活细胞时, 网格保持原样.当周围有4个及以上存活细胞时,原来的存活细胞亦进入死亡状态.(模拟生命数量过多)当周围有3个存活细胞时,空白网格变成存活细胞.(模

  • Python实现生命游戏的示例代码(tkinter版)

    目录 生命游戏(Game of Life) 游戏概述 生存定律 图形结构 代码实现 运行界面 使用简介 后续改进 生命游戏(Game of Life) 由剑桥大学约翰·何顿·康威设计的计算机程序.美国趣味数学大师马丁·加德纳(Martin Gardner,1914-2010)通过<科学美国人>杂志,将康威的生命游戏介绍给学术界之外的广大渎者,一时吸引了各行各业一大批人的兴趣,这时细胞自动机课题才吸引了科学家的注意. 游戏概述 用一个二维表格表示“生存空间”,空间的每个方格中都可放置一个生命细胞

  • 利用Matlab制作三子棋游戏的示例代码

    效果: 注: 由于用uifigure和uiaxes写的会比较慢,改成常规的axes和figure会快很多. 完整代码: function OX_chess fig=uifigure('units','pixels',... 'position',[350 100 500 500],... 'Numbertitle','off',... 'name','OX_chess',... 'Color',[1 1 1],... 'resize','off',... 'menubar','none'); a

  • python实现生命游戏的示例代码(Game of Life)

    生命游戏的算法就不多解释了,百度一下介绍随处可见. 因为网上大多数版本都是基于pygame,matlab等外部库实现的,二维数组大多是用numpy,使用起来学习成本比较高,所以闲暇之余写一个不用外部依赖库,console输出的版本. # -*- coding: utf-8 -*- from time import sleep from copy import deepcopy WORLD_HIGH = 20 #世界长度 WORLD_WIDE = 40 #世界宽度 ALIVE_CON = 3 #复

  • Java制作智能拼图游戏原理及代码

    今天突发奇想,想做一个智能拼图游戏来给哄女友. 需要实现这些功能 第一图片自定义 第二宫格自定义,当然我一开始就想的是3*3 4*4 5*5,没有使用3*5这样的宫格. 第三要实现自动拼图的功能,相信大家知道女人耍游戏都不是很厉害,所以这个自动拼图功能得有. 其他什么暂停.排行就不写了! 现在重点问题出来了 要实现自动拼图功能似乎要求有点高哦!计算机有可不能像人一样只能: 先追究下本质 拼图游戏其实就是排列问题: 排列有这么一个定义:在一个1,2,...,n的排列中,如果一对数的前后位置与大小顺

  • 详解Python 实现元胞自动机中的生命游戏(Game of life)

    简介 细胞自动机(又称元胞自动机),名字虽然很深奥,但是它的行为却是非常美妙的.所有这些怎样实现的呢?我们可以把计算机中的宇宙想象成是一堆方格子构成的封闭空间,尺寸为N的空间就有NN个格子.而每一个格子都可以看成是一个生命体,每个生命都有生和死两种状态,如果该格子生就显示蓝色,死则显示白色.每一个格子旁边都有邻居格子存在,如果我们把33的9个格子构成的正方形看成一个基本单位的话,那么这个正方形中心的格子的邻居就是它旁边的8个格子. 每个格子的生死遵循下面的原则: 1. 如果一个细胞周围有3个细胞

  • Python生命游戏实现原理及过程解析(附源代码)

    1. 生命游戏是什么 生命游戏是英国数学家约翰·何顿·康威在1970年发明的细胞自动机.它包括一个二维矩形世界,这个世界中的每个方格居住着一个活着的或死了的细胞.一个细胞在下一个时刻生死取决于相邻八个方格中活着的或死了的细胞的数量.如果相邻方格活着的细胞数量过多,这个细胞会因为资源匮乏而在下一个时刻死去:相反,如果周围活细胞过少,这个细胞会因太孤单而死去. 规则看起来很简单,但却能演绎出无穷无尽的内容. 滑翔者:每4个回合"它"会向右下角走一格.虽然细胞早就是不同的细胞了,但它能保持原

  • JavaScript 实现生命游戏

    目录 概念介绍 逻辑规则 完整代码 主要实现 概念介绍 元胞自动机(Cellular Automata),是 20 世纪 50 年代初由计算机之父冯·诺依曼(John von Neumann)为了模拟生命系统所具有的自复制功能而提出来的. 生命游戏(Game of Life),或者叫它的全称 John Conway's Game of Life,是英国数学家约翰·康威在 1970 年代所发明的一种元胞自动机. 逻辑规则 在二维平面方格里,每个细胞有两种状态:存活或死亡,而下一时刻的状态完全受它周

随机推荐