... 也不太清楚接下來會發生什麼事情,也很少能夠清晰地預見當行之道。
我想,就趁這個時候好好地回首來時路吧。
這樣做的話,無論是怎樣的道路,都能夠真正的看清楚。-- <在困惑中逐漸變強>,羽生善治著,拙譯(目前尚無官方中文譯本)
昨日只分享幾個狀態碼讓讀者有個感覺,但它可以說是整個遊戲系統端與代理人端交談的重要資訊。有別於昨天的錯誤狀態範例,我今天會展示正確的狀態如何引導代理人行動。這對於之後的 AlphaZero 演算法實作也意外的扮演了一個基礎,這部份我們會在這週後續的系列文就看到。
我大致跟隨電腦科學裡面狀態機的概念。
Ix00
原地踏步昨天已經詳述,算是一個非常特殊的狀態。
Ix03
換你下囉!今天的文章之中的 github 連結都是對應到六月左右的狀態,因為之後開始實作蒙地卡羅,就變得比較複雜了。
在主要的遊戲系統迴圈之中,在 handle_client
的起頭之處,伺服器會用這個狀態碼跟對象代理人打招呼。
Ix01
嗯,你做了一個合乎規則的決定,請繼續可以在 src/core/action.rs
當中看到,每個行動的正確結束的時候,幾乎都是使用這個狀態碼回傳結果。如add_lockdown_by_coord
、add_character
。
至於代理人程式碼中對應的部份,目前為止都沒有發現 Ix01
、Ix03
有什麼特別不同,所以他們也通常是被一起處理。如 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]))
)。這一段的邏輯完全是可以共用於 Ix01
、Ix03
這兩個起手狀態的。
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;
Ix04
,Ix05
,以及 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 花了哪些力氣),而且連下一步的方向,怎麼延續先前的模型的些微長進,持續提煉出棋感,目前還在實驗碰撞中。