下一个记忆大师就是你!


在“色友”一再鄙视下,我开始向大佬妥协,留下了没技术的眼泪。还是感谢一波太傅给了我信心好吧,商业互吹!

#Memory Puzzle 玩法

  在这个游戏中,顾名思义,玩家要记忆每个方块背后的图案与图案颜色,当玩家点开白色方块(即遮住图案的方块,要是不遮住,还玩个P啊!)如果图案与颜色相同,那么判定为匹配,即寻找到了一对匹配。当玩家寻找完全部匹配图案,那么代表游戏胜利,并且游戏结束。
  为了给予玩家提示并且给供时间给玩家记忆图案位置与颜色。开局一条狗,装备靠自己,是兄der就来砍........咳咳咳,跑错场了。
开局一条....职业病职业病,开局向玩家展示全部图案,并且让玩家有时间记忆,之后全部遮盖,游戏就此开始。如果玩家寻找的不是一组匹配的图案,那么白色方块遮住。

游戏封面

以下基本按照编程顺序讲解,先讲主函数主干,然后修饰其他函数枝叶。

#编码风格

@声明与导入

# "Memory Puzzle"
# By CongTsang 543052251@qq.com
# https://congtsang.com/
# Released under a "Simplified BSD" license

import pygame, sys, random
from pygame.locals import *

  这个是写在顶部,用于声明这个游戏是什么,谁开发了她,以及用户在哪儿可以找到更多信息。并且源代码遵从“Simplified BSD”许可,可以自由复制和分享该游戏。
  之后我们import了一些包,是我们开发游戏时需要使用到的组建,其中pygame和sys是一定需要的。例如游戏退出时我们需要sys,摁键触发器我们需要pygame.locals这些诸如此类的包。

@远离幻数

window_x = 640
window_y = 480
FPS = 30
revealSpeed = 5
starevealSpeed = 2
boxSize = 50
gapSize = 10
boardWidth = 8
boardHeight = 5

xMargin = int((window_x - (boardWidth * (boxSize + gapSize))) / 2)
yMargin = int((window_y - (boardHeight * (boxSize + gapSize))) / 2)

  什么是幻数呢,幻数:把直接使用的常数叫做幻数。不使用幻数可以避免后期大量的更改,省时省力,还不耽误嘿嘿嘿。
例如:

windowArea = 640 * 480
......
windowArea = 640 * 480
......
windowArea = 640 * 480

  如上面这些夸张而不失典型的栗子,当你需要更改windowArea时,是不是要找到全部640 * 480,把他们一一更改,多麻烦。
例如:(正解)

window_width = 640
window_height = 480
windowArea = window_width * window_height
.......
windowArea = window_width * window_height
.......
windowArea = window_width * window_height

  这样我们只需要改第一二行就可以适配全局,多的不说了,假如你觉得没必要,当我没说,你开心就好。追不到不给你嘿嘿嘿的时候就知道难受了。当然幻数并不是全部都需要,过犹而不及的事多了,还得自己权衡利弊,多写写代码就会有体会。

@使用assert语句

assert (boardWidth * boardHeight) % 2 == 0, 'Board needs to have an even number of boxes for pairs of matches.'

  assert的作用是预警,顾名思义,在游戏运行前检查比游戏时崩溃要好的多吧。这段意思是表面:我们的匹配总是一对的,因此方块数目应该为偶数所以在运行游戏前检查方块数目是否为偶数报错,比游戏中多出一个独立的方块而让游戏崩溃进入死循环好吧。

@好看的代码

#            R    G    B
Gray     = (100, 100, 100)
Navyblue = (60,   60, 100)
White    = (255, 255, 255)
Red      = (255,   0,   0)
Green    = (0,   255,   0)
Blue     = (0,     0, 255)
Yellow   = (255, 255,   0)
Orange   = (255, 128,   0)
Purple   = (255,   0, 255)
Cyan     = (0,   255, 255)

  像这种统一格式的代码是不是很美观,啥?你说不美观。来人拖出去打死!!!!!这样打代码可以使代码变得易读。
  我们使用的颜色在后期需要用到,你也可以自定义自己的游戏颜色,并且用元组包含起来。什么RGB是啥?来人啊!拖出去打洗!!!!

RGB就是红色、绿色、蓝色这三色的组合,数值0-255。数值越大浓度越高颜色越深,三色混合就有了我们五彩斑斓的世界。够清晰够明了了把。

