教你如何用python开发一款数字推盘小游戏


Posted in Python onApril 14, 2021

今年年初,新一季的《最强大脑》开播了,第一集选拔的时候大家做了一个数字游戏,名叫《数字华容道》,当时何猷君以二十几秒的成绩夺得该项目的冠军,看了这个游戏之后我决定要写一个《数字华容道》的程序,过去了半年,我终于记起了这件事,今天就来实现。

数字推盘游戏(n-puzzle)是一种智力游戏,常见的类型有十五数字推盘游戏和八数字推盘游戏等。十五数字推盘游戏的板上会有十五个方块和一个大小相当于一个方块的空位(供方块移动之用),当15个数字依次排序并且最后一个格子为空位即代表挑战成功。

本文使用 PyQt5 进行设计与实现,PyQt5 是该程序的一个呈现方式,最重要的是算法,学会了算法,完全可以使用 PyGame 或者 Tkinter 实现。

PyQt5安装:pip install PyQt5

本文使用环境:

系统:Windows 10 64位Python版本:3.6

1、布局设计

做一个简版的数字华容道,布局设计如图所示:

教你如何用python开发一款数字推盘小游戏

图中灰色的部分使用 QWidget 作为整个游戏的载体;黄色部分使用 QGridLayout 作为数字方块的布局;红色部分使用 QLabel 作为数字方块。

2、算法设计

如上图所示,本游戏共需要15个方块,每个方块代表一个数字。我们可以使用 一个二维 list 来存储方块上的数字。其实我们要创建一个 4x4 的 list 存储 0~15 各个数字,0 代表空的位置。

2.1 创建并初始化数组

  • 创建数组的方法:
  • 创建一个长度为16的数组,并且在对应位置上保存着 0~15 ;打乱顺序
import random

# 用来存放位置信息的二维数组
blocks = []

# 产生随机数组,0 代表空的位置
arr = range(16)
numbers = random.sample(arr, 16)

for row in range(4):
	blocks.append([])
	for column in range(4):
		blocks[row].append(numbers[row*4 + column])

# 打印结果
for i in range(4):
	print(blocks[i])

[out]
[2, 5, 7, 9]
[11, 8, 4, 12]
[6, 13, 10, 15]
[1, 14, 0, 3]
[Finished in 0.1s]

2.2 移动算法

假如移动之前个数字位置如左图所示,那么当按下左箭头时,会变成如右图所示:

教你如何用python开发一款数字推盘小游戏

可以看到 (1, 2) 和 (1, 3) 两个位置上的数字互换了,即 0 和 8 互换;如果右图所示再次按下左箭头,那么所有数字都不会改变,因为 数字 0 右边没有数了。

总结一下:如果 数字 0 所在位置为 (row, column),并且 column≠3 那么按下左箭头之后,(row, column) 和 (row, column+1) 位置上的数组互换,同理可得:

  • 如果 数字 0 所在位置为 (row, column),并且 column≠0 那么按下右箭头之后,(row, column) 和 (row, column-1) 位置上的数组互换;
  • 如果 数字 0 所在位置为 (row, column),并且 row≠3 那么按下上箭头之后,(row, column) 和 (row+1, column) 位置上的数组互换;
  • 如果 数字 0 所在位置为 (row, column),并且 row≠0 那么按下下箭头之后,(row, column) 和 (row-1, column) 位置上的数组互换;

将移动算法封装成一个函数如下:

# 移动
# zero_row 代表数字0 所在二维数组的行下标,zero_column代表数字0 所在二维数组的列下标
def move(direction):
    if(direction == 'UP'): # 上
        if zero_row != 3:
            blocks[zero_row][zero_column] = blocks[zero_row + 1][zero_column]
            blocks[zero_row + 1][zero_column] = 0
            zero_row += 1
    if(direction == 'DOWN'): # 下
        if zero_row != 0:
            blocks[zero_row][zero_column] = blocks[zero_row - 1][zero_column]
            blocks[zero_row - 1][zero_column] = 0
            zero_row -= 1
    if(direction == 'LEFT'): # 左
        if zero_column != 3:
            blocks[zero_row][zero_column] = blocks[zero_row][zero_column + 1]
            blocks[zero_row][zero_column + 1] = 0
            zero_column += 1
    if(direction == 'RIGHT'): # 右
        if zero_column != 0:
            blocks[zero_row][zero_column] = blocks[zero_row][zero_column - 1]
            blocks[zero_row][zero_column - 1] = 0
            zero_column -= 1

