Roguelike Tutorial in Rust: Part 2

This is Part 2 in a many part series on how to make a roguelike game in Rust. If you’re lost, check out the Table of Contents to figure out where you should go.

Bring Our Heroine To Life #

So far our bold rabbit heroine is just an @ on a screen. To really bring her to life, we need to be able to issue commands (like move, attack, etc) and have the game update the display. Before we get to that a digression…

Game Loop #

At the core of just about every game is the game loop. This is an infinite loop that looks something like this:

loop {
    check_for_user_input();
    update_game_state();
    render_new_results();
}

There’s lots written on the internet about the game loop, but a great place to start is Game Programming Patterns - Game Loop. It goes into a lot of great detail about The Game Loop pattern, how to apply it, when to apply it and some of the common gotchas. Fortunately for us, our roguelike doesn’t actually need to process events all the time. Our game is turn-based. So we can actually stop processing while we wait for user inputs.

Let’s implement our game loop. We’ll just take the code from Part 1 and make it follow the pattern we want.

// src/main.rs
fn main() {
    let mut con = Console::init_root(80, 50, "libtcod Rust tutorial", false);
    let mut exit = false;
    while !(Console::window_closed() || exit) {
        // wait for user input
        let keypress = con.wait_for_keypress(true);

        // update game state
        match keypress.key {
            Special(key_code::Escape) => exit = true,
            _ => {}
        }

        // render
        con.clear();
        con.put_char(40, 25, '@', background_flag::Set);
        con.flush();
    }
}

That’s pretty simple. Just moved some lines around really. We have a problem though… when we launch this we just get a blank white screen.

White Screen Game

That’s not a very exciting game. The problem is that we don’t render the game until after user input has been received, so the first rendering happens as the last item of our loop. We can fix that by adding the render code outside the loop.

fn main() {
    let mut con = Console::init_root(80, 50, "libtcod Rust tutorial", false);
    let mut exit = false;
    // render
    con.clear();
    con.put_char(40, 25, '@', background_flag::Set);
    con.flush();
    while !(Console::window_closed() || exit) {
      // our game loop 
   }
}

That’s great… but now we have redundant code. Let’s just move that into a function that does the rendering for us.

fn render(con: &mut Console) {
    con.clear();
    con.put_char(40, 25, '@', background_flag::Set);
    con.flush();
}

fn main() {
    let mut con = Console::init_root(80, 50, "libtcod Rust tutorial", false);
    let mut exit = false;
    // render
    render(&mut con);
    while !(Console::window_closed() || exit) {
        // wait for user input
        let keypress = con.wait_for_keypress(true);

        // update game state
        match keypress.key {
            Special(key_code::Escape) => exit = true,
            _ => {}
        }

        // render
        render(&mut con);
    }
}

Cool! Now our code is DRY-er. You should be able to build that and see that it works just as we expect.

A word about some syntax. You’ll see that the render function has a method signature of (con: &mut Console). That is saying, “I expect one variable, con, to be a pointer to a mutable object”. And when we call render() we prefix the variable being passed in with &mut which says, “Take this object, and use a mutable pointer to it”. This is only possible because when we instantiated the con variable we used the mut keyword.

So now we have a game loop. Here’s a link to my commit that gets us up to here.

Getting Our Heroine to Move #

Now that our game loop is in the right format, we can start to do things with it. Let’s implement the command to make our rabbit move up on the screen. To do that we need to

Give it a try before looking at my solution below. (From here on out I’ll be removing some sections of code and replacing them with comments if they didn’t change, just to save space).

fn render(con: &mut Console, x: int, y: int) {
    con.clear();
    con.put_char(x, y, '@', background_flag::Set);
    con.flush();
}

fn main() {
    // init window
    let mut charX = 40i;
    let mut charY = 25i;
    // render
    render(&mut con, charX, charY);
    while !(Console::window_closed() || exit) {
        // wait for user input
        // update game state
        match keypress.key {
            Special(key_code::Escape) => exit = true,
            Special(key_code::Up) => {
                if charY >= 1 {
                    charY -= 1;
                }
            },
            _ => {}
        }

        // render
        render(&mut con, charX, charY);
    }
}

Did you run into some of the same issues I did? First I didn’t know what the key_code enum for “up” was, so I looked at the source definition. Then I forgot that libtcod sets the {0,0} cord to the top left, so in order to move up you need to decrease your Y value. Then I didn’t check the out of bounds condition, which allowed me to run the character off screen, and caused a fun crash:

