Tetris Program in 12 lines

Posted by TJ Wei on 星期五, 4月 27, 2007 with No comments
前幾天看到這個用 100 行 python 寫的 Tetris ,對一個小程式來說, 100 行實在是太長了,直覺上覺得不需要這麼長的程式來寫 Tetris。
我覺得大概 50 行左右吧,前提是要某種程度的 readable,就像那個 100 行的程式一樣。
經過修改之後,改出了 30 行的程式,程式如下:
import sys,random
from pygame import *
score,bw,bh,tickcnt,TICK=0,10,20,0,USEREVENT+1
blk={0xf:0xff0000,0x2e:0xff00,0x27:0xff,0x47:0xffff00,0x66:0xffff,0xC6:0x7fff,0x6C:0xff00ff}
board=[[0xf if j== bh else 0]*bw+[0xf]*3 for j in range(bh+3)]
new_piece=lambda pc:([((z>>2)+1,z&3) for z in range(16) if (pc>>z)&1],3,-2,pc)
piece,px,py,pc=new_piece(random.choice(blk.keys()))
collide=lambda piece,px,py: [1 for (i,j) in piece if board[j+py][i+px]]
def tick():
global piece,px,py,pc,tickcnt,board,score
keys=key.get_pressed()
npx=px+(-1 if keys[K_LEFT] else (1 if keys[K_RIGHT] else 0))
npiece=[(j,3-i) for (i,j) in piece] if keys[K_UP] else piece #Rotate
if not collide(npiece,npx,py): piece,px=npiece,npx
if keys[K_DOWN]: py=(j for j in range(py,bh) if collide(piece,px,j+1)).next()
if tickcnt%5==0:
if collide(piece,px,py+1):
if py<0: sys.exit("GAME OVER: score %i"%score)
for i,j in piece: board[j+py][i+px]=pc
piece,px,py,pc=new_piece(random.choice(blk.keys()))
else: py+=1
nb=[l for l in board[:bh] if 0 in l]+board[bh:]
s=len(board)-len(nb)
if s: score,board= score+2**s,[board[-1][:] for j in range(s)]+nb
for i,j,c in [(i,j,blk.get(board[j][i],0)) for i in range(bw) for j in range(bh)]:
draw.rect(scr,blk[pc] if (i-px,j-py) in piece else c,((i*box[0],j*box[1]),box))
dummy,tickcnt = display.flip(),tickcnt+1
if init(): scr = display.set_mode((400,800))
dummy,box=time.set_timer(TICK,100),(scr.get_width()/bw,scr.get_height()/bh)
while 1:{QUIT: sys.exit, TICK: tick}.get(event.wait().type,str)()

其實是 32 行,稍微有一點點濫用了一些東西來把兩行縮成一行,湊成整數(tickcnt+1 那行)。但是基本上不使用 ; , add or 之類的東西來把縮短行數,更不要說用 exec/ compile 之類的東西了。上面的東西跟原來那個 100 行的一樣,需要 python +pygame,雖然說 pygame 已經算是標準的 module,但畢竟不是 python 裡面的東西,下面用內建 Tkinter 寫成 33 行的程式
import sys,random
from Tkinter import *
score,bw,bh,H,W=0,10,20,40,40
blk={0xf:"red",0x2e:"#0f0",0x27:"blue",0x47:"#ff0",0x66:"#0ff",0xC6:"#38f",0x6C:"#f0f"}
board=[[0xf if j== bh else 0]*bw+[0xf]*3 for j in range(bh+3)]
new_piece=lambda pc:([((z>>2)+1,z&3) for z in range(16) if (pc>>z)&1],3,-2,pc)
piece,px,py,pc=new_piece(random.choice(blk.keys()))
collide=lambda piece,px,py: [1 for (i,j) in piece if board[j+py][i+px]]
def tick(e=None):
global piece,px,py,pc,tickcnt,board,score
keys= e.keysym if e else ""
npx=px+(-1 if keys=="Left" else (1 if keys=="Right" else 0))
npiece=[(j,3-i) for (i,j) in piece] if keys=="Up" else piece #Rotate
if not collide(npiece,npx,py): piece,px=npiece,npx
if keys=="Down": py=(j for j in range(py,bh) if collide(piece,px,j+1)).next()
if e==None:
if collide(piece,px,py+1):
if py<0: sys.exit("GAME OVER: score %i"%score)
for i,j in piece: board[j+py][i+px]=pc
piece,px,py,pc=new_piece(random.choice(blk.keys()))
else: py+=1
nb=[l for l in board[:bh] if 0 in l]+board[bh:]
s=len(board)-len(nb)
if s: score,board=score+2**s, [board[-1][:] for j in range(s)]+nb
scr.after(300,tick)
scr.delete("all")
for i,j,c in [(i,j,blk.get(board[j][i],"#000")) for i in range(bw) for j in range(bh)]:
scr.create_rectangle(i*W,j*H,(i+1)*W,(j+1)*H,fill=blk[pc] if (i-px,j-py) in piece else c)
scr=Canvas(width=bw*W,height=bh*H,bg="#000")
scr.after(300,tick)
scr.bind_all("<Key>",tick)
scr.pack()
scr.mainloop()

