Now that our game is bursting with gameplay potential, we can think about those little things that die-hard fans will surely miss during their long playing sessions. One of the most important is, of course, a save/load mechanism! This way they can go to sleep and dream about your scary monsters between play sessions.

Tidy initialization

To choose between continuing a previous game or starting a new one we need a main menu. But wait: our initialization logic and game loop are tightly bound, so they’re not really prepared for these tasks. To avoid code duplication, we need to break them down into meaningful blocks (functions). We can then put them together to change between the menu and the game, start new games or load them, and even go to new dungeon levels. It’s much easier than it sounds, so fear not!

Take a look at your initialization and game loop code, after all the functions. I can identify 4 blocks:

  • System initialization (initialising the tcod window and consoles)

  • Setting up a new game (everything else except for the game loop and the FOV map creation)

  • Creating the FOV map

  • Starting the game loop

We can use these as the building blocks to make up the higher-level tasks like loading the game or moving to a new level:

  • Create a new game: set up the game, create FOV map, start game loop (this is what we have now).

  • Load game: load data (we won’t deal with this block just yet), create FOV map, start game loop.

  • Advance level: set up new level (we won’t deal with this yet either), create FOV map (the game loop is already running and will just continue).

Let’s put everything from main except for the system initialisation and the main loop into a new_game function:

fn new_game(tcod: &mut Tcod) (Game, Vec<Object>) {
    // create object representing the player
    let mut player = Object::new(0, 0, '@', "player", WHITE, true);
    player.alive = true;
    player.fighter = Some(Fighter {
        max_hp: 30,
        hp: 30,
        defense: 2,
        power: 5,
        on_death: DeathCallback::Player,  // <1>
    });

    // the list of objects with just the player
    let mut objects = vec![player];

    let mut game = Game {
        // generate map (at this point it's not drawn to the screen)
        map: make_map(&mut objects),
        messages: Messages::new(),
        inventory: vec![],  // <1>
    };

    initialise_fov(tcod, &game.map);

    // a warm welcoming message!
    game.messages.add(
        "Welcome stranger! Prepare to perish in the Tombs of the Ancient Kings.",
        RED,
    );

    (game, objects)
}

We return a tuple with two elements: the vec of Objects and the Game struct.

new_game is calling initialise_fov so we need to create it and move the FOV-related code to it:

fn initialise_fov(tcod: &mut Tcod, map: &Map) {
    // create the FOV map, according to the generated map
    for y in 0..MAP_HEIGHT {
        for x in 0..MAP_WIDTH {
            tcod.fov.set(
                x,
                y,
                !map[x as usize][y as usize].block_sight,
                !map[x as usize][y as usize].blocked,
            );
        }
    }
}

Finally, the game loop and the few bits before it belong to their own function as well:

fn play_game(tcod: &mut Tcod, game: &mut Game, objects: &mut Vec<Object>) {
    // force FOV "recompute" first time through the game loop
    let mut previous_player_position = (-1, -1);

    while !tcod.root.window_closed() {
        // clear the screen of the previous frame
        tcod.con.clear();

        match input::check_for_event(input::MOUSE | input::KEY_PRESS) {
            Some, Event::Mouse(m)  tcod.mouse = m,
            Some, Event::Key(k)  tcod.key = k,
            _  tcod.key = Default::default(),
        }

        // render the screen
        let fov_recompute = previous_player_position != (objects[PLAYER].pos());  // <1>
        render_all(tcod, game, &objects, fov_recompute);

        tcod.root.flush();

        // handle keys and exit game if needed
        previous_player_position = objects[PLAYER].pos();
        let player_action = handle_keys(tcod, game, objects);
        if player_action == PlayerAction::Exit {
            break;
        }

        // let monsters take their turn
        if objects[PLAYER].alive && player_action != PlayerAction::DidntTakeTurn {
            for id in 0..objects.len() {
                if objects[id].ai.is_some() {
                    ai_take_turn(id, tcod, game, objects);
                }
            }
        }
    }
}

And now we just call new_game and play_game from our slimmed-down main function:

