Python貪喫蛇雙人大戰-升級版

Python貪喫蛇雙人大戰-升級版

在玩了幾天貪喫蛇遊戲之後,(代碼可參考我之前寫的 Python貪喫蛇雙人大戰),家裏小朋友提出了新的需求(用戶反饋)如下:

  1. 豆子不能出現在豆子上。
  2. 豆子不能出現在蛇身上。
  3. 要顯示得分,方便看出來誰的蛇比較長。
  4. 如果蛇頭撞到另一條蛇,自己掛掉,變成豆子
  5. 蛇變成的豆子被吃了之後不重新生成
  6. 燃燒自己身體可以加速,每次燃燒身體減少一格,速度提升至兩倍

趁着是週末,趕緊實現一下新需求,以滿足小朋友的期望。

豆子不出現在重複的位置

爲了實現豆子出現位置不與已有的豆子重複,也不與蛇所在位置重複,修改了豆類的generate函數如下:

    def generate(self, snake1, snake2):
        while self.curNum < self.totalNum:
            x = random.randrange(0,SCREEN_WIDTH/GRID_SIZE)
            y = random.randrange(0,SCREEN_HEIGHT/GRID_SIZE)
            newBeanPos = [int(x*GRID_SIZE),int(y*GRID_SIZE)]
            #檢查豆子位置是否重複
            isBeanRepeated = False
            for bean in self.beans: #豆子不能與豆子位置重複
                if bean.pos == newBeanPos:
                    isBeanRepeated = True
                    break
            for pos in snake1.segments: #豆子不能與蛇位置重複
                if pos == newBeanPos:
                    isBeanRepeated = True
                    break
            for pos in snake2.segments: #豆子不能與蛇位置重複
                if pos == newBeanPos:
                    isBeanRepeated = True
                    break      
            #新生成的豆子在不重複的地方
            if not isBeanRepeated:
                self.beans.append(Bean(self.color, newBeanPos))
                self.curNum = self.curNum + 1   

因爲要判斷蛇位置,所以把蛇作爲參數傳進去了,增加了兩個參數。函數中每次生成豆子的位置之後進行判斷,不重複再增加。
當然,因爲改了函數接口,所以主循環中調用的地方也要增加相對應的參數,相關代碼如下:

    # 初始化豆子
    yellowBeans = Beans(YELLOW, BEAN_NUM)
    yellowBeans.generate(snake1, snake2)
        # 如果豆子被喫掉,則重新生成豆子,否則蛇長度減少一格
        if yellowBeans.beEaten(snake1.headPos):
            yellowBeans.generate(snake1, snake2)
        else:
            snake1.pop()
        if yellowBeans.beEaten(snake2.headPos):
            yellowBeans.generate(snake1, snake2)
        else:
            snake2.pop()

顯示得分

顯示得分這個功能比較簡單,就用蛇增加的長度表示得分即可,而蛇的長度就是蛇類中segments的長度。於是創建一個函數getLen直接返回segments的長度。

    def getLen(self):
        return len(self.segments)

在主循環中創建分數顯示層,設置其字體大小如下:

    # 分數顯示層字體大小
    scoreFont = pygame.font.Font(None, int(32*GRID_SIZE/20))

並在刷新時增加分數層的刷新:

        # 繪製刷新
        playSurface.fill(BLACK) # 繪製pygame顯示層
        yellowBeans.show(playSurface)
        snake1.show(playSurface)
        snake2.show(playSurface)
        # 顯示分數
        scoreSurf = scoreFont.render('P1 Score:{}    vs    P2 Score:{}'.format(snake1.getLen()-3, snake2.getLen()-3), True, WHITE)
        scoreRect = scoreSurf.get_rect()
        scoreRect.midtop = (SCREEN_WIDTH/2, 0)
        playSurface.blit(scoreSurf, scoreRect)
        pygame.display.flip()   # 刷新pygame顯示層

當然,爲了顯示分數,需要最上面一行不產生豆子,所以修改豆類generate函數中的y值如下:

y = random.randrange(1,SCREEN_HEIGHT/GRID_SIZE)

蛇撞掛掉變豆子

這個功能相對而言就複雜一些了,同時考慮到蛇變成的豆子被吃了之後不重新生成,於是重新定義了一個蛇豆類如下:

class SnakeBeans:
    def __init__(self, snake):
        self.color = NORMAL_BEAN_COLOR
        self.beans = []
        for pos in snake.segments:
            self.beans.append(Bean(NORMAL_BEAN_COLOR, pos))
    def beEaten(self, snakePos):
        for bean in self.beans:
            if bean.beEaten(snakePos):
                self.beans.remove(bean)
                return True
        return False
    def show(self, playSurface):
        for bean in self.beans:
            pygame.draw.rect(playSurface,self.color,Rect(bean.pos[0],bean.pos[1],GRID_SIZE,GRID_SIZE))    

因爲蛇豆不需要重生,所以比較簡單,能被喫就可以了。

爲了實現蛇撞掛掉,修改了蛇類的 moveAndAdd 函數,增加了檢查是否撞到另一條蛇代碼,傳入另一條蛇作爲參數,返回是否撞到。

    def moveAndAdd(self, snake=[]):
        # 根據方向移動蛇頭的座標
        if self.direction == 1:
            self.headPos[0] += GRID_SIZE
        if self.direction == -1:
            self.headPos[0] -= GRID_SIZE
        if self.direction == -2:
            self.headPos[1] -= GRID_SIZE
        if self.direction == 2:
            self.headPos[1] += GRID_SIZE
        #檢查蛇頭是否撞到另一條蛇身
        if snake == []:
            self.segments.insert(0,list(self.headPos)) # 在蛇頭插入一格
            return False
        isSnakeHit = False
        for pos in snake.segments:
            if self.headPos == pos:
                isSnakeHit = True
                break
        if not isSnakeHit:
            self.segments.insert(0,list(self.headPos)) # 在蛇頭插入一格
        return isSnakeHit    

