Classic,一个完整实例
用 Lua 写成的内置电脑玩家:它的文件、任务表、一个任务、quirk 注记、留在引擎里的部分,以及同样的命令如何被证明。
本章概述 docs/brains/oabrain-standard.md、docs/brains/authoring.md 和 docs/playing-with-brains.md。那些文档是规范性文本。
Classic 是什么
Classic 就是 brains/classic。它是内置电脑玩家的文档:同样的命令,写成你可以读的样子。内置电脑玩家仍留在引擎里,并且仍是默认。选用 Classic 会让这个包入座。席位留在内置电脑玩家上时,运行的是引擎自己的电脑玩家。
清单写明 works-with 是星号,并且 fullAwareness 为 true。它的描述说,它的打法与内置电脑玩家完全一样。全图视野是清单标志。它允许用于网络游戏,席位会标出它。标识、名字和安装路径不会使一个包成为 Classic。文件必须是这一构建的 brains/classic 文件夹,路径相同,字节相同。
清单 ·oabrain.yaml# SPDX-FileCopyrightText: The Open Annihilation Authors; see COPYRIGHT
# SPDX-License-Identifier: GPL-3.0-only
oabrain: 1
id: classic
name: Classic
description: "The engine's own computer player, written in Lua: it plays exactly as the built-in computer player does."
version: "1.0"
author:
name: The Open Annihilation Authors
tags: [classic, teaching]
works-with: ["*"]
requires:
engine: ">= 0.8.6"
levels:
- id: easy
name: Easy
difficulty: easy
- id: medium
name: Medium
difficulty: medium
- id: hard
name: Hard
difficulty: hard
fullAwareness: true
entry: brain.lua
文件
brain.lua 为 0 到 9 的每个小队建立一个任务。在第一个 tick,onTick 刷新建造列表并为网格播种。之后每个 tick 按倒计时运行小队排序,并运行每一个轮到的任务。
| 路径 | 作用 |
|---|---|
oabrain.yaml | 清单。 |
brain.lua | 席位、任务表和 onTick。 |
lua/build.lua | 给一个类型打分,并从建造者的列表里挑一个。 |
lua/knowledge.lua | 权重、上限、建造网格和金属点。从 me.knowledge 读取引擎的记录。 |
lua/place.lua | 搜索选址,不断加宽,直到搜索和地图一样宽。 |
lua/profile.lua | 读取配置文本并应用权重。 |
lua/report.lua | 写出 report 返回的文本。 |
lua/rules.lua | 读取 Classic 所依据的 ai.* 规则。 |
lua/squads.lua | 把单位分进小队,并给小队一条命令。 |
lua/types.lua | 读取电脑玩家所用的类型字段。 |
lua/tasks/ | structures、construction、strike、rally、air_raid 和 siege。 |
README.md | 一份说明。游戏不加载它。 |
LICENSE | 许可证。 |
任务表
小队 1 和 5 建造建筑。小队 5 只在规则这样说时才做,否则空闲。小队 2 和 6 是突击组。小队 3 和 7 向它们集结。小队 4 是工程。小队 8 是飞行中队。小队 9 是攻城。小队 9 的注释是一处 quirk:排序从不填满那个小队,所以攻城任务只保持自己的时间表,除非已经有单位在里面。
-- …
for index = 0, 9 do
seat.tasks[index] = blank(index)
end
local wave = loaded_rules.attack_wave
seat.tasks[1].kind = "structures"
seat.tasks[1].every = 30
seat.tasks[2].kind = "strike"
seat.tasks[2].min_size = 3
seat.tasks[2].launch = wave
seat.tasks[2].merge_radius = 20000
seat.tasks[2].source = 3
seat.tasks[2].every = 300
seat.tasks[3].kind = "rally"
seat.tasks[3].source = 2
seat.tasks[3].every = 150
seat.tasks[4].kind = "construction"
seat.tasks[4].every = 90
if loaded_rules.squad5_factory_tick then
seat.tasks[5].kind = "structures"
seat.tasks[5].every = 30
else
seat.tasks[5].kind = "idle"
end
seat.tasks[6].kind = "strike"
seat.tasks[6].min_size = 3
seat.tasks[6].launch = wave
seat.tasks[6].merge_radius = 50000
seat.tasks[6].source = 7
seat.tasks[6].every = 300
seat.tasks[7].kind = "rally"
seat.tasks[7].source = 6
seat.tasks[7].every = 150
seat.tasks[8].kind = "air_raid"
seat.tasks[8].every = function()
return 30 + brain.shared_random(900)
end
seat.tasks[8].run = function()
air_raid.run(seat, seat.tasks[8])
end
-- @quirk The sort never fills squad 9, so the siege task only keeps its
-- timetable unless a unit was already in that squad.
local centre = map_centre(game)
seat.tasks[9].kind = "siege"
seat.tasks[9].every = function()
return 30 + brain.shared_random(150)
end
seat.tasks[9].siege_target = point_of(centre)
seat.tasks[9].siege_probe = point_of(centre)
seat.tasks[9].siege_step = point_of(centre)
seat.tasks[9].siege_weight = 0
-- …
在第一个 tick,onTick 刷新建造列表并为网格播种。排序本身就是那个倒计时。它从 30 开始。每个 tick 减一,到底之后回到 30 并调用小队排序。
-- …
function hooks.onTick(tick)
local seat = hooks.seat
if seat.builds_ready ~= 1 then
types.refresh_builds(seat.types, seat.game)
seat.builds_ready = 1
end
if seat.grids_ready ~= 1 then
knowledge.seed_grids(seat)
end
seat.sort_countdown = seat.sort_countdown - 1
if seat.sort_countdown < 1 then
seat.sort_countdown = 30
brain.section("sort")
squads.sort(seat)
end
for index = 0, 9 do
local task = seat.tasks[index]
if task.kind ~= "none" and task.next <= tick then
if task.kind ~= "idle" then
brain.section("task" .. index)
local interval = task.every
if type(interval) == "function" then
interval = interval()
end
task.next = (tick + interval) & 0xFFFFFFFF
task.run()
end
end
end
end
-- …
squads.lua 说明排序做些什么:席位范围内的可选择单位接受常备命令,仍在小队 0 里的单位加入其类型所分到的小队。已经在某个小队里的单位保持那个小队。
一个任务
lua/tasks/rally.lua 就是集结任务的全部。当集结小队和突击组都有成员时,集结小队移动到突击组的中心。
-- SPDX-FileCopyrightText: The Open Annihilation Authors; see COPYRIGHT
-- SPDX-License-Identifier: GPL-3.0-only
-- A waiting squad. When it and the strike group both have members, it moves
-- to the strike group's centre. The timetable is already set.
local squads = require("squads")
local rally = {}
function rally.run(seat, task)
local me = seat.me
if squads.count(me, task.squad) < 1 or squads.count(me, task.source) < 1 then
return
end
local center = squads.centroid(me, task.source)
if center == nil then
return
end
squads.order(me, task.squad, "move", false, nil, squads.fixed_point(center))
end
return rally
quirk 注记
标了 @quirk 的一行,记录内置电脑玩家的一种行为。注记写在脚本里,让读的人在代码旁边看到它。小队 9 的注记在上面的任务表里。另外两条在 profile.lua 里。单词 any 只作为 plan 之后的第一个词才算数,而同一个词会在这一行后来的每一个词上再测一次。可下载类型的 ai_weight 对每个电脑玩家应用一次,因此权重按那个数量累加。
-- The builders' tick. Each builder places a weighted pick near the base,
-- and an idle one patrols home. A capturing builder builds only while the
-- player is under the withhold count and the hold tick has passed.
-- @quirk ai.builder-withhold-threshold moves only the build limit: an idle
-- capturing builder still circles the base from five build-capable units.
工程会压住建造,并且仍然巡逻。没有基地的小型飞行中队,在 ai.patrol-null-enemy-skip 下,抽取一条地图边缘的巡逻,三次抽取与大型中队相同,然后在那里巡逻,而不是把一次巡逻排到最近的敌人身上。
留在引擎里的部分
内置电脑玩家仍然是默认。只有文件与这一构建的文件夹一致时,席位才是 Classic。那项测试授予两种行为,以及第三项:从准备流做一次抽取。brain.shared_random 抽取对局的共享流,也就是单位脚本和模拟所抽取的那一条,因此 Classic 的飞机间隔和攻城间隔与内置电脑玩家保持同步。direct 设为 true 的 unit:stance 写入站立的移动位和开火位,并且不给出命令。任何其他席位都会被拒绝。brain.setup_random 从席位做内置准备时取下的那条流的副本中抽取,并且只推进副本。
任何其他大脑都没有这两个随机函数,直接写入姿态也会被拒绝。brain.section 为每一个大脑安装。在游戏中这个标记什么也不做。全图视野不在这份列表里。它是清单标志。Steady Commander 这些一个也不调用。
如何证明
在影子运行中,Classic 在内置电脑玩家旁边入座。对局打出的是内置电脑玩家的命令。Classic 的命令被记录并比较。任何一个 tick 都不得不同。引擎对那次比较的测试是 sim-brains-shadow 和 sim-brains-shadow-match。
换入之后,Classic 入座在内置电脑玩家原本要打的位置。引擎对此的测试是 sim-brains-classic、sim-brains-classic-data、sim-brains-classic-match、ai-computer-player、match-computer-build、native-trace、native-stage、native-director-render、native-classic-trace、native-classic-stage、native-classic-director-render、native-net-loopback-computer、native-net-loopback-computer-watcher、native-net-loopback-each-ai、native-net-loopback-computer-classic、native-net-loopback-computer-watcher-classic 和 native-net-loopback-each-ai-classic。
一次浸泡打 1,000 场带种子的无头遭遇战,每场 20,000 个 tick,覆盖基础游戏和已发布的模组。相同的种子打出相同的对局。这次浸泡是一项长时间检查:运行 Classic 的席位在整场游戏里都停留在内置电脑玩家的命令上。
如何阅读
你可以把 Classic 读成写下来的内置电脑玩家行为。引擎自己的电脑玩家仍是无人选用大脑时席位所用的玩家。安装 Classic,或打包它,在席位被设成它之前什么也不会改变。