Add underground village, branched rails and upper terrain for alpha.30.6
Build Sanctuary / build (push) Canceled after 0s

This commit is contained in:
koka
2026-09-12 19:04:57 +02:00
parent 16e480bfd0
commit fbd870a4e3
38 changed files with 1180 additions and 53 deletions
+8 -1
View File
@@ -20,6 +20,7 @@ def main():
parser.add_argument("--rendered", type=Path, required=True)
parser.add_argument("--jar", type=Path, required=True)
parser.add_argument("--output", type=Path, required=True)
parser.add_argument("--extra", type=Path, action="append", default=[])
args = parser.parse_args()
if args.output.exists():
raise SystemExit("Refusing to overwrite an existing immutable atlas")
@@ -48,14 +49,20 @@ def main():
files["raw/" + source.name] = raw
for name in ("maps.png", "maps.pdf", "sections.png", "sections.pdf", "summary.json", "provenance.json"):
files[f"figures/{source.stem}/{name}"] = (rendered / name).read_bytes()
for name in ("upper-terrain.png", "upper-terrain.pdf"):
if (rendered / name).is_file(): files[f"figures/{source.stem}/{name}"] = (rendered / name).read_bytes()
cases.append({"seed": data["seed"], "diameter": data["diameter"], "directory": source.stem})
identities = {(c["seed"], c["diameter"]) for c in cases}
if not cases or len(identities) != len(cases):
raise SystemExit("Empty atlas or duplicate seed/size")
for name in ("scripts/terrain_atlas.py", "scripts/terrain-atlas-requirements.txt",
"scripts/archive_terrain_atlas.py", "docs/terrain-atlas.md",
"docs/generation-alpha30.5.md", "docs/testing-alpha30.5.md"):
"scripts/structure_atlas.py", f"docs/generation-alpha{version.split('alpha.')[-1]}.md", f"docs/testing-alpha{version.split('alpha.')[-1]}.md"):
files[name] = (ROOT / name).read_bytes()
for path in args.extra:
key = "structures/" + path.name
if key in files: raise SystemExit("Duplicate extra atlas file")
files[key] = path.read_bytes()
rows = ["# Sanctuary — atlas " + version, "", f"Source : `{commit}`.", "",
"Mesures natives avant décoration ; voir docs/terrain-atlas.md pour les échelles et limites.", "",
"| Diamètre | Graine | Cartes | Coupes |", "|---|---|---|---|"]
+70
View File
@@ -0,0 +1,70 @@
#!/usr/bin/env python3
"""Plot observed village blocks and planned surface rails against the native terrain atlas."""
import argparse,json,hashlib
from pathlib import Path
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
from matplotlib.colors import ListedColormap
import numpy as np
def render(source, terrain, output):
data=json.loads(source.read_text()); native=json.loads(terrain.read_text())
if not data.get('passed') or data['seed']!=str(native['seed']) or data['diameter']!=native['diameter']:
raise ValueError('Mismatched or failed native witnesses')
output.mkdir(parents=True,exist_ok=True)
name=f"structures-{data['diameter']}-{data['seed']}"
columns=np.asarray(native['variants']['current306']['columns'])
xs,zs=np.unique(columns[:,0]),np.unique(columns[:,1]);grid=columns.reshape(len(zs),len(xs),5)
extent=(xs[0]-6,xs[-1]+6,zs[-1]+6,zs[0]-6)
fig,ax=plt.subplots(figsize=(10,10),layout='constrained')
img=ax.imshow(np.ma.masked_where(grid[:,:,2]==0,grid[:,:,2]),extent=extent,cmap='terrain',vmin=0,vmax=400,interpolation='nearest')
freight=[l for l in data['rail_links'] if l['kind']=='FREIGHT']
rail_area=len({(c['x'],c['z']) for c in data['rail_cells'] if c['rail']!='NONE'})
ballast=len({(c['x'],c['z']) for c in data['rail_cells']})
land=int(np.count_nonzero(grid[:,:,2]))*native['map_step']**2
upper=int(np.count_nonzero(grid[:,:,2]>=180))*native['map_step']**2
for link in data['rail_links']:
points=link['points'];ax.plot([p['x'] for p in points],[p['z'] for p in points],color='#601d15' if link['kind']=='FREIGHT' else '#404040',lw=1.5 if link['kind']=='FREIGHT' else .9)
ax.scatter([0],[0],marker='+',s=70,c='black',label='Origine naturelle protégée')
ax.plot([],[],color='#601d15',label='Voies ferrées prévues (lacunes réparables)')
ax.plot([],[],color='#404040',label='Chemins prévus')
ax.set(xlabel='X (blocs)',ylabel='Z (blocs) · nord en haut',aspect='equal')
ax.set_title(f"Sanctuary {data['diameter']} · graine {data['seed']}\nRéseau de surface sur le relief natif 30.6")
ax.legend(fontsize=9,loc='upper left');fig.colorbar(img,ax=ax,shrink=.72,label='Altitude du terrain (Y)')
fig.supxlabel(f"{rail_area} colonnes de voie · {ballast} colonnes de sol aménagé (chemins inclus)\n"
f"{100*ballast/land:.2f} % de lemprise terrestre projetée estimée ; {100*ballast/upper:.2f} % des surfaces ≥ Y180.\n"
"Relief échantillonné tous les 12 blocs ; tracés issus du plan, avant placement complet des rails.",fontsize=9)
for ext in ['png','pdf']:fig.savefig(output/f'{name}-reseau.{ext}',dpi=160)
plt.close(fig)
if 'floor_cut' in data:
floor=data['floor_cut']; ids=np.array(floor['blocks']); y=floor['y']
def category(s):
if s in ('minecraft:air','minecraft:cave_air','minecraft:void_air'):return 0
if s in ('minecraft:stone','minecraft:dirt','minecraft:granite','minecraft:andesite','minecraft:diorite','minecraft:deepslate','minecraft:tuff'):return 1
if 'water' in s:return 4
if 'cobweb' in s:return 3
return 2
pixels=np.vectorize(category)(ids)
fig,axes=plt.subplots(1,3,figsize=(18,7),layout='constrained')
palette=ListedColormap(['#eef1f0','#666d70','#b98651','#976da6','#489dc8'])
axes[0].imshow(pixels,extent=(floor['min_x']-.5,floor['min_x']+ids.shape[1]-.5,floor['min_z']+ids.shape[0]-.5,floor['min_z']-.5),cmap=palette,vmin=0,vmax=4,interpolation='nearest',aspect='equal')
axes[0].set_title(f'Coupe horizontale Y{y}\nBrun : constructions / décor ; violet : toiles')
axes[0].set(xlabel='X (blocs)',ylabel='Z (blocs)')
for ax,cut in zip(axes[1:],data['native_cuts']):
blocks=np.asarray(cut['blocks']);step=cut['step']
ax.imshow(blocks,extent=(cut['min_x']-1,cut['min_x']+blocks.shape[1]*step-1,-1,301),cmap=ListedColormap(['#eef1f0','#666d70','#489dc8','#ea8542']),vmin=0,vmax=3,interpolation='nearest',aspect='equal')
ax.axhline(data['floor'],color='#b98651',lw=.8,ls='--');ax.set(xlabel='X (blocs)',ylabel='Y (blocs)');ax.set_title(f"Coupe verticale Z{cut['z']}\nTrait : sol du village")
fig.suptitle(f"Village abandonné · diamètre {data['diameter']} · graine {data['seed']}\nBlocs réellement générés, après structures et décoration",fontsize=15)
fig.supxlabel(f"{data['diagnostics']['houses']} maisons natives · {data['chests']} coffres · {data['zombie_villagers']} zombie-villageois\n"
"Échantillonnage horizontal : 1 bloc ; vertical : 2 blocs. Aucune interpolation daffichage.",fontsize=10)
for ext in ['png','pdf']:fig.savefig(output/f'{name}-village.{ext}',dpi=160)
plt.close(fig)
result={'seed':data['seed'],'diameter':data['diameter'],'rail_columns':rail_area,'prepared_columns_including_paths':ballast,'estimated_land_area':land,
'ratio_percent':100*ballast/land,'ratio_upper_percent':100*ballast/upper,'structure_source_sha256':hashlib.sha256(source.read_bytes()).hexdigest(),
'terrain_source_sha256':hashlib.sha256(terrain.read_bytes()).hexdigest()}
(output/f'{name}-summary.json').write_text(json.dumps(result,indent=2)+'\n')
print(json.dumps(result))
if __name__=='__main__':
p=argparse.ArgumentParser(description=__doc__);p.add_argument('source',type=Path);p.add_argument('terrain',type=Path);p.add_argument('--output',type=Path,required=True);a=p.parse_args();render(a.source,a.terrain,a.output)
+23 -8
View File
@@ -20,6 +20,7 @@ LABELS = {
"global301": "Bruit 3D global · 30.1",
"stretch304": "Étirement vertical · 30.4",
"current305": "Plateaux + déformation 3D · 30.5",
"current306": "Relief légèrement renforcé · 30.6",
}
@@ -32,8 +33,9 @@ def save(fig, directory, name):
def render(source, output):
raw = source.read_bytes()
data = json.loads(raw)
if data["schema"] != 1 or list(data["variants"]) != list(LABELS):
if data["schema"] != 1 or len(data["variants"]) != 4 or any(k not in LABELS for k in data["variants"]):
raise ValueError(f"Unsupported atlas schema/variants: {source}")
labels = {key: LABELS[key] for key in data["variants"]}
target = output / source.stem
target.mkdir(parents=True, exist_ok=True)
title = f"Sanctuary · diamètre {data['diameter']} · graine {data['seed']}"
@@ -51,11 +53,11 @@ def render(source, output):
extent = (xs[0] - step / 2, xs[-1] + step / 2, zs[-1] + step / 2, zs[0] - step / 2)
fig, axes = plt.subplots(3, 4, figsize=(18, 14), layout="constrained", sharex=True, sharey=True)
fig.suptitle(title + "\nRelief et épaisseur de roche", fontsize=18)
metrics = [(2, "Altitude du sommet (Y)", "terrain", 0, 360),
metrics = [(2, "Altitude du sommet (Y)", "terrain", 0, data["domain_y"][1]),
(3, "Épaisseur solide cumulée (blocs)", "viridis", 0, 300),
(4, "Roche continue sous le sommet (blocs)", "magma", 0, 100)]
for row, (index, label, cmap, vmin, vmax) in enumerate(metrics):
for col, (key, grid) in enumerate(zip(LABELS, maps)):
for col, (key, grid) in enumerate(zip(labels, maps)):
ax = axes[row, col]
values = np.ma.masked_where(grid[:, :, 2] == 0, grid[:, :, index])
image = ax.imshow(values, extent=extent, origin="upper", interpolation="nearest",
@@ -65,7 +67,7 @@ def render(source, output):
if col == 0:
ax.set_ylabel("Z (blocs) · nord en haut")
if row == 0:
ax.set_title(LABELS[key], fontsize=11)
ax.set_title(labels[key], fontsize=11)
ax.plot(0, 0, "+", color="black", ms=8, label="Origine")
fig.colorbar(image, ax=axes[row, :], shrink=.85, label=label, extend="max")
fig.supxlabel(f"Pas horizontal : {step} blocs · vertical : {data['vertical_step']} blocs · gris = aucun solide échantillonné.\n" + provenance, fontsize=9)
@@ -74,7 +76,7 @@ def render(source, output):
section_count = len(variants[0]["sections"])
fig, axes = plt.subplots(4, section_count, figsize=(20, 12), layout="constrained", sharex=True, sharey=True)
fig.suptitle(title + "\nCoupes de densité · proportions spatiales conservées", fontsize=18)
for row, (key, variant) in enumerate(zip(LABELS, variants)):
for row, (key, variant) in enumerate(zip(labels, variants)):
for col, section in enumerate(variant["sections"]):
ax = axes[row, col]
density = np.asarray(section["density"], dtype=float) / data["density_scale"]
@@ -87,17 +89,30 @@ def render(source, output):
if row == 0:
ax.set_title(f"Plan vertical Z = {section['z']}")
if col == 0:
ax.set_ylabel(LABELS[key] + "\nY (blocs)", fontsize=10)
ax.set_ylabel(labels[key] + "\nY (blocs)", fontsize=10)
ax.set_xlabel("X (blocs)")
ax.set_ylim(0, 384)
ax.set_ylim(0, data["domain_y"][1] + 1)
fig.colorbar(image, ax=axes, shrink=.6, label="Densité sans unité · bleu : vide (≤ 0) · rouge : roche (> 0)", extend="both")
fig.supxlabel("Coupes échantillonnées tous les 2 blocs, sans interpolation daffichage ; isosurface zéro en noir.\n" + provenance, fontsize=9)
save(fig, target, "sections")
upper = data["variants"].get("current306", {}).get("upper_sections", [])
if upper:
fig, axes = plt.subplots(1, len(upper), figsize=(5*len(upper), 6), layout="constrained", squeeze=False)
fig.suptitle(title + "\nMorceaux de terrain supérieur · densité native", fontsize=15)
for ax, cut in zip(axes[0], upper):
values = np.array(cut["solid"])
dy=cut["step"]
ax.imshow(values, cmap="Greys", vmin=0, vmax=1, interpolation="nearest", aspect="equal",
extent=(cut["min_x"]-1,cut["min_x"]+values.shape[1]*dy-1,cut["max_y"]-values.shape[0]*dy+1,cut["max_y"]+1))
ax.set_title(f"Z = {cut['z']}"); ax.set_xlabel("X");ax.set_ylabel("Y")
fig.supxlabel("Noir : roche · blanc : air. Aucune interpolation daffichage. Avant végétation et structures.",fontsize=9)
save(fig,target,"upper-terrain")
summary = {"seed": data["seed"], "diameter": data["diameter"], "variants": {}}
base = maps[0][:, :, 2]
common = np.logical_and.reduce([m[:, :, 2] >= 200 for m in maps])
for key, grid in zip(LABELS, maps):
for key, grid in zip(labels, maps):
land = grid[:, :, 2] > 0
uplands = base >= 200
summary["variants"][key] = {