珠寶遊戲

Jason990420發表於2020-04-17

主題: 珠寶遊戲

檔案建立日期: 2020/04/17

最後修訂日期: None

相關軟體資訊:

Win 10 Python 3.7.6 numpy 1.18.2 PySimpleGUI 4.18.2 PIL/Pillow 7.1.1

個人庫 Tool

說明:本文請隨意引用或更改,只須標示出處及作者,作者不保證內容絶對正確無誤,如造成任何後果,請自行負責.

說明

抽了點時間寫了珠寶遊戲, 沒啥好說的.

  • 目的: 水平線或垂直線上連續至少三個一樣的珠寶.
  • 控制: 滑鼠拖弋以交換珠寶
  • 限制: 只限上下左右一格, 交換後不能造成連續至少三個一水平線或垂直線, 則返回
  • 連線後刪除, 上方珠寶下落補滿
  • 不作提示

遊樂畫面

珠寶遊戲

程式碼及說明

  • 庫的匯入
import numpy as np
import PySimpleGUI as sg
import random
import math
import winsound
from time import sleep
from io import BytesIO
from pathlib import Path
from PIL import Image
from Tool import Read_URL, Read_File, Save_File
  • 建立類, 主要是方便引數的儲存及呼叫
    所有常數或常用資料都定義在這裡
    • self.url1/self.urls 珠寶圖片及背景圖片來源
    • self.file1/self.file2 珠寶圖片及背景圖片下載後的存檔, 下次就不用再下載
    • self.x/self.y 每個珠寶格子的座標
    • self.width/self.height 視窗大小
    • self.columns 水平方向的珠寶數
    • self.rows 垂直方向的珠寶數
    • self.sound 聲音的頻率表
    • self.data 不同顏色珠寶的圖片
    • self.kind/self.figure 每一格珠寶的類別及其圖片元件的 ID
