Skip to content

Commit

Permalink
Merge pull request #1652 from wichern/ai_battle
Browse files Browse the repository at this point in the history
Add ai-battle cli
  • Loading branch information
Flamefire committed Jun 26, 2024
2 parents 1d3b15a + ed4fe3f commit 815e692
Show file tree
Hide file tree
Showing 13 changed files with 464 additions and 12 deletions.
8 changes: 5 additions & 3 deletions .github/workflows/unit-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,10 @@ jobs:
# Use C++ 17 standard (default for gcc-11)
# Use system cmake
# Use system boost
- { compiler: gcc-11, os: ubuntu-22.04, type: Debug, cmake: 3.22.1, boost: 1.74.0 }
- { compiler: clang-14, os: ubuntu-22.04, type: Debug, cmake: 3.22.1, boost: 1.74.0, cxx: 17 }
# Also include a (semi-)optimized build to a) shake out issues there an b) run the replay tests
- { compiler: clang-14, os: ubuntu-22.04, type: Debug, cmake: 3.22.1, boost: 1.74.0, cxx: 17 }
- { compiler: gcc-11, os: ubuntu-22.04, type: Debug, cmake: 3.22.1, boost: 1.74.0 }
- { compiler: gcc-11, os: ubuntu-22.04, type: RelWithDebInfo, cmake: 3.22.1, boost: 1.74.0 }
#
# Latest Compilers
# GCC 12 needs boost 1.82 to compile correctly
Expand Down Expand Up @@ -191,7 +193,7 @@ jobs:
run: echo "TRAVIS_BUILD_DIR=$GITHUB_WORKSPACE" >> $GITHUB_ENV

- name: Build
run: tools/ci/travisBuild.sh
run: tools/ci/build.sh

- run: tools/ci/collectCoverageData.sh && external/libutil/tools/ci/uploadCoverageData.sh
if: matrix.coverage && success()
Expand Down
2 changes: 1 addition & 1 deletion appveyor.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ platform:
- x64

environment:
BOOST_ROOT: C:\Libraries\boost_1_77_0
BOOST_ROOT: C:\Libraries\boost_1_83_0
GENERATOR: Visual Studio 16 2019
RTTR_DISABLE_ASSERT_BREAKPOINT: 1

Expand Down
1 change: 1 addition & 0 deletions extras/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ set(CMAKE_VISIBILITY_INLINES_HIDDEN ON)

add_subdirectory(audioDrivers)
add_subdirectory(videoDrivers)
add_subdirectory(ai-battle)
if(RTTR_BUNDLE AND APPLE)
add_subdirectory(macosLauncher)
endif()
Expand Down
11 changes: 11 additions & 0 deletions extras/ai-battle/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# Copyright (C) 2005 - 2024 Settlers Freaks <sf-team at siedler25.org>
#
# SPDX-License-Identifier: GPL-2.0-or-later

add_executable(ai-battle main.cpp HeadlessGame.cpp)
target_link_libraries(ai-battle PRIVATE s25Main Boost::program_options Boost::nowide)

if(WIN32)
include(GatherDll)
gather_dll_copy(ai-battle)
endif()
267 changes: 267 additions & 0 deletions extras/ai-battle/HeadlessGame.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,267 @@
// Copyright (C) 2005 - 2024 Settlers Freaks (sf-team at siedler25.org)
//
// SPDX-License-Identifier: GPL-2.0-or-later

#include "HeadlessGame.h"
#include "EventManager.h"
#include "GlobalGameSettings.h"
#include "PlayerInfo.h"
#include "Savegame.h"
#include "factories/AIFactory.h"
#include "network/PlayerGameCommands.h"
#include "world/GameWorld.h"
#include "world/MapLoader.h"
#include "gameTypes/MapInfo.h"
#include "gameData/GameConsts.h"
#include <boost/nowide/iostream.hpp>
#include <chrono>
#include <cstdio>
#include <sstream>
#ifdef WIN32
# include "Windows.h"
#endif

std::vector<PlayerInfo> GeneratePlayerInfo(const std::vector<AI::Info>& ais);
std::string ToString(const std::chrono::milliseconds& time);
std::string HumanReadableNumber(unsigned num);

namespace bfs = boost::filesystem;
namespace bnw = boost::nowide;
using bfs::canonical;

#ifdef WIN32
HANDLE setupStdOut();
#endif

#if defined(__MINGW32__) && !defined(__clang__)
void printConsole(const char* fmt, ...) __attribute__((format(gnu_printf, 1, 2)));
#elif defined __GNUC__
void printConsole(const char* fmt, ...) __attribute__((format(printf, 1, 2)));
#else
void printConsole(const char* fmt, ...);
#endif

