用python把經典俄羅斯方塊實現了一遍,找到了些兒時的樂趣。因此突發奇想,打算用python寫點經典又確實有趣的小程序形成系列。正統編程之余也給自己找點兒樂趣,換個角度寫程序。
原計劃是寫篇完整的博文對程序算法和函數模塊做個說明,但是在整理程序的時候發現自己給程序加的注釋已經相當詳細,程序以外的文字顯得很多余。正所謂大道至簡,直接將程序代碼貼上來,大家就著代碼、伴著注解,相信對程序的理解應該很容易。
配置文件 elsfk.cfg,定義了單一方向的原始方塊形狀組合,具體的格式說明請參見getConf中的注解。
;1,1,1,1;;
1,1,1,0;1,0,0,0;;
1,1,1,0;0,0,1,0;;
0,1,0,0;1,1,1,0;;
1,1,0,0;1,1,0,0;;
1,1,0,0;0,1,1,0;;
0,1,1,0;1,1,0,0;;
完整程序代碼:
# -*- coding:utf-8 -*-
'''
經典俄羅斯方塊
游戲基於python2.7、pygame1.9.2b8編寫。
游戲注解中出現的術語解釋:
舞台:整個游戲界面,包括堆疊區、成績等顯示區,下個出現方塊預告區。
堆疊區:游戲方塊和活動方塊形狀堆放區域,游戲中主要互動區。
方塊(基礎方塊):這裡的方塊是對基礎的小四方形統稱,每個方塊就是一個正方形。
方塊形狀:指一組以特定方式組合在一起的方塊,也就是大家常說的下落方塊形狀,比如長條,方形,L形等。
固實方塊:特指堆疊區中不能再進行移動,可被消除的基礎方塊集合。
version:1.0
author:lykyl
createdate:2016.9.29
'''
import random,copy
import pygame as pg
from pygame.locals import *
'''
常量聲明
'''
EMPTY_CELL=0 #空區標識,表示沒有方塊
FALLING_BLOCK=1 #下落中的方塊標識,也就是活動方塊。
STATIC_BLOCK=2 #固實方塊標識
'''
全局變量聲明
變量值以sysInit函數中初始化後的結果為准
'''
defaultFont=None #默認字體
screen=None #屏幕輸出對象
backSurface=None #圖像輸出緩沖畫板
score=0 #玩家得分記錄
clearLineScore=0 #玩家清除的方塊行數
level=1 #關卡等級
clock=None #游戲時鐘
nowBlock=None #當前下落中的方塊
nextBlock=None #下一個將出現的方塊
fallSpeed=10 #當前方塊下落速度
beginFallSpeed=fallSpeed #游戲初始時方塊下落速度
speedBuff=0 #下落速度緩沖變量
keyBuff=None #上一次按鍵記錄
maxBlockWidth=10 #舞台堆疊區X軸最大可容納基礎方塊數
maxBlockHeight=18 #舞台堆疊區Y軸最大可容納基礎方塊數
blockWidth=30 #以像素為單位的基礎方塊寬度
blockHeight=30 #以像素為單位的基礎方塊高度
blocks=[] #方塊形狀矩陣四維列表。第一維為不同的方塊形狀,第二維為每個方塊形狀不同的方向(以0下標起始,一共四個方向),第三維為Y軸方塊形狀占用情況,第四維為X軸方塊形狀占用情況。矩陣中0表示沒有方塊,1表示有方塊。
stage=[] #舞台堆疊區矩陣二維列表,第一維為Y軸方塊占用情況,第二維為X軸方塊占用情況。矩陣中0表示沒有方塊,1表示有固實方塊,2表示有活動方塊。
gameOver=False #游戲結束標志
pause=False #游戲暫停標志
def printTxt(content,x,y,font,screen,color=(255,255,255)):
'''顯示文本
args:
content:待顯示文本內容
x,y:顯示坐標
font:字體
screen:輸出的screen
color:顏色
'''
imgTxt=font.render(content,True,color)
screen.blit(imgTxt,(x,y))
class point(object):
'''平面坐標點類
attributes:
x,y:坐標值
'''
def __init__(self,x,y):
self.__x=x
self.__y=y
def getx(self):
return self.__x
def setx(self,x):
self.__x=x
x=property(getx,setx)
def gety(self):
return self.__y
def sety(self,y):
self.__y=y
y=property(gety,sety)
def __str__(self):
return "{x:"+"{:.0f}".format(self.__x)+",y:"+"{:.0f}".format(self.__y)+"}"
class blockSprite(object):
'''
方塊形狀精靈類
下落方塊的定義全靠它了。
attributes:
shape:方塊形狀編號
direction:方塊方向編號
xy,方塊形狀左上角方塊坐標
block:方塊形狀矩陣
'''
def __init__(self,shape,direction,xy):
self.shape=shape
self.direction=direction
self.xy=xy
def chgDirection(self,direction):
'''
改變方塊的方向
args:
direction:1為向右轉,0為向左轉。
'''
dirNumb=len(blocks[self.shape])-1
if direction==1:
self.direction+=1
if self.direction>dirNumb:
self.direction=0
else:
self.direction-=1
if self.direction<0:
self.direction=dirNumb
def clone(self):
'''
克隆本體
return:
返回自身的克隆
'''
return blockSprite(self.shape,self.direction,point(self.xy.x,self.xy.y))
def _getBlock(self):
return blocks[self.shape][self.direction]
block = property(_getBlock)
def getConf(fileName):
'''
從配置文件中讀取方塊形狀數據
每個方塊以4*4矩陣表示形狀,配置文件每行代表一個方塊,用分號分隔矩陣行,用逗號分隔矩陣列,0表示沒有方塊,1表示有方塊。
因為此程序只針對俄羅斯方塊的經典版,所以方塊矩陣大小以硬編碼的形式寫死為4*4。
args:
fileName:配置文件名
'''
global blocks #blocks記錄方塊形狀。
with open(fileName,'rt') as fp:
for temp in fp.readlines():
blocks.append([])
blocksNumb=len(blocks)-1
blocks[blocksNumb]=[]
#每種方塊形狀有四個方向,以0~3表示。配置文件中只記錄一個方向形狀,另外三個方向的矩陣排列在sysInit中通過調用transform計算出來。
blocks[blocksNumb].append([])
row=temp.split(";")
for r in range(len(row)):
col=[]
ct=row[r].split(",")
#對矩陣列數據做規整,首先將非“1”的值全修正成“0”以過濾空字串或回車符。
for c in range(len(ct)):
if ct[c]!="1":
col.append(0)
else:
col.append(1)
#將不足4列的矩陣通過補“0”的方式,補足4列。
for c in range(len(ct)-1,3):
col.append(0)
blocks[blocksNumb][0].append(col)
#如果矩陣某行沒有方塊,則配置文件中可以省略此行,程序會在末尾補上空行數據。
for r in range(len(row)-1,3):
blocks[blocksNumb][0].append([0,0,0,0])
blocks[blocksNumb][0]=formatBlock(blocks[blocksNumb][0])
def sysInit():
'''
系統初始化
包括pygame環境初始化,全局變量賦值,生成每個方塊形狀的四個方向矩陣。
'''
global defaultFont,screen,backSurface,clock,blocks,stage,gameOver,fallSpeed,beginFallSpeed,nowBlock,nextBlock,score,level,clearLineScore,pause
#pygame運行環境初始化
pg.init()
screen=pg.display.set_mode((500,550))
backSurface=pg.Surface((screen.get_rect().width,screen.get_rect().height))
pg.display.set_caption("block")
clock=pg.time.Clock()
pg.mouse.set_visible(False)
#游戲全局變量初始化
defaultFont=pg.font.Font("res/font/yh.ttf",16) #yh.ttf這個字體文件請自行上網搜索下載,如果找不到就隨便用個ttf格式字體文件替換一下。
nowBlock=None
nextBlock=None
gameOver=False
pause=False
score=0
level=1
clearLineScore=0
beginFallSpeed=20
fallSpeed=beginFallSpeed-level*2
#初始化游戲舞台
stage=[]
for y in range(maxBlockHeight):
stage.append([])
for x in range(maxBlockWidth):
stage[y].append(EMPTY_CELL)
#生成每個方塊形狀4個方向的矩陣數據
for x in range(len(blocks)):
#因為重新開始游戲時會調用sysinit對系統所有參數重新初始化,為了避免方向矩陣數據重新生成,需要在此判斷是否已經生成,如果已經生成則跳過。
if len(blocks[x])<2:
t=blocks[x][0]
for i in range(3):
t=transform(t,1)
blocks[x].append(formatBlock(t))
#transform,removeTopBlank,formatBlock這三個函數只為生成方塊形狀4個方向矩陣使用,在游戲其他環節無作用,在閱讀程序時可以先跳過。
def transform(block,direction=0):
'''
生成指定方塊形狀轉換方向後的矩陣數據
args:
block:方塊形狀矩陣參數
direction:轉換的方向,0代表向左,1代表向右
return:
變換方向後的方塊形狀矩陣參數
'''
result=[]
for y in range(4):
result.append([])
for x in range(4):
if direction==0:
result[y].append(block[x][3-y])
else:
result[y].append(block[3-x][y])
return result
def removeTopBlank(block):
'''
清除方塊矩陣頂部空行數據
args:
block:方塊開關矩陣
return:
整理後的方塊矩陣數據
'''
result=copy.deepcopy(block)
blankNumb=0
while sum(result[0])<1 and blankNumb<4:
del result[0]
result.append([0,0,0,0])
blankNumb+=1
return result
def formatBlock(block):
'''
整理方塊矩陣數據,使方塊在矩陣中處於左上角的位置
args:
block:方塊開關矩陣
return:
整理後的方塊矩陣數據
'''
result=removeTopBlank(block)
#將矩陣右轉,用於計算左側X軸線空行,計算完成後再轉回
result=transform(result, 1)
result=removeTopBlank(result)
result=transform(result,0)
return result
def checkDeany(sprite):
'''
檢查下落方塊是否與舞台堆疊區中固實方塊發生碰撞
args:
sprite:下落方塊
return:
如果發生碰撞則返回True
'''
topX=sprite.xy.x
topY=sprite.xy.y
for y in range(len(sprite.block)):
for x in range(len(sprite.block[y])):
if sprite.block[y][x]==1:
yInStage=topY+y
xInStage=topX+x
if yInStage>maxBlockHeight-1 or yInStage<0:
return True
if xInStage>maxBlockWidth-1 or xInStage<0:
return True
if stage[yInStage][xInStage]==STATIC_BLOCK:
return True
return False
def checkLine():
'''
檢測堆疊區是否有可消除的整行固實方塊
根據檢測結果重新生成堆疊區矩陣數據,調用updateScore函數更新玩家積分等數據。
return:
本輪下落周期消除的固實方塊行數
'''
global stage
clearCount=0 #本輪下落周期消除的固實方塊行數
tmpStage=[] #根據消除情況新生成的堆疊區矩陣,在有更新的情況下會替換全局的堆疊區矩陣。
for y in stage:
#因為固實方塊在堆疊矩陣裡以2表示,所以判斷方塊是否已經滿一整行只要計算矩陣行數值合計是否等於堆疊區X軸最大方塊數*2就可以。
if sum(y)>=maxBlockWidth*2:
tmpStage.insert(0,maxBlockWidth*[0])
clearCount+=1
else:
tmpStage.append(y)
if clearCount>0:
stage=tmpStage
updateScore(clearCount)
return clearCount
def updateStage(sprite,updateType=1):
'''
將下落方塊坐標數據更新到堆疊區數據中。下落方塊涉及的坐標在堆疊區中用數字1標識,固實方塊在堆疊區中用數字2標識。
args:
sprite:下落方塊形狀
updateType:更新方式,0代表清除,1代表動態加入,2代表固實加入。
'''
global stage
topX=sprite.xy.x
topY=sprite.xy.y
for y in range(len(sprite.block)):
for x in range(len(sprite.block[y])):
if sprite.block[y][x]==1:
if updateType==0:
if stage[topY+y][topX+x]==FALLING_BLOCK:
stage[topY+y][topX+x]=EMPTY_CELL
elif updateType==1:
if stage[topY+y][topX+x]==EMPTY_CELL:
stage[topY+y][topX+x]=FALLING_BLOCK
else:
stage[topY+y][topX+x]=STATIC_BLOCK
def updateScore(clearCount):
'''
更新玩家游戲記錄,包括積分、關卡、消除方塊行數,並且根據關卡數更新方塊下落速度。
args:
clearCount:本輪下落周期內清除的方塊行數。
return:
當前游戲的最新積分
'''
global score,fallSpeed,level,clearLineScore
prizePoint=0 #額外獎勵分數,同時消除的行數越多,獎勵分值越高。
if clearCount>1:
if clearCount<4:
prizePoint=clearCount**clearCount
else:
prizePoint=clearCount*5
score+=(clearCount+prizePoint)*level
#玩得再牛又有何用? :)
if score>99999999:
score=0
clearLineScore+=clearCount
if clearLineScore>100:
clearLineScore=0
level+=1
if level>(beginFallSpeed/2):
level=1
fallSpeed=beginFallSpeed
fallSpeed=beginFallSpeed-level*2
return score
def drawStage(drawScreen):
'''
在給定的畫布上繪制舞台
args:
drawScreen:待繪制的畫布
'''
staticColor=30,102,76 #固實方塊顏色
activeColor=255,239,0 #方塊形狀顏色
fontColor=200,10,120 #文字顏色
baseRect=0,0,blockWidth*maxBlockWidth+1,blockHeight*maxBlockHeight+1 #堆疊區方框
#繪制堆疊區外框
drawScreen.fill((180,200,170))
pg.draw.rect(drawScreen, staticColor, baseRect,1)
#繪制堆疊區內的所有方塊,包括下落方塊形狀
for y in range(len(stage)):
for x in range(len(stage[y])):
baseRect=x*blockWidth,y*blockHeight,blockWidth,blockHeight
if stage[y][x]==2:
pg.draw.rect(drawScreen, staticColor, baseRect)
elif stage[y][x]==1:
pg.draw.rect(drawScreen, activeColor, baseRect)
#繪制下一個登場的下落方塊形狀
printTxt("Next:",320,350,defaultFont,backSurface,fontColor)
if nextBlock!=None:
for y in range(len(nextBlock.block)):
for x in range(len(nextBlock.block[y])):
baseRect=320+x*blockWidth,380+y*blockHeight,blockWidth,blockHeight
if nextBlock.block[y][x]==1:
pg.draw.rect(drawScreen, activeColor, baseRect)
#繪制關卡、積分、當前關卡消除整行數
printTxt("Level:%d" % level,320,40,defaultFont,backSurface,fontColor)
printTxt("Score:%d" % score,320,70,defaultFont,backSurface,fontColor)
printTxt("Clear:%d" % clearLineScore,320,100,defaultFont,backSurface,fontColor)
#特殊游戲狀態的輸出
if gameOver:
printTxt("GAME OVER",230,200,defaultFont,backSurface,fontColor)
printTxt("<PRESS RETURN TO REPLAY>",200,260,defaultFont,backSurface,fontColor)
if pause:
printTxt("Game pausing",230,200,defaultFont,backSurface,fontColor)
printTxt("<PRESS RETURN TO CONTINUE>",200,260,defaultFont,backSurface,fontColor)
def process():
'''
游戲控制及邏輯處理
'''
global gameOver,nowBlock,nextBlock,speedBuff,backSurface,keyBuff,pause
if nextBlock is None:
nextBlock=blockSprite(random.randint(0,len(blocks)-1),random.randint(0,3),point(maxBlockWidth+4,maxBlockHeight))
if nowBlock is None:
nowBlock=nextBlock.clone()
nowBlock.xy=point(maxBlockWidth//2,0)
nextBlock=blockSprite(random.randint(0,len(blocks)-1),random.randint(0,3),point(maxBlockWidth+4,maxBlockHeight))
#每次生成新的下落方塊形狀時檢測碰撞,如果新的方塊形狀一出現就發生碰撞,則顯然玩家已經沒有機會了。
gameOver=checkDeany(nowBlock)
#游戲失敗後,要將活動方塊形狀做固實處理
if gameOver:
updateStage(nowBlock,2)
'''
對於下落方塊形狀操控以及移動,采用影子形狀進行預判斷。如果沒有碰撞則將變化應用到下落方塊形狀上,否則不變化。
'''
tmpBlock=nowBlock.clone() #影子方塊形狀
'''
處理用戶輸入
對於用戶輸入分為兩部分處理。
第一部分,將退出、暫停、重新開始以及形狀變換的操作以敲擊事件處理。
這樣做的好處是只對敲擊一次鍵盤做出處理,避免用戶按住單一按鍵後程序反復處理影響操控,特別是形狀變換操作,敲擊一次鍵盤換變一次方向,玩家很容易控制。
'''
for event in pg.event.get():
if event.type== pg.QUIT:
sys.exit()
pg.quit()
elif event.type==pg.KEYDOWN:
if event.key==pg.K_ESCAPE:
sys.exit()
pg.quit()
elif event.key==pg.K_RETURN:
if gameOver:
sysInit()
return
elif pause:
pause=False
else:
pause=True
return
elif not gameOver and not pause:
if event.key==pg.K_SPACE:
tmpBlock.chgDirection(1)
elif event.key==pg.K_UP:
tmpBlock.chgDirection(0)
if not gameOver and not pause:
'''
用戶輸入處理第二部分,將左右移動和快速下落的操作以按下事件處理。
這樣做的好處是不需要玩家反復敲擊鍵盤進行操作,保證了操作的連貫性。
由於連續移動的速度太快,不利於定位。所以在程序中采用了簡單的輸入減緩處理,即通過keyBuff保存上一次操作按鍵,如果此次按鍵與上一次按鍵相同,則跳過此輪按鍵處理。
'''
keys=pg.key.get_pressed()
if keys[K_DOWN]:
tmpBlock.xy=point(tmpBlock.xy.x,tmpBlock.xy.y+1)
keyBuff=None
elif keys[K_LEFT]:
if keyBuff!=pg.K_LEFT:
tmpBlock.xy=point(tmpBlock.xy.x-1,tmpBlock.xy.y)
keyBuff=pg.K_LEFT
else:
keyBuff=None
elif keys[K_RIGHT]:
if keyBuff!=pg.K_RIGHT:
tmpBlock.xy=point(tmpBlock.xy.x+1,tmpBlock.xy.y)
keyBuff=pg.K_RIGHT
else:
keyBuff=None
if not checkDeany(tmpBlock):
updateStage(nowBlock,0)
nowBlock=tmpBlock.clone()
#處理自動下落
speedBuff+=1
if speedBuff>=fallSpeed:
speedBuff=0
tmpBlock=nowBlock.clone()
tmpBlock.xy=point(nowBlock.xy.x,nowBlock.xy.y+1)
if not checkDeany(tmpBlock):
updateStage(nowBlock,0)
nowBlock=tmpBlock.clone()
updateStage(nowBlock,1)
else:
#在自動下落過程中一但發生活動方塊形狀的碰撞,則將活動方塊形狀做固實處理,並檢測是否有可消除的整行方塊
updateStage(nowBlock,2)
checkLine()
nowBlock=None
else:
updateStage(nowBlock,1)
drawStage(backSurface)
screen.blit(backSurface,(0,0))
pg.display.update()
clock.tick(40)
def main():
'''
主程序
'''
getConf("elsfk.cfg")
sysInit()
while True:
process()
if __name__ == "__main__":
main()
程序運行截圖: