Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ static HillType()
Register<PlainHillDesigner>();
Register<CornerPusherHillDesigner>();
Register<BubblerHillDesigner>();
Register<NewHillDesigner>();
}

public sealed class PropGridItemsSource : IItemsSource
Expand Down
110 changes: 110 additions & 0 deletions src/Blocktavius.AppDQB2/ScriptNodes/HillDesigners/NewHillDesigner.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
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();
}

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)
{
return null;
}

var settings = new NewHill.Settings()
{
MaxElevation = 80,
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 };
}

class Mutation : StageMutation
{
public required I2DSampler<NewHill.HillItem> 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 topperId;
if (item.Slab != null)
{
topperId = Convert.ToUInt16(item.Slab.AncestorCount % 2 + 4);
}
else
{
topperId = 3;
}

for (int y = 1; y < item.Elevation; y++)
{
chunk.SetBlock(new Point(xz, y), 5);
}
chunk.SetBlock(new Point(xz, item.Elevation), topperId);
}
}
}
}
}
}
257 changes: 257 additions & 0 deletions src/Blocktavius.Core/Generators/Hills/NewHill.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,257 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Blocktavius.Core.Generators.Hills;

/// <summary>
/// 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??
/// </summary>
public static class NewHill
{
public sealed record Settings
{
public required PRNG PRNG { 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;

public static I2DSampler<HillItem> BuildNewHill(Settings settings, Shell shell)
{
var tier = Tier.CreateFirstTier(shell, settings);
while (tier.MinElevation > settings.MinElevation)
{
tier = tier.CreateNextTier();
}
return tier.Array.Cropped;
}

public record struct HillItem
{
public required int Elevation { get; init; }
public required Slab? Slab { get; init; }
}

public sealed class Slab
{
public required IReadOnlyList<XZ> XZs { get; init; }
public required IReadOnlyList<Slab> ParentSlabs { get; init; }

/// <summary>
/// FUTURE - We may allow elevation to vary within the slab, but not yet.
/// </summary>
public required int MinElevation { get; init; }

/// <summary>
/// If no parent slabs exist, set to 0.
/// Else set to 1 + ParentSlabs.Max(x => x.AncestorCount).
/// </summary>
public required int AncestorCount { get; init; }
}

sealed class HillItemArray
{
private readonly MutableList2D<HillItem> array;

public HillItemArray(Rect bounds)
{
this.array = new MutableList2D<HillItem>(new HillItem { Elevation = emptyValue, Slab = null }, bounds);
}

public void Put(XZ xz, HillItem hillItem)
{
array.Put(xz, hillItem);
}

public I2DSampler<HillItem> Cropped => array;

public I2DSampler<bool> AsArea() => array.Project(item => item.Elevation > emptyValue);
}

/// <summary>
/// Each tier is basically "add slabs until previous tier is fully covered."
/// </summary>
sealed class Tier
{
public required Tier? ParentTier { get; init; }
public required Settings Settings { get; init; }
public required IReadOnlyList<Slab> Slabs { get; init; }
public required HillItemArray Array { get; init; }

/// <summary>
/// Must be set to Slabs.Min(slab => slab.MinElevation) when any slabs exist.
/// </summary>
public required int MinElevation { get; init; }

public required IReadOnlyList<ShellItem> NextTierShell { get; init; }

public static Tier CreateFirstTier(Shell shell, Settings settings)
{
if (settings.MinElevation >= settings.MaxElevation)
{
throw new ArgumentException("MinElevation must be less than MaxElevation");
}

var array = new HillItemArray(shell.IslandArea.Bounds);

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,
NextTierShell = shell.ShellItems,
};
}

public Tier CreateNextTier()
{
if (NextTierShell.Count < 1)
{
throw new Exception("Assert fail - empty shell should be impossible");
}

var slabs = new List<Slab>();
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 = NextTierShell.ToList();

while (uncoveredShellPoints.Count > stopAt)
{
var shellItems = currentShellItems;

// For quick lookups of a shell item's indices, handling corners correctly.
var shellItemsIndexMap = new Dictionary<XZ, List<int>>();
for (int i = 0; i < shellItems.Count; i++)
{
var xz = shellItems[i].XZ;
if (!shellItemsIndexMap.TryGetValue(xz, out var indices))
{
indices = new List<int>();
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 = Settings.MinRunLength + Settings.PRNG.NextInt32(Settings.RunLengthRand);
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<XZ> { 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<ShellItem>();
var uniqueXzsInRun = new HashSet<XZ>();
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.Cropped.Sample(item.XZ.Step(item.InsideDirection)))
.Select(hillItem => hillItem.Slab)
.WhereNotNull()
.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) - slabDrop;
}
else
{
ancestorCount = 0;
slabElevation = this.MinElevation - slabDrop;
}

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

currentShellItems = ShellLogic.WalkShellFromPoint(Array.AsArea(), uniqueXzsInRun.First());
}

if (slabs.Count == 0)
{
throw new Exception("Assert fail - empty tier should be impossible");
}

return new Tier
{
ParentTier = this,
Settings = Settings,
Slabs = slabs,
Array = Array,
MinElevation = Math.Min(this.MinElevation, slabs.Min(slab => slab.MinElevation)),
NextTierShell = currentShellItems,
};
}
}
}
2 changes: 1 addition & 1 deletion src/Blocktavius.Core/MutableList2D.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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))
{
Expand Down
Loading