iT邦幫忙

2024 iThome 鐵人賽

DAY 10
2

... 也不太清楚接下來會發生什麼事情,也很少能夠清晰地預見當行之道。
我想,就趁這個時候好好地回首來時路吧。
這樣做的話,無論是怎樣的道路,都能夠真正的看清楚。

-- <在困惑中逐漸變強>,羽生善治著,拙譯(目前尚無官方中文譯本)

昨日只分享幾個狀態碼讓讀者有個感覺,但它可以說是整個遊戲系統端與代理人端交談的重要資訊。有別於昨天的錯誤狀態範例,我今天會展示正確的狀態如何引導代理人行動。這對於之後的 AlphaZero 演算法實作也意外的扮演了一個基礎,這部份我們會在這週後續的系列文就看到。

我大致跟隨電腦科學裡面狀態機的概念。

Ix00 原地踏步

昨天已經詳述,算是一個非常特殊的狀態。

Ix03 換你下囉!

今天的文章之中的 github 連結都是對應到六月左右的狀態,因為之後開始實作蒙地卡羅,就變得比較複雜了。

在主要的遊戲系統迴圈之中,在 handle_client起頭之處,伺服器會用這個狀態碼跟對象代理人打招呼。

Ix01 嗯,你做了一個合乎規則的決定,請繼續

可以在 src/core/action.rs 當中看到,每個行動的正確結束的時候,幾乎都是使用這個狀態碼回傳結果。如add_lockdown_by_coordadd_character

至於代理人程式碼中對應的部份,目前為止都沒有發現 Ix01Ix03 有什麼特別不同,所以他們也通常是被一起處理。如 examples/coord_clients/base.py 這隻被所有代理人(不限於隨機猴子或其他代理人)使用的共同程式當中的主遊玩迴圈 play(它會被一直呼叫直到遊戲結束)

    def play(self):
        data = self.s.recv(CODE_DATA+S)
        self.analyze(data)
        if data[0:4] in (b'Ix01', b'Ix03'):
            self.s.sendall(bytes([self.action]))
...

analyze 是個別代理人定義的方法,對盤面進行分析。像基本隨機猴子的話就會完全不分析亂試一通,但強化學習代理人就會使用傳入的神經網路模型針對盤面做推論。分析完之後,會有一個決定進行的行動 self.action。這個行動會透過網路傳回伺服器端(self.s.sendall(bytes([self.action])))。這一段的邏輯完全是可以共用於 Ix01Ix03 這兩個起手狀態的。

Ix02 嗯,你的標準回合結束!

這個狀態碼會發生在兩個地方。比較直覺的就是標準回合的尾端,標記階段結束之後,但還有另外一個隱藏的可能性,是在棋盤階段完成之後,實際上已經沒有標記可以下(比方說沿途全是敵方英雄、己方殖民地、或已經到達 5 枚標記但同區塊內已經有升級一座殖民地),那也應該標記為完成。

在代理人接收到這個狀態碼的時候,它不需要進行後續的行動,因為這已經是一個換手的訊號。

    def play(self):
...
        if data[0:4] in (b'Ix01', b'Ix03'): # 如前述
...
        elif data[0] != ord('I'): # 如果不是 I 開頭,就表示是有錯誤發生。提供給隨機猴子容錯用的
            self.s.sendall(bytes([self.action]))
        else: # 其他狀況
            if data[0:4] in (b'Ix00', b'Ix02'): # 在原地踏步或標準回合結束時,都會換手給另一方的玩家
                pass

也給各位讀者看看伺服器端的轉換。它取得代理人決定的行動,並且使用 PathogenEngine 提供的 action 系列函式庫呼叫之後,取得這些狀態碼,並且知道標準回合結束,如(一樣是在handle_client當中的片段)

...
                        if stream.update_agent(g, &am.action, &fc, &s) == false {
                            // 如果更新資訊給客戶端出了任何問題,這裡就回傳 `false` 代表連線將斷開
                            return false;
                        } else {
                            // 連線持續時
                            if s.as_bytes()[0] == b'E' {
                                // 有錯誤的情況,這一樣是隨機猴子容錯用的。伺服器端寬容地接受錯誤的著手,並要求重下。
                                // 在這系列文後期,已經很少有這種需求了,畢竟那很浪費時間。
                                continue 'set_marker;
                            } else if s == "Ix02" {
                                // 離開這個迴圈
                                break 'set_marker;
                            }
...
                } // 名為 'set_marker 迴圈的結束區

                // commit the action to the game
                assert_eq!(am.action.action_phase, ActionPhase::Done);
                next(g, &am.action);
