Start › Modding-Infos › Das Lua des Gehirns
Kapitel 08

Das Lua des Gehirns

Manifest und Skript von Steady Commander, die Hooks, was ein Gehirn wissen und tun darf, Zahlen, Zufallsziehungen, Timer und Chat.

Dieses Kapitel fasst docs/brains/oabrain-standard.md, docs/brains/authoring.md und docs/playing-with-brains.md zusammen. Diese Dokumente sind der maßgebliche Text.

Steady Commander

Steady Commander ist das erfundene Gehirn in brains/examples/steady-commander. Die beiden Dateien unten sind so kopiert, wie sie geschrieben sind. works-with ist ein Stern in Anführungszeichen, darum passt das Paket zu jedem Mod und zum Grundspiel. Das Skript gibt eine Tabelle von Hooks zurück. Benenne diese Tabelle anders als brain: eine Datei, die mit local brain beginnt, verdeckt das Globale, das das Profil einrichtet. fits gibt true zurück. save gibt zurück, wie viele Trupps geschickt wurden. Die Angriffswahl ist soon, steady oder late.

Manifest · 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
Einstiegsskript · 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

Der Einstieg gibt eine Tabelle zurück. Eine fehlende Funktion wird übersprungen. fits und start laufen vor dem ersten Tick. Ein Zug liefert dann, in dieser Reihenfolge: eingereihte Einheitenereignisse, Radar und Sonar verloren dann gewonnen, Bündnisänderungen, Niederlagen, Chat, Timer und onTick. Der erste Zug speichert die Bündnisse und wer teilnimmt und meldet keine dieser Änderungen. Höchstens 65.536 Ereignisse werden für einen Zug behalten. Darüber wird das Ereignis verworfen.

HookWannArgumente
fitsVor dem ersten Tick. Gibt true oder einen Grund als Zeichenkette zurück. Alles andere bricht das Einsetzen ab.game
startVor dem ersten Tick. saved ist, was ein geladenes Spiel behalten hat, oder nil.game, me, saved
onUnitCreatedEine Einheit wird erzeugt. Die eigene Einheit des Sitzes. builder ist nil, wenn es keinen gibt.unit, builder
onUnitFinishedEine Einheit wird fertig. Die eigene Einheit des Sitzes. builder ist nil, wenn es keinen gibt oder es die Einheit selbst ist. Ein Fertig-Ereignis wird verworfen, wenn ein annullierter Tod derselben Einheit darauf folgt.unit, builder
onUnitDamagedDie eigene Einheit des Sitzes nimmt Schaden. kind ist weapon, paralyze, heal oder environment. attacker wird nur übergeben, wenn der Sitz diese Einheit kennen darf, und ist sonst nil.unit, attacker, amount, kind
onUnitDestroyedEine Einheit stirbt: die eigene des Sitzes, die eines Verbündeten, ein Gegner auf der Liste der gesehenen Einheiten, oder jeder Tod, wenn das Gehirn volle Sicht hat. Ein Tod mit kind captured ist nicht dieser Hook. wreck ist ein Name und eine Position, oder nil.unit, killer, wreck
onUnitGivenEine Einheit wird übergeben. Der alte Besitzer und der neue Besitzer.unit, old, from, to
onUnitIdleDie eigene Einheit des Sitzes wird untätig.unit
onCommandDoneEin Befehl an der eigenen Einheit des Sitzes wird fertig. result ist done oder failed.unit, order, result
onUnitLoadedEine Einheit wird geladen. Der Besitzer der Einheit und der Besitzer des Transporters.unit, carrier
onUnitUnloadedEine Einheit wird entladen. Der Besitzer der Einheit und der Besitzer des Transporters.unit, carrier
onUnitSeenDer Sitz bekommt eine Einheit zu sehen.unit
onUnitLostDer Sitz verliert eine Einheit aus der Sicht.unit
onPathFailedEine Einheit findet keinen Weg. Der Besitzer der Einheit.unit
onRadarLostEin Kontakt verlässt das Bild. Zuerst verloren, dann gewonnen, für denselben Slot. Eine verbündete Einheit ist nie ein Kontakt.contact
onRadarContactEin Kontakt kommt ins Bild. Ein Kontakt hat keinen Typ, keinen Besitzer und kein Einheiten-Handle.contact
onAllianceChangedEin Bündnis-Bit ändert sich. Die anderen Spieler, in Indexreihenfolge.player, allied
onPlayerDefeatedEin Spieler nimmt nicht mehr teil.player
onChatEine Chat-Zeile hat den Sitz erreicht. from ist der Index des Absenders. text hat vorn keinen Namen. to ist all, allies, enemies oder me. Höchstens 32 Zeilen warten.from, text, to
onTickDas Ende des Zugs. Das Argument ist der Tick des Matches.tick
saveWenn ein Spielstand fragt. Gibt einfache Daten zurück, oder nichts. Ein Fehler gibt den Sitz zurück. Ein Wert, der keine einfachen Daten ist, bleibt aus dem Spielstand draußen.keine
reportWenn die Konsole fragt. Gibt Text zurück, höchstens 64 KiB. Ein Gehirn ohne report schreibt eine Zeile: den Namen aus dem Manifest, ein Leerzeichen, die Version, und gibt keinen Bericht.keine

