Skip to content

Commit 3bd9f02

Browse files
authored
Merge pull request #1949 from MichalLabuda/fix-and-extend-ai-battle
Fix and extend AI-Battle
2 parents f872032 + 51de91b commit 3bd9f02

7 files changed

Lines changed: 145 additions & 15 deletions

File tree

extras/ai-battle/HeadlessGame.cpp

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright (C) 2005 - 2024 Settlers Freaks (sf-team at siedler25.org)
1+
// Copyright (C) 2005 - 2026 Settlers Freaks (sf-team at siedler25.org)
22
//
33
// SPDX-License-Identifier: GPL-2.0-or-later
44

@@ -13,6 +13,7 @@
1313
#include "world/MapLoader.h"
1414
#include "gameTypes/MapInfo.h"
1515
#include "gameData/GameConsts.h"
16+
#include "s25util/colors.h"
1617
#include <boost/nowide/iostream.hpp>
1718
#include <chrono>
1819
#include <cstdio>
@@ -27,6 +28,7 @@ std::string HumanReadableNumber(unsigned num);
2728

2829
namespace bfs = boost::filesystem;
2930
namespace bnw = boost::nowide;
31+
3032
using bfs::canonical;
3133

3234
#ifdef WIN32
@@ -41,13 +43,24 @@ void printConsole(const char* fmt, ...) __attribute__((format(printf, 1, 2)));
4143
void printConsole(const char* fmt, ...);
4244
#endif
4345

44-
HeadlessGame::HeadlessGame(const GlobalGameSettings& ggs, const bfs::path& map, const std::vector<AI::Info>& ais)
46+
HeadlessGame::HeadlessGame(const GlobalGameSettings& ggs, const bfs::path& map, const std::vector<AI::Info>& ais,
47+
const bfs::path& luaPath)
4548
: map_(map), game_(ggs, std::make_unique<EventManager>(0), GeneratePlayerInfo(ais)), world_(game_.world_),
4649
em_(*static_cast<EventManager*>(game_.em_.get()))
4750
{
4851
MapLoader loader(world_);
4952
if(!loader.Load(map))
5053
throw std::runtime_error("Could not load " + map.string());
54+
MapLoader::SetupResources(world_);
55+
56+
if(!luaPath.empty())
57+
{
58+
if(!loader.LoadLuaScript(game_, localState_, luaPath))
59+
throw std::runtime_error("Failed to load Lua script: " + luaPath.string());
60+
world_.GetLua().setSuppressStdout(true);
61+
luaPath_ = luaPath;
62+
bnw::cout << "Lua script loaded: " << luaPath << '\n';
63+
}
5164

5265
players_.clear();
5366
for(unsigned playerId = 0; playerId < world_.GetNumPlayers(); ++playerId)
@@ -138,6 +151,12 @@ void HeadlessGame::RecordReplay(const bfs::path& path, unsigned random_init)
138151
mapInfo.mapData.CompressFromFile(mapInfo.filepath, &mapInfo.mapChecksum);
139152
mapInfo.type = MapType::OldMap;
140153

154+
if(!luaPath_.empty() && bfs::exists(luaPath_))
155+
{
156+
mapInfo.luaFilepath = luaPath_;
157+
mapInfo.luaData.CompressFromFile(luaPath_, &mapInfo.luaChecksum);
158+
}
159+
141160
for(unsigned playerId = 0; playerId < world_.GetNumPlayers(); ++playerId)
142161
replay_.AddPlayer(world_.GetPlayer(playerId));
143162
replay_.ggs = game_.ggs_;
@@ -232,6 +251,7 @@ std::vector<PlayerInfo> GeneratePlayerInfo(const std::vector<AI::Info>& ais)
232251
}
233252
pi.nation = Nation::Romans;
234253
pi.team = Team::None;
254+
pi.color = PLAYER_COLORS[ret.size() % PLAYER_COLORS.size()];
235255
ret.push_back(pi);
236256
}
237257
return ret;

extras/ai-battle/HeadlessGame.h

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
1-
// Copyright (C) 2005 - 2024 Settlers Freaks (sf-team at siedler25.org)
1+
// Copyright (C) 2005 - 2026 Settlers Freaks (sf-team at siedler25.org)
22
//
33
// SPDX-License-Identifier: GPL-2.0-or-later
44

