진행 시간 표시 추가 및 231~250 스테이지 체크포인트 반영
This commit is contained in:
@@ -1,989 +0,0 @@
|
|||||||
// Sokoban map generator (mask-based, reverse search from solved state)
|
|
||||||
// - Uses small hand-made masks (Microban/Novoban 스타일) to shape the outer walls.
|
|
||||||
// - Places goals/boxes in solved state, then pulls boxes away from goals (reverse of push) to guarantee solvability.
|
|
||||||
// - Run: `dotnet run > output.json` (defaults use band config). Optional: `dotnet run <seed> [startId] [endId]`.
|
|
||||||
// Legend: '#' wall, '.' floor, '0' void, 'G' goal, '$' box, '@' player.
|
|
||||||
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Diagnostics;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Text.Json;
|
|
||||||
|
|
||||||
internal static class Program
|
|
||||||
{
|
|
||||||
private static readonly LevelBandConfig[] LevelBands =
|
|
||||||
{
|
|
||||||
new LevelBandConfig
|
|
||||||
{
|
|
||||||
StartId = 3,
|
|
||||||
EndId = 20,
|
|
||||||
BoxCountLow = 1,
|
|
||||||
BoxCountHigh = 2,
|
|
||||||
MinAllowedPushes = 4,
|
|
||||||
MinAllowedTurns = 2,
|
|
||||||
ShapeMasks = MaskLibrary.Microban.Take(8).ToList()
|
|
||||||
},
|
|
||||||
new LevelBandConfig
|
|
||||||
{
|
|
||||||
StartId = 21,
|
|
||||||
EndId = 40,
|
|
||||||
BoxCountLow = 1,
|
|
||||||
BoxCountHigh = 2,
|
|
||||||
MinAllowedPushes = 7,
|
|
||||||
MinAllowedTurns = 3,
|
|
||||||
ShapeMasks = MaskLibrary.Microban.ToList()
|
|
||||||
},
|
|
||||||
new LevelBandConfig
|
|
||||||
{
|
|
||||||
StartId = 41,
|
|
||||||
EndId = 60,
|
|
||||||
BoxCountLow = 2,
|
|
||||||
BoxCountHigh = 3,
|
|
||||||
MinAllowedPushes = 9,
|
|
||||||
MinAllowedTurns = 4,
|
|
||||||
ShapeMasks = MaskLibrary.Microban.ToList()
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
private static readonly GenerationTuning Tuning = new GenerationTuning
|
|
||||||
{
|
|
||||||
MaxAttemptsPerLevel = 2000,
|
|
||||||
MaxMillisecondsPerLevel = 40_000,
|
|
||||||
PushLimitPadding = 2,
|
|
||||||
PushLimitScale = 0.35,
|
|
||||||
DynamicGrowthWindow = 12,
|
|
||||||
DynamicPushIncrement = 2,
|
|
||||||
ReverseSearchMaxDepth = 120,
|
|
||||||
ReverseSearchBreadth = 800,
|
|
||||||
ApplyMaskTransforms = true,
|
|
||||||
MaskWallJitter = 2
|
|
||||||
};
|
|
||||||
|
|
||||||
private const int DefaultSeed = 12345;
|
|
||||||
|
|
||||||
public static void Main(string[] args)
|
|
||||||
{
|
|
||||||
var seed = DefaultSeed;
|
|
||||||
if (args.Length > 0 && int.TryParse(args[0], out var parsedSeed))
|
|
||||||
{
|
|
||||||
seed = parsedSeed;
|
|
||||||
}
|
|
||||||
|
|
||||||
var rng = new Random(seed);
|
|
||||||
var generator = new LevelGenerator(rng, Tuning, LevelBands);
|
|
||||||
|
|
||||||
var startId = LevelBands.Min(b => b.StartId);
|
|
||||||
var endId = LevelBands.Max(b => b.EndId);
|
|
||||||
|
|
||||||
if (args.Length >= 2 && int.TryParse(args[1], out var requestedStart))
|
|
||||||
{
|
|
||||||
startId = requestedStart;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (args.Length >= 3 && int.TryParse(args[2], out var requestedEnd))
|
|
||||||
{
|
|
||||||
endId = requestedEnd;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (startId > endId)
|
|
||||||
{
|
|
||||||
(startId, endId) = (endId, startId);
|
|
||||||
}
|
|
||||||
|
|
||||||
var levels = generator.BuildRange(startId, endId);
|
|
||||||
var options = new JsonSerializerOptions
|
|
||||||
{
|
|
||||||
WriteIndented = true,
|
|
||||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
|
|
||||||
};
|
|
||||||
|
|
||||||
Console.WriteLine(JsonSerializer.Serialize(levels, options));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal sealed class LevelGenerator
|
|
||||||
{
|
|
||||||
private readonly Random _rng;
|
|
||||||
private readonly GenerationTuning _tuning;
|
|
||||||
private readonly IReadOnlyList<LevelBandConfig> _bands;
|
|
||||||
private readonly bool _trace;
|
|
||||||
private readonly HashSet<string> _seenLayouts = new();
|
|
||||||
|
|
||||||
public LevelGenerator(Random rng, GenerationTuning tuning, IReadOnlyList<LevelBandConfig> bands)
|
|
||||||
{
|
|
||||||
_rng = rng;
|
|
||||||
_tuning = tuning;
|
|
||||||
_bands = bands;
|
|
||||||
_trace = Environment.GetEnvironmentVariable("NEKOBAN_DEBUG") == "1";
|
|
||||||
}
|
|
||||||
|
|
||||||
public List<GeneratedLevel> BuildRange(int startId, int endId)
|
|
||||||
{
|
|
||||||
foreach (var band in _bands)
|
|
||||||
{
|
|
||||||
if (band.ShapeMasksExpanded == null || band.ShapeMasksExpanded.Count == 0)
|
|
||||||
{
|
|
||||||
var baseMasks = band.ShapeMasks.Count > 0 ? band.ShapeMasks : MaskLibrary.Microban;
|
|
||||||
band.ShapeMasksExpanded = MaskLibrary.ExpandWithTransforms(baseMasks);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var output = new List<GeneratedLevel>();
|
|
||||||
for (var id = startId; id <= endId; id++)
|
|
||||||
{
|
|
||||||
var band = ResolveBand(id);
|
|
||||||
var level = BuildSingle(id, band);
|
|
||||||
output.Add(level);
|
|
||||||
}
|
|
||||||
|
|
||||||
return output;
|
|
||||||
}
|
|
||||||
|
|
||||||
private GeneratedLevel BuildSingle(int id, LevelBandConfig band)
|
|
||||||
{
|
|
||||||
var stopwatch = Stopwatch.StartNew();
|
|
||||||
var attempts = 0;
|
|
||||||
var failReasons = _trace ? new Dictionary<string, int>() : null;
|
|
||||||
|
|
||||||
while (attempts < _tuning.MaxAttemptsPerLevel &&
|
|
||||||
stopwatch.ElapsedMilliseconds < _tuning.MaxMillisecondsPerLevel)
|
|
||||||
{
|
|
||||||
attempts++;
|
|
||||||
|
|
||||||
var baseMasks = band.ShapeMasks.Count > 0 ? band.ShapeMasks : MaskLibrary.Microban;
|
|
||||||
var prepped = band.ShapeMasksExpanded ?? MaskLibrary.ExpandWithTransforms(baseMasks);
|
|
||||||
band.ShapeMasksExpanded ??= prepped;
|
|
||||||
var mask = MaskLibrary.CreateVariant(
|
|
||||||
MaskLibrary.PickRandom(_rng, prepped),
|
|
||||||
_rng,
|
|
||||||
_tuning.ApplyMaskTransforms,
|
|
||||||
_tuning.MaskWallJitter);
|
|
||||||
|
|
||||||
var canvas = LayoutFactory.FromMask(mask, _rng, _tuning);
|
|
||||||
|
|
||||||
var boxCount = _rng.Next(band.BoxCountLow, band.BoxCountHigh + 1);
|
|
||||||
if (boxCount <= 0)
|
|
||||||
{
|
|
||||||
boxCount = 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!PiecePlacer.TryPlace(canvas, _rng, boxCount, out var goals, out var boxes, out var player))
|
|
||||||
{
|
|
||||||
NoteFail(failReasons, "place");
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
var board = Board.FromCanvas(canvas);
|
|
||||||
var reverse = new ReverseSearch(board, _tuning.ReverseSearchMaxDepth, _tuning.ReverseSearchBreadth);
|
|
||||||
var startState = reverse.FindStartState(goals, band.MinAllowedPushes, _rng);
|
|
||||||
if (startState == null)
|
|
||||||
{
|
|
||||||
NoteFail(failReasons, "reverse_not_found");
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
var solver = new SokobanSolver(board);
|
|
||||||
var solve = solver.SolveDetailed(startState.Player, startState.Boxes, goals);
|
|
||||||
if (solve.IsFail)
|
|
||||||
{
|
|
||||||
NoteFail(failReasons, "verify_unsolvable");
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (solve.Pushes < band.MinAllowedPushes || solve.Turns < band.MinAllowedTurns || solve.Branching < band.MinAllowedBranching)
|
|
||||||
{
|
|
||||||
NoteFail(failReasons, "too_easy");
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
var pushLimit = solve.Pushes + Math.Max(_tuning.PushLimitPadding,
|
|
||||||
(int)Math.Ceiling(solve.Pushes * _tuning.PushLimitScale));
|
|
||||||
|
|
||||||
canvas.ClearDynamic();
|
|
||||||
canvas.Overlay(goals, 'G');
|
|
||||||
canvas.Overlay(startState.Boxes, '$');
|
|
||||||
canvas.Set(startState.Player, '@');
|
|
||||||
|
|
||||||
// Final wall normalization; reject if tokens end up on the edge.
|
|
||||||
if (!LayoutFactory.NormalizeOuterWallsStrict(canvas, failOnTokens: true))
|
|
||||||
{
|
|
||||||
NoteFail(failReasons, "boundary");
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
var lines = canvas.ToLines();
|
|
||||||
var layoutKey = string.Join("|", lines);
|
|
||||||
if (_seenLayouts.Contains(layoutKey))
|
|
||||||
{
|
|
||||||
NoteFail(failReasons, "duplicate");
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
_seenLayouts.Add(layoutKey);
|
|
||||||
|
|
||||||
return new GeneratedLevel
|
|
||||||
{
|
|
||||||
Id = id,
|
|
||||||
Grid = lines,
|
|
||||||
LowestPush = solve.Pushes,
|
|
||||||
PushLimit = pushLimit
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_trace && failReasons != null)
|
|
||||||
{
|
|
||||||
Console.Error.WriteLine($"[trace] level {id} failed. Reasons:");
|
|
||||||
foreach (var kv in failReasons.OrderByDescending(kv => kv.Value))
|
|
||||||
{
|
|
||||||
Console.Error.WriteLine($" - {kv.Key}: {kv.Value}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new InvalidOperationException($"레벨 {id} 생성 실패 (attempts {attempts}).");
|
|
||||||
}
|
|
||||||
|
|
||||||
private LevelBandConfig ResolveBand(int id)
|
|
||||||
{
|
|
||||||
foreach (var band in _bands)
|
|
||||||
{
|
|
||||||
if (id >= band.StartId && id <= band.EndId)
|
|
||||||
{
|
|
||||||
return band;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var last = _bands.Last();
|
|
||||||
var delta = id - last.EndId;
|
|
||||||
var growthStep = Math.Max(1, delta / Math.Max(1, _tuning.DynamicGrowthWindow) + 1);
|
|
||||||
return new LevelBandConfig
|
|
||||||
{
|
|
||||||
StartId = id,
|
|
||||||
EndId = id,
|
|
||||||
BoxCountLow = last.BoxCountLow + growthStep / 2,
|
|
||||||
BoxCountHigh = last.BoxCountHigh + growthStep / 2,
|
|
||||||
MinAllowedPushes = last.MinAllowedPushes + growthStep * _tuning.DynamicPushIncrement,
|
|
||||||
ShapeMasks = last.ShapeMasks
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void NoteFail(IDictionary<string, int>? sink, string reason)
|
|
||||||
{
|
|
||||||
if (sink == null) return;
|
|
||||||
sink[reason] = sink.TryGetValue(reason, out var count) ? count + 1 : 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal sealed class GeneratedLevel
|
|
||||||
{
|
|
||||||
public int Id { get; init; }
|
|
||||||
public List<string> Grid { get; init; } = new();
|
|
||||||
public int LowestPush { get; init; }
|
|
||||||
public int PushLimit { get; init; }
|
|
||||||
}
|
|
||||||
|
|
||||||
internal sealed class LevelBandConfig
|
|
||||||
{
|
|
||||||
public int StartId { get; init; }
|
|
||||||
public int EndId { get; init; }
|
|
||||||
public int BoxCountLow { get; init; }
|
|
||||||
public int BoxCountHigh { get; init; }
|
|
||||||
public int MinAllowedPushes { get; init; }
|
|
||||||
public int MinAllowedTurns { get; init; } = 0;
|
|
||||||
public int MinAllowedBranching { get; init; } = 0;
|
|
||||||
public List<string[]> ShapeMasks { get; init; } = new();
|
|
||||||
public List<string[]>? ShapeMasksExpanded { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
internal sealed class GenerationTuning
|
|
||||||
{
|
|
||||||
public int MaxAttemptsPerLevel { get; init; }
|
|
||||||
public int MaxMillisecondsPerLevel { get; init; }
|
|
||||||
public int PushLimitPadding { get; init; }
|
|
||||||
public double PushLimitScale { get; init; }
|
|
||||||
public int DynamicGrowthWindow { get; init; }
|
|
||||||
public int DynamicPushIncrement { get; init; }
|
|
||||||
public int ReverseSearchMaxDepth { get; init; }
|
|
||||||
public int ReverseSearchBreadth { get; init; }
|
|
||||||
public bool ApplyMaskTransforms { get; init; }
|
|
||||||
public int MaskWallJitter { get; init; }
|
|
||||||
public int PocketCarveMin { get; init; } = 0;
|
|
||||||
public int PocketCarveMax { get; init; } = 0;
|
|
||||||
public int PocketMaxRadius { get; init; } = 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
internal sealed class GridCanvas
|
|
||||||
{
|
|
||||||
private readonly char[] _cells;
|
|
||||||
|
|
||||||
public GridCanvas(int width, int height)
|
|
||||||
{
|
|
||||||
Width = width;
|
|
||||||
Height = height;
|
|
||||||
_cells = Enumerable.Repeat('0', width * height).ToArray();
|
|
||||||
}
|
|
||||||
|
|
||||||
public int Width { get; }
|
|
||||||
public int Height { get; }
|
|
||||||
|
|
||||||
public void Fill(char c) => Array.Fill(_cells, c);
|
|
||||||
|
|
||||||
public void Set(int x, int y, char c)
|
|
||||||
{
|
|
||||||
if (!InBounds(x, y)) return;
|
|
||||||
_cells[y * Width + x] = c;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Set(int index, char c)
|
|
||||||
{
|
|
||||||
if (index < 0 || index >= _cells.Length) return;
|
|
||||||
_cells[index] = c;
|
|
||||||
}
|
|
||||||
|
|
||||||
public char Get(int x, int y) => _cells[y * Width + x];
|
|
||||||
public char Get(int index) => _cells[index];
|
|
||||||
|
|
||||||
public bool InBounds(int x, int y) => x >= 0 && y >= 0 && x < Width && y < Height;
|
|
||||||
|
|
||||||
public void Overlay(IEnumerable<int> indices, char c)
|
|
||||||
{
|
|
||||||
foreach (var idx in indices)
|
|
||||||
{
|
|
||||||
Set(idx, c);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void ClearDynamic()
|
|
||||||
{
|
|
||||||
for (var i = 0; i < _cells.Length; i++)
|
|
||||||
{
|
|
||||||
if (_cells[i] == '@' || _cells[i] == '$')
|
|
||||||
{
|
|
||||||
_cells[i] = '.';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public List<string> ToLines()
|
|
||||||
{
|
|
||||||
var lines = new List<string>(Height);
|
|
||||||
for (var y = 0; y < Height; y++)
|
|
||||||
{
|
|
||||||
lines.Add(new string(_cells, y * Width, Width));
|
|
||||||
}
|
|
||||||
return lines;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal static class LayoutFactory
|
|
||||||
{
|
|
||||||
public static GridCanvas FromMask(string[] mask, Random rng, GenerationTuning tuning)
|
|
||||||
{
|
|
||||||
var height = mask.Length;
|
|
||||||
var width = mask.Max(row => row.Length);
|
|
||||||
var canvas = new GridCanvas(width, height);
|
|
||||||
canvas.Fill('0');
|
|
||||||
|
|
||||||
for (var y = 0; y < height; y++)
|
|
||||||
{
|
|
||||||
var row = mask[y];
|
|
||||||
for (var x = 0; x < row.Length; x++)
|
|
||||||
{
|
|
||||||
canvas.Set(x, y, row[x]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
NormalizeOuterWalls(canvas);
|
|
||||||
|
|
||||||
if (tuning.PocketCarveMax > 0)
|
|
||||||
{
|
|
||||||
AddPockets(canvas, rng, tuning);
|
|
||||||
NormalizeOuterWalls(canvas);
|
|
||||||
}
|
|
||||||
|
|
||||||
return canvas;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Enforce: inside the bounding box of non-void tiles, edges are walls, interior voids become walls.
|
|
||||||
private static void NormalizeOuterWalls(GridCanvas canvas)
|
|
||||||
{
|
|
||||||
var w = canvas.Width;
|
|
||||||
var h = canvas.Height;
|
|
||||||
|
|
||||||
var minX = w;
|
|
||||||
var minY = h;
|
|
||||||
var maxX = -1;
|
|
||||||
var maxY = -1;
|
|
||||||
|
|
||||||
for (var y = 0; y < h; y++)
|
|
||||||
{
|
|
||||||
for (var x = 0; x < w; x++)
|
|
||||||
{
|
|
||||||
var c = canvas.Get(x, y);
|
|
||||||
if (c == '0') continue;
|
|
||||||
if (x < minX) minX = x;
|
|
||||||
if (y < minY) minY = y;
|
|
||||||
if (x > maxX) maxX = x;
|
|
||||||
if (y > maxY) maxY = y;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (maxX < minX || maxY < minY)
|
|
||||||
{
|
|
||||||
return; // nothing to normalize
|
|
||||||
}
|
|
||||||
|
|
||||||
for (var y = minY; y <= maxY; y++)
|
|
||||||
{
|
|
||||||
for (var x = minX; x <= maxX; x++)
|
|
||||||
{
|
|
||||||
var isEdge = x == minX || x == maxX || y == minY || y == maxY;
|
|
||||||
if (isEdge)
|
|
||||||
{
|
|
||||||
canvas.Set(x, y, '#');
|
|
||||||
}
|
|
||||||
else if (canvas.Get(x, y) == '0')
|
|
||||||
{
|
|
||||||
canvas.Set(x, y, '#');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Outside bounding box remains void.
|
|
||||||
}
|
|
||||||
|
|
||||||
public static bool NormalizeOuterWallsStrict(GridCanvas canvas, bool failOnTokens)
|
|
||||||
{
|
|
||||||
var w = canvas.Width;
|
|
||||||
var h = canvas.Height;
|
|
||||||
|
|
||||||
var minX = w;
|
|
||||||
var minY = h;
|
|
||||||
var maxX = -1;
|
|
||||||
var maxY = -1;
|
|
||||||
|
|
||||||
for (var y = 0; y < h; y++)
|
|
||||||
{
|
|
||||||
for (var x = 0; x < w; x++)
|
|
||||||
{
|
|
||||||
var c = canvas.Get(x, y);
|
|
||||||
if (c == '0') continue;
|
|
||||||
if (x < minX) minX = x;
|
|
||||||
if (y < minY) minY = y;
|
|
||||||
if (x > maxX) maxX = x;
|
|
||||||
if (y > maxY) maxY = y;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (maxX < minX || maxY < minY)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (var y = minY; y <= maxY; y++)
|
|
||||||
{
|
|
||||||
for (var x = minX; x <= maxX; x++)
|
|
||||||
{
|
|
||||||
var isEdge = x == minX || x == maxX || y == minY || y == maxY;
|
|
||||||
var c = canvas.Get(x, y);
|
|
||||||
if (isEdge)
|
|
||||||
{
|
|
||||||
if (failOnTokens && (c == 'G' || c == '$' || c == '@'))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
canvas.Set(x, y, '#');
|
|
||||||
}
|
|
||||||
else if (c == '0')
|
|
||||||
{
|
|
||||||
canvas.Set(x, y, '#');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void AddPockets(GridCanvas canvas, Random rng, GenerationTuning tuning)
|
|
||||||
{
|
|
||||||
var pockets = rng.Next(tuning.PocketCarveMin, tuning.PocketCarveMax + 1);
|
|
||||||
for (var i = 0; i < pockets; i++)
|
|
||||||
{
|
|
||||||
var radius = rng.Next(1, tuning.PocketMaxRadius + 1);
|
|
||||||
var x = rng.Next(1, canvas.Width - 1);
|
|
||||||
var y = rng.Next(1, canvas.Height - 1);
|
|
||||||
for (var yy = y - radius; yy <= y + radius; yy++)
|
|
||||||
{
|
|
||||||
for (var xx = x - radius; xx <= x + radius; xx++)
|
|
||||||
{
|
|
||||||
if (!canvas.InBounds(xx, yy)) continue;
|
|
||||||
if (xx == 0 || yy == 0 || xx == canvas.Width - 1 || yy == canvas.Height - 1) continue;
|
|
||||||
var dx = xx - x;
|
|
||||||
var dy = yy - y;
|
|
||||||
if (dx * dx + dy * dy <= radius * radius)
|
|
||||||
{
|
|
||||||
if (canvas.Get(xx, yy) == '#')
|
|
||||||
{
|
|
||||||
canvas.Set(xx, yy, '.');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal static class PiecePlacer
|
|
||||||
{
|
|
||||||
public static bool TryPlace(GridCanvas canvas, Random rng, int boxCount, out HashSet<int> goals, out int[] boxes, out int player)
|
|
||||||
{
|
|
||||||
goals = new HashSet<int>();
|
|
||||||
boxes = Array.Empty<int>();
|
|
||||||
player = -1;
|
|
||||||
|
|
||||||
var candidates = Walkable(canvas).ToList();
|
|
||||||
var minRequired = boxCount + 2;
|
|
||||||
if (candidates.Count < minRequired)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
Shuffle(candidates, rng);
|
|
||||||
|
|
||||||
var softGoals = new List<int>();
|
|
||||||
foreach (var idx in candidates)
|
|
||||||
{
|
|
||||||
if (IsDeadCorner(canvas, idx))
|
|
||||||
{
|
|
||||||
softGoals.Add(idx);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
goals.Add(idx);
|
|
||||||
if (goals.Count == boxCount) break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (goals.Count != boxCount)
|
|
||||||
{
|
|
||||||
foreach (var idx in softGoals)
|
|
||||||
{
|
|
||||||
goals.Add(idx);
|
|
||||||
if (goals.Count == boxCount) break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (goals.Count != boxCount) return false;
|
|
||||||
|
|
||||||
var goalSet = new HashSet<int>(goals);
|
|
||||||
var nonGoals = candidates.Where(i => !goalSet.Contains(i)).ToList();
|
|
||||||
Shuffle(nonGoals, rng);
|
|
||||||
|
|
||||||
boxes = goals.ToArray();
|
|
||||||
Array.Sort(boxes);
|
|
||||||
|
|
||||||
if (nonGoals.Count == 0) return false;
|
|
||||||
|
|
||||||
var reachable = FloodFrom(goalSet.First(), canvas, Array.Empty<int>());
|
|
||||||
if (goalSet.Any(g => !reachable.Contains(g))) return false;
|
|
||||||
|
|
||||||
var playerOptions = nonGoals.Where(reachable.Contains).ToList();
|
|
||||||
if (playerOptions.Count == 0) return false;
|
|
||||||
|
|
||||||
player = playerOptions[rng.Next(playerOptions.Count)];
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static HashSet<int> FloodFrom(int start, GridCanvas canvas, int[] boxes)
|
|
||||||
{
|
|
||||||
var visited = new HashSet<int>();
|
|
||||||
if (IsSolid(canvas.Get(start)) || Array.BinarySearch(boxes, start) >= 0) return visited;
|
|
||||||
|
|
||||||
var queue = new Queue<int>();
|
|
||||||
visited.Add(start);
|
|
||||||
queue.Enqueue(start);
|
|
||||||
|
|
||||||
while (queue.Count > 0)
|
|
||||||
{
|
|
||||||
var current = queue.Dequeue();
|
|
||||||
foreach (var next in NeighborIndices(current, canvas.Width, canvas.Height))
|
|
||||||
{
|
|
||||||
if (next < 0 || next >= canvas.Width * canvas.Height) continue;
|
|
||||||
if (visited.Contains(next)) continue;
|
|
||||||
|
|
||||||
var tile = canvas.Get(next);
|
|
||||||
if (IsSolid(tile) || Array.BinarySearch(boxes, next) >= 0) continue;
|
|
||||||
|
|
||||||
visited.Add(next);
|
|
||||||
queue.Enqueue(next);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return visited;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static IEnumerable<int> Walkable(GridCanvas canvas)
|
|
||||||
{
|
|
||||||
for (var i = 0; i < canvas.Width * canvas.Height; i++)
|
|
||||||
{
|
|
||||||
var tile = canvas.Get(i);
|
|
||||||
if (!IsSolid(tile))
|
|
||||||
{
|
|
||||||
yield return i;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static IEnumerable<int> NeighborIndices(int index, int width, int height)
|
|
||||||
{
|
|
||||||
var x = index % width;
|
|
||||||
var y = index / width;
|
|
||||||
|
|
||||||
if (y > 0) yield return index - width;
|
|
||||||
if (y < height - 1) yield return index + width;
|
|
||||||
if (x > 0) yield return index - 1;
|
|
||||||
if (x < width - 1) yield return index + 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static bool IsDeadCorner(GridCanvas canvas, int idx)
|
|
||||||
{
|
|
||||||
var w = canvas.Width;
|
|
||||||
var h = canvas.Height;
|
|
||||||
var x = idx % w;
|
|
||||||
var y = idx / w;
|
|
||||||
|
|
||||||
var solidNorth = y == 0 || IsSolid(canvas.Get(x, y - 1));
|
|
||||||
var solidSouth = y == h - 1 || IsSolid(canvas.Get(x, y + 1));
|
|
||||||
var solidWest = x == 0 || IsSolid(canvas.Get(x - 1, y));
|
|
||||||
var solidEast = x == w - 1 || IsSolid(canvas.Get(x + 1, y));
|
|
||||||
|
|
||||||
var verticalBlocked = (solidNorth && solidSouth);
|
|
||||||
var horizontalBlocked = (solidWest && solidEast);
|
|
||||||
if (verticalBlocked || horizontalBlocked) return true;
|
|
||||||
|
|
||||||
var cornerNW = solidNorth && solidWest;
|
|
||||||
var cornerNE = solidNorth && solidEast;
|
|
||||||
var cornerSW = solidSouth && solidWest;
|
|
||||||
var cornerSE = solidSouth && solidEast;
|
|
||||||
return cornerNW || cornerNE || cornerSW || cornerSE;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static bool IsSolid(char tile) => tile == '#' || tile == '0';
|
|
||||||
|
|
||||||
private static void Shuffle<T>(IList<T> list, Random rng)
|
|
||||||
{
|
|
||||||
for (var i = list.Count - 1; i > 0; i--)
|
|
||||||
{
|
|
||||||
var j = rng.Next(i + 1);
|
|
||||||
(list[i], list[j]) = (list[j], list[i]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal sealed class Board
|
|
||||||
{
|
|
||||||
private readonly bool[] _walls;
|
|
||||||
|
|
||||||
private Board(int width, int height, bool[] walls)
|
|
||||||
{
|
|
||||||
Width = width;
|
|
||||||
Height = height;
|
|
||||||
_walls = walls;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int Width { get; }
|
|
||||||
public int Height { get; }
|
|
||||||
public int Size => Width * Height;
|
|
||||||
|
|
||||||
public static Board FromCanvas(GridCanvas canvas)
|
|
||||||
{
|
|
||||||
var walls = new bool[canvas.Width * canvas.Height];
|
|
||||||
for (var i = 0; i < walls.Length; i++)
|
|
||||||
{
|
|
||||||
var tile = canvas.Get(i);
|
|
||||||
walls[i] = tile == '#' || tile == '0';
|
|
||||||
}
|
|
||||||
|
|
||||||
return new Board(canvas.Width, canvas.Height, walls);
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool IsSolid(int index) => _walls[index];
|
|
||||||
}
|
|
||||||
|
|
||||||
internal sealed class ReverseSearch
|
|
||||||
{
|
|
||||||
private readonly Board _board;
|
|
||||||
private readonly int _maxDepth;
|
|
||||||
private readonly int _breadth;
|
|
||||||
private readonly int[] _dirs;
|
|
||||||
|
|
||||||
public ReverseSearch(Board board, int maxDepth, int breadth)
|
|
||||||
{
|
|
||||||
_board = board;
|
|
||||||
_maxDepth = maxDepth;
|
|
||||||
_breadth = breadth;
|
|
||||||
_dirs = new[] { -board.Width, board.Width, -1, 1 };
|
|
||||||
}
|
|
||||||
|
|
||||||
public ScrambledState? FindStartState(HashSet<int> goals, int minPush, Random rng)
|
|
||||||
{
|
|
||||||
var boxes = goals.ToArray();
|
|
||||||
Array.Sort(boxes);
|
|
||||||
|
|
||||||
var playerStart = FirstFloorNotGoal(goals);
|
|
||||||
if (playerStart < 0) return null;
|
|
||||||
|
|
||||||
var start = new SolverState(playerStart, boxes);
|
|
||||||
var queue = new Queue<(SolverState State, int Pushes)>();
|
|
||||||
var visited = new HashSet<SolverState>(new StateComparer()) { start };
|
|
||||||
queue.Enqueue((start, 0));
|
|
||||||
|
|
||||||
ScrambledState? best = null;
|
|
||||||
var bestPush = -1;
|
|
||||||
|
|
||||||
while (queue.Count > 0 && visited.Count < _breadth)
|
|
||||||
{
|
|
||||||
var (state, pushes) = queue.Dequeue();
|
|
||||||
if (pushes >= minPush && pushes > bestPush)
|
|
||||||
{
|
|
||||||
bestPush = pushes;
|
|
||||||
best = new ScrambledState(state.Player, state.Boxes);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (pushes >= _maxDepth) continue;
|
|
||||||
|
|
||||||
foreach (var next in Expand(state))
|
|
||||||
{
|
|
||||||
var nextPushes = pushes + (next.Item2 ? 1 : 0);
|
|
||||||
if (nextPushes > _maxDepth) continue;
|
|
||||||
|
|
||||||
var nextState = next.Item1;
|
|
||||||
if (visited.Add(nextState))
|
|
||||||
{
|
|
||||||
queue.Enqueue((nextState, nextPushes));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return best;
|
|
||||||
}
|
|
||||||
|
|
||||||
private int FirstFloorNotGoal(HashSet<int> goals)
|
|
||||||
{
|
|
||||||
for (var i = 0; i < _board.Size; i++)
|
|
||||||
{
|
|
||||||
if (_board.IsSolid(i)) continue;
|
|
||||||
if (goals.Contains(i)) continue;
|
|
||||||
return i;
|
|
||||||
}
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Returns (state, isPull) where isPull indicates a box move (counts as push)
|
|
||||||
private IEnumerable<(SolverState, bool)> Expand(SolverState state)
|
|
||||||
{
|
|
||||||
foreach (var dir in _dirs)
|
|
||||||
{
|
|
||||||
var step = state.Player + dir;
|
|
||||||
if (!IndexInBounds(step) || _board.IsSolid(step) || Array.BinarySearch(state.Boxes, step) >= 0)
|
|
||||||
{
|
|
||||||
// pull candidate?
|
|
||||||
var boxPos = state.Player + dir;
|
|
||||||
var behind = state.Player - dir;
|
|
||||||
if (!IndexInBounds(boxPos) || !IndexInBounds(behind)) continue;
|
|
||||||
var boxIndex = Array.BinarySearch(state.Boxes, boxPos);
|
|
||||||
if (boxIndex < 0) continue;
|
|
||||||
if (_board.IsSolid(behind) || Array.BinarySearch(state.Boxes, behind) >= 0) continue;
|
|
||||||
|
|
||||||
var nextBoxes = state.Boxes.ToArray();
|
|
||||||
nextBoxes[boxIndex] = state.Player;
|
|
||||||
Array.Sort(nextBoxes);
|
|
||||||
yield return (new SolverState(behind, nextBoxes), true);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// player walk
|
|
||||||
yield return (new SolverState(step, state.Boxes), false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool IndexInBounds(int idx) => idx >= 0 && idx < _board.Size;
|
|
||||||
}
|
|
||||||
|
|
||||||
internal sealed class SokobanSolver
|
|
||||||
{
|
|
||||||
private readonly Board _board;
|
|
||||||
private readonly StateComparer _comparer = new();
|
|
||||||
private readonly int[] _dirs;
|
|
||||||
|
|
||||||
public SokobanSolver(Board board)
|
|
||||||
{
|
|
||||||
_board = board;
|
|
||||||
_dirs = new[] { -board.Width, board.Width, -1, 1 };
|
|
||||||
}
|
|
||||||
|
|
||||||
public SolveResult SolveDetailed(int player, int[] boxes, HashSet<int> goals)
|
|
||||||
{
|
|
||||||
Array.Sort(boxes);
|
|
||||||
var start = new SolverState(player, boxes);
|
|
||||||
var visited = new HashSet<SolverState>(_comparer) { start };
|
|
||||||
var parents = new Dictionary<SolverState, (SolverState Parent, int Dir)>(_comparer);
|
|
||||||
var queue = new Queue<SolverState>();
|
|
||||||
queue.Enqueue(start);
|
|
||||||
|
|
||||||
while (queue.Count > 0)
|
|
||||||
{
|
|
||||||
var state = queue.Dequeue();
|
|
||||||
if (IsSolved(state.Boxes, goals))
|
|
||||||
{
|
|
||||||
return BuildResult(state, parents);
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var (next, dir) in Expand(state))
|
|
||||||
{
|
|
||||||
if (visited.Add(next))
|
|
||||||
{
|
|
||||||
parents[next] = (state, dir);
|
|
||||||
queue.Enqueue(next);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return SolveResult.Fail;
|
|
||||||
}
|
|
||||||
|
|
||||||
private IEnumerable<(SolverState State, int Dir)> Expand(SolverState state)
|
|
||||||
{
|
|
||||||
var reachable = ReachableWithoutPushing(state.Player, state.Boxes);
|
|
||||||
foreach (var pos in reachable)
|
|
||||||
{
|
|
||||||
foreach (var dir in _dirs)
|
|
||||||
{
|
|
||||||
var boxPos = pos + dir;
|
|
||||||
var landing = boxPos + dir;
|
|
||||||
if (!IndexInBounds(boxPos) || !IndexInBounds(landing)) continue;
|
|
||||||
|
|
||||||
var boxIndex = Array.BinarySearch(state.Boxes, boxPos);
|
|
||||||
if (boxIndex < 0) continue;
|
|
||||||
if (_board.IsSolid(landing) || Array.BinarySearch(state.Boxes, landing) >= 0) continue;
|
|
||||||
|
|
||||||
var nextBoxes = state.Boxes.ToArray();
|
|
||||||
nextBoxes[boxIndex] = landing;
|
|
||||||
Array.Sort(nextBoxes);
|
|
||||||
yield return (new SolverState(boxPos, nextBoxes), dir);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private SolveResult BuildResult(SolverState solved, Dictionary<SolverState, (SolverState Parent, int Dir)> parents)
|
|
||||||
{
|
|
||||||
var pushes = 0;
|
|
||||||
var turns = 0;
|
|
||||||
var branching = 0;
|
|
||||||
|
|
||||||
var path = new List<(SolverState State, int Dir)>();
|
|
||||||
var current = solved;
|
|
||||||
while (parents.TryGetValue(current, out var entry))
|
|
||||||
{
|
|
||||||
path.Add((current, entry.Dir));
|
|
||||||
current = entry.Parent;
|
|
||||||
}
|
|
||||||
path.Reverse();
|
|
||||||
|
|
||||||
var prevDir = 0;
|
|
||||||
foreach (var (_, dir) in path)
|
|
||||||
{
|
|
||||||
pushes++;
|
|
||||||
if (prevDir != 0 && dir != prevDir) turns++;
|
|
||||||
prevDir = dir;
|
|
||||||
}
|
|
||||||
|
|
||||||
// branching: count states along path that had more than one push option
|
|
||||||
current = solved;
|
|
||||||
while (parents.TryGetValue(current, out var entry2))
|
|
||||||
{
|
|
||||||
var pushOptions = Expand(entry2.Parent).Count();
|
|
||||||
if (pushOptions > 1) branching++;
|
|
||||||
current = entry2.Parent;
|
|
||||||
}
|
|
||||||
|
|
||||||
return new SolveResult(pushes, turns, branching);
|
|
||||||
}
|
|
||||||
|
|
||||||
private List<int> ReachableWithoutPushing(int start, int[] boxes)
|
|
||||||
{
|
|
||||||
var visited = new bool[_board.Size];
|
|
||||||
var queue = new Queue<int>();
|
|
||||||
var output = new List<int>();
|
|
||||||
|
|
||||||
if (_board.IsSolid(start) || Array.BinarySearch(boxes, start) >= 0) return output;
|
|
||||||
|
|
||||||
visited[start] = true;
|
|
||||||
queue.Enqueue(start);
|
|
||||||
output.Add(start);
|
|
||||||
|
|
||||||
while (queue.Count > 0)
|
|
||||||
{
|
|
||||||
var current = queue.Dequeue();
|
|
||||||
foreach (var dir in _dirs)
|
|
||||||
{
|
|
||||||
var next = current + dir;
|
|
||||||
if (!IndexInBounds(next)) continue;
|
|
||||||
if (visited[next]) continue;
|
|
||||||
if (_board.IsSolid(next) || Array.BinarySearch(boxes, next) >= 0) continue;
|
|
||||||
visited[next] = true;
|
|
||||||
queue.Enqueue(next);
|
|
||||||
output.Add(next);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return output;
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool IsSolved(int[] boxes, HashSet<int> goals)
|
|
||||||
{
|
|
||||||
foreach (var b in boxes)
|
|
||||||
{
|
|
||||||
if (!goals.Contains(b)) return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool IndexInBounds(int idx) => idx >= 0 && idx < _board.Size;
|
|
||||||
}
|
|
||||||
|
|
||||||
internal readonly record struct SolverState(int Player, int[] Boxes);
|
|
||||||
|
|
||||||
internal readonly record struct SolveResult(int Pushes, int Turns, int Branching)
|
|
||||||
{
|
|
||||||
public static SolveResult Fail => new(-1, -1, -1);
|
|
||||||
public bool IsFail => Pushes < 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
internal sealed class StateComparer : IEqualityComparer<SolverState>
|
|
||||||
{
|
|
||||||
public bool Equals(SolverState x, SolverState y)
|
|
||||||
{
|
|
||||||
if (x.Player != y.Player) return false;
|
|
||||||
var a = x.Boxes;
|
|
||||||
var b = y.Boxes;
|
|
||||||
if (a.Length != b.Length) return false;
|
|
||||||
for (var i = 0; i < a.Length; i++)
|
|
||||||
{
|
|
||||||
if (a[i] != b[i]) return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int GetHashCode(SolverState obj)
|
|
||||||
{
|
|
||||||
var hash = obj.Player * 397 ^ obj.Boxes.Length;
|
|
||||||
unchecked
|
|
||||||
{
|
|
||||||
for (var i = 0; i < obj.Boxes.Length; i++)
|
|
||||||
{
|
|
||||||
hash = hash * 31 + obj.Boxes[i];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return hash;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal sealed record ScrambledState(int Player, int[] Boxes);
|
|
||||||
57
Program.cs
57
Program.cs
@@ -161,8 +161,9 @@ internal static class Program
|
|||||||
|
|
||||||
var rng = new Random(seed);
|
var rng = new Random(seed);
|
||||||
var levelBands = LoadLevelBands();
|
var levelBands = LoadLevelBands();
|
||||||
|
var totalWatch = Stopwatch.StartNew();
|
||||||
var status = new StatusReporter();
|
var status = new StatusReporter();
|
||||||
var generator = new LevelGenerator(rng, Tuning, levelBands, status);
|
var generator = new LevelGenerator(rng, Tuning, levelBands, status, totalWatch);
|
||||||
|
|
||||||
var startId = levelBands.Min(b => b.StartId);
|
var startId = levelBands.Min(b => b.StartId);
|
||||||
var endId = levelBands.Max(b => b.EndId);
|
var endId = levelBands.Max(b => b.EndId);
|
||||||
@@ -208,7 +209,6 @@ internal static class Program
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var totalWatch = Stopwatch.StartNew();
|
|
||||||
var levels = generator.BuildRange(startId, endId, SaveCheckpoint);
|
var levels = generator.BuildRange(startId, endId, SaveCheckpoint);
|
||||||
totalWatch.Stop();
|
totalWatch.Stop();
|
||||||
|
|
||||||
@@ -458,13 +458,15 @@ internal sealed class LevelGenerator
|
|||||||
private readonly HashSet<string> _seenLayouts = new();
|
private readonly HashSet<string> _seenLayouts = new();
|
||||||
private readonly HashSet<string> _seenPatterns = new();
|
private readonly HashSet<string> _seenPatterns = new();
|
||||||
private readonly StatusReporter _status;
|
private readonly StatusReporter _status;
|
||||||
|
private readonly Stopwatch _totalWatch;
|
||||||
|
|
||||||
public LevelGenerator(Random rng, GenerationTuning tuning, IReadOnlyList<LevelBandConfig> bands, StatusReporter status)
|
public LevelGenerator(Random rng, GenerationTuning tuning, IReadOnlyList<LevelBandConfig> bands, StatusReporter status, Stopwatch totalWatch)
|
||||||
{
|
{
|
||||||
_rng = rng;
|
_rng = rng;
|
||||||
_tuning = tuning;
|
_tuning = tuning;
|
||||||
_bands = bands;
|
_bands = bands;
|
||||||
_status = status;
|
_status = status;
|
||||||
|
_totalWatch = totalWatch;
|
||||||
_trace = Environment.GetEnvironmentVariable("NEKOBAN_DEBUG") == "1";
|
_trace = Environment.GetEnvironmentVariable("NEKOBAN_DEBUG") == "1";
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -483,7 +485,8 @@ internal sealed class LevelGenerator
|
|||||||
for (var id = startId; id <= endId; id++)
|
for (var id = startId; id <= endId; id++)
|
||||||
{
|
{
|
||||||
var band = ResolveBand(id);
|
var band = ResolveBand(id);
|
||||||
var level = BuildSingle(id, band);
|
var levelStopwatch = Stopwatch.StartNew();
|
||||||
|
var level = BuildSingle(id, band, levelStopwatch);
|
||||||
level = TrimLevel(level);
|
level = TrimLevel(level);
|
||||||
output.Add(level);
|
output.Add(level);
|
||||||
onCheckpoint?.Invoke(output, id);
|
onCheckpoint?.Invoke(output, id);
|
||||||
@@ -502,7 +505,7 @@ internal sealed class LevelGenerator
|
|||||||
return new PocketSettings(min, max, radius);
|
return new PocketSettings(min, max, radius);
|
||||||
}
|
}
|
||||||
|
|
||||||
private GeneratedLevel BuildSingle(int id, LevelBandConfig band)
|
private GeneratedLevel BuildSingle(int id, LevelBandConfig band, Stopwatch levelStopwatch)
|
||||||
{
|
{
|
||||||
// 완화 단계를 순차적으로 적용한다. (총 5단계 + 비상 1단계)
|
// 완화 단계를 순차적으로 적용한다. (총 5단계 + 비상 1단계)
|
||||||
var stages = new List<RelaxStage>
|
var stages = new List<RelaxStage>
|
||||||
@@ -546,26 +549,7 @@ internal sealed class LevelGenerator
|
|||||||
c.MinBoxDistance = Math.Max(1, c.MinBoxDistance - 1);
|
c.MinBoxDistance = Math.Max(1, c.MinBoxDistance - 1);
|
||||||
c.MinWallDistance = Math.Max(0, c.MinWallDistance - 1);
|
c.MinWallDistance = Math.Max(0, c.MinWallDistance - 1);
|
||||||
return c;
|
return c;
|
||||||
}),
|
})
|
||||||
new RelaxStage("완화6-최종", bandConfig =>
|
|
||||||
{
|
|
||||||
var c = CloneBand(bandConfig);
|
|
||||||
c.BoxCountLow = 1;
|
|
||||||
c.BoxCountHigh = Math.Max(1, c.BoxCountHigh - 2);
|
|
||||||
c.MinAllowedPushes = 1;
|
|
||||||
c.MinAllowedTurns = 0;
|
|
||||||
c.MinAllowedBranching = 0;
|
|
||||||
c.MinGoalDistance = Math.Max(1, c.MinGoalDistance - 2);
|
|
||||||
c.MinBoxDistance = Math.Max(1, c.MinBoxDistance - 2);
|
|
||||||
c.MinWallDistance = 0;
|
|
||||||
c.ReverseDepthScale = Math.Max(1.0, c.ReverseDepthScale * 2.8);
|
|
||||||
c.ReverseBreadthScale = Math.Max(1.0, c.ReverseBreadthScale * 2.8);
|
|
||||||
c.MaskPadMin = -2;
|
|
||||||
c.MaskPadMax = -2;
|
|
||||||
c.ShapeMasks = MaskLibrary.Medium.Concat(MaskLibrary.Microban).ToList();
|
|
||||||
c.ShapeMasksExpanded = null;
|
|
||||||
return c;
|
|
||||||
}, OverrideRelaxSteps: 6)
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// 밴드 강등 시도: 현재 밴드 -> 직전 10레벨 밴드 -> 직전 20레벨 밴드
|
// 밴드 강등 시도: 현재 밴드 -> 직전 10레벨 밴드 -> 직전 20레벨 밴드
|
||||||
@@ -584,10 +568,10 @@ internal sealed class LevelGenerator
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 시드 변조 재시도 + 완화 단계 루프
|
// 시드 변조 재시도 + 완화 단계 루프
|
||||||
const int seedRetries = 5;
|
var retry = 0;
|
||||||
foreach (var bandCandidate in bandCandidates)
|
while (true)
|
||||||
{
|
{
|
||||||
for (var retry = 0; retry < seedRetries; retry++)
|
foreach (var bandCandidate in bandCandidates)
|
||||||
{
|
{
|
||||||
var jitterSeed = _rng.Next() ^ (id * 7919) ^ (retry * 104729);
|
var jitterSeed = _rng.Next() ^ (id * 7919) ^ (retry * 104729);
|
||||||
var localRng = new Random(jitterSeed);
|
var localRng = new Random(jitterSeed);
|
||||||
@@ -596,18 +580,17 @@ internal sealed class LevelGenerator
|
|||||||
{
|
{
|
||||||
var bandForStage = stage.Adjust(bandCandidate);
|
var bandForStage = stage.Adjust(bandCandidate);
|
||||||
var relaxOverride = stage.OverrideRelaxSteps;
|
var relaxOverride = stage.OverrideRelaxSteps;
|
||||||
if (TryBuildStage(id, bandForStage, stage.Label, localRng, relaxOverride, out var level))
|
if (TryBuildStage(id, bandForStage, stage.Label, localRng, relaxOverride, levelStopwatch, out var level))
|
||||||
{
|
{
|
||||||
return level;
|
return level;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
retry++;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new InvalidOperationException($"레벨 {id} 생성 실패 (모든 완화 단계 시도됨)");
|
private bool TryBuildStage(int id, LevelBandConfig band, string stageLabel, Random rng, int? overrideRelaxSteps, Stopwatch levelStopwatch, out GeneratedLevel level)
|
||||||
}
|
|
||||||
|
|
||||||
private bool TryBuildStage(int id, LevelBandConfig band, string stageLabel, Random rng, int? overrideRelaxSteps, out GeneratedLevel level)
|
|
||||||
{
|
{
|
||||||
var failReasons = _trace ? new Dictionary<string, int>() : null;
|
var failReasons = _trace ? new Dictionary<string, int>() : null;
|
||||||
var baseMasks = band.ShapeMasks.Count > 0 ? band.ShapeMasks : MaskLibrary.Microban;
|
var baseMasks = band.ShapeMasks.Count > 0 ? band.ShapeMasks : MaskLibrary.Microban;
|
||||||
@@ -631,16 +614,16 @@ internal sealed class LevelGenerator
|
|||||||
var reverseDepth = Math.Max(16, (int)Math.Round(_tuning.ReverseSearchMaxDepth * depthScale));
|
var reverseDepth = Math.Max(16, (int)Math.Round(_tuning.ReverseSearchMaxDepth * depthScale));
|
||||||
var reverseBreadth = Math.Max(200, (int)Math.Round(_tuning.ReverseSearchBreadth * breadthScale));
|
var reverseBreadth = Math.Max(200, (int)Math.Round(_tuning.ReverseSearchBreadth * breadthScale));
|
||||||
|
|
||||||
var stopwatch = Stopwatch.StartNew();
|
var stageStopwatch = Stopwatch.StartNew();
|
||||||
var attempts = 0;
|
var attempts = 0;
|
||||||
|
|
||||||
while (attempts < attemptsLimit &&
|
while (attempts < attemptsLimit &&
|
||||||
stopwatch.ElapsedMilliseconds < timeLimit)
|
stageStopwatch.ElapsedMilliseconds < timeLimit)
|
||||||
{
|
{
|
||||||
attempts++;
|
attempts++;
|
||||||
// 진행 상태가 살아 있음을 보여주기 위해, 1초 단위 점(.) 애니메이션을 출력한다.
|
// 진행 상태가 살아 있음을 보여주기 위해, 1초 단위 점(.) 애니메이션을 출력한다.
|
||||||
// 예) "레벨 214 [기본] 생성중.", "..", "..." 식으로 순환.
|
// 예) "레벨 214 [기본] 생성중.", "..", "..." 식으로 순환.
|
||||||
_status.Show($"레벨 {id} [{stageLabel}] 생성중", withDots: true);
|
_status.ShowProgress($"레벨 {id} [{stageLabel}] 생성중", levelStopwatch.Elapsed, _totalWatch.Elapsed);
|
||||||
var mask = MaskLibrary.CreateVariant(
|
var mask = MaskLibrary.CreateVariant(
|
||||||
MaskLibrary.PickRandom(rng, band.ShapeMasksExpanded),
|
MaskLibrary.PickRandom(rng, band.ShapeMasksExpanded),
|
||||||
rng,
|
rng,
|
||||||
@@ -734,7 +717,7 @@ internal sealed class LevelGenerator
|
|||||||
_seenPatterns.Add(patternKey);
|
_seenPatterns.Add(patternKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
_status.Show($"레벨 {id} [{stageLabel}] 생성완료 ({StatusReporter.FormatDuration(stopwatch.Elapsed)})");
|
_status.Show($"레벨 {id} [{stageLabel}] 생성완료 ({StatusReporter.FormatDuration(levelStopwatch.Elapsed)})");
|
||||||
level = new GeneratedLevel
|
level = new GeneratedLevel
|
||||||
{
|
{
|
||||||
Id = id,
|
Id = id,
|
||||||
|
|||||||
@@ -22,7 +22,8 @@ dotnet run -- --trim <입력 json> [출력 json] [startId] [endId]
|
|||||||
- `startId/endId`가 없으면 `levelbalance.json`에 정의된 밴드 범위를 사용합니다.
|
- `startId/endId`가 없으면 `levelbalance.json`에 정의된 밴드 범위를 사용합니다.
|
||||||
- 검증 모드에서는 지정한 JSON의 레벨을 솔버로 풀어보고, 성공/실패와 함께 `moves/ pushes/ turns`를 리포트합니다.
|
- 검증 모드에서는 지정한 JSON의 레벨을 솔버로 풀어보고, 성공/실패와 함께 `moves/ pushes/ turns`를 리포트합니다.
|
||||||
- 트림 모드는 지정한 범위(id)만 외곽의 '0' 패딩을 걷어내서 저장합니다(기본 출력 경로: `trimmed_output.json`).
|
- 트림 모드는 지정한 범위(id)만 외곽의 '0' 패딩을 걷어내서 저장합니다(기본 출력 경로: `trimmed_output.json`).
|
||||||
- 진행 상태는 stderr에 한 줄로 갱신됩니다(벽 생성중/박스 배치중/검증중/생성완료).
|
- 생성 중 매 레벨 단위로 누적 체크포인트 JSON을 `stage_checkpoint.json`에 기록하며, 경로는 `NEKOBAN_CHECKPOINT` 환경변수로 바꿀 수 있습니다.
|
||||||
|
- 진행 상태는 stderr에 한 줄로 갱신되며 점(.) 애니메이션과 현재 레벨/전체 경과 시간이 함께 표시됩니다.
|
||||||
|
|
||||||
## 설정
|
## 설정
|
||||||
- `levelbalance.json`: 레벨 밴드 설정(레벨 범위, 박스 수, 최소 푸시/턴/브랜칭, 마스크 세트/확장 범위, 역탐색 깊이/폭 스케일, 목표/박스 간 최소 거리, 주머니(포켓) 리라이팅). `maskSets`(micro/medium/large), `maskPadMin/max`, `reverseDepthScale/breadthScale`, `minAllowedBranching`, `minGoal/Box/WallDistance`, `pocketCarveMin/Max/Radius`를 밴드별로 조정해 난이도·길이감을 설계합니다.
|
- `levelbalance.json`: 레벨 밴드 설정(레벨 범위, 박스 수, 최소 푸시/턴/브랜칭, 마스크 세트/확장 범위, 역탐색 깊이/폭 스케일, 목표/박스 간 최소 거리, 주머니(포켓) 리라이팅). `maskSets`(micro/medium/large), `maskPadMin/max`, `reverseDepthScale/breadthScale`, `minAllowedBranching`, `minGoal/Box/WallDistance`, `pocketCarveMin/Max/Radius`를 밴드별로 조정해 난이도·길이감을 설계합니다.
|
||||||
@@ -60,3 +61,4 @@ dotnet run -- --trim <입력 json> [출력 json] [startId] [endId]
|
|||||||
- 중복 레이아웃은 제거됩니다.
|
- 중복 레이아웃은 제거됩니다.
|
||||||
- 해법 패턴(푸시/턴/방향 런 요약) 해시를 이용해 유사 해법 레벨도 추가로 걸러냅니다.
|
- 해법 패턴(푸시/턴/방향 런 요약) 해시를 이용해 유사 해법 레벨도 추가로 걸러냅니다.
|
||||||
- 더 느리게 돌려야 하면 `GenerationTuning`의 시도/시간 한도를 조정하세요.
|
- 더 느리게 돌려야 하면 `GenerationTuning`의 시도/시간 한도를 조정하세요.
|
||||||
|
- 밴드 강등/시드 재시도는 성공할 때까지 반복되므로, 오래 걸릴 때는 진행 메시지의 경과 시간을 참고해 중단 여부를 판단하세요.
|
||||||
|
|||||||
@@ -29,6 +29,13 @@ internal sealed class StatusReporter
|
|||||||
Write(message, newline: true);
|
Write(message, newline: true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void ShowProgress(string message, TimeSpan levelElapsed, TimeSpan totalElapsed)
|
||||||
|
{
|
||||||
|
_dotCounter++;
|
||||||
|
var progress = $"{message}{Dots()}({FormatDuration(levelElapsed)} / {FormatDuration(totalElapsed)})";
|
||||||
|
Write(progress, newline: false);
|
||||||
|
}
|
||||||
|
|
||||||
private void Write(string message, bool newline)
|
private void Write(string message, bool newline)
|
||||||
{
|
{
|
||||||
if (_lastMessage == message && !newline) return;
|
if (_lastMessage == message && !newline) return;
|
||||||
@@ -56,9 +63,13 @@ internal sealed class StatusReporter
|
|||||||
|
|
||||||
public static string FormatDuration(TimeSpan elapsed)
|
public static string FormatDuration(TimeSpan elapsed)
|
||||||
{
|
{
|
||||||
|
if (elapsed.TotalHours >= 1)
|
||||||
|
{
|
||||||
|
return $"{(int)elapsed.TotalHours}h{elapsed.Minutes}m{elapsed.Seconds}s";
|
||||||
|
}
|
||||||
if (elapsed.TotalMinutes >= 1)
|
if (elapsed.TotalMinutes >= 1)
|
||||||
{
|
{
|
||||||
return $"{(int)elapsed.TotalMinutes}m {elapsed.Seconds}s";
|
return $"{(int)elapsed.TotalMinutes}m{elapsed.Seconds}s";
|
||||||
}
|
}
|
||||||
return $"{elapsed.Seconds}s";
|
return $"{elapsed.Seconds}s";
|
||||||
}
|
}
|
||||||
|
|||||||
277
stage.json
277
stage.json
@@ -1,277 +0,0 @@
|
|||||||
[
|
|
||||||
{
|
|
||||||
"id": 3,
|
|
||||||
"grid": [
|
|
||||||
"00000000000",
|
|
||||||
"00000000000",
|
|
||||||
"00#######00",
|
|
||||||
"00##..###00",
|
|
||||||
"00#...$@#00",
|
|
||||||
"00#.....#00",
|
|
||||||
"00#...G.#00",
|
|
||||||
"00###.###00",
|
|
||||||
"00#######00",
|
|
||||||
"00000000000",
|
|
||||||
"00000000000"
|
|
||||||
],
|
|
||||||
"lowestPush": 4,
|
|
||||||
"pushLimit": 6
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": 4,
|
|
||||||
"grid": [
|
|
||||||
"######0",
|
|
||||||
"#...##0",
|
|
||||||
"#...##0",
|
|
||||||
"#G$..#0",
|
|
||||||
"#.G$.#0",
|
|
||||||
"#.#@##0",
|
|
||||||
"######0"
|
|
||||||
],
|
|
||||||
"lowestPush": 4,
|
|
||||||
"pushLimit": 6
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": 5,
|
|
||||||
"grid": [
|
|
||||||
"000000000",
|
|
||||||
"0#######0",
|
|
||||||
"0##...##0",
|
|
||||||
"0#@$..##0",
|
|
||||||
"0#.G...#0",
|
|
||||||
"0##$..##0",
|
|
||||||
"0##.G.##0",
|
|
||||||
"0#######0",
|
|
||||||
"000000000"
|
|
||||||
],
|
|
||||||
"lowestPush": 5,
|
|
||||||
"pushLimit": 7
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": 6,
|
|
||||||
"grid": [
|
|
||||||
"######0",
|
|
||||||
"#...##0",
|
|
||||||
"#$.G##0",
|
|
||||||
"#G.$@#0",
|
|
||||||
"#..###0",
|
|
||||||
"#...##0",
|
|
||||||
"######0"
|
|
||||||
],
|
|
||||||
"lowestPush": 6,
|
|
||||||
"pushLimit": 9
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": 7,
|
|
||||||
"grid": [
|
|
||||||
"000000000",
|
|
||||||
"0#######0",
|
|
||||||
"0###..##0",
|
|
||||||
"0#@$...#0",
|
|
||||||
"0#G....#0",
|
|
||||||
"0#.....#0",
|
|
||||||
"0###.###0",
|
|
||||||
"0#######0",
|
|
||||||
"000000000"
|
|
||||||
],
|
|
||||||
"lowestPush": 4,
|
|
||||||
"pushLimit": 6
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": 8,
|
|
||||||
"grid": [
|
|
||||||
"######0",
|
|
||||||
"#...##0",
|
|
||||||
"#..$@#0",
|
|
||||||
"#.G..#0",
|
|
||||||
"#..$##0",
|
|
||||||
"#....#0",
|
|
||||||
"######0"
|
|
||||||
],
|
|
||||||
"lowestPush": 4,
|
|
||||||
"pushLimit": 6
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": 9,
|
|
||||||
"grid": [
|
|
||||||
"######0",
|
|
||||||
"#..@##0",
|
|
||||||
"#.#$##0",
|
|
||||||
"#....#0",
|
|
||||||
"#....#0",
|
|
||||||
"#.G.##0",
|
|
||||||
"######0"
|
|
||||||
],
|
|
||||||
"lowestPush": 4,
|
|
||||||
"pushLimit": 6
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": 10,
|
|
||||||
"grid": [
|
|
||||||
"#######",
|
|
||||||
"#@$GG.#",
|
|
||||||
"#.$...#",
|
|
||||||
"#.....#",
|
|
||||||
"#######",
|
|
||||||
"#######",
|
|
||||||
"0000000"
|
|
||||||
],
|
|
||||||
"lowestPush": 4,
|
|
||||||
"pushLimit": 6
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": 11,
|
|
||||||
"grid": [
|
|
||||||
"######0",
|
|
||||||
"#..@##0",
|
|
||||||
"##$$##0",
|
|
||||||
"#.G..#0",
|
|
||||||
"#G...#0",
|
|
||||||
"#...##0",
|
|
||||||
"######0"
|
|
||||||
],
|
|
||||||
"lowestPush": 5,
|
|
||||||
"pushLimit": 7
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": 12,
|
|
||||||
"grid": [
|
|
||||||
"0000000",
|
|
||||||
"0#####0",
|
|
||||||
"0#...#0",
|
|
||||||
"0#.$.#0",
|
|
||||||
"0#G.$#0",
|
|
||||||
"0#.#@#0",
|
|
||||||
"0#####0",
|
|
||||||
"0000000"
|
|
||||||
],
|
|
||||||
"lowestPush": 4,
|
|
||||||
"pushLimit": 6
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": 13,
|
|
||||||
"grid": [
|
|
||||||
"######0",
|
|
||||||
"#...##0",
|
|
||||||
"#..###0",
|
|
||||||
"#..$@#0",
|
|
||||||
"#$.G##0",
|
|
||||||
"#...##0",
|
|
||||||
"######0"
|
|
||||||
],
|
|
||||||
"lowestPush": 7,
|
|
||||||
"pushLimit": 10
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": 14,
|
|
||||||
"grid": [
|
|
||||||
"0######",
|
|
||||||
"0##...#",
|
|
||||||
"0##.$@#",
|
|
||||||
"0#.G..#",
|
|
||||||
"0##G$##",
|
|
||||||
"0##...#",
|
|
||||||
"0######"
|
|
||||||
],
|
|
||||||
"lowestPush": 5,
|
|
||||||
"pushLimit": 7
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": 15,
|
|
||||||
"grid": [
|
|
||||||
"00000000000",
|
|
||||||
"00000000000",
|
|
||||||
"00#######00",
|
|
||||||
"00##...##00",
|
|
||||||
"00##.$.##00",
|
|
||||||
"00#@$.G.#00",
|
|
||||||
"00##..G##00",
|
|
||||||
"00##...##00",
|
|
||||||
"00#######00",
|
|
||||||
"00000000000",
|
|
||||||
"00000000000"
|
|
||||||
],
|
|
||||||
"lowestPush": 5,
|
|
||||||
"pushLimit": 7
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": 16,
|
|
||||||
"grid": [
|
|
||||||
"0######",
|
|
||||||
"0##.G.#",
|
|
||||||
"0##$.G#",
|
|
||||||
"0#....#",
|
|
||||||
"0#@$.##",
|
|
||||||
"0##...#",
|
|
||||||
"0######"
|
|
||||||
],
|
|
||||||
"lowestPush": 8,
|
|
||||||
"pushLimit": 11
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": 17,
|
|
||||||
"grid": [
|
|
||||||
"0######",
|
|
||||||
"0##...#",
|
|
||||||
"0#...##",
|
|
||||||
"0#....#",
|
|
||||||
"0##$.G#",
|
|
||||||
"0##@..#",
|
|
||||||
"0######"
|
|
||||||
],
|
|
||||||
"lowestPush": 4,
|
|
||||||
"pushLimit": 6
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": 18,
|
|
||||||
"grid": [
|
|
||||||
"00000000000",
|
|
||||||
"00000000000",
|
|
||||||
"00#######00",
|
|
||||||
"00##...##00",
|
|
||||||
"00##G.$##00",
|
|
||||||
"00#.G...#00",
|
|
||||||
"00#...$.#00",
|
|
||||||
"00##..@##00",
|
|
||||||
"00#######00",
|
|
||||||
"00000000000",
|
|
||||||
"00000000000"
|
|
||||||
],
|
|
||||||
"lowestPush": 7,
|
|
||||||
"pushLimit": 10
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": 19,
|
|
||||||
"grid": [
|
|
||||||
"000000000",
|
|
||||||
"0#######0",
|
|
||||||
"0##...##0",
|
|
||||||
"0##G..##0",
|
|
||||||
"0#.G.$@#0",
|
|
||||||
"0##.$.##0",
|
|
||||||
"0##...##0",
|
|
||||||
"0#######0",
|
|
||||||
"000000000"
|
|
||||||
],
|
|
||||||
"lowestPush": 5,
|
|
||||||
"pushLimit": 7
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": 20,
|
|
||||||
"grid": [
|
|
||||||
"00000000000",
|
|
||||||
"00000000000",
|
|
||||||
"00#######00",
|
|
||||||
"00###@###00",
|
|
||||||
"00#..$..#00",
|
|
||||||
"00#...$.#00",
|
|
||||||
"00#.G.G.#00",
|
|
||||||
"00###.###00",
|
|
||||||
"00#######00",
|
|
||||||
"00000000000",
|
|
||||||
"00000000000"
|
|
||||||
],
|
|
||||||
"lowestPush": 4,
|
|
||||||
"pushLimit": 6
|
|
||||||
}
|
|
||||||
]
|
|
||||||
156
stage_231_240.json
Normal file
156
stage_231_240.json
Normal file
@@ -0,0 +1,156 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"id": 231,
|
||||||
|
"grid": [
|
||||||
|
"#########",
|
||||||
|
"#.@....##",
|
||||||
|
"#.$.....#",
|
||||||
|
"#.......#",
|
||||||
|
"#....G..#",
|
||||||
|
"#.#...#.#",
|
||||||
|
"#...#...#",
|
||||||
|
"##.#..###",
|
||||||
|
"###...###",
|
||||||
|
"#########"
|
||||||
|
],
|
||||||
|
"lowestPush": 5,
|
||||||
|
"pushLimit": 7
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 232,
|
||||||
|
"grid": [
|
||||||
|
"########",
|
||||||
|
"###..###",
|
||||||
|
"#.#..###",
|
||||||
|
"#.@....#",
|
||||||
|
"#.$.##.#",
|
||||||
|
"#....#.#",
|
||||||
|
"#.G##..#",
|
||||||
|
"#......#",
|
||||||
|
"###..###",
|
||||||
|
"###..###",
|
||||||
|
"########"
|
||||||
|
],
|
||||||
|
"lowestPush": 2,
|
||||||
|
"pushLimit": 4
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 233,
|
||||||
|
"grid": [
|
||||||
|
"#######",
|
||||||
|
"#.@..##",
|
||||||
|
"##$$#.#",
|
||||||
|
"#.G$..#",
|
||||||
|
"#...$G#",
|
||||||
|
"#.G...#",
|
||||||
|
"#######"
|
||||||
|
],
|
||||||
|
"lowestPush": 26,
|
||||||
|
"pushLimit": 36
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 234,
|
||||||
|
"grid": [
|
||||||
|
"##########",
|
||||||
|
"###@###.##",
|
||||||
|
"##.$#....#",
|
||||||
|
"##G.#.$.##",
|
||||||
|
"#.$.....##",
|
||||||
|
"#.G$######",
|
||||||
|
"#...#...##",
|
||||||
|
"##..#...##",
|
||||||
|
"##..#...##",
|
||||||
|
"###.######",
|
||||||
|
"##########"
|
||||||
|
],
|
||||||
|
"lowestPush": 25,
|
||||||
|
"pushLimit": 34
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 235,
|
||||||
|
"grid": [
|
||||||
|
"###########",
|
||||||
|
"####...#..#",
|
||||||
|
"##.$G#.$$.#",
|
||||||
|
"#.......$@#",
|
||||||
|
"#.G.#######",
|
||||||
|
"##G.#.#..##",
|
||||||
|
"##.#...#.##",
|
||||||
|
"##..#.#..##",
|
||||||
|
"###########",
|
||||||
|
"###########"
|
||||||
|
],
|
||||||
|
"lowestPush": 28,
|
||||||
|
"pushLimit": 38
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 236,
|
||||||
|
"grid": [
|
||||||
|
"########",
|
||||||
|
"####...#",
|
||||||
|
"##@$.GG#",
|
||||||
|
"##.$$..#",
|
||||||
|
"##.#.$G#",
|
||||||
|
"#..#...#",
|
||||||
|
"########"
|
||||||
|
],
|
||||||
|
"lowestPush": 27,
|
||||||
|
"pushLimit": 37
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 237,
|
||||||
|
"grid": [
|
||||||
|
"######",
|
||||||
|
"#.#..#",
|
||||||
|
"#@$.G#",
|
||||||
|
"#.#..#",
|
||||||
|
"######"
|
||||||
|
],
|
||||||
|
"lowestPush": 2,
|
||||||
|
"pushLimit": 4
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 238,
|
||||||
|
"grid": [
|
||||||
|
"#######",
|
||||||
|
"#.G...#",
|
||||||
|
"#.G$$G#",
|
||||||
|
"#...$.#",
|
||||||
|
"#.$#$G#",
|
||||||
|
"##@#..#",
|
||||||
|
"#######"
|
||||||
|
],
|
||||||
|
"lowestPush": 25,
|
||||||
|
"pushLimit": 34
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 239,
|
||||||
|
"grid": [
|
||||||
|
"###########",
|
||||||
|
"##.....####",
|
||||||
|
"##.$.#G$.##",
|
||||||
|
"##...G$G..#",
|
||||||
|
"#.##.$@#..#",
|
||||||
|
"##.####...#",
|
||||||
|
"##........#",
|
||||||
|
"##...#$...#",
|
||||||
|
"##...#....#",
|
||||||
|
"##.#####..#",
|
||||||
|
"###########"
|
||||||
|
],
|
||||||
|
"lowestPush": 25,
|
||||||
|
"pushLimit": 34
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 240,
|
||||||
|
"grid": [
|
||||||
|
"######",
|
||||||
|
"#.G..#",
|
||||||
|
"#..$@#",
|
||||||
|
"#....#",
|
||||||
|
"######"
|
||||||
|
],
|
||||||
|
"lowestPush": 2,
|
||||||
|
"pushLimit": 4
|
||||||
|
}
|
||||||
|
]
|
||||||
19
stage_241.json
Normal file
19
stage_241.json
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"id": 241,
|
||||||
|
"grid": [
|
||||||
|
"##########",
|
||||||
|
"#####..###",
|
||||||
|
"###....@.#",
|
||||||
|
"#.#.###$.#",
|
||||||
|
"#...#...G#",
|
||||||
|
"#G$.#.$..#",
|
||||||
|
"#.#.###..#",
|
||||||
|
"#.#.....##",
|
||||||
|
"###.#..###",
|
||||||
|
"##########"
|
||||||
|
],
|
||||||
|
"lowestPush": 7,
|
||||||
|
"pushLimit": 10
|
||||||
|
}
|
||||||
|
]
|
||||||
133
stage_242_250.json
Normal file
133
stage_242_250.json
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"id": 242,
|
||||||
|
"grid": [
|
||||||
|
"########",
|
||||||
|
"#..#.G.#",
|
||||||
|
"#..#.$.#",
|
||||||
|
"##.$$..#",
|
||||||
|
"##@$G.G#",
|
||||||
|
"####...#",
|
||||||
|
"########"
|
||||||
|
],
|
||||||
|
"lowestPush": 26,
|
||||||
|
"pushLimit": 36
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 243,
|
||||||
|
"grid": [
|
||||||
|
"######",
|
||||||
|
"#....#",
|
||||||
|
"#@$.$#",
|
||||||
|
"###$G#",
|
||||||
|
"###..#",
|
||||||
|
"######"
|
||||||
|
],
|
||||||
|
"lowestPush": 1,
|
||||||
|
"pushLimit": 3
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 244,
|
||||||
|
"grid": [
|
||||||
|
"########",
|
||||||
|
"##.....#",
|
||||||
|
"#@$.G..#",
|
||||||
|
"####.$G#",
|
||||||
|
"##.$$$.#",
|
||||||
|
"##.G.G.#",
|
||||||
|
"########"
|
||||||
|
],
|
||||||
|
"lowestPush": 27,
|
||||||
|
"pushLimit": 37
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 245,
|
||||||
|
"grid": [
|
||||||
|
"########",
|
||||||
|
"#....###",
|
||||||
|
"#...G###",
|
||||||
|
"#.$$.#.#",
|
||||||
|
"#.@###.#",
|
||||||
|
"#.#..#.#",
|
||||||
|
"#..###.#",
|
||||||
|
"#......#",
|
||||||
|
"###...##",
|
||||||
|
"###..###",
|
||||||
|
"########"
|
||||||
|
],
|
||||||
|
"lowestPush": 3,
|
||||||
|
"pushLimit": 5
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 246,
|
||||||
|
"grid": [
|
||||||
|
"##########",
|
||||||
|
"##.......#",
|
||||||
|
"##.#.....#",
|
||||||
|
"#.G#..#..#",
|
||||||
|
"#..#..#..#",
|
||||||
|
"##$####.##",
|
||||||
|
"##@.....##",
|
||||||
|
"####..####",
|
||||||
|
"###.....##",
|
||||||
|
"##########"
|
||||||
|
],
|
||||||
|
"lowestPush": 2,
|
||||||
|
"pushLimit": 4
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 247,
|
||||||
|
"grid": [
|
||||||
|
"#######",
|
||||||
|
"###@..#",
|
||||||
|
"##.$..#",
|
||||||
|
"##..$##",
|
||||||
|
"#.G$G.#",
|
||||||
|
"#..$..#",
|
||||||
|
"##..###",
|
||||||
|
"#######"
|
||||||
|
],
|
||||||
|
"lowestPush": 26,
|
||||||
|
"pushLimit": 36
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 248,
|
||||||
|
"grid": [
|
||||||
|
"#####",
|
||||||
|
"#..@#",
|
||||||
|
"#G$$#",
|
||||||
|
"#.$G#",
|
||||||
|
"#...#",
|
||||||
|
"#####"
|
||||||
|
],
|
||||||
|
"lowestPush": 2,
|
||||||
|
"pushLimit": 4
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 249,
|
||||||
|
"grid": [
|
||||||
|
"########",
|
||||||
|
"##.GG$.#",
|
||||||
|
"##..$@G#",
|
||||||
|
"####...#",
|
||||||
|
"#..$.$.#",
|
||||||
|
"##...###",
|
||||||
|
"########"
|
||||||
|
],
|
||||||
|
"lowestPush": 26,
|
||||||
|
"pushLimit": 36
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 250,
|
||||||
|
"grid": [
|
||||||
|
"######",
|
||||||
|
"#..@.#",
|
||||||
|
"###$$#",
|
||||||
|
"#....#",
|
||||||
|
"##.G.#",
|
||||||
|
"######"
|
||||||
|
],
|
||||||
|
"lowestPush": 2,
|
||||||
|
"pushLimit": 4
|
||||||
|
}
|
||||||
|
]
|
||||||
133
stage_checkpoint.json
Normal file
133
stage_checkpoint.json
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"id": 242,
|
||||||
|
"grid": [
|
||||||
|
"########",
|
||||||
|
"#..#.G.#",
|
||||||
|
"#..#.$.#",
|
||||||
|
"##.$$..#",
|
||||||
|
"##@$G.G#",
|
||||||
|
"####...#",
|
||||||
|
"########"
|
||||||
|
],
|
||||||
|
"lowestPush": 26,
|
||||||
|
"pushLimit": 36
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 243,
|
||||||
|
"grid": [
|
||||||
|
"######",
|
||||||
|
"#....#",
|
||||||
|
"#@$.$#",
|
||||||
|
"###$G#",
|
||||||
|
"###..#",
|
||||||
|
"######"
|
||||||
|
],
|
||||||
|
"lowestPush": 1,
|
||||||
|
"pushLimit": 3
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 244,
|
||||||
|
"grid": [
|
||||||
|
"########",
|
||||||
|
"##.....#",
|
||||||
|
"#@$.G..#",
|
||||||
|
"####.$G#",
|
||||||
|
"##.$$$.#",
|
||||||
|
"##.G.G.#",
|
||||||
|
"########"
|
||||||
|
],
|
||||||
|
"lowestPush": 27,
|
||||||
|
"pushLimit": 37
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 245,
|
||||||
|
"grid": [
|
||||||
|
"########",
|
||||||
|
"#....###",
|
||||||
|
"#...G###",
|
||||||
|
"#.$$.#.#",
|
||||||
|
"#.@###.#",
|
||||||
|
"#.#..#.#",
|
||||||
|
"#..###.#",
|
||||||
|
"#......#",
|
||||||
|
"###...##",
|
||||||
|
"###..###",
|
||||||
|
"########"
|
||||||
|
],
|
||||||
|
"lowestPush": 3,
|
||||||
|
"pushLimit": 5
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 246,
|
||||||
|
"grid": [
|
||||||
|
"##########",
|
||||||
|
"##.......#",
|
||||||
|
"##.#.....#",
|
||||||
|
"#.G#..#..#",
|
||||||
|
"#..#..#..#",
|
||||||
|
"##$####.##",
|
||||||
|
"##@.....##",
|
||||||
|
"####..####",
|
||||||
|
"###.....##",
|
||||||
|
"##########"
|
||||||
|
],
|
||||||
|
"lowestPush": 2,
|
||||||
|
"pushLimit": 4
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 247,
|
||||||
|
"grid": [
|
||||||
|
"#######",
|
||||||
|
"###@..#",
|
||||||
|
"##.$..#",
|
||||||
|
"##..$##",
|
||||||
|
"#.G$G.#",
|
||||||
|
"#..$..#",
|
||||||
|
"##..###",
|
||||||
|
"#######"
|
||||||
|
],
|
||||||
|
"lowestPush": 26,
|
||||||
|
"pushLimit": 36
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 248,
|
||||||
|
"grid": [
|
||||||
|
"#####",
|
||||||
|
"#..@#",
|
||||||
|
"#G$$#",
|
||||||
|
"#.$G#",
|
||||||
|
"#...#",
|
||||||
|
"#####"
|
||||||
|
],
|
||||||
|
"lowestPush": 2,
|
||||||
|
"pushLimit": 4
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 249,
|
||||||
|
"grid": [
|
||||||
|
"########",
|
||||||
|
"##.GG$.#",
|
||||||
|
"##..$@G#",
|
||||||
|
"####...#",
|
||||||
|
"#..$.$.#",
|
||||||
|
"##...###",
|
||||||
|
"########"
|
||||||
|
],
|
||||||
|
"lowestPush": 26,
|
||||||
|
"pushLimit": 36
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 250,
|
||||||
|
"grid": [
|
||||||
|
"######",
|
||||||
|
"#..@.#",
|
||||||
|
"###$$#",
|
||||||
|
"#....#",
|
||||||
|
"##.G.#",
|
||||||
|
"######"
|
||||||
|
],
|
||||||
|
"lowestPush": 2,
|
||||||
|
"pushLimit": 4
|
||||||
|
}
|
||||||
|
]
|
||||||
Reference in New Issue
Block a user