Browse Source

First draft of new algorithm that deliberately links zones with towns

Tomasz Zieliński 7 months ago
parent
commit
f4233aa2fd
1 changed files with 514 additions and 109 deletions
  1. 514 109
      lib/rmg/CZonePlacer.cpp

+ 514 - 109
lib/rmg/CZonePlacer.cpp

@@ -1005,167 +1005,572 @@ void CZonePlacer::assignZones(vstd::RNG * rand)
 	logGlobal->info("Finished zone colouring");
 }
 
+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;
+}
+
 void CZonePlacer::dropRandomRoads(vstd::RNG * rand)
 {
 	auto zones = map.getZones();
-	bool anyDropped;
-
-	do
+	
+	// First, identify zones with towns
+	std::set<TRmgTemplateZoneId> zonesWithTowns;
+	
+	for(const auto & zone : zones)
 	{
-		anyDropped = false;
-
-		// Instead of a simple set, use a multiset to track multiple connections between zones
-		std::map<TRmgTemplateZoneId, std::map<TRmgTemplateZoneId, int>> roadGraph;
-		std::set<rmg::ZoneConnection> randomConnections;
-
-		//Build graph and collect random connections
-		for(const auto & zone : zones)
+		// A zone has towns if it has player towns or neutral towns
+		if(zone.second->getPlayerTowns().getTownCount() || 
+		   zone.second->getPlayerTowns().getCastleCount() ||
+		   zone.second->getNeutralTowns().getTownCount() ||
+		   zone.second->getNeutralTowns().getCastleCount())
+		{
+			zonesWithTowns.insert(zone.first);
+		}
+	}
+	
+	logGlobal->info("Zones with towns: %d", zonesWithTowns.size());
+	
+	if(zonesWithTowns.empty())
+	{
+		// No towns, no roads needed
+		// Mark all roads as FALSE
+		for(auto & zonePtr : zones)
 		{
-			for(const auto & connection : zone.second->getConnections())
+			for(auto & connection : zonePtr.second->getConnections())
 			{
-				if(connection.getRoadOption() == rmg::ERoadOption::ROAD_TRUE)
+				if(connection.getRoadOption() == rmg::ERoadOption::ROAD_RANDOM)
 				{
-					roadGraph[connection.getZoneA()][connection.getZoneB()]++;
-					roadGraph[connection.getZoneB()][connection.getZoneA()]++;
+					auto id = connection.getId();
+					zonePtr.second->setRoadOption(id, rmg::ERoadOption::ROAD_FALSE);
 				}
-				else if(connection.getRoadOption() == rmg::ERoadOption::ROAD_RANDOM)
+			}
+		}
+		return;
+	}
+	
+	// Map to track which connections we've processed (to avoid duplicates)
+	std::set<int> processedConnectionIds;
+	
+	// Map of all connections by their ID for easier access
+	std::map<int, std::pair<TRmgTemplateZoneId, TRmgTemplateZoneId>> connectionMap;
+	
+	// Map true roads between zones
+	std::map<TRmgTemplateZoneId, std::set<TRmgTemplateZoneId>> trueRoads;
+	
+	// Track existing defined connections in template (both TRUE and RANDOM)
+	std::map<TRmgTemplateZoneId, std::set<TRmgTemplateZoneId>> existingConnections;
+	
+	// First pass: collect all connections defined in the template
+	for(auto & zonePtr : zones)
+	{
+		for(auto & connection : zonePtr.second->getConnections())
+		{
+			auto id = connection.getId();
+			auto zoneA = connection.getZoneA();
+			auto zoneB = connection.getZoneB();
+			
+			connectionMap[id] = std::make_pair(zoneA, zoneB);
+			
+			// Track all valid connections defined in template
+			existingConnections[zoneA].insert(zoneB);
+			existingConnections[zoneB].insert(zoneA);
+			
+			if(connection.getRoadOption() == rmg::ERoadOption::ROAD_TRUE)
+			{
+				// Record this as a true road
+				trueRoads[zoneA].insert(zoneB);
+				trueRoads[zoneB].insert(zoneA);
+				
+				logGlobal->info("Keeping pre-configured TRUE road for connection %d between zones %d and %d", 
+					id, zoneA, zoneB);
+			}
+		}
+	}
+	
+	// For each town zone, ensure it has at least one connection if possible
+	for(auto townZone : zonesWithTowns)
+	{
+		// Skip if the town zone already has a TRUE road
+		if(vstd::contains(trueRoads, townZone) && !trueRoads[townZone].empty())
+		{
+			continue;
+		}
+		
+		// Check if the town has any valid connections
+		if(!vstd::contains(existingConnections, townZone) || existingConnections[townZone].empty())
+		{
+			logGlobal->warn("Town zone %d has no valid connections in template", townZone);
+			continue;
+		}
+		
+		// Find the best random connection to mark as TRUE
+		bool foundConnection = false;
+		
+		for(auto & zonePtr : zones)
+		{
+			if(foundConnection) break;
+			
+			for(auto & connection : zonePtr.second->getConnections())
+			{
+				if(connection.getRoadOption() != rmg::ERoadOption::ROAD_RANDOM)
+					continue;
+					
+				auto zoneA = connection.getZoneA();
+				auto zoneB = connection.getZoneB();
+				
+				// Check if this connection involves our town zone
+				if(zoneA == townZone || zoneB == townZone)
 				{
-					roadGraph[connection.getZoneA()][connection.getZoneB()]++;
-					roadGraph[connection.getZoneB()][connection.getZoneA()]++;
-					randomConnections.insert(connection);
+					// Found a connection for this town zone, mark it as TRUE
+					zonePtr.second->setRoadOption(connection.getId(), rmg::ERoadOption::ROAD_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);
+					
+					// Update trueRoads map
+					trueRoads[zoneA].insert(zoneB);
+					trueRoads[zoneB].insert(zoneA);
+					
+					foundConnection = true;
+					break;
 				}
-				// ROAD_FALSE connections are ignored
 			}
 		}
-		logGlobal->info("Remaining random connections: %d", randomConnections.size());
-
-		if(randomConnections.empty())
-			break;
-
-		//Convert to vector for shuffling
-		std::vector<rmg::ZoneConnection> shuffledConnections(randomConnections.begin(), randomConnections.end());
-		RandomGeneratorUtil::randomShuffle(shuffledConnections, *rand);
-
-		//Try each random connection in shuffled order
-		for(const auto & conn : shuffledConnections)
+	}
+	
+	// Union-find data structure for MST algorithm
+	std::map<TRmgTemplateZoneId, TRmgTemplateZoneId> parent;
+	
+	// Initialize all town zones as separate sets
+	for(auto townZone : zonesWithTowns)
+	{
+		parent[townZone] = townZone;
+	}
+	
+	// Process TRUE roads first to build initial MST
+	// Add all TRUE roads that directly connect towns to the MST
+	for(auto & zonePtr : zones)
+	{
+		for(auto & connection : zonePtr.second->getConnections())
 		{
-			auto zoneA = conn.getZoneA();
-			auto zoneB = conn.getZoneB();
-
-			//Check if either zone would become isolated by removing this connection
-			if(roadGraph[zoneA].size() <= 1 || roadGraph[zoneB].size() <= 1)
+			if(connection.getRoadOption() == rmg::ERoadOption::ROAD_TRUE)
 			{
-				//Can't remove this connection as it would isolate a zone
-				continue;
+				auto id = connection.getId();
+				auto zoneA = connection.getZoneA();
+				auto zoneB = connection.getZoneB();
+				
+				// Skip if we've already processed this connection
+				if(processedConnectionIds.find(id) != processedConnectionIds.end())
+					continue;
+					
+				processedConnectionIds.insert(id);
+				
+				// If both ends have towns, add to MST
+				if(vstd::contains(zonesWithTowns, zoneA) && vstd::contains(zonesWithTowns, zoneB))
+				{
+					unionSets(parent, zoneA, zoneB);
+					
+					logGlobal->info("Adding TRUE road between town zones %d and %d to MST", zoneA, zoneB);
+				}
 			}
-
-			bool multipleConnections = roadGraph[zoneA][zoneB] > 1;
-
-			//Check if this is the last road between these zones
-			if(!multipleConnections)
+		}
+	}
+	
+	// Find all paths through TRUE roads
+	std::map<TRmgTemplateZoneId, std::set<TRmgTemplateZoneId>> reachableFromTown;
+	
+	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();
+			
+			// Add this zone to reachable list for the town
+			reachableFromTown[townZone].insert(current);
+			
+			// Check all TRUE road neighbors
+			if(vstd::contains(trueRoads, current))
 			{
-				//Temporarily remove this connection
-				roadGraph[zoneA].erase(zoneB);
-				roadGraph[zoneB].erase(zoneA);
+				for(auto neighbor : trueRoads[current])
+				{
+					if(visited.find(neighbor) == visited.end())
+					{
+						visited.insert(neighbor);
+						q.push(neighbor);
+						
+						// If this is another town, update MST
+						if(vstd::contains(zonesWithTowns, neighbor))
+						{
+							unionSets(parent, townZone, neighbor);
+							
+							logGlobal->info("Adding multi-zone TRUE path between town zones %d and %d to MST", 
+											townZone, neighbor);
+						}
+					}
+				}
+			}
+		}
+	}
+	
+	// Now process RANDOM roads to complete the MST
+	// 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 we've already processed this connection
+				if(processedConnectionIds.find(id) != processedConnectionIds.end())
+					continue;
+					
+				processedConnectionIds.insert(id);
+				
+				// Add to random roads vector
+				randomRoads.push_back(std::make_pair(id, std::make_pair(zoneA, zoneB)));
+			}
+		}
+	}
+	
+	// Process random roads - prioritize direct connections between towns
+	for(auto& road : randomRoads)
+	{
+		auto id = road.first;
+		auto zoneA = road.second.first;
+		auto zoneB = road.second.second;
+		
+		// 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);
+				
+				// Set road to TRUE
+				bool found = false;
+				for(auto & zonePtr : zones)
+				{
+					for(auto & connection : zonePtr.second->getConnections())
+					{
+						if(connection.getId() == id)
+						{
+							zonePtr.second->setRoadOption(id, rmg::ERoadOption::ROAD_TRUE);
+							found = true;
+							break;
+						}
+					}
+					if(found) break;
+				}
+				
+				// Update trueRoads map
+				trueRoads[zoneA].insert(zoneB);
+				trueRoads[zoneB].insert(zoneA);
+				
+				logGlobal->info("Setting RANDOM road to TRUE for direct connection %d between town zones %d and %d", 
+								id, zoneA, zoneB);
 			}
 			else
 			{
-				//Decrement the connection count
-				roadGraph[zoneA][zoneB]--;
-				roadGraph[zoneB][zoneA]--;
+				// Already connected, set to FALSE
+				bool found = false;
+				for(auto & zonePtr : zones)
+				{
+					for(auto & connection : zonePtr.second->getConnections())
+					{
+						if(connection.getId() == id)
+						{
+							zonePtr.second->setRoadOption(id, rmg::ERoadOption::ROAD_FALSE);
+							found = true;
+							break;
+						}
+					}
+					if(found) break;
+				}
+				
+				logGlobal->info("Setting RANDOM road to FALSE for connection %d between town zones %d and %d (already connected)", 
+								id, zoneA, zoneB);
 			}
