轻松一把,写个《扫雷》来玩玩(以wxPython实现)
Posted 悠然红茶
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了轻松一把,写个《扫雷》来玩玩(以wxPython实现)相关的知识,希望对你有一定的参考价值。
1. 概述
相信大家对《扫雷》游戏都不陌生,它规则简单,且颇具可玩性。从技术的角度来说,这个小游戏实现起来并不太难,所以是个很好的练手题目。今天我们就尝试用wxPython来实现一个简单的《扫雷》游戏。(附件里有全部资源和源码,可供大家参考)
下图是我截取的一张游戏效果图,虽然简陋,但已能正常运行。
接下来,我们开始详细讲解。
2. 《扫雷》规则
《扫雷》的游戏规则和操作说明:
- 《扫雷》的基本操作区是个简单的二维地图,长宽随用户选择的游戏难度不同而不同。
- 地图里可操作的基本单元是小格。
- 初始情况下,地图里每个小格都是未打开的。
- 玩家可通过鼠标左键点击打开小格。如果小格里具有地雷,则游戏失败,否则会显示该小格周围8个小格里共埋有多少地雷。如果周围没有地雷,则不显示数字(也就是说不会显示0)。
- 未打开的小格可以通过鼠标右键点击来做标记。
- 点击一次右键,标记为红旗,表示玩家认为此处有雷。如果小格标记有红旗,那么该小格不允许被用户手动或自动打开。
- 再点击一次右键,标记为问号,表示玩家不确定此处是否有雷。
- 继续点击一次右键,清除问号标记。
- 对于已打开的小格,可以通过鼠标左键双击,或鼠标左右键同时点击,来快捷打开其周围未打开的小格。请注意,如果当前小格显示的数字大于0,但周围的红旗标记格数目小于当前小格显示的数字,则不会快捷打开周围小格。
- 如果打开了一个周围雷数为0的小格,则游戏会自动打开其周围8个小格。而如果新打开的小格里仍然含有周围雷数为0的小格,则会进一步继续打开相应小格。如此循环下去。
- 每局游戏从点开一个方块开始计时,并每秒更新已经经过的秒数,直到成功找出所有地雷或中途失败为止。
3. 设计思路
各位朋友不妨先自己思考一下,该如何设计这个游戏。一个明显的单位是就是一个小格,而一局游戏无非是将若干小格组织成一张二维表而已。
一个小格应该有如下几个信息:
1)打开状态,表示其是否已经被点开了;
2)标记状态,表示用鼠标右键点击后,做了什么标记,比如红旗标记、问号标记;
3)地雷信息,表示这个小格里是否具有地雷;
4)周围雷数,表示这个小格紧相邻小格里共含有多少地雷。注意,即便该小格里有一颗地雷,它也是会记录周围的雷数的,只不过在游戏界面上,不会显示这个数字而已。
我们可以这样定义小格:
class MineBlock:
def __init__(self, open_state, block_flag):
self.open_state = open_state
self.block_flag = block_flag
self.around_num = 0
self.has_mine = False
然后,我们可以进一步定义一个主面板类:MinePanel,玩游戏时的主要操作都是在这个面板里完成的。
在wxPython里,wx.Panel类可以理解为基本的控件容器面板,我们的MinePanel就继承于它。
class MinePanel(wx.Panel):
def __init__(self, parent, sz, st):
super().__init__(parent, size=sz, style=st)
self.Bind(wx.EVT_PAINT, self._on_paint)
self.Bind(wx.EVT_SIZE, self._on_size)
self.Bind(wx.EVT_LEFT_DOWN, self._on_mouse_left_down)
self.Bind(wx.EVT_LEFT_UP, self._on_mouse_left_up)
self.Bind(wx.EVT_LEFT_DCLICK, self._on_mouse_left_dbclick)
self.Bind(wx.EVT_RIGHT_DOWN, self._on_mouse_right_down)
self.Bind(wx.EVT_RIGHT_UP, self._on_mouse_right_up)
self.timer = wx.Timer(self)
self.Bind(wx.EVT_TIMER, self._on_timer, self.timer)
self.SetBackgroundStyle(wx.BG_STYLE_PAINT)
self.num_colors = [None, wx.Colour(0, 0, 230), wx.Colour(0, 180, 0),
wx.Colour(210, 0, 0), wx.Colour(220, 50, 180), \\
wx.Colour(30, 200, 200), wx.Colour(240, 240, 30),
wx.Colour(160, 80, 180), wx.Colour(0, 0, 0)]
self.control_panel = ControlPanel(self)
self.block_height = 0
self.red_flag_count = 0
self.num_txt_font = None
self.left_down = False
self.right_down = False
self.red_flag_icon = None
red_flag_icon_file = 'red_flag.png'
if (os.path.exists(red_flag_icon_file)):
self.red_flag_icon = wx.Bitmap(red_flag_icon_file)
在上面的代码里,除了注册了我们感兴趣的事件处理函数外,我们还加入了一个ControlPanel对象,它是做什么的?简单地说,就是负责处理游戏界面里展现剩余雷数、消耗时间、重开按钮的子面板。这个面板只是个逻辑上的概念,所以并不继承于wx.Panel。
我们画一张简单的关系示意图:
需要说明的是,虽然在游戏界面里,所有小格会最终显现成一张二维表格,但实际上我是把这些小格存储到一个一维列表里的。在运行时需注意计算好行列坐标,不要弄错了。
在编写游戏时,一个基本的理念是:以“操作”来修改状态,以“绘制”来反映状态。现在我们可以设想玩一把游戏的大体流程,流程示意图如下:
理清思路后,实现起来就比较简单了。我们先看点击鼠标左键时的动作:
def _on_mouse_left_up(self, event):
self.left_down = False
if (self.control_panel.handle_mouse_left_up(event)):
return
if (self.die or self.success):
return
(v_x, v_y) = event.GetPosition()
if (self.right_down):
self._quick_open(v_x, v_y)
return
(need_refresh, update_rect) = self._try_to_open_a_block(v_x, v_y)
if (need_refresh):
if (update_rect != None):
self.RefreshRect(update_rect)
else:
self.Refresh()
if (not self.die and self.start_time == 0):
self.start_time = time.time()
self.last_time = self.start_time
self.timer.Start(milliseconds=1000)
self._check_success()
- 首先,如果点击的是游戏控制板区域,则由控制板处理:self.control_panel.handle_mouse_left_up()。
- 如果在抬起左键时,右键处于按下状态,则按同时点击左右键处理,其实会尝试执行前文说的快速打开周边8个小格的动作。
- 如果不是以上情况,则按普通的打开一个小格的动作处理,这个也是我们主要关心的动作:_try_to_open_a_block()。
为了尽量减小重绘的范围,_try_to_open_a_block()动作返回的内容里有一个update_rect。我们前文说过,如果新打开的小格里仍然含有周围雷数为0的小格,则会进一步继续打开相应小格,如此循环下去。所以我们一开始是不知道要重绘多大区域的,只有在递归动作完成后,从_try_to_open_a_block()函数返回时,我们才能得到并最终重绘这个区域。
_try_to_open_a_block()的代码如下:
def _try_to_open_a_block(self, x, y):
if (self.rows_num <= 0):
return (False, None)
(x_index, y_index) = self._calc_x_y_index(x, y)
(blk, update_rect) = self._set_block_opened(x_index, y_index)
if (self.die):
return (True, None)
if (blk != None):
return (True, update_rect)
return (False, None)
其中会先计算出要打开哪个坐标的小格,然后用_set_block_opened()设置该小格为“已打开”状态。
def _set_block_opened(self, x_index, y_index):
if (self.die):
return (None, None)
update_rect = None
if (y_index >= 0 and y_index < self.rows_num and x_index >= 0 and x_index < self.cols_num):
block_index = y_index * self.cols_num + x_index
block = self.blocks_map[block_index]
if (block.open_state == OPEN_STATE_NOT_OPEN and block.block_flag != BLOCK_FLAG_REDFLAG):
block.open_state = OPEN_STATE_OPENED
update_rect = self._get_one_block_rect(x_index, y_index)
if (block.has_mine):
print("!!!!! DIE !!!!!!" + " x_index=" + str(x_index) + ", y_index=" + str(y_index))
self.die = True
self.timer.Stop()
else:
if (block.around_num == 0):
rect = self._recursive_open_all_neighbour_zero_around(x_index, y_index)
if (update_rect != None and rect != None):
update_rect.Union(rect)
return (block, update_rect)
return (None, update_rect)
在设置“已打开”状态后,如果发现踩到雷,就结束此局。如果周边没有雷,就开始递归打开无雷的若干小格:_recursive_open_all_neighbour_zero_around()。
def _recursive_open_all_neighbour_zero_around(self, x_index, y_index):
new_open_blocks = []
neighbours = [(x_index - 1, y_index - 1), (x_index, y_index - 1), (x_index + 1, y_index - 1), \\
(x_index - 1, y_index), (x_index + 1, y_index), \\
(x_index - 1, y_index + 1), (x_index, y_index + 1), (x_index + 1, y_index + 1)]
if (self.die):
return None
blocks_rect = None
for nb_pnt in neighbours:
(blk, update_rect) = self._set_block_opened(nb_pnt[0], nb_pnt[1]) # 递归操作
if (self.die):
return None
if (blk != None):
new_open_blocks.append((nb_pnt, blk))
if (blocks_rect != None):
if (update_rect != None):
blocks_rect.Union(update_rect)
else:
blocks_rect = update_rect
else:
pass
return blocks_rect
以上是处理点击左键的动作,接下来在看处理双击的动作:
def _on_mouse_left_dbclick(self, event):
if (self.die or self.success):
return
(v_x, v_y) = event.GetPosition()
(need_refresh, update_rect) = self._try_to_open_neighbours_without_redflag(v_x, v_y)
if (need_refresh):
if (update_rect != None):
self.RefreshRect(update_rect)
else:
self.Refresh()
self._check_success()
主要就是要打开周边没有红旗标记的小格:_try_to_open_neighbours_without_redflag()。
def _try_to_open_neighbours_without_redflag(self, x, y):
total_around = 0
seen_mine_around = 0
count = 0
(x_index, y_index) = self._calc_x_y_index(x, y)
block = self._get_block(x_index, y_index)
if (block == None):
return (False, None)
total_around = block.around_num
neighbours = [(x_index - 1, y_index - 1), (x_index, y_index - 1), (x_index + 1, y_index - 1), \\
(x_index - 1, y_index), (x_index + 1, y_index), \\
(x_index - 1, y_index + 1), (x_index, y_index + 1), (x_index + 1, y_index + 1)]
for nb_pnt in neighbours:
blk = self._get_block(nb_pnt[0], nb_pnt[1])
if (blk != None):
if ((blk.open_state == OPEN_STATE_OPENED and blk.has_mine) or (blk.open_state == OPEN_STATE_NOT_OPEN and blk.block_flag == BLOCK_FLAG_REDFLAG)):
seen_mine_around += 1
if (seen_mine_around < total_around):
return (False, None)
nbs_rect = None
for nb_pnt in neighbours:
(blk, update_rect) = self._set_block_opened(nb_pnt[0], nb_pnt[1])
if (self.die):
return (True, None)
if (blk != None):
count += 1
if (nbs_rect != None):
if (update_rect != None):
nbs_rect.Union(update_rect)
else:
nbs_rect = update_rect
if (count > 0):
return (True, nbs_rect)
return (False, None)
抛开一些零碎逻辑,其中最重要的就是调用_set_block_opened(),正如前文所说,里面也包含着递归打开小格的动作,这里就不赘述了。
4. 小结
扫雷的主要代码就先说这么多,并不十分复杂。作为一个小demo来说,当然没有商业软件那么完备,只供大家轻松一下而已。这个小demo的代码位于https://gitee.com/youranhongcha/python-games.git 仓库的mine_sweeper目录,有兴趣的同学可以clone一份代码看看。
以上是关于轻松一把,写个《扫雷》来玩玩(以wxPython实现)的主要内容,如果未能解决你的问题,请参考以下文章