Главная › Моддинг › Classic: разбор примера
Глава 09

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 ничего не меняет, пока на него не поставлено место.