1. 程式人生 > >用python從0開始寫2048小遊戲

用python從0開始寫2048小遊戲

最近想自己寫個小遊戲練練手,研究了一下也就2048和俄羅斯方塊最簡單了,所以就拿2048開刀了。
研究了一下2048對程式語言的要求,挺低的,就下面2點要求:
1、可以繪製介面,可以繪製圖案和顯示文字
2、可以捕捉使用者輸入(上下左右按鍵,或者對應的滑動)
很多語言都可以滿足這兩點要求,剛好最近寫了幾個python指令碼,就順便學習一下python吧。
調研了一下,開發程式一般使用wxPython包,那就它了。

wxPython基礎知識學習

第一步,先學習寫一個最簡單的wxPython應用程式:

#!/usr/bin/env python
# -*- coding: utf-8 -*-
import
wx app = wx.App() frame = wx.Frame(None, -1, "Hello World") frame.Show() app.MainLoop()

這個程式能看出一個wxPython程式至少要實現這幾點
1、匯入wx包
2、例項化一個App
3、例項話一個Frame
4、顯示這個Frame
5、app進入主迴圈
這個應用只彈出一個標題為Hello World的空介面,看起來沒有任何擴充套件的功能。

然後擴充套件一下這個最簡單的應用,學習一下wxPython的用法

#!/usr/bin/env python
# -*- coding: utf-8 -*-
import wx class MyFrame(wx.Frame): def __init__(self, title): super(MyFrame, self).__init__(None, title=title, size=(250, 250)) self.Bind(wx.EVT_PAINT, self.on_paint) self.Bind(wx.EVT_KEY_DOWN, self.on_key) self.Centre() self.Show() def on_paint(self, event)
:
dc = wx.PaintDC(self) dc.SetBackground(wx.Brush(wx.LIGHT_GREY)) dc.Clear() dc.DrawRoundedRectangle(30, 30, 100, 100, 2) dc.DrawLine(25, 150, 190, 150) dc.DrawText("hello world", 35, 160) def on_key(self, event): key_code = event.GetKeyCode() if key_code == wx.WXK_UP: print("UP") elif key_code == wx.WXK_DOWN: print("down") elif key_code == wx.WXK_LEFT: print("left") elif key_code == wx.WXK_RIGHT: print("right") class MyApp(wx.App): def OnInit(self): frame = MyFrame('appStudy') frame.Show(True) return True if __name__ == "__main__": app = MyApp() app.MainLoop()

這個應用有一點點樣子了,通過繼承App和Frame,定義了MyApp和MyFrame,然後就可以在MyFrame裡面加點功能進去了。在這個appStudy應用中,做了下面幾件事情

  1. 綁定了EVT_PAINT事件和on_paint函式,即app一旦觸發EVT_PAINT事件,就會呼叫on_paint函式。而這個事件在初始化介面的時候是會被呼叫的。所以我們可以把一些繪製圖案的功能放在這個函式裡面。在on_paint函式中
    • 設定背景色為淡灰色
    • 畫了一個圓角矩形
    • 畫了一條線
    • 寫了hello world這幾個字
  2. 綁定了EVT_KEY_DOWN事件和on_key函式,即有按鍵事件發生時,就會呼叫on_key函式。on_key函式中的可以檢測到按的是哪個按鍵

執行這個應用,結果如下
這裡寫圖片描述
Good,繪製介面和捕捉使用者輸入兩個功能都會了,下面就可以開始開發2048這個小遊戲了。

繪製遊戲背景

第一步,先把遊戲背景給畫出來,這個簡單,就是在on_paint裡面
• 設定個背景色
• 畫一個大矩形
• 大矩形裡面畫16個小矩形
如下圖:
這裡寫圖片描述
唯一不知道的是矩形顏色怎麼設定,查了一下SetBrush函式可以設定填充色,setPen可以設定邊框。我們不要邊框,直接設定成透明就可以了。Bingo,很容易就畫出來了。

