How to Make a Text Adventure game in Rust - III - Locations
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 beginning. This 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 |
3 - Locations
The Location Struct
pub struct Location {
pub name: String,
pub description: String,
}
Explanation
1 - Define the Location
struct.
2 -
name
: the name of the location. The parser will use this to
know if a player is navigating to a location.
3 -
description
: a description of the location. This is where we'll
put narrative text that describes a location and brings it to life.
Location
structs. We can use a Rust Vec()
for
that. (Note: A Hashmap might also be a good choice here. I've opted for a
Vec()
because it feels more natural to me.) We also need a place to store the
location of the player. This place will hold an index into the Vec()
that lets
us know where the player is at any time. Since these two variables work together
and will need to be accessed together, we'll put them in a shared struct World
.
pub struct World {
pub player_location: usize,
pub locations: Vec<Location>,
}
Explanation
1 - Define the
World
struct.
2 -
player_location
: the index in the locations Vec()
of the player's
current location.
3 - locations
: a Vec()
of location structs that describe each location. Note that the Vec()
is templatized for Location
structs.
Next we can create some locations. The pattern in Rust for initialization is
to define a new()
function implementation for a structure. The sample below
shows the implementation for new()
on the World
struct.
impl World {
pub fn new() -> Self {
World {
player_location: 0,
locations: vec![
Location {
name: "Bridge".to_string(),
description: "the bridge".to_string(),
},
Location {
name: "Galley".to_string(),
description: "the galley".to_string(),
},
Location {
name: "Cryochamber".to_string(),
description: "the cryochamber".to_string(),
},
],
}
}
// ....
}
Explanation
1 - Implementation for the World
struct.
2-20 -
The new()
function.
4 - Initialize player_location
to the first
location.
5 - Initialize the locations Vec()
with 3 locations
6-9,
10-13, 14-17 - The three locations, each containing a name and description.
We can use the descriptions in a location with a println!()
macro. This
statement:
println!("You are in {}.", world.locations[0].description)
"You are in the bridge."
Note, in much of the game code below we use the format!()
macro instead of println!().
format!()
returns output to a string rather than directly to the screen.
Implementing Locations
update_state()
function.
Previously, the update_state()
function was a stand alone function. For the
change here, we move update_state()
to an associated function (i.e. impl
) on the World
struct.
Secondly, we modify the arms of the contained match to process the commands.
impl World {
// ... fn new()
pub fn update_state(&mut self, command: &Command) -> String {
match command {
Command::Look(noun) => self.do_look(noun),
Command::Go(noun) => self.do_go(noun),
Command::Quit => format!("Quitting.\nThank you for playing!"),
Command::Unknown(input_str) => format!("I don't know how to '{}'.", input_str),
}
}
// ...
}
// Deleted the earlier implementation of update_state()
Explanation
4-11 - The modified update_state()
function.
6 - Look
arm - Matches on Command::Look
, destructures the noun
, and calls do_look()
.
7
- Go
arm - Matches on Command::Go,
destructures the noun
, and calls
do_go()
.
16 - The prior implementation of update_state()
is deleted
as it is no longer needed.
match
in update_state()
calls two new functions - do_look()
and do_go()
.
These functions need access to player_location
and the locations Vec()
, so they
are also implemented as associated functions to the World
struct. do_look()
is implemented as a simple
match
on the passed noun
that checks if the value is "around"
or ""
(in which
case we assume the player meant "around"
). All other nouns are not understood.do_go()
is a bit more complicated. Its implementation
loops through each of the locations to see if the noun
is the name of an
existing location. If it is, it moves the player there and calls do_look()
. Otherwise, do_go()
returns the string "I don't understand where you want
to go."
impl World {
// ... fn new()
// ... fn update_state()
pub fn do_look(&self, noun: &String) -> String {
match noun.as_str() {
"around" | "" => format!(
"{}\nYou are in {}.\n",
self.locations[self.player_location].name,
self.locations[self.player_location].description
),
_ => format!("I don't understand what you want to see.\n"),
}
}
pub fn do_go(&mut self, noun: &String) -> String {
let mut output = String::new();
for (pos, location) in self.locations.iter().enumerate() {
if *noun == location.name.to_lowercase() {
if pos == self.player_location {
output = output + &format!("Wherever you go, there you are.\n");
} else {
self.player_location = pos;
output = output + &format!("OK.\n\n") + &self.do_look(&"around".to_string());
}
break;
}
}
if output.len() == 0 {
format!("I don't understand where you want to go.")
} else {
output
}
}
}
Explanation
5-14 - The do_look()
function.
6 - match
on the
noun
7 - "around"
or "" noun
values
7-11 - Display the name
and
description
of the location
12 - Display 'I don't understand' message
for all other noun
values
16-36 - The do_go()
function.
17 -
Define output
string that will hold printed messages to send to the
screen.
19-29 - Loop over each Location
in location
and if the location
matches the noun
, either move the player to the new location and do a
'look,' or print a message that says 'you're already there.' Break out of
the loop if we've found the correct location.
31-35 - Handle the case
where the location wasn't found. Either way return a string.
World
struct to our
main
function so we can use it in the game.
pub mod rlib;
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 world = rlib::World::new();
let mut output: String;
//
// Main Loop
//
loop {
command = rlib::get_input();
output = world.update_state(&command);
rlib::update_screen(output);
if matches!(command, rlib::Command::Quit) {
break;
}
}
//
// Shutdown and Exit
//
println!("Bye!");
}
Explanation
14 - Create a new mutable instance of the World
struct and
call new()
to initialize the struct.
22 - Call update_state()
as an
implementation function on World
. This passes the world
variable as
&self
in the update_state()
function.
Progress
Adding these changes changes interaction with our game as shown in the output sample below. Players can now move around using 'go <location>' and look at different parts of the environment using 'look around'. Even this simple change raises the level of interest in the game substantially because it allows players to explore the world.
Welcome to Reentry. A space adventure.
You awake in darkness with a pounding headache.
An alarm is flashing and beeping loudly. This doesn't help your headache.
> look around
Bridge
You are in the bridge.
> go galley
OK.
Galley
You are in the galley.
> go cryochamber
OK.
Cryochamber
You are in the cryochamber.
> look
Cryochamber
You are in the cryochamber.
> quit
Quitting.
Thank you for playing!
Bye!
The example here shows very simple descriptions but a good writer (which I am not) could use just this feature alone to create a compelling story that players 'discover' by navigating around and learning about their environment as they go.
Feel free to pull down the code and experiment. Can you add more locations? Better descriptions? Tell a story even?
In the next post, we'll add things for the player to interact with. Knives, space suits, and aliens await.
Is this: "3 - locations: a description of the location. This is where we'll put narrative text that describes a location and brings it to life." intentional? it looks like should be "list of location with name/description"
ReplyDeleteAbsolutely right. Corrected. Thank you!
Delete