Skip to main content

Lua Cheat Sheet

It's not necessary to understand everything on this page, but the more you understand the better you will be at debugging and collaborating.

It's best to take just what you need while creating a mod, and come back to study if you have questions.

For an official reference and documentation on standard Lua functions see: https://www.lua.org/manual/5.4/

Cheat Sheet

Variables

local a = 1

a = a + 1

print(a) -- 2

Functions

You should prefer to use local on functions (see Scopes). This does not include *_init functions they must be visible to the engine.

local function add(a, b)
return a + b
end
-- is the same as:
local add = function(a, b)
return a + b
end

-- this may create a global, seen in the next section
function add(a, b)
return a + b
end
-- is the same as:
add = function(a, b)
return a + b
end

print(add(1, 2)) -- logs 3 in the console

-- multiple return
local function add_multi(a, b, c)
return a + c, b + c
end

local a, b = add_multi(1, 2, 3)
print(a, b) -- 4 5

Globals

You should prefer to use local wherever possible (see Scopes)

local function f()
-- no local attached, modifies the outside world
a = 1

-- local used, does not modify the outside word
local b = 1
b = b + 1
end

f()
print(a, b) -- 1 nil

Scopes

Access to local variables is limited by scope. This can make it easier to debug by reducing where you need to check for errors related to that variable. It also allows you to avoid unwanted external changes from other parts (or even the same part!) of your project using a variable with the same name.

Generally if a section of code ends with end, it qualifies as a scope.

for i = 1, 5 do -- scope start
if true then -- scope start
local c = 1
print(i) -- 1-5

local i = 1
print(i) -- 1, shadows the other i variable
end -- end scope

print(i) -- 1-5
print(c) -- nil, not in scope
end -- end scope

print(i) -- nil, not in scope

Closures

Variables from a scope can escape by using a closure.

local function create_closures()
local v = 1

-- function that accesses / "captures" an external local variable
-- this is called a closure
local function inc()
-- updates v for everyone
v = v + 1
end

-- another closure
local function get()
-- sees the updated value of v, even when inc updates it later
return v
end

-- these functions escape the scope of create_closures, keeping v alive and accessible
return inc, get
end

local inc, get = create_closures()
inc()
inc()
print(get()) -- 2

local inc2, get2 = create_closures()
inc2()
print(get(), get2()) -- 2 1

Arrays and Iteration

-- logs 1 through 5
for i = 1, 5 do
print(i)
end

-- logs 5 through 1
for i = 5, 1, -1 do
print(i)
end

-- logs 1 and 3, we skipped two by incrementing by 2
for i = 1, 5, 2 do
print(i)
end

local list = {"a", "b", "c"}