2.3 是否胜利检测算法

检测是否胜利其实很简单:前15个位置分别对应,最后一个为0即为胜利 ,不过为了避免不必要的计算,我们先检测最后一个是否为 0 ,如果不为0 前面的就不用比较了。具体代码实现如下:

# 检测是否完成
def checkResult():
        # 先检测最右下角是否为0
        if blocks[3][3] != 0:
            return False

        for row in range(4):
            for column in range(4):
            	# 运行到此处说名最右下角已经为0,pass即可
                if row == 3 and column == 3:
                    pass
                # 值是否对应
                elif blocks[row][column] != row * 4 + column + 1:
                    return False

        return True

3、实现

下面讲解所有功能模块的实现。

3.1 框架搭建

创建 QWidget 作为整个游戏的载体:

import sys
from PyQt5.QtWidgets import QWidget, QApplication

class NumberHuaRong(QWidget):
    """ 华容道主体 """
    def __init__(self):
        super().__init__()
        self.initUI()

    def initUI(self):
        # 设置宽和高
        self.setFixedSize(400, 400)
        # 设置标题
        self.setWindowTitle('数字华容道')
        # 设置背景颜色
        self.setStyleSheet("background-color:gray;")
        self.show()

if __name__ == '__main__':
    app = QApplication(sys.argv)
    ex = NumberHuaRong()
    sys.exit(app.exec_())

运行结果如下图所示:

教你如何用python开发一款数字推盘小游戏

3.2 数字方块实现

前面已经提到,用一个二维数组来存放 0~16 个数字,最终我们要转换成一个数字方块,单独创建一个类:

class Block(QLabel):
    """ 数字方块 """
    def __init__(self, number):
        super().__init__()

        self.number = number
        self.setFixedSize(80, 80)

        # 设置字体
        font = QFont()
        font.setPointSize(30)
        font.setBold(True)
        self.setFont(font)

        # 设置字体颜色
        pa = QPalette()
        pa.setColor(QPalette.WindowText, Qt.white)
        self.setPalette(pa)

        # 设置文字位置
        self.setAlignment(Qt.AlignCenter)

        # 设置背景颜色\圆角和文本内容
        if self.number == 0:
            self.setStyleSheet("background-color:white;border-radius:10px;")
        else:
            self.setStyleSheet("background-color:red;border-radius:10px;")
            self.setText(str(self.number))

该类继承自 QLablel ,初始化需要传入一个参数 number ,number就是数字方块上显示的数字。

3.3 将数字转换成方块添加到布局

布局采用 QGridLayout 创建一个 4X4 的 self.gltMain,将16个 Block 添加到 self.gltMain:

def updatePanel(self):
    for row in range(4):
        for column in range(4):
            self.gltMain.addWidget(Block(self.blocks[row][column]), row, column)

    self.setLayout(self.gltMain)

3.4 初始化布局

初始化布局包括随机数据的产生与将数字转换成方块添加到布局:

# 初始化布局
    def onInit(self):
        # 产生随机数组,0 代表空的位置
        arr = range(16)
        self.numbers = random.sample(arr, 16)

        # 将数字方块添加到布局
        for row in range(4):
            self.blocks.append([])
            for column in range(4):
                temp = self.numbers[row * 4 + column]

                if temp == 0:
                    self.zero_row = row
                    self.zero_column = column
                self.blocks[row].append(temp)
                self.gltMain.addWidget(Block(temp), row, column)

3.5 按键检测

QWidget 有一个 keyPressEvent 事件句柄,我们只需要重新实现该方法即可:

# 检测按键
def keyPressEvent(self, event):
    key = event.key()
    if(key == Qt.Key_Up or key == Qt.Key_W):
        self.move(Direction.UP)
    if(key == Qt.Key_Down or key == Qt.Key_S):
        self.move(Direction.DOWN)
    if(key == Qt.Key_Left or key == Qt.Key_A):
        self.move(Direction.LEFT)
    if(key == Qt.Key_Right or key == Qt.Key_D):
        self.move(Direction.RIGHT)
    self.updatePanel()
    if self.checkResult():
        if QMessageBox.Ok == QMessageBox.information(self, '挑战结果', '恭喜您完成挑战!'):
            self.onInit()

按键检测到按键按下之后判断该键值是否为 “↑↓←→”或“WSAD”,并作出相应的移动(move),移动之后刷新布局(updatePannel),最后检测是否完成挑战(checkResult),如果完成挑战,弹出提示框。如果点击了 OK 按钮,游戏重新开始(onInit)。

3.6 试玩测试

至此,所有功能模块介绍完毕,不要着急看完整代码,我们先运行一下程序看是否还有 Bugs。

玩了几局之后发现,并不是所有的局都能都还原,如下面这种情况:

教你如何用python开发一款数字推盘小游戏

如图所示,14 和 15 方块位置反了,无论如何也还原不聊了,这种情况是随机出现的。到底是怎么回事呢?经过一番上网搜索,确实如果只有两个数字的位置反了,无论如何也还原不了的。那这是由什么造成的呢?还记得我们的二维数组是怎么产生的吧,随机的,也就是说可能会随机到无法还原的情况。

如何避免这种情况呢?初始化数组时,所有的位置都是正确的数字,然后使用 move 进行移动打乱。

3.7 改进完善

由于前面已经将各个功能模块单独写成了方法,因此我们只需修改 onInit 方法即可。

# 初始化布局
def onInit(self):
    # 产生顺序数组
    self.numbers = list(range(1, 16))
    self.numbers.append(0)

    # 将数字添加到二维数组
    for row in range(4):
        self.blocks.append([])
        for column in range(4):
            temp = self.numbers[row * 4 + column]

            if temp == 0:
                self.zero_row = row
                self.zero_column = column
            self.blocks[row].append(temp)

    # 打乱数组
    for i in range(500):
        random_num = random.randint(0, 3)
        self.move(Direction(random_num))

    self.updatePanel()

先生成一个顺序数组,里面保存着[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 0],然后转为二维数组 blocks,再后随即移动500次,最后添加到布局。

4、完整代码

import sys
import random
from enum import IntEnum
from PyQt5.QtWidgets import QLabel, QWidget, QApplication, QGridLayout, QMessageBox
from PyQt5.QtGui import QFont, QPalette
from PyQt5.QtCore import Qt

# 用枚举类表示方向
class Direction(IntEnum):
    UP = 0
    DOWN = 1
    LEFT = 2
    RIGHT = 3