class Bejewel():

    def __init__(self):

        self.title = 'Bejeweled Game'
        self.url1 = 'http://pngimg.com/uploads/diamond/diamond_PNG6698.png'
        self.file1 = 'Jewelry.png'
        self.url2 = ('http://b.zol-img.com.cn/sjbizhi/images/4/320x510/'
                     '1366614187468.jpg')
        self.file2 = 'background.png'
        self.font = ('Courier New', 16, 'bold')
        self.w = self.h = 80
        self.columns = 16
        self.rows = 9
        self.kinds = 6  # maximum 7 now        
        self.gap = 10
        self.width = self.columns * (self.w + self.gap) + self.gap
        self.height = self.rows * (self.h + self.gap) + self.gap
        self.steps = 5
        self.d = [(self.h+self.gap)//self.steps for i in range(self.steps-1)]
        self.dx = self.d + [self.w+self.gap-sum(self.d)]
        self.dy = self.d + [self.h+self.gap-sum(self.d)]
        self.background = 'darkgreen'
        self.x = [i*(self.w+self.gap)+self.gap for i in range(self.columns)]
        self.y = [self.height*2-i*(self.w+self.gap)-2*self.gap
                    for i in range(self.rows*2)]
        self.sound = [int(220*(1+(2**(1/12))**i)) for i in range(12)]
        self.data = []
        self.score = 0
        self.figure = None
        self.kind = None
  • 建立新的遊戲內容
    • 珠寶類別由亂生成
    • 其他相關設定
    def Add_All(self):
        if self.figure is not None and self.kind is not None:
            cells = [[x, y] for x in range(self.columns)
                for y in range(self.rows*2) if self.figure[y, x]!=0]
            self.Delete(cells)
        self.score = 0
        self.Score(0)
        self.figure = np.full((self.rows*2, self.columns), 0)
        self.kind = np.random.randint(0, self.kinds-1, size = (
            self.rows*2, self.columns), dtype=np.int8)
        self.exist = np.full((self.rows*2, self.columns), 0, dtype=np.int8)
        for y in range(self.rows*2):
            for x in range(self.columns):
                self.Add_New(self.kind[y, x], x, y)
  • 畫出單一珠寶
    def Add_New(self, value, x, y):
        self.figure[y, x] = draw.DrawImage(
            data=self.data[value], location=(self.x[x], self.y[y]))
  • 陣列轉換成可使用的圖片資料
    def Array_To_Data(self, array):
        image = Image.fromarray(array, mode='RGBA')
        with BytesIO() as output:
            image.save(output, format="PNG")
            data = output.getvalue()
        return data
  • 檢查一條線中連線的珠寶位置
    def Check_Line(self, line, var, direction):
        result, value, count, tmp = [], None, 0, []
        for index, kind in enumerate(line):
            if kind != value:
                if count >= 3:
                    result += tmp
                tmp, value, count = [index], kind, 1
            else:
                count += 1
                tmp.append(index)
        if count >= 3:
            result += tmp
        if direction == 'x':
            result = [[var, y+self.rows] for y in result]
        else:
            result = [[x, var+self.rows] for x in result]
        return result
  • 檢查連線的珠寶, 閃爍珠寶後刪除, 並計分
    def Check_Lines(self):
        result = []
        for x in range(self.columns):
            result += self.Check_Line(self.kind[self.rows:, x], x, 'x')
        for y in range(self.rows):
            result += self.Check_Line(self.kind[y+self.rows, :], y, 'y')
        if result == []:
            return False
        cells = []
        for item in result:
            if item not in cells:
                cells.append(item)
        self.Flash(cells)
        self.Delete(cells)
        self.Score(len(cells))
        return True
  • 刪除列表中的珠寶, 按列表建立標籤, 再一次刪除標籤所對應的珠寶
    def Delete(self, cells):
        if cells == []:
            return
        for item in cells:
            x, y = item
            draw.Widget.addtag_withtag('Delete', self.figure[y, x])
        draw.Widget.delete('Delete')
        draw.Widget.dtag('Delete', 'Delete')
        for x, y in cells:
            self.kind[y, x], self.figure[y, x] = -1, 0
        window.Refresh()
  • 閃爍珠寶, 以左右移動的方式來表達

    def Flash(self, target):
        dxes = [-2] + [4, -4]*3 + [2]
        for x, y in target:
            draw.Widget.addtag_withtag('Remove', self.figure[y, x])
        for dx in dxes:
            draw.Widget.move('Remove', dx, 0)
            window.Refresh()
            sleep(0.05)
  • 下載背景圖, 存檔並顯示
    def Load_background(self):
        if not Path(self.file2).is_file():
            response, data = Read_URL(self.url2, byte=True)
            if data:
                with open(self.file2, 'wb') as f:
                    f.write(data)
            else:
                Signal(f'Cannot get {filename} from web !')
                quit()
        im = Read_File(self.file2)
        im = im.convert(mode='RGBA').resize((self.width, self.height))
        a = np.array(im, dtype=np.uint8)
        data = self.Array_To_Data(a)
        self.picture = draw.DrawImage(data=data, location=(0, self.height))
  • 下載單一珠寶圖片, 改變成不同顏色圖片, 以供使用
    def Load_Icon(self):
        if not Path(self.file1).is_file():
            response, data = Read_URL(self.url1, byte=True)
            if data:
                with open(self.file1, 'wb') as f:
                    f.write(data)
            else:
                Signal(f'Cannot get {elf.file1} from web !')
                quit()
        im = Read_File(self.file1).resize((self.w, self.h))
        a = np.array(im, dtype=np.uint8)
        arrays = []
        for i, j, k in [[0, 1, 2], [0, 0, 0], [2, 2, 2], [2, 0, 2], [0, 2, 0],
                        [2, 2, 1], [1, 1, 0]]:
            t = a.copy()
            t[:,:,0], t[:,:,1], t[:,:,2] = a[:,:,i], a[:,:,j], a[:,: ,k]
            arrays.append(t)
        self.data = [self.Array_To_Data(array) for array in arrays]
  • 珠寶下移後, 最上方的空位補上新的珠寶
    def Move_Add(self, line):
        for x in line:
            value = random.randint(0, self.kinds-1)
            self.kind[0, x] = value
            self.Add_New(value, x, 0)
  • 由下往上檢查空位, 其上方所有的所有珠寶全部下落一格, 再重複檢查, 直到沒有空位
    def Move_All_Down(self):
        while True:
            target_down, line_down = [], []
            for x in range(self.columns):
                for y in range(2*self.rows-1, self.rows-1, -1):
                    if self.kind[y, x] == -1:
                        target_down += [[x, i] for i in range(y-1, -1, -1)]
                        line_down.append(x)
                        break
            if target_down == []:
                return
            self.Move_Down(target_down)
            self.Move_Add(line_down)
  • 按列表建立標籤, 標籤中的珠寶一次全部下移一格, 事實上, 下移一格, 還分五個動作, 每次只移五分之一.
    def Move_Down(self, target):
        for x, y in target:
            draw.Widget.addtag_withtag('Move Down', self.figure[y, x])
            self.kind[y+1, x] = self.kind[y, x]
            self.figure[y+1, x] = self.figure[y, x]
        for dy in self.dy:
            draw.Widget.move('Move Down', 0, dy)
            window.Refresh()
            # sleep(0.01)
        draw.Widget.dtag('Move Down', 'Move Down')
  • 將滑鼠拖弋起點的座標, 轉換成格子位置
    def Position_to_cell(self, x, y):
        x0, offset_x = divmod(x - self.gap, self.w + self.gap)
        y0, offset_y = divmod(self.height*2 - y - self.gap, self.h + self.gap)
        if ((x <= self.gap) or (self.height-y <= self.gap) or
            (offset_x > self.w) or (offset_y > self.h)):
                return None, None
        return x0, y0
  • 遊戲分數的計算更新, 計算方式以可消去格子總數除以3的平方, 再乘以3
    def Score(self, score):
        self.score += int(score**2//3)
        self.score = min(self.score, 999999)
        window.FindElement('Score').Update(
            value='Score {:0>6d}'.format(self.score))
  • 交換兩個珠寶的位置, 交換時分五個僕置變換來實現連續變化
    def Switch(self, x1, y1, x2, y2):
        for i in range(self.steps):
            dx = -self.dx[i] if x1 > x2 else self.dx[i] if x1 < x2 else 0
            dy = self.dy[i] if y1 > y2 else -self.dy[i] if y1 < y2 else 0
            draw.MoveFigure(self.figure[y1, x1], dx, dy)
            draw.MoveFigure(self.figure[y2, x2], -dx, -dy)
            window.Refresh()
            sleep(0.05)
        self.kind[y1, x1], self.kind[y2, x2] = (
            self.kind[y2, x2], self.kind[y1, x1])
        self.figure[y1, x1], self.figure[y2, x2] = (self.figure[y2, x2],
            self.figure[y1, x1])
  • 交換兩個珠寶位置, 如果不能連線, 將退回原位置
    def Switch_Icon(self, here, there):
        x1, y1 = here
        x2, y2 = there
        x0, y0 = self.Position_to_cell(x1, y1)
        if x0 == None:
            return False
        if abs(x2-x1) > abs(y2-y1):
            x, y = (x0+1, y0) if x2>x1 else (x0-1, y0)
        else:
            x, y = (x0, y0-1) if y2>y1 else (x0, y0+1)
        if not (0<=x0<self.columns and 0<=x<self.columns and
                self.rows<=y0<self.rows*2 and self.rows<=y<self.rows*2):
            return False
        self.Switch(x0, y0, x, y)
        if not self.Update():
            self.Switch(x, y, x0, y0)
  • 更新連線的處理, 有連線則上方珠寶下落, 再重複處理, 直到沒有連線, 聲音會隨著次數的增加, 頻率上調
    def Update(self):
        flag = False
        sound = -1
        while self.Check_Lines():
            flag = True
            sound = sound + 1 if sound <11 else 11
            winsound.Beep(self.sound[sound], 100)
            self.Move_All_Down()
        return flag
  • GUI 的按鈕建立
    def Button(self, key):
        return sg.Button(key, font=self.font, size = (10, 1),
            enable_events=True, key=key)
  • GUI 的畫布建立
    def Graph(self):
        return sg.Graph((self.width+1, self.height+1), (-1, -1),
            (self.width, self.height), background_color=self.background,
            drag_submits=True, enable_events=True, key='Graph')
  • GUI 的文字建立
    def Text(self):
        return sg.Text('Score 000000', font=self.font, size = (29, 1),
                       text_color = 'yellow', justification='left',
                       auto_size_text=False, key='Score')
  • 訊息處理, 有訊息會跳出框, 隨後關閉視窗, 結束遊戲
def Signal(string):
    sg.popup(string)
    window.close()
    exit()
  • 珠寶類的建立, 視窗實體化, 載入背景及所有珠寶的圖片資料
B = Bejewel()
layout = [[B.Text(), B.Button('New Game'), B.Button('Quit')], [B.Graph()]]
window = sg.Window(B.title, layout=layout, finalize=True)
draw = window.find_element('Graph')
B.Load_background()
B.Load_Icon()
  • 事件的處理, 主要是四件事
    • 遊戲結束
    • 遊戲開始
    • 滑鼠的拖弋處理
    • 珠寶交換的處理
mouse_down = False
drag = False
old_position = None
start = False
while True:

    event, values = window.read()

    if event in [None, 'Quit']:
        break

    elif event == 'New Game':
        B.Add_All()
        B.Update()

    elif event == 'Graph':
        new_position = values['Graph']
        if (not drag) and (not mouse_down):
            old_position = new_position
            mouse_down = True
        elif (not drag) and mouse_down:
            if new_position != old_position:
                drag = True
    elif event == 'Graph+UP':
        if drag:
            B.Switch_Icon(old_position, new_position)
        mouse_down = False
        drag = False
        old_position = None

window.close()
本作品採用《CC 協議》,轉載必須註明作者和本文連結

Jason Yang

相關文章