Home › Info modding › Il Lua del cervello
Capitolo 08

Il Lua del cervello

Il manifesto e lo script di Steady Commander, gli hook, che cosa un cervello può sapere e fare, i numeri, le estrazioni casuali, i timer e la chat.

Questo capitolo riassume docs/brains/oabrain-standard.md, docs/brains/authoring.md e docs/playing-with-brains.md. Quei documenti sono il testo normativo.

Steady Commander

Steady Commander è il cervello inventato in brains/examples/steady-commander. I due file sotto sono copiati come sono scritti. works-with è un asterisco tra virgolette, così il pacchetto si adatta a qualsiasi mod e al gioco base. Lo script restituisce una tabella di hook. Dai a quella tabella un nome diverso da brain: un file che inizia con local brain oscura il globale che il profilo installa. fits restituisce true. save restituisce quante squadre sono state inviate. La scelta di attacco è soon, steady o 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 di ingresso · 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

Hook

L'ingresso restituisce una tabella. Una funzione mancante viene saltata. fits e start girano prima del primo tick. Un turno consegna poi, in quest'ordine: eventi di unità in coda, radar e sonar persi e poi guadagnati, cambiamenti di alleanza, sconfitte, chat, timer e onTick. Il primo turno memorizza le alleanze e chi partecipa, e non segnala nessuno di quei cambiamenti. Se ne tengono al massimo 65.536 per un turno. Oltre, l'evento viene scartato.

HookQuandoArgomenti
fitsPrima del primo tick. Restituisce true, o un motivo in stringa. Qualsiasi altra cosa impedisce di metterlo al posto.game
startPrima del primo tick. saved è ciò che ha tenuto una partita caricata, o nil.game, me, saved
onUnitCreatedViene creata un'unità. L'unità propria del posto. builder è nil quando non c'è.unit, builder
onUnitFinishedUn'unità finisce. L'unità propria del posto. builder è nil quando non c'è, o quando è l'unità. Un evento di fine viene scartato quando lo segue una morte annullata della stessa unità.unit, builder
onUnitDamagedL'unità propria del posto subisce danno. kind è weapon, paralyze, heal o environment. attacker viene passato solo quando il posto può conoscere quell'unità, altrimenti è nil.unit, attacker, amount, kind
onUnitDestroyedMuore un'unità: quella propria del posto, quella di un alleato, un nemico nell'elenco dei visti, o qualsiasi morte quando il cervello ha la vista completa. Una morte di kind captured non è questo hook. wreck è un nome e una posizione, o nil.unit, killer, wreck
onUnitGivenUn'unità viene ceduta. Il proprietario di prima e il proprietario nuovo.unit, old, from, to
onUnitIdleL'unità propria del posto diventa inattiva.unit
onCommandDoneFinisce un ordine sull'unità propria del posto. result è done o failed.unit, order, result
onUnitLoadedUn'unità viene caricata. Il proprietario dell'unità e il proprietario del trasporto.unit, carrier
onUnitUnloadedUn'unità viene scaricata. Il proprietario dell'unità e il proprietario del trasporto.unit, carrier
onUnitSeenIl posto guadagna la vista di un'unità.unit
onUnitLostIl posto perde di vista un'unità.unit
onPathFailedUn'unità non trova un percorso. Il proprietario dell'unità.unit
onRadarLostUn contatto esce dall'immagine. Perso prima del guadagnato, per lo stesso slot. Un'unità alleata non è mai un contatto.contact
onRadarContactUn contatto entra nell'immagine. Un contatto non ha tipo, né proprietario, né handle di unità.contact
onAllianceChangedCambia un bit di alleanza. Gli altri giocatori, in ordine di indice.player, allied
onPlayerDefeatedUn giocatore smette di partecipare.player
onChatUna riga di chat è arrivata al posto. from è l'indice di chi la manda. text non ha il nome davanti. to è all, allies, enemies o me. Ne aspettano al massimo 32.from, text, to
onTickLa fine del turno. L'argomento è il tick della partita.tick
saveQuando lo chiede un salvataggio. Restituisce dati semplici, o niente. Un errore riconsegna il posto. Un valore che non è un dato semplice resta fuori dal salvataggio.nessuno
reportQuando lo chiede la console. Restituisce testo, al massimo 64 KiB. Un cervello senza report scrive una riga: il nome del manifesto, uno spazio e la versione, e non dà un report.nessuno

Che cosa può sapere un cervello

game è una tabella in sola lettura per la vita del posto. fits e start ricevono quella stessa tabella. Lo standard nomina ogni campo. Questi sono i gruppi.

