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:
@@ -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
|
||||
|
@@ -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");
|
||||
}
|
||||
|
@@ -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)
|
||||
|
@@ -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
203
lib/rmg/CRoadRandomizer.cpp
Normal 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
40
lib/rmg/CRoadRandomizer.h
Normal 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
|
@@ -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;
|
||||
|
@@ -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();
|
||||
|
||||
|
@@ -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()
|
||||
|
Reference in New Issue
Block a user