HeadlessGame::HeadlessGame(const GlobalGameSettings& ggs, const bfs::path& map, const std::vector<AI::Info>& ais)
: map_(map), game_(ggs, std::make_unique<EventManager>(0), GeneratePlayerInfo(ais)), world_(game_.world_),
em_(*static_cast<EventManager*>(game_.em_.get()))
{
MapLoader loader(world_);
if(!loader.Load(map))
throw std::runtime_error("Could not load " + map.string());

players_.clear();
for(unsigned playerId = 0; playerId < world_.GetNumPlayers(); ++playerId)
players_.push_back(AIFactory::Create(world_.GetPlayer(playerId).aiInfo, playerId, world_));

world_.InitAfterLoad();
}

HeadlessGame::~HeadlessGame()
{
Close();
}

void HeadlessGame::Run(unsigned maxGF)
{
AsyncChecksum checksum;
gameStartTime_ = std::chrono::steady_clock::now();
auto nextReport = gameStartTime_ + std::chrono::seconds(1);

game_.Start(false);

while(em_.GetCurrentGF() < maxGF && !game_.IsGameFinished())
{
// In the actual game, the network frame intervall is based on ping (highest_ping < NFW-length < 20*gf_length).
bool isnfw = em_.GetCurrentGF() % 20 == 0;

if(isnfw)
{
if(replay_.IsRecording())
checksum = AsyncChecksum::create(game_);
for(unsigned playerId = 0; playerId < world_.GetNumPlayers(); ++playerId)
{
world_.GetPlayer(playerId);
AIPlayer* player = players_[playerId].get();
PlayerGameCommands cmds;
cmds.gcs = player->FetchGameCommands();

if(replay_.IsRecording() && !cmds.gcs.empty())
{
cmds.checksum = checksum;
replay_.AddGameCommand(em_.GetCurrentGF(), playerId, cmds);
}

for(const gc::GameCommandPtr& gc : cmds.gcs)
gc->Execute(world_, player->GetPlayerId());
}
}

for(auto& player : players_)
player->RunGF(em_.GetCurrentGF(), isnfw);

game_.RunGF();

if(replay_.IsRecording())
replay_.UpdateLastGF(em_.GetCurrentGF());

if(std::chrono::steady_clock::now() > nextReport)
{
nextReport += std::chrono::seconds(1);
PrintState();
}
}
PrintState();
}

void HeadlessGame::Close()
{
bnw::cout << '\n';

if(replay_.IsRecording())
{
replay_.StopRecording();
bnw::cout << "Replay written to " << canonical(replayPath_) << '\n';
}

replay_.Close();
}

void HeadlessGame::RecordReplay(const bfs::path& path, unsigned random_init)
{
// Remove old replay
bfs::remove(path);

replayPath_ = path;

MapInfo mapInfo;
mapInfo.filepath = map_;
mapInfo.mapData.CompressFromFile(mapInfo.filepath, &mapInfo.mapChecksum);
mapInfo.type = MapType::OldMap;

replay_.random_init = random_init;
for(unsigned playerId = 0; playerId < world_.GetNumPlayers(); ++playerId)
replay_.AddPlayer(world_.GetPlayer(playerId));
replay_.ggs = game_.ggs_;
if(!replay_.StartRecording(path, mapInfo))
throw std::runtime_error("Replayfile could not be opened!");
}

void HeadlessGame::SaveGame(const bfs::path& path) const
{
// Remove old savegame
bfs::remove(path);

Savegame save;
for(unsigned playerId = 0; playerId < world_.GetNumPlayers(); ++playerId)
save.AddPlayer(world_.GetPlayer(playerId));
save.ggs = game_.ggs_;
save.ggs.exploration = Exploration::Disabled; // no FOW
save.start_gf = em_.GetCurrentGF();
save.sgd.MakeSnapshot(game_);
save.Save(path, "AI Battle");

bnw::cout << "Savegame written to " << canonical(path) << '\n';
}

std::string ToString(const std::chrono::milliseconds& time)
{
char buffer[90];
const auto hours = std::chrono::duration_cast<std::chrono::hours>(time);
const auto minutes = std::chrono::duration_cast<std::chrono::minutes>(time % std::chrono::hours(1));
const auto seconds = std::chrono::duration_cast<std::chrono::seconds>(time % std::chrono::minutes(1));
snprintf(buffer, std::size(buffer), "%03ld:%02ld:%02ld", static_cast<long int>(hours.count()),
static_cast<long int>(minutes.count()), static_cast<long int>(seconds.count()));
return std::string(buffer);
}

std::string HumanReadableNumber(unsigned num)
{
std::stringstream ss;
ss.imbue(std::locale(""));
ss << std::fixed << num;
return ss.str();
}

