Merge pull request #1 from yishn/snake

Snake
This commit is contained in:
Yichuan Shen
2022-05-21 17:23:56 +02:00
committed by GitHub
9 changed files with 323 additions and 0 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 255 KiB

After

Width:  |  Height:  |  Size: 192 KiB

2
snake/.rustfmt.toml Normal file
View File

@@ -0,0 +1,2 @@
tab_spaces = 2
max_width = 80

20
snake/Cargo.toml Normal file
View File

@@ -0,0 +1,20 @@
[package]
name = "snake"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib", "rlib"]
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
wasm-bindgen = "0.2.80"
js-sys = "0.3.57"
[dependencies.web-sys]
version = "0.3.57"
features = [
"Document", "Element", "HtmlElement", "Window", "console",
"CssStyleDeclaration", "HtmlDivElement", "KeyboardEvent"
]

26
snake/README.md Normal file
View File

@@ -0,0 +1,26 @@
# Snake
[<img src="./thumbnail.png" alt="Thumbnail" width="300" />][video]<br/>
[_Watch Video_][video]
[video]: https://www.youtube.com/watch?v=iR7Q_6quwSI
## Building
Make sure you have [Rust](https://www.rust-lang.org) installed and [wasm-pack](https://rustwasm.github.io/wasm-pack/). To build this project, run:
```
$ wasm-pack build --target web
```
To run this project, you need a static file server. You can install `serve` with npm:
```
$ npm install serve -g
```
Now, start your static file server and open `index.html`:
```
$ serve
```

39
snake/index.html Normal file
View File

@@ -0,0 +1,39 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<title>Snake</title>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<style>
html {
font-size: 200%;
text-align: center;
}
#root {
border: 1px solid grey;
}
.field {
width: 1rem;
height: 1rem;
line-height: 1rem;
text-indent: -.2rem;
}
</style>
</head>
<body>
<div id="root"></div>
<script type="module">
import init from "./pkg/snake.js";
async function main() {
await init();
}
main();
</script>
</body>
</html>

114
snake/src/lib.rs Normal file
View File

@@ -0,0 +1,114 @@
mod random;
mod snake;
use js_sys::Function;
use snake::{Direction, SnakeGame};
use std::{cell::RefCell, rc::Rc};
use wasm_bindgen::{prelude::*, JsCast, UnwrapThrowExt};
use web_sys::{window, HtmlDivElement, HtmlElement, KeyboardEvent};
thread_local! {
static GAME: Rc<RefCell<SnakeGame>> =
Rc::new(RefCell::new(SnakeGame::new(15, 15)));
static HANDLE_TICK: Closure<dyn FnMut()> = Closure::wrap(Box::new(|| {
GAME.with(|game| game.borrow_mut().tick());
render();
}) as Box<dyn FnMut()>);
static HANDLE_KEYDOWN: Closure<dyn FnMut(KeyboardEvent)> =
Closure::wrap(Box::new(|evt: KeyboardEvent| GAME.with(|game| {
let direction = match &evt.key()[..] {
"ArrowUp" => Some(Direction::Up),
"ArrowRight" => Some(Direction::Right),
"ArrowDown" => Some(Direction::Down),
"ArrowLeft" => Some(Direction::Left),
_ => None,
};
if let Some(direction) = direction {
game.borrow_mut().change_direction(direction);
}
})) as Box<dyn FnMut(KeyboardEvent)>)
}
#[wasm_bindgen(start)]
pub fn main() {
HANDLE_TICK.with(|tick_closure| {
window()
.unwrap_throw()
.set_interval_with_callback_and_timeout_and_arguments_0(
tick_closure.as_ref().dyn_ref::<Function>().unwrap_throw(),
200,
)
.unwrap_throw()
});
HANDLE_KEYDOWN.with(|handle_keydown| {
window()
.unwrap_throw()
.add_event_listener_with_callback(
"keydown",
handle_keydown.as_ref().dyn_ref::<Function>().unwrap_throw(),
)
.unwrap_throw();
});
render();
}
pub fn render() {
GAME.with(|game| {
let game = game.borrow();
let document = window().unwrap_throw().document().unwrap_throw();
let root_container = document
.get_element_by_id("root")
.unwrap_throw()
.dyn_into::<HtmlElement>()
.unwrap_throw();
root_container.set_inner_html("");
let width = game.width;
let height = game.height;
root_container
.style()
.set_property("display", "inline-grid")
.unwrap_throw();
root_container
.style()
.set_property(
"grid-template",
&format!("repeat({}, auto) / repeat({}, auto)", width, height),
)
.unwrap_throw();
for y in 0..height {
for x in 0..width {
let pos = (x, y);
let field_element = document
.create_element("div")
.unwrap_throw()
.dyn_into::<HtmlDivElement>()
.unwrap_throw();
field_element.set_class_name("field");
field_element.set_inner_text({
if pos == game.food {
"🍎"
} else if game.snake.get(0) == Some(&pos) {
"❇️"
} else if game.snake.contains(&pos) {
"🟩"
} else {
" "
}
});
root_container.append_child(&field_element).unwrap_throw();
}
}
});
}

