numtris_v05.py

Created by fime

Created on September 18, 2023

12.7 KB

Multi-mode tetris clone object-oriented, suporting versus. I think this wont work on the regular Epsilon, so please try it with Omega/Upsilon/Khi/etc. Needs some polishing that I won’t do….. everyone is free to continue this project with credit to me


from kandinsky import fill_rect,draw_string as dTxt
from time import monotonic as cTime,sleep
from random import randint as rInt,choice
from ion import keydown as key
from math import *
def rect(x,y,w,h,c):fill_rect(int(x),int(y),int(w),int(h),c)

TITLE=" NUMTRIS "
W,H=320,222
VOID,OUT,BLK,A,B,C,D,E,F,G=[c for c in "-*=ABCDEFG"]
PLAY,LINE_ANIM,DEATH,END=0,1,2,3
SOLO_A,SOLO_B,VS_A,VS_B=0,1,2,3
WON,LOST=0,1
TETRA="ITLJZSO"
TETRA_GRID=((8738, 4),(184, 3),(60, 3),(57, 3),(51, 3),(30, 3),(15, 2))
TETRA_COL=(A,B,C,D,E,F,G)

P1_KEYSET,P2_KEYSET=(0,3,1,2,12,13),(38,40,33,45,32,34)
def read_col(hexrvb):return [int("0x"+hexrvb[i:i+2]) for i in range(0,6,2)]

DARK,BRIGHT=(10,10,10),(255,255,255)
FG,BG=(255,255,255),(30,30,30)
BD,SL=(100,100,100),(200,0,0)
P1_COL,P2_COL='40a0f0','c86464'
CUBE_COL="""606060
ff2000
fff000
00ff30
00e0a0
0080ff
ff00e0
ffa000"""

def draw_title(x,y,j):
  rect(x,y,len(TITLE)*10,22,BG)
  c,j=SL,j%len(TITLE)
  for i,l in enumerate(TITLE):dTxt(l,x,y+(j!=i)*2,BRIGHT,c);x+=10
