目錄
- 1、布局設(shè)計(jì)
- 2、算法設(shè)計(jì)
- 2.1 創(chuàng)建并初始化數(shù)組
- 2.2 移動(dòng)算法
- 2.3 是否勝利檢測(cè)算法
- 3、實(shí)現(xiàn)
- 3.1 框架搭建
- 3.2 數(shù)字方塊實(shí)現(xiàn)
- 3.3 將數(shù)字轉(zhuǎn)換成方塊添加到布局
- 3.4 初始化布局
- 3.5 按鍵檢測(cè)
- 3.6 試玩測(cè)試
- 3.7 改進(jìn)完善
- 4、完整代碼
- 5、總結(jié)
今年年初,新一季的《最強(qiáng)大腦》開(kāi)播了,第一集選拔的時(shí)候大家做了一個(gè)數(shù)字游戲,名叫《數(shù)字華容道》,當(dāng)時(shí)何猷君以二十幾秒的成績(jī)奪得該項(xiàng)目的冠軍,看了這個(gè)游戲之后我決定要寫(xiě)一個(gè)《數(shù)字華容道》的程序,過(guò)去了半年,我終于記起了這件事,今天就來(lái)實(shí)現(xiàn)。
數(shù)字推盤(pán)游戲(n-puzzle)是一種智力游戲,常見(jiàn)的類(lèi)型有十五數(shù)字推盤(pán)游戲和八數(shù)字推盤(pán)游戲等。十五數(shù)字推盤(pán)游戲的板上會(huì)有十五個(gè)方塊和一個(gè)大小相當(dāng)于一個(gè)方塊的空位(供方塊移動(dòng)之用),當(dāng)15個(gè)數(shù)字依次排序并且最后一個(gè)格子為空位即代表挑戰(zhàn)成功。
本文使用 PyQt5 進(jìn)行設(shè)計(jì)與實(shí)現(xiàn),PyQt5 是該程序的一個(gè)呈現(xiàn)方式,最重要的是算法,學(xué)會(huì)了算法,完全可以使用 PyGame 或者 Tkinter 實(shí)現(xiàn)。
PyQt5安裝:pip install PyQt5
本文使用環(huán)境:
系統(tǒng):Windows 10 64位Python版本:3.6
1、布局設(shè)計(jì)
做一個(gè)簡(jiǎn)版的數(shù)字華容道,布局設(shè)計(jì)如圖所示:
圖中灰色的部分使用 QWidget 作為整個(gè)游戲的載體;黃色部分使用 QGridLayout 作為數(shù)字方塊的布局;紅色部分使用 QLabel 作為數(shù)字方塊。
2、算法設(shè)計(jì)
如上圖所示,本游戲共需要15個(gè)方塊,每個(gè)方塊代表一個(gè)數(shù)字。我們可以使用 一個(gè)二維 list 來(lái)存儲(chǔ)方塊上的數(shù)字。其實(shí)我們要?jiǎng)?chuàng)建一個(gè) 4x4 的 list 存儲(chǔ) 0~15 各個(gè)數(shù)字,0 代表空的位置。
2.1 創(chuàng)建并初始化數(shù)組
- 創(chuàng)建數(shù)組的方法:
- 創(chuàng)建一個(gè)長(zhǎng)度為16的數(shù)組,并且在對(duì)應(yīng)位置上保存著 0~15 ;打亂順序
import random
# 用來(lái)存放位置信息的二維數(shù)組
blocks = []
# 產(chǎn)生隨機(jī)數(shù)組,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])
# 打印結(jié)果
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 移動(dòng)算法
假如移動(dòng)之前個(gè)數(shù)字位置如左圖所示,那么當(dāng)按下左箭頭時(shí),會(huì)變成如右圖所示:
可以看到 (1, 2) 和 (1, 3) 兩個(gè)位置上的數(shù)字互換了,即 0 和 8 互換;如果右圖所示再次按下左箭頭,那么所有數(shù)字都不會(huì)改變,因?yàn)?數(shù)字 0 右邊沒(méi)有數(shù)了。
總結(jié)一下:如果 數(shù)字 0 所在位置為 (row, column),并且 column≠3 那么按下左箭頭之后,(row, column) 和 (row, column+1) 位置上的數(shù)組互換,同理可得:
- 如果 數(shù)字 0 所在位置為 (row, column),并且 column≠0 那么按下右箭頭之后,(row, column) 和 (row, column-1) 位置上的數(shù)組互換;
- 如果 數(shù)字 0 所在位置為 (row, column),并且 row≠3 那么按下上箭頭之后,(row, column) 和 (row+1, column) 位置上的數(shù)組互換;
- 如果 數(shù)字 0 所在位置為 (row, column),并且 row≠0 那么按下下箭頭之后,(row, column) 和 (row-1, column) 位置上的數(shù)組互換;
將移動(dòng)算法封裝成一個(gè)函數(shù)如下:
# 移動(dòng)
# zero_row 代表數(shù)字0 所在二維數(shù)組的行下標(biāo),zero_column代表數(shù)字0 所在二維數(shù)組的列下標(biāo)
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 是否勝利檢測(cè)算法
檢測(cè)是否勝利其實(shí)很簡(jiǎn)單:前15個(gè)位置分別對(duì)應(yīng),最后一個(gè)為0即為勝利 ,不過(guò)為了避免不必要的計(jì)算,我們先檢測(cè)最后一個(gè)是否為 0 ,如果不為0 前面的就不用比較了。具體代碼實(shí)現(xiàn)如下:
# 檢測(cè)是否完成
def checkResult():
# 先檢測(cè)最右下角是否為0
if blocks[3][3] != 0:
return False
for row in range(4):
for column in range(4):
# 運(yùn)行到此處說(shuō)名最右下角已經(jīng)為0,pass即可
if row == 3 and column == 3:
pass
# 值是否對(duì)應(yīng)
elif blocks[row][column] != row * 4 + column + 1:
return False
return True
3、實(shí)現(xiàn)
下面講解所有功能模塊的實(shí)現(xiàn)。
3.1 框架搭建
創(chuàng)建 QWidget 作為整個(gè)游戲的載體:
import sys
from PyQt5.QtWidgets import QWidget, QApplication
class NumberHuaRong(QWidget):
""" 華容道主體 """
def __init__(self):
super().__init__()
self.initUI()
def initUI(self):
# 設(shè)置寬和高
self.setFixedSize(400, 400)
# 設(shè)置標(biāo)題
self.setWindowTitle('數(shù)字華容道')
# 設(shè)置背景顏色
self.setStyleSheet("background-color:gray;")
self.show()
if __name__ == '__main__':
app = QApplication(sys.argv)
ex = NumberHuaRong()
sys.exit(app.exec_())
運(yùn)行結(jié)果如下圖所示:
3.2 數(shù)字方塊實(shí)現(xiàn)
前面已經(jīng)提到,用一個(gè)二維數(shù)組來(lái)存放 0~16 個(gè)數(shù)字,最終我們要轉(zhuǎn)換成一個(gè)數(shù)字方塊,單獨(dú)創(chuàng)建一個(gè)類(lèi):
class Block(QLabel):
""" 數(shù)字方塊 """
def __init__(self, number):
super().__init__()
self.number = number
self.setFixedSize(80, 80)
# 設(shè)置字體
font = QFont()
font.setPointSize(30)
font.setBold(True)
self.setFont(font)
# 設(shè)置字體顏色
pa = QPalette()
pa.setColor(QPalette.WindowText, Qt.white)
self.setPalette(pa)
# 設(shè)置文字位置
self.setAlignment(Qt.AlignCenter)
# 設(shè)置背景顏色\圓角和文本內(nèi)容
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))
該類(lèi)繼承自 QLablel ,初始化需要傳入一個(gè)參數(shù) number ,number就是數(shù)字方塊上顯示的數(shù)字。
3.3 將數(shù)字轉(zhuǎn)換成方塊添加到布局
布局采用 QGridLayout 創(chuàng)建一個(gè) 4X4 的 self.gltMain,將16個(gè) 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 初始化布局
初始化布局包括隨機(jī)數(shù)據(jù)的產(chǎn)生與將數(shù)字轉(zhuǎn)換成方塊添加到布局:
# 初始化布局
def onInit(self):
# 產(chǎn)生隨機(jī)數(shù)組,0 代表空的位置
arr = range(16)
self.numbers = random.sample(arr, 16)
# 將數(shù)字方塊添加到布局
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 按鍵檢測(cè)
QWidget 有一個(gè) keyPressEvent 事件句柄,我們只需要重新實(shí)現(xiàn)該方法即可:
# 檢測(cè)按鍵
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, '挑戰(zhàn)結(jié)果', '恭喜您完成挑戰(zhàn)!'):
self.onInit()
按鍵檢測(cè)到按鍵按下之后判斷該鍵值是否為 “↑↓←→”或“WSAD”,并作出相應(yīng)的移動(dòng)(move),移動(dòng)之后刷新布局(updatePannel),最后檢測(cè)是否完成挑戰(zhàn)(checkResult),如果完成挑戰(zhàn),彈出提示框。如果點(diǎn)擊了 OK 按鈕,游戲重新開(kāi)始(onInit)。
3.6 試玩測(cè)試
至此,所有功能模塊介紹完畢,不要著急看完整代碼,我們先運(yùn)行一下程序看是否還有 Bugs。
玩了幾局之后發(fā)現(xiàn),并不是所有的局都能都還原,如下面這種情況:
如圖所示,14 和 15 方塊位置反了,無(wú)論如何也還原不聊了,這種情況是隨機(jī)出現(xiàn)的。到底是怎么回事呢?經(jīng)過(guò)一番上網(wǎng)搜索,確實(shí)如果只有兩個(gè)數(shù)字的位置反了,無(wú)論如何也還原不了的。那這是由什么造成的呢?還記得我們的二維數(shù)組是怎么產(chǎn)生的吧,隨機(jī)的,也就是說(shuō)可能會(huì)隨機(jī)到無(wú)法還原的情況。
如何避免這種情況呢?初始化數(shù)組時(shí),所有的位置都是正確的數(shù)字,然后使用 move 進(jìn)行移動(dòng)打亂。
3.7 改進(jìn)完善
由于前面已經(jīng)將各個(gè)功能模塊單獨(dú)寫(xiě)成了方法,因此我們只需修改 onInit 方法即可。
# 初始化布局
def onInit(self):
# 產(chǎn)生順序數(shù)組
self.numbers = list(range(1, 16))
self.numbers.append(0)
# 將數(shù)字添加到二維數(shù)組
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)
# 打亂數(shù)組
for i in range(500):
random_num = random.randint(0, 3)
self.move(Direction(random_num))
self.updatePanel()
先生成一個(gè)順序數(shù)組,里面保存著[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 0]
,然后轉(zhuǎn)為二維數(shù)組 blocks,再后隨即移動(dòng)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
# 用枚舉類(lèi)表示方向
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):
# 設(shè)置方塊間隔
self.gltMain.setSpacing(10)
self.onInit()
# 設(shè)置布局
self.setLayout(self.gltMain)
# 設(shè)置寬和高
self.setFixedSize(400, 400)
# 設(shè)置標(biāo)題
self.setWindowTitle('數(shù)字華容道')
# 設(shè)置背景顏色
self.setStyleSheet("background-color:gray;")
self.show()
# 初始化布局
def onInit(self):
# 產(chǎn)生順序數(shù)組
self.numbers = list(range(1, 16))
self.numbers.append(0)
# 將數(shù)字添加到二維數(shù)組
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)
# 打亂數(shù)組
for i in range(500):
random_num = random.randint(0, 3)
self.move(Direction(random_num))
self.updatePanel()
# 檢測(cè)按鍵
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, '挑戰(zhàn)結(jié)果', '恭喜您完成挑戰(zhàn)!'):
self.onInit()
# 方塊移動(dòng)算法
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)
# 檢測(cè)是否完成
def checkResult(self):
# 先檢測(cè)最右下角是否為0
if self.blocks[3][3] != 0:
return False
for row in range(4):
for column in range(4):
# 運(yùn)行到此處說(shuō)名最右下角已經(jīng)為0,pass即可
if row == 3 and column == 3:
pass
#值是否對(duì)應(yīng)
elif self.blocks[row][column] != row * 4 + column + 1:
return False
return True
class Block(QLabel):
""" 數(shù)字方塊 """
def __init__(self, number):
super().__init__()
self.number = number
self.setFixedSize(80, 80)
# 設(shè)置字體
font = QFont()
font.setPointSize(30)
font.setBold(True)
self.setFont(font)
# 設(shè)置字體顏色
pa = QPalette()
pa.setColor(QPalette.WindowText, Qt.white)
self.setPalette(pa)
# 設(shè)置文字位置
self.setAlignment(Qt.AlignCenter)
# 設(shè)置背景顏色\圓角和文本內(nèi)容
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、總結(jié)
在做的過(guò)程中遇到最大的坑就是隨機(jī)數(shù)組導(dǎo)致無(wú)法還原。另外在做這個(gè)游戲的時(shí)候我已經(jīng)找到還原規(guī)律了,這樣在測(cè)試的時(shí)候可以做完完整測(cè)試,否則根本無(wú)法測(cè)試都挑戰(zhàn)成功那一步。
另外要對(duì)《最強(qiáng)大腦》做一下吐槽:這個(gè)項(xiàng)目根本就是有偏袒的,玩過(guò)的人會(huì)很快,沒(méi)有玩過(guò)的找規(guī)律的時(shí)間就很長(zhǎng)。我在手機(jī)上玩4X4的最快還原用了 33 秒,對(duì)于該節(jié)目的冠軍(即便是玩過(guò))很是敬仰。
到此這篇關(guān)于教你如何用python開(kāi)發(fā)一款數(shù)字推盤(pán)小游戲的文章就介紹到這了,更多相關(guān)python開(kāi)發(fā)數(shù)字推盤(pán)小游戲內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
您可能感興趣的文章:- python實(shí)現(xiàn)滑雪者小游戲
- 教你用Python實(shí)現(xiàn)一個(gè)輪盤(pán)抽獎(jiǎng)小游戲
- python實(shí)現(xiàn)21點(diǎn)小游戲
- 教你用Python寫(xiě)一個(gè)植物大戰(zhàn)僵尸小游戲
- python反編譯教程之2048小游戲?qū)嵗?/li>
- Python實(shí)現(xiàn)我的世界小游戲源代碼
- Python實(shí)現(xiàn)簡(jiǎn)單的2048小游戲
- 基于Python-Pycharm實(shí)現(xiàn)的猴子摘桃小游戲(源代碼)
- Python約瑟夫生者死者小游戲?qū)嵗v解
- 用Python實(shí)現(xiàn)童年貪吃蛇小游戲功能的實(shí)例代碼
- 利用python制作拼圖小游戲的全過(guò)程
- 利用python如何實(shí)現(xiàn)貓捉老鼠小游戲
- 學(xué)會(huì)用Python實(shí)現(xiàn)滑雪小游戲,再也不用去北海道啦