大脑的 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 返回已经派出多少个小队。进攻选择是 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 在第一个 tick 之前运行。随后一个回合按这个顺序交付:排好队的单位事件,雷达和声纳先失去再获得,同盟变化,战败,聊天,计时器,然后 onTick。第一个回合存下同盟以及谁在参与,并且不报告这些变化。一个回合最多保留 65,536 个事件。超出的事件会被丢掉。
| 钩子 | 何时 | 参数 |
|---|---|---|
fits | 第一个 tick 之前。返回 true,或一个字符串理由。其他任何东西都会停止入座。 | game |
start | 第一个 tick 之前。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 | 一个单位死亡:席位自己的、盟友的、已见列表上的敌人,或大脑有全图视野时的任何死亡。kind 为 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。 | tick |
save | 存档来询问时。返回纯数据,或什么都不返回。出错会把席位交还。不是纯数据的值不会写入存档。 | 无 |
report | 控制台来询问时。返回文本,至多 64 KiB。没有 report 的大脑写出一行:清单名字、一个空格、版本,并且不给出报告。 | 无 |
大脑可以知道什么
game 是一张只读表,在席位的整个存续期间都是这一张。fits 和 start 收到同一张表。标准点名每一个字段。下面是这些分组。
| 哪里 | 作用 |
|---|---|
game | 内容包、已开启的扩展、引擎版本、模式、难度、对局是否开放、模组(它的 id、name、version、sides、hacks 和 limits)、按生效顺序的变异器、地图,以及 AI 配置。 |
game.map | 地图的名字、它的尺寸、海平面、潮汐和风、金属和高度、移动类别、可通过性,以及两个位置能否经由某一类别的开放格子相遇。它不查看席位已经看见的东西,也不搜索路径。 |
| 定义 | units、weapons 和 features:对局已加载的记录,在模组和变异器之后。它们是只读的。 |
me | 玩家索引、级别标识、难度、解析后的选项、资源、盟友、席位自己的单位和小队、视野,以及雷达画面。接触在仍是接触时给出一个位置。它没有类型,也没有所有者。 |
| 单位句柄 | 任何句柄都能读 id 和 slot。位置、health 和类型在席位能看见那个存活单位时可以读,或在大脑有全图视野时可以读。命令、小队和 cloak 只在席位自己的存活单位上可以读。类型隐藏伤害时 health 保持 nil,除非席位拥有该单位,或大脑有全图视野。 |
| 全图视野 | 没有这个标志时,game.units、最近的敌人和原始已见列表都是 nil。有这个标志时,game.units 列出每一个被占用的槽位。me:sees 和 me:sees_unit 不把这个标志当作肯定。这个标志不授予共享随机流,也不授予直接姿态。 |
大脑可以做什么
单位句柄接受玩家可以下达的命令。每次调用立刻到达对局。命令在下一个 tick 运行。拒绝时返回 false。第一个失败的检查是 dead、not_own、over_limit(该回合已经向对局传递了 4,096 条命令)、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 | 两个位置能否经由一个移动类别相遇。它不搜索路径,也不改变移动图。 |
数值
fixed 以 65536 分之一计数。轴是 x 向东、z 向南、y 向上。角度是一个整数,65536 为一整圈。f32 保存一个 binary32 值,也就是玩家资源的数值宽度。geometry.direction、geometry.planar_length、geometry.length、geometry.scale、geometry.sine_scaled 和 geometry.cosine_scaled 是引擎自己的朝向、距离,以及缩放后的正弦和余弦。
随机抽取
brain.random(n) 从大脑自己的流中返回 1 到 n 的一个整数。这条流的种子来自对局、玩家以及大脑的标识和版本。从它抽取不会推进对局的共享流。替换 math.random 不会改变 brain.random。
brain.shared_random 抽取对局的共享流,brain.setup_random 抽取席位做内置准备时取下的一份副本。标准为 Classic 安装它们,并说明影子运行在哪里拥有它们。任何其他大脑两者都没有。brain.float_heap_order 按整数距离排序,近的在前,并且为每个席位安装。
计时器
brain.after(ticks, fn) 在那么多个回合之后把 fn 运行一次。brain.every(ticks, fn) 在那时运行,并在它刚遇到的到期 tick 之后,每隔 ticks 个回合再运行。ticks 是从 1 到 2147483647 的整数。每次调用返回一个标识。标识从 1 开始,只增不减。brain.cancel(id) 在移除了一个等待中的计时器时返回 true。一个大脑可以让 1024 个计时器处于等待。到期的计时器在聊天之后、onTick 之前运行。计时器内部的错误会像钩子错误一样停掉席位。
聊天
自上一个回合以来,onChat 对每一行运行一次。缺少 onChat 会被跳过,而那一行仍会被消耗。brain.say(text, to) 立刻把一行交给对局。to 是 all(默认)、allies、enemies,或从 0 到 9 的玩家索引。行已经排队时,调用返回 true。它自己不控制节奏。对局会控制,电脑席位说出的行之间隔 30 个 tick。拒绝是 not_a_seat、empty、command、too_soon、no_target 或 no_chat。