-
-			//Check if graph remains connected as a whole
-			bool canRemove = true;
-
-			// Multiple connections imply this one can be removed without checking
-			if (!multipleConnections)
+			continue;
+		}
+		
+		// Special case: either zone is a town that has no other connections yet
+		if((vstd::contains(zonesWithTowns, zoneA) && (!vstd::contains(trueRoads, zoneA) || trueRoads[zoneA].empty())) ||
+		   (vstd::contains(zonesWithTowns, zoneB) && (!vstd::contains(trueRoads, zoneB) || trueRoads[zoneB].empty())))
+		{
+			// Always mark this as TRUE - it's the only connection for an isolated town
+			bool found = false;
+			for(auto & zonePtr : zones)
 			{
-				std::set<TRmgTemplateZoneId> visited;
-
-				// Get all zones that have road connections
-				std::set<TRmgTemplateZoneId> zonesWithRoads;
-				for(const auto & entry : roadGraph)
+				for(auto & connection : zonePtr.second->getConnections())
 				{
-					if(!entry.second.empty())
+					if(connection.getId() == id)
 					{
-						zonesWithRoads.insert(entry.first);
+						zonePtr.second->setRoadOption(id, rmg::ERoadOption::ROAD_TRUE);
+						found = true;
+						break;
 					}
 				}
-
-				if(!zonesWithRoads.empty())
+				if(found) break;
+			}
+			
+			// Update trueRoads map
+			trueRoads[zoneA].insert(zoneB);
+			trueRoads[zoneB].insert(zoneA);
+			
+			// If either is a town zone, note it specially in the log
+			auto townZoneId = vstd::contains(zonesWithTowns, zoneA) ? zoneA : zoneB;
+			auto otherZoneId = vstd::contains(zonesWithTowns, zoneA) ? zoneB : zoneA;
+			
+			logGlobal->info("Setting RANDOM road to TRUE for connection %d - only connection for isolated town zone %d", 
+							id, townZoneId);
+			continue;
+		}
+		
+		// If one zone has a town and one doesn't
+		TRmgTemplateZoneId townZone = vstd::contains(zonesWithTowns, zoneA) ? zoneA : 
+									   vstd::contains(zonesWithTowns, zoneB) ? zoneB : 0;
+		TRmgTemplateZoneId nonTownZone = vstd::contains(zonesWithTowns, zoneA) ? zoneB : 
+										 vstd::contains(zonesWithTowns, zoneB) ? zoneA : 0;
+		
+		if(townZone && nonTownZone)
+		{
+			// See if this non-town zone can connect to any other town
+			std::set<TRmgTemplateZoneId> possibleConnections;
+			
+			for(auto & otherZonePtr : zones)
+			{
+				if(otherZonePtr.first == nonTownZone) continue;
+				
+				// Check if there's a possible connection to another zone
+				if(vstd::contains(existingConnections, nonTownZone) && 
+				   vstd::contains(existingConnections[nonTownZone], otherZonePtr.first))
 				{
-					//Start DFS from any zone that has roads
-					TRmgTemplateZoneId startZone = *zonesWithRoads.begin();
-					
-					std::stack<TRmgTemplateZoneId> stack;
-					stack.push(startZone);
-
-					while(!stack.empty())
+					// If this other zone is a town or can reach other towns, note it
+					if(vstd::contains(zonesWithTowns, otherZonePtr.first) && otherZonePtr.first != townZone)
 					{
-						auto current = stack.top();
-						stack.pop();
-
-						if(vstd::contains(visited, current))
-							continue;
-
-						visited.insert(current);
-
-						for(auto & neighbor : roadGraph[current])
+						possibleConnections.insert(otherZonePtr.first);
+					}
+				}
+			}
+			
+			if(!possibleConnections.empty())
+			{
+				// This non-town zone can potentially connect to other towns
+				// Check if any of these towns aren't already connected to our town
+				bool canCreateNewConnection = false;
+				
+				for(auto otherTown : possibleConnections)
+				{
+					if(findSet(parent, townZone) != findSet(parent, otherTown))
+					{
+						canCreateNewConnection = true;
+						break;
+					}
+				}
+				
+				// Set road to TRUE if it could create a new connection
+				// This ensures isolated town zones get connected through non-town zones
+				bool found = false;
+				for(auto & zonePtr : zones)
+				{
+					for(auto & connection : zonePtr.second->getConnections())
+					{
+						if(connection.getId() == id)
 						{
-							if(!vstd::contains(visited, neighbor.first))
+							if(canCreateNewConnection)
+							{
+								zonePtr.second->setRoadOption(id, rmg::ERoadOption::ROAD_TRUE);
+								logGlobal->info("Setting RANDOM road to TRUE for connection %d - potential path between town zones", id);
+							}
+							else
 							{
-								stack.push(neighbor.first);
+								zonePtr.second->setRoadOption(id, rmg::ERoadOption::ROAD_FALSE);
+								logGlobal->info("Setting RANDOM road to FALSE for connection %d - won't create new town connections", id);
 							}
+							found = true;
+							break;
 						}
 					}
-
-					//Check if all zones with roads are still reachable
-					for(auto zoneId : zonesWithRoads)
+					if(found) break;
+				}
+				
+				if(canCreateNewConnection)
+				{
+					// Update trueRoads map
+					trueRoads[zoneA].insert(zoneB);
+					trueRoads[zoneB].insert(zoneA);
+				}
+			}
+			else
+			{
+				// This is a dead-end connection to a town zone - keep for connectivity
+				bool found = false;
+				for(auto & zonePtr : zones)
+				{
+					for(auto & connection : zonePtr.second->getConnections())
 					{
-						if(!vstd::contains(visited, zoneId))
+						if(connection.getId() == id)
 						{
-							canRemove = false;
+							zonePtr.second->setRoadOption(id, rmg::ERoadOption::ROAD_TRUE);
+							found = true;
 							break;
 						}
 					}
+					if(found) break;
 				}
+				
+				// Update trueRoads map
+				trueRoads[zoneA].insert(zoneB);
+				trueRoads[zoneB].insert(zoneA);
+				
+				logGlobal->info("Setting RANDOM road to TRUE for connection %d - road to town zone %d", 
+								id, townZone);
 			}
-
-			if(!canRemove)
+		}
+		else
+		{
+			// This is a connection between non-town zones
+			// Set it to FALSE by default
+			bool found = false;
+			for(auto & zonePtr : zones)
 			{
-				//Restore connection and try next one
-				roadGraph[zoneA][zoneB]++;
-				roadGraph[zoneB][zoneA]++;
-				continue;
+				for(auto & connection : zonePtr.second->getConnections())
+				{
+					if(connection.getId() == id)
+					{
+						zonePtr.second->setRoadOption(id, rmg::ERoadOption::ROAD_FALSE);
+						found = true;
+						break;
+					}
+				}
+				if(found) break;
 			}
-
-			//Found a connection we can remove - update in both zones that contain it
-			auto & zonePtr = zones[zoneA];
-			zonePtr->setRoadOption(conn.getId(), rmg::ERoadOption::ROAD_FALSE);
-
-			auto & otherZonePtr = zones[zoneB];
-			otherZonePtr->setRoadOption(conn.getId(), rmg::ERoadOption::ROAD_FALSE);
-
-			logGlobal->info("Dropped random road between %d and %d", zoneA, zoneB);
-			anyDropped = true;
-			break; //Exit loop and rebuild graph
+			
+			logGlobal->info("Setting RANDOM road to FALSE for connection %d between non-town zones %d and %d", 
+							id, zoneA, zoneB);
 		}
