LearnAI ToolsCareerPractice BuildsPlayDevelopmentContact
LuaBeginner~1.5 hours

Text Adventure Game

Build a small explorable world of rooms, items and a locked door, driven by typed commands and Lua tables.

TablesControl FlowFunctionsString Patterns

Overview

Text adventures — games like Zork, where you type "go north" or "take key" — are a classic first project for scripting languages, and Lua is an especially good fit. Tables let you describe the whole world as data: each room is a table with a description, a table of exits and a list of items. The game logic then becomes a handful of small functions that read and update those tables.

Your adventure has four rooms, a few items to collect, a locked vault that only opens if you are carrying the key, and a treasure that wins the game. Commands are parsed with Lua string patterns, so "GO North", "go north" and plain "north" all work.

What You'll Build
  • A rooms table describing every room, its exits and the items on the floor.
  • A player table tracking the current room and an inventory.
  • look, move, take and show_inventory functions that read and update those tables.
  • A locked exit that checks the inventory before letting the player through.
  • A command parser built with string.match and a dispatch table of command handlers.
  • A game loop with io.read that ends on a win or on "quit".

Prerequisites

  • Lua 5.4 installed and the ability to run lua file.lua — lesson 3.
  • Tables as arrays and as dictionaries — lesson 5.
  • Control flow and loops — lesson 6.
  • Functions, including storing functions in tables — lesson 7.
  • Basic string patterns with string.match — lesson 10.

Project Structure

The game is a single file, adventure.lua. The top of the file is pure data — the rooms table — so you can add rooms or change the story without touching any logic. Below it are small action functions, a commands table that maps each verb to its function, and a short loop that reads a line, parses it and dispatches it.

Step 1: Describe the World with Tables

Each room's exits table maps a direction to another room's key. An exit can also be a table with a locked_by field naming the item required to pass — data, not special code, decides which doors are locked. player.inventory uses item names as keys (inventory.key = true), which makes "do I have it?" a single table lookup.

local rooms = {
hall = {
name = "Great Hall",
description = "Dusty banners hang from the walls. Doors lead north and east.",
exits = { north = "library", east = "kitchen" },
items = { "torch" },
},
library = {
name = "Library",
description = "Shelves of crumbling books. A heavy iron door stands to the east.",
exits = { south = "hall", east = { to = "vault", locked_by = "key" } },
items = { "book" },
},
kitchen = {
name = "Kitchen",
description = "Pots and pans everywhere. Something glints under the table.",
exits = { west = "hall" },
items = { "key", "apple" },
},
vault = {
name = "Vault",
description = "Gold coins cover the floor, and a jewelled crown rests on a pedestal.",
exits = { west = "library" },
items = { "crown" },
},
}
local player = { room = "hall", inventory = {}, moves = 0 }

Step 2: Look Around & Move

look prints the room and lists its exits; because pairs visits table keys in no particular order, the exits are collected into an array and sorted so the output is always the same. move checks whether the exit is a plain string or a locked-door table using type(), and only lets the player through if player.inventory[exit.locked_by] is true.

