首頁 > 軟體

Python製作簡易版2048小遊戲

2022-07-27 18:00:41

今天我們來動手實現一款2048小遊戲。這款遊戲的精髓就玩家能夠在於通過滑動螢幕合併相同數位,直到不能再合併為止。玩法可以說是非常的簡單,但挑戰性也是十足的。話不多說,讓我們從0開始實現!

目標效果

大致要實現的效果如下:

設計開始

首先簡單分析一下游戲的邏輯:

  • 輸入移動方向,遊戲內所有方塊都朝指定方向移動
  • 同方向移動的方塊,數位相同則合併,然後生成一個合併的方塊
  • 合併後生成新的方塊,無法生成新方塊時遊戲結束
  • 用一系列的顏色來區分不同分數的方塊(可有可無,純粹是為了美觀)

ok,遊戲內再邏輯已經很清晰了。現在開始實現:

步驟一

新建一個資料夾用來放需要的遊戲素材

步驟二

新建一個python程式,可以命名為2048,放在素材目錄的同級資料夾下

步驟三

匯入需要的依賴庫:

import pygame as py
import sys, random, time, redis, os,math
import numpy as np

依賴庫中的redis是一個額外的資料庫,用來存取遊戲歷史資料,需要的可以考慮安裝,不需要的用excel表代替也可以。

首先需要思考的是,遊戲內的方塊的移動本質上是座標的變換,並且方塊的座標是固定的,也就是說,每次輸入一個方向就按照一個移動函數將所有方塊的座標進行對應的轉換。那麼,如此以來,就需要建立一個座標系用以標記方塊的座標。

因為是4x4的遊戲,那麼就按照(1,1),(1,2),(1,3),...,(4,4)建立遊戲座標,然而相比直接移動座標還是比較麻煩,一個簡單的想法是,每個方塊給一個唯一的標記,如我們需要實現4x4的遊戲,就需要16個記號。而每一個標記就對應了唯一且固定的座標。給出如下程式碼:

# 預載入移動邏輯
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實際上就是{‘{1,1}’:1,’{1,2}‘:2......}。當然如果想設計5x5或者6x6的只需要把迴圈裡面的17和4改成25和5或36和6就行。

ok,有了座標接下來的問題好解決了。

步驟四

在新建的素材資料夾內放入一些圖片方塊(正方形)用來表示每個不同分數的方塊。如下圖所示:

這裡的顏色大家可以隨意選擇,只要不與遊戲背景色太接近即可。在圖片數量夠多的情況下甚至能夠實現顏色動態變換的方塊,當然這都是後話,設定好每個分數的圖片後,再設定一個背景用的圖片,一個遊戲圖示用圖片,一個字型,字型單獨用來顯示文字。

當然,不使用圖片載入遊戲也是可以的,如使用py.draw.rect()也能繪製影象,不過每次載入都繪製影象會佔用遊戲大量運算記憶體,並且使用圖片可以自定義自己的遊戲風格,修改上也非常便利。設定完成之後,定義一個遊戲的初始化模組:

# 主程式
def game_start():
    global screen, rate
    py.init()
    clock = py.time.Clock()
    screen_x = 500  # 請調到合適的大小
    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)
    # 主介面下設計
    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))

遊戲介面的大小請調節到合適的尺寸。接下來載入分數圖片,以便遊戲迴圈時隨時可以呼叫。

# 預載入分數圖
def pre_load_image(background):
    imageList = {}
    imagePath = './素材/分數/'
    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
# 載入分數影象
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)
# 用於確定載入的分數影象的顯示點位
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

這裡用了三個函數來載入遊戲圖片,分表表示:提取圖片名儲存到列表中,繪製遊戲中的2,4,8等等數位在分數圖片上。最後一個函數用於確定每個座標在遊戲介面的顯示位置,並將其一一系結。載入完成影象之後,就需要完成關鍵的移動邏輯,先上程式碼:

