Skip to content
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

Make AIJH Actions Reproducible by Using the Games Random Seed #1739

Open
wants to merge 7 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions extras/ai-battle/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
#include "QuickStartGame.h"
#include "RTTR_Version.h"
#include "RttrConfig.h"
#include "ai/random.h"
#include "files.h"
#include "random/Random.h"
#include "s25util/System.h"
Expand All @@ -30,6 +31,7 @@ int main(int argc, char** argv)
boost::optional<std::string> replay_path;
boost::optional<std::string> savegame_path;
unsigned random_init = static_cast<unsigned>(std::chrono::high_resolution_clock::now().time_since_epoch().count());
unsigned random_ai_init = random_init;

po::options_description desc("Allowed options");
// clang-format off
Expand All @@ -41,6 +43,7 @@ int main(int argc, char** argv)
("replay", po::value(&replay_path),"Filename to write replay to (optional)")
("save", po::value(&savegame_path),"Filename to write savegame to (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")
;
Expand Down Expand Up @@ -85,10 +88,12 @@ int main(int argc, char** argv)
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 << std::endl;

RTTRCONFIG.Init();
RANDOM.Init(random_init);
AI::getRandomGenerator().seed(random_ai_init);

const bfs::path mapPath = RTTRCONFIG.ExpandPath(options["map"].as<std::string>());
const std::vector<AI::Info> ais = ParseAIOptions(options["ai"].as<std::vector<std::string>>());
Expand Down
9 changes: 5 additions & 4 deletions libs/s25main/ai/aijh/AIConstruction.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
#include "addons/const_addons.h"
#include "ai/AIInterface.h"
#include "ai/aijh/AIPlayerJH.h"
#include "ai/random.h"
#include "buildings/noBuildingSite.h"
#include "buildings/nobBaseMilitary.h"
#include "buildings/nobBaseWarehouse.h"
Expand Down Expand Up @@ -465,10 +466,10 @@ helpers::OptionalEnum<BuildingType> AIConstruction::ChooseMilitaryBuilding(const
const BuildingType biggestBld = GetBiggestAllowedMilBuilding().value();

const Inventory& inventory = aii.GetInventory();
if(((rand() % 3) == 0 || inventory.people[Job::Private] < 15)
if((inventory.people[Job::Private] < 15 || AI::random(3))
&& (inventory.goods[GoodType::Stones] > 6 || bldPlanner.GetNumBuildings(BuildingType::Quarry) > 0))
bld = BuildingType::Guardhouse;
if(aijh.getAIInterface().isHarborPosClose(pt, 19) && rand() % 10 != 0 && aijh.ggs.isEnabled(AddonId::SEA_ATTACK))
if(aijh.getAIInterface().isHarborPosClose(pt, 19) && !AI::random(9) && aijh.ggs.isEnabled(AddonId::SEA_ATTACK))
{
if(aii.CanBuildBuildingtype(BuildingType::Watchtower))
return BuildingType::Watchtower;
Expand All @@ -478,7 +479,7 @@ helpers::OptionalEnum<BuildingType> AIConstruction::ChooseMilitaryBuilding(const
{
if(aijh.UpdateUpgradeBuilding() < 0 && bldPlanner.GetNumBuildingSites(biggestBld) < 1
&& (inventory.goods[GoodType::Stones] > 20 || bldPlanner.GetNumBuildings(BuildingType::Quarry) > 0)
&& rand() % 10 != 0)
&& !AI::random(9u))
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't this be random(10u)?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. See my previous comment which includes a "recipe" for translating those. Didn't want to annotate all of them to avoid flooding this PR too much ;-)

{
return biggestBld;
}
Expand All @@ -493,7 +494,7 @@ helpers::OptionalEnum<BuildingType> AIConstruction::ChooseMilitaryBuilding(const
// Prüfen ob Feind in der Nähe
if(milBld->GetPlayer() != playerId && distance < 35)
{
int randmil = rand();
const auto randmil = AI::randomValue(0, std::numeric_limits<int>::max());
bool buildCatapult = randmil % 8 == 0 && aii.CanBuildCatapult()
&& bldPlanner.GetNumAdditionalBuildingsWanted(BuildingType::Catapult) > 0;
// another catapult within "min" radius? ->dont build here!
Expand Down
24 changes: 10 additions & 14 deletions libs/s25main/ai/aijh/AIPlayerJH.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
#include "RttrForeachPt.h"
#include "addons/const_addons.h"
#include "ai/AIEvents.h"
#include "ai/random.h"
#include "boost/filesystem/fstream.hpp"
#include "buildings/noBuildingSite.h"
#include "buildings/nobHarborBuilding.h"
Expand Down Expand Up @@ -39,7 +40,6 @@
#include <algorithm>
#include <array>
#include <memory>
#include <random>
#include <stdexcept>
#include <type_traits>

Expand Down Expand Up @@ -344,10 +344,8 @@ void AIPlayerJH::PlanNewBuildings(const unsigned gf)
DistributeGoodsByBlocking(GoodType::Boards, 30);
DistributeGoodsByBlocking(GoodType::Stones, 50);
// go to the picked random warehouse and try to build around it
int randomStore = rand() % (storehouses.size());
auto it = storehouses.begin();
std::advance(it, randomStore);
const MapPoint whPos = (*it)->GetPos();
const auto storehouse = AI::randomElement(storehouses);
const MapPoint whPos = storehouse->GetPos();
UpdateNodesAround(whPos, 15); // update the area we want to build in first
for(const BuildingType i : bldToTest)
{
Expand All @@ -365,7 +363,7 @@ void AIPlayerJH::PlanNewBuildings(const unsigned gf)
const std::list<nobMilitary*>& militaryBuildings = aii.GetMilitaryBuildings();
if(militaryBuildings.empty())
return;
int randomMiliBld = rand() % militaryBuildings.size();
const int randomMiliBld = static_cast<int>(AI::randomIndex(militaryBuildings));
auto it2 = militaryBuildings.begin();
std::advance(it2, randomMiliBld);
MapPoint bldPos = (*it2)->GetPos();
Expand Down Expand Up @@ -1209,7 +1207,7 @@ void AIPlayerJH::HandleExpedition(const noShip* ship)
aii.FoundColony(ship);
else
{
const unsigned offset = rand() % helpers::MaxEnumValue_v<ShipDirection>;
const unsigned offset = AI::randomValue(0u, helpers::MaxEnumValue_v<ShipDirection> - 1u);
for(auto dir : helpers::EnumRange<ShipDirection>{})
{
dir = ShipDirection((rttr::enum_cast(dir) + offset) % helpers::MaxEnumValue_v<ShipDirection>);
Expand Down Expand Up @@ -1254,9 +1252,7 @@ void AIPlayerJH::HandleTreeChopped(const MapPoint pt)

UpdateNodesAround(pt, 3);

int random = rand();

if(random % 2 == 0)
if(AI::random())
AddMilitaryBuildJob(pt);
else // if (random % 12 == 0)
AddBuildJob(BuildingType::Woodcutter, pt);
Expand Down Expand Up @@ -1535,10 +1531,10 @@ void AIPlayerJH::TryToAttack()
constexpr unsigned limit = 40;
for(const nobMilitary* milBld : militaryBuildings)
{
// We skip the current building with a probability of limit/numMilBlds
// We handle the current building with a probability of limit/numMilBlds
// -> For twice the number of blds as the limit we will most likely skip every 2nd building
// This way we check roughly (at most) limit buildings but avoid any preference for one building over an other
if(rand() % numMilBlds > limit)
if(!AI::random(numMilBlds, limit))
continue;

if(milBld->GetFrontierDistance() == FrontierDistance::Far) // inland building? -> skip it
Expand Down Expand Up @@ -1571,7 +1567,7 @@ void AIPlayerJH::TryToAttack()

// shuffle everything but headquarters and harbors without any troops in them
std::shuffle(potentialTargets.begin() + hq_or_harbor_without_soldiers, potentialTargets.end(),
std::mt19937(std::random_device()()));
AI::getRandomGenerator());

// check for each potential attacking target the number of available attacking soldiers
for(const nobBaseMilitary* target : potentialTargets)
Expand Down Expand Up @@ -1704,7 +1700,7 @@ void AIPlayerJH::TrySeaAttack()
unsigned limit = 15;
unsigned skip = 0;
if(searcharoundharborspots.size() > 15)
skip = std::max<int>(rand() % (searcharoundharborspots.size() / 15 + 1) * 15, 1) - 1;
skip = AI::randomValue(0u, static_cast<unsigned>(searcharoundharborspots.size() / 15u)) * 15u;
for(unsigned i = skip; i < searcharoundharborspots.size() && limit > 0; i++)
{
limit--;
Expand Down
15 changes: 15 additions & 0 deletions libs/s25main/ai/random.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// Copyright (C) 2005 - 2025 Settlers Freaks (sf-team at siedler25.org)
//
// SPDX-License-Identifier: GPL-2.0-or-later

#include "ai/random.h"

namespace AI {

std::minstd_rand& getRandomGenerator()
{
static std::minstd_rand rng(std::random_device{}());
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

are the std rngs defined to have the same randomeness between platforms now? Since for rand() that was implementation specific (that was the reason why we built our own)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The rng yes. There might be differences in the distribution implementations leading to slightly different results or something like that. But IMO we don't really need that here: It is reproducible when run on the same platform which is enough for the current use case, isn't it?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In my understanding std::minstd_rand is using a fixed deterministic algorithm which should produce the same values on all platforms. I did not test this though and it is not important for the AI in my opinion.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correct. It is just the distribution classes which are implementation defined. Found a SO post confirming that.

But still yes: Fine for this use case.

return rng;
}

} // namespace AI
48 changes: 48 additions & 0 deletions libs/s25main/ai/random.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
// Copyright (C) 2005 - 2025 Settlers Freaks (sf-team at siedler25.org)
//
// SPDX-License-Identifier: GPL-2.0-or-later

#pragma once

#include "helpers/random.h"
#include <random>

namespace AI {

std::minstd_rand& getRandomGenerator();

// Return a random value (min and max are included)
template<typename T>
T randomValue(T min = std::numeric_limits<T>::min(), T max = std::numeric_limits<T>::max())
{
return helpers::randomValue(getRandomGenerator(), min, max);
}

// Return a random bool:
// random() ... will return true|false with 50% chance each
// random(15) ... will return true in 1/15 of the cases
// random(20, 5) ... will return true in 5 out of 20 cases, i.e. a probability of 25%. Sames as random(4, 1)
inline bool random(unsigned total = 2u, unsigned chance = 1u)
{
RTTR_Assert(total > 0u);
return (chance >= total) || randomValue(1u, total) <= chance;
}

template<typename ContainerT>
inline unsigned randomIndex(const ContainerT& container)
{
RTTR_Assert(!container.empty());
return randomValue(0u, static_cast<unsigned>(container.size()) - 1u);
Comment on lines +32 to +35
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have just seen we already have this function in helpers:

Suggested change
inline unsigned randomIndex(const ContainerT& container)
{
RTTR_Assert(!container.empty());
return randomValue(0u, static_cast<unsigned>(container.size()) - 1u);
unsigned randomIndex(const ContainerT& container)
{
return helpers::getRandomIndex(getRandomGenerator(), container.size());

(template functions don't need to be marked inline)

There is also helpers::getRandomElement which might be useful in AI code too but that for later.

}

template<typename ContainerT>
inline auto randomElement(const ContainerT& container)
{
RTTR_Assert(!container.empty());
auto it = container.begin();
if(container.size() > 1u)
std::advance(it, randomIndex(container));
return *it;
}

} // namespace AI
Loading