How to make a Text Adventure game in Rust - II.1 - Commands

In this project, we're making a fully functional text adventure game from scratch. The post assumes you've read the earlier posts in the series. To get caught up jump to the first post and start at the beginningThis project is heavily based on the work of Ruud Helderman and closely follows the content and structure of his excellent tutorial series on the same topic using the C language.

This content and all code is licensed under the MIT License.

Photo by Frank Cone

2.1 - Better Command Processing

This brief post will be a bit of a diversion from looking at locations. We'll come back to locations in the next post in the series.

In our last post, we added the main loop and a simple command processing capability. The approach for command processing was based on string matching and felt like a missed opportunity to take advantage of the strong typing that Rust provides. It also was easily confused and was not able to deal with even the simplest mixed case input (i.e 'go north' would parse correctly, but 'Go North' would not).

In this post we'll modify the previous implementation to switch out the verb String representation of Command with an enum based approach for Command that should be more flexible going forward. While we're at it we'll modify the parser to deal with mixed case input from the player.

The first change we need to make is to convert the Command struct to an enum. Lets have a look there first.
pub enum Command {
    Look(String),
    Go(String),
    Quit,
    Unknown(String),
}

pub fn parse(input_str: String) -> Command {
    let lc_input_str = input_str.to_lowercase();
    let mut split_input_iter = lc_input_str.trim().split_whitespace();

    let verb = split_input_iter.next().unwrap_or_default().to_string();
    let noun = split_input_iter.next().unwrap_or_default().to_string();

    match verb.as_str() {
        "look" => Command::Look(noun),
        "go" => Command::Go(noun),
        "quit" => Command::Quit,
        _ => Command::Unknown(input_str.trim().to_string()),
    }
}

Explanation
1 - Define Command. Note that Command is an enum now and not a struct.
2-5 - Enum values for 'Look', 'Go', 'Quit', and Unknown' command actions. The enum's values include an associated String to hold additional parsed input.
8-21 - Note that new() is completely removed, and parse() is no longer an implementation function on Command.
8 - The modified parse() function. The new version accepts a String input and returns a Command.
9 - Creates a lower-case version of the input string. The rest of the function operates on the lower-case value and enables parsing to work with mixed case input. Note that as a result of this approach, input is now completely case insensitive, so if we need that we'll need to revisit this line.
10 - 13 - Unchanged from the earlier implementation.
15 - 20 - Added match statement that processes the extracted verb strings. Because the user enters strings as input, we can't get away from verb strings completely, but we can isolate them in this function. Here we show how we abstracted the verb strings into enum values. In a future step, we might abstract the noun clauses as well.

With Command adjusted, we need to modify all the code locations that depend on the previous implementation. get_input() and update_state() need modifications.
pub fn get_input() -> Command {
    // Prompt
    println!("");
    print!("> ");
    io::stdout().flush().unwrap();

    let mut input_str = String::new();

    io::stdin()
        .read_line(&mut input_str)
        .expect("Failed to read move");
    println!("");

    // Parse & Return
    parse(input_str)
}

pub fn update_state(command: &Command) -> String {
    let output: String;

    match command {
        Command::Look(_) => {
            output = format!("It is very dark, you can see nothing but the flashing light.")
        }
        Command::Go(_) => output = format!("It is too dark to move."),
        Command::Quit => output = format!("Quitting.\nThank you for playing!"),
        Command::Unknown(input_str) => output = format!("I don't know how to '{}'.", input_str),
    }

    // Return
    output
}

Explanation
1-16 Updated version of get_input(). All lines are the same with the exception of the call to parse() near the end.
15 - There are two simplifications in the updated version. First is that we don't need to create a Command struct first. The updated version of parse() creates and returns the Command enum. The second change is that since the function returns the Command object directly, we don't need the separate return line that the prior version had. So, get_input() is shorter and simpler with the updated implementation.
18-31 - Updated version of update_state(). The change in the modified implementation is in the match statement.
22-27 - Modified match statement with arms described for each of the Command enum values.

Finally the last function to update is main().
fn main() {
    //
    // Introduction and Setup
    //
    println!("Welcome to Reentry. A space adventure.");
    println!("");
    println!("You awake in darkness with a pounding headache.");
    println!("An alarm is flashing and beeping loudly. This doesn't help your headache.");
    println!("");

    let mut command: rlib::Command;
    let mut output: String;

    //
    // Main Loop
    //
    loop {
        command = rlib::get_input();
        output = rlib::update_state(&command);
        rlib::update_screen(output);

        if matches!(command, rlib::Command::Quit) {
            break;
        }
    }

    //
    // Shutdown and Exit
    //
    println!("Bye!");
}

Explanation
11 - Modified declaration for command that pre-declares the variable name. New Command enum values will be created by the call to get_input() on line 18.
17-25 - Modified main loop. This implementation will loop forever until break is called on line 23.
18 - Call to get_input(). No change from the prior implementation, but this line returns a new Command each loop.
22-24 - New addition to the main loop to exit when Command::Quit is parsed.

Progress

The changes here were simple, but provide a better foundation for future growth of our command set. Gameplay hasn't changed as a result of these updates, but we've simplified command processing by switching to the use of an enum and isolating string processing for verbs into the parse() function.

We've made a lot of progress, but there is much more to add. Currently the player is limited to just one location and prevents the player from actually moving by saying it is too dark. In the next post, we'll come back to our plan and add locations.

⇂  View Source

Enjoyed this post? Help me create more with a coffee. Never miss out on future posts by following me.

Comments

Popular posts from this blog

How to make a Text Adventure game in Rust - Introduction