How to make a Text Adventure game in Rust - IV - Objects
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 |
4 - Objects
- A pen must be used to write a note to the copilot,
- A sword must be used to kill a thief or scare it off, or
- A switch must be thrown in the bridge of the capsule.
What we could do...
- name: the name of the object.
- description: the description of the object.
- location: where the item/actor is located. Stored as an index into the locations vec!().
pub struct Object {
pub name: String,
pub description: String,
pub location: usize,
}
// ...
objects: vec![
Object {
name: "Photo".to_string(),
description: "a photo of a family. They look familiar".to_string(),
location: 0,
},
Object {
name: "Copilot".to_string(),
description: "your copilot. You've known him for years".to_string(),
location: 1,
},
]
// ...
Note how similar this structure is to the Location struct we created last time. They're so similar that we could merge them and treat everything as an object.pub struct Object {
pub name: String,
pub description: String,
pub location: Option<usize>,
}
// ...
objects: vec![
Object {
name: "Bridge".to_string(),
description: "the bridge".to_string(),
location: None,
},
Object {
name: "Galley".to_string(),
description: "the galley".to_string(),
location: None,
},
Object {
name: "Cryochamber".to_string(),
description: "the cryochamber".to_string(),
location: None,
},
Object {
name: "Photo".to_string(),
description: "a photo of a family. They look familiar".to_string(),
location: Some(0),
},
Object {
name: "Copilot".to_string(),
description: "your copilot. You've known him for years".to_string(),
location: Some(3),
},
Object {
name: "Pen".to_string(),
description: "a pen".to_string(),
location: Some(5),
},
]
// ...
const LOC_BRIDGE: usize = 0;
const LOC_GALLEY: usize = 1;
const LOC_PHOTO: usize = 2;
const LOC_CRYOCHAMBER: usize = 3;
const LOC_PHOTO: usize = 4;
const LOC_COPILOT: usize = 5;
const LOC_PEN: usize = 6;
// Prints "You are in the galley."
println!("You are in {}.",objects[LOC_GALLEY].description);
// Prints all items and actors present in the 'bridge'
for obj in objects {
match obj.location {
Some(obj_location) if obj_location == LOC_BRIDGE =>
println!("{}\n", obj.description);
_ => ();
}
}
But should we?
- How might we represent an item that is also a location? (e.g. a space ship that moves from place to place.)
- How might we represent a location or an item that is also an actor? (e.g. an alien that you could both talk to and pick up.)
- How might we represent items that contain other items? (e.g. a treasure chest that contains gold)
- How might we represent items that contain other actors or actors that hold items? (e.g. a thief that holds a chest (which also holds gold))
- It probably has a location, can be picked up, moved, etc.
- But it might also be a location that a Genie lives in, or that you could go into.
- What if the lamp is animated? Could you talk to it as an actor? Can it DO things like an actor would?
- Locations will (eventually) be connected by 'passages'. Objects with passages that enter or leave are locations. A player can go to them by following a passage.
- Items have a location and can (possibly) picked up and moved. We can use a 'weight' property to control what a player can pick up or not.
- Actors can be interacted with, talked to, fought, etc. Actors may have their own behavior that causes them to move. Actors could even be other players in another process. We could use a 'health' property to determine if they are alive.
See me, touch me, feel me, heal me...
// Expression to move the player to the bridge
objects[LOC_PLAYER].location = Some(LOC_BRIDGE);
// Expression to return a description of the current location
objects[objects[LOC_PLAYER].location.unwrap()].description;
Lets do this
pub struct Object {
pub name: String,
pub description: String,
pub location: Option<usize>,
}
const LOC_BRIDGE: usize = 0;
const LOC_GALLEY: usize = 1;
const LOC_CRYOCHAMBER: usize = 2;
const LOC_PLAYER: usize = 3;
const LOC_PHOTO: usize = 4;
const LOC_CRYOSUIT: usize = 5;
const LOC_COPILOT: usize = 6;
const LOC_PEN: usize = 7;
pub struct World {
pub objects: Vec<Object>,
}
Explanation
1 - Define the
Object
struct. Each in game item, actor and location is represented as an object.
2 -
name
: the short name of the object
3 - description
: a description of the
object. This is where we'll put narrative text that describes an object and brings it to life.
4 - location: the index into the objects Vec()
of the object's current location.
7-13 - LOC_<object> - consts that represent indices of specific objects in the objects Vec().
16 - objects: contains the object structs that represent game elements. Note that the Vec()
is specialized to contain Object
structs. The objects Vec() replaces the previous locations Vec() in the World struct.
impl World {
pub fn new() -> Self {
World {
objects: vec![
// Foo
Object {
name: "Bridge".to_string(),
description: "the bridge".to_string(),
location: None,
},
Object {
name: "Galley".to_string(),
description: "the galley".to_string(),
location: None,
},
Object {
name: "Cryochamber".to_string(),
description: "the cryochamber".to_string(),
location: None,
},
Object {
name: "Yourself".to_string(),
description: "yourself".to_string(),
location: Some(LOC_BRIDGE),
},
Object {
name: "Photo".to_string(),
description: "a photo of a family. They look familiar".to_string(),
location: Some(LOC_BRIDGE),
},
Object {
name: "Cryosuit".to_string(),
description: "a silver suit that will protect you in cryosleep".to_string(),
location: Some(LOC_CRYOCHAMBER),
},
Object {
name: "Copilot".to_string(),
description: "your copilot sleeping in his cryochamber".to_string(),
location: Some(LOC_CRYOCHAMBER),
},
Object {
name: "Pen".to_string(),
description: "a pen".to_string(),
location: Some(LOC_CRYOSUIT),
},
],
}
}
// ...
}
Explanation
1 - Define the
World
struct implementation.
4 - Define the objects Vec(). Each in game item, actor and location is represented as an item in this Vec().
6-45 - Objects that define the game world. The list here mirrors the structure of the image above.
Can you see me?
impl World {
// ...
fn object_has_name(&self, object: &Object, noun: &String) -> bool {
*noun == object.name.to_lowercase()
}
fn get_object_index(&self, noun: &String) -> Option<usize> {
let mut result: Option<usize> = None;
for (pos, object) in self.objects.iter().enumerate() {
if self.object_has_name(&object, noun) {
result = Some(pos);
break;
}
}
result
}
// ...
}
Before looking at the implementation, it will be helpful to consider what it means to be visible. That will drive the implementation. We can say that an object is visible to the player if any of the following conditions are true:
- Is the object the player himself? Players can always see themselves.
- Is the object the location where the player is? (Remember locations are objects too)
- Is the object being 'held' by the player?
- Is the object at the same location as the player?
- Is the object contained by an object the player is holding?
- Is the object contained by an object in the same location as the player?
- Is this object any location?
For the implementation, we need to know where the player is, where the object in question is, and then check all of the above conditions. Sounds simple enough, lets look at the implementation.
impl World {
// ...
fn get_visible(&self, message: &str, noun: &String) -> (String, Option<usize>) {
let mut output = String::new();
let obj_index = self.get_object_index(noun);
let obj_loc = obj_index.and_then(|a| self.objects[a].location);
let obj_container_loc = obj_index
.and_then(|a| self.objects[a].location)
.and_then(|b| self.objects[b].location);
let player_loc = self.objects[LOC_PLAYER].location;
match (obj_index, obj_loc, obj_container_loc, player_loc) {
// Is this even an object? If not, print a message
(None, _, _, _) => {
output = format!("I don't understand {}.\n", message);
(output, None)
}
//
// For all the below cases, we've found an object, but should the player know that?
//
// Is this object the player?
(Some(obj_index), _, _, _) if obj_index == LOC_PLAYER => (output, Some(obj_index)),
//
// Is this object the location where the player is?
(Some(obj_index), _, _, Some(player_loc)) if obj_index == player_loc => {
(output, Some(obj_index))
}
//
// Is this object being held by the player (i.e. 'in' the player)?
(Some(obj_index), Some(obj_loc), _, _) if obj_loc == LOC_PLAYER => {
(output, Some(obj_index))
}
//
// Is this object at the same location as the player?
(Some(obj_index), Some(obj_loc), _, Some(player_loc)) if obj_loc == player_loc => {
(output, Some(obj_index))
}
//
// Is this object any location?
(Some(obj_index), obj_loc, _, _) if obj_loc == None => (output, Some(obj_index)),
//
// Is this object contained by any object held by the player
(Some(obj_index), Some(_), Some(obj_container_loc), _)
if obj_container_loc == LOC_PLAYER =>
{
(output, Some(obj_index))
}
//
// Is this object contained by any object at the player's location?
(Some(obj_index), Some(_), Some(obj_container_loc), Some(player_loc))
if obj_container_loc == player_loc =>
{
(output, Some(obj_index))
}
//
// If none of the above, then we don't know what the noun is.
_ => {
output = format!("You don't see any '{}' here.\n", noun);
(output, None)
}
}
}
}
Explanation
1 - get_visible() is defined as an associated function for the World struct.
4 - get_visible() takes a reference to self, a message (to be displayed if no matching object is found and a noun that the player entered in their command. get_visible() returns a tuple containing an output string and Option indicating the index of the visible object if any object is visible, or None if no object is visible.
5 - output - a String that holds any messages to be sent to the display.
7 - obj_index - an Option that holds the index of the object (i.e. the noun) in the objects Vec().
8 - obj_loc - an Option that holds the index of the location of the object (i.e. the noun) in the objects Vec(). Note the use of and_then(). and_then() evaluates an Option and if the Option is None, it returns None. If it is Some, it executes the closure and returns that result. We use and_then() to accommodate the fact that obj_index could be None.
9 - obj_container_loc - an Option that holds the index of the object that contains the object (i.e. the noun).
10 - obj_index - an Option that holds the index of the object in the objects Vec().
11 - player_loc - an Option that holds the index of the player's location.
14 - match on all four of the above Options.
15-19 - The 'not an object' case. If obj_index is None, then we didn't find an object, return a message string and None.
23-24 - If obj_index is the same as the index of the player object, we've found the player object. The object is visible, return a message string and Some(obj_index).
26-29 - If obj_index is the same as player_loc, our object is the location where the player is. The object is visible, return a message string and Some(obj_index).
31-34 - If obj_loc is the same as player's index, then the object is being held by our player. The object is visible, return a message string and Some(obj_index).
36-39 - If obj_loc is the same as player_loc, then the object is in the same location as the player. The object is visible, return a message string and Some(obj_index).
41-42 - If obj_loc is None, then the object isn't located anywhere. It must be a location and locations are always visible. Return a message string and Some(obj_index).
44-49 - If obj_container_loc is the same as player's index, then the player is holding an object that contains the object. The object is visible, return a message string and Some(obj_index).
51-56 - If obj_container_loc is the same as player_loc, then the object is in a container at the player's location. The object is visible, return a message string and Some(obj_index).
58-62 - Finally, if none of the above cases are true, then the object is NOT visible, return a message string and None.
60 - Note that the message here prints out the noun, and not the object description. This is to prevent the game from giving away the fact that a given object exists. Consider a scenario where the player makes a guess and types "get gold" without knowing that a gold coin exists in the game. If the game responds with "You don't see a gold coin here." it is giving away the fact that there is indeed a gold coin in the game. Replying with just the noun that the player used (i.e. "You don't see any gold here.") doesn't give away any more than the player's own guess.
What do we have here?
impl World {
// ...
pub fn list_objects_at_location(&self, location: usize) -> (String, i32) {
let mut output = String::new();
let mut count: i32 = 0;
for (pos, object) in self.objects.iter().enumerate() {
match (pos, object.location) {
(pos, _) if pos == LOC_PLAYER => continue,
(_, None) => continue,
(_, Some(obj_location)) if obj_location == location => {
if count == 0 {
output = output + &format!("You see:\n");
}
count += 1;
output = output + &format!("{}\n", object.description);
}
_ => continue,
}
}
(output, count)
}
// ...
}
Explanation
7 - Loop through every object
8 - match on the location of each object so we can include in the list if we should.
9 - Exclude the player from the list. It is safe to assume they know that they're in the location.
10 - For objects with a location, include them if the location is this one.
12-14 - Include "You see:" with the first object so that the list is formatted nicely.
21 - Return the output message and the number of objects that are visible.
pub struct World {
pub objects: Vec<Object>,
}
impl World {
// ...
pub fn do_look(&self, noun: &String) -> String {
match noun.as_str() {
"around" | "" => {
let (list_string, _) =
self.list_objects_at_location(self.objects[LOC_PLAYER].location.unwrap());
format!(
"{}\nYou are in {}.\n",
self.objects[self.objects[LOC_PLAYER].location.unwrap()].name,
self.objects[self.objects[LOC_PLAYER].location.unwrap()].description
) + list_string.as_str()
}
_ => format!("I don't understand what you want to see.\n"),
}
}
pub fn do_go(&mut self, noun: &String) -> String {
let (output_vis, obj_opt) = self.get_visible("where you want to go", noun);
let player_loc = self.objects[LOC_PLAYER].location;
match (obj_opt, player_loc) {
(None, _) => output_vis,
(Some(obj_loc), Some(player_loc)) if obj_loc == player_loc => {
format!("Wherever you go, there you are.\n")
}
(Some(obj_loc), _) => {
self.objects[LOC_PLAYER].location = Some(obj_loc);
format!("OK.\n\n") + &self.do_look(&"around".to_string())
}
}
}
}
Explanation
11-12 - Call list_objects_at_location() to show the list of objects at the location. Note the function call returns a tuple that contains the list for display and a count of objects. We don't care about the count yet.
13-17 - Call format!() macro to create an output string that includes the location and visible objects.
24 - Call get_visible() to check if the noun is visible.
26-36 - match on the returned obj_opt (an Option()) to determine the correct action
27 - The object isn't visible. Just return the string that get_visible() sent back.
28 - The noun is visible in some way the internal if statements decide what to do.
29-30 - Is the visible thing the player? Players can't enter themselves, just let them stay where they are.
31-33 - It isn't the player, so move the player there. Set the location of the player object to the current location.
Progress
With these changes, the player can interact with the game in a whole new way. Locations have objects that can be seen as the player moves around as shown below. BUT players can't really interact with the objects at all. They can't be picked up, moved, dropped, etc. For objects to have meaning in the game as more than just baubles, the player must be able to interact with them.
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.
You see:
a photo of a family. They look familiar
> go galley
OK.
Galley
You are in the galley.
> go cryochamber
OK.
Cryochamber
You are in the cryochamber.
You see:
a silver suit that will protect you in cryosleep
your copilot sleeping in his cryochamber
> go cryosuit
OK.
Cryosuit
You are in a silver suit that will protect you in cryosleep.
> go bridge
OK.
Bridge
You are in the bridge.
You see:
a photo of a family. They look familiar
> quit
Quitting.
Thank you for playing!
Bye!
Once again, feel free to pull down the code and experiment. Add some objects. Do they add to your story?
Next post will be about allowing players to interact with objects, carry them around, and maybe even give things to other actor objects.
Comments
Post a Comment