Files
TheriapolisV3/Theriapolis.Tools/Commands/SettlementReport.cs
T
Christopher Wiebe b451f83174 Initial commit: Theriapolis baseline at port/godot branch point
Captures the pre-Godot-port state of the codebase. This is the rollback
anchor for the Godot port (M0 of theriapolis-rpg-implementation-plan-godot-port.md).
All Phase 0 through Phase 6.5 work is included; Phase 7 is in flight.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-30 20:40:51 -07:00

167 lines
8.7 KiB
C#

using Theriapolis.Core;
using Theriapolis.Core.World;
using Theriapolis.Core.World.Generation;
namespace Theriapolis.Tools.Commands;
/// <summary>
/// settlement-report --seed &lt;n&gt; [--data-dir &lt;dir&gt;]
///
/// Runs the full pipeline and prints a human-readable settlement report:
/// narrative anchors, tier breakdown, economy distribution, and PoI list.
/// </summary>
public static class SettlementReport
{
public static int Run(string[] args)
{
ulong seed = 12345;
string dataDir = ResolveDataDir();
for (int i = 0; i < args.Length; i++)
{
switch (args[i].ToLowerInvariant())
{
case "--seed":
if (i + 1 < args.Length)
{
string raw = args[++i];
if (raw.StartsWith("0x", StringComparison.OrdinalIgnoreCase))
seed = Convert.ToUInt64(raw[2..], 16);
else
seed = ulong.Parse(raw);
}
break;
case "--data-dir":
if (i + 1 < args.Length) dataDir = args[++i];
break;
}
}
Console.WriteLine($"[settlement-report] seed=0x{seed:X} data-dir={dataDir}");
if (!Directory.Exists(dataDir))
{
Console.Error.WriteLine($"Data directory not found: {dataDir}");
return 1;
}
var ctx = new WorldGenContext(seed, dataDir)
{
ProgressCallback = (name, _) => Console.Write($"\r Running {name,-28} "),
Log = _ => { }, // suppress detailed logs
};
WorldGenerator.RunAll(ctx);
Console.WriteLine("\r ");
var world = ctx.World;
var ss = world.Settlements;
if (ss.Count == 0)
{
Console.WriteLine("No settlements generated.");
return 0;
}
// ── Narrative anchors ─────────────────────────────────────────────────
Console.WriteLine("═══════════════════════════════════════════════════════");
Console.WriteLine(" NARRATIVE ANCHORS");
Console.WriteLine("═══════════════════════════════════════════════════════");
foreach (var s in ss.Where(s => s.Anchor.HasValue).OrderBy(s => s.Anchor))
{
Console.WriteLine($" [{s.Anchor}]");
Console.WriteLine($" Name : {s.Name}");
Console.WriteLine($" Tier : {s.Tier}");
Console.WriteLine($" Position : ({s.TileX}, {s.TileY})");
Console.WriteLine($" Economy : {s.Economy}");
Console.WriteLine($" Wealth : {s.WealthLevel:F3}");
Console.WriteLine($" Pop : ~{s.Population}");
Console.WriteLine($" River : {s.IsOnRiver}");
Console.WriteLine($" Rail : {s.HasRailStation}");
Console.WriteLine();
}
// ── Tier breakdown ────────────────────────────────────────────────────
Console.WriteLine("═══════════════════════════════════════════════════════");
Console.WriteLine(" SETTLEMENTS BY TIER");
Console.WriteLine("═══════════════════════════════════════════════════════");
for (int tier = 1; tier <= 4; tier++)
{
var ts = ss.Where(s => s.Tier == tier && !s.IsPoi).ToList();
if (ts.Count == 0) continue;
Console.WriteLine($" Tier {tier} ({ts.Count}):");
foreach (var s in ts.OrderBy(s => s.Name))
{
string anchor = s.Anchor.HasValue ? $" [{s.Anchor}]" : "";
Console.WriteLine($" ({s.TileX,4},{s.TileY,4}) {s.Name,-24}{anchor}");
Console.WriteLine($" Economy={s.Economy,-14} Gov={s.Governance,-16} Wealth={s.WealthLevel:F2}");
}
}
Console.WriteLine();
// ── Economy distribution ──────────────────────────────────────────────
Console.WriteLine("═══════════════════════════════════════════════════════");
Console.WriteLine(" ECONOMY DISTRIBUTION (Tier 1-4)");
Console.WriteLine("═══════════════════════════════════════════════════════");
var econGroups = ss.Where(s => !s.IsPoi)
.GroupBy(s => s.Economy)
.OrderByDescending(g => g.Count());
foreach (var g in econGroups)
Console.WriteLine($" {g.Key,-18}: {g.Count(),3}");
Console.WriteLine();
// ── PoI list ──────────────────────────────────────────────────────────
var pois = ss.Where(s => s.IsPoi).ToList();
if (pois.Count > 0)
{
Console.WriteLine("═══════════════════════════════════════════════════════");
Console.WriteLine($" POINTS OF INTEREST ({pois.Count})");
Console.WriteLine("═══════════════════════════════════════════════════════");
foreach (var p in pois.OrderBy(p => p.PoiType).ThenBy(p => p.TileX))
Console.WriteLine($" ({p.TileX,4},{p.TileY,4}) {p.Name,-24} [{p.PoiType}]");
Console.WriteLine();
}
// ── Linear features ────────────────────────────────────────────────────
Console.WriteLine("═══════════════════════════════════════════════════════");
Console.WriteLine(" LINEAR FEATURES");
Console.WriteLine("═══════════════════════════════════════════════════════");
Console.WriteLine($" Rivers : {world.Rivers.Count}");
Console.WriteLine($" Roads : {world.Roads.Count}");
Console.WriteLine($" Rails : {world.Rails.Count}");
// ── Validation summary ─────────────────────────────────────────────────
if (world.StageHashes.TryGetValue("ValidationPass", out ulong vhash))
{
int violations = (int)(vhash / 1000);
int warnings = (int)(vhash % 1000);
Console.WriteLine();
Console.WriteLine("═══════════════════════════════════════════════════════");
Console.WriteLine(" VALIDATION");
Console.WriteLine("═══════════════════════════════════════════════════════");
Console.WriteLine($" Violations : {violations}");
Console.WriteLine($" Warnings : {warnings}");
}
return 0;
}
private static string ResolveDataDir()
{
string local = Path.Combine(AppContext.BaseDirectory, "Data");
if (Directory.Exists(local)) return local;
string? dir = AppContext.BaseDirectory.TrimEnd(
Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
for (int i = 0; i < 6; i++)
{
if (dir is null) break;
string candidate = Path.Combine(dir, "Content", "Data");
if (Directory.Exists(candidate)) return candidate;
dir = Path.GetDirectoryName(dir);
}
return local;
}
}