Skip to content

Recipes

m4bwav edited this page Oct 4, 2026 · 1 revision

Tasks people do with a generated galaxy. Every recipe runs as shown against 1.0.0 and printed the output below it on .NET 10 and on .NET Framework 4.8. The README has more short examples (guarantees, clusters and universes, galaxy extras, units); these go further.

Save and restore a run

Store the seed and the options' code, or a link, never the generated objects. ToCode() writes only the settings that differ from the defaults:

using UniverseGeneration;

var options = Preset.Roguelike with { Names = NameStyle.Invented };
var code = options.ToCode();
Console.WriteLine(code);

var restored = GeneratorOptions.FromCode(code);
Console.WriteLine(restored == options);
var galaxy = Galaxy.Generate("run-2817", restored);
Console.WriteLine(Universe.Link(galaxy.System(5).Address, restored));

prints:

systems=30&weirdness=15&lanes=10&danger=2&names=invented
True
v1-run-2817/galaxy/system/5?systems=30&weirdness=15&lanes=10&danger=2&names=invented

The code says systems=30&weirdness=15&..., not "roguelike": a preset is just a set of option values, so a save file stays readable if presets are tuned later. Universe.At(link) regenerates system 5 with those options; the code stays readable for the whole major version (README).

Compare the presets

using UniverseGeneration;

var presets = new[]
{
    ("Default", Preset.Default), ("SpaceOpera", Preset.SpaceOpera), ("Plausible", Preset.Plausible), ("Pocket", Preset.Pocket),
    ("Roguelike", Preset.Roguelike), ("Cozy", Preset.Cozy), ("Epic", Preset.Epic),
};
foreach (var (name, options) in presets)
{
    var g = Galaxy.Generate("my-seed", options);
    Console.WriteLine($"{name}: {g.Map.Count} systems, {g.Shape}, {g.Lanes.Count} lanes, {g.Map.Count(m => m.Danger >= 8)} at danger 8 or more, code \"{options.ToCode()}\"");
}

prints:

Default: 60 systems, Ring, 71 lanes, 15 at danger 8 or more, code ""
SpaceOpera: 60 systems, Ring, 71 lanes, 15 at danger 8 or more, code "weirdness=10"
Plausible: 60 systems, Ring, 71 lanes, 15 at danger 8 or more, code "starmix=plausible&weirdness=1"
Pocket: 20 systems, Ring, 20 lanes, 8 at danger 8 or more, code "systems=20&planets=6"
Roguelike: 30 systems, Ring, 32 lanes, 15 at danger 8 or more, code "systems=30&weirdness=15&lanes=10&danger=2"
Cozy: 40 systems, Ring, 51 lanes, 6 at danger 8 or more, code "systems=40&weirdness=3&lanes=50&danger=-3"
Epic: 300 systems, Spiral, 415 lanes, 134 at danger 8 or more, code "systems=300"

Default, SpaceOpera and Plausible give the same counts here; they differ in the star mix and the share of outliers. The codes are the presets' exact settings. The shape is Auto in every preset, and here the seed picked a ring for the smaller maps and a spiral for 300 systems.

Draw the map as SVG

Map holds each system's position (X, Y, in game units within Radius of the centre) and Lanes the pairs of map indexes they join:

using System.Text;
using UniverseGeneration;

var galaxy = Galaxy.Generate("my-seed");
var r = galaxy.Radius;
var svg = new StringBuilder();
svg.AppendLine(FormattableString.Invariant($"<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"{-r} {-r} {2 * r} {2 * r}\">"));
foreach (var lane in galaxy.Lanes)
{
    var a = galaxy.Map[lane.A];
    var b = galaxy.Map[lane.B];
    var colour = lane.Bridge ? "red" : "grey";
    svg.AppendLine(FormattableString.Invariant($"<line x1=\"{a.X}\" y1=\"{a.Y}\" x2=\"{b.X}\" y2=\"{b.Y}\" stroke=\"{colour}\" />"));
}

foreach (var m in galaxy.Map)
    svg.AppendLine(FormattableString.Invariant($"<circle cx=\"{m.X}\" cy=\"{m.Y}\" r=\"8\"><title>{m.Name}, danger {m.Danger}</title></circle>"));
svg.AppendLine("</svg>");
File.WriteAllText("galaxy.svg", svg.ToString());
Console.WriteLine(svg.ToString().Split('\n')[0].TrimEnd());
Console.WriteLine($"{galaxy.Lanes.Count} lanes, {galaxy.Lanes.Count(l => l.Bridge)} of them bridges");

prints:

<svg xmlns="http://www.w3.org/2000/svg" viewBox="-1000 -1000 2000 2000">
71 lanes, 6 of them bridges

A bridge is a lane whose loss would cut the map in two, which makes it a natural chokepoint. Format numbers with the invariant culture (FormattableString.Invariant), or a machine set to a comma decimal separator writes an SVG that browsers misread.

Travel times

Distances turns map units into light-years and lane lengths into travel days:

using UniverseGeneration;

var galaxy = Galaxy.Generate("my-seed");
foreach (var lane in galaxy.Lanes.Take(3))
{
    var a = galaxy.Map[lane.A];
    var b = galaxy.Map[lane.B];
    var length = Math.Sqrt((a.X - b.X) * (a.X - b.X) + (a.Y - b.Y) * (a.Y - b.Y));
    Console.WriteLine($"{a.Name} to {b.Name}: {Math.Round(Distances.LightYears(MapLevel.Galaxy, length))} light-years, {Distances.LaneDays(length)} days");
}

prints:

HD 147927 to LHS 4081: 2635 light-years, 1 days
HD 147927 to GJ 829: 6062 light-years, 2 days
HD 165595 to Ross 128: 2691 light-years, 1 days