Was ein Gehirn wissen darf

game ist für die Lebensdauer des Sitzes eine einzige nur lesbare Tabelle. fits und start bekommen dieselbe Tabelle. Der Standard nennt jedes Feld. Das sind die Gruppen.

WoWirkung
gameDas Inhaltsbündel, die eingeschalteten Erweiterungen, die Engine-Version, der Modus, die Schwierigkeit, ob ein Match offen ist, der Mod (seine Kennung, sein Name, seine Version, Seiten, Regeln und Limits), die Mutatoren in Anwendungsreihenfolge, die Karte und das KI-Profil.
game.mapDer Name der Karte, ihre Größen, der Meeresspiegel, Gezeiten und Wind, Metall und Höhen, Bewegungsklassen, die Begehbarkeit und ob zwei Positionen sich durch die offenen Zellen einer Klasse treffen können. Es schaut nicht darauf, was der Sitz gesehen hat, und es sucht keinen Weg.
Definitionenunits, weapons und features: die geladenen Einträge des Matches, nach dem Mod und den Mutatoren. Sie sind nur lesbar.
meDer Spielerindex, die Kennung der Stufe, die Schwierigkeit, die aufgelösten Optionen, Ressourcen, Verbündete, die eigenen Einheiten und Trupps des Sitzes, die Sicht und das Radarbild. Ein Kontakt gibt eine Position, solange er noch ein Kontakt ist. Er hat keinen Typ und keinen Besitzer.
Ein Einheiten-Handleid und slot lassen sich an jedem Handle lesen. Position, health und der Typ lassen sich lesen, wenn der Sitz diese lebende Einheit sehen kann oder das Gehirn volle Sicht hat. Befehle, der Trupp und die Tarnung lassen sich nur an der eigenen lebenden Einheit des Sitzes lesen. health bleibt nil, wenn der Typ Schaden verbirgt, außer wenn der Sitz die Einheit besitzt oder das Gehirn volle Sicht hat.
Volle SichtOhne das Flag sind game.units, der nächste Gegner und die rohe Liste der gesehenen Einheiten nil. Mit dem Flag listet game.units jeden besetzten Slot. me:sees und me:sees_unit werten das Flag nicht als Ja. Das Flag verleiht weder den gemeinsamen Zufallsstrom noch eine direkte Haltung.

Was ein Gehirn tun darf

Ein Einheiten-Handle nimmt die Befehle, die ein Spieler geben kann. Jeder Aufruf erreicht das Match sofort. Der Befehl läuft im nächsten Tick. Eine Ablehnung gibt false zurück. Die erste Prüfung, die scheitert, ist dead, not_own, over_limit (der Zug hat dem Match schon 4.096 Befehle übergeben), not_seen oder not_buildable. Eine Ablehnung des Matches zählt als by_engine.

