Skip to content

Commit

Permalink
Merge pull request #1565 from wichern/quickstart-ai-battle
Browse files Browse the repository at this point in the history
Add `--ai` option to cmdline to quickstart an AI battle
  • Loading branch information
Flamefire committed Mar 30, 2023
2 parents bd35e89 + 34e5838 commit 2982d66
Show file tree
Hide file tree
Showing 10 changed files with 172 additions and 13 deletions.
12 changes: 10 additions & 2 deletions libs/s25client/s25client.cpp
Expand Up @@ -473,8 +473,15 @@ int RunProgram(po::variables_map& options)
if(!InitGame(gameManager))
return 2;

if(options.count("map") && !QuickStartGame(options["map"].as<std::string>()))
return 1;
if(options.count("map"))
{
std::vector<std::string> aiPlayers;
if(options.count("ai"))
aiPlayers = options["ai"].as<std::vector<std::string>>();

if(!QuickStartGame(options["map"].as<std::string>(), aiPlayers))
return 1;
}

// Hauptschleife

Expand Down Expand Up @@ -522,6 +529,7 @@ int main(int argc, char** argv)
desc.add_options()
("help,h", "Show help")
("map,m", po::value<std::string>(),"Map to load")
("ai", po::value<std::vector<std::string>>(),"AI player(s) to add")
("version", "Show version information and exit")
("convert-sounds", "Convert sounds and exit")
;
Expand Down
10 changes: 9 additions & 1 deletion libs/s25main/JoinPlayerInfo.cpp
Expand Up @@ -41,7 +41,13 @@ void JoinPlayerInfo::FixSwappedSaveSlot(JoinPlayerInfo& other)
void JoinPlayerInfo::SetAIName(unsigned playerId)
{
RTTR_Assert(ps == PlayerState::AI);
name = (boost::format((aiInfo.type == AI::Type::Dummy) ? _("Dummy %u") : _("Computer %u")) % playerId).str();
name = MakeAIName(aiInfo, playerId);
}

std::string JoinPlayerInfo::MakeAIName(const AI::Info& aiInfo, unsigned playerId)
{
std::string name =
(boost::format((aiInfo.type == AI::Type::Dummy) ? _("Dummy %u") : _("Computer %u")) % playerId).str();
name += _(" (AI)");

if(aiInfo.type == AI::Type::Default)
Expand All @@ -53,4 +59,6 @@ void JoinPlayerInfo::SetAIName(unsigned playerId)
case AI::Level::Hard: name += _(" (hard)"); break;
}
}

return name;
}
2 changes: 2 additions & 0 deletions libs/s25main/JoinPlayerInfo.h
Expand Up @@ -23,4 +23,6 @@ struct JoinPlayerInfo : public PlayerInfo
void SetAIName(unsigned playerId);
// Recovers fixed data in savegames after player slots are swapped
void FixSwappedSaveSlot(JoinPlayerInfo& other);

static std::string MakeAIName(const AI::Info& aiInfo, unsigned playerId);
};
36 changes: 34 additions & 2 deletions libs/s25main/QuickStartGame.cpp
Expand Up @@ -32,7 +32,26 @@ class SwitchOnStart : public ClientInterface
}
};

bool QuickStartGame(const boost::filesystem::path& mapOrReplayPath, bool singlePlayer)
std::vector<AI::Info> ParseAIOptions(const std::vector<std::string>& aiOptions)
{
std::vector<AI::Info> aiInfos;

for(const std::string& aiOption : aiOptions)
{
const auto aiOption_lower = s25util::toLower(aiOption);
AI::Type type = AI::Type::Dummy;
if(aiOption_lower == "aijh")
type = AI::Type::Default;
else if(aiOption_lower != "dummy")
throw std::invalid_argument("Invalid AI player name: " + aiOption_lower);

aiInfos.push_back({type, AI::Level::Hard});
}

return aiInfos;
}

