Create: Computing AgesDoc Brass
Le langage Brass

Conditions et boucles

Prendre des décisions avec if, répéter avec while, repeat et for, et écrire la boucle principale d'un programme qui ne s'arrête jamais.

Un programme exécute ses lignes de haut en bas. Les conditions choisissent quelles lignes s'exécutent, les boucles exécutent des lignes plusieurs fois. Avec ces deux outils, un ordinateur peut surveiller un coffre à objet renforcé, compter les trains qui passent ou faire tourner le cycle d'une chaîne de presses mécaniques. Cette page suppose que vous connaissez les valeurs et les variables.

Brass
for secondes = 3, 1, -1 do
  print("décollage du dirigeable dans " .. secondes)
  sleep(1)
end
print("décollage !")
Écran
décollage du dirigeable dans 3
décollage du dirigeable dans 2
décollage du dirigeable dans 1
décollage !

if, elseif, else

if n'exécute un bloc que si une condition est vraie :

Brass
local stock = 40
if stock < 64 then
  print("stock bas : démarrez la presse mécanique")
end
Écran
stock bas : démarrez la presse mécanique

La forme est toujours if condition then ... end. La condition peut être n'importe quelle expression ; rappelez-vous que seuls false et nil comptent comme faux (le vrai et le faux en Brass) : if 0 then s'exécute.

else donne le bloc à exécuter sinon, et elseif ajoute d'autres cas. Brass teste les conditions depuis le haut et exécute le bloc de la première qui est vraie ; les autres sont sautées, même si elles sont vraies aussi. L'ordre compte donc : testez le cas le plus strict en premier.

Brass
local remplissage = 0.85   -- le coffre renforcé est plein à 85 %
if remplissage >= 1 then
  print("PLEIN : arrêtez les perceuses")
elseif remplissage >= 0.75 then
  print("presque plein")
elseif remplissage <= 0.1 then
  print("presque vide")
else
  print("ok")
end
Écran
presque plein

Si remplissage >= 0.75 venait en premier, un coffre renforcé plein dirait seulement « presque plein ».

  • elseif s'écrit en un mot. else if ouvre un second if dans le else, qui demande alors son propre end.
  • if x = 5 then est une erreur de compilation ('then' expected near '=') : comparer s'écrit ==.
  • Les blocs courts tiennent sur une ligne : if alimente then rs.set("top", true) end.
  • Plusieurs conditions se combinent avec and, or et not : if carburant > 0 and not arrete then.

while

while répète un bloc tant que sa condition est vraie. La condition est vérifiée avant chaque tour : le bloc peut donc ne jamais s'exécuter.

Brass
local objets = 1000
local piles = 0
while objets >= 64 do
  objets = objets - 64
  piles = piles + 1
end
print(piles .. " piles, " .. objets .. " en trop")
Écran
15 piles, 40 en trop

Quelque chose dans le bloc doit finir par rendre la condition fausse, sinon la boucle ne s'arrête jamais. C'est parfois voulu (voir les boucles principales), et Ctrl+T arrête toujours un programme.

repeat ... until

repeat ... until condition exécute le bloc d'abord et vérifie ensuite : il s'exécute donc au moins une fois. Il s'arrête quand la condition devient vraie (l'inverse de while). La condition peut utiliser les variables local déclarées dans le bloc.

Brass
local essais = 0
repeat
  essais = essais + 1
  local deplaces = essais * 16   -- chaque essai déplace 16 objets
until deplaces >= 64
print("fini en " .. essais .. " essais")
Écran
fini en 4 essais

L'usage classique est l'attente de quelque chose, en regardant toutes les demi-secondes. Ici, une voiture sur une Boucle de détection (inductive_loop.detected()) :

Brass
local boucle = peripheral.wrap("right")
repeat
  sleep(0.5)
until boucle.detected()
print("une voiture attend")

Compter : le for numérique

