Skip to content

ai-battle: support team assignment (--teams) and establish team start-pacts - #1957

Open
FlexApex wants to merge 11 commits into
Return-To-The-Roots:masterfrom
FlexApex:ai-battle-teams
Open

ai-battle: support team assignment (--teams) and establish team start-pacts#1957
FlexApex wants to merge 11 commits into
Return-To-The-Roots:masterfrom
FlexApex:ai-battle-teams

Conversation

@FlexApex

Copy link
Copy Markdown

ai-battle: support team assignment (--teams) and establish team start-pacts

Builds on #1949 (uses its HeadlessGame). Two commits:

1. --teams option for team assignment

Adds a --teams option to the headless ai-battle harness so the AI can be
verified under real team rules instead of a free-for-all:

ai-battle -m Map.swd --ai aijh --ai aijh --ai aijh --ai aijh --teams "0,1;2,3"

Groups are separated by ;, player indices by ,. The first group becomes
Team1, the second Team2, and so on; unlisted players stay teamless. The
assignment is threaded through HeadlessGame into GeneratePlayerInfo, which
sets PlayerInfo::team.

2. Establish team start-pacts in HeadlessGame to match GameClient (avoid desyncs in replays)

GameClient::StartGame calls GamePlayer::MakeStartPacts() for every player on
a fresh map, creating the ally + non-aggression pacts that bind a team together.
HeadlessGame never did this, so a recorded ai-battle with teams had no pacts:
GamePlayer::IsAttackable() returned true for teammates and the AIs attacked
their own team. On replay GameClient does set up the pacts, so the recorded
inter-team attack commands hit allied players and were resolved differently —
producing an object-count divergence (replay desync) without any RANDOM
divergence.

MakeStartPacts() early-returns for Team::None, so the call is a no-op until
players are actually assigned to teams via --teams. Together with the
MapLoader::SetupResources() call already in HeadlessGame, this makes the
headless world setup match the GameClient replay path.

Note

MichalLabuda and others added 10 commits June 18, 2026 02:52
Adds a --teams option (e.g. "0,1;2,3" for a 2v2) that assigns players to
teams in the headless harness, so the AI can be verified under real team
rules instead of a free-for-all. Player indices map to Team1, Team2, ...
in the order the groups are given; unlisted players stay teamless.

The assignment is threaded through HeadlessGame into GeneratePlayerInfo,
which sets PlayerInfo::team. Establishing the actual alliances follows in
the next commit.
…ient

GameClient::StartGame calls GamePlayer::MakeStartPacts() for every player
on a fresh map, creating the ally + non-aggression pacts that bind a team
together. HeadlessGame never did this, so in a recorded ai-battle with
teams the teammates had no pacts and GamePlayer::IsAttackable() returned
true for them: the AIs attacked their own team. On replay GameClient
*does* set up the pacts, so the recorded inter-team attack commands hit
allied players and were resolved differently, producing an object-count
divergence (and replay desync) without any RANDOM divergence.

MakeStartPacts() early-returns for Team::None, so this is a no-op until
players are actually assigned to teams via --teams.
Comment thread extras/ai-battle/main.cpp

ggs.objective = GameObjective::TotalDomination;
HeadlessGame game(ggs, mapPath, ais);
const auto wares = options["wares"].as<std::string>();

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any specific reason to use this construct here but have lua/settings_path "auto-filled"?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No deliberate reason beyond following the file's two pre-existing conventions: the always-present, default-valued options are read inline with options[...].as<>() (this is how objective was already handled), while the genuinely-optional file paths are bound to optionalstd::string via po::value(&var) so their presence can be checked with if(var) (this is how replay/save were already handled). I kept wares like objective, and lua/settings like replay/save.

Comment thread extras/ai-battle/main.cpp Outdated
…skipping

Silently skipping unparseable or unknown [addons] entries hid exactly the
mistakes the settings file is meant to catch (a mistyped key/value or an
accidental section name went unnoticed while the user's intent was not applied).
Make loadAddonsFromIni strict:
- an entry that doesn't parse -> hard error naming the entry,
- an unknown/unsupported addon id (getAddon(id) == null) -> hard error,
- a missing [addons] section -> hard error, except for a fully empty file.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants