From 3c88b4e4a2b94f0b2b2c849877e133fd720b7a52 Mon Sep 17 00:00:00 2001 From: ApexFlex Date: Sun, 28 Jun 2026 12:04:54 +0200 Subject: [PATCH 1/6] ai-battle: add --teams option for team assignment 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. --- extras/ai-battle/HeadlessGame.cpp | 10 +++++----- extras/ai-battle/HeadlessGame.h | 5 ++++- extras/ai-battle/main.cpp | 30 +++++++++++++++++++++++++++++- 3 files changed, 38 insertions(+), 7 deletions(-) diff --git a/extras/ai-battle/HeadlessGame.cpp b/extras/ai-battle/HeadlessGame.cpp index 444b9e8b92..73bf0a956f 100644 --- a/extras/ai-battle/HeadlessGame.cpp +++ b/extras/ai-battle/HeadlessGame.cpp @@ -22,7 +22,7 @@ # include "Windows.h" #endif -std::vector GeneratePlayerInfo(const std::vector& ais); +std::vector GeneratePlayerInfo(const std::vector& ais, const std::vector& teams); std::string ToString(const std::chrono::milliseconds& time); std::string HumanReadableNumber(unsigned num); @@ -44,8 +44,8 @@ void printConsole(const char* fmt, ...); #endif HeadlessGame::HeadlessGame(const GlobalGameSettings& ggs, const bfs::path& map, const std::vector& ais, - const bfs::path& luaPath) - : map_(map), game_(ggs, std::make_unique(0), GeneratePlayerInfo(ais)), world_(game_.world_), + const bfs::path& luaPath, const std::vector& teams) + : map_(map), game_(ggs, std::make_unique(0), GeneratePlayerInfo(ais, teams)), world_(game_.world_), em_(*static_cast(game_.em_.get())) { MapLoader loader(world_); @@ -234,7 +234,7 @@ void HeadlessGame::PrintState() lastReportGf_ = em_.GetCurrentGF(); } -std::vector GeneratePlayerInfo(const std::vector& ais) +std::vector GeneratePlayerInfo(const std::vector& ais, const std::vector& teams) { std::vector ret; for(const AI::Info& ai : ais) @@ -249,7 +249,7 @@ std::vector GeneratePlayerInfo(const std::vector& ais) default: pi.name = "Dummy " + std::to_string(ret.size()); break; } pi.nation = Nation::Romans; - pi.team = Team::None; + pi.team = (ret.size() < teams.size()) ? teams[ret.size()] : Team::None; pi.color = PLAYER_COLORS[ret.size() % PLAYER_COLORS.size()]; ret.push_back(pi); } diff --git a/extras/ai-battle/HeadlessGame.h b/extras/ai-battle/HeadlessGame.h index 578b0a446c..69e9931f03 100644 --- a/extras/ai-battle/HeadlessGame.h +++ b/extras/ai-battle/HeadlessGame.h @@ -9,6 +9,7 @@ #include "Replay.h" #include "ai/AIPlayer.h" #include "gameTypes/AIInfo.h" +#include "gameTypes/TeamTypes.h" #include #include #include @@ -22,8 +23,10 @@ class EventManager; class HeadlessGame { public: + /// teams: optional per-player team assignment (index -> Team). Players sharing a team get start + /// pacts (ally + non-aggression), so only opposing teams fight. Defaults to no teams. HeadlessGame(const GlobalGameSettings& ggs, const boost::filesystem::path& map, const std::vector& ais, - const boost::filesystem::path& luaPath = {}); + const boost::filesystem::path& luaPath = {}, const std::vector& teams = {}); ~HeadlessGame(); void Run(unsigned maxGF = std::numeric_limits::max()); diff --git a/extras/ai-battle/main.cpp b/extras/ai-battle/main.cpp index 497e824a9a..a72c99e57e 100644 --- a/extras/ai-battle/main.cpp +++ b/extras/ai-battle/main.cpp @@ -13,6 +13,7 @@ #include "addons/const_addons.h" #include "ai/random.h" #include "files.h" +#include "gameTypes/TeamTypes.h" #include "random/Random.h" #include "s25util/Log.h" #include "s25util/StringConversion.h" @@ -25,6 +26,7 @@ #include #include #include +#include #if BOOST_VERSION >= 109000 # include using std::optional; @@ -87,6 +89,7 @@ int main(int argc, char** argv) ("help,h", "Show help") ("map,m", po::value()->required(),"Map to load") ("ai", po::value>()->required(),"AI player(s) to add (aijh | dummy)") + ("teams", po::value(),"Team assignment, e.g. \"0,1;2,3\" for a 2v2 (groups separated by ';', player indices by ','). Allied players get start pacts.") ("objective", po::value()->default_value("domination"),"domination(default) | conquer") ("wares", po::value()->default_value("normal"),"Starting wares: vlow | low | normal (default) | alot") ("settings", po::value(&settings_path),"INI file with an [addons] section to configure addon settings (optional)") @@ -213,7 +216,32 @@ int main(int argc, char** argv) } } - HeadlessGame game(ggs, mapPath, ais, lua_path ? RTTRCONFIG.ExpandPath(*lua_path) : bfs::path{}); + // Team assignment, e.g. "0,1;2,3". Player index -> Team (Team1, Team2, ...). + std::vector teams; + if(options.count("teams")) + { + std::stringstream groups(options["teams"].as()); + std::string group; + unsigned teamIdx = 0; + while(std::getline(groups, group, ';')) + { + const Team team = static_cast(static_cast(Team::Team1) + teamIdx); + std::stringstream members(group); + std::string idx; + while(std::getline(members, idx, ',')) + { + if(idx.empty()) + continue; + const unsigned p = static_cast(std::stoul(idx)); + if(p >= teams.size()) + teams.resize(p + 1, Team::None); + teams[p] = team; + } + ++teamIdx; + } + } + + HeadlessGame game(ggs, mapPath, ais, lua_path ? RTTRCONFIG.ExpandPath(*lua_path) : bfs::path{}, teams); if(replay_path) game.RecordReplay(RTTRCONFIG.ExpandPath(*replay_path), random_init); From 8dee346028a87c13d2e7f43fd4743d394b2a1246 Mon Sep 17 00:00:00 2001 From: ApexFlex Date: Sun, 28 Jun 2026 12:05:43 +0200 Subject: [PATCH 2/6] ai-battle: establish team start-pacts in HeadlessGame to match GameClient 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. --- extras/ai-battle/HeadlessGame.cpp | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/extras/ai-battle/HeadlessGame.cpp b/extras/ai-battle/HeadlessGame.cpp index 73bf0a956f..4724d09e84 100644 --- a/extras/ai-battle/HeadlessGame.cpp +++ b/extras/ai-battle/HeadlessGame.cpp @@ -4,6 +4,7 @@ #include "HeadlessGame.h" #include "EventManager.h" +#include "GamePlayer.h" #include "GlobalGameSettings.h" #include "PlayerInfo.h" #include "Savegame.h" @@ -53,6 +54,14 @@ HeadlessGame::HeadlessGame(const GlobalGameSettings& ggs, const bfs::path& map, throw std::runtime_error("Could not load " + map.string()); MapLoader::SetupResources(world_); + // Establish the team alliances (ally + non-aggression pacts) exactly like GameClient::StartGame does + // for a fresh map. Without this, teammates have no pacts and are mutually attackable, so the AIs attack + // their own team; on replay GameClient *does* set up the pacts, so those recorded attack commands are + // handled differently and the replay desyncs (object-count divergence). MakeStartPacts is a no-op for + // teamless players, so this is safe regardless of whether --teams was given. + for(unsigned i = 0; i < world_.GetNumPlayers(); ++i) + world_.GetPlayer(i).MakeStartPacts(); + if(!luaPath.empty()) { if(!loader.LoadLuaScript(game_, localState_, luaPath)) From a7d72b0ab9b160455e5792232d0707394c227d01 Mon Sep 17 00:00:00 2001 From: FlexApex Date: Sun, 16 Aug 2026 13:01:48 +0200 Subject: [PATCH 3/6] ai-battle: fail on invalid/unknown/missing addon settings instead of 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. --- extras/ai-battle/main.cpp | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/extras/ai-battle/main.cpp b/extras/ai-battle/main.cpp index a72c99e57e..ebac6e7250 100644 --- a/extras/ai-battle/main.cpp +++ b/extras/ai-battle/main.cpp @@ -47,26 +47,32 @@ static void loadAddonsFromIni(GlobalGameSettings& ggs, const bfs::path& iniPath) boost::property_tree::ptree tree; boost::property_tree::read_ini(iniPath.string(), tree); + if(tree.empty()) // empty file -> nothing to configure, that's fine + return; + + // Anything else is intentional configuration, so surface mistakes as hard errors instead of + // silently ignoring them (a mistyped section or key/value would otherwise go unnoticed). const auto addons = tree.get_child_optional("addons"); if(!addons) - { - bnw::cout << "Note: no [addons] section in " << iniPath << ", using defaults.\n"; - return; - } + throw std::runtime_error("No [addons] section in " + iniPath.string()); unsigned loaded = 0; for(const auto& entry : *addons) { + AddonId id{}; + unsigned value = 0; try { - const auto id = static_cast(s25util::fromStringClassic(entry.first)); - const auto v = entry.second.get_value(); - ggs.setSelection(id, v); - ++loaded; + id = static_cast(s25util::fromStringClassic(entry.first)); + value = entry.second.get_value(); } catch(const std::exception&) { - // Unknown or invalid entry - skip silently + throw std::runtime_error("Invalid addon entry '" + entry.first + "' in " + iniPath.string()); } + if(!ggs.getAddon(id)) // unknown/unsupported addon id + throw std::runtime_error("Unknown addon id '" + entry.first + "' in " + iniPath.string()); + ggs.setSelection(id, value); + ++loaded; } bnw::cout << "Loaded " << loaded << " addon settings from " << iniPath << '\n'; } From 9051f3dc8f8feb5d1ca29eceee9721185b54798b Mon Sep 17 00:00:00 2001 From: Alexander Grund Date: Sun, 4 Oct 2026 12:57:38 +0200 Subject: [PATCH 4/6] Convert CLI parsing of AI battle to use value-references consistently. Move from explicit type-templates and `count` checks to a struct passing members by reference to Boost.ProgramOptions. --- extras/ai-battle/main.cpp | 103 ++++++++++++++++++++------------------ 1 file changed, 55 insertions(+), 48 deletions(-) diff --git a/extras/ai-battle/main.cpp b/extras/ai-battle/main.cpp index ebac6e7250..c0e08e5ac8 100644 --- a/extras/ai-battle/main.cpp +++ b/extras/ai-battle/main.cpp @@ -13,8 +13,8 @@ #include "addons/const_addons.h" #include "ai/random.h" #include "files.h" -#include "gameTypes/TeamTypes.h" #include "random/Random.h" +#include "gameTypes/TeamTypes.h" #include "s25util/Log.h" #include "s25util/StringConversion.h" #include "s25util/System.h" @@ -82,29 +82,37 @@ int main(int argc, char** argv) bnw::nowide_filesystem(); bnw::args _(argc, argv); - optional replay_path; - optional savegame_path; - optional lua_path; - optional settings_path; - unsigned random_init = static_cast(std::chrono::high_resolution_clock::now().time_since_epoch().count()); - unsigned random_ai_init = random_init; - po::options_description desc("Allowed options"); + struct + { + std::string map; + std::vector ais; + optional teams; + std::string objective, wares; + optional replay_path; + optional savegame_path; + optional lua_path; + optional settings_path; + unsigned random_init = + static_cast(std::chrono::high_resolution_clock::now().time_since_epoch().count()); + unsigned random_ai_init = random_init; + unsigned maxGF = std::numeric_limits::max(); + } opts; // clang-format off desc.add_options() ("help,h", "Show help") - ("map,m", po::value()->required(),"Map to load") - ("ai", po::value>()->required(),"AI player(s) to add (aijh | dummy)") - ("teams", po::value(),"Team assignment, e.g. \"0,1;2,3\" for a 2v2 (groups separated by ';', player indices by ','). Allied players get start pacts.") - ("objective", po::value()->default_value("domination"),"domination(default) | conquer") - ("wares", po::value()->default_value("normal"),"Starting wares: vlow | low | normal (default) | alot") - ("settings", po::value(&settings_path),"INI file with an [addons] section to configure addon settings (optional)") - ("replay", po::value(&replay_path),"Filename to write replay to (optional)") - ("save", po::value(&savegame_path),"Filename to write savegame to (optional)") - ("lua", po::value(&lua_path),"Lua script to execute during the game (optional)") - ("random_init", po::value(&random_init),"Seed value for the random number generator (optional)") - ("random_ai_init", po::value(&random_ai_init),"Seed value for the AI random number generator (optional)") - ("maxGF", po::value()->default_value(std::numeric_limits::max()),"Maximum number of game frames to run (optional)") + ("map,m", po::value(&opts.map)->required(),"Map to load") + ("ai", po::value(&opts.ais)->required(),"AI player(s) to add (aijh | dummy)") + ("teams", po::value(&opts.teams),"Team assignment, e.g. \"0,1;2,3\" for a 2v2 (groups separated by ';', player indices by ','). Allied players get start pacts.") + ("objective", po::value(&opts.objective)->default_value("domination"),"domination(default) | conquer") + ("wares", po::value(&opts.wares)->default_value("normal"),"Starting wares: vlow | low | normal (default) | alot") + ("settings", po::value(&opts.settings_path),"INI file with an [addons] section to configure addon settings (optional)") + ("replay", po::value(&opts.replay_path),"Filename to write replay to (optional)") + ("save", po::value(&opts.savegame_path),"Filename to write savegame to (optional)") + ("lua", po::value(&opts.lua_path),"Lua script to execute during the game (optional)") + ("random_init", po::value(&opts.random_init),"Seed value for the random number generator (optional)") + ("random_ai_init", po::value(&opts.random_ai_init),"Seed value for the AI random number generator (optional)") + ("maxGF", po::value(&opts.maxGF),"Maximum number of game frames to run (optional)") ("version", "Show version information and exit") ; // clang-format on @@ -122,9 +130,9 @@ int main(int argc, char** argv) return 1; } - po::variables_map options; try { + po::variables_map options; po::store(po::command_line_parser(argc, argv).options(desc).run(), options); if(options.count("help")) @@ -154,8 +162,8 @@ int main(int argc, char** argv) for(int i = 0; i < argc; ++i) bnw::cout << argv[i] << " "; bnw::cout << std::endl; - bnw::cout << "random_init: " << random_init << std::endl; - bnw::cout << "random_ai_init: " << random_ai_init << std::endl; + bnw::cout << "random_init: " << opts.random_init << std::endl; + bnw::cout << "random_ai_init: " << opts.random_ai_init << std::endl; bnw::cout << std::endl; RTTRCONFIG.Init(); @@ -164,44 +172,42 @@ int main(int argc, char** argv) const bfs::path logDir = RTTRCONFIG.ExpandPath(s25::folders::logs); bfs::create_directories(logDir); LOG.setLogFilepath(logDir); - RANDOM.Init(random_init); - AI::getRandomGenerator().seed(random_ai_init); + RANDOM.Init(opts.random_init); + AI::getRandomGenerator().seed(opts.random_ai_init); - const bfs::path mapPath = RTTRCONFIG.ExpandPath(options["map"].as()); - const std::vector ais = ParseAIOptions(options["ai"].as>()); + const bfs::path mapPath = RTTRCONFIG.ExpandPath(opts.map); + const std::vector ais = ParseAIOptions(opts.ais); GlobalGameSettings ggs; - const auto objective = options["objective"].as(); - if(objective == "domination") + if(opts.objective == "domination") ggs.objective = GameObjective::TotalDomination; - else if(objective == "conquer") + else if(opts.objective == "conquer") ggs.objective = GameObjective::Conquer3_4; else { - bnw::cerr << "unknown objective: " << objective << std::endl; + bnw::cerr << "unknown objective: " << opts.objective << std::endl; return 1; } - const auto wares = options["wares"].as(); - if(wares == "vlow") + if(opts.wares == "vlow") ggs.startWares = StartWares::VLow; - else if(wares == "low") + else if(opts.wares == "low") ggs.startWares = StartWares::Low; - else if(wares == "normal") + else if(opts.wares == "normal") ggs.startWares = StartWares::Normal; - else if(wares == "alot") + else if(opts.wares == "alot") ggs.startWares = StartWares::ALot; else { - bnw::cerr << "Unknown wares value: " << wares << std::endl; + bnw::cerr << "Unknown wares value: " << opts.wares << std::endl; return 1; } - if(settings_path) + if(opts.settings_path) { - loadAddonsFromIni(ggs, RTTRCONFIG.ExpandPath(*settings_path)); + loadAddonsFromIni(ggs, RTTRCONFIG.ExpandPath(*opts.settings_path)); - bnw::cout << "settings: " << RTTRCONFIG.ExpandPath(*settings_path) << std::endl; + bnw::cout << "settings: " << RTTRCONFIG.ExpandPath(*opts.settings_path) << std::endl; bnw::cout << "addon selections (non-default only):" << std::endl; for(unsigned i = 0; i < ggs.getNumAddons(); ++i) { @@ -224,9 +230,9 @@ int main(int argc, char** argv) // Team assignment, e.g. "0,1;2,3". Player index -> Team (Team1, Team2, ...). std::vector teams; - if(options.count("teams")) + if(opts.teams) { - std::stringstream groups(options["teams"].as()); + std::stringstream groups(*opts.teams); std::string group; unsigned teamIdx = 0; while(std::getline(groups, group, ';')) @@ -247,14 +253,15 @@ int main(int argc, char** argv) } } - HeadlessGame game(ggs, mapPath, ais, lua_path ? RTTRCONFIG.ExpandPath(*lua_path) : bfs::path{}, teams); - if(replay_path) - game.RecordReplay(RTTRCONFIG.ExpandPath(*replay_path), random_init); + HeadlessGame game(ggs, mapPath, ais, opts.lua_path ? RTTRCONFIG.ExpandPath(*opts.lua_path) : bfs::path{}, + teams); + if(opts.replay_path) + game.RecordReplay(RTTRCONFIG.ExpandPath(*opts.replay_path), opts.random_init); - game.Run(options["maxGF"].as()); + game.Run(opts.maxGF); game.Close(); - if(savegame_path) - game.SaveGame(RTTRCONFIG.ExpandPath(*savegame_path)); + if(opts.savegame_path) + game.SaveGame(RTTRCONFIG.ExpandPath(*opts.savegame_path)); } catch(const std::exception& e) { bnw::cerr << e.what() << std::endl; From 0262145ea341dd13f5a1868a804c10d9f290b753 Mon Sep 17 00:00:00 2001 From: Alexander Grund Date: Sun, 4 Oct 2026 12:59:29 +0200 Subject: [PATCH 5/6] Add missing overload to get addon by ID --- libs/s25main/GlobalGameSettings.cpp | 32 +++++++++++++++++++++-------- libs/s25main/GlobalGameSettings.h | 6 ++++-- 2 files changed, 28 insertions(+), 10 deletions(-) diff --git a/libs/s25main/GlobalGameSettings.cpp b/libs/s25main/GlobalGameSettings.cpp index c8a686abb4..743d25f5b9 100644 --- a/libs/s25main/GlobalGameSettings.cpp +++ b/libs/s25main/GlobalGameSettings.cpp @@ -141,13 +141,29 @@ const Addon* GlobalGameSettings::getAddon(unsigned idx) const return addons[idx].addon.get(); } -GlobalGameSettings::AddonWithState* GlobalGameSettings::getAddon(AddonId id) +const Addon* GlobalGameSettings::getAddon(AddonId idx, unsigned& status) const { - auto it = helpers::find_if(addons, [id](const AddonWithState& cur) { return cur.addon->getId() == id; }); - return it != addons.end() ? &*it : nullptr; + const AddonWithState* addon = findAddon(idx); + if(addon) + { + status = addon->status; + return addon->addon.get(); + } + return nullptr; +} + +const Addon* GlobalGameSettings::getAddon(AddonId idx) const +{ + unsigned status; + return getAddon(idx, status); +} + +GlobalGameSettings::AddonWithState* GlobalGameSettings::findAddon(AddonId id) +{ + return const_cast(static_cast(*this).findAddon(id)); } -const GlobalGameSettings::AddonWithState* GlobalGameSettings::getAddon(AddonId id) const +const GlobalGameSettings::AddonWithState* GlobalGameSettings::findAddon(AddonId id) const { auto it = helpers::find_if(addons, [id](const AddonWithState& cur) { return cur.addon->getId() == id; }); return it != addons.end() ? &*it : nullptr; @@ -155,13 +171,13 @@ const GlobalGameSettings::AddonWithState* GlobalGameSettings::getAddon(AddonId i bool GlobalGameSettings::isEnabled(AddonId id) const { - const auto* addon = getAddon(id); + const auto* addon = findAddon(id); return addon && addon->status != addon->addon->getDefaultStatus(); } unsigned GlobalGameSettings::getSelection(AddonId id) const { - const auto* addon = getAddon(id); + const auto* addon = findAddon(id); return addon ? addon->status : 0; } @@ -254,9 +270,9 @@ void GlobalGameSettings::Deserialize(Serializer& ser) void GlobalGameSettings::setSelection(AddonId id, unsigned selection) { - auto* addon = getAddon(id); + auto* addon = findAddon(id); if(!addon) - LOG.write(_("Addon %1$#x not found!\n"), LogTarget::FileAndStderr) % static_cast(id); + LOG.write(_("Addon %1$#x not found!\n"), LogTarget::FileAndStderr) % rttr::enum_cast(id); else addon->status = selection; } diff --git a/libs/s25main/GlobalGameSettings.h b/libs/s25main/GlobalGameSettings.h index f37d52ab12..d4be2e1c36 100644 --- a/libs/s25main/GlobalGameSettings.h +++ b/libs/s25main/GlobalGameSettings.h @@ -38,6 +38,8 @@ class GlobalGameSettings unsigned getNumAddons() const { return addons.size(); } const Addon* getAddon(unsigned idx, unsigned& status) const; const Addon* getAddon(unsigned idx) const; + const Addon* getAddon(AddonId id, unsigned& status) const; + const Addon* getAddon(AddonId id) const; void registerAllAddons(); @@ -69,8 +71,8 @@ class GlobalGameSettings }; void registerAddon(std::unique_ptr addon); - const AddonWithState* getAddon(AddonId id) const; - AddonWithState* getAddon(AddonId id); + AddonWithState* findAddon(AddonId id); + const AddonWithState* findAddon(AddonId id) const; std::vector addons; }; From f18ec7e8cb7f0b0e7ab326da9a1761565360f625 Mon Sep 17 00:00:00 2001 From: Alexander Grund Date: Sun, 4 Oct 2026 14:32:52 +0200 Subject: [PATCH 6/6] Fix clang-tidy warning --- extras/ai-battle/main.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extras/ai-battle/main.cpp b/extras/ai-battle/main.cpp index c0e08e5ac8..e23e38a9ca 100644 --- a/extras/ai-battle/main.cpp +++ b/extras/ai-battle/main.cpp @@ -244,7 +244,7 @@ int main(int argc, char** argv) { if(idx.empty()) continue; - const unsigned p = static_cast(std::stoul(idx)); + const auto p = static_cast(std::stoul(idx)); if(p >= teams.size()) teams.resize(p + 1, Team::None); teams[p] = team;