├── www ├── .gitignore ├── index.js ├── src │ ├── config.js │ ├── storage.js │ ├── controller.js │ ├── game-manager.js │ └── view.js ├── bootstrap.js ├── webpack.config.js ├── .bin │ └── create-wasm-app.js ├── package.json ├── index.html └── README.md ├── .gitignore ├── README.md ├── src ├── utils.rs └── lib.rs └── Cargo.toml /www/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | **/*.rs.bk 3 | Cargo.lock 4 | bin/ 5 | pkg/ 6 | wasm-pack.log 7 | -------------------------------------------------------------------------------- /www/index.js: -------------------------------------------------------------------------------- 1 | 2 | import { GameManager } from './src/game-manager' 3 | 4 | const gameManager = new GameManager() 5 | gameManager.run() -------------------------------------------------------------------------------- /www/src/config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | WIDTH: 17, 3 | HEIGHT: 15, 4 | SPEED: 0.006, 5 | SNAKE_LENGTH: 3, 6 | SNAKE_DIRECTION_X: 1, 7 | SNAKE_DIRECTION_Y: 0, 8 | FPS: 60 9 | } -------------------------------------------------------------------------------- /www/src/storage.js: -------------------------------------------------------------------------------- 1 | export default { 2 | getBestScore: () => parseInt(localStorage.bestScore) || 0, 3 | setBestScore: (bestScore) => localStorage.setItem('bestScore', bestScore) 4 | } -------------------------------------------------------------------------------- /www/bootstrap.js: -------------------------------------------------------------------------------- 1 | // A dependency graph that contains any wasm must all be imported 2 | // asynchronously. This `bootstrap.js` file does the single async import, so 3 | // that no one else needs to worry about it again. 4 | import("./index.js") 5 | .catch(e => console.error("Error importing `index.js`:", e)); 6 | -------------------------------------------------------------------------------- /www/webpack.config.js: -------------------------------------------------------------------------------- 1 | const CopyWebpackPlugin = require("copy-webpack-plugin"); 2 | const path = require('path'); 3 | 4 | module.exports = { 5 | entry: "./bootstrap.js", 6 | output: { 7 | path: path.resolve(__dirname, "dist"), 8 | filename: "bootstrap.js", 9 | }, 10 | mode: "development", 11 | plugins: [ 12 | new CopyWebpackPlugin(['index.html']) 13 | ], 14 | }; 15 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Rust JS Snake Game 2 | We will learn how to export API implemented with Rust to JavaScript app. 3 | > 4 | 5 | ![all text](https://cdn-images-1.medium.com/max/800/1*M8sa3fAx7pOGV5NeX3AVSQ.gif) 6 | 7 | ## [Play](https://radzionc.github.io/rust-js-snake-game/) 8 | 9 | ## [Blog Post](https://geekrodion.com/blog/rustsnake) 10 | 11 | ## Technologies 12 | * Rust 13 | * JS 14 | * WebAssembly 15 | 16 | ## License 17 | 18 | MIT © [RodionChachura](https://geekrodion.com) 19 | -------------------------------------------------------------------------------- /src/utils.rs: -------------------------------------------------------------------------------- 1 | pub fn set_panic_hook() { 2 | // When the `console_error_panic_hook` feature is enabled, we can call the 3 | // `set_panic_hook` function at least once during initialization, and then 4 | // we will get better error messages if our code ever panics. 5 | // 6 | // For more details see 7 | // https://github.com/rustwasm/console_error_panic_hook#readme 8 | #[cfg(feature = "console_error_panic_hook")] 9 | console_error_panic_hook::set_once(); 10 | } 11 | -------------------------------------------------------------------------------- /www/.bin/create-wasm-app.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const { spawn } = require("child_process"); 4 | const fs = require("fs"); 5 | 6 | let folderName = '.'; 7 | 8 | if (process.argv.length >= 3) { 9 | folderName = process.argv[2]; 10 | if (!fs.existsSync(folderName)) { 11 | fs.mkdirSync(folderName); 12 | } 13 | } 14 | 15 | const clone = spawn("git", ["clone", "https://github.com/rustwasm/create-wasm-app.git", folderName]); 16 | 17 | clone.on("close", code => { 18 | if (code !== 0) { 19 | console.error("cloning the template failed!") 20 | process.exit(code); 21 | } else { 22 | console.log("🦀 Rust + 🕸 Wasm = ❤"); 23 | } 24 | }); 25 | -------------------------------------------------------------------------------- /www/src/controller.js: -------------------------------------------------------------------------------- 1 | import { Movement } from "wasm-snake-game"; 2 | 3 | const MOVEMENT_KEYS = { 4 | [Movement.TOP]: [87, 38], 5 | [Movement.RIGHT]: [68, 39], 6 | [Movement.DOWN]: [83, 40], 7 | [Movement.LEFT]: [65, 37] 8 | } 9 | 10 | const STOP_KEY = 32 11 | 12 | export class Controller { 13 | constructor(onStop = () => {}) { 14 | window.addEventListener('keydown', ({ which }) => { 15 | this.movement = Object.keys(MOVEMENT_KEYS).find(key => MOVEMENT_KEYS[key].includes(which)) 16 | }) 17 | window.addEventListener('keyup', ({ which }) => { 18 | this.movement = undefined 19 | if (which === STOP_KEY) { 20 | onStop() 21 | } 22 | }) 23 | } 24 | } -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "rust-js-snake-game" 3 | version = "0.1.0" 4 | authors = ["Rodion Chachura "] 5 | edition = "2018" 6 | 7 | [lib] 8 | crate-type = ["cdylib", "rlib"] 9 | 10 | [features] 11 | default = ["console_error_panic_hook"] 12 | 13 | [dependencies] 14 | wasm-bindgen = "0.2" 15 | js-sys = "0.3.32" 16 | rand = { version = "0.7.2", features = ["wasm-bindgen"] } 17 | 18 | # The `console_error_panic_hook` crate provides better debugging of panics by 19 | # logging them with `console.error`. This is great for development, but requires 20 | # all the `std::fmt` and `std::panicking` infrastructure, so isn't great for 21 | # code size when deploying. 22 | console_error_panic_hook = { version = "0.1.1", optional = true } 23 | 24 | # `wee_alloc` is a tiny allocator for wasm that is only ~1K in code size 25 | # compared to the default allocator's ~10K. It is slower than the default 26 | # allocator, however. 27 | # 28 | # Unfortunately, `wee_alloc` requires nightly Rust when targeting wasm for now. 29 | wee_alloc = { version = "0.4.2", optional = true } 30 | 31 | [dev-dependencies] 32 | wasm-bindgen-test = "0.2" 33 | 34 | [profile.release] 35 | # Tell `rustc` to optimize for small code size. 36 | opt-level = "s" 37 | -------------------------------------------------------------------------------- /www/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "create-wasm-app", 3 | "version": "0.1.0", 4 | "description": "create an app to consume rust-generated wasm packages", 5 | "main": "index.js", 6 | "bin": { 7 | "create-wasm-app": ".bin/create-wasm-app.js" 8 | }, 9 | "scripts": { 10 | "build": "webpack --config webpack.config.js", 11 | "start": "webpack-dev-server", 12 | "deploy": "gh-pages -d dist" 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "git+https://github.com/RodionChachura/rust-js-snake-game.git" 17 | }, 18 | "keywords": [ 19 | "webassembly", 20 | "wasm", 21 | "rust", 22 | "webpack" 23 | ], 24 | "author": "Ashley Williams ", 25 | "license": "(MIT OR Apache-2.0)", 26 | "bugs": { 27 | "url": "https://github.com/RodionChachura/rust-js-snake-game/issues" 28 | }, 29 | "homepage": "https://github.com/RodionChachura/rust-js-snake-game#readme", 30 | "dependencies": { 31 | "wasm-snake-game": "file:../pkg" 32 | }, 33 | "devDependencies": { 34 | "copy-webpack-plugin": "^5.0.0", 35 | "gh-pages": "^2.2.0", 36 | "hello-wasm-pack": "^0.1.0", 37 | "webpack": "^4.29.3", 38 | "webpack-cli": "^3.1.0", 39 | "webpack-dev-server": "^3.1.5" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /www/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Snake Game 7 | 45 | 46 | 47 |
48 |

Now:

49 |

Best:

50 |
51 |
52 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /www/src/game-manager.js: -------------------------------------------------------------------------------- 1 | import { Game, Vector } from 'wasm-snake-game' 2 | 3 | import CONFIG from './config' 4 | import { View } from './view' 5 | import { Controller } from './controller' 6 | import Storage from './storage' 7 | 8 | export class GameManager { 9 | constructor() { 10 | this.restart() 11 | this.view = new View( 12 | this.game.width, 13 | this.game.height, 14 | this.render.bind(this) 15 | ) 16 | this.controller = new Controller( 17 | this.onStop.bind(this) 18 | ) 19 | } 20 | 21 | restart() { 22 | this.game = new Game( 23 | CONFIG.WIDTH, 24 | CONFIG.HEIGHT, 25 | CONFIG.SPEED, 26 | CONFIG.SNAKE_LENGTH, 27 | new Vector( 28 | CONFIG.SNAKE_DIRECTION_X, 29 | CONFIG.SNAKE_DIRECTION_Y 30 | ) 31 | ) 32 | this.lastUpdate = undefined 33 | this.stopTime = undefined 34 | } 35 | 36 | onStop() { 37 | const now = Date.now() 38 | if (this.stopTime) { 39 | this.stopTime = undefined 40 | this.lastUpdate = this.time + now - this.lastUpdate 41 | } else { 42 | this.stopTime = now 43 | } 44 | } 45 | 46 | render() { 47 | this.view.render( 48 | this.game.food, 49 | this.game.get_snake(), 50 | this.game.score, 51 | Storage.getBestScore() 52 | ) 53 | } 54 | 55 | tick() { 56 | if (!this.stopTime) { 57 | const lastUpdate = Date.now() 58 | if (this.lastUpdate) { 59 | this.game.process(lastUpdate - this.lastUpdate, this.controller.movement) 60 | if (this.game.is_over()) { 61 | this.restart() 62 | return 63 | } 64 | if (this.game.score > Storage.getBestScore()) { 65 | Storage.setBestScore(this.game.score) 66 | } 67 | } 68 | this.lastUpdate = lastUpdate 69 | this.render() 70 | } 71 | } 72 | 73 | run() { 74 | setInterval(this.tick.bind(this), 1000 / CONFIG.FPS) 75 | } 76 | } -------------------------------------------------------------------------------- /www/src/view.js: -------------------------------------------------------------------------------- 1 | const getRange = length => [...Array(length).keys()] 2 | 3 | export class View { 4 | constructor(gameWidth, gameHeight, onViewChange = () => {}) { 5 | this.gameWidth = gameWidth 6 | this.gameHeight = gameHeight 7 | this.container = document.getElementById('container') 8 | this.onViewChange = onViewChange 9 | this.setUp() 10 | 11 | window.addEventListener('resize', () => { 12 | const [child] = this.container.children 13 | if (child) { 14 | this.container.removeChild(child) 15 | } 16 | this.setUp() 17 | this.onViewChange() 18 | }) 19 | } 20 | 21 | setUp() { 22 | const { width, height } = this.container.getBoundingClientRect() 23 | this.unitOnScreen = Math.min( 24 | width / this.gameWidth, 25 | height / this.gameHeight 26 | ) 27 | this.projectDistance = distance => distance * this.unitOnScreen 28 | this.projectPosition = position => position.scale_by(this.unitOnScreen) 29 | 30 | const canvas = document.createElement('canvas') 31 | this.container.appendChild(canvas) 32 | this.context = canvas.getContext('2d') 33 | canvas.setAttribute('width', this.projectDistance(this.gameWidth)) 34 | canvas.setAttribute('height', this.projectDistance(this.gameHeight)) 35 | } 36 | 37 | render(food, snake, score, bestScore) { 38 | this.context.clearRect( 39 | 0, 40 | 0, 41 | this.context.canvas.width, 42 | this.context.canvas.height 43 | ) 44 | 45 | this.context.globalAlpha = 0.2 46 | this.context.fillStyle = 'black' 47 | getRange(this.gameWidth).forEach(column => 48 | getRange(this.gameHeight) 49 | .filter(row => (column + row) % 2 === 1) 50 | .forEach(row => 51 | this.context.fillRect( 52 | column * this.unitOnScreen, 53 | row * this.unitOnScreen, 54 | this.unitOnScreen, 55 | this.unitOnScreen 56 | ) 57 | ) 58 | ) 59 | this.context.globalAlpha = 1 60 | 61 | const projectedFood = this.projectPosition(food) 62 | this.context.beginPath() 63 | this.context.arc( 64 | projectedFood.x, 65 | projectedFood.y, 66 | this.unitOnScreen / 2.5, 67 | 0, 68 | 2 * Math.PI 69 | ) 70 | this.context.fillStyle = '#e74c3c' 71 | this.context.fill() 72 | 73 | this.context.lineWidth = this.unitOnScreen 74 | this.context.strokeStyle = '#3498db' 75 | this.context.beginPath() 76 | snake 77 | .map(this.projectPosition) 78 | .forEach(({ x, y }) => this.context.lineTo(x, y)) 79 | this.context.stroke() 80 | 81 | document.getElementById('current-score').innerText = score 82 | document.getElementById('best-score').innerText = bestScore 83 | } 84 | } -------------------------------------------------------------------------------- /www/README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 |

create-wasm-app

4 | 5 | An npm init template for kick starting a project that uses NPM packages containing Rust-generated WebAssembly and bundles them with Webpack. 6 | 7 |

8 | Build Status 9 |

10 | 11 |

12 | Usage 13 | | 14 | Chat 15 |

16 | 17 | Built with 🦀🕸 by The Rust and WebAssembly Working Group 18 |
19 | 20 | ## About 21 | 22 | This template is designed for depending on NPM packages that contain 23 | Rust-generated WebAssembly and using them to create a Website. 24 | 25 | * Want to create an NPM package with Rust and WebAssembly? [Check out 26 | `wasm-pack-template`.](https://github.com/rustwasm/wasm-pack-template) 27 | * Want to make a monorepo-style Website without publishing to NPM? Check out 28 | [`rust-webpack-template`](https://github.com/rustwasm/rust-webpack-template) 29 | and/or 30 | [`rust-parcel-template`](https://github.com/rustwasm/rust-parcel-template). 31 | 32 | ## 🚴 Usage 33 | 34 | ``` 35 | npm init wasm-app 36 | ``` 37 | 38 | ## 🔋 Batteries Included 39 | 40 | - `.gitignore`: ignores `node_modules` 41 | - `LICENSE-APACHE` and `LICENSE-MIT`: most Rust projects are licensed this way, so these are included for you 42 | - `README.md`: the file you are reading now! 43 | - `index.html`: a bare bones html document that includes the webpack bundle 44 | - `index.js`: example js file with a comment showing how to import and use a wasm pkg 45 | - `package.json` and `package-lock.json`: 46 | - pulls in devDependencies for using webpack: 47 | - [`webpack`](https://www.npmjs.com/package/webpack) 48 | - [`webpack-cli`](https://www.npmjs.com/package/webpack-cli) 49 | - [`webpack-dev-server`](https://www.npmjs.com/package/webpack-dev-server) 50 | - defines a `start` script to run `webpack-dev-server` 51 | - `webpack.config.js`: configuration file for bundling your js with webpack 52 | 53 | ## License 54 | 55 | Licensed under either of 56 | 57 | * Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0) 58 | * MIT license ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT) 59 | 60 | at your option. 61 | 62 | ### Contribution 63 | 64 | Unless you explicitly state otherwise, any contribution intentionally 65 | submitted for inclusion in the work by you, as defined in the Apache-2.0 66 | license, shall be dual licensed as above, without any additional terms or 67 | conditions. 68 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | mod utils; 2 | 3 | use wasm_bindgen::prelude::*; 4 | use rand::Rng; 5 | use js_sys::Array; 6 | // When the `wee_alloc` feature is enabled, use `wee_alloc` as the global 7 | // allocator. 8 | #[cfg(feature = "wee_alloc")] 9 | #[global_allocator] 10 | static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT; 11 | 12 | #[wasm_bindgen] 13 | extern { 14 | fn alert(s: &str); 15 | } 16 | 17 | #[wasm_bindgen] 18 | pub fn greet() { 19 | alert("Hello, rust-js-snake-game!"); 20 | } 21 | 22 | static EPSILON: f64 = 0.0000001; 23 | 24 | fn are_equal(one: f64, another: f64) -> bool { 25 | (one - another).abs() < EPSILON 26 | } 27 | 28 | #[wasm_bindgen] 29 | #[derive(Copy, Clone)] 30 | pub struct Vector { 31 | pub x: f64, 32 | pub y: f64, 33 | } 34 | 35 | #[wasm_bindgen] 36 | impl Vector { 37 | #[wasm_bindgen(constructor)] 38 | pub fn new(x: f64, y: f64) -> Vector { 39 | Vector { x, y } 40 | } 41 | 42 | pub fn add(&self, other: &Vector) -> Vector { 43 | Vector::new(self.x + other.x, self.y + other.y) 44 | } 45 | 46 | pub fn subtract(&self, other: &Vector) -> Vector { 47 | Vector::new(self.x - other.x, self.y - other.y) 48 | } 49 | 50 | pub fn scale_by(&self, number: f64) -> Vector { 51 | Vector::new(self.x * number, self.y * number) 52 | } 53 | 54 | pub fn length(&self) -> f64 { 55 | self.x.hypot(self.y) 56 | } 57 | 58 | pub fn normalize(&self) -> Vector { 59 | self.scale_by(1_f64 / self.length()) 60 | } 61 | 62 | pub fn equal_to(&self, other: &Vector) -> bool { 63 | are_equal(self.x, other.x) && are_equal(self.y, other.y) 64 | } 65 | 66 | pub fn is_opposite(&self, other: &Vector) -> bool { 67 | let sum = self.add(other); 68 | sum.equal_to(&Vector::new(0_f64, 0_f64)) 69 | } 70 | 71 | pub fn dot_product(&self, other: &Vector) -> f64 { 72 | self.x * other.x + self.y * other.y 73 | } 74 | } 75 | 76 | pub struct Segment<'a> { 77 | pub start: &'a Vector, 78 | pub end: &'a Vector, 79 | } 80 | 81 | impl<'a> Segment<'a> { 82 | pub fn new(start: &'a Vector, end: &'a Vector) -> Segment<'a> { 83 | Segment { start, end } 84 | } 85 | 86 | pub fn get_vector(&self) -> Vector { 87 | self.end.subtract(&self.start) 88 | } 89 | 90 | pub fn length(&self) -> f64 { 91 | self.get_vector().length() 92 | } 93 | 94 | pub fn is_point_inside(&self, point: &Vector) -> bool { 95 | let first = Segment::new(self.start, point); 96 | let second = Segment::new(point, self.end); 97 | are_equal(self.length(), first.length() + second.length()) 98 | } 99 | 100 | pub fn get_projected_point(&self, point: &Vector) -> Vector { 101 | let vector = self.get_vector(); 102 | let diff = point.subtract(&self.start); 103 | let u = diff.dot_product(&vector) / vector.dot_product(&vector); 104 | let scaled = vector.scale_by(u); 105 | self.start.add(&scaled) 106 | } 107 | } 108 | 109 | fn get_segments_from_vectors(vectors: &[Vector]) -> Vec { 110 | let pairs = vectors[..vectors.len() - 1].iter().zip(&vectors[1..]); 111 | pairs 112 | .map(|(s, e)| Segment::new(s, e)) 113 | .collect::>() 114 | } 115 | 116 | fn get_food(width: i32, height: i32, snake: &[Vector]) -> Vector { 117 | let segments = get_segments_from_vectors(snake); 118 | let mut free_positions: Vec = Vec::new(); 119 | for x in 0..width { 120 | for y in 0..height { 121 | let point = Vector::new(f64::from(x) + 0.5, f64::from(y) + 0.5); 122 | if segments.iter().all(|s| !s.is_point_inside(&point)) { 123 | free_positions.push(point) 124 | } 125 | } 126 | } 127 | let index = rand::thread_rng().gen_range(0, free_positions.len()); 128 | free_positions[index] 129 | } 130 | 131 | #[wasm_bindgen] 132 | pub enum Movement { 133 | TOP, 134 | RIGHT, 135 | DOWN, 136 | LEFT, 137 | } 138 | 139 | #[wasm_bindgen] 140 | pub struct Game { 141 | pub width: i32, 142 | pub height: i32, 143 | pub speed: f64, 144 | snake: Vec, 145 | pub direction: Vector, 146 | pub food: Vector, 147 | pub score: i32, 148 | } 149 | 150 | 151 | #[wasm_bindgen] 152 | impl Game { 153 | #[wasm_bindgen(constructor)] 154 | pub fn new(width: i32, height: i32, speed: f64, snake_length: i32, direction: Vector) -> Game { 155 | let head_x = (f64::from(width) / 2_f64).round() - 0.5; 156 | let head_y = (f64::from(height) / 2_f64).round() - 0.5; 157 | let head = Vector::new(head_x, head_y); 158 | let tailtip = head.subtract(&direction.scale_by(f64::from(snake_length))); 159 | let snake = vec![tailtip, head]; 160 | let food = get_food(width, height, &snake); 161 | 162 | Game { 163 | width: width, 164 | height: height, 165 | speed: speed, 166 | snake: snake, 167 | direction: direction, 168 | food: food, 169 | score: 0, 170 | } 171 | } 172 | 173 | pub fn is_over(&self) -> bool { 174 | let snake_len = self.snake.len(); 175 | let last = self.snake[snake_len - 1]; 176 | let Vector { x, y } = last; 177 | if x < 0_f64 || x > f64::from(self.width) || y < 0_f64 || y > f64::from(self.height) { 178 | return true; 179 | } 180 | if snake_len < 5 { 181 | return false; 182 | } 183 | 184 | let segments = get_segments_from_vectors(&self.snake[..snake_len - 3]); 185 | return segments.iter().any(|segment| { 186 | let projected = segment.get_projected_point(&last); 187 | segment.is_point_inside(&projected) && Segment::new(&last, &projected).length() < 0.5 188 | }); 189 | } 190 | 191 | fn process_movement(&mut self, timespan: f64, movement: Option) { 192 | let distance = self.speed * timespan; 193 | let mut tail: Vec = Vec::new(); 194 | let mut snake_distance = distance; 195 | while self.snake.len() > 1 { 196 | let point = self.snake.remove(0); 197 | let next = &self.snake[0]; 198 | let segment = Segment::new(&point, next); 199 | let length = segment.length(); 200 | if length >= snake_distance { 201 | let vector = segment.get_vector().normalize().scale_by(snake_distance); 202 | tail.push(point.add(&vector)); 203 | break; 204 | } else { 205 | snake_distance -= length; 206 | } 207 | } 208 | tail.append(&mut self.snake); 209 | self.snake = tail; 210 | let old_head = self.snake.pop().unwrap(); 211 | let new_head = old_head.add(&self.direction.scale_by(distance)); 212 | if movement.is_some() { 213 | let new_direction = match movement.unwrap() { 214 | Movement::TOP => Vector { 215 | x: 0_f64, 216 | y: -1_f64, 217 | }, 218 | Movement::RIGHT => Vector { x: 1_f64, y: 0_f64 }, 219 | Movement::DOWN => Vector { x: 0_f64, y: 1_f64 }, 220 | Movement::LEFT => Vector { 221 | x: -1_f64, 222 | y: 0_f64, 223 | }, 224 | }; 225 | if !self.direction.is_opposite(&new_direction) 226 | && !self.direction.equal_to(&new_direction) 227 | { 228 | let Vector { x: old_x, y: old_y } = old_head; 229 | let old_x_rounded = old_x.round(); 230 | let old_y_rounded = old_y.round(); 231 | let new_x_rounded = new_head.x.round(); 232 | let new_y_rounded = new_head.y.round(); 233 | 234 | let rounded_x_changed = !are_equal(old_x_rounded, new_x_rounded); 235 | let rounded_y_changed = !are_equal(old_y_rounded, new_y_rounded); 236 | if rounded_x_changed || rounded_y_changed { 237 | let (old, old_rounded, new_rounded) = if rounded_x_changed { 238 | (old_x, old_x_rounded, new_x_rounded) 239 | } else { 240 | (old_y, old_y_rounded, new_y_rounded) 241 | }; 242 | let breakpoint_component = old_rounded 243 | + (if new_rounded > old_rounded { 244 | 0.5_f64 245 | } else { 246 | -0.5_f64 247 | }); 248 | let breakpoint = if rounded_x_changed { 249 | Vector::new(breakpoint_component, old_y) 250 | } else { 251 | Vector::new(old_x, breakpoint_component) 252 | }; 253 | let vector = 254 | new_direction.scale_by(distance - (old - breakpoint_component).abs()); 255 | let head = breakpoint.add(&vector); 256 | 257 | self.snake.push(breakpoint); 258 | self.snake.push(head); 259 | self.direction = new_direction; 260 | return; 261 | } 262 | } 263 | } 264 | self.snake.push(new_head); 265 | } 266 | 267 | fn process_food(&mut self) { 268 | let snake_len = self.snake.len(); 269 | let head_segment = Segment::new(&self.snake[snake_len - 2], &self.snake[snake_len - 1]); 270 | 271 | if head_segment.is_point_inside(&self.food) { 272 | let tail_end = &self.snake[0]; 273 | let before_tail_end = &self.snake[1]; 274 | let tail_segment = Segment::new(before_tail_end, &tail_end); 275 | let new_tail_end = tail_end.add(&tail_segment.get_vector().normalize()); 276 | self.snake[0] = new_tail_end; 277 | self.food = get_food(self.width, self.height, &self.snake); 278 | self.score += 1; 279 | } 280 | } 281 | 282 | pub fn process(&mut self, timespan: f64, movement: Option) { 283 | self.process_movement(timespan, movement); 284 | self.process_food(); 285 | } 286 | 287 | pub fn get_snake(&self) -> Array { 288 | self.snake.clone().into_iter().map(JsValue::from).collect() 289 | } 290 | } --------------------------------------------------------------------------------