#!/usr/bin/env python
# -*- coding: utf-8 -*-
import wx


class MyFrame(wx.Frame):
    PANEL_ORIG_POINT = wx.Point(15, 15)

    def __init__(self, title):
        super(MyFrame, self).__init__(None, title=title, size=(500, 550))
        self.Bind(wx.EVT_PAINT, self.on_paint)
        self.Centre()
        self.SetFocus()
        self.Show()

    def on_paint(self, e):
        self.draw_tiles()

    def draw_tiles(self):
        dc = wx.ClientDC(self)
        dc.SetBackground(wx.Brush("#FAF8EF"))
        dc.Clear()
        dc.SetBrush(wx.Brush("#C0B0A0"))
        dc.SetPen(wx.Pen("", 1, wx.TRANSPARENT))
        dc.DrawRoundedRectangle(self.PANEL_ORIG_POINT.x, self.PANEL_ORIG_POINT.y, 450, 450, 5)
        for row in range(4):
            for column in range(4):
                dc.SetBrush(wx.Brush("#CCC0B3"))
                dc.DrawRoundedRectangle(self.PANEL_ORIG_POINT.x + 110 * column + 10,
                                        self.PANEL_ORIG_POINT.y + 110 * row + 10, 100, 100, 5)


class MyApp(wx.App):
    def OnInit(self):
        frame = MyFrame('2048')
        frame.Show(True)
        return True

if __name__ == "__main__":
    app=MyApp()
    app.MainLoop()

進一步繪製介面,按鍵事件處理

背景介面畫完之後,就需要考慮實現這幾個功能:

  1. 處理按鍵事件
  2. 在小方塊上寫數字,以及根據方塊的不同數字重新整理不同的顏色。

功能1簡單,功能2就寫個測試函式,在按下空格鍵的時候觸發吧。
顏色嘛,就寫個VALUE_COLOR_DEF的map好了,key就是2、4、8……這些數字,value就是顏色。
看起來也不難,得加一個全域性性質的類變數tile_values,用二維陣列的方法來記錄每個小方塊當前的值;然後在空格鍵的時候呼叫test_update_tiles函式給每個小方塊設定個不同的值,然後重新整理一下介面,這一步很容易就寫好了。
實現的過程中遇到個小問題,發現EVT_PAINT經常會觸發,簡單,寫個變數 is_inited來控制讓背景只畫1次。

#!/usr/bin/env python
# -*- coding: utf-8 -*-
import wx