local function look()
local room = rooms[player.room]
print("== " .. room.name .. " ==")
print(room.description)
if #room.items > 0 then
print("You see: " .. table.concat(room.items, ", "))
end
local exits = {}
for direction in pairs(room.exits) do
exits[#exits + 1] = direction
end
table.sort(exits)
print("Exits: " .. table.concat(exits, ", "))
end
local function move(direction)
local exit = rooms[player.room].exits[direction]
if exit == nil then
print("You can't go " .. direction .. " from here.")
return
end
if type(exit) == "table" then
if not player.inventory[exit.locked_by] then
print("The door is locked. You need a " .. exit.locked_by .. ".")
return
end
print("You unlock the door with the " .. exit.locked_by .. ".")
exit = exit.to
end
player.room = exit
player.moves = player.moves + 1
look()
end

Step 3: Take Items & Show the Inventory

take searches the room's items array with a numeric for loop and uses table.remove to delete the item at that position. It returns true when the crown is picked up, which is how the game loop knows the player has won.

local function take(item)
local items = rooms[player.room].items
for i, name in ipairs(items) do
if name == item then
table.remove(items, i)
player.inventory[item] = true
print("Taken: " .. item)
return item == "crown" -- taking the crown wins the game
end
end
print("There is no " .. item .. " here.")
end
local function show_inventory()
local names = {}
for name in pairs(player.inventory) do
names[#names + 1] = name
end
table.sort(names)
if #names == 0 then
print("You are carrying nothing.")
else
print("You are carrying: " .. table.concat(names, ", "))
end
end

Step 4: Parse Commands & Run the Game Loop

The pattern "^(%a+)%s*(.-)%s*$" splits a line into a first word (%a+ — one or more letters) and the rest, trimming spaces. A directions set turns a bare "north" into "go north", and the commands table maps each verb to a function, so adding a new command is one new line instead of another elseif branch.

local directions = { north = true, south = true, east = true, west = true }
local commands = {
look = function() look() end,
go = function(arg) move(arg) end,
take = function(arg) return take(arg) end,
inventory = function() show_inventory() end,
help = function()
print("Commands: look, go <direction>, <direction>, take <item>, inventory, quit")
end,
}
local function parse(line)
local verb, rest = line:lower():match("^(%a+)%s*(.-)%s*$")
if verb and directions[verb] then
return "go", verb -- "north" is short for "go north"
end
return verb, rest
end

Complete Code

adventure.lua
-- adventure.lua — run with: lua adventure.lua
local rooms = {
hall = {
name = "Great Hall",
description = "Dusty banners hang from the walls. Doors lead north and east.",
exits = { north = "library", east = "kitchen" },
items = { "torch" },
},
library = {
name = "Library",
description = "Shelves of crumbling books. A heavy iron door stands to the east.",
exits = { south = "hall", east = { to = "vault", locked_by = "key" } },
items = { "book" },
},
kitchen = {
name = "Kitchen",
description = "Pots and pans everywhere. Something glints under the table.",
exits = { west = "hall" },
items = { "key", "apple" },
},
vault = {
name = "Vault",
description = "Gold coins cover the floor, and a jewelled crown rests on a pedestal.",
exits = { west = "library" },
items = { "crown" },
},
}
local player = { room = "hall", inventory = {}, moves = 0 }
local function look()
local room = rooms[player.room]
print("== " .. room.name .. " ==")
print(room.description)
if #room.items > 0 then
print("You see: " .. table.concat(room.items, ", "))
end
local exits = {}
for direction in pairs(room.exits) do
exits[#exits + 1] = direction
end
table.sort(exits)
print("Exits: " .. table.concat(exits, ", "))
end
local function move(direction)
local exit = rooms[player.room].exits[direction]
if exit == nil then
print("You can't go " .. direction .. " from here.")
return
end
if type(exit) == "table" then
if not player.inventory[exit.locked_by] then
print("The door is locked. You need a " .. exit.locked_by .. ".")
return
end
print("You unlock the door with the " .. exit.locked_by .. ".")
exit = exit.to
end
player.room = exit
player.moves = player.moves + 1
look()
end
local function take(item)
local items = rooms[player.room].items
for i, name in ipairs(items) do
if name == item then
table.remove(items, i)
player.inventory[item] = true
print("Taken: " .. item)
return item == "crown" -- taking the crown wins the game
end
end
print("There is no " .. item .. " here.")
end
local function show_inventory()
local names = {}
for name in pairs(player.inventory) do
names[#names + 1] = name
end
table.sort(names)
if #names == 0 then
print("You are carrying nothing.")
else
print("You are carrying: " .. table.concat(names, ", "))
end
end
local directions = { north = true, south = true, east = true, west = true }
local commands = {
look = function() look() end,
go = function(arg) move(arg) end,
take = function(arg) return take(arg) end,
inventory = function() show_inventory() end,
help = function()
print("Commands: look, go <direction>, <direction>, take <item>, inventory, quit")
end,
}
local function parse(line)
local verb, rest = line:lower():match("^(%a+)%s*(.-)%s*$")
if verb and directions[verb] then
return "go", verb -- "north" is short for "go north"
end
return verb, rest
end
print("Welcome to the Castle! Type 'help' for commands.")
print()
look()
while true do
io.write("\n> ")
local line = io.read()
if line == nil then break end -- end of input
local verb, arg = parse(line)
if verb == "quit" then
print("Goodbye!")
break
end
local handler = commands[verb or ""]
if handler == nil then
print("I don't understand that. Type 'help' for commands.")
elseif (verb == "go" or verb == "take") and arg == "" then
print(verb == "go" and "Go where?" or "Take what?")
elseif handler(arg) == true then
print(("\n*** You found the crown in %d moves. You win! ***"):format(player.moves))
break
end
end

Sample Run

lua adventure.lua

Click Run to see what this code prints.

Extend This Project

  • Add a drop <item> command that moves an item from the inventory back into the current room.
  • Add a dark cellar that can only be explored while carrying the torch.
  • Give items descriptions (items = { { name = "key", text = "A small brass key." } }) and an examine <item> command.
  • Move the rooms table into its own file, world.lua, and load it with require("world") (lesson 11).
  • Add a move limit and a score that depends on how many items the player collected.

Summary

You built a data-driven game where the world is just nested tables and the logic is a handful of small functions. You used pairs and ipairs, table.sort, table.remove and table.concat, parsed free-form text with a string pattern, and replaced a long if/elseif chain with a table of functions — the same "functions in tables" idea that powers Lua's modules and objects.