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
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
iftermina conend. Loselseify elelseson 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.
elseifva junto.else if(separado) abre unifnuevo dentro delelsey te pide unendextra.- La condición puede ser cualquier valor: recuerda de L02 que solo
nilyfalseson falsos.
Un if en una sola línea también es válido y el juego lo usa mucho para salir temprano:
local jugador = getPlayer()
if not jugador then return end -- en el menú principal no hay jugador2. Comparaciones
| Operador | Significa |
|---|---|
== | igual |
~= | distinto (no !=, que es error de sintaxis) |
<, >, <=, >= | menor, mayor, menor o igual, mayor o igual |
Reglas que debes conocer:
==entre tipos distintos siempre esfalse, sin error:1 == "1"esfalse. Si un número te llega como texto (de un archivo, de un cuadro de texto, desplit), conviértelo contonumberantes 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 cuandorounddevolví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"estrue). Para ordenar sin importar mayúsculas, comparaa: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
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")
endand 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:
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")
endAdemás, and y or no devuelven true/false: devuelven uno de sus operandos.
| Expresión | Devuelve |
|---|---|
a and b | a si a es falso (nil o false), si no b |
a or b | a si a es verdadero, si no b |
De ahí sale el modismo de "valor por defecto", que ya viste con CursoLab = CursoLab or {}:
local cantidad = nil
local n = cantidad or 1 -- 1: si no hay cantidad, usa 1Precedencia: not se aplica antes que las comparaciones, y las comparaciones antes que and, que va antes que or. La trampa típica:
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énAnte 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í:
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):
(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.
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:
local puedeEditar
if soloLectura then
puedeEditar = false
else
puedeEditar = true
end
-- o, en este caso particular: local puedeEditar = not soloLecturafunction 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:
local isRaining = _isRaining
if isRaining == nil then
isRaining = getClimateManager():getPrecipitationIntensity() > 0
end5. for numérico
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
#listay la lista cambia dentro del bucle, elforno se entera. - La variable (
i) eslocalal bucle: no existe después delend, 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 losdouble(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.)
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 EmpanadaEl juego hace lo mismo con listas de Java, que empiezan en 0 (L06). En media/lua/server/metazones/metazoneHandler.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.
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):
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).
local dado
repeat
dado = ZombRand(1, 7)
print("[CursoLab] salió", dado)
until dado == 6Un detalle útil: las variables local del cuerpo se pueden usar en el until.
local vueltas = 0
repeat
vueltas = vueltas + 1
local tirada = ZombRand(100)
until tirada < 5 or vueltas >= 50En media/lua/server/BuildingObjects/ISWalkToCursor.lua el juego busca hacia abajo, piso por piso, un suelo firme:
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).
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) -- 3Dos reglas de Lua 5.1:
breaktiene que ser lo último de su bloque. Escribir algo después delbreaken el mismo bloque es un error de sintaxis. Por eso casi siempre va justo antes de unend.- En bucles anidados,
breaksolo sale del de adentro. Para salir de los dos, usa una variable bandera:
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
end9. 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.
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.
for i = 1, #lista do
local item = lista[i]
if item ~= "Podrida" then
-- trabajo con item
print("[CursoLab] procesando", item)
end
endCuando 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:
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
endPatró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.)
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])
endPatró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).
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
endEjercicios
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
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:
- Imprime
10, 8, 6, 4, 2, 0. - Imprime los números del 1 al 30 que no son múltiplos de 3, sin
continue. - 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 conreturn).
Listo cuando: las tres salidas son correctas.
Solución
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])
endRecuerda 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.
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.
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
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 conbreak.
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
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))
endEl "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