# task '<main>' failed at 'assertion failed: x >= 0 && y >= 0', src/lib.rs:115
# An unknown error occurred

Let’s implement down next. This time we only need to:

 fn main() {
    let conX = 80i;
    let conY = 50i;
    let mut con = Console::init_root(conX, conY, "libtcod Rust tutorial", false);
    // init variables
    // render
    while !(Console::window_closed() || exit) {
        // wait for user input

        // update game state
        match keypress.key {
            Special(key_code::Escape) => exit = true,
            Special(key_code::Up) => {
                if charY >= 1 {
                    charY -= 1;
                }
            },
            Special(key_code::Down) => {
                if charY < (conY - 1) {
                    charY +=1;
                }
            },
            _ => {}
        }

        // render
    }
}

I ran into just one gotcha here. We have to store the height of the window in conY so that we can keep our heroine within the window bounds. What I didn’t realize at first was that the last Y point in the window is one less than the height (seems obvious in retrospect because we start at point 0).

Now that we have all the pieces in places, let’s do both left and right:

Special(key_code::Left) => {
    if charX >= 1 {
        charX -= 1;
    }
},
Special(key_code::Right) => {
    if charX < (conX - 1) {
        charX += 1;
    }
},

This one was super-duper easy. We just added those two conditions. Now our heroine is as free as a bird! She can move in all directions. But she’s pretty lonely… let’s add a dog to our game world! Dogs and rabbits can be friends… right? For now, let’s just make it wander around the screen randomly.

So, we’ll need to do several things here:

Let’s take them 1 at a time:

// add the x and y variables
let mut charX = 40i;
let mut charY = 25i;
let mut dogX  = 10i;
let mut dogY  = 10i;

Easy enough. Note the i at the end of these? Rust has multiple integer types. int, uint, and more. You can see the full list here. Next we update our render method:

fn render(con: &mut Console, x: int, y: int, dogX: int, dogY: int) {
    con.clear();
    con.put_char(x, y, '@', background_flag::Set);
    con.put_char(dogX, dogY, 'd', background_flag::Set);
    con.flush();
}

We change our calls to render:

// render
render(&mut con, charX, charY, dogX, dogY);

Now let’s make our dog move. To do this we’ll generate two random ints, from -1 to 1. One of these will be our x-offset, and the other will be our y-offset.

// use the random namespace
use std::rand::Rng;

loop {
        // wait for input
        // update game
        let offset_x = std::rand::task_rng().gen_range(0, 3i) - 1;
        if (dogX + offset_x) > 0 && (dogX + offset_x) < (conX - 1) {
            dogX += offset_x;
        }

        let offset_y = std::rand::task_rng().gen_range(0, 3i) - 1;
        if (dogY + offset_y) > 0 && (dogY + offset_y) < (conY - 1) {
            dogY += offset_y;
        }
        // render
}

You should be able to cargo run your game at this point and have a d that dances around the screen.

Cleanup #

While this all works, the code has gotten pretty messy along the way. Here’s my full src/main.rs file:

extern crate tcod;
use tcod::{Console, background_flag, key_code, Special};
use std::rand::Rng;

fn render(con: &mut Console, x: int, y: int, dogX: int, dogY: int) {
    con.clear();
    con.put_char(x, y, '@', background_flag::Set);
    con.put_char(dogX, dogY, 'd', background_flag::Set);
    con.flush();
}

fn main() {
    let conX = 80i;
    let conY = 50i;
    let mut con = Console::init_root(conX, conY, "libtcod Rust tutorial", false);
    let mut exit = false;
    let mut charX = 40i;
    let mut charY = 25i;
    let mut dogX  = 10i;
    let mut dogY  = 10i;
    // render
    render(&mut con, charX, charY, dogX, dogY);
    while !(Console::window_closed() || exit) {
        // wait for user input
        let keypress = con.wait_for_keypress(true);

        // update game state
        match keypress.key {
            Special(key_code::Escape) => exit = true,
            Special(key_code::Up) => {
                if charY >= 1 {
                    charY -= 1;
                }
            },
            Special(key_code::Down) => {
                if charY < (conY - 1) {
                    charY +=1;
                }
            },
            Special(key_code::Left) => {
                if charX >= 1 {
                    charX -= 1;
                }
            },
            Special(key_code::Right) => {
                if charX < (conX - 1) {
                    charX += 1;
                }
            },
            _ => {}
        }

        let offset_x = std::rand::task_rng().gen_range(0, 3i) - 1;
        if (dogX + offset_x) > 0 && (dogX + offset_x) < (conX - 1) {
            dogX += offset_x;
        }

        let offset_y = std::rand::task_rng().gen_range(0, 3i) - 1;
        if (dogY + offset_y) > 0 && (dogY + offset_y) < (conY - 1) {
            dogY += offset_y;
        }

        // render
        render(&mut con, charX, charY, dogX, dogY);
    }
}

