# title: 音のないピアノ
# Original piano art, actual Pyxel runtime, no sound dependency.
from pathlib import Path
import sys,json,hashlib
import pyxel
from model import *
ROOT=Path(__file__).resolve().parent
WIDTH=448;HEIGHT=480
MESSAGES={'ready':'どの鍵を試す？ 目標の灯りを作ろう。',
 'press':'変わった灯りが、この鍵の配線。','success':'目標と一致！ 全部を調べなくてよい。',
 'undo':'灯りを戻した。観測メモは残る。','reset':'灯りは000。観測メモは残る。',
 'empty':'戻す灯りがありません。メモは残る。','invalid':'左右は、ひとつずつ。','complete':'目標と一致しています。'}
class App:
    def __init__(self):
        self.glyph=json.loads((ROOT/'assets/glyphs.json').read_text(encoding='utf-8'))
        self.level=0;self.title=True;self.pause=False;self.lock=False;self.frame=0
        self.demo='--demo' in sys.argv;self.dimming='--baseline' not in sys.argv
        self.events=[];self.snap=None;self.reset_case()
        self.controls=tuple(getattr(pyxel,'KEY_'+k) for k in ('SPACE','N','P','R','Z','M','UP','RIGHT','DOWN','LEFT'))
        pyxel.init(WIDTH,HEIGHT,title='026 音のないピアノ',fps=30,display_scale=1,capture_sec=30,headless='--headless' in sys.argv)
        pyxel.run(self.update,self.draw)
    @property
    def board(self):return LEVELS[self.level]
    def reset_case(self):
        self.state=initial(self.board);self.selected=0;self.scroll=0;self.pause=False;self.reason='ready'
    def text(self,x,y,s,c=7):
        for ch in str(s):
            if ord(ch)<128:pyxel.text(x,y+3,ch,c);x+=6
            else:
                for a,b in self.glyph[ch]:pyxel.pset(x+a,y+b,c)
                x+=12
    def record(self,kind,action=None):
        self.events.append({'frame':self.frame,'event':kind,'action':action,'level':self.level+1,
          'lamps':bits(self.state.lamps),'notes':self.state.notes,'candidates':[i+1 for i in candidates(self.board,self.state)],
          'presses':len(self.state.journal),'won':won(self.board,self.state),'opened':self.state.opened,
          'title':self.title,'pause':self.pause,'selected':self.selected,'hash':state_hash(self.state)})
    def act(self,action):
        r=transition(self.board,self.state,action);self.state=r.state;self.reason=r.event;self.scroll=0
        self.record(r.event,action)
    def switch(self,n):
        self.level=n%len(LEVELS);self.reset_case();self.record('level')
    def update(self):
        self.frame+=1
        if pyxel.btnp(pyxel.KEY_F12):self.snap='manual_runtime';self.record('screenshot')
        if self.demo:
            schedule={30:('start',None),80:('press',0),135:('undo',None),175:('press',1),
              235:('level',1),285:('press',0),350:('press',3),430:('level',2),
              490:('press',0),555:('press',1)}
            if self.frame in schedule:
                op,arg=schedule[self.frame]
                if op=='start':self.title=False;self.record('start')
                elif op=='level':self.switch(arg)
                elif op=='undo':self.act('undo')
                else:self.selected=arg;self.act(arg)
            if self.frame in (110,200,320,390,590):self.snap=f'runtime_{self.frame}'
            if self.frame==720:
                pyxel.screencast(str(ROOT/'media/silent_piano_automatic_silent.gif'),scale=1)
                self.save_events('replay.json');pyxel.quit()
            return
        # Release gate handles simultaneous AND staggered carried keys; no time-based cooldown.
        if self.lock:
            if not any(pyxel.btn(k) for k in self.controls):self.lock=False
            return
        if self.title:
            if pyxel.btnp(pyxel.KEY_SPACE):
                self.title=False;self.reset_case();self.lock=True;self.record('start');return
            if pyxel.btnp(pyxel.KEY_LEFT) and not pyxel.btn(pyxel.KEY_RIGHT):self.switch(self.level-1)
            elif pyxel.btnp(pyxel.KEY_RIGHT) and not pyxel.btn(pyxel.KEY_LEFT):self.switch(self.level+1)
            return
        if pyxel.btnp(pyxel.KEY_M):self.title=True;self.pause=False;self.lock=True;self.record('menu');return
        if pyxel.btnp(pyxel.KEY_P):self.pause=not self.pause;self.lock=True;self.record('pause');return
        if self.pause:return
        if pyxel.btnp(pyxel.KEY_R):self.act('reset');self.lock=True;return
        if pyxel.btnp(pyxel.KEY_Z):self.act('undo');self.lock=True;return
        if pyxel.btnp(pyxel.KEY_UP):self.scroll=min(max(0,len(self.state.journal)-3),self.scroll+1)
        elif pyxel.btnp(pyxel.KEY_DOWN):self.scroll=max(0,self.scroll-1)
        if won(self.board,self.state):
            if pyxel.btnp(pyxel.KEY_N):self.switch(self.level+1);self.lock=True
            return
        left=pyxel.btnp(pyxel.KEY_LEFT);right=pyxel.btnp(pyxel.KEY_RIGHT)
        if (left or right) and pyxel.btn(pyxel.KEY_LEFT) and pyxel.btn(pyxel.KEY_RIGHT):self.reason='invalid';return
        if left or right:
            self.selected=(self.selected+(-1 if left else 1))%len(self.state.notes)
            self.record('select');return
        if pyxel.btnp(pyxel.KEY_SPACE):self.act(self.selected)
    def save_events(self,name):
        data={'label':'Actual Pyxel automatic input; silent' if self.demo else 'Agent-operated Linux native input QA, not human playtest',
          'fps':30,'actual_diagrams':[l.actual_index+1 for l in LEVELS],
          'source_sha256':{n:hashlib.sha256((ROOT/n).read_bytes()).hexdigest() for n in ('app.py','model.py')},'events':self.events}
        (ROOT/'media'/name).write_text(json.dumps(data,ensure_ascii=False,indent=2),encoding='utf-8')
    def bulbs(self,x,y,value,space=15,radius=5,dim=False):
        for k,b in enumerate(bits(value)):
            xx=x+k*space;color=(5 if dim else 10) if b=='1' else (1 if not dim else 0)
            pyxel.circ(xx,y,radius,color);pyxel.circb(xx,y,radius,5 if dim else 6)
            if radius>=8:self.text(xx-3,y-7,b,1 if b=='1' else 6)
            elif b=='1':pyxel.pset(xx,y,7 if not dim else 13)
    def draw(self):
        pyxel.cls(1);self.text(12,4,'026 音のないピアノ',7)
        self.text(12,24,f'{"ABC"[self.level]}  {self.board.title}',10)
        # Piano lid and two physical lamp triplets, consistently left-to-right 100/010/001.
        pyxel.rect(12,46,424,123,4);pyxel.rectb(12,46,424,123,9)
        self.text(30,50,'いま '+bits(self.state.lamps),7);self.text(252,50,'目標 '+bits(self.board.target),10)
        self.bulbs(72,85,self.state.lamps,36,13);self.bulbs(294,85,self.board.target,36,13)
        pyxel.line(208,58,222,58,9);pyxel.line(208,97,222,97,9)
        n=len(self.state.notes);kw=384//n
        for k in range(n):
            x=32+k*kw;selected=k==self.selected
            pyxel.rect(x,112,kw-5,43,7);pyxel.rectb(x,112,kw-5,43,0)
            if selected:pyxel.rectb(x-2,110,kw-1,47,10)
            pyxel.rect(x+kw-27,112,13,20,0)
            self.text(x+9,122,'ABCD'[k],1);self.text(x+25,141,'選択' if selected else '',4)
        self.text(12,174,MESSAGES[self.reason],10 if won(self.board,self.state) else 7)
        # A permanent full transcript can be read with up/down. Undo does not erase it.
        total=len(self.state.journal);end=max(0,total-self.scroll);start=max(0,end-3)
        pyxel.rect(12,196,424,80,0);self.text(18,198,f'観測記録 {start+1 if total else 0}-{end}/{total}  上下で読む',6)
        if not total:self.text(20,222,'鍵を押すと、前 -> 後 / 反転を記録。',5)
        for j,obs in enumerate(self.state.journal[start:end]):
            self.text(20,217+j*18,f'{start+j+1:03}  {"ABCD"[obs.key]}  {bits(obs.before)} -> {bits(obs.after)}  / {bits(obs.mask)}',7)
        # Possible rows are observational knowledge, separate from the hidden actual row.
        alive=candidates(self.board,self.state)
        self.text(12,281,f'候補図  観測に合う図 {len(alive)}枚  x は矛盾',6)
        xs=[115+80*k for k in range(4)]
        self.text(20,302,'反転する灯り',13)
        for k in range(n):self.text(xs[k]+10,302,'ABCD'[k],7)
        for i,row in enumerate(self.board.diagrams):
            y=323+i*21;ok=i in alive;dim=not ok and self.dimming
            if ok:pyxel.rectb(14,y-2,420,20,6)
            self.text(22,y,('o ' if ok else 'x ')+f'図{i+1}',5 if dim else 7)
            for k,value in enumerate(row):
                self.bulbs(xs[k],y+7,value,12,4,dim)
                self.text(xs[k]+37,y,bits(value),5 if dim else 13)
        # Known masks derive only from the rows matching recorded evidence, never actual_index.
        y=408;self.text(18,y,'確かな配線',10)
        for k,v in enumerate(known_masks(self.board,self.state)):
            self.text(xs[k],y,'?' if v is None else bits(v),10 if v is not None else 13)
        actual=str(self.state.actual_index+1)+' (開封済)' if self.state.opened else '? (未開封)'
        self.text(12,430,f'実物図 {actual}   押下 {len(self.state.journal)}回',6)
        self.text(12,449,'N 次の面  Z 灯りを戻す  R 000へ' if won(self.board,self.state) else '左右 選択  SPACE 押す  Z 戻す  R 000',7)
        self.text(12,465,'自動操作・無音' if self.demo else 'Z/R メモは残る  P 説明  M 面選択',13)
        if self.title or self.pause:
            pyxel.rect(18,96,412,287,1);pyxel.rectb(18,96,412,287,7)
            self.text(120,110,'灯りで調べるピアノ',10)
            lines=('音が出ない。けれど、鍵は灯りを変える。','目標と同じ3灯を作れば修理完了。',
              '各鍵は、配線図の灯りを反転する。','同じ鍵を2回押すと、灯りは元どおり。',
              '実物は候補図のどれか。面ごとに固定。','観測に合う図から、確かな配線が分かる。',
              '全部の配線を特定しなくても成功。','Z/Rは灯りだけ戻す。観測メモは残る。',
              '面を選び直すと、メモも最初から。')
            for i,line in enumerate(lines):self.text(30,140+i*23,line,7)
            self.text(36,357,'左右で面を選ぶ  SPACE 開始' if self.title else 'Pで戻る。実験回数に制限はない。',10)
        if self.snap:
            pyxel.screenshot(str(ROOT/f'media/{self.snap}.png'),scale=2)
            self.save_events('replay_partial.json' if self.demo else 'gui_events.json');self.snap=None
if __name__=='__main__':App()