class MyFrame(wx.Frame):
    PANEL_ORIG_POINT = wx.Point(15, 15)
    VALUE_COLOR_DEF = {
        0: "#CCC0B3",
        2: "#EEE4DA",
        4: "#EDE0C8",
        8: "#F2B179",
        16: "#F59563",
        32: "#F67C5F",
        64: "#F65E3B",
        128: "#EDCF72",
        256: "#EDCF72",
        512: "#EDCF72",
        1024: "#EDCF72",
        2048: "#EDCF72",
        4096: "#EDCF72",
        8192: "#EDCF72",
        16384: "#EDCF72",
        32768: "#EDCF72"
    }
    tile_values = [[0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0]]
    is_inited = False

    def __init__(self, title):
        super(MyFrame, self).__init__(None, title=title, size=(500, 550))
        self.Bind(wx.EVT_PAINT, self.on_paint)
        self.Bind(wx.EVT_KEY_DOWN, self.on_key)
        self.Centre()
        self.SetFocus()
        self.Show()

    def on_paint(self, e):
        if not self.is_inited:
            self.draw_tiles()
            self.is_inited = True

    def draw_tiles(self):
        dc = wx.ClientDC(self)
        dc.SetBackground(wx.Brush("#FAF8EF"))
        dc.Clear()
        dc.SetBrush(wx.Brush("#C0B0A0"))
        dc.SetPen(wx.Pen("", 1, wx.TRANSPARENT))
        dc.DrawRoundedRectangle(self.PANEL_ORIG_POINT.x, self.PANEL_ORIG_POINT.y, 450, 450, 5)
        for row in range(4):
            for column in range(4):
                tile_value = self.tile_values[row][column]
                tile_color = self.VALUE_COLOR_DEF[tile_value]
                dc.SetBrush(wx.Brush(tile_color))
                dc.DrawRoundedRectangle(self.PANEL_ORIG_POINT.x + 110 * column + 10,
                                        self.PANEL_ORIG_POINT.y + 110 * row + 10, 100, 100, 5)
                dc.SetTextForeground("#707070")
                text_font = wx.Font(30, wx.SWISS, wx.NORMAL, wx.BOLD, face=u"Roboto")
                dc.SetFont(text_font)
                if tile_value != 0:
                    size = dc.GetTextExtent(str(tile_value))
                    if size[0] > 100:
                        text_font = wx.Font(24, wx.SWISS, wx.NORMAL, wx.BOLD, face=u"Roboto")
                        dc.SetFont(text_font)
                        size = dc.GetTextExtent(str(tile_value))
                    dc.DrawText(str(tile_value), self.PANEL_ORIG_POINT.x + 110 * column + 10 + (100 - size[0]) / 2,
                                self.PANEL_ORIG_POINT.y + 110 * row + 10 + (100 - size[1]) / 2)

    def on_key(self, event):
        key_code = event.GetKeyCode()
        if key_code == wx.WXK_UP:
            print("UP")
        elif key_code == wx.WXK_DOWN:
            print("down")
        elif key_code == wx.WXK_LEFT:
            print("left")
        elif key_code == wx.WXK_RIGHT:
            print("right")
        elif key_code == wx.WXK_SPACE:
            self.test_update_tiles()

    def test_update_tiles(self):
        self.tile_values = [[0, 2, 4, 8], [16, 32, 64, 128], [256, 512, 1024, 2048], [4096, 8192, 16384, 32768]]
        self.draw_tiles()


class MyApp(wx.App):
    def OnInit(self):
        frame = MyFrame('2048')
        frame.Show(True)
        return True


if __name__ == "__main__":
    app = MyApp()
    app.MainLoop()

執行一下,按一下空格鍵,不錯,就是我想要的。不好意思的是,這裡的顏色是下了個2048的小遊戲,玩出了所有顏色之後截圖,然後用畫板的拾色器取出來的顏色。
這裡寫圖片描述

遊戲演算法的實現

到了這裡,最重要的來了,需要實現按鍵之後的演算法了。需要做這幾件事情:

  1. 使用者按鍵之後,要計算出每個小方塊中新的數字
  2. 在初始化遊戲時或者按鍵處理之後,需要隨機生成2或者4的數字,填充到空的方塊中。

對於演算法1,以按左鍵為例,我發現每一行都各自獨立的,所以針對每一行進行計算就行了。所以我設計出來的演算法分兩步走:

  1. 把相鄰需要合併的數字合併
  2. 把所有非0的格子向左移動,最後的格子填充0

舉例如下:
[2, 2, 4, 4 ] -> [4, 0, 8, 0] -> [4, 8, 0, 0]
如果是按右鍵,就把這一行逆序之後,再按照向左移動處理,處理完之後再逆序一下就行了,如下:
[2, 2, 4, 4 ] -> [4, 4, 2, 2] -> [8, 0, 4, 0] -> [8, 4, 0, 0] -> [0, 0, 4, 8 ]
如果是按上下鍵,就把整個tile_values矩陣行列轉換一下,然後就可以按照左右按鍵處理了,處理完之後再行列轉換回來
對於功能2的實現,新增一個add_random_tile的函式,在所有空(值為0)的小方塊中隨機挑一個,然後再隨機生成2或者4填充進去就可以了。