雖然說 33 小於 100,但是其實沒什麼好得意的,畢竟寫 100 行程式的看起來像是個菜鳥。
但寫出來可以驗證我的估計,順便練習一下程式。正當我要把以上兩個程式碼放上 blog 時,發現有人寫出 Tetris in 15 lines。雖然我寫得短的時候不會覺得得意,但是 15 比 30 小很多,心理還是不是滋味,所以就連過去看看他玩什麼花樣。他的程式碼如下:
import random,pygame;R=range;L=len;W=10;Q=H=20;P=pygame;G=255;v=lambda b:['0'*W
]*(H-L(b))+b;a=p=x=y=c=d=0;D=P.display;T=P.USEREVENT;m=lambda i,j:j-y in R(L(p)
)and i-x in R(L(p[0]))and'0'!=p[j-y][i-x]and`d+1`or b[j][i];S=D.set_mode((W*Q,H
*Q));k=lambda p,x,y:1-max(p[i][j]!='0'and(y+i>=H or x+j<0or x+j>=W or'0'!=b[i+y
][j+x])for j in R(L(p[0]))for i in R(L(p)));n=E=b=v([]);P.time.set_timer(T,100)
exec("while E!=P.QUIT:\n E=P.event.wait().type;K=P.key.get_pressed()\n if n:d="
"random.randrange(7);q='111';p=[['1'*4],['010',q],['110','011'],['011','110'],"
"[q,'100'],['11','11'],[q,'001']][d];x=W/2-L(p[0])/2;y=n=0\n if E==T:\n [P.dr"
"aw.rect(S,[(h,g,f)for f in(0,G)for g in(0,G)for h in(0,G)][int(m(i/H,i%H))],("
"(i/H*Q,i%H*Q),(Q,Q)))for i in R(W*H)];t=''.join;z=[t([p[L(p)-1-i][j]for i in "
"R(L(p))])for j in R(L(p[0]))];p,x,y=[p,z][K[P.K_UP]*k(z,x,y)],x-(K[P.K_LEFT]*"
"k(p,x-1,y))+(K[P.K_RIGHT]*k(p,x+1,y)),y+(K[P.K_DOWN]*k(p,x,y+1));B=[t([m(i,j)"
"for i in R(W)])for j in R(H)];e=sum(r.find('0')<0for r in B);D.flip();c+=1\n "
" if c%5<1:\n if k(p,x,y+1)<1:\n if y<1:break\n a+=e and 2**e;n=b=v([r"
" for r in B if~r.find('0')])\n else:y+=1\n");print 'GAME OVER: score %i'%a