There are redundancies all over the place. Look at all these ...X, ...Y variables we have for starters. Let’s wrap them up into a struct.

struct Point {
    x: int,
    y: int
}

This way we can encapsulate the fact that these x’s and y’s belong together. We’ll have to change a few method signatures and some code in a few places:

extern crate tcod;
use tcod::{Console, background_flag, key_code, Special};
use std::rand::Rng;

struct Point {
    x: int,
    y: int
}

fn render(con: &mut Console, c_point: Point, d_point: Point) {
    con.clear();
    con.put_char(c_point.x, c_point.y, '@', background_flag::Set);
    con.put_char(d_point.x, d_point.y, 'd', background_flag::Set);
    con.flush();
}

fn main() {
    let conX = 80i;
    let conY = 50i;
    let mut con = Console::init_root(conX, conY, "libtcod Rust tutorial", false);
    let mut exit = false;
    let mut char_point = Point { x: 40, y: 25 };
    let mut dog_point  = Point { x: 10, y: 10 };
    // render
    render(&mut con, char_point, dog_point);
    while !(Console::window_closed() || exit) {
        // wait for user input
        let keypress = con.wait_for_keypress(true);

        // update game state
        match keypress.key {
            Special(key_code::Escape) => exit = true,
            Special(key_code::Up) => {
                if char_point.y >= 1 {
                    char_point.y -= 1;
                }
            },
            Special(key_code::Down) => {
                if char_point.y < (conY - 1) {
                    char_point.y +=1;
                }
            },
            Special(key_code::Left) => {
                if char_point.x >= 1 {
                    char_point.x -= 1;
                }
            },
            Special(key_code::Right) => {
                if char_point.x < (conX - 1) {
                    char_point.x += 1;
                }
            },
            _ => {}
        }

        let offset_x = std::rand::task_rng().gen_range(0, 3i) - 1;
        if (dog_point.x + offset_x) > 0 && (dog_point.x + offset_x) < (conX - 1) {
            dog_point.x += offset_x;
        }

        let offset_y = std::rand::task_rng().gen_range(0, 3i) - 1;
        if (dog_point.y + offset_y) > 0 && (dog_point.y + offset_y) < (conY - 1) {
            dog_point.y += offset_y;
        }

        // render
        render(&mut con, char_point, dog_point);
    }
}

Let’s setup another struct for the window bounds. It’ll be composed of two Points, a min and a max.

struct Bound {
    min: Point,
    max: Point
}

And we can change conX and conY to this new data structure.

let window_bounds = Bound { min: Point { x: 0, y: 0 }, max: Point { x: 79, y: 49 } };

And everywhere we had conX we use window_bounds.max.x and conY becomes window_bounds.max.y.

Now we can do some cool things. First thing we can do is move the logic for offsetting a point into the Point struct:

impl Point {
    fn offset_x(&self, offset: int) -> Point {
        Point { x: self.x + offset, y: self.y }
    }

    fn offset_y(&self, offset: int) -> Point {
        Point { x: self.x, y: self.y + offset }
    }

    fn offset(&self, offset: Point) -> Point {
        Point { x: self.x + offset.x, y: self.y + offset.y }
    }
}

Then we can move our logic for checking whether or not a point is inside the window to the Bounds struct:

enum Contains {
    DoesContain,
    DoesNotContain
}

impl Bound {
    fn contains(&self, point: Point) -> Contains {
        if
            point.x >= self.min.x &&
            point.x <= self.max.x &&
            point.y >= self.min.y &&
            point.y <= self.max.y
        {
            DoesContain
        } else {
            DoesNotContain
        }
    }
}

And lastly, we can update all of our game loop logic:

// update game state
let mut offset = Point { x: 0, y: 0 };
match keypress.key {
    Special(key_code::Escape) => exit = true,
    Special(key_code::Up) => {
        offset.y = -1;
    },
    Special(key_code::Down) => {
        offset.y = 1;
    },
    Special(key_code::Left) => {
        offset.x = -1;
    },
    Special(key_code::Right) => {
        offset.x = 1;
    },
    _ => {}
}

match window_bounds.contains(char_point.offset(offset)) {
    DoesContain    => char_point = char_point.offset(offset),
    DoesNotContain => {}
}