11
snake/src/random.rs Normal file
View File

@@ -0,0 +1,11 @@
use wasm_bindgen::prelude::*;
#[wasm_bindgen]
extern "C" {
#[wasm_bindgen(js_namespace = Math)]
fn random() -> f64;
}
pub fn random_range(min: usize, max: usize) -> usize {
(random() * (max - min) as f64).floor() as usize + min
}

111
snake/src/snake.rs Normal file
View File

@@ -0,0 +1,111 @@
use crate::random::random_range;
use std::collections::VecDeque;
pub type Position = (usize, usize);
#[derive(Debug, Clone, Copy)]
pub enum Direction {
Up,
Right,
Down,
Left,
}
#[derive(Debug)]
pub struct SnakeGame {
pub width: usize,
pub height: usize,
pub snake: VecDeque<Position>, // Head is the first item, tail is the last item
pub direction: Direction,
next_direction: Direction,
pub food: Position,
pub finished: bool,
}
impl SnakeGame {
pub fn new(width: usize, height: usize) -> Self {
Self {
width,
height,
snake: [((width - 3).max(0), height / 2)].into_iter().collect(),
direction: Direction::Left,
next_direction: Direction::Left,
food: (2.min(width - 1), height / 2),
finished: false,
}
}
pub fn change_direction(&mut self, direction: Direction) {
if self.finished {
return;
}
match (self.direction, direction) {
(Direction::Up, Direction::Up)
| (Direction::Up, Direction::Down)
| (Direction::Right, Direction::Right)
| (Direction::Right, Direction::Left)
| (Direction::Down, Direction::Up)
| (Direction::Down, Direction::Down)
| (Direction::Left, Direction::Right)
| (Direction::Left, Direction::Left) => {}
(_, direction) => self.next_direction = direction,
}
}
pub fn is_valid(&self, (x, y): Position) -> bool {
x < self.width && y < self.height
}
pub fn tick(&mut self) {
if self.finished && self.snake.len() == 0 {
return;
}
self.direction = self.next_direction;
let (x, y) = self.snake[0];
// WARNING: There's no explicit underflow handling here
// (will panic in debug build)
let new_head = match self.direction {
Direction::Up => (x, y - 1),
Direction::Right => (x + 1, y),
Direction::Down => (x, y + 1),
Direction::Left => (x - 1, y),
};
if !self.is_valid(new_head) || self.snake.contains(&new_head) {
// Lose conditions
self.finished = true;
} else {
if new_head != self.food {
// Do not pop tail when eating food to make snake longer
self.snake.pop_back();
} else {
let free_positions = (0..self.height)
.flat_map(|y| (0..self.width).map(move |x| (x, y)))
.filter(|pos| !self.snake.contains(pos))
.collect::<Vec<_>>();
if free_positions.is_empty() {
self.finished = true;
return;
}
self.food = free_positions[random_range(0, free_positions.len())];
}
self.snake.push_front(new_head);
}
}
}
#[cfg(test)]
mod tests {
use super::SnakeGame;
#[test]
fn test() {
println!("{:?}", SnakeGame::new(10, 10));
}
}

BIN
snake/thumbnail.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 113 KiB