[找回失落的记忆]树莓派+Pygame模块编写俄罗斯方块

昨天晚上,在家整理东西的时候, 突然翻找出来很早以前小时候的 Gameboy(可能不是这个名字了), 装电池的那种,很古老,只有一款游戏就是俄罗斯方块, 突然回忆就拉回了小学时代和同学一起比拼俄罗斯方块分数的日子,每天都无忧无虑的,不用考虑房贷,车保险….然后突然有一个想法,为什么不用树莓派做一个游戏机呢? 网上教程一堆一堆的, 都是用 retropie, lakka之类的系统实现一个模拟器的平台, 我觉得不够极客, 如果自己用 python 写一个游戏不是更好? 又能巩固 python 的代码能力,又可以让树莓派一机多用, 不仅只是个游戏机, 还可以是下载器,gitlab, 或者是一个 OSMC, 家庭媒体中心呢?

翻找了一下我的 Maker 材料箱, 找到一个吃灰已久的树莓派3B, 通过官方下载全新的镜像,然后烧录系统, 更新系统,配置 ssh, 配置主机名,配置字符集,配置 IP 地址等等一系列操作后. 利用 python 命令执行一下看看版本信息:

$ python -V
Python 2.7.10

就用默认版本吧, 然后去检查一下 pygame 的状态,默认情况下, pygame 已经集成在系统里面了, 如果没有要去官方站点上下载一下并安装, 连接如下: http://pygame.org/wiki/tutorials,官方的这个链接上已经说明:http://pygame.org/wiki/GettingStarted#Raspberry PI

Raspberry PI

This comes with pygame already installed on the default raspberian installation.

那么接下来的事儿就简单多了. 下面是我准备的材料:

  1. 树莓派3 B 型  x1
  2. 16GB class 10 的 TF 卡一张, 最好有SD 卡套方便进行系统烧录
  3. 5v2.5A 电源一套,为了方便,我这里采用的是52Pi 官方的 USB-HUB, 专为树莓派设计, 购买点我
  4. 5寸电阻触摸屏( GPIO 引脚控制), 购买点我 
  5. 外壳你可以自己 DIY 或者利用以下链接下载 STL 文件然后转码成 Gcode 交给3D 打印机打印.3D打印外壳链接

好了,我要开始操作了,首先你要做的事儿是把设备接驳起来,像这样:

然后启动后配置好网络后执行我写好的脚本就可以进行分辨率调节:

git clone https://github.com/yoyojacky/52Pi.git

cd ~/52Pi/

chmod +x restool.sh

sudo ./restool.sh

然后重启, 屏幕的分辨率和电容触摸的驱动就加载好了.

后面需要执行屏幕校准:

cd ~/52Pi/

chmod +x calibrator.sh

sudo ./calibrator.sh

基本配置告一段落了,然后检查 pygame 的版本:

pip list

或者通过 python 的 IDE 直接进行查询,在终端键入python 然后回车继续输入如下 python 代码:

 import  pygame

pygame.ver

看到如下图示:

说明pygame 的模块已经安装过,系统中包含这个模块了,就可以开始下面的操作了.

然后创建一个目录:

mkdir -pv  /home/pi/mygame/music

sudo vim.tiny  /home/pi/mygame/russianblock.py

我一次性创建了一个目录结构, mygame 目录下面创建一个 python 文件, 名字为: russianblock.py

然后可以两首 MP3的歌曲进去, 也可以直接下载我提供的素材:

Have A Drink On Me,Rolling in the Deep

然后继续添加如下python 代码:

#!/usr/bin/env python
# -*- coding: utf-8 -*-
import random, time, pygame, sys
from pygame.locals import *

FPS = 25
WINDOWWIDTH = 640
WINDOWHEIGHT = 480
BOXSIZE = 20
BOARDWIDTH = 10
BOARDHEIGHT = 20
BLANK = '.'

MOVESIDEWAYSFREQ = 0.15
MOVEDOWNFREQ = 0.1

XMARGIN = int((WINDOWWIDTH - BOARDWIDTH * BOXSIZE) / 2)
TOPMARGIN = WINDOWHEIGHT - (BOARDHEIGHT * BOXSIZE) - 5
#        R  G  B
WHITE    = (255, 255, 255)
GRAY    = (185, 185, 185)
BLACK    = ( 0,  0,  0)
RED     = (155,  0,  0)
LIGHTRED  = (175, 20, 20)
GREEN    = ( 0, 155,  0)
LIGHTGREEN = ( 20, 175, 20)
BLUE    = ( 0,  0, 155)
LIGHTBLUE  = ( 20, 20, 175)
YELLOW   = (155, 155,  0)
LIGHTYELLOW = (175, 175, 20)

