mirror of
https://github.com/haven1433/HexManiacAdvance.git
synced 2026-03-21 17:34:13 -05:00
Wave Function Collapse improvements
This commit is contained in:
parent
b0172ba7f1
commit
0450227114
|
|
@ -12,6 +12,7 @@ namespace HavenSoft.HexManiac.Core.Models {
|
|||
public override int GetHashCode() => X * 101 + Y * 37;
|
||||
public static Point operator *(Point a, int num) => new Point(a.X * num, a.Y * num);
|
||||
public static Point operator +(Point a, Point b) => new Point(a.X + b.X, a.Y + b.Y);
|
||||
public static Point operator -(Point a) => new Point(-a.X, -a.Y);
|
||||
public static Point operator -(Point a, Point b) => new Point(a.X - b.X, a.Y - b.Y);
|
||||
public static bool operator ==(Point a, Point b) => a.Equals(b);
|
||||
public static bool operator !=(Point a, Point b) => !a.Equals(b);
|
||||
|
|
|
|||
|
|
@ -11,6 +11,8 @@ using System;
|
|||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using static HavenSoft.HexManiac.Core.ViewModels.Map.MapSliderIcons;
|
||||
|
||||
namespace HavenSoft.HexManiac.Core.ViewModels.Map {
|
||||
|
|
@ -952,11 +954,13 @@ namespace HavenSoft.HexManiac.Core.ViewModels.Map {
|
|||
/// If blockIndex is not valid, it's ignored.
|
||||
/// </summary>
|
||||
public void DrawBlock(ModelDelta token, int blockIndex, int collisionIndex, double x, double y) {
|
||||
waveFunctionActive?.Cancel();
|
||||
var (xx, yy) = ConvertCoordinates(x, y);
|
||||
DrawBlock(token, blockIndex, collisionIndex, xx, yy);
|
||||
}
|
||||
|
||||
public void DrawBlock(ModelDelta token, int blockIndex, int collisionIndex, int xx, int yy) {
|
||||
waveFunctionActive?.Cancel();
|
||||
var layout = GetLayout();
|
||||
var (width, height) = (layout.GetValue("width"), layout.GetValue("height"));
|
||||
var border = GetBorderThickness(layout);
|
||||
|
|
@ -987,6 +991,7 @@ namespace HavenSoft.HexManiac.Core.ViewModels.Map {
|
|||
}
|
||||
|
||||
public void Draw9Grid(ModelDelta token, int[,] grid, double x, double y) {
|
||||
waveFunctionActive?.Cancel();
|
||||
var layout = GetLayout();
|
||||
var (width, height) = (layout.GetValue("width"), layout.GetValue("height"));
|
||||
var (xx, yy) = ConvertCoordinates(x, y);
|
||||
|
|
@ -1138,6 +1143,7 @@ namespace HavenSoft.HexManiac.Core.ViewModels.Map {
|
|||
private int[,]? innerCornersFor9Grid;
|
||||
// 9-grid is always drawn 2x2
|
||||
public void Draw9Grid(ModelDelta token, int[,] grid, int x, int y) {
|
||||
waveFunctionActive?.Cancel();
|
||||
var innerCornersFor9Grid = this.innerCornersFor9Grid ?? new[,] { { grid[1, 1], grid[1, 1] }, { grid[1, 1], grid[1, 1] } }; // copy so that there's no reference changing during the method
|
||||
var layout = GetLayout();
|
||||
var (width, height) = (layout.GetValue("width"), layout.GetValue("height"));
|
||||
|
|
@ -1166,6 +1172,7 @@ namespace HavenSoft.HexManiac.Core.ViewModels.Map {
|
|||
}
|
||||
|
||||
public void DrawBlocks(ModelDelta token, int[,] tiles, Point source, Point destination) {
|
||||
waveFunctionActive?.Cancel();
|
||||
while (Math.Abs(destination.X - source.X) % tiles.GetLength(0) != 0) destination -= new Point(1, 0);
|
||||
while (Math.Abs(destination.Y - source.Y) % tiles.GetLength(1) != 0) destination -= new Point(0, 1);
|
||||
|
||||
|
|
@ -1190,6 +1197,7 @@ namespace HavenSoft.HexManiac.Core.ViewModels.Map {
|
|||
}
|
||||
|
||||
public void RepeatBlock(Func<ModelDelta> futureToken, IReadOnlyList<int> blockOptions, int collision, int x, int y, int w, int h, bool refreshScreen) {
|
||||
waveFunctionActive?.Cancel();
|
||||
var layout = GetLayout();
|
||||
var (width, height) = (layout.GetValue("width"), layout.GetValue("height"));
|
||||
var start = layout.GetAddress("blockmap");
|
||||
|
|
@ -1217,6 +1225,7 @@ namespace HavenSoft.HexManiac.Core.ViewModels.Map {
|
|||
}
|
||||
|
||||
public void RepeatBlocks(Func<ModelDelta> futureToken, int[,] blockValues, int x, int y, int w, int h, bool refreshScreen) {
|
||||
waveFunctionActive?.Cancel();
|
||||
var layout = GetLayout();
|
||||
var (width, height) = (layout.GetValue("width"), layout.GetValue("height"));
|
||||
var start = layout.GetAddress("blockmap");
|
||||
|
|
@ -1257,6 +1266,7 @@ namespace HavenSoft.HexManiac.Core.ViewModels.Map {
|
|||
}
|
||||
|
||||
public void PaintBlock(ModelDelta token, int blockIndex, int collisionIndex, double x, double y) {
|
||||
waveFunctionActive?.Cancel();
|
||||
if (blockIndex == -1) return;
|
||||
(x, y) = ((x - leftEdge) / spriteScale, (y - topEdge) / spriteScale);
|
||||
(x, y) = (x / 16, y / 16);
|
||||
|
|
@ -1280,6 +1290,7 @@ namespace HavenSoft.HexManiac.Core.ViewModels.Map {
|
|||
}
|
||||
|
||||
private void PaintBlock(ModelDelta token, Point p, Point size, int start, int before, int after) {
|
||||
waveFunctionActive?.Cancel();
|
||||
if (before == after) return;
|
||||
if (p.X < 0 || p.Y < 0 || p.X >= size.X || p.Y >= size.Y) return;
|
||||
var address = start + (p.Y * size.X + p.X) * 2;
|
||||
|
|
@ -1292,6 +1303,7 @@ namespace HavenSoft.HexManiac.Core.ViewModels.Map {
|
|||
}
|
||||
|
||||
public void PaintBlockBag(ModelDelta token, List<int> blockIndexes, int collisionIndex, double x, double y) {
|
||||
waveFunctionActive?.Cancel();
|
||||
if (blockIndexes.Count < 1) return;
|
||||
(x, y) = ((x - leftEdge) / spriteScale, (y - topEdge) / spriteScale);
|
||||
(x, y) = (x / 16, y / 16);
|
||||
|
|
@ -1352,7 +1364,10 @@ namespace HavenSoft.HexManiac.Core.ViewModels.Map {
|
|||
}
|
||||
|
||||
/// <param name="wave">A function that, for a given x/y pair, returns a superposition of possible block probabilities for that cell, based on its known neighbors.</param>
|
||||
public void PaintWaveFunction(ModelDelta token, double x, double y, Func<int, int, WaveCell> wave) {
|
||||
public async void PaintWaveFunction(ModelDelta token, double x, double y, Func<int, int, WaveCell> wave, CancellationToken cancelToken) {
|
||||
waveFunctionActive?.Cancel();
|
||||
waveFunctionActive = CancellationTokenSource.CreateLinkedTokenSource(cancelToken);
|
||||
cancelToken = waveFunctionActive.Token;
|
||||
(x, y) = ((x - leftEdge) / spriteScale, (y - topEdge) / spriteScale);
|
||||
(x, y) = (x / 16, y / 16);
|
||||
var layout = GetLayout();
|
||||
|
|
@ -1374,26 +1389,81 @@ namespace HavenSoft.HexManiac.Core.ViewModels.Map {
|
|||
|
||||
// initial wave function collapse values
|
||||
foreach (var cell in allCells) toDraw[cell] = wave(cell.X, cell.Y);
|
||||
}
|
||||
|
||||
// reduction loop: find the most restricted cell, collapse it, then propogate its new restrictions
|
||||
while (toDraw.Count > 0) {
|
||||
var cellsWithRestrictions = toDraw.Values.Where(cell => cell.HasRestrictions);
|
||||
var smallest = cellsWithRestrictions.Select(v => v.Probabilities.Count).Min(); // what is the smallest number of options
|
||||
// reduction loop: find the most restricted cell, collapse it, then propogate its new restrictions
|
||||
var stack = new List<CollapseAttempt>();
|
||||
int delayCount = 0;
|
||||
var attemptLimit = 10;
|
||||
if (toDraw.Count < 300) attemptLimit *= 2;
|
||||
while (toDraw.Count > 0) {
|
||||
var cellsWithRestrictions = toDraw.Values.Where(cell => cell.HasRestrictions);
|
||||
var smallest = cellsWithRestrictions.Select(v => v.Probabilities.Count).Min(); // what is the smallest number of options
|
||||
|
||||
if (smallest > 0 && (stack.Count == 0 || stack.Sum(item => item.AttemptCount) < attemptLimit) && !cancelToken.IsCancellationRequested) {
|
||||
// good case: we have a cell to callapse
|
||||
var restrictedCells = cellsWithRestrictions.Where(cell => cell.Probabilities.Count == smallest).ToList(); // get the cells with that least number of options
|
||||
var cell = rnd.From(restrictedCells);
|
||||
var point = toDraw.Keys.Single(key => toDraw[key] == cell);
|
||||
Fill(point, cell.Collapse(rnd));
|
||||
|
||||
var attempt = cell.Collapse(rnd);
|
||||
var uncollapsedNeighbors = GetUncollapsedNeighbors(point, toDraw);
|
||||
stack.Add(new(new(point, cell), attempt & 0x3FF, uncollapsedNeighbors));
|
||||
lock (pixelWriteLock) Fill(point, cell.Collapse(rnd));
|
||||
|
||||
toDraw.Remove(point);
|
||||
foreach (var neighbor in new List<Point> { point - right, point + right, point - down, point + down, point - right - down, point + right - down, point - right + down, point + right + down }) {
|
||||
if (!toDraw.ContainsKey(neighbor)) continue;
|
||||
toDraw[neighbor] = wave(neighbor.X, neighbor.Y); // re-evaluate from scratch now that a new neighbor has been found
|
||||
}
|
||||
} else {
|
||||
// bad case:
|
||||
// (1) last collapse caused a contradiction - need to backtrack
|
||||
// or
|
||||
// (2) we've been backtracking too much along this line, we need to back out and try something else.
|
||||
if (stack.Count == 0) {
|
||||
viewPort.RaiseError("Wave Function Collapse failed: no valid options remain.");
|
||||
break;
|
||||
}
|
||||
var lastAttempt = stack.Last();
|
||||
stack.RemoveAt(stack.Count - 1);
|
||||
// restore the last attempt's cell and neighbors
|
||||
toDraw[lastAttempt.SpotToCollapse.CollapsedPoint] = lastAttempt.SpotToCollapse.CellBeforeCollapse;
|
||||
foreach (var neighbor in lastAttempt.NeighborsBeforeCollapse) {
|
||||
toDraw[neighbor.CollapsedPoint] = neighbor.CellBeforeCollapse;
|
||||
}
|
||||
lock (pixelWriteLock) Fill(lastAttempt.SpotToCollapse.CollapsedPoint, 0); // reset to uncollapsed
|
||||
// remove the chosen collapse from options
|
||||
var badOption = lastAttempt.SpotToCollapse.CellBeforeCollapse.Probabilities.Single(prob => prob.Block == lastAttempt.ChosenCollapse);
|
||||
lastAttempt.SpotToCollapse.CellBeforeCollapse.Probabilities.Remove(badOption);
|
||||
if (stack.Count > 0) stack[^1].AttemptCount += 1;
|
||||
}
|
||||
delayCount += 1;
|
||||
if (delayCount == 50) {
|
||||
await Task.Delay(5);
|
||||
delayCount = 0;
|
||||
ClearPixelCache();
|
||||
}
|
||||
}
|
||||
|
||||
ClearPixelCache();
|
||||
}
|
||||
|
||||
private static List<WaveSpot> GetUncollapsedNeighbors(Point point, Dictionary<Point, WaveCell> toDraw) {
|
||||
var results = new List<WaveSpot>();
|
||||
Point right = new(1, 0), down = new(0, 1);
|
||||
foreach (var direction in new[] { -right - down, -right, -right + down, down, -down, right + down, right, right - down }) {
|
||||
if (!toDraw.TryGetValue(point + direction, out var cell)) continue;
|
||||
results.Add(new(point + direction, cell));
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
private record WaveSpot(Point CollapsedPoint, WaveCell CellBeforeCollapse);
|
||||
private record CollapseAttempt(WaveSpot SpotToCollapse, int ChosenCollapse, List<WaveSpot> NeighborsBeforeCollapse) {
|
||||
public int AttemptCount { get; set; }
|
||||
};
|
||||
|
||||
#endregion
|
||||
|
||||
#region Events
|
||||
|
|
@ -1576,7 +1646,9 @@ namespace HavenSoft.HexManiac.Core.ViewModels.Map {
|
|||
}
|
||||
}
|
||||
|
||||
private CancellationTokenSource? waveFunctionActive;
|
||||
private void ResizeMapData(MapDirection direction, int amount) {
|
||||
waveFunctionActive?.Cancel();
|
||||
if (amount == 0) return;
|
||||
var token = tokenFactory();
|
||||
var map = GetMapModel();
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ using System.Collections.Generic;
|
|||
using System.Collections.ObjectModel;
|
||||
using System.ComponentModel;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Input;
|
||||
|
||||
|
|
@ -777,7 +778,7 @@ namespace HavenSoft.HexManiac.Core.ViewModels.Map {
|
|||
PrimaryMap.SurfConnection.FollowConnection();
|
||||
return;
|
||||
}
|
||||
DrawDown(x, y, click);
|
||||
if (DrawDown(x, y, click)) waveFunctionActive?.Cancel();
|
||||
}
|
||||
|
||||
public void PrimaryMove(double x, double y) {
|
||||
|
|
@ -796,7 +797,8 @@ namespace HavenSoft.HexManiac.Core.ViewModels.Map {
|
|||
}
|
||||
|
||||
Point drawSource, lastDraw;
|
||||
private void DrawDown(double x, double y, PrimaryInteractionStart click) {
|
||||
private bool DrawDown(double x, double y, PrimaryInteractionStart click) {
|
||||
bool didDraw = true;
|
||||
interactionType = PrimaryInteractionType.Draw;
|
||||
if ((click & PrimaryInteractionStart.ControlClick) != 0) interactionType = PrimaryInteractionType.RectangleDraw;
|
||||
if (use9Grid && IsValid9GridSelection) {
|
||||
|
|
@ -824,17 +826,20 @@ namespace HavenSoft.HexManiac.Core.ViewModels.Map {
|
|||
lastDraw = drawSource;
|
||||
if (click == PrimaryInteractionStart.ControlClick) RectangleDrawMove(x, y);
|
||||
else if (interactionType == PrimaryInteractionType.Draw9Grid) Draw9Grid(x, y);
|
||||
else if (click == PrimaryInteractionStart.Click) DrawMove(x, y);
|
||||
else if (click == PrimaryInteractionStart.Click) didDraw = DrawMove(x, y);
|
||||
}
|
||||
return didDraw;
|
||||
}
|
||||
|
||||
private void DrawMove(double x, double y) {
|
||||
private bool DrawMove(double x, double y) {
|
||||
var didDraw = false;
|
||||
var map = MapUnderCursor(x, y);
|
||||
if (map != null) {
|
||||
using (map.DeferPropertyNotifications()) {
|
||||
if (drawMultipleTiles && tilesToDraw != null) {
|
||||
var tilePosition = ToBoundedMapTilePosition(map, x, y, tilesToDraw.GetLength(0), tilesToDraw.GetLength(1));
|
||||
map.DrawBlocks(history.CurrentChange, tilesToDraw, drawSource, tilePosition);
|
||||
didDraw = true;
|
||||
} else {
|
||||
var tilePosition = ToBoundedMapTilePosition(map, x, y, 1, 1);
|
||||
if (drawBlockIndex < 0 && collisionIndex < 0) {
|
||||
|
|
@ -843,16 +848,20 @@ namespace HavenSoft.HexManiac.Core.ViewModels.Map {
|
|||
lastDraw = tilePosition;
|
||||
FillBackup();
|
||||
SwapBlocks(lastDraw, drawSource);
|
||||
didDraw = true;
|
||||
}
|
||||
} else if (blockBag.Contains(drawBlockIndex)) {
|
||||
map.DrawBlock(history.CurrentChange, rnd.From(blockBag), collisionIndex, x, y);
|
||||
didDraw = true;
|
||||
} else {
|
||||
map.DrawBlock(history.CurrentChange, drawBlockIndex, collisionIndex, x, y);
|
||||
didDraw = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Hover(x, y);
|
||||
return didDraw;
|
||||
}
|
||||
|
||||
private void RectangleDrawMove(double x, double y) {
|
||||
|
|
@ -962,9 +971,12 @@ namespace HavenSoft.HexManiac.Core.ViewModels.Map {
|
|||
eventCreationType = EventCreationType.None;
|
||||
// user wants to do a wave function collapse at this position
|
||||
if (map != primaryMap) return;
|
||||
map.PaintWaveFunction(history.CurrentChange, x, y, RunWaveFunctionCollapseWithCollision);
|
||||
if (waveFunctionPrimary == null) CalculateWaveCollapseProbabilities();
|
||||
waveFunctionActive = new();
|
||||
map.PaintWaveFunction(history.CurrentChange, x, y, RunWaveFunctionCollapseWithCollision, waveFunctionActive.Token);
|
||||
}
|
||||
}
|
||||
private CancellationTokenSource? waveFunctionActive;
|
||||
|
||||
#region Rectangle-Drawing helper methods
|
||||
|
||||
|
|
@ -1971,7 +1983,6 @@ namespace HavenSoft.HexManiac.Core.ViewModels.Map {
|
|||
// if we still can't find a neighbor after that, leave it blank.
|
||||
|
||||
private IList<CollapseProbability> 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;
|
||||
|
|
@ -1994,11 +2005,6 @@ namespace HavenSoft.HexManiac.Core.ViewModels.Map {
|
|||
|
||||
if (probabilities.Count == 0) return null; // no known neighbors, no restrictions... yet
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
// combine the list of probabilities down to a single list by merging from all the neighbors.
|
||||
// the block is constrained in options based on _all_ its neighbors together.
|
||||
// if the neighbor can only be A/B based on the left and only B/C based on the top, it must be B.
|
||||
|
|
@ -2014,23 +2020,14 @@ namespace HavenSoft.HexManiac.Core.ViewModels.Map {
|
|||
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 {
|
||||
if (merged.Count != 0) {
|
||||
probabilities.Add(merged);
|
||||
}
|
||||
}
|
||||
|
||||
// possible failure state: no probabilities were found
|
||||
// pick block '1' which is most likely safe.
|
||||
if (probabilities.Count == 0 || probabilities[0].Count == 0) {
|
||||
// no restriction, pick any block
|
||||
//var (availableBlocks, _) = PrimaryMap.MapRepointer.EstimateBlockCount(layout.Element, true);
|
||||
//return availableBlocks.Range().Select(i => new CollapseProbability(i)).ToList();
|
||||
return new List<CollapseProbability> { new CollapseProbability(1) { Count = 1 } };
|
||||
}
|
||||
// possible failure state: no probabilities were found. We had probabilities before merging, but none survived.
|
||||
// we're returning 'null' if there are no restrictions. This is the opposite: the restrictions are so tight as to have no valid results.
|
||||
if (probabilities.Count == 0) return new List<CollapseProbability>();
|
||||
|
||||
// new version that returns the current probabilities, which need collapsing
|
||||
return probabilities[0];
|
||||
|
|
|
|||
|
|
@ -1714,7 +1714,16 @@
|
|||
</Popup>
|
||||
<local:AngleButton Direction="Right" Content="Edit Border Block" DataContext="{Binding PrimaryMap.BorderEditor}" Command="{res:MethodCommand ToggleBorderEditor}" Margin="0,0,5,0" x:Name="EditBorderButton" />
|
||||
<Grid Width="24" Height="24" Name="WaveFunction" Background="Transparent"
|
||||
MouseLeftButtonDown="EventTemplateDown" Margin="0,0,5,0" Tag="{x:Static map:EventCreationType.WaveFunction}" ToolTipService.InitialShowDelay="0" ToolTip="Drag to fill an identical-block region with randomized blocks">
|
||||
MouseLeftButtonDown="EventTemplateDown" Margin="0,0,5,0" Tag="{x:Static map:EventCreationType.WaveFunction}" ToolTipService.InitialShowDelay="0">
|
||||
<Grid.ToolTip>
|
||||
<TextBlock>
|
||||
Drop to auto-fill a region of identical blocks with random blocks.
|
||||
<LineBreak />
|
||||
Blocks will attempt to fill at a rate of 1000 per second, but will backtrack if it cannot find a valid fill.
|
||||
<LineBreak />
|
||||
Editing the map during auto-fill will cancel the auto-fill.
|
||||
</TextBlock>
|
||||
</Grid.ToolTip>
|
||||
<Rectangle Fill="{DynamicResource Primary}" Margin="2" />
|
||||
<Path Data="{res:Icon Dice}" Fill="{DynamicResource Secondary}" Stretch="Fill" />
|
||||
</Grid>
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user