5
\$\begingroup\$

This is my first ever program in Rust. I've made it using only the book, the reference, and any documentation about the crates/functions I was using on the official rust lang website.

I have the feeling this could be cleaned up a lot. I'm looking for suggestions.

extern crate rand;
extern crate device_query;

use std::{vec, thread::sleep, time::Duration};
use rand::*;
use device_query::{DeviceQuery, DeviceState, Keycode};

fn main(){
    let device_state = DeviceState::new();
    let mut rows = [['⬛'; 11]; 11];
    let mut score: i32 = 0;
    let mut direction: u8 = 0; // = Up, 1 = Left, 2 = Down, 3= Right
    let mut positions:Vec<(usize, usize)>  = vec![(5,5)]; //every square taken by our slithery friend
    let mut close_game: bool = false;
    rows[5][5] = '🟩';
    rows[3][5] = '🍎';
    while !close_game {
        print!("\x1B[2J\x1B[1;1H");
        direction = input_direction(direction, &device_state);
        close_game = game_tick(&mut rows, &mut positions, direction, &mut score);
        draw_game(&rows, &score);
        sleep(Duration::from_millis(300));
    }
    println!("Game over! Final Score: {}", score);
} 

fn draw_game(a: &[[char;11];11], score: &i32){
    println!("");
    println!("  Score:{:02} teo.snake", &score);
    for i in 1..10 {
        print!("  ");
        for j in 1..10 {
            print!("{}", a[i][j]);
        }
        println!("");
    }
}

fn game_tick(a: &mut[[char;11];11], vec: &mut Vec<(usize, usize)>, d: u8, s: &mut i32)-> bool {
    let mut game_over: bool = false;
    let newpos: (usize, usize)= newposition(d, vec[0], &mut game_over);
    if game_over == true {
        return game_over
    }

    for i in 0..vec.len() {
        if newpos == vec[i] {
            game_over = true;
            return game_over
        }
    }
    vec.insert(0, newpos);

    let mut next_fruitx:usize;
    let mut next_fruity:usize;
    let mut checkpass: bool;

    'looptillvalid: loop {
        checkpass = true;
        next_fruitx = rand::thread_rng().gen_range(1..10);
        next_fruity = rand::thread_rng().gen_range(1..10);
        for i in 0..vec.len() {
            if (next_fruitx, next_fruity) == vec[i] {
                checkpass = false;
                break;
            }
        }
        if checkpass == true {    
            break 'looptillvalid;
        }
    }
    let lastpos: (usize, usize) = vec.pop().unwrap();
    a[lastpos.0][lastpos.1] = '⬛';

    match a[newpos.0][newpos.1] {
        '🍎' => { vec.push(lastpos);
            a[newpos.0][newpos.1] = '🟩'; 
            a[lastpos.0][lastpos.1] = '🟩';
            *s += 1; 
            game_over = false;
            a[next_fruitx][next_fruity] = '🍎';
        }
         _  => { a[newpos.0][newpos.1] = '🟩';
            game_over = false}
    }

    game_over
}

fn newposition(d: u8, first: (usize, usize), go: &mut bool) -> (usize, usize){
    let mut ret: (usize, usize) = (0,0);
    match d {
        0 => { ret.0 = first.0 - 1; ret.1 = first.1}
        1 => { ret.0 = first.0; ret.1 = first.1 - 1}
        2 => { ret.0 = first.0 + 1; ret.1 = first.1}
        3 => { ret.0 = first.0; ret.1 = first.1 + 1}
        _ => {println!("Invalid Direction")}
    }

    if ret.0 < 1 || ret.0 > 9 || ret.1 < 1 || ret.1 > 9 {
        *go = true;
    }

    ret
}

fn input_direction(d: u8, d_state: &DeviceState) -> u8{

    let mut dir: u8 = d;
    
    let keys: Vec<Keycode> = d_state.get_keys(); 
        if keys.len() != 0 {
            match keys[0] {
                
                Keycode::Up => {dir = 0}
                Keycode::Left => {dir = 1}
                Keycode::Down => {dir = 2}
                Keycode::Right => {dir = 3}
                _ => ()
            }
        }
    dir
}
\$\endgroup\$

1 Answer 1

5
\$\begingroup\$

Clippy

One of the most important tools when writing good rust code is clippy. I recommend to configure your editor to run clippy on-the fly. It has very nice messages and includes an explanation for most lints. Let's go through some of it's suggestions:

warning: empty string literal in `println!`
  --> src/main.rs:31:5
   |
31 |     println!("");
   |     ^^^^^^^^^--^
   |              |
   |              help: remove the empty string
   |
   = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#println_empty_string
   = note: `#[warn(clippy::println_empty_string)]` on by default
warning: equality checks against true are unnecessary
  --> src/main.rs:45:8
   |
45 |     if game_over == true {
   |        ^^^^^^^^^^^^^^^^^ help: try simplifying it as shown: `game_over`
   |
   = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#bool_comparison
   = note: `#[warn(clippy::bool_comparison)]` on by default

This one actually applies to all popular languages. Let's see one more:

warning: length comparison to zero
   --> src/main.rs:115:8
    |
115 |     if keys.len() != 0 {
    |        ^^^^^^^^^^^^^^^ help: using `!is_empty` is clearer and more explicit: `!keys.is_empty()`
    |
    = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#len_zero
    = note: `#[warn(clippy::len_zero)]` on by default

Then, it has many issues related to iteration. In rust, it is unidiomatic to use explicit indexing when you can iterate over the actual values instead:

    for row in a {
        print!("  ");
        for elem in row {
            print!("{}", elem);
        }
        println!();
    }
    for pos in &*vec {
        if newpos == *pos {
            game_over = true;
            return game_over
        }
    }

Here, the assignment to game_over is actually useless, because you can just return true directly.

Use Rust 2021

The 2021 is the latest edition is the newest and should be used for all new code. If you're following a book or tutorial and it tells you to extern crate, it's very outdated.

Control flow logic

It may just be me, but I find the control flow of the loop in gametick very confusing. I think it is more readable if written this way:

    loop {
        next_fruitx = thread_rng().gen_range(1..10);
        next_fruity = thread_rng().gen_range(1..10);
        if vec.contains(&(next_fruitx, next_fruity)) {
            break;
        }
    }

vec.contains can also be used in other places. Finding these is an exercise to the reader.

At the end of game_tick, both branches of the match (which reads very nicely btw) contain the line game_over = false. They can be removed and replaced with a single false at the end of the function.

Constants

There are multiple occurrences of various constants in the code. This is both not DRY and also an instance of a magic number. Things I would extract into named constants are 11, 9, and the emojis. This reduces the risk of typos that can become logic errors.

Data Structures

You use a lot of Tuples for Coordinates, where I think a simple Struct with x and y fields could be more readable.

Since direction only has 4 valid values, I would make an enum for it. That way, you can remove the "Invalid direction" error, which should be unreachable.

I'd also write a 2-valued enum for the return type of game_tick, as the meaning of true and false aren't very obvious.

Thanks for reading all of this. Feel free to make a new post with updated code.

\$\endgroup\$
1
  • \$\begingroup\$ Amazing answer, thank you so much \$\endgroup\$
    – teoscuraa
    Commented Oct 26, 2023 at 16:06

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

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