From 0b1c3006b455ed46e81a9a35b476afba5c7f545e Mon Sep 17 00:00:00 2001 From: Ryan Kramer Date: Thu, 8 Jan 2026 12:15:05 -0600 Subject: [PATCH 01/10] see if Gemini can do this... --- .../Generators/Hills/NewHill.cs | 149 ++++++++++++++++++ src/Blocktavius.Core/Rect.cs | 2 + src/Blocktavius.Core/Shell.cs | 33 ++-- .../Mutations/PutHillMutation.cs | 2 +- 4 files changed, 167 insertions(+), 19 deletions(-) create mode 100644 src/Blocktavius.Core/Generators/Hills/NewHill.cs diff --git a/src/Blocktavius.Core/Generators/Hills/NewHill.cs b/src/Blocktavius.Core/Generators/Hills/NewHill.cs new file mode 100644 index 0000000..119b2f5 --- /dev/null +++ b/src/Blocktavius.Core/Generators/Hills/NewHill.cs @@ -0,0 +1,149 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Blocktavius.Core.Generators.Hills; + +public static class NewHill +{ + public sealed record Settings + { + public required PRNG PRNG { get; init; } + public int MaxElevation { get; init; } + public int MinElevation { get; init; } + } + + const int emptyValue = -1; + + public static I2DSampler BuildNewHill(Settings settings, Shell shell) + { + if (settings.MinElevation >= settings.MaxElevation) + { + throw new ArgumentException("MinElevation must be less than MaxElevation"); + } + + int expansion = settings.MaxElevation - settings.MinElevation; + var bounds = shell.IslandArea.Bounds.Expand(expansion * 2); + var array = new MutableArray2D(bounds, new HillItem() { Elevation = emptyValue, Slab = null }); + } + + record struct HillItem + { + public required int Elevation { get; init; } + public required Slab? Slab { get; init; } + } + + sealed class Slab + { + public required IReadOnlyList XZs { get; init; } + public required IReadOnlyList ParentSlabs { get; init; } + + /// + /// FUTURE - We may allow elevation to vary within the slab, but not yet. + /// + public required int MinElevation { get; init; } + + /// + /// If no parent slabs exist, set to 0. + /// Else set to 1 + ParentSlabs.Max(x => x.AncestorCount). + /// + public required int AncestorCount { get; init; } + } + + /// + /// Each tier is basically "add slabs until previous tier is fully covered." + /// + sealed class Tier + { + public required Tier? ParentTier { get; init; } + public required Settings Settings { get; init; } + public required IReadOnlyList Slabs { get; init; } + public required MutableArray2D Array { get; init; } + + /// + /// Must be set to Slabs.Min(slab => slab.MinElevation) when any slabs exist. + /// + public required int MinElevation { get; init; } + + public static Tier CreateFirstTier(Shell shell, Settings settings) + { + if (settings.MinElevation >= settings.MaxElevation) + { + throw new ArgumentException("MinElevation must be less than MaxElevation"); + } + + int expansion = settings.MaxElevation - settings.MinElevation; + var bounds = shell.IslandArea.Bounds.Expand(expansion * 2); + var array = new MutableArray2D(bounds, new HillItem() { Elevation = emptyValue, Slab = null }); + + foreach (var xz in shell.IslandArea.Bounds.Enumerate()) + { + if (shell.IslandArea.Sample(xz)) + { + array.Put(xz, new HillItem { Elevation = settings.MaxElevation, Slab = null }); + } + } + + return new Tier + { + ParentTier = null, + Settings = settings, + Slabs = [], + Array = array, + MinElevation = settings.MaxElevation, + }; + } + + public Tier CreateNextTier() + { + // It's possible we might create holes... let's ignore them for now until I can see an example. + var shellToCover = ShellLogic.ComputeShells(Array.Project(x => x.Elevation > emptyValue)) + .Where(shell => !shell.IsHole) + .Single(); + + var currentShell = shellToCover; + + // While shellToCover is not fully covered by slabs: + // * choose random run from currentShell such that it covers at least one item from shellToCover + // * create slab for that run + // - must find all parent slabs + // - elevation for that slab should be 1 less than min from all parents (or 1 less than max elevation if no parents) + // * add that slab to the mutable array + // * recompute currentShell + + // == EXAMPLE == + // Very rough idea of how to create a slab: + int length = 4 + Settings.PRNG.NextInt32(8); + int start = Settings.PRNG.NextInt32(currentShell.ShellItems.Count - length); + var exampleRun = currentShell.ShellItems.Skip(start).Take(length); + // TODO must ensure that exampleRun includes at least one XZ from shellToCover... + var parentSlabs = exampleRun.Select(item => Array.Sample(item.XZ.Step(item.InsideDirection))) + .Select(hillItem => hillItem.Slab) + .WhereNotNull() + .Distinct() + .ToList(); + + int ancestorCount; + int slabElevation; + if (parentSlabs.Any()) + { + ancestorCount = parentSlabs.Max(x => x.AncestorCount) + 1; + slabElevation = parentSlabs.Min(x => x.MinElevation) - 1; + } + else + { + ancestorCount = 0; + slabElevation = Settings.MaxElevation - 1; + } + var slab = new Slab + { + AncestorCount = ancestorCount, + MinElevation = slabElevation, + ParentSlabs = parentSlabs, + XZs = exampleRun.Select(x => x.XZ).ToList(), + }; + } + } +} diff --git a/src/Blocktavius.Core/Rect.cs b/src/Blocktavius.Core/Rect.cs index 29269f6..baafe6f 100644 --- a/src/Blocktavius.Core/Rect.cs +++ b/src/Blocktavius.Core/Rect.cs @@ -43,6 +43,8 @@ public record Rect(XZ start, XZ end) public bool IsZero => Size.X < 1 || Size.Z < 1; + public Rect Expand(int amount) => new Rect(this.start.Add(-amount, -amount), this.end.Add(amount, amount)); + public static Rect Union(IEnumerable boxes) => DoUnion(boxes); public static Rect Union(params IEnumerable[] dater) => DoUnion(dater); diff --git a/src/Blocktavius.Core/Shell.cs b/src/Blocktavius.Core/Shell.cs index 152721c..5d6f37c 100644 --- a/src/Blocktavius.Core/Shell.cs +++ b/src/Blocktavius.Core/Shell.cs @@ -97,7 +97,7 @@ public static class ShellLogic /// readonly record struct WalkState(XZ shellPosition, Direction insideDir) { - public WalkState Advance(IArea area, List itemCollector) + public WalkState Advance(I2DSampler area, List itemCollector) { itemCollector.Add(new ShellItem() { @@ -108,7 +108,7 @@ public WalkState Advance(IArea area, List itemCollector) var aheadDir = insideDir.TurnLeft90; var aheadPos = shellPosition.Add(aheadDir.Step); - if (area.InArea(aheadPos)) + if (area.Sample(aheadPos)) { // inside corner, stay at the same position and turn left itemCollector.Add(new ShellItem() @@ -119,7 +119,7 @@ public WalkState Advance(IArea area, List itemCollector) }); return new WalkState(shellPosition, aheadDir); } - else if (!area.InArea(aheadPos.Add(insideDir.Step))) + else if (!area.Sample(aheadPos.Add(insideDir.Step))) { // outside corner itemCollector.Add(new ShellItem() @@ -138,7 +138,7 @@ public WalkState Advance(IArea area, List itemCollector) } } - static bool TryBuildShell(IArea area, IslandInfo island, out List items) + static bool TryBuildShell(I2DSampler area, IslandInfo island, out List items) { items = new(); if (island.MustIncludeStates.Count == 0) @@ -171,12 +171,9 @@ static bool TryBuildShell(IArea area, IslandInfo island, out List ite /// Most of this logic must use the expanded bounds, since shell items /// can be outside of the area's bounds. /// - private static Rect ExpandBounds(IArea area) - { - return new Rect(area.Bounds.start.Add(-1, -1), area.Bounds.end.Add(1, 1)); - } + private static Rect ExpandBounds(I2DSampler area) => area.Bounds.Expand(1); - public static IReadOnlyList ComputeShells(IArea area) + public static IReadOnlyList ComputeShells(I2DSampler area) { List shells = new(); @@ -217,14 +214,14 @@ sealed class IslandInfo public required I2DSampler IslandArea { get; init; } } - private static List FindIslands(IArea area) + private static List FindIslands(I2DSampler area) { List infos = new(); const int Empty = -1; var islandFullMap = new MutableArray2D(area.Bounds, Empty); - foreach (var xz in area.Bounds.Enumerate().Where(area.InArea)) + foreach (var xz in area.Bounds.Enumerate().Where(area.Sample)) { if (islandFullMap.Sample(xz) == Empty) { @@ -243,7 +240,7 @@ private static List FindIslands(IArea area) foreach (var dir in allDirections) { var neighbor = current.Add(dir.Step); - if (area.InArea(neighbor)) + if (area.Sample(neighbor)) { if (islandFullMap.Sample(neighbor) == Empty) { @@ -283,7 +280,7 @@ private static List FindIslands(IArea area) /// Finds all points that are not inside the area which can "escape" to the border. /// Used for hole detection. /// - private static HashSet ComputeOutsidePoints(IArea area) + private static HashSet ComputeOutsidePoints(I2DSampler area) { var searchBounds = ExpandBounds(area); var outsidePoints = new HashSet(); @@ -292,13 +289,13 @@ private static HashSet ComputeOutsidePoints(IArea area) for (int x = searchBounds.start.X; x < searchBounds.end.X; x++) { var top = new XZ(x, searchBounds.start.Z); - if (!area.InArea(top) && outsidePoints.Add(top)) + if (!area.Sample(top) && outsidePoints.Add(top)) { floodQueue.Enqueue(top); } var bottom = new XZ(x, searchBounds.end.Z - 1); - if (!area.InArea(bottom) && outsidePoints.Add(bottom)) + if (!area.Sample(bottom) && outsidePoints.Add(bottom)) { floodQueue.Enqueue(bottom); } @@ -307,13 +304,13 @@ private static HashSet ComputeOutsidePoints(IArea area) for (int z = searchBounds.start.Z; z < searchBounds.end.Z; z++) { var left = new XZ(searchBounds.start.X, z); - if (!area.InArea(left) && outsidePoints.Add(left)) + if (!area.Sample(left) && outsidePoints.Add(left)) { floodQueue.Enqueue(left); } var right = new XZ(searchBounds.end.X - 1, z); - if (!area.InArea(right) && outsidePoints.Add(right)) + if (!area.Sample(right) && outsidePoints.Add(right)) { floodQueue.Enqueue(right); } @@ -325,7 +322,7 @@ private static HashSet ComputeOutsidePoints(IArea area) foreach (var direction in allDirections) { var neighbor = current.Add(direction.Step); - if (searchBounds.Contains(neighbor) && !area.InArea(neighbor) && outsidePoints.Add(neighbor)) + if (searchBounds.Contains(neighbor) && !area.Sample(neighbor) && outsidePoints.Add(neighbor)) { floodQueue.Enqueue(neighbor); } diff --git a/src/Blocktavius.DQB2/Mutations/PutHillMutation.cs b/src/Blocktavius.DQB2/Mutations/PutHillMutation.cs index 519115c..e661f85 100644 --- a/src/Blocktavius.DQB2/Mutations/PutHillMutation.cs +++ b/src/Blocktavius.DQB2/Mutations/PutHillMutation.cs @@ -21,7 +21,7 @@ internal override void Apply(IMutableStage stage) var elevation = Sampler.Sample(xz); if (elevation > 0) { - for (int y = 1; y <= elevation; y++) + for (int y = elevation - 20; y <= elevation; y++) { chunk.SetBlock(new Point(xz, y), Block); } From cadd37722be5bf571dc26062d95e43b7f029cfd8 Mon Sep 17 00:00:00 2001 From: Ryan Kramer Date: Thu, 8 Jan 2026 12:16:36 -0600 Subject: [PATCH 02/10] oops missed this --- src/Blocktavius.Core/Generators/Hills/NewHill.cs | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/Blocktavius.Core/Generators/Hills/NewHill.cs b/src/Blocktavius.Core/Generators/Hills/NewHill.cs index 119b2f5..6b7fc74 100644 --- a/src/Blocktavius.Core/Generators/Hills/NewHill.cs +++ b/src/Blocktavius.Core/Generators/Hills/NewHill.cs @@ -19,14 +19,12 @@ public sealed record Settings public static I2DSampler BuildNewHill(Settings settings, Shell shell) { - if (settings.MinElevation >= settings.MaxElevation) + var tier = Tier.CreateFirstTier(shell, settings); + while (tier.MinElevation > settings.MinElevation) { - throw new ArgumentException("MinElevation must be less than MaxElevation"); + tier = tier.CreateNextTier(); } - - int expansion = settings.MaxElevation - settings.MinElevation; - var bounds = shell.IslandArea.Bounds.Expand(expansion * 2); - var array = new MutableArray2D(bounds, new HillItem() { Elevation = emptyValue, Slab = null }); + return tier.Array.Project(x => x.Elevation); } record struct HillItem From a712466929fc7309514cf98e53c9706a331e9e7b Mon Sep 17 00:00:00 2001 From: Ryan Kramer Date: Thu, 8 Jan 2026 12:58:52 -0600 Subject: [PATCH 03/10] oops --- src/Blocktavius.Core/Generators/Hills/NewHill.cs | 2 ++ src/Blocktavius.Core/Shell.cs | 2 ++ 2 files changed, 4 insertions(+) diff --git a/src/Blocktavius.Core/Generators/Hills/NewHill.cs b/src/Blocktavius.Core/Generators/Hills/NewHill.cs index 6b7fc74..e0bb092 100644 --- a/src/Blocktavius.Core/Generators/Hills/NewHill.cs +++ b/src/Blocktavius.Core/Generators/Hills/NewHill.cs @@ -142,6 +142,8 @@ public Tier CreateNextTier() ParentSlabs = parentSlabs, XZs = exampleRun.Select(x => x.XZ).ToList(), }; + + throw new Exception("TODO"); } } } diff --git a/src/Blocktavius.Core/Shell.cs b/src/Blocktavius.Core/Shell.cs index 5d6f37c..ae80f50 100644 --- a/src/Blocktavius.Core/Shell.cs +++ b/src/Blocktavius.Core/Shell.cs @@ -173,6 +173,8 @@ static bool TryBuildShell(I2DSampler area, IslandInfo island, out List private static Rect ExpandBounds(I2DSampler area) => area.Bounds.Expand(1); + public static IReadOnlyList ComputeShells(IArea area) => ComputeShells(area.AsSampler()); + public static IReadOnlyList ComputeShells(I2DSampler area) { List shells = new(); From 81e9b3cd57f4f8f3a327b51cf656f4f29c636e57 Mon Sep 17 00:00:00 2001 From: Ryan Kramer Date: Thu, 8 Jan 2026 14:07:38 -0600 Subject: [PATCH 04/10] found a shell logic bug... --- .../ScriptNodes/HillDesigners/HillType.cs | 1 + .../HillDesigners/NewHillDesigner.cs | 69 ++++++ .../Generators/Hills/NewHill.cs | 201 +++++++++++++----- .../Mutations/PutHillMutation.cs | 2 +- .../Mutations/RepairSeaMutation.cs | 2 +- src/Blocktavius.DQB2/StageMutation.cs | 4 +- src/Blocktavius.Tests/ShellLogicTests.cs | 69 ++++-- src/Blocktavius.Tests/TestUtil.cs | 53 ++++- 8 files changed, 326 insertions(+), 75 deletions(-) create mode 100644 src/Blocktavius.AppDQB2/ScriptNodes/HillDesigners/NewHillDesigner.cs diff --git a/src/Blocktavius.AppDQB2/ScriptNodes/HillDesigners/HillType.cs b/src/Blocktavius.AppDQB2/ScriptNodes/HillDesigners/HillType.cs index 76a365a..a10bc9f 100644 --- a/src/Blocktavius.AppDQB2/ScriptNodes/HillDesigners/HillType.cs +++ b/src/Blocktavius.AppDQB2/ScriptNodes/HillDesigners/HillType.cs @@ -40,6 +40,7 @@ static HillType() Register(); Register(); Register(); + Register(); } public sealed class PropGridItemsSource : IItemsSource diff --git a/src/Blocktavius.AppDQB2/ScriptNodes/HillDesigners/NewHillDesigner.cs b/src/Blocktavius.AppDQB2/ScriptNodes/HillDesigners/NewHillDesigner.cs new file mode 100644 index 0000000..08bac14 --- /dev/null +++ b/src/Blocktavius.AppDQB2/ScriptNodes/HillDesigners/NewHillDesigner.cs @@ -0,0 +1,69 @@ +using Blocktavius.AppDQB2.Persistence; +using Blocktavius.Core; +using Blocktavius.Core.Generators.Hills; +using Blocktavius.DQB2; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Blocktavius.AppDQB2.ScriptNodes.HillDesigners; + +sealed class NewHillDesigner : ShellBasedHillDesigner +{ + public override IPersistentHillDesigner ToPersistModel() + { + throw new NotImplementedException(); + } + + protected override StageMutation? CreateMutation(HillDesignContext context, Shell shell) + { + if (shell.IsHole) + { + return null; + } + + var settings = new NewHill.Settings() + { + MaxElevation = 80, + MinElevation = 60, + PRNG = context.Prng.AdvanceAndClone(), + }; + var hill = NewHill.BuildNewHill(settings, shell); + return new Mutation { Sampler = hill }; + } + + class Mutation : StageMutation + { + public required I2DSampler Sampler { get; init; } + + public override void Apply(IMutableStage stage) + { + foreach (var chunk in Enumerate(Sampler.Bounds, stage)) + { + foreach (var xz in chunk.Offset.Bounds.Intersection(Sampler.Bounds).Enumerate()) + { + var item = Sampler.Sample(xz); + if (item.Elevation > 0) + { + ushort blockId; + if (item.Slab != null) + { + blockId = Convert.ToUInt16(item.Slab.AncestorCount % 2 + 4); + } + else + { + blockId = 3; + } + + for (int y = 1; y <= item.Elevation; y++) + { + chunk.SetBlock(new Point(xz, y), blockId); + } + } + } + } + } + } +} diff --git a/src/Blocktavius.Core/Generators/Hills/NewHill.cs b/src/Blocktavius.Core/Generators/Hills/NewHill.cs index e0bb092..8774551 100644 --- a/src/Blocktavius.Core/Generators/Hills/NewHill.cs +++ b/src/Blocktavius.Core/Generators/Hills/NewHill.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using System.Text; using System.Threading.Tasks; @@ -17,23 +18,27 @@ public sealed record Settings const int emptyValue = -1; - public static I2DSampler BuildNewHill(Settings settings, Shell shell) + public static I2DSampler BuildNewHill(Settings settings, Shell shell) { - var tier = Tier.CreateFirstTier(shell, settings); + int expansion = settings.MaxElevation - settings.MinElevation; + var bounds = shell.IslandArea.Bounds.Expand(expansion * 2); + var array = new MutableArray2D(bounds, new HillItem() { Elevation = emptyValue, Slab = null }); + + var tier = Tier.CreateFirstTier(shell, settings, array); while (tier.MinElevation > settings.MinElevation) { tier = tier.CreateNextTier(); } - return tier.Array.Project(x => x.Elevation); + return tier.Array; //.Project(x => x.Elevation); } - record struct HillItem + public record struct HillItem { public required int Elevation { get; init; } public required Slab? Slab { get; init; } } - sealed class Slab + public sealed class Slab { public required IReadOnlyList XZs { get; init; } public required IReadOnlyList ParentSlabs { get; init; } @@ -65,17 +70,13 @@ sealed class Tier /// public required int MinElevation { get; init; } - public static Tier CreateFirstTier(Shell shell, Settings settings) + public static Tier CreateFirstTier(Shell shell, Settings settings, MutableArray2D array) { if (settings.MinElevation >= settings.MaxElevation) { throw new ArgumentException("MinElevation must be less than MaxElevation"); } - int expansion = settings.MaxElevation - settings.MinElevation; - var bounds = shell.IslandArea.Bounds.Expand(expansion * 2); - var array = new MutableArray2D(bounds, new HillItem() { Elevation = emptyValue, Slab = null }); - foreach (var xz in shell.IslandArea.Bounds.Enumerate()) { if (shell.IslandArea.Sample(xz)) @@ -96,54 +97,154 @@ public static Tier CreateFirstTier(Shell shell, Settings settings) public Tier CreateNextTier() { - // It's possible we might create holes... let's ignore them for now until I can see an example. + // The shell of the parent tier defines what we need to cover in this tier. var shellToCover = ShellLogic.ComputeShells(Array.Project(x => x.Elevation > emptyValue)) .Where(shell => !shell.IsHole) - .Single(); - - var currentShell = shellToCover; - - // While shellToCover is not fully covered by slabs: - // * choose random run from currentShell such that it covers at least one item from shellToCover - // * create slab for that run - // - must find all parent slabs - // - elevation for that slab should be 1 less than min from all parents (or 1 less than max elevation if no parents) - // * add that slab to the mutable array - // * recompute currentShell - - // == EXAMPLE == - // Very rough idea of how to create a slab: - int length = 4 + Settings.PRNG.NextInt32(8); - int start = Settings.PRNG.NextInt32(currentShell.ShellItems.Count - length); - var exampleRun = currentShell.ShellItems.Skip(start).Take(length); - // TODO must ensure that exampleRun includes at least one XZ from shellToCover... - var parentSlabs = exampleRun.Select(item => Array.Sample(item.XZ.Step(item.InsideDirection))) - .Select(hillItem => hillItem.Slab) - .WhereNotNull() - .Distinct() - .ToList(); - - int ancestorCount; - int slabElevation; - if (parentSlabs.Any()) + .FirstOrDefault(); // TODO duplicate shell bug + + // If there's no shell, there's nothing to do. + if (shellToCover is null) { - ancestorCount = parentSlabs.Max(x => x.AncestorCount) + 1; - slabElevation = parentSlabs.Min(x => x.MinElevation) - 1; + throw new Exception("Impossible"); } - else + + var slabs = new List(); + var uncoveredShellPoints = shellToCover.ShellItems.Select(item => item.XZ).ToHashSet(); + + // All slabs in this new tier will have the same elevation. + // TODO WRONG! + //int slabElevation = MinElevation - 1; + + while (uncoveredShellPoints.Count > 0) { - ancestorCount = 0; - slabElevation = Settings.MaxElevation - 1; + // The "current" shell is the shell of the tier-in-progress, which can change with each new slab. + var TODO = ShellLogic.ComputeShells(Array.Project(x => x.Elevation > emptyValue)) + .Where(shell => !shell.IsHole) + .ToList(); + if (TODO.Count > 1) + { + var sb = new StringBuilder(); + for (int zz = Array.Bounds.start.Z; zz < Array.Bounds.end.Z; zz++) + { + for (int xx = Array.Bounds.start.X; xx < Array.Bounds.end.X; xx++) + { + sb.Append(Array.Sample(new XZ(xx, zz)).Elevation > emptyValue ? "x" : "_"); + } + sb.AppendLine(); + } + var blah = sb.ToString(); + + Debugger.Break(); // TODO how is this possible?? + } + var currentShell = TODO.First(); + + var shellItems = currentShell.ShellItems; + + // For quick lookups of a shell item's indices, handling corners correctly. + var shellItemsIndexMap = new Dictionary>(); + for (int i = 0; i < shellItems.Count; i++) + { + var xz = shellItems[i].XZ; + if (!shellItemsIndexMap.TryGetValue(xz, out var indices)) + { + indices = new List(); + shellItemsIndexMap[xz] = indices; + } + indices.Add(i); + } + + // Pick a random point from the original shell that we haven't covered yet. + var seedXZ = uncoveredShellPoints.ElementAt(Settings.PRNG.NextInt32(uncoveredShellPoints.Count)); + + // If this seed point is no longer on the current shell's boundary, it means an overlapping + // slab has already covered it and made it an "internal" point. We can consider it covered. + if (!shellItemsIndexMap.TryGetValue(seedXZ, out var seedIndices)) + { + uncoveredShellPoints.Remove(seedXZ); + continue; + } + + // Determine the run shape based on unique XZ coordinates. + int uniqueXzCount = 4 + Settings.PRNG.NextInt32(4); + int seedPositionInRun = Settings.PRNG.NextInt32(uniqueXzCount); + int uniqueXzsBeforeSeed = seedPositionInRun; + + // Find the start of the run by walking backwards from the seed. + int seedShellIndex = seedIndices[0]; + int startShellIndex = seedShellIndex; + var uniqueXzsFound = new HashSet { seedXZ }; + + for (int i = 0; i < shellItems.Count && uniqueXzsFound.Count <= uniqueXzsBeforeSeed; i++) + { + startShellIndex = (seedShellIndex - i + shellItems.Count) % shellItems.Count; + uniqueXzsFound.Add(shellItems[startShellIndex].XZ); + } + + // Collect the full run of shell items by walking forwards from the start. + var slabRunItems = new List(); + var uniqueXzsInRun = new HashSet(); + for (int i = 0; i < shellItems.Count && uniqueXzsInRun.Count < uniqueXzCount; i++) + { + int currentIndex = (startShellIndex + i) % shellItems.Count; + var currentItem = shellItems[currentIndex]; + slabRunItems.Add(currentItem); + uniqueXzsInRun.Add(currentItem.XZ); + } + + // Find parent slabs by looking at the neighbors of the run items. + var parentSlabs = slabRunItems + .Select(item => Array.Sample(item.XZ.Step(item.InsideDirection))) + .Select(hillItem => hillItem.Slab) + .WhereNotNull() + .Distinct() + .ToList(); + + int ancestorCount; + int slabElevation; + if (parentSlabs.Any()) + { + ancestorCount = parentSlabs.Max(x => x.AncestorCount) + 1; + slabElevation = parentSlabs.Min(x => x.MinElevation) - 1; + } + else + { + ancestorCount = 0; + slabElevation = this.MinElevation - 1; + } + + var slabXZs = slabRunItems.Select(x => x.XZ).ToList(); + var newSlab = new Slab + { + AncestorCount = ancestorCount, + MinElevation = slabElevation, + ParentSlabs = parentSlabs, + XZs = slabXZs, + }; + slabs.Add(newSlab); + + // Apply the new slab to the work-in-progress array. + var newHillItem = new HillItem { Elevation = slabElevation, Slab = newSlab }; + foreach (var xz in uniqueXzsInRun) // Use unique XZs for updating array and coverage + { + // It's possible for this to overwrite a HillItem from a previous slab in this same tier. This is intentional. + Array.Put(xz, newHillItem); + uncoveredShellPoints.Remove(xz); + } } - var slab = new Slab + + if (slabs.Count == 0) { - AncestorCount = ancestorCount, - MinElevation = slabElevation, - ParentSlabs = parentSlabs, - XZs = exampleRun.Select(x => x.XZ).ToList(), - }; + throw new Exception("Assert fail - empty tier should be impossible"); + } - throw new Exception("TODO"); + return new Tier + { + ParentTier = this, + Settings = Settings, + Slabs = slabs, + Array = Array, + MinElevation = Math.Min(this.MinElevation, slabs.Min(slab => slab.MinElevation)), + }; } } } diff --git a/src/Blocktavius.DQB2/Mutations/PutHillMutation.cs b/src/Blocktavius.DQB2/Mutations/PutHillMutation.cs index e661f85..4f5bae4 100644 --- a/src/Blocktavius.DQB2/Mutations/PutHillMutation.cs +++ b/src/Blocktavius.DQB2/Mutations/PutHillMutation.cs @@ -12,7 +12,7 @@ sealed class PutHillMutation : StageMutation public required I2DSampler Sampler { get; init; } public required ushort Block { get; init; } - internal override void Apply(IMutableStage stage) + public override void Apply(IMutableStage stage) { foreach (var chunk in Enumerate(Sampler.Bounds, stage)) { diff --git a/src/Blocktavius.DQB2/Mutations/RepairSeaMutation.cs b/src/Blocktavius.DQB2/Mutations/RepairSeaMutation.cs index c113266..c468cb4 100644 --- a/src/Blocktavius.DQB2/Mutations/RepairSeaMutation.cs +++ b/src/Blocktavius.DQB2/Mutations/RepairSeaMutation.cs @@ -13,7 +13,7 @@ public sealed class RepairSeaMutation : StageMutation public required int SeaLevel { get; init; } public LiquidFamily LiquidFamily { get; init; } = LiquidFamily.Seawater; - internal override void Apply(IMutableStage stage) + public override void Apply(IMutableStage stage) { var p = new RepairSeaOperation.Params { diff --git a/src/Blocktavius.DQB2/StageMutation.cs b/src/Blocktavius.DQB2/StageMutation.cs index f7da719..8df2e2e 100644 --- a/src/Blocktavius.DQB2/StageMutation.cs +++ b/src/Blocktavius.DQB2/StageMutation.cs @@ -9,7 +9,7 @@ namespace Blocktavius.DQB2; public abstract class StageMutation { - internal abstract void Apply(IMutableStage stage); + public abstract void Apply(IMutableStage stage); protected static IEnumerable Enumerate(Rect bounds, IMutableStage stage) { @@ -45,7 +45,7 @@ sealed class CompositeMutation : StageMutation public required IReadOnlyList Mutations { get; init; } public required ColumnCleanupMode? ColumnCleanupMode { get; init; } - internal override void Apply(IMutableStage stage) + public override void Apply(IMutableStage stage) { foreach (var m in Mutations) { diff --git a/src/Blocktavius.Tests/ShellLogicTests.cs b/src/Blocktavius.Tests/ShellLogicTests.cs index ff8815b..5ecde83 100644 --- a/src/Blocktavius.Tests/ShellLogicTests.cs +++ b/src/Blocktavius.Tests/ShellLogicTests.cs @@ -8,26 +8,6 @@ namespace Blocktavius.Tests [TestClass] public class ShellLogicTests { - private class TestArea : IArea - { - private readonly HashSet points; - public Rect Bounds { get; } - - public TestArea(IEnumerable points) - { - this.points = new HashSet(points); - if (this.points.Any()) - { - Bounds = Rect.GetBounds(this.points); - } - else - { - Bounds = Rect.Zero; - } - } - - public bool InArea(XZ xz) => points.Contains(xz); - } [TestMethod] public void ComputeShells_SimpleSquare_CreatesOneOuterShell() @@ -213,5 +193,54 @@ public void outside_corner_regression() Direction.SouthEast ); } + + [TestMethod] + public void duplicate_shell_regression() + { + var area = TestUtil.CreateAreaFromAscii(@" +_______xxx____________________ +___xx_xxxxx___________________ +__xxxxxxxx____________________ +_xxxxxxxxxx___________________ +__xxxxxxxxxx_xxxxxxx__________ +_xxxxxxxxxxxxxxxxxxx__________ +_xxxxxxxxxxxxxxxxxxxx_________ +_xxxxxxxxxxxxxxxxxxxx_x_______ +_xxxxxxxxxxxxxxxxxxxxxx_______ +_xxxxxxxxxxxxxxxxxxxxxxx______ +__xxxxxxxxxxxxxxxxxxxxxxx_____ +__xxxxxxxxxxxxxxxxxxxxxxxx____ +__xxxxxxxxxxxxxxxxxxxxxxx_____ +__xxxxxxxxxxxxxxxxxxxxxxx_____ +_xxxxxxxxxxxxxxxxxxxxxxxx_____ +_xxxxxxxxxxxxxxxxxxxxxxxxx____ +_xxxxxxxxxxxxxxxxxxxxxxxxxxxx_ +_xxxxxxxxxxxxxxxxxxxxxxxxxxxx_ +_xxxxxxxxxxxxxxxxxxxxxxxxxxxx_ +_xxxxxxxxxxxxxxxxxxxxxxxxxxxx_ +xxxxxxxxxxxxxxxxxxxxxxxxxxxxx_ +xxxxxxxxxxxxxxxxxxxxxxxxxxxxx_ +_xxxxxxxxxxxxxxxxxxxxxxxxxxxx_ +_xxxxxxxxxxxxxxxxxxxxxxxxxxxxx +_xxxxxxxxxxxxxxxxxxxxxxxxxxxx_ +_xxxxxxxxxxxxxxxxxxxxxxxxxxxx_ +_xxxxxxxxxxxxxxxxxxxxxxxxxx___ +xxxxxxxxxxxxxxxxxxxxxxxxxx____ +xxxxxxxxxxxxxxxxxxxxxxxx______ +xxxxxxxxxxxxxxxxxxxxxxxx______ +xxxxxxxxxxxxxxxxxxxxxxxxx_____ +x_xxxxxxxxxxxxxxxxxxxxx_______ +__xxxxxxxxxxxxxxxxxx_xx_______ +______xxxxxxxxxxxxxxx_________ +_____xxxxxxxxxxxx_____________ +_________xx_xx__x_____________ +________xxx___________________"); + + var shells = ShellLogic.ComputeShells(area) + .Where(s => !s.IsHole) + .ToList(); + + Assert.AreEqual(1, shells.Count); + } } } \ No newline at end of file diff --git a/src/Blocktavius.Tests/TestUtil.cs b/src/Blocktavius.Tests/TestUtil.cs index 59919cc..2869aca 100644 --- a/src/Blocktavius.Tests/TestUtil.cs +++ b/src/Blocktavius.Tests/TestUtil.cs @@ -1,4 +1,5 @@ -using Blocktavius.DQB2; +using Blocktavius.Core; +using Blocktavius.DQB2; using System; using System.Collections.Generic; using System.Linq; @@ -7,10 +8,60 @@ namespace Blocktavius.Tests; +public class TestArea : IArea +{ + private readonly HashSet points; + public Rect Bounds { get; } + + public TestArea(IEnumerable points) + { + this.points = new HashSet(points); + if (this.points.Any()) + { + Bounds = Rect.GetBounds(this.points); + } + else + { + Bounds = Rect.Zero; + } + } + + public bool InArea(XZ xz) => points.Contains(xz); +} + static class TestUtil { public static readonly string SnapshotRoot; + public static IArea CreateAreaFromAscii(string pattern) + { + var lines = pattern.Replace("\r\n", "\n").Split('\n', StringSplitOptions.RemoveEmptyEntries); + + if (lines is null || lines.Length == 0) + { + return new TestArea(Enumerable.Empty()); + } + + var width = lines[0].Length; + if (lines.Any(line => line.Length != width)) + { + throw new ArgumentException("All lines in the pattern must have the same length."); + } + + var points = new HashSet(); + for (int z = 0; z < lines.Length; z++) + { + for (int x = 0; x < width; x++) + { + if (lines[z][x] != '_') + { + points.Add(new XZ(x, z)); + } + } + } + return new TestArea(points); + } + static TestUtil() { var assemblyLocation = AppContext.BaseDirectory; From 337553e31d9c216c3ae44d83668f5b22aa566f30 Mon Sep 17 00:00:00 2001 From: Ryan Kramer Date: Thu, 8 Jan 2026 16:01:23 -0600 Subject: [PATCH 05/10] fix build and alias InArea to reduce diff size --- src/Blocktavius.Core/Shell.cs | 18 +++++++++--------- src/Blocktavius.Core/Util.cs | 2 ++ 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/src/Blocktavius.Core/Shell.cs b/src/Blocktavius.Core/Shell.cs index a8d2d92..a1a4cf3 100644 --- a/src/Blocktavius.Core/Shell.cs +++ b/src/Blocktavius.Core/Shell.cs @@ -108,7 +108,7 @@ public WalkState Advance(I2DSampler area, List itemCollector) var aheadDir = insideDir.TurnLeft90; var aheadPos = shellPosition.Add(aheadDir.Step); - if (area.Sample(aheadPos)) + if (area.InArea(aheadPos)) { // inside corner, stay at the same position and turn left var diagonalDir = insideDir.TurnLeft45; @@ -123,7 +123,7 @@ public WalkState Advance(I2DSampler area, List itemCollector) } return new WalkState(shellPosition, aheadDir); } - else if (!area.Sample(aheadPos.Add(insideDir.Step))) + else if (!area.InArea(aheadPos.Add(insideDir.Step))) { // outside corner var diagonalDir = insideDir.TurnRight45; @@ -233,7 +233,7 @@ private static List FindIslands(I2DSampler area) const int Empty = -1; var islandFullMap = new MutableArray2D(area.Bounds, Empty); - foreach (var xz in area.Bounds.Enumerate().Where(area.Sample)) + foreach (var xz in area.Bounds.Enumerate().Where(area.InArea)) { if (islandFullMap.Sample(xz) == Empty) { @@ -252,7 +252,7 @@ private static List FindIslands(I2DSampler area) foreach (var dir in allDirections) { var neighbor = current.Add(dir.Step); - if (area.Sample(neighbor)) + if (area.InArea(neighbor)) { if (islandFullMap.Sample(neighbor) == Empty) { @@ -301,13 +301,13 @@ private static HashSet ComputeOutsidePoints(I2DSampler area) for (int x = searchBounds.start.X; x < searchBounds.end.X; x++) { var top = new XZ(x, searchBounds.start.Z); - if (!area.Sample(top) && outsidePoints.Add(top)) + if (!area.InArea(top) && outsidePoints.Add(top)) { floodQueue.Enqueue(top); } var bottom = new XZ(x, searchBounds.end.Z - 1); - if (!area.Sample(bottom) && outsidePoints.Add(bottom)) + if (!area.InArea(bottom) && outsidePoints.Add(bottom)) { floodQueue.Enqueue(bottom); } @@ -316,13 +316,13 @@ private static HashSet ComputeOutsidePoints(I2DSampler area) for (int z = searchBounds.start.Z; z < searchBounds.end.Z; z++) { var left = new XZ(searchBounds.start.X, z); - if (!area.Sample(left) && outsidePoints.Add(left)) + if (!area.InArea(left) && outsidePoints.Add(left)) { floodQueue.Enqueue(left); } var right = new XZ(searchBounds.end.X - 1, z); - if (!area.Sample(right) && outsidePoints.Add(right)) + if (!area.InArea(right) && outsidePoints.Add(right)) { floodQueue.Enqueue(right); } @@ -334,7 +334,7 @@ private static HashSet ComputeOutsidePoints(I2DSampler area) foreach (var direction in cardinalDirections) { var neighbor = current.Add(direction.Step); - if (searchBounds.Contains(neighbor) && !area.Sample(neighbor) && outsidePoints.Add(neighbor)) + if (searchBounds.Contains(neighbor) && !area.InArea(neighbor) && outsidePoints.Add(neighbor)) { floodQueue.Enqueue(neighbor); } diff --git a/src/Blocktavius.Core/Util.cs b/src/Blocktavius.Core/Util.cs index d4e7a7e..10d061c 100644 --- a/src/Blocktavius.Core/Util.cs +++ b/src/Blocktavius.Core/Util.cs @@ -22,6 +22,8 @@ public static IEnumerable WhereNotNull(this IEnumerable sequence) public static IEnumerable EmptyIfNull(this IEnumerable? sequence) => sequence ?? Enumerable.Empty(); + public static bool InArea(this I2DSampler area, XZ xz) => area.Sample(xz); + sealed class Translator : I2DSampler { private readonly I2DSampler sampler; From 57b7a496499f3b72a3ba4e6eb5ddf45b66c48d6e Mon Sep 17 00:00:00 2001 From: Ryan Kramer Date: Thu, 8 Jan 2026 16:06:34 -0600 Subject: [PATCH 06/10] shell bug is gone now --- .../Generators/Hills/NewHill.cs | 23 +++---------------- 1 file changed, 3 insertions(+), 20 deletions(-) diff --git a/src/Blocktavius.Core/Generators/Hills/NewHill.cs b/src/Blocktavius.Core/Generators/Hills/NewHill.cs index 8774551..7f6e46b 100644 --- a/src/Blocktavius.Core/Generators/Hills/NewHill.cs +++ b/src/Blocktavius.Core/Generators/Hills/NewHill.cs @@ -100,7 +100,7 @@ public Tier CreateNextTier() // The shell of the parent tier defines what we need to cover in this tier. var shellToCover = ShellLogic.ComputeShells(Array.Project(x => x.Elevation > emptyValue)) .Where(shell => !shell.IsHole) - .FirstOrDefault(); // TODO duplicate shell bug + .Single(); // If there's no shell, there's nothing to do. if (shellToCover is null) @@ -118,26 +118,9 @@ public Tier CreateNextTier() while (uncoveredShellPoints.Count > 0) { // The "current" shell is the shell of the tier-in-progress, which can change with each new slab. - var TODO = ShellLogic.ComputeShells(Array.Project(x => x.Elevation > emptyValue)) + var currentShell = ShellLogic.ComputeShells(Array.Project(x => x.Elevation > emptyValue)) .Where(shell => !shell.IsHole) - .ToList(); - if (TODO.Count > 1) - { - var sb = new StringBuilder(); - for (int zz = Array.Bounds.start.Z; zz < Array.Bounds.end.Z; zz++) - { - for (int xx = Array.Bounds.start.X; xx < Array.Bounds.end.X; xx++) - { - sb.Append(Array.Sample(new XZ(xx, zz)).Elevation > emptyValue ? "x" : "_"); - } - sb.AppendLine(); - } - var blah = sb.ToString(); - - Debugger.Break(); // TODO how is this possible?? - } - var currentShell = TODO.First(); - + .Single(); var shellItems = currentShell.ShellItems; // For quick lookups of a shell item's indices, handling corners correctly. From e709752282fac63c23a2ec15409c88bb8d7c9d8a Mon Sep 17 00:00:00 2001 From: Ryan Kramer Date: Thu, 8 Jan 2026 16:35:13 -0600 Subject: [PATCH 07/10] add snapshot for new hill --- .../HillDesigners/NewHillDesigner.cs | 2 +- .../Generators/Hills/NewHill.cs | 45 +++++++++++++----- src/Blocktavius.Tests/SnapshotTests.cs | 26 ++++++++++ .../Snapshots/ElevationImages/NewHill01.png | Bin 0 -> 1250 bytes 4 files changed, 61 insertions(+), 12 deletions(-) create mode 100644 src/Blocktavius.Tests/Snapshots/ElevationImages/NewHill01.png diff --git a/src/Blocktavius.AppDQB2/ScriptNodes/HillDesigners/NewHillDesigner.cs b/src/Blocktavius.AppDQB2/ScriptNodes/HillDesigners/NewHillDesigner.cs index 08bac14..3434349 100644 --- a/src/Blocktavius.AppDQB2/ScriptNodes/HillDesigners/NewHillDesigner.cs +++ b/src/Blocktavius.AppDQB2/ScriptNodes/HillDesigners/NewHillDesigner.cs @@ -27,7 +27,7 @@ public override IPersistentHillDesigner ToPersistModel() var settings = new NewHill.Settings() { MaxElevation = 80, - MinElevation = 60, + MinElevation = 25, PRNG = context.Prng.AdvanceAndClone(), }; var hill = NewHill.BuildNewHill(settings, shell); diff --git a/src/Blocktavius.Core/Generators/Hills/NewHill.cs b/src/Blocktavius.Core/Generators/Hills/NewHill.cs index 7f6e46b..7a375e6 100644 --- a/src/Blocktavius.Core/Generators/Hills/NewHill.cs +++ b/src/Blocktavius.Core/Generators/Hills/NewHill.cs @@ -20,16 +20,12 @@ public sealed record Settings public static I2DSampler BuildNewHill(Settings settings, Shell shell) { - int expansion = settings.MaxElevation - settings.MinElevation; - var bounds = shell.IslandArea.Bounds.Expand(expansion * 2); - var array = new MutableArray2D(bounds, new HillItem() { Elevation = emptyValue, Slab = null }); - - var tier = Tier.CreateFirstTier(shell, settings, array); + var tier = Tier.CreateFirstTier(shell, settings); while (tier.MinElevation > settings.MinElevation) { tier = tier.CreateNextTier(); } - return tier.Array; //.Project(x => x.Elevation); + return tier.Array.Crop(); } public record struct HillItem @@ -55,6 +51,29 @@ public sealed class Slab public required int AncestorCount { get; init; } } + sealed class HillItemArray + { + private readonly MutableArray2D array; + private readonly Rect.BoundsFinder boundsFinder = new(); + + public HillItemArray(Rect bounds) + { + this.array = new MutableArray2D(bounds, new HillItem { Elevation = emptyValue, Slab = null }); + } + + public void Put(XZ xz, HillItem hillItem) + { + array.Put(xz, hillItem); + boundsFinder.Include(xz); + } + + public I2DSampler Uncropped => array; + + public I2DSampler Crop() => array.Crop(boundsFinder.CurrentBounds() ?? array.Bounds); + + public I2DSampler AsArea() => array.Project(item => item.Elevation > emptyValue); + } + /// /// Each tier is basically "add slabs until previous tier is fully covered." /// @@ -63,20 +82,24 @@ sealed class Tier public required Tier? ParentTier { get; init; } public required Settings Settings { get; init; } public required IReadOnlyList Slabs { get; init; } - public required MutableArray2D Array { get; init; } + public required HillItemArray Array { get; init; } /// /// Must be set to Slabs.Min(slab => slab.MinElevation) when any slabs exist. /// public required int MinElevation { get; init; } - public static Tier CreateFirstTier(Shell shell, Settings settings, MutableArray2D array) + public static Tier CreateFirstTier(Shell shell, Settings settings) { if (settings.MinElevation >= settings.MaxElevation) { throw new ArgumentException("MinElevation must be less than MaxElevation"); } + // Reserve space and just hope it's enough... TODO make this bulletproof! + int expansion = settings.MaxElevation - settings.MinElevation; + var array = new HillItemArray(shell.IslandArea.Bounds.Expand(expansion * 2)); + foreach (var xz in shell.IslandArea.Bounds.Enumerate()) { if (shell.IslandArea.Sample(xz)) @@ -98,7 +121,7 @@ public static Tier CreateFirstTier(Shell shell, Settings settings, MutableArray2 public Tier CreateNextTier() { // The shell of the parent tier defines what we need to cover in this tier. - var shellToCover = ShellLogic.ComputeShells(Array.Project(x => x.Elevation > emptyValue)) + var shellToCover = ShellLogic.ComputeShells(Array.AsArea()) .Where(shell => !shell.IsHole) .Single(); @@ -118,7 +141,7 @@ public Tier CreateNextTier() while (uncoveredShellPoints.Count > 0) { // The "current" shell is the shell of the tier-in-progress, which can change with each new slab. - var currentShell = ShellLogic.ComputeShells(Array.Project(x => x.Elevation > emptyValue)) + var currentShell = ShellLogic.ComputeShells(Array.AsArea()) .Where(shell => !shell.IsHole) .Single(); var shellItems = currentShell.ShellItems; @@ -176,7 +199,7 @@ public Tier CreateNextTier() // Find parent slabs by looking at the neighbors of the run items. var parentSlabs = slabRunItems - .Select(item => Array.Sample(item.XZ.Step(item.InsideDirection))) + .Select(item => Array.Uncropped.Sample(item.XZ.Step(item.InsideDirection))) .Select(hillItem => hillItem.Slab) .WhereNotNull() .Distinct() diff --git a/src/Blocktavius.Tests/SnapshotTests.cs b/src/Blocktavius.Tests/SnapshotTests.cs index 7aad843..f596eec 100644 --- a/src/Blocktavius.Tests/SnapshotTests.cs +++ b/src/Blocktavius.Tests/SnapshotTests.cs @@ -60,6 +60,32 @@ public void CornerPusher01() AssertMatches(hill, "CornerPusher01.png"); } + [TestMethod] + public void NewHill01() + { + var prng = PRNG.Deserialize("12345-67890-12345-67890-12345-67890"); + + var area = TestUtil.CreateAreaFromAscii(@" +xxxx +x__x +x__x +xxxx"); + + // The NewHill generator only works on a single outer shell. + var shell = ShellLogic.ComputeShells(area).Where(s => !s.IsHole).Single(); + + var settings = new NewHill.Settings + { + MaxElevation = 30, + MinElevation = 10, + PRNG = prng, + }; + + var hill = NewHill.BuildNewHill(settings, shell).Project(i => i.Elevation); + + AssertMatches(hill, "NewHill01.png"); + } + private static void AssertMatches(I2DSampler cliff, string imageName) { var snapshotImage = CreateImageFromSampler(cliff); diff --git a/src/Blocktavius.Tests/Snapshots/ElevationImages/NewHill01.png b/src/Blocktavius.Tests/Snapshots/ElevationImages/NewHill01.png new file mode 100644 index 0000000000000000000000000000000000000000..eaa0c669421592f5990dc5192dbe8292df9e7344 GIT binary patch literal 1250 zcmV<81ReW{P)Px#1ZP1_K>z@;j|==^1poj808mU+MF0Q*0RaI50s;dA0|W#F1qB5L1_lQQ2M7oV z2?+@b3JMDg3k(bl4Gj$r4h|0w4-gO#5fKp*5)u;=6BHB_6%`d078Vy57Z?~A85tQG z8X6lL8yp-Q9UUDW9v&YbA0QwgAt50mA|fLrBP1jwB_$;$CMG8*CnzW=DJdx`Dk>{0 zD=aK5EiElBE-o)GFEB7LF)=YRGBPtWGc+_bH8nLhHa0gmH#j&rIXO8xIyyT$J3Kr* zJv}`>K0ZG`KR`f0K|w)6LPA4BLqtSGMMXtMMn*?RM@UFWNl8gcN=i#hOH52mO-)Ts zPEJoxPf$=$QBhG+Qc_b>Q&dz`RaI41R#sP6S6EnBSy@?HT3TCMTU=aRU0q#XUS3~c zUtnNhVPRonVq#-sV`OAxWo2b%W@cw+XJ}|>X=!O{YHDk1Yiw+6ZEbCCZf7mzbECnVFfInwp!No1C1Sot>SYo}QndpP-Ll?si~=|s;aB2tE{Z7t*x!DuCA}IuduMNv9YnTva++Yv$V9dwY9aj zwzjvox45{txw*Nzy1Ki&yS%)-y}iA@zP`V|zreu2!NI}8!otJD!^FhI#l^+O#>U6T z$H>UY$;rve%F4^j%goHo&CSiu&d$%z&(P4&(b3V;($dq@)6~?|)z#J3*4Ee8*Vx$D z+1c6J+S=RO+uYpT-QC^Z-rnEe-{9cj;o;%p;^O1ulq(=H}<;=jiC@>FMd} z>gwz3>+J08?d|RE?(XmJ@9^;O@$vEU^78ZZ^Yrxe_4W1k_V)Mp_xSku`T6D0c%M_K~y+TWs+fe z#2^qvS3nHJ00tre12KRB1YiII7+3)qhye^B00U#7l|8y}JLWnVjzvgvap_Ao|`8gfNattA%lCv=5fnAhju3V7jlo;})DH+QR zX$Ggr@oz!RRhsm!swt^P0^HENTJVk0n^vTejkk+#?5nNyF<#wd-}m)97heW=CRs4DA0P_~9q2z~8#Xy;srw2W6; zM@_1=NsgJ(uHj87@aX{FQ6Zw^m`9aE`~l^c2Id^&cqm4cQC=^G#eGHUIzs M07*qoM6N<$g5Hx$GXMYp literal 0 HcmV?d00001 From b61e1edd0a0a5018c46f411cd701ac5acfbe74bd Mon Sep 17 00:00:00 2001 From: Ryan Kramer Date: Thu, 8 Jan 2026 17:29:06 -0600 Subject: [PATCH 08/10] make it faster --- .../HillDesigners/NewHillDesigner.cs | 11 ++-- .../Generators/Hills/NewHill.cs | 15 +++--- src/Blocktavius.Core/Shell.cs | 48 ++++++++++++++++++ .../Snapshots/ElevationImages/NewHill01.png | Bin 1250 -> 1345 bytes 4 files changed, 61 insertions(+), 13 deletions(-) diff --git a/src/Blocktavius.AppDQB2/ScriptNodes/HillDesigners/NewHillDesigner.cs b/src/Blocktavius.AppDQB2/ScriptNodes/HillDesigners/NewHillDesigner.cs index 3434349..0a4d81d 100644 --- a/src/Blocktavius.AppDQB2/ScriptNodes/HillDesigners/NewHillDesigner.cs +++ b/src/Blocktavius.AppDQB2/ScriptNodes/HillDesigners/NewHillDesigner.cs @@ -47,20 +47,21 @@ public override void Apply(IMutableStage stage) var item = Sampler.Sample(xz); if (item.Elevation > 0) { - ushort blockId; + ushort topperId; if (item.Slab != null) { - blockId = Convert.ToUInt16(item.Slab.AncestorCount % 2 + 4); + topperId = Convert.ToUInt16(item.Slab.AncestorCount % 2 + 4); } else { - blockId = 3; + topperId = 3; } - for (int y = 1; y <= item.Elevation; y++) + for (int y = 1; y < item.Elevation; y++) { - chunk.SetBlock(new Point(xz, y), blockId); + chunk.SetBlock(new Point(xz, y), 5); } + chunk.SetBlock(new Point(xz, item.Elevation), topperId); } } } diff --git a/src/Blocktavius.Core/Generators/Hills/NewHill.cs b/src/Blocktavius.Core/Generators/Hills/NewHill.cs index 7a375e6..83efb40 100644 --- a/src/Blocktavius.Core/Generators/Hills/NewHill.cs +++ b/src/Blocktavius.Core/Generators/Hills/NewHill.cs @@ -134,17 +134,11 @@ public Tier CreateNextTier() var slabs = new List(); var uncoveredShellPoints = shellToCover.ShellItems.Select(item => item.XZ).ToHashSet(); - // All slabs in this new tier will have the same elevation. - // TODO WRONG! - //int slabElevation = MinElevation - 1; + var currentShellItems = shellToCover.ShellItems.ToList(); while (uncoveredShellPoints.Count > 0) { - // The "current" shell is the shell of the tier-in-progress, which can change with each new slab. - var currentShell = ShellLogic.ComputeShells(Array.AsArea()) - .Where(shell => !shell.IsHole) - .Single(); - var shellItems = currentShell.ShellItems; + var shellItems = currentShellItems; // For quick lookups of a shell item's indices, handling corners correctly. var shellItemsIndexMap = new Dictionary>(); @@ -236,6 +230,11 @@ public Tier CreateNextTier() Array.Put(xz, newHillItem); uncoveredShellPoints.Remove(xz); } + + if (uncoveredShellPoints.Count > 0) + { + currentShellItems = ShellLogic.WalkShellFromPoint(Array.AsArea(), uniqueXzsInRun.First()); + } } if (slabs.Count == 0) diff --git a/src/Blocktavius.Core/Shell.cs b/src/Blocktavius.Core/Shell.cs index a1a4cf3..046b8f3 100644 --- a/src/Blocktavius.Core/Shell.cs +++ b/src/Blocktavius.Core/Shell.cs @@ -146,6 +146,54 @@ public WalkState Advance(I2DSampler area, List itemCollector) } } + private static WalkState FindFirstWalkState(I2DSampler area, XZ pointInArea) + { + var queue = new Queue(); + var visited = new HashSet(); + + if (area.InArea(pointInArea)) + { + queue.Enqueue(pointInArea); + visited.Add(pointInArea); + } + + while (queue.Count > 0) + { + var current = queue.Dequeue(); + foreach (var dir in cardinalDirections) + { + var neighbor = current.Add(dir.Step); + if (area.InArea(neighbor)) + { + if (visited.Add(neighbor)) + { + queue.Enqueue(neighbor); + } + } + else + { + return new WalkState(neighbor, dir.Turn180); + } + } + } + + throw new Exception("Could not find any edge for the given area."); + } + + public static List WalkShellFromPoint(I2DSampler area, XZ pointInArea) + { + var startState = FindFirstWalkState(area, pointInArea); + var items = new List(); + var current = startState; + do + { + current = current.Advance(area, items); + } + while (current != startState); + + return items; + } + static bool TryBuildShell(I2DSampler area, IslandInfo island, out List items) { items = new(); diff --git a/src/Blocktavius.Tests/Snapshots/ElevationImages/NewHill01.png b/src/Blocktavius.Tests/Snapshots/ElevationImages/NewHill01.png index eaa0c669421592f5990dc5192dbe8292df9e7344..81a9ae6fde5db6a570066a6f8ce59d96434753db 100644 GIT binary patch delta 525 zcmV+o0`mRh3Bd{>iBL{Q4GJ0x0000DNk~Le0000W0000U2m=5B0D9Ey8?hll1Aod% zL_t(IPi>QJ+1wxyMMuCIhye^B00S7n00J-&0~kO+48#Bi5P*R-5CiSGY_je1w)e+= zFo$7gxY)l6_xJx5V+_GLcmI9F%7*wA6NK|W2=BeyfLM+x=bTvZcTDm=D6LjgO1YGj zLa-NlfM6UPf@gEi`wkI8e1-53K7YixVr(v@LXb$+swfFOThZRVqNWrSV&N{_l%N!a z_l}wlDVbv^>;|Yzduz8bT5F}W1`ugZaWzkdR@pu0=Vw2Sk0@Rh^0r(IwuV3hz<1~O6KGUQX>IFCim@R;Uzks=oK7Z3*0p<-H z$G+>*6y}n=r-)jQW4_lBV|0L=0T{!kcwF&ws>Evnj1xd}+8h<+Tw6QO*@wO7oc#lM zuSrv)CSCV-G62dT%&eGZW=;Uktx4|yr}crl9gI7Z0Gk1D*_t$SAcvjOL6-pA>SFbf zBPnuL?9f$IOktkwOKMV1hcR<7^G&C*}8V+OMd`)7CB*$@no2fout=m zd49uua2&O~AHFF>$bOrQFYC*&yr93|;A=etTuvf|S6O%Lr)0D6ykN86VMQw6T=bGh P00000NkvXXu0mjf-+1bH delta 430 zcmV;f0a5EH3AOHhrAO_mEBzH~Q`I9fa z%)(5N-|`|my5F8!7VkYMg#UCg7($3KhQH=@U7?fZjQKeo#&QfHp^~#O+uci*D?zt@SZp-G(1f z@SAQvtM-1LFEkEl31Pda+;p5@=p6kJ=!CN*53NQ;Cgp^|8(p_$q1;@cwiH|wuVs% zeeHH==Uj-ij8|GmO{%p?j+xg(6M1pVVX6dzUTr`HbBqS6)m%zW@viy4F#LVM&J5MU zx06BVWkc7Mna$-lL|h$?zLVU(wGbcc)aoo5E=N85m21zpR$-ZNj>o<96vMCeA1Lw- Y*$e{nO Date: Fri, 9 Jan 2026 11:17:49 -0600 Subject: [PATCH 09/10] more speedup --- .../HillDesigners/NewHillDesigner.cs | 42 ++++++++++++++- .../Generators/Hills/NewHill.cs | 48 ++++++++++-------- .../Snapshots/ElevationImages/NewHill01.png | Bin 1345 -> 1363 bytes src/Blocktavius.Tests/Test1.cs | 28 ++++++++++ 4 files changed, 97 insertions(+), 21 deletions(-) diff --git a/src/Blocktavius.AppDQB2/ScriptNodes/HillDesigners/NewHillDesigner.cs b/src/Blocktavius.AppDQB2/ScriptNodes/HillDesigners/NewHillDesigner.cs index 0a4d81d..c55bc37 100644 --- a/src/Blocktavius.AppDQB2/ScriptNodes/HillDesigners/NewHillDesigner.cs +++ b/src/Blocktavius.AppDQB2/ScriptNodes/HillDesigners/NewHillDesigner.cs @@ -17,6 +17,41 @@ public override IPersistentHillDesigner ToPersistModel() throw new NotImplementedException(); } + private int _minRunLength = 4; + public int MinRunLength + { + get => _minRunLength; + set => ChangeProperty(ref _minRunLength, value); + } + + private int _maxRunLength = 15; + public int MaxRunLength + { + get => _maxRunLength; + set => ChangeProperty(ref _maxRunLength, value); + } + + private int _coveragePerTier = 80; + public int CoveragePerTier + { + get => _coveragePerTier; + set => ChangeProperty(ref _coveragePerTier, Math.Clamp(value, 0, 100)); + } + + private int _minDrop = 1; + public int MinDrop + { + get => _minDrop; + set => ChangeProperty(ref _minDrop, value); + } + + private int _maxDrop = 3; + public int MaxDrop + { + get => _maxDrop; + set => ChangeProperty(ref _maxDrop, value); + } + protected override StageMutation? CreateMutation(HillDesignContext context, Shell shell) { if (shell.IsHole) @@ -27,8 +62,13 @@ public override IPersistentHillDesigner ToPersistModel() var settings = new NewHill.Settings() { MaxElevation = 80, - MinElevation = 25, + MinElevation = 14, + MinRunLength = MinRunLength, + RunLengthRand = Math.Max(0, 1 + MaxRunLength - MinRunLength), + RequiredCoveragePerTier = CoveragePerTier, PRNG = context.Prng.AdvanceAndClone(), + MinDrop = MinDrop, + DropRand = Math.Max(0, 1 + MaxDrop - MinDrop), }; var hill = NewHill.BuildNewHill(settings, shell); return new Mutation { Sampler = hill }; diff --git a/src/Blocktavius.Core/Generators/Hills/NewHill.cs b/src/Blocktavius.Core/Generators/Hills/NewHill.cs index 83efb40..0a46249 100644 --- a/src/Blocktavius.Core/Generators/Hills/NewHill.cs +++ b/src/Blocktavius.Core/Generators/Hills/NewHill.cs @@ -7,13 +7,22 @@ namespace Blocktavius.Core.Generators.Hills; +/// +/// TODO - Similar to the CornerPusherHill, but we allow overlapping slabs in the same tier... +/// But maybe they are really the same algorithm with different config settings?? +/// public static class NewHill { public sealed record Settings { public required PRNG PRNG { get; init; } - public int MaxElevation { get; init; } - public int MinElevation { get; init; } + public required int MaxElevation { get; init; } + public required int MinElevation { get; init; } + public int MinRunLength { get; init; } = 4; + public int RunLengthRand { get; init; } = 4; + public int RequiredCoveragePerTier { get; set; } = 100; + public int MinDrop { get; set; } = 1; + public int DropRand { get; set; } = 1; } const int emptyValue = -1; @@ -89,6 +98,8 @@ sealed class Tier /// public required int MinElevation { get; init; } + public required IReadOnlyList NextTierShell { get; init; } + public static Tier CreateFirstTier(Shell shell, Settings settings) { if (settings.MinElevation >= settings.MaxElevation) @@ -115,28 +126,26 @@ public static Tier CreateFirstTier(Shell shell, Settings settings) Slabs = [], Array = array, MinElevation = settings.MaxElevation, + NextTierShell = shell.ShellItems, }; } public Tier CreateNextTier() { - // The shell of the parent tier defines what we need to cover in this tier. - var shellToCover = ShellLogic.ComputeShells(Array.AsArea()) - .Where(shell => !shell.IsHole) - .Single(); - - // If there's no shell, there's nothing to do. - if (shellToCover is null) + if (NextTierShell.Count < 1) { - throw new Exception("Impossible"); + throw new Exception("Assert fail - empty shell should be impossible"); } var slabs = new List(); - var uncoveredShellPoints = shellToCover.ShellItems.Select(item => item.XZ).ToHashSet(); + var uncoveredShellPoints = NextTierShell.Select(item => item.XZ).ToHashSet(); + int neededCoverage = uncoveredShellPoints.Count * Math.Clamp(Settings.RequiredCoveragePerTier, 0, 100) / 100; + int stopAt = uncoveredShellPoints.Count - neededCoverage; + stopAt = Math.Clamp(stopAt, 0, uncoveredShellPoints.Count - 1); - var currentShellItems = shellToCover.ShellItems.ToList(); + var currentShellItems = NextTierShell.ToList(); - while (uncoveredShellPoints.Count > 0) + while (uncoveredShellPoints.Count > stopAt) { var shellItems = currentShellItems; @@ -165,7 +174,7 @@ public Tier CreateNextTier() } // Determine the run shape based on unique XZ coordinates. - int uniqueXzCount = 4 + Settings.PRNG.NextInt32(4); + int uniqueXzCount = Settings.MinRunLength + Settings.PRNG.NextInt32(Settings.RunLengthRand); int seedPositionInRun = Settings.PRNG.NextInt32(uniqueXzCount); int uniqueXzsBeforeSeed = seedPositionInRun; @@ -199,17 +208,18 @@ public Tier CreateNextTier() .Distinct() .ToList(); + int slabDrop = Settings.MinDrop + Settings.PRNG.NextInt32(Settings.DropRand); int ancestorCount; int slabElevation; if (parentSlabs.Any()) { ancestorCount = parentSlabs.Max(x => x.AncestorCount) + 1; - slabElevation = parentSlabs.Min(x => x.MinElevation) - 1; + slabElevation = parentSlabs.Min(x => x.MinElevation) - slabDrop; } else { ancestorCount = 0; - slabElevation = this.MinElevation - 1; + slabElevation = this.MinElevation - slabDrop; } var slabXZs = slabRunItems.Select(x => x.XZ).ToList(); @@ -231,10 +241,7 @@ public Tier CreateNextTier() uncoveredShellPoints.Remove(xz); } - if (uncoveredShellPoints.Count > 0) - { - currentShellItems = ShellLogic.WalkShellFromPoint(Array.AsArea(), uniqueXzsInRun.First()); - } + currentShellItems = ShellLogic.WalkShellFromPoint(Array.AsArea(), uniqueXzsInRun.First()); } if (slabs.Count == 0) @@ -249,6 +256,7 @@ public Tier CreateNextTier() Slabs = slabs, Array = Array, MinElevation = Math.Min(this.MinElevation, slabs.Min(slab => slab.MinElevation)), + NextTierShell = currentShellItems, }; } } diff --git a/src/Blocktavius.Tests/Snapshots/ElevationImages/NewHill01.png b/src/Blocktavius.Tests/Snapshots/ElevationImages/NewHill01.png index 81a9ae6fde5db6a570066a6f8ce59d96434753db..60856ffe8db796b601fd577299dbe1a80e6706bc 100644 GIT binary patch delta 543 zcmV+)0^t3@3eyT9iBL{Q4GJ0x0000DNk~Le0000X0000V2m=5B0A5N|fUzM#1Ap8} zL_t(IPi2!~f#e_zg%MB#7{EXTU;qO#fB+0&00Ri9ff}d*1dM?i7z1}-fSujFdp~Ac z_(+qN;6C#1nA)$u<-K#kqeDdY+ULRKeQ+U&k5MG9rIgY)3?KnOVq_gcoTQ{0H;jB> zlk)(Y?7@kuW)5tC6of-ja?VN=3x8j0oiATNm{BE{!dZ2#wOU)ts+-^=0ziavB5kdk zngv<8`ru z7~?UG*C5Cu7Glkq#5vyY=Xt+3L0#LlgMJu6koO;C1o>81@O3iuOkAeO0e|5f#C@Nw zQyY#f0uwQ#bkbPF3!=4Ba3lmH0*;qb&)YrrmFKZ?8@^{+V1x@%&9U;hVLYxrWFmuehxJ~usVXmLoy*9DDcM8Gk<(Tyrr1k4QcTebS zrI1<2B)mj2?HsleFB^_UEOfvUfzWDAZ%WlmB>EKtgbBAG1XmDJr1ix3boTJaA)0N1 zSl`nc`TJZ4S}PFPYIDp$Vw4U4j{)L}L`@v#Mnt<0R_4^+MpiDeJGXYfliXs%Mlo_* h=k|ZVKl-`t++VygLcK;$c~1ZU002ovPDHLkV1n9|?+*X~ delta 525 zcmV+o0`mRS3c(5?iBL{Q4GJ0x0000DNk~Le0000W0000U2m=5B0D9Ey8?hll1Aod% zL_t(IPi>QJ+1wxyMMuCIhye^B00S7n00J-&0~kO+48#Bi5P*R-5CiSGY_je1w)e+= zFo$7gxY)l6_xJx5V+_GLcmI9F%7*wA6NK|W2=BeyfLM+x=bTvZcTDm=D6LjgO1YGj zLa-NlfM6UPf@gEi`wkI8e1-53K7YixVr(v@LXb$+swfFOThZRVqNWrSV&N{_l%N!a z_l}wlDVbv^>;|Yzduz8bT5F}W1`ugZaWzkdR@pu0=Vw2Sk0@Rh^0r(IwuV3hz<1~O6KGUQX>IFCim@R;Uzks=oK7Z3*0p<-H z$G+>*6y}n=r-)jQW4_lBV|0L=0T{!kcwF&ws>Evnj1xd}+8h<+Tw6QO*@wO7oc#lM zuSrv)CSCV-G62dT%&eGZW=;Uktx4|yr}crl9gI7Z0Gk1D*_t$SAcvjOL6-pA>SFbf zBPnuL?9f$IOktkwOKMV1hcR<7^G&C*}8V+OMd`)7CB*$@no2fout=m zd49uua2&O~AHFF>$bOrQFYC*&yr93|;A=etTuvf|S6O%Lr)0D6ykN86VMQw6T=bGh P00000NkvXXu0mjf!olil diff --git a/src/Blocktavius.Tests/Test1.cs b/src/Blocktavius.Tests/Test1.cs index 6e26ba8..dd9f272 100644 --- a/src/Blocktavius.Tests/Test1.cs +++ b/src/Blocktavius.Tests/Test1.cs @@ -1,6 +1,7 @@ using Blocktavius.Core; using Blocktavius.Core.Generators; using Blocktavius.Core.Generators.BasicHill; +using Blocktavius.Core.Generators.Hills; namespace Blocktavius.Tests { @@ -66,6 +67,33 @@ public void AdamantCliffKnownBug() Assert.Fail("ATTENTION: Is the bug in Adamant Cliff fixed? Or is it just fixed for this particular seed?"); } + [TestMethod] + public void ExerciseNewHill() + { + var prng = PRNG.Create(new Random()); + Console.WriteLine(prng.Serialize()); + + var area = TestUtil.CreateAreaFromAscii(@" +xxxxxx +x____x +x_____ +x____x +xxxxxx"); + + var shell = ShellLogic.ComputeShells(area).Single(); + var settings = new NewHill.Settings + { + MaxElevation = 30, + MinElevation = 10, + PRNG = prng, + }; + + for (int i = 0; i < 1000; i++) + { + NewHill.BuildNewHill(settings, shell); + } + } + [TestMethod] public void ExerciseTileTagger() { From c80573e0b781b5a045f358988be83fa74947d1b7 Mon Sep 17 00:00:00 2001 From: Ryan Kramer Date: Fri, 9 Jan 2026 13:53:01 -0600 Subject: [PATCH 10/10] use MutableList2D instead of fixed size array --- .../Generators/Hills/NewHill.cs | 18 ++++++------------ src/Blocktavius.Core/MutableList2D.cs | 2 +- 2 files changed, 7 insertions(+), 13 deletions(-) diff --git a/src/Blocktavius.Core/Generators/Hills/NewHill.cs b/src/Blocktavius.Core/Generators/Hills/NewHill.cs index 0a46249..dce2bd1 100644 --- a/src/Blocktavius.Core/Generators/Hills/NewHill.cs +++ b/src/Blocktavius.Core/Generators/Hills/NewHill.cs @@ -34,7 +34,7 @@ public static I2DSampler BuildNewHill(Settings settings, Shell shell) { tier = tier.CreateNextTier(); } - return tier.Array.Crop(); + return tier.Array.Cropped; } public record struct HillItem @@ -62,23 +62,19 @@ public sealed class Slab sealed class HillItemArray { - private readonly MutableArray2D array; - private readonly Rect.BoundsFinder boundsFinder = new(); + private readonly MutableList2D array; public HillItemArray(Rect bounds) { - this.array = new MutableArray2D(bounds, new HillItem { Elevation = emptyValue, Slab = null }); + this.array = new MutableList2D(new HillItem { Elevation = emptyValue, Slab = null }, bounds); } public void Put(XZ xz, HillItem hillItem) { array.Put(xz, hillItem); - boundsFinder.Include(xz); } - public I2DSampler Uncropped => array; - - public I2DSampler Crop() => array.Crop(boundsFinder.CurrentBounds() ?? array.Bounds); + public I2DSampler Cropped => array; public I2DSampler AsArea() => array.Project(item => item.Elevation > emptyValue); } @@ -107,9 +103,7 @@ public static Tier CreateFirstTier(Shell shell, Settings settings) throw new ArgumentException("MinElevation must be less than MaxElevation"); } - // Reserve space and just hope it's enough... TODO make this bulletproof! - int expansion = settings.MaxElevation - settings.MinElevation; - var array = new HillItemArray(shell.IslandArea.Bounds.Expand(expansion * 2)); + var array = new HillItemArray(shell.IslandArea.Bounds); foreach (var xz in shell.IslandArea.Bounds.Enumerate()) { @@ -202,7 +196,7 @@ public Tier CreateNextTier() // Find parent slabs by looking at the neighbors of the run items. var parentSlabs = slabRunItems - .Select(item => Array.Uncropped.Sample(item.XZ.Step(item.InsideDirection))) + .Select(item => Array.Cropped.Sample(item.XZ.Step(item.InsideDirection))) .Select(hillItem => hillItem.Slab) .WhereNotNull() .Distinct() diff --git a/src/Blocktavius.Core/MutableList2D.cs b/src/Blocktavius.Core/MutableList2D.cs index aac67ca..44b323f 100644 --- a/src/Blocktavius.Core/MutableList2D.cs +++ b/src/Blocktavius.Core/MutableList2D.cs @@ -41,7 +41,7 @@ public T Sample(XZ xz) return defaultValue; } - public void Set(XZ xz, T value) + public void Put(XZ xz, T value) { if (!xLookup.TryGetValue(xz.X, out var zLookup)) {