当我们有了置换表之后,就可以用多种方式来优化走法顺序。

优化走法顺序

在之前我们只用历史表作优化,从现在开始采用多种优化方式:

(1) 如果置换表中有过该局面的数据,但无法完全利用,那么多数情况下它是浅一层搜索中产生截断的走法,我们可以首先尝试它。

(2) 然后是两个杀手走法(如果其中某个杀手走法与置换表走法一样,那么可以跳过)。

(3) 然后生成全部走法,按历史表排序,再依次搜索(可以排除置换表走法和两个杀手走法)。

define.go

//走法排序阶段
const (
	PhaseHash     = 0
	PhaseKiller1  = 1
	PhaseKiller2  = 2
	PhaseGenMoves = 3
	PhaseRest     = 4
)

SortStruct结构体

打开rule.go,增加:

//SortStruct 走法排序结构
type SortStruct struct {
	mvHash    int   //置换表走法
	mvKiller1 int   //杀手走法
	mvKiller2 int   //杀手走法
	nPhase    int   //当前阶段
	nIndex    int   // 当前采用第几个走法
	nGenMoves int   // 总共有几个走法
	mvs       []int // 所有的走法
}

PositionStruct方法

增加initSort和nextSort方法:

//initSort 初始化,设定置换表走法和两个杀手走法
func (p *PositionStruct) initSort(mvHash int, s *SortStruct) {
	if s == nil {
		return
	}

	s.mvHash = mvHash
	s.mvKiller1 = p.search.mvKillers[p.nDistance][0]
	s.mvKiller2 = p.search.mvKillers[p.nDistance][1]
	s.nPhase = PhaseHash
}

//nextSort 得到下一个走法
func (p *PositionStruct) nextSort(s *SortStruct) int {
	if s == nil {
		return 0
	}

	switch s.nPhase {
	case PhaseHash:
		//置换表着法启发,完成后立即进入下一阶段;
		s.nPhase = PhaseKiller1
		if s.mvHash != 0 {
			return s.mvHash
		}
		fallthrough
	case PhaseKiller1:
		//杀手着法启发(第一个杀手着法),完成后立即进入下一阶段;
		s.nPhase = PhaseKiller2
		if s.mvKiller1 != s.mvHash && s.mvKiller1 != 0 && p.legalMove(s.mvKiller1) {
			return s.mvKiller1
		}
		fallthrough
	case PhaseKiller2:
		//杀手着法启发(第二个杀手着法),完成后立即进入下一阶段;
		s.nPhase = PhaseGenMoves
		if s.mvKiller2 != s.mvHash && s.mvKiller2 != 0 && p.legalMove(s.mvKiller2) {
			return s.mvKiller2
		}
		fallthrough
	case PhaseGenMoves:
		//生成所有着法,完成后立即进入下一阶段;
		s.nPhase = PhaseRest
		s.nGenMoves = p.generateMoves(s.mvs, false)
		s.mvs = s.mvs[:s.nGenMoves]
		sort.Slice(s.mvs, func(a, b int) bool {
			return p.search.nHistoryTable[a] > p.search.nHistoryTable[b]
		})
		s.nIndex = 0
		fallthrough
	case PhaseRest:
		//对剩余着法做历史表启发;
		for s.nIndex < s.nGenMoves {
			mv := s.mvs[s.nIndex]
			s.nIndex++
			if mv != s.mvHash && mv != s.mvKiller1 && mv != s.mvKiller2 {
				return mv
			}
		}
	default:
		// 5. 没有着法了,返回零。
	}

	return 0
}

//setBestMove 对最佳走法的处理
func (p *PositionStruct) setBestMove(mv, nDepth int) {
	p.search.nHistoryTable[mv] += nDepth * nDepth
	if p.search.mvKillers[p.nDistance][0] != mv {
		p.search.mvKillers[p.nDistance][1] = p.search.mvKillers[p.nDistance][0]
		p.search.mvKillers[p.nDistance][0] = mv
	}
}

修改NewPositionStruct,增加hashTable初始化:

//NewPositionStruct 初始化棋局
func NewPositionStruct() *PositionStruct {
	p := &PositionStruct{
		zobr: &ZobristStruct{
			dwKey:   0,
			dwLock0: 0,
			dwLock1: 0,
		},
		zobrist: &Zobrist{
			Player: &ZobristStruct{
				dwKey:   0,
				dwLock0: 0,
				dwLock1: 0,
			},
		},
		search: &Search{},
	}
	if p == nil {
		return nil
	}

	for i := 0; i < MaxMoves; i++ {
		tmpMoveStruct := &MoveStruct{}
		p.mvsList[i] = tmpMoveStruct
	}

	for i := 0; i < HashSize; i++ {
		p.search.hashTable[i] = &HashItem{}
	}

	p.zobrist.initZobrist()
	return p
}

