/// <summary> /// Label all nodes according to whether they are (1) on the shortest path, (2) are on a loop to the shortest path, /// (3) are on a path resulting in a dead end, and (4) are not connected to the maze. /// All labels are inclusive, i.e. the starting point of a dead end is a node labeled as on the shorted path. /// The shortest path is found using Dijkstra's algorithm. /// </summary> /// <returns>List of nodes with path labels.</returns> /// <param name="mazeFrame">List of nodes.</param> protected void LabelNodesWRTPath(ref MazeFrame mazeFrame) { // // This function labels each node according to the following booleans: // // onShortestPath - whether node is on the path found using psuedo Dijkstra's method // onLoop - whether node is on path that diverges from the shortest path, but returns to it eventually // onDeadEnd - whether the node is on a dead end, defined as a path that (1) diverges from the shortest path and doesn't return // notConnectedToPath - whether node has a connection on path or is superfluous visual filler // (junction nodes have more than one label) // Find shortest path from beginning to end using Dijkstra's algorithm // From wikipedia/other: /* * 1) Mark all nodes unvisited, mark selected initial node with a current distance of 0 and the rest with infinity. * 2) Set the non-visited node with the smallest current distance as the current node C. * 3) For each neighbour N of current node C: add the current distance of C with the weight of the edge * connecting C-N. If it's smaller than the current distance of N, set it as the new current distance of N. * 4) Mark the current node C as visited. * 5) If end not touched yet, and if there are non-visited nodes, go to step 2. */ // Check start/end node active status if (!mazeFrame.StartNode.IsActive || !mazeFrame.EndNode.IsActive) { throw new System.Exception("Start/end nodes are required to be active."); } // Prep storage of labels and initilaze as false int activeNodeCount = 0; foreach (MazeNode nd in mazeFrame.Nodes) { if (nd.IsActive) { activeNodeCount++; } } Dictionary <string, bool> onShortestPath = new Dictionary <string, bool>(activeNodeCount); Dictionary <string, bool> onDeadEnd = new Dictionary <string, bool>(activeNodeCount); Dictionary <string, bool> onLoop = new Dictionary <string, bool>(activeNodeCount); Dictionary <string, bool> notConnectedToPath = new Dictionary <string, bool>(activeNodeCount); foreach (MazeNode nd in mazeFrame.Nodes) { if (nd.IsActive) { onShortestPath.Add(nd.Identifier, false); onDeadEnd.Add(nd.Identifier, false); onLoop.Add(nd.Identifier, false); notConnectedToPath.Add(nd.Identifier, false); } } // Find start and end node MazeNode startNode = mazeFrame.StartNode; MazeNode endNode = mazeFrame.EndNode; // Create bookkeeping for visits and set all nodes to unvisitied Dictionary <string, bool> isVisited = new Dictionary <string, bool>(mazeFrame.Nodes.Count); foreach (MazeNode nd in mazeFrame.Nodes) { if (nd.IsActive) { isVisited.Add(nd.Identifier, false); } else { isVisited.Add(nd.Identifier, true); } } // Ignore nodes without neighbors foreach (MazeNode nd in mazeFrame.Nodes) { if (nd.ConnectedNeighbors.Count == 0) { isVisited[nd.Identifier] = true; } } // Set distance to start, and initialize at infinity Dictionary <string, float> distToStart = new Dictionary <string, float>(activeNodeCount); foreach (MazeNode nd in mazeFrame.Nodes) { if (nd.IsActive) { distToStart.Add(nd.Identifier, float.PositiveInfinity); } } // Start search MazeNode currentNode; distToStart[startNode.Identifier] = 0; int count = 0; bool endFound = false; while (!endFound) { // Set current unvisited node based on distance to start int currentNodeInd = -1; float lastDist = float.PositiveInfinity; for (int i = 0; i < mazeFrame.Nodes.Count; i++) { if (!isVisited[mazeFrame.Nodes[i].Identifier]) { float currDist = distToStart[mazeFrame.Nodes[i].Identifier]; if (currDist <= lastDist) { currentNodeInd = i; lastDist = currDist; } } } currentNode = mazeFrame.Nodes[currentNodeInd]; // Find unvisited neighbors of current node List <MazeNode> unvisitedNeighbors = new List <MazeNode>(); foreach (MazeNode nb in currentNode.ConnectedNeighbors) { if (isVisited[nb.Identifier] == false) { unvisitedNeighbors.Add(nb); } } // For each neighbor, set distToStart as min of (currentNode disttostart + dist to current) and neighbor disttostart foreach (MazeNode nb in unvisitedNeighbors) { // Distance in case of non-equidistant nodes //float distToCurrentNode = Vector3.Distance(currentNode.position, nb.position); // Distance in case of equidistant nodes float distToCurrentNode = 1; // Update neighbor distToStart[nb.Identifier] = Mathf.Min((distToStart[currentNode.Identifier] + distToCurrentNode), distToStart[nb.Identifier]); } // Set current node to visisted isVisited[currentNode.Identifier] = true; // end reached? endFound |= currentNode == endNode; // Safety check count++; if (count > mazeFrame.Nodes.Count) { throw new System.Exception("Finding shortest path loop did not terminate correctly."); } } // Store shortest path length in maze frame now that we know it int shortestPathLength = (int)distToStart[endNode.Identifier]; // Backtrack along distToStart and label each node as being on the shortest path currentNode = endNode; onShortestPath[currentNode.Identifier] = true; count = 0; while (currentNode != startNode) { // Find neighbor with shortest distance to start MazeNode nextNodeOnPath = currentNode; foreach (MazeNode nb in currentNode.ConnectedNeighbors) { if (nb.IsActive && distToStart[nb.Identifier] < distToStart[nextNodeOnPath.Identifier]) // There is ALWAYS a node closer than the current one { nextNodeOnPath = nb; } } // Update and continue currentNode = nextNodeOnPath; onShortestPath[currentNode.Identifier] = true; // Safety check count++; if (count > mazeFrame.Nodes.Count) { throw new System.Exception("Finding shortest path loop did not terminate correctly."); } } /* Label each node as onLoop/onDeadEnd/notConnectedToPath as follows: * * 1) Find all nodes that are adjacent to the path * 2) For each of these: * 3) Create stack to keep unlabeled nodes, and set allLabeled flag * 4) While !allLabeled * 5) Create list of unlabeled neighbors of current node that are not on the stack * 6) If current node has 1+ unlabeled neighbors --> explore * 7) Add current node to stack and set an unlabeled neighbor not on the stack as current node * 8) If current node has 0 unlabeled neighbors not on the stack --> label and backtrack * 9) If current node has only 1 connected neighbor --> onDeadEnd * 10) If more than one connected neighbors, and one was onLoop --> onLoop * 11) If more than one connected neighbors, and one of them was onShortestPath (or two if at seed node) --> onLoop (loop connection found!) * 12) If more than one connected neighbors, and no onLoop/onShortestPath --> onDeadEnd (backtracking from only dead ends) * 13) If stack is not empty, pop node and set as current node, go to 5) * 14) If stack is empty, all nodes are labeled, set allLabeled to true; * * 15) Set all junctions by finding all nodes adjacent to onDeadEnd/onLoop, and set them likewise. * 16) Set all unlabeled nodes to notConnectedToPath */ // First, find unlabeled nodes that are connected to the path (1) List <MazeNode> unlabeledSeedNode = new List <MazeNode>(); for (int i = 0; i < mazeFrame.Nodes.Count; i++) { if (mazeFrame.Nodes[i].IsActive && !onShortestPath[mazeFrame.Nodes[i].Identifier]) { foreach (MazeNode nb in mazeFrame.Nodes[i].ConnectedNeighbors) { if (nb.IsActive && onShortestPath[nb.Identifier]) { unlabeledSeedNode.Add(mazeFrame.Nodes[i]); break; } } } } // Start the search (2) for (int inode = 0; inode < unlabeledSeedNode.Count; inode++) { // set seed node MazeNode seedNode = unlabeledSeedNode[inode]; if (onDeadEnd[seedNode.Identifier] || onLoop[seedNode.Identifier]) // check whether node was touched from a previous cycle below { continue; } // Create stack for to be labeled nodes Stack <MazeNode> nodesToLabel = new Stack <MazeNode>(); // FIXME should I initialize with conservative estimate? // Search from the starting node until the stack is empty (all are labeled) (4) count = 0; currentNode = seedNode; bool allLabeled = false; while (!allLabeled) { // Parse current neighbors List <MazeNode> unlabeledNeighborsNotOnStack = new List <MazeNode>(); bool hasOnShortestPath = false; bool hasOnLoop = false; int shortestPathCount = 0; foreach (MazeNode nb in currentNode.ConnectedNeighbors) { if (nb.IsActive) { if (onShortestPath[nb.Identifier]) { shortestPathCount++; } else if (onLoop[nb.Identifier]) { hasOnLoop = true; } else if (onDeadEnd[nb.Identifier]) { } else { if (!nodesToLabel.Contains(nb)) { unlabeledNeighborsNotOnStack.Add(nb); } } } } // If we're at the seed node, the first onShortestPath node is ignored (it's the hook), otherwise any one is fine if (currentNode == seedNode) { hasOnShortestPath = shortestPathCount > 1; } else { hasOnShortestPath = shortestPathCount > 0; } // Move forward or label and backtrack // If current node has an unlabeled node not on the stack --> explore (6) if (unlabeledNeighborsNotOnStack.Count > 0) { // Add current node to stack and set first neighbor not on stack (order doesn't matter) as current node foreach (MazeNode nb in unlabeledNeighborsNotOnStack) { nodesToLabel.Push(currentNode); currentNode = nb; break; } } else // If there are no more unlabeled nodes that are not on the stack, we can label and backtrack { int activeConnectedNeighborsCount = 0; foreach (MazeNode nb in currentNode.ConnectedNeighbors) { if (nb.IsActive) { activeConnectedNeighborsCount++; } } if (activeConnectedNeighborsCount <= 1) // easiest case { onDeadEnd[currentNode.Identifier] = true; } else if (hasOnLoop || (hasOnShortestPath && currentNode != seedNode)) { onLoop[currentNode.Identifier] = true; } else { onDeadEnd[currentNode.Identifier] = true; } // Backtrack if there are still unlabeled, otherwise end if (nodesToLabel.Count != 0) { currentNode = nodesToLabel.Pop(); } else { allLabeled = true; } } // Safety check count++; if (count > mazeFrame.Nodes.Count * 2) { throw new System.Exception("Labeling nodes loop did not terminate correctly."); } } } // Set junctions // onDeadEnd junctions List <MazeNode> nodesNeighboringOnDeadEnd = new List <MazeNode>(); List <MazeNode> nodesNeighboringOnLoop = new List <MazeNode>(); for (int i = 0; i < mazeFrame.Nodes.Count; i++) { if (!mazeFrame.Nodes[i].IsActive) { continue; } // Every node neighboring an onDeadEnd is a junction for a dead ending path foreach (MazeNode nb in mazeFrame.Nodes[i].ConnectedNeighbors) { if (nb.IsActive && onDeadEnd[nb.Identifier]) { nodesNeighboringOnDeadEnd.Add(mazeFrame.Nodes[i]); break; } } // Only nodes that are onShortestPath neighboring an onLoop can be an onLoop junction if (onShortestPath[mazeFrame.Nodes[i].Identifier]) { foreach (MazeNode nb in mazeFrame.Nodes[i].ConnectedNeighbors) { if (nb.IsActive && onLoop[nb.Identifier]) { nodesNeighboringOnLoop.Add(mazeFrame.Nodes[i]); break; } } } } foreach (MazeNode nd in nodesNeighboringOnDeadEnd) { onDeadEnd[nd.Identifier] = true; } foreach (MazeNode nd in nodesNeighboringOnLoop) { onLoop[nd.Identifier] = true; } // Find remaining nodes, and label them as notConnectedToPath foreach (MazeNode nd in mazeFrame.Nodes) { if (nd.IsActive && !onShortestPath[nd.Identifier] && !onLoop[nd.Identifier] && !onDeadEnd[nd.Identifier]) { notConnectedToPath[nd.Identifier] = true; } } // Assign elements to maze frame int shortestPathInd = 0; bool indNotFound = true; while (indNotFound) { if (mazeFrame.ShortestPathInd.Contains(shortestPathInd)) { shortestPathInd++; } else { indNotFound = false; } } mazeFrame.SetShortestPathLength(shortestPathLength, shortestPathInd); mazeFrame.SetOnShortestPath(onShortestPath, shortestPathInd); mazeFrame.SetOnDeadEnd(onDeadEnd); mazeFrame.SetOnLoop(onLoop); mazeFrame.SetNotConnectedToPath(notConnectedToPath); }