# python3 -I 07_py_sim.py <repo> <test_set.json> <ts.json> <model>
# Independent transcription of gca_simulation/gca_judging (hybrid registered by the platform: speech path of eval_reproduce.py@93c1474,
# NSP path of main.py@624efc5) fed with the same replies; every request is compared byte for byte. Official files are not imported.
import ast, json, re, subprocess, sys, copy
repo, ts_path, sim_path, model = sys.argv[1:5]
def show(c,p): return subprocess.run(['git','-C',repo,'show',f'{c}:{p}'],capture_output=True,check=True).stdout.decode()
critic=ast.literal_eval(ast.parse(show('624efc5','gca_evaluation/prompts.py')).body[0].value)
def rit(d):
    c=re.sub(r'\[.*?\]','',d); c='\n'.join(l.strip() for l in c.split('\n')); c=re.sub(r'\n+','\n',c)
    c=re.sub(r'\s*<\s*system[_\s]+think(?:ing)?\s*>.*?</\s*system[_\s]+think(?:ing)?\s*>\s*','',c,flags=re.S).strip()
    c=re.sub(r'\s*<\s*role[_\s]+think(?:ing)?\s*>.*?</\s*role[_\s]+think(?:ing)?\s*>\s*','',c,flags=re.S).strip()
    c=re.sub(r'\s*<\s*[_\s]+think(?:ing)?\s*>.*?</\s*[_\s]+think(?:ing)?\s*>\s*','',c,flags=re.S).strip()
    return c.strip()
def asn(d,s):
    if any(l.strip().startswith(f"{s}:") or l.strip().startswith(f"{s}：") for l in d.split('\n')): return d
    return f"{s}: {d}"
def nsp_parse(r):
    m=re.findall(r'(?:\*\*|#)?\s*Next\s+Speaker\s*(?:\*\*|#)?\s*(?:[:\-]|\bis\b)?\s*\*?([^\n\*]+)',r,re.IGNORECASE)
    n=None
    if m:
        c=m[-1].strip().rstrip('.,!"\'').replace('*','')
        if c: n=c
    if n is None: n=r.split(':')[0].strip() if ':' in r else r
    return n
def char_prompt(book,character,profile,scenario,motivation,others,decorator,example):
    of="Your output should include **thought**, **speech**, and **action**. Use [your thought] for thoughts, which others can't see. Use (your action) for actions, which others can see."
    if example: of="Your output should include **thought**, **speech**, and **action**. Use [your thought] for thoughts, which others can't see, e.g. [I'm terrified, but I must appear strong.]. Use (your action) for actions, which others can see, such as (watches silently, trying to control her fear and anger)."
    s=''
    if others:
        for o,p in others.items():
            if o!=character: s+=f"{decorator}{o}: {p}\n\n"
    if motivation: motivation=f"===Your Inner Thoughts===\n{motivation}\n\n"
    if s: s=f"===Information about the other Characters===\n{s}\n\n"
    sp=f"You are {character} from {book}.\n\n==={character}'s Profile===\n{profile}\n\n===Current Scenario===\n{scenario}\n\n{s}{motivation}\n\n"
    sp+=f"===Requirements===\n{of}\n\n"
    return sp
def env_prompt(major,scenario,role):
    major=[c for c in major if c!='Environment']
    return f"""You are {role} for a role-playing game. Your task is to provide the environmental feedback: Based on the characters' interactions, dialogues, and actions, describe the resulting changes in the environment. This includes:
   - Physical changes in the setting
   - Reactions of background characters or crowds
   - Ambient sounds, weather changes, or atmospheric shifts
   - Any other relevant environmental details

Your descriptions should be vivid and help set the scene, but avoid dictating the actions or dialogue of the main characters (including {major}).

Important notes:
- You may include actions and reactions of minor characters or crowds, as long as they're not main characters (including {major}).
- Keep your environmental descriptions concise but impactful, typically 1-3 sentences.
- Respond to subtle cues in the characters' interactions to create a dynamic, reactive environment.
- Your output should match the tone, setting, and cultural context of the scenario.

===The scenario is as follows===
{scenario}"""
def nsp_prompt(allc,scenario):
    return f"""Your task is to predict the next speaker for a role-playing game. That is, you need to determine which character (or the Environment) might act next based on their previous interactions. The Environment is a special role that provides the environmental feedback. Choose a name from this list: {allc}. If it's unclear who should act next, output "random". If you believe the scene or conversation should conclude, output "<END CHAT>".

===The scenario is as follows===
{scenario}

===Output Format===
You must structure your response as follows:
1. Reasoning: Explain your thought process.
2. Next Speaker: Select exactly one character name from the list: {allc}.
"""
class Agent:
    def __init__(self,m,name,sp,sysrole,nameprefix):
        self.name=name; self.sp=sp.strip('\n')
        if name not in('Environment','NSP'): self.sp=self.sp+'\n\nSpeak concisely as humans, instead of being verbose. Limit your response to 60 words.\n\n'
        if nameprefix: self.sp=self.sp+f'Start your response with "{name}: ". Avoid speaking as other characters.\n\n'
        self.messages=[{"role":sysrole,"content":self.sp}]
    def update(self,role,message):
        if message:
            if self.messages and self.messages[-1]['role']==role: self.messages[-1]['content']=self.messages[-1]['content']+'\n\n'+message
            else: self.messages.append({"role":role,"content":message})