fn main() {
    tcod::system::set_fps(LIMIT_FPS);

    let root = Root::initializer()
        .font("arial10x10.png", FontLayout::Tcod)
        .font_type(FontType::Greyscale)
        .size(SCREEN_WIDTH, SCREEN_HEIGHT)
        .title("Rust/libtcod tutorial")
        .init();

    let mut tcod = Tcod {
        root,
        con: Offscreen::new(MAP_WIDTH, MAP_HEIGHT),
        panel: Offscreen::new(SCREEN_WIDTH, PANEL_HEIGHT),
        fov: FovMap::new(MAP_WIDTH, MAP_HEIGHT),
        key: Default::default(),
        mouse: Default::default(),
    };

    let (mut game, mut objects) = new_game(&mut tcod);
    play_game(&mut tcod, &mut game, &mut objects);
}

let (a, b) = some_tuple is how we turn a tuple into its parts. We have to put mut in front of each one so Rust lets us change them later.

You can think of let (mut a, mut b) as two separate bindings: let mut a = …​ and let mut b = …​. Except since new_game returns a tuple, we can’t really have them as separate.

Anyway, the game should compile again and the setup code is more modular. Which will come in handy in the coming sections.

The main menu

To keep our main menu from appearing a bit bland, it would be pretty cool to show a neat background image below it. Fortunately, tcod lets us load and display images!

Since libtcod emulates a console, we can’t directly show arbitrary images, since we can’t access the console’s pixels. We can, however, modify the background color of every console cell to match the color of a pixel from the image. The downside is that the image will be in a very low resolution.

However, libtcod can do a neat trick: by using specialized characters, and modifying both foreground and background colors, we can double the resolution! This is called subcell resolution, and this page of the docs shows some images of the effect (at the end of the page).

This means that, for our 80x50 cells console, we need a 160x100 pixels image. We’ll be using the image from the original Python tutorial.

fn main_menu(tcod: &mut Tcod) {
    let img = tcod::image::Image::from_file("menu_background.png") (1)
        .ok()
        .expect("Background image not found");  (2)

    while !tcod.root.window_closed() {  (3)
        // show the background image, at twice the regular console resolution
        tcod::image::blit_2x(&img, (0, 0), (-1, -1), &mut tcod.root, (0, 0));

        // show options and wait for the player's choice
        let choices = &["Play a new game", "Continue last game", "Quit"];
        let choice = menu("", choices, 24, &mut tcod.root);

        match choice {  (4)
            Some(0) => {
                // new game
                let (mut game, mut objects) = new_game(tcod);
                play_game(tcod, &mut game, &mut objects);
            }
            Some(2) => {
                // quit
                break;
            }
            _ => {}  (5)
        }
    }
}
1 Load the background image
2 Exit if the loading failed
3 Show the main menu in a loop — this lets us play another game after the current one ends
4 Either start a new game or quit
5 If the player selects anything else, keep showing the menu

Now replace the calls to new_game and play_game in main with:

main_menu(&mut tcod);

If you try it out now, you’ll see a nice menu with a dungeon-y backdrop!

Now let’s add the game’s title and some credits. You’ll probably want to modify the values. Put this in the main_menu before calling the menu function:

tcod.root.set_default_foreground(LIGHT_YELLOW);
tcod.root.print_ex(
    SCREEN_WIDTH / 2,
    SCREEN_HEIGHT / 2 - 4,
    BackgroundFlag::None,
    TextAlignment::Center,
    "TOMBS OF THE ANCIENT KINGS",
);
tcod.root.print_ex(
    SCREEN_WIDTH / 2,
    SCREEN_HEIGHT - 2,
    BackgroundFlag::None,
    TextAlignment::Center,
    "By Yours Truly",
);

You’ll notice that the menu rectangle starts with a blank line. That is because the header string is empty, but root.get_height_rect reports its height as 1 by default.

To make the line go away, we need to check that condition in the menu function:

// calculate total height for the header (after auto-wrap) and one line per option
let header_height = if header.is_empty() {
    0
} else {
    root.get_height_rect(0, 0, width, SCREEN_HEIGHT, header)
};
let height = options.len() as i32 + header_height;

Finally, when you start a game, go back to the main menu with Escape and start another game results in a bug! Parts of the first game are still visible in the second game. To fix that, we need to clear the console.

At the end of initialise_fov:

// unexplored areas start black (which is the default background color)
tcod.con.clear();

There it is, a neat main menu, and with only a handful of lines of code!

Saving and loading

Storing a game state to disk (and then reloading it) is not conceptually hard: You could imagine just taking all the data from our game and objects variables and writing them to a file value by value.

It would, however, be a huge hassle that would require a ton of code, you’d need to define a way to structure the data in the file and there’s a good chance you’d get a lot of bugs at first.