BORDERCOLOR = BLUE
BGCOLOR = BLACK
TEXTCOLOR = WHITE
TEXTSHADOWCOLOR = GRAY
COLORS   = (   BLUE,   GREEN,   RED,   YELLOW)
LIGHTCOLORS = (LIGHTBLUE, LIGHTGREEN, LIGHTRED, LIGHTYELLOW)
assert len(COLORS) == len(LIGHTCOLORS) # each color must have light color

TEMPLATEWIDTH = 5
TEMPLATEHEIGHT = 5

S_SHAPE_TEMPLATE = [['.....',
           '.....',
           '..OO.',
           '.OO..',
           '.....'],
          ['.....',
           '..O..',
           '..OO.',
           '...O.',
           '.....']]

Z_SHAPE_TEMPLATE = [['.....',
           '.....',
           '.OO..',
           '..OO.',
           '.....'],
          ['.....',
           '..O..',
           '.OO..',
           '.O...',
           '.....']]

I_SHAPE_TEMPLATE = [['..O..',
           '..O..',
           '..O..',
           '..O..',
           '.....'],
          ['.....',
           '.....',
           'OOOO.',
           '.....',
           '.....']]

O_SHAPE_TEMPLATE = [['.....',
           '.....',
           '.OO..',
           '.OO..',
           '.....']]

J_SHAPE_TEMPLATE = [['.....',
           '.O...',
           '.OOO.',
           '.....',
           '.....'],
          ['.....',
           '..OO.',
           '..O..',
           '..O..',
           '.....'],
          ['.....',
           '.....',
           '.OOO.',
           '...O.',
           '.....'],
          ['.....',
           '..O..',
           '..O..',
           '.OO..',
           '.....']]

L_SHAPE_TEMPLATE = [['.....',
           '...O.',
           '.OOO.',
           '.....',
           '.....'],
          ['.....',
           '..O..',
           '..O..',
           '..OO.',
           '.....'],
          ['.....',
           '.....',
           '.OOO.',
           '.O...',
           '.....'],
          ['.....',
           '.OO..',
           '..O..',
           '..O..',
           '.....']]

T_SHAPE_TEMPLATE = [['.....',
           '..O..',
           '.OOO.',
           '.....',
           '.....'],
          ['.....',
           '..O..',
           '..OO.',
           '..O..',
           '.....'],
          ['.....',
           '.....',
           '.OOO.',
           '..O..',
           '.....'],
          ['.....',
           '..O..',
           '.OO..',
           '..O..',
           '.....']]

PIECES = {'S': S_SHAPE_TEMPLATE,
     'Z': Z_SHAPE_TEMPLATE,
     'J': J_SHAPE_TEMPLATE,
     'L': L_SHAPE_TEMPLATE,
     'I': I_SHAPE_TEMPLATE,
     'O': O_SHAPE_TEMPLATE,
     'T': T_SHAPE_TEMPLATE}


def main():
  global FPSCLOCK, DISPLAYSURF, BASICFONT, BIGFONT
  pygame.init()
  FPSCLOCK = pygame.time.Clock()
  DISPLAYSURF = pygame.display.set_mode((WINDOWWIDTH, WINDOWHEIGHT))
  BASICFONT = pygame.font.Font('freesansbold.ttf', 18)
  BIGFONT = pygame.font.Font('freesansbold.ttf', 100)
  pygame.display.set_caption('Tetromino')

  showTextScreen('YoYoJacky')
  while True: # game loop
    if random.randint(0, 1) == 0:
      pygame.mixer.music.load('music/Have A Drink On Me.mp3')
    else:
      pygame.mixer.music.load('music/Rolling In The Deep.mp3')
    pygame.mixer.music.play(-1, 0.0)
    runGame()
    pygame.mixer.music.stop()
    showTextScreen('Game Over')


