One file is fine until it is four hundred lines. Lua's answer is small and, by now, predictable: a module is a table that a file returns. There is no module keyword and no export list.
-- bay.lua - a module is just a table you return
local bay = {}
bay.capacity = 6local docked = 0
function bay.dock(count) docked = math.min(bay.capacity, docked + count) return dockedend
function bay.status() return docked .. " of " .. bay.capacityend
return bayEvery idea in that file is one you already have. A table (1.10). Functions stored in it (1.7). A local captured by those functions, which makes it private (1.8). And the last line hands the table to whoever asked.
local bay = require("bay")
print(bay.status())0 of 6require("bay") finds bay.lua next to your file, runs it once, and gives you what it returned. Note the name has no .lua on it.
"Runs it once" is the part worth remembering. Require the same module from five files and the file executes a single time; everyone gets the same table, and so everyone shares its state. That is usually what you want and occasionally a surprise.
The variable name on your side is yours to choose — local dock = require('bay') works just as well. The module has no say in what you call it, which is a nice consequence of it being an ordinary value.
Almost nothing in Lua stops your program. A missing variable is nil, a missing table key is nil, a wrong argument count is tolerated. You have been shown this repeatedly, and it is why Lua bugs tend to surface far away from their cause.
error is how you stop on purpose, and pcall — protected call — is how you run something that might fail without going down with it.
local ok, err = pcall(function() error("reactor offline", 0)end)
print(ok, err)false reactor offlinepcall always returns at least two things: whether it survived, and then either the results or the error message. It is the multiple returns from Activity 1.7 doing something genuinely load-bearing.
The 0 in error("...", 0) means do not attach a file and line number. Leave it out and you get main.lua:2: reactor offline, which is usually more useful — you will see both forms in the checkpoint.
assert is error with the condition built in: carry on if the value is truthy, stop with your message if it is not.
local function readCharge(source) assert(type(source) == "table", "expected a table") return source.charge or 0end
print(readCharge({ charge = 98 }))print(readCharge({}))980Use it at the top of a function, on the arguments. That is where an assert earns its keep — it moves the failure from wherever the bad value eventually caused trouble to the moment it arrived, which is the difference between a two-minute bug and an hour.
Remember Activity 1.5 while you write them. assert(count) passes when count is 0, because zero is truthy — that is the check you wanted. But it also passes for a string, a table, or anything else that is not nil, so assert what you actually mean: assert(type(count) == "number").
Do not reach for pcall everywhere. Wrapping your whole program in one turns a crash with a line number into silence, which is worse. Use it where failure is expected and recoverable — reading a save file that might not exist, loading an image that might be missing — and let genuine bugs crash loudly while you can still see them.
This activity needs two files in the same folder. Create bay.lua with the module shown above, then start a fresh main.lua beside it.
capacity.bay.docked from outside. It is nil — the counter is a local inside the module, so nothing out here can reach it. That is a closure doing exactly what Activity 1.8 said it would.-- main.lua - Skynest bay controller
local bay = require("bay")
print("Capacity:", bay.capacity)
bay.dock(2)print("Status:", bay.status())
bay.dock(9)print("Status:", bay.status())
print("Hidden counter:", bay.docked)Capacity: 6Status: 2 of 6Status: 6 of 6Hidden counter: nilreadCharge(source) that asserts its argument is a table, then returns source.charge or 0.-- main.lua - Skynest bay controller
local bay = require("bay")
print("Capacity:", bay.capacity)
bay.dock(2)print("Status:", bay.status())
bay.dock(9)print("Status:", bay.status())
print("Hidden counter:", bay.docked)
local function readCharge(source) assert(type(source) == "table", "expected a table") return source.charge or 0end
print("Charge:", readCharge({ charge = 98 }))print("Charge:", readCharge({}))Capacity: 6Status: 2 of 6Status: 6 of 6Hidden counter: nilCharge: 98Charge: 0readCharge with a string through pcall, and print both values it returns.pcall, this time with 0 as the second argument.0 made.-- main.lua - Skynest bay controller
local bay = require("bay")
print("Capacity:", bay.capacity)
bay.dock(2)print("Status:", bay.status())
bay.dock(9)print("Status:", bay.status())
print("Hidden counter:", bay.docked)
local function readCharge(source) assert(type(source) == "table", "expected a table") return source.charge or 0end
print("Charge:", readCharge({ charge = 98 }))print("Charge:", readCharge({}))
local ok, err = pcall(readCharge, "not a table")print("Survived:", ok, err)
local ok2, err2 = pcall(function() error("reactor offline", 0)end)print("Survived:", ok2, err2)undock(count) function to the module that refuses to go below zero, and give it its own assert.Your finished file should produce this.
Capacity: 6Status: 2 of 6Status: 6 of 6Hidden counter: nilCharge: 98Charge: 0Survived: false main.lua:16: expected a tableSurvived: false reactor offlineThat is Unit 1. You have the whole language: five types, three loops, functions that return several things, closures, one data structure doing five jobs, and a way to split a program across files. Nothing has been held back for later.
Unit 2 adds a window, a game loop, and a way to draw. It adds no new Lua. Every enemy will be an instance from Activity 1.11, every list of bullets will be walked backwards as in Activity 1.9, and every timer will be a closure from Activity 1.8.