Python GUI tkinter 开发连连看小游戏

完整的源码:Python GUI tkinter 开发连连看小游戏 源码

游戏的三点要素

地图

  • 地图背 景是10*10的方格
  • 每个方格内随机填充一 个蔬菜或水果

音效

  • 背景音乐
  • 鼠标点击蔬菜或水果的音乐

游戏规则

  • 连续点击两个方格
  • 方格内图片相同且可连接就消除这两个图片
  • 所有方格内图片消除后游戏完成结束

搭建游戏窗口

    def window_center(self, width, height):
        # 创建居中的窗口
        screenwidth = self.windows.winfo_screenwidth()  # 获取桌面屏幕的宽度
        screenheight = self.windows.winfo_screenheight()  # 获取桌面屏幕的高度
        size = "%dx%d+%d+%d" % (
        width, height, screenwidth / 2 - width / 2, screenheight / 2 - height / 2)  # 宽x高+X轴位置+Y轴位置
        self.windows.geometry(size)

注意:
tk.geometry() 对于这个方法,我们一般按照标准形式是"400x400+20+20"这样的参数,但是这里面的乘号是小写字母x不是X和*,也不是×。
同样也要求必须是整数,不能带小数点

添加菜单

两种菜单:

  • 下拉式菜单
  • 弹出式菜单
def add_components(self):
	# 创建菜单
	self.menubar = tk.Menu(self.windows, bg="lightgrey", fg="black")
	self.file_menu = tk.Menu(self.menubar, bg="lightgrey", fg="black")
	self.file_menu.add_command(label="新游戏", command=self.file_menu_clicked, accelerator="Ctrl+N")
	self.menubar.add_cascade(label="游戏", menu=self.file_menu)
	self.windows.configure(menu=self.menubar)

添加背景音乐

	def play_music(self, music, volume=0.5):
	    pygame.mixer.music.load(music)
	    pygame.mixer.music.set_volume(volume)
	    pygame.mixer.music.play()
	
	def stop_music(self):
	    pygame.mixer.music.stop()

添加游戏背景画布

  • 定义 canvas
  • 使用canvas绘制图片
def add_components(self):
	# 创建背景画布的canvas
	self.canvas = tk.Canvas(self.windows, bg="white", width=800, height=750)
	self.canvas.pack()
	
 def draw_background(self):
        self.background_im = ImageTk.PhotoImage(file="images/bg.png")
        self.canvas.create_image((0,0),anchor='nw', image=self.background_im) # 从0,0点开始 nw左上角对齐

设计游戏地图

分析一下图形,由行和列组成,各有10个小格,共有100个区域。

在这里插入图片描述

数组实现

    def init_map(self):
        """
        初始化地图数组
        0,1,2...24
        :return:
        """
        records = []
        for i in range(0, self._iconCount):
            for j in range(0, 4):
                records.append(i)
        np.random.shuffle(records)  # 所有元素随机排序
        self._map = np.array(records).reshape(10, 10)

点位与坐标的关系

游戏的背景图片,上面和左边都留有空白。取位置的至少需要考虑小图片的宽高和边框,更加横纵排位取坐标点。
在这里插入图片描述

class MainWindow:
	# 省略之前的代码 函数 。。。
	# 以下新增
    def getX(self, row):
        """
        获取row的X轴的起始坐标
        :return:
        """
        return self._margin + row * self._iconWidth

    def getY(self, column):
        """
        获取column的Y轴的起始坐标
        :return:
        """
        return self._margin + column * self._iconHeight

    def get_origin_Coordinate(self, row, column):
        """
        获取点位的左上角原点坐标
        """
        return self.getX(row), self.getY(column)

    def get_gamePoint(self, x, y):
        """
        获取玩家点击的x,y坐标在游戏地图上的点位
        :param x:
        :param y:
        :return:
        """
        for row in range(0, self._gameSize):
            x1 = self.getX(row)
            x2 = self.getX(row + 1)
            if x1 <= x < x2:
                point_row = row
        for column in range(0, self._gameSize):
            j1 = self.getY(column)
            j2 = self.getY(column + 1)
            if j1 <= y < j2:
                point_column = column
        return Point(point_row,point_column)

