WENOPAL

L03 · Decisiones y bucles

if/elseif/else, comparaciones, and/or y su trampa, for con paso negativo, while, repeat-until, break, y cómo vivir sin continue ni goto en Kahlua.

Curso técnico · Build 42 · Actualizado el

Objetivo: que tu código tome decisiones y repita trabajo: elegir qué hacer según el estado del jugador, recorrer listas, reintentar hasta lograr algo. Y conocer las dos trampas clásicas: el a and b or c con false, y la falta de continue.

1. if, elseif, else

lua
local hambre = getPlayer():getStats():get(CharacterStat.HUNGER)   -- de 0 a 1

if hambre >= 0.7 then
    print("[CursoLab] me muero de hambre")
elseif hambre >= 0.3 then
    print("[CursoLab] algo comería")
else
    print("[CursoLab] estoy bien")
end
  • Cada if termina con end. Los elseif y el else son opcionales.
  • Se revisan en orden y se ejecuta solo el primer bloque cuya condición sea verdadera. Por eso el umbral más alto va primero.
  • elseif va junto. else if (separado) abre un if nuevo dentro del else y te pide un end extra.
  • La condición puede ser cualquier valor: recuerda de L02 que solo nil y false son falsos.

Un if en una sola línea también es válido y el juego lo usa mucho para salir temprano:

lua
local jugador = getPlayer()
if not jugador then return end   -- en el menú principal no hay jugador

2. Comparaciones

OperadorSignifica
==igual
~=distinto (no !=, que es error de sintaxis)
<, >, <=, >=menor, mayor, menor o igual, mayor o igual

Reglas que debes conocer:

  • == entre tipos distintos siempre es false, sin error: 1 == "1" es false. Si un número te llega como texto (de un archivo, de un cuadro de texto, de split), conviértelo con tonumber antes de comparar.
  • < y > entre tipos distintos dan error: 1 < "2" hace que Kahlua diga __lt not defined for operand. Es lo que viste en el ejercicio L1.4 cuando round devolvía texto.
  • Los textos se comparan alfabéticamente, carácter por carácter según su código: las mayúsculas van antes que las minúsculas ("Zapato" < "abeja" es true). Para ordenar sin importar mayúsculas, compara a:lower() < b:lower().
  • Las tablas y los objetos se comparan por identidad: dos tablas con el mismo contenido no son == (L05).

3. and, or, not

lua
local jugador = getPlayer()
local dormido = jugador:isAsleep()
local hambre = jugador:getStats():get(CharacterStat.HUNGER)

if not dormido and hambre > 0.5 then
    print("[CursoLab] despierto y con hambre")
end

if isClient() or isServer() then
    print("[CursoLab] estamos en multijugador")
end

and y or son de corto circuito: si con el lado izquierdo ya se sabe el resultado, el derecho no se evalúa. Eso se usa a cada rato para no tocar un nil:

lua
local item = jugador:getPrimaryHandItem()          -- nil si no tiene nada en la mano
if item and item:getType() == "Axe" then           -- si item es nil, no llama a getType()
    print("[CursoLab] tienes un hacha en la mano")
end

Además, and y or no devuelven true/false: devuelven uno de sus operandos.

ExpresiónDevuelve
a and ba si a es falso (nil o false), si no b
a or ba si a es verdadero, si no b

De ahí sale el modismo de "valor por defecto", que ya viste con CursoLab = CursoLab or {}:

lua
local cantidad = nil
local n = cantidad or 1      -- 1: si no hay cantidad, usa 1

Precedencia: not se aplica antes que las comparaciones, y las comparaciones antes que and, que va antes que or. La trampa típica:

lua
local tipo = "Axe"
if not tipo == "Axe" then ... end   -- MAL: es (not tipo) == "Axe", o sea false == "Axe", siempre false
if tipo ~= "Axe" then ... end       -- BIEN
if not (tipo == "Axe") then ... end -- BIEN también

Ante la duda, usa paréntesis.