@使用常量

Donut = 'donut'  # 甜甜圈
Square = 'square' # 正方形
Diamond = 'diamond' # 钻石
Lines = 'lines' # 线条阴影
Oval = 'oval' # 椭圆

  如上面所示,也提到了一点幻数的感觉,基本都是一样的概念,避免后期大量更改,也使代码易读,减少键盘敲击量,因为懒。

@够多的图案

allColors = (Red, Green, Blue, Yellow, Orange, Purple, Cyan)
allShapes = (Donut, Square, Diamond, Lines, Oval)
assert len(allColors) * len(allShapes) * 2 >= boardWidth * boardHeight, 'Board is too big for the number of shapes/color defined.'

  为了让游戏拥有足够多的图案和组合图标,我们需要一个元组来保存这些东西。我定义了5个图案和7种颜色,你们可以定义多种颜色,图案的话,如果你知道这些图案怎么画(运用数学知识)那么可以多加一些图案,我数学垃圾就不加了。这里assert用于判断图案乘颜色一共35个组合然后需要一对,就拥有了70个方块空间的游戏板,那么boardWidth、boardHeight相乘需要小于等于70就可以了。游戏才能正常进行。

关于使用元组问题的话,因为元组不可变,相对于列表就固定比较多了,这是为了程序员知道这是不可变的,在之后的编程中不会想去更改元组,不然程序员就会用列表了。

@关于全局变量

def main():
    global FpsClock, Screen
    pygame.init()
    FpsClock = pygame.time.Clock()
    Screen = pygame.display.set_mode((window_x, window_y))
    pygame.display.set_caption("Memory Puzzle")

  之所以使用全局变量是因为在其他的函数我们需要使用它,所以呢声明为全局变量FpsClockScreen
  但是,过多的全局变量并不是优点,这会让程序容易起BUG,因为可能在某个函数中修改了这个全局变量,导致游戏崩溃,那么该去哪里找这个变量修改的位置呢,答案是很难,除非几个函数,但是大量的函数中,你能记得起哪里修改了,那么你很特别,非常的有慧根,那就来玩这款游戏吧!!!

#主函数

我们开始进入主函数讲解部分,代码按顺序讲。

@数据结构

mainBoard = getRandomizedBoard()
revealedBoxes = generateRevealedBoxesData(False)

  getRandomizedBoard()函数返回一个数据结构,表示当前游戏面板的状态。generateRevealedBoxesData(False)这个函数返回一个数据结构,表示当前哪些方块被遮盖了。这些返回值都是2D列表。

数据结构

例如:

mainBoard = [[(DONUT, BULE), (LINES, BULE), (SQUARE, ORANGE)],[(SQUARE, GREEN), (DONUT, BULE), (DIAMOND, YELLOW)],
            [(SQUARE, GREEN), (OVAL, YELLOW), (SQUARE, ORANGE)],[(DIAMOND, YELLOW), (LINES, BULE), (OVAL, YELLOW)]]

  上面的栗子是一个4X3的游戏板。这个时候我们通过mainBoard[i][j]就可以得出那个图案与颜色元组了,例如mainBoard[2][1]取出来就是(SQUARE, GREEN)这个元组值,要是不清楚可以先学学python的语法吧,这里我不多赘述了。毕竟游戏要紧,要是真的不会,可以在下方留言里提问。

@开始游戏动画

mouse_x = 0 # 用于存储鼠标X轴像素
mouse_y = 0 # 用于存储鼠标Y轴像素
firstSelection = None
Screen.fill(bgColor)
starGameAnimation(mainBoard)

  firstSelection这个变量我们用来存储玩家第一个点击的方块,把(mouse_x, mouse_y)存到这里面。Screen.fill(bgColor)是用于渲染背景的,填充bgColor颜色,他是一个元组形式的值。例如上面的颜色变量。starGameAnimation(mainBoard)这个函数是用来为游戏提供动画效果。具体的慢慢看下去。

@游戏主循环

