Start › Modding-Infos › Classic, ein ausgearbeitetes Beispiel
Kapitel 09

Classic, ein ausgearbeitetes Beispiel

Der eingebaute Computerspieler, in Lua geschrieben: seine Dateien, die Aufgabentabelle, eine Aufgabe, die Notizen zu den Eigenheiten, was in der Engine bleibt, und wie dieselben Befehle nachgewiesen werden.

Dieses Kapitel fasst docs/brains/oabrain-standard.md, docs/brains/authoring.md und docs/playing-with-brains.md zusammen. Diese Dokumente sind der maßgebliche Text.

Was Classic ist

Classic ist brains/classic. Es ist die Dokumentation des eingebauten Computerspielers: dieselben Befehle, so geschrieben, dass du sie lesen kannst. Der eingebaute Spieler bleibt in der Engine und bleibt der Standard. Classic zu wählen setzt dieses Paket ein. Den Sitz auf dem eingebauten Spieler zu lassen führt den eigenen Computerspieler der Engine aus.

Das Manifest sagt, works-with ist ein Stern, und fullAwareness ist true. Seine Beschreibung sagt, es spielt genau so wie der eingebaute Computerspieler. Volle Sicht ist das Flag des Manifests. Sie ist in einem Netzwerkspiel erlaubt, und der Sitz kennzeichnet sie. Die Kennung, der Name und der Installationspfad machen ein Paket nicht zu Classic. Die Dateien müssen der Ordner brains/classic dieses Builds sein, dieselben Pfade und dieselben Bytes.

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

Die Dateien

brain.lua baut eine Aufgabe für jeden Trupp von 0 bis 9. Beim ersten Tick frischt onTick die Baulisten auf und setzt die Gitter an. Jeder Tick führt dann die Trupp-Sortierung an ihrem Countdown aus und führt jede Aufgabe aus, deren Zug gekommen ist.

PfadWirkung
oabrain.yamlDas Manifest.
brain.luaDer Sitz, die Aufgabentabelle und onTick.
lua/build.luaBewertet einen Typ und wählt einen aus der Liste eines Bauers.
lua/knowledge.luaGewichte, Limits, die Baugitter und die Metallstellen. Liest den Eintrag der Engine aus me.knowledge.
lua/place.luaSucht einen Bauplatz und weitet die Suche, bis sie so breit ist wie die Karte.
lua/profile.luaLiest den Profiltext und wendet Gewichte an.
lua/report.luaSchreibt den Text, den report zurückgibt.
lua/rules.luaLiest die ai.*-Regeln, nach denen Classic spielt.
lua/squads.luaSortiert Einheiten in Trupps und gibt einem Trupp einen Befehl.
lua/types.luaLiest die Typfelder, die der Computerspieler nutzt.
lua/tasks/structures, construction, strike, rally, air_raid und siege.
README.mdEine Notiz. Das Spiel lädt sie nicht.
LICENSEDie Lizenz.

Die Aufgabentabelle

Die Trupps 1 und 5 bauen Gebäude. Trupp 5 tut das nur, wenn die Regeln es sagen, und bleibt sonst untätig. Die Trupps 2 und 6 sind Angriffsgruppen. Die Trupps 3 und 7 sammeln sich auf sie. Trupp 4 ist der Bau. Trupp 8 ist die Flugzeugstaffel. Trupp 9 ist die Belagerung. Der Kommentar zu Trupp 9 ist eine Eigenheit: die Sortierung füllt diesen Trupp nie, darum hält die Belagerungsaufgabe nur ihren Zeitplan, außer eine Einheit war schon darin.

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

Beim ersten Tick frischt onTick die Baulisten auf und setzt die Gitter an. Die Sortierung selbst ist der Countdown. Er beginnt bei 30. Jeder Tick zieht eins ab, und unten kehrt er auf 30 zurück und ruft die Trupp-Sortierung.

-- …
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 sagt, was die Sortierung tut: wählbare Einheiten in der Reichweite des Sitzes nehmen Dauerbefehle an, und eine Einheit, die noch in Trupp 0 ist, geht in den Trupp, in den ihr Typ sie einsortiert. Eine Einheit, die schon in einem Trupp ist, behält diesen Trupp.