# 移動邏輯
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值移動
            if nextNumberScore == 0:
                newScoreList[number] = oldNumberScore
                newScoreList[num] = 0
            # 無法移動
            elif num == number:
                pass
            # 合併移動
            elif oldNumberScore == nextNumberScore and num != number:
                newScoreList[number] = 2 * oldNumberScore
                newScoreList[num] = 0
            # 邊界移動
            elif oldNumberScore != nextNumberScore and 1 in syn or 4 not in syn:
                pass
            # 非邊界移動
            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

首先匯入預先確定好的座標,移動變數。根據前面分析的遊戲邏輯,每次輸入移動向量後遊戲內的所有方塊都需要移動,相同分數的方塊需要一次性合併到一起,並且不能留空。詳細分析一下就是:

  1. 輸入一個移動向量(x,y),如(+1,0)表示方塊向右移動一格。
  2. 對所有的原座標進行計算並保留為移動後坐標,提取前後兩次座標對應的分數
  3. 從1號標記開始迴圈判斷:
  4. 0值移動:如果移動後的分數為0,用舊座標分數替代新座標的分數,並刪除舊座標的分數
  5. 無法移動:移動後的座標與移動前的座標相同,那麼不做改變
  6. 合併移動:新舊座標對應的分數相同,那麼新座標分數x2,舊座標分數刪除
  7. 邊界移動:方塊已經處於移動的邊界,無法移動,不做修改
  8. 非邊界移動:新舊座標對應的分數不同,且新座標的下一個座標對應的分數也不同,不做修改;新舊座標對應的分數不同,且新座標的下一個座標對應的分數相同,修改
  9. 迴圈整個邏輯,直到所有座標對應的分數不再發生改變

通過上述分析,移動邏輯函數實現了輸入一個方向遊戲內的分數動態發生變化。最後我們還需要一個遊戲結束的函數:

# 遊戲結束
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()

一個鍵盤控制程式碼,實現鍵盤控制遊戲:

# 鍵盤控制函數
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

一個新方塊生成器,實現每次合併之後能在空白方塊處隨機生成2或4中的一個新分數,生成概率按照當前遊戲中的2和4的數量為基礎。

# 隨機得分生成
def random_score(score_list):
    values = list(score_list.values())
    pro = [2] * (2 + values.count(2)) + [4] * (1 + values.count(4))  # 以當前分數圖中2或4出現的頻率為概率
    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'  # 遊戲結束
    else:
        score_list[random.choice(blank)] = random.choice(pro)
        return score_list

一個得分統計器,每次遊戲執行是統計當前得分和歷史最高得分:

# 統計並記錄當前得分
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)

最後補充完整的遊戲啟動器:

# 主程式
def game_start():
    global screen, rate
    py.init()
    clock = py.time.Clock()
    screen_x = 500  # 請調到合適的大小
    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)
    # 主介面下設計
    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))
    # 主介面上部分設計
    # 預載入資料
    draw_best(BackGround)
    posList = image_pos_list(bgSecondRect)
    imageList = pre_load_image(bgSecondRect)
    scoreList = dict(zip(list(range(1, 17)), [0] * 15 + [2]))  # 分數表
    numberPos = pre_move()
    scoreList = random_score(scoreList)
    totalScore=0
    # 主迴圈
    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)  # 移動數位
                scoreList = random_score(scoreList)  # 在按下按鍵後生成新的數位
        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  # 遊戲主介面下的寬度佔整個遊戲介面寬度的比例
    rate2 = 0.7  # 遊戲主介面下的高度佔整個遊戲介面高度的比例
    internalWidth = 0.1  # 間隙比例
    FPS = 50  # 遊戲影格率
    game_start()

步驟五

啟動遊戲

執行之前別忘了啟動redis伺服器。執行效果圖:(遊戲介面設計的不夠好。。。。,本來打算再加入一些小道具比如說:復原,全螢幕合併等功能)

寫在最後:有時間的話考慮再做一個選單介面。最後給個懶人包:2048提取碼:utfu

到此這篇關於Python製作簡易版2048小遊戲的文章就介紹到這了,更多相關Python 2048內容請搜尋it145.com以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援it145.com!


IT145.com E-mail:sddin#qq.com