這個人的確比那個 100 行的那個人要厲害不少,不過呢,這個程式用到了 exec,然後把所有東西變成字串,餵進 exec 裡,所以是個不太可讀的程式,基本上只要 81*15 個字元內,就能夠變成 15 行了(他的程式長度是 1213)。我看了一下我的程式,字元數是比較多,但是主要是因為變數基本上都還是可讀的緣故,所以稍加修改之後,就能夠讓字元數少於 1100 。但是問題是行數,我還是認為使用 exec 已經犯規了。所以想說在不使用 exec 的情形下,讓行數減少。
這裡,就被迫要使用 ; 來合併行了。但是合併行還是有限制的,因為 if /while/for 等等 block 不能任意合併。所以,我用了一些技巧用 [ ] 來取代迴圈,用其他一些程式碼來取代 if/else。這樣,程式變長了,但是幾乎就能把所有的行數縮成一行(如果沒有字數限制)。
在一行是 80 字元的限制下,我寫了 15 行的程式如下(處理落下和 16 的那個相同,和100 不同):
from pygame import *;R,T,e=range,USEREVENT+1,[1];W,H=10,20;E={0xf:0xff0000,0x2e:
0xff00,0x27:0xff,0x47:0xffff00,0x66:0xffff,0xC6:0x7fff,0x6C:0xff00ff};B=[[0xf if
j==H else 0]*W+[0xf]*3 for j in R(H+3)];S=n=0;import sys,random as C;C=C.choice
O=lambda:(lambda Z:([((z/4)+1,z&3) for z in R(8) if (Z>>z)&1],3,-2,Z))(C(E.keys(
)));P,X,Y,Z=O();L=lambda P,X,Y:[1 for (i,j) in P if B[j+Y][i+X]];d=display;e=[1
];init();F=d.set_mode((400,800));time.set_timer(T,100);l=len
while (0 if d.flip() or e.__setitem__(0,event.wait().type) else e[0])!=QUIT:
if e[0]==T:K=key.get_pressed();U=X+(-1 if K[K_LEFT] else(1 if K[K_RIGHT] else 0
));V=Y+(1 if K[K_DOWN] else 0);Q=[(j,3-i) for i,j in P] if K[K_UP] else P;(P,X,Y
)=(P,X,Y) if L(Q,U,V) else(Q,U,V);n%5 or(L(P,X,Y+1) and((Y<0 and sys.exit("GAME\
OVER: score %i"
%S))or[B[j+Y].__setitem__(i+X,Z) for i,j in P]));(P,X,Y,Z)=(P,X,Y
,Z) if n%5 else(O() if L(P,X,Y+1) else(P,X,Y+1,Z));n+=1;D=[z for z in B[:H] if 0
in z]+B[H:];s=l(B)-l(D);(S,B)=(S+2**s,[B[-1][:] for j in R(s)]+D) if s else(S,B
);[draw.rect(F,E[Z] if (i-X,j-Y) in P else c,((i*40,j*40),(40,40))) for i,j,c in
[(z%W,z/W,E.get(B[z/W][z%W],0)) for z in R(W*H)]]
這樣,算是一個滿意的結果,行數雖然一樣,但是字元比較少,而且程式碼都是直接餵給 python的,沒有經過 exec。如果要使用 exec 的話,那縮到 14 行是沒有問的。
事實上,我還可以縮到 12 行,就像標題上寫的,很簡單,因為 python 內建有壓縮功能,要把程式碼當成字串,那當然可以壓縮。
exec("""eJxtUu9P4kAQ/c5fsV/QXTrUFjg17a3JFaoYUZDfsNmYCltuOWhJ25yC8X+/3QJqzDVp++a9
6XRm9sn1Jk4ylG5TSIJoHq9RkKJ7t07vzdnvWM6EGyaK3GwXwVoguc8uuX066Pldf+g/9A3bHUGT2hZU
LNenb9Zr6Kg7tNQF1mtFHCKNL3KsUG2PDvz5+SFSuK7xxR6f1w/fhuG761HGVIRkiJaUoiYSq1Qgi5dG
huZ5qYrCOEFLJCOkRlkI3DSqhLttugrWz/MATR3MMN6d1Yhhw+6kSvL83Wf+JdHF8fTqakdObA5VKFdg
StwejajldmAME5jSNq5j3/wjtikmhLitY/lcd5idV8USlkRX7uiSHlsaE86kMebclZHMMHGv6Vymm1Ww
NVORPa3jucC4prZxaVmqaibXIhc0SHAfbEUX5iJEXUycAlqs4udghQ49QQQe9Nw7qroyF+qrTSLSVMzV
bwZ0bOCyrbu4Y3dPLf+6z/ebwx9k9/ameWT1z4d0Ynyqjfbo4UN0HynDS6iW5X57asx8Sn7MHnQOuZ2C
pqI4Qy38CAMYEifvlubBXiz+oNRS0+ighXPZsPV4OTP5aTnKl6Z4VQs7vfl176P20O86KJ3FiUBFeVrs
EZ37tRHny67pVKv/Pzel6DYdNDGoXUANynafdvCY08wnsnS044bHmg53U7oSEfZIWb8aJJ8hdXrg0Z5R
KZVSYB4r25w5/LsTU8KNRuHYKMy0wvCuOILd2Qh8fWbYYwpzpkgO6hS+m3NUahKuVzNPghczEbMMX4PP
pnmfWJbHsCxPDpbLD2AGGMtSzYKlehBQ5gL11pMfjReu5EZZJNIbePktVwLZztvj4Lb/sXboO933vDvx
V0SZ+RJo75rZdiMgzRKCSeEfrBs8BQ=="
"".decode("base64").decode("zip"))
不過壓縮之後的東西不是文字,我用 base64 再轉過,才能餵到 python 裡面。所以這樣減肥的效果有限。但是這樣有個好處,因為簡單。原來我是打算用一個 dict 然後對程式碼來做多重替換,這樣可以省下不少空間。比方說裡面用 lambda 定義的一些程式,現在我可以用 re 來對程式碼替換了。簡單的說就是一個簡單的巨集功能。但是用 zip 的方式更聰明,因為手動的建字典建巨集工作,現在由 zip 幫你做了。
當然 base64 是不理想的,其實只要把 \x00 \\ \r 過濾掉,可以直接餵給 python,而且 exec後面也不用放括號,還有很多地方的空格也能減少。
這個程式絕對不會是最短的,我相信如果要這樣搞,十行是絕對可能的,我還是比較喜歡 33 行那兩個可讀的程式。

Categories: ,