Luckily, there are ways of automating most of this that make saving and loading quite painless. Here’s the teaser:

fn save_game(game: &Game, objects: &[Object]) -> Result<(), Box<dyn Error>> {  (1)
    let save_data = serde_json::to_string(&(game, objects))?;  (2)
    let mut file = File::create("savegame")?;  (3)
    file.write_all(save_data.as_bytes())?;  (4)
    Ok(())  (5)
}
1 save game and objects — they contain all our game state. The saving can fail so return a Result which could be Ok or Err
2 convert both objects and game into json
3 create a file called "savegame" — that’s where we’ll write the game state
4 write the json-ified game state to the file
5 if nothing went wrong return Ok, indicating success

Don’t mind the ? operator at the end of the line for now, it’s there for error handling and we’ll explain it in a bit.

The first line (serde_json::to_string(&some_data)) takes the data we want to save (objects and the game state in our case) and turns it to a JSON-encoded String.

That functionality comes from the serde and related crates so we need to add them to our [dependencies] in the Cargo.toml file and we’ll also enable the "serialization" feature in the tcod crate:

[dependencies]
tcod = { version = "0.15", features = ["serialization"] }
rand = "0.3"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"

The serde crate provides the main functionality for serializing and deserializing of Rust data. This includes the Serialize and Deserialize traits which describe what to do for a given struct/enum/whatever. The optional derive feature provides a way to derive those traits so we don’t need to implement them ourselves and finally serde_json converts any serializable struct to and from JSON. If we wanted to use a different format (YAML, TOML or anything else, we’d replace this crate with another one that supports what you want).

Now let’s add those traits our source code, including the save_game function and try to compile:

use std::error::Error;
use std::fs::File;
use std::io::{Read, Write};

use serde::{Deserialize, Serialize};

