1
0

feat: use wasm to create the frontend

This commit is contained in:
Rokas Puzonas 2022-06-03 01:14:32 +03:00
parent 5b66ea248e
commit 3872637b42
4 changed files with 278 additions and 110 deletions

View File

@ -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"
]

38
index.html Normal file
View File

@ -0,0 +1,38 @@
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Snake</title>
<style>
html {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}
#root {
border: solid;
}
.field {
text-decoration: none;
text-align: center;
width: 1.2rem;
height: 1.2rem;
line-height: 1.2rem;
}
</style>
</head>
<body>
<div id="root"></div>
<script type="module">
import init from "./pkg/rust_wasm_snake.js"
async function main() {
await init()
}
main()
</script>
</body>
</html>

View File

@ -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<RefCell<SnakeGame>> = Rc::new(RefCell::new(SnakeGame::new(20, 15)));
static TICK_CLOSURE: Closure<dyn FnMut()> = Closure::wrap(Box::new({
let game = GAME.with(|game| game.clone());
move || {
game.borrow_mut().tick();
render();
}
}) as Box<dyn FnMut()>);
static HANDLE_KEYDOWN: Closure<dyn FnMut(KeyboardEvent)> = 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<Position>,
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::<Function>().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::<Function>().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::<HtmlElement>()
.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::<HtmlDivElement>().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<Position> {
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);
}
})
}

134
src/snake.rs Normal file
View File

@ -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<Position>,
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<Position> {
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);
}
}