class Point:
    # 游戏中的点位
    def __init__(self,row,column):
        self.row=row
        self.column = column

    def isEqual(self, point):
        if self.row == point.row and self.column == point.column:
             return True
        else:
            return False

提取游戏的素材图标

把这些水果和蔬菜的小图片,提取出来
在这里插入图片描述

思路分析

在这里插入图片描述
第一张图片,0,0 右下角 w,h
第二张图片,w,0 右下角 2w,h

class MainWindow:
	# 省略之前的代码 函数 。。。
	# 以下新增
    def extractSmalllconList(self):
        # 提取小图片素材到icons列表中
        image_source = Image.open("images/fruits.png")
        for index in range(0,self._iconCount):
            # 裁剪图片,指定图片的左上角和右下角
            region = image_source.crop((index*self._iconWidth,0,(index+1)*self._iconWidth,self._iconHeight))
            self._icons.append(ImageTk.PhotoImage(region))

小图标绘制思路分析

当前是一个地图,10*10格子总共100个,每个位置称为一个点位。
每个各自左上的原点,是小图标开始的位置,再把上面切割好的图片,根据原点位置放入格子中,形成一个图像。
在根据随机函数,把每个小图标随机的放置在这100个格子中,绘制出地图。
在这里插入图片描述

图像绘制在地图上

def __init__(self):
	# 添加新的代码
	
 	# 准备小图标的图片
 	self.extractSmalllconList()
 		
def file_menu_clicked(self):
	self.stop_music()
	self.init_map()
	self.draw_map() # 把绘制地图放在菜单点击事件中

 def draw_map(self):
        # 根据地图绘制小图标
        for row in range(0,self._gameSize):
            for column in range(0, self._gameSize):
                x,y = self.get_origin_Coordinate(row, column)
                self.canvas.create_image((x,y), image=self._icons[self._map[row][column]], anchor='nw')
	

添加游戏动作,消除小图标

  1. 添加点击的音效
  2. 点击小图标后,有一个红色的边框,表示选中状态
  3. 当再次点击与第一次点击位置相同时,取消选中状态
  4. 再次点击不是第一次点击位置,判断图片是否相同,相同的话判断是否连通。连通消除,不是连通的取消选中状态。
  5. 不是相同图片,取消选中状态。

audio放入音效
在这里插入图片描述

class MainWindow():
 	# 省略之前的代码 函数 。。。
	# 以下新增
    _isFirst = True  # 第一次点击小头像
    _isGameStart = False  # 游戏是否开始
    
    NONE_LINK = 0  # 不连通
    LINK_LINK = 1  # 连通
    NEIGHBOR_LINK = 10  # 相邻连通
    
    EMPTY = -1

    def addComponents(self):
        # 省略之前的代码 函数 。。。
		# 以下新增
		
        # 添加绑定事件
        self.canvas.bind('', self.clickCanvas) # 绑定鼠标左键
        self.canvas.bind('', self.eggClickCanvas) # 鼠标中键
   
	def clickCanvas(self, event):
		if self._isGameStart:
		    point = self.getGamePoint(event.x, event.y)
		    if self._isFirst:
		        print('第一次点击')
		        self.playMusic('audio/click1.mp3')
		        self._isFirst = False
		        self.drawSelectedArea(point) # 选择的点位标红框
		        self._formerPoint = point
		    else:
		        print('第二次点击')
		        self.playMusic('audio/click2.mp3')
		        if point.isEqual(self._formerPoint):
		            print('两次点击的点位相同')
		            self.canvas.delete('rectRedOne') # 删除红框
		            self._isFirst = True
		        else:
		            print('两次点击的点位不同')
		            type = self.getLinkType(self._formerPoint, point)
		            if type['type'] != self.NONE_LINK:
		                self.clearLinkedBlocks(self._formerPoint, point)
		                self.canvas.delete('rectRedOne')
		                self._isFirst = True

    def drawSelectedArea(self, point):
        """选择的点位标红框"""
        lt_x, lt_y = self.getOriginCoordinate(point.row, point.column) # 左上角
        rb_x, rb_y = self.getOriginCoordinate(point.row + 1, point.column + 1) # 右下角,下一行下一列格子的左上角位置
        self.canvas.create_rectangle(lt_x, lt_y, rb_x, rb_y, outline='red', tags='rectRedOne')

    def getLinkType(self, p1, p2):
        """取得两个点位的连通情况"""
        if self._map[p1.row][p1.column] != self._map[p2.row][p2.column]:
            return {'type': self.NONE_LINK}

        if self.isNeighbor(p1, p2):
            print('两个小头像是相邻连通')
            return {'type': self.NEIGHBOR_LINK}

    def isNeighbor(self, p1, p2):
        """判断两个点位是否相邻"""
        # 垂直方向
        if p1.column == p2.column:
            # 大小判断
            if p2.row < p1.row:
                if p2.row + 1 == p1.row:
                    return True
            else:
                if p1.row + 1 == p2.row:
                    return True
        # 水平方向
        if p1.row == p2.row:
            # 大小判断
            if p2.column < p1.column:
                if p2.column + 1 == p1.column:
                    return True
            else:
                if p1.column + 1 == p2.column:
                    return True

        return False

    def clearLinkedBlocks(self, p1, p2):
        """消除两个点位的小头像"""
        print('消除选中的两个点位上的小头像')
        self.canvas.delete('image%d%d' % (p1.row, p1.column))
        self.canvas.delete('image%d%d' % (p2.row, p2.column))

        self._map[p1.row][p1.column] = self.EMPTY
        self._map[p2.row][p2.column] = self.EMPTY

        self.playMusic('audio/link.mp3')

