A fixed seed fixes every fit: RunStart seeds the run's item draws from the galaxy's seed
ScenarioStage.Generated picked from ItemManager.Random, which is clock-seeded per process and advances in play, so a relaunch of Djinni Shakedown or Long Haul in one session gave the player a different fit; every branded lot's workmanship and role fills came from the same clock stream, so arena gear and explicit fits varied in quality too. Galaxy now keeps the seed it was generated at, and RunStart.GenerateArena and Stage each reseed the item manager's random from it, one stream per step, so what runs between them cannot shift either. SeedRules relaunches every fixed-seed scenario around a clock-free Main Galaxy launch and a scrambled item random, and compares the galaxy (links, factions, homes, exit too), the arena down to each item's maker and workmanship, and the staged fits. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
4d1334e044
commit
02e7fd4b7e
5 changed files with 69 additions and 20 deletions
|
|
@ -28,6 +28,10 @@ public class Galaxy
|
|||
public Dictionary<Faction, FactionRelationship> FactionRelationships { get; } = new Dictionary<Faction, FactionRelationship>();
|
||||
private Action<string> Log { get; }
|
||||
public bool IsPrelude { get; }
|
||||
|
||||
// The seed this galaxy was generated at, which also seeds its run's item draws (RunStart); zero for a galaxy
|
||||
// restored from a save.
|
||||
public uint Seed { get; }
|
||||
|
||||
private HashSet<CultRecordKey> _containedFactions;
|
||||
private GalaxyZone[] _exitPath;
|
||||
|
|
@ -104,7 +108,8 @@ public class Galaxy
|
|||
Background = background;
|
||||
Log = log;
|
||||
var factions = cache.GetAll<Faction>();
|
||||
var random = new Random(seed == 0 ? (uint) (DateTime.Now.Ticks % uint.MaxValue) : seed);
|
||||
Seed = seed == 0 ? (uint) (DateTime.Now.Ticks % uint.MaxValue) : seed;
|
||||
var random = new Random(Seed);
|
||||
Factions = factions.OrderBy(x => random.NextFloat()).Take(settings.MegaCount).ToArray();
|
||||
foreach (var f in Factions) FactionRelationships[f] = FactionRelationship.Neutral;
|
||||
|
||||
|
|
@ -159,7 +164,8 @@ public class Galaxy
|
|||
|
||||
Background = background;
|
||||
Log = log;
|
||||
var random = new Random(seed == 0 ? (uint) (DateTime.Now.Ticks % uint.MaxValue) : seed);
|
||||
Seed = seed == 0 ? (uint) (DateTime.Now.Ticks % uint.MaxValue) : seed;
|
||||
var random = new Random(Seed);
|
||||
|
||||
var factions = new List<Faction>();
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Random = CultMath.Random;
|
||||
|
||||
// The owner of a new run (docs/scenarios-cut.md, R.4). Every new run is a scenario: RunStart generates its galaxy,
|
||||
// generates its arena (the galaxy's entrance zone) under the scenario's Ambient rule, and admits what the scenario
|
||||
|
|
@ -21,9 +22,12 @@ public static class RunStart
|
|||
return scenario.Generate(stage) ?? throw new InvalidOperationException($"Scenario {scenario.Name} generated no galaxy.");
|
||||
}
|
||||
|
||||
public static ZonePack GenerateArena(ItemManager itemManager, ZoneGenerationSettings zoneSettings, Galaxy galaxy, Scenario scenario) =>
|
||||
galaxy.Entrance.PackedContents = ZoneGenerator.GenerateZone(
|
||||
public static ZonePack GenerateArena(ItemManager itemManager, ZoneGenerationSettings zoneSettings, Galaxy galaxy, Scenario scenario)
|
||||
{
|
||||
SeedItems(itemManager, galaxy, 1);
|
||||
return galaxy.Entrance.PackedContents = ZoneGenerator.GenerateZone(
|
||||
itemManager, zoneSettings, galaxy, galaxy.Entrance, galaxy.IsPrelude, scenario.Ambient);
|
||||
}
|
||||
|
||||
// Runs the scenario's Stage, then admits its player and everything it placed: each entity's stance set both ways
|
||||
// with the player, a pilot for each piloted one. Returns null, with every failure listed and the arena unchanged,
|
||||
|
|
@ -31,6 +35,7 @@ public static class RunStart
|
|||
public static Staged Stage(ItemManager itemManager, Zone arena, Scenario scenario, string startingHull,
|
||||
TutorialGenerationSettings tutorialGenerationSettings, List<string> failures)
|
||||
{
|
||||
SeedItems(itemManager, arena.Galaxy, 2);
|
||||
var stage = new ScenarioStage(itemManager, arena, startingHull, tutorialGenerationSettings);
|
||||
scenario.Stage(stage);
|
||||
var reported = failures.Count;
|
||||
|
|
@ -51,4 +56,10 @@ public static class RunStart
|
|||
}
|
||||
return new Staged { Player = player, Entities = stage.Placed.Select(placed => placed.Entity).ToList() };
|
||||
}
|
||||
|
||||
// A new run's item draws (each lot's quality, each generated fit) come from its galaxy's seed, not the process clock,
|
||||
// so a fixed seed fixes the arena's gear and the player's fit. Each step draws its own stream, so whatever runs
|
||||
// between them (populating the level) cannot shift it.
|
||||
private static void SeedItems(ItemManager itemManager, Galaxy galaxy, uint step) =>
|
||||
itemManager.Random = new Random(galaxy.Seed * 0x9E3779B1u + step);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,7 +10,8 @@ public abstract class Scenario
|
|||
public abstract string Brief { get; }
|
||||
|
||||
// Zero draws the seed from the clock, so each launch is a new galaxy. Anything else fixes the galaxy, its
|
||||
// background and its arena, so every launch has the same layout.
|
||||
// background, its arena and every fit, each item's maker and workmanship included, so every launch has the same
|
||||
// layout.
|
||||
public virtual uint Seed => 0;
|
||||
|
||||
// False: the arena keeps its planets, orbits and stations but generates no ship or turret, so nothing wanders into
|
||||
|
|
|
|||
|
|
@ -64,8 +64,8 @@ public sealed class ScenarioStage
|
|||
// A hull with nothing fitted.
|
||||
public ScenarioFit Bare(string hull) => Fit(hull);
|
||||
|
||||
// A fit generated as the game generates its ships, for the faction when one is given. An empty hull means any
|
||||
// ship hull.
|
||||
// A fit generated as the game generates its ships, for the faction when one is given, from the run's item draws,
|
||||
// which RunStart seeds from the galaxy's seed. An empty hull means any ship hull.
|
||||
public ScenarioFit Generated(string hull, Faction faction = null)
|
||||
{
|
||||
var generator = new LoadoutGenerator(ref _items.Random, _items, Galaxy, Arena.GalaxyZone, faction, 2);
|
||||
|
|
|
|||
|
|
@ -132,24 +132,55 @@ public sealed partial class RunStartTests
|
|||
Assert.Contains(main.arena.Pack.Entities, entity => entity is ShipPack { IsPlayerShip: false });
|
||||
}
|
||||
|
||||
// Everything a galaxy's layout fixes: each zone's name, position, owner, factions and links, the homes, the
|
||||
// entrance, the exit and the background.
|
||||
private static string Layout(Galaxy galaxy) =>
|
||||
string.Join(";", galaxy.Zones.Select(zone => $"{zone.Name}@{zone.Position}:{zone.Owner?.Name}")) +
|
||||
$" entrance {Array.IndexOf(galaxy.Zones, galaxy.Entrance)} noise {galaxy.Background.NoisePosition}";
|
||||
string.Join(";", galaxy.Zones.Select(zone =>
|
||||
$"{zone.Name}@{zone.Position}:{zone.Owner?.Name}:[{string.Join(",", zone.Factions.Select(faction => faction.Name))}]" +
|
||||
$":[{string.Join(",", zone.AdjacentZones.Select(adjacent => Array.IndexOf(galaxy.Zones, adjacent)).OrderBy(i => i))}]")) +
|
||||
$" homes {string.Join(",", galaxy.HomeZones.Select(home => $"{home.Key.Name}={Array.IndexOf(galaxy.Zones, home.Value)}").OrderBy(home => home))}" +
|
||||
$" entrance {Array.IndexOf(galaxy.Zones, galaxy.Entrance)} exit {Array.IndexOf(galaxy.Zones, galaxy.Exit)} noise {galaxy.Background.NoisePosition}";
|
||||
|
||||
private static string Layout(ZonePack arena) =>
|
||||
$"{arena.Radius} {arena.Mass} {arena.Planets.Count()} {arena.Orbits.Count()} {arena.Entities.Count()}";
|
||||
// An arena's bodies and every entity in it, down to each item's design, maker, workmanship and role fills.
|
||||
private string Layout(ZonePack arena) =>
|
||||
$"{arena.Radius} {arena.Mass} planets [{string.Join(",", arena.Planets.Select(planet => _cache.Get(planet)).Select(body => $"{body.GetType().Name}:{body.Mass}"))}]" +
|
||||
$" orbits [{string.Join(",", arena.Orbits.Select(orbit => _cache.Get(orbit).Distance))}]" +
|
||||
$" entities [{string.Join(",", arena.Entities.Select(Gear))}]";
|
||||
|
||||
// A fixed seed fixes the galaxy, its background and its arena, launch after launch in one session. Seed zero takes
|
||||
// the clock, so two launches at two clock readings are two galaxies.
|
||||
// What a launch staged: the player and everything placed, each where it is and with exactly what it carries.
|
||||
private string Fits(RunStart.Staged staged) =>
|
||||
string.Join(" | ", staged.Entities.Prepend(staged.Player).Select(entity => $"{entity.Name}@{entity.Position}:{Gear(EntitySerializer.Pack(entity))}"));
|
||||
|
||||
private string Gear(EntityPack pack) =>
|
||||
$"{pack.GetType().Name}{(pack is ShipPack ship ? "@" + ship.Position : "")}:{string.Join("/", EntitySerializer.Items(pack).Select(Item))}";
|
||||
|
||||
private string Item(ItemInstance item)
|
||||
{
|
||||
var name = _items.GetData(item)?.Name;
|
||||
if (!(item is CraftedItemInstance crafted)) return name;
|
||||
var lot = _items.GetLot(crafted);
|
||||
return $"{name}<{(lot.Origin as Attributed)?.Faction.Key.Value}>~{lot.Quality}" +
|
||||
string.Concat((lot.Roles ?? new List<RoleFill>()).Select(fill => $"/{fill.Role}={fill.Quality}"));
|
||||
}
|
||||
|
||||
// A fixed seed fixes the galaxy, its background, its arena and every fit, each item's maker and workmanship
|
||||
// included, launch after launch in one session, whatever ran in between. Seed zero takes the clock, so two launches
|
||||
// at two clock readings are two galaxies.
|
||||
[Fact]
|
||||
public void SeedRules()
|
||||
{
|
||||
var arena = new DjinniShakedown();
|
||||
Assert.NotEqual(0u, arena.Seed);
|
||||
var first = Launch(arena);
|
||||
var second = Launch(arena);
|
||||
Assert.Equal(Layout(first.galaxy), Layout(second.galaxy));
|
||||
Assert.Equal(Layout(first.arena.Pack), Layout(second.arena.Pack));
|
||||
foreach (var scenario in Scenarios.Tests)
|
||||
{
|
||||
Assert.NotEqual(0u, scenario.Seed);
|
||||
var first = Launch(scenario);
|
||||
Launch(new MainGalaxy(), Inputs(() => 4242));
|
||||
_items.Random = new CultMath.Random(0xC0FFEE); // play draws from the run's item random
|
||||
var second = Launch(scenario);
|
||||
Assert.True(first.failures.Count == 0, $"{scenario.Name}: {string.Join("; ", first.failures)}");
|
||||
Assert.Equal(Layout(first.galaxy), Layout(second.galaxy));
|
||||
Assert.Equal(Layout(first.arena.Pack), Layout(second.arena.Pack));
|
||||
Assert.Equal(Fits(first.staged), Fits(second.staged));
|
||||
}
|
||||
|
||||
var clock = new Queue<uint>(new[] { 1000u, 2000u });
|
||||
var inputs = Inputs(() => clock.Dequeue());
|
||||
|
|
|
|||
Loading…
Reference in a new issue