用Go写一个中国象棋(十八)| 开局库
现在AI一开局不会总是跳正马了,根据开局库,它大部分时候走中炮,有时也走仙人指路(进兵)或飞相。
searchRoot
可是当它脱离开局库时,仍然摆脱不了思维的单一性,例如我们第一步走边兵,它仍旧只会跳同一边的正马。
解决办法是在根节点处,让一个不是最好的走法也能在一定的几率取代前一个走法:
//searchRoot 根节点的Alpha-Beta搜索过程
func (p *PositionStruct) searchRoot(nDepth int) int {
vl, nNewDepth := 0, 0
vlBest := -MateValue
//初始化走法排序结构
tmpSort := &SortStruct{
mvs: make([]int, MaxGenMoves),
}
p.initSort(p.search.mvResult, tmpSort)
//逐一走这些走法,并进行递归
for mv := p.nextSort(tmpSort); mv != 0; mv = p.nextSort(tmpSort) {
if p.makeMove(mv) {
if p.inCheck() {
nNewDepth = nDepth
} else {
nNewDepth = nDepth - 1
}
if vlBest == -MateValue {
vl = -p.searchFull(-MateValue, MateValue, nNewDepth, true)
} else {
vl = -p.searchFull(-vlBest-1, -vlBest, nNewDepth, false)
if vl > vlBest {
vl = -p.searchFull(-MateValue, -vlBest, nNewDepth, true)
}
}
p.undoMakeMove()
if vl > vlBest {
vlBest = vl
p.search.mvResult = mv
if vlBest > -WinValue && vlBest < WinValue {
vlBest += int(rand.Int31()&RandomMask) - int(rand.Int31()&RandomMask)
}
}
}
}
p.RecordHash(HashPV, vlBest, nDepth, p.search.mvResult)
p.setBestMove(p.search.mvResult, nDepth)
return vlBest
}
长将判负策略
由于单方面长将不变作负的规则,以前如果发生这种情况,直接给予-MateValue的值,再根据杀棋步数作调整。但是由于长将判负并不是对某个单纯局面的评分,而是跟路线有关的,所以使用置换表时就会产生非常严重的后果——某个局面的信息可能来自另一条不同的路线。
解决办法就是:获取置换表时把“利用长将判负策略搜索到的局面”过滤掉。为此把长将判负的局面定为BanValue(MateValue- 100),如果某个局面分值在WinValue(MateValue- 200)和BanValue之间,那么这个局面就是“利用长将判负策略搜索到的局面”。
我们仍旧把部分“利用长将判负策略搜索到的局面”记录到置换表,因为这些局面提供的最佳走法是有启发价值的。反过来说,如果“利用长将判负策略搜索到的局面”没有最佳走法,那么这种局面就没有必要记录到置换表了。
//drawValue 和棋分值
func (p *PositionStruct) drawValue() int {
if p.nDistance&1 == 0 {
return -DrawValue
}
return DrawValue
}
//repValue 重复局面分值
func (p *PositionStruct) repValue(nRepStatus int) int {
vlReturn := 0
if nRepStatus&2 != 0 {
vlReturn += p.nDistance - BanValue
}
if nRepStatus&4 != 0 {
vlReturn += BanValue - p.nDistance
}
if vlReturn == 0 {
return p.drawValue()
}
return vlReturn
}
//RecordHash 保存置换表项
func (p *PositionStruct) RecordHash(nFlag, vl, nDepth, mv int) {
hsh := p.search.hashTable[p.zobr.dwKey&(HashSize-1)]
if hsh.ucDepth > nDepth {
return
}
hsh.ucFlag = nFlag
hsh.ucDepth = nDepth
if vl > WinValue {
//可能导致搜索的不稳定性,并且没有最佳着法,立刻退出
if mv == 0 && vl <= BanValue {
return
}
hsh.svl = vl + p.nDistance
} else if vl < -WinValue {
if mv == 0 && vl >= -BanValue {
return //同上
}
hsh.svl = vl - p.nDistance
} else {
hsh.svl = vl
}
hsh.wmv = mv
hsh.dwLock0 = p.zobr.dwLock0
hsh.dwLock1 = p.zobr.dwLock1
p.search.hashTable[p.zobr.dwKey&(HashSize-1)] = hsh
}
//RecordHash 保存置换表项
func (p *PositionStruct) RecordHash(nFlag, vl, nDepth, mv int) {
hsh := p.search.hashTable[p.zobr.dwKey&(HashSize-1)]
if hsh.ucDepth > nDepth {
return
}
hsh.ucFlag = nFlag
hsh.ucDepth = nDepth
if vl > WinValue {
//可能导致搜索的不稳定性,并且没有最佳着法,立刻退出
if mv == 0 && vl <= BanValue {
return
}
hsh.svl = vl + p.nDistance
} else if vl < -WinValue {
if mv == 0 && vl >= -BanValue {
return //同上
}
hsh.svl = vl - p.nDistance
} else {
hsh.svl = vl
}
hsh.wmv = mv
hsh.dwLock0 = p.zobr.dwLock0
hsh.dwLock1 = p.zobr.dwLock1
p.search.hashTable[p.zobr.dwKey&(HashSize-1)] = hsh
}
//searchMain 迭代加深搜索过程
func (p *PositionStruct) searchMain() {
//清空历史表
for i := 0; i < 65536; i++ {
p.search.nHistoryTable[i] = 0
}
//清空杀手走法表
for i := 0; i < LimitDepth; i++ {
for j := 0; j < 2; j++ {
p.search.mvKillers[i][j] = 0
}
}
//清空置换表
for i := 0; i < HashSize; i++ {
p.search.hashTable[i].ucDepth = 0
p.search.hashTable[i].ucFlag = 0
p.search.hashTable[i].svl = 0
p.search.hashTable[i].wmv = 0
p.search.hashTable[i].wReserved = 0
p.search.hashTable[i].dwLock0 = 0
p.search.hashTable[i].dwLock1 = 0
}
//初始化定时器
start := time.Now()
//初始步数
p.nDistance = 0
//搜索开局库
p.search.mvResult = p.searchBook()
if p.search.mvResult != 0 {
p.makeMove(p.search.mvResult)
if p.repStatus(3) == 0 {
p.undoMakeMove()
return
}
p.undoMakeMove()
}
//检查是否只有唯一走法
vl := 0
mvs := make([]int, MaxGenMoves)
nGenMoves := p.generateMoves(mvs, false)
for i := 0; i < nGenMoves; i++ {
if p.makeMove(mvs[i]) {
p.undoMakeMove()
p.search.mvResult = mvs[i]
vl++
}
}
if vl == 1 {
return
}
//迭代加深过程
rand.Seed(time.Now().UnixNano())
for i := 1; i <= LimitDepth; i++ {
vl = p.searchRoot(i)
//搜索到杀棋,就终止搜索
if vl > WinValue || vl < -WinValue {
break
}
//超过一秒,就终止搜索
if time.Now().Sub(start).Milliseconds() > 1000 {
break
}
}
}
searchMain
搜索一个局面时,首先不做Alpha-Beta搜索,而是查找BookTable中有没有对应的项,有的话就直接返回一个走法。
//searchMain 迭代加深搜索过程
func (p *PositionStruct) searchMain() {
//清空历史表
for i := 0; i < 65536; i++ {
p.search.nHistoryTable[i] = 0
}
//清空杀手走法表
for i := 0; i < LimitDepth; i++ {
for j := 0; j < 2; j++ {
p.search.mvKillers[i][j] = 0
}
}
//清空置换表
for i := 0; i < HashSize; i++ {
p.search.hashTable[i].ucDepth = 0
p.search.hashTable[i].ucFlag = 0
p.search.hashTable[i].svl = 0
p.search.hashTable[i].wmv = 0
p.search.hashTable[i].wReserved = 0
p.search.hashTable[i].dwLock0 = 0
p.search.hashTable[i].dwLock1 = 0
}
//初始化定时器
start := time.Now()
//初始步数
p.nDistance = 0
//搜索开局库
p.search.mvResult = p.searchBook()
if p.search.mvResult != 0 {
p.makeMove(p.search.mvResult)
if p.repStatus(3) == 0 {
p.undoMakeMove()
return
}
p.undoMakeMove()
}
//检查是否只有唯一走法
vl := 0
mvs := make([]int, MaxGenMoves)
nGenMoves := p.generateMoves(mvs, false)
for i := 0; i < nGenMoves; i++ {
if p.makeMove(mvs[i]) {
p.undoMakeMove()
p.search.mvResult = mvs[i]
vl++
}
}
if vl == 1 {
return
}
//迭代加深过程
rand.Seed(time.Now().UnixNano())
for i := 1; i <= LimitDepth; i++ {
vl = p.searchRoot(i)
//搜索到杀棋,就终止搜索
if vl > WinValue || vl < -WinValue {
break
}
//超过一秒,就终止搜索
if time.Now().Sub(start).Milliseconds() > 1000 {
break
}
}
}
如果小伙伴们想学习更多这方面的知识,可以访问http://www.xqbase.com/computer/stepbystep6.htm
最后感言
写到这里,中国象棋程序就全部完成了。
你可以用go pprof分析考察程序的关键部分,并加以改进。程序下了几百盘棋以后,所有的统计数字就都有了,对你的代码修改一些地方(搜索算法,评价函数等等),然后再打很多比赛来确认你改得是否有效。
有bug,github已经提交issue
看到了,一起学习
你好,请问你联系方式多少?我些问题请教一下