Início › Info de modding › O Lua do cérebro
Capítulo 08

O Lua do cérebro

O manifesto e o script do Steady Commander, os hooks, o que um cérebro pode saber e fazer, números, sorteios aleatórios, temporizadores e chat.

Este capítulo resume docs/brains/oabrain-standard.md, docs/brains/authoring.md e docs/playing-with-brains.md. Esses documentos são o texto normativo.

Steady Commander

O Steady Commander é o cérebro fictício em brains/examples/steady-commander. Os dois ficheiros abaixo são copiados tal como estão escritos. works-with é um asterisco entre aspas, por isso o pacote serve para qualquer mod e para o jogo base. O script devolve uma tabela de hooks. Dê a essa tabela outro nome que não brain: um ficheiro que comece por local brain oculta a variável global que o perfil instala. fits devolve true. save devolve quantos esquadrões foram enviados. A escolha attack é soon, steady ou late.

Manifesto · 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

O script de entrada devolve uma tabela. Uma função em falta é ignorada. fits e start correm antes do primeiro tick. Depois, cada turno entrega, por esta ordem: eventos de unidade em fila, radar e sonar perdidos e depois ganhos, alterações de alianças, derrotas, chat, temporizadores e onTick. O primeiro turno guarda as alianças e quem está a participar, e não comunica nenhuma dessas alterações. São mantidos no máximo 65 536 eventos por turno. Para além disso, o evento é descartado.

HookQuandoArgumentos
fitsAntes do primeiro tick. Devolve true, ou um motivo em texto. Qualquer outra coisa impede a colocação no lugar.game
startAntes do primeiro tick. saved é o que um jogo carregado guardou, ou nil.game, me, saved
onUnitCreatedUma unidade é criada. Uma unidade do próprio lugar. builder é nil quando não há construtor.unit, builder
onUnitFinishedUma unidade fica concluída. Uma unidade do próprio lugar. builder é nil quando não há construtor ou quando é a própria unidade. Um evento de conclusão é descartado quando se lhe segue uma morte cancelada da mesma unidade.unit, builder
onUnitDamagedUma unidade do próprio lugar sofre dano. kind é weapon, paralyze, heal ou environment. attacker só é passado quando o lugar pode conhecer essa unidade, e é nil nos outros casos.unit, attacker, amount, kind
onUnitDestroyedUma unidade morre: uma do próprio lugar, uma de um aliado, um inimigo da lista de unidades vistas, ou qualquer morte quando o cérebro tem visão total. Uma morte do tipo captured não passa por este hook. wreck é um nome e uma posição, ou nil.unit, killer, wreck
onUnitGivenUma unidade é oferecida. O dono antigo e o novo dono.unit, old, from, to
onUnitIdleUma unidade do próprio lugar fica inativa.unit
onCommandDoneUma ordem numa unidade do próprio lugar termina. result é done ou failed.unit, order, result
onUnitLoadedUma unidade é carregada. O dono da unidade e o dono do transporte.unit, carrier
onUnitUnloadedUma unidade é descarregada. O dono da unidade e o dono do transporte.unit, carrier
onUnitSeenO lugar passa a ver uma unidade.unit
onUnitLostO lugar deixa de ver uma unidade.unit
onPathFailedUma unidade não consegue encontrar um caminho. O dono da unidade.unit
onRadarLostUm contacto sai da imagem. Perdido antes de ganho, para o mesmo slot. Uma unidade aliada nunca é um contacto.contact
onRadarContactUm contacto entra na imagem. Um contacto não tem tipo, dono nem identificador de unidade.contact
onAllianceChangedUm bit de aliança muda. Os outros jogadores, pela ordem do índice.player, allied
onPlayerDefeatedUm jogador deixa de participar.player
onChatUma linha de chat chegou ao lugar. from é o índice de quem a enviou. text não tem o nome à frente. to é all, allies, enemies ou me. Ficam à espera no máximo 32 linhas.from, text, to
onTickO fim do turno. O argumento é o tick da partida.tick
saveQuando um jogo guardado o pede. Devolve dados simples, ou nada. Um erro devolve o lugar ao computador integrado. Um valor que não seja um dado simples fica fora do jogo guardado.nenhum
reportQuando a consola o pede. Devolve texto, no máximo 64 KiB. Um cérebro sem relatório escreve uma linha, o nome do manifesto, um espaço e a versão, e não dá relatório.nenhum