bool QuickStartGame(const boost::filesystem::path& mapOrReplayPath, const std::vector<std::string>& ais)
{
if(!exists(mapOrReplayPath))
{
Expand All @@ -48,7 +67,19 @@ bool QuickStartGame(const boost::filesystem::path& mapOrReplayPath, bool singleP
if(SETTINGS.sound.musicEnabled)
MUSICPLAYER.Play();

const CreateServerInfo csi(singlePlayer ? ServerType::Local : ServerType::Direct, SETTINGS.server.localPort,
// An AI-battle is a single-player game.
const bool isSinglePlayer = !ais.empty();
std::vector<AI::Info> aiInfos;
try
{
aiInfos = ParseAIOptions(ais);
} catch(const std::invalid_argument& e)
{
LOG.write(e.what());
return false;
}

const CreateServerInfo csi(isSinglePlayer ? ServerType::Local : ServerType::Direct, SETTINGS.server.localPort,
_("Unlimited Play"));

LOG.write(_("Loading game...\n"));
Expand All @@ -59,6 +90,7 @@ bool QuickStartGame(const boost::filesystem::path& mapOrReplayPath, bool singleP
if((extension == ".sav" && GAMECLIENT.HostGame(csi, mapOrReplayPath, MapType::Savegame))
|| ((extension == ".swd" || extension == ".wld") && GAMECLIENT.HostGame(csi, mapOrReplayPath, MapType::OldMap)))
{
GAMECLIENT.SetAIBattlePlayers(std::move(aiInfos));
WINDOWMANAGER.ShowAfterSwitch(std::make_unique<iwConnecting>(csi.type, nullptr));
return true;
} else
Expand Down
9 changes: 7 additions & 2 deletions libs/s25main/QuickStartGame.h
Expand Up @@ -4,8 +4,13 @@

#pragma once

#include "gameTypes/AIInfo.h"
#include <boost/filesystem/path.hpp>
#include <string>
#include <vector>

/// Tries to start a game (map, savegame or replay) and returns whether this was successfull
bool QuickStartGame(const boost::filesystem::path& mapOrReplayPath, bool singlePlayer = false);
/// Parse --ai flags specified in the command line
std::vector<AI::Info> ParseAIOptions(const std::vector<std::string>& aiOptions);

/// Tries to start a game (map, savegame, replay, or ai-battle) and returns whether this was successfull
bool QuickStartGame(const boost::filesystem::path& mapOrReplayPath, const std::vector<std::string>& ais);
2 changes: 1 addition & 1 deletion libs/s25main/desktops/dskGameInterface.cpp
Expand Up @@ -765,7 +765,7 @@ bool dskGameInterface::Msg_KeyDown(const KeyEvent& ke)
const bool allowHumanAI = true;
#endif // !NDEBUG
if(GAMECLIENT.GetState() == ClientState::Game && allowHumanAI && !GAMECLIENT.IsReplayModeOn())
GAMECLIENT.ToggleHumanAIPlayer();
GAMECLIENT.ToggleHumanAIPlayer(AI::Info(AI::Type::Default, AI::Level::Easy));
return true;
}
case KeyType::F11: // Music player (midi files)
Expand Down
21 changes: 19 additions & 2 deletions libs/s25main/desktops/dskGameLobby.cpp
Expand Up @@ -254,10 +254,27 @@ dskGameLobby::dskGameLobby(ServerType serverType, std::shared_ptr<GameLobby> gam
}
}

if(IsSinglePlayer() && !gameLobby_->isSavegame())
if(GAMECLIENT.IsAIBattleModeOn())
{
const auto& aiBattlePlayers = GAMECLIENT.GetAIBattlePlayers();

// Initialize AI battle players
for(unsigned i = 0; i < gameLobby_->getNumPlayers(); i++)
{
if(i < aiBattlePlayers.size())
lobbyHostController->SetPlayerState(i, PlayerState::AI, aiBattlePlayers[i]);
else
lobbyHostController->CloseSlot(i); // Close remaining slots
}

// Set name of host to the corresponding AI for local player
if(localPlayerId_ < aiBattlePlayers.size())
lobbyHostController->SetName(localPlayerId_,
JoinPlayerInfo::MakeAIName(aiBattlePlayers[localPlayerId_], localPlayerId_));
} else if(IsSinglePlayer() && !gameLobby_->isSavegame())
{
// Setze initial auf KI
for(unsigned char i = 0; i < gameLobby_->getNumPlayers(); i++)
for(unsigned i = 0; i < gameLobby_->getNumPlayers(); i++)
{
if(!gameLobby_->getPlayer(i).isHost)
lobbyHostController->SetPlayerState(i, PlayerState::AI, AI::Info(AI::Type::Default, AI::Level::Easy));
Expand Down
15 changes: 13 additions & 2 deletions libs/s25main/network/GameClient.cpp
Expand Up @@ -85,6 +85,8 @@ bool GameClient::Connect(const std::string& server, const std::string& password,
{
Stop();

RTTR_Assert(aiBattlePlayers_.empty());

// Name und Password kopieren
clientconfig.server = server;
clientconfig.password = password;
Expand Down Expand Up @@ -226,6 +228,8 @@ void GameClient::Stop()

state = ClientState::Stopped;
LOG.write("client state changed to stop\n");

aiBattlePlayers_.clear();
}

std::shared_ptr<GameLobby> GameClient::GetGameLobby()
Expand Down Expand Up @@ -352,6 +356,8 @@ void GameClient::GameLoaded()
SendNothingNC(id);
}
}
if(IsAIBattleModeOn())
ToggleHumanAIPlayer(aiBattlePlayers_[GetPlayerId()]);
}
SendNothingNC();
}
Expand Down Expand Up @@ -1535,6 +1541,11 @@ bool GameClient::StartReplay(const boost::filesystem::path& path)
return true;
}

void GameClient::SetAIBattlePlayers(std::vector<AI::Info> aiInfos)
{
aiBattlePlayers_ = std::move(aiInfos);
}

unsigned GameClient::GetGlobalAnimation(const unsigned short max, const unsigned char factor_numerator,
const unsigned char factor_denumerator, const unsigned offset)
{
Expand Down Expand Up @@ -1807,15 +1818,15 @@ unsigned GameClient::GetTournamentModeDuration() const
return 0;
}

void GameClient::ToggleHumanAIPlayer()
void GameClient::ToggleHumanAIPlayer(const AI::Info& aiInfo)
{
RTTR_Assert(!IsReplayModeOn());
auto it = helpers::find_if(game->aiPlayers_,
[id = this->GetPlayerId()](const auto& player) { return player.GetPlayerId() == id; });
if(it != game->aiPlayers_.end())
game->aiPlayers_.erase(it);
else
game->AddAIPlayer(CreateAIPlayer(GetPlayerId(), AI::Info(AI::Type::Default, AI::Level::Easy)));
game->AddAIPlayer(CreateAIPlayer(GetPlayerId(), aiInfo));
}

void GameClient::RequestSwapToPlayer(const unsigned char newId)
Expand Down
13 changes: 12 additions & 1 deletion libs/s25main/network/GameClient.h
Expand Up @@ -11,6 +11,7 @@
#include "ILocalGameState.h"
#include "NetworkPlayer.h"
#include "factories/GameCommandFactory.h"
#include "gameTypes/AIInfo.h"
#include "gameTypes/ChatDestination.h"
#include "gameTypes/MapInfo.h"
#include "gameTypes/Nation.h"
Expand Down Expand Up @@ -129,6 +130,12 @@ class GameClient final :

/// Lädt ein Replay und startet dementsprechend das Spiel
bool StartReplay(const boost::filesystem::path& path);

/// When a non-empty vector is given then an AI battle with the given AIs is started
void SetAIBattlePlayers(std::vector<AI::Info> aiInfos);
const std::vector<AI::Info>& GetAIBattlePlayers() const { return aiBattlePlayers_; }
bool IsAIBattleModeOn() const { return !aiBattlePlayers_.empty(); }

void SetPause(bool pause);
void TogglePause() { SetPause(!framesinfo.isPaused); }
/// Schaltet FoW im Replaymodus ein/aus
Expand Down Expand Up @@ -164,7 +171,8 @@ class GameClient final :
void SystemChat(const std::string& text) override;
void SystemChat(const std::string& text, unsigned char fromPlayerIdx);

void ToggleHumanAIPlayer();
/// Toggle current player to be an AI player of the given type
void ToggleHumanAIPlayer(const AI::Info& aiInfo);

NetworkPlayer& GetMainPlayer() { return mainPlayer; }

Expand Down Expand Up @@ -300,6 +308,9 @@ class GameClient final :

std::unique_ptr<ReplayInfo> replayinfo;
bool replayMode;

/// Configured players for an AI battle.
std::vector<AI::Info> aiBattlePlayers_;
};