功能更復雜了,順便調整了一下程式碼結構,於是現在的程式碼變成了下面這個樣子:

#!/usr/bin/env python
# -*- coding: utf-8 -*-
import wx
import copy
import random


class MyFrame(wx.Frame):
    PANEL_ORIG_POINT = wx.Point(15, 15)
    VALUE_COLOR_DEF = {
        0: "#CCC0B3",
        2: "#EEE4DA",
        4: "#EDE0C8",
        8: "#F2B179",
        16: "#F59563",
        32: "#F67C5F",
        64: "#F65E3B",
        128: "#EDCF72",
        256: "#EDCF72",
        512: "#EDCF72",
        1024: "#EDCF72",
        2048: "#EDCF72",
        4096: "#EDCF72",
        8192: "#EDCF72",
        16384: "#EDCF72",
        32768: "#EDCF72"
    }
    tile_values = [[0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0]]
    is_inited = False

    def __init__(self, title):
        super(MyFrame,self).__init__(None, title=title, size=(500, 550))
        self.Bind(wx.EVT_PAINT, self.on_paint)
        self.Bind(wx.EVT_KEY_DOWN, self.on_key)
        self.Centre()
        self.SetFocus()
        self.Show()

    def on_paint(self, e):
        if not self.is_inited:
            self.start_game()
            self.is_inited = True

    def add_random_tile(self):
        empty_tiles = [(row, col) for row in range(len(self.tile_values)) for col in range(len(self.tile_values[0]))
                       if self.tile_values[row][col] == 0]
        if len(empty_tiles) != 0:
            row, col = empty_tiles[random.randint(0, len(empty_tiles) - 1)]
            # value should be 2 or 4
            self.tile_values[row][col] = 2 ** random.randint(1, 2)
            return True
        else:
            return False

    def on_key(self, event):
        key_code = event.GetKeyCode()
        temp_tile_values = copy.deepcopy(self.tile_values)
        if key_code == wx.WXK_UP:
            self.on_key_up()
        elif key_code == wx.WXK_DOWN:
            self.on_key_down()
        elif key_code == wx.WXK_LEFT:
            self.on_key_left()
        elif key_code == wx.WXK_RIGHT:
            self.on_key_right()
        elif key_code == wx.WXK_SPACE:
            self.test_update_tiles()
            return
        self.add_random_tile()
        self.draw_tiles()

    def update_single_row_value(self, row_value, positive):
        num_cols = len(row_value)
        if not positive:
            temp_data = copy.deepcopy(row_value)
            row_value = [temp_data[num_cols - 1 - i] for i in range(num_cols)]
        for i in range(num_cols):
            if row_value[i] == 0:
                continue
            for j in range(i + 1, num_cols):
                if row_value[j] == row_value[i]:
                    row_value[i] *= 2
                    row_value[j] = 0
                    break
                elif row_value[j] > row_value[i]:
                    break
        for i in range(num_cols):
            if row_value[i] != 0:
                continue
            for j in range(i + 1, num_cols):
                if row_value[j] != 0:
                    row_value[i] = row_value[j]
                    row_value[j] = 0
                    break
        if not positive:
            temp_data = copy.deepcopy(row_value)
            row_value = [temp_data[num_cols - 1 - i] for i in range(num_cols)]
        return row_value

    def on_key_up(self):
        temp_tile_values = [[row[i] for row in self.tile_values] for i in range(len(self.tile_values[0]))]
        for row in range(len(self.tile_values)):
            temp_tile_values[row] = self.update_single_row_value(temp_tile_values[row], True)
        self.tile_values = [[row[i] for row in temp_tile_values] for i in range(len(temp_tile_values[0]))]

    def on_key_down(self):
        temp_tile_values = [[row[i] for row in self.tile_values] for i in range(len(self.tile_values[0]))]
        for row in range(len(self.tile_values)):
            temp_tile_values[row] = self.update_single_row_value(temp_tile_values[row], False)
        self.tile_values = [[row[i] for row in temp_tile_values] for i in range(len(temp_tile_values[0]))]

    def on_key_left(self):
        for row in range(len(self.tile_values)):
            self.tile_values[row] = self.update_single_row_value(self.tile_values[row], True)

    def on_key_right(self):
        for row in range(len(self.tile_values)):
            self.tile_values[row] = self.update_single_row_value(self.tile_values[row], False)

    def init_screen(self):
        dc = wx.ClientDC(self)
        dc.SetBackground(wx.Brush("#FAF8EF"))
        dc.Clear()
        dc.SetBrush(wx.Brush("#C0B0A0"))
        dc.SetPen(wx.Pen("", 1, wx.TRANSPARENT))
        dc.DrawRoundedRectangle(self.PANEL_ORIG_POINT.x, self.PANEL_ORIG_POINT.y, 450, 450, 5)

    def draw_tiles(self):
        dc = wx.ClientDC(self)
        dc.SetBackground(wx.Brush("#F0F0E0"))
        dc.SetBrush(wx.Brush("#C0B0A0"))
        dc.SetPen(wx.Pen("", 1, wx.TRANSPARENT))
        for row in range(4):
            for column in range(4):
                tile_value = self.tile_values[row][column]
                tile_color = self.VALUE_COLOR_DEF[tile_value]
                dc.SetBrush(wx.Brush(tile_color))
                dc.DrawRoundedRectangle(self.PANEL_ORIG_POINT.x + 110 * column + 10,
                                        self.PANEL_ORIG_POINT.y + 110 * row + 10, 100, 100, 5)
                dc.SetTextForeground("#707070")
                text_font = wx.Font(30, wx.SWISS, wx.NORMAL, wx.BOLD, face=u"Roboto")
                dc.SetFont(text_font)
                if tile_value != 0:
                    size = dc.GetTextExtent(str(tile_value))
                    if size[0] > 100:
                        text_font = wx.Font(24, wx.SWISS, wx.NORMAL, wx.BOLD, face=u"Roboto")
                        dc.SetFont(text_font)
                        size = dc.GetTextExtent(str(tile_value))
                    dc.DrawText(str(tile_value), self.PANEL_ORIG_POINT.x + 110 * column + 10 + (100 - size[0]) / 2,
                                self.PANEL_ORIG_POINT.y + 110 * row + 10 + (100 - size[1]) / 2)

    def start_game(self):
        self.tile_values = [[0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0]]
        self.init_screen()
        self.add_random_tile()
        self.add_random_tile()
        self.draw_tiles()

    def test_update_tiles(self):
        self.tile_values = [[0, 2, 4, 8], [16, 32, 64, 128], [256, 512, 1024, 2048], [4096, 8192, 16384, 32768]]
        self.draw_tiles()


