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.
Тривалий тест (soak) грає 1000 сутичок без інтерфейсу з заданими seed, по 20 000 тіків кожна, на базовій грі й опублікованих модах. Той самий seed грає той самий матч. Soak — це довга перевірка того, що місце з Classic упродовж усієї гри не відходить від наказів вбудованого гравця.
Як це читати
Читай Classic як записану поведінку вбудованого компʼютерного гравця. Власний компʼютерний гравець рушія — це й далі той гравець, якого використовує місце, коли ніхто не обрав мозок. Установлення чи пакування Classic нічого не змінює, доки на місце не призначено цей мозок.