# title: 逆向き倉庫
# Original warehouse art; Noto-derived Japanese glyphs under SIL OFL. Silent.
from pathlib import Path
import sys,json,hashlib
import pyxel
from model import *
ROOT=Path(__file__).resolve().parent
CELL=30;OX=57;OY=66
REASONS={'ready':'同じ番号の床へ、ふた箱とも。','walk':'こっちを押すと、向こうは逆へ。',
 'push':'ふた箱、同時に動いた。','walk_wall':'そこは壁。Zで一歩戻せます。',
 'box_wall_1':'1 の行先が壁：全員そのまま。','box_wall_2':'2 の行先が壁：全員そのまま。',
 'meet':'行先が重なる：全員そのまま。','cross':'箱同士は、すれ違えません。',
 'player':'人の足元は通れません。','undo':'一手戻しました。','complete':'ふた箱とも、到着！','invalid':'矢印はひとつずつ。'}

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.preview=True;self.lock=False
        self.frame=0;self.linked='--baseline' not in sys.argv;self.demo='--demo' in sys.argv
        self.events=[];self.snap=None;self.flash=0;self.reset()
        self.controls=tuple(getattr(pyxel,'KEY_'+k) for k in ('SPACE','N','P','R','Z','M','UP','RIGHT','DOWN','LEFT'))
        self.arrows={getattr(pyxel,'KEY_'+k):a for k,a in [('UP','U'),('RIGHT','R'),('DOWN','D'),('LEFT','L')]}
        pyxel.init(384,420,title='021 逆向き倉庫',fps=30,display_scale=2,capture_sec=30,headless='--headless' in sys.argv)
        pyxel.run(self.update,self.draw)
    @property
    def state(self):return self.timeline.state
    @property
    def board(self):return LEVELS[self.level]
    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 reset(self):
        self.timeline=Timeline(initial(self.level));self.pause=False;self.reason='ready';self.last_result=None;self.flash=0
    def record(self,kind,action=None):
        self.events.append({'frame':self.frame,'event':kind,'action':action,'level':self.level+1,
            'moves':self.state.moves,'pushes':self.state.pushes,'won':won(self.board,self.state),
            'reason':self.reason,'pause':self.pause,'title':self.title,'player':self.state.player,'crates':self.state.crates,'hash':state_hash(self.state)})
    def move(self,action):
        self.timeline,r=apply(self.board,self.timeline,action,self.linked);self.last_result=r;self.reason=r.reason;self.flash=12
        self.record('move' if r.accepted else 'blocked',action)
    def back(self):
        self.timeline=undo(self.timeline);self.reason='undo';self.last_result=None;self.record('undo')
    def switch(self,n):self.level=n%3;self.reset();self.record('level')
    def update(self):
        self.frame+=1;self.flash=max(0,self.flash-1)
        if pyxel.btnp(pyxel.KEY_F12):self.snap='manual_runtime';self.record('screenshot')
        if self.demo:
            if self.frame==30:self.title=False;self.record('start')
            schedule={55:('move','R'),100:('level',1),125:('move','R'),280:('undo',None),305:('move','R'),350:('level',2)}
            for k,a in enumerate('DRULUR'):schedule[165+k*15]=('move',a)
            for k,a in enumerate('UUURRRRRDD'):schedule[390+k*18]=('move',a)
            if self.frame in schedule:
                op,arg=schedule[self.frame]
                if op=='move':self.move(arg)
                elif op=='level':self.switch(arg)
                else:self.back()
            if self.frame in (75,140,265,580):self.snap=f'runtime_{self.frame}'
            if self.frame==720:
                pyxel.screencast(str(ROOT/'media/reverse_crates_automatic_silent.gif'),scale=1)
                self.save_events('replay.json');pyxel.quit()
            return
        # Transitions gate all potentially carried keys until a real release frame.
        # No timer: a deliberate release and next-frame press stays responsive.
        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();self.lock=True;self.record('start');return
            if pyxel.btnp(pyxel.KEY_LEFT):self.switch(self.level-1)
            elif pyxel.btnp(pyxel.KEY_RIGHT):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_R):self.reset();self.lock=True;self.record('reset');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_Z):self.back();return
        if won(self.board,self.state):
            if pyxel.btnp(pyxel.KEY_N):self.switch(self.level+1);self.lock=True
            return
        if pyxel.btnp(pyxel.KEY_SPACE):self.preview=not self.preview;self.record('preview');return
        keys=[a for k,a in self.arrows.items() if pyxel.btnp(k,12,4)]
        held=sum(bool(pyxel.btn(k)) for k in self.arrows)
        if len(keys)==1 and held<=1:self.move(keys[0])
        elif len(keys)>1 or (keys and held>1):self.reason='invalid'
    def save_events(self,name):
        data={'label':'自動操作・無音 / actual Pyxel renderer' if self.demo else 'Agent-operated native Linux technical QA; not human playtest',
              'fps':30,'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 center(self,p):return OX+p[0]*CELL+15,OY+p[1]*CELL+15
    def arrow(self,p,d,color,off=0):
        x,y=self.center(p);x+=off;y-=off;dx,dy=d
        x0,y0=x-dx*6,y-dy*6;x1,y1=x+dx*11,y+dy*11
        pyxel.line(x0,y0,x1,y1,color)
        pyxel.line(x1,y1,x1-dx*4+dy*3,y1-dy*4-dx*3,color)
        pyxel.line(x1,y1,x1-dx*4-dy*3,y1-dy*4+dx*3,color)
    def cue(self):
        for a,d in DIRECTIONS.items():
            if add(self.state.player,d) in self.state.crates:return a,transition(self.board,self.state,a,self.linked)
        return None,None
    def draw(self):
        pyxel.cls(1);self.text(10,4,'021 逆向き倉庫',7)
        self.text(10,25,f'{"ABC"[self.level]}  {self.board.title}',10)
        self.text(296,25,'逆向き' if self.linked else '単独',6)
        # Floor and brickwork. Goal numbers match the numbered cable pair.
        for y in range(self.board.height):
            for x in range(self.board.width):
                px,py=OX+x*CELL,OY+y*CELL
                if (x,y) in self.board.walls:
                    pyxel.rect(px,py,CELL,CELL,5);pyxel.rectb(px,py,CELL,CELL,1)
                    pyxel.line(px,py+14,px+29,py+14,13);pyxel.line(px+12,py,px+12,py+13,13);pyxel.line(px+22,py+15,px+22,py+29,13)
                else:
                    pyxel.rect(px,py,CELL,CELL,3);pyxel.rectb(px,py,CELL,CELL,1);pyxel.pset(px+5,py+6,11)
        for i,p in enumerate(self.board.goals):
            x,y=self.center(p);pyxel.rectb(x-12,y-12,25,25,10);pyxel.rectb(x-10,y-10,21,21,4)
            self.text(x-3,y-7,str(i+1),10)
        # Overhead two-axis reversing hoist, a fictional machine rather than realistic rope physics.
        c0=self.center(self.state.crates[0]);c1=self.center(self.state.crates[1])
        for x,y in (c0,c1):
            for yy in range(54,int(y)-10,4):pyxel.line(x,yy,x,yy+1,9)
            pyxel.circb(x,54,4,10)
        pyxel.line(c0[0],54,c1[0],54,9);pyxel.rect(171,45,42,17,4);pyxel.rectb(171,45,42,17,9)
        self.text(174,47,'反転機',10)
        a,r=self.cue()
        if self.preview and not self.title and r and r.destinations:
            for p in r.destinations:
                x,y=self.center(p);pyxel.rectb(x-14,y-14,29,29,6 if r.accepted else 8)
            for p in r.blocked:
                x,y=self.center(p);pyxel.line(x-10,y-10,x+10,y+10,8);pyxel.line(x+10,y-10,x-10,y+10,8)
        for i,p in enumerate(self.state.crates):
            x,y=self.center(p);pyxel.rect(x-11,y-9,24,23,0);pyxel.rect(x-12,y-12,24,24,9);pyxel.rectb(x-12,y-12,24,24,4)
            pyxel.line(x-10,y-10,x+9,y+9,4);pyxel.line(x+9,y-10,x-10,y+9,4)
            pyxel.circ(x,y,7,10);self.text(x-3,y-6,str(i+1),1)
            if p==self.board.goals[i]:pyxel.rectb(x-14,y-14,28,28,11)
        x,y=self.center(self.state.player)
        pyxel.elli(x-8,y+6,17,6,0);pyxel.rect(x-7,y-1,14,12,12);pyxel.rect(x-6,y-10,12,10,15)
        pyxel.rect(x-8,y-12,16,4,6);pyxel.rect(x-5,y-14,10,3,6);pyxel.pset(x-3,y-6,1);pyxel.pset(x+3,y-6,1)
        pyxel.rect(x-7,y+9,5,4,1);pyxel.rect(x+2,y+9,5,4,1)
        if self.preview and not self.title and r and r.destinations:
            d=DIRECTIONS[a];self.arrow(self.state.crates[r.pushed],d,7,8)
            if self.linked:self.arrow(self.state.crates[1-r.pushed],(-d[0],-d[1]),6,8)
        if self.last_result and not self.last_result.accepted and self.flash:
            for p in self.last_result.blocked:
                x,y=self.center(p);pyxel.rectb(x-14,y-14,29,29,8)
        pyxel.rect(0,282,384,138,1)
        success=won(self.board,self.state)
        self.text(10,286,'ふた箱とも、到着！' if success else REASONS[self.reason],10 if success else 7)
        self.text(10,308,f'押し {self.state.pushes}   歩き {self.state.moves-self.state.pushes}   時間制限なし',6)
        self.text(10,332,'N 次の室  Z 戻す  R 同じ室' if success else '矢印 歩く・押す  Z 一手戻す',10 if success else 7)
        self.text(10,354,'SPACE 予告切替  R 最初から  P 説明',7)
        self.text(10,376,'M 部屋選択  ESC 終了',6)
        self.text(10,398,'自動操作・無音' if self.demo else '同じ紋章の箱は、上下左右が逆。',13)
        if self.title or self.pause:
            pyxel.rect(18,92,348,184,1);pyxel.rectb(18,92,348,184,7)
            self.text(90,105,'逆向きに動く荷揚げ機',10)
            self.text(32,135,'箱1と2を、同じ番号の床へ。')
            self.text(32,159,'片方を押すと、相棒は逆へ。')
            self.text(32,183,'どちらかが詰まると、両方止まる。')
            self.text(32,207,'歩きも押しも、Zで一手戻せる。')
            self.text(32,241,'左右で室を選ぶ  SPACE 開始' if self.title else 'Pで戻る。押しても失う物はない。',10)
        if self.snap:
            pyxel.screenshot(str(ROOT/f'media/{self.snap}.png'),scale=2);self.save_events('gui_events.json' if not self.demo else 'replay_partial.json');self.snap=None

if __name__=='__main__':App()