4. El truco a and b or c (y su trampa)

Lua 5.1 no tiene operador ternario (cond ? x : y). Se imita así:

lua
local horas = 1
local palabra = horas == 1 and "hora" or "horas"

Funciona porque: si la condición es verdadera, cond and "hora" da "hora", que es verdadero, y el or lo devuelve. Si es falsa, cond and "hora" da false y el or devuelve "horas". El juego lo usa en ISCampingMenu.timeString (lo viste en L02):

lua
(hours == 1 and getText("IGUI_Gametime_hour") or getText("IGUI_Gametime_hours"))

La trampa: si el valor del medio (b) es false o nil, el truco se rompe y siempre devuelve c.

lua
local soloLectura = true
local puedeEditar = soloLectura and false or true
print(puedeEditar)   -- true. Esperabas false.

soloLectura and false da false, y false or true da true. El truco solo es seguro cuando estás seguro de que b nunca es false ni nil. Si no, usa un if:

lua
local puedeEditar
if soloLectura then
    puedeEditar = false
else
    puedeEditar = true
end
-- o, en este caso particular: local puedeEditar = not soloLectura
lua
function forageSystem.getWeatherBonus(_def, _isRaining, _isSnowing, _hasRained)
    if not _def then return 1; end;
    local isRaining = _isRaining ~= nil and _isRaining or getClimateManager():getPrecipitationIntensity() > 0;

La intención es: "si me pasaron _isRaining, úsalo; si es nil, pregúntale al clima". Pero si alguien llama con _isRaining = false ("no está lloviendo, créeme"), _isRaining ~= nil and _isRaining da false, y el or termina preguntando al clima de todas formas. El valor false que pasaste se ignora. Es justo la trampa de esta sección, en el código del juego. La versión correcta:

lua
local isRaining = _isRaining
if isRaining == nil then
    isRaining = getClimateManager():getPrecipitationIntensity() > 0
end

5. for numérico

lua
for i = 1, 5 do
    print("[CursoLab] vuelta", i)      -- 1, 2, 3, 4, 5
end

for i = 0, 100, 25 do                  -- el tercer número es el paso
    print(i)                           -- 0, 25, 50, 75, 100
end

for i = 10, 1, -1 do                   -- paso negativo: cuenta hacia atrás
    print(i)                           -- 10, 9, ..., 1
end
  • El inicio, el límite y el paso se calculan una sola vez, al empezar. Si el límite es #lista y la lista cambia dentro del bucle, el for no se entera.
  • La variable (i) es local al bucle: no existe después del end, y cambiarla adentro no altera las vueltas.
  • Si el inicio ya pasó el límite (for i = 5, 1 do), el bucle no corre ni una vez. Para contar hacia atrás hace falta el -1.
  • Evita pasos con decimales (for x = 0, 1, 0.1): por la imprecisión de los double (L02), la última vuelta puede no ocurrir.

Contar hacia atrás para borrar

Cuando borras elementos de una lista mientras la recorres, recórrela de atrás para adelante. Si vas hacia adelante, al borrar el elemento i el siguiente se corre a la posición i y te lo saltas. (table.remove lo ves en L05.)

lua
local comidas = { "Sopaipilla", "Podrida", "Empanada", "Podrida" }
for i = #comidas, 1, -1 do
    if comidas[i] == "Podrida" then
        table.remove(comidas, i)
    end
end
print(comidas)   -- quedan Sopaipilla y Empanada

El juego hace lo mismo con listas de Java, que empiezan en 0 (L06). En media/lua/server/metazones/metazoneHandler.lua:

lua
for i=dirs:size(),1,-1 do
    local dirName = dirs:get(i-1)

6. while

Repite mientras la condición sea verdadera. La condición se revisa antes de cada vuelta, así que puede no correr nunca.

lua
local intentos = 0
local dado = 0
while dado ~= 6 and intentos < 100 do
    dado = ZombRand(1, 7)
    intentos = intentos + 1
