Snake game features in Rust

Shanit PaulShanit Paul
5 min read

GitHub Link

Feature 1: Wall-through travel -

Initially, if the snake touched the walls around the game screen it would die and the game would reset.

I removed some constraints to enable the snake to travel through the wall and come out the opposite side first.

So, I updated the move_forward() function in the implementation of the snake trait.

For the Up Direction:

  • When the snake is moving up, it decreases its y coordinate (last_y - 1).

  • To handle wrapping, it adds height to ensure the value doesn't become negative, and then it takes a modulo height to wrap around if the new y coordinate goes above the top boundary.

  • This effectively moves the snake to the bottom of the screen when it tries to go beyond the top boundary.

    pub fn move_forward(&mut self, dir: Option<Direction>, width: i32, height: i32) {
       // rest of the code........
        // changed code =====>
        let new_block= match self.direction {
            Direction::Up => Block { x: last_x, y: (last_y - 1 + height) % height },
            Direction::Down => Block { x: last_x, y: (last_y + 1) % height },
            Direction::Left => Block { x: (last_x - 1 + width) % width, y: last_y },
            Direction::Right => Block { x: (last_x + 1) % width, y: last_y },
        };
       // changed code =====>
       // rest of the code
    }

Similarly, for other directions, the same logic is applied to accommodate the snake to travel through walls.


Feature 2: In-game Obstacles-

For this, I chose to draw cactuses to match the Desert aesthetic of the game. I already had a draw_rectangle() function to create the borders with

// Dark green color of cactuses
const OBSTACLE_COLOR: Color = [0.0, 0.3, 0.1, 1.0]; 
pub struct Obstacle { // co-ordinates
    x: i32,
    y: i32,
}
pub struct Game {
    snake: Snake,
    obstacles: Vec<Obstacle>, // a vector of Obstacle co-ordinates
    // rest of the properties of game...
}

Now I have included the add_obstacles() function to the Game struct's implementation block, which takes a mutable self-reference from the Game. This function :

  • Clears previous obstacles

  • Loops for 4 obstacles

  • Takes random x and y coordinates between the window space

  • If the coordinates overlap with the Snake or any food then take another coordinates.

  • Now push the coordinates into the obstacles vector.

This function is used while creating any new game in the make_new() function.

impl Game {
   pub fn make_new(width: i32, height: i32) -> Game {
        let mut game = Game {snake: Snake::make_new(2, 2), obstacles: Vec::new(), waiting_time: 0.0, food_exists: true, 
            food_x: 10, food_y: 12, width, height, game_over: false,
        };
        game.add_obstacles(); // obstacles added
        game
    }
// ===========>
    // rest of the functions.......
// ===========>
    pub fn add_obstacles(&mut self) {
        self.obstacles.clear(); // clearing any previous obstacles
        let mut rng = thread_rng();
        for _ in 0..4 { // 4 obstacles
            // choose random coordinates within the window
            let mut x = rng.gen_range(2..self.width - 2);
            let mut y = rng.gen_range(2..self.height - 2);
            // checking if the obstacle overlaps with any of the food items or the snake itself
            while self.snake.overlap_tail(x, y) || self.check_overlap_with_food(x, y) {
            // choosing random coordinates again until it doesn't match with food or snake
                x = rng.gen_range(2..self.width - 2);
                y = rng.gen_range(2..self.height - 2);
            }
            self.obstacles.push(Obstacle { x, y });
        }
    }
}

So, I also add the overlap_tail() function in the Snake struct implementation block which takes the snake's self-reference and the obstacles' coordinates as parameters.

impl Snake {
// ===========>
    // rest of the functions.......
// ===========>
    // function to match the coordinate of obstacle with the snake's
    pub fn overlap_tail(&self, x: i32,y: i32)-> bool {
        for block in &self.body {
            if x== block.x && y == block.y{
                return true;
            }
        }
        return false;
    }
}

and the check_overlap_with_food() function in the implementation block of the Game struct which takes a self-reference of the Game and the coordinates of the obstacles as parameters.

impl Game {
// ===========>
    // rest of the functions.......
// ===========>
    // function to match the coordinate of obstacle with the food's
    fn check_overlap_with_food(&self, x: i32, y: i32) -> bool {
        // creating a vector with food item's coordinates 
        let food_positions = vec![
            (self.food_x, self.food_y),  
            (self.food_x - 1, self.food_y - 1),  
            (self.food_x + 1, self.food_y + 1)
        ];
        // creating a vector with obstacle's coordinates 
        let obstacle_positions = vec![
            (x, y),            
            (x, y + 1), (x, y - 1),      
            (x, y + 2), (x, y - 2),
            (x - 1, y - 1), (x + 1, y + 1)     
        ];
        // matching them
        for (ox, oy) in obstacle_positions {
            if food_positions.contains(&(ox, oy)) {
                return true;
            }
        }
        false // default return
    }
}

In the draw() function of the game struct implementation a loop is run through the obstacles vector to draw the cactuses.

impl Game {
// ===========>
    // rest of the functions.......
// ===========>
    pub fn draw(&self, con: &Context, g: &mut G2d) {
       // rest of the code
       for obs in &self.obstacles {
            draw_rectangle(OBSTACLE_COLOR, obs.x, obs.y-2, 1, 5, con, g);
            draw_rotated_rectangle(OBSTACLE_COLOR, obs.x - 1, obs.y - 1, 2.0, 0.4, true, con, g);
            draw_rotated_rectangle(OBSTACLE_COLOR, obs.x + 1, obs.y + 1, 2.0, 0.4, false, con, g,);
        }
        // rest of the code
    }
}

Here I have used the draw-rectangle() function ( previously defined) to create the body of the cactus.

And I have created a new function draw_rotated_rectangle() that is similar to the draw-rectangle() function but it creates slanted/rotated rectangles perfect for the arms of the cactuses. Here's the function for reference:

use piston_window::{rectangle, Context, G2d, Transformed};
use piston_window::types::Color;
// ===========>
    // rest of the code.......
// ===========>
pub fn draw_rotated_rectangle( color: Color, x: i32, y: i32, width: f32, 
    height: f32, clockwise: bool, con: &Context, g: &mut G2d, // parameters
) {
    let gui_x = to_coord(x);
    let gui_y = to_coord(y);

    let rotation_angle = if clockwise {
        std::f64::consts::FRAC_PI_4 // 45 degrees clockwise
    } else {
        -std::f64::consts::FRAC_PI_4 // 45 degrees counterclockwise
    };
    // using the rectangle method from piston window crate
    rectangle(
        color,
        [
            gui_x,
            gui_y,
            BLOCK_SIZE * (width as f64),
            BLOCK_SIZE * (height as f64),
        ],
        con.transform
            .trans(gui_x, gui_y)
            .rot_rad(rotation_angle) 
            .trans(-gui_x, -gui_y), 
        g,
    );
}

And that's how the cute cactuses are ready. So make sure you avoid them while navigating the snake :)

Follow me for more Rust and other tech content!!

6
Subscribe to my newsletter

Read articles from Shanit Paul directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Shanit Paul
Shanit Paul