# CROSSY ROAD for NumWorks (N0110 / N0115 / N0120)
# Arrows: hop | OK / EXE: start & retry
# Close-up voxel view; dirty-region drawing, 30 Hz frame budget.
from kandinsky import fill_rect as F,draw_string as S
from ion import *
from time import sleep,monotonic
from random import randint as ri,choice as ch
W=320;C=28;R=7;TOP=26
GA=(142,214,74);GB=(130,202,64);RD=(84,88,100);WT=(80,190,240)
LG=(160,100,52);TR=(56,160,64);NV=(30,40,80);WH=(255,255,255)
CARS=((236,64,64),(64,128,236),(255,196,40),(170,90,210),(250,130,40))
GL={'C':'111100100100111','R':'110101110101101','O':'111101101101111','S':'111100111001111','Y':'101101010010010','A':'010101111101101','D':'110101101101110'}
KS=(KEY_UP,KEY_DOWN,KEY_LEFT,KEY_RIGHT);pv=[0]*4;sh=[0]
def dk(c):return (c[0]*3//5,c[1]*3//5,c[2]*3//5)
def big(t,x,y,s):
  for o,c in((s//2,NV),(0,WH)):
    for k in range(len(t)):
      g=GL[t[k]]
      for b in range(15):
        if g[b]=='1':F(x+k*4*s+(b%3)*s+o,y+(b//3)*s+o,s,s,c)
def box(x,y,w,h,c):
  x=int(x);y=int(y);a=max(0,x);b=min(W,x+w)
  if b>a:F(a,y,b-a,h,c)
def B(x,y,a,b,w,h,c):box(x+a*C//20,y+b*C//20,max(1,w*C//20),max(1,h*C//20),c)
def mk(t,safe=0):
  if t==0:
    sh[0]^=1;tr=[]
    for _ in range(ri(0,2)):
      c=ri(0,10)
      if not(safe and 4<c<8):tr.append(c)
    col=GA if sh[0] else GB
    return [0,0,[],0,col,tr,dk(col)]
  s=ch((1,1.5,2,2.5))*ch((-1,1))
  if t==1:n=ri(1,2);l=ch((42,42,70));c=ch(CARS)
  else:n=ri(2,3);l=ch((84,112));c=LG
  g=W//n
  return [t,s,[i*g+ri(0,max(0,g-l-10)) for i in range(n)],l,c,[],dk(c)]
def nl(p):return mk(ri(1,2)) if p[0]==0 else mk(ch((0,0,1,1,2)))
def ground(i,x=0,w=W):
  L=lanes[i];y=TOP+i*C;c=L[4] if L[0]==0 else RD if L[0]==1 else WT
  box(x,y,w,C,c);box(x,y+24,w,4,dk(c))
  if L[0]==1:
    for k in range(0,W,56):
      a=max(int(x),k+17);b=min(int(x+w),k+39)
      if b>a:box(a,y,b-a,2,(190,190,190))
  for k in L[5]:
    z=k*C
    if z+28>x and z<x+w:
      B(z,y,4,15,16,4,L[6]);B(z,y,2,11,16,6,(33,96,38));B(z,y,2,1,16,10,TR)
def objects(i):
  L=lanes[i];y=TOP+i*C;l=L[3];c=L[4]
  for x in L[2]:
    x=int(x)
    if x+l+5<0 or x>W:continue
    if L[0]==1:
      box(x+4,y+21,l,5,(50,52,60));box(x,y+14,l,8,L[6]);box(x,y+3,l,11,c)
      box(x+l//4,y+4,l//2,7,(200,230,255));box(x+4,y+22,7,4,(20,20,20));box(x+l-11,y+22,7,4,(20,20,20))
    else:
      box(x,y+18,l,6,L[6]);box(x,y+5,l,13,LG);box(x,y+5,4,13,(210,160,100))
def me():
  x=int(px);y=TOP+py*C
  B(x,y,5,15,14,3,(40,40,40));B(x,y,3,11,14,6,(200,200,210));B(x,y,3,2,14,9,WH)
  B(x,y,8,0,4,3,(230,50,50));B(x,y,8,12,4,3,(255,150,30));B(x,y,5,6,2,2,(0,0,0));B(x,y,13,6,2,2,(0,0,0))
def hud():S(str(sc)+'   ',6,3,WH,NV)
def full():
  for i in range(R):ground(i);objects(i)
  me();hud()
def blk(L,c):return L[0]==0 and c in L[5]
def splash():
  F(0,0,W,222,WT);F(0,150,W,72,GA)
  big('CROSSY',68,26,8);big('ROAD',100,78,8);S('Press OK to play',80,170,WH,NV)
  while not(keydown(KEY_OK) or keydown(KEY_EXE)):sleep(0.05)
def reset():
  global lanes,px,py,sc,dead
  lanes=[mk(0)]
  for i in range(R-1):lanes.append(nl(lanes[-1]))
  lanes.reverse()
  for i in range(4,R):lanes[i]=mk(0,1)
  px=168;py=5;sc=0;dead=0
  for j in range(4):pv[j]=keydown(KS[j])
  F(0,0,W,TOP,NV);big('CROSSY',104,4,3);big('ROAD',181,4,3);full()
def keys():
  r=-1
  for j in range(4):
    d=keydown(KS[j])
    if d and not pv[j] and r<0:r=j
    pv[j]=d
  return r
splash();reset()
while 1:
  started=monotonic()
  if dead:
    if keydown(KEY_OK) or keydown(KEY_EXE):reset()
    sleep(0.04);continue
  o=py;ox=px;k=keys();cl=round(px/C);mv=0;scroll=0
  if k==0 and not blk(lanes[py-1],cl):
    sc+=1;mv=1
    if py<=3:lanes.pop();lanes.insert(0,nl(lanes[0]));scroll=1
    else:py-=1
  elif k==1 and py<R-1 and not blk(lanes[py+1],cl):py+=1;mv=1
  elif k==2 and px>=C and not blk(lanes[py],cl-1):px-=C;mv=1
  elif k==3 and px<=W-2*C and not blk(lanes[py],cl+1):px+=C;mv=1
  if mv and lanes[py][0]!=2:px=round(px/C)*C
  # Erase old sprites first, then paint each moving row just once.
  if not scroll:ground(o,int(ox),C)
  for i in range(R):
    L=lanes[i]
    if L[0]:
      l=L[3];m=W+l+28
      for j in range(len(L[2])):
        x=L[2][j]
        if not scroll:ground(i,int(x),l+5)
        L[2][j]=(x+L[1]+l+28)%m-l-28
  L=lanes[py]
  if L[0]==2:px+=L[1]
  cx=px+C/2;hit=0
  for x in L[2]:
    if x<cx<x+L[3]:hit=1
  if L[0]==2:
    if not hit or px<-5 or px>W-C+5:dead=1
  elif L[0]==1 and hit:dead=1
  if scroll:full()
  else:
    for i in range(R):
      if lanes[i][0]:objects(i)
    me()
    if mv:hud()
  if dead:
    F(70,80,180,64,NV);S('GAME OVER',115,88,(255,210,40),NV)
    S('Score '+str(sc),115,106,WH,NV);S('OK to retry',105,124,WH,NV)
  sleep(max(0,1/30-(monotonic()-started)))
