실제 예제: 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 | 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의 주석은 특이 동작입니다. 정렬이 이 분대를 결코 채우지 않으므로, 이미 유닛이 들어 있지 않은 한 siege 작업은 일정만 유지합니다.
-- …
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가 rally 작업의 전부입니다. 집결 분대와 타격대에 모두 구성원이 있으면, 집결 분대가 타격대의 중심으로 이동합니다.
-- 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을 설치하거나 패키징해도, 자리에 지정하기 전까지는 아무것도 바뀌지 않습니다.