while True:
    mouseClicked = False
    Screen.fill(bgColor)
    drawBoard(mainBoard, revealedBoxes)

    keys = pygame.key.get_pressed()
    for event in pygame.event.get():
        if event.type == QUIT or keys[K_ESCAPE]:
            pygame.quit()
            sys.exit()
        elif event.type == MOUSEMOTION:
            mouse_x, mouse_y = event.pos
        elif event.type == MOUSEBUTTONUP:
            mouse_x, mouse_y = event.pos
            mouseClicked = True

  一个游戏是通过不断的循环而刷新游戏界面的,所以我们会使用while True:来使我们的游戏循环起来。但是游戏一旦进入循环怎么退出呢?这就要使用到输入的问题了,一般来说玩家点击关闭窗口按钮后,或者点击Esc键时,游戏会退出,当然你可以自定义键,不过还是按常规出牌的好,不然玩家不玩你游戏了。你还怎么嘿嘿嘿。
  因而,我们使用一个循环for event in pygame.event.get():来处理事件,pygame.event.get()是获取游戏的全部事件,呈线性吧,一条一条取出来存到event里。后面的代码我相信不用解释了吧,应该通俗易懂了。不过要提一下的是event.pos返回一个元组,代表鼠标在窗口的位置。
MOUSEMOTION是鼠标移动,MOUSEBUTTONUP是鼠标摁下后释放。

@鼠标定位

box_x, box_y = getBoxAtPixel(mouse_x, mouse_y)
if box_x != None and box_y != None:
    if not revealedBoxes[box_x][box_y]:
        drawHighlightBox(box_x, box_y)
    if not revealedBoxes[box_x][box_y] and mouseClicked:
        revealBoxesAnimation(mainBoard, [(box_x, box_y)])
        revealedBoxes[box_x][box_y] = True

  getBoxAtPixel(mouse_x, mouse_y)这个函数返回俩个整数元组,把像素值转化为方块位置(例如:(1,1)),表示当前鼠标位于哪一个白色方块上,这有利于我们对mainBoard传入(1,1)(mainBoard[1][1])取方块。若鼠标不在方块上,则返回(None, None)。
  revealedBoxes[box_x][box_y]这个函数是表示鼠标点击的方块是否被遮盖。如果没有遮盖就为这个方块打高光drawHighlightBox(box_x, box_y),提示用户鼠标在方块上。

数据结构

  revealBoxesAnimation(mainBoard, [(box_x, box_y)])函数是当用户点击并且当前方块没有被遮盖(没有匹配过)时,播放白色方块消失的动画,传入的列表代表点击的那个方块位置。revealedBoxes[box_x][box_y] = True这个表示那个位置的方块无遮盖了,这个变量负责记录游戏状态已经更新的数据结构。

@处理第一次点击

if firstSelection == None:
    firstSelection = (box_x, box_y)
else:
    icon1shape, icon1color = getShapeAndColor(mainBoard, firstSelection[0], firstSelection[1])
    icon2shape, icon2color = getShapeAndColor(mainBoard, box_x, box_y)

  这里判断玩家有没有点击方块,因为一开始我们把firstSelection设为None,所以先存下我们第一次点击的图案和颜色。然后程序因为是一直循环,当第二次点击时,if语句判断为False,所以进入到else语句,使用getShapeAndColor(mainBoard, firstSelection[0], firstSelection[1])这个函数返回点击方块下的图案与颜色。所以返回我们第一次点击的和第二次点击的图案和颜色。

@处理匹配

if icon1shape != icon2shape or icon1color != icon2color:
    pygame.time.wait(500)
    coverBoxesAnimation(mainBoard, [(firstSelection[0], firstSelection[1]), (box_x, box_y)])
    revealedBoxes[firstSelection[0]][firstSelection[1]] = False
    revealedBoxes[box_x][box_y] = False
elif hasWon(revealedBoxes):
    gameWonAnimation(mainBoard)
    pygame.time.wait(500)

    mainBoard = getRandomizedBoard()
    revealedBoxes = generateRevealedBoxesData(False)

    drawBoard(mainBoard, revealedBoxes)
    pygame.display.update()
    pygame.time.wait(500)

    starGameAnimation(mainBoard)
