4
\$\begingroup\$

I made a small rust snake game in order to teach myself rust. I would like to know what I am doing well and poorly, and how to improve my rust code

Cargo.toml

[package]
name = "snake-rs"
version = "0.1.0"
authors = ["Hurricane996 <levijwillrich@gmail.com>"]
edition = "2018"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
sdl2 = "0.34"
rand = "0.8"

[profile.release]
lto = true

src/main.rs

extern crate sdl2;
extern crate rand;

mod constants;
mod fruit;
mod snake;
mod vector2;

use constants::BOARD_WIDTH;
use constants::BOARD_HEIGHT;
use constants::CELL_SIZE;

use fruit::Fruit;

use snake::Snake;

use vector2::Vector2;

use sdl2::event::Event;
use sdl2::keyboard::Keycode;
use sdl2::pixels::Color;

use std::collections::VecDeque;
use std::time::Duration;

fn main() {
    let sdl_context = sdl2::init().unwrap();
    let video_subsystem = sdl_context.video().unwrap();
    let window = video_subsystem.window("Rust Snake", BOARD_WIDTH*CELL_SIZE, BOARD_HEIGHT*CELL_SIZE)
        .position_centered()
        .build()
        .unwrap();
    let mut canvas = window.into_canvas().build().unwrap();
    let mut event_pump = sdl_context.event_pump().unwrap();
    let mut rng = rand::thread_rng();
    let mut snake = Snake::new();
    let mut fruit = Fruit::new();
    let mut frame_counter = 0;
    let mut input_stack = VecDeque::<Vector2>::with_capacity(32);

    fruit.mv(&mut rng);

    'running: loop {
        for event in event_pump.poll_iter() {
            match event {
                Event::Quit {..} => {
                    break 'running;
                },
                Event::KeyDown {keycode,..} => {
                    match keycode.expect("") {
                        Keycode::Up | Keycode:: W => {
                            input_stack.push_front(-Vector2::J);
                        }
                        Keycode::Down | Keycode:: S => {
                            input_stack.push_front(Vector2::J);
                        }
                        Keycode::Left | Keycode:: A => {
                            input_stack.push_front(-Vector2::I);
                        }
                        Keycode::Right | Keycode:: D => {
                            input_stack.push_front(Vector2::I);
                        }
                        _ => {}
                    }
                }
            _ => {}
            }
        }
    
        frame_counter+=1;
        if frame_counter > Snake::SPEED {
            frame_counter = 0;
            snake.mv(&mut input_stack, &fruit);
        
            if !snake.safe() {break 'running; }
          
            if snake.is_eating_fruit(&fruit) {
                fruit.mv(&mut rng);
            }
        }
    
        canvas.set_draw_color(Color::BLACK);
        canvas.clear();
    
        snake.draw(&mut canvas);
        fruit.draw(&mut canvas);
    
        canvas.present();
        std::thread::sleep(Duration::new(0, 1_000_000_000u32 / 60));
    }
}

src/fruit.rs

use crate::constants::BOARD_WIDTH;
use crate::constants::BOARD_HEIGHT;
use crate::constants::CELL_SIZE;

use crate::vector2::Vector2;

use rand::Rng;

use sdl2::pixels::Color;
use sdl2::rect::Rect;
use sdl2::render::Canvas;
use sdl2::render::RenderTarget;

pub struct Fruit(pub Vector2);

impl Fruit {
    pub fn new() -> Self {
        Fruit ( Vector2 {
            x: 0,
            y: 0
        })
    }

    pub fn mv<R: Rng>(&mut self, rng: &mut R ) {
        self.0.x = rng.gen_range(0..BOARD_WIDTH) as i32;
        self.0.y = rng.gen_range(0..BOARD_HEIGHT) as i32;
    }

    pub fn draw<T: RenderTarget>(&mut self, canvas: &mut Canvas<T>) {
        canvas.set_draw_color(Color::RED);
        canvas.fill_rect(Rect::new(self.0.x * CELL_SIZE as i32, self.0.y * CELL_SIZE as i32, CELL_SIZE, CELL_SIZE)).unwrap();
    }
}

src/snake.rs

use crate::constants::BOARD_HEIGHT;
use crate::constants::BOARD_WIDTH;
use crate::constants::CELL_SIZE;
use crate::fruit::Fruit;
use crate::vector2::Vector2;

use sdl2::pixels::Color;
use sdl2::rect::Rect;
use sdl2::render::Canvas;
use sdl2::render::RenderTarget;

use std::collections::VecDeque;

pub struct Snake {
    direction: Vector2,

    head: Vector2,
    body: VecDeque::<Vector2>
}

impl Snake {
    pub fn new() -> Self {
        let mut s = Snake {
            head: Vector2::new((BOARD_WIDTH/2) as i32, (BOARD_HEIGHT/2) as i32),
            direction: Vector2::I,
            body: VecDeque::<Vector2>::with_capacity((BOARD_WIDTH*BOARD_HEIGHT) as usize),
        };
  
        for i in 1..(Self::INITIAL_SIZE) {
            let i = i as i32;
            s.body.push_back(Vector2::new(s.head.x - i, s.head.y))
        }

        return s;
    }