def fade_col(c1,c2,f):return tuple((1-f)*v1+f*v2 for v1,v2 in zip(c1,c2))
def draw_bg():
  rect(0,0,W,H,BG)
  tetras,c=[],BD
  for t in TETRA:tetras+=[load_tetra(t)]
  for i in range(W//20*H//20):
    draw_tetra(tetras[i%len(tetras)],10+i%(W//20)*20,10+i//(W//20)*20,2,c)
def draw_cube(x,y,s,c,typ=None):
  u=1
  rect(x,y,s,s,fade_col(c,DARK,0.5))
  rect(x+u,y+u,s-2*u,s-2*u,c)
def draw_void(x,y,w,h,c):
  rect(x,y,w,h,c)
  c=fade_col(c,BRIGHT,0.2)
  rect(x+w//2,y+h//2,1,1,c)
def draw_tetra(tetra,x,y,s,col):
  w=tetra[1]
  for i,c in enumerate(tetra[0]):
    if int(c):draw_cube(x+i%w*s,y+i//w*s,s,col)
def load_tetra(symb):
  i=TETRA.index(symb)
  r,w=TETRA_GRID[i]
  s=bin(r)[2:]
  return ["0"*(w**2-len(s))+s,w,TETRA_COL[i],[0,0]]
def str_set_char(string,i,char):
  return string[:i]+char+string[i+1:]
def sec(t):return cTime()-t
def transform_tetra(ttr,x=0,y=0,rot=0):
  n_ttr=ttr[:3]+[ttr[3][:]]
  n_ttr[3][0]+=x
  n_ttr[3][1]+=y
  rot%=4
  while rot>0:
    n_ttr[0],w,grid="",ttr[1],n_ttr[0]
    rot-=1
    for i in range(w**2):
      n_ttr[0]+=grid[(w-i-1)%w*w+i//w%w]
  return n_ttr
def draw_frame(x,y,w,h,c1,s=2):
  c2=fade_col(c1,(0,0,0),0.5)
  rect(x-2*s-1,y-2*s-1,w+4*s+2,h+4*s+2,c2)
  rect(x-2*s,y-2*s,w+4*s,h+4*s,c2)
  rect(x-s,y-2*s,w+3*s,h+3*s,c1)
  rect(x,y-2,w+2,h+2,c2)
def shuffle(lst):
  new=[]
  while len(new)<len(lst):
    c=choice(lst)
    if not c in new:new+=[c]
  return new
def open_color_set(set):
  return [read_col(hx) for hx in set.split("\n")]

class TetrisGame():
  games_cnt=0
  def __init__(
it,x=0,y=0,
w=10,h=20,
s=10,
col="FF0000",
bg_col="202020",
p=None,
lvl=0):
    TetrisGame.games_cnt+=1
    it.id=it.games_cnt+0
    it.grid=VOID*w*h
    it.x,it.y,it.w,it.h,it.s=x,y,w,h,s
    it.inputs=[]
    it.anim_stat=-1
    it.spd,it.ln_cnt,it.lvl,it.pnt=1,0,lvl,0
    it.bg_col,it.ln_col=read_col(bg_col),read_col(col)
    it.colors=open_color_set(CUBE_COL)
    it.ttr_lst=shuffle(TETRA)
    it.tetra=["",0,VOID,[0,0]]
    it.status,it.paused=PLAY,0
    it.blk_theshold,it.move_theshold=0.3,0.3
    it.tmrs={"fall":cTime(),"blkd":0,"move":-1,"rot":0,"drop":0,"anim":0}
    it.update_panel=p
    it.del_lines=[]
    draw_frame(it.x,it.y,it.w*it.s,it.h*it.s,it.ln_col)
    it.disp_grid()
    it.next_tetra(addPnt=0)
    
  def get_col_set(it,symb):
    return it.colors[(BLK,A,B,C,D,E,F,G).index(symb)]
      
  def process(it):
    if it.paused:return
    if it.status==PLAY:
      n_ttr=transform_tetra(it.tetra,y=1)
      if not (it.hit_box(n_ttr)):
        it.tmrs["blkd"]=0
        if sec(it.tmrs["fall"])>it.spd:
          it.tmrs["fall"]=cTime()
          it.disp_tetra(hide=1)
          it.tetra=n_ttr
          it.disp_tetra()
      elif it.tmrs["blkd"]==0:
        it.tmrs["blkd"]=cTime()
      elif it.tmrs["blkd"]>0 and sec(it.tmrs["blkd"])>it.blk_theshold:
        it.tmrs["blkd"]=0
        it.next_tetra()
      inp=it.inputs
      move=("R" in inp)-("L" in inp)
      if move:
        if it.tmrs["move"]<0:it.tmrs["move"]=cTime()
        elif cTime()-it.tmrs["move"]<it.move_theshold:move=0
        else:it.tmrs["move"]=0
      else:it.tmrs["move"]=-1
      fall,rot="D" in inp,(">" in inp)-("<" in inp)
      if rot:
        if not it.tmrs["rot"]:it.tmrs["rot"]=1
        else:rot=0
      else:it.tmrs["rot"]=0
      n_ttr=transform_tetra(it.tetra,x=move,y=fall,rot=rot)
      if not it.hit_box(n_ttr) and (move or fall or rot):
        it.disp_tetra(hide=1)
        it.pnt+=fall
        it.tetra=n_ttr
        it.disp_tetra()
      if "U" in inp and not it.tmrs["drop"] and not it.tmrs["blkd"]:
        it.disp_tetra(hide=1)
        it.tmrs["drop"]=1
        n_ttr=it.tetra
        c=0
        while not it.hit_box(n_ttr):
          c+=1
          n_ttr=transform_tetra(n_ttr,y=1)
        if c>0:
          it.tetra[3][1]=n_ttr[3][1]-1
          it.disp_tetra()
          it.pnt+=20
      elif not "U" in inp:it.tmrs["drop"]=0
    elif it.status==LINE_ANIM:
      if sec(it.tmrs["anim"])>0.1:it.tmrs["anim"]=cTime()
      else:return
      if it.anim_stat<0:it.anim_stat=0
      it.anim_stat+=1
      for l in it.detect_line():
        if it.anim_stat%2:draw_void(it.x,it.y+l*it.s,it.w*it.s,it.s,it.bg_col)
        else:
          for x in range(it.w):it.disp_case(x,l)
      if it.anim_stat==5:
        for l in it.detect_line():it.pop_line(l)
        it.disp_grid()
        it.disp_tetra()
        it.status=PLAY
        it.tmrs["fall"]=cTime()
        it.anim_stat=-1
    elif it.status==DEATH:
      if sec(it.tmrs["anim"])>0.1:it.tmrs["anim"]=cTime()
      else:return
      if it.anim_stat<0:it.anim_stat=0
      y=it.anim_stat
      it.set_line(y,BLK*it.w)
      for x in range(it.w):
        it.disp_case(x,y)
      if it.anim_stat==it.h-1:it.status=END
      it.anim_stat+=1
    it.inputs=[]
      
  def next_tetra(it,addPnt=1):
    it.ttr_lst.pop(0)
    it.ttr_lst+=[choice(TETRA)]
    it.tetra_2grid()
    it.tetra=load_tetra(it.ttr_lst[0])
    lines=len(it.detect_line())
    if addPnt:it.pnt+=(20,100,200,500,1000)[min(lines,4)]
    if lines!=0:
      it.status=LINE_ANIM
      it.lvl+=(it.ln_cnt%10+lines)//10
      it.ln_cnt+=lines
    else:
      it.tetra[3]=[it.w//2-2,0]
      if it.hit_box(it.tetra):it.status=DEATH
      it.disp_tetra()
    it.spd=1-(it.lvl/3.5/sqrt(it.lvl+1))
    try:it.update_panel()
    except:pass
        
  def pause(it):
    if it.paused==1:
      it.paused=0
      it.disp_grid()
      it.tmrs["fall"]=cTime()-it.tmrs["fall"]
      if it.status==PLAY:it.disp_tetra()
      return
    x,y,w,h=it.x,it.y,it.w*it.s,it.h*it.s
    it.paused=1
    it.tmrs["fall"]=cTime()-it.tmrs["fall"]
    rect(x,y,w,h,it.bg_col)
    it.disp_msg("PAUSED")
    
  def disp_msg(it,txt):
    x,y,w,h=it.x,it.y,it.w*it.s,it.h*it.s
    rect(x,it.y+h//2-10,w,20,it.ln_col)
    dTxt(txt,x+w//2-5*len(txt),y+h//2-10,BG,it.ln_col)

  def disp_grid(it):
    for x in range(it.w):
      for y in range(it.h):
        it.disp_case(x,y)

  def hit_box(it,tetra):
    scan=""
    w,symb,pos=tetra[1:]
    for i,c in enumerate(tetra[0]):
      if int(c):scan+=it.get_case(pos[0]+i%w,pos[1]+i//w)
    return scan!=4*VOID
  
  def randomize_grid(it,diff,start):
    for l in range(start,it.h):
      while not (0<it.get_line(l).count(VOID)<it.w):
        txt=""
        for i in range(it.w):txt+=choice((BLK,)*6+(VOID,)*(diff//2+2))
        it.set_line(l,txt)
    it.disp_grid()
    it.disp_tetra()

  def disp_tetra(it,hide=0):
    x,y=it.tetra[3]
    if hide:
      w=it.tetra[1]
      for i,c in enumerate(it.tetra[0]):
        if int(c):it.disp_case(i%w+x,i//w+y)
      return
    symb=it.tetra[2]
    draw_tetra(it.tetra,it.x+x*it.s,it.y+y*it.s,it.s,it.get_col_set(symb))
    
  def disp_case(it,xin,yin):
    ch,x,y,s=it.grid[it.w*yin+xin],it.x+(xin*it.s),it.y+(yin*it.s),it.s
    if ch==VOID:
      draw_void(x,y,s,s,it.bg_col)
      return
    draw_cube(x,y,s,it.get_col_set(ch))
  
  def get_case(it,x,y):
    if 0<=x<it.w and 0<=y<it.h:return it.grid[y*it.w+x]
    else:return OUT

  def set_line(it,id,line):
    it.grid=it.grid[:it.w*id]+line+it.grid[it.w*(id+1):]

  def get_line(it,id):
    return it.grid[it.w*id:it.w*(id+1)]

  def pop_line(it,id):
    it.del_lines+=[id]
    prev_grid=it.grid[:]
    for y in range(1,id+1):
      it.set_line(y,prev_grid[(y-1)*it.w:y*it.w])
    it.set_line(0,VOID*it.w)

  def detect_line(it):
    lines_id=[]
    for i in range(it.h):
      if not VOID in it.get_line(i):
        lines_id+=[i]
    return lines_id
  
  def tetra_2grid(it):
    ctt,w,c,pos=it.tetra
    for i,v in enumerate(ctt):
      if int(v):
        it.grid=str_set_char(it.grid,pos[0]+pos[1]*it.w+i%w+i//w*it.w,c)
            
def init_panel(x,y,w,h,c,games,mode):
  rect(x,y,w,h,BG)
  for i,game in enumerate(games):
    x2,y2=x+w//2+(-16,(-38,6)[i])[mode>SOLO_B],y+h-40
    draw_frame(x2,y2,32,32,game.ln_col,1)
  txt="SCORE"
  dTxt(txt,x+w//2-len(txt)*4,y+5,c,BG,1)
  txt="LINES  LVL"
  dTxt(txt,x+w//2-len(txt)*4,y+h//2-30,c,BG,1)
  txt="NEXT"
  dTxt(txt,x+w//2-len(txt)*4,y+h-60,c,BG,1)

def update_panel(x,y,w,h,c,games,mode):
  for i,game in enumerate(games):
    txt=str(game.pnt)
    dTxt(txt,x+w//2-len(txt)*5,y+20+20*i,game.ln_col,BG)
    txt="%i "%(game.ln_cnt)
    dTxt(txt,x+w//2-len(txt)*10,y+h//2-10+20*i,game.ln_col,BG)
    txt=" %i"%(game.lvl)
    dTxt(txt,x+w//2,y+h//2-10+20*i,game.ln_col,BG)
    symb=game.ttr_lst[1]
    ttr_id=TETRA.index(symb)
    x2,y2=x+w//2+(-16,(-38,6)[i])[mode>SOLO_B],y+h-40
    rect(x2,y2,32,32,BG)
    draw_tetra(load_tetra(symb),x2+(4-TETRA_GRID[ttr_id][1])*4,y2+(4-TETRA_GRID[ttr_id][1])*4,8,game.get_col_set(TETRA_COL[ttr_id]))
  rect(x+w//2,y+h//2-30,1,60,c)

def play_game(mode=SOLO_A,diff=0,w=10,h=20):
  draw_bg()
  vs=mode>1
  s=(10,8)[vs]
  offset=(W-(w*s)*(1+vs)-90)//(3+vs)
  panel_prop=(offset*2+s*w,10,90,200,(150,150,150))
  def disp_panel():update_panel(*panel_prop+(games,mode))
  games=[TetrisGame(x=offset,y=10,w=w,h=h,col=P1_COL,s=s,p=disp_panel,lvl=diff)]
  if vs:games+=[TetrisGame(x=w*s+90+3*offset,y=10,w=w,h=h,col=P2_COL,s=s,p=disp_panel,lvl=diff)]
  keysets=(P1_KEYSET,P2_KEYSET)[:vs+1]
  pause_btn=0
  lines_to_del=h//2+h//3-diff//2
  if mode in (SOLO_B,VS_B):
    for game in games:game.randomize_grid(diff,lines_to_del)
  end=()

  draw_frame(*panel_prop)
  init_panel(*panel_prop+(games,mode))
  disp_panel()
  while end==():
    if key(17):
      if not pause_btn:
        for game in games:game.pause()
      pause_btn=1
    else:pause_btn=0
    for game,keyset in zip(games,keysets):
      for i,k in enumerate(keyset):
        if key(k):game.inputs+=["LRUD<>"[i]]
      game.process()
      if game.status==END:
        end=(LOST,game.id)
      if mode in (SOLO_B,VS_B) and game.status==PLAY:
        if not BLK in game.grid:end=(WON,game.id)
  for game in games:
    w=(end[0]==WON)==(end[1]==game.id)
    game.disp_msg(("LOST","WON")[w])

class Elmnt:
  H,TYPE=0,None
  def __init__(it,name):it.name=name
class Sld(Elmnt):
  H,TYPE=44,"sld"
  def __init__(it,name,content):
    super().__init__(name)
    it.content,it.selec=content,0
  def draw(it,x,y,hilight=0):
    dTxt(it.name,x,y,FG,BG)
    y+=20
    for i,elem in enumerate(it.content):
      elem="%s"%(elem)
      w=len(elem)*10+8
      rect(x,y,w,22,(BD,SL)[hilight*(i==it.selec)])
      rect(x+2,y+2,w-4,18,BG)
      dTxt(elem,x+4,y+2,(BD,FG)[i==it.selec],BG)
      x+=w
  def slide(it,x):
    it.selec+=x
    it.selec%=len(it.content)
class Btn(Elmnt):
  H,TYPE=24,"btn"
  def draw(it,x,y,hilight=0):
    txt="→ %s"%(it.name)
    w=len(txt)*10+8
    rect(x,y,w,22,(BD,SL)[hilight])
    rect(x+2,y+2,w-4,18,BG)
    dTxt(txt,x+4,y+2,FG,BG)
class Sep(Elmnt):
  H,TYPE=2,"sep"
  def draw(it,x,y,hilight=0):rect(x,y,50,2,BD)
class Lbl(Elmnt):
  H,TYPE=18,"lbl"
  def draw(it,x,y,hilight=0):dTxt(it.name,x,y,FG,BG)
      
def menu(
  elems=[
Btn("Play"),
Sld("Level",list(range(0,10))),
Sld("Mode",["Solo","Versus"]),
Sld("Game type",["Survival","Mining"]),
Btn("Back")]):
  X,s=130,0
  while elems[s].TYPE in ("sep","lbl"):s=(s+1)%len(elems)
  def get_elem(name):
    for e in elems:
      if e.name==name:return e
  def draw():
    y=10
    for i,elem in enumerate(elems):
      elem.draw(X,y,s==i)
      y+=elem.H+4
  draw_bg()
  draw()
  j,t=0,cTime()
  setup_demo=lambda:TetrisGame(25,50,lvl=20,s=8,col="808080")
  demo=setup_demo()
  while 1:
    if cTime()-t>0.2:t=cTime();j+=1;draw_title(20,20,j)
    demo.inputs+=[choice("LRUD<>")]
    demo.process()
    if demo.status==DEATH:demo=setup_demo()
    scan=[k for k in (0,1,2,3,4) if key(k)]
    if scan==[]:continue
    if elems[s].TYPE=="sld":
      if 0 in scan:elems[s].slide(-1)
      if 3 in scan:elems[s].slide(1)
    if elems[s].TYPE=="btn" and 4 in scan:break
    up=0
    if 1 in scan:up=-1
    if 2 in scan:up=1
    s=(s+up)%len(elems)
    while elems[s].TYPE in ("sep","lbl"):s=(s+up)%len(elems)
    draw()
    sleep(0.1)
  mode=get_elem("Game type").selec+get_elem("Mode").selec*2
  diff=get_elem("Level").selec
  btn=elems[s].name
  return btn,mode,diff

btn,mode,diff=menu()
if btn=="Play":
  play_game(mode=mode,diff=diff)