firstSelection = None

  接着上面,我们就要判断两次点击是否匹配。pygame.time.wait(500)这里我们让游戏停止500ms,为的是让玩家认识到自己匹配不成功,显示图案500ms让玩家记住。然后coverBoxesAnimation(mainBoard, [(firstSelection[0], firstSelection[1]), (box_x, box_y)])这个函数把不匹配的一对方块遮盖,之后我们把之前标记游戏状态的变量revealedBoxes让不匹配图案变为False(即没点击过)
  如果玩家点击的是最后一对,我们可以用gameWonAnimation(mainBoard)来检测玩家赢与否,赢了的话,那么gameWonAnimation(mainBoard)函数绘制赢后的动画效果。等待500ms后,重新刷新游戏板,getRandomizedBoard()drawBoard(mainBoard, revealedBoxes)函数用于生成随机图案组合来开始新的一局,返回列表的列表(即2D“数组”)。然后pygame.display.update()是用来刷新屏幕的,因为我们开始了新的一局。
  我们还有一种情况是匹配了,但是游戏还要继续,所以我们直接用一句firstSelection = None来表示下一次匹配。因为游戏一直循环嘛。

@刷新游戏

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

  这里我们使用屏幕刷新和限制游戏帧率来对我们电脑好一点,不然电脑会以最快的速度运行这个游戏,帧数估计上千,电脑立马发热,所以我们要把帧数调整为合适的值。FpsClock.tick(FPS)这个函数用于限制游戏循环速度,每秒30帧。在主函数一开头FpsClock = pygame.time.Clock()我们声明了FpsClock并且设为全局变量。

#枝叶辅助函数

我们开始进入主函数没有实现的辅助函数,这样写有利于我们知道游戏骨架,然后慢慢修饰枝叶。在开发中这种方法非常有用。共勉!

@揭开方块

def generateRevealedBoxesData(val):
    revealedBoxes = []
    for i in range(boardWidth):
        revealedBoxes.append([val] * boardHeight)
    return revealedBoxes

  generateRevealedBoxesData(False)在主函数中,我们一开始是这样传入val参数,我们利用boardWidth值来循环多少次,我们创建的是垂直列,这才符合[x][y]的感觉,不然就要反人类的使用[y][x]来表示水平行了。传入的False代表方块未被点击过,即遮盖状态。我们使用append追加后面的数据集。然后返回这个游戏板到mainBoard。

@获取所有图标

def getRandomizedBoard():
    icons = []
    for color in allColors:
        for shape in allShapes:
            icons.append((shape, color))

    random.shuffle(icons)
    numIconUsed = int(boardWidth * boardHeight / 2)
    icons = icons[:numIconUsed] * 2
    random.shuffle(icons)

    board = []
    for x in range(boardWidth):
        column = []
        for y in range(boardHeight):
            column.append(icons[0])
            del icons[0]
        board.append(column)
    return board

  这里我们实现了随机生成游戏面板图案和颜色。使用icons来记录我们的图案和颜色集合。然后打乱这些集合random.shuffle(icons),之后因为我们需要一对,之前只生成了每样一个,所以我们需要复制以下每一个样式,icons = icons[:numIconUsed] * 2,这就生成了每样一对了。然后再次打乱。
  之后我们使用board来保存我们的图案样式,通过追加方式添加icons的第一组样式,然后通过del来剔除添加过后的样式,这样所有的样式就会向前移动一个位置。

什么是剔除?
剔除图

@开始动画

def starGameAnimation(board):
    pygame.time.wait(500)
    coveredBoxes = generateRevealedBoxesData(False)
    boxes = []
    for x in range(boardWidth):
        for y in range(boardHeight):
            boxes.append((x, y))
    random.shuffle(boxes)
    boxGroups = __splitIntoGroupsOf(15, boxes)

    drawBoard(board, coveredBoxes)
    for boxGroup in boxGroups:
        revealBoxesAnimation(board, boxGroup)
        coverBoxesAnimation(board, boxGroup)

  这里我们讲解开始动画的实现,我们把(x, y)元组添加到boxes里面,然后打乱boxes元组,然后通过__splitIntoGroupsOf(15, boxes)函数来切割整个boxes元组,分为15个一组(后面不足15就少一点为一组),函数返回一个以15个元组为一列的列表。然后遍历这个列表,有几组就几次,调用revealBoxesAnimation(board, boxGroup)coverBoxesAnimation(board, boxGroup)来显示和隐藏白色方块。

@切割列表

def __splitIntoGroupsOf(groupSize, theList):
    result = []
    for i in range(0, len(theList), groupSize):
        result.append(theList[i:i + groupSize])
    return result

  这个函数不在主函数中,我就添加了__这个东西来标记辅助函数,这个函数把传入的列表List切割成几份。range(0, len(theList), groupSize)表示0-整个List长度,并且步长为groupSize,例如一个列表有5个,步长为2.那么就分为0,2,4,5。即List[0:2],List[2:4],List[4:5]这四份,存储在result中返回。

