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); + } +}