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.
| Hook | Quando | Argumentos |
|---|---|---|
fits | Antes do primeiro tick. Devolve true, ou um motivo em texto. Qualquer outra coisa impede a colocação no lugar. | game |
start | Antes do primeiro tick. saved é o que um jogo carregado guardou, ou nil. | game, me, saved |
onUnitCreated | Uma unidade é criada. Uma unidade do próprio lugar. builder é nil quando não há construtor. | unit, builder |
onUnitFinished | Uma 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 |
onUnitDamaged | Uma 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 |
onUnitDestroyed | Uma 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 |
onUnitGiven | Uma unidade é oferecida. O dono antigo e o novo dono. | unit, old, from, to |
onUnitIdle | Uma unidade do próprio lugar fica inativa. | unit |
onCommandDone | Uma ordem numa unidade do próprio lugar termina. result é done ou failed. | unit, order, result |
onUnitLoaded | Uma unidade é carregada. O dono da unidade e o dono do transporte. | unit, carrier |
onUnitUnloaded | Uma unidade é descarregada. O dono da unidade e o dono do transporte. | unit, carrier |
onUnitSeen | O lugar passa a ver uma unidade. | unit |
onUnitLost | O lugar deixa de ver uma unidade. | unit |
onPathFailed | Uma unidade não consegue encontrar um caminho. O dono da unidade. | unit |
onRadarLost | Um contacto sai da imagem. Perdido antes de ganho, para o mesmo slot. Uma unidade aliada nunca é um contacto. | contact |
onRadarContact | Um contacto entra na imagem. Um contacto não tem tipo, dono nem identificador de unidade. | contact |
onAllianceChanged | Um bit de aliança muda. Os outros jogadores, pela ordem do índice. | player, allied |
onPlayerDefeated | Um jogador deixa de participar. | player |
onChat | Uma 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 |
onTick | O fim do turno. O argumento é o tick da partida. | tick |
save | Quando 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 |
report | Quando 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.
| Onde | O que faz |
|---|---|
game | O 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.map | O 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ções | units, weapons e features: os registos carregados da partida, depois do mod e dos mutadores. São só de leitura. |
me | O í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 unidade | id 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 total | Sem 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.
| Chamada | O que faz |
|---|---|
unit:move | Um movimento em terra. |
unit:patrol | Uma patrulha. |
unit:attack | Um ataque a uma unidade. |
unit:attack_ground | Um ataque a terreno aberto. |
unit:attack_point | Um ataque a um ponto, sem fila. |
unit:dgun | O ataque especial, a uma posição ou a uma unidade. |
unit:guard | Proteger uma unidade. |
unit:repair | Reparar uma unidade. |
unit:help | Ajudar uma construção. |
unit:reclaim | Reciclar uma unidade, ou um elemento numa posição. |
unit:capture | Capturar uma unidade. |
unit:load | Carregar uma unidade. |
unit:unload | Descarregar numa posição. |
unit:build | Uma construção móvel. facing é de 0 a 3. |
unit:queue | Uma construção de fábrica. count é de 1 a 1000, e a predefinição é 1. |
unit:requeue | Altera a quantidade em fila. delta é um número inteiro diferente de zero, de -1000 a 1000. |
unit:stop | Parar. |
unit:on | Define o bit de ativo. |
unit:cloak | Camuflar. |
unit:self_destruct | Liga ou desliga a autodestruição dessa unidade. |
unit:stance | move é 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:assign | Coloca 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.say | Entrega uma linha de chat à partida. O painel Chat explica como. |
brain.site | Testa uma célula de construção. facing é de 0 a 3. check é site ou yard. |
brain.place | Percorre 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:reachable | Se 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:route | Estima 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.