2048 是最近火紅的手機遊戲 three 的類似作品。該遊戲之操作僅需要使用上下左右鍵來操作格子,所有有數字的格子皆會朝向使用者所需入的方向移動,且移動至邊界為止。如果,在移動時兩個「相同數字」的格子發生「碰撞」的話,則該兩個數字會相加合併為一格較大的數字。在每次移動時,剩餘的空格區域將會隨機產生一格 2 或 4。遊戲的目標就是要產生 2048 這個數字才會獲勝,若是已經沒有空格區域且無法再進行任何方向的移動時,則遊戲失敗。遊戲規則的部分我就不再贅述了,讓我們回到主題。

由於只需要上下左右的操作,因此我就想說來寫個程式讓它隨機操作,看有沒有機會獲勝,有點類似「無限猴子定理」的感覺。本來想說用 Python 來寫看看,找到了一個 autopy 的套件。但似乎已經有三年沒有維護了,不支援 OS X 10.9 只好作罷。接著突然想到我的偶像 vgod 所開發的 Sikuli,又被稱作「圖形化按鍵精靈」。而且 Sikuli 本身就是採用 Python 的語法來編寫程式,並且可以使用 Python 的套件庫,使得它更加強大。

下載位置:http://www.sikuli.org/download.html

Sikuli 是個用 Java 所寫成的程式,也基於使用 Java,令 Sikuli 可以運行在任何有安裝 JRE 的系統上。其支援 OS X、Linux 與 Windows。從上面的連結下載安裝檔後執行,會有一些簡單的設定要選擇,接著就會產生一個 SikuliX-IDE.app 的應用程式。再將該應用程式移至 Applications 資料夾裡面即可開始使用。

下圖就是 Sikuli 的界面:

SikuliX-IDE

左邊的側欄有一些函式可以使用,那些函式終有出現相機圖示就表示 Sikuli 會要你截取螢幕的某個部分。

Sikuli 的圖像操作

按下 exists 的函式後,Sikuli 會要我們截取一塊圖片。我就選取 Dock 上的 Safari 圖示好了(見下圖)。

Safari on Dock

於是我們就可以看到 SikuliX-IDE 上出現了 exists(Safari 圖示) 這樣的語法(見下圖)。

這個 exists 函式是用來偵測畫面上是否存在這樣的物件。所以讓我們更進一步來修改程式,假設畫面上出現了 Safari 的圖示,就輸出 Hello Safari!,否則輸出 Safari, I miss you.

1
2
3
4
if exists(Safari 圖示):
    print "Hello Safari!"
else:
    print "Safari, I miss you."

code in SikuliX-IDE

於是我們按下 Run 即可開始運行,由於我的 Dock 會自動隱藏,所以當我開始執行後,理論上會在 SikuliX-IDE 下方的窗格中看到 Safari, I miss you. 的輸出。

而結果卻不是這樣,這是因為在 SikuliX-IDE 上本身就存在 Safari 的圖示,所以不管怎樣 Sikuli 都可以找到 Safari 的圖示。我們需要先將 SikuliX-IDE 的視窗給隱藏才行。於是我在程式的最前面加上一行 wait(5),讓我有 5 秒的時間把 SikuliX-IDE 從畫面中移開。

1
2
3
4
5
wait(5)
if exists(Safari 圖示):
    print "Hello Safari!"
else:
    print "Safari, I miss you."

code in SikuliX-IDE

這樣一來輸出結果終於符合預期了!我也可以趁這 5 秒,把滑鼠移至邊界讓 Dock 浮現出來,進而使得 Sikuli 可以找到 Safari 的圖示。假設,再來我們希望找到 Safari 的圖示後,就開啟 Safari。再把程式碼修改一下:

1
2
3
4
5
6
wait(5)
if exists(Safari 圖示):
    print "Hello Safari!"
    click(Safari 圖示)
else:
    print "Safari, I miss you."

code in SikuliX-IDE

將可以看到 Sikuli 找到 Safari 的圖示後,會將滑鼠移至該位置進行點擊,於是 Safari 就被開啟了!