let offset_x = std::rand::task_rng().gen_range(0, 3i) - 1;
match window_bounds.contains(dog_point.offset_x(offset_x)) {
    DoesContain    => dog_point = dog_point.offset_x(offset_x),
    DoesNotContain => {}
}

let offset_y = std::rand::task_rng().gen_range(0, 3i) - 1;
match window_bounds.contains(dog_point.offset_y(offset_y)) {
    DoesContain    => dog_point = dog_point.offset_y(offset_y),
    DoesNotContain => {}
}

This gives us some pretty good building blocks to work off of. But there’s something that still bugs me. It’s centered around our char_point, dog_point and the render function. Imagine the final state of our game. We’ll have the character and tons of levels, each with dozens of monsters in it. Are going to add a new monster_point for every point? That doesn’t seem to make sense to me. Fortunately, Game Programming Patterns has a helpful pattern, The Update Method.

The Update Method #

The Update Method is great. It basically says “make every object in your game responsible for updating itself.” Makes sense. Let’s keep all that logic in one centralized place.

We’ll add a new struct for tracking the characters in our game. Let’s just call it Character for now. They’ll have a position and a display_char.

struct Character {
    position:     Point,
    display_char: char
}

Now let’s replace our point markers with the fully fledged structs, change the render method, and replace all instances of char_point and dog_point:

fn render(con: &mut Console, ch: Character, dog: Character) {
    con.clear();
    con.put_char(ch.position.x, ch.position.y, ch.display_char, background_flag::Set);
    con.put_char(dog.position.x, dog.position.y, dog.display_char, background_flag::Set);
    con.flush();
}

let mut ch  = Character { position: Point { x: 40, y: 25 }, display_char: '@' };
let mut dog = Character { position: Point { x: 10, y: 10 }, display_char: 'd' };

match window_bounds.contains(ch.position.offset(offset)) {
    DoesContain    => ch.position = ch.position.offset(offset),
    DoesNotContain => {}
}

// etc

That previous step didn’t actually help solve our problem. We still have to deal with the ch variable everywhere and this dog variable everywhere. Let’s actually implement The Update Method now.

This is by far the biggest refactor we’ve seen so far. You should try it on your own, and we’ll compare notes after you’ve got it. If you get stuck, the Rust docs, Game Loop Method, and Mozilla IRC #rust channel are great places to ask for help.

Okay, here’s my solution:

extern crate tcod;
use tcod::{Console, background_flag, key_code, Special};
use std::rand::Rng;

struct Point {
    x: int,
    y: int
}

impl Point {
    fn offset_x(&self, offset: int) -> Point {
        Point { x: self.x + offset, y: self.y }
    }

    fn offset_y(&self, offset: int) -> Point {
        Point { x: self.x, y: self.y + offset }
    }

    fn offset(&self, offset: Point) -> Point {
        Point { x: self.x + offset.x, y: self.y + offset.y }
    }
}

struct Bound {
    min: Point,
    max: Point
}

enum Contains {
    DoesContain,
    DoesNotContain
}

impl Bound {
    fn contains(&self, point: Point) -> Contains {
        if
            point.x >= self.min.x &&
            point.x <= self.max.x &&
            point.y >= self.min.y &&
            point.y <= self.max.y
        {
            DoesContain
        } else {
            DoesNotContain
        }
    }
}

struct Game {
    exit:          bool,
    window_bounds: Bound
}

struct Character {
    position:     Point,
    display_char: char
}

impl Character {
    fn new(x: int, y: int, dc: char) -> Character {
        Character { position: Point { x: x, y: y }, display_char: dc }
    }
}

struct NPC {
    position:     Point,
    display_char: char
}

impl NPC {
    fn new(x: int, y: int, dc: char) -> NPC {
        NPC { position: Point { x: x, y: y }, display_char: dc }
    }
}

trait Updates{
    fn update(&mut self, tcod::KeyState, Game);
    fn render(&self, &mut Console);
}

impl Updates for Character {
    fn update(&mut self, keypress: tcod::KeyState, game: Game) {
        let mut offset = Point { x: 0, y: 0 };
        match keypress.key {
            Special(key_code::Up) => {
                offset.y = -1;
            },
            Special(key_code::Down) => {
                offset.y = 1;
            },
            Special(key_code::Left) => {
                offset.x = -1;
            },
            Special(key_code::Right) => {
                offset.x = 1;
            },
            _ => {}
        }

        match game.window_bounds.contains(self.position.offset(offset)) {
            DoesContain    => self.position = self.position.offset(offset),
            DoesNotContain => {}
        }
    }

