Objetivo: escribir funciones que reciben lo que necesitan, devuelven uno o varios resultados y se pueden pasar como valor, que es justo lo que hace un mod cada vez que se engancha a un evento del juego.
1. Definir y llamar
Una función es un bloque de código con nombre que puedes ejecutar cuando quieras. Se define con function ... end y se llama con paréntesis:
local function saludar(nombre)
print("[CursoLab] Hola, " .. nombre)
end
saludar("Kate") -- [CursoLab] Hola, Kate
saludar("Baldspot") -- [CursoLab] Hola, BaldspotEn Lua una función es un valor, igual que un número o un string. Estas dos formas son lo mismo:
local function doble(x) return x * 2 end
local doble = function(x) return x * 2 end -- casi igual (ver la sección 7)Como viste en L01, todo lo que no lleva local va al entorno global, que es uno solo para el juego y todos los mods. Una función global llamada saludar o contar puede chocar con la de otro mod. En CursoLab las funciones públicas van dentro de la tabla del mod y las privadas son local:
CursoLab = CursoLab or {}
local function formatear(texto) -- privada: solo este archivo la ve
return "[CursoLab] " .. texto
end
function CursoLab.log(texto) -- pública: CursoLab.log("...") desde cualquier lado
print(formatear(texto))
endfunction CursoLab.log(texto) es una forma corta de escribir CursoLab.log = function(texto) ... end: guarda la función en el campo log de la tabla.
Una función existe cuando se ejecuta su definición
Lua lee tu archivo de arriba hacia abajo. Una local function solo existe desde la línea donde se define. Si la usas antes, Lua busca una global con ese nombre, no la encuentra y la llamada falla:
local function a()
return b() -- aquí b todavía no es local: Lua busca una global b
end
local function b()
return 1
end
print(a()) -- error: la global b es nilEn Kahlua el error dice algo como Object tried to call nil in ... seguido del nombre de una función. Cuando lo veas, la pregunta es: ¿qué nombre era nil en ese momento? La solución es definir b antes que a, o declarar local b arriba y asignarla después (sección 7).
2. Parámetros: los que faltan son nil, los que sobran se ignoran
Lua no revisa cuántos argumentos le pasas a una función:
local function info(nombre, edad, ciudad)
print(nombre, edad, ciudad)
end
info("Kate", 27, "Riverside") -- Kate 27 Riverside
info("Kate") -- Kate nil nil (los que faltan valen nil)
info("Kate", 27, "Riverside", "extra", 99) -- los que sobran se descartan sin avisoEsto es cómodo, pero también es la fuente de muchos errores silenciosos: si te equivocas en el orden de los argumentos, Lua no se queja hasta que algo usa un nil donde esperaba un número.
local function curar(cantidad, jugador)
-- ...
end
curar(getPlayer(), 10) -- orden al revés: cantidad es el jugador, jugador es 10Un buen hábito: cuando una función recibe algo obligatorio, revisa al principio y sal con return si no vino:
local function anunciar(jugador, texto)
if not jugador then return end
jugador:Say(texto or "...")
end3. Valores por defecto con or
Como un parámetro que falta vale nil, y nil or x da x, la forma idiomática de dar un valor por defecto es:
local function repetir(texto, veces)
veces = veces or 1
for i = 1, veces do
print("[CursoLab] " .. texto)
end
end
repetir("hola") -- una vez
repetir("hola", 3) -- tres vecesLa trampa: false también activa el or
En L02 viste que false y nil son los únicos valores falsos. Por eso x or valor reemplaza también un false que te pasaron a propósito:
local function avisar(texto, mostrarEnPantalla)
mostrarEnPantalla = mostrarEnPantalla or true -- MAL
-- avisar("hola", false) termina con mostrarEnPantalla == true
endPara booleanos, compara con nil:
local function avisar(texto, mostrarEnPantalla)
if mostrarEnPantalla == nil then mostrarEnPantalla = true end
if mostrarEnPantalla then
getPlayer():Say(texto)
end
print("[CursoLab] " .. texto)
endRegla práctica: or para números, strings y tablas por defecto; == nil para booleanos.
4. Varios retornos
Una función puede devolver más de un valor. El que llama los recibe en el mismo orden:
local function minMax(a, b, c)
return math.min(a, b, c), math.max(a, b, c)
end
local menor, mayor = minMax(7, 2, 9)
print(menor, mayor) -- 2 9Las reglas de cómo se reparten:
local a = minMax(7, 2, 9) -- a = 2 (el segundo se pierde)
local a, b, c = minMax(7, 2, 9) -- a = 2, b = 9, c = nil
print(minMax(7, 2, 9)) -- 2 9 (se pasan los dos a print)
print(minMax(7, 2, 9), "fin") -- 2 fin (en medio de una lista solo cuenta el primero)
print((minMax(7, 2, 9))) -- 2 (los paréntesis dejan solo el primero)La regla importante: solo la última expresión de una lista se expande a todos sus valores. Si la llamada no va al final, aporta un solo valor.
Un uso muy común es devolver "resultado y explicación": true si salió bien, o false y el motivo si no.
local function puedeComer(jugador)
if not jugador then return false, "no hay jugador" end
if jugador:isDead() then return false, "está muerto" end
return true
end
local ok, motivo = puedeComer(getPlayer())
if not ok then
print("[CursoLab] No puede comer: " .. motivo)
endlocal function getNameAndNumFromSpriteName(name)
local len = string.len(name)
local num = nil
local strName = nil
for i = len, 1, -1 do
local sub = string.sub(name, i, i)
if sub == "_" then
strName = string.sub(name, 1, i - 1)
num = tonumber(string.sub(name, i + 1, len))
break
end
end
return strName, num
endY en la línea 2333 la usan con dos variables: local tname, tnum = getNameAndNumFromSpriteName(itemSprite). Fíjate que si el nombre no tiene _ devuelve nil, nil, y el que llama tiene que revisarlo.
5. Varargs: ... y select('#', ...)
Una función puede aceptar cualquier cantidad de argumentos con ... al final de sus parámetros. Dentro de la función, ... representa "todos los que sobraron":
local function sumar(...)
local total = 0
for _, n in ipairs({...}) do
total = total + n
end
return total
end
print(sumar(1, 2, 3)) -- 6
print(sumar()) -- 0Herramientas para trabajar con ...:
| Expresión | Qué da |
|---|---|
{...} | Una tabla con los argumentos (desde el índice 1) |
select('#', ...) | Cuántos argumentos llegaron, contando los nil |
select(n, ...) | El argumento n y todos los que siguen |
unpack(t) | Lo contrario de {...}: los elementos de una lista como valores sueltos |
Recuerda que en Kahlua no existen table.pack ni table.unpack: se usa unpack a secas.
El problema de los nil
{...} mete los argumentos en una tabla, pero si alguno es nil queda un hueco, y en L05 vas a ver que # e ipairs no se llevan bien con los huecos. select('#', ...) sí cuenta los nil:
local function contar(...)
print(select('#', ...))
end
contar(1, nil, 3) -- 3
contar(nil, nil) -- 2
contar() -- 0Para recorrer todos los argumentos aunque haya nil, usa select('#', ...) como límite y select(i, ...) para tomar cada uno:
local function logTodo(...)
local n = select('#', ...)
local partes = {}
for i = 1, n do
local valor = (select(i, ...)) -- los paréntesis dejan solo el valor i
partes[i] = tostring(valor)
end
print("[CursoLab] " .. table.concat(partes, " | "))
end
logTodo("vida", nil, 42) -- [CursoLab] vida | nil | 42Un caso muy útil de varargs es pasar los argumentos tal cual a otra función:
function CursoLab.logf(formato, ...)
print("[CursoLab] " .. string.format(formato, ...))
end
CursoLab.logf("Zombis muertos: %d en %d minutos", 12, 30)function ISUIElement:getMaxWidthOfElements(...)
local uis = {...}
local width = 0
for _,ui in ipairs(uis) do
width = math.max(width, ui:getWidth())
end
return width
endY en shared/TimedActions/ISWaitWhileGettingUp.lua, línea 52, el juego hace lo contrario: acepta hasta 10 argumentos con nombre y usa select("#", ...) para quejarse si llegan más. El comentario explica por qué no usó ... para todo:
function ISWaitWhileGettingUp:setOnComplete(func, p1, p2, p3, p4, p5, p6, p7, p8, p9, p10, ...)
local nArgs = select("#", ...)
if nArgs > 0 then
error("only 10 additional arguments are supported,")
end
self.onCompleteFunc = func
-- Any of these may be nil, so we can't take advantage of "..." syntax :-(
self.onCompleteArgs = { p1=p1, p2=p2, p3=p3, p4=p4, p5=p5, p6=p6, p7=p7, p8=p8, p9=p9, p10=p10 }
end"Cualquiera puede ser nil": es justo el problema de los huecos. Ese patrón (func más p1 a p10) lo vas a ver en todos los menús contextuales del juego.
6. Funciones como valores: callbacks y eventos
Como una función es un valor, puedes guardarla en una variable, en una tabla o pasarla a otra función. Una función que le pasas a otra para que la llame "después" se llama callback. Así funciona todo el sistema de eventos de PZ:
local function alEmpezar()
print("[CursoLab] Empezó la partida")
end
Events.OnGameStart.Add(alEmpezar) -- le pasas la función, el juego la llama cuando correspondef no es lo mismo que f()
alEmpezar es la función. alEmpezar() la ejecuta en ese momento y lo que pasas es lo que devolvió (aquí, nada: nil). Es uno de los errores más comunes:
Events.OnGameStart.Add(alEmpezar) -- bien: registra la función
Events.OnGameStart.Add(alEmpezar()) -- mal: la ejecuta YA, al cargar el archivo, y registra nil-- Events.OnPostDistributionMerge.Add(StoryClutter.Init())
Events.OnLoadedMapZones.Add(StoryClutter.Init())StoryClutter.Init no termina con return, así que StoryClutter.Init() vale nil. Esa línea ejecuta Init en el momento en que se carga el archivo y le pasa nil a Add: la función no queda enganchada a OnLoadedMapZones. Y no vas a ver ningún error: el Add de Java (zombie.Lua.Event) ignora en silencio cualquier cosa que no sea una función de Lua. Hasta el código del juego tiene este error: léelo siempre dos veces cuando escribas .Add(.
Funciones anónimas
Puedes escribir la función directo dentro de la llamada, sin nombre:
Events.EveryTenMinutes.Add(function()
print("[CursoLab] Pasaron diez minutos de juego")
end)Es cómodo, pero tiene un costo: para sacar un handler con Events.X.Remove necesitas la misma función que registraste. Una función anónima no tiene nombre con el que pedirla de vuelta, así que no la puedes quitar. Si alguna vez vas a querer sacarla, guárdala en una variable:
local function cadaDiez()
print("[CursoLab] tick de diez minutos")
end
Events.EveryTenMinutes.Add(cadaDiez)
-- más tarde:
Events.EveryTenMinutes.Remove(cadaDiez)Otro detalle del Add de Java: no revisa si la función ya estaba registrada. Si llamas dos veces a Events.EveryTenMinutes.Add(cadaDiez), cadaDiez corre dos veces cada diez minutos. (En L08 vas a ver eventos y handlers a fondo.)
Cada evento pasa sus propios argumentos
El juego llama a tu función con argumentos que dependen del evento. Tu función los recibe como parámetros normales:
local function alMorirZombi(zombi)
print("[CursoLab] Murió un zombi: " .. tostring(zombi))
end
Events.OnZombieDead.Add(alMorirZombi)No adivines la firma: busca en el juego un Events.NombreDelEvento.Add y mira qué parámetros recibe la función que registran (el módulo 06 lo explica).
Tablas de funciones: elegir qué hacer según un nombre
Guardar funciones en una tabla, con el nombre como clave, reemplaza una cadena larga de if/elseif:
local comandos = {}
function comandos.hola(jugador)
jugador:Say("Hola")
end
function comandos.hora(jugador)
jugador:Say("Son las " .. getGameTime():getHour())
end
local function ejecutar(nombre, jugador)
local f = comandos[nombre]
if not f then
print("[CursoLab] Comando desconocido: " .. tostring(nombre))
return
end
f(jugador)
end
ejecutar("hola", getPlayer())VehicleCommands.OnClientCommand = function(module, command, player, args)
if module == 'vehicle' and Commands[command] then
local argStr = ''
args = args or {}
for k,v in pairs(args) do
argStr = argStr..' '..k..'='..tostring(v)
end
noise('received %s %s %s %s', module, command, tostring(player), argStr)
Commands[command](player, args)
end
end
Events.OnClientCommand.Add(VehicleCommands.OnClientCommand)Fíjate en tres cosas del capítulo: args = args or {} (valor por defecto), Commands[command](player, args) (sacar una función de una tabla y llamarla) y .Add(VehicleCommands.OnClientCommand) sin paréntesis.
7. Funciones locales y recursión
Una función recursiva se llama a sí misma. Sirve para problemas que contienen versiones más chicas del mismo problema: una tabla que tiene tablas adentro, un contenedor que tiene contenedores adentro.
local function cuentaRegresiva(n)
if n <= 0 then -- caso base: aquí se detiene
print("[CursoLab] ¡Ya!")
return
end
print("[CursoLab] " .. n)
cuentaRegresiva(n - 1) -- se llama con un problema más chico
end
cuentaRegresiva(3) -- 3, 2, 1, ¡Ya!Toda recursión necesita un caso base que no se llame de nuevo. Sin él, la función se llama una y otra vez hasta que el intérprete corta con un error Stack overflow.
Ojo con la profundidad aunque tengas caso base: en Kahlua la pila es chica. La VM acepta como máximo 1000 llamadas anidadas (y 3000 casillas para las variables de todas ellas), así que una recursión de miles de niveles falla con Stack overflow. Para recorrer algo muy largo, usa un bucle; la recursión es para cosas anidadas pocos niveles, como tablas dentro de tablas.
local function vs local f = function
Aquí sí hay una diferencia entre las dos formas de la sección 1:
local function fact(n) -- bien: fact ya existe como local dentro de su propio cuerpo
if n <= 1 then return 1 end
return n * fact(n - 1)
end
local fact2 = function(n) -- mal para recursión: dentro del cuerpo, fact2 todavía
if n <= 1 then return 1 end -- no es local, así que Lua busca una global fact2 (nil)
return n * fact2(n - 1)
endlocal function f equivale a local f seguido de f = function ... end: primero crea la variable y después le asigna la función, por eso la función se puede ver a sí misma. Usa siempre local function para funciones locales.
Si dos funciones locales se llaman entre sí, declara la segunda antes:
local esImpar -- existe, vale nil por ahora
local function esPar(n)
if n == 0 then return true end
return esImpar(n - 1)
end
function esImpar(n) -- asigna la local de arriba, no crea una global
if n == 0 then return false end
return esPar(n - 1)
end
print(esPar(10)) -- truefunction luautils.countItemsRecursive(_containerList, _itemsNum)
local itemsNum = _itemsNum or 0;
if #_containerList > 0 then
local nextContainerList = {};
for i = 1, #_containerList do
local containerObj = _containerList[i];
local containerList = containerObj:getItemsFromCategory("Container");
for j = 0, containerList:size() - 1 do
table.insert(nextContainerList, containerList:get(j):getInventory());
end;
itemsNum = itemsNum + containerObj:getItems():size();
end;
return luautils.countItemsRecursive(nextContainerList, itemsNum);
else
return itemsNum;
end;
endEl caso base es "la lista de contenedores está vacía". Cada llamada baja un nivel (las mochilas dentro de las mochilas) y le pasa el total acumulado a la siguiente. Los :size() y :get(j) son de listas de Java, que ves en L06.
Los nombres se buscan cuando la función corre, no cuando se escribe
Lua no revisa que una función llamada adentro exista hasta que esa línea se ejecuta. Un error de tipeo puede quedar escondido por años si esa rama no corre nunca:
function DeepPrintDistributionTable (_dist, tab)
if type(_dist) == "table" then
for k,v in pairs(_dist) do
print(tab .. " " .. tostring(k))
DeepPrintTable(v, tab.." ")
end
else
print(tab .. tostring(_dist))
end
endLa intención es recursiva, pero adentro llama a DeepPrintTable, que no existe en ningún archivo Lua del juego ni en el .jar. El archivo carga sin problema; el error solo aparecería el día que alguien llame a DeepPrintDistributionTable con una tabla que tenga algo adentro. Por eso, cuando escribas una función, pruébala desde la consola al menos una vez por cada rama.
8. Alcance: quién ve qué variable
Una variable local existe desde la línea donde se declara hasta el end del bloque que la contiene (una función, un if, un for, un do ... end, o el archivo completo):
local contador = 0 -- visible en todo el archivo desde aquí
local function sumar()
local paso = 1 -- visible solo dentro de sumar
contador = contador + paso
end
sumar()
print(contador) -- 1
print(paso) -- nil: paso no existe aquí (Lua busca una global paso)Los parámetros de una función son locales de esa función. La variable de un for es local del bucle:
for i = 1, 3 do end
print(i) -- nilSombra: una local tapa a otra con el mismo nombre
local nombre = "CursoLab"
local function prueba()
local nombre = "otra cosa" -- tapa a la de afuera dentro de esta función
print(nombre) -- otra cosa
end
prueba()
print(nombre) -- CursoLabFunciona, pero confunde. Si te pasa sin querer (por ejemplo un parámetro llamado player y una local player más abajo), el código hace algo distinto de lo que lees.
Una función ve las locales de afuera
Una función puede leer y cambiar las variables local que estaban visibles donde se escribió, aunque se llame mucho después. Por eso sumar() de arriba puede cambiar contador. Esto permite que un handler de evento guarde estado entre una llamada y otra sin usar globales:
local minutos = 0
local function cadaMinuto()
minutos = minutos + 1
if minutos % 60 == 0 then
print("[CursoLab] Pasó otra hora de juego")
end
end
Events.EveryOneMinute.Add(cadaMinuto)minutos es local al archivo: ningún otro mod la puede pisar. A estas funciones que "recuerdan" variables de afuera se les llama closures; las ves a fondo en L08 y L10.
Resumen de alcance
| Dónde declaras | Quién la ve |
|---|---|
local x al principio del archivo | Todo lo que viene después en ese archivo |
local x dentro de una función | Solo esa función (desde esa línea) |
| Parámetro de función | Solo esa función |
Variable del for | Solo el cuerpo del bucle |
Sin local | Todo el juego y todos los mods (evítalo) |
Ejercicios
Prueba todo en tu archivo de CursoLab o en la consola de debug (L01).
Ejercicio L4.1 · Valores por defecto
Escribe CursoLab.anunciar(texto, veces, enPantalla):
textopor defecto es"Hola, sobreviviente".vecespor defecto es 1.enPantallapor defecto estrue; si estrue, además de imprimir en la consola, el jugador dice el texto congetPlayer():Say(texto).
Listo cuando: CursoLab.anunciar(), CursoLab.anunciar("Zombis", 2) y CursoLab.anunciar("Silencio", 1, false) hacen lo esperado, y el tercero no muestra nada sobre la cabeza del jugador.
Solución
CursoLab = CursoLab or {}
function CursoLab.anunciar(texto, veces, enPantalla)
texto = texto or "Hola, sobreviviente"
veces = veces or 1
if enPantalla == nil then enPantalla = true end -- no uses "or" con booleanos
for i = 1, veces do
print("[CursoLab] " .. texto)
end
if enPantalla then
local jugador = getPlayer()
if jugador then jugador:Say(texto) end
end
endEjercicio L4.2 · Dos retornos
Escribe CursoLab.separarTipo(tipoCompleto) que reciba un nombre completo de ítem como "Base.Apple" y devuelva dos valores: el módulo ("Base") y el nombre ("Apple"). Si no tiene punto, devuelve nil y un texto que explique el problema.
Listo cuando: print(CursoLab.separarTipo("Base.Apple")) imprime Base Apple y print(CursoLab.separarTipo("Apple")) imprime nil y tu mensaje.
Solución
function CursoLab.separarTipo(tipoCompleto)
if type(tipoCompleto) ~= "string" then
return nil, "no es un string"
end
local punto = tipoCompleto:find(".", 1, true) -- true: busca el punto literal
if not punto then
return nil, "falta el módulo en '" .. tipoCompleto .. "'"
end
return tipoCompleto:sub(1, punto - 1), tipoCompleto:sub(punto + 1)
end
local modulo, nombre = CursoLab.separarTipo("Base.Apple")
print(modulo, nombre) -- Base Apple
print(CursoLab.separarTipo("Apple")) -- nil falta el módulo en 'Apple'Ejercicio L4.3 · Un log con varargs
Escribe CursoLab.debug(...) que imprima todos sus argumentos separados por " | ", con el prefijo [CursoLab], mostrando los nil también. Al final debe decir cuántos argumentos recibió.
Listo cuando: CursoLab.debug("hp", nil, 3.5, true) imprime [CursoLab] hp | nil | 3.5 | true (4 args) y CursoLab.debug() imprime [CursoLab] (0 args).
Solución
function CursoLab.debug(...)
local n = select('#', ...)
local partes = {}
for i = 1, n do
partes[i] = tostring((select(i, ...)))
end
print("[CursoLab] " .. table.concat(partes, " | ") .. " (" .. n .. " args)")
endCon {...} e ipairs el nil del segundo argumento cortaría el recorrido: por eso se usa select('#', ...) como límite.
Ejercicio L4.4 · Un handler que se puede quitar
Registra en Events.EveryOneMinute una función que cuente los minutos de juego e imprima el total cada 10 minutos. Escribe además CursoLab.pararReloj(), que saca el handler del evento.
Listo cuando: después de llamar a CursoLab.pararReloj() desde la consola, el mensaje deja de aparecer.
Solución
local minutos = 0
local function reloj()
minutos = minutos + 1
if minutos % 10 == 0 then
print("[CursoLab] Minutos de juego contados: " .. minutos)
end
end
Events.EveryOneMinute.Add(reloj)
function CursoLab.pararReloj()
Events.EveryOneMinute.Remove(reloj) -- la misma función que registraste
print("[CursoLab] Reloj detenido en " .. minutos)
endSi hubieras escrito Events.EveryOneMinute.Add(function() ... end), no tendrías con qué llamar a Remove.
Ejercicio L4.5 · Recursión
Escribe CursoLab.sumaDigitos(n) que sume los dígitos de un número entero positivo usando recursión (sumaDigitos(1984) es 1 + 9 + 8 + 4 = 22). Pista: el último dígito es n % 10 y el resto es math.floor(n / 10).
Listo cuando: sumaDigitos(1984) da 22, sumaDigitos(7) da 7 y sumaDigitos(0) da 0.
Solución
local function sumaDigitos(n)
if n < 10 then return n end -- caso base: un solo dígito
return (n % 10) + sumaDigitos(math.floor(n / 10))
end
CursoLab.sumaDigitos = sumaDigitosCon números positivos, % da lo mismo en Kahlua y en Lua estándar. Con negativos no (L02): por eso el ejercicio pide positivos.
Desafío L4
Arma un mini sistema de comandos para la consola: CursoLab.cmd(nombre, ...).
- Los comandos viven en una tabla local
comandos, con el nombre como clave y la función como valor. - Cada comando recibe los argumentos extra y devuelve
true, resultadoofalse, motivo. CursoLab.cmdimprime[CursoLab] OK: resultadoo[CursoLab] Error: motivo, y si el comando no existe dice cuáles hay.- Implementa al menos:
sumar(cualquier cantidad de números; error si alguno no es número),repetir(texto y veces, con veces por defecto 2) ydi(el jugador dice el texto con:Say).
Listo cuando: CursoLab.cmd("sumar", 1, 2, 3) imprime OK: 6, CursoLab.cmd("sumar", 1, "x") imprime un error claro, CursoLab.cmd("repetir", "ja") imprime OK: jaja y CursoLab.cmd("bailar") lista los comandos disponibles.
Una solución
CursoLab = CursoLab or {}
local comandos = {}
function comandos.sumar(...)
local total = 0
for i = 1, select('#', ...) do
local v = (select(i, ...))
if type(v) ~= "number" then
return false, "el argumento " .. i .. " no es un número: " .. tostring(v)
end
total = total + v
end
return true, total
end
function comandos.repetir(texto, veces)
if not texto then return false, "falta el texto" end
veces = veces or 2
return true, string.rep(texto, veces)
end
function comandos.di(texto)
local jugador = getPlayer()
if not jugador then return false, "no hay jugador" end
jugador:Say(texto or "...")
return true, "dicho"
end
local function nombresDeComandos()
local nombres = {}
for nombre in pairs(comandos) do
table.insert(nombres, nombre)
end
table.sort(nombres)
return table.concat(nombres, ", ")
end
function CursoLab.cmd(nombre, ...)
local f = comandos[nombre]
if not f then
print("[CursoLab] No existe '" .. tostring(nombre) .. "'. Comandos: " .. nombresDeComandos())
return
end
local ok, resultado = f(...)
if ok then
print("[CursoLab] OK: " .. tostring(resultado))
else
print("[CursoLab] Error: " .. tostring(resultado))
end
endnombresDeComandos usa pairs, table.insert, table.sort y table.concat: todo eso es el tema de L05.
Anterior: L03 · Decisiones y bucles · Siguiente: L05 · Tablas