The days are game-scale: one day per 100 game units of lane or part of one, at least one (the LaneDays documentation), not light speed. Distances.TravelDays does the same for links between galaxies and clusters. For other units, Units.Convert and Units.Map cover lengths, masses, temperatures and times (README, "Units").

Find a seed

Generation is cheap and lazy, so searching seeds for one that fits a test is practical. Here: a Pocket galaxy with a garden world at danger 2 or less:

using UniverseGeneration;

var seed = Enumerable.Range(0, 1000).Select(i => "colony-" + i).First(s =>
    Galaxy.Generate(s, Preset.Pocket).Systems.Any(sys => sys.Danger <= 2 && sys.Planets.Any(p => p.Kind == PlanetKind.Garden)));
Console.WriteLine(seed);

prints:

colony-3

When you only need the thing to exist somewhere, Require is simpler: Preset.Pocket with { Require = Guarantee.GardenWorld } puts one in every galaxy (README, "Presets, options and addresses").

Export to a file

ToJson writes every field, the same text on every runtime, with no reflection:

using UniverseGeneration;

var system = Galaxy.Generate("my-seed").System(0);
var json = system.ToJson(indented: true);
File.WriteAllText("system.json", json);
foreach (var line in json.Split('\n').Take(6))
    Console.WriteLine(line.TrimEnd());

prints:

{
  "schema": "universe-generator/1",
  "type": "starSystem",
  "address": "v1-my-seed/galaxy/system/0",
  "name": "HD 147927",
  "star": {

Each export starts with schema and a type in camelCase, which a reader in another language can switch on:

using UniverseGeneration;

var universe = Universe.Generate("my-seed");
var cluster = universe.Cluster(0);
var galaxy = cluster.Galaxy(0);
var system = galaxy.System(0);
foreach (var json in new[] { universe.ToJson(), cluster.ToJson(), universe.Voids[0].ToJson(), galaxy.ToJson(), system.ToJson(), system.Planets[0].ToJson() })
    Console.WriteLine(json.Substring(0, json.IndexOf(",\"address\"")) + "}");
Console.WriteLine(system.Address);

prints:

{"schema":"universe-generator/1","type":"universe"}
{"schema":"universe-generator/1","type":"galaxyCluster"}
{"schema":"universe-generator/1","type":"cosmicVoid","index":0}
{"schema":"universe-generator/1","type":"galaxy"}
{"schema":"universe-generator/1","type":"starSystem"}
{"schema":"universe-generator/1","type":"planet"}
v1-my-seed/universe/cluster/0/galaxy/0/system/0

Lists that are generated when first read (a galaxy's systems, a cluster's galaxies) are written as null unless you pass children: true. A galaxy of 60 systems with children: true is about 900,000 characters (Same seed, same galaxy).

Your own system names

A hook runs on every system as it is made, wherever it is made, so a name list of your own survives Universe.At as long as you pass the same hooks:

using UniverseGeneration;

var names = new[] { "Avalon", "Brigid", "Caer Sidi" };
var hooks = new GeneratorHooks
{
    OnSystem = s => int.TryParse(s.Address.Split('/').Last(), out var i) && i < names.Length ? s with { Name = names[i] } : s,
};
var galaxy = Galaxy.Generate("my-seed", null, hooks);
foreach (var s in galaxy.Systems.Take(4))
    Console.WriteLine($"{s.Name}, first planet {s.Planets.FirstOrDefault()?.Name ?? "none"}");

prints:

Avalon, first planet HD 147927 e
Brigid, first planet HD 165595 b
Caer Sidi, first planet HD 28101 c
HD 157953, first planet HD 157953 d

The planets keep the catalogue name, since the hook changed only the system's Name; rename them in OnPlanet if they should follow. For built-in invented names instead of catalogue names, set Names = NameStyle.Invented. To attach your own data rather than replace a field, use Custom (README, "Hooks and custom fields").

An ocean-heavy temperate zone

The weight tables behind star classes, rocky planet kinds and giant planets' moons can be changed by id:

using UniverseGeneration;

Console.WriteLine(string.Join(", ", GeneratorTables.Ids));
var temperate = GeneratorTables.Default.Table("planet-kinds.temperate");
Console.WriteLine(string.Join(", ", temperate.Entries.Select(e => $"{e.Name} {e.Weight}")));

var wet = GeneratorTables.Default.With("wet-worlds", new WeightTable
{
    Id = "planet-kinds.temperate",
    Entries = temperate.Entries.Select(e => e.Name == "Ocean" ? e with { Weight = e.Weight * 4 } : e).ToArray(),
});
GeneratorTables.Register(wet);

int Oceans(GeneratorOptions o) => Galaxy.Generate("my-seed", o).Systems.SelectMany(s => s.Planets).Count(p => p.Kind == PlanetKind.Ocean);
Console.WriteLine($"{Oceans(Preset.Default)} ocean worlds, then {Oceans(Preset.Default with { Tables = "wet-worlds" })}");

prints:

star-classes.young, star-classes.mature, star-classes.old, star-classes.plausible, planet-kinds.hot, planet-kinds.warm, planet-kinds.temperate, planet-kinds.cold, moon-kinds.giant
Rocky 22, Ocean 25, Garden 22, Desert 15, Barren 10
6 ocean worlds, then 13

The tables' id goes into the option code (tables=wet-worlds), so a link carries it, but not the tables themselves: every program that reads the link must register the same tables under the same id first, or it is refused (errors). tables.ToJson() and GeneratorTables.FromJson(json) move a set between programs as a file.

Clone this wiki locally