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() }); }