class NumberHuaRong(QWidget):
    """ 华容道主体 """
    def __init__(self):
        super().__init__()
        self.blocks = []
        self.zero_row = 0
        self.zero_column = 0
        self.gltMain = QGridLayout()

        self.initUI()

    def initUI(self):      
        # 设置方块间隔
        self.gltMain.setSpacing(10)

        self.onInit()

        # 设置布局
        self.setLayout(self.gltMain)
        # 设置宽和高
        self.setFixedSize(400, 400)
        # 设置标题
        self.setWindowTitle('数字华容道')
        # 设置背景颜色
        self.setStyleSheet("background-color:gray;")
        self.show()

    # 初始化布局
    def onInit(self):
        # 产生顺序数组
        self.numbers = list(range(1, 16))
        self.numbers.append(0)

        # 将数字添加到二维数组
        for row in range(4):
            self.blocks.append([])
            for column in range(4):
                temp = self.numbers[row * 4 + column]

                if temp == 0:
                    self.zero_row = row
                    self.zero_column = column
                self.blocks[row].append(temp)

        # 打乱数组
        for i in range(500):
            random_num = random.randint(0, 3)
            self.move(Direction(random_num))

        self.updatePanel()

    # 检测按键
    def keyPressEvent(self, event):
        key = event.key()
        if(key == Qt.Key_Up or key == Qt.Key_W):
            self.move(Direction.UP)
        if(key == Qt.Key_Down or key == Qt.Key_S):
            self.move(Direction.DOWN)
        if(key == Qt.Key_Left or key == Qt.Key_A):
            self.move(Direction.LEFT)
        if(key == Qt.Key_Right or key == Qt.Key_D):
            self.move(Direction.RIGHT)
        self.updatePanel()
        if self.checkResult():
            if QMessageBox.Ok == QMessageBox.information(self, '挑战结果', '恭喜您完成挑战!'):
                self.onInit()

    # 方块移动算法
    def move(self, direction):
        if(direction == Direction.UP): # 上
            if self.zero_row != 3:
                self.blocks[self.zero_row][self.zero_column] = self.blocks[self.zero_row + 1][self.zero_column]
                self.blocks[self.zero_row + 1][self.zero_column] = 0
                self.zero_row += 1
        if(direction == Direction.DOWN): # 下
            if self.zero_row != 0:
                self.blocks[self.zero_row][self.zero_column] = self.blocks[self.zero_row - 1][self.zero_column]
                self.blocks[self.zero_row - 1][self.zero_column] = 0
                self.zero_row -= 1
        if(direction == Direction.LEFT): # 左
            if self.zero_column != 3:
                self.blocks[self.zero_row][self.zero_column] = self.blocks[self.zero_row][self.zero_column + 1]
                self.blocks[self.zero_row][self.zero_column + 1] = 0
                self.zero_column += 1
        if(direction == Direction.RIGHT): # 右
            if self.zero_column != 0:
                self.blocks[self.zero_row][self.zero_column] = self.blocks[self.zero_row][self.zero_column - 1]
                self.blocks[self.zero_row][self.zero_column - 1] = 0
                self.zero_column -= 1

    def updatePanel(self):
        for row in range(4):
            for column in range(4):
                self.gltMain.addWidget(Block(self.blocks[row][column]), row, column)

        self.setLayout(self.gltMain)

    # 检测是否完成
    def checkResult(self):
        # 先检测最右下角是否为0
        if self.blocks[3][3] != 0:
            return False

        for row in range(4):
            for column in range(4):
                # 运行到此处说名最右下角已经为0,pass即可
                if row == 3 and column == 3:
                    pass
                #值是否对应
                elif self.blocks[row][column] != row * 4 + column + 1:
                    return False

        return True

class Block(QLabel):
    """ 数字方块 """

    def __init__(self, number):
        super().__init__()

        self.number = number
        self.setFixedSize(80, 80)

        # 设置字体
        font = QFont()
        font.setPointSize(30)
        font.setBold(True)
        self.setFont(font)

        # 设置字体颜色
        pa = QPalette()
        pa.setColor(QPalette.WindowText, Qt.white)
        self.setPalette(pa)

        # 设置文字位置
        self.setAlignment(Qt.AlignCenter)

        # 设置背景颜色\圆角和文本内容
        if self.number == 0:
            self.setStyleSheet("background-color:white;border-radius:10px;")
        else:
            self.setStyleSheet("background-color:red;border-radius:10px;")
            self.setText(str(self.number))


if __name__ == '__main__':
    app = QApplication(sys.argv)
    ex = NumberHuaRong()
    sys.exit(app.exec_())

5、总结

在做的过程中遇到最大的坑就是随机数组导致无法还原。另外在做这个游戏的时候我已经找到还原规律了,这样在测试的时候可以做完完整测试,否则根本无法测试都挑战成功那一步。

另外要对《最强大脑》做一下吐槽:这个项目根本就是有偏袒的,玩过的人会很快,没有玩过的找规律的时间就很长。我在手机上玩4X4的最快还原用了 33 秒,对于该节目的冠军(即便是玩过)很是敬仰。