    fn render(&self, console: &mut Console) {
        console.put_char(self.position.x, self.position.y, self.display_char, background_flag::Set);
    }
}

impl Updates for NPC {
    fn update(&mut self, keypress: tcod::KeyState, game: Game) {
        let offset_x = std::rand::task_rng().gen_range(0, 3i) - 1;
        match game.window_bounds.contains(self.position.offset_x(offset_x)) {
            DoesContain    => self.position = self.position.offset_x(offset_x),
            DoesNotContain => {}
        }

        let offset_y = std::rand::task_rng().gen_range(0, 3i) - 1;
        match game.window_bounds.contains(self.position.offset_y(offset_y)) {
            DoesContain    => self.position = self.position.offset_y(offset_y),
            DoesNotContain => {}
        }
    }

    fn render(&self, console: &mut Console) {
        console.put_char(self.position.x, self.position.y, self.display_char, background_flag::Set);
    }
}

fn render(con: &mut Console, objs: &Vec<Box<Updates>>) {
    con.clear();
    for i in objs.iter() {
        i.render(con);
    }
    con.flush();
}

fn update(objs: &mut Vec<Box<Updates>>, keypress: tcod::KeyState, game: Game) {
    for i in objs.mut_iter() {
        i.update(keypress, game);
    }
}

fn main() {
    let mut game = Game { exit: false, window_bounds: Bound { min: Point { x: 0, y: 0 }, max: Point { x: 79, y: 49 } } };
    let mut con = Console::init_root(game.window_bounds.max.x + 1, game.window_bounds.max.y + 1, "libtcod Rust tutorial", false);
    let c = box Character::new(40, 25, '@') as Box<Updates>;
    let d = box NPC::new(10, 10, 'd') as Box<Updates>;
    let mut objs: Vec<Box<Updates>> = vec![
        c, d
    ];

    render(&mut con, &objs);
    while !(Console::window_closed() || game.exit) {
        // wait for user input
        let keypress = con.wait_for_keypress(true);

        // update game state
        match keypress.key {
            Special(key_code::Escape) => game.exit = true,
            _                         => {}
        }
        update(&mut objs, keypress, game);

        // render
        render(&mut con, &objs);
    }
}

If you’re thinking, “Wtf?”, I don’t blame you. This took me a couple hours to figure out.

I’ll go through the changes one by one, starting at the top of the file. The first obvious change is the creation of two new structs, Game and NPC, and the implementation of the ::new() method on both NPC and Character.

struct Game {
    exit:          bool,
    window_bounds: Bound
}

impl Character {
    fn new(x: int, y: int, dc: char) -> Character {
        Character { position: Point { x: x, y: y }, display_char: dc }
    }
}

struct NPC {
    position:     Point,
    display_char: char
}

impl NPC {
    fn new(x: int, y: int, dc: char) -> NPC {
        NPC { position: Point { x: x, y: y }, display_char: dc }
    }
}

The ::new method is fairly straight-forward. It just returns a new NPC or Character with the right traits. I split the two out because, according to The Update Method pattern, each type of entity should be responsible for updating itself. In order to have a distinct #update() method for the dog and our hero, they have to be different types of entities.

The Game struct is just a placeholder for now. I needed to be able to pass around certain pieces of game state and wanted to do them all in one place.

The next change is the introduction of a new trait, Updates.

trait Updates{
    fn update(&mut self, tcod::KeyState, Game);
    fn render(&self, &mut Console);
}

Traits are pretty cool in rust. They’re like interfaces that can be selectively applied to a type. One of the really powerful things is that you can cast a pointer to the type of one of its traits, like this:

// create a new character and grab a reference (pointer) to it
let ch = &Character::new()

// cast ch to type &Updates (a reference to an Update type)
ch as &Updates

We’ll see how useful this is later on. Now that we have our trait, we need to implement it for our two types. Our trait defines two methods, update and render. First, we’ll implement that on our Character:

impl Updates for Character {
    fn update(&mut self, keypress: tcod::KeyState, game: Game) {
        let mut offset = Point { x: 0, y: 0 };
        match keypress.key {
            Special(key_code::Up) => {
                offset.y = -1;
            },
            Special(key_code::Down) => {
                offset.y = 1;
            },
            Special(key_code::Left) => {
                offset.x = -1;
            },
            Special(key_code::Right) => {
                offset.x = 1;
            },
            _ => {}
        }

        match game.window_bounds.contains(self.position.offset(offset)) {
            DoesContain    => self.position = self.position.offset(offset),
            DoesNotContain => {}
        }
    }

