Create: Computing AgesBrass Docs
The Brass language

Conditions and loops

Make decisions with if, repeat work with while, repeat and for, and write the main loop of a program that never stops.

A program runs its lines from top to bottom. Conditions choose which lines run, loops run lines several times. With these two tools, a computer can watch a vault, count passing trains, or step a Mechanical Press line through its cycle. This page assumes you know values and variables.

Brass
for seconds = 3, 1, -1 do
  print("airship launch in " .. seconds)
  sleep(1)
end
print("liftoff!")
Screen
airship launch in 3
airship launch in 2
airship launch in 1
liftoff!

if, elseif, else

if runs a block only when a condition is true:

Brass
local stock = 40
if stock < 64 then
  print("low stock: start the Mechanical Press")
end
Screen
low stock: start the Mechanical Press

The shape is always if condition then ... end. The condition can be any expression; remember that only false and nil count as false (truth in Brass), so if 0 then runs.

else gives the block to run otherwise, and elseif adds more cases. Brass tests the conditions from the top and runs the block of the first one that is true; the others are skipped, even if they are true too. So order matters: test the strictest case first.

Brass
local fill = 0.85   -- the vault is 85 % full
if fill >= 1 then
  print("FULL: stop the drills")
elseif fill >= 0.75 then
  print("almost full")
elseif fill <= 0.1 then
  print("almost empty")
else
  print("ok")
end
Screen
almost full

Had fill >= 0.75 come first, a full vault would only say "almost full".

  • elseif is one word. else if opens a second if inside the else, which then needs its own end.
  • if x = 5 then is a compile error ('then' expected near '='): comparing is ==.
  • Short blocks fit on one line: if powered then rs.set("top", true) end.
  • Several conditions combine with and, or and not: if fuel > 0 and not stopped then.

while

while repeats a block as long as its condition is true. The condition is checked before each round, so the block may run zero times.

Brass
local items = 1000
local stacks = 0
while items >= 64 do
  items = items - 64
  stacks = stacks + 1
end
print(stacks .. " stacks, " .. items .. " left over")
Screen
15 stacks, 40 left over

Something in the block must eventually make the condition false, or the loop never ends. That is sometimes what you want (see main loops), and Ctrl+T always stops a program.

repeat ... until

repeat ... until condition runs the block first and checks afterwards, so it always runs at least once. It stops when the condition becomes true (the opposite of while). The condition can use the local variables declared inside the block.

Brass
local tries = 0
repeat
  tries = tries + 1
  local moved = tries * 16   -- each try moves 16 items
until moved >= 64
print("done after " .. tries .. " tries")
Screen
done after 4 tries

The classic use is waiting for something, checking every half second. Here, a car on an Inductive Loop Detector (inductive_loop.detected()):

Brass
local loop = peripheral.wrap("right")
repeat
  sleep(0.5)
until loop.detected()
print("a car is waiting")

Counting: the numeric for

for i = first, last do ... end runs the block once for each number from first to last, both included. A third number sets the step; a negative step counts down.

Brass
for floor = 1, 3 do
  print("elevator at floor " .. floor)
end
for t = 10, 1, -4 do
  write(t, " ")
end
print()
for x = 0, 1, 0.25 do
  write(x, " ")
end
print()
Screen
elevator at floor 1
elevator at floor 2
elevator at floor 3
10 6 2
0 0.25 0.5 0.75 1

What to know about it:

  • first, last and the step are computed once, before the first round. Changing last inside the loop does not change the number of rounds.
  • The loop variable is a local of the loop, a copy of the counter: changing it in the block does not change the next round. After end it no longer exists.
  • When first is already past last, the block does not run at all: for i = 1, #list on an empty list is safe.
  • The step cannot be zero ('for' step is zero), and all three values must be numbers ('for' initial value must be a number).
Brass
for i = 1, 3 do
  i = i * 10   -- changes this round's copy only
  print(i)