基於圖形的偵測與操作,Sikuli 將可以利用這些工程實現更複雜的界面操作。而這篇文章就是要利用 Sikuli 來玩 2048,讓我們回到主題吧!

2048 的方向操作

2048 的操作是利用上下左右這四個按鍵來操作,所以最基本的功能就是模擬這幾個按鍵。在 SikuliX-IDE 的側欄中,有個 Keyboard Actions 的項目,就是用來進行鍵盤的事件的模擬。假設我們有一個 type("e") 的敘述,則 Sikuli 就會按下鍵盤的 e 鍵。至於上下左右鍵的話,則分別是 Key.UPKey.DOWNKey.LEFTKey.RIGHT。想看了解更多可以到 Sikuli 的官方文件(連結)中查詢。

一開始我們就先讓 Sikuli 進行上下左右各一次吧!程式碼如下:

1
2
3
4
5
wait(2)
type(Key.UP)
type(Key.DOWN)
type(Key.LEFT)
type(Key.RIGHT)

執行後,來到開啟著 2048 的網頁,就會看到 Sikuli 瞬間就動完上下左右四步。但只有四步怎麼可能玩的完,所以我們讓他走個 20 次上下左右好了。

1
2
3
4
5
6
wait(2)
for i in xrange(20):
    type(Key.UP)
    type(Key.DOWN)
    type(Key.LEFT)
    type(Key.RIGHT)

基本上這樣就完成讓 Sikuli 玩 2048 的功能了。不過由於要更加符合「無限猴子定理」,決定加上亂數的功能。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import random
wait(2)
for i in xrange(100):

    direction = random.randint(0,3)
    if direction == 0:
        type(Key.UP)
    if direction == 1:
        type(Key.DOWN)
    if direction == 2:
        type(Key.LEFT)
    if direction == 3:
        type(Key.RIGHT)

自動開啟 2048

每一次都要自己手動移到開著 2048 網頁實在太麻煩,甚至要自己開好 2048 的網頁也是麻煩。因此決定讓 Sikuli 自己幫我做好這些事情。首先透過 openApp 這個函式來幫我開啟 Safari,只要加入 openApp("Safari") 這行敘述即可開啟 Safari。

再利用 type 來幫我們輸入網址,在 Safari 中按下 ⌘ + l 即可將跳至網址列。接著一樣利用 type 來輸入網址,程式碼如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import random
openApp("Safari")
wait(2);

type("l", KeyModifier.CMD)
type("http://gabrielecirulli.github.io/2048/")
type(Key.ENTER)
wait(2);

for i in xrange(100):

    direction = random.randint(0,3)
    if direction == 0:
        type(Key.UP)
    if direction == 1:
        type(Key.DOWN)
    if direction == 2:
        type(Key.LEFT)
    if direction == 3:
        type(Key.RIGHT)

就可以看著 Sikuli 自動打開 Safari 並來到 2048 的網頁,接著就開始隨機亂動了。

自動按下 Try Again

不過,還有個問題。可憐的 Sikuli 如果輸了,都需要我們幫它按下 Try again。為了讓 Sikuli 可以不再經過我們來幫它按 Try again,我們終於需要借助到 Sikuli 強大的圖像查找與操作功能了!

只需要透過前面提到的 existsclick 這兩個函式即可完成該功能。就像剛剛在偵測 Safari 一樣,只是這次改成偵測 Try again 的按鈕罷了!程式碼如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import random
openApp("Safari")
wait(2);

type("l", KeyModifier.CMD)
type("http://gabrielecirulli.github.io/2048/")
type(Key.ENTER)
wait(2);

for i in xrange(20):

    if exists(Try again 圖片):
        click(Try again 圖片)

    direction = random.randint(0,3)
    if direction == 0:
        type(Key.UP)
    if direction == 1:
        type(Key.DOWN)
    if direction == 2:
        type(Key.LEFT)
    if direction == 3:
        type(Key.RIGHT)

code in SikuliX-IDE

