実例: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のそれぞれにタスクを1つ作ります。最初のティックで、onTickは建設リストを更新し、グリッドを初期化します。その後は各ティックで、カウントダウンに従って分隊の振り分けを行い、順番が来たタスクを実行します。
| パス | 内容 |
|---|---|
oabrain.yaml | マニフェスト。 |
brain.lua | 席、タスク表、onTick。 |
lua/build.lua | 種類を採点し、建設ユニットのリストから1つ選びます。 |
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のコメントは癖を記したものです。振り分けがこの分隊を埋めることはないので、すでにユニットが入っていない限り、攻城タスクは予定を刻むだけです。
-- …
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から始まります。ティックごとに1を引き、下限に達すると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の注記は、上のタスク表にあります。ほかに2つがprofile.luaにあります。anyという語は、planの直後の語としてだけ数えられ、同じ語がその行の以降の語すべてで改めて調べられます。ダウンロードで追加された種類のai_weightはコンピューター1人ごとに1回適用されるので、重みはその人数分だけ掛け合わされます。
-- 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のもとで、大きな部隊と同じ3回の抽選でマップ端の巡回地点を引き、最も近い敵への巡回を予約する代わりに、そこを巡回します。
エンジンに残る部分
内蔵コンピューターが既定であることは変わりません。席がClassicになるのは、そのファイルがこのビルドのフォルダーと一致するときだけです。その判定によって2つの挙動と、初期設定の乱数列からの3つ目の引き方が許されます。brain.shared_randomは対戦の共有乱数列、つまりユニットスクリプトとシミュレーションが引くのと同じ乱数列から引くので、Classicの航空部隊と攻城の間隔は内蔵コンピューターとそろったままになります。directをtrueにしたunit:stanceは、常設の移動と射撃のビットを書き込み、命令は出しません。ほかの席では拒否されます。brain.setup_randomは、席の内蔵の初期設定時に取ったその乱数列のコピーから引き、進むのはそのコピーだけです。
ほかのブレインはどちらの乱数関数も持たず、直接のスタンス書き込みは拒否されます。brain.sectionはすべてのブレインにインストールされ、ゲーム中、この印は何もしません。全視界はこの一覧に入りません。それはマニフェストのフラグです。Steady Commanderはこれらのどれも呼びません。
証明の方法
シャドー実行では、Classicが内蔵コンピューターと並んで席に着きます。対戦で使われるのは内蔵コンピューターの命令で、Classicの命令は記録されて比較されます。1ティックでも違ってはいけません。この比較のためのエンジンのテストは、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です。
耐久テストでは、シードを固定したヘッドレスのスカーミッシュを、ベースゲームと公開済みのMODで1,000回、それぞれ20,000ティック行います。同じシードからは同じ対戦が生まれます。耐久テストは、Classicを使う席がゲーム全体を通して内蔵コンピューターの命令から外れないことを確かめる、長時間の確認です。
読み方
Classicは、内蔵コンピューターの挙動を書き起こしたものとして読んでください。誰もブレインを選ばなかったときに席が使うのは、変わらずエンジン本来のコンピューターです。Classicをインストールしても、パックしても、席に設定するまでは何も変わりません。