///////////////////////////////////////////////////////////////////////////////
Expand Down
65 changes: 65 additions & 0 deletions tests/s25Main/simple/testParseAIOptions.cpp
@@ -0,0 +1,65 @@
// Copyright (C) 2005 - 2021 Settlers Freaks (sf-team at siedler25.org)
//
// SPDX-License-Identifier: GPL-2.0-or-later

#include "QuickStartGame.h"
#include "enum_cast.hpp"
#include <boost/test/unit_test.hpp>

// LCOV_EXCL_START
namespace boost { namespace test_tools { namespace tt_detail {
template<>
struct print_log_value<AI::Type>
{
void operator()(std::ostream& os, const AI::Type type) { os << static_cast<unsigned>(rttr::enum_cast(type)); }
};
}}} // namespace boost::test_tools::tt_detail
// LCOV_EXCL_STOP

BOOST_AUTO_TEST_SUITE(ParseAIOptionsTests)

BOOST_AUTO_TEST_CASE(EmptyOptions)
{
std::vector<std::string> options;
std::vector<AI::Info> aiInfos = ParseAIOptions(options);
BOOST_TEST(aiInfos.empty());
}

BOOST_AUTO_TEST_CASE(ParsingAIJH)
{
std::vector<std::string> options = {"aijh", "AIJH", "AiJh"};
std::vector<AI::Info> aiInfos = ParseAIOptions(options);
BOOST_TEST_REQUIRE(aiInfos.size() == 3u);
BOOST_TEST(aiInfos[0].type == AI::Type::Default);
BOOST_TEST(aiInfos[1].type == AI::Type::Default);
BOOST_TEST(aiInfos[2].type == AI::Type::Default);
}

