-
Notifications
You must be signed in to change notification settings - Fork 100
ai-battle: support team assignment (--teams) and establish team start-pacts #1957
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from 10 commits
06ab684
0b91823
9c00cab
8e40140
490b70f
28b52fd
96c5682
02cc027
94ae86c
2841770
42ba31a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,4 +1,4 @@ | ||
| // Copyright (C) 2005 - 2024 Settlers Freaks (sf-team at siedler25.org) | ||
| // Copyright (C) 2005 - 2026 Settlers Freaks (sf-team at siedler25.org) | ||
| // | ||
| // SPDX-License-Identifier: GPL-2.0-or-later | ||
|
|
||
|
|
@@ -7,16 +7,25 @@ | |
| #include "QuickStartGame.h" | ||
| #include "RTTR_Version.h" | ||
| #include "RttrConfig.h" | ||
| #include "addons/Addon.h" | ||
| #include "addons/AddonBool.h" | ||
| #include "addons/AddonList.h" | ||
| #include "addons/const_addons.h" | ||
| #include "ai/random.h" | ||
| #include "files.h" | ||
| #include "gameTypes/TeamTypes.h" | ||
| #include "random/Random.h" | ||
| #include "s25util/StringConversion.h" | ||
| #include "s25util/System.h" | ||
|
|
||
| #include <boost/filesystem.hpp> | ||
| #include <boost/nowide/args.hpp> | ||
| #include <boost/nowide/filesystem.hpp> | ||
| #include <boost/nowide/iostream.hpp> | ||
| #include <boost/program_options.hpp> | ||
| #include <boost/property_tree/ini_parser.hpp> | ||
| #include <iomanip> | ||
| #include <sstream> | ||
| #if BOOST_VERSION >= 109000 | ||
| # include <optional> | ||
| using std::optional; | ||
|
|
@@ -29,13 +38,47 @@ namespace bnw = boost::nowide; | |
| namespace bfs = boost::filesystem; | ||
| namespace po = boost::program_options; | ||
|
|
||
| static void loadAddonsFromIni(GlobalGameSettings& ggs, const bfs::path& iniPath) | ||
| { | ||
| if(!bfs::exists(iniPath)) | ||
| throw std::runtime_error("Settings file not found: " + iniPath.string()); | ||
|
|
||
| boost::property_tree::ptree tree; | ||
| boost::property_tree::read_ini(iniPath.string(), tree); | ||
|
|
||
| const auto addons = tree.get_child_optional("addons"); | ||
| if(!addons) | ||
| { | ||
| bnw::cout << "Note: no [addons] section in " << iniPath << ", using defaults.\n"; | ||
| return; | ||
| } | ||
|
|
||
| unsigned loaded = 0; | ||
| for(const auto& entry : *addons) | ||
| { | ||
| try | ||
| { | ||
| const auto id = static_cast<AddonId>(s25util::fromStringClassic<unsigned>(entry.first)); | ||
| const auto v = entry.second.get_value<unsigned>(); | ||
| ggs.setSelection(id, v); | ||
| ++loaded; | ||
| } catch(const std::exception&) | ||
| { | ||
| // Unknown or invalid entry - skip silently | ||
| } | ||
| } | ||
| bnw::cout << "Loaded " << loaded << " addon settings from " << iniPath << '\n'; | ||
| } | ||
|
|
||
| int main(int argc, char** argv) | ||
| { | ||
| bnw::nowide_filesystem(); | ||
| bnw::args _(argc, argv); | ||
|
|
||
| optional<std::string> replay_path; | ||
| optional<std::string> savegame_path; | ||
| optional<std::string> lua_path; | ||
| optional<std::string> settings_path; | ||
| unsigned random_init = static_cast<unsigned>(std::chrono::high_resolution_clock::now().time_since_epoch().count()); | ||
| unsigned random_ai_init = random_init; | ||
|
|
||
|
|
@@ -44,20 +87,31 @@ int main(int argc, char** argv) | |
| desc.add_options() | ||
| ("help,h", "Show help") | ||
| ("map,m", po::value<std::string>()->required(),"Map to load") | ||
| ("ai", po::value<std::vector<std::string>>()->required(),"AI player(s) to add") | ||
| ("objective", po::value<std::string>()->default_value("domination"),"domination(default)|conquer") | ||
| ("ai", po::value<std::vector<std::string>>()->required(),"AI player(s) to add (aijh | dummy)") | ||
| ("teams", po::value<std::string>(),"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<std::string>()->default_value("domination"),"domination(default) | conquer") | ||
| ("wares", po::value<std::string>()->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<unsigned>()->default_value(std::numeric_limits<unsigned>::max()),"Maximum number of game frames to run (optional)") | ||
| ("version", "Show version information and exit") | ||
| ; | ||
| // clang-format on | ||
|
|
||
| const auto printHelp = [&](std::ostream& os) { | ||
| os << desc | ||
| << "\nNote: path arguments support the <RTTR_USERDATA> placeholder " | ||
| "(game data folder: SAVES, REPLAYS, MAPS, PRESETS)." | ||
| << std::endl; | ||
| }; | ||
|
|
||
| if(argc == 1) | ||
| { | ||
| bnw::cerr << desc << std::endl; | ||
| printHelp(bnw::cerr); | ||
| return 1; | ||
| } | ||
|
|
||
|
|
@@ -68,7 +122,7 @@ int main(int argc, char** argv) | |
|
|
||
| if(options.count("help")) | ||
| { | ||
| bnw::cout << desc << std::endl; | ||
| printHelp(bnw::cout); | ||
| return 0; | ||
| } | ||
| if(options.count("version")) | ||
|
|
@@ -83,7 +137,7 @@ int main(int argc, char** argv) | |
| } catch(const std::exception& e) | ||
| { | ||
| bnw::cerr << "Error: " << e.what() << std::endl; | ||
| bnw::cerr << desc << std::endl; | ||
| printHelp(bnw::cerr); | ||
| return 1; | ||
| } | ||
|
|
||
|
|
@@ -116,15 +170,79 @@ int main(int argc, char** argv) | |
| return 1; | ||
| } | ||
|
|
||
| ggs.objective = GameObjective::TotalDomination; | ||
| HeadlessGame game(ggs, mapPath, ais); | ||
| const auto wares = options["wares"].as<std::string>(); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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"?
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. |
||
| if(wares == "vlow") | ||
| ggs.startWares = StartWares::VLow; | ||
| else if(wares == "low") | ||
| ggs.startWares = StartWares::Low; | ||
| else if(wares == "normal") | ||
| ggs.startWares = StartWares::Normal; | ||
| else if(wares == "alot") | ||
| ggs.startWares = StartWares::ALot; | ||
| else | ||
| { | ||
| bnw::cerr << "Unknown wares value: " << wares << std::endl; | ||
| return 1; | ||
| } | ||
|
|
||
| if(settings_path) | ||
| { | ||
| loadAddonsFromIni(ggs, RTTRCONFIG.ExpandPath(*settings_path)); | ||
|
|
||
| bnw::cout << "settings: " << RTTRCONFIG.ExpandPath(*settings_path) << std::endl; | ||
| bnw::cout << "addon selections (non-default only):" << std::endl; | ||
| for(unsigned i = 0; i < ggs.getNumAddons(); ++i) | ||
| { | ||
| unsigned status = 0; | ||
| const Addon* addon = ggs.getAddon(i, status); | ||
| if(addon && status != addon->getDefaultStatus()) | ||
| { | ||
| bnw::cout << " [0x" << std::hex << std::setw(8) << std::setfill('0') | ||
| << static_cast<unsigned>(addon->getId()) << std::dec << "] " << addon->getName() << " = "; | ||
| if(const auto* listAddon = dynamic_cast<const AddonList*>(addon)) | ||
| bnw::cout << listAddon->getOptionName(status); | ||
| else if(dynamic_cast<const AddonBool*>(addon)) | ||
| bnw::cout << (status ? "True" : "False"); | ||
| else | ||
| bnw::cout << status; | ||
| bnw::cout << std::endl; | ||
| } | ||
| } | ||
| } | ||
|
|
||
| // Team assignment, e.g. "0,1;2,3". Player index -> Team (Team1, Team2, ...). | ||
| std::vector<Team> teams; | ||
| if(options.count("teams")) | ||
| { | ||
| std::stringstream groups(options["teams"].as<std::string>()); | ||
| std::string group; | ||
| unsigned teamIdx = 0; | ||
| while(std::getline(groups, group, ';')) | ||
| { | ||
| const Team team = static_cast<Team>(static_cast<uint8_t>(Team::Team1) + teamIdx); | ||
| std::stringstream members(group); | ||
| std::string idx; | ||
| while(std::getline(members, idx, ',')) | ||
| { | ||
| if(idx.empty()) | ||
| continue; | ||
| const unsigned p = static_cast<unsigned>(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(*replay_path, random_init); | ||
| game.RecordReplay(RTTRCONFIG.ExpandPath(*replay_path), random_init); | ||
|
|
||
| game.Run(options["maxGF"].as<unsigned>()); | ||
| game.Close(); | ||
| if(savegame_path) | ||
| game.SaveGame(*savegame_path); | ||
| game.SaveGame(RTTRCONFIG.ExpandPath(*savegame_path)); | ||
| } catch(const std::exception& e) | ||
| { | ||
| bnw::cerr << e.what() << std::endl; | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.