國慶期間閒不住,用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.七、pygame1.9.2b8編寫。 遊戲註解中出現的術語解釋: 舞臺:整個遊戲界面,包括堆疊區、成績等顯示區,下個出現方塊預告區。 堆疊區:遊戲方塊和活動方塊形狀堆放區域,遊戲中主要互動區。 方塊(基礎方塊):這裏的方塊是對基礎的小四方形統稱,每一個方塊就是一個正方形。 方塊形狀:指一組以特定方式組合在一塊兒的方塊,也就是你們常說的下落方塊形狀,好比長條,方形,L形等。 固實方塊:特指堆疊區中不能再進行移動,可被消除的基礎方塊集合。 version:1.0 author:lykyl createdate:2016.9.29 ''' import sys 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()
程序運行截圖:
app