BOOST_AUTO_TEST_CASE(ParsingDummy)
{
std::vector<std::string> options = {"dummy", "Dummy", "DUMMY"};
std::vector<AI::Info> aiInfos = ParseAIOptions(options);
BOOST_TEST_REQUIRE(aiInfos.size() == 3u);
BOOST_TEST(aiInfos[0].type == AI::Type::Dummy);
BOOST_TEST(aiInfos[1].type == AI::Type::Dummy);
BOOST_TEST(aiInfos[2].type == AI::Type::Dummy);
}

BOOST_AUTO_TEST_CASE(ParsingMultiple)
{
std::vector<std::string> options = {"dummy", "aijh", "dummy", "aijh"};
std::vector<AI::Info> aiInfos = ParseAIOptions(options);
BOOST_TEST_REQUIRE(aiInfos.size() == 4u);
BOOST_TEST(aiInfos[0].type == AI::Type::Dummy);
BOOST_TEST(aiInfos[1].type == AI::Type::Default);
BOOST_TEST(aiInfos[2].type == AI::Type::Dummy);
BOOST_TEST(aiInfos[3].type == AI::Type::Default);
}

BOOST_AUTO_TEST_CASE(ParsingFailed)
{
std::vector<std::string> options = {"invalid"};
BOOST_CHECK_THROW(ParseAIOptions(options), std::invalid_argument);
}

BOOST_AUTO_TEST_SUITE_END()

0 comments on commit 2982d66

Please sign in to comment.