游戏玩法规则分析

1. 相邻相连

在这里插入图片描述

2. 直线相连

第一次与第二次点击,判断从点位小的向点位大的,逐步平移判断是否存在,空的就继续平移,能达到第二次点击位置就判断相连。垂直方向同理。
在这里插入图片描述

3.一个角的相连

P1和P2两个位置存在一个拐角的时候,取P1的行和P2的列的交点,交点为P3,判断P1与P3是否水平直连,P2与P3是否垂直直连,满足这两个条件,就判断P1与P2相连。

在这里插入图片描述

4.两个角相连

  • 两个图标的位置P1,P2
  • 找出两个位置P3,P4,看P1与P3是否直连,P2与P4是否是直连,P3与P4是否直连。如果满足三个条件,P1与P2直连。
  • 根据这个思路,找出P3,P4的位置。从(0,3)开始向右遍历,碰到空点位,创建P3,开始子循环,(0,6)开始向右遍历,找到空点创建P4,根据直连逻辑,判断P3,P4是否连接。 当不存在P3,P4连接,继续循环。
    在这里插入图片描述

5. 三个角相连

在这里插入图片描述

6.更多角相连

需要一种算法,专门解决多角情况。
在这里插入图片描述

直连算法实现

class MainWindow():
 	# 省略之前的代码 函数 。。。
	# 以下新增
    LINE_LINK = 11  # 直线相连


	def isStraightLink(self, p1, p2):
		"""判断两个点位是否直线相连"""
	    # 水平方向判断
	    if p1.row == p2.row:
	        if p1.column > p2.column: # 找小的
	            start = p2.column
	            end = p1.column
	        else:
	            start = p1.column
	            end = p2.column
	        for column in range(start + 1, end):
	            if self._map[p1.row][column] != self.EMPTY: # p1.row 行 一样的
	                return False
	        return True
	    # 垂直方向判断
	    elif p1.column == p2.column:
	        if p1.row > p2.row: # 找小的
	            start = p2.row
	            end = p1.row
	        else:
	            start = p1.row
	            end = p2.row
	        for row in range(start + 1, end):
	            if self._map[row][p1.column] != self.EMPTY:  # p1.column列 一样的
	                return False
	        return True
	
	def getLinkType(self, p1, p2):
	    """取得两个点位的连通情况"""
	    if self._map[p1.row][p1.column] != self._map[p2.row][p2.column]:
	        return {'type': self.NONE_LINK}
		
	    if self.isNeighbor(p1, p2):
	        print('两个小头像是相邻连通')
	        return {'type': self.NEIGHBOR_LINK}
	    elif self.isStraightLink(p1, p2):  # 写的是这个函数 直连算法
	        print('两个小头像是直线相连')
	        return {'type': self.LINE_LINK}

