Classic: разбор примера
Встроенный компьютерный игрок на Lua: файлы, таблица задач, одна задача, заметки о причудах, что остаётся в движке и как доказано, что приказы совпадают.
Эта глава кратко пересказывает docs/brains/oabrain-standard.md, docs/brains/authoring.md и docs/playing-with-brains.md. Нормативный текст — в этих документах.
Что такое Classic
Classic — это brains/classic. Это документация встроенного компьютерного игрока: те же приказы, записанные так, чтобы их можно было прочитать. Встроенный игрок остаётся в движке и остаётся игроком по умолчанию. Выбор Classic сажает на место этот пакет. Если оставить место за встроенным игроком, играет собственный компьютерный игрок движка.
В манифесте works-with — звёздочка, а fullAwareness равен true. В его описании сказано, что он играет точно так же, как встроенный компьютерный игрок. Полный обзор — это флаг манифеста. Он разрешён в сетевой игре, и место его показывает. id, название и путь установки не делают пакет 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. На первом тике onTick обновляет списки строительства и заполняет сетки. Затем на каждом тике по обратному отсчёту выполняется сортировка отрядов и каждая задача, чья очередь пришла.
| Путь | Что делает |
|---|---|
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 | Читает правила ai.*, по которым играет Classic. |
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 описывает причуду: сортировка никогда не заполняет этот отряд, поэтому задача осады лишь соблюдает своё расписание, если в отряде уже не было юнита.
-- …
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
-- …
На первом тике onTick обновляет списки строительства и заполняет сетки. Сама сортировка — это обратный отсчёт. Он начинается с 30. Каждый тик вычитает единицу, а на нуле счётчик возвращается к 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 фиксирует поведение, которое есть у встроенного компьютерного игрока. Заметка стоит в скрипте, чтобы читатель видел её рядом с кодом. Заметка об отряде 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.
Задача construction придерживает постройки, но всё равно патрулирует. Небольшое авиакрыло без базы при ai.patrol-null-enemy-skip выбирает патруль у края карты, делая те же три случайные выборки, что и большое крыло, и патрулирует там, вместо того чтобы ставить в очередь патруль к ближайшему противнику.
Что остаётся в движке
Встроенный компьютерный игрок остаётся игроком по умолчанию. Место считается Classic, только если его файлы совпадают с папкой этой сборки. Эта проверка открывает два поведения и третье — выборку из потока подготовки. brain.shared_random берёт значения из общего потока матча, того же, из которого берут скрипты юнитов и симуляция, поэтому интервалы авиации и осады у Classic идут в ногу со встроенным игроком. unit:stance с direct, равным true, записывает постоянные биты движения и огня и не отдаёт приказа. Любому другому месту это запрещено. brain.setup_random берёт значения из копии этого потока, снятой при встроенной подготовке места, и сдвигает только копию.
У любого другого мозга нет ни одной из этих двух случайных функций, а прямая запись стойки отклоняется. brain.section устанавливается для каждого мозга. В игре эта отметка ничего не делает. Полного обзора в этом списке нет. Это флаг манифеста. Steady Commander ничего из этого не вызывает.
Как это доказывается
В теневом режиме Classic сидит рядом со встроенным игроком. Матч играет приказами встроенного игрока. Приказы Classic записываются и сравниваются. Ни один тик не может отличаться. Тесты движка для этого сравнения — 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 тиков каждая, на основной игре и опубликованных модах. Одно и то же зерно даёт один и тот же матч. Длительный прогон — это долгая проверка того, что место с Classic всю игру придерживается приказов встроенного игрока.
Как это читать
Читайте Classic как записанное поведение встроенного компьютерного игрока. Собственный компьютерный игрок движка по-прежнему играет за место, когда никто не выбрал мозг. Установка или упаковка Classic ничего не меняет, пока на него не поставлено место.