O que um cérebro pode saber

game é uma tabela só de leitura durante toda a vida do lugar. fits e start recebem essa mesma tabela. A norma indica todos os campos. Estes são os grupos.

OndeO que faz
gameO conjunto de conteúdos, as expansões ativas, a versão do motor, o modo, a regra de início com que a partida colocou os jogadores (fixa ou aleatória), a dificuldade, se a partida é aberta, o mod (o seu id, nome, versão, lados, ajustes e limites), os mutadores pela ordem de aplicação, o mapa e o perfil de IA.
game.mapO nome do mapa, as suas dimensões, o nível do mar, a maré e o vento, o metal e as alturas, os pontos de metal e geotérmicos, as posições iniciais pela ordem de StartPos, as classes de movimento, a transitabilidade, se duas posições se podem ligar pelas células abertas de uma classe, e uma estimativa da rota que uma unidade de uma classe faria entre duas posições: o seu comprimento, quantas células atravessa e, se pedido, os pontos de viragem. Não tem em conta o que o lugar viu. Saber se duas posições se podem ligar não procura um caminho, e a estimativa da rota procura.
Definiçõesunits, weapons e features: os registos carregados da partida, depois do mod e dos mutadores. São só de leitura.
meO índice do jogador, o id do nível, a dificuldade, as opções resolvidas, os recursos, os aliados, todos os jogadores da partida e quantos são, as unidades e os esquadrões do próprio lugar, a visão e a imagem do radar. Cada jogador vem com a sua posição inicial quando a regra de início a dá a conhecer (todos os jogadores com inícios fixos, só a do próprio lugar com inícios aleatórios) e com os jogadores que considera aliados. Um contacto de radar dá uma posição enquanto continua a ser um contacto. Não tem tipo nem dono.
Um identificador de unidadeid e slot leem-se em qualquer identificador. A posição, a vida e o tipo leem-se quando o lugar consegue ver essa unidade viva, ou quando o cérebro tem visão total. As ordens, o esquadrão e a camuflagem só se leem numa unidade viva do próprio lugar. health fica a nil quando o tipo oculta o dano, a não ser que o lugar seja dono da unidade ou o cérebro tenha visão total.
Visão totalSem a marca, game.units, o inimigo mais próximo e a lista bruta de unidades vistas são nil. Com a marca, game.units lista todos os slots ocupados. me:sees e me:sees_unit não tratam a marca como um sim. A marca não concede o fluxo aleatório partilhado nem uma postura direta.

O que um cérebro pode fazer

Um identificador de unidade aceita as ordens que um jogador pode dar. Cada chamada chega de imediato à partida. A ordem é executada no tick seguinte. Uma recusa devolve false. A primeira verificação que falha é dead, not_own, over_limit (o turno já passou 4096 ordens à partida), not_seen ou not_buildable. Uma recusa da partida conta como by_engine.