class MyApp(wx.App):
    def OnInit(self):
        frame = MyFrame('2048')
        frame.Show(True)
        return True


if __name__ == "__main__":
    app = MyApp()
    app.MainLoop()

做到這裡,遊戲已經可以玩了,好開心^_^。可是遊戲還不會判斷是否結束。

結束判斷

同時滿足下面2個條件,就可以判定遊戲結束了:

  1. 所有格子的值全都是非0
  2. 沒有任意兩個相鄰的格子值相同,相鄰包括上下左右

於是加一個函式is_game_over來實現這個功能,如果確定已經結束了,那就彈出個提示框來讓使用者重新開始。兩個函式設計如下:

    def is_game_over(self):
        # exist 0 or there is a neighbour with the same value
        print ("is_game_over")
        num_rows = len(self.tile_values)
        num_cols = len(self.tile_values[0])
        for i in range(num_rows):
            for j in range(num_cols):
                if self.tile_values[i][j] == 0 or \
                        (j < num_cols-1 and self.tile_values[i][j] == self.tile_values[i][j + 1]) or \
                        (i < num_rows-1 and self.tile_values[i][j] == self.tile_values[i + 1][j]):
                    return False
        return True

    def game_over(self):
        if wx.MessageBox(u"遊戲結束,是否再來一局?", u"Game Over", wx.YES_NO) == wx.YES:
            self.start_game()