for i = premier, dernier do ... end exécute le bloc une fois pour chaque nombre de premier à dernier, les deux compris. Un troisième nombre fixe le pas ; un pas négatif compte à rebours.

Brass
for etage = 1, 3 do
  print("ascenseur à l'étage " .. etage)
end
for t = 10, 1, -4 do
  write(t, " ")
end
print()
for x = 0, 1, 0.25 do
  write(x, " ")
end
print()
Écran
ascenseur à l'étage 1
ascenseur à l'étage 2
ascenseur à l'étage 3
10 6 2
0 0.25 0.5 0.75 1

Ce qu'il faut savoir :

  • premier, dernier et le pas sont calculés une seule fois, avant le premier tour. Changer dernier dans la boucle ne change pas le nombre de tours.
  • La variable de boucle est une locale de la boucle, une copie du compteur : la modifier dans le bloc ne change pas le tour suivant. Après end, elle n'existe plus.
  • Quand premier a déjà dépassé dernier, le bloc ne s'exécute pas du tout : for i = 1, #liste sur une liste vide ne pose aucun problème.
  • Le pas ne peut pas valoir zéro ('for' step is zero), et les trois valeurs doivent être des nombres ('for' initial value must be a number).
Brass
for i = 1, 3 do
  i = i * 10   -- ne change que la copie de ce tour
  print(i)
end
print(i)       -- la variable de boucle n'existe plus
Écran
10
20
30
nil
Pour les habitués de Lua

Une boucle écrite au premier niveau d'un fichier (hors de toute fonction) garde sa variable dans un emplacement du fichier, comme toute locale de fichier. Une fonction créée dans une telle boucle n'a pas sa propre copie de la variable : elle lit l'emplacement, qui contient la dernière valeur une fois la boucle finie. Dans une fonction, le même code est une erreur de compilation (voir la règle de portée).

Brass
local actions = {}
for i = 1, 3 do
  actions[i] = function() return i end
end
print(actions[1](), actions[3]())
Écran
3   3

Parcourir une table : pairs, ipairs, for v in

Pour passer en revue les éléments d'une table, on utilise une boucle for ... in :

boucleparcourt
for i, v in ipairs(liste) doles positions 1, 2, 3... avec leur valeur, jusqu'au premier nil
for v in liste doles mêmes valeurs, sans la position (un raccourci de Brass)
for k, v in pairs(t) dochaque clé et sa valeur : la partie liste d'abord, dans l'ordre, puis les autres clés dans l'ordre où elles ont été ajoutées
Brass
local joueurs = {"Steve", "Alex", "Notch"}
for i, nom in ipairs(joueurs) do
  print(i, nom)
end
for nom in joueurs do
  write(nom, " ")
end
print()
local vitesses = {presse = 64, mixeur = 128, scie = 32}
for machine, rpm in pairs(vitesses) do
  print(machine, rpm)
end
Écran
1   Steve
2   Alex
3   Notch
Steve Alex Notch
presse  64
mixeur  128
scie    32
  • for k, v in t (une table avec deux variables) est une erreur : use pairs() or ipairs() to iterate a table with two variables. Choisissez : ipairs pour une liste, pairs pour tout le reste.
  • _ est le nom habituel d'une variable dont on n'a pas besoin : for _, objet in ipairs(objets) do.
  • Un for générique prend au plus deux variables.
  • Modifier ou supprimer des clés existantes pendant un pairs ne pose pas de problème ; les clés ajoutées pendant la boucle peuvent ne pas être visitées. Retirer des éléments d'une liste en la parcourant vers l'avant en fait sauter : parcourez-la à l'envers (voir les listes).

Sortir d'une boucle : break

break quitte la boucle où il se trouve, tout de suite, et le programme reprend après son end. C'est ainsi qu'on arrête de chercher une fois qu'on a trouvé. Ici, le premier emplacement vide d'un coffre de 9 emplacements :