    fn render(&self, console: &mut Console) {
        console.put_char(self.position.x, self.position.y, self.display_char, background_flag::Set);
    }
}

Look familiar? I pretty much just moved the code from the main function into the update function for Character. I’ll do the same thing for NPC:

impl Updates for NPC {
    fn update(&mut self, keypress: tcod::KeyState, game: Game) {
        let offset_x = std::rand::task_rng().gen_range(0, 3i) - 1;
        match game.window_bounds.contains(self.position.offset_x(offset_x)) {
            DoesContain    => self.position = self.position.offset_x(offset_x),
            DoesNotContain => {}
        }

        let offset_y = std::rand::task_rng().gen_range(0, 3i) - 1;
        match game.window_bounds.contains(self.position.offset_y(offset_y)) {
            DoesContain    => self.position = self.position.offset_y(offset_y),
            DoesNotContain => {}
        }
    }

    fn render(&self, console: &mut Console) {
        console.put_char(self.position.x, self.position.y, self.display_char, background_flag::Set);
    }
}

Now that we’ve contained all the logic for updating and rendering a unit to that unit, we have all the pieces in place to finish implementing The Update Method pattern. Jump down the file to where we initialize our rabbit and dog, and you’ll see I’ve changed them to:

let c = box Character::new(40, 25, '@') as Box<Updates>;
let d = box NPC::new(10, 10, 'd') as Box<Updates>;
let mut objs: Vec<Box<Updates>> = vec![
    c, d
];

There’s some confusing syntax there, I’ll walk through it. First, we create two values, c and d. One is a Character and the other a Dog, but they’re both using the as keyword to cast them to Update. Also notice the box and Box syntax, which basically says, this is a heap allocated variable. Then we initialize a new value, objs to be type Vec<Box<Updates>>. That basically says “A vector of boxes of type Updates”. This was one of the more difficult parts for me to wrap my head around. The Rust docs have a good writeup on pointers in Rust.

Now we have a vector of all our objects that need to be updated and rendered in our game loop, let’s change our render method and introduce a new update method:

fn render(con: &mut Console, objs: &Vec<Box<Updates>>) {
    con.clear();
    for i in objs.iter() {
        i.render(con);
    }
    con.flush();
}

fn update(objs: &mut Vec<Box<Updates>>, keypress: tcod::KeyState, game: Game) {
    for i in objs.mut_iter() {
        i.update(keypress, game);
    }
}

In the render method we now loop through our objs vector and call render on each item individually, and in our update method we do the same thing. Note the mut_iter() in the update method. This is because the update method needs to have a mutable reference to self to work. Getting my references and mutable state correct is the hardest part of learning Rust (for me) so far.

That was a really big but really powerful change-set. Here’s a commit to where we just ended up.

Structure #

There’s one last thing I want to do before ending this tutorial. Right now everything is in a giant file, src/main.rs. That’s silly, we have directories for a reason. Let’s use them. We’ll use Rust modules to move different pieces of code into different files and include them in our main file. We’ll start with Point because it’s the smallest.

The first step is to create a new file src/lib.rs. This file will act like a manifest, pointing to all the other modules we create. For now, leave it empty. Our directory structure looks like this now:

.
├── Cargo.lock
├── Cargo.toml
├── src
│   ├── lib.rs
│   └── main.rs

Now create a new folder, src/util/ and a new file in it, src/util/mod.rs. Copy the following from src/main.rs into src/util/mod.rs so it looks like this:

struct Point {
    x: int,
    y: int
}

impl Point {
    fn offset_x(&self, offset: int) -> Point {
        Point { x: self.x + offset, y: self.y }
    }

    fn offset_y(&self, offset: int) -> Point {
        Point { x: self.x, y: self.y + offset }
    }

    fn offset(&self, offset: Point) -> Point {
        Point { x: self.x + offset.x, y: self.y + offset.y }
    }
}

Now in src/lib.rs add a single line:

pub mod util;

And finally, add to the top of src/main.rs:

// below extern create tcod;
extern crate dwemthys;

// after all other extern crate lines
use dwemthys::util::Point;

This tells our main.rs file to use the dwemthys crate (a Rust module) and the util submodule and the Point struct in the top level namespace.

If you try to build right now you’ll get a whole ton of errors, and they should all be complaining about accessing private fields of Point. This is because everything in a module is private by default, and must explicitly marked as public. So we add a bunch of pub in the src/util/mod.rs file:

