轻松一把,写个《扫雷》来玩玩(以wxPython实现)

悠然红茶 2020-11-17 21:28:06
Python Git gitee wxpython around


轻松一把,写个《扫雷》来玩玩

(以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一份代码看看。
 

 

版权声明
本文为[悠然红茶]所创,转载请带上原文链接,感谢
https://my.oschina.net/youranhongcha/blog/4721784

  1. 利用Python爬虫获取招聘网站职位信息
  2. Using Python crawler to obtain job information of recruitment website
  3. Several highly rated Python libraries arrow, jsonpath, psutil and tenacity are recommended
  4. Python装饰器
  5. Python实现LDAP认证
  6. Python decorator
  7. Implementing LDAP authentication with Python
  8. Vscode configures Python development environment!
  9. In Python, how dare you say you can't log module? ️
  10. 我收藏的有关Python的电子书和资料
  11. python 中 lambda的一些tips
  12. python中字典的一些tips
  13. python 用生成器生成斐波那契数列
  14. python脚本转pyc踩了个坑。。。
  15. My collection of e-books and materials about Python
  16. Some tips of lambda in Python
  17. Some tips of dictionary in Python
  18. Using Python generator to generate Fibonacci sequence
  19. The conversion of Python script to PyC stepped on a pit...
  20. Python游戏开发,pygame模块,Python实现扫雷小游戏
  21. Python game development, pyGame module, python implementation of minesweeping games
  22. Python实用工具,email模块,Python实现邮件远程控制自己电脑
  23. Python utility, email module, python realizes mail remote control of its own computer
  24. 毫无头绪的自学Python,你可能连门槛都摸不到!【最佳学习路线】
  25. Python读取二进制文件代码方法解析
  26. Python字典的实现原理
  27. Without a clue, you may not even touch the threshold【 Best learning route]
  28. Parsing method of Python reading binary file code
  29. Implementation principle of Python dictionary
  30. You must know the function of pandas to parse JSON data - JSON_ normalize()
  31. Python实用案例,私人定制,Python自动化生成爱豆专属2021日历
  32. Python practical case, private customization, python automatic generation of Adu exclusive 2021 calendar
  33. 《Python实例》震惊了,用Python这么简单实现了聊天系统的脏话,广告检测
  34. "Python instance" was shocked and realized the dirty words and advertisement detection of the chat system in Python
  35. Convolutional neural network processing sequence for Python deep learning
  36. Python data structure and algorithm (1) -- enum type enum
  37. 超全大厂算法岗百问百答(推荐系统/机器学习/深度学习/C++/Spark/python)
  38. 【Python进阶】你真的明白NumPy中的ndarray吗?
  39. All questions and answers for algorithm posts of super large factories (recommended system / machine learning / deep learning / C + + / spark / Python)
  40. [advanced Python] do you really understand ndarray in numpy?
  41. 【Python进阶】Python进阶专栏栏主自述:不忘初心,砥砺前行
  42. [advanced Python] Python advanced column main readme: never forget the original intention and forge ahead
  43. python垃圾回收和缓存管理
  44. java调用Python程序
  45. java调用Python程序
  46. Python常用函数有哪些?Python基础入门课程
  47. Python garbage collection and cache management
  48. Java calling Python program
  49. Java calling Python program
  50. What functions are commonly used in Python? Introduction to Python Basics
  51. Python basic knowledge
  52. Anaconda5.2 安装 Python 库(MySQLdb)的方法
  53. Python实现对脑电数据情绪分析
  54. Anaconda 5.2 method of installing Python Library (mysqldb)
  55. Python implements emotion analysis of EEG data
  56. Master some advanced usage of Python in 30 seconds, which makes others envy it
  57. python爬取百度图片并对图片做一系列处理
  58. Python crawls Baidu pictures and does a series of processing on them
  59. python链接mysql数据库
  60. Python link MySQL database