@像素转换坐标

def getBoxAtPixel(x, y):
    for box_x in range(boardWidth):
        for box_y in range(boardHeight):
            left, top = __leftTopCoordsOfBox(box_x, box_y)
            boxRect = pygame.Rect(left, top, boxSize, boxSize)
            if boxRect.collidepoint(x, y):
                return (box_x, box_y)
    return (None, None)

  因为我们需要计算鼠标是否在某个方块上,所以需要把像素转换为我们游戏方块坐标,因此通过这个函数实现,__leftTopCoordsOfBox(box_x, box_y)该函数返回方块的左边和顶部像素位置,然后用pygame.Rect(left, top, boxSize, boxSize)返回一个方块的对象,该对象有许多变量,例如:boxRect.sizeboxRect.width等等。接着判断鼠标(x, y)是否与方块重叠(即是否发生碰撞boxRect.collidepoint(x, y))是的话返回这个方块的坐标位置,这就实现了转换。若鼠标不在方块上,就返回(None, None)。

@获取左边和顶部

def __leftTopCoordsOfBox(box_x, box_y):
    left = box_x * (boxSize + gapSize) + xMargin
    top = box_y * (boxSize + gapSize) + yMargin
    return (left, top)

  函数返回一个(left, top)的元组。xMarginyMargin这俩变量在幻数那一节提到,但没具体分析,这里讲一下他是干嘛的:顾名思义,他是游戏面板与窗口的外间距。x代表与左边窗口的,y是上边窗口的。

示意图

@遮盖与揭开

def coverBoxesAnimation(board, boxesToCover):
    for coverage in range(0, boxSize + starevealSpeed, starevealSpeed):
        __drawBoxCovers(board, boxesToCover, coverage)

  这里又运用到了range的功能,我们以方块为全部长度,加上遮盖速度是为了最后的步长加上它后刚好使方块遮住,因为动画使一帧一帧实现的,我们就可以利用我们眼睛的残像来制作动画效果。


def revealBoxesAnimation(board, boxesToReveal):
    for coverage in range(boxSize, -1, -revealSpeed):
        __drawBoxCovers(board, boxesToReveal, coverage)

  和遮盖一样的道理,从方块大小开始依次递减来绘画出动画。这里我把遮盖和揭开速度设成2和5,方便遮盖时玩家能有充足的时间记忆,揭开动画快点是省时间。

@绘制遮盖方块

def __drawBoxCovers(board, boxes, coverage):
    for box in boxes:
        left, top = __leftTopCoordsOfBox(box[0], box[1])
        pygame.draw.rect(Screen, bgColor, (left, top, boxSize, boxSize))
        shape, color = getShapeAndColor(board, box[0], box[1])
        __drawIcon(shape, color, box[0], box[1])
        if coverage > 0:
            pygame.draw.rect(Screen, boxColor, (left, top, coverage, boxSize))
    pygame.display.update()
    FpsClock.tick(FPS)

  我们通过pygame自带的画图函数pygame.draw.rect(Screen, bgColor, (left, top, boxSize, boxSize))来画一个矩形,第一个参数表示在哪里画,之后是什么颜色,表示方位、大小的一个元组。getShapeAndColor(board, box[0], box[1])该函数读取游戏板后返回(形状, 颜色)元组。然后__drawIcon(shape, color, box[0], box[1])用来绘制每个图案与颜色。如果遮盖coverage没有弄完,就继续生成矩形来生成一系列动画。

@获取样式

def getShapeAndColor(board, box_x, box_y):
    return board[box_x][box_y][0], board[box_x][box_y][1]

  这里通过之前存储的图案元组,返回相应位置的样式。

@绘制图案(核心)