pub struct Point {
    pub x: int,
    pub y: int
}

impl Point {
    pub fn offset_x(&self, offset: int) -> Point {
        Point { x: self.x + offset, y: self.y }
    }

    pub fn offset_y(&self, offset: int) -> Point {
        Point { x: self.x, y: self.y + offset }
    }

    pub fn offset(&self, offset: Point) -> Point {
        Point { x: self.x + offset.x, y: self.y + offset.y }
    }
}

Things that have to be marked pub are: struct definitions, traits (like x and y in our struct definition) and fns. Now if you build it should all just work. Let’s do the same thing with Bound and Contains:

// src/util/mod.rs
pub enum Contains {
    DoesContain,
    DoesNotContain
}

pub struct Bound {
    pub min: Point,
    pub max: Point
}

impl Bound {
    pub fn contains(&self, point: Point) -> Contains {
        if
            point.x >= self.min.x &&
            point.x <= self.max.x &&
            point.y >= self.min.y &&
            point.y <= self.max.y
        {
            DoesContain
        } else {
            DoesNotContain
        }
    }
}

And we modify our use statement in src/main.rs to look like this:

use dwemthys::util::{Point, Bound}

If you try to build at this point you’ll get some interesting errors:

/Users/jmcfarland/code/rust/dwemthys/src/main.rs:36:13: 36:27 error: unreachable pattern [E0001] (pass `--explain E0001` to see a detailed explanation)
/Users/jmcfarland/code/rust/dwemthys/src/main.rs:36             DoesNotContain => {}
                                                                ^~~~~~~~~~~~~~
/Users/jmcfarland/code/rust/dwemthys/src/main.rs:42:13: 42:27 error: unreachable pattern [E0001] (pass `--explain E0001` to see a detailed explanation)
/Users/jmcfarland/code/rust/dwemthys/src/main.rs:42             DoesNotContain => {}
                                                                ^~~~~~~~~~~~~~
/Users/jmcfarland/code/rust/dwemthys/src/main.rs:83:13: 83:27 error: unreachable pattern [E0001] (pass `--explain E0001` to see a detailed explanation)
/Users/jmcfarland/code/rust/dwemthys/src/main.rs:83             DoesNotContain => {}

Apparently our compiler thinks we can’t ever get to the DoesNotContain case, even though I’m pretty sure we can still get there. The solution to this is a little in-elegant, but we need to raise DoesNotContain and DoesContain to the top-level namespace (or qualify them):

use dwemthys::util::{Point, Bound, DoesContain, DoesNotContain};

Next we’ll move the Game struct to it’s own module. This one is fairly straight forward. Note the use util::Bound at the top as Game uses the Bound struct:

use util::Bound;

pub struct Game {
    pub exit:          bool,
    pub window_bounds: Bound
}

Then we we add it to the src/lib.rs file:

pub mod game;

And we should be able to build just fine. Next let’s try to move out one of our move complicated structs, Character. This one requires creating two mods actually, because both NPC and Character rely on the Update trait, so let’s start there:

// src/traits/mod.rs
extern crate tcod;
use self::tcod::{Console};
use game::Game;

pub trait Updates {
    fn update(&mut self, tcod::KeyState, Game);
    fn render(&self, &mut Console);
}

Interestingly, it looks like the methods in a trait don’t need to be marked as pub, just the top-level trait itself. Once we have that, we can add pub mod traits; to our src/lib.rs file and use dwemthys::traits::Updates to our src/main.rs file.

Now let’s do the same thing for Character:

extern crate tcod;
use self::tcod::{Console, background_flag, key_code, Special};

use traits::Updates;
use util::{Point, DoesContain, DoesNotContain};
use game::Game;

pub struct Character {
    pub position:     Point,
    pub display_char: char
}

impl Character {
    pub fn new(x: int, y: int, dc: char) -> Character {
        Character { position: Point { x: x, y: y }, display_char: dc }
    }
}

impl Updates for Character {
    fn update(&mut self, keypress: tcod::KeyState, game: Game) {
        let mut offset = Point { x: 0, y: 0 };
        match keypress.key {
            Special(key_code::Up) => {
                offset.y = -1;
            },
            Special(key_code::Down) => {
                offset.y = 1;
            },
            Special(key_code::Left) => {
                offset.x = -1;
            },
            Special(key_code::Right) => {
                offset.x = 1;
            },
            _ => {}
        }

        match game.window_bounds.contains(self.position.offset(offset)) {
            DoesContain    => self.position = self.position.offset(offset),
            DoesNotContain => {}
        }
    }