searchFull

修改searchFull,使用nextSort来返回走法列表:

//searchFull 超出边界(Fail-Soft)的Alpha-Beta搜索过程
func (p *PositionStruct) searchFull(vlAlpha, vlBeta, nDepth int, bNoNull bool) int {
	vl, mvHash := 0, 0

	if p.nDistance > 0 {
		//到达水平线,则调用静态搜索(注意:由于空步裁剪,深度可能小于零)
		if nDepth <= 0 {
			return p.searchQuiesc(vlAlpha, vlBeta)
		}

		//检查重复局面(注意:不要在根节点检查,否则就没有走法了)
		vl = p.repStatus(1)
		if vl != 0 {
			return p.repValue(vl)
		}

		//到达极限深度就返回局面评价
		if p.nDistance == LimitDepth {
			return p.evaluate()
		}

		//尝试置换表裁剪,并得到置换表走法
		vl, mvHash = p.probeHash(vlAlpha, vlBeta, nDepth)
		if vl > -MateValue {
			return vl
		}

		//尝试空步裁剪(根节点的Beta值是"MateValue",所以不可能发生空步裁剪)
		if !bNoNull && !p.inCheck() && p.nullOkay() {
			p.nullMove()
			vl = -p.searchFull(-vlBeta, 1-vlBeta, nDepth-NullDepth-1, true)
			p.undoNullMove()
			if vl >= vlBeta {
				return vl
			}
		}
	} else {
		mvHash = 0
	}

	//初始化最佳值和最佳走法
	nHashFlag := HashAlpha
	//是否一个走法都没走过(杀棋)
	vlBest := -MateValue
	//是否搜索到了Beta走法或PV走法,以便保存到历史表
	mvBest := 0

	//初始化走法排序结构
	tmpSort := &SortStruct{
		mvs: make([]int, MaxGenMoves),
	}
	p.initSort(mvHash, tmpSort)

	//逐一走这些走法,并进行递归
	for mv := p.nextSort(tmpSort); mv != 0; mv = p.nextSort(tmpSort) {
		if p.makeMove(mv) {
			// 将军延伸
			if p.inCheck() {
				vl = -p.searchFull(-vlBeta, -vlAlpha, nDepth, false)
			} else {
				vl = -p.searchFull(-vlBeta, -vlAlpha, nDepth-1, false)
			}
			p.undoMakeMove()

			//进行Alpha-Beta大小判断和截断
			if vl > vlBest {
				//找到最佳值(但不能确定是Alpha、PV还是Beta走法)
				vlBest = vl
				//vlBest就是目前要返回的最佳值,可能超出Alpha-Beta边界
				if vl >= vlBeta {
					//找到一个Beta走法, Beta走法要保存到历史表, 然后截断
					nHashFlag = HashBeta
					mvBest = mv
					break
				}
				if vl > vlAlpha {
					//找到一个PV走法,PV走法要保存到历史表,缩小Alpha-Beta边界
					nHashFlag = HashPV
					mvBest = mv
					vlAlpha = vl
				}
			}
		}
	}

	//所有走法都搜索完了,把最佳走法(不能是Alpha走法)保存到历史表,返回最佳值
	if vlBest == -MateValue {
		//如果是杀棋,就根据杀棋步数给出评价
		return p.nDistance - MateValue
	}
	//记录到置换表
	p.RecordHash(nHashFlag, vlBest, nDepth, mvBest)
	if mvBest != 0 {
		p.setBestMove(mvBest, nDepth)
		if p.nDistance == 0 {
			//如果不是Alpha走法,就将最佳走法保存到历史表
			p.search.mvResult = mvBest
		}
	}
	return vlBest
}

searchMain

修改searchMain,增加对置换表和杀手走法表的清空处理:

//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

	// 迭代加深过程
	vl := 0
	rand.Seed(time.Now().UnixNano())
	for i := 1; i <= LimitDepth; i++ {
		vl = p.searchFull(-MateValue, MateValue, i, false)
		// 搜索到杀棋,就终止搜索
		if vl > WinValue || vl < -WinValue {
			break
		}
		// 超过一秒,就终止搜索
		if time.Now().Sub(start).Milliseconds() > 1000 {
			break
		}
	}
}

运行程序,会发现AI的下棋速度提升了很多,因为AI不需要每次都生成全部的走法。

如果小伙伴们想学习更多这方面知识,可以访问http://www.xqbase.com/computer/stepbystep5.htm

在下一节中,我们将学习如何使用开局库?

0

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

发表评论

error: Content is protected !!