Eine Aufgabe

lua/tasks/rally.lua ist die ganze Sammelaufgabe. Wenn der Sammeltrupp und die Angriffsgruppe beide Mitglieder haben, bewegt sich der Sammeltrupp zur Mitte der Angriffsgruppe.

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

Die Notizen zu den Eigenheiten

Eine Zeile mit @quirk hält ein Verhalten fest, das der eingebaute Computerspieler hat. Die Notiz steht im Skript, damit ein Leser sie neben dem Code sieht. Die Notiz zu Trupp 9 steht in der Aufgabentabelle oben. Zwei weitere stehen in profile.lua. Das Wort any zählt nur als das erste Wort nach plan, und dasselbe Wort wird an jedem späteren Wort der Zeile erneut geprüft. Der ai_weight eines herunterladbaren Typs wird einmal je Computerspieler angewandt, darum vervielfacht sich das Gewicht mit dieser Anzahl.

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

Der Bau hält Bauten zurück und patrouilliert trotzdem. Eine kleine Flugzeugstaffel ohne Basis zieht unter ai.patrol-null-enemy-skip eine Patrouille am Kartenrand, dieselben drei Ziehungen, die eine große Staffel nimmt, und patrouilliert dort, statt eine Patrouille auf den nächsten Gegner einzureihen.

Was in der Engine bleibt

Der eingebaute Computerspieler bleibt der Standard. Ein Sitz ist nur dann Classic, wenn seine Dateien zum Ordner dieses Builds passen. Diese Prüfung gewährt zwei Verhaltensweisen und eine dritte Ziehung aus dem Setup-Strom. brain.shared_random zieht aus dem gemeinsamen Strom des Matches, aus dem auch Einheitenskripte und die Simulation ziehen, damit die Intervalle von Flugzeugen und Belagerung bei Classic im Takt mit dem eingebauten Spieler bleiben. unit:stance mit direct auf true schreibt die Bits für Bewegungs- und Feuerhaltung und gibt keinen Befehl. Jeder andere Sitz wird abgelehnt. brain.setup_random zieht aus einer Kopie dieses Stroms, die beim eingebauten Setup des Sitzes genommen wurde, und rückt nur die Kopie voran.

Jedes andere Gehirn hat keine der beiden Zufallsfunktionen, und ein direktes Schreiben der Haltung wird abgelehnt. brain.section ist für jedes Gehirn eingerichtet. In einem Spiel tut die Markierung nichts. Volle Sicht steht nicht in dieser Liste. Sie ist das Flag des Manifests. Steady Commander ruft keine davon auf.

Wie es nachgewiesen wird

Im Schatten wird Classic neben den eingebauten Spieler gesetzt. Die Befehle des eingebauten Spielers sind die, die das Match spielt. Die Befehle von Classic werden aufgezeichnet und verglichen. Kein Tick darf abweichen. Die Tests der Engine für diesen Vergleich sind sim-brains-shadow und sim-brains-shadow-match.

Im Austausch wird Classic dort eingesetzt, wo der eingebaute Spieler spielen würde. Die Tests der Engine dafür sind 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 und native-net-loopback-each-ai-classic.

Ein Dauertest spielt 1.000 gesäte Gefechte ohne Oberfläche, je 20.000 Ticks, über das Grundspiel und die veröffentlichten Mods. Derselbe Seed spielt dasselbe Match. Der Dauertest ist die lange Prüfung, dass ein Sitz, der Classic ausführt, über ein ganzes Spiel bei den Befehlen des eingebauten Spielers bleibt.

Wie man es liest

Lies Classic als das aufgeschriebene Verhalten des eingebauten Computerspielers. Der eigene Computerspieler der Engine ist weiter der Spieler, den ein Sitz nutzt, wenn niemand ein Gehirn gewählt hat. Classic zu installieren oder zu packen ändert nichts, bis ein Sitz darauf gesetzt ist.