Unfortunately, the compilation will fail:

   Compiling roguelike-tutorial v0.1.0 (file:///home/thomas/personal/code/roguelike-tutorial)
error[E0277]: the trait bound `Game: serde::Serialize` is not satisfied
    --> src/bin/part-10-menu-saving.rs:1140:28
     |
1140 |     let save_data = serde_json::to_string(&(game, objects))?;
     |                     ^^^^^^^^^^^^^^^^^^^^^ the trait `serde::Serialize` is not implemented for `Game`
     |
     = note: required because of the requirements on the impl of `serde::Serialize` for `&Game`
     = note: required because of the requirements on the impl of `serde::Serialize` for `(&[Object], &Game)`
     = note: required by `serde_json::to_string`

Apparently, we need to implement the Serialize trait. That tells Rust how to encode each bit of data in our structs. We can do it manually, but it would be error-prone and really tedious. Luckily, we can just use #[derive(Serialize)] and have Rust do it for us!

#[derive(Serialize)]  (1)
struct Game {
    map: Map,
    messages: Messages,
    inventory: Vec<Object>,
}
1 The Game struct can be now serialised…​ sort of

If you try to compile it now, you’ll see that the complaint has shifted from Game to Object. We’ll need to derive Serialize for every struct and enum we’ll be saving.

There is a complementary trait called Deserialize which goes the other way: from a serialised representation to a struct.

So let’s add them all at once:

#[derive(Serialize, Deserialize)]
struct Messages {
    ...
}

#[derive(Serialize, Deserialize)]
struct Game {
    ...
}

/// A tile of the map and its properties
#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
struct Tile {
    ...
}

/// This is a generic object: the player, a monster, an item, the stairs...
/// It's always represented by a character on screen.
#[derive(Debug, Serialize, Deserialize)]
struct Object {
    ...
}

// combat-related properties and methods (monster, player, NPC).
#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
struct Fighter {
    ...
}

#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
enum DeathCallback {
    ...
}

#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
enum Ai {
    ...
}

#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
enum Item {
    ...
}

It’s a bit of a bother but not the end of the world. After that, our code should be compiling, though it will warn that save_game is not actually called from anywhere. Let’s fix that!

Traditionally, roguelikes only let you save when you’re quitting the game, so we’ll call save_game in play_game right before the break that ends the game:

if player_action == PlayerAction::Exit {
    save_game(game, objects).unwrap();
    break;
}

If you run the game now and then quit it, you should see a new file called savegame created. You can look inside it — thanks to it being JSON-encoded, it’s actually somewhat readable. It will contain all the objects and the entire game state.

But if we’re not able to do anything with the save file, what use does it have? We need to add a load_game function, too:

fn load_game() -> Result<(Game, Vec<Object>), Box<dyn Error>> {
    let mut json_save_state = String::new();
    let mut file = File::open("savegame")?;
    file.read_to_string(&mut json_save_state)?;
    let result = serde_json::from_str::<(Game, Vec<Object>)>(&json_save_state)?;
    Ok(result)
}

It’s basically just the reverse of save_game: we read the save file contents to a String, then we decode it into our objects Vec and Game struct and if it all succeeds, return the pair.

In main_menu we’ll handle the load game choice now:

match choice {
    Some(0) => {  // new game ... }
    Some(1) => {
        // load game
        let (mut objects, mut game) = load_game().unwrap();
        initialise_fov(tcod, &game.map);
        play_game(&mut objects, &mut game, tcod);
    }
    Some(2) => {  // quit ...}
    _ => {}
}

To make things compile, you’ll have to derive Deserialize to every struct and enum that has the Serialize trait, too.

After that, you should be able to load a previously saved game and continue playing!

But what if there is no game to load? Or if the file gets corrupted? We haven’t talked about error handling much, but now we need to.

The Rust book has a whole chapter on error handling so we’ll do only a tiny introduction. You should read that chapter.

Both save_game and load_game return a Result value. Result is similar to Option in that it has two possibilities it can return — one that usually indicates a success and the other failure. But in Result’s case, the failure can have associated data as well. The successful variant is called `Ok and the failure is Err.

There’s also the Error trait which represents an error and lets you get its textual description. All the file-handling serialisation errors in our save/load code implement Error.

So, looking at save_game, the serde_json::to_string call returns either Ok(String) with the encoded value or Err(serde_json::error::Error) on failure. File::create and the write_all method work similarly although with different success and error types.

Since we can return more then one error type, we return Box<Error> instead. That lets us return any type that implements Error and the caller can get at the description and the raw error if they want.

The {?}[? operator] is a convenient way of saying "If this failed, return from the function with error immediately, otherwise give me the success value". The operator also does an extra conversion to the error type specified in the functions return value.

So let mut file = File::create("savegame")?; is almost equivalent to

fn save_game(game: &Game, objects: &[Object]) -> Result<(), Box<dyn Error>> {
  ...
  let mut file = match File::create("savegame") {
      Ok(f) => f,
      Err(e) => return Err(e)
  };
  ...
}

Only difference is that the ? operator also does the conversion of the error to whatever Error type the calling function asks for. In our case, since we’re using Box<Error>, no conversion will actually be done.

So, that explains the save/load game functions. But what about using their results?

If you tried to compile the game, you’ve seen this warning:

cargo build
   Compiling roguelike-tutorial v0.1.0 (file:///home/thomas/code/roguelike-tutorial)
src/bin/part-10-menu-saving.rs:1123:13: 1123:38 warning: unused result which must be used, #[warn(unused_must_use)] on by default
src/bin/part-10-menu-saving.rs:1123             save_game(objects, game);
                                                ^~~~~~~~~~~~~~~~~~~~~~~~~

It did not cause an error but Rust is being passive-aggressive about how we’re calling a function that can fail and then ignoring it.

So, we can keep ignoring it, try to actually handle the error or simply crash by calling unwrap :-)

Unwrap will make Rust happy that we’ve processed the Result, but it will simply abort the program whenever we get Err back.

Replace the call to save_game with save_game(objects, game).unwrap().

You have to realise however, that if this ever happens, it will make your users really unhappy. If save_game fails for any reason, your game will just quit without any warning and the player will lose their progress. You should always try to handle errors gracefully.

Let’s do that when we load the game. Calling load_game right now uses unwrap as well, so if there is no game to load or something similar, we’ll just quit the game.

We could just print a message and let the player start a new game instead:

Some(1) => {
    // load game
    match load_game() {
        Ok((mut game, mut objects)) => {
            initialise_fov(tcod, &game.map);
            play_game(tcod, &mut game, &mut objects);
        }
        Err(_e) => {
            msgbox("\nNo saved game to load.\n", 24, &mut tcod.root);
            continue;
        }
    }
}

And we’ll add a function to display messages that relies on menu to do all the heavy lifting:

fn msgbox(text: &str, width: i32, root: &mut Root) {
    let options: &[&str] = &[];
    menu(text, options, width, root);
}

And that’s it! The actual saving and loading code was quite small, but we had to learn a ton of new stuff to understand it.

Continue to the next part.