關於本博客
這個博客不是把五子棋算法研究透徹之後再寫的,而是一邊研究算法一邊寫代碼,同時一邊寫博客,所以有些博文的順序不太對,比如 Zobrist 其實應該放在算殺之前就講的。不過這並沒有大的影響,總體上的順序是OK的。
另外,這一系列博客講的五子棋代碼其實是一個開源的項目,源碼地址: https://github.com/lihongxun945/gobang
由於是邊寫代碼邊寫博客,所以博客中的代碼不是最新的,甚至是有bug的,所以源碼請儘量參考上述開源項目中的代碼。比如之前講極大極小值搜索改爲負極大值的時候,對玩家的評分就出現了一個重要bug,在後序的提交中修正了這個bug。
Zobrist
Zobrist 是一個快速Hash算法,非常適合用在各種棋類遊戲中(事實上也是在各種棋類遊戲中有大量應用)。
我們前面講了負極大值搜索和算殺,其實很多時候會有重複的搜索,比如這種:
[7,7],[8,7],[7,6],[7,9]
其實它和下面這種的走法只是順序不同 ,最終走出來的局面是一樣的:
[7,6],[7,9],[7,7],[8,7]
那麼如果我們搜索中碰到了上面兩種情況,我們會對兩種情況都進行一次打分,而其實有了第一次的打分,完全可以緩存起來,第二次就不用打分直接使用緩存數據了。除了這種情況,其實以前的搜索結果也可以存下來,可以用在啓發式搜索中。
那麼現在的問題就是,我們應該怎麼表示一種局面呢?顯然需要通過一種哈希算法,而且這個算法不能太慢,不然可能反而會降低搜索速度。而 Zobrist 就是一種滿足我們需求的快速數組哈希算法。關於Zobrist算法請參考 https://en.wikipedia.org/wiki/Ben_Zobrist
Zobrist 效率非常高,每下一步棋,只需要進行一次 異或
操作,相對於對每一步棋的打分來說,這一次異或操作帶來的性能消耗可以忽略不計。Zobrist具體實現如下:
- 初始化一個兩個
Zobrist[M][M]
的二維數組,其中M是五子棋的棋盤寬度。當然也可以是Zobrist[M*M]
的一維數組。設置兩個是爲了一個表示黑棋,一個表示白旗。 - 上述數組的每一個都填上一個隨機數,至少保證是32位的長度(即32bit),最好是64位。初始鍵值也設置一個隨機數。
- 每下一步棋,則用當前鍵值異或Zobrist數組裏對應位置的隨機數,得到的結果即爲新的鍵值。如果是刪除棋子(悔棋),則再異或一次即可。
對應的JS代碼如下:
var Zobrist = function(size) {
this.size = size || 15;
}
Zobrist.prototype.init = function() {
this.com = [];
this.hum = [];
for(var i=0;i<this.size*this.size;i++) {
this.com.push(this._rand());
this.hum.push(this._rand());
}
this.code = this._rand();
}
Zobrist.prototype._rand = function() {
return Math.floor(Math.random() * 1000000000); //再多一位就溢出了。。
}
Zobrist.prototype.go = function(x, y, role) {
var index = this.size * x + y;
this.code ^= (role == R.com ? this.com[index] : this.hum[index]);
return this.code;
}
源碼在 zobrist.js
文件裏。
注意每次走棋都要進行一次zobrist操作。千萬不要自行設計哈希函數,除非你能保證你的哈希函數比一次64位整數的異或操作更簡單,並且同時證明衝突的概率很低。Zobrist數組中的隨機數的 質量
很重要,不過我用JS內置的 Math.random()
生成的隨機數暫時沒有發現問題,如果這個隨機度不夠高,可以考慮換用一些更好的隨機函數。
有了這個快速hash算法,我們就可以通過一個64位的整數來表示一個棋局。至於該存哪些信息,該怎麼使用,下一篇再講。