mirror of
https://github.com/yishn/lets-code.git
synced 2026-04-16 03:00:29 -04:00
Binary file not shown.
|
Before Width: | Height: | Size: 255 KiB After Width: | Height: | Size: 192 KiB |
2
snake/.rustfmt.toml
Normal file
2
snake/.rustfmt.toml
Normal file
@@ -0,0 +1,2 @@
|
||||
tab_spaces = 2
|
||||
max_width = 80
|
||||
20
snake/Cargo.toml
Normal file
20
snake/Cargo.toml
Normal 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
26
snake/README.md
Normal 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
39
snake/index.html
Normal 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
114
snake/src/lib.rs
Normal 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
11
snake/src/random.rs
Normal 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
111
snake/src/snake.rs
Normal 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
BIN
snake/thumbnail.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 113 KiB |
Reference in New Issue
Block a user