我一直相信,使生活如此美丽的,是我们藏起来的真诚和童心。

六一节快到了,520可以不过,但是儿童节绝对不能错过,毕竟谁还不是个几百个月的宝宝了,而且凭借自身智商,过这个节完全不成问题,哈哈哈哈哈。

作为程序媛,当然要用代码来过节啦,那就一起用 Python 开发个连连看小游戏来欢度六一吧!

从零造轮子那是不可能的,毕竟 Python 完全零基础,所以还是采取参考别人的例子的方式,从理解分析外加改造的过程中,来学习新的知识~

先来看一下最后的效果:

蹦蹦哒哒过六一:用Python开发连连看小游戏_python

实现步骤如下:

一. 安装依赖包

安装 pygame 依赖包

python -m pip install pygame
二. 代码流程分析

导入需要的依赖

import sys, time
import math, random
import threading
import pygame
from pygame.locals import *

 声明全局的设置类,并实例化设置对象

# 全局的设置类
class Settings:
    # __init__方法负责对象的初始化
    def __init__(self):
        # 定义游戏窗口大小(800*640)
        self.screen_size = (self.screen_width, self.screen_height) = (800, 640)
        # 游戏中的块数排列(8行*10列)
        self.game_size = (self.game_row, self.game_col) = (8, 10)
        # 连连看的总块数
        self.map_total = self.game_row * self.game_col
        # 定义元素个数(共16种图片)
        self.element_num = 16
        self.bg_color = (220, 220, 220)
        self.title = '智障儿童连连看'
        self.win_image = './images/win.png'
        self.lose_image = './images/lose.png'
        self.grid_size = 80
        self.scale_size = (76, 76)
        self.points = []
 
 
# 实例化设置对象
settings = Settings()

声明全局变量

# 图像列表映射
map_list = []
#global map_list
image_list = []
# 点击右上角直接结束游戏标志位
is_exit = False
# 时间截止标志位
time_out = False;

声明图片按钮类,即每一个消除的小方块

# 图片按钮类
class ImageBtn:
 
    __checkable = True
    __checked = False
 
    def __init__(self, screen, image_path, x, y, number, element):
        self.x = x
        self.y = y
        # 元素标号(1-16)
        self.element = element
        self.number = number
        self.screen = screen
        # 图片原始大小为 200x200,所以要进行缩放
        self.image = pygame.transform.scale(pygame.image.load(image_path), settings.scale_size)
 
    def __del__(self):
        pass
 
    def display(self):
        # 描边
        if self.__checked:
            pygame.draw.rect(self.image, (0,205,205,255), (0,0,self.image.get_width()-1,self.image.get_height()-1), 2)
        self.screen.blit(self.image, (self.x, self.y))
 
    def hide(self):
        self.__checked = False
        self.__checkable = False
        # 图片不可见
        self.image.fill((255, 255,240))
 
    def is_checkable(self):
        return self.__checkable
 
    def click(self):
        self.__checked = not self.__checked
        return self.__checked
 
    def reset(self):
        self.__checked = False
 
    def get_geometry(self):
        return (int(self.x), int(self.y), settings.scale_size[0], settings.scale_size[1])

 定义最核心的 水平扫描方法 horizontal_scan 和 垂直扫描方法 vertical_scan