def to_request(messages,claude):
    messages=copy.deepcopy(messages)
    if claude and messages and messages[0]['role']=='system': messages[0]['role']='user'
    merged=[]
    for m in messages:
        if m['role']=='user' and merged and merged[-1]['role']=='user': merged[-1]['content']+=m['content']
        else: merged.append(copy.deepcopy(m))
    return merged
ds=json.load(open(ts_path)); sims=json.load(open(sim_path))
bad=0; total=0; firstbad=None; outcomes={}
for circ,T in zip(ds,sims):
    fam=T['family']; plot=circ['plot']; book=circ['book']
    sp=list(circ['speaking_characters_w_env'])
    if 'Environment' not in sp: sp.append('Environment')
    major=circ['major_characters']; pc=[c['name'] for c in plot['key_characters']]
    inv={}
    for ch in sp:
        if ch=='Environment': continue
        p=circ['character_profiles'].get(ch,'')
        if ch in pc:
            info=[c for c in plot['key_characters'] if c.get('name','')==ch][0]
            if 'description' in info: p=info.get('description','').strip('\n')+'\n\n'+p.strip('\n')
        p=p.strip(' \n')
        if p!='': inv[ch]=p
    agents={}
    claude=fam['systemToUserInRequest']; srole=fam['systemRole']
    for ch in sp+['NSP']:
        if ch=='NSP': a=Agent('nsp',ch,nsp_prompt(sp,circ['scenario']),'system',False)
        elif ch=='Environment': a=Agent('env',ch,env_prompt(major,circ['scenario'],T['choices']['environmentRole']['value']),'system',False)
        else:
            mot=[c.get('thought','') for c in circ['key_characters'] if c.get('name','')==ch]; mot=mot[0] if mot else ''
            dec=T['choices']['separators'].get(ch,{}).get('value','')
            a=Agent('x',ch,char_prompt(book,ch,inv.get(ch,'').strip(' \n'),circ['scenario'],mot,inv,dec,fam['outputExample']),srole,fam['namePrefix'])
        a.update('user',"===Conversation Start===\n\n")
        agents[ch]=a
    cur=sp[0]; rec=[]; i_call=0; calls=T['calls']; ended=False
    for i_round in range(20):
        if cur=="<END CHAT>": break
        for actor in [cur,'NSP']:
            c=calls[i_call]; i_call+=1
            agent=agents[actor]
            is_claude=claude if actor not in('NSP','Environment') else False
            req=to_request(agent.messages,is_claude)
            total+=1
            if req!=c['messages'] or c['who']!=actor:
                bad+=1
                if firstbad is None: firstbad=(T['si'],i_call,actor,json.dumps(req)[:600],json.dumps(c['messages'])[:600])
            if actor=='NSP':
                resp=c['raw']; nxt=nsp_parse(resp); inval=False
                if nxt=="<END CHAT>" and i_round>=5: cur="<END CHAT>"
                elif nxt in sp and nxt!=cur: cur=nxt
                else:
                    inval=True
                    # the platform's ordered candidates and seeded draw stand in for the official set + global stream (registered)
                    cand=set(major+['Environment'])-{cur}
                    if not cand: cand=set(sp)-{cur}
                    if set(c['candidates'] or [])!=cand: bad+=1
                    cur=c['next']
                rec.append({"role":'NSP',"content":nxt})
                if not inval: agent.update('assistant',resp)
                else:
                    agent.update('assistant',nxt); agent.update('user',agent.messages[0]['content'])
                outcomes['fallback' if inval else ('end' if cur=="<END CHAT>" else 'next')]=outcomes.get('fallback' if inval else ('end' if cur=="<END CHAT>" else 'next'),0)+1
            else:
                resp=asn(c['text'],actor)
                rec.append({"role":actor,"content":resp})
                for o,a2 in agents.items():
                    if o==actor: a2.update('assistant',resp)
                    else: a2.update('user',rit(resp))
    if i_call!=len(calls): bad+=1; print('call count differs',T['si'],i_call,len(calls))
    if rec!=T['record']: bad+=1; print('record differs',T['si'])
    # judging
    s=[m for m in rec if m['role']!='NSP']
    ref=circ['dialogues']
    s=[m if m['role']=='Environment' else {**m,'content':rit(m['content'])} for m in s]
    ref=[m if m['character']=='Environment' else {**m,'message':rit(m['message'])} for m in ref]
    sim_str='\n\n'.join([m['content'].strip('\n') for m in s])
    ref_str='\n\n'.join([f"{m['character']}: {m['message']}".strip('\n') for m in ref])
    prof='\n\n'.join([f"### {k}\n\n{v.strip('')}" for k,v in inv.items()])
    R=len([m for m in s if m['role']!='Environment'])
    if R!=T['R']: bad+=1
    for d in ['Storyline Consistency','Anthropomorphism','Character Fidelity','Storyline Quality']:
        p=critic['self-play-deduct-template'].replace('{book}',book).replace('{plot_summary}',plot['summary']).replace('{scenario}',circ['scenario']).replace('{character_profiles}',prof).replace('{original_conversation}',ref_str).replace('{major_characters}',', '.join(major)).replace('{additional_instructions}','').replace('{dimension_name}',d).replace('{dimension_brief}',critic['dimension_details'][d]['dimension_brief']).replace('{dimension_criteria}',critic['dimension_details'][d]['dimension_criteria'])
        total+=1
        if [{"role":"system","content":p},{"role":"user","content":sim_str}]!=T['judge'][d]:
            bad+=1
            if firstbad is None: firstbad=('judge',T['si'],d)
print('model',model,'requests compared',total,'mismatches',bad,'outcomes',outcomes)
if firstbad: print('first',firstbad)
