Refactor world bootstrap and allow empty startup worlds

This commit is contained in:
2026-03-29 13:22:48 -04:00
parent 640e147ea8
commit 0bb72bee35
79 changed files with 173146 additions and 9235 deletions

View File

@@ -4,114 +4,65 @@ namespace SpaceGame.Api.Universe.Scenario;
public sealed class SystemGenerationService
{
internal List<SolarSystemDefinition> PrepareAuthoredSystems(IReadOnlyList<SolarSystemDefinition> authoredSystems) =>
authoredSystems
private const float KnownSystemSelectionChance = 0.5f;
internal List<SolarSystemDefinition> PrepareKnownSystems(IReadOnlyList<SolarSystemDefinition> knownSystems) =>
knownSystems
.Select((system, index) => EnsureStrategicResourceCoverage(CloneSystemDefinition(system), index))
.ToList();
internal List<SolarSystemDefinition> ExpandSystems(
IReadOnlyList<SolarSystemDefinition> authoredSystems,
int targetSystemCount)
internal List<SolarSystemDefinition> GenerateSystems(
IReadOnlyList<SolarSystemDefinition> knownSystems,
WorldGenerationOptions worldGenerationOptions)
{
var systems = authoredSystems
.Select(CloneSystemDefinition)
.ToList();
if (targetSystemCount <= 0)
if (worldGenerationOptions.TargetSystemCount <= 0)
{
return [];
}
if (systems.Count > targetSystemCount)
if (knownSystems.Count == 0)
{
return TrimSystemsToTarget(systems, targetSystemCount);
throw new InvalidOperationException("World generation requires at least one known system template.");
}
if (systems.Count >= targetSystemCount || authoredSystems.Count == 0)
{
return systems;
}
var systems = new List<SolarSystemDefinition>(worldGenerationOptions.TargetSystemCount);
var availableKnownSystems = knownSystems.Select(CloneSystemDefinition).ToList();
var templateSystems = knownSystems.Select(CloneSystemDefinition).ToList();
var existingIds = systems
.Select(system => system.Id)
.ToHashSet(StringComparer.Ordinal);
var generatedPositions = BuildGalaxyPositions(
authoredSystems.Select(system => ToVector(system.Position)).ToList(),
targetSystemCount - systems.Count);
var existingIds = new HashSet<string>(StringComparer.Ordinal);
var occupiedPositions = new List<Vector3>();
var generatedSystemCount = 0;
for (var index = systems.Count; index < targetSystemCount; index += 1)
for (var slotIndex = 0; slotIndex < worldGenerationOptions.TargetSystemCount; slotIndex += 1)
{
var template = authoredSystems[index % authoredSystems.Count];
var name = GeneratedSystemNames[(index - authoredSystems.Count) % GeneratedSystemNames.Length];
var id = BuildGeneratedSystemId(name, index + 1);
if (ShouldUseKnownSystem(worldGenerationOptions, slotIndex, availableKnownSystems.Count))
{
var knownSystemIndex = SelectKnownSystemIndex(worldGenerationOptions.Seed, slotIndex, availableKnownSystems.Count);
var knownSystem = availableKnownSystems[knownSystemIndex];
availableKnownSystems.RemoveAt(knownSystemIndex);
systems.Add(knownSystem);
existingIds.Add(knownSystem.Id);
occupiedPositions.Add(ToVector(knownSystem.Position));
continue;
}
var template = templateSystems[generatedSystemCount % templateSystems.Count];
var name = GeneratedSystemNames[generatedSystemCount % GeneratedSystemNames.Length];
var id = BuildGeneratedSystemId(name, generatedSystemCount + 1);
while (!existingIds.Add(id))
{
id = $"{id}-x";
}
systems.Add(CreateGeneratedSystem(template, name, id, index - authoredSystems.Count, generatedPositions[index - authoredSystems.Count]));
var position = BuildGeneratedSystemPosition(occupiedPositions, generatedSystemCount);
systems.Add(CreateGeneratedSystem(template, name, id, generatedSystemCount, position));
occupiedPositions.Add(position);
generatedSystemCount += 1;
}
return systems;
}
private static List<SolarSystemDefinition> TrimSystemsToTarget(IReadOnlyList<SolarSystemDefinition> systems, int targetSystemCount)
{
var selected = new List<SolarSystemDefinition>(targetSystemCount);
void AddById(string systemId)
{
var system = systems.FirstOrDefault(candidate => string.Equals(candidate.Id, systemId, StringComparison.Ordinal));
if (system is not null && selected.All(candidate => !string.Equals(candidate.Id, systemId, StringComparison.Ordinal)))
{
selected.Add(system);
}
}
foreach (var preferredSystemId in SystemSelectionPolicy.PreferredSystemIds)
{
AddById(preferredSystemId);
}
foreach (var system in systems)
{
if (selected.Count >= targetSystemCount)
{
break;
}
if (selected.Any(candidate => string.Equals(candidate.Id, system.Id, StringComparison.Ordinal)))
{
continue;
}
selected.Add(system);
}
if (selected.Count > 0 && selected.Count <= 4)
{
ApplyCompactGalaxyLayout(selected);
}
return selected;
}
private static void ApplyCompactGalaxyLayout(IReadOnlyList<SolarSystemDefinition> systems)
{
var compactPositions = new[]
{
new[] { 0f, 0f, 0f },
new[] { 2.6f, 0.02f, -0.42f },
new[] { -2.4f, -0.04f, 0.56f },
new[] { 0.52f, 0.04f, 2.48f },
};
for (var index = 0; index < systems.Count && index < compactPositions.Length; index += 1)
{
systems[index].Position = compactPositions[index];
}
}
private static SolarSystemDefinition CreateGeneratedSystem(
SolarSystemDefinition template,
string label,
@@ -260,30 +211,38 @@ public sealed class SystemGenerationService
return system;
}
private static List<Vector3> BuildGalaxyPositions(IReadOnlyCollection<Vector3> occupiedPositions, int count)
private static Vector3 BuildGeneratedSystemPosition(IReadOnlyCollection<Vector3> occupiedPositions, int generatedIndex)
{
var allPositions = occupiedPositions.ToList();
var generated = new List<Vector3>(count);
for (var index = 0; index < count; index += 1)
for (var attempt = 0; attempt < 64; attempt += 1)
{
Vector3? accepted = null;
for (var attempt = 0; attempt < 64; attempt += 1)
var candidate = ComputeGeneratedSystemPosition(generatedIndex, attempt);
if (allPositions.All(existing => existing.DistanceTo(candidate) >= MinimumSystemSeparation))
{
var candidate = ComputeGeneratedSystemPosition(index, attempt);
if (allPositions.All(existing => existing.DistanceTo(candidate) >= MinimumSystemSeparation))
{
accepted = candidate;
break;
}
return candidate;
}
accepted ??= ComputeFallbackGeneratedSystemPosition(index);
generated.Add(accepted.Value);
allPositions.Add(accepted.Value);
}
return generated;
return ComputeFallbackGeneratedSystemPosition(generatedIndex);
}
private static bool ShouldUseKnownSystem(
WorldGenerationOptions worldGenerationOptions,
int slotIndex,
int remainingKnownSystemCount)
{
if (!worldGenerationOptions.UseKnownSystems || remainingKnownSystemCount <= 0)
{
return false;
}
return Hash01(worldGenerationOptions.Seed, 700 + slotIndex) >= KnownSystemSelectionChance;
}
private static int SelectKnownSystemIndex(int seed, int slotIndex, int remainingKnownSystemCount)
{
var selection = Hash01(seed, 900 + slotIndex);
return Math.Min((int)(selection * remainingKnownSystemCount), remainingKnownSystemCount - 1);
}
private static Vector3 ComputeGeneratedSystemPosition(int generatedIndex, int attempt)