    fn render(&self, console: &mut Console) {
        console.put_char(self.position.x, self.position.y, self.display_char, background_flag::Set);
    }
}

This is pretty straight forward at this point. Include all the things our module relies on, make sure we mark the right functions and definitions as public. Nothing new here [=

Let’s do the same thing with NPC:

extern crate tcod;
use self::tcod::{Console, background_flag};

use traits::Updates;
use util::{Point, DoesContain, DoesNotContain};
use game::Game;

use std;
use std::rand::Rng;

pub struct NPC {
    position:     Point,
    display_char: char
}

impl NPC {
    pub fn new(x: int, y: int, dc: char) -> NPC {
        NPC { position: Point { x: x, y: y }, display_char: dc }
    }
}

impl Updates for NPC {
    fn update(&mut self, keypress: tcod::KeyState, game: Game) {
        let offset_x = std::rand::task_rng().gen_range(0, 3i) - 1;
        match game.window_bounds.contains(self.position.offset_x(offset_x)) {
            DoesContain    => self.position = self.position.offset_x(offset_x),
            DoesNotContain => {}
        }

        let offset_y = std::rand::task_rng().gen_range(0, 3i) - 1;
        match game.window_bounds.contains(self.position.offset_y(offset_y)) {
            DoesContain    => self.position = self.position.offset_y(offset_y),
            DoesNotContain => {}
        }
    }

    fn render(&self, console: &mut Console) {
        console.put_char(self.position.x, self.position.y, self.display_char, background_flag::Set);
    }
}

Same sort of deal. Copy a bunch of code over, make sure we have the right use statements, mark everything as pub and we’re good to go. At this point you should have a directory structure that looks like this:

.
├── Cargo.lock
├── Cargo.toml
├── src
│   ├── character
│   │   └── mod.rs
│   ├── game
│   │   └── mod.rs
│   ├── lib.rs
│   ├── main.rs
│   ├── npc
│   │   └── mod.rs
│   ├── traits
│   │   └── mod.rs
│   └── util
│       └── mod.rs

And a src/main.rs that looks like this:

extern crate tcod;
extern crate dwemthys;

use dwemthys::util::{Point, Bound};
use dwemthys::traits::Updates;
use dwemthys::game::Game;
use dwemthys::character::Character;
use dwemthys::npc::NPC;

use tcod::{Console, key_code, Special};

fn render(con: &mut Console, objs: &mut Vec<Box<Updates>>) {
    con.clear();
    for i in objs.iter() {
        i.render(con);
    }
    con.flush();
}

fn update(objs: &mut Vec<Box<Updates>>, keypress: tcod::KeyState, game: Game) {
    for i in objs.mut_iter() {
        i.update(keypress, game);
    }
}

fn main() {
    let mut game = Game { exit: false, window_bounds: Bound { min: Point { x: 0, y: 0 }, max: Point { x: 79, y: 49 } } };
    let mut con = Console::init_root(game.window_bounds.max.x + 1, game.window_bounds.max.y + 1, "libtcod Rust tutorial", false);
    let c = box Character::new(40, 25, '@') as Box<Updates>;
    let d = box NPC::new(10, 10, 'd') as Box<Updates>;
    let mut objs: Vec<Box<Updates>> = vec![
        c, d
    ];

    render(&mut con, &objs);
    while !(Console::window_closed() || game.exit) {
        // wait for user input
        let keypress = con.wait_for_keypress(true);

        // update game state
        match keypress.key {
            Special(key_code::Escape) => game.exit = true,
            _                         => {}
        }
        update(&mut objs, keypress, game);

        // render
        render(&mut con, &objs);
    }
}

Nice! It only has three methods. I think it’s safe to stop there for now.

Conclusion #

Holy crap! That was a lot more work than I thought it was going to be. I hope you made it this far without too much trouble. There’s a lot I’d like to change about our current implementation, but I think we’re in a good state. We did what we set out to do, we made our heroine come alive. Here’s a tag to the end state after this part.

Next #

Part 3: Combat!

Previous #

Table of Contents #

Table of Contents

Previous #

Part 1: Setup and First Pass

 
195
Kudos
 
195
Kudos

Now read this

Roguelike Tutorial in Rust

Part 0: Why # In mid-April of 2014 I was stuck in a hotel room in Manhattan with the stomach flu. In a fever induced stupor, I started reading Steve Klabnik’s Rust for Rubyists. It’s a great primer for the language, and if you haven’t... Continue →