Skip to content

[Tutorial] Math and Lua Tips for BoI Modding⚓︎

Lua Tips⚓︎

Iterate over tables⚓︎

The best way to iterate over a table is by using the ipairs() or pairs() functions.

ipairs⚓︎

ipairs() allows you to iterate over the table, providing the index of the element and the element value. The function does only work for tables without any keys!

1
2
3
4
local exampleTable = {"apple", 69, 1337, -3}
for i, value in ipairs(exampleTable) do
    print(i, value)
end
Result
1
2
3
4
1 apple
2 69
3 1337
4 -3

pairs⚓︎

pairs() allows you to iterate over the table, providing the key of the element and the element value. The function should be used for tables with keys defined!

Note: Tables with keys are always unsorted.

1
2
3
4
local exampleTable = {["a"]="apple", [532]=69, ["something"]=1337, ["OwO"]=-3}
for key, value in pairs(exampleTable) do
    print(key, value)
end
Result
1
2
3
4
something 1337
a apple
532 69
OwO -3

Number of Table entries⚓︎

Tables without keys⚓︎

For tables that don't have keys, you can use a simple # (Hashtag) in front of the table name to get the number of table entries.

1
2
local exampleTable = {"apple", 69, 1337, -3}
print(#exampleTable) -- Result: 4

Tables with keys⚓︎

For tables that have keys, you need to count the number of entries with a loop to get the number of table entries.

1
2
3
4
5
6
local exampleTable = {["a"]="apple", [532]=69, ["something"]=1337, ["OwO"]=-3}
local counter = 0
for _ in pairs(exampleTable) do
    counter = counter + 1
end
print(counter) -- Result: 4

0 vs 1 based indexing⚓︎

0-based indexing will loop over a collection from 0 to length-1.

1-based indexing will loop over a collection from 1 to length.

Isaac modding can be confusing because you could run into both of these scenarios within the same code file.

CppContainer collections⚓︎

These collection types are generated by the game and use 0-based indexing.

1
2
3
4
5
local roomEntities = Game():GetRoom():GetEntities() -- cppcontainer
for i = 0, #roomEntities - 1 do
    local entity = roomEntities:Get(i)
    print(entity.Type, entity.Variant, entity.SubType)
end

Lua tables⚓︎

Lua tables without keys use 1-based indexing. You can create your own tables or they could be generated by the game.

Note: This is for demonstration purposes. It's easier to use ipairs() in this case.

1
2
3
4
5
local roomEntities = Isaac.GetRoomEntities() -- table
for i = 1, #roomEntities do
    local entity = roomEntities[i]
    print(entity.Type, entity.Variant, entity.SubType)
end

Randomness⚓︎

There's two ways to grab random items with the modding API.

RNG():RandomInt(max) will give you a number from 0 to max-1.

math.random(max) will give you a number from 1 to max.

1
2
3
4
5
6
7
local items = { "item 1", "item 2", "item 3" } -- table

local rng = RNG()
rng:SetSeed(Random(), 1)
print(items[rng:RandomInt(#items) + 1])

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

Math tips⚓︎

Modulo (Remainder of a division)⚓︎

The Modulo operator % is a very powerful tool in programming, because it can save you lots of code. It basically returns the whole number that would remain from a division by a given number.

Usage⚓︎

1
local remainder = dividend % divisor
Since the remain of a division is never negative or bigger than the divisor, the Remainder will always have a value between 0 and divisor-1.

Examples
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
-- ...
-3 % 3 -- = 0
-2 % 3 -- = 1
-1 % 3 -- = 2
0 % 3 -- = 0
1 % 3 -- = 1
2 % 3 -- = 2
3 % 3 -- = 0
4 % 3 -- = 1
5 % 3 -- = 2
-- ...

local isOddNumber = 123115 % 2 -- Result = 1. Would be 0 if its an even number
local isDivideableBySeven = 123429292 % 7 -- Result = 0. Therefore, its divideable by 7

Floor Division⚓︎

While not particularly useful and easily replaceable by using math.floor, floor division can help with simplifying and organising code. It works similarly to the regular division operator, but rounds the result down to the nearest integer.

Usage⚓︎

1
local floorQuotient = dividend // divisor
It can be used in combination with the Modulo operator to recreate the dividend, as follows:
1
local dividend = (floorQuotient * divisor) + remainder
Keep in mind it always rounds to a number equal or lower than the result of the division. This means -11//5 will return -3 whereas 11//5 will retun 2. This makes it so making a number negative can generate different results if you do so before or after using the Floor Quotient operator.

Examples
1
2
3
4
5
6
7
8
-- ...
-11//2 -- = -6
11//2 -- = 5
-11//-2 -- = 5
-- ...

local floorRightAngle = 170//90 * 90 -- Result = 90. 
local roundRightAngle = (170+45)//90 - 45 -- Result = 180

Integer vs Float⚓︎

type(x) will return "number" if the variable is a number, but it won't tell you if it's an integer or float.

math.type(x) will return "integer" or "float", or nil if it's not a number.

1
2
3
4
5
6
local i = 1
local f = 1.0
print(type(i)) -- number
print(type(f)) -- number
print(math.type(i)) -- integer
print(math.type(f)) -- float

Last update: February 28, 2023