Le Lua du cerveau
Le manifeste et le script de Steady Commander, les hooks, ce qu'un cerveau peut savoir et faire, les nombres, les tirages aléatoires, les minuteurs et le chat.
Ce chapitre résume docs/brains/oabrain-standard.md, docs/brains/authoring.md et docs/playing-with-brains.md. Ces documents sont le texte normatif.
Steady Commander
Steady Commander est le cerveau fictif de brains/examples/steady-commander. Les deux fichiers ci-dessous sont copiés tels qu'ils sont écrits. works-with est une étoile entre guillemets, donc le paquet convient à n'importe quel mod et au jeu de base. Le script renvoie une table de hooks. Nommez cette table autrement que brain : un fichier qui commence par local brain masque la globale que le profil installe. fits renvoie true. save renvoie combien d'escouades ont été envoyées. Le choix d'attaque est soon, steady ou late.
Manifeste ·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 d'entrée · 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
L'entrée renvoie une seule table. Une fonction absente est sautée. fits et start tournent avant le premier tick. Un tour livre ensuite, dans cet ordre : événements d'unités en file, radar et sonar perdus puis gagnés, changements d'alliance, défaites, chat, minuteurs, et onTick. Le premier tour mémorise les alliances et qui participe, et ne signale aucun de ces changements. Au plus 65 536 événements sont gardés pour un tour. Au-delà, l'événement est écarté.
| Hook | Quand | Arguments |
|---|---|---|
fits | Avant le premier tick. Renvoie true, ou une raison en chaîne. Tout le reste arrête la mise en siège. | game |
start | Avant le premier tick. saved est ce qu'une partie chargée a gardé, ou nil. | game, me, saved |
onUnitCreated | Une unité est créée. L'unité propre du siège. builder vaut nil quand il n'y en a pas. | unit, builder |
onUnitFinished | Une unité est achevée. L'unité propre du siège. builder vaut nil quand il n'y en a pas ou que c'est l'unité. Un événement de fin est écarté quand une mort annulée de la même unité le suit. | unit, builder |
onUnitDamaged | L'unité propre du siège subit des dégâts. kind vaut weapon, paralyze, heal ou environment. attacker n'est passé que quand le siège peut connaître cette unité, et vaut nil sinon. | unit, attacker, amount, kind |
onUnitDestroyed | Une unité meurt : celle du siège, celle d'un allié, un ennemi de la liste des vus, ou toute mort quand le cerveau a la vue complète. Une mort de kind captured n'est pas ce hook. wreck est un nom et une position, ou nil. | unit, killer, wreck |
onUnitGiven | Une unité est donnée. L'ancien propriétaire et le nouveau. | unit, old, from, to |
onUnitIdle | L'unité propre du siège devient inactive. | unit |
onCommandDone | Un ordre sur l'unité propre du siège se termine. result vaut done ou failed. | unit, order, result |
onUnitLoaded | Une unité est chargée. Le propriétaire de l'unité et le propriétaire du transporteur. | unit, carrier |
onUnitUnloaded | Une unité est déchargée. Le propriétaire de l'unité et le propriétaire du transporteur. | unit, carrier |
onUnitSeen | Le siège gagne la vue d'une unité. | unit |
onUnitLost | Le siège perd la vue d'une unité. | unit |
onPathFailed | Une unité ne trouve pas de chemin. Le propriétaire de l'unité. | unit |
onRadarLost | Un contact quitte l'image. Perdu avant gagné pour le même emplacement. Une unité alliée n'est jamais un contact. | contact |
onRadarContact | Un contact entre dans l'image. Un contact n'a ni type, ni propriétaire, ni handle d'unité. | contact |
onAllianceChanged | Un bit d'alliance change. Les autres joueurs, dans l'ordre des index. | player, allied |
onPlayerDefeated | Un joueur cesse de participer. | player |
onChat | Une ligne de chat est parvenue au siège. from est l'index de l'expéditeur. text n'a pas de nom devant. to vaut all, allies, enemies ou me. Au plus 32 lignes attendent. | from, text, to |
onTick | La fin du tour. L'argument est le tick de la partie. | tick |
save | Quand une sauvegarde le demande. Renvoie des données simples, ou rien. Une erreur remet le siège. Une valeur qui n'est pas des données simples est laissée hors de la sauvegarde. | aucun |
report | Quand la console le demande. Renvoie du texte, au plus 64 Kio. Un cerveau sans report écrit une ligne : le nom du manifeste, un espace, la version, et ne donne pas de rapport. | aucun |
Ce qu'un cerveau peut savoir
game est une seule table en lecture seule pour la vie du siège. fits et start reçoivent cette même table. Le standard nomme chaque champ. Voici les groupes.
| Où | Rôle |
|---|---|
game | Le bundle de contenu, les extensions qui sont actives, la version du moteur, le mode, la difficulté, si une partie est ouverte, le mod (id, name, version, sides, hacks et limits), les mutateurs dans l'ordre d'application, la carte, et le profil IA. |
game.map | Le nom de la carte, ses tailles, le niveau de la mer, la marée et le vent, le métal et les hauteurs, les classes de mouvement, la praticabilité, et si deux positions peuvent se rejoindre par les cases ouvertes d'une classe. Cela ne regarde pas ce que le siège a vu, et cela ne cherche pas de chemin. |
| Définitions | units, weapons et features : les fiches chargées de la partie, après le mod et les mutateurs. Elles sont en lecture seule. |
me | L'index du joueur, l'identifiant du niveau, la difficulté, les options résolues, les ressources, les alliés, les unités et les escouades propres du siège, la vue, et l'image radar. Un contact donne une position tant qu'il est encore un contact. Il n'a ni type ni propriétaire. |
| Un handle d'unité | id et slot se lisent sur tout handle. La position, health et le type se lisent quand le siège peut voir cette unité vivante, ou quand le cerveau a la vue complète. Les ordres, l'escouade et le cloak ne se lisent que sur l'unité vivante propre du siège. health reste nil quand le type cache les dégâts, sauf si le siège possède l'unité ou si le cerveau a la vue complète. |
| Vue complète | Sans le drapeau, game.units, l'ennemi le plus proche et la liste vue brute valent nil. Avec le drapeau, game.units liste chaque emplacement occupé. me:sees et me:sees_unit ne traitent pas le drapeau comme un oui. Le drapeau n'accorde ni le flux aléatoire partagé ni une stance directe. |
Ce qu'un cerveau peut faire
Un handle d'unité prend les ordres qu'un joueur peut donner. Chaque appel atteint la partie aussitôt. L'ordre s'exécute au tick suivant. Un refus renvoie false. Le premier contrôle qui échoue est dead, not_own, over_limit (le tour a déjà passé 4 096 ordres à la partie), not_seen, ou not_buildable. Un refus de la partie compte comme by_engine.
| Appel | Rôle |
|---|---|
unit:move | Un déplacement au sol. |
unit:patrol | Une patrouille. |
unit:attack | Une attaque sur une unité. |
unit:attack_ground | Une attaque sur le terrain libre. |
unit:attack_point | Une attaque sur un point, hors file. |
unit:dgun | L'attaque spéciale, sur une position ou une unité. |
unit:guard | Garder une unité. |
unit:repair | Réparer une unité. |
unit:help | Aider une construction. |
unit:reclaim | Recycler une unité, ou un élément à une position. |
unit:capture | Capturer une unité. |
unit:load | Charger une unité. |
unit:unload | Décharger à une position. |
unit:build | Une construction mobile. facing va de 0 à 3. |
unit:queue | Une construction d'usine. count va de 1 à 1 000, et la valeur par défaut est 1. |
unit:requeue | Change le nombre en file. delta est un entier non nul de -1 000 à 1 000. |
unit:stop | Arrêt. |
unit:on | Règle le bit actif. |
unit:cloak | Occultation. |
unit:self_destruct | Bascule l'autodestruction pour cette seule unité. |
unit:stance | move vaut hold, manoeuvre ou roam. fire vaut hold, return ou at_will. direct mis à true écrit les bits de posture et ne donne pas d'ordre, et seul Classic peut le faire. |
me:assign | Met une unité propre du siège dans une escouade. Une unité que le siège ne possède pas, ou un handle mort, renvoie false. |
brain.say | Remet une ligne de chat à la partie. Le panneau de chat dit comment. |
brain.site | Teste une case de construction. facing va de 0 à 3. check vaut site ou yard. |
brain.place | Parcourt des anneaux depuis un point et renvoie la première case où la vérification de site réussit, et le centre de cette case, ou nil. |
map:reachable | Si deux positions peuvent se rejoindre par une classe de mouvement. Cela ne cherche pas de chemin et ne change pas la carte de mouvement. |
Nombres
fixed compte des 65 536es. Les axes sont x vers l'est, z vers le sud et y vers le haut. Un angle est un entier, 65 536 pour un tour complet. f32 tient une valeur binary32, la largeur des ressources d'un joueur. geometry.direction, geometry.planar_length, geometry.length, geometry.scale, geometry.sine_scaled et geometry.cosine_scaled sont le cap du moteur lui-même, ses distances, et son sinus et son cosinus mis à l'échelle.
Tirages aléatoires
brain.random(n) renvoie un entier de 1 à n, issu du flux propre du cerveau. Ce flux est ensemencé à partir de la partie, du joueur et de l'identifiant et de la version du cerveau. Les tirages qui en viennent n'avancent pas le flux partagé de la partie. Remplacer math.random ne change pas brain.random.
brain.shared_random tire du flux partagé de la partie, et brain.setup_random tire d'une copie prise à la mise en place intégrée du siège. Le standard les installe pour Classic, et dit où une exécution shadow les a. Tout autre cerveau n'a ni l'un ni l'autre. brain.float_heap_order trie des distances entières, la plus proche d'abord, et il est installé pour chaque siège.
Minuteurs
brain.after(ticks, fn) exécute fn une fois, ce nombre de tours plus tard. brain.every(ticks, fn) l'exécute alors, puis de nouveau tous les ticks tours après le tick d'échéance qu'il vient de rencontrer. ticks est un entier de 1 à 2 147 483 647. Chaque appel renvoie un identifiant. Les identifiants commencent à 1 et ne font que croître. brain.cancel(id) renvoie true quand il a retiré un minuteur en attente. Un cerveau peut garder 1 024 minuteurs en attente. Les minuteurs échus tournent après le chat et avant onTick. Une erreur dans un minuteur arrête le siège comme le fait une erreur de hook.
Chat
onChat tourne une fois par ligne depuis le dernier tour. Un onChat absent est sauté, et la ligne est tout de même consommée. brain.say(text, to) remet aussitôt une ligne à la partie. to vaut all (la valeur par défaut), allies, enemies, ou un index de joueur de 0 à 9. L'appel renvoie true quand la ligne a été mise en file. Il ne se cadence pas de lui-même. La partie le fait, à 30 ticks entre les lignes qu'un siège ordinateur dit. Un refus est not_a_seat, empty, command, too_soon, no_target ou no_chat.