├── .gitignore ├── Cargo.toml ├── README.md └── src ├── main.rs ├── draw.rs ├── snake.rs └── game.rs /.gitignore: -------------------------------------------------------------------------------- 1 | target -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "snake" 3 | version = "0.1.0" 4 | authors = ["tensor-programming "] 5 | 6 | [dependencies] 7 | rand = "0.8" 8 | piston_window = "0.117.0" -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # snake-tutorial 2 | 3 | # A snake game made with Rust 4 | 5 | ## Run `cargo run` to run the app, run `cargo build` to build an executable file. 6 | 7 | ### Check out the Youtube Tutorial for this [Rust Tutorial](https://youtu.be/DnT_7M7L7vo). Here is our [Youtube Channel](https://www.youtube.com/channel/UCYqCZOwHbnPwyjawKfE21wg) Subscribe for more content. 8 | 9 | ### Check out our blog at [tensor-programming.com](http://tensor-programming.com/). 10 | 11 | ### Our [Twitter](https://twitter.com/TensorProgram), our [facebook](https://www.facebook.com/Tensor-Programming-1197847143611799/) and our [Steemit](https://steemit.com/@tensor). 12 | 13 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | extern crate piston_window; 2 | extern crate rand; 3 | 4 | mod draw; 5 | mod snake; 6 | mod game; 7 | 8 | use piston_window::*; 9 | use piston_window::types::Color; 10 | 11 | use crate::game::Game; 12 | use crate::draw::to_coord_u32; 13 | 14 | const BACK_COLOR: Color = [0.5, 0.5, 0.5, 1.0]; 15 | 16 | 17 | 18 | fn main() { 19 | let (width, height) = (30, 30); 20 | 21 | let mut window: PistonWindow = 22 | WindowSettings::new("Snake", [to_coord_u32(width), to_coord_u32(height)]) 23 | .exit_on_esc(true) 24 | .build() 25 | .unwrap(); 26 | 27 | let mut game = Game::new(width, height); 28 | while let Some(event) = window.next() { 29 | if let Some(Button::Keyboard(key)) = event.press_args() { 30 | game.key_pressed(key); 31 | } 32 | window.draw_2d(&event, |c, g, _| { 33 | clear(BACK_COLOR, g); 34 | game.draw(&c, g); 35 | }); 36 | 37 | event.update(|arg| { 38 | game.update(arg.dt); 39 | }); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/draw.rs: -------------------------------------------------------------------------------- 1 | use piston_window::{rectangle, Context, G2d}; 2 | use piston_window::types::Color; 3 | 4 | const BLOCK_SIZE: f64 = 25.0; 5 | 6 | pub fn to_coord(game_coord: i32) -> f64 { 7 | (game_coord as f64) * BLOCK_SIZE 8 | } 9 | 10 | pub fn to_coord_u32(game_coord: i32) -> u32 { 11 | to_coord(game_coord) as u32 12 | } 13 | 14 | pub fn draw_block(color: Color, x: i32, y: i32, con: &Context, g: &mut G2d) { 15 | let gui_x = to_coord(x); 16 | let gui_y = to_coord(y); 17 | 18 | rectangle( 19 | color, 20 | [gui_x, gui_y, BLOCK_SIZE, BLOCK_SIZE], 21 | con.transform, 22 | g, 23 | ); 24 | } 25 | 26 | pub fn draw_rectangle( 27 | color: Color, 28 | x: i32, 29 | y: i32, 30 | width: i32, 31 | height: i32, 32 | con: &Context, 33 | g: &mut G2d, 34 | ) { 35 | let x = to_coord(x); 36 | let y = to_coord(y); 37 | 38 | rectangle( 39 | color, 40 | [ 41 | x, 42 | y, 43 | BLOCK_SIZE * (width as f64), 44 | BLOCK_SIZE * (height as f64), 45 | ], 46 | con.transform, 47 | g, 48 | ); 49 | } 50 | -------------------------------------------------------------------------------- /src/snake.rs: -------------------------------------------------------------------------------- 1 | use std::collections::LinkedList; 2 | use piston_window::{Context, G2d}; 3 | use piston_window::types::Color; 4 | 5 | use crate::draw::draw_block; 6 | 7 | const SNAKE_COLOR: Color = [0.00, 0.80, 0.00, 1.0]; 8 | #[derive(Copy, Clone, PartialEq)] 9 | pub enum Direction { 10 | Up, 11 | Down, 12 | Left, 13 | Right, 14 | } 15 | 16 | impl Direction { 17 | pub fn opposite(&self) -> Direction { 18 | match *self { 19 | Direction::Up => Direction::Down, 20 | Direction::Down => Direction::Up, 21 | Direction::Left => Direction::Right, 22 | Direction::Right => Direction::Left, 23 | } 24 | } 25 | } 26 | #[derive(Debug, Clone)] 27 | struct Block { 28 | x: i32, 29 | y: i32, 30 | } 31 | 32 | pub struct Snake { 33 | direction: Direction, 34 | body: LinkedList, 35 | tail: Option, 36 | } 37 | 38 | impl Snake { 39 | pub fn new(x: i32, y: i32) -> Snake { 40 | let mut body: LinkedList = LinkedList::new(); 41 | body.push_back(Block { 42 | x: x + 2, 43 | y, 44 | }); 45 | body.push_back(Block { 46 | x: x + 1, 47 | y, 48 | }); 49 | body.push_back(Block { 50 | x, 51 | y, 52 | }); 53 | 54 | Snake { 55 | direction: Direction::Right, 56 | body, 57 | tail: None, 58 | } 59 | } 60 | 61 | pub fn draw(&self, con: &Context, g: &mut G2d) { 62 | for block in &self.body { 63 | draw_block(SNAKE_COLOR, block.x, block.y, con, g); 64 | } 65 | } 66 | 67 | pub fn head_position(&self) -> (i32, i32) { 68 | let head_block = self.body.front().unwrap(); 69 | (head_block.x, head_block.y) 70 | } 71 | 72 | pub fn move_forward(&mut self, dir: Option) { 73 | match dir { 74 | Some(d) => self.direction = d, 75 | None => (), 76 | } 77 | 78 | let (last_x, last_y): (i32, i32) = self.head_position(); 79 | 80 | let new_block = match self.direction { 81 | Direction::Up => Block { 82 | x: last_x, 83 | y: last_y - 1, 84 | }, 85 | Direction::Down => Block { 86 | x: last_x, 87 | y: last_y + 1, 88 | }, 89 | Direction::Left => Block { 90 | x: last_x - 1, 91 | y: last_y, 92 | }, 93 | Direction::Right => Block { 94 | x: last_x + 1, 95 | y: last_y, 96 | }, 97 | }; 98 | self.body.push_front(new_block); 99 | let removed_block = self.body.pop_back().unwrap(); 100 | self.tail = Some(removed_block); 101 | } 102 | 103 | pub fn head_direction(&self) -> Direction { 104 | self.direction 105 | } 106 | 107 | pub fn next_head(&self, dir: Option) -> (i32, i32) { 108 | let (head_x, head_y): (i32, i32) = self.head_position(); 109 | 110 | let mut moving_dir = self.direction; 111 | match dir { 112 | Some(d) => moving_dir = d, 113 | None => {} 114 | } 115 | 116 | match moving_dir { 117 | Direction::Up => (head_x, head_y - 1), 118 | Direction::Down => (head_x, head_y + 1), 119 | Direction::Left => (head_x - 1, head_y), 120 | Direction::Right => (head_x + 1, head_y), 121 | } 122 | } 123 | 124 | pub fn restore_tail(&mut self) { 125 | let blk = self.tail.clone().unwrap(); 126 | self.body.push_back(blk); 127 | } 128 | 129 | pub fn overlap_tail(&self, x: i32, y: i32) -> bool { 130 | let mut ch = 0; 131 | for block in &self.body { 132 | if x == block.x && y == block.y { 133 | return true; 134 | } 135 | 136 | ch += 1; 137 | if ch == self.body.len() - 1 { 138 | break; 139 | } 140 | } 141 | return false; 142 | } 143 | } -------------------------------------------------------------------------------- /src/game.rs: -------------------------------------------------------------------------------- 1 | use piston_window::types::Color; 2 | use piston_window::*; 3 | 4 | use rand::{thread_rng, Rng}; 5 | 6 | use crate::draw::{draw_block, draw_rectangle}; 7 | use crate::snake::{Direction, Snake}; 8 | 9 | const FOOD_COLOR: Color = [0.80, 0.00, 0.00, 1.0]; 10 | const BORDER_COLOR: Color = [0.00, 0.00, 0.00, 1.0]; 11 | const GAMEOVER_COLOR: Color = [0.90, 0.00, 0.00, 0.5]; 12 | 13 | const MOVING_PERIOD: f64 = 0.1; 14 | const RESTART_TIME: f64 = 1.0; 15 | 16 | pub struct Game { 17 | snake: Snake, 18 | 19 | food_exists: bool, 20 | food_x: i32, 21 | food_y: i32, 22 | 23 | width: i32, 24 | height: i32, 25 | 26 | game_over: bool, 27 | waiting_time: f64, 28 | } 29 | 30 | impl Game { 31 | pub fn new(width: i32, height: i32) -> Game { 32 | Game { 33 | snake: Snake::new(2, 2), 34 | waiting_time: 0.0, 35 | food_exists: true, 36 | food_x: 6, 37 | food_y: 4, 38 | width, 39 | height, 40 | game_over: false, 41 | } 42 | } 43 | 44 | pub fn key_pressed(&mut self, key: Key) { 45 | if self.game_over { 46 | return; 47 | } 48 | 49 | let dir = match key { 50 | Key::Up => Some(Direction::Up), 51 | Key::Down => Some(Direction::Down), 52 | Key::Left => Some(Direction::Left), 53 | Key::Right => Some(Direction::Right), 54 | _ => Some(self.snake.head_direction()), 55 | }; 56 | 57 | if let Some(dir) = dir { 58 | if dir == self.snake.head_direction().opposite() { 59 | return; 60 | } 61 | } 62 | 63 | self.update_snake(dir); 64 | } 65 | 66 | pub fn draw(&self, con: &Context, g: &mut G2d) { 67 | self.snake.draw(con, g); 68 | 69 | if self.food_exists { 70 | draw_block(FOOD_COLOR, self.food_x, self.food_y, con, g); 71 | } 72 | 73 | draw_rectangle(BORDER_COLOR, 0, 0, self.width, 1, con, g); 74 | draw_rectangle(BORDER_COLOR, 0, self.height - 1, self.width, 1, con, g); 75 | draw_rectangle(BORDER_COLOR, 0, 0, 1, self.height, con, g); 76 | draw_rectangle(BORDER_COLOR, self.width - 1, 0, 1, self.height, con, g); 77 | 78 | if self.game_over { 79 | draw_rectangle(GAMEOVER_COLOR, 0, 0, self.width, self.height, con, g); 80 | } 81 | } 82 | 83 | pub fn update(&mut self, delta_time: f64) { 84 | self.waiting_time += delta_time; 85 | 86 | if self.game_over { 87 | if self.waiting_time > RESTART_TIME { 88 | self.restart(); 89 | } 90 | return; 91 | } 92 | 93 | if !self.food_exists { 94 | self.add_food(); 95 | } 96 | 97 | if self.waiting_time > MOVING_PERIOD { 98 | self.update_snake(None); 99 | } 100 | } 101 | 102 | fn check_eating(&mut self) { 103 | let (head_x, head_y): (i32, i32) = self.snake.head_position(); 104 | if self.food_exists && self.food_x == head_x && self.food_y == head_y { 105 | self.food_exists = false; 106 | self.snake.restore_tail(); 107 | } 108 | } 109 | 110 | fn check_if_snake_alive(&self, dir: Option) -> bool { 111 | let (next_x, next_y) = self.snake.next_head(dir); 112 | 113 | if self.snake.overlap_tail(next_x, next_y) { 114 | return false; 115 | } 116 | 117 | next_x > 0 && next_y > 0 && next_x < self.width - 1 && next_y < self.height - 1 118 | } 119 | 120 | fn add_food(&mut self) { 121 | let mut rng = thread_rng(); 122 | 123 | let mut new_x = rng.gen_range(1..self.width - 1); 124 | let mut new_y = rng.gen_range(1..self.height - 1); 125 | while self.snake.overlap_tail(new_x, new_y) { 126 | new_x = rng.gen_range(1..self.width - 1); 127 | new_y = rng.gen_range(1..self.height - 1); 128 | } 129 | 130 | self.food_x = new_x; 131 | self.food_y = new_y; 132 | self.food_exists = true; 133 | } 134 | 135 | fn update_snake(&mut self, dir: Option) { 136 | if self.check_if_snake_alive(dir) { 137 | self.snake.move_forward(dir); 138 | self.check_eating(); 139 | } else { 140 | self.game_over = true; 141 | } 142 | self.waiting_time = 0.0; 143 | } 144 | 145 | fn restart(&mut self) { 146 | self.snake = Snake::new(2, 2); 147 | self.waiting_time = 0.0; 148 | self.food_exists = true; 149 | self.food_x = 6; 150 | self.food_y = 4; 151 | self.game_over = false; 152 | } 153 | } 154 | --------------------------------------------------------------------------------