AufrufWirkung
unit:moveEine Bodenbewegung.
unit:patrolEine Patrouille.
unit:attackEin Angriff auf eine Einheit.
unit:attack_groundEin Angriff auf offenes Gelände.
unit:attack_pointEin Angriff auf einen Punkt, nicht eingereiht.
unit:dgunDer Spezialangriff, auf eine Position oder eine Einheit.
unit:guardEine Einheit bewachen.
unit:repairEine Einheit reparieren.
unit:helpEinen Bau unterstützen.
unit:reclaimEine Einheit recyceln, oder ein Geländeobjekt an einer Position.
unit:captureEine Einheit übernehmen.
unit:loadEine Einheit laden.
unit:unloadAn einer Position entladen.
unit:buildEin mobiler Bau. facing ist 0 bis 3.
unit:queueEin Fabrikbau. count ist 1 bis 1.000, und der Standard ist 1.
unit:requeueÄndert die eingereihte Zahl. delta ist eine Ganzzahl ungleich null von -1.000 bis 1.000.
unit:stopAnhalten.
unit:onSetzt das Aktiv-Bit.
unit:cloakTarnen.
unit:self_destructSchaltet die Selbstzerstörung für diese eine Einheit um.
unit:stancemove ist hold, manoeuvre oder roam. fire ist hold, return oder at_will. direct auf true gesetzt schreibt die Bits der Dauerbefehle und gibt keinen Befehl, und nur Classic darf das.
me:assignSetzt eine eigene Einheit des Sitzes in einen Trupp. Eine Einheit, die der Sitz nicht besitzt, oder ein totes Handle gibt false zurück.
brain.sayGibt eine Chat-Zeile an das Match. Der Abschnitt Chat sagt, wie.
brain.sitePrüft eine Bauzelle. facing ist 0 bis 3. check ist site oder yard.
brain.placeGeht in Ringen von einem Punkt aus und gibt die erste Zelle zurück, an der die site-Prüfung zutrifft, und den Mittelpunkt dieser Zelle, oder nil.
map:reachableOb zwei Positionen sich durch eine Bewegungsklasse treffen können. Es sucht keinen Weg und ändert die Bewegungskarte nicht.

Zahlen

fixed zählt 65.536stel. Die Achsen sind x nach Osten, z nach Süden und y nach oben. Ein Winkel ist eine Ganzzahl, 65.536 für eine volle Umdrehung. f32 hält einen binary32-Wert von der Breite der Ressourcen eines Spielers. geometry.direction, geometry.planar_length, geometry.length, geometry.scale, geometry.sine_scaled und geometry.cosine_scaled sind Kurs, Abstände und skalierter Sinus und Kosinus der Engine selbst.

Zufallsziehungen

brain.random(n) gibt eine Ganzzahl von 1 bis n zurück, aus dem eigenen Strom des Gehirns. Dieser Strom wird aus dem Match, dem Spieler und der Kennung und der Version des Gehirns gesät. Ziehungen daraus rücken den gemeinsamen Strom des Matches nicht voran. math.random zu ersetzen ändert brain.random nicht.

brain.shared_random zieht aus dem gemeinsamen Strom des Matches, und brain.setup_random zieht aus einer Kopie, die beim eingebauten Setup des Sitzes genommen wurde. Der Standard richtet sie für Classic ein und sagt, wo ein Schattenlauf sie hat. Jedes andere Gehirn hat keines von beiden. brain.float_heap_order sortiert ganzzahlige Abstände, der nächste zuerst, und ist für jeden Sitz eingerichtet.

Timer

brain.after(ticks, fn) führt fn einmal aus, so viele Züge später. brain.every(ticks, fn) führt dann aus und wieder alle ticks Züge nach dem fälligen Tick, den es gerade erreicht hat. ticks ist eine Ganzzahl von 1 bis 2147483647. Jeder Aufruf gibt eine Kennung zurück. Die Kennungen beginnen bei 1 und wachsen nur. brain.cancel(id) gibt true zurück, wenn es einen wartenden Timer entfernt hat. Ein Gehirn darf 1024 Timer wartend halten. Fällige Timer laufen nach dem Chat und vor onTick. Ein Fehler in einem Timer stoppt den Sitz wie ein Fehler in einem Hook.

Chat

onChat läuft einmal je Zeile seit dem letzten Zug. Ein fehlendes onChat wird übersprungen, und die Zeile wird trotzdem verbraucht. brain.say(text, to) gibt eine Zeile sofort an das Match. to ist all (der Standard), allies, enemies oder ein Spielerindex von 0 bis 9. Der Aufruf gibt true zurück, wenn die Zeile eingereiht wurde. Er taktet sich nicht selbst. Das Match tut es, mit 30 Ticks zwischen Zeilen, die ein Computersitz sagt. Eine Ablehnung ist not_a_seat, empty, command, too_soon, no_target oder no_chat.