到此这篇关于教你如何用python开发一款数字推盘小游戏的文章就介绍到这了,更多相关python开发数字推盘小游戏内容请搜索三水点靠木以前的文章或继续浏览下面的相关文章希望大家以后多多支持三水点靠木!

Python 相关文章推荐
Python的Django框架可适配的各种数据库介绍
Jul 15 Python
对python中的高效迭代器函数详解
Oct 18 Python
python使用Turtle库绘制动态钟表
Nov 19 Python
Python文件读写常见用法总结
Feb 22 Python
Django model update的多种用法介绍
Mar 28 Python
Python 图像对比度增强的几种方法(小结)
Sep 25 Python
python根据时间获取周数代码实例
Sep 30 Python
Django框架模板用法入门教程
Nov 04 Python
Python实现CNN的多通道输入实例
Jan 17 Python
关于Keras模型可视化教程及关键问题的解决
Jan 24 Python
使用遗传算法求二元函数的最小值
Feb 11 Python
Python调用C/C++的方法解析
Aug 05 Python
深度学习详解之初试机器学习
正确的理解和使用Django信号(Signals)
Apr 14 #Python
编写python程序的90条建议
Apr 14 #Python
Python基础知识之变量的详解
理解深度学习之深度学习简介
Apr 14 #Python
python基于scrapy爬取京东笔记本电脑数据并进行简单处理和分析
深度学习小工程练习之垃圾分类详解
You might like
php 各种应用乱码问题的解决方法
2010/05/09 PHP
SWFUpload与CI不能正确上传识别文件MIME类型解决方法分享
2011/04/18 PHP
PHP详解ASCII码对照表与字符转换
2011/12/05 PHP
深入解析PHP中的(伪)多线程与多进程
2013/07/01 PHP
一漂亮的PHP图片验证码实例
2014/03/21 PHP
laravel 实现上传图片到本地和前台访问示例
2019/10/21 PHP
seajs中模块的解析规则详解和模块使用总结
2014/03/12 Javascript
javascript圆盘抽奖程序实现原理和完整代码例子
2014/06/03 Javascript
原生javascript实现简单的datagrid数据表格
2015/01/02 Javascript
浅析jQuery事件之on()方法绑定多个选择器,多个事件
2016/04/27 Javascript
jQuery实现邮箱下拉列表自动补全功能
2016/09/08 Javascript
AngularJS Phonecat实例讲解
2016/11/21 Javascript
jQuery实现遮罩层登录对话框
2016/12/29 Javascript
JavaScript中发出HTTP请求最常用的方法
2018/07/12 Javascript
vue2.0获取鼠标位置的方法
2018/09/13 Javascript
详解JavaScript的数据类型以及数据类型的转换
2019/04/20 Javascript
微信小程序Page中data数据操作和函数调用方法
2019/05/08 Javascript
Electron-vue开发的客户端支付收款工具的实现
2019/05/24 Javascript
微信小程序自定义顶部组件customHeader的示例代码
2020/06/03 Javascript
Selenium(Python web测试工具)基本用法详解
2018/08/10 Python
在win10和linux上分别安装Python虚拟环境的方法步骤
2019/05/09 Python
Python 获取异常(Exception)信息的几种方法
2020/12/29 Python
世界上最大的专业美容用品零售商:Sally Beauty
2017/07/02 全球购物
PyQt 如何创建自定义QWidget
2021/03/24 Python
信息部岗位职责
2013/11/12 职场文书
情人节活动策划方案
2014/02/27 职场文书
热爱祖国演讲稿
2014/05/04 职场文书
个人整改措施落实情况汇报
2014/10/29 职场文书
向雷锋同志学习倡议书
2015/04/27 职场文书
2015年乡镇残联工作总结
2015/05/13 职场文书
运动会致辞稿
2015/07/29 职场文书
老人院义工活动感想
2015/08/07 职场文书
图文详解matlab原始处理图像几何变换
2021/07/09 Python
SQL 聚合、分组和排序
2021/11/11 MySQL
分析Python list操作为什么会错误
2021/11/17 Python
Java代码规范与质量检测插件SonarLint的使用
2022/08/05 Java/Android