end
print("[CursoLab] saqué " .. dado .. " en " .. intentos .. " intentos")

El juego usa while true do con break para leer un archivo línea por línea hasta que se acaba (media/lua/client/OptionScreens/InviteFriends.lua):

lua
local reader = getFileReader("invited.ini", true)
while true do
    local line = reader:readLine()
    if not line then
        reader:close()
        break
    end
    local steamID = line:trim()

readLine() devuelve nil al final del archivo, y if not line then ... break end sale del bucle.

7. repeat ... until

Como while, pero la condición se revisa al final: el cuerpo corre al menos una vez. Y el bucle sigue hasta que la condición sea verdadera (al revés que while).

lua
local dado
repeat
    dado = ZombRand(1, 7)
    print("[CursoLab] salió", dado)
until dado == 6

Un detalle útil: las variables local del cuerpo se pueden usar en el until.

lua
local vueltas = 0
repeat
    vueltas = vueltas + 1
    local tirada = ZombRand(100)
until tirada < 5 or vueltas >= 50

En media/lua/server/BuildingObjects/ISWalkToCursor.lua el juego busca hacia abajo, piso por piso, un suelo firme:

lua
repeat
    z = z - 1;
    testSquare = getWorld():getCell():getGridSquare(x, y, z);
    if testSquare and testSquare:TreatAsSolidFloor()  then
        validSquare = true;
        break;
    end;
until
    validSquare
    or IsoChunkMap.isGridSquareOutOfRangeZ(z)

Fíjate en el límite de seguridad: además de encontrar el suelo, el bucle para si z se sale del rango del mapa.

8. break

break sale del bucle más interno en el que está (sirve en for, while y repeat).

lua
local inventario = { "Clavos", "Martillo", "Sopaipilla", "Tabla" }
local encontrado = nil
for i = 1, #inventario do
    if inventario[i] == "Sopaipilla" then
        encontrado = i
        break                  -- ya está, no sigas buscando
    end
end
print("[CursoLab] posición:", encontrado)   -- 3

Dos reglas de Lua 5.1:

  • break tiene que ser lo último de su bloque. Escribir algo después del break en el mismo bloque es un error de sintaxis. Por eso casi siempre va justo antes de un end.
  • En bucles anidados, break solo sale del de adentro. Para salir de los dos, usa una variable bandera:
lua
local listo = false
for x = 1, 10 do
    for y = 1, 10 do
        if x * y == 42 then
            listo = true
            break              -- sale del for de y
        end
    end
    if listo then break end    -- sale del for de x
end

9. No hay continue ni goto

Kahlua es Lua 5.1: no existe continue (no lo tiene ninguna versión oficial de Lua) ni goto (que llegó en Lua 5.2). Si escribes cualquiera de los dos, el archivo no compila y no se carga.

lua
for k,v in ipairs(objects) do
    -- Lua seriously needs a "continue", and yes, i refuse to use goto :D
    if v.type ~= "Vegitation" and v.type ~= "DeepForest" and v.type ~= "Forest" and v.type ~= "TownZone" and v.type ~= "Farm" and v.type ~= "FarmLand" and v.type ~= "TrailerPark"  and v.type ~= "Nav" then

("Lua de verdad necesita un continue, y sí, me niego a usar goto".) En Kahlua ni siquiera tendrían goto a mano. La solución que eligieron es el patrón 1 de abajo: envolver todo el cuerpo en un if con la condición invertida.

Tres patrones para reemplazar continue. El objetivo de todos: "si este elemento no me sirve, pasa al siguiente".

Patrón 1: invertir la condición. En vez de "si no sirve, continue", escribe "si sirve, haz el trabajo". Es el más simple y el que usa el juego.

lua
for i = 1, #lista do
    local item = lista[i]
    if item ~= "Podrida" then
        -- trabajo con item
        print("[CursoLab] procesando", item)
    end
end

