fix(assets): address PR #119 review — 10 issues across planet-gen pipeline

1. Fix pclass.title() underscore bug in scaffold headings (216+ files)
2. Add 5 extended classes to batch.py valid_classes set
3. Add atmosphere rim colors for cold_arid/hot_arid/tropical/boreal/temperate_terminator
4. Add cloud/tilt ranges for extended classes (boreal 55-75%, tropical 60-80%)
5. Fix legend overflow at 1024px + deduplicate rainforest labels
6. Move _check_habitability() inside loop (was only checking last body)
7. Preserve gas_giant_ringed distinction in profile table
8. Drop meaningless terrain fields from gas giant frontmatter
9. Update stale docstrings/comments for 1024x512 default
10. Add infernal ring color

All lookup tables (CLASS_TILT, CLASS_CLOUD, CLASS_POLAR_ICE, CLASS_GEOTHERMAL,
CLASS_OBLATENESS, atmo_colors, tectonic_map, substrate_map) now include the
5 extended planet classes. Body content requires full regeneration.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-06 19:18:08 +02:00
co-authored by Claude Opus 4.6
parent 4f1bf0f279
commit 467c21a0f3
4 changed files with 122 additions and 78 deletions
+14 -11
View File
@@ -96,7 +96,7 @@ def _read_frontmatter(md_path: Path) -> dict:
# Validation (dry-run)
# ─────────────────────────────────────────────────────────────────────────────
REQUIRED_FIELDS = ["id", "planet_class", "seed", "star", "orbit", "physical", "terrain"]
REQUIRED_FIELDS = ["id", "planet_class", "seed", "star", "orbit", "physical"]
REQUIRED_STAR = ["type", "luminosity_solar"]
REQUIRED_ORBIT = ["distance_au", "period_days", "axial_tilt_deg"]
REQUIRED_PHYSICAL = ["gravity_g", "atmosphere"]
@@ -130,25 +130,28 @@ def _validate_body_def(bd: dict, body_dir: Path) -> list:
if f not in phys:
errors.append(f"{bid}: missing physical.{f}")
terrain = bd.get("terrain", {})
for f in REQUIRED_TERRAIN:
if f not in terrain:
errors.append(f"{bid}: missing terrain.{f}")
lf = terrain.get("land_fraction")
if lf is not None and (lf < 0 or lf > 1):
errors.append(f"{bid}: terrain.land_fraction = {lf} — must be [0, 1]")
seed = bd.get("seed")
if seed is None or not isinstance(seed, int):
errors.append(f"{bid}: seed must be an integer, got {seed!r}")
pclass = bd.get("planet_class", "")
valid_classes = {"temperate", "oceanic", "forest", "arid", "frozen",
"volcanic", "barren", "gas_giant", "gas_giant_ringed"}
"volcanic", "barren", "gas_giant", "gas_giant_ringed",
"cold_arid", "hot_arid", "tropical", "boreal",
"temperate_terminator"}
if pclass not in valid_classes:
errors.append(f"{bid}: planet_class '{pclass}' not in {valid_classes}")
is_gas = pclass in ("gas_giant", "gas_giant_ringed")
terrain = bd.get("terrain", {})
if not is_gas:
for f in REQUIRED_TERRAIN:
if f not in terrain:
errors.append(f"{bid}: missing terrain.{f}")
lf = terrain.get("land_fraction")
if lf is not None and (lf < 0 or lf > 1):
errors.append(f"{bid}: terrain.land_fraction = {lf} — must be [0, 1]")
return errors
+92 -52
View File
@@ -142,47 +142,67 @@ PLANET_CLASS_MAP = {
# planet_class → axial tilt range [min, max] degrees
# Tidal locking check overrides this for short-period bodies
CLASS_TILT = {
"temperate": (10, 35),
"oceanic": (5, 25),
"forest": (10, 40),
"arid": (5, 30),
"frozen": (15, 60), # high tilt → seasonal extremes → frozen
"volcanic": (2, 20),
"barren": (0, 45),
"temperate": (10, 35),
"temperate_terminator": (0, 5), # tidally locked by definition
"oceanic": (5, 25),
"forest": (10, 40),
"arid": (5, 30),
"cold_arid": (10, 40),
"hot_arid": (2, 15),
"tropical": (5, 15),
"boreal": (20, 50),
"frozen": (15, 60), # high tilt → seasonal extremes → frozen
"volcanic": (2, 20),
"barren": (0, 45),
}
# planet_class → geothermal flux
CLASS_GEOTHERMAL = {
"volcanic": "extreme",
"temperate": "low",
"oceanic": "low",
"forest": "low",
"arid": "low",
"frozen": "low",
"barren": "low",
"volcanic": "extreme",
"temperate": "low",
"temperate_terminator": "low",
"oceanic": "low",
"forest": "low",
"arid": "low",
"cold_arid": "low",
"hot_arid": "low",
"tropical": "low",
"boreal": "low",
"frozen": "low",
"barren": "low",
}
# planet_class → polar ice latitude (fraction of 01, where 1 = poles)
# Lower = ice caps extend further toward equator
CLASS_POLAR_ICE = {
"temperate": (0.72, 0.85),
"oceanic": (0.80, 0.92),
"forest": (0.75, 0.88),
"arid": (0.90, 0.99),
"frozen": (0.10, 0.40),
"volcanic": (0.95, 1.00),
"barren": (0.92, 1.00),
"temperate": (0.72, 0.85),
"temperate_terminator": (0.75, 0.90),
"oceanic": (0.80, 0.92),
"forest": (0.75, 0.88),
"arid": (0.90, 0.99),
"cold_arid": (0.50, 0.70),
"hot_arid": (0.95, 1.00),
"tropical": (0.88, 0.96),
"boreal": (0.45, 0.65),
"frozen": (0.10, 0.40),
"volcanic": (0.95, 1.00),
"barren": (0.92, 1.00),
}
# planet_class → oblateness range
CLASS_OBLATENESS = {
"temperate": (0.001, 0.005),
"oceanic": (0.001, 0.004),
"forest": (0.001, 0.005),
"arid": (0.001, 0.004),
"frozen": (0.001, 0.003),
"volcanic": (0.002, 0.008),
"barren": (0.000, 0.003),
"temperate": (0.001, 0.005),
"temperate_terminator": (0.001, 0.004),
"oceanic": (0.001, 0.004),
"forest": (0.001, 0.005),
"arid": (0.001, 0.004),
"cold_arid": (0.001, 0.004),
"hot_arid": (0.001, 0.004),
"tropical": (0.001, 0.005),
"boreal": (0.001, 0.004),
"frozen": (0.001, 0.003),
"volcanic": (0.002, 0.008),
"barren": (0.000, 0.003),
}
# Gas giant band palettes available
@@ -190,13 +210,18 @@ from biome_config import GAS_PALETTE_SELECTION as GAS_PALETTES
# planet_class → cloud coverage base range
CLASS_CLOUD = {
"temperate": (0.35, 0.55),
"oceanic": (0.55, 0.75),
"forest": (0.40, 0.60),
"arid": (0.05, 0.20),
"frozen": (0.20, 0.45),
"volcanic": (0.60, 0.85),
"barren": (0.00, 0.05),
"temperate": (0.35, 0.55),
"temperate_terminator": (0.30, 0.50),
"oceanic": (0.55, 0.75),
"forest": (0.40, 0.60),
"arid": (0.05, 0.20),
"cold_arid": (0.10, 0.25),
"hot_arid": (0.02, 0.10),
"tropical": (0.60, 0.80),
"boreal": (0.55, 0.75),
"frozen": (0.20, 0.45),
"volcanic": (0.60, 0.85),
"barren": (0.00, 0.05),
}
# Atmosphere classes that allow clouds
@@ -219,6 +244,7 @@ RING_COLOURS = {
"saturnian": [0.88, 0.78, 0.55], # warm golden
"icy": [0.85, 0.90, 0.95], # pale ice
"sulfuric": [0.75, 0.70, 0.30], # sulphur-tinted
"infernal": [0.60, 0.25, 0.15], # dark ember
}
# ---------------------------------------------------------------------------
@@ -487,13 +513,18 @@ def _build_body_def(
# ── Atmosphere colour — from star type + planet class ─────────────────
atmo_colors = {
"temperate": [0.45, 0.65, 1.00],
"oceanic": [0.40, 0.60, 1.00],
"forest": [0.42, 0.68, 0.80],
"arid": [0.90, 0.72, 0.50],
"frozen": [0.75, 0.88, 1.00],
"volcanic": [0.55, 0.40, 0.30],
"barren": None,
"temperate": [0.45, 0.65, 1.00],
"temperate_terminator": [0.45, 0.65, 1.00],
"oceanic": [0.40, 0.60, 1.00],
"forest": [0.42, 0.68, 0.80],
"arid": [0.90, 0.72, 0.50],
"cold_arid": [0.82, 0.58, 0.40],
"hot_arid": [0.90, 0.72, 0.50],
"tropical": [0.42, 0.68, 0.80],
"boreal": [0.45, 0.65, 1.00],
"frozen": [0.75, 0.88, 1.00],
"volcanic": [0.55, 0.40, 0.30],
"barren": None,
}
atmo_color = atmo_colors.get(planet_class)
if atmo_density == "none":
@@ -523,8 +554,11 @@ def _build_body_def(
# ── Tectonics ─────────────────────────────────────────────────────────
tectonic_map = {
"volcanic": "extreme", "temperate": "active",
"temperate_terminator": "active",
"oceanic": "active", "forest": "active",
"arid": "low", "frozen": "low", "barren": "none",
"arid": "low", "cold_arid": "low",
"hot_arid": "low", "tropical": "active",
"boreal": "active", "frozen": "low", "barren": "none",
}
tectonics = tectonic_map.get(planet_class, "low")
tectonics = ov.get("tectonics", tectonics)
@@ -548,13 +582,18 @@ def _build_body_def(
# ── Substrate ─────────────────────────────────────────────────────────
substrate_map = {
"volcanic": "sulfuric",
"arid": "silicate",
"frozen": "ice",
"barren": "silicate",
"temperate":"silicate",
"oceanic": "silicate",
"forest": "silicate",
"volcanic": "sulfuric",
"arid": "silicate",
"cold_arid": "silicate",
"hot_arid": "silicate",
"tropical": "silicate",
"boreal": "silicate",
"frozen": "ice",
"barren": "silicate",
"temperate": "silicate",
"temperate_terminator": "silicate",
"oceanic": "silicate",
"forest": "silicate",
}
substrate = substrate_map.get(planet_class, "silicate")
if hydro == "subsurface" and planet_class == "frozen":
@@ -695,8 +734,9 @@ def _build_body_def(
"render": render_cfg,
}
# Gas giant extras
# Gas giants: drop meaningless terrain fields
if gas_giant_cfg:
body_def.pop("terrain", None)
body_def["gas_giant"] = gas_giant_cfg
if rings_cfg:
body_def["rings"] = rings_cfg
@@ -776,6 +816,7 @@ def parse_system(
log.info(f" {bid:20s} {body_def['planet_class']:20s} "
f"scale={body_def['body_scale']:6s} "
f"seed={body_def['seed']}")
_check_habitability(body_def)
# Write output files
if out_dir:
@@ -786,7 +827,6 @@ def parse_system(
json.dump(bd, f, indent=2)
log.info(f"Wrote {len(body_defs)} body definitions → {out_dir}/")
_check_habitability(body_def)
return body_defs
+12 -11
View File
@@ -1,7 +1,7 @@
"""
render_heightmap.py
-------------------
Renders a 4096×2048 annotated equirectangular heightmap PNG from a terrain dict.
Renders an annotated equirectangular heightmap PNG from a terrain dict.
This is the PRIMARY output of the planet generator pipeline.
The globe render is a separate downstream step that reads the same terrain dict.
@@ -54,7 +54,7 @@ from biome_config import (
OUT_W = 1024
OUT_H = 512
UI_SCALE = OUT_W / 1024 # 4.0 — all pixel sizes scale with this
UI_SCALE = OUT_W / 1024 # 1.0 at default 1024 — all pixel sizes scale with this
# ---------------------------------------------------------------------------
# Biome colour palette
@@ -327,9 +327,9 @@ def _biome_legend_items(terrain: dict) -> list:
LABELS = {
0: "ocean deep", 1: "ocean", 2: "coastal water",
3: "coast", 5: "rainforest", 6: "trop. forest",
3: "coast", 5: "trop. rainforest", 6: "trop. forest",
7: "savanna", 8: "grassland", 9: "forest",
10: "rainforest", 11: "boreal", 12: "shrubland",
10: "temp. rainforest", 11: "boreal", 12: "shrubland",
13: "temperate desert", 14: "desert", 15: "hot desert",
16: "tundra", 17: "ice / snow", 18: "mountain rock",
19: "lava field", 20: "chemosyn. mat", 21: "thermophilic",
@@ -365,12 +365,13 @@ def _render_legend(img: Image.Image, terrain: dict) -> Image.Image:
leg_y = OUT_H - int(34 * UI_SCALE)
font = _load_font(int(10 * UI_SCALE))
gap = int(6 * UI_SCALE)
step = int(108 * UI_SCALE)
# Scale step to fit all items within image width
max_items = len(items)
max_step = (OUT_W - 2 * pad_x) // max(max_items, 1)
step = min(int(108 * UI_SCALE), max_step)
lx = pad_x
for label, rgb in items:
if lx + step > OUT_W - pad_x:
break
draw.rectangle([(lx, leg_y), (lx + sw, leg_y + sh)], fill=rgb)
draw.text((lx + sw + gap, leg_y), label,
fill=(185, 192, 205), font=font)
@@ -452,15 +453,15 @@ if __name__ == "__main__":
import sys, json, time
if len(sys.argv) < 2:
print("Usage: python3 render_heightmap.py body_def.json [--small]")
print("Usage: python3 render_heightmap.py body_def.json [--large]")
sys.exit(1)
with open(sys.argv[1]) as f:
bd = json.load(f)
# --small flag renders at 1024×512 for fast iteration
small = "--small" in sys.argv
w, h = (1024, 512) if small else (OUT_W, OUT_H)
# --large flag renders at 4096×2048 for high-res review
large = "--large" in sys.argv
w, h = (4096, 2048) if large else (OUT_W, OUT_H)
from planet_simulation import simulate
+4 -4
View File
@@ -61,7 +61,7 @@ def _body_prose(bd: dict, system_dir: Path) -> str:
"""Generate markdown content below the frontmatter."""
name = bd.get("name") or bd.get("id")
bid = bd["id"]
pclass = bd.get("planet_class", "unknown").replace("_ringed", "")
pclass = bd.get("planet_class", "unknown")
btype = bd.get("body_type", "planet")
wiki = bd.get("wiki", {})
phys = bd.get("physical", {})
@@ -76,11 +76,11 @@ def _body_prose(bd: dict, system_dir: Path) -> str:
# Type line
if btype == "moon":
lines.append(f"{pclass.title()} moon.")
lines.append(f"{pclass.replace('_', ' ').title()} moon.")
elif pclass in ("gas_giant", "gas_giant_ringed"):
lines.append(f"Gas giant.")
else:
lines.append(f"{pclass.title()} {btype}.")
lines.append(f"{pclass.replace('_', ' ').title()} {btype}.")
lines.append("")
# System link
@@ -92,7 +92,7 @@ def _body_prose(bd: dict, system_dir: Path) -> str:
lines.append("")
lines.append(f"![Globe](globe.png)")
lines.append("")
if pclass not in ("gas_giant",):
if pclass not in ("gas_giant", "gas_giant_ringed"):
lines.append(f"![Heightmap](heightmap.png)")
lines.append("")