# title: 折りたたみ駅
# Actual Pyxel renderer, original paper/rail art; licensed Noto glyphs. Silent.
from pathlib import Path
import json,sys,hashlib
import pyxel
from model import *
ROOT=Path(__file__).resolve().parent
W,H=480,492
MESSAGES={'ready':'駅を重ねて、行先の紙へ乗り換える。','fold':'紙を折りました。駅の番号は変わりません。',
 'unfold':'紙を開きました。乗客は乗った駅に残ります。','transfer':'重なった駅へ、乗り換えました。',
 'rail':'印刷された線路で、一駅進みました。','suspended':'運休中。この線路をまたぐ折れ目を開こう。',
 'no_route':'矢印で行先を選び、SPACEで移動。','unused':'この面では、その折れ目は使いません。',
 'complete':'目的の駅へ到着！紙も全部開きました。','undo':'一手戻しました。切符の駅も元どおり。','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.lock=False;self.frame=0;self.demo='--demo' in sys.argv
    self.suspend='--baseline' not in sys.argv;self.events=[];self.snap=None;self.reset()
    self.controls=tuple(getattr(pyxel,'KEY_'+k) for k in ('SPACE','N','M','P','R','Z','X','C','UP','RIGHT','DOWN','LEFT'))
    pyxel.init(W,H,title='025 折りたたみ駅',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]
 @property
 def state(self):return self.timeline.state
 def text(self,x,y,s,c=7):
    for ch in str(s):
     for a,b in self.glyph[ch]:pyxel.pset(x+a,y+b,c)
     x+=6 if ord(ch)<128 else 12
 def reset(self):
    self.timeline=Timeline(initial(self.board));self.selected=None;self.pause=False;self.reason='ready';self.history=[];self.animation=None;self.flash=0
 def record(self,event,action=None):
    self.events.append({'frame':self.frame,'event':event,'action':action,'level':self.level+1,'station':self.state.station,'v':self.state.v,'h':self.state.h,'folds':self.state.folds,'moves':self.state.moves,'hash':state_hash(self.state),'won':won(self.board,self.state),'pause':self.pause,'title':self.title,'reason':self.reason})
 def act(self,action):
    before=self.state;self.timeline,r=apply(self.board,self.timeline,action,self.suspend);self.reason=r.reason
    if r.accepted:
     label={'V':'Vを'+('開く' if before.v else '折る'),'H':'Hを'+('開く' if before.h else '折る')}.get(action)
     if label is None:label=action[2:]+('へ乗換' if action.startswith('T:') else 'へ列車')
     self.history.append(label);self.selected=None;self.lock=True;self.flash=15;self.animation=(before,self.state,action)
    self.record('action' if r.accepted else 'blocked',action)
 def back(self):
    old=self.timeline;self.timeline=undo(old)
    if old!=self.timeline and self.history:self.history.pop()
    self.selected=None;self.animation=None;self.reason='undo';self.lock=True;self.record('undo')
 def switch(self,n):self.level=n%3;self.reset();self.lock=True;self.record('level')
 def save_events(self,name):
    (ROOT/'media').mkdir(exist_ok=True)
    (ROOT/'media'/name).write_text(json.dumps({'label':'Automatic silent input; actual Pyxel renderer' if self.demo else 'Native Linux agent-operated 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},ensure_ascii=False,indent=2),encoding='utf-8')
 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:
     # Script calls the same atomic actions and undo used by the keyboard.
     schedule={30:('start',None),55:('act','V'),80:('act','T:G'),105:('act','V'),140:('level',1),165:('act','V'),190:('act','T:Q'),220:('act','R:R'),265:('act','V'),290:('act','R:R'),320:('act','H'),345:('act','T:G'),370:('act','H'),405:('level',2),430:('act','V'),455:('act','T:A'),480:('act','R:B'),505:('act','T:C'),530:('act','R:D'),555:('act','H'),580:('act','T:G'),605:('act','H'),630:('act','V'),670:('undo',None),690:('act','V')}
     if self.frame in schedule:
      op,arg=schedule[self.frame]
      if op=='start':self.title=False;self.record('start')
      elif op=='act':self.act(arg)
      elif op=='level':self.switch(arg)
      else:self.back()
     if self.frame in (90,240,302,510,590,650):self.snap=f'runtime_{self.frame}'
     if self.frame==720:
      pyxel.screencast(str(ROOT/'media/folding_station_automatic_silent.gif'),scale=1);self.save_events('replay.json');pyxel.quit()
     return
    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)
     return
    names=('X','C','SPACE','UP','RIGHT','DOWN','LEFT')
    pressed=[n for n in names if pyxel.btnp(getattr(pyxel,'KEY_'+n))]
    held=[n for n in names if pyxel.btn(getattr(pyxel,'KEY_'+n))]
    if len(pressed)>1 or (pressed and len(held)>1):self.reason='invalid';return
    if not pressed:return
    n=pressed[0]
    if n in ('X','C'):self.act('V' if n=='X' else 'H');return
    opts=options(self.board,self.state,self.suspend)
    if n=='SPACE':
     if self.selected is not None and self.selected<len(opts):self.act(opts[self.selected].action)
     else:self.reason='no_route';self.record('unselected')
     return
    if opts:
     self.selected=(0 if n in ('RIGHT','DOWN') else len(opts)-1) if self.selected is None else (self.selected+(1 if n in ('RIGHT','DOWN') else -1))%len(opts)
     self.reason='no_route';self.record('select',opts[self.selected].action)
 def dashed(self,a,b,c):
    x,y=a;u,v=b;steps=max(abs(u-x),abs(v-y),1)
    for i in range(0,int(steps)+1,5):
     j=min(i+2,steps);pyxel.line(x+(u-x)*i/steps,y+(v-y)*i/steps,x+(u-x)*j/steps,y+(v-y)*j/steps,c)
 def map(self,ox,oy,step,s,mini=False,reference=False):
    def pt(p):
     q=p if reference else project(p,s.v,s.h)
     return ox+q[0]*step,oy+q[1]*step
    width=4 if reference or not s.v else 2;height=4 if reference or not s.h else 2
    pyxel.rect(ox-step//2,oy-step//2,width*step,height*step,15 if not mini else 1)
    pyxel.rectb(ox-step//2,oy-step//2,width*step,height*step,4 if not mini else 5)
    for x in range(width):
     for y in range(height):pyxel.pset(ox+x*step,oy+y*step,4)
    if reference or not s.v:self.dashed((ox+1.5*step,oy-step/2),(ox+1.5*step,oy+3.5*step),13)
    if reference or not s.h:self.dashed((ox-step/2,oy+1.5*step),(ox+3.5*step,oy+1.5*step),13)
    for rail in self.board.rails:
     off=blocked(rail,s,self.suspend) if not reference else ()
     points=[pt(p) for p in rail.points]
     for a,b in zip(points,points[1:]):
      if off or mini:self.dashed(a,b,8 if off else 6)
      else:
       pyxel.line(a[0],a[1],b[0],b[1],5);pyxel.line(a[0]+2,a[1]+2,b[0]+2,b[1]+2,5)
     if off:
      x,y=points[len(points)//2];pyxel.line(x-4,y-4,x+4,y+4,8);pyxel.line(x+4,y-4,x-4,y+4,8)
    groups={}
    for st in self.board.stations:groups.setdefault(pt(st.pos),[]).append(st.id)
    for (x,y),ids in groups.items():
     if mini:
      pyxel.circb(x,y,3,10 if len(ids)>1 else 6)
      if s.station in ids:pyxel.pset(x,y,10)
      continue
     for j,id in enumerate(ids):
      bx=x-(len(ids)*20-2)//2+j*20;by=y-8
      pyxel.rect(bx,by,18,17,10 if id==s.station else 1);pyxel.rectb(bx,by,18,17,8 if id==self.board.goal else 5)
      self.text(bx+6,by+1,id,1 if id==s.station else 7)
      if id==s.station:pyxel.tri(bx+5,by-5,bx+13,by-5,bx+9,by-1,8)
    if not reference and not mini and self.animation and self.flash:
     before,after,action=self.animation
     if action.startswith('R:'):
      rail=next(r for r in self.board.rails if set((r.a,r.b))==set((before.station,after.station)))
      route=rail.points if rail.a==before.station else rail.points[::-1];t=(15-self.flash)/15*(len(route)-1);i=min(int(t),len(route)-2);frac=t-i;a,b=pt(route[i]),pt(route[i+1]);x=a[0]+(b[0]-a[0])*frac;y=a[1]+(b[1]-a[1])*frac;pyxel.rect(x-4,y-3,8,6,12)
 def draw(self):
    pyxel.cls(1);self.text(12,5,'025 折りたたみ駅',7);self.text(12,27,f'{"ABC"[self.level]}  {self.board.title}',10)
    self.text(320,7,'切符: '+self.state.station+' 駅',10);self.text(320,27,'目的: G + 全て開く',6)
    self.text(18,56,'いまの紙 / 駅の番号を選ぶ',7);self.text(310,56,'開いた図 / 参照',6)
    self.map(49,110,54,self.state);self.map(316,99,40,self.state,reference=True)
    self.text(18,309,'X  V:'+('閉' if self.state.v else '開'),10);self.text(139,309,'C  H:'+('未使用' if 'H' not in self.board.folds else '閉' if self.state.h else '開'),10)
    self.text(292,246,'X 後の予告',6);self.text(386,246,'C 後の予告' if 'H' in self.board.folds else '折れ目なし',6)
    nextv=transition(self.board,self.state,'V',self.suspend,terminal=False).state;self.map(302,276,16,nextv,mini=True)
    if 'H' in self.board.folds:
     nexth=transition(self.board,self.state,'H',self.suspend,terminal=False).state;self.map(401,276,16,nexth,mini=True)
    pyxel.line(12,332,468,332,5)
    opts=options(self.board,self.state,self.suspend)
    self.text(14,340,'行先',6)
    if not opts:self.text(78,340,'重なる駅も、乗れる線路もありません。',13)
    for i,o in enumerate(opts):
     y=340+i*20;selected=i==self.selected
     if selected:pyxel.rect(73,y-2,392,19,2)
     label=('乗換' if o.kind=='transfer' else '列車')+' → '+o.target+' 駅'
     if o.blocked:label+='  運休 ('+'/'.join(o.blocked)+')'
     self.text(78,y,('> ' if selected else '  ')+label,8 if o.blocked else 10 if selected else 7)
    message=MESSAGES['complete' if won(self.board,self.state) else self.reason]
    self.text(14,388,message,10 if won(self.board,self.state) else 7)
    hist=' / '.join(self.history[-3:]);self.text(14,408,hist if hist else '折る → 乗換 → 開く。駅の所属を運ぼう。',6)
    self.text(14,433,'N 次の面  Z 戻す  R 同じ面' if won(self.board,self.state) else '矢印 選ぶ  SPACE 移動  X 縦折り  C 横折り',7)
    self.text(14,454,'Z 戻す  R 最初から  P 説明  M 面選択',7)
    self.text(14,476,'自動入力・無音 / 実ゲーム映像' if self.demo else f'折り {self.state.folds} / 移動 {self.state.moves} / 時間制限なし',13)
    if not self.suspend:self.text(302,309,'比較: 運休なし',8)
    if self.title or self.pause:
     pyxel.rect(22,119,436,222,1);pyxel.rectb(22,119,436,222,7)
     lines=['紙に乗ったまま、駅を旅する。','Xは縦、Cは横。折ると遠い駅が重なる。','矢印で行先を選び、SPACEで乗換・列車。','折れ目をまたぐ線路は、折ると運休。','乗った駅の番号は切符に残ります。','G駅へ移り、紙を全部開けば到着。']
     for i,s in enumerate(lines):self.text(38,133+i*27,s,10 if i==0 else 7)
     self.text(38,309,'左右で面を選ぶ  SPACE 開始' if self.title else 'P 再開 / Zで何手でも戻せます',10)
    if self.snap:
     pyxel.screenshot(str(ROOT/f'media/{self.snap}.png'),scale=1);self.save_events('replay_partial.json' if self.demo else 'gui_events.json');self.snap=None
if __name__=='__main__':App()