end
print(i)       -- the loop variable is gone
Screen
10
20
30
nil
For Lua programmers

A loop written at the top level of a file (outside any function) keeps its variable in a chunk-level slot, like any chunk-level local. A function created inside such a loop does not get its own copy of the variable: it reads the slot, which holds the last value once the loop is over. Inside a function, the same code is a compile error (see the scoping rule).

Brass
local handlers = {}
for i = 1, 3 do
  handlers[i] = function() return i end
end
print(handlers[1](), handlers[3]())
Screen
3   3

Going through a table: pairs, ipairs, for v in

To visit the elements of a table, use a for ... in loop:

loopvisits
for i, v in ipairs(list) dopositions 1, 2, 3... with their value, until the first nil
for v in list dothe same values, without the position (a Brass shortcut)
for k, v in pairs(t) doevery key and its value: the list part first, in order, then the other keys in the order they were added
Brass
local players = {"Steve", "Alex", "Notch"}
for i, name in ipairs(players) do
  print(i, name)
end
for name in players do
  write(name, " ")
end
print()
local speeds = {press = 64, mixer = 128, saw = 32}
for machine, rpm in pairs(speeds) do
  print(machine, rpm)
end
Screen
1   Steve
2   Alex
3   Notch
Steve Alex Notch
press   64
mixer   128
saw 32
  • for k, v in t (a table with two variables) is an error: use pairs() or ipairs() to iterate a table with two variables. Choose: ipairs for a list, pairs for anything else.
  • _ is the usual name for a variable you do not need: for _, item in ipairs(items) do.
  • A generic for takes at most two variables.
  • Changing or removing existing keys while pairs runs is safe; keys added during the loop may not be visited. Removing elements from a list while going through it forwards skips elements: go backwards instead (see lists).

Leaving a loop: break

break leaves the loop it is in, right away, and the program continues after its end. It is how you stop searching once you have found what you were looking for. Here, the first empty slot of a 9-slot chest:

Brass
-- what chest.get(slot) returns for each slot: nil means empty
local slots = {}
slots[1] = {name = "minecraft:cobblestone", count = 64}
slots[2] = {name = "minecraft:cobblestone", count = 64}
slots[4] = {name = "minecraft:iron_ingot", count = 3}
local size = 9

local free = nil
for slot = 1, size do
  if slots[slot] == nil then
    free = slot
    break
  end
end
print("first empty slot: " .. tostring(free))
Screen
first empty slot: 3

The loop counts up to size rather than #slots, because a table with gaps has no reliable length. With a real chest, the same loop reads for slot = 1, chest.size() do and tests chest.get(slot) == nil (inventory.size(), inventory.get()). If no slot is free, free stays nil, which is why it is printed through tostring.

break only leaves the innermost loop. And Brass has no continue: to skip the rest of a round, put that rest inside an if:

Brass
for _, item in ipairs({"coal", "", "iron", "gold"}) do
  if item ~= "" then        -- skips empty names
    write(item, " ")
  end
end
print()
Screen
coal iron gold

break outside a loop is a compile error: 'break' outside a loop.

Loops inside loops

A loop can contain another loop: the inner one runs completely for each round of the outer one. That is how you go through a grid, rows then columns. A wall of lamps lit in a checkerboard:

Brass
for row = 1, 3 do
  local line = ""
  for col = 1, 8 do
    if (row + col) % 2 == 0 then
      line = line .. "#"
    else
      line = line .. "."
    end
  end
  print(line)
end
Screen
#.#.#.#.
.#.#.#.#
#.#.#.#.

To leave both loops at once, set a variable before the inner break and test it in the outer loop (or put the loops in a function and return from it):

Brass
local map = {
  {"stone", "stone", "coal"},
  {"stone", "diamond", "stone"},
  {"iron", "stone", "stone"},
}
local found = nil
for row = 1, #map do
  for col = 1, #map[row] do
    if map[row][col] == "diamond" then
      found = row .. "," .. col
      break                 -- leaves the inner loop
    end
  end
  if found then break end   -- leaves the outer loop
