1
0
mirror of https://github.com/vcmi/vcmi.git synced 2025-09-16 09:26:28 +02:00

Verified - Algorithm creates only one path between any two towns, as intended

Randomize optional roads
This commit is contained in:
DjWarmonger
2025-08-09 07:11:40 +02:00
committed by GitHub
10 changed files with 360 additions and 58 deletions

View File

@@ -218,6 +218,7 @@ set(lib_MAIN_SRCS
rmg/CRmgTemplate.cpp
rmg/CRmgTemplateStorage.cpp
rmg/CZonePlacer.cpp
rmg/CRoadRandomizer.cpp
rmg/TileInfo.cpp
rmg/Zone.cpp
rmg/Functions.cpp
@@ -675,6 +676,7 @@ set(lib_MAIN_HEADERS
rmg/CRmgTemplate.h
rmg/CRmgTemplateStorage.h
rmg/CZonePlacer.h
rmg/CRoadRandomizer.h
rmg/TileInfo.h
rmg/Zone.h
rmg/RmgMap.h

View File

@@ -25,6 +25,7 @@
#include "../constants/StringConstants.h"
#include "../filesystem/Filesystem.h"
#include "CZonePlacer.h"
#include "CRoadRandomizer.h"
#include "TileInfo.h"
#include "Zone.h"
#include "Functions.h"
@@ -328,6 +329,10 @@ void CMapGenerator::genZones()
{
placer->placeZones(rand.get());
placer->assignZones(rand.get());
placer->RemoveRoadsForWideConnections();
CRoadRandomizer roadRandomizer(*map);
roadRandomizer.dropRandomRoads(rand.get());
logGlobal->info("Zones generated successfully");
}

View File

@@ -365,6 +365,28 @@ std::vector<ZoneConnection> ZoneOptions::getConnections() const
return connectionDetails;
}
std::vector<ZoneConnection>& ZoneOptions::getConnectionsRef()
{
return connectionDetails;
}
void ZoneOptions::setRoadOption(int connectionId, rmg::ERoadOption roadOption)
{
for(auto & connection : connectionDetails)
{
if(connection.getId() == connectionId)
{
connection.setRoadOption(roadOption);
logGlobal->info("Set road option for connection %d between zones %d and %d to %s",
connectionId, connection.getZoneA(), connection.getZoneB(),
roadOption == rmg::ERoadOption::ROAD_TRUE ? "true" :
roadOption == rmg::ERoadOption::ROAD_FALSE ? "false" : "random");
return;
}
}
logGlobal->warn("Failed to find connection with ID %d in zone %d", connectionId, id);
}
std::vector<TRmgTemplateZoneId> ZoneOptions::getConnectedZoneIds() const
{
return connectedZoneIds;
@@ -533,7 +555,7 @@ ZoneConnection::ZoneConnection():
zoneB(-1),
guardStrength(0),
connectionType(rmg::EConnectionType::GUARDED),
hasRoad(rmg::ERoadOption::ROAD_TRUE)
hasRoad(rmg::ERoadOption::ROAD_RANDOM)
{
}
@@ -588,12 +610,22 @@ rmg::ERoadOption ZoneConnection::getRoadOption() const
{
return hasRoad;
}
void ZoneConnection::setRoadOption(rmg::ERoadOption roadOption)
{
hasRoad = roadOption;
}
bool operator==(const ZoneConnection & l, const ZoneConnection & r)
{
return l.id == r.id;
}
bool operator<(const ZoneConnection & l, const ZoneConnection & r)
{
return l.id < r.id;
}
void ZoneConnection::serializeJson(JsonSerializeFormat & handler)
{
static const std::vector<std::string> connectionTypes =
@@ -607,9 +639,9 @@ void ZoneConnection::serializeJson(JsonSerializeFormat & handler)
static const std::vector<std::string> roadOptions =
{
"random",
"true",
"false",
"random"
"false"
};
if (handler.saving)

View File

@@ -88,9 +88,9 @@ enum class EConnectionType
enum class ERoadOption
{
ROAD_RANDOM = 0,
ROAD_TRUE,
ROAD_FALSE,
ROAD_RANDOM
ROAD_FALSE
};
class DLL_LINKAGE ZoneConnection
@@ -111,10 +111,12 @@ public:
int getGuardStrength() const;
rmg::EConnectionType getConnectionType() const;
rmg::ERoadOption getRoadOption() const;
void setRoadOption(rmg::ERoadOption roadOption);
void serializeJson(JsonSerializeFormat & handler);
friend bool operator==(const ZoneConnection &, const ZoneConnection &);
friend bool operator<(const ZoneConnection &, const ZoneConnection &);
private:
int id;
TRmgTemplateZoneId zoneA;
@@ -222,8 +224,12 @@ public:
void addConnection(const ZoneConnection & connection);
std::vector<ZoneConnection> getConnections() const;
std::vector<ZoneConnection>& getConnectionsRef();
std::vector<TRmgTemplateZoneId> getConnectedZoneIds() const;
// Set road option for a specific connection by ID
void setRoadOption(int connectionId, rmg::ERoadOption roadOption);
void serializeJson(JsonSerializeFormat & handler);
EMonsterStrength::EMonsterStrength monsterStrength;

203
lib/rmg/CRoadRandomizer.cpp Normal file
View File

@@ -0,0 +1,203 @@
/*
* CRoadRandomizer.cpp, part of VCMI engine
*
* Authors: listed in file AUTHORS in main folder
*
* License: GNU General Public License v2.0 or later
* Full text of license available in license.txt file, in main folder
*
*/
#include "StdInc.h"
#include "CRoadRandomizer.h"
#include "RmgMap.h"
#include "Zone.h"
#include "Functions.h"
#include "CRmgTemplate.h"
#include <vstd/RNG.h>
VCMI_LIB_NAMESPACE_BEGIN
CRoadRandomizer::CRoadRandomizer(RmgMap & map)
: map(map)
{
}
TRmgTemplateZoneId findSet(std::map<TRmgTemplateZoneId, TRmgTemplateZoneId> & parent, TRmgTemplateZoneId x)
{
if(parent[x] != x)
parent[x] = findSet(parent, parent[x]);
return parent[x];
}
void unionSets(std::map<TRmgTemplateZoneId, TRmgTemplateZoneId> & parent, TRmgTemplateZoneId x, TRmgTemplateZoneId y)
{
TRmgTemplateZoneId rx = findSet(parent, x);
TRmgTemplateZoneId ry = findSet(parent, y);
if(rx != ry)
parent[rx] = ry;
}
/*
Random road generation requirements:
- Every town should be connected via road
- There should be exactly one road betwen any two towns (connected MST)
- This excludes cases when there are multiple obligatory road connections betwween two zones
- Road cannot end in a zone without town
- Wide connections should have no road
*/
void CRoadRandomizer::dropRandomRoads(vstd::RNG * rand)
{
logGlobal->info("Starting road randomization");
auto zones = map.getZones();
// Helper lambda to set road option for all instances of a connection
auto setRoadOptionForConnection = [&zones](int connectionId, rmg::ERoadOption roadOption)
{
// Update all instances of this connection (A→B and B→A) to have the same road option
for(auto & zonePtr : zones)
{
for(auto & connection : zonePtr.second->getConnections())
{
if(connection.getId() == connectionId)
{
zonePtr.second->setRoadOption(connectionId, roadOption);
}
}
}
};
// Identify zones with towns
std::set<TRmgTemplateZoneId> zonesWithTowns;
for(const auto & zone : zones)
{
if(zone.second->getPlayerTowns().getTownCount() ||
zone.second->getPlayerTowns().getCastleCount() ||
zone.second->getNeutralTowns().getTownCount() ||
zone.second->getNeutralTowns().getCastleCount())
{
zonesWithTowns.insert(zone.first);
}
}
logGlobal->info("Found %d zones with towns", zonesWithTowns.size());
if(zonesWithTowns.empty())
{
// No towns, no roads needed - mark all RANDOM roads as FALSE
for(auto & zonePtr : zones)
{
for(auto & connection : zonePtr.second->getConnections())
{
if(connection.getRoadOption() == rmg::ERoadOption::ROAD_RANDOM)
{
setRoadOptionForConnection(connection.getId(), rmg::ERoadOption::ROAD_FALSE);
}
}
}
return;
}
// Initialize Union-Find for all zones to track connectivity
std::map<TRmgTemplateZoneId, TRmgTemplateZoneId> parent;
// Track if a component (represented by its root) contains a town
std::map<TRmgTemplateZoneId, bool> componentHasTown;
for(const auto & zone : zones)
{
auto zoneId = zone.first;
parent[zoneId] = zoneId;
componentHasTown[zoneId] = vstd::contains(zonesWithTowns, zoneId);
}
// Process all connections that are already set to TRUE
for(auto & zonePtr : zones)
{
for(auto & connection : zonePtr.second->getConnections())
{
if(connection.getRoadOption() == rmg::ERoadOption::ROAD_TRUE)
{
auto zoneA = connection.getZoneA();
auto zoneB = connection.getZoneB();
auto rootA = findSet(parent, zoneA);
auto rootB = findSet(parent, zoneB);
if(rootA != rootB)
{
bool hasTown = componentHasTown[rootA] || componentHasTown[rootB];
parent[rootA] = rootB;
componentHasTown[rootB] = hasTown;
}
}
}
}
// Collect all RANDOM connections to be processed
std::vector<rmg::ZoneConnection> randomRoads;
std::map<int, bool> processedConnectionIds;
for(auto & zonePtr : zones)
{
for(auto & connection : zonePtr.second->getConnections())
{
if(connection.getRoadOption() == rmg::ERoadOption::ROAD_RANDOM)
{
// Ensure we only add each connection once
if(processedConnectionIds.find(connection.getId()) == processedConnectionIds.end())
{
randomRoads.push_back(connection);
processedConnectionIds[connection.getId()] = true;
}
}
}
}
RandomGeneratorUtil::randomShuffle(randomRoads, *rand);
// Process random roads using Kruskal's-like algorithm to connect components
for(const auto& connection : randomRoads)
{
auto zoneA = connection.getZoneA();
auto zoneB = connection.getZoneB();
auto rootA = findSet(parent, zoneA);
auto rootB = findSet(parent, zoneB);
bool roadSet = false;
// Only build roads if they connect different components
if(rootA != rootB)
{
bool townInA = componentHasTown[rootA];
bool townInB = componentHasTown[rootB];
// Connect components if at least one of them contains a town.
// This ensures we connect all town components and extend them,
// but never connect two non-town components together.
if(townInA || townInB)
{
logGlobal->info("Setting RANDOM road to TRUE for connection %d between zones %d and %d to connect town components.", connection.getId(), zoneA, zoneB);
setRoadOptionForConnection(connection.getId(), rmg::ERoadOption::ROAD_TRUE);
// Union the sets
bool hasTown = townInA || townInB;
parent[rootA] = rootB;
componentHasTown[rootB] = hasTown;
roadSet = true;
}
}
if(!roadSet)
{
// This road was not chosen, either because it creates a cycle or connects two non-town areas
setRoadOptionForConnection(connection.getId(), rmg::ERoadOption::ROAD_FALSE);
}
}
logGlobal->info("Finished road generation - created minimal spanning tree connecting all towns");
}
VCMI_LIB_NAMESPACE_END

40
lib/rmg/CRoadRandomizer.h Normal file
View File

@@ -0,0 +1,40 @@
/*
* CRoadRandomizer.h, part of VCMI engine
*
* Authors: listed in file AUTHORS in main folder
*
* License: GNU General Public License v2.0 or later
* Full text of license available in license.txt file, in main folder
*
*/
#pragma once
#include "../GameConstants.h"
VCMI_LIB_NAMESPACE_BEGIN
namespace vstd
{
class RNG;
}
class RmgMap;
class CRoadRandomizer
{
public:
explicit CRoadRandomizer(RmgMap & map);
~CRoadRandomizer() = default;
void dropRandomRoads(vstd::RNG * rand);
private:
RmgMap & map;
};
// Helper functions for Union-Find (Disjoint Set Union)
TRmgTemplateZoneId findSet(std::map<TRmgTemplateZoneId, TRmgTemplateZoneId> & parent, TRmgTemplateZoneId x);
void unionSets(std::map<TRmgTemplateZoneId, TRmgTemplateZoneId> & parent, TRmgTemplateZoneId x, TRmgTemplateZoneId y);
VCMI_LIB_NAMESPACE_END

View File

@@ -18,6 +18,7 @@
#include "../mapping/CMapEditManager.h"
#include "../GameLibrary.h"
#include "CMapGenOptions.h"
#include "CRmgTemplate.h"
#include "RmgMap.h"
#include "Zone.h"
#include "Functions.h"
@@ -1004,6 +1005,24 @@ void CZonePlacer::assignZones(vstd::RNG * rand)
logGlobal->info("Finished zone colouring");
}
void CZonePlacer::RemoveRoadsForWideConnections()
{
auto zones = map.getZones();
for(auto & zonePtr : zones)
{
for(auto & connection : zonePtr.second->getConnections())
{
if(connection.getConnectionType() == rmg::EConnectionType::WIDE)
{
zonePtr.second->setRoadOption(connection.getId(), rmg::ERoadOption::ROAD_FALSE);
}
}
}
}
const TDistanceMap& CZonePlacer::getDistanceMap()
{
return distancesBetweenZones;

View File

@@ -46,6 +46,7 @@ public:
void placeOnGrid(vstd::RNG* rand);
float scaleForceBetweenZones(const std::shared_ptr<Zone> zoneA, const std::shared_ptr<Zone> zoneB) const;
void assignZones(vstd::RNG * rand);
void RemoveRoadsForWideConnections();
const TDistanceMap & getDistanceMap();

View File

@@ -110,14 +110,9 @@ void ConnectionsPlacer::init()
POSTFUNCTION(RoadPlacer);
POSTFUNCTION(ObjectManager);
auto id = zone.getId();
for(auto c : map.getMapGenOptions().getMapTemplate()->getConnectedZoneIds())
for (auto c : zone.getConnections())
{
// Only consider connected zones
if (c.getZoneA() == id || c.getZoneB() == id)
{
addConnection(c);
}
addConnection(c);
}
}
@@ -477,8 +472,11 @@ void ConnectionsPlacer::collectNeighbourZones()
bool ConnectionsPlacer::shouldGenerateRoad(const rmg::ZoneConnection& connection) const
{
return connection.getRoadOption() == rmg::ERoadOption::ROAD_TRUE ||
(connection.getRoadOption() == rmg::ERoadOption::ROAD_RANDOM && zone.getRand().nextDouble(0, 1) >= 0.5f);
if (connection.getRoadOption() == rmg::ERoadOption::ROAD_RANDOM)
logGlobal->error("Random road between zones %d and %d", connection.getZoneA(), connection.getZoneB());
else
logGlobal->info("Should generate road between zones %d and %d: %d", connection.getZoneA(), connection.getZoneB(), connection.getRoadOption() == rmg::ERoadOption::ROAD_TRUE);
return connection.getRoadOption() == rmg::ERoadOption::ROAD_TRUE;
}
void ConnectionsPlacer::createBorder()