一个角相连算法实现

P1与P2之间,找一个交叉点P3
会出现两个点,有一个满足就可以!
在这里插入图片描述

def isEmptyInMap(self, point):
	"""判断一个点位是否为空"""
    if self._map[point.row][point.column] == self.EMPTY:
        return True
    else:
        return False

def isOneCornerLink(self, p1, p2):
	"""一个角相连算法"""
    pointCorner = Point(p1.row, p2.column)
    if self.isEmptyInMap(pointCorner) and self.isStraightLink(p1, pointCorner) and self.isStraightLink(p2, pointCorner):
        return pointCorner

    pointCorner = Point(p2.row, p1.column)
    if self.isEmptyInMap(pointCorner) and self.isStraightLink(p1, pointCorner) and self.isStraightLink(p2, pointCorner):
        return pointCorner

    return False

def getLinkType(self, p1, p2):
	"""取得两个点位的连通情况"""
    if self._map[p1.row][p1.column] != self._map[p2.row][p2.column]:
        return {'type': self.NONE_LINK}

    if self.isNeighbor(p1, p2):
        print('两个小头像是相邻连通')
        return {'type': self.NEIGHBOR_LINK}
    elif self.isStraightLink(p1, p2):
        print('两个小头像是直线相连')
        return {'type': self.LINE_LINK}
    elif self.isOneCornerLink(p1, p2): # 添加 一个角相连算法
    	return {'type': self.ONE_LINK}

两个角相连算法实现

  1. 当P1点与P3点相连,P2点与P4点相连,P3与P4相连,判定P1与P2相连。
  2. 画出两条线,(0,3)当成P3,(0,6)当成P4,P3开始逐个遍历,存在图标的点跳过,找到空位置点,此时P1与P3水平直线相连;开始遍历P4,方法相同找到P2与P4直线相连,找到一个P4与P3垂直方向直线相连,判断出P1与P2相连。
  3. 直到遍历结束,没有相连的两点,判断不相连。
    在这里插入图片描述
def isTwoCornerLink(self, p1, p2):
     """两个角相连算法"""
     # 水平方向判断
     for column in range(0, self._gameSize):
     	if column == p1.column or column == p2.column:
            continue
        pointCorner1 = Point(p1.row, column)
        pointCorner2 = Point(p2.row, column)
        if self.isStraightLink(p1, pointCorner1) \
                and self.isStraightLink(pointCorner1, pointCorner2) \
                and self.isStraightLink(pointCorner2, p2) \
                and self.isEmptyInMap(pointCorner1) \
                and self.isEmptyInMap(pointCorner2):
            return {'p1': pointCorner1, 'p2': pointCorner2}

    # 垂直方向判断
    for row in range(0, self._gameSize):
        if row == p1.row or row == p2.row:
            continue
        pointCorner1 = Point(row, p1.column)
        pointCorner2 = Point(row, p2.column)
        if self.isStraightLink(p1, pointCorner1) \
                and self.isStraightLink(pointCorner1, pointCorner2) \
                and self.isStraightLink(pointCorner2, p2) \
                and self.isEmptyInMap(pointCorner1) \
                and self.isEmptyInMap(pointCorner2):
            return {'p1': pointCorner1, 'p2': pointCorner2}

    return False

def getLinkType(self, p1, p2):
	"""取得两个点位的连通情况"""
    if self._map[p1.row][p1.column] != self._map[p2.row][p2.column]:
        return {'type': self.NONE_LINK}

    if self.isNeighbor(p1, p2):
        print('两个小头像是相邻连通')
        return {'type': self.NEIGHBOR_LINK}
    elif self.isStraightLink(p1, p2):
        print('两个小头像是直线相连')
        return {'type': self.LINE_LINK}
    elif self.isOneCornerLink(p1, p2): 
    	return {'type': self.ONE_LINK}
    elif self.isTwoCornerLink(p1, p2): # 两个角相连算法
     return {'type': self.TWO_LINK}

优化连通性判断算法

Astar算法原理:

