<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>中国象棋 &#8211; wqh博客</title>
	<atom:link href="https://wangqianhong.com/tag/%E4%B8%AD%E5%9B%BD%E8%B1%A1%E6%A3%8B/feed/" rel="self" type="application/rss+xml" />
	<link>https://wangqianhong.com</link>
	<description>和而不同</description>
	<lastBuildDate>Fri, 04 Dec 2020 08:47:08 +0000</lastBuildDate>
	<language>zh-CN</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
	

<image>
	<url>https://wangqianhong.com/wp-content/uploads/2020/09/cropped-1-1-1-32x32.png</url>
	<title>中国象棋 &#8211; wqh博客</title>
	<link>https://wangqianhong.com</link>
	<width>32</width>
	<height>32</height>
</image> 
	<item>
		<title>用Go写一个中国象棋（十八）&#124; 开局库</title>
		<link>https://wangqianhong.com/2020/12/%e7%94%a8go%e5%86%99%e4%b8%80%e4%b8%aa%e4%b8%ad%e5%9b%bd%e8%b1%a1%e6%a3%8b%ef%bc%88%e5%8d%81%e5%85%ab%ef%bc%89-%e5%bc%80%e5%b1%80%e5%ba%93/</link>
					<comments>https://wangqianhong.com/2020/12/%e7%94%a8go%e5%86%99%e4%b8%80%e4%b8%aa%e4%b8%ad%e5%9b%bd%e8%b1%a1%e6%a3%8b%ef%bc%88%e5%8d%81%e5%85%ab%ef%bc%89-%e5%bc%80%e5%b1%80%e5%ba%93/#comments</comments>
		
		<dc:creator><![CDATA[wqh_work]]></dc:creator>
		<pubDate>Fri, 04 Dec 2020 08:22:01 +0000</pubDate>
				<category><![CDATA[技术文章]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[中国象棋]]></category>
		<guid isPermaLink="false">https://wangqianhong.com/?p=579</guid>

					<description><![CDATA[<p>现在AI一开局不会总是跳正马了，根据开局库，它大部分时候走中炮，有时也走仙人指路(进兵)或飞相。 s&#8230; <a href="https://wangqianhong.com/2020/12/%e7%94%a8go%e5%86%99%e4%b8%80%e4%b8%aa%e4%b8%ad%e5%9b%bd%e8%b1%a1%e6%a3%8b%ef%bc%88%e5%8d%81%e5%85%ab%ef%bc%89-%e5%bc%80%e5%b1%80%e5%ba%93/" class="more-link read-more" rel="bookmark">继续阅读 <span class="screen-reader-text">用Go写一个中国象棋（十八）&#124; 开局库</span><i class="fa fa-arrow-right"></i></a></p>
<p><a rel="nofollow" href="https://wangqianhong.com/2020/12/%e7%94%a8go%e5%86%99%e4%b8%80%e4%b8%aa%e4%b8%ad%e5%9b%bd%e8%b1%a1%e6%a3%8b%ef%bc%88%e5%8d%81%e5%85%ab%ef%bc%89-%e5%bc%80%e5%b1%80%e5%ba%93/">用Go写一个中国象棋（十八）| 开局库</a>最先出现在<a rel="nofollow" href="https://wangqianhong.com">wqh博客</a>。</p>
]]></description>
										<content:encoded><![CDATA[
<p>现在AI一开局不会总是跳正马了，根据开局库，它大部分时候走中炮，有时也走仙人指路(进兵)或飞相。</p>



<h3>searchRoot</h3>



<p>可是当它脱离开局库时，仍然摆脱不了思维的单一性，例如我们第一步走边兵，它仍旧只会跳同一边的正马。</p>



<p>解决办法是在根节点处，让一个不是最好的走法也能在一定的几率取代前一个走法：</p>



<pre class="wp-block-code"><code>//searchRoot 根节点的Alpha-Beta搜索过程
func (p *PositionStruct) searchRoot(nDepth int) int {
	vl, nNewDepth := 0, 0
	vlBest := -MateValue

	//初始化走法排序结构
	tmpSort := &amp;SortStruct{
		mvs: make(&#91;]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 &amp;&amp; vlBest &lt; WinValue {
					vlBest += int(rand.Int31()&amp;RandomMask) - int(rand.Int31()&amp;RandomMask)
				}
			}
		}
	}
	p.RecordHash(HashPV, vlBest, nDepth, p.search.mvResult)
	p.setBestMove(p.search.mvResult, nDepth)
	return vlBest
}</code></pre>



<h3>长将判负策略</h3>



<p>由于单方面长将不变作负的规则，以前如果发生这种情况，直接给予-MateValue的值，再根据杀棋步数作调整。但是由于长将判负并不是对某个单纯局面的评分，而是跟路线有关的，所以使用置换表时就会产生非常严重的后果——某个局面的信息可能来自另一条不同的路线。</p>



<p>解决办法就是：获取置换表时把“利用长将判负策略搜索到的局面”过滤掉。为此把长将判负的局面定为BanValue(MateValue- 100)，如果某个局面分值在WinValue(MateValue- 200)和BanValue之间，那么这个局面就是“利用长将判负策略搜索到的局面”。</p>



<p>我们仍旧把部分“利用长将判负策略搜索到的局面”记录到置换表，因为这些局面提供的最佳走法是有启发价值的。反过来说，如果“利用长将判负策略搜索到的局面”没有最佳走法，那么这种局面就没有必要记录到置换表了。</p>



<pre class="wp-block-code"><code>//drawValue 和棋分值
func (p *PositionStruct) drawValue() int {
	if p.nDistance&amp;1 == 0 {
		return -DrawValue
	}

	return DrawValue
}
</code></pre>



<pre class="wp-block-code"><code>//repValue 重复局面分值
func (p *PositionStruct) repValue(nRepStatus int) int {
	vlReturn := 0
	if nRepStatus&amp;2 != 0 {
		vlReturn += p.nDistance - BanValue
	}
	if nRepStatus&amp;4 != 0 {
		vlReturn += BanValue - p.nDistance
	}

	if vlReturn == 0 {
		return p.drawValue()
	}

	return vlReturn
}</code></pre>