可以發現整個 Sikuli 移動的速度變得非常慢,其中一個原因是 exists 預設的 timeout 有點久,所以決定在 exists 中加入一個額外的參數 0.001 來加快搜索的時間。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import random
openApp("Safari")
wait(2);

type("l", KeyModifier.CMD)
type("http://gabrielecirulli.github.io/2048/")
type(Key.ENTER)
wait(2);

for i in xrange(20):

    if exists(Try again 圖片, 0.001):
        click(Try again 圖片)

    direction = random.randint(0,3)
    if direction == 0:
        type(Key.UP)
    if direction == 1:
        type(Key.DOWN)
    if direction == 2:
        type(Key.LEFT)
    if direction == 3:
        type(Key.RIGHT)

可以發現已經比剛剛快速許多了,但仍舊不及原本的超高速移動。這是因為圖片搜索勢必會花費較多的時間,基本上無可避免。

判斷遊戲結果

遊戲失敗的畫面如下圖:

game over

如果打算判斷是否存在 Game Over 的字樣似乎不太可行,因為後面的數字每次都不太一樣。所以我只打算判斷 Game Over 字樣的中間那一小小區域。也就是 Game Over 字樣與遊戲框線交集的部分(見下圖)。

Part of Game Over

於是再修改一下程式碼,使得該程式在出現 Try again 時就判斷是否也存在 Game Over 字樣。若是存在則按下 Try again,否則就離開程式。因為我其實還沒有贏過,所以沒有 You Win! 的字樣可以判斷獲勝XD。程式碼如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import random
openApp("Safari")
wait(2);

type("l", KeyModifier.CMD)
type("http://gabrielecirulli.github.io/2048/")
type(Key.ENTER)
wait(2);

for i in xrange(20):

    if exists(Try again, 0.001):
        if exists(Game Over 字樣, 0.001):
            click(Try again)
        else:
            break

    direction = random.randint(0,3)
    if direction == 0:
        type(Key.UP)
    if direction == 1:
        type(Key.DOWN)
    if direction == 2:
        type(Key.LEFT)
    if direction == 3:
        type(Key.RIGHT)

code in SikuliX-IDE

終止 Sikuli

前面的程式碼都需要指定運行次數,這也大大限制了 Sikuli 的玩 2048 的時間。但是,若是改成無窮回圈的話,Sikuli 就永遠不會退出也是有點麻煩。因此我決定加上一個終止 Sikuli 的機制。看了看遊戲畫面,好像可以使用 2048 的遊戲標題來判斷。假設畫面上有 2048 的標題就表示遊戲還要繼續,若是 2048 的標題消失了,就表示可能切換到其他視窗了,於是就終止 Sikuli 的運行。2048 的標題圖像請見下圖:

2048 title

程式碼的部分就再加上一個 2048 標題的判斷,並將回圈的部分改成無窮回圈,程式碼如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import random
openApp("Safari")
wait(2);

type("l", KeyModifier.CMD)
type("http://gabrielecirulli.github.io/2048/")
type(Key.ENTER)
wait(2);

while True:

    if not exists(2048 標題, 0.001):
        break

    if exists(Try again, 0.001):
        if exists(Game Over 字樣, 0.001):
            click(Try again)
        else:
            break

    direction = random.randint(0,3)
    if direction == 0:
        type(Key.UP)
    if direction == 1:
        type(Key.DOWN)
    if direction == 2:
        type(Key.LEFT)
    if direction == 3:
        type(Key.RIGHT)

code in SikuliX-IDE

這樣就大功告成啦!有興趣抓取程式碼的話,可以到我的 github 上下載。

github 連結:https://github.com/KuoE0/Sikuli-play-2048

未來有時間的話,再來研究怎麼進行 AI 操作。正好寫完這篇時就看到一個朋友貼了 2048 的攻略,以及在 StackOverflow 上相關的演算法討論。一起附上給有興趣的朋友們,如果願意幫我修改的話也非常歡迎到 github 上 clone 回去修改!

後記:我睡覺時就放著給他跑,大約跑了七個多小時。結果,沒獲勝半次!!