Home › Modding Info › Classic, a worked example
Chapter 09

Classic, a worked example

The built-in computer player written in Lua: its files, the task table, one task, the quirk notes, what stays in the engine, and how the same orders are proven.

This chapter summarises docs/brains/oabrain-standard.md, docs/brains/authoring.md and docs/playing-with-brains.md. Those docs are the normative text.

What Classic is

Classic is brains/classic. It is documentation of the built-in computer player: the same orders, written so you can read them. The built-in player stays in the engine and stays the default. Choosing Classic seats this package. Leaving the seat on the built-in player runs the engine's own computer player.

The manifest says works-with is a star, and fullAwareness is true. Its description says it plays exactly as the built-in computer player does. Full awareness is the manifest flag. It is allowed in a network game, and the seat flags it. The id, the name and the install path do not make a package Classic. The files have to be this build's brains/classic folder, the same paths and the same 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

The files

brain.lua builds one task for each squad from 0 to 9. On the first tick onTick refreshes the build lists and seeds the grids. Each tick then runs the squad sort on its countdown and runs each task whose turn has come.

PathWhat it does
oabrain.yamlThe manifest.
brain.luaThe seat, the task table and onTick.
lua/build.luaScores a type and picks one from a builder's list.
lua/knowledge.luaWeights, limits, the build grids and the metal spots. Reads the engine's record from me.knowledge.
lua/place.luaSearches for a site, widening until the search is as wide as the map.
lua/profile.luaReads the profile text and applies weights.
lua/report.luaWrites the text report returns.
lua/rules.luaReads the ai.* rules Classic plays by.
lua/squads.luaSorts units into squads and gives a squad an order.
lua/types.luaReads the type fields the computer player uses.
lua/tasks/structures, construction, strike, rally, air_raid and siege.
README.mdA note. The game does not load it.
LICENSEThe licence.

The task table

Squads 1 and 5 build structures. Squad 5 does that only when the rules say so, and otherwise idles. Squads 2 and 6 are strike groups. Squads 3 and 7 rally onto them. Squad 4 is construction. Squad 8 is the aircraft wing. Squad 9 is siege. The comment on squad 9 is a quirk: the sort never fills that squad, so the siege task only keeps its timetable unless a unit was already in it.

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

On the first tick onTick refreshes the build lists and seeds the grids. The sort itself is the countdown. It starts at 30. Each tick subtracts one, and at the bottom it returns to 30 and calls the squad sort.

-- …
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 says what the sort does: selectable units in the seat's range take standing orders, and a unit still in squad 0 joins the squad its type sorts into. A unit already in a squad keeps that squad.

One task

lua/tasks/rally.lua is the whole of the rally task. When the rally squad and the strike group both have members, the rally squad moves to the strike group's centre.

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

The quirk notes

A line marked @quirk records a behaviour the built-in computer player has. The note is in the script so a reader sees it beside the code. The squad 9 note is in the task table above. Two more sit in profile.lua. The word any counts only as the first word after plan, and that same word is tested again on every later word of the line. A downloadable type's ai_weight is applied once per computer player, so the weight compounds by that count.

-- 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 withholds builds, and still patrols. A small aircraft wing with no base, under ai.patrol-null-enemy-skip, draws a map-edge patrol, the same three draws a large wing takes, and patrols there instead of queueing a patrol on the nearest enemy.

What stays in the engine

The built-in computer player remains the default. A seat is Classic only when its files match this build's folder. That test grants two behaviours, and a third draw from the set-up stream. brain.shared_random draws the match's shared stream, the one unit scripts and the simulation draw from, so Classic's aircraft and siege intervals stay in step with the built-in player. unit:stance with direct set to true writes the standing move and fire bits and gives no order. Any other seat is refused. brain.setup_random draws from a copy of that stream taken at the seat's built-in set-up, and advances only the copy.

Any other brain has neither random function, and a direct stance write is refused. brain.section is installed for every brain. In a game the mark does nothing. Full awareness is not in this list. It is the manifest flag. Steady Commander calls none of them.

How it is proven

In shadow, Classic is seated beside the built-in player. The built-in player's orders are the ones the match plays. Classic's orders are recorded and compared. No tick may differ. The engine's tests for that comparison are sim-brains-shadow and sim-brains-shadow-match.

Swapped in, Classic is seated where the built-in player would play. The engine's tests for that are 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 and native-net-loopback-each-ai-classic.

A soak plays 1,000 seeded headless skirmishes, 20,000 ticks each, over the base game and the published mods. The same seed plays the same match. The soak is the long check that a seat running Classic stays on the built-in player's orders for a whole game.

How to read it

Read Classic as the built-in computer player's behaviour, written down. The engine's own computer player is still the player a seat uses when nobody chose a brain. Installing Classic, or packing it, changes nothing until a seat is set to it.