Text adventures (sometimes called interactive fiction) are a classic and celebrated game genre — see 50 Years of Text Games, for example. Mini Micro has not neglected this genre; among the built-in demos is a 5-minute text adventure called The Greedy Gargoyle, which you can play on the web right here.
If you have Mini Micro running on your own computer, you can also run this demo with run "/sys/demo/textAdventure".
But did you know that this demo is more than just a demo - it's a foundation you can import and use to make your own text adventures?
In this post, we'll walk through making a little custom adventure with three rooms to explore, and an object you can pick up and move around.
The import trick
Because textAdventure.ms is located in /sys/demo rather than /sys/lib, it's not on the standard import path. So we need to add /sys/demo/ to env.importPaths before we try to import it. Then, to make the various classes and methods in that module easier to use, we're going to copy the contents of the imported module into locals. So the top of the program will look like this:
if env.importPaths[0] != "/sys/demo" then
env.importPaths.insert 0, "/sys/demo"
end if
import "textAdventure"
for id in textAdventure.indexes
locals[id] = @textAdventure[id]
end for
In Mini Micro, reset the program state, then edit, and paste in the above code. Give it a run. You can then check to be sure it worked by entering Object at the prompt. Object is the base of the textAdventure class hierarchy; if it returns a map containing keys like "location", "contents", and "name", then it worked.
Required Globals
Next, there are a small number of globals values needed by the textAdventure code, which are not automatically defined when it's imported this way. So edit again and add this code:
// Globals required by the textAdventure code:
lamp = {}
lamp.lit = true
done = false
normalColor = color.aqua // (or whatever you prefer)
(This will probably be improved in Mini Micro 2, but having this code won't do any harm in any case.)
...And Create!
Now the fun part: build your virtual world! A world in a text adventure consists of a bunch of objects, including rooms (locations), exits (doors/passageways connecting rooms), and things (items). You create these by simply instantiating various classes in textAdventure.ms, and configuring their properties.
For this tutorial, we're going to make a little world consisting of a small one-room house, with a front yard and an attic. So, let's start with the yard. Edit your program again and add this code:
// Now, let's define our universe!
yard = new Room
yard.name = "Front Yard"
yard.desc = "You're standing in the yard in front of a small house." +
" The grass is long and overgrown with weeds that almost hide " +
"an old car tire. The house lies directly to the north."
Player.moveTo yard
Not too hard, right? We're creating a new Room, and giving it a name and description. And then, to make the player start in this room, we add a Player.moveTo yard call.
The Main Loop
Finally, to get our first playable demo, we need a main program. This clears the screen, does the initial look at the player's location, and then enters a simple main loop that takes input from the player and handles it.
// Main loop.
text.color = normalColor
clear
look
while not done
text.color = color.gray
cmd = input(">").lower
text.color = normalColor
handleCommand cmd
end while
Add the above code to your program, and then run it. It should be a playable but very minimal game. You can use the i or inventory command to check your inventory (which is empty), you can look again at the room you're in, and... well, that's about it so far.
Create some more!
Let's make the world bigger! The code below creates two more locations, house and attic, and then connects them all up with doors. Insert this code after Player.moveTo yard, but before // Main loop.
house = new Room
house.name = "Small House"
house.desc = "You're on the main floor of a small one-room house."
attic = new Room
attic.name = "Attic"
attic.desc = "You're in the attic of the small house. It's dusty " +
"and full of cobwebs. Wan light comes from a tiny window at one " +
"end. The only exit is down."
doors = Exit.connect(yard, "north", house)
doors[0].altNames = ["house", "door", "n"]
doors[0].salient = false
doors[1].altNames = ["exit", "leave", "out", "door", "s"]
doors[1].salient = false
doors = Exit.connect(house, "up", attic)
doors[0].altNames = ["attic", "trapdoor", "n"]
doors[0].type = "trapdoor"
doors[0].fullName = "trapdoor in the ceiling"
doors[0].open = false
doors[1].altNames = ["exit", "leave", "out", "door", "d"]
doors[1].salient = false
In this particular example, the door configuration is longer than the locations themselves, but that's not always the case. Here's what's going on with the exits:
- Each call to
Exit.connectreturns two exits; element[0]in the forward direction (e.g. yard to house), and element[1]in the reverse direction (e.g. house to yard). - Giving a generous supply of
altNamesallows the player more freedom in what they type to move around; instead of alwaysgo north, they can saygo house, ordoor, or justn. - Setting
salient = falsekeeps the door from being automatically listed among the room contents. Try commenting that line out and see how it behaves. - Setting
typejust gives the engine a more colorful term for the exit than "door"; similarly, settingfullNamehelps color the description that appears for a door that is salient. - Finally, doors default to already open; set
open = falseto make it closed.
Run the program at this point, and you should be able to navigate from the yard to the house, and then up into the attic, and back.
Add some things!
A game consisting only of empty rooms isn't a very interesting game! The core of any adventure is a set of things you can pick up, carry around, and interact with. Our description of the front yard claimed there was a tire in the weeds. This brings up an important design rule for any text adventure game:
If you mention an object in a description, players will try to grab it.
So, a well-written game has actual objects (possibly with salient = false) that correspond to any object mentioned in the descriptions.
In this case, we mentioned a tire, so we better make a tire. And to keep things simple, we'll take it out of the room description and let the game engine list it when present. So, start by editing the code that sets yard.desc to:
yard.desc = "You're standing in the yard in front of a small house." +
" The house lies directly to the north."
And then, after creating the doors (or really anywhere after yard is built), add new code for the tire:
tire = new Thing
tire.name = "old tire"
tire.altNames = ["tire", "tyre"]
tire.desc = "It's just an old, worn-out car tire."
tire.moveTo yard
tire.descInRoom = function
if self.location != yard then return super.descInRoom
printWrap "The grass is long and overgrown with weeds that " +
"almost hide an old car tire."
end function
This example is a little fancier than your typical room object, but I couldn't help myself; tweaking the behavior of objects to make them "special" is a large part of the magic of text adventure games. And the magic in this case is relatively minor: it's just overriding the descInRoom function, so that if it is located in the yard, it prints a special description about it being among the weeds.
There are other ways we could have handled this; perhaps better would be to make the tire nonsalient, and give
yarda customdescfunction that includes the bit about the tire when the tire is present, and a different message about weeds (but no tire) when it is not. This is left as an exercise for the reader.
Now run the game again. Try picking the tire up, carrying it around, dropping it in the house or the attic, or back in the yard, and be sure to look at your surroundings to verify it's all working.
Overriding the stock code
You might notice a minor bug that doesn't appear in my screen shot above -- there's an extra space every time the game says an old tire. It turns out this is a bug in textAdventure.ms that was never noticed because none of the items in the stock game begin with a vowel.
But good news! You have the power to replace any imported method. Add this code near the top of your program, after the required globals:
// Fix the minor bug in the Thing.a function
Thing.a = function
if "aeiou".indexOf(self.name[0]) != null then return "an"
return "a"
end function
This code is copied directly out of textAdventure.ms, but "an " has been replaced with "an" (without the extra space). Problem solved.
Here's the complete tutorial code if you need it.
// Demo text adventure.
if env.importPaths[0] != "/sys/demo" then
env.importPaths.insert 0, "/sys/demo"
end if
import "textAdventure"
for id in textAdventure.indexes
locals[id] = @textAdventure[id]
end for
// Globals required by the textAdventure code:
lamp = {}
lamp.lit = true
done = false
normalColor = color.aqua
// Fix the minor bug in the Thing.a function
Thing.a = function
if "aeiou".indexOf(self.name[0]) != null then return "an"
return "a"
end function
// Now, let's define our universe!
yard = new Room
yard.name = "Front Yard"
yard.desc = "You're standing in the yard in front of a small house." +
" The house lies directly to the north."
Player.moveTo yard
house = new Room
house.name = "Small House"
house.desc = "You're on the main floor of a small one-room house."
attic = new Room
attic.name = "Attic"
attic.desc = "You're in the attic of the small house. It's dusty " +
"and full of cobwebs. Wan light comes from a tiny window at one " +
"end. The only exit is down."
doors = Exit.connect(yard, "north", house)
doors[0].altNames = ["house", "door", "n"]
doors[0].salient = false
doors[1].altNames = ["exit", "leave", "out", "door", "s"]
doors[1].salient = false
doors = Exit.connect(house, "up", attic)
doors[0].altNames = ["attic", "trapdoor", "n"]
doors[0].type = "trapdoor"
doors[0].fullName = "trapdoor in the ceiling"
doors[0].open = false
doors[1].altNames = ["exit", "leave", "out", "door", "d"]
doors[1].salient = false
tire = new Thing
tire.name = "old tire"
tire.altNames = ["tire", "tyre"]
tire.desc = "It's just an old, worn-out car tire."
tire.moveTo yard
tire.descInRoom = function
if self.location != yard then return super.descInRoom
printWrap "The grass is long and overgrown with weeds that " +
"almost hide an old car tire."
end function
// Main loop.
text.color = normalColor
clear
look
while not done
text.color = color.gray
cmd = input(">").lower
text.color = normalColor
handleCommand cmd
end while
Go forth and create!
You have now created a simple world with a house, an attic accessible via a trapdoor, and an overgrown front yard containing an old tire you can manipulate. That's a foundation for high adventure if I've ever heard one.
Where you go from here is up to you. Read textAdventure.ms (in Mini Micro or on GitHub) to learn the full set of object types, properties, and methods available to you. They include things like countable objects (like the coins in The Greedy Gargoyle) and liquid (like the water in the pool).
I'd recommend you start with the built-in functions and classes, and sketch out your world as much as you can with these. Then start building in special behaviors. This is how you make puzzles: objects that react based on where other objects are, or what state they're in, or special commands entered by the user.
Or, if you don't like puzzles, fill your world with mobs and craft a combat system. Add magic if you like. Or maybe you prefer ray guns and rocket ships. It's all possible.
Do you have any fond memories of text adventures you've played, or is the genre new to you? What new adventures would you love to see? Share your thoughts in the comments below!



Top comments (0)