Lua мозку
Маніфест і скрипт Steady Commander, хуки, що мозок може знати й робити, числа, випадкові значення, таймери та чат.
Цей розділ стисло викладає docs/brains/oabrain-standard.md, docs/brains/authoring.md і docs/playing-with-brains.md. Нормативний текст — саме ці документи.
Steady Commander
Steady Commander — вигаданий мозок у brains/examples/steady-commander. Два файли нижче скопійовано так, як їх написано. works-with — зірочка в лапках, тож пакет підходить для будь-якого моду й базової гри. Скрипт повертає таблицю хуків. Назви цю таблицю як завгодно, тільки не brain: файл, що починається з local brain, затіняє глобальну змінну, яку встановлює профіль. fits повертає true. save повертає, скільки загонів уже відправлено. Вибір attack — soon, steady або late.
Маніфест ·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
Вхідний скрипт · 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
Хуки
Вхідний скрипт повертає одну таблицю. Відсутня функція пропускається. fits і start виконуються перед першим тіком. Далі один хід доставляє в такому порядку: події юнітів із черги, втрати, а потім появи радарних і сонарних контактів, зміни союзів, поразки, чат, таймери й onTick. Перший хід запамʼятовує союзи й те, хто бере участь, і не повідомляє про жодну з цих змін. За один хід зберігається щонайбільше 65 536 подій. Понад це подія відкидається.
| Хук | Коли | Аргументи |
|---|---|---|
fits | Перед першим тіком. Повертає true або рядок із причиною. Будь-що інше скасовує посадку на місце. | game |
start | Перед першим тіком. saved — те, що зберегла завантажена гра, або nil. | game, me, saved |
onUnitCreated | Юніт створено. Власний юніт місця. builder — nil, коли будівельника немає. | unit, builder |
onUnitFinished | Юніт добудовано. Власний юніт місця. builder — nil, коли будівельника немає або коли це сам юніт. Подія завершення відкидається, коли за нею йде скасована загибель того самого юніта. | unit, builder |
onUnitDamaged | Власний юніт місця отримує пошкодження. kind — weapon, paralyze, heal або environment. attacker передається лише тоді, коли місце може знати про цей юніт, інакше — nil. | unit, attacker, amount, kind |
onUnitDestroyed | Юніт гине: власний юніт місця, юніт союзника, ворог зі списку побачених або будь-яка загибель, коли мозок має повну видимість. Загибель типу captured не потрапляє в цей хук. wreck — назва й позиція або nil. | unit, killer, wreck |
onUnitGiven | Юніт передано. Старий і новий власник. | unit, old, from, to |
onUnitIdle | Власний юніт місця стає бездіяльним. | unit |
onCommandDone | Наказ для власного юніта місця завершено. result — done або failed. | unit, order, result |
onUnitLoaded | Юніт завантажено в транспорт. Власник юніта й власник транспорту. | unit, carrier |
onUnitUnloaded | Юніт вивантажено. Власник юніта й власник транспорту. | unit, carrier |
onUnitSeen | Місце помічає юніт. | unit |
onUnitLost | Місце втрачає юніт із поля зору. | unit |
onPathFailed | Юніт не може знайти шлях. Власник юніта. | unit |
onRadarLost | Контакт зникає з картини. Для того самого слота втрата йде перед появою. Союзний юніт ніколи не буває контактом. | contact |
onRadarContact | Контакт зʼявляється на картині. Контакт не має типу, власника й дескриптора юніта. | contact |
onAllianceChanged | Змінюється біт союзу. Інші гравці, в порядку індексів. | player, allied |
onPlayerDefeated | Гравець перестає брати участь. | player |
onChat | Рядок чату дійшов до місця. from — індекс відправника. text — без імені на початку. to — all, allies, enemies або me. Чекати може щонайбільше 32 рядки. | from, text, to |
onTick | Кінець ходу. Аргумент — тік матчу. | tick |
save | Коли цього просить збереження. Повертає прості дані або нічого. Помилка повертає місце вбудованому гравцеві. Значення, що не є простими даними, не потрапляє до збереження. | немає |
report | Коли просить консоль. Повертає текст, щонайбільше 64 KiB. Мозок без report пише один рядок: назву з маніфесту, пробіл і версію — і не дає звіту. | немає |
Що мозок може знати
game — одна таблиця лише для читання на весь час існування місця. fits і start отримують ту саму таблицю. Стандарт називає кожне поле. Ось групи.
| Де | Що робить |
|---|---|
game | Комплект вмісту, увімкнені доповнення, версія рушія, режим, правило старту, за яким матч розставив гравців (fixed або random), складність, чи відкритий матч, мод (його id, назва, версія, сторони, хаки й ліміти), мутатори в порядку застосування, мапа та профіль ШІ. |
game.map | Назва мапи, її розміри, рівень моря, приплив і вітер, метал і висоти, родовища металу й геотермальні джерела, стартові позиції в порядку StartPos, класи руху, прохідність, чи можуть дві позиції зʼєднатися через відкриті клітинки класу, і оцінка маршруту, яким пройшов би юніт класу між двома позиціями: його довжина, скільки клітинок він перетинає і, на запит, його точки повороту. Це не залежить від того, що бачило місце. Перевірка, чи можуть дві позиції зʼєднатися, не шукає шлях, а оцінка маршруту — шукає. |
| Визначення | units, weapons і features: завантажені записи матчу після моду й мутаторів. Вони доступні лише для читання. |
me | Індекс гравця, id рівня, складність, остаточні параметри, ресурси, союзники, кожен гравець у грі та їхня кількість, власні юніти й загони місця, поле зору й радарна картина. Для кожного гравця вказано його старт там, де правило старту робить його відомим (усі гравці з фіксованими стартами, лише власний старт місця з випадковими), і гравців, яких він вважає союзниками. Радарний контакт дає позицію, поки лишається контактом. Він не має типу й власника. |
| Дескриптор юніта | id і slot читаються на будь-якому дескрипторі. Позиція, здоровʼя і тип читаються, коли місце бачить цей живий юніт або коли мозок має повну видимість. Накази, загін і маскування читаються лише на власному живому юніті місця. health лишається nil, коли тип приховує пошкодження, якщо тільки юніт не належить місцю або мозок не має повної видимості. |
| Повна видимість | Без прапорця game.units, найближчий ворог і сирий список побачених — nil. З прапорцем game.units перелічує кожен зайнятий слот. me:sees і me:sees_unit не вважають прапорець відповіддю «так». Прапорець не дає доступу до спільного випадкового потоку чи прямого запису режиму. |
Що мозок може робити
Дескриптор юніта приймає ті накази, які може віддати гравець. Кожен виклик одразу доходить до матчу. Наказ виконується в наступному тіку. Відмова повертає false. Перша невдала перевірка — dead, not_own, over_limit (за цей хід матчу вже передано 4096 наказів), not_seen або not_buildable. Відмова матчу зараховується як by_engine.
| Виклик | Що робить |
|---|---|
unit:move | Рух по землі. |
unit:patrol | Патрулювання. |
unit:attack | Атака на юніт. |
unit:attack_ground | Атака на відкриту місцевість. |
unit:attack_point | Атака на точку, без черги. |
unit:dgun | Спеціальна атака на позицію чи юніт. |
unit:guard | Охороняти юніт. |
unit:repair | Ремонтувати юніт. |
unit:help | Допомогти з будівництвом. |
unit:reclaim | Утилізувати юніт або обʼєкт у позиції. |
unit:capture | Захопити юніт. |
unit:load | Завантажити юніт. |
unit:unload | Вивантажити в позиції. |
unit:build | Будівництво мобільним юнітом. facing — від 0 до 3. |
unit:queue | Будівництво на заводі. count — від 1 до 1000, за замовчуванням 1. |
unit:requeue | Змінює кількість у черзі. delta — ненульове ціле число від -1000 до 1000. |
unit:stop | Стоп. |
unit:on | Встановлює біт активності. |
unit:cloak | Маскування. |
unit:self_destruct | Перемикає самознищення для цього одного юніта. |
unit:stance | move — hold, manoeuvre або roam. fire — hold, return або at_will. direct, установлений у true, записує біти постійних наказів і не віддає наказу; так може лише Classic. |
me:assign | Додає один із власних юнітів місця до загону. Юніт, що не належить місцю, або мертвий дескриптор повертає false. |
brain.say | Передає матчу один рядок чату. Як саме — див. панель «Чат». |
brain.site | Перевіряє клітинку для будівлі. facing — від 0 до 3. check — site або yard. |
brain.place | Обходить кільця від точки й повертає першу клітинку, де виконується перевірка ділянки, і центр цієї клітинки, або nil. |
map:reachable | Чи можуть дві позиції зʼєднатися для класу руху. Не шукає шлях і не змінює карту руху. |
map:route | Оцінює маршрут, яким пройшов би юніт класу руху, і нічого не змінює в матчі. Виклик списує одну інструкцію на кожну розгорнуту клітинку плюс 16. Він зупиняється після 1 048 576 розгорнутих клітинок і повертає nil. Запит точок повороту шукає й списує вдруге. Мозку, якому треба лише знати, чи зʼєднуються два місця на мапі, варто спершу викликати map:reachable. |
Числа
fixed рахує в 1/65 536. Осі: x — схід, z — південь, y — угору. Кут — ціле число, 65 536 на повний оберт. f32 зберігає одне значення binary32 — розрядність ресурсів гравця. geometry.direction, geometry.planar_length, geometry.length, geometry.scale, geometry.sine_scaled і geometry.cosine_scaled — це власні напрямок, відстані й масштабовані синус і косинус рушія.
Випадкові значення
brain.random(n) повертає ціле число від 1 до n із власного потоку мозку. Цей потік ініціалізується від матчу, гравця, id і версії мозку. Вибірки з нього не просувають спільний потік матчу. Заміна math.random не змінює brain.random.
brain.shared_random бере значення зі спільного потоку матчу, а brain.setup_random — з копії, знятої під час вбудованого налаштування місця. Стандарт встановлює їх для Classic і вказує, де їх має тіньовий запуск. Жоден інший мозок не має жодної з них. brain.float_heap_order сортує цілочисельні відстані, від найближчої, і встановлюється для кожного місця.
Таймери
brain.after(ticks, fn) виконує fn один раз через стільки ходів. brain.every(ticks, fn) виконує її тоді й знову кожні ticks ходів після щойно досягнутого тіку. ticks — ціле число від 1 до 2147483647. Кожен виклик повертає id. id починаються з 1 і лише зростають. brain.cancel(id) повертає true, коли видалив таймер, що очікував. Мозок може тримати в очікуванні 1024 таймери. Таймери, час яких настав, виконуються після чату й перед onTick. Помилка в таймері зупиняє місце так само, як помилка в хуку.
Чат
onChat виконується один раз на кожен рядок від останнього ходу. Відсутній onChat пропускається, але рядок однаково вважається обробленим. brain.say(text, to) одразу передає матчу один рядок. to — all (за замовчуванням), allies, enemies або індекс гравця від 0 до 9. Виклик повертає true, коли рядок поставлено в чергу. Сам він темп не обмежує. Це робить матч: 30 тіків між рядками, які каже місце компʼютера. Відмова — not_a_seat, empty, command, too_soon, no_target або no_chat.