def __drawIcon(shape, color, box_x, box_y):
    quarter = int(boxSize * 0.25)
    half = int(boxSize * 0.5)

    left, top = __leftTopCoordsOfBox(box_x, box_y)

    if shape == Donut:
        pygame.draw.circle(Screen, color, (left + half, top + half), half - 5)
        pygame.draw.circle(Screen, bgColor, (left + half, top + half), quarter - 5)
    elif shape == Square:
        pygame.draw.rect(Screen, color, (left + quarter, top + quarter, boxSize - half, boxSize - half))
    elif shape == Diamond:
        pygame.draw.polygon(Screen, color, (
        (left + half, top), (left + boxSize - 1, top + half), (left + half, top + boxSize - 1), (left, top + half)))
    elif shape == Lines:
        for i in range(0, boxSize, 10):
            pygame.draw.line(Screen, color, (left, top + i), (left + i, top))
            pygame.draw.line(Screen, color, (left + i, top + boxSize - 1), (left + boxSize - 1, top + i))
    elif shape == Oval:
        pygame.draw.ellipse(Screen, color, (left, top + quarter, boxSize, half))

  到了这一步,就是体现我们数学绘画的关键核心步骤了,如果你数学好,对图形了解特别清楚,那么这里你可以随便添加自己想要的图形,那么我这里给出了栗子。通过quarterhalf来存储1/4和1/2处的像素。然后判断我们读取的图案是什么形状,绘制相应图案。这个你们自己看着办吧。一一解释有些累赘。其实我就是想掩盖数学垃圾的事实,别说出来。相信你们可以盲目分析出来。

@绘制游戏板

def drawBoard(board, revealed):
    for box_x in range(boardWidth):
        for box_y in range(boardHeight):
            left, top = __leftTopCoordsOfBox(box_x, box_y)
            if not revealed[box_x][box_y]:
                pygame.draw.rect(Screen, boxColor, (left, top, boxSize, boxSize))
            else:
                shape, color = getShapeAndColor(board, box_x, box_y)
                __drawIcon(shape, color, box_x, box_y)

  这个函数针对每一个方块都调用一次__drawIcon(shape, color, box_x, box_y),遍历每一个方块,绘制完后绘制一个白色方块遮住。

@打高光

def drawHighlightBox(box_x, box_y):
    left, top = __leftTopCoordsOfBox(box_x, box_y)
    pygame.draw.rect(Screen, highlightColor, (left - 5, top - 5, boxSize + 10, boxSize + 10), 4)

  这个函数在之前提到过,用于鼠标悬停在某一方块上时,高亮当前方块。

@判断输赢

def hasWon(revealedBoxes):
    for i in revealedBoxes:
        if False in i:
            return False
    return True

  该函数通过取每个方块的标志,revealedBoxes这个表示游戏状态表示的变量记录是否被揭开。若每个方块都被揭开,那么该游戏就判断为赢了。其中运用if False in i语句来匹配i是否有False。

@绘制赢后动画

def gameWonAnimation(board):
    coveredBoxes = generateRevealedBoxesData(True)
    color1 = lightbgColor
    color2 = bgColor

    for i in range(5):
        color1, color2 = color2, color1
        Screen.fill(color1)
        drawBoard(board, coveredBoxes)
        __print_text(font_1, window_x / 6 + 50, window_y / 8, 'You Win!')
        pygame.display.update()
        pygame.time.wait(500)

  我们把每个方块的标志revealedBoxes都置为True,然后通过画面闪动颜色来表示赢了。range(5)表示闪动5次,然后用__print_text(font_1, window_x / 6 + 50, window_y / 8, 'You Win!')函数来打印文字。之后刷新游戏屏幕,每次刷新等500ms。

@打印文字

def __print_text(font, x, y, text, color=(255, 255, 255)):
    imgText = font.render(text, True, color)
    Screen.blit(imgText, (x, y))

  我们运用pygame库里的字体函数render来渲染我们的文字,以此来显示在我们的窗口上。不过在之前需要初始化以下。如下代码。

pygame.font.init()
font_1 = pygame.font.Font(None, 60) # None表示使用默认字体,60表示文字大小

#源代码

import pygame, sys, random
from pygame.locals import *

window_x = 640
window_y = 480
FPS = 30
revealSpeed = 5
starevealSpeed = 2
boxSize = 50
gapSize = 10
boardWidth = 8
boardHeight = 5
assert (boardWidth * boardHeight) % 2 == 0, 'Board needs to have an even number of boxes for pairs of matches.'
xMargin = int((window_x - (boardWidth * (boxSize + gapSize))) / 2)
yMargin = int((window_y - (boardHeight * (boxSize + gapSize))) / 2)

#            R    G    B
Gray     = (100, 100, 100)
Navyblue = (60,   60, 100)
White    = (255, 255, 255)
Red      = (255,   0,   0)
Green    = (0,   255,   0)
Blue     = (0,     0, 255)
Yellow   = (255, 255,   0)
Orange   = (255, 128,   0)
Purple   = (255,   0, 255)
Cyan     = (0,   255, 255)

