现在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分析考察程序的关键部分,并加以改进。程序下了几百盘棋以后,所有的统计数字就都有了,对你的代码修改一些地方(搜索算法,评价函数等等),然后再打很多比赛来确认你改得是否有效。

0

本文为原创文章,转载请注明出处,欢迎访问作者网站(和而不同)

发表评论

error: Content is protected !!
blank