# 水平扫描
def horizontal_scan(points):
    """
    水平标定
    以 p1 和 p2 所在的两个行作为基准线,然后用垂直线在有效范围内扫描,
    一旦发现可行路径,直接返回。
    hLine1    和 hLine2     分别是扫描的上边线和下边线
    leftLimit 和 rightLimit 分别是扫描的左边线和右边线
    """
    # 设置的列数为10
    column = settings.game_col
    p1_x = int(points[0].number % column)
    p1_y = int(points[0].number / column)
    p2_x = int(points[1].number % column)
    p2_y = int(points[1].number / column)
 
    # 如果 p1 和 p2 在同一行,则不符合要求
    if p1_y == p2_y:
        return False
 
    # 记录两条水平基准线
    # hLine1 为上水平线,hLine2 为下水平线
    if p1_y < p2_y:
        hLine1 = p1_y
        hLine2 = p2_y
    else:
        hLine1 = p2_y
        hLine2 = p1_y
 
    # 初始化左、右边界线为 0
    leftLimit = 0;
    rightLimit = column-1
    # 寻找左边界线
    i = p1_x
    # 第一次扫描
    # 当 i 大于 0 时,才进入循环
    while i > 0:
        # 判断左边点是否为空
        if map_list[p1_y * column+ i - 1] != 0:
            break
        # 当左边点为空时会继续扫描下一个左边点
        i -= 1
    leftLimit = i;
    # 第二次扫描
    i = p2_x
    while i > 0:
        if map_list[p2_y * column + i - 1] != 0:
            break
        i -= 1
    # leftLimit 记录左边界线,该界线所在的点为空或p1、p2本身
    if i > leftLimit:
        leftLimit = i
    # 如果 leftLimit 为 0,说明p1、p2已经在外界接通了,直接返回
    if leftLimit == 0:
        return True
    # 寻找右边界线
    i = p1_x
    while i < column-1:
        if map_list[p1_y * column + i + 1] != 0:
            break;
        i += 1
    rightLimit = i
    i = p2_x
    while i < column-1:
        if map_list[p2_y * column + i + 1] != 0:
            break
        i += 1
 
    if i < rightLimit:
        rightLimit = i
 
    if rightLimit == column-1:
        return True    # Bug
 
    # 判断 leftLimit 和 rightLimit
    if leftLimit > rightLimit:
        # 如果左边界线超出右边界线,则无法连接
        return False
    else:
        # 从左往右扫描
        for i in range(leftLimit, rightLimit+1):
            #print("从左往右扫描:%d -> %d"%(i, rightLimit))
            j = hLine1 + 1
            for j in range(hLine1+1, hLine2):
                # 只要当前列有阻碍,马上跳出
                if map_list[j * column + i] != 0:
                    # 回退一行
                    #j -= 1
                    break
                j += 1
            if j == hLine2:
                # 水平扫描成功
                return True
        return False;
 
# 垂直扫描
def vertical_scan(points):
    """
    垂直标定
    以 p1 和 p2 所在的两个列作为基准线,然后用水平线在有效范围内扫描,
    一旦发现可行路径,直接返回。
    """
    row = settings.game_row
    column = settings.game_col
    p1_x = int(points[0].number % column)
    p1_y = int(points[0].number / column)
    p2_x = int(points[1].number % column)
    p2_y = int(points[1].number / column)
 
    # 如果 p1 和 p2 在同一列,则不符合要求
    if p1_x == p2_x:
        return False
    # 记录两条垂直基准线
    if p1_x < p2_x:
        vLine1 = p1_x # 左垂直线
        vLine2 = p2_x # 右垂直线
    else:
        vLine1 = p2_x
        vLine2 = p1_x
 
    # 初始化上、下边界线
    topLimit = 0
    bottomLimit = row-1
 
    # 寻找上边界线
    i = p1_y
    # 第一次扫描
    # 当 i 大于 0 时,才进入循环
    while i > 0:
        if map_list[p1_x + (i-1) * column] != 0:
            break # 判断上边点是否为空
        i -= 1 # 当上边点为空时会继续扫面下一个上边点
    topLimit = i
    # 第二次扫描
    i = p2_y
    while i > 0:
        if map_list[p2_x + (i-1) * column] != 0:
            break
        i -= 1
    # topLimit 记录上边界线,该界线所在的点为空或p1、p2本身
    if i > topLimit:
        topLimit = i
 
    # 如果 topLimit 为 0,说明p1、p2已经在外界接通了,直接返回
    if topLimit == 0:
        return True
 
    # 寻找下边界线
    i = p1_y
    while i < row-1:
        if map_list[p1_x + (i+1) * column] != 0:
            break
        i += 1
    bottomLimit = i
    i = p2_y
    while i < row-1:
        if map_list[p2_x + (i+1) * column] != 0:
            break
        i += 1
 
    if i < bottomLimit:
        bottomLimit = i
 
    if bottomLimit == row-1:
        return True
 
    # 判断 topLimit 和 bottomLimit
    if topLimit > bottomLimit:
        # 如果上边界线超出下边界线,则无法连接
        return False
    else:
        # 从上往下扫描
        for i in range(topLimit, bottomLimit+1):
            j = vLine1 + 1
            for j in range(vLine1+1, vLine2):
                # 只要当前行有阻碍,马上跳出
                if map_list[i * column + j] != 0:
                    # 回退一列
                    #j -= 1
                    break
                j += 1
            if j == vLine2:
                # 垂直扫描成功
                return True
        return False

