Files
AIGame/Tools/verify_skill_slice.py

65 lines
3.7 KiB
Python

"""Check the C++ slice trace, immutable inputs, and TangoFlux audio provenance."""
import hashlib
import json
from pathlib import Path
import numpy as np
from scipy.io import wavfile
from export_skill_slice import canonical
ROOT=Path(__file__).resolve().parents[1]
DESIGN=ROOT/'Doc/Design/Combat/SkillSlice'
def main():
DESIGN.mkdir(parents=True,exist_ok=True)
trace_path=ROOT/'AIClient/Assets/Bundles/Combat/SkillSlice/trace.json'
trace=json.loads(trace_path.read_text(encoding='utf-8'))
exported=json.loads((DESIGN/'export.json').read_text(encoding='utf-8'))
assert hashlib.sha256(trace_path.read_bytes()).hexdigest()==exported['traceSha256']
for row in exported['inputs']:
snapshot=(ROOT/row['snapshot']).read_bytes()
assert hashlib.sha256(snapshot).hexdigest()==row['sourceSha256']
assert trace[row['field']]==json.loads(snapshot)
assert hashlib.sha256(canonical(trace[row['field']])).hexdigest()==row['payloadSha256']
casts={};shots={};hits=set();deaths=set();last=-1
for seq,e in enumerate(trace['events']):
assert e['seq']==seq and e['tick']>=last
last=e['tick'];kind=e['kind'];cast=e.get('cast')
if kind=='cast_start':
assert cast not in casts
casts[cast]=e
elif kind=='release': assert e['tick']==casts[cast]['release']
elif kind=='projectile':
key=(cast,e['target']);assert key not in shots
shots[key]=e
assert e['arrival']>e['tick']
elif kind=='hit':
key=(cast,e['target']);assert key not in hits
hits.add(key)
if e['source']==1: assert e['tick']==shots[key]['arrival']
else: assert e['tick']==casts[cast]['release']
elif kind=='death':
assert e['target'] not in deaths
deaths.add(e['target'])
assert trace['events'][-1]['kind']=='battle_end' and trace['won']
audio=json.loads((ROOT/'Doc/Design/Sound/BattleSkills/Slice/generation.json').read_text(encoding='utf-8'))
rows=[]
for item in audio['items']:
source=ROOT/item['source'];target=ROOT/item['output']
assert hashlib.sha256(source.read_bytes()).hexdigest()==item['sourceSha256']
assert hashlib.sha256(target.read_bytes()).hexdigest()==item['outputSha256']
assert hashlib.sha256(json.dumps(item['request'],sort_keys=True).encode()).hexdigest()==item['requestHash']
assert item['job']['status']=='succeeded'
rate,samples=wavfile.read(target);x=samples.astype(float)/32768
assert rate==44100 and samples.dtype==np.int16 and x.shape[1]==2
assert .1<len(x)/rate<1 and np.isfinite(x).all()
assert np.max(np.abs(x))<.95 and np.max(np.abs(x[:4410]))>.05
assert np.max(np.abs(x[-10:]))<.003
rms=float(20*np.log10(np.sqrt(np.mean(x*x))));assert rms>-36
rows.append(dict(id=item['id'],seconds=len(x)/rate,rmsDb=rms,clippedSamples=int(np.sum(np.abs(x)>=.999))))
assert len(rows)==5
report=dict(status='pass',events=len(trace['events']),casts=len(casts),projectiles=len(shots),hits=len(hits),deaths=len(deaths),traceSha256=hashlib.sha256(trace_path.read_bytes()).hexdigest(),frozenInputs={r['source']:r['sourceSha256'] for r in exported['inputs']},currentSourceChanged=[r['source'] for r in exported['inputs'] if hashlib.sha256((ROOT/r['source']).read_bytes()).hexdigest()!=r['sourceSha256']],audio=rows,subjectiveListening='not_asserted',productionIntegrated=False)
(DESIGN/'validation.json').write_text(json.dumps(report,indent=2),encoding='utf-8')
print(f"PASS trace: {len(casts)} casts, {len(shots)} projectiles, {len(hits)} unique hits; five audio clips and frozen source hashes")
if __name__=='__main__':main()