Python制作簡(jiǎn)易版2048小游戲
今天我們來(lái)動(dòng)手實(shí)現(xiàn)一款2048小游戲。這款游戲的精髓就玩家能夠在于通過(guò)滑動(dòng)屏幕合并相同數(shù)字,直到不能再合并為止。玩法可以說(shuō)是非常的簡(jiǎn)單,但挑戰(zhàn)性也是十足的。話(huà)不多說(shuō),讓我們從0開(kāi)始實(shí)現(xiàn)!
目標(biāo)效果
大致要實(shí)現(xiàn)的效果如下:
設(shè)計(jì)開(kāi)始
首先簡(jiǎn)單分析一下游戲的邏輯:
- 輸入移動(dòng)方向,游戲內(nèi)所有方塊都朝指定方向移動(dòng)
- 同方向移動(dòng)的方塊,數(shù)字相同則合并,然后生成一個(gè)合并的方塊
- 合并后生成新的方塊,無(wú)法生成新方塊時(shí)游戲結(jié)束
- 用一系列的顏色來(lái)區(qū)分不同分?jǐn)?shù)的方塊(可有可無(wú),純粹是為了美觀)
ok,游戲內(nèi)再邏輯已經(jīng)很清晰了?,F(xiàn)在開(kāi)始實(shí)現(xiàn):
步驟一
新建一個(gè)文件夾用來(lái)放需要的游戲素材
步驟二
新建一個(gè)python程序,可以命名為2048,放在素材目錄的同級(jí)文件夾下
步驟三
導(dǎo)入需要的依賴(lài)庫(kù):
import pygame as py import sys, random, time, redis, os,math import numpy as np
依賴(lài)庫(kù)中的redis是一個(gè)額外的數(shù)據(jù)庫(kù),用來(lái)存取游戲歷史數(shù)據(jù),需要的可以考慮安裝,不需要的用excel表代替也可以。
首先需要思考的是,游戲內(nèi)的方塊的移動(dòng)本質(zhì)上是坐標(biāo)的變換,并且方塊的坐標(biāo)是固定的,也就是說(shuō),每次輸入一個(gè)方向就按照一個(gè)移動(dòng)函數(shù)將所有方塊的坐標(biāo)進(jìn)行對(duì)應(yīng)的轉(zhuǎn)換。那么,如此以來(lái),就需要建立一個(gè)坐標(biāo)系用以標(biāo)記方塊的坐標(biāo)。
因?yàn)槭?x4的游戲,那么就按照(1,1),(1,2),(1,3),...,(4,4)建立游戲坐標(biāo),然而相比直接移動(dòng)坐標(biāo)還是比較麻煩,一個(gè)簡(jiǎn)單的想法是,每個(gè)方塊給一個(gè)唯一的標(biāo)記,如我們需要實(shí)現(xiàn)4x4的游戲,就需要16個(gè)記號(hào)。而每一個(gè)標(biāo)記就對(duì)應(yīng)了唯一且固定的坐標(biāo)。給出如下代碼:
# 預(yù)加載移動(dòng)邏輯 def pre_move(): numberPos = {} for num in range(1, 17): row1, row2 = divmod(num, 4) row = row1 + np.sign(row2) column = [row2 if row2 != 0 else 4][0] numberPos['{}'.format([row, column])] = num return numberPos
這里的numberPos實(shí)際上就是{‘{1,1}’:1,’{1,2}‘:2......}。當(dāng)然如果想設(shè)計(jì)5x5或者6x6的只需要把循環(huán)里面的17和4改成25和5或36和6就行。
ok,有了坐標(biāo)接下來(lái)的問(wèn)題好解決了。
步驟四
在新建的素材文件夾內(nèi)放入一些圖片方塊(正方形)用來(lái)表示每個(gè)不同分?jǐn)?shù)的方塊。如下圖所示:
這里的顏色大家可以隨意選擇,只要不與游戲背景色太接近即可。在圖片數(shù)量夠多的情況下甚至能夠?qū)崿F(xiàn)顏色動(dòng)態(tài)變換的方塊,當(dāng)然這都是后話(huà),設(shè)定好每個(gè)分?jǐn)?shù)的圖片后,再設(shè)置一個(gè)背景用的圖片,一個(gè)游戲圖標(biāo)用圖片,一個(gè)字體,字體單獨(dú)用來(lái)顯示文字。
當(dāng)然,不使用圖片加載游戲也是可以的,如使用py.draw.rect()也能繪制圖像,不過(guò)每次加載都繪制圖像會(huì)占用游戲大量運(yùn)算內(nèi)存,并且使用圖片可以自定義自己的游戲風(fēng)格,修改上也非常便利。設(shè)置完成之后,定義一個(gè)游戲的初始化模塊:
# 主程序 def game_start(): global screen, rate py.init() clock = py.time.Clock() screen_x = 500 # 請(qǐng)調(diào)到合適的大小 screen_y = math.ceil(screen_x * rate / rate2) screen = py.display.set_mode((screen_x, screen_y), depth=32) py.display.set_caption("終極2048") BackGround = [251, 248, 239] # 灰色 Icon = py.image.load('./素材/icon.png').convert_alpha() py.display.set_icon(Icon) screen.fill(color=BackGround) # 主界面下設(shè)計(jì) width = math.floor(screen_x * rate) bgSecond = py.image.load('./素材/BG_02.png').convert_alpha() bgSecond = py.transform.smoothscale(bgSecond, (width, width)) bgSecondRect = bgSecond.get_rect() bgSecondRect.topleft = math.floor(screen_x * (1 - rate) / 2), math.floor(screen_y * (1 - rate2))
游戲界面的大小請(qǐng)調(diào)節(jié)到合適的尺寸。接下來(lái)加載分?jǐn)?shù)圖片,以便游戲循環(huán)時(shí)隨時(shí)可以調(diào)用。
# 預(yù)加載分?jǐn)?shù)圖 def pre_load_image(background): imageList = {} imagePath = './素材/分?jǐn)?shù)/' image_filenames = [i for i in os.listdir(imagePath)] width = math.floor(background.width * (1 - internalWidth) / 4) for name in image_filenames: image = py.transform.smoothscale(py.image.load(imagePath + name).convert_alpha(), (width, width)) imageList[name.replace('.png', '')] = image return imageList # 加載分?jǐn)?shù)圖像 def draw_image(score_list, image_list, pos_list): for pos_num in score_list: score = score_list[pos_num] scoreSurf = BasicFont01.render('{}'.format(score), True, (0, 0, 0)) scoreRect = scoreSurf.get_rect() if score <= 4096: image = image_list['{}'.format(score)] else: image = image_list['4096'] imageRect = image.get_rect() imageRect.topleft = pos_list['{}'.format(pos_num)] scoreRect.center = imageRect.center screen.blit(image, imageRect) if score > 0: screen.blit(scoreSurf, scoreRect) # 圖像位置列表,表示為(x,y) # 用于確定加載的分?jǐn)?shù)圖像的顯示點(diǎn)位 def image_pos_list(background): pre_x = background.topleft[0] pre_y = background.topleft[-1] internalLong = math.ceil(internalWidth / 5 * background.width) imageLong = math.floor((1 - internalWidth) / 4 * background.width) posList = dict(zip(list(range(1, 17)), [''] * 16)) for num in range(1, 17): row1, row2 = divmod(num, 4) row = row1 + np.sign(row2) column = [row2 if row2 != 0 else 4][0] image_x = pre_x + internalLong * column + imageLong * (column - 1) image_y = pre_y + internalLong * row + imageLong * (row - 1) posList['{}'.format(num)] = (image_x, image_y) return posList
這里用了三個(gè)函數(shù)來(lái)加載游戲圖片,分表表示:提取圖片名保存到列表中,繪制游戲中的2,4,8等等數(shù)字在分?jǐn)?shù)圖片上。最后一個(gè)函數(shù)用于確定每個(gè)坐標(biāo)在游戲界面的顯示位置,并將其一一綁定。加載完成圖像之后,就需要完成關(guān)鍵的移動(dòng)邏輯,先上代碼:
# 移動(dòng)邏輯 def number_move(number_pos, move_input, score_list): values = list(number_pos.values()) keys = list(number_pos.keys()) numberPosReverse = dict(zip(values, keys)) newScoreList = score_list.copy() oldScoreList = {} while newScoreList != oldScoreList: oldScoreList = newScoreList.copy() for num in range(1, 17): pos = eval(numberPosReverse[num]) x, y = pos[0] + move_input[0], pos[1] + move_input[1] pos[0] = [x if 1 <= x <= 4 else pos[0]][0] pos[1] = [y if 1 <= y <= 4 else pos[1]][0] number = number_pos['{}'.format(pos)] oldNumberScore = newScoreList[num] nextNumberScore = newScoreList[number] syn = list(map(lambda x, y: abs(x) * abs(y), move_input, pos)) # 0值移動(dòng) if nextNumberScore == 0: newScoreList[number] = oldNumberScore newScoreList[num] = 0 # 無(wú)法移動(dòng) elif num == number: pass # 合并移動(dòng) elif oldNumberScore == nextNumberScore and num != number: newScoreList[number] = 2 * oldNumberScore newScoreList[num] = 0 # 邊界移動(dòng) elif oldNumberScore != nextNumberScore and 1 in syn or 4 not in syn: pass # 非邊界移動(dòng) elif oldNumberScore != nextNumberScore and 1 not in syn and 4 not in syn: x, y = pos[0] + move_input[0], pos[1] + move_input[1] next2NumberScore = newScoreList[number_pos['{}'.format([x, y])]] if next2NumberScore != nextNumberScore: pass elif next2NumberScore == nextNumberScore: newScoreList[number_pos['{}'.format([x, y])]] = 2 * next2NumberScore newScoreList[number] = oldNumberScore newScoreList[num] = 0 return newScoreList
首先導(dǎo)入預(yù)先確定好的坐標(biāo),移動(dòng)變量。根據(jù)前面分析的游戲邏輯,每次輸入移動(dòng)向量后游戲內(nèi)的所有方塊都需要移動(dòng),相同分?jǐn)?shù)的方塊需要一次性合并到一起,并且不能留空。詳細(xì)分析一下就是:
- 輸入一個(gè)移動(dòng)向量(x,y),如(+1,0)表示方塊向右移動(dòng)一格。
- 對(duì)所有的原坐標(biāo)進(jìn)行計(jì)算并保留為移動(dòng)后坐標(biāo),提取前后兩次坐標(biāo)對(duì)應(yīng)的分?jǐn)?shù)
- 從1號(hào)標(biāo)記開(kāi)始循環(huán)判斷:
- 0值移動(dòng):如果移動(dòng)后的分?jǐn)?shù)為0,用舊坐標(biāo)分?jǐn)?shù)替代新坐標(biāo)的分?jǐn)?shù),并刪除舊坐標(biāo)的分?jǐn)?shù)
- 無(wú)法移動(dòng):移動(dòng)后的坐標(biāo)與移動(dòng)前的坐標(biāo)相同,那么不做改變
- 合并移動(dòng):新舊坐標(biāo)對(duì)應(yīng)的分?jǐn)?shù)相同,那么新坐標(biāo)分?jǐn)?shù)x2,舊坐標(biāo)分?jǐn)?shù)刪除
- 邊界移動(dòng):方塊已經(jīng)處于移動(dòng)的邊界,無(wú)法移動(dòng),不做修改
- 非邊界移動(dòng):新舊坐標(biāo)對(duì)應(yīng)的分?jǐn)?shù)不同,且新坐標(biāo)的下一個(gè)坐標(biāo)對(duì)應(yīng)的分?jǐn)?shù)也不同,不做修改;新舊坐標(biāo)對(duì)應(yīng)的分?jǐn)?shù)不同,且新坐標(biāo)的下一個(gè)坐標(biāo)對(duì)應(yīng)的分?jǐn)?shù)相同,修改
- 循環(huán)整個(gè)邏輯,直到所有坐標(biāo)對(duì)應(yīng)的分?jǐn)?shù)不再發(fā)生改變
通過(guò)上述分析,移動(dòng)邏輯函數(shù)實(shí)現(xiàn)了輸入一個(gè)方向游戲內(nèi)的分?jǐn)?shù)動(dòng)態(tài)發(fā)生變化。最后我們還需要一個(gè)游戲結(jié)束的函數(shù):
# 游戲結(jié)束 def game_over(score,bg): ip = '127.0.0.1' password = None r = redis.Redis(host=ip, password=password, port=6379, db=2, decode_responses=True) r.hset('2048','{}'.format(time.localtime()),score) py.draw.rect(screen,bg,[0,0,screen.get_width(),screen.get_height()],0) BasicFont02 = py.font.SysFont('/素材/simkai.ttf', 40) overSurf = BasicFont01.render('Game Over', True, (0, 0, 0)) overRect = overSurf.get_rect() overRect.center = (math.floor(screen.get_width() / 2), math.floor(screen.get_height() / 2)) scoreSurf = BasicFont02.render('最終得分:', True, (0, 0, 0)) scoreRect = scoreSurf.get_rect() scoreRect.center = (math.floor(screen.get_width() / 2), math.floor(screen.get_height() * 0.6)) numberSurf = BasicFont02.render('{}'.format(score), True, (0, 0, 0)) numberRect = numberSurf.get_rect() numberRect.center = (math.floor(screen.get_width() / 2), math.floor(screen.get_height() * 0.7)) time.sleep(3) sys.exit()
一個(gè)鍵盤(pán)控制代碼,實(shí)現(xiàn)鍵盤(pán)控制游戲:
# 鍵盤(pán)控制函數(shù) def keyboard_ctrl(event): move_output = [0, 0] if event.key == py.K_UP: move_output = [-1, 0] elif event.key == py.K_DOWN: move_output = [1, 0] elif event.key == py.K_RIGHT: move_output = [0, 1] elif event.key == py.K_LEFT: move_output = [0, -1] return move_output
一個(gè)新方塊生成器,實(shí)現(xiàn)每次合并之后能在空白方塊處隨機(jī)生成2或4中的一個(gè)新分?jǐn)?shù),生成概率按照當(dāng)前游戲中的2和4的數(shù)量為基礎(chǔ)。
# 隨機(jī)得分生成 def random_score(score_list): values = list(score_list.values()) pro = [2] * (2 + values.count(2)) + [4] * (1 + values.count(4)) # 以當(dāng)前分?jǐn)?shù)圖中2或4出現(xiàn)的頻率為概率 blank = [[i if score_list[i] == 0 else 0][0] for i in range(1, 17)] blank = list(set(blank)) blank.remove(0) if not blank: return 'GameOver' # 游戲結(jié)束 else: score_list[random.choice(blank)] = random.choice(pro) return score_list
一個(gè)得分統(tǒng)計(jì)器,每次游戲運(yùn)行是統(tǒng)計(jì)當(dāng)前得分和歷史最高得分:
# 統(tǒng)計(jì)并記錄當(dāng)前得分 def record_score(score_list, background): totalScore = 0 values = list(score_list.values()) for i in values: totalScore += i scoreSurf = BasicFont01.render('得分:{}'.format(totalScore), True, (0, 0, 0)) scoreRect = scoreSurf.get_rect() scoreRect.topleft = (math.floor(0.1 * screen.get_width()), math.floor(0.05 * screen.get_height())) scoreRect.width = math.floor((rate - 0.15) / 2 * screen.get_width()) scoreRect.height = math.floor((1 - rate2) / 3 * 2 * screen.get_height()) py.draw.rect(screen, background, [scoreRect.topleft[0], scoreRect.topleft[1], scoreRect.width, scoreRect.height], 0) screen.blit(scoreSurf, scoreRect) return totalScore # 繪制歷史最高得分 def draw_best(background): ip = '127.0.0.1' password = None r = redis.Redis(host=ip, password=password, port=6379, db=2, decode_responses=True) scores=[eval(i) for i in list(r.hgetall('2048').values())] best_scores=max(scores) scoreSurf=BasicFont01.render('最高得分:{}'.format(best_scores),True,(0,0,0)) scoreRect=scoreSurf.get_rect() scoreRect.width = math.floor((rate - 0.15) / 2 * screen.get_width()) scoreRect.height = math.floor((1 - rate2) / 3 * 2 * screen.get_height()) scoreRect.topright = (math.floor(0.9 * screen.get_width()), math.floor(0.05 * screen.get_height())) py.draw.rect(screen, background, [scoreRect.topleft[0], scoreRect.topleft[1], scoreRect.width, scoreRect.height], 0) screen.blit(scoreSurf, scoreRect)
最后補(bǔ)充完整的游戲啟動(dòng)器:
# 主程序 def game_start(): global screen, rate py.init() clock = py.time.Clock() screen_x = 500 # 請(qǐng)調(diào)到合適的大小 screen_y = math.ceil(screen_x * rate / rate2) screen = py.display.set_mode((screen_x, screen_y), depth=32) py.display.set_caption("終極2048") BackGround = [251, 248, 239] # 灰色 Icon = py.image.load('./素材/icon.png').convert_alpha() py.display.set_icon(Icon) screen.fill(color=BackGround) # 主界面下設(shè)計(jì) width = math.floor(screen_x * rate) bgSecond = py.image.load('./素材/BG_02.png').convert_alpha() bgSecond = py.transform.smoothscale(bgSecond, (width, width)) bgSecondRect = bgSecond.get_rect() bgSecondRect.topleft = math.floor(screen_x * (1 - rate) / 2), math.floor(screen_y * (1 - rate2)) # 主界面上部分設(shè)計(jì) # 預(yù)加載數(shù)據(jù) draw_best(BackGround) posList = image_pos_list(bgSecondRect) imageList = pre_load_image(bgSecondRect) scoreList = dict(zip(list(range(1, 17)), [0] * 15 + [2])) # 分?jǐn)?shù)表 numberPos = pre_move() scoreList = random_score(scoreList) totalScore=0 # 主循環(huán) while True: screen.blit(bgSecond, bgSecondRect) # 刷新屏幕 if scoreList == 'GameOver': game_over(totalScore,BackGround) draw_image(scoreList, imageList, posList) # 繪制得分 totalScore = record_score(scoreList, BackGround) key = py.key.get_pressed() if key[py.K_ESCAPE]: exit() for event in py.event.get(): if event.type == py.QUIT: sys.exit() elif event.type == py.KEYDOWN: move_input = keyboard_ctrl(event) # 按下按鍵 scoreList = number_move(numberPos, move_input, scoreList) # 移動(dòng)數(shù)字 scoreList = random_score(scoreList) # 在按下按鍵后生成新的數(shù)字 py.display.update() clock.tick(FPS) if __name__ == '__main__': py.font.init() BasicFont01 = py.font.Font('./素材/simkai.ttf', 30) screen = py.display.set_mode((500, 500)) rate = 0.95 # 游戲主界面下的寬度占整個(gè)游戲界面寬度的比例 rate2 = 0.7 # 游戲主界面下的高度占整個(gè)游戲界面高度的比例 internalWidth = 0.1 # 間隙比例 FPS = 50 # 游戲幀率 game_start()
步驟五
啟動(dòng)游戲
運(yùn)行之前別忘了啟動(dòng)redis服務(wù)器。運(yùn)行效果圖:(游戲界面設(shè)計(jì)的不夠好。。。。,本來(lái)打算再加入一些小道具比如說(shuō):撤銷(xiāo),全屏合并等功能)
寫(xiě)在最后:有時(shí)間的話(huà)考慮再做一個(gè)菜單界面。最后給個(gè)懶人包:2048提取碼:utfu
到此這篇關(guān)于Python制作簡(jiǎn)易版2048小游戲的文章就介紹到這了,更多相關(guān)Python 2048內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
pyqt實(shí)現(xiàn).ui文件批量轉(zhuǎn)換為對(duì)應(yīng).py文件腳本
今天小編就為大家分享一篇pyqt實(shí)現(xiàn).ui文件批量轉(zhuǎn)換為對(duì)應(yīng).py文件腳本,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2019-06-06Python socket實(shí)現(xiàn)的簡(jiǎn)單通信功能示例
這篇文章主要介紹了Python socket實(shí)現(xiàn)的簡(jiǎn)單通信功能,結(jié)合實(shí)例形式分析了Python socket通信的相關(guān)概念、原理、客戶(hù)端與服務(wù)器端實(shí)現(xiàn)技巧以及socketserver模塊多并發(fā)簡(jiǎn)單實(shí)現(xiàn)方法,需要的朋友可以參考下2018-08-08python批量修改文件夾及其子文件夾下的文件內(nèi)容
這篇文章主要為大家詳細(xì)介紹了python批量修改文件夾及其子文件夾下的文件內(nèi)容,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2019-03-03淺談python的dataframe與series的創(chuàng)建方法
今天小編就為大家分享一篇淺談python的dataframe與series的創(chuàng)建方法,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2018-11-11python簡(jiǎn)單實(shí)現(xiàn)9宮格圖片實(shí)例
在本篇內(nèi)容里小編給各位分享的是一篇關(guān)于python實(shí)現(xiàn)朋友圈中的九宮格圖片的實(shí)例講解,有需要的朋友們可以參考下。2020-09-09Python命令行參數(shù)解析之a(chǎn)rgparse模塊詳解
這篇文章主要介紹了Python命令行參數(shù)解析之a(chǎn)rgparse模塊詳解,argparse?是?Python?的一個(gè)標(biāo)準(zhǔn)庫(kù),用于命令行參數(shù)的解析,這意味著我們無(wú)需在代碼中手動(dòng)為變量賦值,而是可以直接在命令行中向程序傳遞相應(yīng)的參數(shù),再由變量去讀取這些參數(shù),需要的朋友可以參考下2023-08-08解決python replace函數(shù)替換無(wú)效問(wèn)題
在本篇文章里小編給大家整理的是一篇關(guān)于python replace函數(shù)替換無(wú)效問(wèn)題的解決方法,需要的朋友們可以參考下。2020-01-01Windows下Eclipse+PyDev配置Python+PyQt4開(kāi)發(fā)環(huán)境
這篇文章主要介紹了Windows下Eclipse+PyDev配置Python+PyQt4開(kāi)發(fā)環(huán)境的相關(guān)資料,需要的朋友可以參考下2016-05-05