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"