...
            // 結束這一次的 handle_client 處理,回到主迴圈
            return true;

三種結束碼 Ix04Ix05,以及 Ix06

承上,觀察 handle_client回到主迴圈的狀況是

    while let Phase::Main(x) = g.phase {
        let turn: usize = x.try_into().unwrap();
        // 這就是對應到剛才的片段的 handle_client 的呼叫,turn 值作為索引在 0 和 1 之間跳動
        // 類似伺服器全權管理著誰可以和棋盤互動。
        // 先前如果任何地方回傳 false,就會被這裡解讀成 Ix06 的斷線狀態
        if !handle_client(&mut s[turn % 2], &mut g) {
            s[turn % 2].update_agent(&g, &ea, &ec, &"Ix06");
            s[1 - turn % 2].update_agent(&g, &ea, &ec, &"Ix06");
            drop(s);
            break;
        }
        // 如果遊戲結束了,那分別傳送勝負結果。
        if g.is_ended() {
            s[turn % 2].update_agent(&g, &ea, &ec, &"Ix04");
            s[1 - turn % 2].update_agent(&g, &ea, &ec, &"Ix05");
            break;
        }
    }

Ix06 在這個版本的伺服器是結束碼,讓還活著的代理人能夠接收到遊戲斷線的消息。然而這個機制如同第八天提到的,已經修改成斷線就由隨機步數生成的機制頂上持續對局。

其他,Ix04 是通知勝利,Ix05 是通知敗局。如同先前在目前狀態小節當中持續更新的那樣,我讓醫療方的代理人會回報自己的勝率,就是透過這個機制來統計的,如 examples/coord_clients/main.py 當中,

...
        if args.side == 'Doctor' and a.result == b'Ix04':
            # 若這是代表醫療方的代理人且獲得了勝利的終局碼
            doctor_wins = doctor_wins + 1

    # 離開遊戲迴圈之後...
    # 離開程式之前,做個總結
    if args.side == 'Doctor':
        print(f"{doctor_wins/args.batch*100:6.2f} %", file=sys.stderr)

之後,在蒙地卡羅實作時,也發現了有更多不同狀態的需求。屆時再與大家分享相關部份。

目前狀況

延續昨天,我可以完全使用強化版的隨機對局,快速地生成殘局譜資料集。但是有一個過於貪心的嘗試,也是蠻好笑的。我在五分鐘左右的時間生成超過八十萬局疫途對戰資料。第一個貪心之處在於收集這些譜做成資料集。由於目前殘局譜生成器的寫法,是一直呼叫 write_all,且一直都沒有關閉檔案或是釋放記憶體,以至於原來這個程式在我的筆電上(16GB RAM)沒辦法直接吃下這八十萬局。大概超過 50% 就會觸發 OOM。

其實 OOM 的時候,我們可能會想像 kernel 氣定神閒地叫起 oom killer 進行作業,但在那種臨界狀態,許多不對勁的事情都會發生,我甚至看到工作目錄下多了一個文字檔,不得不說有點恐怖。

之後刻意收集裡面的短手數對局,也就是疫病方在 7、9、11、13、15 手時獲勝,以及醫療方 8、10、12、14、16 手時獲勝的最後一手的一手詰殘局譜。這樣收集起來也有數十萬筆,但這次踢到了鐵板,直接照著這個集合訓練,表現也沒有變好!

目前是不太妙的狀態。不但大致上拋棄了 AlphaZero 的精神(而且是系列文還來不及講到我為了實作 AlphaZero 花了哪些力氣),而且連下一步的方向,怎麼延續先前的模型的些微長進,持續提煉出棋感,目前還在實驗碰撞中。


上一篇
狀態碼 (1/2 ),以及原地踏步
下一篇
導入單元測試
系列文
DeltaPathogen:國產雙人不對稱抽象棋「疫途」之桌遊 AI 實戰30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言