Stranger Wings Part II #
In Part I of Stranger Wings we set up a game world and commands letting the player move around. Now we’ll need to add some more data to define the items and objects in the game, and commands for interacting with them.
Items #
Items in Stranger Wings will expand on the ideas from Infinite Treasure II. We’ll have a table that contains information about the kinds of items in the game, and an inventory to store items that the player has picked up.
The items we need are: pants, cooked wings, frozen chicken, and a crowbar.
Item Data #
In Infinite Treasure II the loot data was a simple array of strings, but for Stranger Wings we’ll use another table of tables that can contain both the names of items and their descriptions.
items = {
pants = {
description = "Your pants. How did you manage to lose these in the first place?"
},
wings = {
description = "Freshly cooked Stranger Wings fried chicken wings."
},
chicken = {
description = "A bag of frozen Stranger Wings chicken wings, ready to be cooked."
},
crowbar = {
description = "It's a crowbar, for opening things."
},
}
And we’ll start off with the same empty array for the player’s inventory.
inventory = {}
You might notice that I’ve used single words for each of the items, “chicken” instead of “frozen chicken” and so on.
This is a bit of a cheat to make parsing player input easier. Handling player commands gets a little more complicated if the items can contain spaces, so I’m dodging the issue for now by using single words. You can describe this to your players as a “technical limitation of the engine”, nobody can blame you for that!
Items In The World #
The player can aquire items in this game in two different ways. They can find them lying around in the enviroment, and simply pick them up. And second, they can use an item on an object, which will remove that item from their inventory, and give them a different item in return.
We’ll deal with picking up items first, and then handle the second case when we implement objects.
First we’ll need to place the items in the world, so we’ll add an array of items to the locations in the world that will have items.
Add the crowbar to the East Basement location:
east_basement = {
description = "You are in the Stranger Wings' basement.",
directions = {
west = "west_basement",
up = "kitchen",
},
items = { "crowbar" },
},
And add the chicken to the walk in freezer:
freezer = {
description = "You stand in a walk-in freezer, it is very cold. " ..
"One might even say freezing.",
directions = {
north = "kitchen",
},
items = { "chicken" },
},
Note that these strings eg. "crowbar"
need to exactly match the names of the items in the items
table, since these will be used to look up the descriptions from that table.
Finding Items #
Now there are items in the world, but they aren’t doing much until the player can find and interact with them.
To let the player discover items in the world, we’ll list the items in the current location when the player uses the LOOK command.
function command_look()
local location = world_locations[player_location]
print(location.description)
if location.items ~= nil and #location.items > 0 then
print("You see the following items:")
for i,item_name in ipairs(location.items) do
print(i, item_name)
end
end
end
First we check to make sure that the items
value in the current location
table is not equal to nil
, and also that it contains more than zero items. Remember that the #
operator will tell us the length of the array location.items
.
Then we loop over the array using the ipairs()
function provided by Lua.
ipairs()
is similar to the the pairs()
function we’ve used previously, but with a few important differences:
ipairs()
only works on arraysipairs()
will ensure that the table entries are returned in numerical order.pairs()
can return they key value pairs in any order.
Arrays vs. Maps
Remember that arrays look like this:
loot_data = { "coins", "old boots", "sandwiches", "racoons" }
Each entry is listed in order, and there are no key names.
Whereas map style tables look like this:
directions = {
east = "kitchen",
down = "west_basement",
},
Each entry has a key name which is assigned to a value.
pairs()
will work on both types of table, but does not guarantee that the items are returned in any particular order.
ipairs()
will only work on array type tables, but will guarantee that the items are returned in order.
This is all well and good, but the output is a bit technical looking:
Enter command: look
You are in the Stranger Wings' basement.
You see the following items:
1 crowbar
It would be nicer to print out the name and description of each item. We already have the name, and we can look up the item from the global items
table, and print the description
value that it contains:
function command_look()
local location = world_locations[player_location]
print(location.description)
if location.items ~= nil and #location.items > 0 then
print("You see the following items:")
for i,item_name in ipairs(location.items) do
local item = items[item_name]
print(string.format(" %s: %s", item_name, item.description))
end
end
end
And now our item listing is more descriptive and nicely formatted:
Enter command: look
You are in the Stranger Wings' basement.
You see the following items:
crowbar: It's a crowbar, for opening things.
Picking Up Items #
In order to pick up items from the environment we’ll implement the TAKE command. If the player types take crowbar
, when we’ll check to see if there is a crowbar in the room, and if there is, add it to our inventory, and remove it from the room’s list of items.
Let’s start by adding the new command to main_loop()
:
function main_loop()
print("Welcome to Stranger Wings\n")
local game_over = false
while not game_over do
io.write("\nEnter command: ")
local player_command = string.lower(io.read())
local split_command = split_words(player_command)
local cmd = split_command[1]
if cmd == "quit" then
game_over = true
elseif cmd == "look" then
command_look()
elseif cmd == "go" then
local where = split_command[2]
command_go(where)
elseif cmd == "take" then
local what = split_command[2]
command_take(what)
end
end
end
Now we can write the new function command_take()
, but we’ll do it in a couple of steps.
First of all, we’ll need to do some error checking, and give the player some appropriate feedback:
function command_take(item_name)
if item_name == nil then
print("What do you want to take?")
return
end
local location_info = world_locations[player_location]
if location_info.items == nil or #location_info.items == 0 then
print("There's nothing there to take.")
return
end
-- we're not finished yet
end
If they just type take
without specifying what to take, then item_name
will be nil
, in this case, we print out a message asking them what they want to take, and then return from the function, so the rest of the function won’t be processed.
Next we look up the location_info
table for the current location, and check if the items
table in that location is nil
(which it will be for some of the locations), or if it has no items in it, which will be the case once we’ve picked up all of the items in that location. In either case, we can just tell the user, there are no items available to be taken.
If you run the game now, you should receive appropriate messages when attempting to take nothing, or take something when there is nothing present:
Welcome to Stranger Wings
Enter command: take
What do you want to take?
Enter command: take crowbar
There's nothing there to take.
Finding Things In Arrays #
Now to actually handle taking the items from the location. Our first step will be to check and see if the item the user has requested is in the items
array of the current location. How should we do that?
If this table was a map, we could check location_info.items[item_name]
to see if that key was present and had a value. But it’s not a map, it’s an array.
In this case, we need to search the array to find the index of the item we’re looking for. Lua doesn’t provide a build in function for one, but it’s easy to make one:
function find_index_of(t, value)
for i, v in ipairs(t) do
if v == value then
return i
end
end
end
We loop through the array using ipairs()
, and check if the value at each index matches the one we’re looking for. If it does, we return the index, and if nothing is found, the function exists without returning anything, the caller will receive nil
as the return value.
Now we can implement the rest of command_take()
:
function command_take(item_name)
if item_name == nil then
print("What do you want to take?")
return
end
local location_info = world_locations[player_location]
if location_info.items == nil or #location_info.items == 0 then
print("There's nothing there to take.")
return
end
local item_index = find_index_of(location_info.items, item_name)
if item_index == nil then
print(string.format("There is no %s here.", item_name))
return
end
table.remove(location_info.items, item_index)
table.insert(inventory, item_name)
print(string.format("You take the %s", item_name))
end
We call find_index_of()
to search the location_info.items
array for the value in item_name
. If the return value is nil
, it isn’t in the array, and we print an appropriate message.
If we do find the index of the requested item, we remove it from the location’s items
table using the build in function table.remove()
and pass in the index of the item to remove.
Then we append the item_name
value into our inventory array, calling the built in function table.insert()
.
It’s also possible to pass a position intotable.insert()
to specify the location you want to insert the value at, eg.table.insert(inventory, 1, item_name)
would insert the value at the beginning of the array, and move any other items down to make room. But calling it without specifying a position will append the value to the end.
Listing Our Inventory #
While we’re at it, let’s also add an INVENTORY command, to let the player look at the items they are currently carrying.
Add the inventory
command to the main_loop()
:
-- ...
elseif cmd == "take" then
local what = split_command[2]
command_take(what)
elseif cmd == "inventory" then
command_inventory()
end
-- ...
And implement command_inventory()
:
function command_inventory()
print(string.format("You are carrying %d items", #inventory))
for i, item_name in ipairs(inventory) do
local item = items[item_name]
print(string.format(" %s: %s", item_name, item.description))
end
end
Since we’re using inventory
as an array, we can use the #
operator to print how many items it contains.
Then, similar to the LOOK command, we loop over the items in the player’s inventory
, and print the name and description of each item, by looking up the description in the global items
table.
Now we can walk around the world, look for and pick up items, and list the items in our inventory:
Welcome to Stranger Wings
Enter command: go down
You go down
Enter command: go east
You go east
Enter command: look
You are in the Stranger Wings' basement.
You see the following items:
crowbar: It's a crowbar, for opening things.
Enter command: take crowbar
You take the crowbar
Enter command: look
You are in the Stranger Wings' basement.
Enter command: inventory
You are carrying 1 items
crowbar: It's a crowbar, for opening things.
Enter command: go up
You go up
Enter command: go south
You go south
Enter command: look
You stand in a walk-in freezer, it is very cold. One might even say freezing.
You see the following items:
chicken: A bag of frozen Stranger Wings chicken wings, ready to be cooked.
Enter command: take chicken
You take the chicken
Enter command: inventory
You are carrying 2 items
crowbar: It's a crowbar, for opening things.
chicken: A bag of frozen Stranger Wings chicken wings, ready to be cooked.
Don’t Repeat Yourself #
In fact, the loop to print out the items in command_inventory()
is nearly identical to the code in command_look()
, the only difference is the name of the array that we’re printing out. This is an opportunity to create a helper function that prints an item list, and we can call that function from both places.
We can create a function called print_items()
which takes an array as a parameter:
function print_items(item_array)
for i, item_name in ipairs(item_array) do
local item = items[item_name]
print(string.format(" %s: %s", item_name, item.description))
end
end
And now we can simplify both command_inventory()
and command_look()
to call print_items()
to do the actual listing, passing in which array we want to list:
function command_inventory()
print(string.format("You are carrying %d items", #inventory))
print_items(inventory)
end
function command_look()
local location = world_locations[player_location]
print(location.description)
if location.items ~= nil and #location.items > 0 then
print("You see the following items:")
print_items(location.items)
end
end
Code So Far
world_locations = {
alley = {
description = "You stand in the alley beside Stranger Wings chicken restaurant.",
directions = {
east = "kitchen",
down = "west_basement",
},
},
kitchen = {
description = "You are in the Stranger Wings' restaurant kitchen.",
directions = {
north = "dining",
east = "storage",
south = "freezer",
west = "alley",
down = "east_basement",
},
},
west_basement = {
description = "You are in the Stranger Wings' basement.",
directions = {
east = "east_basement",
},
},
east_basement = {
description = "You are in the Stranger Wings' basement.",
directions = {
west = "west_basement",
up = "kitchen",
},
items = { "crowbar" },
},
dining = {
description = "You stand among the tables and chairs of the Stranger Wings' dining area.",
directions = {
south = "kitchen",
},
},
storage = {
description = "You are in a storage room surrounded by shelves.",
directions = {
west = "kitchen",
},
},
freezer = {
description = "You stand in a walk-in freezer, it is very cold. " ..
"One might even say freezing.",
directions = {
north = "kitchen",
},
items = { "chicken" },
},
}
items = {
pants = {
description = "Your pants. How did you manage to lose these in the first place?"
},
wings = {
description = "Delicious Stranger Wings fried chicken wings."
},
chicken = {
description = "A bag of frozen Stranger Wings chicken wings, ready to be cooked."
},
crowbar = {
description = "It's a crowbar, for opening things."
},
}
player_location = "alley"
inventory = {}
function split_words(str)
local t = {}
local function helper(word)
table.insert(t, word)
end
string.gsub(str, "%S+", helper)
return t
end
function find_index_of(t, value)
for i, v in ipairs(t) do
if v == value then
return i
end
end
end
function print_items(item_array)
for i, item_name in ipairs(item_array) do
local item = items[item_name]
print(string.format(" %s: %s", item_name, item.description))
end
end
function command_look()
local location = world_locations[player_location]
print(location.description)
if location.items ~= nil and #location.items > 0 then
print("You see the following items:")
print_items(location.items)
end
end
function command_go(direction)
local location_info = world_locations[player_location]
local destination = location_info.directions[direction]
if destination ~= nil then
player_location = destination
print(string.format("You go %s", direction))
else
print(string.format("You can not go %s", direction))
end
end
function command_take(item_name)
if item_name == nil then
print("What do you want to take?")
return
end
local location_info = world_locations[player_location]
if location_info.items == nil or #location_info.items == 0 then
print("There's nothing there to take.")
return
end
local item_index = find_index_of(location_info.items, item_name)
if item_index == nil then
print(string.format("There is no %s here.", item_name))
return
end
table.remove(location_info.items, item_index)
table.insert(inventory, item_name)
print(string.format("You take the %s", item_name))
end
function command_inventory()
print(string.format("You are carrying %d items", #inventory))
print_items(inventory)
end
function main_loop()
print("Welcome to Stranger Wings\n")
local game_over = false
while not game_over do
io.write("\nEnter command: ")
local player_command = string.lower(io.read())
local split_command = split_words(player_command)
local cmd = split_command[1]
if cmd == "quit" then
game_over = true
elseif cmd == "look" then
command_look()
elseif cmd == "go" then
local where = split_command[2]
command_go(where)
elseif cmd == "take" then
local what = split_command[2]
command_take(what)
elseif cmd == "inventory" then
command_inventory()
end
end
end
main_loop()
Objects #
In this game an “object” will be some object in the environment that can’t be picked up, but can be interacted with by using an item on the object, and then receiving a new item in return.
Object Data #
We’ll set up two objects, the deep fryer (which accepts frozen chicken and returns fried wings), and Bitey the cat (who accepts fried wings and returns your pants). The fact that Bitey is presumably a living creature while the deep freezer is not, doesn’t really make any difference for the sake of how we’re going to handle them on the programming side.
We’ll define objects in a similar way to items, but in addition to having a description, we’ll also need to describe what items they will accept, and what items they will return in exchange. We can also include a message that gets printed to describe the action taking place during the exchange.
objects = {
deepfryer = {
description = "A deep fryer, familiar from your many days spent frying Stranger Wings " ..
"brand chicken wings.",
accept_items = {
chicken = {
message = "You toss the frozen chicken into the fryer, " ..
"and skilfully prepare a serving of Stranger Wings.",
give_item = "wings",
}
}
},
bitey = {
description = "The restaurant owner's cat Bitey. " ..
"Bitey seems to have more eyes than is usually expected, and sits purring comfortably " ..
"on your pants.",
accept_items = {
wings = {
message = "You offer some delicious Stranger Wings to Bitey. " ..
"The cat happily accepts, and jumps off your pants to eat the wings. " ..
"You deftly grab your pants while Bitey is distracted.",
give_item = "pants",
}
}
}
}
We’ll also need to add tables to indicate which objects are present in which locations. The deep fryer goes in the kitchen location, and Bitey goes in the storage room:
world_locations = {
-- ...
kitchen = {
description = "You are in the Stranger Wings' restaurant kitchen.",
directions = {
north = "dining",
east = "storage",
south = "freezer",
west = "alley",
down = "east_basement",
},
objects = { "deepfryer" },
},
-- ...
storage = {
description = "You are in a storage room surrounded by shelves.",
directions = {
west = "kitchen",
},
objects = { "bitey" },
},
-- ...
}
Here we added an objects
array to each location, containing the list of objects present. Currently there’s only one, but using an array lets you easily add more later if you want to.
Finding Objects #
Like items, we’ll need a way for the player to discover what objects are in each location. And similar to items, we can include the description of the objects when the player uses the LOOK command.
We can define a new function to print out objects instead of items, by looking up the object description from the global objects
table:
function print_objects(object_array)
for i, object_name in ipairs(object_array) do
local object = objects[object_name]
print(string.format(" %s: %s", object_name, object.description))
end
end
And then call our new print_objects()
from command_look()
, if there are any objects in the current location:
function command_look()
local location = world_locations[player_location]
print(location.description)
if location.items ~= nil and #location.items > 0 then
print("You see the following items:")
print_items(location.items)
end
if location.objects ~= nil and #location.objects > 0 then
print("You can see:")
print_objects(location.objects)
end
end
You should now be able to see the objects listed in their respective locations when looking around the environment:
Enter command: look
You are in the Stranger Wings' restaurant kitchen.
You can see:
deepfryer: A deep fryer, familiar from your many days spent frying Stranger Wings brand chicken wings.
Enter command: go east
You go east
Enter command: look
You are in a storage room surrounded by shelves.
You can see:
bitey: The restaurant owner's cat Bitey. Bitey seems to have more eyes than is usually expected,
and sits purring comfortably on your pants.
Using Items On Objects #
Using things on other things is a core mechanic of adventure games.
To use an item with an object, we’ll add a command in the format USE X ON Y, where X is the name of the item, and Y is the name of the object, eg. use chicken on deepfryer
. The “on” isn’t really necessary, but it does make the command sound more natural.
We can even easily support some variations, like put chicken in deepfryer
, and give wings to bitey
by checking for a few different verbs as the command, and then just ignoring the 3rd word entirely. You can go through the extra steps to verify that the 3rd word makes sense according to the command used, if you want to.
As it is, the player could type something like use chicken monkey deepfryer
and it would still work, which may not be ideal, but that’s open to interpretation.
function main_loop()
-- ...
elseif cmd == "inventory" then
command_inventory()
elseif cmd == "use" or cmd == "give" or cmd == "put" then
local item = split_command[2]
-- ignore the 3rd word
local object = split_command[4]
command_use(item, object)
end
-- ...
This checks if cmd
equals “use” or “give” or “put”, ignores whatever the 3rd word is, and passes the item and object names to the function command_use()
.
function command_use(item_name, object_name)
if item_name == nil or object_name == nil then
print("Use what on what?")
return
end
local item_index = find_index_of(inventory, item_name)
if item_index == nil then
print(string.format("I don't have a %s.", item_name))
return
end
local location_info = world_locations[player_location]
if location_info.objects == nil or #location_info.objects == 0 then
print("There's nothing to use that on here.")
return
end
local object_index = find_index_of(location_info.objects, object_name)
if object_index == nil then
print(string.format("There's no %s here.", object_name))
return
end
local object_info = objects[object_name]
local accept_info = object_info.accept_items[item_name]
if accept_info == nil then
print(string.format("I can't use %s on %s.", item_name, object_name))
return
end
table.remove(inventory, item_index)
table.insert(inventory, accept_info.give_item)
print(accept_info.message)
end
The majority of the code in command_use()
is to determine whether the user can actually perform the command they have requested.
- If either
item_name
orobject_name
arenil
then the user didn’t provide the item or object names. - If
item_index
isnil
, the the given item name wasn’t found in our inventory. - If
location_info.objects
isnil
, or has a length of zero, then there are no objects in the current location to interact with. - If
object_index
isnil
, the given object name wasn’t found in the current location’sobjects
array. - If
accept_info
isnil
, the object’saccept_items
table doesn’t contain the given item, and so it shouldn’t accept it.
If we pass all of those checks, we should be able to perform the requested action. We can remove the used item that we found in out inventory by calling table.remove()
and specifying its index which we found. Then we add the newly received item by calling table.insert()
and specifying the value to append to the end of the array.
Note that there are at least two other variables that could potentially be
nil
: we didn’t check for eitherlocation_info
orobject_info
beingnil
before using them.
player_location
should only ever be set to a valid location name, solocation_info
should always be valid.Similarly,
location_info.objects
should only list valid objects that exist in theobjects
table, and we checkedobject_index
already to make sure that the specified object was indeed listed in the current location.If either of these values ends up being
nil
that’s a bug and the game will crash! Maybe the missing data needs to be added, or there could be a typo leading to a mismatch.
At this point, you should be able to walk around the environment, pick up all of the items, and use them on the objects that require them in order to receive a different item in exchange:
Enter command: look
You stand in a walk-in freezer, it is very cold. One might even say freezing.
You see the following items:
chicken: A bag of frozen Stranger Wings chicken wings, ready to be cooked.
Enter command: take chicken
You take the chicken
Enter command: go north
You go north
Enter command: look
You are in the Stranger Wings' restaurant kitchen.
You can see:
deepfryer: A deep fryer, familiar from your many days spent frying Stranger Wings brand chicken wings.
Enter command: put chicken in deepfryer
You toss the frozen chicken into the fryer, and skilfully prepare a serving of Stranger Wings.
Enter command: inventory
You are carrying 1 items
wings: Delicious Stranger Wings fried chicken wings.
Enter command: go east
You go east
Enter command: look
You are in a storage room surrounded by shelves.
You can see:
bitey: The restaurant owner's cat Bitey. Bitey seems to have more eyes than is usually expected, and
sits purring comfortably on your pants.
Enter command: give wings to bitey
You offer some delicious Stranger Wings to Bitey. The cat happily accepts, and jumps off your pants to
eat the wings. You deftly grab your pants while Bitey is distracted.
Enter command: inventory
You are carrying 1 items
pants: Your pants. How did you manage to lose these in the first place?
Code So Far
world_locations = {
alley = {
description = "You stand in the alley beside Stranger Wings chicken restaurant.",
directions = {
east = "kitchen",
down = "west_basement",
},
},
kitchen = {
description = "You are in the Stranger Wings' restaurant kitchen.",
directions = {
north = "dining",
east = "storage",
south = "freezer",
west = "alley",
down = "east_basement",
},
objects = { "deepfryer" },
},
west_basement = {
description = "You are in the Stranger Wings' basement.",
directions = {
east = "east_basement",
},
},
east_basement = {
description = "You are in the Stranger Wings' basement.",
directions = {
west = "west_basement",
up = "kitchen",
},
items = { "crowbar" },
},
dining = {
description = "You stand among the tables and chairs of the Stranger Wings' dining area.",
directions = {
south = "kitchen",
},
},
storage = {
description = "You are in a storage room surrounded by shelves.",
directions = {
west = "kitchen",
},
objects = { "bitey" },
},
freezer = {
description = "You stand in a walk-in freezer, it is very cold. " ..
"One might even say freezing.",
directions = {
north = "kitchen",
},
items = { "chicken" },
},
}
items = {
pants = {
description = "Your pants. How did you manage to lose these in the first place?"
},
wings = {
description = "Delicious Stranger Wings fried chicken wings."
},
chicken = {
description = "A bag of frozen Stranger Wings chicken wings, ready to be cooked."
},
crowbar = {
description = "It's a crowbar, for opening things."
},
}
objects = {
deepfryer = {
description = "A deep fryer, familiar from your many days spent frying Stranger Wings " ..
"brand chicken wings.",
accept_items = {
chicken = {
message = "You toss the frozen chicken into the fryer, " ..
"and skilfully prepare a serving of Stranger Wings.",
give_item = "wings",
}
}
},
bitey = {
description = "The restaurant owner's cat Bitey. " ..
"Bitey seems to have more eyes than is usually expected, and sits purring comfortably " ..
"on your pants.",
accept_items = {
wings = {
message = "You offer some delicious Stranger Wings to Bitey. " ..
"The cat happily accepts, and jumps off your pants to eat the wings. " ..
"You deftly grab your pants while Bitey is distracted.",
give_item = "pants",
}
}
}
}
player_location = "alley"
inventory = {}
function split_words(str)
local t = {}
local function helper(word)
table.insert(t, word)
end
string.gsub(str, "%S+", helper)
return t
end
function find_index_of(t, value)
for i, v in ipairs(t) do
if v == value then
return i
end
end
end
function print_items(item_array)
for i, item_name in ipairs(item_array) do
local item = items[item_name]
print(string.format(" %s: %s", item_name, item.description))
end
end
function print_objects(object_array)
for i, object_name in ipairs(object_array) do
local object = objects[object_name]
print(string.format(" %s: %s", object_name, object.description))
end
end
function command_look()
local location = world_locations[player_location]
print(location.description)
if location.items ~= nil and #location.items > 0 then
print("You see the following items:")
print_items(location.items)
end
if location.objects ~= nil and #location.objects > 0 then
print("You can see:")
print_objects(location.objects)
end
end
function command_go(direction)
local location_info = world_locations[player_location]
local destination = location_info.directions[direction]
if destination ~= nil then
player_location = destination
print(string.format("You go %s", direction))
else
print(string.format("You can not go %s", direction))
end
end
function command_take(item_name)
if item_name == nil then
print("What do you want to take?")
return
end
local location_info = world_locations[player_location]
if location_info.items == nil or #location_info.items == 0 then
print("There's nothing there to take.")
return
end
local item_index = find_index_of(location_info.items, item_name)
if item_index == nil then
print(string.format("There is no %s here.", item_name))
return
end
table.remove(location_info.items, item_index)
table.insert(inventory, item_name)
print(string.format("You take the %s", item_name))
end
function command_inventory()
print(string.format("You are carrying %d items", #inventory))
print_items(inventory)
end
function command_use(item_name, object_name)
if item_name == nil or object_name == nil then
print("Use what on what?")
return
end
local item_index = find_index_of(inventory, item_name)
if item_index == nil then
print(string.format("I don't have a %s.", item_name))
return
end
local location_info = world_locations[player_location]
if location_info.objects == nil or #location_info.objects == 0 then
print("There's nothing to use that on here.")
return
end
local object_index = find_index_of(location_info.objects, object_name)
if object_index == nil then
print(string.format("There's no %s here.", object_name))
return
end
local object_info = objects[object_name]
local accept_info = object_info.accept_items[item_name]
if accept_info == nil then
print(string.format("I can't use %s on %s.", item_name, object_name))
return
end
table.remove(inventory, item_index)
table.insert(inventory, accept_info.give_item)
print(accept_info.message)
end
function main_loop()
print("Welcome to Stranger Wings\n")
local game_over = false
while not game_over do
io.write("\nEnter command: ")
local player_command = string.lower(io.read())
local split_command = split_words(player_command)
local cmd = split_command[1]
if cmd == "quit" then
game_over = true
elseif cmd == "look" then
command_look()
elseif cmd == "go" then
local where = split_command[2]
command_go(where)
elseif cmd == "take" then
local what = split_command[2]
command_take(what)
elseif cmd == "inventory" then
command_inventory()
elseif cmd == "use" or cmd == "give" or cmd == "put" then
local item = split_command[2]
-- ignore the 3rd word
local object = split_command[4]
command_use(item, object)
end
end
end
main_loop()
Restrictions Make The Game #
To finally put the puzzle part into the game, we’re going to need to add some more restrictions on where you can go and when.
Blocked Directions #
To start with, let’s properly block off the alley, so that you can’t enter the restaurant just by going through the door (that’s boring!), and provide some useful story telling if the player tries to go north out onto the street, as well as a generally more descriptive description of the alley itself.
We’ll add a new direction of travel for north
and replace the string value for east
with a table containing the key blocked
, if this key exists, we’ll print out the contents of the message instead of letting the player go that way.
world_locations = {
alley = {
description = "You stand in the alley beside Stranger Wings chicken restaurant. " ..
"To the east is the back door of the restaurant, to the north is the street. " ..
"There's also a disgusting dumpster, which you can't seem to see the bottom of...",
directions = {
north = {
blocked = "The ally opens onto the street, but you're not about " ..
"to go out there without your pants. Wait, where did your pants go?",
},
east = {
blocked = "The back door to the Stranger Wings restaurant stands firmly locked. " ..
"You won't be getting in this way.",
},
down = "west_basement",
},
},
-- ...
Then in command_go()
we’ll need to check the type of the destination value that we look up in the directions
table. If it’s a string like we’ve been using so far then we’ll allow the player to go to that location as before; but if it’s a table, we’ll look for more information inside the table to see what we should do:
function command_go(direction)
local location_info = world_locations[player_location]
local destination = location_info.directions[direction]
local dest_type = type(destination)
if dest_type == "string" then
player_location = destination
print(string.format("You go %s", direction))
elseif dest_type == "table" then
if destination.blocked ~= nil then
print(destination.blocked)
end
else
print(string.format("You can not go %s", direction))
end
end
For now the only thing the table should contain is the key blocked
, and if it’s set, then we print its value and don’t change the player’s location.
Welcome to Stranger Wings
Enter command: look
You stand in the alley beside Stranger Wings chicken restaurant. To the east is the back door of the
restaurant, to the north is the street. There's also a disgusting dumpster, which you can't seem to
see the bottom of...
Enter command: go north
The ally opens onto the street, but you're not about to go out there without your pants. Wait, where did
your pants go?
Enter command: go east
The back door to the Stranger Wings restaurant stands firmly locked. You won't be getting in this way.
Enter command: go down
You go down
This is all well and good for permanently blocked directions, but we’ll also want the player to travel in certain directions only if they have found a required item that allows them to do so.
Doors And Keys #
Ah, that classic game mechanic, finding keys for doors. It might be a literal key, or it might be a more general requirement of some kind, a pass code written on piece of paper, a magic crystal, or a crowbar.
We’ll implement key requirements in two places, one that requires the player to have the crowbar
item in order to enter the walk-in freezer, and another that requires the player to have their pants
in order to exit the restaurant, thus winning the game!
Instead of using the blocked
key, we’ll add a new key called require
to the south
table in the kitchen, which will specify the item that needs to be in the player’s inventory before they can go in this direction.
If they don’t have the item, we’ll print the contents of the require_fail
message, and if they do have the item, we’ll print the contents of the require_pass
message, and then take them to the location specified by location
.
--...
kitchen = {
description = "You are in the Stranger Wings' restaurant kitchen.",
directions = {
north = "dining",
east = "storage",
south = {
require = "crowbar",
require_fail = "The freezer door is frozen shut, it won't budge! " ..
"If only you had something to pry it open with...",
require_pass = "You use the crowbar to crack the ice around the freezer door " ..
"and pry it open.",
location = "freezer",
},
down = "east_basement",
},
objects = { "deepfryer" },
},
--...
Then modifying command_go()
to handle the new data:
function command_go(direction)
local location_info = world_locations[player_location]
local destination = location_info.directions[direction]
local dest_type = type(destination)
if dest_type == "string" then
player_location = destination
print(string.format("You go %s", direction))
elseif dest_type == "table" then
if destination.blocked ~= nil then
print(destination.blocked)
elseif destination.require ~= nil then
local item_index = find_index_of(inventory, destination.require)
if item_index ~= nil then
player_location = destination.location
print(destination.require_pass)
else
print(destination.require_fail)
end
end
else
print(string.format("You can not go %s", direction))
end
end
Now the player needs to pick up the crowbar before they can get into the freezer:
Enter command: go south
The freezer door is frozen shut, it won't budge! If only you had something to pry it open with...
Enter command: go down
You go down
Enter command: take crowbar
You take the crowbar
Enter command: go up
You go up
Enter command: go south
You use the crowbar to crack the ice around the freezer door and pry it open.
Enter command: look
You stand in a walk-in freezer, it is very cold. One might even say freezing.
You see the following items:
chicken: A bag of frozen Stranger Wings chicken wings, ready to be cooked.
Winning #
At long last, the final piece of the puzzle, the player needs to have their pants
in order to leave through the front door of the restaurant.
We’ll add the requirements to the dining
location, and add a new key win
to indicate that successfully going in this direction ends the game.
-- ...
dining = {
description = "You stand among the tables and chairs of the Stranger Wings' dining area.",
directions = {
north = {
require = "pants",
require_fail = "The front door opens out onto the street, but there's no way " ..
"you're going out there without your pants! How embarrassing!",
require_pass = "You've finally done it. Pants in hand, you confidently stride " ..
"out the front door of the restaurant, hop on your bicycle, and ride " ..
"off into the sunset.\n\nThe End.",
win = true,
},
south = "kitchen",
},
},
-- ...
All we need to do is check for the new win
value in command_go()
:
function command_go(direction)
local location_info = world_locations[player_location]
local destination = location_info.directions[direction]
local dest_type = type(destination)
if dest_type == "string" then
player_location = destination
print(string.format("You go %s", direction))
elseif dest_type == "table" then
if destination.blocked ~= nil then
print(destination.blocked)
elseif destination.require ~= nil then
local item_index = find_index_of(inventory, destination.require)
if item_index ~= nil then
print(destination.require_pass)
if destination.win then
game_over = true
else
player_location = destination.location
end
else
print(destination.require_fail)
end
end
else
print(string.format("You can not go %s", direction))
end
end
If destination.win
is set, we set game_over
to true so that our main_loop()
will exit, instead of taking the player to a new location.
There’s one small problem though, currently game_over
is a local variable inside of main_loop()
, which means setting it inside of command_go()
is actually setting a different, global variable with the same name, which isn’t the one that main_loop()
is going to check.
To remedy this, find the line:
local game_over = false
… in main_loop()
and cut it, then paste it at the top of the file along with player_location
and inventory
so that this same game_over
variable can be accessed from anywhere in the same file.
local game_over = false
local player_location = "alley"
local inventory = {}
Now when command_go()
sets game_over
to true
, it’s accessing the same variable that is being used in main_loop()
, which will cause main_loop()
to exit.
Enter command: give wings to bitey
You offer some delicious Stranger Wings to Bitey. The cat happily accepts, and jumps off your pants to
eat the wings. You deftly grab your pants while Bitey is distracted.
Enter command: inventory
You are carrying 2 items
crowbar: It's a crowbar, for opening things.
pants: Your pants. How did you manage to lose these in the first place?
Enter command: go west
You go west
Enter command: go north
You go north
Enter command: look
You stand among the tables and chairs of the Stranger Wings' dining area.
Enter command: go north
You've finally done it. Pants in hand, you confidently stride out the front door of the restaurant, hop
on your bicycle, and ride off into the sunset.
The End.
Code So Far
world_locations = {
alley = {
description = "You stand in the alley beside Stranger Wings chicken restaurant. " ..
"To the east is the back door of the restaurant, to the north is the street. " ..
"There's also a disgusting dumpster, which you can't seem to see the bottom of...",
directions = {
north = {
blocked = "The ally opens onto the street, but you're not about " ..
"to go out there without your pants. Wait, where did your pants go?",
},
east = {
blocked = "The back door to the Stranger Wings restaurant stands firmly locked. " ..
"You won't be getting in this way.",
},
down = "west_basement",
},
},
kitchen = {
description = "You are in the Stranger Wings' restaurant kitchen.",
directions = {
north = "dining",
east = "storage",
south = {
require = "crowbar",
require_fail = "The freezer door is frozen shut, it won't budge! " ..
"If only you had something to pry it open with...",
require_pass = "You use the crowbar to crack the ice around the freezer door " ..
"and pry it open.",
location = "freezer",
},
down = "east_basement",
},
objects = { "deepfryer" },
},
west_basement = {
description = "You are in the Stranger Wings' basement.",
directions = {
east = "east_basement",
},
},
east_basement = {
description = "You are in the Stranger Wings' basement.",
directions = {
west = "west_basement",
up = "kitchen",
},
items = { "crowbar" },
},
dining = {
description = "You stand among the tables and chairs of the Stranger Wings' dining area.",
directions = {
north = {
require = "pants",
require_fail = "The front door opens out onto the street, but there's no way " ..
"you're going out there without your pants! How embarassing!",
require_pass = "You've finally done it. Pants in hand, you confidently stride " ..
"out the front door of the restaurant, hop on your bicycle, and ride " ..
"off into the sunset.\n\nThe End.",
win = true,
},
south = "kitchen",
},
},
storage = {
description = "You are in a storage room surrounded by shelves.",
directions = {
west = "kitchen",
},
objects = { "bitey" },
},
freezer = {
description = "You stand in a walk-in freezer, it is very cold. " ..
"One might even say freezing.",
directions = {
north = "kitchen",
},
items = { "chicken" },
},
}
items = {
pants = {
description = "Your pants. How did you manage to lose these in the first place?"
},
wings = {
description = "Delicious Stranger Wings fried chicken wings."
},
chicken = {
description = "A bag of frozen Stranger Wings chicken wings, ready to be cooked."
},
crowbar = {
description = "It's a crowbar, for opening things."
},
}
objects = {
deepfryer = {
description = "A deep fryer, familiar from your many days spent frying Stranger Wings " ..
"brand chicken wings.",
accept_items = {
chicken = {
message = "You toss the frozen chicken into the fryer, " ..
"and skilfully prepare a serving of Stranger Wings.",
give_item = "wings",
}
}
},
bitey = {
description = "The restaurant owner's cat Bitey. " ..
"Bitey seems to have more eyes than is usually expected, and sits purring comfortably " ..
"on your pants.",
accept_items = {
wings = {
message = "You offer some delicious Stranger Wings to Bitey. " ..
"The cat happily accepts, and jumps off your pants to eat the wings. " ..
"You deftly grab your pants while Bitey is distracted.",
give_item = "pants",
}
}
}
}
local game_over = false
local player_location = "alley"
local inventory = {}
function split_words(str)
local t = {}
local function helper(word)
table.insert(t, word)
end
string.gsub(str, "%S+", helper)
return t
end
function find_index_of(t, value)
for i, v in ipairs(t) do
if v == value then
return i
end
end
end
function print_items(item_array)
for i, item_name in ipairs(item_array) do
local item = items[item_name]
print(string.format(" %s: %s", item_name, item.description))
end
end
function print_objects(object_array)
for i, object_name in ipairs(object_array) do
local object = objects[object_name]
print(string.format(" %s: %s", object_name, object.description))
end
end
function command_look()
local location = world_locations[player_location]
print(location.description)
if location.items ~= nil and #location.items > 0 then
print("You see the following items:")
print_items(location.items)
end
if location.objects ~= nil and #location.objects > 0 then
print("You can see:")
print_objects(location.objects)
end
end
function command_go(direction)
local location_info = world_locations[player_location]
local destination = location_info.directions[direction]
local dest_type = type(destination)
if dest_type == "string" then
player_location = destination
print(string.format("You go %s", direction))
elseif dest_type == "table" then
if destination.blocked ~= nil then
print(destination.blocked)
elseif destination.require ~= nil then
local item_index = find_index_of(inventory, destination.require)
if item_index ~= nil then
print(destination.require_pass)
if destination.win then
game_over = true
else
player_location = destination.location
end
else
print(destination.require_fail)
end
end
else
print(string.format("You can not go %s", direction))
end
end
function command_take(item_name)
if item_name == nil then
print("What do you want to take?")
return
end
local location_info = world_locations[player_location]
if location_info.items == nil or #location_info.items == 0 then
print("There's nothing there to take.")
return
end
local item_index = find_index_of(location_info.items, item_name)
if item_index == nil then
print(string.format("There is no %s here.", item_name))
return
end
table.remove(location_info.items, item_index)
table.insert(inventory, item_name)
print(string.format("You take the %s", item_name))
end
function command_inventory()
print(string.format("You are carrying %d items", #inventory))
print_items(inventory)
end
function command_use(item_name, object_name)
if item_name == nil or object_name == nil then
print("Use what on what?")
return
end
local item_index = find_index_of(inventory, item_name)
if item_index == nil then
print(string.format("I don't have a %s.", item_name))
return
end
local location_info = world_locations[player_location]
if location_info.objects == nil or #location_info.objects == 0 then
print("There's nothing to use that on here.")
return
end
local object_index = find_index_of(location_info.objects, object_name)
if object_index == nil then
print(string.format("There's no %s here.", object_name))
return
end
local object_info = objects[object_name]
local accept_info = object_info.accept_items[item_name]
if accept_info == nil then
print(string.format("I can't use %s on %s.", item_name, object_name))
return
end
table.remove(inventory, item_index)
table.insert(inventory, accept_info.give_item)
print(accept_info.message)
end
function main_loop()
print("Welcome to Stranger Wings\n")
while not game_over do
io.write("\nEnter command: ")
local player_command = string.lower(io.read())
local split_command = split_words(player_command)
local cmd = split_command[1]
if cmd == "quit" then
game_over = true
elseif cmd == "look" then
command_look()
elseif cmd == "go" then
local where = split_command[2]
command_go(where)
elseif cmd == "take" then
local what = split_command[2]
command_take(what)
elseif cmd == "inventory" then
command_inventory()
elseif cmd == "use" or cmd == "give" or cmd == "put" then
local item = split_command[2]
-- ignore the 3rd word
local object = split_command[4]
command_use(item, object)
end
end
end
main_loop()
Epilogue #
This is as far as I’ll take the text adventure Stranger Wings but there are still a lot of things that could be improved, so by all means, improve them!
The rooms could use better descriptions, and it would be useful to list the possible directions a player can go automatically instead of having to write them into the descriptions manually.
And there are some issues that could use fixing, for example Bitey the cat will continue to be described as sitting on your pants, even after the player has successfully retrieved them. You could add a variable to track the state of whether Bitey has your pants or not, and display an appropriate message.
You could create an entirely different story by replacing the data in world_locations
, items
, and objects
with your own creations without even changing any of the rest of the code.
If you’ve stuck it out this far, congratulations! From now on, we’ll be focusing on more “modern” games, the kind with pictures and sounds.
Next - Love2D