A racing game in Rust

In celebration of Rust's 1.0 release, here's my first rust program. I'm not up to a full rust tutorial, so instead I'll just dump this and hope it's instructive for someone.

There's really not much to say about the program. There's a Box<> wrapping type that's not necessary, except a co-worker dared me to heap-allocate some things. He wanted to make a point that dealing with a lot of dynamic, heap-allocated objects is where Rust's lifetime system becomes difficult and unwieldy. I didn't find that, but this program is admittedly quite trivial.

A curses program that uses sleep() for its game loop requires a second thread for taking input, so there's a channel setup.

I'm a little ashamed of the unwrap() calls, since they represent unhandled null checks, and negate some benefits of using a language that claims memory safety as a priority. That's on me, though, since I could check my Option<> return values instead of taking the easy way out.

src/main.rs

#![feature(collections)]

extern crate collections;
extern crate ncurses;
extern crate rand;

use std::char;
use std::thread;
use std::sync::mpsc::channel;
use std::sync::mpsc::Receiver;
use std::sync::mpsc::TryRecvError;
use collections::vec_deque::VecDeque;
use ncurses::*;
use rand::Rng;

const FRAMERATE: u32 = 2;
const MAP_VISIBLE_WIDTH: usize = 10;
const ROAD_WIDTH: usize = 3;
const HILL_CHANCE_ONE_IN_X: u32 = 7;

fn get_road_char(terrain: &Terrain) -> u64 {
    match terrain {
        &Terrain::None => '_' as u64,
        &Terrain::Hill => 'A' as u64,
    }
}

fn is_crash(player: &PlayerState, map: &VecDeque<Box<MapSlice>>) -> bool {
    match map.get(player.x as usize).unwrap().cells[player.y as usize] {
        Terrain::None => false,
        _ => true
    }
}

fn draw(player: &PlayerState, map: &VecDeque<Box<MapSlice>>) {
    clear();

    for i in 0..map.0 {
        let &slice = &map.get(i).unwrap();
        for j in 0..slice.cells.0 {
            mvaddch(3+j as i32,3+i as i32,get_road_char(&slice.cells[j]));
        }
    }

    mvaddch( 3+player.y, 3+player.x, match is_crash(player,map) {
        true => '*' as u64,
        _ => '>' as u64
    });

    mvaddstr( 8, 3, "a and d to move, q to quit");
}

struct PlayerState {
    x: i32,
    y: i32,
}

enum Terrain {
    None,
    Hill
}

// a single vertical slice of the map
struct MapSlice {
    cells: [Terrain; ROAD_WIDTH]
}

fn gameloop(rx: Receiver<char>) {
    let mut player = PlayerState { x: 0, y: 1 };
    let mut map = VecDeque::<Box<MapSlice>>::new(); // Box is gratuitous heap allocation
    let mut rng = rand::thread_rng();

    // Start game loop
    loop {

        while map.0 < MAP_VISIBLE_WIDTH {

            // TODO: populate in a less repetitive way?
            let cells = [match rng.gen_weighted_bool(HILL_CHANCE_ONE_IN_X) {
                true => Terrain::Hill,
                _ => Terrain::None,
            },match rng.gen_weighted_bool(HILL_CHANCE_ONE_IN_X) {
                true => Terrain::Hill,
                _ => Terrain::None,
            },match rng.gen_weighted_bool(HILL_CHANCE_ONE_IN_X) {
                true => Terrain::Hill,
                _ => Terrain::None,
            }];
            let slice = Box::new(MapSlice { cells: cells });
            map.push_back(slice);
        }

        // Read all input characters since last '
        let mut quit = false;
        while !quit {
            let res = rx.try_recv();
            match res {
                Ok(val) => {
                    printw(format!("Got {}\n",val).as_ref());
                    match val {
                        'q' => quit = true,
                        'a' => {
                            player.y -= 1;
                            if player.y < 0 { player.y = 0 }
                        },
                        'd' => {
                            player.y += 1;
                            if player.y > 2 { player.y = 2 }
                        },
                        _ => ()
                    }
                },
                Err(TryRecvError::Empty) => break,
                Err(TryRecvError::Disconnected) => panic!("Channel disconnected")
            };
        }

        draw(&player,&map);
        refresh();

        if quit || is_crash(&player,&map) { break }

        map.pop_front();

        // Sleep until next '
        thread::sleep_ms(1000/FRAMERATE);
    }

}

fn main() {

    // ncurses screen initialization
    initscr();
    clear();
    noecho();
    curs_set(CURSOR_VISIBILITY::CURSOR_INVISIBLE);

    // Make a channel for input thread to send characters to main thread
    let (tx,rx) = channel();

    // start input thread
    {
        let tx = tx.clone();
        thread::spawn(move || {
            loop {
                let c: i32 = getch();
                tx.send(char::from_u32(c as u32).unwrap()).unwrap();
            }
        });
    }

    refresh();

    gameloop(rx);

    thread::sleep_ms(2000);

    // Clean up
    endwin();
}

Cargo.toml

[package]
name = "rustcurse"
version = "0.1.0"
authors = ["Erik Mackdanz "]

[dependencies]
ncurses="5.73.0"
rand="0.3.8"
05/18/2015 by stasibear Permalink