DoveCosa fa
gameIl bundle dei contenuti, le espansioni che sono accese, la versione del motore, la modalità, la difficoltà, se una partita è aperta, la mod (identificativo, nome, versione, fazioni, regole e limiti), i mutatori nell'ordine in cui si applicano, la mappa e il profilo IA.
game.mapIl nome della mappa, le sue dimensioni, il livello del mare, la marea e il vento, il metallo e le altezze, le classi di movimento, la percorribilità, e se due posizioni possono raggiungersi attraverso le celle aperte di una classe. Non guarda che cosa ha visto il posto, e non cerca un percorso.
Definizioniunits, weapons e features: le schede caricate della partita, dopo la mod e i mutatori. Sono in sola lettura.
meL'indice del giocatore, l'identificativo del livello, la difficoltà, le opzioni risolte, le risorse, gli alleati, le unità e le squadre proprie del posto, la vista e l'immagine radar. Un contatto dà una posizione finché resta un contatto. Non ha tipo né proprietario.
Un handle di unitàid e slot si leggono su qualsiasi handle. Posizione, salute e tipo si leggono quando il posto può vedere quell'unità viva, o quando il cervello ha la vista completa. Gli ordini, la squadra e l'occultamento si leggono solo sull'unità viva propria del posto. health resta nil quando il tipo nasconde il danno, salvo che il posto possieda l'unità o il cervello abbia la vista completa.
Vista completaSenza il flag, game.units, il nemico più vicino e l'elenco dei visti grezzo sono nil. Con il flag, game.units elenca ogni slot occupato. me:sees e me:sees_unit non trattano il flag come un sì. Il flag non concede il flusso casuale condiviso né una stance diretta.

Che cosa può fare un cervello

Un handle di unità prende gli ordini che può dare un giocatore. Ogni chiamata arriva subito alla partita. L'ordine viene eseguito nel tick successivo. Un rifiuto restituisce false. Il primo controllo che fallisce è dead, not_own, over_limit (il turno ha già passato 4.096 ordini alla partita), not_seen o not_buildable. Un rifiuto della partita conta come by_engine.

ChiamataCosa fa
unit:moveUn movimento a terra.
unit:patrolUn pattugliamento.
unit:attackUn attacco a un'unità.
unit:attack_groundUn attacco sul terreno aperto.
unit:attack_pointUn attacco a un punto, non in coda.
unit:dgunL'attacco speciale, su una posizione o un'unità.
unit:guardSorveglia un'unità.
unit:repairRipara un'unità.
unit:helpAiuta una costruzione.
unit:reclaimRicicla un'unità, o un elemento in una posizione.
unit:captureCattura un'unità.
unit:loadCarica un'unità.
unit:unloadScarica in una posizione.
unit:buildUna costruzione mobile. facing va da 0 a 3.
unit:queueUna costruzione di fabbrica. count va da 1 a 1000, e il valore predefinito è 1.
unit:requeueCambia il conteggio in coda. delta è un intero diverso da zero, da -1000 a 1000.
unit:stopFerma.
unit:onImposta il bit attivo.
unit:cloakOcculta.
unit:self_destructCommuta l'autodistruzione di quell'unica unità.
unit:stancemove è hold, manoeuvre o roam. fire è hold, return o at_will. direct impostato a true scrive i bit permanenti e non dà un ordine, e solo Classic può farlo.
me:assignMette un'unità propria del posto in una squadra. Un'unità che il posto non possiede, o un handle morto, restituisce false.
brain.sayConsegna una riga di chat alla partita. Il pannello della chat dice come.
brain.siteProva una cella di costruzione. facing va da 0 a 3. check è site o yard.
brain.placePercorre anelli da un punto e restituisce la prima cella in cui vale il controllo di sito, e il centro di quella cella, o nil.
map:reachableSe due posizioni possono raggiungersi attraverso una classe di movimento. Non cerca un percorso e non cambia la mappa di movimento.

Numeri

fixed conta 65.536esimi. Gli assi sono x est, z sud e y su. Un angolo è un intero, 65.536 per un giro completo. f32 tiene un valore binary32, la larghezza delle risorse di un giocatore. geometry.direction, geometry.planar_length, geometry.length, geometry.scale, geometry.sine_scaled e geometry.cosine_scaled sono la prua, le distanze e il seno e il coseno scalati propri del motore.

Estrazioni casuali

brain.random(n) restituisce un intero da 1 a n, dal flusso proprio del cervello. Quel flusso è seminato dalla partita, dal giocatore e dall'identificativo e dalla versione del cervello. Le estrazioni da quel flusso non fanno avanzare il flusso condiviso della partita. Sostituire math.random non cambia brain.random.

brain.shared_random estrae il flusso condiviso della partita, e brain.setup_random estrae una copia presa alla preparazione integrata del posto. Lo standard li installa per Classic, e dice dove li ha un'esecuzione in ombra. Nessun altro cervello ha né l'uno né l'altro. brain.float_heap_order ordina distanze intere, la più vicina per prima, ed è installato per ogni posto.

Timer

brain.after(ticks, fn) esegue fn una volta, quel numero di turni dopo. brain.every(ticks, fn) si esegue allora, e di nuovo ogni ticks turni dopo il tick di scadenza che ha appena rispettato. ticks è un intero da 1 a 2147483647. Ogni chiamata restituisce un identificativo. Gli identificativi partono da 1 e crescono soltanto. brain.cancel(id) restituisce true quando ha tolto un timer in attesa. Un cervello può tenere 1024 timer in attesa. I timer scaduti girano dopo la chat e prima di onTick. Un errore dentro un timer ferma il posto come fa un errore di un hook.

Chat

onChat gira una volta per riga dall'ultimo turno. Un onChat mancante viene saltato, e la riga viene consumata lo stesso. brain.say(text, to) consegna subito una riga alla partita. to è all (il valore predefinito), allies, enemies, o un indice di giocatore da 0 a 9. La chiamata restituisce true quando la riga è stata messa in coda. Non si cadenza da sola. Lo fa la partita, a 30 tick fra le righe che dice un posto computer. Un rifiuto è not_a_seat, empty, command, too_soon, no_target o no_chat.