El Lua del cerebro
El manifiesto y el script de Steady Commander, los hooks, lo que un cerebro puede saber y hacer, los números, las tiradas aleatorias, los temporizadores y el chat.
Este capítulo resume docs/brains/oabrain-standard.md, docs/brains/authoring.md y docs/playing-with-brains.md. Esos documentos son el texto normativo.
Steady Commander
Steady Commander es el cerebro inventado en brains/examples/steady-commander. Los dos archivos de abajo se copian tal como están escritos. works-with es un asterisco entre comillas, así que el paquete encaja con cualquier mod y con el juego base. El script devuelve una tabla de hooks. Nombra esa tabla con algo distinto de brain: un archivo que empieza por local brain hace sombra al global que instala el perfil. fits devuelve true. save devuelve cuántas escuadras se han enviado. La elección de ataque es soon, steady o late.
Manifiesto ·oabrain.yaml# SPDX-FileCopyrightText: The Open Annihilation Authors; see COPYRIGHT
# SPDX-License-Identifier: GPL-3.0-only
oabrain: 1
id: steady-commander
name: Steady Commander
version: "1.0"
author:
name: Ridge Workshop
description: "Builds a little economy, then a factory, and sends a squad when it is ready."
tags:
- economy
- teaching
works-with:
- "*"
levels:
- id: easy
name: Easy
difficulty: easy
- id: medium
name: Medium
difficulty: medium
- id: hard
name: Hard
difficulty: hard
options:
attack:
type: choice
label: "How soon to attack"
values: ["soon", "steady", "late"]
default: "steady"
labels: ["Soon", "Steady", "Late"]
entry: brain.lua
Script de entrada · brain.lua-- SPDX-FileCopyrightText: The Open Annihilation Authors; see COPYRIGHT
-- SPDX-License-Identifier: GPL-3.0-only
-- Economy, then one land factory. Combat units gather until the attack
-- option's count is met, then go to the nearest enemy in sight.
-- save() keeps how many squads have been sent.
local hooks = {}
local sent, armed = 0, false
local commander, metal_type, energy_type, factory_type, combat_type
local metal_done, energy_done, factory_ordered = 0, 0, false
local factories, squad, sent_ids = {}, {}, {}
local ready = false
local function real_of(record, key)
local value = record[key]
if type(value) == "userdata" and type(value.tonumber) == "function" then
return value:tonumber()
end
return type(value) == "number" and value or 0
end
local function positive(record, key)
return real_of(record, key) > 0
end
local function words(record)
local found, list = {}, record.category
if type(list) ~= "table" then return found end
for index = 1, #list do found[list[index]] = true end
return found
end
-- A category the commander lacks, or no categories, so a copy is skipped.
local function labelled(record, commander_words)
local any, fresh = false, false
for word in pairs(words(record)) do
any, fresh = true, fresh or commander_words[word] ~= true
end
return fresh or not any
end
local function land(record)
return real_of(record, "minwaterdepth") <= 0
end
local function land_combat(record)
return record.canmove == true and record.canattack == true and record.builder ~= true
and record.canfly ~= true and land(record)
end
local function energy_record(record)
return positive(record, "energymake") or positive(record, "makesmetal")
or positive(record, "windgenerator") or positive(record, "tidalgenerator")
end
local function pick(list)
if #list == 0 then return nil end
return list[brain.random(#list)]
end
local function classify(list, commander_words)
local metals, energies, plants = {}, {}, {}
for index = 1, #list do
local record = list[index]
if record.commander ~= true and labelled(record, commander_words) then
if positive(record, "extractsmetal") and land(record) then
metals[#metals + 1] = record
elseif energy_record(record) and land(record) then
energies[#energies + 1] = record
elseif record.builder == true and record.bmcode == 0 and land(record) then
local built, fights = game.build_list(record), {}
if type(built) == "table" then
for slot = 1, #built do
if land_combat(built[slot]) then fights[#fights + 1] = built[slot] end
end
end
local fight = pick(fights)
if fight ~= nil then plants[#plants + 1] = { plant = record, unit = fight } end
end
end
end
return metals, energies, plants
end
local function squad_size()
if me.options.attack == "soon" then return 4 end
if me.options.attack == "late" then return 12 end
return 8
end
local function plan_counts()
if me.difficulty == "easy" then return 1, 1 end
if me.difficulty == "hard" then return 2, 2 end
return 2, 1
end
local function find_commander()
local found = nil
me:each_unit(function(unit)
if found == nil and unit.type ~= nil and unit.type.commander == true then found = unit end
end)
return found
end
local function free(unit)
local kind, _, idle = unit:head_order()
return kind == nil or idle == true
end
local function place_build(builder, record)
if builder.x == nil or builder.z == nil then return false end
local facing = brain.random(4) - 1
local _, _, x, z = brain.place(record, { x = builder.x, z = builder.z }, 200, facing)
if x == nil then return false end
return builder:build(record, { x = x, z = z }, facing) == true
end
local function note_finished(unit)
local record = unit.type
if record == nil or record.commander == true or unit.alive ~= true then return end
if positive(record, "extractsmetal") then metal_done = metal_done + 1
elseif energy_record(record) then energy_done = energy_done + 1
elseif record.builder == true and record.bmcode == 0 then
factories[unit.id], factory_ordered = unit, true
elseif land_combat(record) and sent_ids[unit.id] ~= true then squad[unit.id] = unit
end
end
local function nearest(origin)
local best, best_distance
for index = 1, #me.seen do
local unit = me.seen[index]
if unit.alive == true and unit.owner ~= me.player and me:allied(unit.owner) ~= true then
local distance = brain.distance(origin, unit)
if distance ~= nil and (best_distance == nil or distance < best_distance) then
best, best_distance = unit, distance
end
end
end
return best
end
local function count_squad()
local count = 0
for _ in pairs(squad) do count = count + 1 end
return count
end
local function try_send()
if count_squad() < squad_size() then return end
local origin = (commander ~= nil and commander.alive == true) and commander or nil
if origin == nil then
for _, unit in pairs(squad) do origin = unit break end
end
local target = origin ~= nil and nearest(origin) or nil
if target == nil then return end
for id, unit in pairs(squad) do
if unit.alive == true then unit:attack(target) end
sent_ids[id] = true
end
squad, sent = {}, sent + 1
end
local function prepare()
if ready then return end
if commander == nil or commander.alive ~= true then commander = find_commander() end
if commander == nil or commander.type == nil then return end
local list = game.build_list(commander.type)
if type(list) ~= "table" or #list == 0 then return end
local metals, energies, plants = classify(list, words(commander.type))
metal_type, energy_type = pick(metals), pick(energies)
local plant = pick(plants)
if plant ~= nil then factory_type, combat_type = plant.plant, plant.unit end
ready = metal_type ~= nil or energy_type ~= nil or factory_type ~= nil
end
local function build_next()
prepare()
if commander == nil or commander.alive ~= true then commander = find_commander() end
if commander == nil or not free(commander) then return end
local metal_need, energy_need = plan_counts()
if metal_type ~= nil and metal_done < metal_need then
if place_build(commander, metal_type) then return end
end
if energy_type ~= nil and energy_done < energy_need then
if place_build(commander, energy_type) then return end
end
if factory_type ~= nil and not factory_ordered then
if place_build(commander, factory_type) then factory_ordered = true end
end
end
local function queue_plants()
if combat_type == nil then return end
for _, plant in pairs(factories) do
if plant.alive == true and free(plant) then plant:queue(combat_type) end
end
end
local function step()
build_next()
queue_plants()
try_send()
end
function hooks.fits() return true end
function hooks.start(_, _, saved)
sent = type(saved) == "table" and type(saved.sent) == "number" and saved.sent or 0
metal_done, energy_done, factory_ordered, factories, squad = 0, 0, false, {}, {}
metal_type, energy_type, factory_type, combat_type = nil, nil, nil, nil
ready, commander = false, find_commander()
prepare()
me:each_unit(function(unit)
if unit.finished == true then note_finished(unit) end
end)
if not armed then
brain.every(30, step)
armed = true
end
end
function hooks.onUnitFinished(unit)
note_finished(unit)
try_send()
end
function hooks.onUnitIdle(unit)
local record = unit.type
if record == nil then return end
if record.commander == true then
commander = unit
build_next()
elseif record.builder == true and record.bmcode == 0 then
factories[unit.id] = unit
queue_plants()
end
end
function hooks.save() return { sent = sent } end
return hooks
Hooks
La entrada devuelve una tabla. Una función que falta se salta. fits y start corren antes del primer tick. Un turno entrega después, en este orden: eventos de unidad encolados, radar y sonar perdidos y luego ganados, cambios de alianza, derrotas, chat, temporizadores y onTick. El primer turno guarda las alianzas y quién participa, y no informa de ninguno de esos cambios. Como mucho se guardan 65.536 eventos de un turno. Pasado eso, el evento se descarta.
| Hook | Cuándo | Argumentos |
|---|---|---|
fits | Antes del primer tick. Devuelve true, o un motivo en texto. Cualquier otra cosa impide sentarlo. | game |
start | Antes del primer tick. saved es lo que conservó una partida cargada, o nil. | game, me, saved |
onUnitCreated | Se crea una unidad. La unidad propia del asiento. builder es nil cuando no lo hay. | unit, builder |
onUnitFinished | Una unidad termina. La unidad propia del asiento. builder es nil cuando no lo hay o cuando es la unidad. Un evento de unidad terminada se descarta cuando lo sigue una muerte cancelada de la misma unidad. | unit, builder |
onUnitDamaged | La unidad propia del asiento recibe daño. kind es weapon, paralyze, heal o environment. attacker se pasa solo cuando el asiento puede conocer esa unidad, y en caso contrario es nil. | unit, attacker, amount, kind |
onUnitDestroyed | Muere una unidad: la propia del asiento, la de un aliado, un enemigo de la lista de vistos, o cualquier muerte cuando el cerebro tiene visión total. Una muerte de kind captured no es este hook. wreck es un nombre y una posición, o nil. | unit, killer, wreck |
onUnitGiven | Se cede una unidad. El dueño anterior y el dueño nuevo. | unit, old, from, to |
onUnitIdle | La unidad propia del asiento queda inactiva. | unit |
onCommandDone | Termina una orden sobre la unidad propia del asiento. result es done o failed. | unit, order, result |
onUnitLoaded | Se carga una unidad. El dueño de la unidad y el dueño del transporte. | unit, carrier |
onUnitUnloaded | Se descarga una unidad. El dueño de la unidad y el dueño del transporte. | unit, carrier |
onUnitSeen | El asiento gana visión de una unidad. | unit |
onUnitLost | El asiento pierde de vista una unidad. | unit |
onPathFailed | Una unidad no encuentra un camino. El dueño de la unidad. | unit |
onRadarLost | Un contacto sale de la imagen. Perdido antes que ganado, para el mismo slot. Una unidad aliada nunca es un contacto. | contact |
onRadarContact | Un contacto entra en la imagen. Un contacto no tiene tipo, ni dueño, ni manejador de unidad. | contact |
onAllianceChanged | Cambia un bit de alianza. Los demás jugadores, en orden de índice. | player, allied |
onPlayerDefeated | Un jugador deja de participar. | player |
onChat | Una línea de chat llegó al asiento. from es el índice de quien la envía. text no lleva el nombre delante. to es all, allies, enemies o me. Como mucho esperan 32 líneas. | from, text, to |
onTick | El final del turno. El argumento es el tick de la partida. | tick |
save | Cuando lo pide un guardado. Devuelve datos planos, o nada. Un error devuelve el asiento. Un valor que no es dato plano se deja fuera del guardado. | ninguno |
report | Cuando lo pide la consola. Devuelve texto, como mucho 64 KiB. Un cerebro sin report escribe una línea: el nombre del manifiesto, un espacio y la versión, y no da informe. | ninguno |
Qué puede saber un cerebro
game es una tabla de solo lectura mientras vive el asiento. fits y start reciben esa misma tabla. El estándar nombra cada campo. Estos son los grupos.
| Dónde | Qué hace |
|---|---|
game | El bundle de contenido, las expansiones que están activas, la versión del motor, el modo, la dificultad, si una partida está abierta, el mod (su identificador, nombre, versión, bandos, hacks y límites), los mutadores en el orden en que se aplican, el mapa y el perfil de IA. |
game.map | El nombre del mapa, sus tamaños, el nivel del mar, la marea y el viento, el metal y las alturas, las clases de movimiento, la transitabilidad, y si dos posiciones pueden alcanzarse por las celdas abiertas de una clase. No mira lo que ha visto el asiento, y no busca un camino. |
| Definiciones | units, weapons y features: los registros cargados de la partida, después del mod y de los mutadores. Son de solo lectura. |
me | El índice del jugador, el identificador del nivel, la dificultad, las opciones ya resueltas, los recursos, los aliados, las unidades y escuadras propias del asiento, la visión y la imagen del radar. Un contacto da una posición mientras sigue siendo un contacto. No tiene tipo ni dueño. |
| Un manejador de unidad | id y slot se leen en cualquier manejador. La posición, la salud y el tipo se leen cuando el asiento puede ver esa unidad viva, o cuando el cerebro tiene visión total. Las órdenes, la escuadra y el camuflaje se leen solo en la unidad viva propia del asiento. health sigue nil cuando el tipo oculta el daño, salvo que el asiento posea la unidad o el cerebro tenga visión total. |
| Visión total | Sin la bandera, game.units, el enemigo más cercano y la lista de vistos en bruto son nil. Con la bandera, game.units lista cada slot ocupado. me:sees y me:sees_unit no tratan la bandera como un sí. La bandera no concede el flujo aleatorio compartido ni una postura directa. |
Qué puede hacer un cerebro
Un manejador de unidad acepta las órdenes que puede dar un jugador. Cada llamada llega a la partida en el acto. La orden corre en el siguiente tick. Un rechazo devuelve false. La primera comprobación que falla es dead, not_own, over_limit (el turno ya ha pasado 4.096 órdenes a la partida), not_seen o not_buildable. Un rechazo de la partida cuenta como by_engine.
| Llamada | Qué hace |
|---|---|
unit:move | Un movimiento por tierra. |
unit:patrol | Una patrulla. |
unit:attack | Un ataque a una unidad. |
unit:attack_ground | Un ataque a terreno abierto. |
unit:attack_point | Un ataque a un punto, sin encolar. |
unit:dgun | El ataque especial, sobre una posición o una unidad. |
unit:guard | Vigilar una unidad. |
unit:repair | Reparar una unidad. |
unit:help | Ayudar en una construcción. |
unit:reclaim | Reciclar una unidad, o un elemento en una posición. |
unit:capture | Capturar una unidad. |
unit:load | Cargar una unidad. |
unit:unload | Descargar en una posición. |
unit:build | Una construcción móvil. facing va de 0 a 3. |
unit:queue | Una construcción de fábrica. count va de 1 a 1000, y el valor por defecto es 1. |
unit:requeue | Cambia la cantidad encolada. delta es un entero distinto de cero, de -1000 a 1000. |
unit:stop | Detener. |
unit:on | Pone el bit de activo. |
unit:cloak | Camuflar. |
unit:self_destruct | Conmuta la autodestrucción de esa sola unidad. |
unit:stance | move es hold, manoeuvre o roam. fire es hold, return o at_will. direct en true escribe los bits permanentes y no da orden, y solo Classic puede hacerlo. |
me:assign | Pone una de las unidades propias del asiento en una escuadra. Una unidad que el asiento no posee, o un manejador muerto, devuelve false. |
brain.say | Entrega una línea de chat a la partida. El panel de chat dice cómo. |
brain.site | Prueba una celda de construcción. facing va de 0 a 3. check es site o yard. |
brain.place | Recorre anillos desde un punto y devuelve la primera celda en la que se cumple la comprobación de sitio, y el centro de esa celda, o nil. |
map:reachable | Si dos posiciones pueden alcanzarse por una clase de movimiento. No busca un camino y no cambia el mapa de movimiento. |
Números
fixed cuenta 65.536avos. Los ejes son x al este, z al sur e y hacia arriba. Un ángulo es un entero, 65.536 para un giro completo. f32 guarda un valor binary32, el ancho de los recursos de un jugador. geometry.direction, geometry.planar_length, geometry.length, geometry.scale, geometry.sine_scaled y geometry.cosine_scaled son el rumbo, las distancias y el seno y el coseno escalados del propio motor.
Tiradas aleatorias
brain.random(n) devuelve un entero de 1 a n, del flujo propio del cerebro. Ese flujo se siembra a partir de la partida, el jugador y el identificador y la versión del cerebro. Las tiradas de ese flujo no avanzan el flujo compartido de la partida. Sustituir math.random no cambia brain.random.
brain.shared_random extrae del flujo compartido de la partida, y brain.setup_random extrae de una copia tomada en la preparación integrada del asiento. El estándar los instala para Classic, y dice dónde los tiene una ejecución en sombra. Ningún otro cerebro tiene ni uno ni otro. brain.float_heap_order ordena distancias enteras, la más cercana primero, y se instala en cada asiento.
Temporizadores
brain.after(ticks, fn) ejecuta fn una vez, ese número de turnos después. brain.every(ticks, fn) se ejecuta entonces, y otra vez cada ticks turnos después del tick de vencimiento que acaba de cumplir. ticks es un entero de 1 a 2147483647. Cada llamada devuelve un identificador. Los identificadores empiezan en 1 y solo crecen. brain.cancel(id) devuelve true cuando quitó un temporizador que esperaba. Un cerebro puede tener 1024 temporizadores en espera. Los temporizadores vencidos corren después del chat y antes de onTick. Un error dentro de un temporizador detiene el asiento igual que un error de hook.
Chat
onChat corre una vez por cada línea desde el último turno. Si falta onChat se salta, y la línea se consume igual. brain.say(text, to) entrega una línea a la partida en el acto. to es all (el valor por defecto), allies, enemies, o un índice de jugador de 0 a 9. La llamada devuelve true cuando la línea quedó encolada. No se espacia sola. Lo hace la partida, a 30 ticks entre líneas que dice un asiento de ordenador. Un rechazo es not_a_seat, empty, command, too_soon, no_target o no_chat.