增加計分和最高記錄功能

沒有分數這遊戲可不好玩,得把這個加上。分數的計算,就每次把被合併了的數字加到總分上去好了。介面設計成如下:
這裡寫圖片描述
直接畫文字也可以,不過我發現個更好的工具:StaticText控制元件。用這個控制元件之後,更新分數就只要修改控制元件內容就可以。這一步要做下面幾件事:

  • 初始化介面的時候繪製4個StaticText控制元件
  • 用一個檔案來加下“記錄”的分數,開始遊戲的時候要讀出分數,結束遊戲的時候,如果破紀錄了要更新檔案
  • 在每次按鍵的時候,要計算當前分數。如果分數有變化,需要更新介面上的“積分”

因為在最上方增加了4個控制元件,所以整個遊戲介面也往下移

增加重新開始功能

對比我手機上下載的2048的遊戲,還缺少一個重新開始按鈕,那就把這個功能加上吧,於是介面就變成了下面這個樣子。
這裡寫圖片描述
按鈕使用button控制元件實現。最終程式碼變成了:

#!/usr/bin/env python
# -*- coding: utf-8 -*-
import wx
import copy
import random

'''
    1. 繪製空介面,即學習程式語言的基本功能
    2. 繪製4×4的方塊
    3. 捕捉上下左右按鍵,測試是否可以更新方塊顏色和數字
    4. 初始化介面(random 兩個位置,再random兩個數字2或者4放入位置),在按鍵事件中增加演算法,更新對應方塊的數字和顏色
    5. 增加結束條件
    6. 增加計分和最高記錄功能
    7、增加重新開始按鈕
'''