Astar算法步骤

  • 将起点A加入open list中 (open list 待检查的列表)
  • 查看起点A相邻节点,把其中可走节点加入open list中
  • 把A从open list移到 close list中 (close list 封闭列表 再寻找节点就不再关注了)
  • 从 open list 中 查找代价最低的节点。代价:起点到当前节点的距离 + 当前节点道终点的距离
    起点到当前节点的距离:已经走过的步数
    当前节点道终点的距离:估算的步数
  • 检查代价最低节点相邻节点 是否可行
  • 重复以上两步直到结束
    结束条件:
  1. 终止节点加到close list中
  2. open list为空

Astar算法实现

# -!- coding:utf-8 -!-


class Point:
    """游戏中的点位"""

    def __init__(self, row, column):
        self.row = row
        self.column = column

    def isEqual(self, point):
        if self.row == point.row and self.column == point.column:
            return True
        else:
            return False


class Node:
    """节点"""

    def __init__(self, point: Point, endPoint: Point):
        """
        初始化
        :param point: 点位
        :param endPoint: 终止点位
        """
        self.point = point  # 点位
        self.father = None  # 父节点
        self.g = 0  # 到起点的步数
        self.h = abs(endPoint.row - point.row) + abs(endPoint.column - point.column)  # 到终止节点的估算步数


class AStar:
    """
    A星算法
    """

    def __init__(self, map, startNode: Node, endNode: Node, passTag):
        """
        初始化函数
        :param map:地图
        :param startNode:开始节点
        :param endNode: 终止节点
        :param passTag: 可行走标记
        """
        self.openList = []  # 待探索节点列表
        self.closeList = []  # 已探索节点列表
        self.map = map  # 地图
        self.startNode = startNode  # 开始节点
        self.endNode = endNode  # 终止节点
        self.passTag = passTag  # 可行走标记

    def findMinFNode(self):
        """
        查找代价最低节点
        :return: Node
        """
        oneNode = self.openList[0]
        for node in self.openList:
            if node.g + node.h < oneNode.g + oneNode.h:
                oneNode = node
        return oneNode

    def nodeInCloseList(self, nearNode: Node):
        """
        节点是否在close list中
        :param nearNode: 待判断的节点
        :return: Node
        """
        for node in self.closeList:
            if node.point.isEqual(nearNode.point):
                return node
        return False

    def nodeInOpenList(self, nearNode: Node):
        """
        节点是否在open list中
        :param nearNode: 待判断的节点
        :return: Node
        """
        for node in self.openList:
            if node.point.isEqual(nearNode.point):
                return node
        return False

    def searchNearNode(self, minFNode: Node, offsetX, offsetY):
        """
        查找邻居节点
        :param minFNode: 最小代价节点
        :param offsetX: X轴偏移量
        :param offsetY: Y轴偏移量
        :return: Node 或 None
        """
        nearPoint = Point(minFNode.point.row + offsetX, minFNode.point.column + offsetY)
        nearNode = Node(nearPoint, self.endNode.point)

        # 越界检查
        if nearNode.point.row < 0 or nearNode.point.column < 0 or nearNode.point.row > len(
                self.map) - 1 or nearNode.point.column > len(self.map[0]) - 1:
            print('越界')
            return

        # 障碍检查
        if self.map[nearNode.point.row][nearNode.point.column] != self.passTag and not nearNode.point.isEqual(
                self.endNode.point):
            print('障碍')
            return

        # 判断是否在close list中
        if self.nodeInCloseList(nearNode):
            print('在close list中')
            return

        print("找到可行节点")

        if not self.nodeInCloseList(nearNode):
            self.openList.append(nearNode)
            nearNode.father = minFNode
            # 计算g值
            step = 1
            node = nearNode
            while node.point.isEqual(self.startNode.point):
                step += 1
                node = node.father
            nearNode.g = step
        return nearNode

    def start(self):
        if self.map[self.endNode.point.row][self.endNode.point.column] == self.passTag:
            return
        print("起点: ", self.startNode.point.column, self.startNode.point.row)
        print("终点: ", self.endNode.point.column, self.endNode.point.row)

        # 1.将起点加入open list
        self.openList.append(self.startNode)

        while True:
            # 2.从open list中查找代价最低的节点
            minFNode = self.findMinFNode()
            # 3.从open list中移除,并加入close list
            self.openList.remove(minFNode)
            self.closeList.append(minFNode)
            # 4.查找四个邻居节点
            self.searchNearNode(minFNode, 0, -1)  # 向上查找
            self.searchNearNode(minFNode, 1, 0)  # 向右查找
            self.searchNearNode(minFNode, 0, 1)  # 向下查找
            self.searchNearNode(minFNode, -1, 0)  # 向左查找

            # 5.判断是否终止
            endNode = self.nodeInCloseList(self.endNode)
            if endNode:
                print('两个节点是连通的')
                path = []
                node = endNode
                while not node.point.isEqual(self.startNode.point):
                    path.append(node)
                    if node.father:
                        node = node.father

                path.reverse()  # 逆向排序 得到起点到终点的路径
                return path

            if len(self.openList) == 0:
                print('两个节点不连通')
                return None

