local Y=require("yaru.core") local gfx=require("yaru.gfx") local ui=require("yaru.ui") local M={} local BW,BH=10,20 local SHAPES={ {{0,1},{1,1},{2,1},{3,1},c=0x33DBFF}, {{0,0},{1,0},{0,1},{1,1},c=0xFFDB40}, {{1,0},{0,1},{1,1},{2,1},c=0xCC49BF}, {{1,0},{2,0},{0,1},{1,1},c=0x33DB40}, {{0,0},{1,0},{1,1},{2,1},c=0xFF2440}, {{0,0},{0,1},{1,1},{2,1},c=0x0049FF}, {{2,0},{0,1},{1,1},{2,1},c=0xFF9240}, } local BEST="/home/.local/share/quadrapassel.best" function M.open() local st={} local win=Y.newWindow{app="tetris",title="Quadrapassel",w=58,h=34,fixed=true} local function cells(p,rot,px,py) local r={} for _,b in ipairs(SHAPES[p])do local x,y=b[1],b[2] for _=1,rot%4 do if p~=2 then x,y=(p==1 and 3 or 2)-y,x end end r[#r+1]={px+x,py+y} end return r end local function fits(p,rot,px,py) for _,c in ipairs(cells(p,rot,px,py))do if c[1]<1 or c[1]>BW or c[2]>BH then return false end if c[2]>=1 and st.grid[(c[2]-1)*BW+c[1]]then return false end end return true end local function spawn() st.p,st.rot,st.x,st.y=st.nextP or math.random(#SHAPES),0,4,0 st.nextP=math.random(#SHAPES) if not fits(st.p,st.rot,st.x,st.y)then st.over=true if st.score>st.best then st.best=st.score require("filesystem").makeDirectory("/home/.local/share") local f=io.open(BEST,"w")if f then f:write(tostring(st.best))f:close()end end Y.notify("Game over","Score "..st.score..", "..st.lines.." lines",{actionLabel="New Game",action=function()st.reset()end}) end end function st.reset() st.grid,st.score,st.lines,st.level,st.over,st.paused={},0,0,1,false,false local f=io.open(BEST)st.best=f and tonumber(f:read("*a"))or 0 if f then f:close()end st.nextP=nil spawn() end local function lock() for _,c in ipairs(cells(st.p,st.rot,st.x,st.y))do if c[2]>=1 then st.grid[(c[2]-1)*BW+c[1]]=SHAPES[st.p].c end end local cleared=0 for y=BH,1,-1 do local full=true for x=1,BW do if not st.grid[(y-1)*BW+x]then full=false break end end if full then cleared=cleared+1 for yy=y,2,-1 do for x=1,BW do st.grid[(yy-1)*BW+x]=st.grid[(yy-2)*BW+x]end end for x=1,BW do st.grid[x]=nil end end end if cleared>0 then for y=BH,1,-1 do local full=true for x=1,BW do if not st.grid[(y-1)*BW+x]then full=false break end end if full then return lock()end end end st.lines=st.lines+cleared st.score=st.score+({0,100,300,500,800})[cleared+1]*st.level st.level=1+st.lines//10 spawn() end local function step() if fits(st.p,st.rot,st.x,st.y+1)then st.y=st.y+1 else lock()end end local function shade(c,f) local r,g,b=(c>>16)*f,((c>>8)&255)*f,(c&255)*f return math.min(255,math.floor(r))<<16|math.min(255,math.floor(g))<<8|math.min(255,math.floor(b)) end local PX=3 local function block(px,py,c,ghost) if ghost then gfx.prect(px,py,PX,1,c);gfx.prect(px,py+PX-1,PX,1,c) gfx.prect(px,py,1,PX,c);gfx.prect(px+PX-1,py,1,PX,c) return end gfx.prect(px,py,PX,PX,c) gfx.prect(px,py,PX,1,shade(c,1.35));gfx.prect(px,py,1,PX,shade(c,1.35)) gfx.prect(px+1,py+PX-1,PX-1,1,shade(c,0.6));gfx.prect(px+PX-1,py+1,1,PX-1,shade(c,0.6)) end function win.draw(w,cw,ch) local t=Y.theme w.tickInterval=(not st.over and not st.paused)and math.max(0.1,0.6-(st.level-1)*0.05)or nil local ox,oy=3,3 gfx.prrect(ox-1,oy-1,BW*PX+2,BH*PX+2,0x1E1E1E) gfx.prect(ox,oy,BW*PX,BH*PX,0x0F0F0F) for y=1,BH do for x=1,BW do local c=st.grid[(y-1)*BW+x] if c then block(ox+(x-1)*PX,oy+(y-1)*PX,c)end end end if not st.over then local gy=st.y while fits(st.p,st.rot,st.x,gy+1)do gy=gy+1 end for _,c in ipairs(cells(st.p,st.rot,st.x,gy))do if c[2]>=1 then block(ox+(c[1]-1)*PX,oy+(c[2]-1)*PX,0x3C3C3C,true)end end for _,c in ipairs(cells(st.p,st.rot,st.x,st.y))do if c[2]>=1 then block(ox+(c[1]-1)*PX,oy+(c[2]-1)*PX,SHAPES[st.p].c)end end end local x=ox+BW*PX+4 gfx.text(x,2,"Next",t.dim) for _,c in ipairs(cells(st.nextP,0,0,0))do block(x+c[1]*PX,6+c[2]*PX,SHAPES[st.nextP].c)end local info={{"Score",st.score},{"Lines",st.lines},{"Level",st.level},{"Best",st.best}} for i,kv in ipairs(info)do gfx.text(x,7+i*2,kv[1],t.dim) gfx.text(x+8,7+i*2,tostring(kv[2]),t.fg) end ui.button(w,x,19,st.over and"New Game"or(st.paused and"Resume"or"Pause"),function() if st.over then st.reset()else st.paused=not st.paused end end,st.over and"suggested"or nil,12) gfx.text(x,22,"←→ move ↑ rotate",t.dim) gfx.text(x,23,"↓ drop Space: slam",t.dim) if st.paused then gfx.text(ox+BW*PX//2-3,(oy+BH*PX//2)//2,"Paused",0xFFFFFF,0x0F0F0F)end end function win.tick(w)if not st.over and not st.paused then step()end Y.redraw(w)end function win.key(w,char,code) local K=Y.K if char==112 or char==80 then st.paused=not st.paused return end if st.over or st.paused then if code==K.enter then st.reset()end return end if code==K.left and fits(st.p,st.rot,st.x-1,st.y)then st.x=st.x-1 elseif code==K.right and fits(st.p,st.rot,st.x+1,st.y)then st.x=st.x+1 elseif code==K.up then for _,kick in ipairs({0,-1,1,-2,2})do if fits(st.p,st.rot+1,st.x+kick,st.y)then st.rot,st.x=st.rot+1,st.x+kick break end end elseif code==K.down then step();st.score=st.score+1 elseif char==32 then while fits(st.p,st.rot,st.x,st.y+1)do st.y=st.y+1;st.score=st.score+2 end lock() end end function win.onclose()st.paused=true return true end st.reset() return win end return M