55
#pragma once
66

77
#include "Game.h"
8+
#include "ILocalGameState.h"
89
#include "Replay.h"
910
#include "ai/AIPlayer.h"
1011
#include "gameTypes/AIInfo.h"
@@ -21,7 +22,8 @@ class EventManager;
2122
class HeadlessGame
2223
{
2324
public:
24-
HeadlessGame(const GlobalGameSettings& ggs, const boost::filesystem::path& map, const std::vector<AI::Info>& ais);
25+
HeadlessGame(const GlobalGameSettings& ggs, const boost::filesystem::path& map, const std::vector<AI::Info>& ais,
26+
const boost::filesystem::path& luaPath = {});
2527
~HeadlessGame();
2628

2729
void Run(unsigned maxGF = std::numeric_limits<unsigned>::max());
@@ -33,6 +35,15 @@ class HeadlessGame
3335
private:
3436
void PrintState();
3537

38+
struct LocalState : ILocalGameState
39+
{
40+
unsigned GetPlayerId() const override { return 0; }
41+
bool IsHost() const override { return true; }
42+
std::string FormatGFTime(unsigned) const override { return ""; }
43+
void SystemChat(const std::string&) override {}
44+
};
45+
46+
LocalState localState_;
3647
boost::filesystem::path map_;
3748
Game game_;
3849
GameWorld& world_;
@@ -41,6 +52,7 @@ class HeadlessGame
4152

4253
Replay replay_;
4354
boost::filesystem::path replayPath_;
55+
boost::filesystem::path luaPath_;
4456

4557
unsigned lastReportGf_ = 0;
4658
std::chrono::steady_clock::time_point gameStartTime_;

extras/ai-battle/main.cpp

Lines changed: 100 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright (C) 2005 - 2024 Settlers Freaks (sf-team at siedler25.org)
1+
// Copyright (C) 2005 - 2026 Settlers Freaks (sf-team at siedler25.org)
22
//
33
// SPDX-License-Identifier: GPL-2.0-or-later
44

@@ -7,16 +7,23 @@
77
#include "QuickStartGame.h"
88
#include "RTTR_Version.h"
99
#include "RttrConfig.h"
10+
#include "addons/Addon.h"
11+
#include "addons/AddonBool.h"
12+
#include "addons/AddonList.h"
13+
#include "addons/const_addons.h"
1014
#include "ai/random.h"
1115
#include "files.h"
1216
#include "random/Random.h"
17+
#include "s25util/StringConversion.h"
1318
#include "s25util/System.h"
1419

1520
#include <boost/filesystem.hpp>
1621
#include <boost/nowide/args.hpp>
1722
#include <boost/nowide/filesystem.hpp>
1823
#include <boost/nowide/iostream.hpp>
1924
#include <boost/program_options.hpp>
25+
#include <boost/property_tree/ini_parser.hpp>
26+
#include <iomanip>
2027
#if BOOST_VERSION >= 109000
2128
# include <optional>
2229
using std::optional;
@@ -29,13 +36,47 @@ namespace bnw = boost::nowide;
2936
namespace bfs = boost::filesystem;
3037
namespace po = boost::program_options;
3138

39+
static void loadAddonsFromIni(GlobalGameSettings& ggs, const bfs::path& iniPath)
40+
{
41+
if(!bfs::exists(iniPath))
42+
throw std::runtime_error("Settings file not found: " + iniPath.string());
43+
44+
boost::property_tree::ptree tree;
45+
boost::property_tree::read_ini(iniPath.string(), tree);
46+
47+
const auto addons = tree.get_child_optional("addons");
48+
if(!addons)
49+
{
50+
bnw::cout << "Note: no [addons] section in " << iniPath << ", using defaults.\n";
51+
return;
52+
}
53+
54+
unsigned loaded = 0;
55+
for(const auto& entry : *addons)
56+
{
57+
try
58+
{
59+
const auto id = static_cast<AddonId>(s25util::fromStringClassic<unsigned>(entry.first));
60+
const auto v = entry.second.get_value<unsigned>();
61+
ggs.setSelection(id, v);
62+
++loaded;
63+
} catch(const std::exception&)
64+
{
65+
// Unknown or invalid entry - skip silently
66+
}
67+
}
68+
bnw::cout << "Loaded " << loaded << " addon settings from " << iniPath << '\n';
69+
}
70+
3271
int main(int argc, char** argv)
3372
{
3473
bnw::nowide_filesystem();
3574
bnw::args _(argc, argv);
3675

3776
optional<std::string> replay_path;
3877
optional<std::string> savegame_path;
78+
optional<std::string> lua_path;
79+
optional<std::string> settings_path;
3980
unsigned random_init = static_cast<unsigned>(std::chrono::high_resolution_clock::now().time_since_epoch().count());
4081
unsigned random_ai_init = random_init;
4182

@@ -44,20 +85,30 @@ int main(int argc, char** argv)
4485
desc.add_options()
4586
("help,h", "Show help")
4687
("map,m", po::value<std::string>()->required(),"Map to load")
47-
("ai", po::value<std::vector<std::string>>()->required(),"AI player(s) to add")
48-
("objective", po::value<std::string>()->default_value("domination"),"domination(default)|conquer")
88+
("ai", po::value<std::vector<std::string>>()->required(),"AI player(s) to add (aijh | dummy)")
89+
("objective", po::value<std::string>()->default_value("domination"),"domination(default) | conquer")
90+
("wares", po::value<std::string>()->default_value("normal"),"Starting wares: vlow | low | normal (default) | alot")
91+
("settings", po::value(&settings_path),"INI file with an [addons] section to configure addon settings (optional)")
4992
("replay", po::value(&replay_path),"Filename to write replay to (optional)")
5093
("save", po::value(&savegame_path),"Filename to write savegame to (optional)")
94+
("lua", po::value(&lua_path),"Lua script to execute during the game (optional)")
5195
("random_init", po::value(&random_init),"Seed value for the random number generator (optional)")
5296
("random_ai_init", po::value(&random_ai_init),"Seed value for the AI random number generator (optional)")
5397
("maxGF", po::value<unsigned>()->default_value(std::numeric_limits<unsigned>::max()),"Maximum number of game frames to run (optional)")
5498
("version", "Show version information and exit")
5599
;
56100
// clang-format on
57101

102+
const auto printHelp = [&](std::ostream& os) {
103+
os << desc
104+
<< "\nNote: path arguments support the <RTTR_USERDATA> placeholder "
105+
"(game data folder: SAVES, REPLAYS, MAPS, PRESETS)."
106+
<< std::endl;
107+
};
108+
58109
if(argc == 1)
59110
{
60-
bnw::cerr << desc << std::endl;
111+
printHelp(bnw::cerr);
61112
return 1;
62113
}
63114

@@ -68,7 +119,7 @@ int main(int argc, char** argv)
68119

69120
if(options.count("help"))
70121
{
71-
bnw::cout << desc << std::endl;
122+
printHelp(bnw::cout);
72123
return 0;
73124
}
74125
if(options.count("version"))
@@ -83,7 +134,7 @@ int main(int argc, char** argv)
83134
} catch(const std::exception& e)
84135
{
85136
bnw::cerr << "Error: " << e.what() << std::endl;
86-
bnw::cerr << desc << std::endl;
137+
printHelp(bnw::cerr);
87138
return 1;
88139
}
89140

@@ -116,15 +167,54 @@ int main(int argc, char** argv)
116167
return 1;
117168
}
118169