於是在主循環中就可以判斷是否撞到,撞到則爆豆子,自己再重生:

            if snake1.moveAndAdd(snake2): #撞到了另一條蛇身
                snake1Beans = SnakeBeans(snake1)
                snake1.respawn()
                continue

然後這時候發現重生出問題了,屏幕上東西太多,隨機位置重生容易衝突,所以決定單獨提取一個函數出來判斷是否有空位置,如下:

def isEmptyInArea(area, normalBeanss=[], snakeBeanss=[], snakes=[]):
    ''' area是位置的列表
    '''
    for pos in area:
        if normalBeanss != []:
            for nbs in normalBeanss:
                for nb in nbs.beans:
                    if nb.pos == pos:
                        return False
        if snakeBeanss != []:
            for sbs in snakeBeanss:
                if sbs != []:
                    for sb in sbs.beans:
                        if sb.pos == pos:
                            return False
        if snakes != []:
            for snake in snakes:
                for snakePos in snake.segments:
                    if snakePos == pos:
                        return False
    return True

輸入參數area是位置的列表,normalBeanss是普通豆子的列表,snakeBeanss是蛇豆的列表,snakes是蛇的列表。這個函數思路很簡單,就是拿area中的一個點一個點去判斷是否跟其他幾個列表中的位置衝突。
有了這個函數之後,一些代碼就可以重寫了,於是把蛇類的respawn函數改寫如下,提取了一個generate函數出來,在初始化函數中也能用。

    def __init__(self, color, headColor, ctrlKeys):
        self.color = color
        self.headColor = headColor
        self.ctrlKeys = ctrlKeys #按[上,下,左,右]的順序
        self.segments = []
        self.generate()
    def generate(self):
        x = random.randrange(SNAKE_INIT_POS_MARGIN, SCREEN_WIDTH/GRID_SIZE-SNAKE_INIT_POS_MARGIN)
        y = random.randrange(SNAKE_INIT_POS_MARGIN, SCREEN_HEIGHT/GRID_SIZE-SNAKE_INIT_POS_MARGIN)
        self.direction = random.choice([-2,2,-1,1]) #([-2,2,-1,1]) # 方向[-2,2,-1,1]分別表示[上,下,左,右]
        self.headPos = [int(x*GRID_SIZE), int(y*GRID_SIZE)]
        self.segments.clear()
        self.segments.insert(0, list(self.headPos))
        for i in range(SNAKE_INIT_LEN-1):
            self.moveAndAdd()  
    def respawn(self, normalBeanss=[], snakeBeanss=[], snakes=[]):
        self.generate()
        while not isEmptyInArea(self.segments, normalBeanss, snakeBeanss, snakes):
            self.generate()

燃燒加速

最後來實現燃燒自己身體來加速的功能。給蛇類增加兩個成員變量:

        self.vel = SNAKE_VEL_SLOW
        self.velCount = 0

修改之前的changeDirection函數爲handleKey,增加了加速鍵的處理,速度分兩檔SNAKE_VEL_FAST和SNAKE_VEL_SLOW,並設置了加速的時間SNAKE_ACC_TIME:

    def handleKey(self, pressKey):
        directions = [-2,2,-1,1] #([-2,2,-1,1]) # 方向[-2,2,-1,1]分別表示[上,下,左,右]
        for direct, key in zip(directions, self.ctrlKeys[:4]):
            if key == pressKey and direct + self.direction != 0:
                self.direction = direct
        if pressKey == self.ctrlKeys[4]  and self.vel != SNAKE_VEL_FAST: #按下了加速鍵
            if self.getLen() > SNAKE_INIT_LEN:
                self.pop()                 #燃燒自己
                self.vel = SNAKE_VEL_FAST  #來加速
                self.velCount = SNAKE_ACC_TIME

初始化時需要修改,首先需要增加加速按鍵:

    ctrlKeys1 = [ord('w'),ord('s'),ord('a'),ord('d'),ord('g')]
    ctrlKeys2 = [K_UP,K_DOWN,K_LEFT,K_RIGHT,ord('m')]

然後增加輔助變量:

    snake1VelCount = 1
    snake2VelCount = 1

主循環中涉及到蛇運動處理的地方都加上判斷前提,如:

        if snake1VelCount > 1:
            if snake1.moveAndAdd(snake2): #撞到了另一條蛇身
                snake1Beans = SnakeBeans(snake1)
                snake1.respawn([normBeans],[snake1Beans,snake2Beans],[snake2])
                continue

最後增加輔助變量的處理,以實現加速功能:

        if snake1VelCount > 1:  
            if snake1.vel == SNAKE_VEL_FAST:
                snake1VelCount = 2
            else:
                snake1VelCount = 1
        else:
            snake1VelCount = snake1VelCount + 1

至此,就可以和我家小朋友再次一起愉快的玩耍了 😃
貪喫蛇

以上都是我改版需求實現的過程,希望能給大家提供一些幫助和思路,若是自己整理不出來完整的代碼(按照我以上的思路應該是可以從之前的版本改出來的),可以到此處下載。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章