2048 解題程式的原理

棋盤下方的提示和自動遊玩按鈕,給的不是事先寫好的建議。它們會在你的瀏覽器裡,針對眼前這個棋盤本身做一次真正的搜尋,回傳評分最高的一步。提示會在棋盤上顯示一個箭頭,自動遊玩則會一直走這一步,直到對局結束。什麼都不會傳到伺服器。

從空棋盤開始、全部交給這個搜尋,它有 91.4% 的對局能湊到 2048 方塊。下面說明它是怎麼做到的。

為什麼用期望值最大化,而不是下棋的演算法

處理賽局樹的經典工具是極小化極大:假設對手一定會選最糟的應對,並照這個來規劃。可是 2048 沒有對手,有的只是隨機出現的方塊,而這個差別意義重大。

用極小化極大思路下棋的玩家,會把新方塊當成惡意的,認定 4 一定會出現在最糟的格子裡。結果是把運算資源全花在防備三十次裡不一定出現一次的棋盤上,下出多疑、偏守的棋。期望值最大化則不同,它把每一種實際可能出現的情況照各自的機率加權,再取平均。它是照機率下棋,而不是照惡夢下棋;只要隨機沒有惡意,這就是正確的模型。

所以搜尋會在兩種節點之間交替。在玩家節點,四個方向全都試一遍,留下結果最好的那個。在機率節點,在每一個空格裡以 0.9 的權重放一個 2、以 0.1 的權重放一個 4,分別評估後取平均。這跟遊戲本身出方塊時的 90 比 10 比例完全相同。

解題程式眼中的「好棋盤」

搜尋不可能一路展開到對局結束,所以解題程式在搜尋的最底層,用四項指標幫棋盤打分數,再照下面的權重加總。

指標權重衡量的是什麼
空格2.7空著的格子數本身。這是份量最重的一項,因為格子用完正是輸棋的原因。
單調性1沿著一排或一列,數字是不是往同一個方向平穩變大,而不是忽大忽小。
角落1最大的方塊在四個角落之一時,照它的大小給的加分。
平滑度0.1大小差很多的方塊彼此相鄰時的扣分。512 旁邊的 2 永遠併不起來,只是個負擔。

四項裡有三項,不過是把大家常給的建議翻成了算式:留下空格,保持排列,把大方塊放在角落。把這些建議變成判斷的,是權重。只要空格能多出夠多,搜尋會心甘情願接受角落的位置稍微差一點。而人往往剛好把這筆交易算反了。

每一項用的都不是方塊上的數字本身,而是以 2 為底的對數。這樣從 2 到 4 的差距,跟從 1024 到 2048 的差距算得一樣大。不然棋盤上最大的那個方塊會把其他一切都蓋過去。

把運算花在需要的地方

機率節點在每個空格上都要分岔,而且每次分兩支。空格有十個的寬鬆棋盤,每一層就是二十個分支;空格只剩三個的擁擠棋盤,只有六個。把兩者搜到一樣的深度是本末倒置:寬鬆的棋盤容易活下去,搜尋起來卻很吃力;擁擠的棋盤剛好相反。

所以深度會隨棋盤調整。

空格搜尋深度
7 個以上往前看 2 步
4~6 個往前看 3 步
3 個以下往前看 4 步

寬鬆的棋盤輕輕帶過,變擠了就更加謹慎。這剛好複製了高手的直覺。也因為這樣,不管在對局的哪個階段,從點下去到箭頭出現的等待時間都能維持在舒服的範圍內。

實際實力

在 500 局完整的對局裡,湊到 2048 的對局是 91.4%,得分中位數是 37,004 分,對局長度的中位數是 1,943 步。包含對照組的完整結果裡,可以把這些數字放回脈絡中來看。

有必要說清楚:這並不是最頂尖的 2048 機器人。公開的解題程式使用位元棋盤表示法、事先算好的移動表和置換表快取,搜得深得多,能湊到 32768。這個解題程式在瀏覽器的主執行緒上用一般陣列執行,步與步之間不做快取,而且為了不讓網頁卡頓,限制在往前看 4 步以內。它不是為了刷紀錄,而是為了用幾乎感覺不到等待的速度,回答「這裡該怎麼走?」這個問題。

不會毀掉遊戲樂趣的用法

用好提示的方法,不是照著它走。先自己決定這一步,再按提示。箭頭一致的話,沒什麼新東西可學;可是不一致的話,你就找到了自己看錯的那個棋盤,而實力進步的種子就在那裡。

自動遊玩與其當救援投手,不如當示範賽來用更有價值。在你自己覺得難的局面讓它跑一跑,看它守住哪個方向、刻意不做哪些合併。隨時按方向鍵,就能馬上把操作拿回來。

解題程式是大約 250 行、沒有任何相依套件的 JavaScript,以一般檔案的形式放在 /js/ai.js。想確認這裡寫的內容,可以用瀏覽器的檢視原始碼直接讀它。

準備好試試看了嗎?

開始玩 2048
試試我們的每日挑戰