119-
ggs.objective = GameObjective::TotalDomination;
120-
HeadlessGame game(ggs, mapPath, ais);
170+
const auto wares = options["wares"].as<std::string>();
171+
if(wares == "vlow")
172+
ggs.startWares = StartWares::VLow;
173+
else if(wares == "low")
174+
ggs.startWares = StartWares::Low;
175+
else if(wares == "normal")
176+
ggs.startWares = StartWares::Normal;
177+
else if(wares == "alot")
178+
ggs.startWares = StartWares::ALot;
179+
else
180+
{
181+
bnw::cerr << "Unknown wares value: " << wares << std::endl;
182+
return 1;
183+
}
184+
185+
if(settings_path)
186+
{
187+
loadAddonsFromIni(ggs, RTTRCONFIG.ExpandPath(*settings_path));
188+
189+
bnw::cout << "settings: " << RTTRCONFIG.ExpandPath(*settings_path) << std::endl;
190+
bnw::cout << "addon selections (non-default only):" << std::endl;
191+
for(unsigned i = 0; i < ggs.getNumAddons(); ++i)
192+
{
193+
unsigned status = 0;
194+
const Addon* addon = ggs.getAddon(i, status);
195+
if(addon && status != addon->getDefaultStatus())
196+
{
197+
bnw::cout << " [0x" << std::hex << std::setw(8) << std::setfill('0')
198+
<< static_cast<unsigned>(addon->getId()) << std::dec << "] " << addon->getName() << " = ";
199+
if(const auto* listAddon = dynamic_cast<const AddonList*>(addon))
200+
bnw::cout << listAddon->getOptionName(status);
201+
else if(dynamic_cast<const AddonBool*>(addon))
202+
bnw::cout << (status ? "True" : "False");
203+
else
204+
bnw::cout << status;
205+
bnw::cout << std::endl;
206+
}
207+
}
208+
}
209+
210+
HeadlessGame game(ggs, mapPath, ais, lua_path ? RTTRCONFIG.ExpandPath(*lua_path) : bfs::path{});
121211
if(replay_path)
122-
game.RecordReplay(*replay_path, random_init);
212+
game.RecordReplay(RTTRCONFIG.ExpandPath(*replay_path), random_init);
123213