Cuando las condiciones para saltar son varias, el if se vuelve largo (como el del juego). Guarda la condición en una variable con nombre para que se lea mejor:

lua
for i = 1, #lista do
    local item = lista[i]
    local sirve = item ~= nil and item ~= "" and item ~= "Podrida"
    if sirve then
        print("[CursoLab] procesando", item)
    end
end

Patrón 2: una función con return temprano. Mueve el cuerpo del bucle a una función local: dentro de ella, return hace de continue. Es el más limpio cuando el cuerpo es largo. (Las funciones las ves en L04.)

lua
local function procesar(item)
    if item == nil or item == "" then return end   -- "continue"
    if item == "Podrida" then return end           -- "continue"
    print("[CursoLab] procesando", item)
end

for i = 1, #lista do
    procesar(lista[i])
end

Patrón 3: repeat ... until true con break. Un repeat que da una sola vuelta, donde break salta al final de esa vuelta. Funciona, pero se lee peor: úsalo solo cuando los otros dos no calzan, y nunca si dentro necesitas un break de verdad para el bucle de afuera (este break solo sale del repeat).

lua
for i = 1, #lista do
    repeat
        local item = lista[i]
        if item == "Podrida" then break end   -- actúa como "continue"
        print("[CursoLab] procesando", item)
    until true
end

Ejercicios

Ejercicio L3.1 · Semáforo de hambre

Escribe en 42/media/lua/client/CursoLab_L03.lua un handler de Events.EveryTenMinutes (cada diez minutos de juego) que lea el hambre del jugador (getPlayer():getStats():get(CharacterStat.HUNGER), de 0 a 1) e imprima verde (menos de 0.25), amarillo (de 0.25 a menos de 0.6) o rojo (0.6 o más), con el valor redondeado a 2 decimales.

Listo cuando: ves un mensaje cada diez minutos de juego y el color cambia a medida que pasa el tiempo. (En modo debug puedes subir el hambre desde el menú de debug para probar más rápido.)

Solución
lua
Events.EveryTenMinutes.Add(function()
    local jugador = getPlayer()
    if not jugador then return end

    local hambre = jugador:getStats():get(CharacterStat.HUNGER)
    local color
    if hambre >= 0.6 then
        color = "rojo"
    elseif hambre >= 0.25 then
        color = "amarillo"
    else
        color = "verde"
    end
    print(string.format("[CursoLab] hambre %.2f: %s", hambre, color))
end)

El orden de los umbrales importa: si revisaras primero hambre >= 0.25, un 0.8 caería en "amarillo".

Ejercicio L3.2 · Cuenta regresiva y saltos

En la consola, con un solo for cada uno:

  1. Imprime 10, 8, 6, 4, 2, 0.
  2. Imprime los números del 1 al 30 que no son múltiplos de 3, sin continue.
  3. Recorre local lista = { "Sopaipilla", "", "Empanada", "Podrida", "Mote" } e imprime solo los ítems que no están vacíos ni son "Podrida", usando el patrón 2 (función con return).

Listo cuando: las tres salidas son correctas.

Solución
lua
for i = 10, 0, -2 do print(i) end

for i = 1, 30 do
    if i % 3 ~= 0 then print(i) end
end

local lista = { "Sopaipilla", "", "Empanada", "Podrida", "Mote" }
local function mostrar(item)
    if item == "" then return end
    if item == "Podrida" then return end
    print("[CursoLab]", item)
end
for i = 1, #lista do
    mostrar(lista[i])
end

Recuerda escribir el punto 3 completo en una sola ejecución de la consola: el local function mostrar no sobrevive a la siguiente.

Ejercicio L3.3 · Arreglar la trampa

Este código quiere mostrar "oculto" si CursoLab.mostrarIcono es false, y si no está configurado (nil), usar true por defecto. Tiene dos errores con and/or. Encuéntralos y corrígelos.

lua
CursoLab = CursoLab or {}
CursoLab.mostrarIcono = false