class MyFrame(wx.Frame):
    PANEL_ORIG_POINT = wx.Point(15, 100)
    VALUE_COLOR_DEF = {
        0: "#CCC0B3",
        2: "#EEE4DA",
        4: "#EDE0C8",
        8: "#F2B179",
        16: "#F59563",
        32: "#F67C5F",
        64: "#F65E3B",
        128: "#EDCF72",
        256: "#EDCF72",
        512: "#EDCF72",
        1024: "#EDCF72",
        2048: "#EDCF72",
        4096: "#EDCF72",
        8192: "#EDCF72",
        16384: "#EDCF72",
        32768: "#EDCF72"
    }
    tile_values = [[0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0]]
    is_inited = False
    score = 0
    record = 0

    def __init__(self, title):
        super(MyFrame, self).__init__(None, title=title, size=(500, 600))
        self.init_widgets()
        self.Bind(wx.EVT_PAINT, self.on_paint)
        self.Bind(wx.EVT_KEY_DOWN, self.on_key)
        self.Centre()
        self.SetFocus()
        self.Show()

    def on_paint(self, e):
        if not self.is_inited:
            self.start_game()
            self.is_inited = True

    def add_random_tile(self):
        empty_tiles = [(row, col) for row in range(len(self.tile_values)) for col in range(len(self.tile_values[0]))
                       if self.tile_values[row][col] == 0]
        if len(empty_tiles) != 0:
            row, col = empty_tiles[random.randint(0, len(empty_tiles) - 1)]
            # value should be 2 or 4
            self.tile_values[row][col] = 2 ** random.randint(1, 2)
            return True
        else:
            return False

    def on_key(self, event):
        key_code = event.GetKeyCode()
        temp_tile_values = copy.deepcopy(self.tile_values)
        if key_code == wx.WXK_UP:
            self.on_key_up()
        elif key_code == wx.WXK_DOWN:
            self.on_key_down()
        elif key_code == wx.WXK_LEFT:
            self.on_key_left()
        elif key_code == wx.WXK_RIGHT:
            self.on_key_right()
        elif key_code == wx.WXK_SPACE:
            self.test_update_tiles()
            return
        if temp_tile_values == self.tile_values:
            if self.is_game_over():
                self.game_over()
        else:
            self.draw_score()
            self.add_random_tile()
            self.draw_tiles()

    def update_single_row_value(self, row_value, positive):
        num_cols = len(row_value)
        if not positive:
            temp_data = copy.deepcopy(row_value)
            row_value = [temp_data[num_cols - 1 - i] for i in range(num_cols)]
        for i in range(num_cols):
            if row_value[i] == 0:
                continue
            for j in range(i + 1, num_cols):
                if row_value[j] == row_value[i]:
                    self.score += row_value[j]
                    row_value[i] *= 2
                    row_value[j] = 0
                    break
                elif row_value[j] > row_value[i]:
                    break
        for i in range(num_cols):
            if row_value[i] != 0:
                continue
            for j in range(i + 1, num_cols):
                if row_value[j] != 0:
                    row_value[i] = row_value[j]
                    row_value[j] = 0
                    break
        if not positive:
            temp_data = copy.deepcopy(row_value)
            row_value = [temp_data[num_cols - 1 - i] for i in range(num_cols)]
        return row_value

    def on_key_up(self):
        temp_tile_values = [[row[i] for row in self.tile_values] for i in range(len(self.tile_values[0]))]
        for row in range(len(self.tile_values)):
            temp_tile_values[row] = self.update_single_row_value(temp_tile_values[row], True)
        self.tile_values = [[row[i] for row in temp_tile_values] for i in range(len(temp_tile_values[0]))]

    def on_key_down(self):
        temp_tile_values = [[row[i] for row in self.tile_values] for i in range(len(self.tile_values[0]))]
        for row in range(len(self.tile_values)):
            temp_tile_values[row] = self.update_single_row_value(temp_tile_values[row], False)
        self.tile_values = [[row[i] for row in temp_tile_values] for i in range(len(temp_tile_values[0]))]

    def on_key_left(self):
        for row in range(len(self.tile_values)):
            self.tile_values[row] = self.update_single_row_value(self.tile_values[row], True)

    def on_key_right(self):
        for row in range(len(self.tile_values)):
            self.tile_values[row] = self.update_single_row_value(self.tile_values[row], False)

    def on_btn_restart(self, e):
        self.game_over()

    def init_widgets(self):
        self.label_score_text = wx.StaticText(self, -1, u"積分", (50, 15), (100, 30), wx.ALIGN_CENTER)
        self.label_score_text.Font = wx.Font(24, wx.SWISS, wx.NORMAL, wx.BOLD, face=u"Roboto")
        self.label_score_text.SetForegroundColour("#0000A0")
        self.label_score_text.SetBackgroundColour("#FAF8EF")

        self.label_record_text = wx.StaticText(self, -1, u"記錄", (200, 15), (100, 30), wx.ALIGN_CENTER)
        self.label_record_text.Font = wx.Font(24, wx.SWISS, wx.NORMAL, wx.BOLD, face=u"Roboto")
        self.label_record_text.SetForegroundColour("#0000A0")
        self.label_record_text.SetBackgroundColour("#FAF8EF")

        self.score_text = wx.StaticText(self, -1, "0", (50, 50), (100, 30), wx.ALIGN_CENTER)
        self.score_text.Font = wx.Font(24, wx.SWISS, wx.NORMAL, wx.BOLD, face=u"Roboto")
        self.score_text.SetForegroundColour("#0000A0")

        self.record_text = wx.StaticText(self, -1, str(self.record), (200, 50), (100, 30), wx.ALIGN_CENTER)
        self.record_text.Font = wx.Font(24, wx.SWISS, wx.NORMAL, wx.BOLD, face=u"Roboto")
        self.record_text.SetForegroundColour("#0000A0")

        self.restart_btn = wx.Button(self, -1, u"重新\n開始", (350, 10), (80, 80), wx.ALIGN_CENTER)
        self.restart_btn.Font = wx.Font(24, wx.SWISS, wx.NORMAL, wx.BOLD, face=u"Roboto")
        self.restart_btn.SetForegroundColour("#0000A0")
        self.restart_btn.Bind(wx.EVT_BUTTON, self.on_btn_restart)

    def init_screen(self):
        dc = wx.ClientDC(self)
        dc.SetBackground(wx.Brush("#FAF8EF"))
        dc.Clear()
        dc.SetBrush(wx.Brush("#C0B0A0"))
        dc.SetPen(wx.Pen("", 1, wx.TRANSPARENT))
        dc.DrawRoundedRectangle(self.PANEL_ORIG_POINT.x, self.PANEL_ORIG_POINT.y, 450, 450, 5)

        self.score_text.SetLabel("0")
        self.record_text.SetLabel(str(self.record))

    def draw_score(self):
        self.score_text.SetLabel(str(self.score))

    def draw_tiles(self):
        dc = wx.ClientDC(self)
        dc.SetBackground(wx.Brush("#F0F0E0"))
        dc.SetBrush(wx.Brush("#C0B0A0"))
        dc.SetPen(wx.Pen("", 1, wx.TRANSPARENT))
        for row in range(4):
            for column in range(4):
                tile_value = self.tile_values[row][column]
                tile_color = self.VALUE_COLOR_DEF[tile_value]
                dc.SetBrush(wx.Brush(tile_color))
                dc.DrawRoundedRectangle(self.PANEL_ORIG_POINT.x + 110 * column + 10,
                                        self.PANEL_ORIG_POINT.y + 110 * row + 10, 100, 100, 5)
                dc.SetTextForeground("#707070")
                text_font = wx.Font(30, wx.SWISS, wx.NORMAL, wx.BOLD, face=u"Roboto")
                dc.SetFont(text_font)
                if tile_value != 0:
                    size = dc.GetTextExtent(str(tile_value))
                    if size[0] > 100:
                        text_font = wx.Font(24, wx.SWISS, wx.NORMAL, wx.BOLD, face=u"Roboto")
                        dc.SetFont(text_font)
                        size = dc.GetTextExtent(str(tile_value))
                    dc.DrawText(str(tile_value), self.PANEL_ORIG_POINT.x + 110 * column + 10 + (100 - size[0]) / 2,
                                self.PANEL_ORIG_POINT.y + 110 * row + 10 + (100 - size[1]) / 2)

    def start_game(self):
        self.tile_values = [[0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0]]
        self.score = 0
        try:
            with open("record.txt") as fp:
                self.record = int(fp.read())
                # print("read record: %d" % self.record)
        except (IOError, ValueError), err:
            print("read record error: %s" % err)
            self.record = 0

        self.init_screen()
        self.add_random_tile()
        self.add_random_tile()
        self.draw_tiles()

    def is_game_over(self):
        # exist 0 or there is a neighbour with the same value
        # print ("is_game_over")
        num_rows = len(self.tile_values)
        num_cols = len(self.tile_values[0])
        for i in range(num_rows):
            for j in range(num_cols):
                if self.tile_values[i][j] == 0 or \
                        (j < num_cols - 1 and self.tile_values[i][j] == self.tile_values[i][j + 1]) or \
                        (i < num_rows - 1 and self.tile_values[i][j] == self.tile_values[i + 1][j]):
                    return False
        return True

    def game_over(self):
        if self.score > self.record:
            self.record = self.score
            try:
                with open("record.txt", "w") as fp:
                    fp.write(str(self.score))
            except IOError as err:
                print err
        if wx.MessageBox(u"遊戲結束,是否再來一局?", u"Game Over", wx.YES_NO) == wx.YES:
            self.start_game()

    def test_update_tiles(self):