feat: use wasm to create the frontend
This commit is contained in:
parent
5b66ea248e
commit
3872637b42
13
Cargo.toml
13
Cargo.toml
@ -3,5 +3,18 @@ name = "rust-wasm-snake"
|
|||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
crate-type = ["cdylib", "rlib"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
rand = "0.8.5"
|
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
38
index.html
Normal 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>
|
205
src/lib.rs
205
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)]
|
thread_local! {
|
||||||
pub enum Direction {
|
static GAME: Rc<RefCell<SnakeGame>> = Rc::new(RefCell::new(SnakeGame::new(20, 15)));
|
||||||
Up,
|
static TICK_CLOSURE: Closure<dyn FnMut()> = Closure::wrap(Box::new({
|
||||||
Down,
|
let game = GAME.with(|game| game.clone());
|
||||||
Left,
|
move || {
|
||||||
Right,
|
game.borrow_mut().tick();
|
||||||
|
render();
|
||||||
}
|
}
|
||||||
|
}) as Box<dyn FnMut()>);
|
||||||
|
|
||||||
#[derive(Debug)]
|
static HANDLE_KEYDOWN: Closure<dyn FnMut(KeyboardEvent)> = Closure::wrap(Box::new({
|
||||||
pub struct SnakeGame {
|
let game = GAME.with(|game| game.clone());
|
||||||
width: usize,
|
move |e: KeyboardEvent| {
|
||||||
height: usize,
|
let direction = match &e.key()[..] {
|
||||||
snake: VecDeque<Position>,
|
"ArrowUp" => Direction::Up,
|
||||||
snake_direction: Direction,
|
"ArrowDown" => Direction::Down,
|
||||||
food: Position,
|
"ArrowRight" => Direction::Right,
|
||||||
game_over: bool
|
"ArrowLeft" => Direction::Left,
|
||||||
}
|
_ => return
|
||||||
|
|
||||||
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 => (-1, 0),
|
|
||||||
Direction::Down => (1, 0),
|
|
||||||
Direction::Left => (-1, 0),
|
|
||||||
Direction::Right => (1, 0),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if !self.is_valid_move(head, dx, dy) {
|
game.borrow_mut().change_direction(direction);
|
||||||
self.game_over = true;
|
}
|
||||||
return;
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
let new_head = (
|
#[wasm_bindgen(start)]
|
||||||
(head.0 as i32 + dx) as usize,
|
pub fn main() {
|
||||||
(head.1 as i32 + dy) as usize
|
console::log_1(&"Staring...".into());
|
||||||
);
|
|
||||||
self.snake.push_front(new_head);
|
|
||||||
|
|
||||||
if new_head == self.food {
|
TICK_CLOSURE.with(|tick_closure| {
|
||||||
let new_food = self.get_unoccupied_position();
|
window()
|
||||||
if let Some(new_food) = new_food {
|
.unwrap_throw()
|
||||||
self.food = new_food;
|
.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();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn render() {
|
||||||
|
GAME.with(|game| {
|
||||||
|
let game = game.borrow();
|
||||||
|
let document = window()
|
||||||
|
.unwrap_throw()
|
||||||
|
.document()
|
||||||
|
.unwrap_throw();
|
||||||
|
|
||||||
|
let root = document
|
||||||
|
.get_element_by_id("root")
|
||||||
|
.unwrap_throw()
|
||||||
|
.dyn_into::<HtmlElement>()
|
||||||
|
.unwrap_throw();
|
||||||
|
|
||||||
|
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();
|
||||||
|
|
||||||
|
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");
|
||||||
|
|
||||||
|
let text = if pos == game.food {
|
||||||
|
"🍎"
|
||||||
|
} else if Some(&pos) == game.snake.front() {
|
||||||
|
"❇️"
|
||||||
|
} else if game.snake.contains(&pos) {
|
||||||
|
"🟩"
|
||||||
} else {
|
} else {
|
||||||
self.game_over = true;
|
" "
|
||||||
}
|
};
|
||||||
} else {
|
|
||||||
self.snake.pop_back();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
elem.set_inner_text(text);
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
#[test]
|
|
||||||
fn it_works() {
|
|
||||||
let game = SnakeGame::new(10, 10);
|
|
||||||
|
|
||||||
println!("{:?}", game);
|
root.append_child(&elem).unwrap_throw();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
134
src/snake.rs
Normal file
134
src/snake.rs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user