end
print("diamond at " .. found)
Screen
diamond at 2,2

Blocks: do ... end

do ... end makes a block and nothing else. Locals declared inside disappear at its end, which keeps temporary variables out of the way. At the top level of a file it has a useful side effect: a function defined inside can keep using the block's locals (they are chunk-level), while the rest of the file cannot see them.

Brass
do
  local count = 0            -- hidden from the rest of the file
  function next_ticket()
    count = count + 1
    return count
  end
end
print(next_ticket(), next_ticket(), next_ticket())
print(count)
Screen
1   2   3
nil

Stopping early

A return at the top level of a file ends the program, there and then. It is the simplest way to give up when something is missing, before the main part of the program:

Brass
local chest = peripheral.wrap("left")
if chest == nil then
  print("no chest on the left")
  return
end
print("watching " .. chest.size() .. " slots")

return must be the last statement of its block, which is why it sits inside the if. error("no chest on the left") stops the program too, but prints the message in red as an error (see Errors). Inside a function, return leaves only that function: see Functions.

Main loops: programs that never end

Most programs in a world never finish: they watch, react, and start again. They are a while true do ... end loop around the work, with a pause in it. There are two kinds of pause.

Sleeping a fixed time with sleep, to check something regularly (a vault, a tank, the time of day):

Brass
local chest = peripheral.wrap("left")
while true do
  local iron = chest.count("minecraft:iron_ingot")
  rs.set("top", iron < 64)   -- alarm lamp when iron runs low
  sleep(5)                   -- look again in 5 seconds
end

Waiting for an event with os.pull_event, to react the moment something happens (a redstone change, a key, a message, a timer). The computer sleeps until then:

Brass
local count = 0
while true do
  os.pull_event("redstone")   -- sleeps until a redstone input changes
  if rs.get("left") > 0 then
    count = count + 1
    print("trains so far: " .. count)
  end
end

Events are explained in Events, timers and clocks in Time. A loop over events can be tried without any block around: os.queue_event adds events to the computer's own queue.

Brass
os.queue_event("order", "iron")
os.queue_event("order", "gold")
os.queue_event("stop")
while true do
  local event = os.pull_event()
  if event.name == "stop" then break end
  print("crafting " .. event.data)
end
print("orders done")
Screen
crafting iron
crafting gold
orders done
What about a loop with no pause?

It does not freeze the game or lag the server. Each computer runs a fixed budget of instructions per tick (1,200 for a Personal Computer at 256 RPM, see Computers); when the budget is spent, the program simply pauses until the next tick and goes on from there. An endless loop just spreads over time.

But it wastes the computer's time: a loop that checks a lever without pausing asks the same question hundreds of times per tick, while the lever can only change once per tick, and the server spends real time running it. A sleep, even a short one (sleep(0.05) is one tick), or an os.pull_event lets the computer rest until there is something to do. See Performance.

sleep counts in seconds and rounds up to whole ticks (a tick is 1/20 of a second), with a minimum of one tick.

A complete example: a sequencer

A sequencer turns redstone outputs on and off in a fixed order, like the sequencer example. The steps live in a list, and a for loop walks through it:

Brass
local steps = {
  {side = "left", time = 1},    -- push the piston
  {side = "right", time = 2},   -- run the press
  {side = "top", time = 1},     -- open the gate
}
for i, step in ipairs(steps) do
  print("step " .. i .. ": " .. step.side .. " for " .. step.time .. " s")
  rs.set(step.side, true)
  sleep(step.time)
  rs.set(step.side, false)
end
Screen
step 1: left for 1 s
step 2: right for 2 s
step 3: top for 1 s

To repeat the cycle forever, wrap the for in while true do ... end. Adding a step is now adding a line to the list, not rewriting the program. That idea, keeping the data apart from the code that walks through it, comes back in Tables.