调用Astar算法

def getLinkTypeAStar(self, p1, p2):
    """通过A星算法取得两个点位的连通情况"""
    if self._map[p1.row][p1.column] != self._map[p2.row][p2.column]:
        return {'type': self.NONE_LINK}

    startNode = Node(p1, p2)
    endNode = Node(p2, p2)
    pathList = AStar(self._map, startNode, endNode, self.EMPTY).start()
    if pathList:
        return {'type': self.LINK_LINK}
    else:
        return {'type': self.NONE_LINK}

def clickCanvas(self, event):
    if self._isGameStart:
        point = self.getGamePoint(event.x, event.y)
        if self._isFirst:
            print('第一次点击')
            self.playMusic('audio/click1.mp3')
            self._isFirst = False
            self.drawSelectedArea(point)
            self._formerPoint = point
        else:
            print('第二次点击')
            self.playMusic('audio/click2.mp3')
            if point.isEqual(self._formerPoint):
                print('两次点击的点位相同')
                self.canvas.delete('rectRedOne')
                self._isFirst = True
            else:
                print('两次点击的点位不同')
                type = self.getLinkTypeAStar(self._formerPoint, point) # 修改成AStar算法
                if type['type'] != self.NONE_LINK:
                    self.clearLinkedBlocks(self._formerPoint, point)
                    self.canvas.delete('rectRedOne')
                    self._isFirst = True

添加彩蛋 便于测试

点击鼠标中键,直接删除一个小图标

def clearOneBlock(self, p1):
    """消除玩家点击的小头像"""
    print('消除选中的一个点位上的小头像')
    self.canvas.delete('image%d%d' % (p1.row, p1.column))
    self._map[p1.row][p1.column] = self.EMPTY

def eggClickCanvas(self, event):
     """彩蛋功能"""
     if self._isGameStart:
         point = self.getGamePoint(event.x, event.y)
         self.clearOneBlock(point)

def addComponents(self):
    # 创建菜单
    # 省略之前的代码 函数 。。。
 # 以下新增
    self.canvas.bind('', self.eggClickCanvas) # 绑定事件  鼠标中键

绘制最短路径及消除框

def drawPathPoint(self, point):
    """路径点位标绿框"""
    lt_x, lt_y = self.getOriginCoordinate(point.row, point.column)
    rb_x, rb_y = self.getOriginCoordinate(point.row + 1, point.column + 1)
    self.canvas.create_rectangle(lt_x, lt_y, rb_x, rb_y, outline='green',
                                 tags='path%d%d' % (point.row, point.column))

def getLinkTypeAStar(self, p1, p2):
    """通过A星算法取得两个点位的连通情况"""
    if self._map[p1.row][p1.column] != self._map[p2.row][p2.column]:
        return {'type': self.NONE_LINK}

    startNode = Node(p1, p2)
    endNode = Node(p2, p2)
    pathList = AStar(self._map, startNode, endNode, self.EMPTY).start()
    if pathList:
        # 绘制路径
        for node in pathList:
            self.drawPathPoint(node.point) # 添加 路径点位标绿框
        return {'type': self.LINK_LINK}
    else:
        return {'type': self.NONE_LINK}              

你可能感兴趣的:(Python知识点,python,pygame,gui)