目录
1题目名称
2课程设计目的
3题目分析
4代码功能分析
5设计结果
6完整代码
人物头像连连看
根据课程所学内容进行练习、实践,巩固所学知识,提高编程能力。熟练掌握pygame,random等模块的应用,锻炼逻辑思维能力、解决实际问题能力。
首先分析连连看游戏的操作指南:第一次使用鼠标点击一图像,该图像此时为“被选中”,以特殊方式显示;再次以鼠标点击其他图像,若该图像与被选中的图像相同,且把第一个图像到第二个图像连起来,中间的直线不超过3根,则消掉这一对图像,否则第一个图像恢复成未被选中状态,而第二个图像变成被选中状态。
然后得出设计思路如下:
①生成成对的图片元素
②将图片元素打乱排布
③定义什么才算“相连”(两张图片的连线不多于3跟直线,或者说转角不超过2个)
④实现“相连”判断算法
⑤消除图片元素并判断是否消除完毕
涉及到的模块:sys,time,math,random,pygame,os
①首先导入模块sys、time、math、random、pygame、os
②定义类setting,类里面定义函数init,用来设置游戏窗口、标题、背景颜色、位置等,并且实例化设置类对象。
③定义类imagebtn,类里面定义了函数init、del、display、hide、is_checkable、click、reset、get_geometry。init用来缩放并加载图片素材;del用来防止语法错误;display用来描边;hide用来图像填充;is_checkable、click、reset用来返回check的状态;get_geometry用来获取几何图形。
④定义水平扫描的函数horizontal_scan。以 p1 和 p2 所在的两个行作为基准线,然后用垂直线在有效范围内扫描,一旦发现可行路径,直接返回。hLine1 和hLine2分别是扫描的上边线和下边线,leftLimit 和 rightLimit 分别是扫描的左边线和右边线。分为两次扫描,第一次扫描判断左边点是否为空,第二次扫描判断左右界限的位置,列出可能发生的情况,判断是否连接成功。
⑤定义垂直扫描的函数vertical_scan。以 p1 和 p2 所在的两个列作为基准线,然后用水平线在有效范围内扫描,一旦发现可行路径,直接返回。toplimit和bottomLimit分别作为扫描的上边线和下边线,vline1和vLine2分别是扫描的左垂直线和右垂直线。分两次扫描,第一次判断上边点是否为空,第二次扫描判断上下边线的位置,列出可能发生的情况,判断是否连接成功。
⑥定义函数can_clear,判断两个图片是否能消除。
⑦定义函数check_event,捕获、控制event。通过判断图片是否能相连接,决定是否清除图片或者恢复图片状态(取消被选中);图片还没有全被清除就继续执行程序,如果已经全被清除就退出程序。
⑧定义函数build_map,构建游戏地图布局。将成对的图片素材随机分布在窗口。
⑨定义函数is_over,判断图像是否全被清除。
⑩定义主函数,初始化pygame,创建游戏窗口,设置标题,加载游戏背景和背景音乐,准备图片元素,创建图片列表。游戏时,调用子函数执行;游戏结束时,弹出you win的画面,结束游戏。
运行截图:
游戏开始时:
游戏中:
游戏结束:
import sys, time
import math, random
import pygame
import os
from pygame.locals import *
# 全局的设置类
class Settings:
def __init__(self):
# 定义游戏窗口大小,为背景图片的一半
self.screen_size = (self.screen_width, self.screen_height) = (800, 480)
self.game_size = (self.game_row, self.game_col) = (6, 10)
self.map_total = self.game_row * self.game_col
self.element_num = 12
self.bg_color = (220, 220, 220)
self.title = '人物头像连连看'
self.win_image = 'images/you_win.png'
self.grid_size = 80
self.scale_size = (76, 76)
self.points = []
# 实例化设置对象
settings = Settings()
# 图像列表映射
map_list = []
#global map_list
image_list = []
# 图片按钮
class ImageBtn:
__checkable = True
__checked = False
def __init__(self, screen, image_path, x, y, number, element):
self.x = x
self.y = y
self.element = element
self.number = number
self.screen = screen
self.image = pygame.image.load(image_path)
# 因为图片原始大小为 200x200,所以要进行缩放
self.image = pygame.transform.scale(self.image, 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)
else:
pygame.draw.rect(self.image, (0,205,205,0), (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])
# 水平扫描
def horizontal_scan(points):
"""
水平标定
以 p1 和 p2 所在的两个行作为基准线,然后用垂直线在有效范围内扫描,
一旦发现可行路径,直接返回。
hLine1 和 hLine2 分别是扫描的上边线和下边线
leftLimit 和 rightLimit 分别是扫描的左边线和右边线
"""
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
print("Horizontal Scanning ...")
# 记录两条水平基准线
# 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
print("top:%d, bottom:%d, left:%d, right:%d" %(hLine1, hLine2, leftLimit, rightLimit))
# 寻找左边界线
i = p1_x
# 第一次扫描
# 当 i 大于 0 时,才进入循环
while i > 0:
# 判断左边点是否为空
if map_list[p1_y * column+ i - 1] != 0:
break
# 当左边点为空时会继续扫面下一个左边点
i -= 1
leftLimit = i;
print("第一次扫描(left)")
print("top:%d, bottom:%d, left:%d, right:%d" %(hLine1, hLine2, leftLimit, rightLimit))
# 第二次扫描
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
print("第二次扫描(left)")
print("top:%d, bottom:%d, left:%d, right:%d" %(hLine1, hLine2, leftLimit, rightLimit))
# 如果 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
print("第一次扫描(right)")
print("top:%d, bottom:%d, left:%d, right:%d" %(hLine1, hLine2, leftLimit, rightLimit))
i = p2_x
while i < column-1:
if map_list[p2_y * column + i + 1] != 0:
break
i += 1
if i < rightLimit:
rightLimit = i
print("第二次扫描(right)")
print("top:%d, bottom:%d, left:%d, right:%d" %(hLine1, hLine2, leftLimit, rightLimit))
if rightLimit == column-1:
return True
# 判断 leftLimit 和 rightLimit
if leftLimit > rightLimit:
# 如果左边界线超出右边界线,则无法连接
print("不能相连: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):
print(" j=%d"%j)
# 只要当前列有阻碍,马上跳出
if map_list[j * column + i] != 0:
# 回退一行
#j -= 1
break
j += 1
if j == hLine2:
print("最复杂的水平扫描成功!")
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
print("Vertical Scanning ...")
# 记录两条垂直基准线
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
print("第一次扫描(top)")
print("top:%d, bottom:%d, left:%d, right:%d" %(topLimit, bottomLimit, vLine1, vLine2))
# 第二次扫描
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
print("第二次扫描(top)")
print("top:%d, bottom:%d, left:%d, right:%d" %(topLimit, bottomLimit, vLine1, vLine2))
# 如果 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
print("第一次扫描(bottom)")
print("top:%d, bottom:%d, left:%d, right:%d" %(topLimit, bottomLimit, vLine1, vLine2))
i = p2_y
while i < row-1:
if map_list[p2_x + (i+1) * column] != 0:
break
i += 1
if i < bottomLimit:
bottomLimit = i
print("第二次扫描(bottom)")
print("top:%d, bottom:%d, left:%d, right:%d" %(topLimit, bottomLimit, vLine1, vLine2))
if bottomLimit == row-1:
return True
# 判断 topLimit 和 bottomLimit
if topLimit > bottomLimit:
# 如果上边界线超出下边界线,则无法连接
print("不能相连:topLimit > bottomLimit")
return False
else:
# 从上往下扫描
for i in range(topLimit, bottomLimit+1):
print("从上往下扫描:%d -> %d"%(i, bottomLimit))
j = vLine1 + 1
for j in range(vLine1+1, vLine2):
print(" j=%d"%j)
# 只要当前行有阻碍,马上跳出
if map_list[i * column + j] != 0:
# 回退一列
break
j += 1
if j == vLine2:
print("最复杂的垂直扫描成功!")
return True
return False
# 判断能否消除
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):
for event in pygame.event.get():
if event.type == QUIT:
print("exit...")
pygame.quit()
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("inside:", id(btn))
# 如果图片还没被消除
if btn.is_checkable():
print("Block:", geo)
print("number: %d, element: %d" %(btn.number, btn.element))
print("> checked")
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:
print("+++++++++++", point.number)
print("-----------", map_list)
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)
else:
print("Here is no picture")
# 可以退出了
break
# 构建游戏布局地图
def build_map():
t_list = []
m_list = []
# 构建成对数据
for i in range(0, settings.map_total, 2):
# 随机生成成对的图片元素标号(1-12),存放于 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
def main():
# 初始化 Pygame
pygame.init()
# 创建一个游戏窗口
screen = pygame.display.set_mode(settings.screen_size, 0, 0)
# 设置窗口标题
pygame.display.set_caption(settings.title)
# 在窗口中加载游戏背景
#background = pygame.image.load(settings.bg_image)
# 加载背景音乐
ROOTDIR = os.getcwd()
pygame.mixer.init()
pygame.mixer.music.load(os.path.join(ROOTDIR, "audios/Long Lost Penpal.mp3"))
pygame.mixer.music.set_volume(0.6)
pygame.mixer.music.play(-1)
# 准备图片元素
global map_list
map_list = build_map()
nc = 0
for m in map_list:
nc += 1
print("{0:>2}".format(m), end=' ')
if nc % 10 == 0:
print("")
# 创建图片列表
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 play:
if is_over():
play = False
for im in image_list:
im.display()
pygame.display.update()
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)
# 判断是否为主运行程序,是则调用 main()
if __name__ == '__main__':
main()