此为《算法的乐趣》读书笔记,我用javascript(ES6)重新实现算法。
华容道游戏看似简单,但求解需要设计的数据结构比较复杂,还牵涉到棋类游戏的棋局判断,所以整个过程还是挺费劲的。我尽量用面向对象的思想来进行封装,整个过程将分成几个部分记录下来,今天是第二部分,棋局处理Zobrist算法原理及实现。
Zobrist算法原理
Zobrist哈希算法是一种适用于棋类游戏的棋局编码方式,通过建立一个特殊的转换表,对棋盘上每一个位置的所有可能状态赋予一个绝不重复的随机编码,通过对不同位置上的随机编码进行异或计算,实现在极低冲突率的前提下将复杂的棋局编码为一个整数类型哈希值的功能。
Zobrist哈希算法步骤:
- 识别出棋局的最小单位(格子或交叉点),确定每个最小单位上的所有可能的状态数。以华容道的棋局为例,最小单位就是20个小格子,每个格子有五种状态,分别是空状态、被横长方形占据、被坚长方形占据、被小方格占据和被大方格占据。
- 为每个单位上的所有状态都分配一个随机的编码值。棋类游戏一般需要“行数×列数×状态数”个状态,以华容道为例,需要为5×4×5=100个状态分配编码值。
- 对指定的棋局,对每个单位上的状态用对应的编码值(随机数)做异或运算,最后得到一个哈希值。
Zobrist哈希算法优点:
- 冲突概率小,只要随机编码值的范围够大,棋局哈希冲突的概率非常小,实际应用中基本上不考虑冲突的情况。
- 棋局发生变化时,不必对整个棋局重新计算哈希值,只需要计算发生变化的那些最小单元的状态变化即可。
Zobrist算法实现
编码表定义
编码表定义为一个三维数组。
class Zobrist{
constructor(){ //三维表属性
this.zobHash = []
for(let i = 0; i < HRD_GAME_ROW; i++) //初始化
{
this.zobHash.push([])
for(let j = 0; j < HRD_GAME_COL; j++)
{
this.zobHash[i].push([])
for(let k = 0; k < MAX_WARRIOR_TYPE; k++)
{
do{
var tmp = Math.random()
tmp = Math.floor(tmp * Math.pow(2,15)) //对16位随机整数值
}while(!tmp) //跳过零值
this.zobHash[i][j].push(tmp)
}
}
}
}
get(i,j,k){ //get接口
return this.zobHash[i][j][k]
}
}
计算棋局Zobrist哈希值
对棋盘的格子逐个处理,根据棋盘格子的武将信息获取武将的类型,从而获取该类型对应的编码值,用此编码值参与哈希值进行异或运算。
function getZobristHash(zobHash, state)
{
let hash = 0;
let heroes = state.heroes;
for(let i = 1; i <= HRD_GAME_ROW; i++)
{
for(let j = 1; j <= HRD_GAME_COL; j++)
{
let index = state.board[i][j] - 1; //取得格子上武将序号
let type = (index >= 0 && index < heroes.length) ? heroes[index].type : 0; //数组索引值超出范围,定为零
hash ^= zobHash.get(i - 1,j - 1,type); //异或计算
// console.log(index+'--'+type+'--'+zobHash[i - 1][j - 1][type]+'<=>'+hash)
}
}
return hash;
}
取镜像Zobrist哈希值
棋盘状态左右镜像问题:两个棋局虽然武将的位置不一样,但是如果忽略武将的名字信息,单纯从形状上看是左右对称的镜像结构。对于华容道游戏来说,这种左右镜像的情况对于滑动棋子寻求结果的影响是一样的。
镜像即左右对称,进行一个坐标变换即可得到。
function getMirrorZobristHash(zobHash, state)
{
let hash = 0;
let heroes = state.heroes;
for(let i = 1; i <= HRD_GAME_ROW; i++)
{
for(let j = 1; j <= HRD_GAME_COL; j++)
{
let index = state.board[i][j] - 1;
let type = (index >= 0 && index < heroes.length) ? heroes[index].type : 0;
//(HRD_GAME_COL - 1) - (j - 1)) 坐标变换
hash ^= zobHash.get(i - 1,HRD_GAME_COL - j,type);
}
}
return hash;
}
程序测试
设计了三个棋局,测试目标:同一个棋局的Zobrist哈希与镜像哈希相等,镜像棋局的Zobrist哈希与其镜像哈希相等。
var zobHash = new Zobrist()
var gameState = new HrdGameState()
var gameStateL = new HrdGameState()
var gameStateR = new HrdGameState()
var hs = [new Warrior(WARRIOR_TYPE.HT_VBAR,0,0),
new Warrior(WARRIOR_TYPE.HT_BOX,1,0),
new Warrior(WARRIOR_TYPE.HT_VBAR,3,0),
new Warrior(WARRIOR_TYPE.HT_VBAR,0,2),
new Warrior(WARRIOR_TYPE.HT_HBAR,1,2),
new Warrior(WARRIOR_TYPE.HT_VBAR,3,2),
new Warrior(WARRIOR_TYPE.HT_BLOCK,0,4),
new Warrior(WARRIOR_TYPE.HT_BLOCK,1,3),
new Warrior(WARRIOR_TYPE.HT_BLOCK,2,3),
new Warrior(WARRIOR_TYPE.HT_BLOCK,3,4)
]
var hsl = [ new Warrior(WARRIOR_TYPE.HT_BOX,0,0),
new Warrior(WARRIOR_TYPE.HT_VBAR,2,0),
new Warrior(WARRIOR_TYPE.HT_VBAR,3,0),
new Warrior(WARRIOR_TYPE.HT_HBAR,0,2),
new Warrior(WARRIOR_TYPE.HT_BLOCK,2,2),
new Warrior(WARRIOR_TYPE.HT_VBAR,3,2),
new Warrior(WARRIOR_TYPE.HT_VBAR,0,3),
new Warrior(WARRIOR_TYPE.HT_BLOCK,1,3),
new Warrior(WARRIOR_TYPE.HT_BLOCK,1,4),
new Warrior(WARRIOR_TYPE.HT_BLOCK,3,4)
]
var hsr = [ new Warrior(WARRIOR_TYPE.HT_VBAR,0,0),
new Warrior(WARRIOR_TYPE.HT_VBAR,1,0),
new Warrior(WARRIOR_TYPE.HT_BOX,2,0),
new Warrior(WARRIOR_TYPE.HT_VBAR,0,2),
new Warrior(WARRIOR_TYPE.HT_BLOCK,1,2),
new Warrior(WARRIOR_TYPE.HT_HBAR,2,2),
new Warrior(WARRIOR_TYPE.HT_BLOCK,2,3),
new Warrior(WARRIOR_TYPE.HT_VBAR,3,3),
new Warrior(WARRIOR_TYPE.HT_BLOCK,0,4),
new Warrior(WARRIOR_TYPE.HT_BLOCK,2,4)
]
gameState.initState(hs)
gameStateL.initState(hsl)
gameStateR.initState(hsr)
console.dir(getZobristHash(zobHash,gameStateL) + '--' + getMirrorZobristHash(zobHash,gameStateR)) //两值相等
完整代码
代码托管在开源中国,其中的hyd.js即华容道解法。
https://gitee.com/zhoutk/test
小结
尽量使用面向对象的思想来解决问题,让数据和操作绑定在一起,努力使代码容易看懂。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。