ChamadaO que faz
unit:moveUm movimento em terra.
unit:patrolUma patrulha.
unit:attackUm ataque a uma unidade.
unit:attack_groundUm ataque a terreno aberto.
unit:attack_pointUm ataque a um ponto, sem fila.
unit:dgunO ataque especial, a uma posição ou a uma unidade.
unit:guardProteger uma unidade.
unit:repairReparar uma unidade.
unit:helpAjudar uma construção.
unit:reclaimReciclar uma unidade, ou um elemento numa posição.
unit:captureCapturar uma unidade.
unit:loadCarregar uma unidade.
unit:unloadDescarregar numa posição.
unit:buildUma construção móvel. facing é de 0 a 3.
unit:queueUma construção de fábrica. count é de 1 a 1000, e a predefinição é 1.
unit:requeueAltera a quantidade em fila. delta é um número inteiro diferente de zero, de -1000 a 1000.
unit:stopParar.
unit:onDefine o bit de ativo.
unit:cloakCamuflar.
unit:self_destructLiga ou desliga a autodestruição dessa unidade.
unit:stancemove é hold, manoeuvre ou roam. fire é hold, return ou at_will. direct definido como true escreve os bits permanentes e não dá nenhuma ordem, e só o Classic o pode fazer.
me:assignColoca uma das unidades do próprio lugar num esquadrão. Uma unidade de que o lugar não é dono, ou um identificador morto, devolve false.
brain.sayEntrega uma linha de chat à partida. O painel Chat explica como.
brain.siteTesta uma célula de construção. facing é de 0 a 3. check é site ou yard.
brain.placePercorre anéis a partir de um ponto e devolve a primeira célula em que a verificação do local se cumpre, e o centro dessa célula, ou nil.
map:reachableSe duas posições se podem ligar através de uma classe de movimento. Não procura um caminho nem altera o mapa de movimento.
map:routeEstima a rota que uma unidade de uma classe de movimento faria, e não altera nada na partida. A chamada cobra uma instrução por cada célula expandida, mais 16. Para ao fim de 1 048 576 células expandidas e devolve nil. Pedir os pontos de viragem faz uma nova procura e cobra uma segunda vez. Um cérebro que só precise de saber se dois locais se ligam deve perguntar primeiro a map:reachable.

Números

fixed conta em 65536 avos. Os eixos são x para leste, z para sul e y para cima. Um ângulo é um número inteiro, 65536 por volta completa. f32 contém um valor binary32, a largura dos recursos de um jogador. geometry.direction, geometry.planar_length, geometry.length, geometry.scale, geometry.sine_scaled e geometry.cosine_scaled são a orientação, as distâncias e o seno e cosseno escalados do próprio motor.

Sorteios aleatórios

brain.random(n) devolve um número inteiro de 1 a n, a partir do fluxo próprio do cérebro. Esse fluxo tem como semente a partida, o jogador e o id e a versão do cérebro. Os sorteios feitos a partir dele não fazem avançar o fluxo partilhado da partida. Substituir math.random não altera brain.random.

brain.shared_random sorteia a partir do fluxo partilhado da partida, e brain.setup_random sorteia a partir de uma cópia feita na preparação integrada do lugar. A norma instala-os para o Classic e diz onde uma execução sombra os tem. Nenhum outro cérebro tem qualquer deles. brain.float_heap_order ordena distâncias inteiras, das mais próximas para as mais distantes, e é instalado para todos os lugares.

Temporizadores

brain.after(ticks, fn) corre fn uma vez, esse número de turnos depois. brain.every(ticks, fn) corre nesse momento e de novo a cada ticks turnos depois do tick devido que acabou de cumprir. ticks é um número inteiro de 1 a 2147483647. Cada chamada devolve um id. Os ids começam em 1 e só crescem. brain.cancel(id) devolve true quando removeu um temporizador em espera. Um cérebro pode manter 1024 temporizadores em espera. Os temporizadores devidos correm depois do chat e antes de onTick. Um erro dentro de um temporizador para o lugar, tal como um erro num hook.

Chat

onChat corre uma vez por cada linha desde o último turno. Um onChat em falta é ignorado, e a linha é consumida na mesma. brain.say(text, to) entrega de imediato uma linha à partida. to é all (a predefinição), allies, enemies ou um índice de jogador de 0 a 9. A chamada devolve true quando a linha ficou em fila. Não controla o próprio ritmo. É a partida que o faz, com 30 ticks entre as linhas que um lugar de computador diz. Uma recusa é not_a_seat, empty, command, too_soon, no_target ou no_chat.