教你如何用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中何种情况下需要使用断言
Apr 01 Python
python获取本地计算机名字的方法
Apr 29 Python
Python使用Mechanize模块编写爬虫的要点解析
Mar 31 Python
Python操作csv文件实例详解
Jul 31 Python
django manage.py扩展自定义命令方法
May 27 Python
pycharm创建一个python包方法图解
Apr 10 Python
Python字符串对象实现原理详解
Jul 01 Python
Python测试模块doctest使用解析
Aug 10 Python
使用Python进行中文繁简转换的实现代码
Oct 18 Python
Python List列表对象内置方法实例详解
Oct 22 Python
python em算法的实现
Oct 03 Python
python pygame 开发五子棋双人对弈
May 02 Python
深度学习详解之初试机器学习
正确的理解和使用Django信号(Signals)
Apr 14 #Python
编写python程序的90条建议
Apr 14 #Python
Python基础知识之变量的详解
理解深度学习之深度学习简介
Apr 14 #Python
python基于scrapy爬取京东笔记本电脑数据并进行简单处理和分析
深度学习小工程练习之垃圾分类详解
You might like
MySQL修改密码方法总结
2008/03/25 PHP
php eval函数用法 PHP中eval()函数小技巧
2012/10/31 PHP
PH P5.2至5.5、5.6的新增功能详解
2014/07/14 PHP
smarty自定义函数htmlcheckboxes用法实例
2015/01/22 PHP
简单解决新浪SAE无法上传文件的问题
2015/05/13 PHP
js onpropertychange输入框 事件获取属性
2009/03/26 Javascript
JavaScript控制图片加载完成后调用回调函数的方法
2015/03/20 Javascript
jQuery结合ajax实现动态加载文本内容
2015/05/19 Javascript
SpringMVC框架下JQuery传递并解析Json格式的数据是如何实现的
2015/12/10 Javascript
jQuery 中的 DOM 操作
2016/04/26 Javascript
easyui-combobox 实现简单的自动补全功能示例
2016/11/08 Javascript
浅谈jQuery before和insertBefore的区别
2016/12/04 Javascript
JS中检测数据类型的几种方式及优缺点小结
2016/12/12 Javascript
js+html5生成自动排列对话框实例
2017/10/09 Javascript
谈谈vue中mixin的一点理解
2017/12/12 Javascript
Vue+Flask实现简单的登录验证跳转的示例代码
2018/01/13 Javascript
JavaScript中import用法总结
2019/01/20 Javascript
vue-cli打包后本地运行dist文件中的index.html操作
2020/08/12 Javascript
Python学习资料
2007/02/08 Python
python多线程扫描端口示例
2014/01/16 Python
Python中的左斜杠、右斜杠(正斜杠和反斜杠)
2016/08/30 Python
Python实现的桶排序算法示例
2017/11/29 Python
Python如何计算语句执行时间
2019/11/22 Python
记一次Django响应超慢的解决过程
2020/09/17 Python
详解HTML5布局和HTML5标签
2020/10/26 HTML / CSS
La Redoute英国官网:法国时尚品牌
2017/04/27 全球购物
莫斯科绝对前卫最秘密的商店:SVMoscow
2017/10/23 全球购物
Urban Outfitters德国官网:美国跨国生活方式零售公司
2018/05/21 全球购物
酒店管理毕业生自我鉴定
2014/03/02 职场文书
人力资源管理专业自荐书
2014/07/07 职场文书
美术课外活动总结
2014/07/08 职场文书
食品安全承诺书范文
2014/08/29 职场文书
基层党组织整改方案
2014/10/25 职场文书
小学生教师节广播稿
2015/08/19 职场文书
创业计划书之物流运送
2019/09/17 职场文书
导游词之上海杜莎夫人蜡像馆
2019/11/22 职场文书