    pub fn mv(&mut self, input_stack: &mut VecDeque::<Vector2>, fruit: &Fruit) {
        //this pushes the old head, so the body does not contain the head.
        self.body.push_front(self.head);

        'process_input: loop {
            let maybe_input = input_stack.pop_back();
        
            match maybe_input {
                Some(input) => {
                    if input != self.direction && input != -self.direction {
                        self.direction = input;
                        break 'process_input;
                    }
                }
                None => { break 'process_input; }
           }
        
        }
        
        self.head = self.head + self.direction;

        if self.head != fruit.0 {
            self.body.pop_back();
        }
    }

    pub fn is_eating_fruit(&self, fruit: &Fruit) -> bool {
        self.head == fruit.0 || self.body.iter().any(|&i|i==fruit.0)
    }
  
    pub fn safe(&self) -> bool {
        !(
            self.body.iter().any(|&i|i==self.head) ||
            self.head.x < 0||
            self.head.y < 0 ||
            self.head.x >= BOARD_WIDTH as i32 ||
            self.head.y >= BOARD_HEIGHT as i32
        )
    }

    pub fn draw<T: RenderTarget>(&self, canvas: &mut Canvas<T>) {
        canvas.set_draw_color(Color::GREEN);

        canvas.fill_rect(Rect::new(self.head.x * CELL_SIZE as i32, self.head.y * CELL_SIZE as i32, CELL_SIZE, CELL_SIZE)).unwrap();

        for segment in &self.body {
            canvas.fill_rect(Rect::new(segment.x * CELL_SIZE as i32, segment.y * CELL_SIZE as i32, CELL_SIZE, CELL_SIZE )).unwrap();
        }
    }

    pub const SPEED: u32 = 15;

    const INITIAL_SIZE: u32 = 3;
}

src/Vector2.rs

use std::ops;

#[derive(Copy,Clone,PartialEq)]
pub struct Vector2 {
    pub x: i32,
    pub y: i32
}

impl Vector2 {
    pub fn new(x: i32, y: i32) -> Vector2 {
        Vector2 {
            x: x,
            y: y
        }
    }

    pub const I : Vector2 = Vector2 {x: 1,y: 0};
    pub const J : Vector2 = Vector2 {x: 0,y: 1};
}

impl ops::Add<Vector2> for Vector2 {
    type Output = Self;
    fn add(self, rhs: Self) -> Self::Output {
        Vector2 {
            x: self.x + rhs.x,
            y: self.y + rhs.y
        }
    }
}

impl ops::Neg for Vector2 {
    type Output = Self;

    fn neg(self) -> Self::Output {
        Vector2 {
            x: -self.x,
            y: -self.y
        }
    }
}

src/constants.rs

pub const BOARD_HEIGHT: u32 = 16;
pub const BOARD_WIDTH: u32 = 16;

pub const CELL_SIZE: u32 = 32;
\$\endgroup\$
3
\$\begingroup\$

First of all, there are some quick things I'd change. extern crate [crate name]; is not needed in the 2018 edition of rust, and as such can be removed. Running clippy also shows some easy changes - firstly, in the Vector2::new function, you can change Vector2 { x: x, y: y } to Vector2 { x, y }. You can also replace the return s; with just s in Snake::new, as if a semicolon is omitted on the last line of a block, the value is returned.

There are also some style decisions I would personally change. Firstly, running cargo fmt can clean up a lot of minor issues (such as no spaces between items in the derive macros). Another is your using statements - you can replace

use constants::BOARD_HEIGHT;
use constants::BOARD_WIDTH;
use constants::CELL_SIZE;

with

use constants::{BOARD_HEIGHT, BOARD_WIDTH, CELL_SIZE};

which personally I prefer, but it is up to you. Similar things can be done with the other blocks, such as

use std::collections::VecDeque;
use std::time::Duration;

to

use std::{collections::VecDeque, time::Duration};

Now for code changes:

  • The 'running label in main and the 'process_input in Snake::mv should be removed as there are no nested loops, so a normal break works fine. In general, you only want to use labels for situations like wanting to break out of an outer loop while in an inner loop.
  • The .unwrap() calls can be replaced with .expect([error message]) calls if you still want to panic on failures, but using expect will give more information. This will make it easier to spot what is actually going wrong.
  • Functions and structs should have doc comments explaining what they're doing. For example, Vector2::I could be commented as follows to better explain it. This is not strictly necessary, but most editors will show the text when function calls/constants/structs are hovered over, so it can help a lot with understanding what programs are doing.
/// The unit vector representing (1, 0).
pub const I: Vector2 = Vector2 { x: 1, y: 0 };
  • Type annotations on input_stack in main and body in Snake::new for VecDeque::<Vector2> can be removed - so
let mut input_stack = VecDeque::<Vector2>::with_capacity(32);

would become

let mut input_stack = VecDeque::with_capacity(32);

And for what you've done well:

  • Using traits for operations such as Add is a very good habit.
  • Good use of iterators in functions like Snake::safe. Iterators are almost always more idiomatic than alternatives, so it is a good idea to get comfortable to using them (as well as methods like map and fold).
  • Initializing vectors with a given capacity is very handy for reducing the number of re-allocations, and this is a perfect use case for them - since you know the upper bound of how many items will ever be in the vector.
  • Using constants throughout your code (such as Snake::SPEED), compared to the alternative of littering magic numbers throughout your code.
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.