Brass
-- ce que coffre.get(emplacement) renvoie pour chaque emplacement : nil = vide
local emplacements = {}
emplacements[1] = {name = "minecraft:cobblestone", count = 64}
emplacements[2] = {name = "minecraft:cobblestone", count = 64}
emplacements[4] = {name = "minecraft:iron_ingot", count = 3}
local taille = 9

local libre = nil
for n = 1, taille do
  if emplacements[n] == nil then
    libre = n
    break
  end
end
print("premier emplacement vide : " .. tostring(libre))
Écran
premier emplacement vide : 3

La boucle compte jusqu'à taille et pas jusqu'à #emplacements, car une table à trous n'a pas de longueur fiable. Avec un vrai coffre, la même boucle s'écrit for n = 1, coffre.size() do et teste coffre.get(n) == nil (inventory.size(), inventory.get()). Si aucun emplacement n'est libre, libre reste à nil, d'où le tostring à l'affichage.

break ne quitte que la boucle la plus intérieure. Et Brass n'a pas de continue : pour sauter la fin d'un tour, mettez cette fin dans un if :

Brass
for _, objet in ipairs({"charbon", "", "fer", "or"}) do
  if objet ~= "" then        -- saute les noms vides
    write(objet, " ")
  end
end
print()
Écran
charbon fer or

break hors d'une boucle est une erreur de compilation : 'break' outside a loop.

Des boucles dans des boucles

Une boucle peut en contenir une autre : la boucle intérieure s'exécute en entier à chaque tour de la boucle extérieure. C'est ainsi qu'on parcourt une grille, les lignes puis les colonnes. Un mur de lampes allumées en damier :

Brass
for ligne = 1, 3 do
  local texte = ""
  for colonne = 1, 8 do
    if (ligne + colonne) % 2 == 0 then
      texte = texte .. "#"
    else
      texte = texte .. "."
    end
  end
  print(texte)
end
Écran
#.#.#.#.
.#.#.#.#
#.#.#.#.

Pour quitter les deux boucles d'un coup, remplissez une variable avant le break intérieur et testez-la dans la boucle extérieure (ou mettez les boucles dans une fonction et sortez-en avec return) :

Brass
local carte = {
  {"pierre", "pierre", "charbon"},
  {"pierre", "diamant", "pierre"},
  {"fer", "pierre", "pierre"},
}
local trouve = nil
for ligne = 1, #carte do
  for colonne = 1, #carte[ligne] do
    if carte[ligne][colonne] == "diamant" then
      trouve = ligne .. "," .. colonne
      break                  -- quitte la boucle intérieure
    end
  end
  if trouve then break end   -- quitte la boucle extérieure
end
print("diamant en " .. trouve)
Écran
diamant en 2,2

Les blocs : do ... end

do ... end crée un bloc, rien de plus. Les locales déclarées dedans disparaissent à son end, ce qui range les variables temporaires. Au premier niveau d'un fichier, il a un effet bien pratique : une fonction définie dedans peut continuer à utiliser les locales du bloc (ce sont des locales de fichier), alors que le reste du fichier ne les voit pas.

Brass
do
  local compte = 0            -- caché au reste du fichier
  function ticket_suivant()
    compte = compte + 1
    return compte
  end
end
print(ticket_suivant(), ticket_suivant(), ticket_suivant())
print(compte)
Écran
1   2   3
nil

S'arrêter plus tôt

Un return au premier niveau d'un fichier termine le programme, sur-le-champ. C'est la façon la plus simple d'abandonner quand quelque chose manque, avant la partie principale du programme :

Brass
local coffre = peripheral.wrap("left")
if coffre == nil then
  print("pas de coffre à gauche")
  return
end
print("je surveille " .. coffre.size() .. " emplacements")

return doit être la dernière instruction de son bloc, d'où sa place dans le if. error("pas de coffre à gauche") arrête aussi le programme, mais affiche le message en rouge, comme une erreur (voir Erreurs). Dans une fonction, return ne quitte que cette fonction : voir Fonctions.