定义其他操作方法,包括 判断是否能消除事件处理(结束事件和点击事件)构建游戏图片布局检查所有图片是否全部消除游戏倒计时实现 共5个方法。

# 判断能否消除
def can_clear(points):
    # 如果两个元素不相同,还连个屁
    if points[0].element != points[1].element:
        return False
    else:
        # 如果两个元素垂直可以相连或者水平可以相连
        if vertical_scan(points) or horizontal_scan(points):
            return True
        else:
            return False
 
# 事件处理(结束事件,按钮点击事件)
def check_event(btn_list):
    """
        Key event capture and key_control
    """
    for event in pygame.event.get():
        if event.type == QUIT:
            global is_exit
            is_exit = True
            #print("exit...")
            sys.exit()
        # 判断是否按下按钮
        if event.type == MOUSEBUTTONDOWN:
            pos = pygame.mouse.get_pos()
            #print("Mouse button down, position:", pos)
 
            for btn in btn_list:
                geo = btn.get_geometry()
                x = geo[0]; y = geo[1]; w = geo[2]; h = geo[3]
                # 判断是否在图片块范围内
                if pos[0] > x and pos[0] < x+w and pos[1] > y and pos[1] < y+h:
                    #print("在图片块范围内:", id(btn))
                    # 如果图片还没被消除
                    if btn.is_checkable():
 
                        if not btn.click():
                            # 点自己是无效的
                            settings.points.clear()
                            break
 
                        # 前面已经记录了一个点
                        if settings.points != []:
                            settings.points.append(btn)
                            # 检查是否相同、能否相连
                            if can_clear(settings.points):
                                # 消灭它们
                                for point in settings.points:
                                    map_list[point.number] = 0
                                    point.number = 0
                                    point.hide()
                            else:
                                # 不匹配,恢复图片状态
                                for point in settings.points:
                                    point.reset()
                            # 判断完毕,清除记录的点
                            settings.points.clear()
 
                        # 前面没有记录点
                        else:
                            # 记录第一个点
                            settings.points.append(btn)
                    # 如果图片已经被消除
                    break
 
 
# 构建游戏图片布局
def build_map():
    t_list = []
    m_list = []
    # 成对数据构建
    for i in range(0, settings.map_total, 2):
        # 随机生成成对的图片元素标号(1-16),存放于 tmp_list
        e = math.ceil(random.random()*settings.element_num)
        # double append
        t_list.append(e)
        t_list.append(e)
    # 对全部数据进行打乱
    for i in range(0, settings.map_total, 1):
        # 将 tmp_list 中的图片元素随机排列在 m_list
        index = int(random.random()*(settings.map_total-i))
        m_list.append(t_list[index])
        # 删除已保存到m_list中的元素
        t_list.pop(index)
    return m_list
 
 
# 检查是否全部消除
def is_over():
    for each in map_list:
        if each > 0:
            return False
    return True
 
 