print(#list) -- 3, # is used to get the length of a list

-- prints "a", then "b", then "c"
for i = 1, #list do
print(list[i])
end

-- prints "a", then "b", then "c"
for i, value in ipairs(list) do
print(value)
end

Conditions

-- nil and false are "falsy":

if nil then
print("this never executes")
end

if false then
print("this never executes")
end

-- everything else is truthy:

local f = function() end

if true and 0 and "" and {} and f then
print("this executes")
end

Guard Statements

Guard statements allow you to avoid nesting, which prevents code from travelling to the right and reduces the amount of overlapping scopes you need to be aware of while debugging.

The trade off is taller code, but it's generally easier to understand when more conditions are added.

Popular videos on the topic:

function nested(x, y)
local tile = Field.tile_at(x, y)

if tile and tile:is_walkable() then
local next_tile = tile:get_tile(tile:facing(), 1)

if next_tile and next_tile:is_walkable() then
print("hello!")
end
end
end
-- vs
function guarded(x, y)
local tile = Field.tile_at(x, y)

if tile and tile:is_walkable() then
-- the rest of the function is skipped, and no values are returned
-- making sure your inputs are valid and exiting early otherwise is called a guard
return
end

local next_tile = tile:get_tile(tile:facing(), 1)

if next_tile and next_tile:is_walkable() then
print("hello!")
end
end

for _, e in ipairs(entities) do
if not e:deleted() then
local next_tile = e:get_tile(e:facing(), 1)

if next_tile and next_tile:is_walkable() then
print("hello!")
end
end
end
-- vs
for _, e in ipairs(entities) do
if e:deleted() then
-- skips to the ::continue:: label
goto continue
end

local next_tile = e:get_tile(e:facing(), 1)

if next_tile and next_tile:is_walkable() then
print("hello!")
end

::continue::
end

Math

print(1 + 1) -- 2
print(1 - 1) -- 0
print(2 * 3) -- 6
print(10 / 3) -- 3.333
print(10 // 3) -- 3, integer division
print(5 % 3) -- 2, the remainder of division
print(2 ^ 4) -- 16.0, 2 to the power of 4

print(1 - 1 * 3) -- -2, operator precedence
print((1 - 1) * 3) -- 0

print(math.max(1, 2, 3)) -- 3, picks the larger number
print(math.min(1, 2, 3)) -- 1, picks the smaller number

print(math.ceil(1.1)) -- 2
print(math.floor(1.1)) -- 1

-- bitwise operations, lua works on 64 bit numbers, but we'll use 4 bits to simplify
-- 1 = 0001
-- 2 = 0010
-- 3 = 0011
-- 4 = 0100
-- 5 = 0101
-- 6 = 0110
-- 7 = 0111

-- bitwise and, it's like an `and` operation on every bit
print(1 & 1) -- 1, last bit was 1 in both
print(1 & 2) -- 0, no bits were 1 in the same column
print(1 & 3) -- 1, the last bit was 1 in both
print(3 & 2) -- 2, the second to last bit was 1 in both
print(3 & 7) -- 3, every bit in 3 matched against a bit in 7

-- bitwise or, similar to an `or` operation on all bits
print(1 | 2) -- 3, enabled the last two bits
print(1 | 3) -- 3, the matching bit was already on in 3
print(3 | 2) -- 3, the matching bit was already on in 3

-- unary bitwise not, similar to `not value` on all bits
print(~0) -- inverts bits, every bit is on in this number
print(~1) -- every bit except the last bit is on

-- bitwise xor
print(3 ~ 2) -- 1, the matching bit was disabled and preserved the rest
print(3 ~ 1) -- 2, the matching bit was disabled and preserved the rest
print(7 ~ 3) -- 4, disabled matching bits and preserved the rest
print(3 ~ 7) -- 4, disabled matching bits and preserved the rest

-- combining bitwise `and` with `not` to disable bits
print(7 & ~1) -- 6, inverted 1 so only other bits could pass / every bit in 1 was disabled
print(7 & ~3) -- 4, inverted 3 so only other bits could pass / every bit in 3 was disabled

-- bit shift
print(6 >> 1) -- 3, shifted bits to the right by 1: 0110 -> 0011
print(4 >> 2) -- 1, shifted bits to the right by 2: 0110 -> 0001
print(1 << 1) -- 2, shifted bits to the left by 1: 0001 -> 0010
print(1 << 2) -- 4, shifted bits to the left by 2: 0001 -> 0100

-- hexadecimal
print(0xff) -- 255

String Concatination

print("a".."b") -- "ab"
print("a"..1) -- "a1"
print(1.."b") -- error, but due to 1. parsing as 1.0
print(1 .."b") -- "1b", any separation or avoiding a literal value helps
print("a"..false) -- everything else errors

-- print accepts multiple values and will separate with a tab
print("a", "b") -- ~"a b"

Removing values from a list while iterating

Watch out for this issue.

local list = {"a", "b", "c"}

-- iterating in reverse to avoid issues with and reduced performance from shifting O(n)
-- prints "c" -> "b" -> "a"
for i = #list, 1, -1 do
print(list[i])
table.remove(list, i)
end

list = {"a", "b", "c"}

-- advanced: swap remove, zero performance penalty from shifting: O(1), but this can change the order of your list
-- prints "c" -> "b" -> "a"
for i = #list, 1, -1 do
local value = list[i]

list[i] = list[#list] -- move the last value to the current index
list[#list] = nil -- setting the last element to nil shrinks the list

print(value)
end

Appending to a list

local list = {}

table.insert(list, "a") -- appends to the end
table.insert(list, "b") -- appends to the end
table.insert(list, 1, "c") -- inserts at 1, shifts everything at that position to the right
list[#list + 1] = "d" -- also appends to the end by setting the value at the next index
list[#list + 2] = "e" -- creates a gap and does not properly append

-- "c" -> "a" -> "b" -> "d"
for _, value in ipairs(list) do
print(value)
end

Random value from a list

local list = {"a", "b", "c"}

print(list[math.random(#list)])

Wrapping values from a list

local list = {"a", "b", "c"}

-- "a" -> "b" -> "c" repeating
for i = 1, 6 do
print(list[(i - 1) % #list + 1])
end

-- same output as above
for i = 0, 5 do
print(list[i % #list + 1])
end

-- same output as above
local i = 1

for _ = 1, 6 do
print(list[i])
-- resolve the next index
i = i % #list + 1
end

Tables

Similar syntax to lists, as lists are a feature of tables.

Tables can store values with a key. If the key is a number within a sequence starting at 1, it will be stored in the "list" part of the table.

You can delete keys by setting its value to nil.

local key = "b"
local t = { a = 1, [key] = 2, ["Key with spaces"] = 3 }

print(t["a"], t["b"], t["Key with spaces"]) -- 1 2 3
print(t.a, t.b) -- 1 2

-- defining a function on a table key, note adding local is unnecessary and will cause errors
-- as the function will be already tied to the table
t.f = function() return "hi" end
-- same as:
function t.f() return "hi" end

print(t.f()) -- "hi"


-- using a colon creates a parameter called self
function t:f() return self.a end
print(t.f(t))
-- same as
function t.f(self) return self.a end
-- same as
function t.f(input) return input.a end
print(t.f(t))

-- using a colon when calling this function will assign the table to the first parameter
print(t:f())

Common Bugs

Index Zero

In most programming languages, lists start at zero. In lua lists start at one.

local list = { 5 }
print(list[0]) -- nil
print(list[1]) -- 5

Zero Indexed Wrapping

For correct examples, click here.

-- attempting to wrap around the list as if we're using a zero indexed language
print(list[3 % #list]) -- nil

Attempting to remove items while iterating

For correct examples, click here.

local list = {"a", "b", "c"}

-- incorrect: prints "a" -> "c" -> "nil", before creating an out of bounds error
for i = 1, #list do
print(list[i])
table.remove(list, i)
end

Global Closure Clash

For correct examples, click here.

local function create_closure()
local v = 0

-- note: given a name and missing local
-- this is the same as `get_and_inc = function()`, meaning we're working with a global variable
function get_and_inc()
v = v + 1
return v
end

return function()
-- since local wasn't used, we're really accessing a global here
return get_and_inc()
end
end

local get_and_inc1 = create_closure()
local get_and_inc2 = create_closure()

print(get_and_inc1()) -- 1
print(get_and_inc1()) -- 2
print(get_and_inc2()) -- 3 this isn't intended!