local mostrar = CursoLab.mostrarIcono or true
local texto = mostrar and "visible" or "oculto"
print("[CursoLab] icono:", texto)

Listo cuando: con mostrarIcono = false imprime oculto, con mostrarIcono = true imprime visible, y con mostrarIcono = nil imprime visible.

Solución

El error está en la primera línea: false or true da true, así que el false configurado se pierde (es la misma trampa de forageSystem.getWeatherBonus). La segunda línea en realidad es segura, porque "visible" nunca es falso; el problema es que recibe un mostrar equivocado.

lua
CursoLab = CursoLab or {}
CursoLab.mostrarIcono = false

local mostrar = CursoLab.mostrarIcono
if mostrar == nil then
    mostrar = true
end
local texto = mostrar and "visible" or "oculto"
print("[CursoLab] icono:", texto)

Regla práctica: x or valorPorDefecto sirve para textos, números y tablas, pero no para booleanos que pueden ser false a propósito.

Ejercicio L3.4 · Hasta que salga

En la consola, con repeat ... until, tira ZombRand(1, 7) hasta que salgan dos seises seguidos. Imprime cuántas tiradas hicieron falta. Ponle un límite de 1000 tiradas para no congelar el juego.

Listo cuando: al ejecutarlo varias veces obtienes números distintos y nunca se congela.

Solución
lua
local tiradas = 0
local anterior = 0
local actual = 0
repeat
    anterior = actual
    actual = ZombRand(1, 7)
    tiradas = tiradas + 1
until (anterior == 6 and actual == 6) or tiradas >= 1000
print("[CursoLab] tiradas:", tiradas)

En promedio hacen falta unas 42 tiradas. Con el límite de 1000 es casi imposible no lograrlo, pero si pasa, el bucle termina igual.

Desafío L3

Simula 1000 búsquedas de loot en un contenedor con esta regla, sin usar continue:

  • 50 % de las veces el contenedor está vacío (no cuentes nada más).
  • Si no está vacío, tira ZombRand(100): menos de 5 es "Escopeta", de 5 a 29 es "Comida", el resto es "Basura".
  • Si sale "Escopeta" 3 veces, imprime en qué búsqueda salió la tercera y termina la simulación con break.

Al final imprime cuántas búsquedas hiciste y cuántas veces salió cada cosa, con string.format y porcentajes con un decimal.

Listo cuando: los porcentajes se parecen a lo esperado (alrededor de 50 % vacíos; entre los no vacíos, 5 / 25 / 70) en las simulaciones que llegan a muchas búsquedas, y las que encuentran 3 escopetas paran antes de 1000.

Una solución
lua
local vacios, escopetas, comidas, basuras = 0, 0, 0, 0
local busquedas = 0

for i = 1, 1000 do
    busquedas = i
    if ZombRand(100) < 50 then
        vacios = vacios + 1
    else
        local tirada = ZombRand(100)
        if tirada < 5 then
            escopetas = escopetas + 1
            if escopetas == 3 then
                print("[CursoLab] tercera escopeta en la búsqueda " .. i)
                break
            end
        elseif tirada < 30 then
            comidas = comidas + 1
        else
            basuras = basuras + 1
        end
    end
end

local llenos = busquedas - vacios
print(string.format("[CursoLab] %d búsquedas, %.1f%% vacías", busquedas, vacios * 100 / busquedas))
if llenos > 0 then
    print(string.format("[CursoLab] escopeta %.1f%%, comida %.1f%%, basura %.1f%%",
        escopetas * 100 / llenos, comidas * 100 / llenos, basuras * 100 / llenos))
end

El "vacío" usa el patrón 1: en vez de saltar con continue, todo el trabajo va en el else. Guardamos busquedas = i en cada vuelta porque la variable del for no existe después del bucle. El if llenos > 0 evita dividir por cero, que en Kahlua daría nan o inf en vez de un error.


Anterior: L02 · Números, texto y booleanos · Siguiente: L04 · Funciones