#!/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)