Learn how to create an interactive text adventure with object-oriented design in ShitRust.
In this tutorial, we'll build a text adventure game called "The Enchanted Forest Adventure". This project will demonstrate several advanced ShitRust features:
Our game will feature:
By the end of this tutorial, you'll have a complete text adventure game and understand how to expand it with your own ideas.
This tutorial assumes you have completed the Calculator and Todo List tutorials or have equivalent experience with ShitRust.
Before diving into coding, let's outline the structure of our game:
Our adventure will take place in an enchanted forest with the following locations:
Let's start implementing these components one by one.
First, let's set up our imports and define the Item struct:
// Import required modules
import { stdin, stdout } from "std/io";
import { Vector, HashMap } from "std/collections";
import { Random } from "std/math";
// Define Item struct
struct Item {
name: string,
description: string,
can_be_taken: bool,
// Constructor for creating a new item
static fn new(name: string, description: string, can_be_taken: bool) -> Item {
return Item {
name: name,
description: description,
can_be_taken: can_be_taken
};
}
// Display the item description
fn examine() -> void {
println(self.description);
}
}
The Item struct has three fields:
name: The name of the item (e.g., "key", "book")description: A detailed description of the itemcan_be_taken: A boolean indicating if the player can pick up the itemWe've also defined two methods:
new: A static method (constructor) to create a new itemexamine: A method to display the item's descriptionNext, let's define the Room struct:
// Define Room struct
struct Room {
id: string,
name: string,
description: string,
exits: HashMap, // direction -> room_id
items: Vector- ,
// Constructor for creating a new room
static fn new(id: string, name: string, description: string) -> Room {
return Room {
id: id,
name: name,
description: description,
exits: HashMap
::new(),
items: Vector- ::new()
};
}
// Add an exit to the room
fn add_exit(direction: string, room_id: string) -> void {
self.exits.insert(direction, room_id);
}
// Add an item to the room
fn add_item(item: Item) -> void {
self.items.push(item);
}
// Remove an item from the room
fn remove_item(item_name: string) -> result
- {
for i in 0..self.items.length() {
if self.items[i].name.to_lowercase() == item_name.to_lowercase() {
let item = self.items[i];
if !item.can_be_taken {
return err("You can't take the " + item_name + ".");
}
self.items.remove(i);
return ok(item);
}
}
return err("There is no " + item_name + " here.");
}
// Check if a room has an exit in a given direction
fn has_exit(direction: string) -> bool {
return self.exits.contains_key(direction);
}
// Get the ID of the room in a given direction
fn get_exit(direction: string) -> string {
return self.exits[direction];
}
// Display the room description and available exits
fn describe() -> void {
println(self.name);
println(self.description);
// List items in the room
if self.items.length() > 0 {
println("\nYou can see:");
for item in self.items {
println("- " + item.name);
}
}
// List available exits
println("\nExits:");
let directions = self.exits.keys();
for direction in directions {
println("- " + direction);
}
}
}
The Room struct has five fields:
id: A unique identifier for the room (e.g., "cave", "entrance")name: The display name of the roomdescription: A detailed description of the roomexits: A HashMap mapping directions to room IDsitems: A Vector of items in the roomThe Room struct has several methods for managing exits, items, and displaying information.
Now, let's define the Player struct:
// Define Player struct
struct Player {
current_room_id: string,
inventory: Vector- ,
health: int,
// Constructor for creating a new player
static fn new(starting_room_id: string) -> Player {
return Player {
current_room_id: starting_room_id,
inventory: Vector
- ::new(),
health: 100
};
}
// Add an item to the player's inventory
fn add_item(item: Item) -> void {
self.inventory.push(item);
}
// Remove an item from the player's inventory
fn remove_item(item_name: string) -> result
- {
for i in 0..self.inventory.length() {
if self.inventory[i].name.to_lowercase() == item_name.to_lowercase() {
let item = self.inventory[i];
self.inventory.remove(i);
return ok(item);
}
}
return err("You don't have a " + item_name + ".");
}
// Check if the player has a specific item
fn has_item(item_name: string) -> bool {
for item in self.inventory {
if item.name.to_lowercase() == item_name.to_lowercase() {
return true;
}
}
return false;
}
// Display the player's inventory
fn show_inventory() -> void {
if self.inventory.length() == 0 {
println("Your inventory is empty.");
return;
}
println("Inventory:");
for item in self.inventory {
println("- " + item.name);
}
}
}
The Player struct has three fields:
current_room_id: The ID of the room the player is currently ininventory: A Vector of items the player is carryinghealth: The player's health pointsThe Player struct has methods for managing inventory and checking player status.
Finally, let's implement the Game struct, which ties everything together:
// Define Game struct
struct Game {
rooms: HashMap,
player: Player,
running: bool,
random: Random,
// Constructor for creating a new game
static fn new() -> Game {
let game = Game {
rooms: HashMap::new(),
player: Player::new("entrance"), // Start in the entrance
running: true,
random: Random::new()
};
// Initialize the game world
game.setup_world();
return game;
}
// Setup the game world with rooms and items
fn setup_world() -> void {
// Create rooms
let entrance = Room::new("entrance", "Forest Entrance",
"You stand at the entrance to a dark, mysterious forest. " +
"The trees loom overhead, their branches swaying in the gentle breeze.");
entrance.add_exit("north", "clearing");
entrance.add_exit("east", "path");
entrance.add_item(Item::new("sign", "A wooden sign that reads: 'Beware of the forest creatures!'", false));
let clearing = Room::new("clearing", "Forest Clearing",
"A small clearing in the forest. Sunlight filters through the canopy above. " +
"There's a strange stone pedestal in the center.");
clearing.add_exit("south", "entrance");
clearing.add_exit("west", "cave");
clearing.add_item(Item::new("flowers", "Beautiful wildflowers growing in patches.", true));
clearing.add_item(Item::new("pedestal", "A stone pedestal with a slot that seems to fit something shiny.", false));
// Add more rooms here...
// Add rooms to the game
self.rooms.insert("entrance", entrance);
self.rooms.insert("clearing", clearing);
// Add more room insertions here...
}
The Game struct contains:
The setup_world method creates all the rooms, adds exits and items, and builds the game world.
Next, we need to implement methods for handling player commands:
// Get the current room
fn current_room() -> Room {
return self.rooms[self.player.current_room_id];
}
// Process a player command
fn process_command(command: string) -> void {
let parts = command.to_lowercase().trim().split(" ");
let action = parts[0];
match action {
"go" | "move" | "walk" => {
if parts.length() < 2 {
println("Go where?");
return;
}
let direction = parts[1];
self.move_player(direction);
},
"look" => {
self.current_room().describe();
},
"examine" | "inspect" => {
if parts.length() < 2 {
println("Examine what?");
return;
}
let item_name = parts[1];
self.examine_item(item_name);
},
// More commands here...
"help" => {
self.show_help();
},
"quit" | "exit" => {
println("Are you sure you want to quit? (y/n)");
let answer = stdin.read_line().trim().to_lowercase();
if answer == "y" || answer == "yes" {
self.running = false;
}
},
_ => {
println("I don't understand that command. Type 'help' for a list of commands.");
}
}
}
The process_command method parses player input and calls the appropriate game methods based on the command.
Finally, we need a method to start the game:
// Start the game
fn start() -> void {
println("=== The Enchanted Forest Adventure ===");
println("Type 'help' for a list of commands.\n");
// Show the initial room description
self.current_room().describe();
// Main game loop
while self.running {
print("\n> ");
let command = stdin.read_line();
self.process_command(command);
}
println("Thanks for playing!");
}
The start method displays the initial game information, shows the starting room description, and then enters the main game loop.
To run the game, we need a simple main function:
// Main function
fn main() -> void {
let game = Game::new();
game.start();
}
To play the game:
adventure_game.srshitrust run adventure_game.sr
Here are the commands you can use in the game:
go [direction] - Move in a direction (north, south, east, west)look - Look around the current locationexamine [item] - Examine an item more closelytake [item] - Take an item from the roomdrop [item] - Drop an item from your inventoryinventory (or inv, i) - Show your inventoryuse [item] - Use an item in your inventoryhelp - Show this help messagequit - Quit the gameNow that you've built a basic text adventure, here are some ideas to extend and improve it:
These extensions will give you more practice with ShitRust and help you build a more complex and engaging adventure game.
Congratulations on completing this tutorial! You've built a text adventure game that demonstrates object-oriented programming, state management, and interactive gameplay.
The complete code for the Text Adventure Game is available in the adventure_game.sr file.
Check out our other tutorials to continue your ShitRust journey:
Or explore the Language Reference to learn more about ShitRust's features.