public static GeneratedMaze Generate(MonoRandom rnd)
        {
            // PART 1: GENERATE MAZE (walls)
            var walls = Ut.NewArray(3 * sw * sw, ix =>
            {
                var r = (ix / 3) % sw - Size;
                var q = (ix / 3) / sw - Size;
                var h = new Hex(q, r);
                return(h.Distance < Size || h.GetNeighbor(ix % 3).Distance < Size);
            });
            var allWalls = (bool[])walls.Clone();

            var stack  = new Stack <Hex>();
            var curHex = new Hex(0, 0);

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

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

            // Step 1.2: Go through all submazes and make sure they’re all connected and all have at least one exit on each side
            // This is parallelizable and uses multiple threads
            var allSubmazes = Hex.LargeHexagon(Size - SubmazeSize + 1).Select(h => (Hex?)h).ToArray();
            Hex?lastHex1 = null, lastHex2 = null;

            while (true)
            {
                var candidateCounts = new Dictionary <int, int>();

                for (var smIx = 0; smIx < allSubmazes.Length; smIx++)
                {
                    if (allSubmazes[smIx] == null)
                    {
                        continue;
                    }

                    var centerHex = allSubmazes[smIx].Value;

                    // We do not need to examine this submaze if the wall we last removed isn’t even in it
                    if (lastHex1 != null && (lastHex1.Value - centerHex).Distance > SubmazeSize &&
                        lastHex2 != null && (lastHex2.Value - centerHex).Distance > SubmazeSize)
                    {
                        continue;
                    }

                    var validity = DetermineSubmazeValidity(centerHex, walls);
                    if (validity.IsValid)
                    {
                        allSubmazes[smIx] = null;
                        continue;
                    }

                    // Find out which walls might benefit from removing
                    foreach (var fh in validity.Filled)
                    {
                        var neighbors = fh.Neighbors;
                        for (var dir = 0; dir < neighbors.Length; dir++)
                        {
                            var th     = neighbors[dir];
                            var offset = th - centerHex;
                            if ((offset.Distance < SubmazeSize && walls[wallIndex(fh, dir)] && !validity.Filled.Contains(th)) ||
                                (offset.Distance == SubmazeSize && offset.GetEdges(SubmazeSize).Any(e => !validity.EdgesReachable[e])))
                            {
                                candidateCounts.IncSafe(wallIndex(fh, dir));
                            }
                        }
                    }
                }

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

                // Remove one wall out of the “most wanted”
                var topScore   = 0;
                var topScorers = new List <int>();
                foreach (var kvp in candidateCounts)
                {
                    if (kvp.Value > topScore)
                    {
                        topScore = kvp.Value;
                        topScorers.Clear();
                        topScorers.Add(kvp.Key);
                    }
                    else if (kvp.Value == topScore)
                    {
                        topScorers.Add(kvp.Key);
                    }
                }
                topScorers.Sort();
                var randomWall = topScorers[rnd.Next(0, topScorers.Count)];
                walls[randomWall] = false;

                var rcdir = randomWall % 3;
                lastHex1 = new Hex((randomWall / 3) / sw - Size, (randomWall / 3) % sw - Size);
                lastHex2 = lastHex1.Value.GetNeighbor(rcdir);
            }

            // Step 1.3: Put as many walls back in as possible
            var missingWalls = Enumerable.Range(0, allWalls.Length).Where(ix => allWalls[ix] && !walls[ix]).ToList();

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

                var affectedHex1 = new Hex((randomMissingWall / 3) / sw - Size, (randomMissingWall / 3) % sw - Size);
                var affectedHex2 = affectedHex1.GetNeighbor(randomMissingWall % 3);

                foreach (var centerHex in rnd.ShuffleFisherYates(Hex.LargeHexagon(Size - SubmazeSize + 1).ToList()))
                {
                    // We do not need to examine this submaze if the wall we put in isn’t even in it
                    if (((affectedHex1 - centerHex).Distance <= SubmazeSize ||
                         (affectedHex2 - centerHex).Distance <= SubmazeSize) &&
                        !DetermineSubmazeValidity(centerHex, walls).IsValid)
                    {
                        // This wall cannot be added, take it back out.
                        walls[randomMissingWall] = false;
                        break;
                    }
                }
            }

            // PART 2: GENERATE MARKINGS
tryAgain:
            var markings = new Marking[sw * sw];
            // List Circle and Hexagon twice so that triangles don’t completely dominate the distribution
            var allowedMarkings = new[] { Marking.Circle, Marking.Circle, Marking.Hexagon, Marking.Hexagon, Marking.TriangleDown, Marking.TriangleLeft, Marking.TriangleRight, Marking.TriangleUp };

            // Step 2.1: Put random markings in until there are no more ambiguities
            while (!areMarkingsUnique(markings))
            {
                var availableHexes = Hex.LargeHexagon(Size)
                                     .Where(h => markings[markingIndex(h)] == Marking.None &&
                                            h.Neighbors.SelectMany(n1 => n1.Neighbors)
                                            .All(n2 => n2.Distance >= Size || markings[markingIndex(n2)] == Marking.None))
                                     .ToArray();
                if (availableHexes.Length == 0)
                {
                    goto tryAgain;
                }
                var randomHex = availableHexes[rnd.Next(0, availableHexes.Length)];
                markings[markingIndex(randomHex)] = allowedMarkings[rnd.Next(0, allowedMarkings.Length)];
            }

            // Step 2.2: Find markings to remove again
            var removableMarkings = markings.SelectIndexWhere(m => m != Marking.None).ToList();

            while (removableMarkings.Count > 0)
            {
                var tryRemoveIndex = rnd.Next(0, removableMarkings.Count);
                var tryRemove      = removableMarkings[tryRemoveIndex];
                removableMarkings.RemoveAt(tryRemoveIndex);
                var prevMarking = markings[tryRemove];
                markings[tryRemove] = Marking.None;
                if (!areMarkingsUnique(markings))
                {
                    // No longer unique — put it back in
                    markings[tryRemove] = prevMarking;
                }
            }

            return(new GeneratedMaze(walls, markings));
        }