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

Classic, um exemplo comentado

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

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

O Classic é brains/classic. É documentação do computador integrado: as mesmas ordens, escritas para que as possa ler. O jogador integrado continua no motor e continua a ser a predefinição. Escolher o Classic coloca este pacote no lugar. Deixar o lugar no jogador integrado corre o computador do próprio motor.

O manifesto diz que works-with é um asterisco e que fullAwareness é true. A sua descrição diz que joga exatamente como o computador integrado. A visão total é a marca do manifesto. É permitida numa partida em rede, e o lugar assinala-a. O id, o nome e o caminho de instalação não fazem de um pacote o Classic. Os ficheiros têm de 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 ficheiros

brain.lua constrói uma tarefa para cada esquadrão, de 0 a 9. No primeiro tick, onTick atualiza as listas de construção e inicializa as grelhas. Depois, em cada tick, corre a ordenação dos esquadrões segundo a sua contagem decrescente e corre cada tarefa cuja vez tenha chegado.

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 grelhas de construção e os pontos de metal. Lê o registo do motor a partir de me.knowledge.
lua/place.luaProcura um local e alarga a procura até esta ser tão larga como o mapa.
lua/profile.luaLê o texto do perfil e aplica pesos.
lua/report.luaEscreve o texto que report devolve.
lua/rules.luaLê as regras ai.* pelas quais o Classic joga.
lua/squads.luaDistribui as unidades por 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ó o faz quando as regras o indicam, e de resto fica inativo. Os esquadrões 2 e 6 são grupos de ataque. Os esquadrões 3 e 7 reúnem-se com eles. O esquadrão 4 é a construção. O esquadrão 8 é a esquadrilha aérea. O esquadrão 9 é o cerco. O comentário sobre o esquadrão 9 é uma peculiaridade: a ordenação nunca preenche esse esquadrão, por isso a tarefa de cerco só mantém o seu horário, a não ser que já lá estivesse uma unidade.

-- …
    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 grelhas. A própria ordenação é a contagem decrescente. Começa em 30. Cada tick subtrai um e, ao chegar ao fim, volta a 30 e chama a ordenação dos 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 ordenação faz: as unidades selecionáveis no alcance do lugar recebem ordens permanentes, e uma unidade que ainda esteja no esquadrão 0 junta-se ao esquadrão em que o seu tipo se ordena. Uma unidade que já esteja num esquadrão mantém-se nele.

Uma tarefa

lua/tasks/rally.lua é a tarefa de reunião completa. Quando o esquadrão de reunião e o grupo de ataque têm ambos membros, o esquadrão de reunião desloca-se para 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 sobre peculiaridades

Uma linha marcada com @quirk regista um comportamento que o computador integrado tem. A nota está no script para que quem o lê a veja ao lado do código. A nota do esquadrão 9 está na tabela de tarefas acima. Há mais duas 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 transferível é aplicado uma vez por cada computador, por isso o peso acumula-se 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 construção retém construções e continua a patrulhar. Uma pequena esquadrilha aérea sem base, com ai.patrol-null-enemy-skip, sorteia uma patrulha junto ao limite do mapa, com os mesmos três sorteios de uma esquadrilha grande, e patrulha aí em vez de pôr em fila uma patrulha sobre o inimigo mais próximo.

O que fica no motor

O computador integrado continua a ser a predefinição. Um lugar só é o Classic quando os seus ficheiros coincidem com a pasta desta compilação. Esse teste concede dois comportamentos, e um terceiro sorteio a partir do fluxo de preparação. brain.shared_random sorteia a partir do fluxo partilhado da partida, o mesmo de que os scripts das unidades e a simulação sorteiam, para que os intervalos aéreos e de cerco do Classic acompanhem os do jogador integrado. unit:stance com direct definido 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 a partir de uma cópia desse fluxo feita na preparação integrada do lugar, e só faz avançar a cópia.

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

Como se prova

Em sombra, o Classic é colocado 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 diferir. Os testes do motor para essa comparação são sim-brains-shadow e sim-brains-shadow-match.

Em substituição, o Classic é colocado 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 1000 escaramuças sem interface, com semente, de 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 que corre o Classic se mantém nas ordens do jogador integrado durante uma partida inteira.

Como o ler

Leia o Classic como o comportamento do computador integrado, posto por escrito. O computador do próprio motor continua a ser o jogador que um lugar usa quando ninguém escolheu um cérebro. Instalar o Classic, ou empacotá-lo, não altera nada até um lugar lhe ser atribuído.