Les boucles principales : des programmes qui ne finissent jamais

La plupart des programmes d'un monde ne se terminent jamais : ils surveillent, réagissent, et recommencent. C'est une boucle while true do ... end autour du travail, avec une pause dedans. Il y a deux sortes de pause.

Dormir un temps fixe avec sleep, pour vérifier quelque chose régulièrement (un coffre renforcé, un réservoir, l'heure du jour) :

Brass
local coffre = peripheral.wrap("left")
while true do
  local fer = coffre.count("minecraft:iron_ingot")
  rs.set("top", fer < 64)   -- lampe d'alarme quand le fer manque
  sleep(5)                  -- on regarde à nouveau dans 5 secondes
end

Attendre un évènement avec os.pull_event, pour réagir dès que quelque chose se passe (un changement de redstone, une touche, un message, un minuteur). L'ordinateur dort jusque-là :

Brass
local compte = 0
while true do
  os.pull_event("redstone")   -- dort jusqu'à ce qu'une entrée de redstone change
  if rs.get("left") > 0 then
    compte = compte + 1
    print("trains passés : " .. compte)
  end
end

Les évènements sont expliqués dans Évènements, les minuteurs et les horloges dans Le temps. Une boucle d'évènements s'essaie sans aucun bloc autour : os.queue_event ajoute des évènements dans la file de l'ordinateur lui-même.

Brass
os.queue_event("commande", "fer")
os.queue_event("commande", "or")
os.queue_event("fin")
while true do
  local evenement = os.pull_event()
  if evenement.name == "fin" then break end
  print("fabrication : " .. evenement.data)
end
print("commandes terminées")
Écran
fabrication : fer
fabrication : or
commandes terminées
Et une boucle sans pause ?

Elle ne fige pas le jeu et ne fait pas ramer le serveur. Chaque ordinateur exécute un budget fixe d'instructions par tick (1 200 pour un Micro-ordinateur à 256 tr/min, voir Ordinateurs) ; quand le budget est épuisé, le programme se met simplement en pause jusqu'au tick suivant et reprend là où il en était. Une boucle sans fin s'étale juste dans le temps.

Mais elle gaspille le temps de l'ordinateur : une boucle qui surveille un levier sans pause pose la même question des centaines de fois par tick, alors que le levier ne peut changer qu'une fois par tick, et le serveur passe du vrai temps à l'exécuter. Un sleep, même court (sleep(0.05) dure un tick), ou un os.pull_event laisse l'ordinateur se reposer jusqu'à ce qu'il y ait quelque chose à faire. Voir Performances.

sleep compte en secondes et arrondit au tick supérieur (un tick dure 1/20 de seconde), avec un minimum d'un tick.

Un exemple complet : un séquenceur

Un séquenceur allume et éteint des sorties de redstone dans un ordre fixé, comme l'exemple sequencer. Les étapes sont rangées dans une liste, et une boucle for la parcourt :

Brass
local etapes = {
  {cote = "left", duree = 1},    -- pousser le piston
  {cote = "right", duree = 2},   -- faire tourner la presse
  {cote = "top", duree = 1},     -- ouvrir la porte
}
for i, etape in ipairs(etapes) do
  print("étape " .. i .. " : " .. etape.cote .. " pendant " .. etape.duree .. " s")
  rs.set(etape.cote, true)
  sleep(etape.duree)
  rs.set(etape.cote, false)
end
Écran
étape 1 : left pendant 1 s
étape 2 : right pendant 2 s
étape 3 : top pendant 1 s

Pour répéter le cycle à l'infini, entourez le for d'un while true do ... end. Ajouter une étape revient maintenant à ajouter une ligne à la liste, pas à réécrire le programme. Cette idée, séparer les données du code qui les parcourt, revient dans Tables.