用pygame实现滑动拼图(Slide Puzzle)小游戏
介绍
滑动拼图(Slide Puzzle) 是一款策略类游戏,把一幅图案分为相等的若干正方型小方块,取走其中一块制造出一块空位,利用多余的空格滑动其他小方块,把打乱的图案重新形成拼图。
这个游戏可以用Python语言和Pygame框架来开发,如下图所示。 在这里,用阿拉伯数字代替了拼图,只要把数字按顺序排列,就算解谜成功。但是,由于打乱拼图时采用了随机打乱的方式,有可能无解(一般情况下都是无解,小概率有解),所以增加了一点魔法,可以互换两个滑块上的内容,但是只能使用3次,如果用了3次魔法还是解不出来,那就只能New Game了。
文件目录
编写这个小游戏用到的代码和素材文件如下(在文末附有下载链接): myDirectory/
- sound/
- bg_music.mp3 # 背景音乐
- click.ogg # 点击音效
- hit.wav # 解谜成功音效
- run.py # 游戏运行程序
- Game.py # 游戏模块,生成游戏板、处理玩家鼠标点击事件等
- Player.py # 玩家模块,记录玩家得分、点击次数等
- RGB.py # 颜色模块,定义了所有RGB颜色常量
- GameConst.py # 常量模块,定义了游戏用到的常量
现在分别介绍一下.py 文件中的代码。
游戏运行程序
运行游戏用的脚本run.py 的全部代码如下:
import pygame, sys
import Game, GameConst, Player
from pygame.locals import *
def main():
game = Game.Game(GameConst.fps, GameConst.window_width, GameConst.window_height, 'Slide Puzzle')
player = Player.Player()
game.init_board(player)
while True:
for event in pygame.event.get():
if event.type == QUIT:
pygame.quit()
sys.exit()
elif event.type == MOUSEMOTION:
player.mouseX, player.mouseY = event.pos
elif event.type == pygame.KEYDOWN:
keys = pygame.key.get_pressed()
game.respond_to_keys(keys)
game.display_map(player)
game.respond_to_mouse_motion(player)
b1, b2, b3 = pygame.mouse.get_pressed()
game.respond_to_click(b1, b2, b3, player)
pygame.display.update()
game.tick()
if __name__ == '__main__':
while True:
main()
在这里显示了游戏的主要逻辑,包括定义一个main() 函数,建立game 对象和player 对象,用game.init_board() 方法初始化游戏板,建立游戏循环,在游戏循环中用game.display_map() 方法绘制游戏板,用game.respond_to_mouse_motion() 方法处理鼠标移动事件,用game.respond_to_click() 方法处理鼠标点击事件,最后将绘制好的游戏板打印到屏幕上,以及生成游戏的时间控制。
游戏模块
游戏模块Game.py 的全部代码如下:
import pygame, sys, random
import RGB, GameConst, Player
from pygame.locals import *
class Game():
def __init__(self, fps, window_width, window_height, caption):
pygame.init()
self.__f = fps
self.__c = pygame.time.Clock()
self.window_width = window_width
self.window_height = window_height
self.DISPLAYSURF = pygame.display.set_mode((self.window_width, self.window_height))
self.FONT1 = pygame.font.SysFont('stzhongsong', 18)
self.FONT2 = pygame.font.SysFont('Arial', 26)
self.__caption = caption
pygame.display.set_caption(caption)
self.message_F1 = []
self.message_1 = []
self.message_4 = []
self.tiles = list(range(1, 16))
self.answer = [[1 for i in range(5)] for j in range(5)]
self.answer_bak = [[1 for i in range(5)] for j in range(5)]
self.rect1 = 0
self.rect2 = 0
def _getd(self): return self.DISPLAYSURF
def _setd(self, value): self.DISPLAYSURF = value
screen = property(_getd, _setd)
def _getf(self): return self.__f
def _setf(self, value): self.__f = value
FPS = property(_getf, _setf)
def _getc(self): return self.__c
def _setc(self, value): self.__c = value
fpsClock = property(_getc, _setc)
def _getcaption(self): return self.__caption
def _setcaption(self, value): self.__caption = value
caption = property(_getcaption, _setcaption)
def init_board(self, player):
''' 初始化游戏板。'''
self.clear_message()
self.add_message_F1('Generating new puzzle...')
self.add_message_1('Reset')
self.add_message_1('New Game')
self.add_message_1('魔法:互换')
player.game_process = GameConst.GAMING
player.slide_times = 0
player.magic = 3
random.shuffle(self.tiles)
for i in range(GameConst.MapBlock_X):
for j in range(GameConst.MapBlock_Y):
if (i, j) == (3, 3):
continue
self.answer[i][j] = self.tiles[i*GameConst.MapBlock_X + j]
self.answer_bak[i][j] = self.tiles[i*GameConst.MapBlock_X + j]
self.answer[3][3] = 0
self.answer_bak[3][3] = 0
def add_message_F1(self, s):
''' 往消息队列中添加信息。'''
self.message_F1.append(s)
if len(self.message_F1) == 5:
self.message_F1 = []
self.message_F1.append(s)
def add_message_1(self, s):
''' 往功能按钮队列中添加内容。'''
self.message_1.append(s)
def tick(self):
''' 自动的暂停,控制帧速率 '''
self.fpsClock.tick(self.FPS)
def fill(self, color):
self.DISPLAYSURF.fill(color)
def print_text(self, font, x, y, text, color=RGB.White, shadow=True):
''' 使用选定的字体font,在(x, y)位置开始以颜色color打印文本text。'''
imgText = font.render(text, True, color)
self.DISPLAYSURF.blit(imgText, (x, y))
def print_text_from_center(self, font, centerx, centery, text, color=RGB.White, shadow=True):
''' 使用选定的字体font,在(x, y)位置开始以颜色color打印文本text。'''
imgText = font.render(text, True, color)
imgRect = imgText.get_rect()
(imgRect.centerx, imgRect.centery) = (centerx, centery)
(x, y) = imgRect.topleft
self.DISPLAYSURF.blit(imgText, (x, y))
def print_message_F1(self):
''' 在过场动画信息框内打印消息。'''
s = self.message_F1
left, top, width, height = GameConst.MsgBoxF1
for i, v in enumerate(s):
self.print_text(self.FONT1, left+GameConst.TextMargin, top+GameConst.TextMargin+i*GameConst.FontHeight, v, RGB.White)
def print_message_1(self):
''' 在位置信息框内打印消息。'''
s = self.message_1
left, top, width, height = GameConst.MsgBox1
for i, v in enumerate(s):
self.print_text(self.FONT1, left, top+i*GameConst.FontHeight, v, RGB.White)
def print_message_4(self):
''' 在位置信息框内打印消息。'''
s = self.message_4
left, top, width, height = GameConst.MsgBox4
for i, v in enumerate(s):
self.print_text(self.FONT1, left, top+i*GameConst.FontHeight, v, RGB.White)
def print_slide_times(self, player):
''' 打印滑块移动次数。'''
left, top, width, height = GameConst.MsgBox5
self.print_text(self.FONT1, left, top+GameConst.FontHeight, '已滑动次数:{}'.format(player.slide_times), RGB.White)
def print_magic_times(self, player):
''' 打印滑块移动次数。'''
left, top, width, height = GameConst.MsgBox6
self.print_text(self.FONT1, left, top+GameConst.FontHeight, '剩余魔法次数:{}'.format(player.magic), RGB.White)
def display_map(self, player):
self.fill(RGB.Black)
pygame.draw.rect(self.DISPLAYSURF, RGB.DarkSlateGray1, GameConst.MapBox, 1)
for i in range(0, GameConst.MapBlock_X):
for j in range(0, GameConst.MapBlock_Y):
if self.answer[i][j] == 0:
continue
left, top, width, height = GameConst.MapBox
position = left + j * GameConst.MapBlockSize + 1, top + i * GameConst.MapBlockSize + 1, GameConst.MapBlockSize-2, GameConst.MapBlockSize-2
pygame.draw.rect(self.DISPLAYSURF, RGB.Gainsboro, position, 1)
position = left + j * GameConst.MapBlockSize + 5, top + i * GameConst.MapBlockSize + 5, GameConst.MapBlockSize-10, GameConst.MapBlockSize-10
pygame.draw.rect(self.DISPLAYSURF, RGB.White, position, 0)
self.print_text_from_center(self.FONT2, left + j * GameConst.MapBlockSize + 50, top + i * GameConst.MapBlockSize + 50, str(self.answer[i][j]), RGB.Black)
self.print_message_F1()
self.print_message_1()
self.print_message_4()
self.print_slide_times(player)
self.print_magic_times(player)
def respond_to_click(self, b1, b2, b3, player):
''' 对鼠标点击的响应。'''
x, y = player.mouseX, player.mouseY
if b3:
pass
if b1:
if player.game_process == GameConst.GAMING:
MapBox = pygame.Rect(GameConst.MapBox)
if MapBox.collidepoint(x, y):
for i in range(0, GameConst.MapBlock_X):
for j in range(0, GameConst.MapBlock_Y):
if self.answer[i][j] == 0:
continue
left, top, width, height = GameConst.MapBox
position = left + j * GameConst.MapBlockSize + 5, top + i * GameConst.MapBlockSize + 5, GameConst.MapBlockSize-10, GameConst.MapBlockSize-10
rect = pygame.Rect(position)
if rect.collidepoint(x, y):
left, top, width, height = rect
judge = self.can_move(i, j)
self.move(judge, i, j, player)
MsgBox1 = pygame.Rect(GameConst.MsgBox1)
if MsgBox1.collidepoint(x, y):
left, top, width, height = GameConst.MsgBox1
for i in range(4):
for j in range(4):
self.answer[i][j] = self.answer_bak[i][j]
MsgBox2 = pygame.Rect(GameConst.MsgBox2)
if MsgBox2.collidepoint(x, y):
left, top, width, height = GameConst.MsgBox2
self.init_board()
MsgBox3 = pygame.Rect(GameConst.MsgBox3)
if MsgBox3.collidepoint(x, y):
left, top, width, height = GameConst.MsgBox3
if player.magic > 0:
player.magic -= 1
self.add_message_F1('互换两块板子,用鼠标在两块板子上依次点击:')
player.game_process = GameConst.CHANGING
else:
self.add_message_F1('魔法次数已经用完,无法交换。')
elif player.game_process == GameConst.CHANGING:
MapBox = pygame.Rect(GameConst.MapBox)
if MapBox.collidepoint(x, y):
for i in range(0, GameConst.MapBlock_X):
for j in range(0, GameConst.MapBlock_Y):
if self.answer[i][j] == 0:
continue
left, top, width, height = GameConst.MapBox
position = left + j * GameConst.MapBlockSize + 5, top + i * GameConst.MapBlockSize + 5, GameConst.MapBlockSize-10, GameConst.MapBlockSize-10
rect = pygame.Rect(position)
if rect.collidepoint(x, y):
if self.rect1 == 0:
self.rect1 = (i, j)
else:
self.rect2 = (i, j)
self.change(self.rect1, self.rect2)
self.rect1 = 0
self.rect2 = 0
player.game_process = GameConst.GAMING
elif player.game_process == GameConst.HERO_WIN:
MsgBox2 = pygame.Rect(GameConst.MsgBox2)
if MsgBox2.collidepoint(x, y):
left, top, width, height = GameConst.MsgBox2
self.init_board(player)
def change(self, rect1, rect2):
''' 交换两个滑块的内容。'''
tmp = self.answer[rect1[0]][rect1[1]]
self.answer[rect1[0]][rect1[1]] = self.answer[rect2[0]][rect2[1]]
self.answer[rect2[0]][rect2[1]] = tmp
def move(self, judge, i, j, player):
''' 移动滑块,即,交换当前滑块和空滑块的内容。'''
if judge == 'left':
player.get_sounds()['click'].play()
self.answer[i-1][j] = self.answer[i][j]
self.answer[i][j] = 0
player.slide_times += 1
elif judge == 'right':
player.get_sounds()['click'].play()
self.answer[i+1][j] = self.answer[i][j]
self.answer[i][j] = 0
player.slide_times += 1
elif judge == 'top':
player.get_sounds()['click'].play()
self.answer[i][j-1] = self.answer[i][j]
self.answer[i][j] = 0
player.slide_times += 1
elif judge == 'down':
player.get_sounds()['click'].play()
self.answer[i][j+1] = self.answer[i][j]
self.answer[i][j] = 0
player.slide_times += 1
else:
self.add_message_F1('can not move.')
if self.check_answer():
player.get_sounds()['hit'].play()
player.game_process = GameConst.HERO_WIN
self.message_4 = ['恭喜,你赢了!按Esc退出...']
def check_answer(self):
''' 检查滑块是否顺序排列。'''
for i in range(GameConst.MapBlock_X):
for j in range(GameConst.MapBlock_Y):
a = j + i * GameConst.MapBlock_X + 1
if (i, j) == (3, 3):
a = 0
if self.answer[i][j] != a:
return False
return True
def can_move(self, i, j):
''' 判断滑块是否能够移动。'''
if self.answer[i-1][j] == 0:
return 'left'
elif self.answer[i+1][j] == 0:
return 'right'
elif self.answer[i][j-1] == 0:
return 'top'
elif self.answer[i][j+1] == 0:
return 'down'
else:
return False
def respond_to_mouse_motion(self, player):
''' 对鼠标移动的响应。'''
x, y = player.mouseX, player.mouseY
MapBox = pygame.Rect(GameConst.MapBox)
if MapBox.collidepoint(x, y):
left, top, width, height = GameConst.MapBox
pygame.draw.rect(self.DISPLAYSURF, RGB.GhostWhite, (left, top, width+5, height+5), 4)
for i in range(0, GameConst.MapBlock_X):
for j in range(0, GameConst.MapBlock_Y):
if self.answer[i][j] == 0:
continue
left, top, width, height = GameConst.MapBox
position = left + j * GameConst.MapBlockSize + 5, top + i * GameConst.MapBlockSize + 5, GameConst.MapBlockSize-10, GameConst.MapBlockSize-10
rect = pygame.Rect(position)
if rect.collidepoint(x, y):
left, top, width, height = rect
pygame.draw.rect(self.DISPLAYSURF, RGB.GhostWhite, (left, top, width+3, height+3), 2)
MsgBox1 = pygame.Rect(GameConst.MsgBox1)
if MsgBox1.collidepoint(x, y):
left, top, width, height = GameConst.MsgBox1
pygame.draw.rect(self.DISPLAYSURF, RGB.GhostWhite, (left, top, width+2, height+2), 1)
MsgBox2 = pygame.Rect(GameConst.MsgBox2)
if MsgBox2.collidepoint(x, y):
left, top, width, height = GameConst.MsgBox2
pygame.draw.rect(self.DISPLAYSURF, RGB.GhostWhite, (left, top, width+2, height+2), 1)
MsgBox3 = pygame.Rect(GameConst.MsgBox3)
if MsgBox3.collidepoint(x, y):
left, top, width, height = GameConst.MsgBox3
pygame.draw.rect(self.DISPLAYSURF, RGB.GhostWhite, (left, top, width+2, height+2), 1)
def clear_message(self):
self.message_1 = []
self.message_4 = []
self.message_F1 = []
def respond_to_keys(self, keys):
''' 对按键输入的响应。'''
if keys[K_ESCAPE]:
pygame.quit()
sys.exit()
在这个文件中建立了Game 类,增加了一些方法,其中重要的有:
display_map() ,用来绘制游戏板。respond_to_mouse_motion() ,对鼠标移动的响应,在这里主要是移动到滑块或按钮上高亮显示。respond_to_click() ,对鼠标点击的响应。
屏幕上消息的打印在这里采用了队列的方式管理,就是建立一个数组:
self.message_F1 = []
然后增加在这个消息框内打印文字的方法:
def print_message_F1(self):
left, top, width, height = GameConst.MsgBoxF1
for i, v in enumerate(self.message_F1):
self.print_text(self.FONT1, left+GameConst.TextMargin, top+GameConst.TextMargin+i*GameConst.FontHeight, v, RGB.White)
在GameConst.py 中定义了MsgBoxF1 常量,标记出了这个消息框的左上角坐标和宽高:
MsgBoxF1 = (0, 0, 800, 600)
然后增加在消息框内添加文本的方法:
def add_message_F1(self, s):
''' 往消息队列中添加信息。'''
self.message_F1.append(s)
if len(self.message_F1) == 5:
self.message_F1 = []
self.message_F1.append(s)
在这里设置为只显示4行消息,如果文本超过4行,则覆盖之前的。
然后在display_map() 中调用print_message_F1() 方法:
def display_map(self, player):
self.fill(RGB.Black)
...
self.print_message_F1()
由于display_map() 在游戏循环中不断调用,就保证了message_F1 中的文本的持续显示。
这样,在后续要打印消息的时候就可以直接往数组中添加文本,而不必考虑在屏幕的哪个位置上显示等细节。
if player.magic > 0:
player.magic -= 1
self.add_message_F1('互换两块板子,用鼠标在两块板子上依次点击:')
player.game_process = GameConst.CHANGING
else:
self.add_message_F1('魔法次数已经用完,无法交换。')
有了这些方法,在游戏屏幕上打印文本就像在控制台使用print() 函数一样方便。
也可以增加清空消息的方法,在适当的时候(比如初始化游戏板)调用:
def clear_message(self):
self.message_1 = []
self.message_4 = []
self.message_F1 = []
玩家模块
玩家模块Player.py 的全部代码如下:
import pygame, os, GameConst
class Player():
def __init__(self):
self.__x = 0
self.__y = 0
self.__p = GameConst.GAMING
self.__slide_times = 0
self.__magic = 3
pygame.mixer.init()
self.sounds = {}
self.load_sounds()
def get_sounds(self):
return self.sounds
def _getmagic(self): return self.__magic
def _setmagic(self, value): self.__magic = value
magic = property(_getmagic, _setmagic)
def _getslide_times(self): return self.__slide_times
def _setslide_times(self, value): self.__slide_times = value
slide_times = property(_getslide_times, _setslide_times)
def _getp(self): return self.__p
def _setp(self, value): self.__p = value
game_process = property(_getp, _setp)
def _getmousex(self): return self.__x
def _setmousex(self, value): self.__x = value
mouseX = property(_getmousex, _setmousex)
def _getmousey(self): return self.__y
def _setmousey(self, value): self.__y = value
mouseY = property(_getmousey, _setmousey)
def load_sounds(self):
''' 搜索当前目录下的sound文件夹,然后载入背景音乐(.mp3)和音效(.wav, .ogg)。'''
sound_files = os.listdir('sound')
for x in sound_files:
t = x.split(".")
if t[1] == 'mp3':
pygame.mixer.music.load('sound/{}.{}'.format(t[0], t[1]))
pygame.mixer.music.set_volume(0.1875)
pygame.mixer.music.play(-1, 0.0)
else:
self.sounds[t[0]] = pygame.mixer.Sound('sound/{}.{}'.format(t[0], t[1]))
在这里默认当前目录下有一个名叫sound的文件夹,其中包括了音效文件(.mp3, .wav, .ogg)。用os.listdir() 方法列出了这个文件夹内的文件,如果是.mp3格式则作为背景音乐直接播放,其他的文件以文件名作为键值存在self.sounds 这个字典里,要用的时候直接引用:
player.get_sounds()['click'].play()
颜色模块
游戏模块RGB.py 的部分代码如下:
Black = (0, 0, 0)
White = (255, 255, 255)
DarkSlateGray1 = (151, 255, 255)
Gainsboro = (220, 220, 220)
GhostWhite = (248, 248, 255)
以上是本游戏用到的颜色,全部代码请见RGB.py 。
源代码已经上传到GitCode: 下唐人 / slide_puzzle · GitCode
|