71 lines
5.5 KiB
Python
71 lines
5.5 KiB
Python
#!/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)
|