prototype for wave function collapse

given an area of all the same block, replace all those blocks with blocks that match the surrounding non-same blocks based on what blocks should 'match'. Note that this is a single-pass algorithm right now, which means that it creates a bunch of bad blocks as it gets further from the edge and more conflicts are found.

We may try to update this more in the future, or we may just go with something simpler like a maze generator using border blocks or 9-grid. But the results of this are tantalizing...
This commit is contained in:
haven1433 2023-02-18 00:53:05 -06:00
parent affcbe7a0d
commit ff3f2bd3df
5 changed files with 242 additions and 7 deletions

View File

@ -290,6 +290,20 @@ namespace HavenSoft.HexManiac.Core {
var index = rnd.Next(list.Count);
return list[index];
}
public static T Ensure<T>(this IList<T> list, Func<T, bool> predicate, T element) {
var existing = list.FirstOrDefault(predicate);
if (existing != null) return existing;
list.Add(element);
return element;
}
public static V Ensure<K, V>(this IDictionary<K, V> dict, K key, Func<V> valueFactory) {
if (dict.TryGetValue(key, out var result)) return result;
var value = valueFactory();
dict[key] = value;
return value;
}
}
public static class NativeProcess {

View File

@ -995,10 +995,10 @@ namespace HavenSoft.HexManiac.Core.ViewModels.Map {
var address = start + ((yy + y) * width + xx + x) * 2;
// var block = blockValues[xx % blockValues.GetLength(0), yy % blockValues.GetLength(1)];
var blockValue = model.ReadMultiByteValue(address, 2);
var originalBlockValue = blockValue;
lastDrawVal = blockValue;
if (block >= 0) blockValue = (blockValue & 0xFC00) + block;
if (collision >= 0) blockValue = (blockValue & 0x3FF) + (collision << 10);
if (blockValue != originalBlockValue) {
if (blockValue != lastDrawVal) {
model.WriteMultiByteValue(address, 2, futureToken(), blockValue);
changeCount++;
}
@ -1112,6 +1112,49 @@ namespace HavenSoft.HexManiac.Core.ViewModels.Map {
ClearPixelCache();
}
public void PaintWaveFunction(ModelDelta token, double x, double y, Func<int, int, int> wave) {
(x, y) = ((x - leftEdge) / spriteScale, (y - topEdge) / spriteScale);
(x, y) = (x / 16, y / 16);
var layout = GetLayout();
var (width, height) = (layout.GetValue("width"), layout.GetValue("height"));
var border = GetBorderThickness(layout);
var (xx, yy) = ((int)x - border.West, (int)y - border.North);
if (xx < 0 || yy < 0 || xx > width || yy > height) return;
var start = layout.GetAddress("blockmap");
// first pass: set all the effected spaces to 0 so they won't count
var toDraw = new Queue<Point>();
toDraw.Enqueue(new(xx, yy));
var drawn = new List<Point>();
lock (pixelWriteLock) {
while (toDraw.Count > 0) {
var p = toDraw.Dequeue();
if (drawn.Contains(p)) continue;
var address = start + (p.Y * width + (p.X - 1)) * 2;
if (p.X - 1 > 0 && model.ReadMultiByteValue(address, 2) == lastDrawVal) toDraw.Enqueue(new(p.X - 1, p.Y));
address = start + (p.Y * width + (p.X + 1)) * 2;
if (p.X - 1 > 0 && model.ReadMultiByteValue(address, 2) == lastDrawVal) toDraw.Enqueue(new(p.X + 1, p.Y));
address = start + ((p.Y - 1) * width + p.X) * 2;
if (p.X - 1 > 0 && model.ReadMultiByteValue(address, 2) == lastDrawVal) toDraw.Enqueue(new(p.X, p.Y - 1));
address = start + ((p.Y + 1) * width + p.X) * 2;
if (p.X - 1 > 0 && model.ReadMultiByteValue(address, 2) == lastDrawVal) toDraw.Enqueue(new(p.X, p.Y + 1));
address = start + (p.Y * width + p.X) * 2;
model.WriteMultiByteValue(address, 2, token, 0);
drawn.Add(p);
}
// second pass: wave-fill in the reverse order (outside in)
drawn.Reverse();
foreach (var p in drawn) {
var targetVal = wave(p.X, p.Y);
var address = start + (p.Y * width + p.X) * 2;
model.WriteMultiByteValue(address, 2, token, targetVal);
}
}
ClearPixelCache();
}
#endregion
#region Events

View File

@ -93,6 +93,7 @@ namespace HavenSoft.HexManiac.Core.ViewModels.Map {
public EventTemplate Templates => templates;
private int PrimaryBlocks { get; }
private int PrimaryTiles { get; }
private string hoverPoint, zoomLevel;
@ -312,6 +313,7 @@ namespace HavenSoft.HexManiac.Core.ViewModels.Map {
history.Bind(nameof(history.HasDataChange), (sender, e) => NotifyPropertyChanged(nameof(Name)));
var isFRLG = model.IsFRLG();
PrimaryTiles = isFRLG ? 640 : 512;
PrimaryBlocks = PrimaryTiles;
this.format = new Format(model);
var map = new BlockMapViewModel(fileSystem, Tutorials, viewPort, format, 3, 0);
@ -625,15 +627,17 @@ namespace HavenSoft.HexManiac.Core.ViewModels.Map {
Point drawSource, lastDraw;
private void DrawDown(double x, double y, PrimaryInteractionStart click) {
interactionType = PrimaryInteractionType.Draw;
if (click == PrimaryInteractionStart.ControlClick) interactionType = PrimaryInteractionType.RectangleDraw;
if ((click & PrimaryInteractionStart.ControlClick) != 0) interactionType = PrimaryInteractionType.RectangleDraw;
if (click == PrimaryInteractionStart.ShiftClick) interactionType = PrimaryInteractionType.Draw9Grid;
var map = MapUnderCursor(x, y);
if (click == PrimaryInteractionStart.DoubleClick) {
if ((click & PrimaryInteractionStart.DoubleClick) != 0) {
if (drawBlockIndex < 0 && collisionIndex < 0) {
// nothing to paint
interactionType = PrimaryInteractionType.None;
} else {
if (map != null && !drawMultipleTiles) {
if (interactionType == PrimaryInteractionType.RectangleDraw && map != null) {
map.PaintWaveFunction(history.CurrentChange, x, y, RunWaveFunctionCollapseWithColision);
} else if (map != null && !drawMultipleTiles) {
if (blockBag.Contains(drawBlockIndex)) {
map.PaintBlockBag(history.CurrentChange, blockBag, collisionIndex, x, y);
} else {
@ -1572,6 +1576,172 @@ namespace HavenSoft.HexManiac.Core.ViewModels.Map {
#endregion
#region Wave Function Collapse
public record CollapseProbability(int Block) { public int Count { get; set; } };
public record WaveNeighbors(List<CollapseProbability> Left, List<CollapseProbability> Right, List<CollapseProbability> Up, List<CollapseProbability> Down);
Dictionary<long, WaveNeighbors[]> waveFunctionPrimary;
Dictionary<long, WaveNeighbors[]> waveFunctionSecondary;
Dictionary<long, WaveNeighbors[]> waveFunctionMixed;
private void CalculateWaveCollapseProbabilities() {
waveFunctionPrimary = new();
waveFunctionSecondary = new();
waveFunctionMixed = new();
foreach (var bank in AllMapsModel.Create(model, () => history.CurrentChange)) {
foreach (var map in bank) {
var cells = map.Blocks;
var primary = map.Layout.PrimaryBlockset.Start;
var secondary = map.Layout.SecondaryBlockset.Start;
var mixed = ((long)primary << 32) + secondary;
var primaryWaveNeighbors = waveFunctionPrimary.Ensure(primary, () => new WaveNeighbors[PrimaryBlocks]);
var secondaryWaveNeighbors = waveFunctionSecondary.Ensure(secondary, () => new WaveNeighbors[TotalBlocks - PrimaryBlocks]);
var mixedWaveNeighbors = waveFunctionMixed.Ensure(mixed, () => new WaveNeighbors[TotalBlocks]);
for (int x = 0; x < cells.Width; x++) {
for (int y = 0; y < cells.Height; y++) {
var center = cells[x, y];
if (IsPrimaryBlock(center)) {
CheckPrimary(cells, x - 1, y, center, primaryWaveNeighbors, mixedWaveNeighbors, wn => wn.Left);
CheckPrimary(cells, x + 1, y, center, primaryWaveNeighbors, mixedWaveNeighbors, wn => wn.Right);
CheckPrimary(cells, x, y - 1, center, primaryWaveNeighbors, mixedWaveNeighbors, wn => wn.Up);
CheckPrimary(cells, x, y + 1, center, primaryWaveNeighbors, mixedWaveNeighbors, wn => wn.Down);
} else {
CheckSecondary(cells, x - 1, y, center, secondaryWaveNeighbors, mixedWaveNeighbors, wn => wn.Left);
CheckSecondary(cells, x + 1, y, center, secondaryWaveNeighbors, mixedWaveNeighbors, wn => wn.Right);
CheckSecondary(cells, x, y - 1, center, secondaryWaveNeighbors, mixedWaveNeighbors, wn => wn.Up);
CheckSecondary(cells, x, y + 1, center, secondaryWaveNeighbors, mixedWaveNeighbors, wn => wn.Down);
}
}
}
}
}
}
private int RunWaveFunctionCollapseWithColision(int xx, int yy) {
var blockIndex = RunWaveFunctionCollapse(xx, yy);
var preferredCollision = GetPreferredCollision(blockIndex);
return (preferredCollision << 10) | blockIndex;
}
private int RunWaveFunctionCollapse(int xx, int yy) {
if (waveFunctionPrimary == null) CalculateWaveCollapseProbabilities();
var layout = new LayoutModel(PrimaryMap.GetLayout());
var primary = layout.PrimaryBlockset.Start;
var secondary = layout.SecondaryBlockset.Start;
var mixed = ((long)primary << 32) + secondary;
var primaryWaveNeighbors = waveFunctionPrimary.Ensure(primary, () => new WaveNeighbors[PrimaryBlocks]);
var secondaryWaveNeighbors = waveFunctionSecondary.Ensure(secondary, () => new WaveNeighbors[TotalBlocks - PrimaryBlocks]);
var mixedWaveNeighbors = waveFunctionMixed.Ensure(mixed, () => new WaveNeighbors[TotalBlocks]);
var cells = layout.BlockMap;
var probabilities = new List<List<CollapseProbability>>();
AddProbabilities(probabilities, cells, xx - 1, yy, primaryWaveNeighbors, secondaryWaveNeighbors, mixedWaveNeighbors, wv => wv.Right);
AddProbabilities(probabilities, cells, xx + 1, yy, primaryWaveNeighbors, secondaryWaveNeighbors, mixedWaveNeighbors, wv => wv.Left);
AddProbabilities(probabilities, cells, xx, yy - 1, primaryWaveNeighbors, secondaryWaveNeighbors, mixedWaveNeighbors, wv => wv.Down);
AddProbabilities(probabilities, cells, xx, yy + 1, primaryWaveNeighbors, secondaryWaveNeighbors, mixedWaveNeighbors, wv => wv.Up);
// remove all the empty probability sets (they're not adding any restrictions)
for (int i = probabilities.Count - 1; i >= 0; i--) {
if (probabilities[i].Count == 0) probabilities.RemoveAt(i);
}
// the block is constricted in options based on its neighbors
// if the neighbor can only be A/B based on the left and only B/C based on the top, it must be B.
while (probabilities.Count > 1) {
var last = probabilities[probabilities.Count - 1];
var next = probabilities[probabilities.Count - 2];
probabilities.RemoveAt(probabilities.Count - 1);
probabilities.RemoveAt(probabilities.Count - 1);
var merged = new List<CollapseProbability>();
foreach (var cp1 in last) {
var cp2 = next.FirstOrDefault(match => match.Block == cp1.Block);
if (cp2 == null) continue; // no match
merged.Add(new(cp1.Block) { Count = cp1.Count + cp2.Count });
}
if (merged.Count == 0) {
// couldn't find a match
// matching one is better than matching neither
probabilities.Add(rnd.Next(2) == 0 ? last : next);
} else {
probabilities.Add(merged);
}
}
if (probabilities.Count == 0 || probabilities[0].Count == 0) {
// no restriction, pick any block
var (availableBlocks, _) = PrimaryMap.MapRepointer.EstimateBlockCount(layout.Element, true);
return rnd.Next(availableBlocks);
} else if (probabilities[0].Count == 1) {
// only one option, go with that
return probabilities[0][0].Block;
}
// pick one block from among the available blocks at random (weighted based on use)
var totalOptions = probabilities[0].Sum(cp => cp.Count);
var selection = rnd.Next(totalOptions);
var index = 0;
while (selection > probabilities[0][index].Count) {
selection -= probabilities[0][index].Count;
index += 1;
}
return probabilities[0][index].Block;
}
private void AddProbabilities(List<List<CollapseProbability>> probabilities, BlockCells cells, int xx, int yy, WaveNeighbors[] primaryWaveNeighbors, WaveNeighbors[] secondaryWaveNeighbors, WaveNeighbors[] mixedWaveNeighbors, Func<WaveNeighbors, List<CollapseProbability>> reverse) {
if (xx < 0 || yy < 0 || xx >= cells.Width || yy >= cells.Height) return;
var edge = cells[xx, yy].Tile;
if (edge == 0) return;
if (IsPrimaryBlock(cells[xx, yy])) {
var mergedProbabilities = new List<CollapseProbability>();
if (primaryWaveNeighbors[edge] != null) mergedProbabilities.AddRange(reverse(primaryWaveNeighbors[edge]));
if (mixedWaveNeighbors[edge] != null) mergedProbabilities.AddRange(reverse(mixedWaveNeighbors[edge]));
probabilities.Add(mergedProbabilities);
} else {
var mergedProbabilities = new List<CollapseProbability>();
if (secondaryWaveNeighbors[edge - PrimaryBlocks] != null) mergedProbabilities.AddRange(reverse(secondaryWaveNeighbors[edge - PrimaryBlocks]));
if (mixedWaveNeighbors[edge] != null) mergedProbabilities.AddRange(reverse(mixedWaveNeighbors[edge]));
probabilities.Add(mergedProbabilities);
}
}
private void CheckPrimary(BlockCells cells, int x, int y, BlockCell center, WaveNeighbors[] primaryWaveNeighbors, WaveNeighbors[] mixedWaveNeighbors, Func<WaveNeighbors, List<CollapseProbability>> direction) {
var primaryElement = primaryWaveNeighbors[center.Tile];
var mixedElement = mixedWaveNeighbors[center.Tile];
if (x < 0 || x >= cells.Width || y < 0 || y >= cells.Height) return;
var edge = cells[x, y];
if (IsPrimaryBlock(edge)) {
if (primaryElement == null) primaryElement = primaryWaveNeighbors[center.Tile] = new(new(), new(), new(), new());
var collapse = direction(primaryElement).Ensure(cp => cp.Block == edge.Tile, new CollapseProbability(edge.Tile));
collapse.Count += 1;
} else {
if (mixedElement == null) mixedElement = mixedWaveNeighbors[center.Tile] = new(new(), new(), new(), new());
var collapse = direction(mixedElement).Ensure(cp => cp.Block == edge.Tile, new CollapseProbability(edge.Tile));
collapse.Count += 1;
}
}
private void CheckSecondary(BlockCells cells, int x, int y, BlockCell center, WaveNeighbors[] secondaryWaveNeighbors, WaveNeighbors[] mixedWaveNeighbors, Func<WaveNeighbors, List<CollapseProbability>> direction) {
var secondaryElement = secondaryWaveNeighbors[center.Tile - PrimaryBlocks];
var mixedElement = mixedWaveNeighbors[center.Tile];
if (x < 0 || x >= cells.Width || y < 0 || y >= cells.Height) return;
var edge = cells[x, y];
if (IsPrimaryBlock(edge)) {
if (secondaryElement == null) secondaryElement = secondaryWaveNeighbors[center.Tile - PrimaryBlocks] = new(new(), new(), new(), new());
var collapse = direction(secondaryElement).Ensure(cp => cp.Block == edge.Tile, new CollapseProbability(edge.Tile));
collapse.Count += 1;
} else {
if (mixedElement == null) mixedElement = mixedWaveNeighbors[center.Tile] = new(new(), new(), new(), new());
var collapse = direction(mixedElement).Ensure(cp => cp.Block == edge.Tile, new CollapseProbability(edge.Tile));
collapse.Count += 1;
}
}
private bool IsPrimaryBlock(BlockCell cell) => cell.Tile < PrimaryBlocks;
private int TotalBlocks => 1024;
#endregion
private Dictionary<long, int[]> preferredCollisionsPrimary;
private Dictionary<long, int[]> preferredCollisionsSecondary;
private void CountCollisionForBlocks() {
@ -1708,7 +1878,14 @@ namespace HavenSoft.HexManiac.Core.ViewModels.Map {
public enum EventCreationType { None, Object, Warp, Script, Signpost, Fly }
public enum PrimaryInteractionStart { None, Click, DoubleClick, ControlClick, ShiftClick }
[Flags]
public enum PrimaryInteractionStart {
None = 0,
Click = 1,
DoubleClick = 2,
ControlClick = 4,
ShiftClick = 8,
}
public enum PrimaryInteractionType { None, Draw, Event, RectangleDraw, Draw9Grid }
public record BlocksetCache(ObservableCollection<string> Primary, ObservableCollection<string> Secondary) {

View File

@ -359,7 +359,7 @@ namespace HavenSoft.HexManiac.Core.ViewModels.Map {
DataMoved.Raise(this, new("Block", blockStart));
}
private (int currentCount, int maxCount) EstimateBlockCount(ModelArrayElement layout, bool primary) {
public (int currentCount, int maxCount) EstimateBlockCount(ModelArrayElement layout, bool primary) {
var blocksetName = primary ? Format.PrimaryBlockset : Format.SecondaryBlockset;
if (layout == null) return (0, 0);
var blockset = layout.GetSubTable(blocksetName)[0];

View File

@ -125,6 +125,7 @@ namespace HavenSoft.HexManiac.WPF.Controls {
if (e.ClickCount == 2) interactionStart = PrimaryInteractionStart.DoubleClick;
if (Keyboard.Modifiers == ModifierKeys.Shift) interactionStart = PrimaryInteractionStart.ShiftClick;
if (Keyboard.Modifiers == ModifierKeys.Control) interactionStart = PrimaryInteractionStart.ControlClick;
if (e.ClickCount == 2 && Keyboard.Modifiers == ModifierKeys.Control) interactionStart = PrimaryInteractionStart.ControlClick | PrimaryInteractionStart.DoubleClick;
vm.PrimaryDown(p.X, p.Y, interactionStart);
} else if (e.MiddleButton == MouseButtonState.Pressed) {
withinMapInteraction = MouseButton.Middle;