游戏的三点要素
地图
- 地图背 景是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')
添加游戏动作,消除小图标
- 添加点击的音效
- 点击小图标后,有一个红色的边框,表示选中状态
- 当再次点击与第一次点击位置相同时,取消选中状态
- 再次点击不是第一次点击位置,判断图片是否相同,相同的话判断是否连通。连通消除,不是连通的取消选中状态。
- 不是相同图片,取消选中状态。
audio放入音效
class MainWindow():
# 省略之前的代码 函数 。。。
# 以下新增
_isFirst = True # 第一次点击小头像
_isGameStart = False # 游戏是否开始
NONE_LINK = 0 # 不连通
LINK_LINK = 1 # 连通
NEIGHBOR_LINK = 10 # 相邻连通
EMPTY = -1
def addComponents(self):
# 省略之前的代码 函数 。。。
# 以下新增
# 添加绑定事件
self.canvas.bind('<Button-1>', self.clickCanvas) # 绑定鼠标左键
self.canvas.bind('<Button-2>', 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}
两个角相连算法实现
- 当P1点与P3点相连,P2点与P4点相连,P3与P4相连,判定P1与P2相连。
- 画出两条线,(0,3)当成P3,(0,6)当成P4,P3开始逐个遍历,存在图标的点跳过,找到空位置点,此时P1与P3水平直线相连;开始遍历P4,方法相同找到P2与P4直线相连,找到一个P4与P3垂直方向直线相连,判断出P1与P2相连。
- 直到遍历结束,没有相连的两点,判断不相连。
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 中 查找代价最低的节点。代价:起点到当前节点的距离 + 当前节点道终点的距离
起点到当前节点的距离:已经走过的步数
当前节点道终点的距离:估算的步数 - 检查代价最低节点相邻节点 是否可行
- 重复以上两步直到结束
结束条件:
- 终止节点加到close list中
- 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('<Button-2>', 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}