-	} while(anyDropped);
-
+	}
+	
+	// Update town connectivity after adding all roads
+	// Re-run BFS from each town zone through TRUE roads to see if we need more connections
+	std::map<TRmgTemplateZoneId, std::set<TRmgTemplateZoneId>> connectedTownGroups;
+	
+	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();
+			
+			// Check all TRUE road neighbors
+			if(vstd::contains(trueRoads, current))
+			{
+				for(auto neighbor : trueRoads[current])
+				{
+					if(visited.find(neighbor) == visited.end())
+					{
+						visited.insert(neighbor);
+						q.push(neighbor);
+						
+						// If this is another town, add to connected group
+						if(vstd::contains(zonesWithTowns, neighbor))
+						{
+							connectedTownGroups[townZone].insert(neighbor);
+						}
+					}
+				}
+			}
+		}
+	}
 	
-	// Mark all remaining random connections as TRUE
+	// Set any remaining RANDOM roads to FALSE
 	for(auto & zonePtr : zones)
 	{
 		for(auto & connection : zonePtr.second->getConnections())
-		{			
+		{
 			if(connection.getRoadOption() == rmg::ERoadOption::ROAD_RANDOM)
 			{
 				auto id = connection.getId();
-				zonePtr.second->setRoadOption(id, rmg::ERoadOption::ROAD_TRUE);
+				zonePtr.second->setRoadOption(id, rmg::ERoadOption::ROAD_FALSE);
+				logGlobal->info("Setting remaining RANDOM road to FALSE for connection %d", id);
 			}
 		}
 	}
+	
+	// Log the final town connectivity information
+	logGlobal->info("======== Town Connectivity Report ========");
+	for(auto townZone : zonesWithTowns)
+	{
+		std::string connections;
+		
+		// Find all adjacent zones connected by roads
+		std::set<TRmgTemplateZoneId> adjacentConnections;
+		
+		for(auto & zonePtr : zones)
+		{
+			if(zonePtr.first == townZone)
+			{
+				for(auto & connection : zonePtr.second->getConnections())
+				{
+					if(connection.getRoadOption() == rmg::ERoadOption::ROAD_TRUE)
+					{
+						auto otherZoneId = connection.getOtherZoneId(townZone);
+						adjacentConnections.insert(otherZoneId);
+					}
+				}
+			}
+		}
+		
+		if(!adjacentConnections.empty())
+		{
+			for(auto connectedZone : adjacentConnections)
+			{
+				if(!connections.empty())
+					connections += ", ";
+				connections += std::to_string(connectedZone);
+			}
+		}
+		
+		if(connections.empty())
+			connections = "None (no road connections)";
+		
+		logGlobal->info("Zone %d with town connected to adjacent zones: %s", townZone, connections);
+	}
+	
+	logGlobal->info("Finished road generation - created minimal spanning tree connecting all towns");
 }
 
 const TDistanceMap& CZonePlacer::getDistanceMap()