# 倒计时实现(默认120秒,超时则判断为游戏失败)
def game_clock():
    clock_ = 120
    while clock_>0:
        global is_exit
        if is_exit:
            break
        clock_ = clock_ - 1
        time.sleep(1)
        print("倒计时还剩:", clock_)
    print("时间停止,游戏结束啦!")
    global time_out
    time_out = True;

定义主函数调用 

def main():
    # 初始化 Pygame
    pygame.init()
    # 创建一个游戏窗口
    screen = pygame.display.set_mode(settings.screen_size, 0, 0)
    # 设置窗口标题
    pygame.display.set_caption(settings.title)
    # 设置倒计时计时器
    t = threading.Thread(target=game_clock,args=());
    t.start();
    
    # 准备图片元素
    global map_list
    map_list = build_map()
 
    # 创建图片列表
    for i in range(0, settings.map_total):
        x = int(i%settings.game_col) * settings.grid_size + (settings.grid_size -settings.scale_size[0])/2
        y = int(i/settings.game_col) * settings.grid_size + (settings.grid_size -settings.scale_size[0])/2
        element = './images/element_'+str(map_list[i])+'.png'
        image_list.append(ImageBtn(screen, element, x, y, i, map_list[i]))
 
    play = True
    i = 0
    
    while True:
        # 背景颜色填充
        screen.fill(settings.bg_color)
        # 如果倒计时结束,提示游戏输了
        if time_out:
            youlose = pygame.image.load(settings.lose_image)
            screen.blit(youlose, ((settings.screen_width-youlose.get_width())/2,(settings.screen_height-youlose.get_height())/2))
        elif play:
            if is_over():
                play = False
            for im in image_list:
                im.display()
        else:
            youwin = pygame.image.load(settings.win_image)
            screen.blit(youwin, ((settings.screen_width-youwin.get_width())/2,(settings.screen_height-youwin.get_height())/2))
        pygame.display.update()
        
        # 检查按键事件
        check_event(image_list)
        time.sleep(0.04)

这里有一个需要注意的地方是,我们的倒计时是需要单独开一个线程执行的,不然会阻塞主线程:

# 设置倒计时计时器
t = threading.Thread(target=game_clock,args=());
t.start();
三. 总结和提升

通过对这个案例的学习和改造,我们大概了解了 pygame 的常用写法,来补充下基础知识吧。

1. pygame模块

蹦蹦哒哒过六一:用Python开发连连看小游戏_python_02蹦蹦哒哒过六一:用Python开发连连看小游戏_python_03

2. Hello World快速入手模板

#导入pygame库
import pygame
#导入一些常用的函数和常量
from pygame.locals import *
#向sys模块的exit函数用来退出程序
from sys import exit
 
# 指定图像文件名称
background_image_filename = 'back.jpg'
mouse_image_filename = 'center.jpg'
 
#初始化pygame,为使用硬件做准备
pygame.init()
#创建窗口
screen = pygame.display.set_mode((480, 480), 0, 32)
#设置窗口标题
pygame.display.set_caption("Hello, World!")
#加载并转换图像
background = pygame.image.load(background_image_filename).convert()
mouse_cursor = pygame.image.load(mouse_image_filename).convert_alpha()
 
#游戏主循环
while True:
    for event in pygame.event.get():
        #接收到退出事件后退出程序
        if event.type == QUIT:
            exit()
    #将背景图画上去
    screen.blit(background, (0,0))
    #获得鼠标位置
    x, y = pygame.mouse.get_pos()
    #计算光标的左上角位置
    x-= mouse_cursor.get_width() / 2
    y-= mouse_cursor.get_height() / 2
    #把光标画上去
    screen.blit(mouse_cursor, (x, y))
    #刷新一下画面
    pygame.display.update()

随便找两张图片,重命名为代码中的名字即可,需要放在代码同层路径下。