示例#1
0
        public static HexamazeInfo GenerateHexamaze()
        {
            const int mazeSize      = 12;
            const int smallMazeSize = 4;

            var rnd = new Random(26);   // This random seed gives the most walls (all but 501)
            var totalWallsRemoved = 0;
            var walls             = new AutoDictionary <Hex, bool[]>(_ => new bool[3] {
                true, true, true
            });
            var removeWall = Ut.Lambda((Hex hex, int n) => { walls[n < 3 ? hex : hex.Neighbors[n]][n % 3] = false; totalWallsRemoved++; });
            var hasWall    = Ut.Lambda((Hex hex, int n) => walls[n < 3 ? hex : hex.Neighbors[n]][n % 3]);
            var stack      = new Stack <Hex>();
            Hex curHex     = new Hex(0, 0);

            stack.Push(curHex);
            var taken = new HashSet <Hex> {
                curHex
            };
            var markings = new HashSet <Hex>();

            // Step 1: generate a single giant maze
            while (true)
            {
                var neighbors = curHex.Neighbors;
                var availableNeighborIndices = neighbors.SelectIndexWhere(n => !taken.Contains(n) && n.Distance < mazeSize).ToArray();
                if (availableNeighborIndices.Length == 0)
                {
                    if (stack.Count == 0)
                    {
                        break;
                    }
                    curHex = stack.Pop();
                    continue;
                }
                var nextNeighborIndex = availableNeighborIndices[rnd.Next(availableNeighborIndices.Length)];
                removeWall(curHex, nextNeighborIndex);
                stack.Push(curHex);
                curHex = neighbors[nextNeighborIndex];
                taken.Add(curHex);
            }

            // Step 2: Go through all submazes and make sure they’re all connected and all have at least one exit on each side
            while (true)
            {
                var candidateCounts = new Dictionary <Tuple <Hex, int>, int>();

                foreach (var centerHex in Hex.LargeHexagon(mazeSize - smallMazeSize + 1))
                {
                    var filled = new HashSet <Hex> {
                        centerHex
                    };
                    var queue          = filled.ToQueue();
                    var edgesReachable = new bool[6];

                    // Flood-fill as much of the maze as possible
                    while (queue.Count > 0)
                    {
                        var hex = queue.Dequeue();
                        var ns  = hex.Neighbors;
                        for (int n = 0; n < 6; n++)
                        {
                            var offset = ns[n] - centerHex;
                            if (offset.Distance < smallMazeSize && !hasWall(hex, n) && filled.Add(ns[n]))
                            {
                                queue.Enqueue(ns[n]);
                            }
                            if (offset.Distance == smallMazeSize && !hasWall(hex, n))
                            {
                                foreach (var edge in offset.GetEdges(smallMazeSize))
                                {
                                    edgesReachable[edge] = true;
                                }
                            }
                        }
                    }

                    var isHexAllFilled       = filled.Count >= 3 * smallMazeSize * (smallMazeSize - 1) + 1;
                    var areAllEdgesReachable = !edgesReachable.Contains(false);

                    if (!isHexAllFilled || !areAllEdgesReachable)
                    {
                        // Consider removing a random wall
                        var candidates1 = filled.SelectMany(fh => fh.Neighbors.Select((th, n) => new { FromHex = fh, Direction = n, ToHex = th, Offset = th - centerHex }))
                                          .Where(inf =>
                                                 (inf.Offset.Distance < smallMazeSize && hasWall(inf.FromHex, inf.Direction) && !filled.Contains(inf.ToHex)) ||
                                                 (inf.Offset.Distance == smallMazeSize && inf.Offset.GetEdges(smallMazeSize).Any(e => !edgesReachable[e])))
                                          .ToArray();
                        foreach (var candidate in candidates1)
                        {
                            candidateCounts.IncSafe(Tuple.Create(candidate.Direction < 3 ? candidate.FromHex : candidate.ToHex, candidate.Direction % 3));
                        }
                        if (candidates1[0].Offset.Distance < smallMazeSize)
                        {
                            filled.Add(candidates1[0].ToHex);
                            queue.Enqueue(candidates1[0].ToHex);
                        }
                        else
                        {
                            foreach (var edge in candidates1[0].Offset.GetEdges(smallMazeSize))
                            {
                                edgesReachable[edge] = true;
                            }
                        }
                    }
                }

                if (candidateCounts.Count == 0)
                {
                    break;
                }

                //*
                // Remove one wall out of the “most wanted”
                var topScores       = candidateCounts.Values.Distinct().Order().TakeLast(1).ToArray();
                var candidates2     = candidateCounts.Where(kvp => topScores.Contains(kvp.Value)).ToArray();
                var randomCandidate = candidates2[rnd.Next(candidates2.Length)];
                removeWall(randomCandidate.Key.Item1, randomCandidate.Key.Item2);

                /*/
                 * // Remove any one wall
                 * var candidates2 = candidateCounts.Keys.ToArray();
                 * var randomCandidate = candidates2[rnd.Next(candidates2.Length)];
                 * removeWall(randomCandidate.Item1, randomCandidate.Item2);
                 * /**/

                Console.Write($"Walls removed: {totalWallsRemoved}  \r");
            }

            // Step 3: Put as many walls back in as possible
            var missingWalls = walls.SelectMany(kvp => kvp.Value.Select((w, i) => new { Hex = kvp.Key, Index = i, IsWall = w })).Where(inf => !inf.IsWall).ToList();

            while (missingWalls.Count > 0)
            {
                var randomMissingWallIndex = rnd.Next(missingWalls.Count);
                var randomMissingWall      = missingWalls[randomMissingWallIndex];
                missingWalls.RemoveAt(randomMissingWallIndex);
                walls[randomMissingWall.Hex][randomMissingWall.Index] = true;

                bool possible = true;
                foreach (var centerHex in Hex.LargeHexagon(mazeSize - smallMazeSize + 1))
                {
                    var filled = new HashSet <Hex> {
                        centerHex
                    };
                    var queue          = filled.ToQueue();
                    var edgesReachable = new bool[6];

                    // Flood-fill as much of the maze as possible
                    while (queue.Count > 0)
                    {
                        var hex = queue.Dequeue();
                        var ns  = hex.Neighbors;
                        for (int n = 0; n < 6; n++)
                        {
                            var offset = ns[n] - centerHex;
                            if (offset.Distance < smallMazeSize && !hasWall(hex, n) && filled.Add(ns[n]))
                            {
                                queue.Enqueue(ns[n]);
                            }
                            if (offset.Distance == smallMazeSize && !hasWall(hex, n))
                            {
                                foreach (var edge in offset.GetEdges(smallMazeSize))
                                {
                                    edgesReachable[edge] = true;
                                }
                            }
                        }
                    }

                    if (filled.Count < 3 * smallMazeSize * (smallMazeSize - 1) + 1 || edgesReachable.Contains(false))
                    {
                        // This wall cannot be added, take it back out.
                        walls[randomMissingWall.Hex][randomMissingWall.Index] = false;
                        possible = false;
                        break;
                    }
                }

                if (possible)
                {
                    totalWallsRemoved--;
                    Console.Write($"Walls removed: {totalWallsRemoved}  \r");
                }
            }

            Console.WriteLine();

            return(new HexamazeInfo
            {
                Size = mazeSize,
                SubmazeSize = smallMazeSize,
                Markings = markings.Select((h, ix) => new { Hex = h, Index = ix }).ToDictionary(inf => inf.Hex, inf => (Marking)(inf.Index)),
                Walls = walls.ToDictionary()
            });
        }