def runGame():
  # setup variables for the start of the game
  board = getBlankBoard()
  lastMoveDownTime = time.time()
  lastMoveSidewaysTime = time.time()
  lastFallTime = time.time()
  movingDown = False # note: there is no movingUp variable
  movingLeft = False
  movingRight = False
  score = 0
  level, fallFreq = calculateLevelAndFallFreq(score)

  fallingPiece = getNewPiece()
  nextPiece = getNewPiece()

  while True: # game loop
    if fallingPiece == None:
      # No falling piece in play, so start a new piece at the top
      fallingPiece = nextPiece
      nextPiece = getNewPiece()
      lastFallTime = time.time() # reset lastFallTime

      if not isValidPosition(board, fallingPiece):
        return # can't fit a new piece on the board, so game over

    checkForQuit()
    for event in pygame.event.get(): # event handling loop
      if event.type == KEYUP:
        if (event.key == K_p):
          # Pausing the game
          DISPLAYSURF.fill(BGCOLOR)
          pygame.mixer.music.stop()
          showTextScreen('Paused') # pause until a key press
          pygame.mixer.music.play(-1, 0.0)
          lastFallTime = time.time()
          lastMoveDownTime = time.time()
          lastMoveSidewaysTime = time.time()
        elif (event.key == K_LEFT or event.key == K_a):
          movingLeft = False
        elif (event.key == K_RIGHT or event.key == K_d):
          movingRight = False
        elif (event.key == K_DOWN or event.key == K_s):
          movingDown = False

      elif event.type == KEYDOWN:
        # moving the piece sideways
        if (event.key == K_LEFT or event.key == K_a) and isValidPosition(board, fallingPiece, adjX=-1):
          fallingPiece['x'] -= 1
          movingLeft = True
          movingRight = False
          lastMoveSidewaysTime = time.time()

        elif (event.key == K_RIGHT or event.key == K_d) and isValidPosition(board, fallingPiece, adjX=1):
          fallingPiece['x'] += 1
          movingRight = True
          movingLeft = False
          lastMoveSidewaysTime = time.time()

        # rotating the piece (if there is room to rotate)
        elif (event.key == K_UP or event.key == K_w):
          fallingPiece['rotation'] = (fallingPiece['rotation'] + 1) % len(PIECES[fallingPiece['shape']])
          if not isValidPosition(board, fallingPiece):
            fallingPiece['rotation'] = (fallingPiece['rotation'] - 1) % len(PIECES[fallingPiece['shape']])
        elif (event.key == K_q): # rotate the other direction
          fallingPiece['rotation'] = (fallingPiece['rotation'] - 1) % len(PIECES[fallingPiece['shape']])
          if not isValidPosition(board, fallingPiece):
            fallingPiece['rotation'] = (fallingPiece['rotation'] + 1) % len(PIECES[fallingPiece['shape']])

        # making the piece fall faster with the down key
        elif (event.key == K_DOWN or event.key == K_s):
          movingDown = True
          if isValidPosition(board, fallingPiece, adjY=1):
            fallingPiece['y'] += 1
          lastMoveDownTime = time.time()

        # move the current piece all the way down
        elif event.key == K_SPACE:
          movingDown = False
          movingLeft = False
          movingRight = False
          for i in range(1, BOARDHEIGHT):
            if not isValidPosition(board, fallingPiece, adjY=i):
              break
          fallingPiece['y'] += i - 1

    # handle moving the piece because of user input
    if (movingLeft or movingRight) and time.time() - lastMoveSidewaysTime > MOVESIDEWAYSFREQ:
      if movingLeft and isValidPosition(board, fallingPiece, adjX=-1):
        fallingPiece['x'] -= 1
      elif movingRight and isValidPosition(board, fallingPiece, adjX=1):
        fallingPiece['x'] += 1
      lastMoveSidewaysTime = time.time()

    if movingDown and time.time() - lastMoveDownTime > MOVEDOWNFREQ and isValidPosition(board, fallingPiece, adjY=1):
      fallingPiece['y'] += 1
      lastMoveDownTime = time.time()

    # let the piece fall if it is time to fall
    if time.time() - lastFallTime > fallFreq:
      # see if the piece has landed
      if not isValidPosition(board, fallingPiece, adjY=1):
        # falling piece has landed, set it on the board
        addToBoard(board, fallingPiece)
        score += removeCompleteLines(board)
        level, fallFreq = calculateLevelAndFallFreq(score)
        fallingPiece = None
      else:
        # piece did not land, just move the piece down
        fallingPiece['y'] += 1
        lastFallTime = time.time()

    # drawing everything on the screen
    DISPLAYSURF.fill(BGCOLOR)
    drawBoard(board)
    drawStatus(score, level)
    drawNextPiece(nextPiece)
    if fallingPiece != None:
      drawPiece(fallingPiece)

    pygame.display.update()
    FPSCLOCK.tick(FPS)


def makeTextObjs(text, font, color):
  surf = font.render(text, True, color)
  return surf, surf.get_rect()