void HeadlessGame::PrintState()
{
static bool first_run = true;
if(first_run)
first_run = false;
else
printConsole("\x1b[%dA", 8 + world_.GetNumPlayers()); // Move cursor back up

printConsole("┌───────────────┬───────────────────────┬───────────────────────┬────────────────┐\n");
printConsole(
"│ GF %10s │ Game Clock %s │ Wall Clock %s │ %7s GF/sec │\n", HumanReadableNumber(em_.GetCurrentGF()).c_str(),
ToString(SPEED_GF_LENGTHS[GameSpeed::Normal] * em_.GetCurrentGF()).c_str(), // elapsed time
ToString(std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::steady_clock::now() - gameStartTime_))
.c_str(), // wall clock
HumanReadableNumber(em_.GetCurrentGF() - lastReportGf_).c_str()); // GF per second
printConsole("└───────────────┴───────────────────────┴───────────────────────┴────────────────┘\n");
printConsole("\n");
printConsole("┌────────────────────────┬─────────────────┬─────────────┬───────────┬───────────┐\n");
printConsole("│ Player │ Country │ Buildings │ Military │ Gold │\n");
printConsole("├────────────────────────┼─────────────────┼─────────────┼───────────┼───────────┤\n");
for(unsigned playerId = 0; playerId < world_.GetNumPlayers(); ++playerId)
{
const GamePlayer& player = world_.GetPlayer(playerId);
printConsole("│ %s%-22s%s │ %15s │ %11s │ %9s │ %9s │\n", player.IsDefeated() ? "\x1b[9m" : "",
player.name.c_str(), player.IsDefeated() ? "\x1b[29m" : "",
HumanReadableNumber(player.GetStatisticCurrentValue(StatisticType::Country)).c_str(),
HumanReadableNumber(player.GetStatisticCurrentValue(StatisticType::Buildings)).c_str(),
HumanReadableNumber(player.GetStatisticCurrentValue(StatisticType::Military)).c_str(),
HumanReadableNumber(player.GetStatisticCurrentValue(StatisticType::Gold)).c_str());
}
printConsole("└────────────────────────┴─────────────────┴─────────────┴───────────┴───────────┘\n");

lastReportGf_ = em_.GetCurrentGF();
}

std::vector<PlayerInfo> GeneratePlayerInfo(const std::vector<AI::Info>& ais)
{
std::vector<PlayerInfo> ret;
for(const AI::Info& ai : ais)
{
PlayerInfo pi;
pi.ps = PlayerState::Occupied;
pi.aiInfo = ai;
switch(ai.type)
{
case AI::Type::Default: pi.name = "AIJH " + std::to_string(ret.size()); break;
case AI::Type::Dummy:
default: pi.name = "Dummy " + std::to_string(ret.size()); break;
}
pi.nation = Nation::Romans;
pi.team = Team::None;
ret.push_back(pi);
}
return ret;
}

#ifdef WIN32
HANDLE setupStdOut()
{
HANDLE h = GetStdHandle(STD_OUTPUT_HANDLE);
SetConsoleMode(h, ENABLE_PROCESSED_OUTPUT | ENABLE_VIRTUAL_TERMINAL_PROCESSING);
SetConsoleOutputCP(65001);
return h;
}
#endif

void printConsole(const char* fmt, ...)
{
char buffer[512];
va_list args;
va_start(args, fmt);
const int len = vsnprintf(buffer, sizeof(buffer), fmt, args);
va_end(args);
if(len > 0 && (size_t)len < sizeof(buffer))
{
#ifdef WIN32
static auto h = setupStdOut();
WriteConsoleA(h, buffer, len, 0, 0);
#else
bnw::cout << buffer;
#endif
}
}
47 changes: 47 additions & 0 deletions extras/ai-battle/HeadlessGame.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
// Copyright (C) 2005 - 2024 Settlers Freaks (sf-team at siedler25.org)
//
// SPDX-License-Identifier: GPL-2.0-or-later

#pragma once

#include "Game.h"
#include "Replay.h"
#include "ai/AIPlayer.h"
#include "gameTypes/AIInfo.h"
#include <boost/filesystem.hpp>
#include <chrono>
#include <limits>
#include <vector>

class GameWorld;
class GlobalGameSettings;
class EventManager;

/// Run an ai-only game without user-interface.
class HeadlessGame
{
public:
HeadlessGame(const GlobalGameSettings& ggs, const boost::filesystem::path& map, const std::vector<AI::Info>& ais);
~HeadlessGame();

void Run(unsigned maxGF = std::numeric_limits<unsigned>::max());
void Close();

void RecordReplay(const boost::filesystem::path& path, unsigned random_init);
void SaveGame(const boost::filesystem::path& path) const;

private:
void PrintState();

boost::filesystem::path map_;
Game game_;
GameWorld& world_;
EventManager& em_;
std::vector<std::unique_ptr<AIPlayer>> players_;

Replay replay_;
boost::filesystem::path replayPath_;

unsigned lastReportGf_ = 0;
std::chrono::steady_clock::time_point gameStartTime_;
};
Loading

0 comments on commit 815e692

Please sign in to comment.