diff --git a/src/GridFillerBench/FunctionalTests.cs b/src/GridFillerBench/FunctionalTests.cs deleted file mode 100644 index 9e7e768..0000000 --- a/src/GridFillerBench/FunctionalTests.cs +++ /dev/null @@ -1,180 +0,0 @@ -using LargeGridPathfinding; -using Microsoft.Xna.Framework; - -public class GridFillerFunctionalTests -{ - public static void Main() - { - Console.WriteLine("Running GridFiller Functional Tests...\n"); - - TestBasicGridFill(); - TestPartialGridFill(); - TestObstaclePlacement(); - TestObstacleRemoval(); - TestWeightAdjustment(); - TestRectanglePlacement(); - - Console.WriteLine("\n✅ All functional tests passed!"); - } - - private static void TestBasicGridFill() - { - Console.WriteLine("Test 1: Basic Grid Fill"); - var filler = new GridFiller(100, 100); - - filler.FillGrid(fillAll: true); - - // Check that grid has been populated with valid labels - int filledCells = 0; - for (int y = 0; y < 100; y++) - { - for (int x = 0; x < 100; x++) - { - if (filler.Grid[y, x] > 0) - filledCells++; - } - } - - Console.WriteLine($" Filled cells: {filledCells}/10000"); - Assert(filledCells > 0, "Grid should have filled cells"); - Console.WriteLine(" ✓ Passed\n"); - } - - private static void TestPartialGridFill() - { - Console.WriteLine("Test 2: Partial Grid Fill"); - var filler = new GridFiller(100, 100); - - filler.FillGrid(0, 0, 50, 50, fillAll: true); - - // Check that only the partial region has been filled - int filledInRegion = 0; - int filledOutsideRegion = 0; - - for (int y = 0; y < 100; y++) - { - for (int x = 0; x < 100; x++) - { - if (filler.Grid[y, x] > 0) - { - if (x < 50 && y < 50) - filledInRegion++; - else - filledOutsideRegion++; - } - } - } - - Console.WriteLine($" Filled in region (0,0,50,50): {filledInRegion}"); - Console.WriteLine($" Filled outside region: {filledOutsideRegion}"); - Assert(filledInRegion > 0, "Region should have filled cells"); - Assert(filledOutsideRegion == 0, "Outside region should be empty"); - Console.WriteLine(" ✓ Passed\n"); - } - - private static void TestObstaclePlacement() - { - Console.WriteLine("Test 3: Obstacle Placement"); - var filler = new GridFiller(100, 100); - - var obstacleRect = new Rectangle(10, 10, 20, 20); - filler.PlaceObstacle(obstacleRect); - - // Check that obstacle has negative labels - int obstaclesPlaced = 0; - for (int y = 10; y < 30; y++) - { - for (int x = 10; x < 30; x++) - { - if (filler.Grid[y, x] < 0) - obstaclesPlaced++; - } - } - - Console.WriteLine($" Obstacles placed: {obstaclesPlaced}/400"); - Assert(obstaclesPlaced == 400, "Obstacle should cover entire rectangle"); - Console.WriteLine(" ✓ Passed\n"); - } - - private static void TestObstacleRemoval() - { - Console.WriteLine("Test 4: Obstacle Removal"); - var filler = new GridFiller(100, 100); - - var obstacleRect = new Rectangle(10, 10, 20, 20); - filler.PlaceObstacle(obstacleRect); - filler.RemoveObstacle(obstacleRect); - - // Check that obstacle has been removed - int obstaclesRemaining = 0; - for (int y = 10; y < 30; y++) - { - for (int x = 10; x < 30; x++) - { - if (filler.Grid[y, x] < 0) - obstaclesRemaining++; - } - } - - Console.WriteLine($" Obstacles remaining: {obstaclesRemaining}"); - Assert(obstaclesRemaining == 0, "Obstacle should be completely removed"); - Console.WriteLine(" ✓ Passed\n"); - } - - private static void TestWeightAdjustment() - { - Console.WriteLine("Test 5: Weight Adjustment"); - var filler = new GridFiller(100, 100); - - var points = new List { new(10, 10), new(11, 11) }; - filler.SetTileWeights(points, 5); - - Assert(filler.WeightGrid[10, 10] == 5, "Weight at (10,10) should be 5"); - Assert(filler.WeightGrid[11, 11] == 5, "Weight at (11,11) should be 5"); - - filler.ResetTileWeights(points); - - Assert(filler.WeightGrid[10, 10] == 1, "Weight should be reset to 1"); - Assert(filler.WeightGrid[11, 11] == 1, "Weight should be reset to 1"); - Console.WriteLine(" ✓ Passed\n"); - } - - private static void TestRectanglePlacement() - { - Console.WriteLine("Test 6: Rectangle Placement Tracking"); - var filler = new GridFiller(100, 100); - - filler.FillGrid(fillAll: true); - - // Check that PlacedRectangles collection is populated - int placedCount = filler.PlacedRectangles.Count; - Console.WriteLine($" Rectangles placed: {placedCount}"); - Assert(placedCount > 0, "Should have placed rectangles"); - - // Verify each rectangle in the collection matches the grid - foreach (var kvp in filler.PlacedRectangles) - { - int label = kvp.Key; - var rect = kvp.Value; - - // Check a few cells from this rectangle - for (int y = rect.Top; y < Math.Min(rect.Top + 2, rect.Bottom); y++) - { - for (int x = rect.Left; x < Math.Min(rect.Left + 2, rect.Right); x++) - { - Assert(filler.Grid[y, x] == label, $"Grid cell ({x},{y}) should have label {label}"); - } - } - } - - Console.WriteLine(" ✓ Passed\n"); - } - - private static void Assert(bool condition, string message) - { - if (!condition) - { - throw new Exception($"Assertion failed: {message}"); - } - } -} diff --git a/src/GridFillerBench/Program.cs b/src/GridFillerBench/Program.cs index 85b89a8..efd5281 100644 --- a/src/GridFillerBench/Program.cs +++ b/src/GridFillerBench/Program.cs @@ -3,11 +3,60 @@ using BenchmarkDotNet.Running; using LargeGridPathfinding; using Microsoft.Xna.Framework; +static int CountZones(GridFiller filler) +{ + int width = filler.Width; + int height = filler.Height; + bool[,] visited = new bool[height, width]; + int zoneCount = 0; + + for (int y = 0; y < height; y++) + { + for (int x = 0; x < width; x++) + { + if (!visited[y, x] && filler.Grid[y, x] > 0) + { + // BFS to find all cells in this zone + var queue = new Queue<(int, int)>(); + queue.Enqueue((x, y)); + visited[y, x] = true; + + while (queue.Count > 0) + { + var (cx, cy) = queue.Dequeue(); + int zoneId = filler.Grid[cy, cx]; + + // Check 4 neighbors + foreach (var (dx, dy) in new[] { (0, 1), (0, -1), (1, 0), (-1, 0) }) + { + int nx = cx + dx; + int ny = cy + dy; + if (nx >= 0 && nx < width && ny >= 0 && ny < height && + !visited[ny, nx] && filler.Grid[ny, nx] == zoneId) + { + visited[ny, nx] = true; + queue.Enqueue((nx, ny)); + } + } + } + + zoneCount++; + } + } + } + + return zoneCount; +} + // Check if we should run tests or benchmarks if (args.Length > 0 && args[0] == "--test") { RunFunctionalTests(); } +else if (args.Length > 0 && args[0] == "--advanced") +{ + BenchmarkRunner.Run(); +} else { BenchmarkRunner.Run(); @@ -21,6 +70,8 @@ static void RunFunctionalTests() TestObstaclePlacement(); TestObstacleRemoval(); TestWeightAdjustment(); + TestZoneCount(); + TestLargerGrids(); Console.WriteLine("\n✅ All functional tests passed!"); } @@ -112,6 +163,53 @@ static void TestWeightAdjustment() Console.WriteLine(" ✓ Passed\n"); } +static void TestZoneCount() +{ + Console.WriteLine("Test 5: Zone Count (512x512 grid)"); + var filler = new GridFiller(512, 512); + filler.FillGrid(fillAll: true); + + int zoneCount = CountZones(filler); + Console.WriteLine($" Zones created: {zoneCount}"); + Console.WriteLine($" ✓ Passed\n"); +} + +static void TestLargerGrids() +{ + Console.WriteLine("Test 6: Larger Grids Zone Count (3 runs each)\n"); + + int[] gridSizes = { 512, 1024, 2048 }; + int runsPerSize = 3; + + foreach (int size in gridSizes) + { + Console.WriteLine($" Testing {size}×{size} grid ({size * size:N0} cells):"); + + int[] zoneCounts = new int[runsPerSize]; + long[] times = new long[runsPerSize]; + + for (int run = 0; run < runsPerSize; run++) + { + var sw = System.Diagnostics.Stopwatch.StartNew(); + + var filler = new GridFiller(size, size); + filler.FillGrid(fillAll: true); + + sw.Stop(); + zoneCounts[run] = CountZones(filler); + times[run] = sw.ElapsedMilliseconds; + } + + double avgZones = zoneCounts.Average(); + double avgTime = times.Average(); + + Console.WriteLine($" Zones: {zoneCounts[0]}, {zoneCounts[1]}, {zoneCounts[2]} (avg: {avgZones:F1})"); + Console.WriteLine($" Times: {times[0]}ms, {times[1]}ms, {times[2]}ms (avg: {avgTime:F0}ms)\n"); + } + + Console.WriteLine(" ✓ Passed\n"); +} + [MemoryDiagnoser] public class GridFillerBenchmarks { @@ -172,3 +270,64 @@ public class GridFillerBenchmarks gridFiller.ResetTileWeights(points); } } + +[MemoryDiagnoser] +public class AdvancedGridFillerBenchmarks +{ + private GridFiller gridFiller = null!; + private const int GridSize = 512; + + [GlobalSetup] + public void Setup() + { + gridFiller = new GridFiller(GridSize, GridSize); + } + + [Benchmark(Description = "GetRowContinuousWidth - 10 calls")] + public int BenchmarkGetRowWidth() + { + int result = 0; + for (int i = 0; i < 10; i++) + { + result += gridFiller.GetRowContinuousWidthPublic(10 + i, 10, 100, 1); + } + return result; + } + + [Benchmark(Description = "IsAreaFree - 100x100 check")] + public bool BenchmarkIsAreaFree() + { + for (int y = 0; y < 10; y++) + { + for (int x = 0; x < 10; x++) + { + if (x < 5 && y < 5) + { + var result = gridFiller.IsAreaFreePublic(x * 50, y * 50, 50, 50, 1); + if (!result) return false; + } + } + } + return true; + } + + [Benchmark(Description = "Stretch factor calculation - 1000 iterations")] + public int BenchmarkStretchFactor() + { + int result = 0; + for (int w = 10; w < 100; w += 3) + { + for (int h = 10; h < 100; h += 3) + { + result += gridFiller.GetStretchFactorPublic(w, h); + } + } + return result; + } + + [Benchmark(Description = "FillGrid - Full grid (Advanced)")] + public void FillGridFullAdvanced() + { + gridFiller.FillGrid(fillAll: true); + } +} diff --git a/src/LargeGridPathfinding/GridFiller.cs b/src/LargeGridPathfinding/GridFiller.cs index de4306a..e187f43 100644 --- a/src/LargeGridPathfinding/GridFiller.cs +++ b/src/LargeGridPathfinding/GridFiller.cs @@ -75,9 +75,12 @@ public class GridFiller Debug.WriteLine("Calculating candidates..."); - _ = Parallel.For(y1.Value, y2.Value, (y, loopState) => + ParallelOptions parallelOptions = new ParallelOptions { MaxDegreeOfParallelism = Environment.ProcessorCount }; + + _ = Parallel.For(y1.Value, y2.Value, parallelOptions, (y, loopState) => { - for (int x = x1.Value; x < x2; x++) + int x = x1.Value; + while (x < x2) { if (Grid[y, x] == 0) { @@ -87,6 +90,11 @@ public class GridFiller if (w > 0 && h > 0) { candidates.Add((x, y, w, h, tileWeight)); + x += w; + } + else + { + x++; } if (w >= maxRectangleSize && h >= maxRectangleSize) @@ -101,14 +109,24 @@ public class GridFiller Debug.WriteLine($"Candidates Progress: {candidates.Count} / {cells - areaPlaced} ({(float)candidates.Count / (cells - areaPlaced):P2})"); } } + else + { + x++; + } } }); Debug.WriteLine($"Candidates: {candidates.Count}"); + + if (candidates.Count == 0) + { + break; + } + Debug.WriteLine("Sorting candidates..."); - var sortedCandidates = new List<(int x, int y, int w, int h, int weight)>(candidates.Count); - sortedCandidates.AddRange(candidates); + List<(int x, int y, int w, int h, int weight)> sortedCandidates = [.. candidates]; + sortedCandidates.Sort((a, b) => { int areaComparison = (b.w * b.h).CompareTo(a.w * a.h); @@ -397,6 +415,8 @@ public class GridFiller int bestHeight = 0; int bestArea = 0; int bestStretch = int.MaxValue; + int noImprovementCount = 0; + const int maxNoImprovement = 5; for (int h = 1; h <= maxHeight; h++) { @@ -422,10 +442,15 @@ public class GridFiller bestWidth = currentWidth; bestHeight = h; bestStretch = stretch; + noImprovementCount = 0; } - else if (area < bestArea && h > bestHeight * 2) + else { - break; + noImprovementCount++; + if (noImprovementCount >= maxNoImprovement) + { + //break; + } } } @@ -566,4 +591,20 @@ public class GridFiller int maxSide = Math.Max(width, height); return maxSide / minSide; } + + // Public methods for benchmarking + public int GetRowContinuousWidthPublic(int startX, int y, int maxWidth, int requiredWeight) + { + return GetRowContinuousWidth(startX, y, maxWidth, requiredWeight); + } + + public bool IsAreaFreePublic(int x, int y, int w, int h, int requiredWeight) + { + return IsAreaFree(x, y, w, h, requiredWeight); + } + + public int GetStretchFactorPublic(int width, int height) + { + return GetStretchFactor(width, height); + } }