진행 시간 표시 추가 및 231~250 스테이지 체크포인트 반영

This commit is contained in:
JiWoong Sul
2025-11-25 17:17:40 +09:00
parent 422e78d2db
commit ed84c9d9e8
10 changed files with 478 additions and 1304 deletions

View File

@@ -161,8 +161,9 @@ internal static class Program
var rng = new Random(seed);
var levelBands = LoadLevelBands();
var totalWatch = Stopwatch.StartNew();
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 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);
totalWatch.Stop();
@@ -458,13 +458,15 @@ internal sealed class LevelGenerator
private readonly HashSet<string> _seenLayouts = new();
private readonly HashSet<string> _seenPatterns = new();
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;
_tuning = tuning;
_bands = bands;
_status = status;
_totalWatch = totalWatch;
_trace = Environment.GetEnvironmentVariable("NEKOBAN_DEBUG") == "1";
}
@@ -483,7 +485,8 @@ internal sealed class LevelGenerator
for (var id = startId; id <= endId; id++)
{
var band = ResolveBand(id);
var level = BuildSingle(id, band);
var levelStopwatch = Stopwatch.StartNew();
var level = BuildSingle(id, band, levelStopwatch);
level = TrimLevel(level);
output.Add(level);
onCheckpoint?.Invoke(output, id);
@@ -502,7 +505,7 @@ internal sealed class LevelGenerator
return new PocketSettings(min, max, radius);
}
private GeneratedLevel BuildSingle(int id, LevelBandConfig band)
private GeneratedLevel BuildSingle(int id, LevelBandConfig band, Stopwatch levelStopwatch)
{
// 완화 단계를 순차적으로 적용한다. (총 5단계 + 비상 1단계)
var stages = new List<RelaxStage>
@@ -546,26 +549,7 @@ internal sealed class LevelGenerator
c.MinBoxDistance = Math.Max(1, c.MinBoxDistance - 1);
c.MinWallDistance = Math.Max(0, c.MinWallDistance - 1);
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레벨 밴드
@@ -584,10 +568,10 @@ internal sealed class LevelGenerator
}
// 시드 변조 재시도 + 완화 단계 루프
const int seedRetries = 5;
foreach (var bandCandidate in bandCandidates)
var retry = 0;
while (true)
{
for (var retry = 0; retry < seedRetries; retry++)
foreach (var bandCandidate in bandCandidates)
{
var jitterSeed = _rng.Next() ^ (id * 7919) ^ (retry * 104729);
var localRng = new Random(jitterSeed);
@@ -596,18 +580,17 @@ internal sealed class LevelGenerator
{
var bandForStage = stage.Adjust(bandCandidate);
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;
}
}
}
retry++;
}
throw new InvalidOperationException($"레벨 {id} 생성 실패 (모든 완화 단계 시도됨)");
}
private bool TryBuildStage(int id, LevelBandConfig band, string stageLabel, Random rng, int? overrideRelaxSteps, out GeneratedLevel level)
private bool TryBuildStage(int id, LevelBandConfig band, string stageLabel, Random rng, int? overrideRelaxSteps, Stopwatch levelStopwatch, out GeneratedLevel level)
{
var failReasons = _trace ? new Dictionary<string, int>() : null;
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 reverseBreadth = Math.Max(200, (int)Math.Round(_tuning.ReverseSearchBreadth * breadthScale));
var stopwatch = Stopwatch.StartNew();
var stageStopwatch = Stopwatch.StartNew();
var attempts = 0;
while (attempts < attemptsLimit &&
stopwatch.ElapsedMilliseconds < timeLimit)
stageStopwatch.ElapsedMilliseconds < timeLimit)
{
attempts++;
// 진행 상태가 살아 있음을 보여주기 위해, 1초 단위 점(.) 애니메이션을 출력한다.
// 예) "레벨 214 [기본] 생성중.", "..", "..." 식으로 순환.
_status.Show($"레벨 {id} [{stageLabel}] 생성중", withDots: true);
_status.ShowProgress($"레벨 {id} [{stageLabel}] 생성중", levelStopwatch.Elapsed, _totalWatch.Elapsed);
var mask = MaskLibrary.CreateVariant(
MaskLibrary.PickRandom(rng, band.ShapeMasksExpanded),
rng,
@@ -734,7 +717,7 @@ internal sealed class LevelGenerator
_seenPatterns.Add(patternKey);
}
_status.Show($"레벨 {id} [{stageLabel}] 생성완료 ({StatusReporter.FormatDuration(stopwatch.Elapsed)})");
_status.Show($"레벨 {id} [{stageLabel}] 생성완료 ({StatusReporter.FormatDuration(levelStopwatch.Elapsed)})");
level = new GeneratedLevel
{
Id = id,