Início › Info de modding › Classic, um exemplo comentado
Capítulo 09

Classic, um exemplo comentado

O computador integrado escrito em Lua: os arquivos, a tabela de tarefas, uma tarefa, as notas de peculiaridades, o que fica no motor e como se prova que as ordens são iguais.

Este capítulo resume docs/brains/oabrain-standard.md, docs/brains/authoring.md e docs/playing-with-brains.md. Esses documentos são o texto normativo.

O que é o Classic

Classic é brains/classic. Ele é a documentação do computador integrado: as mesmas ordens, escritas para que você possa lê-las. O jogador integrado continua no motor e continua sendo o padrão. Escolher o Classic põe este pacote no lugar. Deixar o lugar no jogador integrado roda o computador do próprio motor.

O manifesto diz que works-with é um asterisco e que fullAwareness é true. A descrição diz que ele joga exatamente como o computador integrado. A visão total é a flag do manifesto. Ela é permitida numa partida em rede, e o lugar a sinaliza. O ID, o nome e o caminho de instalação não fazem de um pacote o Classic. Os arquivos precisam ser a pasta brains/classic desta compilação, com os mesmos caminhos e os mesmos bytes.

Manifesto · 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

Os arquivos

brain.lua monta uma tarefa para cada esquadrão de 0 a 9. No primeiro tick, onTick atualiza as listas de construção e inicializa as grades. Depois, a cada tick, ele roda a separação de esquadrões na sua contagem regressiva e roda cada tarefa cuja vez chegou.

CaminhoO que faz
oabrain.yamlO manifesto.
brain.luaO lugar, a tabela de tarefas e onTick.
lua/build.luaPontua um tipo e escolhe um da lista de um construtor.
lua/knowledge.luaPesos, limites, as grades de construção e os pontos de metal. Lê o registro do motor em me.knowledge.
lua/place.luaProcura um local, ampliando a busca até ela ficar do tamanho do mapa.
lua/profile.luaLê o texto do perfil e aplica os pesos.
lua/report.luaEscreve o texto que report retorna.
lua/rules.luaLê as regras ai.* que o Classic segue.
lua/squads.luaSepara as unidades em esquadrões e dá uma ordem a um esquadrão.
lua/types.luaLê os campos de tipo que o computador usa.
lua/tasks/structures, construction, strike, rally, air_raid e siege.
README.mdUma nota. O jogo não a carrega.
LICENSEA licença.

A tabela de tarefas

Os esquadrões 1 e 5 constroem estruturas. O esquadrão 5 só faz isso quando as regras mandam; do contrário, fica ocioso. Os esquadrões 2 e 6 são grupos de ataque. Os esquadrões 3 e 7 se reagrupam neles. O esquadrão 4 é de construção. O esquadrão 8 é a ala aérea. O esquadrão 9 é de cerco. O comentário sobre o esquadrão 9 é uma peculiaridade: a separação nunca preenche esse esquadrão, então a tarefa de cerco só mantém seu horário, a não ser que uma unidade já estivesse nele.

-- …
    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
-- …

No primeiro tick, onTick atualiza as listas de construção e inicializa as grades. A separação em si é a contagem regressiva. Ela começa em 30. Cada tick subtrai um, e quando chega ao fim ela volta a 30 e chama a separação de esquadrões.

-- …
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 diz o que a separação faz: unidades selecionáveis no alcance do lugar recebem ordens permanentes, e uma unidade ainda no esquadrão 0 entra no esquadrão em que seu tipo se encaixa. Uma unidade que já está num esquadrão continua nele.

Uma tarefa

lua/tasks/rally.lua é a tarefa de reagrupamento inteira. Quando o esquadrão de reagrupamento e o grupo de ataque têm membros, o esquadrão de reagrupamento vai até o centro do grupo de ataque.

-- 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

As notas de peculiaridades

Uma linha marcada com @quirk registra um comportamento que o computador integrado tem. A nota fica no script para que quem lê a veja ao lado do código. A nota do esquadrão 9 está na tabela de tarefas acima. Outras duas ficam em profile.lua. A palavra any só conta como a primeira palavra depois de plan, e essa mesma palavra é testada de novo em cada palavra seguinte da linha. O ai_weight de um tipo baixável é aplicado uma vez por jogador do computador, então o peso se acumula por esse número.

-- 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.

A tarefa de construção retém as obras e mesmo assim patrulha. Uma ala aérea pequena sem base, com ai.patrol-null-enemy-skip, sorteia uma patrulha na borda do mapa, os mesmos três sorteios que uma ala grande faz, e patrulha ali em vez de pôr na fila uma patrulha até o inimigo mais próximo.

O que fica no motor

O computador integrado continua sendo o padrão. Um lugar só é Classic quando seus arquivos correspondem à pasta desta compilação. Esse teste concede dois comportamentos, e um terceiro sorteio da sequência de preparação. brain.shared_random sorteia da sequência compartilhada da partida, a mesma de que os scripts de unidade e a simulação sorteiam, então os intervalos de aeronaves e de cerco do Classic ficam em sincronia com o jogador integrado. unit:stance com direct como true escreve os bits permanentes de movimento e de fogo e não dá nenhuma ordem. Qualquer outro lugar é recusado. brain.setup_random sorteia de uma cópia dessa sequência tirada na preparação integrada do lugar, e só avança a cópia.

Nenhum outro cérebro tem qualquer das duas funções aleatórias, e uma escrita direta de postura é recusada. brain.section é instalado para todos os cérebros. Numa partida, a marcação não faz nada. A visão total não está nesta lista. Ela é a flag do manifesto. O Steady Commander não chama nenhuma delas.

Como se prova

No modo sombra, o Classic é posto ao lado do jogador integrado. As ordens do jogador integrado são as que a partida joga. As ordens do Classic são gravadas e comparadas. Nenhum tick pode ser diferente. Os testes do motor para essa comparação são sim-brains-shadow e sim-brains-shadow-match.

Na substituição, o Classic é posto onde o jogador integrado jogaria. Os testes do motor para isso são 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 e native-net-loopback-each-ai-classic.

Um teste de resistência joga 1.000 escaramuças sem interface, com sementes definidas e 20.000 ticks cada, sobre o jogo base e os mods publicados. A mesma semente joga a mesma partida. O teste de resistência é a verificação longa de que um lugar rodando o Classic segue as ordens do jogador integrado durante uma partida inteira.

Como ler

Leia o Classic como o comportamento do computador integrado, posto no papel. O computador do próprio motor continua sendo o jogador que um lugar usa quando ninguém escolheu um cérebro. Instalar o Classic, ou empacotá-lo, não muda nada até que um lugar seja definido para ele.