pygame.font.init()
font_1 = pygame.font.Font(None, 60)

bgColor = Navyblue
lightbgColor = Gray
boxColor = White
highlightColor = Cyan

Donut = 'donut'
Square = 'square'
Diamond = 'diamond'
Lines = 'lines'
Oval = 'oval'

allColors = (Red, Green, Blue, Yellow, Orange, Purple, Cyan)
allShapes = (Donut, Square, Diamond, Lines, Oval)
assert len(allColors) * len(
    allShapes) * 2 >= boardWidth * boardHeight, 'Board is too big for the number of shapes/color defined.'


def __print_text(font, x, y, text, color=(255, 255, 255)):
    imgText = font.render(text, True, color)
    Screen.blit(imgText, (x, y))


def __splitIntoGroupsOf(groupSize, theList):
    result = []
    for i in range(0, len(theList), groupSize):
        result.append(theList[i:i + groupSize])
    return result


def __drawBoxCovers(board, boxes, coverage):
    for box in boxes:
        left, top = __leftTopCoordsOfBox(box[0], box[1])
        pygame.draw.rect(Screen, bgColor, (left, top, boxSize, boxSize))
        shape, color = getShapeAndColor(board, box[0], box[1])
        __drawIcon(shape, color, box[0], box[1])
        if coverage > 0:
            pygame.draw.rect(Screen, boxColor, (left, top, coverage, boxSize))
    pygame.display.update()
    FpsClock.tick(FPS)


def __leftTopCoordsOfBox(box_x, box_y):
    left = box_x * (boxSize + gapSize) + xMargin
    top = box_y * (boxSize + gapSize) + yMargin
    return (left, top)


def __drawIcon(shape, color, box_x, box_y):
    quarter = int(boxSize * 0.25)
    half = int(boxSize * 0.5)

    left, top = __leftTopCoordsOfBox(box_x, box_y)

    if shape == Donut:
        pygame.draw.circle(Screen, color, (left + half, top + half), half - 5)
        pygame.draw.circle(Screen, bgColor, (left + half, top + half), quarter - 5)
    elif shape == Square:
        pygame.draw.rect(Screen, color, (left + quarter, top + quarter, boxSize - half, boxSize - half))
    elif shape == Diamond:
        pygame.draw.polygon(Screen, color, (
        (left + half, top), (left + boxSize - 1, top + half), (left + half, top + boxSize - 1), (left, top + half)))
    elif shape == Lines:
        for i in range(0, boxSize, 10):
            pygame.draw.line(Screen, color, (left, top + i), (left + i, top))
            pygame.draw.line(Screen, color, (left + i, top + boxSize - 1), (left + boxSize - 1, top + i))
    elif shape == Oval:
        pygame.draw.ellipse(Screen, color, (left, top + quarter, boxSize, half))


# in main function
def getRandomizedBoard():
    icons = []
    for color in allColors:
        for shape in allShapes:
            icons.append((shape, color))

    random.shuffle(icons)
    numIconUsed = int(boardWidth * boardHeight / 2)
    icons = icons[:numIconUsed] * 2
    random.shuffle(icons)

    board = []
    for x in range(boardWidth):
        column = []
        for y in range(boardHeight):
            column.append(icons[0])
            del icons[0]
        board.append(column)
    return board


def generateRevealedBoxesData(val):
    revealedBoxes = []
    for i in range(boardWidth):
        revealedBoxes.append([val] * boardHeight)
    return revealedBoxes


def starGameAnimation(board):
    pygame.time.wait(500)
    coveredBoxes = generateRevealedBoxesData(False)
    boxes = []
    for x in range(boardWidth):
        for y in range(boardHeight):
            boxes.append((x, y))
    random.shuffle(boxes)
    boxGroups = __splitIntoGroupsOf(15, boxes)

    drawBoard(board, coveredBoxes)
    for boxGroup in boxGroups:
        revealBoxesAnimation(board, boxGroup)
        coverBoxesAnimation(board, boxGroup)


def coverBoxesAnimation(board, boxesToCover):
    for coverage in range(0, boxSize + starevealSpeed, starevealSpeed):
        __drawBoxCovers(board, boxesToCover, coverage)


