Files
sanctuary-beta/scripts/structure_atlas.py
2026-09-12 19:04:57 +02:00

71 lines
5.5 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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)