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, timers 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
Steady Commander é um cérebro inventado, que fica em brains/examples/steady-commander. Os dois arquivos abaixo estão copiados como foram escritos. works-with é um asterisco entre aspas, então o pacote serve para qualquer mod e para o jogo base. O script retorna uma tabela de hooks. Dê a essa tabela um nome diferente de brain: um arquivo que começa com local brain encobre a global que o perfil instala. fits retorna true. save retorna quantos esquadrões foram enviados. A escolha de ataque é 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
A entrada retorna uma tabela. Uma função ausente é ignorada. fits e start rodam antes do primeiro tick. Depois, cada turno entrega, nesta ordem: eventos de unidade enfileirados, radar e sonar perdidos e depois ganhos, mudanças de aliança, derrotas, chat, timers e onTick. O primeiro turno guarda as alianças e quem está participando, e não informa nenhuma dessas mudanças. No máximo 65.536 eventos são guardados por turno. Além disso, o evento é descartado.
| Hook | Quando | Argumentos |
|---|---|---|
fits | Antes do primeiro tick. Retorna true ou uma string com o motivo. Qualquer outra coisa impede que o cérebro ocupe o lugar. | game |
start | Antes do primeiro tick. saved é o que um jogo carregado guardou, ou nil. | game, me, saved |
onUnitCreated | Uma unidade é criada. A unidade é do próprio lugar. builder é nil quando não há nenhum. | unit, builder |
onUnitFinished | Uma unidade fica pronta. A unidade é do próprio lugar. builder é nil quando não há nenhum ou quando é a própria unidade. Um evento de conclusão é descartado quando uma morte cancelada da mesma unidade vem depois dele. | 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 aquela unidade; do contrário, é nil. | unit, attacker, amount, kind |
onUnitDestroyed | Uma unidade morre: do próprio lugar, de um aliado, um inimigo da lista de vistos, 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 é doada. O dono antigo e o novo dono. | unit, old, from, to |
onUnitIdle | Uma unidade do próprio lugar fica ociosa. | 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 encontra caminho. O dono da unidade. | unit |
onRadarLost | Um contato sai do quadro do radar. Para o mesmo slot, o perdido vem antes do ganho. Uma unidade aliada nunca é um contato. | contact |
onRadarContact | Um contato entra no quadro do radar. Um contato não tem tipo, dono nem handle de unidade. | contact |
onAllianceChanged | Um bit de aliança muda. Os outros jogadores, na ordem dos índices. | player, allied |
onPlayerDefeated | Um jogador deixa de participar. | player |
onChat | Uma linha de chat chegou ao lugar. from é o índice de quem enviou. text não traz o nome no começo. to é all, allies, enemies ou me. No máximo 32 linhas ficam esperando. | from, text, to |
onTick | O fim do turno. O argumento é o tick da partida. | tick |
save | Quando um salvamento pede. Retorna dados simples, ou nada. Um erro devolve o lugar ao jogador integrado. Um valor que não é dado simples fica fora do jogo salvo. | nenhum |
report | Quando o console pede. Retorna texto, no máximo 64 KiB. Um cérebro sem report escreve uma linha, com 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 somente leitura durante toda a vida do lugar. fits e start recebem essa mesma tabela. O padrão cita cada campo. Estes são os grupos.
| Onde | O que faz |
|---|---|
game | O conjunto de conteúdo, as expansões ativas, a versão do motor, o modo, a regra de início com que a partida posicionou os jogadores (fixed ou random), a dificuldade, se a partida é aberta, o mod (ID, nome, versão, lados, ajustes e limites), os mutadores na ordem de aplicação, o mapa e o perfil da IA. |
game.map | O nome do mapa, seus tamanhos, nível do mar, maré e vento, metal e alturas, os pontos de metal e geotérmicos, as posições iniciais na ordem de StartPos, as classes de movimento, a transitabilidade, se duas posições podem se encontrar pelas células livres de uma classe, e uma estimativa da rota que uma unidade de uma classe faria entre duas posições: seu comprimento, quantas células ela cruza e, quando pedido, seus pontos de virada. Nada disso leva em conta o que o lugar viu. Saber se duas posições se encontram não busca um caminho, e a estimativa de rota busca. |
| Definições | units, weapons e features: os registros carregados da partida, depois do mod e dos mutadores. São somente 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 o quadro do radar. Cada jogador vem com seu ponto de início quando a regra de início o torna conhecido (todos os jogadores com inícios fixos, só o do próprio lugar com inícios aleatórios) e com os jogadores que ele considera aliados. Um contato de radar dá uma posição enquanto ainda é um contato. Ele não tem tipo nem dono. |
| Um handle de unidade | id e slot podem ser lidos em qualquer handle. Posição, vida e tipo podem ser lidos quando o lugar vê aquela unidade viva, ou quando o cérebro tem visão total. Ordens, esquadrão e camuflagem só podem ser lidos numa unidade viva do próprio lugar. health continua nil quando o tipo esconde o dano, a menos que o lugar seja dono da unidade ou o cérebro tenha visão total. |
| Visão total | Sem a flag, game.units, o inimigo mais próximo e a lista bruta de vistos são nil. Com a flag, game.units lista todos os slots ocupados. me:sees e me:sees_unit não tratam a flag como um sim. A flag não concede a sequência aleatória compartilhada nem uma postura direta. |
O que um cérebro pode fazer
Um handle de unidade aceita as ordens que um jogador pode dar. Cada chamada chega à partida na hora. A ordem roda no tick seguinte. Uma recusa retorna false. A primeira verificação que falha é dead, not_own, over_limit (o turno já passou 4.096 ordens à partida), not_seen ou not_buildable. Uma recusa da partida conta como by_engine.
| Chamada | O que faz |
|---|---|
unit:move | Um movimento por terra. |
unit:patrol | Uma patrulha. |
unit:attack | Um ataque a uma unidade. |
unit:attack_ground | Um ataque ao terreno aberto. |
unit:attack_point | Um ataque a um ponto, fora da fila. |
unit:dgun | O ataque especial, numa posição ou numa unidade. |
unit:guard | Proteger uma unidade. |
unit:repair | Reparar uma unidade. |
unit:help | Ajudar numa 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 feita por um construtor móvel. facing vai de 0 a 3. |
unit:queue | Uma construção de fábrica. count vai de 1 a 1000, e o padrão é 1. |
unit:requeue | Muda a quantidade na fila. delta é um 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 daquela unidade. |
unit:stance | move é hold, manoeuvre ou roam. fire é hold, return ou at_will. direct como true escreve os bits permanentes e não dá nenhuma ordem, e só o Classic pode fazer isso. |
me:assign | Põe uma das unidades do próprio lugar num esquadrão. Uma unidade que o lugar não possui, ou um handle morto, retorna false. |
brain.say | Entrega uma linha de chat à partida. O painel de chat explica como. |
brain.site | Testa uma célula de construção. facing vai de 0 a 3. check é site ou yard. |
brain.place | Percorre anéis a partir de um ponto e retorna a primeira célula em que a verificação de local passa, e o centro dessa célula, ou nil. |
map:reachable | Se duas posições podem se encontrar por uma classe de movimento. Não busca um caminho e não muda o mapa de movimento. |
map:route | Estima a rota que uma unidade de uma classe de movimento faria, sem mudar nada na partida. A chamada cobra uma instrução por célula expandida, mais 16. Ela para depois de 1.048.576 células expandidas e retorna nil. Pedir os pontos de virada faz a busca e a cobrança uma segunda vez. Um cérebro que só precisa saber se dois lugares se encontram deve perguntar primeiro a map:reachable. |
Números
fixed conta em unidades de 1/65.536. Os eixos são x para leste, z para sul e y para cima. Um ângulo é um inteiro, 65.536 para uma volta completa. f32 guarda 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 direção, as distâncias e o seno e o cosseno escalados do próprio motor.
Sorteios aleatórios
brain.random(n) retorna um inteiro de 1 a n, da sequência própria do cérebro. A semente dessa sequência vem da partida, do jogador e do ID e da versão do cérebro. Sorteios nela não avançam a sequência compartilhada da partida. Substituir math.random não muda brain.random.
brain.shared_random sorteia da sequência compartilhada da partida, e brain.setup_random sorteia de uma cópia tirada na preparação integrada do lugar. O padrão os instala para o Classic e diz onde uma execução em modo sombra os tem. Nenhum outro cérebro tem qualquer um dos dois. brain.float_heap_order ordena distâncias inteiras, da mais próxima para a mais distante, e é instalado para todos os lugares.
Timers
brain.after(ticks, fn) roda fn uma vez, depois de tantos turnos. brain.every(ticks, fn) roda nesse momento e de novo a cada ticks turnos depois do tick devido que acabou de cumprir. ticks é um inteiro de 1 a 2147483647. Cada chamada retorna um ID. Os IDs começam em 1 e só crescem. brain.cancel(id) retorna true quando removeu um timer em espera. Um cérebro pode manter 1024 timers em espera. Os timers vencidos rodam depois do chat e antes de onTick. Um erro dentro de um timer para o lugar do mesmo jeito que um erro de hook.
Chat
onChat roda uma vez por linha desde o último turno. Um onChat ausente é ignorado, e a linha é consumida mesmo assim. brain.say(text, to) entrega uma linha à partida na hora. to é all (o padrão), allies, enemies, ou um índice de jogador de 0 a 9. A chamada retorna true quando a linha entrou na fila. Ela não controla o próprio ritmo. Quem controla é a partida, 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.