def terminate():
  pygame.quit()
  sys.exit()


def checkForKeyPress():
  # Go through event queue looking for a KEYUP event.
  # Grab KEYDOWN events to remove them from the event queue.
  checkForQuit()

  for event in pygame.event.get([KEYDOWN, KEYUP]):
    if event.type == KEYDOWN:
      continue
    return event.key
  return None


def showTextScreen(text):
  # This function displays large text in the
  # center of the screen until a key is pressed.
  # Draw the text drop shadow
  titleSurf, titleRect = makeTextObjs(text, BIGFONT, TEXTSHADOWCOLOR)
  titleRect.center = (int(WINDOWWIDTH / 2), int(WINDOWHEIGHT / 2))
  DISPLAYSURF.blit(titleSurf, titleRect)

  # Draw the text
  titleSurf, titleRect = makeTextObjs(text, BIGFONT, TEXTCOLOR)
  titleRect.center = (int(WINDOWWIDTH / 2) - 3, int(WINDOWHEIGHT / 2) - 3)
  DISPLAYSURF.blit(titleSurf, titleRect)

  # Draw the additional "Press a key to play." text.
  pressKeySurf, pressKeyRect = makeTextObjs('Press a key to play.', BASICFONT, TEXTCOLOR)
  pressKeyRect.center = (int(WINDOWWIDTH / 2), int(WINDOWHEIGHT / 2) + 100)
  DISPLAYSURF.blit(pressKeySurf, pressKeyRect)

  while checkForKeyPress() == None:
    pygame.display.update()
    FPSCLOCK.tick()


def checkForQuit():
  for event in pygame.event.get(QUIT): # get all the QUIT events
    terminate() # terminate if any QUIT events are present
  for event in pygame.event.get(KEYUP): # get all the KEYUP events
    if event.key == K_ESCAPE:
      terminate() # terminate if the KEYUP event was for the Esc key
    pygame.event.post(event) # put the other KEYUP event objects back


def calculateLevelAndFallFreq(score):
  # Based on the score, return the level the player is on and
  # how many seconds pass until a falling piece falls one space.
  level = int(score / 10) + 1
  fallFreq = 0.27 - (level * 0.02)
  return level, fallFreq

def getNewPiece():
  # return a random new piece in a random rotation and color
  shape = random.choice(list(PIECES.keys()))
  newPiece = {'shape': shape,
        'rotation': random.randint(0, len(PIECES[shape]) - 1),
        'x': int(BOARDWIDTH / 2) - int(TEMPLATEWIDTH / 2),
        'y': -2, # start it above the board (i.e. less than 0)
        'color': random.randint(0, len(COLORS)-1)}
  return newPiece


def addToBoard(board, piece):
  # fill in the board based on piece's location, shape, and rotation
  for x in range(TEMPLATEWIDTH):
    for y in range(TEMPLATEHEIGHT):
      if PIECES[piece['shape']][piece['rotation']][y][x] != BLANK:
        board[x + piece['x']][y + piece['y']] = piece['color']


def getBlankBoard():
  # create and return a new blank board data structure
  board = []
  for i in range(BOARDWIDTH):
    board.append([BLANK] * BOARDHEIGHT)
  return board


def isOnBoard(x, y):
  return x >= 0 and x < BOARDWIDTH and y < BOARDHEIGHT


def isValidPosition(board, piece, adjX=0, adjY=0):
  # Return True if the piece is within the board and not colliding
  for x in range(TEMPLATEWIDTH):
    for y in range(TEMPLATEHEIGHT):
      isAboveBoard = y + piece['y'] + adjY < 0
      if isAboveBoard or PIECES[piece['shape']][piece['rotation']][y][x] == BLANK:
        continue
      if not isOnBoard(x + piece['x'] + adjX, y + piece['y'] + adjY):
        return False
      if board[x + piece['x'] + adjX][y + piece['y'] + adjY] != BLANK:
        return False
  return True

def isCompleteLine(board, y):
  # Return True if the line filled with boxes with no gaps.
  for x in range(BOARDWIDTH):
    if board[x][y] == BLANK:
      return False
  return True


