feat(docs): assign GJ catalog IDs to all 300 star systems
All systems use Gliese-Jahreiss (GJ) designations as primary catalog. 35 real nearby stars assigned by proximity to Gateway; 265 fabricated across GJ sub-catalogs (main, southern, supplement, post-settlement). Notable assignments: - Gateway (S-001): GJ 71 / Tau Ceti - Alpha Centauri A/B (S-090): GJ 559A, binary junction at hop 3 - Delta Pavonis (S-265): GJ 780, core dead_end at hop 2 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
+901
-301
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,323 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
assign-astro-ids.py
|
||||
Assigns astronomical_id and proper_name fields to all 300 systems in star-map.json.
|
||||
|
||||
All systems use Gliese-Jahreiss (GJ) catalog designations as primary identifier.
|
||||
The GJ catalog is the catalog of nearby stars — a natural fit for the Settled Reach.
|
||||
|
||||
Logic:
|
||||
- 36 real stars assigned by proximity/topology matching (closest to Gateway first)
|
||||
- ~264 remaining systems get fabricated GJ numbers
|
||||
- All assignments are deterministic (seeded RNG from system_id hash)
|
||||
- Gateway (S-001) gets GJ 71 (Tau Ceti)
|
||||
"""
|
||||
|
||||
import json
|
||||
import hashlib
|
||||
import random
|
||||
from pathlib import Path
|
||||
|
||||
STAR_MAP_PATH = Path(__file__).parent.parent / "docs/design/star-map.json"
|
||||
|
||||
# ─── Real star pool ────────────────────────────────────────────────────────────
|
||||
# All nearby stars have GJ designations. Fields: (gj_id, proper_name, star_class, distance_ly, notes)
|
||||
REAL_STARS = [
|
||||
("GJ 551", "Proxima Centauri", "M", 4.2, None),
|
||||
("GJ 559A", "Alpha Centauri A", "G", 4.4, None),
|
||||
("GJ 559B", "Alpha Centauri B", "K", 4.4, "binary_companion"),
|
||||
("GJ 699", "Barnard's Star", "M", 5.9, None),
|
||||
("GJ 244", "Sirius", "A", 8.6, "too_hot"),
|
||||
("GJ 411", "Lalande 21185", "M", 8.3, None),
|
||||
("GJ 144", "Epsilon Eridani", "K", 10.5, None),
|
||||
("GJ 447", "Ross 128", "M", 10.9, None),
|
||||
("GJ 887", "Lacaille 9352", "M", 10.7, None),
|
||||
("GJ 280", "Procyon", "F", 11.4, None),
|
||||
("GJ 820", "61 Cygni", "K", 11.4, None),
|
||||
("GJ 845", "Epsilon Indi", "K", 11.8, None),
|
||||
("GJ 71", "Tau Ceti", "G", 11.9, "gateway"),
|
||||
("GJ 380", "Groombridge 1618", "K", 15.9, None),
|
||||
("GJ 702", "70 Ophiuchi", "K", 16.6, None),
|
||||
("GJ 166", "Omicron2 Eridani", "K", 16.4, None),
|
||||
("GJ 768", "Altair", "A", 16.8, "too_hot"),
|
||||
("GJ 764", "Sigma Draconis", "G", 18.8, None),
|
||||
("GJ 570", "Gliese 570", "K", 19.2, None),
|
||||
("GJ 139", "82 Eridani", "G", 19.8, None),
|
||||
("GJ 780", "Delta Pavonis", "G", 19.9, None),
|
||||
("GJ 667C", "Gliese 667 C", "M", 22.7, None),
|
||||
("GJ 68", "107 Piscium", "K", 24.4, None),
|
||||
("GJ 19", "Beta Hydri", "G", 24.4, None),
|
||||
("GJ 34", "Eta Cassiopeiae", "G", 24.6, None),
|
||||
("GJ 53", "Mu Cassiopeiae", "G", 24.6, None),
|
||||
("GJ 881", "Fomalhaut", "A", 25.1, "too_hot"),
|
||||
("GJ 721", "Vega", "A", 25.3, "too_hot"),
|
||||
("GJ 178", "Pi3 Orionis", "F", 26.2, None),
|
||||
("GJ 475", "Chara", "G", 27.4, None),
|
||||
("GJ 506", "61 Virginis", "G", 27.8, None),
|
||||
("GJ 250", "Gliese 250", "K", 28.4, None),
|
||||
("GJ 183", "HR 1614", "K", 28.4, None),
|
||||
("GJ 137", "Kappa1 Ceti", "G", 29.8, None),
|
||||
("GJ 451", "Groombridge 1830", "G", 29.9, None),
|
||||
("GJ 2046", "HD 40307", "K", 42.0, None),
|
||||
]
|
||||
|
||||
# Collect all reserved GJ numbers (as strings for consistent comparison)
|
||||
RESERVED_GJ = set()
|
||||
for cat_id, _, _, _, _ in REAL_STARS:
|
||||
RESERVED_GJ.add(cat_id)
|
||||
|
||||
|
||||
def system_rng(system_id: str, salt: str = "") -> random.Random:
|
||||
"""Return a seeded RNG deterministic for this system_id + salt."""
|
||||
h = hashlib.sha256(f"{system_id}:{salt}".encode()).digest()
|
||||
seed = int.from_bytes(h[:8], "big")
|
||||
return random.Random(seed)
|
||||
|
||||
|
||||
def fabricate_gj(system_id: str, used: set[str]) -> str:
|
||||
"""
|
||||
Fabricate a plausible GJ catalog designation.
|
||||
|
||||
Uses the full range of GJ numbering styles for authenticity:
|
||||
- GJ 1-999: main catalog (sparse — most taken by real stars)
|
||||
- GJ 1001-1299: southern extension
|
||||
- GJ 2001-2159: supplement (Gliese & Jahreiss 1979)
|
||||
- GJ 3001-3999: supplement (CNS3, faint nearby stars)
|
||||
- GJ 4001-4383: supplement (CNS4)
|
||||
- GJ 5001+: our fabrication range for "post-settlement survey"
|
||||
|
||||
We weight toward the supplementary catalogs (3xxx, 4xxx, 5xxx) since
|
||||
the main catalog numbers are more likely to collide with real stars.
|
||||
"""
|
||||
rng = system_rng(system_id, "GJ_fab")
|
||||
|
||||
# Weighted range selection:
|
||||
# 10% main (500-999), 10% southern (1001-1299), 10% supplement2 (2001-2159),
|
||||
# 30% supplement3 (3001-3999), 15% supplement4 (4001-4383), 25% post-settlement (5001-6500)
|
||||
ranges = [
|
||||
(500, 999, 0.10),
|
||||
(1001, 1299, 0.10),
|
||||
(2001, 2159, 0.10),
|
||||
(3001, 3999, 0.30),
|
||||
(4001, 4383, 0.15),
|
||||
(5001, 6500, 0.25),
|
||||
]
|
||||
|
||||
for _ in range(2000):
|
||||
# Pick a range by weight
|
||||
roll = rng.random()
|
||||
cumulative = 0.0
|
||||
lo, hi = 3001, 3999 # default
|
||||
for r_lo, r_hi, weight in ranges:
|
||||
cumulative += weight
|
||||
if roll < cumulative:
|
||||
lo, hi = r_lo, r_hi
|
||||
break
|
||||
|
||||
n = rng.randint(lo, hi)
|
||||
candidate = f"GJ {n}"
|
||||
if candidate not in RESERVED_GJ and candidate not in used:
|
||||
used.add(candidate)
|
||||
return candidate
|
||||
|
||||
# Fallback: scan upward in post-settlement range
|
||||
n = 5001
|
||||
while f"GJ {n}" in RESERVED_GJ or f"GJ {n}" in used:
|
||||
n += 1
|
||||
candidate = f"GJ {n}"
|
||||
used.add(candidate)
|
||||
return candidate
|
||||
|
||||
|
||||
def topology_rank(node: dict) -> int:
|
||||
"""Higher = more notable for real-star assignment priority."""
|
||||
scores = {"hub": 5, "junction": 4, "loop_member": 3, "through_route": 2, "spur_end": 1, "dead_end": 0}
|
||||
return scores.get(node.get("gate_topology", ""), 0)
|
||||
|
||||
|
||||
def main():
|
||||
with open(STAR_MAP_PATH, "r") as f:
|
||||
data = json.load(f)
|
||||
|
||||
nodes = data["nodes"]
|
||||
|
||||
# ─── Step 1: Sort systems for real-star matching ──────────────────────────
|
||||
sortable = sorted(nodes, key=lambda n: (
|
||||
n.get("hop_distance_from_gateway", 999),
|
||||
-topology_rank(n),
|
||||
n["system_id"]
|
||||
))
|
||||
|
||||
# ─── Step 2: Pin Gateway to Tau Ceti (GJ 71) ─────────────────────────────
|
||||
gateway_star = next(s for s in REAL_STARS if s[4] == "gateway")
|
||||
assigned_catalog_ids: set[str] = {gateway_star[0]}
|
||||
assignments: dict[str, tuple[str, str | None]] = {}
|
||||
assignments["S-001"] = (gateway_star[0], gateway_star[1])
|
||||
|
||||
# ─── Step 3: Separate hot A-type stars for special handling ───────────────
|
||||
hot_stars = [s for s in REAL_STARS if s[4] == "too_hot"]
|
||||
normal_stars = [s for s in REAL_STARS if s[4] not in ("gateway", "too_hot", "binary_companion")]
|
||||
|
||||
# Sort normal stars by distance from Sol
|
||||
normal_stars_sorted = sorted(normal_stars, key=lambda s: s[3])
|
||||
|
||||
# ─── Step 4: Handle Alpha Centauri binary ─────────────────────────────────
|
||||
alpha_a = next(s for s in REAL_STARS if s[0] == "GJ 559A")
|
||||
alpha_b = next(s for s in REAL_STARS if s[0] == "GJ 559B")
|
||||
|
||||
# Find a binary-type system within hop 5 for Alpha Centauri
|
||||
binary_systems = [n for n in sortable if n.get("star_type") == "binary"]
|
||||
binary_assigned = False
|
||||
for bs in binary_systems:
|
||||
if bs["system_id"] == "S-001":
|
||||
continue
|
||||
if bs.get("hop_distance_from_gateway", 999) <= 5:
|
||||
assignments[bs["system_id"]] = (alpha_a[0], alpha_a[1])
|
||||
assigned_catalog_ids.add(alpha_a[0])
|
||||
assigned_catalog_ids.add(alpha_b[0]) # mark companion as used
|
||||
binary_assigned = True
|
||||
print(f" Binary: {bs['system_id']} -> {alpha_a[0]} (Alpha Centauri A/B)")
|
||||
break
|
||||
|
||||
if not binary_assigned:
|
||||
# No binary found; Alpha A stays in normal pool, skip B
|
||||
assigned_catalog_ids.add(alpha_b[0])
|
||||
print(" No binary system within hop 5; Alpha Centauri B skipped.")
|
||||
|
||||
# Remove already-assigned stars from normal pool
|
||||
normal_pool = [s for s in normal_stars_sorted if s[0] not in assigned_catalog_ids]
|
||||
|
||||
# ─── Step 5: Assign normal real stars by proximity ────────────────────────
|
||||
pool_idx = 0
|
||||
for node in sortable:
|
||||
if pool_idx >= len(normal_pool):
|
||||
break
|
||||
sid = node["system_id"]
|
||||
if sid in assignments:
|
||||
continue
|
||||
|
||||
star = normal_pool[pool_idx]
|
||||
assignments[sid] = (star[0], star[1])
|
||||
assigned_catalog_ids.add(star[0])
|
||||
pool_idx += 1
|
||||
|
||||
# ─── Step 6: Assign hot A-type stars to notable hubs/junctions ────────────
|
||||
hub_junction_candidates = sorted(
|
||||
[n for n in nodes if n["system_id"] not in assignments
|
||||
and n.get("gate_topology") in ("hub", "junction")],
|
||||
key=lambda n: (-topology_rank(n), n.get("hop_distance_from_gateway", 999))
|
||||
)
|
||||
|
||||
for i, hot_star in enumerate(hot_stars):
|
||||
if i < len(hub_junction_candidates):
|
||||
sid = hub_junction_candidates[i]["system_id"]
|
||||
else:
|
||||
# Fallback: any unassigned system
|
||||
sid = next(n["system_id"] for n in sortable if n["system_id"] not in assignments)
|
||||
assignments[sid] = (hot_star[0], hot_star[1])
|
||||
assigned_catalog_ids.add(hot_star[0])
|
||||
|
||||
real_count = len(assignments)
|
||||
print(f"\nReal star assignments: {real_count}")
|
||||
|
||||
# ─── Step 7: Fabricate GJ IDs for remaining systems ───────────────────────
|
||||
used_gj: set[str] = set(RESERVED_GJ) # protect all real GJ numbers
|
||||
|
||||
fabricated_count = 0
|
||||
for node in nodes:
|
||||
sid = node["system_id"]
|
||||
if sid not in assignments:
|
||||
fab_id = fabricate_gj(sid, used_gj)
|
||||
assignments[sid] = (fab_id, None)
|
||||
fabricated_count += 1
|
||||
|
||||
# ─── Step 8: Apply to nodes ───────────────────────────────────────────────
|
||||
for node in nodes:
|
||||
sid = node["system_id"]
|
||||
cat_id, proper = assignments[sid]
|
||||
node["astronomical_id"] = cat_id
|
||||
node["proper_name"] = proper
|
||||
|
||||
# ─── Step 9: Write back ───────────────────────────────────────────────────
|
||||
with open(STAR_MAP_PATH, "w") as f:
|
||||
json.dump(data, f, indent=2)
|
||||
f.write("\n")
|
||||
|
||||
# ─── Step 10: Summary report ──────────────────────────────────────────────
|
||||
print(f"\n{'='*60}")
|
||||
print("ASTRONOMICAL ID ASSIGNMENT COMPLETE")
|
||||
print(f"{'='*60}")
|
||||
print(f"Total systems: {len(nodes)}")
|
||||
print(f"Real star assigned: {real_count}")
|
||||
print(f"Fabricated: {fabricated_count}")
|
||||
|
||||
# GJ range breakdown
|
||||
range_counts = {"main (1-999)": 0, "southern (1001-1299)": 0, "supp2 (2001-2159)": 0,
|
||||
"supp3 (3001-3999)": 0, "supp4 (4001-4383)": 0, "post-settlement (5001+)": 0, "special": 0}
|
||||
for node in nodes:
|
||||
aid = node["astronomical_id"]
|
||||
if not aid.startswith("GJ "):
|
||||
range_counts["special"] += 1
|
||||
continue
|
||||
# Parse the number
|
||||
num_str = aid[3:].rstrip("ABC")
|
||||
try:
|
||||
num = int(num_str)
|
||||
except ValueError:
|
||||
range_counts["special"] += 1
|
||||
continue
|
||||
if num <= 999:
|
||||
range_counts["main (1-999)"] += 1
|
||||
elif num <= 1299:
|
||||
range_counts["southern (1001-1299)"] += 1
|
||||
elif num <= 2159:
|
||||
range_counts["supp2 (2001-2159)"] += 1
|
||||
elif num <= 3999:
|
||||
range_counts["supp3 (3001-3999)"] += 1
|
||||
elif num <= 4383:
|
||||
range_counts["supp4 (4001-4383)"] += 1
|
||||
else:
|
||||
range_counts["post-settlement (5001+)"] += 1
|
||||
|
||||
print("\nGJ range distribution:")
|
||||
for label, count in range_counts.items():
|
||||
if count > 0:
|
||||
pct = count / len(nodes) * 100
|
||||
print(f" {label:28s} {count:3d} ({pct:.1f}%)")
|
||||
|
||||
# Real star assignment table
|
||||
print(f"\nReal star assignments by hop distance:")
|
||||
print(f" {'System':8s} {'Hop':4s} {'Topology':14s} {'Type':6s} {'GJ ID':10s} {'Proper Name'}")
|
||||
print(f" {'─'*8} {'─'*4} {'─'*14} {'─'*6} {'─'*10} {'─'*25}")
|
||||
|
||||
real_sids = {sid for sid, (_, name) in assignments.items() if name is not None}
|
||||
node_by_id = {n["system_id"]: n for n in nodes}
|
||||
|
||||
for node in sorted(
|
||||
[node_by_id[sid] for sid in real_sids],
|
||||
key=lambda n: (n.get("hop_distance_from_gateway", 999), n["system_id"])
|
||||
):
|
||||
sid = node["system_id"]
|
||||
hop = node.get("hop_distance_from_gateway", "?")
|
||||
topo = node.get("gate_topology", "?")
|
||||
stype = node.get("star_type", "?")
|
||||
cat = node["astronomical_id"]
|
||||
name = node["proper_name"]
|
||||
print(f" {sid:8s} {str(hop):4s} {topo:14s} {stype:6s} {cat:10s} {name}")
|
||||
|
||||
# Verify no duplicates
|
||||
all_astro_ids = [n["astronomical_id"] for n in nodes]
|
||||
unique_ids = set(all_astro_ids)
|
||||
if len(unique_ids) != len(all_astro_ids):
|
||||
dupes = [aid for aid in all_astro_ids if all_astro_ids.count(aid) > 1]
|
||||
print(f"\nWARNING: {len(all_astro_ids) - len(unique_ids)} duplicate astronomical_id(s)!")
|
||||
for d in sorted(set(dupes)):
|
||||
print(f" DUPE: {d}")
|
||||
else:
|
||||
print(f"\nCollision check: PASSED — all {len(unique_ids)} astronomical_ids unique.")
|
||||
|
||||
print(f"\nWritten to: {STAR_MAP_PATH}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user