Add underground village, branched rails and upper terrain for alpha.30.6
Build Sanctuary / build (push) Canceled after 0s
Build Sanctuary / build (push) Canceled after 0s
This commit is contained in:
@@ -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 |", "|---|---|---|---|"]
|
||||
|
||||
@@ -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 l’emprise 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 d’affichage.",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)
|
||||
@@ -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 d’affichage ; 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 d’affichage. 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] = {
|
||||
|
||||
Reference in New Issue
Block a user