| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564 |
- /*
- * 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 road connetions 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;
- }
-
- // Track direct connections between zones (both TRUE and RANDOM)
- std::map<TRmgTemplateZoneId, std::map<TRmgTemplateZoneId, bool>> directConnections;
-
- // First pass: find all TRUE connections
- for(auto & zonePtr : zones)
- {
- for(auto & connection : zonePtr.second->getConnections())
- {
- auto zoneA = connection.getZoneA();
- auto zoneB = connection.getZoneB();
-
- if(connection.getRoadOption() == rmg::ERoadOption::ROAD_TRUE)
- {
- // Mark that these zones are directly connected by a TRUE road
- directConnections[zoneA][zoneB] = true;
- directConnections[zoneB][zoneA] = true;
- }
- }
- }
-
- // Track all available connections in the template (for graph connectivity analysis)
- std::map<TRmgTemplateZoneId, std::set<TRmgTemplateZoneId>> allConnections;
- std::map<std::pair<TRmgTemplateZoneId, TRmgTemplateZoneId>, int> connectionIds;
-
- // Build adjacency list for available connections and track connection IDs
- for(auto & zonePtr : zones)
- {
- for(auto & connection : zonePtr.second->getConnections())
- {
- auto zoneA = connection.getZoneA();
- auto zoneB = connection.getZoneB();
-
- if(connection.getRoadOption() == rmg::ERoadOption::ROAD_TRUE ||
- connection.getRoadOption() == rmg::ERoadOption::ROAD_RANDOM)
- {
- // Only include TRUE and RANDOM roads in connectivity analysis
- allConnections[zoneA].insert(zoneB);
- allConnections[zoneB].insert(zoneA);
-
- // Track connection ID for later use
- connectionIds[{std::min(zoneA, zoneB), std::max(zoneA, zoneB)}] = connection.getId();
- }
- }
- }
-
- // Check if there's a path between all town zones using all available connections
- // This is to verify if global connectivity is even possible
- std::map<TRmgTemplateZoneId, std::set<TRmgTemplateZoneId>> reachableTowns;
-
- for(auto startTown : zonesWithTowns)
- {
- std::queue<TRmgTemplateZoneId> q;
- std::set<TRmgTemplateZoneId> visited;
-
- q.push(startTown);
- visited.insert(startTown);
-
- while(!q.empty())
- {
- auto current = q.front();
- q.pop();
-
- for(auto neighbor : allConnections[current])
- {
- if(!vstd::contains(visited, neighbor))
- {
- visited.insert(neighbor);
- q.push(neighbor);
-
- if(vstd::contains(zonesWithTowns, neighbor))
- {
- reachableTowns[startTown].insert(neighbor);
- }
- }
- }
- }
- }
-
- // Initialize Union-Find for MST tracking
- std::map<TRmgTemplateZoneId, TRmgTemplateZoneId> parent;
- for(auto townZone : zonesWithTowns)
- {
- parent[townZone] = townZone;
- }
-
- // Add all TRUE roads connecting town zones to MST
- 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();
-
- // If both zones have towns, add to MST
- if(vstd::contains(zonesWithTowns, zoneA) && vstd::contains(zonesWithTowns, zoneB))
- {
- unionSets(parent, zoneA, zoneB);
- }
- }
- }
- }
-
- // Process all paths through true roads (BFS)
- for(auto townZone : zonesWithTowns)
- {
- std::queue<TRmgTemplateZoneId> q;
- std::set<TRmgTemplateZoneId> visited;
-
- q.push(townZone);
- visited.insert(townZone);
-
- while(!q.empty())
- {
- auto current = q.front();
- q.pop();
-
- for(auto & otherZone : directConnections[current])
- {
- if(otherZone.second && !vstd::contains(visited, otherZone.first))
- {
- visited.insert(otherZone.first);
- q.push(otherZone.first);
-
- // If this is another town, update MST
- if(vstd::contains(zonesWithTowns, otherZone.first))
- {
- unionSets(parent, townZone, otherZone.first);
- }
- }
- }
- }
- }
-
- // Process RANDOM connections
- // First, ensure each town has at least one connection
- for(auto townZone : zonesWithTowns)
- {
- // Skip if already has a TRUE road
- if(!directConnections[townZone].empty())
- continue;
-
- // Find a random connection to upgrade to TRUE
- for(auto & zonePtr : zones)
- {
- for(auto & connection : zonePtr.second->getConnections())
- {
- if(connection.getRoadOption() == rmg::ERoadOption::ROAD_RANDOM &&
- (connection.getZoneA() == townZone || connection.getZoneB() == townZone))
- {
- auto zoneA = connection.getZoneA();
- auto zoneB = connection.getZoneB();
-
- // Don't upgrade if these zones are already directly connected by a TRUE road
- if(vstd::contains(directConnections[zoneA], zoneB) && directConnections[zoneA][zoneB])
- continue;
-
- // Upgrade to TRUE
- setRoadOptionForConnection(connection.getId(), rmg::ERoadOption::ROAD_TRUE);
- directConnections[zoneA][zoneB] = true;
- directConnections[zoneB][zoneA] = true;
-
- auto otherZone = (zoneA == townZone) ? zoneB : zoneA;
- logGlobal->info("Setting RANDOM road to TRUE for connection %d to ensure town zone %d has at least one connection (to zone %d)",
- connection.getId(), townZone, otherZone);
-
- break;
- }
- }
-
- // Stop if we've found a connection
- if(!directConnections[townZone].empty())
- break;
- }
- }
-
- // Process remaining RANDOM roads - prioritize town connectivity
- // First collect all RANDOM roads
- std::vector<std::pair<int, std::pair<TRmgTemplateZoneId, TRmgTemplateZoneId>>> randomRoads;
-
- for(auto & zonePtr : zones)
- {
- for(auto & connection : zonePtr.second->getConnections())
- {
- if(connection.getRoadOption() == rmg::ERoadOption::ROAD_RANDOM)
- {
- auto id = connection.getId();
- auto zoneA = connection.getZoneA();
- auto zoneB = connection.getZoneB();
-
- // Skip if these zones are already directly connected by a TRUE road
- if(vstd::contains(directConnections[zoneA], zoneB) && directConnections[zoneA][zoneB])
- {
- setRoadOptionForConnection(id, rmg::ERoadOption::ROAD_FALSE);
- logGlobal->info("Setting RANDOM road to FALSE for connection %d - duplicate of TRUE road between zones %d and %d",
- id, zoneA, zoneB);
- continue;
- }
-
- randomRoads.push_back(std::make_pair(id, std::make_pair(zoneA, zoneB)));
- }
- }
- }
-
- RandomGeneratorUtil::randomShuffle(randomRoads, *rand);
-
- // Process random roads - first connect town zones
- for(auto& road : randomRoads)
- {
- auto id = road.first;
- auto zoneA = road.second.first;
- auto zoneB = road.second.second;
-
- bool setToTrue = false;
-
- // If both zones have towns, check if they're already connected in the MST
- if(vstd::contains(zonesWithTowns, zoneA) && vstd::contains(zonesWithTowns, zoneB))
- {
- if(findSet(parent, zoneA) != findSet(parent, zoneB))
- {
- // Not connected, add this road to MST
- unionSets(parent, zoneA, zoneB);
- setToTrue = true;
-
- logGlobal->info("Setting RANDOM road to TRUE for connection %d between town zones %d and %d",
- id, zoneA, zoneB);
- }
- }
- // If one zone has a town and one doesn't
- else if(vstd::contains(zonesWithTowns, zoneA) || vstd::contains(zonesWithTowns, zoneB))
- {
- TRmgTemplateZoneId townZone = vstd::contains(zonesWithTowns, zoneA) ? zoneA : zoneB;
- TRmgTemplateZoneId nonTownZone = vstd::contains(zonesWithTowns, zoneA) ? zoneB : zoneA;
-
- // Check if this town already has at least one TRUE connection
- if(directConnections[townZone].empty())
- {
- setToTrue = true;
- logGlobal->info("Setting RANDOM road to TRUE for connection %d - only connection for town zone %d",
- id, townZone);
- }
- else
- {
- // See if this non-town zone connects to another town zone
- // This could be a potential bridge zone to connect towns
- bool connectsToOtherTown = false;
- TRmgTemplateZoneId otherTownZone = 0;
-
- for(auto connectedZone : allConnections[nonTownZone])
- {
- if(vstd::contains(zonesWithTowns, connectedZone) && connectedZone != townZone)
- {
- otherTownZone = connectedZone;
- connectsToOtherTown = true;
- break;
- }
- }
-
- if(connectsToOtherTown && findSet(parent, townZone) != findSet(parent, otherTownZone))
- {
- // This non-town zone can help connect two town zones that are not yet connected
- setToTrue = true;
- logGlobal->info("Setting RANDOM road to TRUE for connection %d - bridge through non-town zone %d to connect towns %d and %d",
- id, nonTownZone, townZone, otherTownZone);
- }
- }
- }
-
- // Update all zones with this connection
- setRoadOptionForConnection(id, setToTrue ? rmg::ERoadOption::ROAD_TRUE : rmg::ERoadOption::ROAD_FALSE);
-
- if(setToTrue)
- {
- directConnections[zoneA][zoneB] = true;
- directConnections[zoneB][zoneA] = true;
- }
- }
-
- // Check if we have a connected graph for town zones after initial MST
- std::map<TRmgTemplateZoneId, std::set<TRmgTemplateZoneId>> connectedComponents;
- std::set<TRmgTemplateZoneId> processedTowns;
-
- for(auto townZone : zonesWithTowns)
- {
- if(vstd::contains(processedTowns, townZone))
- continue;
-
- std::set<TRmgTemplateZoneId> component;
- for(auto otherTown : zonesWithTowns)
- {
- if(findSet(parent, townZone) == findSet(parent, otherTown))
- {
- component.insert(otherTown);
- processedTowns.insert(otherTown);
- }
- }
-
- if(!component.empty())
- connectedComponents[townZone] = component;
- }
-
- // If we have more than one component, try to connect them if possible
- if(connectedComponents.size() > 1)
- {
- logGlobal->warn("Found %d disconnected town components, trying to connect them", connectedComponents.size());
-
- // Create a list of components
- std::vector<std::set<TRmgTemplateZoneId>> components;
- for(auto & component : connectedComponents)
- {
- components.push_back(component.second);
- }
-
- // For each pair of components, try to find a path between them
- for(size_t i = 0; i < components.size() - 1; i++)
- {
- bool foundBridge = false;
-
- for(size_t j = i + 1; j < components.size() && !foundBridge; j++)
- {
- // Try to find a path between any two towns in different components
- for(auto townA : components[i])
- {
- if(foundBridge) break;
-
- for(auto townB : components[j])
- {
- // Check if there's a path between townA and townB in the original template
- if(vstd::contains(reachableTowns[townA], townB))
- {
- // There's a path, now find the specific path to enable roads on
- std::queue<TRmgTemplateZoneId> q;
- std::map<TRmgTemplateZoneId, TRmgTemplateZoneId> prev;
-
- q.push(townA);
- prev[townA] = 0; // Mark as visited with no predecessor
-
- bool found = false;
- while(!q.empty() && !found)
- {
- auto current = q.front();
- q.pop();
-
- for(auto next : allConnections[current])
- {
- if(!vstd::contains(prev, next))
- {
- prev[next] = current;
- q.push(next);
-
- if(next == townB)
- {
- found = true;
- break;
- }
- }
- }
- }
-
- // Now reconstruct the path and set all roads on this path to TRUE
- if(found)
- {
- std::vector<TRmgTemplateZoneId> path;
- TRmgTemplateZoneId current = townB;
-
- while(current != townA)
- {
- path.push_back(current);
- current = prev[current];
- }
- path.push_back(townA);
-
- // Reverse to get path from townA to townB
- std::reverse(path.begin(), path.end());
-
- logGlobal->info("Found path between town zones %d and %d, enabling all roads on this path", townA, townB);
-
- // Enable all roads on this path
- for(size_t k = 0; k < path.size() - 1; k++)
- {
- auto zoneA = path[k];
- auto zoneB = path[k+1];
-
- auto minZone = std::min(zoneA, zoneB);
- auto maxZone = std::max(zoneA, zoneB);
-
- if(vstd::contains(connectionIds, std::make_pair(minZone, maxZone)))
- {
- auto connectionId = connectionIds[std::make_pair(minZone, maxZone)];
-
- // Enable this road if it's not already TRUE
- for(auto & zonePtr : zones)
- {
- for(auto & connection : zonePtr.second->getConnections())
- {
- if(connection.getId() == connectionId &&
- connection.getRoadOption() == rmg::ERoadOption::ROAD_RANDOM)
- {
- setRoadOptionForConnection(connectionId, rmg::ERoadOption::ROAD_TRUE);
- directConnections[zoneA][zoneB] = true;
- directConnections[zoneB][zoneA] = true;
-
- logGlobal->info("Setting RANDOM road to TRUE for connection %d to connect components - part of path between towns %d and %d",
- connectionId, townA, townB);
-
- break;
- }
- }
- }
- }
- }
-
- // Update Union-Find to merge components
- unionSets(parent, townA, townB);
- foundBridge = true;
- break;
- }
- }
- }
- }
- }
-
- if(!foundBridge)
- {
- logGlobal->warn("Could not find a path between component with towns [%s] and other components",
- [&components, i]() {
- std::string result;
- for(auto town : components[i])
- {
- if(!result.empty()) result += ", ";
- result += std::to_string(town);
- }
- return result;
- }().c_str());
- }
- }
- }
-
- // Final check for connectivity between town zones
- std::set<TRmgTemplateZoneId> connectedTowns;
- if(!zonesWithTowns.empty())
- {
- auto firstTown = *zonesWithTowns.begin();
- for(auto town : zonesWithTowns)
- {
- if(findSet(parent, firstTown) == findSet(parent, town))
- {
- connectedTowns.insert(town);
- }
- }
- }
-
- logGlobal->info("Final town connectivity: %d connected out of %d total town zones",
- connectedTowns.size(), zonesWithTowns.size());
-
- logGlobal->info("Finished road generation - created minimal spanning tree connecting all towns");
- }
- VCMI_LIB_NAMESPACE_END
|