diff --git a/Cargo.toml b/Cargo.toml
index 36133b4..502b524 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -3,5 +3,18 @@ name = "rust-wasm-snake"
version = "0.1.0"
edition = "2021"
+[lib]
+crate-type = ["cdylib", "rlib"]
+
[dependencies]
rand = "0.8.5"
+wasm-bindgen = "0.2.80"
+js-sys = "0.3.57"
+getrandom = { version = "0.2.6", features = ["js"] }
+
+[dependencies.web-sys]
+version = "0.3.57"
+features = [
+ "Window", "console", "Document", "HtmlElement", "Element",
+ "CssStyleDeclaration", "HtmlDivElement", "KeyboardEvent"
+]
diff --git a/index.html b/index.html
new file mode 100644
index 0000000..f828761
--- /dev/null
+++ b/index.html
@@ -0,0 +1,38 @@
+
+
+
+
+
+ Snake
+
+
+
+
+
+
+
+
diff --git a/src/lib.rs b/src/lib.rs
index 75943b5..2d613bc 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -1,126 +1,109 @@
-use std::collections::VecDeque;
+mod snake;
-use rand::prelude::*;
+use std::{cell::RefCell, rc::Rc};
-pub type Position = (usize, usize);
+use js_sys::Function;
+use snake::Direction;
+use snake::SnakeGame;
+use wasm_bindgen::prelude::*;
+use wasm_bindgen::JsCast;
+use web_sys::{console, window, HtmlElement, HtmlDivElement, KeyboardEvent};
-#[derive(Debug, PartialEq)]
-pub enum Direction {
- Up,
- Down,
- Left,
- Right,
+thread_local! {
+ static GAME: Rc> = Rc::new(RefCell::new(SnakeGame::new(20, 15)));
+ static TICK_CLOSURE: Closure = Closure::wrap(Box::new({
+ let game = GAME.with(|game| game.clone());
+ move || {
+ game.borrow_mut().tick();
+ render();
+ }
+ }) as Box);
+
+ static HANDLE_KEYDOWN: Closure = Closure::wrap(Box::new({
+ let game = GAME.with(|game| game.clone());
+ move |e: KeyboardEvent| {
+ let direction = match &e.key()[..] {
+ "ArrowUp" => Direction::Up,
+ "ArrowDown" => Direction::Down,
+ "ArrowRight" => Direction::Right,
+ "ArrowLeft" => Direction::Left,
+ _ => return
+ };
+
+ game.borrow_mut().change_direction(direction);
+ }
+ }))
}
-#[derive(Debug)]
-pub struct SnakeGame {
- width: usize,
- height: usize,
- snake: VecDeque,
- snake_direction: Direction,
- food: Position,
- game_over: bool
+#[wasm_bindgen(start)]
+pub fn main() {
+ console::log_1(&"Staring...".into());
+
+ TICK_CLOSURE.with(|tick_closure| {
+ window()
+ .unwrap_throw()
+ .set_interval_with_callback_and_timeout_and_arguments_0(
+ tick_closure.as_ref().dyn_ref::().unwrap_throw(),
+ 350
+ )
+ .unwrap_throw()
+ });
+
+ HANDLE_KEYDOWN.with(|handle_keydown| {
+ window()
+ .unwrap_throw()
+ .add_event_listener_with_callback(
+ "keydown",
+ handle_keydown.as_ref().dyn_ref::().unwrap_throw(),
+ )
+ .unwrap_throw();
+ });
+
+ render();
}
-fn rand_position(width: usize, height: usize) -> Position {
- let mut rng = thread_rng();
- (rng.gen_range(0..width), rng.gen_range(0..height))
-}
+pub fn render() {
+ GAME.with(|game| {
+ let game = game.borrow();
+ let document = window()
+ .unwrap_throw()
+ .document()
+ .unwrap_throw();
-fn get_opposite_direction(direction: &Direction) -> Direction {
- match direction {
- Direction::Up => Direction::Down,
- Direction::Down => Direction::Up,
- Direction::Right => Direction::Left,
- Direction::Left => Direction::Right,
- }
-}
+ let root = document
+ .get_element_by_id("root")
+ .unwrap_throw()
+ .dyn_into::()
+ .unwrap_throw();
-impl SnakeGame {
- pub fn new(width: usize, height: usize) -> SnakeGame {
- Self {
- width,
- height,
- snake: VecDeque::from([(width / 2, height / 2)]),
- snake_direction: Direction::Left,
- food: rand_position(width, height),
- game_over: false
- }
- }
+ root.set_inner_html("");
+ root.style()
+ .set_property("display", "inline-grid")
+ .unwrap_throw();
+ root.style()
+ .set_property("grid-template", &format!("repeat({}, auto) / repeat({}, auto)", game.height, game.width))
+ .unwrap_throw();
- pub fn change_direction(&mut self, direction: Direction) {
- // Because you can't start going in the opposite direction you are
- // currently going
- if get_opposite_direction(&self.snake_direction) == direction {
- return;
- }
+ for y in 0..game.height {
+ for x in 0..game.width {
+ let pos = (x, y);
+ let elem = document.create_element("div").unwrap_throw().dyn_into::().unwrap_throw();
+ elem.set_class_name("field");
- self.snake_direction = direction
- }
+ let text = if pos == game.food {
+ "🍎"
+ } else if Some(&pos) == game.snake.front() {
+ "❇️"
+ } else if game.snake.contains(&pos) {
+ "🟩"
+ } else {
+ " "
+ };
- fn is_valid_move(&self, (x, y): Position, dx: i32, dy: i32) -> bool {
- let new_x = x as i32 + dx;
- let new_y = y as i32 + dy;
- 0 <= new_x && new_x < self.width as i32 && 0 <= new_y && new_y < self.height as i32
- }
+ elem.set_inner_text(text);
- fn get_unoccupied_position(&self) -> Option {
- let mut rng = thread_rng();
- (0..self.height)
- .flat_map(|y| (0..self.width).map(move |x| (x, y)))
- .filter(|pos| !self.snake.contains(pos))
- .choose(&mut rng)
- }
-
- pub fn tick(&mut self) {
- if self.game_over {
- return;
- }
-
- let head = self.snake.front();
- if head.is_none() {
- return;
- }
- let head = *head.unwrap();
-
- let (dx, dy): (i32, i32) = match &self.snake_direction {
- Direction::Up => (-1, 0),
- Direction::Down => (1, 0),
- Direction::Left => (-1, 0),
- Direction::Right => (1, 0),
- };
-
- if !self.is_valid_move(head, dx, dy) {
- self.game_over = true;
- return;
- }
-
- let new_head = (
- (head.0 as i32 + dx) as usize,
- (head.1 as i32 + dy) as usize
- );
- self.snake.push_front(new_head);
-
- if new_head == self.food {
- let new_food = self.get_unoccupied_position();
- if let Some(new_food) = new_food {
- self.food = new_food;
- } else {
- self.game_over = true;
+ root.append_child(&elem).unwrap_throw();
}
- } else {
- self.snake.pop_back();
}
- }
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
- #[test]
- fn it_works() {
- let game = SnakeGame::new(10, 10);
-
- println!("{:?}", game);
- }
+ })
}
diff --git a/src/snake.rs b/src/snake.rs
new file mode 100644
index 0000000..6aeb873
--- /dev/null
+++ b/src/snake.rs
@@ -0,0 +1,134 @@
+use std::collections::VecDeque;
+
+use rand::prelude::*;
+
+pub type Position = (usize, usize);
+
+#[derive(Debug, PartialEq)]
+pub enum Direction {
+ Up,
+ Down,
+ Left,
+ Right,
+}
+
+#[derive(Debug)]
+pub struct SnakeGame {
+ pub width: usize,
+ pub height: usize,
+ pub snake: VecDeque,
+ pub snake_direction: Direction,
+ pub food: Position,
+ pub game_over: bool,
+}
+
+fn rand_position(width: usize, height: usize) -> Position {
+ let mut rng = thread_rng();
+ (rng.gen_range(0..width), rng.gen_range(0..height))
+}
+
+fn get_opposite_direction(direction: &Direction) -> Direction {
+ match direction {
+ Direction::Up => Direction::Down,
+ Direction::Down => Direction::Up,
+ Direction::Right => Direction::Left,
+ Direction::Left => Direction::Right,
+ }
+}
+
+impl SnakeGame {
+ pub fn new(width: usize, height: usize) -> SnakeGame {
+ Self {
+ width,
+ height,
+ snake: VecDeque::from([(width / 2, height / 2)]),
+ snake_direction: Direction::Left,
+ food: rand_position(width, height),
+ game_over: false,
+ }
+ }
+
+ pub fn change_direction(&mut self, direction: Direction) {
+ // Because you can't start going in the opposite direction you are
+ // currently going
+ if get_opposite_direction(&self.snake_direction) == direction {
+ return;
+ }
+
+ self.snake_direction = direction
+ }
+
+ fn is_valid_move(&self, (x, y): Position, dx: i32, dy: i32) -> bool {
+ let new_x = x as i32 + dx;
+ let new_y = y as i32 + dy;
+ 0 <= new_x
+ && new_x < self.width as i32
+ && 0 <= new_y
+ && new_y < self.height as i32
+ }
+
+ fn get_unoccupied_position(&self) -> Option {
+ let mut rng = thread_rng();
+ (0..self.height)
+ .flat_map(|y| (0..self.width).map(move |x| (x, y)))
+ .filter(|pos| !self.snake.contains(pos))
+ .choose(&mut rng)
+ }
+
+ pub fn tick(&mut self) {
+ if self.game_over {
+ return;
+ }
+
+ let head = self.snake.front();
+ if head.is_none() {
+ return;
+ }
+ let head = *head.unwrap();
+
+ let (dx, dy): (i32, i32) = match &self.snake_direction {
+ Direction::Up => (0, -1),
+ Direction::Down => (0, 1),
+ Direction::Left => (-1, 0),
+ Direction::Right => (1, 0),
+ };
+
+ if !self.is_valid_move(head, dx, dy) {
+ self.game_over = true;
+ return;
+ }
+
+ let new_head =
+ ((head.0 as i32 + dx) as usize, (head.1 as i32 + dy) as usize);
+ if self.snake.contains(&new_head) {
+ self.game_over = true;
+ return;
+ }
+
+ self.snake.push_front(new_head);
+ if new_head == self.food {
+ let new_food = self.get_unoccupied_position();
+ if let Some(new_food) = new_food {
+ self.food = new_food;
+ } else {
+ self.game_over = true;
+ }
+ } else {
+ self.snake.pop_back();
+ }
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ #[test]
+ fn it_works() {
+ let mut game = SnakeGame::new(10, 10);
+
+ println!("{:?}", game);
+ game.change_direction(Direction::Up);
+ game.tick();
+ println!("{:?}", game);
+ }
+}