# title: ひと箱の天気
# Original pixel weather-box art. Silent. Noto-derived Japanese glyphs: SIL OFL.
from pathlib import Path
from dataclasses import replace
import json,sys,hashlib
import pyxel
from model import *
ROOT=Path(__file__).resolve().parent
W=384;H=432;CELL=44;OX=60;OY=70;DY=38
NAMES={'S':'源','F':'花','B':'受け皿','K':'開閉皿','roof':'屋根','drain':'細管','inlet':'取入口','pipe':'細管','gate':'水門'}
REASONS={'ready':'花の土へ、2滴を届けよう。','move':'雨の入口と、日差しの先を見よう。','sun':'1滴を皿から雲へ。水は増えない。','rain':'1滴を雨に。流れ着く先を見よう。','full':'雲は3滴まで。先に降らせよう。','no_dish':'真下に皿がない。','shade':'日差しは屋根・閉水門を通れない。','dry':'この皿は空。','empty':'雲は空。皿から日差しで集めよう。','edge':'箱の端。水は箱の中を巡る。','invalid':'矢印はひとつずつ。','undo':'雲も水も、一手前へ。','complete':'花が咲いた！','reset':'同じ箱を、最初から。'}

class App:
 def __init__(self):
  self.glyph=json.loads((ROOT/'assets/glyphs.json').read_text(encoding='utf-8'))
  self.level=0;self.baseline='--baseline' in sys.argv;self.title=True;self.pause=False;self.lock=False;self.action_lock=False
  self.frame=0;self.demo='--demo' in sys.argv;self.events=[];self.snap=None;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(W,H,title='030 ひと箱の天気',fps=30,display_scale=2,capture_sec=30,headless='--headless' in sys.argv)
  pyxel.run(self.update,self.draw)
 @property
 def board(self):return replace(LEVELS[self.level],gate_linked=False) if self.baseline else LEVELS[self.level]
 @property
 def state(self):return self.timeline.state
 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.board));self.pause=False;self.reason='ready';self.animation=();self.anim_frame=0;self.action_lock=False
 def record(self,kind,action=None):
  self.events.append({'frame':self.frame,'event':kind,'action':action,'level':self.level+1,'moves':self.state.moves,'x':self.state.x,'cloud':self.state.cloud,'water':self.state.water,'absorbed':self.state.absorbed,'total':total(self.state),'gate_open':gate_open(self.board,self.state.water),'won':won(self.board,self.state),'reason':self.reason,'pause':self.pause,'title':self.title,'hash':state_hash(self.state)})
 def move(self,action):
  self.timeline,r=apply(self.board,self.timeline,action);self.reason=r.reason
  self.animation=r.trace if r.accepted and action in 'UD' else ();self.anim_frame=0
  self.record('action' if r.accepted else 'blocked',action)
 def back(self):
  self.timeline=undo(self.timeline);self.reason='undo';self.animation=();self.anim_frame=0;self.record('undo')
 def switch(self,n):self.level=n%3;self.reset();self.record('level')
 def update(self):
  self.frame+=1
  if pyxel.btnp(pyxel.KEY_F12):self.snap='manual_runtime';self.record('screenshot')
  if self.animation and not self.pause:
   self.anim_frame+=1
   if self.anim_frame>=len(self.animation)*4:self.animation=()
  if self.demo:
   schedule={25:('start',None),175:('level',1),430:('level',2)}
   for f,path in ((42,'UURRRRDD'),(195,'UURRRRDLURRDD'),(450,'UUURRDRRDD')):
    for i,a in enumerate(path):schedule[f+i*16]=('move',a)
   if self.frame in schedule:
    op,a=schedule[self.frame]
    if op=='start':self.title=False;self.record('start')
    elif op=='level':self.switch(a)
    else:self.move(a)
   if self.frame in (160,306,332,416,552,622,650):self.snap=f'runtime_{self.frame}'
   if self.frame==750:
    pyxel.screencast(str(ROOT/'media/box_weather_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);self.lock=True
   elif pyxel.btnp(pyxel.KEY_RIGHT):self.switch(self.level+1);self.lock=True
   return
  if pyxel.btnp(pyxel.KEY_M):self.title=True;self.pause=False;self.lock=True;self.animation=();self.record('menu');return
  if pyxel.btnp(pyxel.KEY_R):self.reset();self.lock=True;self.reason='reset';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();self.lock=True;return
  if won(self.board,self.state):
   if pyxel.btnp(pyxel.KEY_N):self.switch(self.level+1);self.lock=True
   return
  # Observe release even during transport; a fresh press on the first free frame counts.
  if self.action_lock and not any(pyxel.btn(k) for k in self.arrows):self.action_lock=False
  if self.animation:
   if any(pyxel.btnp(k) for k in self.arrows):self.action_lock=True
   return
  if self.action_lock:return
  keys=[a for k,a in self.arrows.items() if pyxel.btnp(k)]
  held=sum(bool(pyxel.btn(k)) for k in self.arrows)
  if len(keys)==1 and held<=1:self.move(keys[0]);self.action_lock=True
  elif len(keys)>1 or (keys and held>1):self.reason='invalid';self.action_lock=True
 def save_events(self,name):
  data={'label':'自動入力の機能確認・無音 / actual Pyxel renderer' if self.demo else 'Agent-operated native Linux input 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 xy(self,n):return OX+n.x*CELL,OY+n.y*DY
 def arrow(self,x1,y1,x2,y2,c):
  pyxel.line(x1,y1,x2,y2,c)
  dx=x2-x1;dy=y2-y1;d=max(abs(dx),abs(dy),1);dx/=d;dy/=d
  pyxel.line(x2,y2,x2-dx*6+dy*3,y2-dy*6-dx*3,c);pyxel.line(x2,y2,x2-dx*6-dy*3,y2-dy*6+dx*3,c)
 def drop(self,x,y,full,c=12):
  if full:pyxel.tri(x,y-4,x-3,y+1,x+3,y+1,c);pyxel.circ(x,y+2,3,c)
  else:pyxel.circb(x,y+2,3,5)
 def draw(self):
  l=self.board;s=self.state
  water=self.animation[min(self.anim_frame//4,len(self.animation)-1)] if self.animation else s.water
  active=replace(s,water=water);opened=gate_open(l,water)
  pyxel.cls(1);self.text(12,5,'030 ひと箱の天気',7);self.text(12,25,f'{"ABC"[self.level]}  {l.title}',10)
  self.text(288,5,f'総水量 {total(active)}滴',6)
  pyxel.rect(24,48,336,252,0);pyxel.rectb(24,48,336,252,5);pyxel.rectb(27,51,330,246,13)
  for i in range(7):
   x=OX+i*CELL
   self.text(x-3,52,str(i+1),5)
   for yy in range(93,270,9):pyxel.pset(x,yy,1)
  # The bottom return gutter catches every miss and every overflow; never deletes water.
  self.arrow(348,286,104,286,6);self.arrow(104,286,104,275,6)
  self.text(30,276,'戻り樋',6)
  # Pipes are narrow transparent conduits, not roofs. They do not intercept sun/rain.
  for n in l.nodes:
   if n.target and n.target!='S' and n.kind!='dish':
    target=l.nodes[index(l,n.target)];x,y=self.xy(n);xx,yy=self.xy(target)
    color=5 if n.kind=='gate' and not opened else 6
    self.arrow(x,y+5,xx,yy-13,color)
   if n.kind=='gate':
    x,y=self.xy(n);self.arrow(x+18,y,348,y,5 if opened else 6);self.arrow(348,y,348,285,5 if opened else 6)
    # Dashed mechanical linkage: K's one drop is the physical counterweight.
    for yy in range(160,247,7):pyxel.line(192,yy,192,yy+2,9)
    for xx in range(192,272,7):pyxel.line(xx,160,xx+2,160,9)
  sun,rain=preview(l,s)
  for i,n in enumerate(l.nodes):
   x,y=self.xy(n)
   if n.kind=='dish':
    pyxel.rect(x-19,y-7,38,20,4);pyxel.rect(x-16,y-7,32,15,1);pyxel.line(x-20,y-9,x-20,y+13,9);pyxel.line(x+20,y-9,x+20,y+13,9)
    for k in range(n.capacity):self.drop(x-10+k*10,y-3,k<water[i])
    self.text(x-18,y+15,f'{n.name} {water[i]}/{n.capacity}',7)
    if n.name=='F':
     pyxel.line(x,y-11,x,y-34,11);pyxel.line(x,y-21,x-7,y-27,3)
     bloom=water[i]>=2 and opened
     for dx,dy in ((0,-5),(5,0),(0,5),(-5,0)):pyxel.circ(x+dx,y-36+dy,4,10 if bloom else 5)
     pyxel.circ(x,y-36,3,9 if bloom else 3);self.text(x+23,y-13,'花',10)
    elif n.name=='K':self.text(x-17,y-27,'開閉皿',9)
    elif n.name=='S':self.text(x-12,y-27,'源',6)
    elif n.name=='B':self.text(x-18,y-27,'受け皿',6)
   elif n.kind=='roof':
    pyxel.tri(x-21,y-9,x+21,y-9,x-18,y+6,4);self.arrow(x+13,y-4,x-17,y+7,10);self.text(x-12,y-27,'屋根',9)
   elif n.kind=='inlet':
    pyxel.line(x-17,y-9,x-9,y+8,6);pyxel.line(x+17,y-9,x+9,y+8,6);pyxel.line(x-9,y+8,x+9,y+8,6);self.text(x-18,y-27,'取入口',6)
   elif n.kind=='gate':
    pyxel.rect(x-19,y-10,38,14,3 if opened else 4);pyxel.rectb(x-19,y-10,38,14,11 if opened else 9)
    if opened:pyxel.rect(x-8,y-10,16,14,0)
    self.text(x-18,y-30,'門:開' if opened else '門:閉',11 if opened else 9)
   else:pyxel.circb(x,y,4,6)
   if n.kind!='dish' and water[i]:self.drop(x,y-3,True)
   if not self.title and not self.pause:
    if i==rain:pyxel.rectb(x-23,y-13,47,29,12)
    if i==sun:pyxel.rectb(x-21,y-11,43,25,10)
  # Cloud belly always shows exactly three capacity slots.
  cx=OX+s.x*CELL;cy=78
  pyxel.elli(cx-22,cy-8,44,22,7);pyxel.circ(cx-8,cy-8,12,7);pyxel.circ(cx+7,cy-10,11,7)
  for i in range(3):self.drop(cx-11+i*11,cy-1,i<s.cloud,12)
  if self.reason=='sun' and self.animation:pyxel.circb(cx-29,cy-5,5,10)
  # Meaningful counts and pre-action local preview; never displays a solution sequence.
  pyxel.rect(0,302,W,130,1)
  success=won(l,s) and not self.animation
  msg='花が咲いた！ 水は箱の中に残る。' if success else REASONS[self.reason]
  self.text(10,305,msg,10 if success else 7)
  if self.level==2:
   self.text(10,325,'花2滴 ＋ 開閉皿1滴で門を開く' if not self.baseline else '比較用: 門は常に開く',9)
  else:self.text(10,325,'花2滴で開花。余りの水は源へ戻る。',6)
  sunlabel='なし' if sun is None else NAMES[l.nodes[sun].name]
  self.text(10,345,f'日差し: {sunlabel}   雨: {NAMES[l.nodes[rain].name]}',6)
  self.text(10,365,'N 次の箱  Z 一手戻す  R 最初から' if success else '左右 雲  上 日差し  下 雨  Z 戻す',7)
  self.text(10,385,'R 最初から  P 説明  M 箱選択',7)
  self.text(10,407,'自動入力の機能確認・無音' if self.demo else '長押しなし。1回押して、1滴。',13)
  if self.title or self.pause:
   pyxel.rect(18,94,348,180,1);pyxel.rectb(18,94,348,180,7)
   self.text(102,106,'花に降らせる天気箱',10)
   self.text(32,133,'左右で雲。上で皿から1滴を雲へ。')
   self.text(32,155,'下で雨1滴。花に2滴で開花。')
   self.text(32,177,'屋根・閉水門は日差しを遮る。')
   self.text(32,199,'細管は透明。水を減らさず運ぶ。')
   self.text(32,221,'失う水はなし。Zで一手戻せる。')
   self.text(32,249,'左右で箱選択  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()
