WENOPAL

L04 · Funciones

Definir y llamar funciones en Lua, parámetros opcionales, varios retornos, varargs, recursión y funciones como valores para los eventos del juego.

Curso técnico · Build 42 · Actualizado el

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:

lua
local function saludar(nombre)
    print("[CursoLab] Hola, " .. nombre)
end

saludar("Kate")      -- [CursoLab] Hola, Kate
saludar("Baldspot")  -- [CursoLab] Hola, Baldspot

En Lua una función es un valor, igual que un número o un string. Estas dos formas son lo mismo:

lua
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:

lua
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))
end

function 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:

lua
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 nil

En 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:

lua
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 aviso

Esto 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.

lua
local function curar(cantidad, jugador)
    -- ...
end

curar(getPlayer(), 10)   -- orden al revés: cantidad es el jugador, jugador es 10

Un buen hábito: cuando una función recibe algo obligatorio, revisa al principio y sal con return si no vino:

lua
local function anunciar(jugador, texto)
    if not jugador then return end
    jugador:Say(texto or "...")
end

3. 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:

lua
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 veces

La 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:

lua
local function avisar(texto, mostrarEnPantalla)
    mostrarEnPantalla = mostrarEnPantalla or true   -- MAL
    -- avisar("hola", false) termina con mostrarEnPantalla == true
end

Para booleanos, compara con nil:

lua
local function avisar(texto, mostrarEnPantalla)
    if mostrarEnPantalla == nil then mostrarEnPantalla = true end
    if mostrarEnPantalla then
        getPlayer():Say(texto)
    end
    print("[CursoLab] " .. texto)
end

Regla 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:

lua
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  9

Las reglas de cómo se reparten:

lua
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.

lua
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)
end
lua
local 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
end

Y 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":

lua
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())             -- 0

Herramientas para trabajar con ...:

ExpresiónQué 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:

lua
local function contar(...)
    print(select('#', ...))
end

contar(1, nil, 3)   -- 3
contar(nil, nil)    -- 2
contar()            -- 0

Para recorrer todos los argumentos aunque haya nil, usa select('#', ...) como límite y select(i, ...) para tomar cada uno:

lua
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 | 42

Un caso muy útil de varargs es pasar los argumentos tal cual a otra función:

lua
function CursoLab.logf(formato, ...)
    print("[CursoLab] " .. string.format(formato, ...))
end

CursoLab.logf("Zombis muertos: %d en %d minutos", 12, 30)
lua
function ISUIElement:getMaxWidthOfElements(...)
    local uis = {...}
    local width = 0
    for _,ui in ipairs(uis) do
        width = math.max(width, ui:getWidth())
    end
    return width
end

Y 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:

lua
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:

lua
local function alEmpezar()
    print("[CursoLab] Empezó la partida")
end

Events.OnGameStart.Add(alEmpezar)    -- le pasas la función, el juego la llama cuando corresponde

f 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:

lua
Events.OnGameStart.Add(alEmpezar)     -- bien: registra la función
Events.OnGameStart.Add(alEmpezar())   -- mal: la ejecuta YA, al cargar el archivo, y registra nil
lua
-- 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:

lua
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:

lua
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:

lua
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:

lua
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())
lua
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.

lua
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:

lua
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)
end

local 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:

lua
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))   -- true
lua
function 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;
end

El 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:

lua
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
end

La 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):

lua
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:

lua
for i = 1, 3 do end
print(i)   -- nil

Sombra: una local tapa a otra con el mismo nombre

lua
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)                     -- CursoLab

Funciona, 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:

lua
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 declarasQuién la ve
local x al principio del archivoTodo lo que viene después en ese archivo
local x dentro de una funciónSolo esa función (desde esa línea)
Parámetro de funciónSolo esa función
Variable del forSolo el cuerpo del bucle
Sin localTodo 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):

  • texto por defecto es "Hola, sobreviviente".
  • veces por defecto es 1.
  • enPantalla por defecto es true; si es true, además de imprimir en la consola, el jugador dice el texto con getPlayer():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
lua
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
end

Ejercicio 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
lua
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
lua
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)")
end

Con {...} 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
lua
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)
end

Si 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
lua
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 = sumaDigitos

Con 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, resultado o false, motivo.
  • CursoLab.cmd imprime [CursoLab] OK: resultado o [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) y di (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
lua
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
end

nombresDeComandos usa pairs, table.insert, table.sort y table.concat: todo eso es el tema de L05.


Anterior: L03 · Decisiones y bucles · Siguiente: L05 · Tablas