def drawBoard(board, revealed):
    for box_x in range(boardWidth):
        for box_y in range(boardHeight):
            left, top = __leftTopCoordsOfBox(box_x, box_y)
            if not revealed[box_x][box_y]:
                pygame.draw.rect(Screen, boxColor, (left, top, boxSize, boxSize))
            else:
                shape, color = getShapeAndColor(board, box_x, box_y)
                __drawIcon(shape, color, box_x, box_y)


def getBoxAtPixel(x, y):
    for box_x in range(boardWidth):
        for box_y in range(boardHeight):
            left, top = __leftTopCoordsOfBox(box_x, box_y)
            boxRect = pygame.Rect(left, top, boxSize, boxSize)
            if boxRect.collidepoint(x, y):
                return (box_x, box_y)
    return (None, None)


def drawHighlightBox(box_x, box_y):
    left, top = __leftTopCoordsOfBox(box_x, box_y)
    pygame.draw.rect(Screen, highlightColor, (left - 5, top - 5, boxSize + 10, boxSize + 10), 4)


def getShapeAndColor(board, box_x, box_y):
    return board[box_x][box_y][0], board[box_x][box_y][1]


def revealBoxesAnimation(board, boxesToReveal):
    for coverage in range(boxSize, -1, -revealSpeed):
        __drawBoxCovers(board, boxesToReveal, coverage)


def hasWon(revealedBoxes):
    for i in revealedBoxes:
        if False in i:
            return False
    return True


def gameWonAnimation(board):
    coveredBoxes = generateRevealedBoxesData(True)
    color1 = lightbgColor
    color2 = bgColor

    for i in range(5):
        color1, color2 = color2, color1
        Screen.fill(color1)
        drawBoard(board, coveredBoxes)
        __print_text(font_1, window_x / 6 + 50, window_y / 8, 'You Win!')
        pygame.display.update()
        pygame.time.wait(500)


def main():
    global FpsClock, Screen
    pygame.init()
    FpsClock = pygame.time.Clock()
    Screen = pygame.display.set_mode((window_x, window_y))
    pygame.display.set_caption("Memory Puzzle")

    mouse_x = 0
    mouse_y = 0

    mainBoard = getRandomizedBoard()
    revealedBoxes = generateRevealedBoxesData(False)

    firstSelection = None
    Screen.fill(bgColor)
    starGameAnimation(mainBoard)

    while True:
        mouseClicked = False
        Screen.fill(bgColor)
        drawBoard(mainBoard, revealedBoxes)

        keys = pygame.key.get_pressed()
        for event in pygame.event.get():
            if event.type == QUIT or keys[K_ESCAPE]:
                pygame.quit()
                sys.exit()
            elif event.type == MOUSEMOTION:
                mouse_x, mouse_y = event.pos
            elif event.type == MOUSEBUTTONUP:
                mouse_x, mouse_y = event.pos
                mouseClicked = True

        box_x, box_y = getBoxAtPixel(mouse_x, mouse_y)
        if box_x != None and box_y != None:
            if not revealedBoxes[box_x][box_y]:
                drawHighlightBox(box_x, box_y)
            if not revealedBoxes[box_x][box_y] and mouseClicked:
                revealBoxesAnimation(mainBoard, [(box_x, box_y)])
                revealedBoxes[box_x][box_y] = True
                if firstSelection == None:
                    firstSelection = (box_x, box_y)
                else:
                    icon1shape, icon1color = getShapeAndColor(mainBoard, firstSelection[0], firstSelection[1])
                    icon2shape, icon2color = getShapeAndColor(mainBoard, box_x, box_y)
                    if icon1shape != icon2shape or icon1color != icon2color:
                        pygame.time.wait(500)
                        coverBoxesAnimation(mainBoard, [(firstSelection[0], firstSelection[1]), (box_x, box_y)])
                        revealedBoxes[firstSelection[0]][firstSelection[1]] = False
                        revealedBoxes[box_x][box_y] = False
                    elif hasWon(revealedBoxes):
                        gameWonAnimation(mainBoard)
                        pygame.time.wait(500)

                        mainBoard = getRandomizedBoard()
                        revealedBoxes = generateRevealedBoxesData(False)

                        drawBoard(mainBoard, revealedBoxes)
                        pygame.display.update()
                        pygame.time.wait(500)

                        starGameAnimation(mainBoard)
                    firstSelection = None

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


if __name__ == '__main__':
    main()

爱狂笑的孩子运气不会差