https://github.com/yiransheng/rust-snake-wasm
A snake game in rust and webassembly
https://github.com/yiransheng/rust-snake-wasm
blockade rust snake-game wasm wasm-bindgen web-assymbly
Last synced: 12 months ago
JSON representation
A snake game in rust and webassembly
- Host: GitHub
- URL: https://github.com/yiransheng/rust-snake-wasm
- Owner: yiransheng
- License: mit
- Created: 2018-03-22T05:17:16.000Z (over 8 years ago)
- Default Branch: master
- Last Pushed: 2018-12-08T21:34:43.000Z (over 7 years ago)
- Last Synced: 2025-04-14T12:06:25.866Z (about 1 year ago)
- Topics: blockade, rust, snake-game, wasm, wasm-bindgen, web-assymbly
- Language: Rust
- Size: 1.32 MB
- Stars: 58
- Watchers: 2
- Forks: 2
- Open Issues: 1
-
Metadata Files:
- Readme: README.MD
- License: LICENSE
Awesome Lists containing this project
README
# Rust WASM Snake Game
Play Link: [http://yiransheng.github.io/rust-snake-wasm/](http://yiransheng.github.io/rust-snake-wasm/)

A weirdly designed snake game in rust & WebAssembley, primarily aimed for retained mode rendering targeting canvas. Two non-browser targets are also included in [./non_browser](./non_browser).
## Features
* Snake movements are animated and smoothed over
* Variable render speed, hold down direction keys to accelerate (opposite direction key to deaccelerate)

* VIM key bindings supported (h,j,k,l)
* `no_std`
* Retained rendering; a game of life like appoach to game states
* Perhaps too over-engineered, trying to explore various fun and unique abstractions afforded by rust
* Minimal world model completely decoupled from drawing code, easily ported over to other environments and rendering targets (although in trying to run it with piston, retained rendering doesn't quite work..)
* ..and enabled writing unit tests nicely like so:
```rust
#[test]
fn test_death() {
let snake_string = indoc!(
"
..........
>>>>>>v...
......v...
..^<<<<...
.........*"
);
let afterwards = indoc!(
"
..........
.oooooo...
..o...o...
..ooooo...
.........*"
);
let mut world: World = World::from_ascii(snake_string);
while let Ok(_) = world.step(None) {}
assert_matches!(world.step(None), Err(UpdateError::CollideBody));
assert_eq!(&afterwards, &world.grid.to_string());
}
```
## Similar Projects
Other snakesssssss (with rust and wasm):
* https://github.com/alamminsalo/snakewasm
* https://github.com/dmacewen/snake_wasm
* https://github.com/tensor-programming/wasm_snake_example
* https://github.com/mardiros/wasm-snake
* https://github.com/BurNiinTRee/Snake
* https://github.com/DarrenTsung/wrg-snake
* https://github.com/kirjavascript/snake-rs
## Usage
### Stuff to Install
1. `wasm32-unknown-unknown` target and `wasm-bindgen-cli`
```
rustup target add wasm32-unknown-unknown --toolchain nightly
cargo +nightly install wasm-bindgen-cli
```
2. `node` and `yarn`
3. (optional) `wasm-opt` install from: [https://github.com/WebAssembly/binaryen](https://github.com/WebAssembly/binaryen)
### Build
Development
```
make dev
```
Build (cargo release build, `no_std`, `wee_alloc`, `webpack --mode=production` and `wasm-opt`)
```
make
# serve ./docs
```
Test
```
cargo test
```
## Brief Overview
The core structure of this game is `World` (mod: `world`), its side-effects/outputs are:
```rust
enum WorldUpdate {
SetBlock { block: Block, at: Coordinate },
Clear { prev_block: Block, at: Coordinate },
SetWorldSize(u16, u16),
}
enum UpdateError {
HeadDetached, // panics, game bug, should not happen
TailDetached, // panics, game bug, should not happen
OutOfBound,
CollideBody,
}
type Result = ::std::result;
```
`World` itself is just:
```rust
struct World {
fn initialize(&'a mut self) -> impl Iterator {
// ...
}
fn tear_down(&mut self) {
// ...
}
fn step(&mut self, cmd: Option) -> Result> {
// ...
}
}
```
It's complete devoid of drawing code and does not concern itself with game start/stop and other game loop level controls. This allowed itself to be reused for very different runtimes (browser, terminal etc., Although when trying to adapt it to a `piston_window`, not exposing its internal state really drove things into a corner).
`initialize` returns an `Iterator` of `WorldUpdate`, this simplifies things for renderers, as they only need to deal with `WorldUpdate` data type alone, and do not need to worry about behaving differently during initialization vs. normal game play.
This formulation of game world is abstracted out as a `trait` (generic lifetime ``m` allows `initialize` to return a iterator that borrows the struct itself).
```rust
trait Stateful<'m> {
type Cmd;
type Init: IntoIterator + 'm;
type Update;
type Error: Into;
fn initialize(&'m mut self) -> Self::Init;
fn step(
&mut self,
cmd: Option,
) -> Result, Self::Error>;
fn tear_down(&mut self);
}
```
Two other addons that implement this `trait` are `RenderSpeed` and `Dead`, responsible for controlling acceleration and gameover/restart respectively. In the end all things are glued together with provided combinator methods on `Stateful` `trait` :
```rust
let world: World = WorldBuilder::new()
.width(64)
.height(32)
.set_snake(1, 1)
.extend(Direction::East)
.extend(Direction::East)
.extend(Direction::East)
.extend(Direction::East)
.build_with_seed([123; 16]);
let game = world
.zip_with(RenderSpeed::new(Direction::East), VariableFrame::pack)
.alternating::(Dead::new())
.make_game(CanvasEnv::new());
```
This same code is pretty much reused for `piston_snake` and `terminal_snake` as well, just using different `Env` (than `CanvasEnv`) implementations there.
Game loop is provided by `js` side:
```rust
#[wasm_bindgen(module = "./game-loop")]
extern "C" {
type GameLoop;
#[wasm_bindgen(constructor)]
fn new(run: &Closure) -> GameLoop;
#[wasm_bindgen(method)]
fn start(this: &GameLoop) -> bool;
#[wasm_bindgen(method)]
fn stop(this: &GameLoop) -> bool;
}
```
`js` class `GameLoop` takes a closure from `wasm`, and run it on a `requestAnimationFrame` loop, supplying a `u8` for pressed key code in each tick.
Finally game update code is packaged as a `Generator`. Since rust does not allow sending data into generator (unlike `javaScript` and `python`), a channel-like "`Sender`" is also returned to feed in data/commands (it's just a `Rc>>` under the hood), but I imagine in a multi-threaded context by using `std::sync::mpsc::channel` , this pattern would still work.
```rust
let (tx, mut generator) = game.new_game();
let each_tick = Closure::wrap(Box::new(move |key: u8| {
let key = Key::from(key);
tx.send(key);
unsafe {
generator.resume();
}
}) as Box);
let game_loop = GameLoop::new(&each_tick);
game_loop.start();
each_tick.forget();
```
The returned `generator` uses `Generator` `yield` syntax to encode a simple state machine that alternates between rendering ticks and game logic tick. Getting [this piece](./src/system/state.rs#L252) to compile (and not leak memory) took me a long time to figure out, but it was a pretty good exercise to understand `rust` ownership model on a deeper level.
While this architecture is largely unnecessary for such a simple game (and probably does not scale to real world games at all) - going about in in a very generic and modular way and having everything tied together in the end was still very satisfying.