<pre class="wp-block-code"><code>//RecordHash 保存置换表项
func (p *PositionStruct) RecordHash(nFlag, vl, nDepth, mv int) {
	hsh := p.search.hashTable&#91;p.zobr.dwKey&amp;(HashSize-1)]
	if hsh.ucDepth > nDepth {
		return
	}
	hsh.ucFlag = nFlag
	hsh.ucDepth = nDepth
	if vl > WinValue {
		//可能导致搜索的不稳定性，并且没有最佳着法，立刻退出
		if mv == 0 &amp;&amp; vl &lt;= BanValue {
			return
		}
		hsh.svl = vl + p.nDistance
	} else if vl &lt; -WinValue {
		if mv == 0 &amp;&amp; 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&#91;p.zobr.dwKey&amp;(HashSize-1)] = hsh
}</code></pre>



<pre class="wp-block-code"><code>//RecordHash 保存置换表项
func (p *PositionStruct) RecordHash(nFlag, vl, nDepth, mv int) {
	hsh := p.search.hashTable&#91;p.zobr.dwKey&amp;(HashSize-1)]
	if hsh.ucDepth > nDepth {
		return
	}
	hsh.ucFlag = nFlag
	hsh.ucDepth = nDepth
	if vl > WinValue {
		//可能导致搜索的不稳定性，并且没有最佳着法，立刻退出
		if mv == 0 &amp;&amp; vl &lt;= BanValue {
			return
		}
		hsh.svl = vl + p.nDistance
	} else if vl &lt; -WinValue {
		if mv == 0 &amp;&amp; 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&#91;p.zobr.dwKey&amp;(HashSize-1)] = hsh
}</code></pre>



<pre class="wp-block-code"><code>//searchMain 迭代加深搜索过程
func (p *PositionStruct) searchMain() {
	//清空历史表
	for i := 0; i &lt; 65536; i++ {
		p.search.nHistoryTable&#91;i] = 0
	}
	//清空杀手走法表
	for i := 0; i &lt; LimitDepth; i++ {
		for j := 0; j &lt; 2; j++ {
			p.search.mvKillers&#91;i]&#91;j] = 0
		}
	}
	//清空置换表
	for i := 0; i &lt; HashSize; i++ {
		p.search.hashTable&#91;i].ucDepth = 0
		p.search.hashTable&#91;i].ucFlag = 0
		p.search.hashTable&#91;i].svl = 0
		p.search.hashTable&#91;i].wmv = 0
		p.search.hashTable&#91;i].wReserved = 0
		p.search.hashTable&#91;i].dwLock0 = 0
		p.search.hashTable&#91;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(&#91;]int, MaxGenMoves)
	nGenMoves := p.generateMoves(mvs, false)
	for i := 0; i &lt; nGenMoves; i++ {
		if p.makeMove(mvs&#91;i]) {
			p.undoMakeMove()
			p.search.mvResult = mvs&#91;i]
			vl++
		}
	}
	if vl == 1 {
		return
	}

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



<h3>searchMain</h3>



<p>搜索一个局面时，首先不做Alpha-Beta搜索，而是查找BookTable中有没有对应的项，有的话就直接返回一个走法。</p>



<pre class="wp-block-code"><code>//searchMain 迭代加深搜索过程
func (p *PositionStruct) searchMain() {
	//清空历史表
	for i := 0; i &lt; 65536; i++ {
		p.search.nHistoryTable&#91;i] = 0
	}
	//清空杀手走法表
	for i := 0; i &lt; LimitDepth; i++ {
		for j := 0; j &lt; 2; j++ {
			p.search.mvKillers&#91;i]&#91;j] = 0
		}
	}
	//清空置换表
	for i := 0; i &lt; HashSize; i++ {
		p.search.hashTable&#91;i].ucDepth = 0
		p.search.hashTable&#91;i].ucFlag = 0
		p.search.hashTable&#91;i].svl = 0
		p.search.hashTable&#91;i].wmv = 0
		p.search.hashTable&#91;i].wReserved = 0
		p.search.hashTable&#91;i].dwLock0 = 0
		p.search.hashTable&#91;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(&#91;]int, MaxGenMoves)
	nGenMoves := p.generateMoves(mvs, false)
	for i := 0; i &lt; nGenMoves; i++ {
		if p.makeMove(mvs&#91;i]) {
			p.undoMakeMove()
			p.search.mvResult = mvs&#91;i]
			vl++
		}
	}
	if vl == 1 {
		return
	}

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



<p>如果小伙伴们想学习更多这方面的知识，可以访问<a href="http://www.xqbase.com/computer/stepbystep6.htm" target="_blank" rel="noreferrer noopener">http://www.xqbase.com/computer/stepbystep6.htm</a></p>



<h3>最后感言</h3>



<p>写到这里，中国象棋程序就全部完成了。</p>



<p>你可以用go pprof分析考察程序的关键部分，并加以改进。程序下了几百盘棋以后，所有的统计数字就都有了，对你的代码修改一些地方(搜索算法，评价函数等等)，然后再打很多比赛来确认你改得是否有效。</p>
<p><a rel="nofollow" href="https://wangqianhong.com/2020/12/%e7%94%a8go%e5%86%99%e4%b8%80%e4%b8%aa%e4%b8%ad%e5%9b%bd%e8%b1%a1%e6%a3%8b%ef%bc%88%e5%8d%81%e5%85%ab%ef%bc%89-%e5%bc%80%e5%b1%80%e5%ba%93/">用Go写一个中国象棋（十八）| 开局库</a>最先出现在<a rel="nofollow" href="https://wangqianhong.com">wqh博客</a>。</p>
]]></content:encoded>
					
					<wfw:commentRss>https://wangqianhong.com/2020/12/%e7%94%a8go%e5%86%99%e4%b8%80%e4%b8%aa%e4%b8%ad%e5%9b%bd%e8%b1%a1%e6%a3%8b%ef%bc%88%e5%8d%81%e5%85%ab%ef%bc%89-%e5%bc%80%e5%b1%80%e5%ba%93/feed/</wfw:commentRss>
			<slash:comments>3</slash:comments>
		
		
			</item>
		<item>
		<title>用Go写一个中国象棋（十七）&#124; 开局库</title>
		<link>https://wangqianhong.com/2020/11/%e7%94%a8go%e5%86%99%e4%b8%80%e4%b8%aa%e4%b8%ad%e5%9b%bd%e8%b1%a1%e6%a3%8b%ef%bc%88%e5%8d%81%e4%b8%83%ef%bc%89-%e5%bc%80%e5%b1%80%e5%ba%93/</link>
					<comments>https://wangqianhong.com/2020/11/%e7%94%a8go%e5%86%99%e4%b8%80%e4%b8%aa%e4%b8%ad%e5%9b%bd%e8%b1%a1%e6%a3%8b%ef%bc%88%e5%8d%81%e4%b8%83%ef%bc%89-%e5%bc%80%e5%b1%80%e5%ba%93/#respond</comments>
		
		<dc:creator><![CDATA[wqh_work]]></dc:creator>
		<pubDate>Mon, 30 Nov 2020 09:17:28 +0000</pubDate>
				<category><![CDATA[技术文章]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[中国象棋]]></category>
		<guid isPermaLink="false">https://wangqianhong.com/?p=566</guid>

					<description><![CDATA[<p>到目前为止，象棋AI基本上都完成了，但在和AI对弈的过程中会发现几个细节问题： (1)&#160;对&#8230; <a href="https://wangqianhong.com/2020/11/%e7%94%a8go%e5%86%99%e4%b8%80%e4%b8%aa%e4%b8%ad%e5%9b%bd%e8%b1%a1%e6%a3%8b%ef%bc%88%e5%8d%81%e4%b8%83%ef%bc%89-%e5%bc%80%e5%b1%80%e5%ba%93/" class="more-link read-more" rel="bookmark">继续阅读 <span class="screen-reader-text">用Go写一个中国象棋（十七）&#124; 开局库</span><i class="fa fa-arrow-right"></i></a></p>
<p><a rel="nofollow" href="https://wangqianhong.com/2020/11/%e7%94%a8go%e5%86%99%e4%b8%80%e4%b8%aa%e4%b8%ad%e5%9b%bd%e8%b1%a1%e6%a3%8b%ef%bc%88%e5%8d%81%e4%b8%83%ef%bc%89-%e5%bc%80%e5%b1%80%e5%ba%93/">用Go写一个中国象棋（十七）| 开局库</a>最先出现在<a rel="nofollow" href="https://wangqianhong.com">wqh博客</a>。</p>
]]></description>
										<content:encoded><![CDATA[
<p>到目前为止，象棋AI基本上都完成了，但在和AI对弈的过程中会发现几个细节问题：</p>



<p>(1)&nbsp;对于同一个局面，总是走固定的走法。</p>



<p>(2)&nbsp;搜索算法是否能更优化一些。</p>



<p>(3)&nbsp;有些杀棋局面会走出莫名其妙的走法。</p>



<h3>开局库</h3>



<p>开局库几乎是每个象棋程序必备的部件，它的好处是：</p>



<p>(1)&nbsp;即使再笨的程序，开局库能使得它们在开局阶段看上去不那么业余。</p>



<p>(2)&nbsp;通过随机选择走法，让开局灵活多变，增加对弈的趣味性。</p>



<p>打开define.go：</p>



<pre class="wp-block-code"><code>//BanValue 长将判负的分值，低于该值将不写入置换表
const BanValue = MateValue - 100
//WinValue 搜索出胜负的分值界限，超出此值就说明已经搜索出杀棋了
const WinValue = MateValue - 200
//RandomMask 随机性分值
const RandomMask = 7
//BookSize 开局库大小
const BookSize = 16384</code></pre>



<h3>BookItem</h3>



<p>打开rule.go，增加：</p>



<pre class="wp-block-code"><code>//BookItem 开局库项结构
type BookItem struct {
	dwLock uint32
	wmv    int
	wvl    int
}</code></pre>



<p>dwLock&nbsp;记录了局面&nbsp;Zobrist&nbsp;校验码中的&nbsp;dwLock1。</p>



<p>wmv&nbsp;是走法。</p>



<p>wvl&nbsp;是权重(随机选择走法的几率，仅当两个相同的&nbsp;dwLock&nbsp;有不同的&nbsp;wmv&nbsp;时，wvl&nbsp;的值才有意义)。</p>



<p>修改Search，增加开局库：</p>



<pre class="wp-block-code"><code>//Search 与搜索有关的全局变量
type Search struct {
	mvResult      int                 // 电脑走的棋
	nHistoryTable &#91;65536]int          // 历史表
	mvKillers     &#91;LimitDepth]&#91;2]int  // 杀手走法表
	hashTable     &#91;HashSize]*HashItem // 置换表
	BookTable     &#91;]*BookItem         // 开局库
}</code></pre>



<h3>开局库方法</h3>



<pre class="wp-block-code"><code>//loadBook 加载开局库
func (p *PositionStruct) loadBook() bool {
	file, err := os.Open("./res/book.dat")
	if err != nil {
		fmt.Print(err)
		return false
	}
	defer file.Close()
	reader := bufio.NewReader(file)
	if reader == nil {
		return false
	}

	for {
		line, _, err := reader.ReadLine()
		if err != nil {
			if err == io.EOF {
				break
			} else {
				fmt.Print(err)
				return false
			}
		}
		tmpLine := string(line)
		tmpResult := strings.Split(tmpLine, ",")
		if len(tmpResult) == 3 {
			tmpItem := &amp;BookItem{}
			tmpdwLock, err := strconv.ParseUint(tmpResult&#91;0], 10, 32)
			if err != nil {
				fmt.Print(err)
				continue
			}
			tmpItem.dwLock = uint32(tmpdwLock)
			tmpwmv, err := strconv.ParseInt(tmpResult&#91;1], 10, 32)
			if err != nil {
				fmt.Print(err)
				continue
			}
			tmpItem.wmv = int(tmpwmv)
			tmpwvl, err := strconv.ParseInt(tmpResult&#91;2], 10, 32)
			if err != nil {
				fmt.Print(err)
				continue
			}
			tmpItem.wvl = int(tmpwvl)

			p.search.BookTable = append(p.search.BookTable, tmpItem)
		}
	}
	return true
}
</code></pre>



<p>由于开局库是按照dwLock排序的，因此可以用二分查找。找到一项以后，把它前后dwLock相同的所有项都取出，从中随机选择一个wmv：</p>



<pre class="wp-block-code"><code>//searchBook 搜索开局库
func (p *PositionStruct) searchBook() int {
	bkToSearch := &amp;BookItem{}
	mvs := make(&#91;]int, MaxGenMoves)
	vls := make(&#91;]int, MaxGenMoves)

	bookSize := len(p.search.BookTable)
	//如果没有开局库，则立即返回
	if bookSize &lt;= 0 {
		return 0
	}

	//搜索当前局面
	bMirror := false
	bkToSearch.dwLock = p.zobr.dwLock1
	lpbk := sort.Search(bookSize, func(i int) bool {
		return p.search.BookTable&#91;i].dwLock >= bkToSearch.dwLock
	})

	//如果没有找到，那么搜索当前局面的镜像局面
	if lpbk == bookSize || (lpbk &lt; bookSize &amp;&amp; p.search.BookTable&#91;lpbk].dwLock != bkToSearch.dwLock) {
		bMirror = true
		posMirror := NewPositionStruct()
		p.mirror(posMirror)
		bkToSearch.dwLock = posMirror.zobr.dwLock1
		lpbk = sort.Search(bookSize, func(i int) bool {
			return p.search.BookTable&#91;i].dwLock >= bkToSearch.dwLock
		})
	}
	//如果镜像局面也没找到，则立即返回
	if lpbk == bookSize || (lpbk &lt; bookSize &amp;&amp; p.search.BookTable&#91;lpbk].dwLock != bkToSearch.dwLock) {
		return 0
	}
	//如果找到，则向前查第一个开局库项
	for lpbk >= 0 &amp;&amp; p.search.BookTable&#91;lpbk].dwLock == bkToSearch.dwLock {
		lpbk--
	}
	lpbk++
	//把走法和分值写入到"mvs"和"vls"数组中
	vl, nBookMoves, mv := 0, 0, 0
	for lpbk &lt; bookSize &amp;&amp; p.search.BookTable&#91;lpbk].dwLock == bkToSearch.dwLock {
		if bMirror {
			mv = mirrorMove(p.search.BookTable&#91;lpbk].wmv)
		} else {
			mv = p.search.BookTable&#91;lpbk].wmv
		}
		if p.legalMove(mv) {
			mvs&#91;nBookMoves] = mv
			vls&#91;nBookMoves] = p.search.BookTable&#91;lpbk].wvl
			vl += vls&#91;nBookMoves]
			nBookMoves++
			if nBookMoves == MaxGenMoves {
				// 防止"book.dat"中含有异常数据
				break
			}
		}
		lpbk++
	}
	if vl == 0 {
		// 防止"book.dat"中含有异常数据
		return 0
	}
	//根据权重随机选择一个走法
	vl = rand.Intn(vl)
	i := 0
	for i = 0; i &lt; nBookMoves; i++ {
		vl -= vls&#91;i]
		if vl &lt; 0 {
			break
		}
	}
	return mvs&#91;i]
}</code></pre>



<h3>&nbsp;mirror</h3>



<p>为了压缩开局库的容量，所有对称的局面只用一项，所以当一个局面在BookTable中找不到时，还应该试一下它的对称局面是否在BookTable中。</p>



<pre class="wp-block-code"><code>//mirror 对局面镜像
func (p *PositionStruct) mirror(posMirror *PositionStruct) {
	pc := 0
	posMirror.clearBoard()
	for sq := 0; sq &lt; 256; sq++ {
		pc = p.ucpcSquares&#91;sq]
		if pc != 0 {
			posMirror.addPiece(mirrorSquare(sq), pc)
		}
	}
	if p.sdPlayer == 1 {
		posMirror.changeSide()
	}
	posMirror.setIrrev()
}</code></pre>



<h3>加载开局库</h3>



<p>打开game.go，修改为：</p>



<pre class="wp-block-code"><code>//NewGame 创建象棋程序
func NewGame() bool {
	game := &amp;Game{
		images:         make(map&#91;int]*ebiten.Image),
		audios:         make(map&#91;int]*audio.Player),
		singlePosition: NewPositionStruct(),
	}
	if game == nil || game.singlePosition == nil {
		return false
	}

	var err error
	//音效器
	game.audioContext, err = audio.NewContext(48000)
	if err != nil {
		fmt.Print(err)
		return false
	}

	//加载资源
	if ok := game.loadResource(); !ok {
		return false
	}

	//加载开局库
	game.singlePosition.loadBook()
	game.singlePosition.startup()

	//设置窗口，接收信息
	ebiten.SetWindowSize(BoardWidth, BoardHeight)
	ebiten.SetWindowTitle("中国象棋")
	if err := ebiten.RunGame(game); err != nil {
		log.Fatal(err)
		return false
	}

	return true
}</code></pre>



<p>在下一节中，我们将学习如何如何优化算法？</p>
<p><a rel="nofollow" href="https://wangqianhong.com/2020/11/%e7%94%a8go%e5%86%99%e4%b8%80%e4%b8%aa%e4%b8%ad%e5%9b%bd%e8%b1%a1%e6%a3%8b%ef%bc%88%e5%8d%81%e4%b8%83%ef%bc%89-%e5%bc%80%e5%b1%80%e5%ba%93/">用Go写一个中国象棋（十七）| 开局库</a>最先出现在<a rel="nofollow" href="https://wangqianhong.com">wqh博客</a>。</p>
]]></content:encoded>
					
					<wfw:commentRss>https://wangqianhong.com/2020/11/%e7%94%a8go%e5%86%99%e4%b8%80%e4%b8%aa%e4%b8%ad%e5%9b%bd%e8%b1%a1%e6%a3%8b%ef%bc%88%e5%8d%81%e4%b8%83%ef%bc%89-%e5%bc%80%e5%b1%80%e5%ba%93/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>用Go写一个中国象棋（十六）&#124; 置换表</title>
		<link>https://wangqianhong.com/2020/11/%e7%94%a8go%e5%86%99%e4%b8%80%e4%b8%aa%e4%b8%ad%e5%9b%bd%e8%b1%a1%e6%a3%8b%ef%bc%88%e5%8d%81%e5%85%ad%ef%bc%89-%e7%bd%ae%e6%8d%a2%e8%a1%a8/</link>
					<comments>https://wangqianhong.com/2020/11/%e7%94%a8go%e5%86%99%e4%b8%80%e4%b8%aa%e4%b8%ad%e5%9b%bd%e8%b1%a1%e6%a3%8b%ef%bc%88%e5%8d%81%e5%85%ad%ef%bc%89-%e7%bd%ae%e6%8d%a2%e8%a1%a8/#respond</comments>
		
		<dc:creator><![CDATA[wqh_work]]></dc:creator>
		<pubDate>Thu, 26 Nov 2020 09:11:20 +0000</pubDate>
				<category><![CDATA[技术文章]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[中国象棋]]></category>
		<guid isPermaLink="false">https://wangqianhong.com/?p=556</guid>

					<description><![CDATA[<p>当我们有了置换表之后，就可以用多种方式来优化走法顺序。 优化走法顺序 在之前我们只用历史表作优化，从&#8230; <a href="https://wangqianhong.com/2020/11/%e7%94%a8go%e5%86%99%e4%b8%80%e4%b8%aa%e4%b8%ad%e5%9b%bd%e8%b1%a1%e6%a3%8b%ef%bc%88%e5%8d%81%e5%85%ad%ef%bc%89-%e7%bd%ae%e6%8d%a2%e8%a1%a8/" class="more-link read-more" rel="bookmark">继续阅读 <span class="screen-reader-text">用Go写一个中国象棋（十六）&#124; 置换表</span><i class="fa fa-arrow-right"></i></a></p>
<p><a rel="nofollow" href="https://wangqianhong.com/2020/11/%e7%94%a8go%e5%86%99%e4%b8%80%e4%b8%aa%e4%b8%ad%e5%9b%bd%e8%b1%a1%e6%a3%8b%ef%bc%88%e5%8d%81%e5%85%ad%ef%bc%89-%e7%bd%ae%e6%8d%a2%e8%a1%a8/">用Go写一个中国象棋（十六）| 置换表</a>最先出现在<a rel="nofollow" href="https://wangqianhong.com">wqh博客</a>。</p>
]]></description>
										<content:encoded><![CDATA[
<p>当我们有了置换表之后，就可以用多种方式来优化走法顺序。</p>



<h3>优化走法顺序</h3>



<p>在之前我们只用历史表作优化，从现在开始采用多种优化方式：</p>



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



<p>(2)&nbsp;然后是两个杀手走法(如果其中某个杀手走法与置换表走法一样，那么可以跳过)。</p>



<p>(3)&nbsp;然后生成全部走法，按历史表排序，再依次搜索(可以排除置换表走法和两个杀手走法)。</p>



<h3>define.go</h3>



<pre class="wp-block-code"><code>//走法排序阶段
const (
	PhaseHash     = 0
	PhaseKiller1  = 1
	PhaseKiller2  = 2
	PhaseGenMoves = 3
	PhaseRest     = 4
)</code></pre>



<h3>SortStruct结构体</h3>



<p>打开rule.go，增加：</p>



<pre class="wp-block-code"><code>//SortStruct 走法排序结构
type SortStruct struct {
	mvHash    int   //置换表走法
	mvKiller1 int   //杀手走法
	mvKiller2 int   //杀手走法
	nPhase    int   //当前阶段
	nIndex    int   // 当前采用第几个走法
	nGenMoves int   // 总共有几个走法
	mvs       &#91;]int // 所有的走法
}</code></pre>



<h3>PositionStruct方法</h3>



<p>增加initSort和nextSort方法：</p>



<pre class="wp-block-code"><code>//initSort 初始化，设定置换表走法和两个杀手走法
func (p *PositionStruct) initSort(mvHash int, s *SortStruct) {
	if s == nil {
		return
	}

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

	return 0
}

//setBestMove 对最佳走法的处理
func (p *PositionStruct) setBestMove(mv, nDepth int) {
	p.search.nHistoryTable&#91;mv] += nDepth * nDepth
	if p.search.mvKillers&#91;p.nDistance]&#91;0] != mv {
		p.search.mvKillers&#91;p.nDistance]&#91;1] = p.search.mvKillers&#91;p.nDistance]&#91;0]
		p.search.mvKillers&#91;p.nDistance]&#91;0] = mv
	}
}</code></pre>



<p>修改NewPositionStruct，增加hashTable初始化：</p>



<pre class="wp-block-code"><code>//NewPositionStruct 初始化棋局
func NewPositionStruct() *PositionStruct {
	p := &amp;PositionStruct{
		zobr: &amp;ZobristStruct{
			dwKey:   0,
			dwLock0: 0,
			dwLock1: 0,
		},
		zobrist: &amp;Zobrist{
			Player: &amp;ZobristStruct{
				dwKey:   0,
				dwLock0: 0,
				dwLock1: 0,
			},
		},
		search: &amp;Search{},
	}
	if p == nil {
		return nil
	}

	for i := 0; i &lt; MaxMoves; i++ {
		tmpMoveStruct := &amp;MoveStruct{}
		p.mvsList&#91;i] = tmpMoveStruct
	}

	for i := 0; i &lt; HashSize; i++ {
		p.search.hashTable&#91;i] = &amp;HashItem{}
	}

	p.zobrist.initZobrist()
	return p
}</code></pre>



<h3>searchFull</h3>



<p>修改searchFull，使用nextSort来返回走法列表：</p>



<pre class="wp-block-code"><code>//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 &lt;= 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 &amp;&amp; !p.inCheck() &amp;&amp; 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 := &amp;SortStruct{
		mvs: make(&#91;]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
}</code></pre>



<h3>searchMain</h3>



<p>修改searchMain，增加对置换表和杀手走法表的清空处理：</p>



<pre class="wp-block-code"><code>//searchMain 迭代加深搜索过程
func (p *PositionStruct) searchMain() {
	// 清空历史表
	for i := 0; i &lt; 65536; i++ {
		p.search.nHistoryTable&#91;i] = 0
	}
	// 清空杀手走法表
	for i := 0; i &lt; LimitDepth; i++ {
		for j := 0; j &lt; 2; j++ {
			p.search.mvKillers&#91;i]&#91;j] = 0
		}
	}
	// 清空置换表
	for i := 0; i &lt; HashSize; i++ {
		p.search.hashTable&#91;i].ucDepth = 0
		p.search.hashTable&#91;i].ucFlag = 0
		p.search.hashTable&#91;i].svl = 0
		p.search.hashTable&#91;i].wmv = 0
		p.search.hashTable&#91;i].wReserved = 0
		p.search.hashTable&#91;i].dwLock0 = 0
		p.search.hashTable&#91;i].dwLock1 = 0
	}

	// 初始化定时器
	start := time.Now()
	// 初始步数
	p.nDistance = 0

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



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



<p>如果小伙伴们想学习更多这方面知识，可以访问<a href="http://www.xqbase.com/computer/stepbystep5.htm" target="_blank" rel="noreferrer noopener">http://www.xqbase.com/computer/stepbystep5.htm</a></p>



<p>在下一节中，我们将学习如何使用开局库？</p>
<p><a rel="nofollow" href="https://wangqianhong.com/2020/11/%e7%94%a8go%e5%86%99%e4%b8%80%e4%b8%aa%e4%b8%ad%e5%9b%bd%e8%b1%a1%e6%a3%8b%ef%bc%88%e5%8d%81%e5%85%ad%ef%bc%89-%e7%bd%ae%e6%8d%a2%e8%a1%a8/">用Go写一个中国象棋（十六）| 置换表</a>最先出现在<a rel="nofollow" href="https://wangqianhong.com">wqh博客</a>。</p>
]]></content:encoded>
					
					<wfw:commentRss>https://wangqianhong.com/2020/11/%e7%94%a8go%e5%86%99%e4%b8%80%e4%b8%aa%e4%b8%ad%e5%9b%bd%e8%b1%a1%e6%a3%8b%ef%bc%88%e5%8d%81%e5%85%ad%ef%bc%89-%e7%bd%ae%e6%8d%a2%e8%a1%a8/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>用Go写一个中国象棋（十五）&#124; 置换表</title>
		<link>https://wangqianhong.com/2020/11/%e7%94%a8go%e5%86%99%e4%b8%80%e4%b8%aa%e4%b8%ad%e5%9b%bd%e8%b1%a1%e6%a3%8b%ef%bc%88%e5%8d%81%e4%ba%94%ef%bc%89-%e7%bd%ae%e6%8d%a2%e8%a1%a8/</link>
					<comments>https://wangqianhong.com/2020/11/%e7%94%a8go%e5%86%99%e4%b8%80%e4%b8%aa%e4%b8%ad%e5%9b%bd%e8%b1%a1%e6%a3%8b%ef%bc%88%e5%8d%81%e4%ba%94%ef%bc%89-%e7%bd%ae%e6%8d%a2%e8%a1%a8/#respond</comments>
		
		<dc:creator><![CDATA[wqh_work]]></dc:creator>
		<pubDate>Sun, 22 Nov 2020 02:05:27 +0000</pubDate>
				<category><![CDATA[技术文章]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[中国象棋]]></category>
		<guid isPermaLink="false">https://wangqianhong.com/?p=545</guid>

					<description><![CDATA[<p>在下棋的过程中，我们发现每走一步，AI都需要生成所有走法，导致AI下棋速度很慢，我们可以加入置换表来&#8230; <a href="https://wangqianhong.com/2020/11/%e7%94%a8go%e5%86%99%e4%b8%80%e4%b8%aa%e4%b8%ad%e5%9b%bd%e8%b1%a1%e6%a3%8b%ef%bc%88%e5%8d%81%e4%ba%94%ef%bc%89-%e7%bd%ae%e6%8d%a2%e8%a1%a8/" class="more-link read-more" rel="bookmark">继续阅读 <span class="screen-reader-text">用Go写一个中国象棋（十五）&#124; 置换表</span><i class="fa fa-arrow-right"></i></a></p>
<p><a rel="nofollow" href="https://wangqianhong.com/2020/11/%e7%94%a8go%e5%86%99%e4%b8%80%e4%b8%aa%e4%b8%ad%e5%9b%bd%e8%b1%a1%e6%a3%8b%ef%bc%88%e5%8d%81%e4%ba%94%ef%bc%89-%e7%bd%ae%e6%8d%a2%e8%a1%a8/">用Go写一个中国象棋（十五）| 置换表</a>最先出现在<a rel="nofollow" href="https://wangqianhong.com">wqh博客</a>。</p>
]]></description>
										<content:encoded><![CDATA[
<p>在下棋的过程中，我们发现每走一步，AI都需要生成所有走法，导致AI下棋速度很慢，我们可以加入置换表来解决这个问题。</p>



<h3>置换表</h3>



<p>象棋的搜索树可以用图来表示，而置换结点可以引向以前搜索过的子树上。置换表可以用来检测这种情况，从而避免重复劳动。</p>



<p>例如，一层的搜索显示“1. 51”是最好的落子法，那么在做两层的搜索时你先搜索“1. 51”。如果返回“1. 51 67”，那么你在做三层的搜索时仍旧先搜索这条路线。这样做之所以有好的效果，是因为第一次搜索的线路通常是好的，而Alpha-Beta对落子法的顺序特别敏感。</p>



<p>如果“1.51 2.67”以后的局面已经搜索过了，那就没有必要再搜索“1.51 2.67”以后的局面了。省去重复的工作，这是置换表的一大特色。</p>



<p>但是在一般的中局局面里，置换表的另一个作用更为重要。每个散列项里都有局面中最好的落子法，首先搜索好的落子法可以大幅度提高搜索效率。因此如果在散列项里找到最好的落子法，那么首先搜索这个落子法，就会改进落子法顺序，减少分枝因子，从而在短的时间内搜索得更深。</p>



<h3>define.go</h3>



<pre class="wp-block-code"><code>//HashSize 置换表大小
const HashSize = 1 &lt;&lt; 20
//HashAlpha ALPHA节点的置换表项
const HashAlpha = 1
//HashBeta BETA节点的置换表项
const HashBeta = 2
//HashPV PV节点的置换表项
const HashPV = 3</code></pre>



<h4>HashItem结构体</h4>



<p>打开rule.go，增加HashItem：</p>



<pre class="wp-block-code"><code>//HashItem 置换表项结构
type HashItem struct {
	ucDepth   int    //深度
	ucFlag    int    //标志
	svl       int    //分值
	wmv       int    //最佳走法
	dwLock0   uint32 //校验码
	dwLock1   uint32 //校验码
	wReserved int    //保留
}</code></pre>



<p>置换表非常简单，以局面的&nbsp;Zobrist Key % HashSize&nbsp;作为索引值。每个置换表项存储的内容：ucDepth深度，ucFlag标志，svl分值，wmv最佳走法，dwLock0和dwLock1校验码。</p>



<h3>Search结构体</h3>



<p>修改Search结构体，增加mvKillers和hashTable：</p>



<pre class="wp-block-code"><code>//Search 与搜索有关的全局变量
type Search struct {
	mvResult      int                 // 电脑走的棋
	nHistoryTable &#91;65536]int          // 历史表
	mvKillers     &#91;LimitDepth]&#91;2]int  // 杀手走法表
	hashTable     &#91;HashSize]*HashItem // 置换表
}</code></pre>



<p>mvKillers保存兄弟节点中产生Beta截断的走法。杀手走法产生截断的可能性极大，兄弟节点中的走法未必在当前节点下能走，所以在尝试杀手走法以前先要对它进行走法合理性的判断。</p>



<p>之前我们写过&nbsp;LegalMove&nbsp;这个函数，如果杀手走法确实产生截断了，那么后面耗时更多的&nbsp;generateMove&nbsp;就可以不用执行了。</p>



<p>我们可以把距离根节点的步数(nDistance)作为索引值，构造一个杀手走法表。每个杀手走法表项存有两个杀手走法，走法一比走法二优先：存一个走法时，走法二被走法一替换，走法一被新走法替换；取走法时，先取走法一，后取走法二。</p>



<h3>PositionStruct方法</h3>



<p>如何利用置换表信息：</p>



<p>(1)&nbsp;检查局面所对应的置换表项，如果Zobrist Lock校验码匹配，那么我们就认为命中(Hit)了。</p>



<p>(2)&nbsp;是否能直接利用置换表中的结果，取决于两个因素：A.&nbsp;深度是否达到要求，B.&nbsp;非PV节点还需要考虑边界。</p>



<p>第二种情况是最好的(完全利用)，ProbeHash返回一个非-MATE_VALUE的值，这样就能不对该节点进行展开了。</p>



<p>如果仅仅符合第一种情况，那么该置换表项的信息仍旧是有意义的——它的最佳走法给了我们一定的启发(部分利用)。</p>



<pre class="wp-block-code"><code>//probeHash 提取置换表项
func (p *PositionStruct) probeHash(vlAlpha, vlBeta, nDepth int) (int, int) {
	hsh := p.search.hashTable&#91;p.zobr.dwKey&amp;(HashSize-1)]
	if hsh.dwLock0 != p.zobr.dwLock0 || hsh.dwLock1 != p.zobr.dwLock1 {
		return -MateValue, 0
	}
	mv := hsh.wmv
	bMate := false
	if hsh.svl > WinValue {
		hsh.svl -= p.nDistance
		bMate = true
	} else if hsh.svl &lt; -WinValue {
		hsh.svl += p.nDistance
		bMate = true
	}
	if hsh.ucDepth >= nDepth || bMate {
		if hsh.ucFlag == HashBeta {
			if hsh.svl >= vlBeta {
				return hsh.svl, mv
			}
			return -MateValue, mv
		} else if hsh.ucFlag == HashAlpha {
			if hsh.svl &lt;= vlAlpha {
				return hsh.svl, mv
			}
			return -MateValue, mv
		}
		return hsh.svl, mv
	}
	return -MateValue, mv
}</code></pre>



<p>RecordHash采用深度优先的替换策略，在判断深度后，将&nbsp;Hash&nbsp;表项中的每个值填上：</p>



<pre class="wp-block-code"><code>//RecordHash 保存置换表项
func (p *PositionStruct) RecordHash(nFlag, vl, nDepth, mv int) {
	hsh := p.search.hashTable&#91;p.zobr.dwKey&amp;(HashSize-1)]
	if hsh.ucDepth > nDepth {
		return
	}
	hsh.ucFlag = nFlag
	hsh.ucDepth = nDepth
	if vl > WinValue {
		hsh.svl = vl + p.nDistance
	} else if vl &lt; -WinValue {
		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&#91;p.zobr.dwKey&amp;(HashSize-1)] = hsh
}</code></pre>



<p>在下一节中，我们将学习如何使用置换表对走法进行排序？</p>
<p><a rel="nofollow" href="https://wangqianhong.com/2020/11/%e7%94%a8go%e5%86%99%e4%b8%80%e4%b8%aa%e4%b8%ad%e5%9b%bd%e8%b1%a1%e6%a3%8b%ef%bc%88%e5%8d%81%e4%ba%94%ef%bc%89-%e7%bd%ae%e6%8d%a2%e8%a1%a8/">用Go写一个中国象棋（十五）| 置换表</a>最先出现在<a rel="nofollow" href="https://wangqianhong.com">wqh博客</a>。</p>
]]></content:encoded>
					
					<wfw:commentRss>https://wangqianhong.com/2020/11/%e7%94%a8go%e5%86%99%e4%b8%80%e4%b8%aa%e4%b8%ad%e5%9b%bd%e8%b1%a1%e6%a3%8b%ef%bc%88%e5%8d%81%e4%ba%94%ef%bc%89-%e7%bd%ae%e6%8d%a2%e8%a1%a8/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>用Go写一个中国象棋（十四）&#124; 象棋AI进阶</title>
		<link>https://wangqianhong.com/2020/11/%e7%94%a8go%e5%86%99%e4%b8%80%e4%b8%aa%e4%b8%ad%e5%9b%bd%e8%b1%a1%e6%a3%8b%ef%bc%88%e5%8d%81%e5%9b%9b%ef%bc%89-%e8%b1%a1%e6%a3%8bai%e8%bf%9b%e9%98%b6/</link>
					<comments>https://wangqianhong.com/2020/11/%e7%94%a8go%e5%86%99%e4%b8%80%e4%b8%aa%e4%b8%ad%e5%9b%bd%e8%b1%a1%e6%a3%8b%ef%bc%88%e5%8d%81%e5%9b%9b%ef%bc%89-%e8%b1%a1%e6%a3%8bai%e8%bf%9b%e9%98%b6/#respond</comments>
		
		<dc:creator><![CDATA[wqh_work]]></dc:creator>
		<pubDate>Thu, 19 Nov 2020 00:39:08 +0000</pubDate>
				<category><![CDATA[技术文章]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[中国象棋]]></category>
		<guid isPermaLink="false">https://wangqianhong.com/?p=537</guid>

					<description><![CDATA[<p>前面几节已经把象棋AI进行了优化，下面我们把优化后的算法运用到象棋程序中。 aiMove 打开gam&#8230; <a href="https://wangqianhong.com/2020/11/%e7%94%a8go%e5%86%99%e4%b8%80%e4%b8%aa%e4%b8%ad%e5%9b%bd%e8%b1%a1%e6%a3%8b%ef%bc%88%e5%8d%81%e5%9b%9b%ef%bc%89-%e8%b1%a1%e6%a3%8bai%e8%bf%9b%e9%98%b6/" class="more-link read-more" rel="bookmark">继续阅读 <span class="screen-reader-text">用Go写一个中国象棋（十四）&#124; 象棋AI进阶</span><i class="fa fa-arrow-right"></i></a></p>
<p><a rel="nofollow" href="https://wangqianhong.com/2020/11/%e7%94%a8go%e5%86%99%e4%b8%80%e4%b8%aa%e4%b8%ad%e5%9b%bd%e8%b1%a1%e6%a3%8b%ef%bc%88%e5%8d%81%e5%9b%9b%ef%bc%89-%e8%b1%a1%e6%a3%8bai%e8%bf%9b%e9%98%b6/">用Go写一个中国象棋（十四）| 象棋AI进阶</a>最先出现在<a rel="nofollow" href="https://wangqianhong.com">wqh博客</a>。</p>
]]></description>
										<content:encoded><![CDATA[
<p>前面几节已经把象棋AI进行了优化，下面我们把优化后的算法运用到象棋程序中。</p>



<h3>aiMove</h3>



<p>打开game.go，增加重复局面的处理：</p>



<pre class="wp-block-code"><code>//aiMove AI移动
func (g *Game) aiMove(screen *ebiten.Image) {
	//AI走一步棋
	g.singlePosition.searchMain()
	g.singlePosition.makeMove(g.singlePosition.search.mvResult)
	//把AI走的棋标记出来
	g.mvLast = g.singlePosition.search.mvResult
	//检查重复局面
	vlRep := g.singlePosition.repStatus(3)
	if g.singlePosition.isMate() {
		//如果分出胜负，那么播放胜负的声音
		g.playAudio(MusicGameWin)
		g.showValue = "Your Lose!"
		g.bGameOver = true
	} else if vlRep > 0 {
		vlRep = g.singlePosition.repValue(vlRep)
		//vlRep是对玩家来说的分值
		if vlRep &lt; -WinValue {
			g.playAudio(MusicGameLose)
			g.showValue = "Your Lose!"
		} else {
			if vlRep > WinValue {
				g.playAudio(MusicGameWin)
				g.showValue = "Your Lose!"
			} else {
				g.playAudio(MusicGameWin)
				g.showValue = "Your Draw!"
			}
		}
		g.bGameOver = true
	} else if g.singlePosition.nMoveNum > 100 {
		g.playAudio(MusicGameWin)
		g.showValue = "Your Draw!"
		g.bGameOver = true
	} else {
		//如果没有分出胜负，那么播放将军、吃子或一般走子的声音
		if g.singlePosition.inCheck() {
			g.playAudio(MusicJiang)
		} else {
			if g.singlePosition.captured() {
				g.playAudio(MusicEat)
			} else {
				g.playAudio(MusicPut)
			}
		}
		if g.singlePosition.captured() {
			g.singlePosition.setIrrev()
		}
	}
}</code></pre>



<h3>clickSquare</h3>



<pre class="wp-block-code"><code>//clickSquare 点击格子处理
func (g *Game) clickSquare(screen *ebiten.Image, sq int) {
	pc := 0
	if g.bFlipped {
		pc = g.singlePosition.ucpcSquares&#91;squareFlip(sq)]
	} else {
		pc = g.singlePosition.ucpcSquares&#91;sq]
	}

	if (pc &amp; sideTag(g.singlePosition.sdPlayer)) != 0 {
		//如果点击自己的棋子，那么直接选中
		g.sqSelected = sq
		g.playAudio(MusicSelect)
	} else if g.sqSelected != 0 &amp;&amp; !g.bGameOver {
		//如果点击的不是自己的棋子，但有棋子选中了(一定是自己的棋子)，那么走这个棋子
		mv := move(g.sqSelected, sq)
		if g.singlePosition.legalMove(mv) {
			if g.singlePosition.makeMove(mv) {
				g.mvLast = mv
				g.sqSelected = 0
				// 检查重复局面
				vlRep := g.singlePosition.repStatus(3)
				if g.singlePosition.isMate() {
					// 如果分出胜负，那么播放胜负的声音，并且弹出不带声音的提示框
					g.playAudio(MusicGameWin)
					g.showValue = "Your Win!"
					g.bGameOver = true
				} else if vlRep > 0 {
					vlRep = g.singlePosition.repValue(vlRep)
					if vlRep > WinValue {
						g.playAudio(MusicGameLose)
						g.showValue = "Your Lose!"
					} else {
						if vlRep &lt; -WinValue {
							g.playAudio(MusicGameWin)
							g.showValue = "Your Win!"
						} else {
							g.playAudio(MusicGameWin)
							g.showValue = "Your Draw!"
						}
					}
					g.bGameOver = true
				} else if g.singlePosition.nMoveNum > 100 {
					g.playAudio(MusicGameWin)
					g.showValue = "Your Draw!"
					g.bGameOver = true
				} else {
					if g.singlePosition.checked() {
						g.playAudio(MusicJiang)
					} else {
						if g.singlePosition.captured() {
							g.playAudio(MusicEat)
							g.singlePosition.setIrrev()
						} else {
							g.playAudio(MusicPut)
						}
					}

					g.aiMove(screen)
				}
			} else {
				g.playAudio(MusicJiang) // 播放被将军的声音
			}
		}
		//如果根本就不符合走法(例如马不走日字)，那么不做任何处理
	}
}</code></pre>



<h3>Update</h3>



<pre class="wp-block-code"><code>//Update 更新状态，1秒60帧
func (g *Game) Update(screen *ebiten.Image) error {
	if inpututil.IsMouseButtonJustPressed(ebiten.MouseButtonLeft) {
		if g.bGameOver {
			g.bGameOver = false
			g.showValue = ""
			g.sqSelected = 0
			g.mvLast = 0
			g.singlePosition.startup()
		} else {
			x, y := ebiten.CursorPosition()
			x = Left + (x-BoardEdge)/SquareSize
			y = Top + (y-BoardEdge)/SquareSize
			g.clickSquare(screen, squareXY(x, y))
		}
	}

	g.drawBoard(screen)
	if g.bGameOver {
		g.messageBox(screen)
	}
	return nil
}</code></pre>



<p>运行程序，再与AI对弈的时候，会发现AI已经变聪明了许多，也不会出现长将的局面。</p>



<p>如果小伙伴们想学习更多这方面的知识，可以访问<a href="http://www.xqbase.com/computer/stepbystep4.htm" target="_blank" rel="noreferrer noopener">http://www.xqbase.com/computer/stepbystep4.htm</a></p>



<p>在下一节中，我们将学习如何使用转换表？</p>
<p><a rel="nofollow" href="https://wangqianhong.com/2020/11/%e7%94%a8go%e5%86%99%e4%b8%80%e4%b8%aa%e4%b8%ad%e5%9b%bd%e8%b1%a1%e6%a3%8b%ef%bc%88%e5%8d%81%e5%9b%9b%ef%bc%89-%e8%b1%a1%e6%a3%8bai%e8%bf%9b%e9%98%b6/">用Go写一个中国象棋（十四）| 象棋AI进阶</a>最先出现在<a rel="nofollow" href="https://wangqianhong.com">wqh博客</a>。</p>
]]></content:encoded>
					
					<wfw:commentRss>https://wangqianhong.com/2020/11/%e7%94%a8go%e5%86%99%e4%b8%80%e4%b8%aa%e4%b8%ad%e5%9b%bd%e8%b1%a1%e6%a3%8b%ef%bc%88%e5%8d%81%e5%9b%9b%ef%bc%89-%e8%b1%a1%e6%a3%8bai%e8%bf%9b%e9%98%b6/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>用Go写一个中国象棋（十三）&#124; 象棋AI进阶</title>
		<link>https://wangqianhong.com/2020/11/%e7%94%a8go%e5%86%99%e4%b8%80%e4%b8%aa%e4%b8%ad%e5%9b%bd%e8%b1%a1%e6%a3%8b%ef%bc%88%e5%8d%81%e4%b8%89%ef%bc%89-%e8%b1%a1%e6%a3%8bai%e8%bf%9b%e9%98%b6/</link>
					<comments>https://wangqianhong.com/2020/11/%e7%94%a8go%e5%86%99%e4%b8%80%e4%b8%aa%e4%b8%ad%e5%9b%bd%e8%b1%a1%e6%a3%8b%ef%bc%88%e5%8d%81%e4%b8%89%ef%bc%89-%e8%b1%a1%e6%a3%8bai%e8%bf%9b%e9%98%b6/#comments</comments>
		
		<dc:creator><![CDATA[wqh_work]]></dc:creator>
		<pubDate>Sun, 15 Nov 2020 11:25:35 +0000</pubDate>
				<category><![CDATA[技术文章]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[中国象棋]]></category>
		<guid isPermaLink="false">https://wangqianhong.com/?p=507</guid>

					<description><![CDATA[<p>假设目前棋局搜索深度为N，那么AI只会考虑N以内的利益，而对N+1及以后的局势没有任何考虑。那么造成&#8230; <a href="https://wangqianhong.com/2020/11/%e7%94%a8go%e5%86%99%e4%b8%80%e4%b8%aa%e4%b8%ad%e5%9b%bd%e8%b1%a1%e6%a3%8b%ef%bc%88%e5%8d%81%e4%b8%89%ef%bc%89-%e8%b1%a1%e6%a3%8bai%e8%bf%9b%e9%98%b6/" class="more-link read-more" rel="bookmark">继续阅读 <span class="screen-reader-text">用Go写一个中国象棋（十三）&#124; 象棋AI进阶</span><i class="fa fa-arrow-right"></i></a></p>
<p><a rel="nofollow" href="https://wangqianhong.com/2020/11/%e7%94%a8go%e5%86%99%e4%b8%80%e4%b8%aa%e4%b8%ad%e5%9b%bd%e8%b1%a1%e6%a3%8b%ef%bc%88%e5%8d%81%e4%b8%89%ef%bc%89-%e8%b1%a1%e6%a3%8bai%e8%bf%9b%e9%98%b6/">用Go写一个中国象棋（十三）| 象棋AI进阶</a>最先出现在<a rel="nofollow" href="https://wangqianhong.com">wqh博客</a>。</p>
]]></description>
										<content:encoded><![CDATA[
<p>假设目前棋局搜索深度为N，那么AI只会考虑N以内的利益，而对N+1及以后的局势没有任何考虑。那么造成的结果就是AI就是一个视力为N的近视眼，很容易因为N以内的短期利益而造成大局上的劣势，这就是水平线效应。</p>



<h3><strong>水平线效应</strong></h3>



<p>到目前为止，搜索算法都会把局面推演到固定的深度，但是这未必是件好事。例如，假设程序最多可以用迭代加深的Alpha-Beta算法搜索到5层，那么来看下这几个例子：</p>



<p>1.&nbsp;沿着某条路线，你发现在第3层有将死或逼和的局面。显然你不想再搜索下去了，因为游戏的最终目的达到了。搜索5层不仅是浪费时间，而且可能会让电脑自己把自己引入不合理的状态。</p>



<p>2.&nbsp;现在假设在5层你吃到了兵。程序可能会认为这个局面稍稍有利，并且会这么走下去。然而，如果你看得更深远些，可能会发现吃了兵以后你的帅就处于被将的状态！</p>



<p>3.&nbsp;最后，假设你的帅被将了，不管你怎么走，都会在第4层被对手吃掉，但是有一条路线可以坚持到第6层。如果你的搜索深度是5，那么在第4层被吃掉的路线会被检测出来，这些情况会评估成灾难局面，但是唯一能使帅在第6层(超出了搜索树)将死的那条路线，对于AI来说不能被发现的。</p>



<p>那么如果你要通过牺牲一个车来延缓帅的被吃呢？对AI来说，在第4层丢车要比丢帅损失小，所以在这个搜索水平上，它情愿丢一个那么大的子，来推迟那个可怜的帅的被吃。</p>



<h3>如何解决水平线效应</h3>



<p>解决水平线效应的方法有以下几种：</p>



<h4>(1)&nbsp;静态(Quiescence)搜索</h4>



<p>进入静态搜索时，要考虑两种情况，一是不被将军的情况，首先尝试不走是否能够截断，然后搜索所有吃子的走法(可以按照MVV/LVA排序)。二是被将军的情况，这时就必须生成所有的走法了(可以按照历史表排序)。</p>



<p>打开define.go，增加cucMvvLva：</p>



<pre class="wp-block-code"><code>//cucMvvLva MVV/LVA每种子力的价值
var cucMvvLva = &#91;24]int{
	0, 0, 0, 0, 0, 0, 0, 0,
	5, 1, 1, 3, 4, 3, 2, 0,
	5, 1, 1, 3, 4, 3, 2, 0}</code></pre>



<p>打开rule.go，增加：</p>



<pre class="wp-block-code"><code>//mvvLva 求MVV/LVA值
func (p *PositionStruct) mvvLva(mv int) int {
	return (cucMvvLva&#91;p.ucpcSquares&#91;dst(mv)]] &lt;&lt; 3) - cucMvvLva&#91;p.ucpcSquares&#91;src(mv)]]
}</code></pre>



<pre class="wp-block-code"><code>//searchQuiesc 静态(Quiescence)搜索过程
func (p *PositionStruct) searchQuiesc(vlAlpha, vlBeta int) int {
	nGenMoves := 0
	mvs := make(&#91;]int, MaxGenMoves)

	//检查重复局面
	vl := p.repStatus(1)
	if vl != 0 {
		return p.repValue(vl)
	}

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

	vlBest := -MateValue
	//这样可以知道，是否一个走法都没走过(杀棋)
	if p.inCheck() {
		//如果被将军，则生成全部走法
		nGenMoves = p.generateMoves(mvs, false)
		mvs = mvs&#91;:nGenMoves]
		sort.Slice(mvs, func(a, b int) bool {
			return p.search.nHistoryTable&#91;a] > p.search.nHistoryTable&#91;b]
		})
	} else {
		//如果不被将军，先做局面评价
		vl = p.evaluate()
		if vl > vlBest {
			vlBest = vl
			if vl >= vlBeta {
				return vl
			}
			if vl > vlAlpha {
				vlAlpha = vl
			}
		}

		//如果局面评价没有截断，再生成吃子走法
		nGenMoves = p.generateMoves(mvs, true)
		mvs = mvs&#91;:nGenMoves]
		sort.Slice(mvs, func(a, b int) bool {
			return p.mvvLva(mvs&#91;a]) > p.mvvLva(mvs&#91;b])
		})
	}

	//逐一走这些走法，并进行递归
	for i := 0; i &lt; nGenMoves; i++ {
		if p.makeMove(mvs&#91;i]) {
			vl = -p.searchQuiesc(-vlBeta, -vlAlpha)
			p.undoMakeMove()

			//进行Alpha-Beta大小判断和截断
			if vl > vlBest {
				//找到最佳值(但不能确定是Alpha、PV还是Beta走法)
				//"vlBest"就是目前要返回的最佳值，可能超出Alpha-Beta边界
				vlBest = vl
				//找到一个Beta走法
				if vl >= vlBeta {
					//Beta截断
					return vl
				}
				//找到一个PV走法
				if vl > vlAlpha {
					//缩小Alpha-Beta边界
					vlAlpha = vl
				}
			}
		}
	}

	//所有走法都搜索完了，返回最佳值
	if vlBest == -MateValue {
		return p.nDistance - MateValue
	}
	return vlBest
}</code></pre>



<h4>(2)&nbsp;空步(Null-Move)裁剪</h4>



<p>空步裁剪的代码非常简单，但某些条件下并不适用，一是被将军的情况下，二是进入残局时(自己一方的子力总价值小于某个阈值)，三是不要连续做两次空步裁剪，否则会导致搜索的退化。</p>



<pre class="wp-block-code"><code>//nullMove 走一步空步
func (p *PositionStruct) nullMove() {
	dwKey := p.zobr.dwKey
	p.changeSide()
	p.mvsList&#91;p.nMoveNum].set(0, 0, false, dwKey)
	p.nMoveNum++
	p.nDistance++
}

//undoNullMove 撤消走一步空步
func (p *PositionStruct) undoNullMove() {
	p.nDistance--
	p.nMoveNum--
	p.changeSide()
}

//nullOkay 判断是否允许空步裁剪
func (p *PositionStruct) nullOkay() bool {
	if p.sdPlayer == 0 {
		return p.vlRed > NullMargin
	}
	return p.vlBlack > NullMargin
}</code></pre>



<h4>(3)&nbsp;将军延伸</h4>



<p>修改searchFull，增加bNoNull判断：</p>



<pre class="wp-block-code"><code>//searchFull 超出边界(Fail-Soft)的Alpha-Beta搜索过程
func (p *PositionStruct) searchFull(vlAlpha, vlBeta, nDepth int, bNoNull bool) int {
	vl:= 0

	//到达水平线，则调用静态搜索(注意：由于空步裁剪，深度可能小于零)
	if nDepth &lt;= 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 &amp;&amp; !p.inCheck() &amp;&amp; p.nullOkay() {
		p.nullMove()
		vl = -p.searchFull(-vlBeta, 1-vlBeta, nDepth-NullDepth-1, true)
		p.undoNullMove()
		if vl >= vlBeta {
			return vl
		}
	}

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

	//初始化走法排序结构
	tmpSort := &amp;SortStruct{
		mvs: make(&#91;]int, MaxGenMoves),
	}
	p.initSort(mvHash, 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
			}
			// PVS
			if vlBest == -MateValue {
				vl = -p.searchFull(-vlBeta, -vlAlpha, nNewDepth, false)
			} else {
				vl = -p.searchFull(-vlAlpha-1, -vlAlpha, nNewDepth, false)
				if vl > vlAlpha &amp;&amp; vl &lt; vlBeta {
					vl = -p.searchFull(-vlBeta, -vlAlpha, nNewDepth, 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 {
		//如果不是Alpha走法，就将最佳走法保存到历史表
		p.setBestMove(mvBest, nDepth)
	}
	return vlBest
}</code></pre>



<h3><strong>水平线效应</strong>总结</h3>



<p>这里需要注意的是静态搜索和将军延伸会带来一个问题——遇到“解将还将”的局面，搜索就会无止境地进行下去，直到程序崩溃；所以要用到前一节进到的内容，进行重复局面的检查。</p>



<p>最后要说一句：象棋中的很多局面(其他游戏也一样)太不可预测了，实在很难恰当地评估。评价函数只能在“静态”的局面下起作用，即这些局面在不久的将来不可能发生很大的变化。</p>



<p>在下一节中，我们将学习界面上对应的修改？</p>
<p><a rel="nofollow" href="https://wangqianhong.com/2020/11/%e7%94%a8go%e5%86%99%e4%b8%80%e4%b8%aa%e4%b8%ad%e5%9b%bd%e8%b1%a1%e6%a3%8b%ef%bc%88%e5%8d%81%e4%b8%89%ef%bc%89-%e8%b1%a1%e6%a3%8bai%e8%bf%9b%e9%98%b6/">用Go写一个中国象棋（十三）| 象棋AI进阶</a>最先出现在<a rel="nofollow" href="https://wangqianhong.com">wqh博客</a>。</p>
]]></content:encoded>
					
					<wfw:commentRss>https://wangqianhong.com/2020/11/%e7%94%a8go%e5%86%99%e4%b8%80%e4%b8%aa%e4%b8%ad%e5%9b%bd%e8%b1%a1%e6%a3%8b%ef%bc%88%e5%8d%81%e4%b8%89%ef%bc%89-%e8%b1%a1%e6%a3%8bai%e8%bf%9b%e9%98%b6/feed/</wfw:commentRss>
			<slash:comments>1</slash:comments>
		
		
			</item>
		<item>
		<title>用Go写一个中国象棋（十二）&#124; 象棋AI进阶</title>
		<link>https://wangqianhong.com/2020/11/%e7%94%a8go%e5%86%99%e4%b8%80%e4%b8%aa%e4%b8%ad%e5%9b%bd%e8%b1%a1%e6%a3%8b%ef%bc%88%e5%8d%81%e4%ba%8c%ef%bc%89-%e8%b1%a1%e6%a3%8bai%e8%bf%9b%e9%98%b6/</link>
					<comments>https://wangqianhong.com/2020/11/%e7%94%a8go%e5%86%99%e4%b8%80%e4%b8%aa%e4%b8%ad%e5%9b%bd%e8%b1%a1%e6%a3%8b%ef%bc%88%e5%8d%81%e4%ba%8c%ef%bc%89-%e8%b1%a1%e6%a3%8bai%e8%bf%9b%e9%98%b6/#respond</comments>
		
		<dc:creator><![CDATA[wqh_work]]></dc:creator>
		<pubDate>Thu, 12 Nov 2020 02:25:27 +0000</pubDate>
				<category><![CDATA[技术文章]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[中国象棋]]></category>
		<guid isPermaLink="false">https://wangqianhong.com/?p=514</guid>

					<description><![CDATA[<p>在开始下一节内容之前，我们先来讲解一下重复局面，因为下一节要用到。 重复局面 如果棋局面(同一方走的&#8230; <a href="https://wangqianhong.com/2020/11/%e7%94%a8go%e5%86%99%e4%b8%80%e4%b8%aa%e4%b8%ad%e5%9b%bd%e8%b1%a1%e6%a3%8b%ef%bc%88%e5%8d%81%e4%ba%8c%ef%bc%89-%e8%b1%a1%e6%a3%8bai%e8%bf%9b%e9%98%b6/" class="more-link read-more" rel="bookmark">继续阅读 <span class="screen-reader-text">用Go写一个中国象棋（十二）&#124; 象棋AI进阶</span><i class="fa fa-arrow-right"></i></a></p>
<p><a rel="nofollow" href="https://wangqianhong.com/2020/11/%e7%94%a8go%e5%86%99%e4%b8%80%e4%b8%aa%e4%b8%ad%e5%9b%bd%e8%b1%a1%e6%a3%8b%ef%bc%88%e5%8d%81%e4%ba%8c%ef%bc%89-%e8%b1%a1%e6%a3%8bai%e8%bf%9b%e9%98%b6/">用Go写一个中国象棋（十二）| 象棋AI进阶</a>最先出现在<a rel="nofollow" href="https://wangqianhong.com">wqh博客</a>。</p>
]]></description>
										<content:encoded><![CDATA[
<p>在开始下一节内容之前，我们先来讲解一下重复局面，因为下一节要用到。</p>



<h3><strong>重复局面</strong></h3>



<p>如果棋局面(同一方走的情况下)重复三次，就可以宣布和棋。如果AI领先一个车但是它陷入长将，那将是非常糟糕的，对手会在你即将取得胜利的时候宣布和棋。</p>



<p>要解决这个问题，就必须检测以前出现过的局面，并采取对策。如果AI懂得重复，它就可以根据盘面上局势的需要，来谋求重复或避免重复。如果AI即将输棋，那么它应该试图寻找长将，反之应该避免。</p>



<h3>如何检查</h3>



<p>检查重复局面的办法很简单，每走一个走法就把当前局面的校验码记录下来，再看看前几个局面的校验码是否与当前值相等。</p>



<p>当重复局面发生时，就要根据双方的将军情况来判定胜负——单方面长将者判负(返回杀棋分数而不必要继续搜索了)，双长将或双方都存在非将走法则判和(返回和棋分数)。</p>



<pre class="wp-block-code"><code>//repValue 重复局面分值
func (p *PositionStruct) repValue(nRepStatus int) int {
	vlReturn := 0
	if nRepStatus&amp;2 != 0 {
		vlReturn += p.nDistance - MateValue
	}
	if nRepStatus&amp;4 != 0 {
		vlReturn += MateValue - p.nDistance
	}

	if vlReturn == 0 {
		return -DrawValue
	}

	return vlReturn
}</code></pre>



<pre class="wp-block-code"><code>//repStatus 检测重复局面
func (p *PositionStruct) repStatus(nRecur int) int {
	bSelfSide, bPerpCheck, bOppPerpCheck := false, true, true
	lpmvs := &#91;MaxMoves]*MoveStruct{}
	for i := 0; i &lt; MaxMoves; i++ {
		lpmvs&#91;i] = p.mvsList&#91;i]
	}

	for i := p.nMoveNum - 1; i >= 0 &amp;&amp; lpmvs&#91;i].wmv != 0 &amp;&amp; lpmvs&#91;i].ucpcCaptured == 0; i-- {
		if bSelfSide {
			bPerpCheck = bPerpCheck &amp;&amp; lpmvs&#91;i].ucbCheck
			if lpmvs&#91;i].dwKey == p.zobr.dwKey {
				nRecur--
				if nRecur == 0 {
					result := 1
					if bPerpCheck {
						result += 2
					}
					if bOppPerpCheck {
						result += 4
					}
					return result
				}
			}
		} else {
			bOppPerpCheck = bOppPerpCheck &amp;&amp; lpmvs&#91;i].ucbCheck
		}
		bSelfSide = !bSelfSide
	}
	return 0
}</code></pre>



<p>起初bPerpCheck(本方长将)和bOppPerpCheck(对方长将)都设为true，当一方存在非将走法时就改为false，这样&nbsp;RepStatus&nbsp;的返回值有有这几种可能：</p>



<p>1.&nbsp;返回0，表示没有重复局面。</p>



<p>2.&nbsp;返回1，表示存在重复局面，但双方都无长将(判和)。</p>



<p>3.&nbsp;返回3(=1+2)，表示存在重复局面，本方单方面长将(判本方负)。</p>



<p>4.&nbsp;返回5(=1+4)，表示存在重复局面，对方单方面长将(判对方负)。</p>



<p>5.&nbsp;返回7(=1+2+4)，表示存在重复局面，双方长将(判和)。</p>



<h3>generateMoves</h3>



<p>generateMoves修改为根据bCapture生成不同的走法，如果为true只生成吃子走法；如果为false生成所有走法。</p>



<pre class="wp-block-code"><code>//generateMoves 生成所有走法，如果bCapture为true则只生成吃子走法
func (p *PositionStruct) generateMoves(mvs &#91;]int, bCapture bool) int {
	nGenMoves, pcSrc, sqDst, pcDst, nDelta := 0, 0, 0, 0, 0
	pcSelfSide := sideTag(p.sdPlayer)
	pcOppSide := oppSideTag(p.sdPlayer)

	for sqSrc := 0; sqSrc &lt; 256; sqSrc++ {
		if !inBoard(sqSrc) {
			continue
		}

		// 找到一个本方棋子，再做以下判断：
		pcSrc = p.ucpcSquares&#91;sqSrc]
		if (pcSrc &amp; pcSelfSide) == 0 {
			continue
		}

		// 根据棋子确定走法
		switch pcSrc - pcSelfSide {
		case PieceJiang:
			for i := 0; i &lt; 4; i++ {
				sqDst = sqSrc + ccJiangDelta&#91;i]
				if !inFort(sqDst) {
					continue
				}
				pcDst = p.ucpcSquares&#91;sqDst]
				if (bCapture &amp;&amp; (pcDst&amp;pcOppSide) != 0) || (!bCapture &amp;&amp; (pcDst&amp;pcSelfSide) == 0) {
					mvs&#91;nGenMoves] = move(sqSrc, sqDst)
					nGenMoves++
				}
			}
			break
		case PieceShi:
			for i := 0; i &lt; 4; i++ {
				sqDst = sqSrc + ccShiDelta&#91;i]
				if !inFort(sqDst) {
					continue
				}
				pcDst = p.ucpcSquares&#91;sqDst]
				if (bCapture &amp;&amp; (pcDst&amp;pcOppSide) != 0) || (!bCapture &amp;&amp; (pcDst&amp;pcSelfSide) == 0) {
					mvs&#91;nGenMoves] = move(sqSrc, sqDst)
					nGenMoves++
				}
			}
			break
		case PieceXiang:
			for i := 0; i &lt; 4; i++ {
				sqDst = sqSrc + ccShiDelta&#91;i]
				if !(inBoard(sqDst) &amp;&amp; noRiver(sqDst, p.sdPlayer) &amp;&amp; p.ucpcSquares&#91;sqDst] == 0) {
					continue
				}
				sqDst += ccShiDelta&#91;i]
				pcDst = p.ucpcSquares&#91;sqDst]
				if (bCapture &amp;&amp; (pcDst&amp;pcOppSide) != 0) || (!bCapture &amp;&amp; (pcDst&amp;pcSelfSide) == 0) {
					mvs&#91;nGenMoves] = move(sqSrc, sqDst)
					nGenMoves++
				}
			}
			break
		case PieceMa:
			for i := 0; i &lt; 4; i++ {
				sqDst = sqSrc + ccJiangDelta&#91;i]
				if p.ucpcSquares&#91;sqDst] != 0 {
					continue
				}
				for j := 0; j &lt; 2; j++ {
					sqDst = sqSrc + ccMaDelta&#91;i]&#91;j]
					if !inBoard(sqDst) {
						continue
					}
					pcDst = p.ucpcSquares&#91;sqDst]
					if (bCapture &amp;&amp; (pcDst&amp;pcOppSide) != 0) || (!bCapture &amp;&amp; (pcDst&amp;pcSelfSide) == 0) {
						mvs&#91;nGenMoves] = move(sqSrc, sqDst)
						nGenMoves++
					}
				}
			}
			break
		case PieceJu:
			for i := 0; i &lt; 4; i++ {
				nDelta = ccJiangDelta&#91;i]
				sqDst = sqSrc + nDelta
				for inBoard(sqDst) {
					pcDst = p.ucpcSquares&#91;sqDst]
					if pcDst == 0 {
						if !bCapture {
							mvs&#91;nGenMoves] = move(sqSrc, sqDst)
							nGenMoves++
						}
					} else {
						if (pcDst &amp; pcOppSide) != 0 {
							mvs&#91;nGenMoves] = move(sqSrc, sqDst)
							nGenMoves++
						}
						break
					}
					sqDst += nDelta
				}

			}
			break
		case PiecePao:
			for i := 0; i &lt; 4; i++ {
				nDelta = ccJiangDelta&#91;i]
				sqDst = sqSrc + nDelta
				for inBoard(sqDst) {
					pcDst = p.ucpcSquares&#91;sqDst]
					if pcDst == 0 {
						if !bCapture {
							mvs&#91;nGenMoves] = move(sqSrc, sqDst)
							nGenMoves++
						}
					} else {
						break
					}
					sqDst += nDelta
				}
				sqDst += nDelta
				for inBoard(sqDst) {
					pcDst = p.ucpcSquares&#91;sqDst]
					if pcDst != 0 {
						if (pcDst &amp; pcOppSide) != 0 {
							mvs&#91;nGenMoves] = move(sqSrc, sqDst)
							nGenMoves++
						}
						break
					}
					sqDst += nDelta
				}
			}
			break
		case PieceBing:
			sqDst = squareForward(sqSrc, p.sdPlayer)
			if inBoard(sqDst) {
				pcDst = p.ucpcSquares&#91;sqDst]
				if (bCapture &amp;&amp; (pcDst&amp;pcOppSide) != 0) || (!bCapture &amp;&amp; (pcDst&amp;pcSelfSide) == 0) {
					mvs&#91;nGenMoves] = move(sqSrc, sqDst)
					nGenMoves++
				}
			}
			if hasRiver(sqSrc, p.sdPlayer) {
				for nDelta = -1; nDelta &lt;= 1; nDelta += 2 {
					sqDst = sqSrc + nDelta
					if inBoard(sqDst) {
						pcDst = p.ucpcSquares&#91;sqDst]
						if (bCapture &amp;&amp; (pcDst&amp;pcOppSide) != 0) || (!bCapture &amp;&amp; (pcDst&amp;pcSelfSide) == 0) {
							mvs&#91;nGenMoves] = move(sqSrc, sqDst)
							nGenMoves++
						}
					}
				}
			}
			break
		}
	}
	return nGenMoves
}</code></pre>



<h3>PositionStruct方法</h3>



<pre class="wp-block-code"><code>//inCheck 是否被将军
func (p *PositionStruct) inCheck() bool {
	return p.mvsList&#91;p.nMoveNum-1].ucbCheck
}</code></pre>



<pre class="wp-block-code"><code>//captured 上一步是否吃子
func (p *PositionStruct) captured() bool {
	return p.mvsList&#91;p.nMoveNum-1].ucpcCaptured != 0
}
</code></pre>



<p>修改isMate中的generateMoves参数：</p>



<pre class="wp-block-code"><code>//isMate 判断是否被将死
func (p *PositionStruct) isMate() bool {
	pcCaptured := 0
	mvs := make(&#91;]int, MaxGenMoves)
	nGenMoveNum := p.generateMoves(mvs, false)
	for i := 0; i &lt; nGenMoveNum; i++ {
		pcCaptured = p.movePiece(mvs&#91;i])
		if !p.checked() {
			p.undoMovePiece(mvs&#91;i], pcCaptured)
			return false
		}

		p.undoMovePiece(mvs&#91;i], pcCaptured)
	}
	return true
}</code></pre>



<p>修改makeMove，把走法保存到mvsList里面，而不是返回：</p>



<pre class="wp-block-code"><code>//makeMove 走一步棋
func (p *PositionStruct) makeMove(mv int) bool {
	dwKey := p.zobr.dwKey
	pcCaptured := p.movePiece(mv)
	if p.checked() {
		p.undoMovePiece(mv, pcCaptured)
		return false
	}
	p.changeSide()
	p.mvsList&#91;p.nMoveNum].set(mv, pcCaptured, p.checked(), dwKey)
	p.nMoveNum++
	p.nDistance++
	return true
}</code></pre>



<pre class="wp-block-code"><code>//undoMakeMove 撤消走一步棋
func (p *PositionStruct) undoMakeMove() {
	p.nDistance--
	p.nMoveNum--
	p.changeSide()
	p.undoMovePiece(p.mvsList&#91;p.nMoveNum].wmv, p.mvsList&#91;p.nMoveNum].ucpcCaptured)
}</code></pre>



<h3>searchMain</h3>



<p>修改searchFull参数：</p>



<pre class="wp-block-code"><code>//searchMain 迭代加深搜索过程
func (p *PositionStruct) searchMain() {
	// 清空历史表
	for i := 0; i &lt; 65536; i++ {
		p.search.nHistoryTable&#91;i] = 0
	}

	// 初始化定时器
	start := time.Now()
	// 初始步数
	p.nDistance = 0

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



<p>在下一节中，我们将学习什么是水平线效应？如何解决水平线效应？</p>
<p><a rel="nofollow" href="https://wangqianhong.com/2020/11/%e7%94%a8go%e5%86%99%e4%b8%80%e4%b8%aa%e4%b8%ad%e5%9b%bd%e8%b1%a1%e6%a3%8b%ef%bc%88%e5%8d%81%e4%ba%8c%ef%bc%89-%e8%b1%a1%e6%a3%8bai%e8%bf%9b%e9%98%b6/">用Go写一个中国象棋（十二）| 象棋AI进阶</a>最先出现在<a rel="nofollow" href="https://wangqianhong.com">wqh博客</a>。</p>
]]></content:encoded>
					
					<wfw:commentRss>https://wangqianhong.com/2020/11/%e7%94%a8go%e5%86%99%e4%b8%80%e4%b8%aa%e4%b8%ad%e5%9b%bd%e8%b1%a1%e6%a3%8b%ef%bc%88%e5%8d%81%e4%ba%8c%ef%bc%89-%e8%b1%a1%e6%a3%8bai%e8%bf%9b%e9%98%b6/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>用Go写一个中国象棋（十一）&#124; 象棋AI进阶</title>
		<link>https://wangqianhong.com/2020/11/%e7%94%a8go%e5%86%99%e4%b8%80%e4%b8%aa%e4%b8%ad%e5%9b%bd%e8%b1%a1%e6%a3%8b%ef%bc%88%e5%8d%81%e4%b8%80%ef%bc%89-%e8%b1%a1%e6%a3%8bai%e8%bf%9b%e9%98%b6/</link>
					<comments>https://wangqianhong.com/2020/11/%e7%94%a8go%e5%86%99%e4%b8%80%e4%b8%aa%e4%b8%ad%e5%9b%bd%e8%b1%a1%e6%a3%8b%ef%bc%88%e5%8d%81%e4%b8%80%ef%bc%89-%e8%b1%a1%e6%a3%8bai%e8%bf%9b%e9%98%b6/#respond</comments>
		
		<dc:creator><![CDATA[wqh_work]]></dc:creator>
		<pubDate>Tue, 10 Nov 2020 02:27:21 +0000</pubDate>
				<category><![CDATA[技术文章]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[中国象棋]]></category>
		<guid isPermaLink="false">https://wangqianhong.com/?p=481</guid>

					<description><![CDATA[<p>到上一章结束，我们的AI已经会基本的走棋了，但是很多时候却很低能，我们需要做一些改进，让AI变得更聪&#8230; <a href="https://wangqianhong.com/2020/11/%e7%94%a8go%e5%86%99%e4%b8%80%e4%b8%aa%e4%b8%ad%e5%9b%bd%e8%b1%a1%e6%a3%8b%ef%bc%88%e5%8d%81%e4%b8%80%ef%bc%89-%e8%b1%a1%e6%a3%8bai%e8%bf%9b%e9%98%b6/" class="more-link read-more" rel="bookmark">继续阅读 <span class="screen-reader-text">用Go写一个中国象棋（十一）&#124; 象棋AI进阶</span><i class="fa fa-arrow-right"></i></a></p>
<p><a rel="nofollow" href="https://wangqianhong.com/2020/11/%e7%94%a8go%e5%86%99%e4%b8%80%e4%b8%aa%e4%b8%ad%e5%9b%bd%e8%b1%a1%e6%a3%8b%ef%bc%88%e5%8d%81%e4%b8%80%ef%bc%89-%e8%b1%a1%e6%a3%8bai%e8%bf%9b%e9%98%b6/">用Go写一个中国象棋（十一）| 象棋AI进阶</a>最先出现在<a rel="nofollow" href="https://wangqianhong.com">wqh博客</a>。</p>
]]></description>
										<content:encoded><![CDATA[
<p>到上一章结束，我们的AI已经会基本的走棋了，但是很多时候却很低能，我们需要做一些改进，让AI变得更聪明。</p>



<h3>Zobrist校验码</h3>



<p>在象棋博弈的研究中，经常需要使用到哈希表的技术。对于Alpha-Beta来说，提高效率不可避免的需要用到置换表技术(后面章节会介绍)，也就是一种特殊的哈希表。</p>



<p>Zobrist就是一种非常有效的将局面映射为一个独特的哈希值的方法，对于任何一个不同的局面，其使用Zobrist所算出来的哈希值是完全不同的。</p>



<p>Zobrist校验码技术有以下几个作用：</p>



<p>(1)&nbsp;用Zobrist键值来实现置换表。置换表会以前搜索过的局面，让你节省很多搜索的时间。置换表还有一个并不起眼的作用是，它可以帮助你改善着法的顺序。</p>



<p>(2)&nbsp;用Zobrist键值来实现兵型的散列表。你可以用一个键值只记录棋盘上的兵，对兵型做了很复杂的分析后，在散列表中记录分析的结果，它可以为你评估兵型节省很多时间。</p>



<p>(3)&nbsp;可以用Zobrist键值制造一个很小的散列表，来检测当前着法路线中有没有重复局面，以便发现长将或其他导致和局的着法。</p>



<p>(4)&nbsp;可以用Zobrist键值创建支持置换的开局库。</p>



<h3>RC4Struct</h3>



<p>打开define.go，增加如下代码：</p>



<pre class="wp-block-code"><code>const (
	//MaxMoves 最大的历史走法数
	MaxMoves = 256
	//LimitDepth 最大的搜索深度
	LimitDepth = 64
	//DrawValue 和棋时返回的分数(取负值)
	DrawValue = 20
	//NullMargin 空步裁剪的子力边界
	NullMargin = 400
	//NullDepth 空步裁剪的裁剪深度
	NullDepth = 2
)</code></pre>



<p>打开rule.go，增加如下代码：</p>



<pre class="wp-block-code"><code>//RC4Struct RC4密码流生成器
type RC4Struct struct {
	s    &#91;256]int
	x, y int
}

//initZero 用空密钥初始化密码流生成器
func (r *RC4Struct) initZero() {
	j := 0
	for i := 0; i &lt; 256; i++ {
		r.s&#91;i] = i
	}
	for i := 0; i &lt; 256; i++ {
		j = (j + r.s&#91;i]) &amp; 255
		r.s&#91;i], r.s&#91;j] = r.s&#91;j], r.s&#91;i]
	}
}

//nextByte 生成密码流的下一个字节
func (r *RC4Struct) nextByte() uint32 {
	r.x = (r.x + 1) &amp; 255
	r.y = (r.y + r.s&#91;r.x]) &amp; 255
	r.s&#91;r.x], r.s&#91;r.y] = r.s&#91;r.y], r.s&#91;r.x]
	return uint32(r.s&#91;(r.s&#91;r.x]+r.s&#91;r.y])&amp;255])
}

//nextLong 生成密码流的下四个字节
func (r *RC4Struct) nextLong() uint32 {
	uc0 := r.nextByte()
	uc1 := r.nextByte()
	uc2 := r.nextByte()
	uc3 := r.nextByte()
	return uc0 + (uc1 &lt;&lt; 8) + (uc2 &lt;&lt; 16) + (uc3 &lt;&lt; 24)
}

//ZobristStruct Zobrist结构
type ZobristStruct struct {
	dwKey   uint32
	dwLock0 uint32
	dwLock1 uint32
}

//initZero 用零填充Zobrist
func (z *ZobristStruct) initZero() {
	z.dwKey, z.dwLock0, z.dwLock1 = 0, 0, 0
}

//initRC4 用密码流填充Zobrist
func (z *ZobristStruct) initRC4(rc4 *RC4Struct) {
	z.dwKey = rc4.nextLong()
	z.dwLock0 = rc4.nextLong()
	z.dwLock1 = rc4.nextLong()
}

//xor1 执行XOR操作
func (z *ZobristStruct) xor1(zobr *ZobristStruct) {
	z.dwKey ^= zobr.dwKey
	z.dwLock0 ^= zobr.dwLock0
	z.dwLock1 ^= zobr.dwLock1
}

//xor2 执行XOR操作
func (z *ZobristStruct) xor2(zobr1, zobr2 *ZobristStruct) {
	z.dwKey ^= zobr1.dwKey ^ zobr2.dwKey
	z.dwLock0 ^= zobr1.dwLock0 ^ zobr2.dwLock0
	z.dwLock1 ^= zobr1.dwLock1 ^ zobr2.dwLock1
}

//Zobrist Zobrist表
type Zobrist struct {
	Player *ZobristStruct          //走子方
	Table  &#91;14]&#91;256]*ZobristStruct //所有棋子
}

//initZobrist 初始化Zobrist表
func (z *Zobrist) initZobrist() {
	rc4 := &amp;RC4Struct{}
	rc4.initZero()
	z.Player.initRC4(rc4)
	for i := 0; i &lt; 14; i++ {
		for j := 0; j &lt; 256; j++ {
			z.Table&#91;i]&#91;j] = &amp;ZobristStruct{}
			z.Table&#91;i]&#91;j].initRC4(rc4)
		}
	}
}</code></pre>



<p>dwKey在检查重复局面时用，也作为置换表的键值。</p>



<p>dwLock0和dwLock1用作置换表的校验值，另外，dwLock1还是查找开局库的依据(后面章节会介绍)。</p>



<p>initZobrist生成Zobrist校验码</p>



<h3>MoveStruct</h3>



<p>保存玩家走法信息：</p>



<pre class="wp-block-code"><code>//MoveStruct 历史走法信息
type MoveStruct struct {
	ucpcCaptured int  //是否吃子
	ucbCheck     bool //是否将军
	wmv          int  //走法
	dwKey        uint32
}

//set 设置
func (m *MoveStruct) set(mv, pcCaptured int, bCheck bool, dwKey uint32) {
	m.wmv = mv
	m.ucpcCaptured = pcCaptured
	m.ucbCheck = bCheck
	m.dwKey = dwKey
}</code></pre>



<h3>PositionStruct结构体</h3>



<p>修改PositionStruct结构体：</p>



<pre class="wp-block-code"><code>//PositionStruct 局面结构
type PositionStruct struct {
	sdPlayer    int                   //轮到谁走，0=红方，1=黑方
	vlRed       int                   //红方的子力价值
	vlBlack     int                   //黑方的子力价值
	nDistance   int                   //距离根节点的步数
	nMoveNum    int                   //历史走法数
	ucpcSquares &#91;256]int              //棋盘上的棋子
	mvsList     &#91;MaxMoves]*MoveStruct //历史走法信息列表
	zobr        *ZobristStruct        //走子方zobrist校验码
	zobrist     *Zobrist              //所有棋子zobrist校验码
	search      *Search
}</code></pre>



<p>zobr走子方zobrist校验码，表示当前走子的一方。</p>



<p>zobrist所有棋子zobrist校验码，包含了红黑双方棋子。</p>



<h3>PositionStruct方法</h3>



<p>修改NewPositionStruct，增加校验码的初始化：</p>



<pre class="wp-block-code"><code>//NewPositionStruct 初始化棋局
func NewPositionStruct() *PositionStruct {
	p := &amp;PositionStruct{
		zobr: &amp;ZobristStruct{
			dwKey:   0,
			dwLock0: 0,
			dwLock1: 0,
		},
		zobrist: &amp;Zobrist{
			Player: &amp;ZobristStruct{
				dwKey:   0,
				dwLock0: 0,
				dwLock1: 0,
			},
		},
		search: &amp;Search{},
	}
	if p == nil {
		return nil
	}

	for i := 0; i &lt; MaxMoves; i++ {
		tmpMoveStruct := &amp;MoveStruct{}
		p.mvsList&#91;i] = tmpMoveStruct
	}
	p.zobrist.initZobrist()
	return p
}</code></pre>



<pre class="wp-block-code"><code>// setIrrev 清空(初始化)历史走法信息
func (p *PositionStruct) setIrrev() {
	p.mvsList&#91;0].set(0, 0, p.checked(), p.zobr.dwKey)
	p.nMoveNum = 1
}</code></pre>



<p>如果要改变走子方，只要异或一个“走子方”的键值就可以了：</p>



<pre class="wp-block-code"><code>//changeSide 交换走子方
func (p *PositionStruct) changeSide() {
	p.sdPlayer = 1 - p.sdPlayer
	p.zobr.xor1(p.zobrist.Player)
}</code></pre>



<p>如果一个棋子动过了，Zobrist会得到完全不同的键值，所以这两个键值不会挤在一块儿或者冲突，把它们用作散列表键值的时候会非常有效。</p>



<p>另一个优点在于，键值的产生是可以逐步进行的。例如，最左边的黑车起始位置是51，那么键值里一定异或过一个“zobrist[20][51]”。如果你再次异或这个值，那么根据异或的工作原理，这个黑车就从键值里删除了。</p>



<p>这就是说，如果你有当前局面的键值，并且需要把最左边的黑车在起始位置前进一格，你只要异或一个“黑车在51”的键值，把黑车从51移走，并且异或一个“黑车在67”的键值，把黑车放在67上。比起从头开始一个个棋子去异或，这样做可以得到同样的键值。</p>



<pre class="wp-block-code"><code>//addPiece 在棋盘上放一枚棋子
func (p *PositionStruct) addPiece(sq, pc int) {
	p.ucpcSquares&#91;sq] = pc
	// 红方加分，黑方(注意"cucvlPiecePos"取值要颠倒)减分
	if pc &lt; 16 {
		p.vlRed += cucvlPiecePos&#91;pc-8]&#91;sq]
		p.zobr.xor1(p.zobrist.Table&#91;pc-8]&#91;sq])
	} else {
		p.vlBlack += cucvlPiecePos&#91;pc-16]&#91;squareFlip(sq)]
		p.zobr.xor1(p.zobrist.Table&#91;pc-9]&#91;sq])
	}
}</code></pre>



<pre class="wp-block-code"><code>//delPiece 从棋盘上拿走一枚棋子
func (p *PositionStruct) delPiece(sq, pc int) {
	p.ucpcSquares&#91;sq] = 0
	// 红方减分，黑方(注意"cucvlPiecePos"取值要颠倒)加分
	if pc &lt; 16 {
		p.vlRed -= cucvlPiecePos&#91;pc-8]&#91;sq]
		p.zobr.xor1(p.zobrist.Table&#91;pc-8]&#91;sq])
	} else {
		p.vlBlack -= cucvlPiecePos&#91;pc-16]&#91;squareFlip(sq)]
		p.zobr.xor1(p.zobrist.Table&#91;pc-9]&#91;sq])
	}
}</code></pre>



<p>修改startup，增加校验码初始化：</p>



<pre class="wp-block-code"><code>//startup 初始化棋盘
func (p *PositionStruct) startup() {
	pc := 0
	p.sdPlayer, p.vlRed, p.vlBlack, p.nDistance = 0, 0, 0, 0
	p.zobr.initZero()
	for i := 0; i &lt; 256; i++ {
		p.ucpcSquares&#91;i] = 0
	}
	for sq := 0; sq &lt; 256; sq++ {
		pc = cucpcStartup&#91;sq]
		if pc != 0 {
			p.addPiece(sq, pc)
		}
	}
	p.setIrrev()
}</code></pre>



<p>在下一节中，我们将学习如何检查重复局面？</p>
<p><a rel="nofollow" href="https://wangqianhong.com/2020/11/%e7%94%a8go%e5%86%99%e4%b8%80%e4%b8%aa%e4%b8%ad%e5%9b%bd%e8%b1%a1%e6%a3%8b%ef%bc%88%e5%8d%81%e4%b8%80%ef%bc%89-%e8%b1%a1%e6%a3%8bai%e8%bf%9b%e9%98%b6/">用Go写一个中国象棋（十一）| 象棋AI进阶</a>最先出现在<a rel="nofollow" href="https://wangqianhong.com">wqh博客</a>。</p>
]]></content:encoded>
					
					<wfw:commentRss>https://wangqianhong.com/2020/11/%e7%94%a8go%e5%86%99%e4%b8%80%e4%b8%aa%e4%b8%ad%e5%9b%bd%e8%b1%a1%e6%a3%8b%ef%bc%88%e5%8d%81%e4%b8%80%ef%bc%89-%e8%b1%a1%e6%a3%8bai%e8%bf%9b%e9%98%b6/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>用Go写一个中国象棋（十）&#124; 象棋AI</title>
		<link>https://wangqianhong.com/2020/11/%e7%94%a8go%e5%86%99%e4%b8%80%e4%b8%aa%e4%b8%ad%e5%9b%bd%e8%b1%a1%e6%a3%8b%ef%bc%88%e5%8d%81%ef%bc%89-%e8%b1%a1%e6%a3%8bai/</link>
					<comments>https://wangqianhong.com/2020/11/%e7%94%a8go%e5%86%99%e4%b8%80%e4%b8%aa%e4%b8%ad%e5%9b%bd%e8%b1%a1%e6%a3%8b%ef%bc%88%e5%8d%81%ef%bc%89-%e8%b1%a1%e6%a3%8bai/#respond</comments>
		
		<dc:creator><![CDATA[wqh_work]]></dc:creator>
		<pubDate>Wed, 04 Nov 2020 09:13:51 +0000</pubDate>
				<category><![CDATA[技术文章]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[中国象棋]]></category>
		<guid isPermaLink="false">https://wangqianhong.com/?p=463</guid>

					<description><![CDATA[<p>Alpha -Beta搜索算法是机器博弈领域中最为重要的算法之一，这里我们不展开讨论，只做简单的介绍&#8230; <a href="https://wangqianhong.com/2020/11/%e7%94%a8go%e5%86%99%e4%b8%80%e4%b8%aa%e4%b8%ad%e5%9b%bd%e8%b1%a1%e6%a3%8b%ef%bc%88%e5%8d%81%ef%bc%89-%e8%b1%a1%e6%a3%8bai/" class="more-link read-more" rel="bookmark">继续阅读 <span class="screen-reader-text">用Go写一个中国象棋（十）&#124; 象棋AI</span><i class="fa fa-arrow-right"></i></a></p>
<p><a rel="nofollow" href="https://wangqianhong.com/2020/11/%e7%94%a8go%e5%86%99%e4%b8%80%e4%b8%aa%e4%b8%ad%e5%9b%bd%e8%b1%a1%e6%a3%8b%ef%bc%88%e5%8d%81%ef%bc%89-%e8%b1%a1%e6%a3%8bai/">用Go写一个中国象棋（十）| 象棋AI</a>最先出现在<a rel="nofollow" href="https://wangqianhong.com">wqh博客</a>。</p>
]]></description>
										<content:encoded><![CDATA[
<p>Alpha -Beta搜索算法是机器博弈领域中最为重要的算法之一，这里我们不展开讨论，只做简单的介绍，有兴趣的小伙伴请自行研究。</p>



<h3>Alpha -Beta</h3>



<p>在象棋里，双方棋手都知道每个棋子在哪里，他们轮流走并且可以走任何合理的落子法。下棋的目的就是将死对方，或者避免被将死，或者有时争取和棋是最好的选择。</p>



<p>把象棋定义为一棵有根的树(即“博弈树”)，一个结点就代表棋类的一个局面，子结点就是这个局面走一步可以到达的一个局面。</p>



<p>Alpha-Beta算法的基本思想——只要有一步好的落子法，就能淘汰其他可能导致灾难的结点，而这样的结点是很多的。当不同路线的局面发生重复时可以节省下分析局面的时间。</p>



<h3>历史表</h3>



<p>历史表是很好的走法排序依据，用来保存杀棋落子法，因为这个落子法今后还可能用到。</p>



<p>记录每种落子法的数值，当搜索算法认为某个落子法很有用时，它会让历史表增加这步的数值：</p>



<pre class="wp-block-code"><code>//Search 与搜索有关的全局变量
type Search struct {
	mvResult      int        // 电脑走的棋
	nHistoryTable &#91;65536]int // 历史表
}</code></pre>



<p>修改PositionStruct：</p>



<pre class="wp-block-code"><code>//PositionStruct 局面结构
type PositionStruct struct {
	sdPlayer    int      // 轮到谁走，0=红方，1=黑方
	vlRed       int      //红方的子力价值
	vlBlack     int      //黑方的子力价值
	nDistance   int      // 距离根节点的步数
	ucpcSquares &#91;256]int // 棋盘上的棋子
	search      *Search
}</code></pre>



<p>修改NewPositionStruct，初始化search：</p>



<pre class="wp-block-code"><code>//NewPositionStruct 初始化棋局
func NewPositionStruct() *PositionStruct {
	p := &amp;PositionStruct{
		search: &amp;Search{},
	}
	if p == nil {
		return nil
	}
	return p
}</code></pre>



<h3>searchFull</h3>



<p>增加searchFull函数：</p>



<pre class="wp-block-code"><code>//searchFull 超出边界(Fail-Soft)的Alpha-Beta搜索过程
func (p *PositionStruct) searchFull(vlAlpha, vlBeta, nDepth int) int {
	vl:= 0

	//到达水平线，则返回局面评价值
	if nDepth &lt;= 0 {
		return p.evaluate()
	}

	vlBest := -MateValue //是否一个走法都没走过(杀棋)
	mvBest := 0          //是否搜索到了Beta走法或PV走法，以便保存到历史表

	mvs := make(&#91;]int, MaxGenMoves)
	nGenMoves := p.generateMoves(mvs)
	mvs = mvs&#91;:nGenMoves]
	sort.Slice(mvs, func(a, b int) bool {
		return p.search.nHistoryTable&#91;a] > p.search.nHistoryTable&#91;b]
	})

	//逐一走这些走法，并进行递归
	for i := 0; i &lt; nGenMoves; i++ {
		if ok, pcCaptured := p.makeMove(mvs&#91;i]); ok {
			vl = -p.searchFull(-vlBeta, -vlAlpha, nDepth-1)
			p.undoMakeMove(mvs&#91;i], pcCaptured)

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

	//所有走法都搜索完了，把最佳走法(不能是Alpha走法)保存到历史表，返回最佳值
	if vlBest == -MateValue {
		//如果是杀棋，就根据杀棋步数给出评价
		return p.nDistance - MateValue
	}

	if mvBest != 0 {
		//如果不是Alpha走法，就将最佳走法保存到历史表
		p.search.nHistoryTable&#91;mvBest] += nDepth * nDepth
		if p.nDistance == 0 {
			// 搜索根节点时，总是有一个最佳走法(因为全窗口搜索不会超出边界)，将这个走法保存下来
			p.search.mvResult = mvBest
		}
	}
	return vlBest
}</code></pre>



<h3>searchMain</h3>



<p>迭代加深目前最明显的效果是充分发挥历史表的作用——浅层搜索结束后，历史表中积累了大量非常宝贵的数据，这将大幅度减少深层搜索的时间。</p>



<p>在迭代加深的基础上实现时间控制，这将是非常简单的：</p>



<pre class="wp-block-code"><code>//searchMain 迭代加深搜索过程
func (p *PositionStruct) searchMain() {
	// 清空历史表
	for i := 0; i &lt; 65536; i++ {
		p.search.nHistoryTable&#91;i] = 0
	}

	// 初始化定时器
	start := time.Now()
	// 初始步数
	p.nDistance = 0

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



<h3>aiMove</h3>



<p>打开game.go，增加aiMove函数：</p>



<pre class="wp-block-code"><code>//aiMove AI移动
func (g *Game) aiMove(screen *ebiten.Image) {
	//AI走一步棋
	g.singlePosition.searchMain()
	_, pcCaptured := g.singlePosition.makeMove(g.singlePosition.search.mvResult)
	//把AI走的棋标记出来
	g.mvLast = g.singlePosition.search.mvResult
	if g.singlePosition.isMate() {
		//如果分出胜负，那么播放胜负的声音
		g.playAudio(MusicGameWin)
		g.showValue = "Your Lose!"
		g.bGameOver = true
	} else {
		//如果没有分出胜负，那么播放将军、吃子或一般走子的声音
		if g.singlePosition.checked() {
			g.playAudio(MusicJiang)
		} else {
			if pcCaptured != 0 {
				g.playAudio(MusicEat)
			} else {
				g.playAudio(MusicPut)
			}
		}
	}
}</code></pre>



<h3>clickSquare</h3>



<p>修改clickSquare，增加电脑走棋：</p>



<pre class="wp-block-code"><code>//clickSquare 点击格子处理
func (g *Game) clickSquare(screen *ebiten.Image, sq int) {
	pc := 0
	if g.bFlipped {
		pc = g.singlePosition.ucpcSquares&#91;squareFlip(sq)]
	} else {
		pc = g.singlePosition.ucpcSquares&#91;sq]
	}

	if (pc &amp; sideTag(g.singlePosition.sdPlayer)) != 0 {
		//如果点击自己的棋子，那么直接选中
		g.sqSelected = sq
		g.playAudio(MusicSelect)
	} else if g.sqSelected != 0 &amp;&amp; !g.bGameOver {
		//如果点击的不是自己的棋子，但有棋子选中了(一定是自己的棋子)，那么走这个棋子
		mv := move(g.sqSelected, sq)
		if g.singlePosition.legalMove(mv) {
			if ok, pc := g.singlePosition.makeMove(mv); ok {
				g.mvLast = mv
				g.sqSelected = 0
				if g.singlePosition.isMate() {
					// 如果分出胜负，那么播放胜负的声音，并且弹出不带声音的提示框
					g.playAudio(MusicGameWin)
					g.showValue = "Your Win!"
					g.bGameOver = true
				} else {
					// 如果没有分出胜负，那么播放将军、吃子或一般走子的声音
					if g.singlePosition.checked() {
						g.playAudio(MusicJiang)
					} else {
						if pc != 0 {
							g.playAudio(MusicEat)
						} else {
							g.playAudio(MusicPut)
						}
					}
					g.aiMove(screen)
				}
			} else {
				g.playAudio(MusicJiang) // 播放被将军的声音
			}
		}
		//如果根本就不符合走法(例如马不走日字)，那么不做任何处理
	}
}</code></pre>



<p>运行程序，可以看到电脑已经会自己走棋了！现在我们可以和电脑进行简单的对弈了！</p>



<p>如果小伙伴们想学习更多这方面的知识，可以访问：</p>



<p><a href="http://www.xqbase.com/computer/stepbystep3.htm" target="_blank" rel="noreferrer noopener">http://www.xqbase.com/computer/stepbystep3.htm</a></p>



<p>在下一节中，我们将学习如何使AI变得更聪明？</p>
<p><a rel="nofollow" href="https://wangqianhong.com/2020/11/%e7%94%a8go%e5%86%99%e4%b8%80%e4%b8%aa%e4%b8%ad%e5%9b%bd%e8%b1%a1%e6%a3%8b%ef%bc%88%e5%8d%81%ef%bc%89-%e8%b1%a1%e6%a3%8bai/">用Go写一个中国象棋（十）| 象棋AI</a>最先出现在<a rel="nofollow" href="https://wangqianhong.com">wqh博客</a>。</p>
]]></content:encoded>
					
					<wfw:commentRss>https://wangqianhong.com/2020/11/%e7%94%a8go%e5%86%99%e4%b8%80%e4%b8%aa%e4%b8%ad%e5%9b%bd%e8%b1%a1%e6%a3%8b%ef%bc%88%e5%8d%81%ef%bc%89-%e8%b1%a1%e6%a3%8bai/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>用Go写一个中国象棋（九）&#124; 象棋AI</title>
		<link>https://wangqianhong.com/2020/10/%e7%94%a8go%e5%86%99%e4%b8%80%e4%b8%aa%e4%b8%ad%e5%9b%bd%e8%b1%a1%e6%a3%8b%ef%bc%88%e4%b9%9d%ef%bc%89-%e8%b1%a1%e6%a3%8bai/</link>
					<comments>https://wangqianhong.com/2020/10/%e7%94%a8go%e5%86%99%e4%b8%80%e4%b8%aa%e4%b8%ad%e5%9b%bd%e8%b1%a1%e6%a3%8b%ef%bc%88%e4%b9%9d%ef%bc%89-%e8%b1%a1%e6%a3%8bai/#respond</comments>
		
		<dc:creator><![CDATA[wqh_work]]></dc:creator>
		<pubDate>Thu, 29 Oct 2020 01:46:24 +0000</pubDate>
				<category><![CDATA[技术文章]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[中国象棋]]></category>
		<guid isPermaLink="false">https://wangqianhong.com/?p=441</guid>

					<description><![CDATA[<p>我们已经让电脑学会了如何去获得每个棋的走法，但电脑并不知道每步棋是好棋还是坏棋，所以我们需要加入棋子&#8230; <a href="https://wangqianhong.com/2020/10/%e7%94%a8go%e5%86%99%e4%b8%80%e4%b8%aa%e4%b8%ad%e5%9b%bd%e8%b1%a1%e6%a3%8b%ef%bc%88%e4%b9%9d%ef%bc%89-%e8%b1%a1%e6%a3%8bai/" class="more-link read-more" rel="bookmark">继续阅读 <span class="screen-reader-text">用Go写一个中国象棋（九）&#124; 象棋AI</span><i class="fa fa-arrow-right"></i></a></p>
<p><a rel="nofollow" href="https://wangqianhong.com/2020/10/%e7%94%a8go%e5%86%99%e4%b8%80%e4%b8%aa%e4%b8%ad%e5%9b%bd%e8%b1%a1%e6%a3%8b%ef%bc%88%e4%b9%9d%ef%bc%89-%e8%b1%a1%e6%a3%8bai/">用Go写一个中国象棋（九）| 象棋AI</a>最先出现在<a rel="nofollow" href="https://wangqianhong.com">wqh博客</a>。</p>
]]></description>
										<content:encoded><![CDATA[
<p>我们已经让电脑学会了如何去获得每个棋的走法，但电脑并不知道每步棋是好棋还是坏棋，所以我们需要加入棋子力价值。</p>



<h3>棋子力价值</h3>



<p>我们给每个棋子不同的力价值，并且适当调整——棋子力价值是跟它的绝对位置相关的。</p>



<p>最明显的例子是中国象棋中的兵(卒)，过河前我们给它很低的分数，过河后分数大涨，越接近九宫格分数越高，九宫中心甚至接近一个马或炮的值。</p>



<p>打开define.go，增加下面的内容</p>



<pre class="wp-block-code"><code>const (
	//LimitDepth 最大的搜索深度
	LimitDepth = 32
	//MateValue 最高分值，即将死的分值
	MateValue = 10000
	//WinValue 搜索出胜负的分值界限，超出此值就说明已经搜索出杀棋了
	WinValue = MateValue - 100
	//AdvancedValue 先行权分值
	AdvancedValue = 3
)

// 子力位置价值表
var cucvlPiecePos = &#91;7]&#91;256]int{
	{ // 帅(将)
		0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
		0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
		0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
		0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
		0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
		0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
		0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
		0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
		0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
		0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
		0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0,
		0, 0, 0, 0, 0, 0, 2, 2, 2, 0, 0, 0, 0, 0, 0, 0,
		0, 0, 0, 0, 0, 0, 11, 15, 11, 0, 0, 0, 0, 0, 0, 0,
		0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
		0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
		0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
	{ // 仕(士)
		0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
		0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
		0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
		0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
		0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
		0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
		0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
		0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
		0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
		0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
		0, 0, 0, 0, 0, 0, 20, 0, 20, 0, 0, 0, 0, 0, 0, 0,
		0, 0, 0, 0, 0, 0, 0, 23, 0, 0, 0, 0, 0, 0, 0, 0,
		0, 0, 0, 0, 0, 0, 20, 0, 20, 0, 0, 0, 0, 0, 0, 0,
		0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
		0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
		0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
	{ // 相(象)
		0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
		0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
		0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
		0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
		0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
		0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
		0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
		0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
		0, 0, 0, 0, 0, 20, 0, 0, 0, 20, 0, 0, 0, 0, 0, 0,
		0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
		0, 0, 0, 18, 0, 0, 0, 23, 0, 0, 0, 18, 0, 0, 0, 0,
		0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
		0, 0, 0, 0, 0, 20, 0, 0, 0, 20, 0, 0, 0, 0, 0, 0,
		0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
		0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
		0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
	{ // 马
		0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
		0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
		0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
		0, 0, 0, 90, 90, 90, 96, 90, 96, 90, 90, 90, 0, 0, 0, 0,
		0, 0, 0, 90, 96, 103, 97, 94, 97, 103, 96, 90, 0, 0, 0, 0,
		0, 0, 0, 92, 98, 99, 103, 99, 103, 99, 98, 92, 0, 0, 0, 0,
		0, 0, 0, 93, 108, 100, 107, 100, 107, 100, 108, 93, 0, 0, 0, 0,
		0, 0, 0, 90, 100, 99, 103, 104, 103, 99, 100, 90, 0, 0, 0, 0,
		0, 0, 0, 90, 98, 101, 102, 103, 102, 101, 98, 90, 0, 0, 0, 0,
		0, 0, 0, 92, 94, 98, 95, 98, 95, 98, 94, 92, 0, 0, 0, 0,
		0, 0, 0, 93, 92, 94, 95, 92, 95, 94, 92, 93, 0, 0, 0, 0,
		0, 0, 0, 85, 90, 92, 93, 78, 93, 92, 90, 85, 0, 0, 0, 0,
		0, 0, 0, 88, 85, 90, 88, 90, 88, 90, 85, 88, 0, 0, 0, 0,
		0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
		0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
		0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
	{ // 车
		0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
		0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
		0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
		0, 0, 0, 206, 208, 207, 213, 214, 213, 207, 208, 206, 0, 0, 0, 0,
		0, 0, 0, 206, 212, 209, 216, 233, 216, 209, 212, 206, 0, 0, 0, 0,
		0, 0, 0, 206, 208, 207, 214, 216, 214, 207, 208, 206, 0, 0, 0, 0,
		0, 0, 0, 206, 213, 213, 216, 216, 216, 213, 213, 206, 0, 0, 0, 0,
		0, 0, 0, 208, 211, 211, 214, 215, 214, 211, 211, 208, 0, 0, 0, 0,
		0, 0, 0, 208, 212, 212, 214, 215, 214, 212, 212, 208, 0, 0, 0, 0,
		0, 0, 0, 204, 209, 204, 212, 214, 212, 204, 209, 204, 0, 0, 0, 0,
		0, 0, 0, 198, 208, 204, 212, 212, 212, 204, 208, 198, 0, 0, 0, 0,
		0, 0, 0, 200, 208, 206, 212, 200, 212, 206, 208, 200, 0, 0, 0, 0,
		0, 0, 0, 194, 206, 204, 212, 200, 212, 204, 206, 194, 0, 0, 0, 0,
		0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
		0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
		0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
	{ // 炮
		0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
		0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
		0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
		0, 0, 0, 100, 100, 96, 91, 90, 91, 96, 100, 100, 0, 0, 0, 0,
		0, 0, 0, 98, 98, 96, 92, 89, 92, 96, 98, 98, 0, 0, 0, 0,
		0, 0, 0, 97, 97, 96, 91, 92, 91, 96, 97, 97, 0, 0, 0, 0,
		0, 0, 0, 96, 99, 99, 98, 100, 98, 99, 99, 96, 0, 0, 0, 0,
		0, 0, 0, 96, 96, 96, 96, 100, 96, 96, 96, 96, 0, 0, 0, 0,
		0, 0, 0, 95, 96, 99, 96, 100, 96, 99, 96, 95, 0, 0, 0, 0,
		0, 0, 0, 96, 96, 96, 96, 96, 96, 96, 96, 96, 0, 0, 0, 0,
		0, 0, 0, 97, 96, 100, 99, 101, 99, 100, 96, 97, 0, 0, 0, 0,
		0, 0, 0, 96, 97, 98, 98, 98, 98, 98, 97, 96, 0, 0, 0, 0,
		0, 0, 0, 96, 96, 97, 99, 99, 99, 97, 96, 96, 0, 0, 0, 0,
		0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
		0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
		0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
	{ // 兵(卒)
		0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
		0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
		0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
		0, 0, 0, 9, 9, 9, 11, 13, 11, 9, 9, 9, 0, 0, 0, 0,
		0, 0, 0, 19, 24, 34, 42, 44, 42, 34, 24, 19, 0, 0, 0, 0,
		0, 0, 0, 19, 24, 32, 37, 37, 37, 32, 24, 19, 0, 0, 0, 0,
		0, 0, 0, 19, 23, 27, 29, 30, 29, 27, 23, 19, 0, 0, 0, 0,
		0, 0, 0, 14, 18, 20, 27, 29, 27, 20, 18, 14, 0, 0, 0, 0,
		0, 0, 0, 7, 0, 13, 0, 16, 0, 13, 0, 7, 0, 0, 0, 0,
		0, 0, 0, 7, 0, 7, 0, 15, 0, 7, 0, 7, 0, 0, 0, 0,
		0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
		0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
		0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
		0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
		0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
		0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}}</code></pre>



<h3>PositionStruct结构体</h3>



<p>打开rule.go，修改PositionStruct结构体：</p>



<pre class="wp-block-code"><code>type PositionStruct struct {
	sdPlayer    int      // 轮到谁走，0=红方，1=黑方
	vlRed       int      //红方的子力价值
	vlBlack     int      //黑方的子力价值
	nDistance   int      // 距离根节点的步数
	ucpcSquares &#91;256]int // 棋盘上的棋子
}</code></pre>



<p> vlRed和vlBlack表示红黑双方局面棋子力价值。</p>



<p>nDistance表示搜索的深度。</p>



<h3>PositionStruct方法</h3>



<p>在放入棋子(addPiece)和拿走棋子(delPiece)的时候，计算红黑方的棋力值，有了棋力值就能用评价函数(evaluate)对当前局面进行评价：</p>



<pre class="wp-block-code"><code>//addPiece 在棋盘上放一枚棋子
func (p *PositionStruct) addPiece(sq, pc int) {
	p.ucpcSquares&#91;sq] = pc
	// 红方加分，黑方(注意"cucvlPiecePos"取值要颠倒)减分
	if pc &lt; 16 {
		p.vlRed += cucvlPiecePos&#91;pc-8]&#91;sq]
	} else {
		p.vlBlack += cucvlPiecePos&#91;pc-16]&#91;squareFlip(sq)]
	}
}</code></pre>



<pre class="wp-block-code"><code>//delPiece 从棋盘上拿走一枚棋子
func (p *PositionStruct) delPiece(sq, pc int) {
	p.ucpcSquares&#91;sq] = 0
	// 红方减分，黑方(注意"cucvlPiecePos"取值要颠倒)加分
	if pc &lt; 16 {
		p.vlRed -= cucvlPiecePos&#91;pc-8]&#91;sq]
	} else {
		p.vlBlack -= cucvlPiecePos&#91;pc-16]&#91;squareFlip(sq)]
	}
}</code></pre>



<pre class="wp-block-code"><code>//evaluate 局面评价函数
func (p *PositionStruct) evaluate() int {
	if p.sdPlayer == 0 {
		return p.vlRed - p.vlBlack + AdvancedValue
	}

	return p.vlBlack - p.vlRed + AdvancedValue
}</code></pre>



<p>在startup函数里不再直接赋值，而是使用addPiece用来计算子力价值：</p>



<pre class="wp-block-code"><code>//startup 初始化棋盘
func (p *PositionStruct) startup() {
	pc := 0
	p.sdPlayer, p.vlRed, p.vlBlack, p.nDistance = 0, 0, 0, 0
	for i := 0; i &lt; 256; i++ {
		p.ucpcSquares&#91;i] = 0
	}
	for sq := 0; sq &lt; 256; sq++ {
		pc = cucpcStartup&#91;sq]
		if pc != 0 {
			p.addPiece(sq, pc)
		}
	}
}
</code></pre>



<p>在走子的时候，同步修改子力价值 ：</p>



<pre class="wp-block-code"><code>//movePiece 搬一步棋的棋子
func (p *PositionStruct) movePiece(mv int) int {
	sqSrc := src(mv)
	sqDst := dst(mv)
	pcCaptured := p.ucpcSquares&#91;sqDst]
	if pcCaptured != 0 {
		p.delPiece(sqDst, pcCaptured)
	}
	pc := p.ucpcSquares&#91;sqSrc]
	p.delPiece(sqSrc, pc)
	p.addPiece(sqDst, pc)
	return pcCaptured
}</code></pre>



<pre class="wp-block-code"><code>//undoMovePiece 撤消搬一步棋的棋子
func (p *PositionStruct) undoMovePiece(mv, pcCaptured int) {
	sqSrc := src(mv)
	sqDst := dst(mv)
	pc := p.ucpcSquares&#91;sqDst]
	p.delPiece(sqDst, pc)
	p.addPiece(sqSrc, pc)
	if pcCaptured != 0 {
		p.addPiece(sqDst, pcCaptured)
	}
}
</code></pre>



<pre class="wp-block-code"><code>//makeMove 走一步棋
func (p *PositionStruct) makeMove(mv int) (bool, int) {
	pcCaptured := p.movePiece(mv)
	if p.checked() {
		p.undoMovePiece(mv, pcCaptured)
		return false, pcCaptured
	}
	p.changeSide()
	p.nDistance++
	return true, pcCaptured
}</code></pre>



<pre class="wp-block-code"><code>//undoMakeMove 撤消走一步棋
func (p *PositionStruct) undoMakeMove(mv, pcCaptured int) {
	p.nDistance--
	p.changeSide()
	p.UndoMakeMove(mv, pcCaptured)
}</code></pre>



<p>有了对当前局面的评价，我们可以获得红黑双方的子力价值，就能找出最佳的局面，从而得出最佳的走法。</p>



<p>在下一节中，我们将学习如何使用Alpha-Beta算法搜索出最佳走法？</p>
<p><a rel="nofollow" href="https://wangqianhong.com/2020/10/%e7%94%a8go%e5%86%99%e4%b8%80%e4%b8%aa%e4%b8%ad%e5%9b%bd%e8%b1%a1%e6%a3%8b%ef%bc%88%e4%b9%9d%ef%bc%89-%e8%b1%a1%e6%a3%8bai/">用Go写一个中国象棋（九）| 象棋AI</a>最先出现在<a rel="nofollow" href="https://wangqianhong.com">wqh博客</a>。</p>
]]></content:encoded>
					
					<wfw:commentRss>https://wangqianhong.com/2020/10/%e7%94%a8go%e5%86%99%e4%b8%80%e4%b8%aa%e4%b8%ad%e5%9b%bd%e8%b1%a1%e6%a3%8b%ef%bc%88%e4%b9%9d%ef%bc%89-%e8%b1%a1%e6%a3%8bai/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
	</channel>
</rss>