124214
game.Run(options["maxGF"].as<unsigned>());
125215
game.Close();
126216
if(savegame_path)
127-
game.SaveGame(*savegame_path);
217+
game.SaveGame(RTTRCONFIG.ExpandPath(*savegame_path));
128218
} catch(const std::exception& e)
129219
{
130220
bnw::cerr << e.what() << std::endl;

libs/libGamedata/lua/LuaInterfaceBase.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -168,5 +168,5 @@ bool LuaInterfaceBase::validateUTF8(const std::string& scriptTxt)
168168

169169
void LuaInterfaceBase::log(const std::string& msg)
170170
{
171-
logger_.write("%s\n") % msg;
171+
logger_.write("%s\n", suppressStdout_ ? LogTarget::File : LogTarget::FileAndStdout) % msg;
172172
}

libs/libGamedata/lua/LuaInterfaceBase.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ class LuaInterfaceBase
3535
/// Disable or re-enable throwing an exception on error.
3636
/// Note: If error throwing is disabled you have to use HasErrorOccurred to detect an error situation
3737
void setThrowOnError(bool doThrow);
38+
void setSuppressStdout(bool suppress) { suppressStdout_ = suppress; }
3839
bool hasErrorOccurred() const { return errorOccured_; }
3940
void clearErrorOccured() { errorOccured_ = false; }
4041

@@ -60,6 +61,7 @@ class LuaInterfaceBase
6061

6162
private:
6263
Log& logger_;
64+
bool suppressStdout_ = false;
6365
/// Sticky flag to signal an occurred error during execution of lua code
6466
bool errorOccured_;
6567
std::map<std::string, std::string> translations_;

libs/s25main/addons/AddonList.cpp

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,11 @@ unsigned AddonList::getNumOptions() const
2626
return options.size();
2727
}
2828

29+
const std::string& AddonList::getOptionName(unsigned status) const
30+
{
31+
return options.at(status);
32+
}
33+
2934
AddonList::Gui::Gui(const AddonList& addon, Window& window, bool readonly) : AddonGui(addon, window, readonly)
3035
{
3136
DrawPoint cbPos(430, 0);

libs/s25main/addons/AddonList.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ class AddonList : public Addon
2525
std::vector<std::string> options, unsigned defaultStatus = 0);
2626

2727
unsigned getNumOptions() const override;
28+
const std::string& getOptionName(unsigned status) const;
2829

2930
std::unique_ptr<AddonGui> createGui(Window& window, bool readonly) const override;
3031

0 commit comments

Comments
 (0)