def removeCompleteLines(board):
  # Remove any completed lines on the board, move everything above them down, and return the number of complete lines.
  numLinesRemoved = 0
  y = BOARDHEIGHT - 1 # start y at the bottom of the board
  while y >= 0:
    if isCompleteLine(board, y):
      # Remove the line and pull boxes down by one line.
      for pullDownY in range(y, 0, -1):
        for x in range(BOARDWIDTH):
          board[x][pullDownY] = board[x][pullDownY-1]
      # Set very top line to blank.
      for x in range(BOARDWIDTH):
        board[x][0] = BLANK
      numLinesRemoved += 1
      # Note on the next iteration of the loop, y is the same.
      # This is so that if the line that was pulled down is also
      # complete, it will be removed.
    else:
      y -= 1 # move on to check next row up
  return numLinesRemoved


def convertToPixelCoords(boxx, boxy):
  # Convert the given xy coordinates of the board to xy
  # coordinates of the location on the screen.
  return (XMARGIN + (boxx * BOXSIZE)), (TOPMARGIN + (boxy * BOXSIZE))


def drawBox(boxx, boxy, color, pixelx=None, pixely=None):
  # draw a single box (each tetromino piece has four boxes)
  # at xy coordinates on the board. Or, if pixelx & pixely
  # are specified, draw to the pixel coordinates stored in
  # pixelx & pixely (this is used for the "Next" piece).
  if color == BLANK:
    return
  if pixelx == None and pixely == None:
    pixelx, pixely = convertToPixelCoords(boxx, boxy)
  pygame.draw.rect(DISPLAYSURF, COLORS[color], (pixelx + 1, pixely + 1, BOXSIZE - 1, BOXSIZE - 1))
  pygame.draw.rect(DISPLAYSURF, LIGHTCOLORS[color], (pixelx + 1, pixely + 1, BOXSIZE - 4, BOXSIZE - 4))


def drawBoard(board):
  # draw the border around the board
  pygame.draw.rect(DISPLAYSURF, BORDERCOLOR, (XMARGIN - 3, TOPMARGIN - 7, (BOARDWIDTH * BOXSIZE) + 8, (BOARDHEIGHT * BOXSIZE) + 8), 5)

  # fill the background of the board
  pygame.draw.rect(DISPLAYSURF, BGCOLOR, (XMARGIN, TOPMARGIN, BOXSIZE * BOARDWIDTH, BOXSIZE * BOARDHEIGHT))
  # draw the individual boxes on the board
  for x in range(BOARDWIDTH):
    for y in range(BOARDHEIGHT):
      drawBox(x, y, board[x][y])


def drawStatus(score, level):
  # draw the score text
  scoreSurf = BASICFONT.render('Score: %s' % score, True, TEXTCOLOR)
  scoreRect = scoreSurf.get_rect()
  scoreRect.topleft = (WINDOWWIDTH - 150, 20)
  DISPLAYSURF.blit(scoreSurf, scoreRect)

  # draw the level text
  levelSurf = BASICFONT.render('Level: %s' % level, True, TEXTCOLOR)
  levelRect = levelSurf.get_rect()
  levelRect.topleft = (WINDOWWIDTH - 150, 50)
  DISPLAYSURF.blit(levelSurf, levelRect)


def drawPiece(piece, pixelx=None, pixely=None):
  shapeToDraw = PIECES[piece['shape']][piece['rotation']]
  if pixelx == None and pixely == None:
    # if pixelx & pixely hasn't been specified, use the location stored in the piece data structure
    pixelx, pixely = convertToPixelCoords(piece['x'], piece['y'])

  # draw each of the boxes that make up the piece
  for x in range(TEMPLATEWIDTH):
    for y in range(TEMPLATEHEIGHT):
      if shapeToDraw[y][x] != BLANK:
        drawBox(None, None, piece['color'], pixelx + (x * BOXSIZE), pixely + (y * BOXSIZE))


def drawNextPiece(piece):
  # draw the "next" text
  nextSurf = BASICFONT.render('Next:', True, TEXTCOLOR)
  nextRect = nextSurf.get_rect()
  nextRect.topleft = (WINDOWWIDTH - 120, 80)
  DISPLAYSURF.blit(nextSurf, nextRect)
  # draw the "next" piece
  drawPiece(piece, pixelx=WINDOWWIDTH-120, pixely=100)


if __name__ == '__main__':
  main()

写好以后,先用命令执行测试一下:

sudo python russianblock.py

执行结果看到如下内容就说明成功了:

随便按一个键就可以开始玩儿了,使用键盘的上下左右控制移动, 用空格控制反转,记得把你的耳机插入树莓派3.5mm 的音频复合接口, 然后开始享受游戏的乐趣吧!

如果失败,音乐就停啦!

好了! 这个简单的教程就写到这里, 后期教大家利用 pygame 和树莓派制作更多的好玩儿的小游戏!