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.
- A
roomstable describing every room, its exits and the items on the floor. - A
playertable tracking the current room and an inventory. look,move,takeandshow_inventoryfunctions that read and update those tables.- A locked exit that checks the inventory before letting the player through.
- A command parser built with
string.matchand a dispatch table of command handlers. - A game loop with
io.readthat 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()endStep 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, ", ")) endendStep 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, restendComplete Code
-- adventure.lua — run with: lua adventure.lualocal 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, ", ")) endend
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, restend
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 endendSample Run
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 anexamine <item>command. - Move the
roomstable into its own file,world.lua, and load it withrequire("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.