教你如何用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中的魔法方法深入理解
Jul 09 Python
Python中防止sql注入的方法详解
Feb 25 Python
python通过opencv实现批量剪切图片
Nov 13 Python
python机器学习之神经网络实现
Oct 13 Python
python读取word文档,插入mysql数据库的示例代码
Nov 07 Python
对python实现模板生成脚本的方法详解
Jan 30 Python
在python里协程使用同步锁Lock的实例
Feb 19 Python
python+os根据文件名自动生成文本
Mar 21 Python
树莓派使用USB摄像头和motion实现监控
Jun 22 Python
python读写配置文件操作示例
Jul 03 Python
OpenCV+face++实现实时人脸识别解锁功能
Aug 28 Python
PO模式在selenium自动化测试框架的优势
Mar 20 Python
深度学习详解之初试机器学习
正确的理解和使用Django信号(Signals)
Apr 14 #Python
编写python程序的90条建议
Apr 14 #Python
Python基础知识之变量的详解
理解深度学习之深度学习简介
Apr 14 #Python
python基于scrapy爬取京东笔记本电脑数据并进行简单处理和分析
深度学习小工程练习之垃圾分类详解
You might like
PHP获取网址的顶级域名函数代码
2012/09/24 PHP
YII2.0框架行为(Behavior)深入详解
2019/07/26 PHP
浅析PHP中的 inet_pton 网络函数
2019/12/16 PHP
javascript setTimeout()传递函数参数(包括传递对象参数)
2010/04/07 Javascript
js面向对象设计用{}好还是function(){}好(构造函数)
2011/10/23 Javascript
基于JQuery实现的类似购物商城的购物车
2011/12/06 Javascript
解决Extjs 4 Panel作为Window组件的子组件时出现双重边框问题
2013/01/11 Javascript
jQuery之简单的表单验证实例
2016/07/07 Javascript
AngularJS入门教程之双向绑定详解
2016/08/18 Javascript
AngularJS实现Input格式化的方法
2016/11/07 Javascript
Chrome浏览器的alert弹窗禁止再次弹出后恢复的方法
2016/12/30 Javascript
不得不看之JavaScript构造函数及new运算符
2017/08/21 Javascript
解决layui中table异步数据请求不支持自定义返回数据格式的问题
2018/08/19 Javascript
bootstrapTable+ajax加载数据 refresh更新数据
2018/08/31 Javascript
vue+axios实现文件下载及vue中使用axios的实例
2018/09/21 Javascript
jQuery+ajax实现批量删除功能完整示例
2019/06/06 jQuery
JS数组方法reduce的用法实例分析
2020/03/03 Javascript
ant-design-vue 时间选择器赋值默认时间的操作
2020/10/27 Javascript
python批量提交沙箱问题实例
2014/10/08 Python
python中的__slots__使用示例
2015/02/26 Python
Python cookbook(数据结构与算法)从序列中移除重复项且保持元素间顺序不变的方法
2018/03/13 Python
python如何拆分含有多种分隔符的字符串
2018/03/20 Python
对Python 2.7 pandas 中的read_excel详解
2018/05/04 Python
python和shell获取文本内容的方法
2018/06/05 Python
关于HTML5 Placeholder新标签低版本浏览器下不兼容的问题分析及解决办法
2016/01/27 HTML / CSS
挪威太阳镜和眼镜网上商城:SmartBuyGlasses挪威
2016/08/20 全球购物
世界最大的私人旅行指南出版商:孤独星球
2016/08/23 全球购物
英语专业推荐信
2013/11/16 职场文书
经济贸易专业自荐信
2014/06/11 职场文书
医院安全生产月活动总结
2014/07/05 职场文书
党员个人剖析材料2014
2014/10/08 职场文书
党风廉政建设调研报告
2015/01/01 职场文书
中秋节慰问信
2015/02/15 职场文书
python 中yaml文件用法大全
2021/07/04 Python
Android学习之BottomSheetDialog组件的使用
2022/06/21 Java/Android
Spring Cloud OAuth2实现自定义token返回格式
2022/06/25 Java/Android