<?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>Go &#8211; wqh博客</title>
	<atom:link href="https://wangqianhong.com/tag/go/feed/" rel="self" type="application/rss+xml" />
	<link>https://wangqianhong.com</link>
	<description>和而不同</description>
	<lastBuildDate>Sun, 12 Oct 2025 15:17:39 +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>Go &#8211; wqh博客</title>
	<link>https://wangqianhong.com</link>
	<width>32</width>
	<height>32</height>
</image> 
	<item>
		<title>Hugo 介绍</title>
		<link>https://wangqianhong.com/2022/10/hugo-%e4%bb%8b%e7%bb%8d/</link>
					<comments>https://wangqianhong.com/2022/10/hugo-%e4%bb%8b%e7%bb%8d/#respond</comments>
		
		<dc:creator><![CDATA[wqh_work]]></dc:creator>
		<pubDate>Sat, 22 Oct 2022 00:36:00 +0000</pubDate>
				<category><![CDATA[技术文章]]></category>
		<category><![CDATA[Go]]></category>
		<guid isPermaLink="false">https://wangqianhong.com/?p=3898</guid>

					<description><![CDATA[<p>概览 Hugo 是用 Go 写的高速静态站点生成器（Static Site Generator），以&#8230; <a href="https://wangqianhong.com/2022/10/hugo-%e4%bb%8b%e7%bb%8d/" class="more-link read-more" rel="bookmark">继续阅读 <span class="screen-reader-text">Hugo 介绍</span><i class="fa fa-arrow-right"></i></a></p>
<p><a rel="nofollow" href="https://wangqianhong.com/2022/10/hugo-%e4%bb%8b%e7%bb%8d/">Hugo 介绍</a>最先出现在<a rel="nofollow" href="https://wangqianhong.com">wqh博客</a>。</p>
]]></description>
										<content:encoded><![CDATA[
<h3>概览</h3>



<p>Hugo 是用 <strong>Go</strong> 写的高速静态站点生成器（Static Site Generator），以超快的构建速度、灵活的模板系统和丰富的功能集合著称，适合博客、文档、公司站、Landing Page 等场景。 (<a href="https://github.com/gohugoio/hugo">GitHub</a>)</p>



<h3>核心特点</h3>



<ul><li><strong>极快的构建速度</strong>：能在秒级甚至毫秒/页级别生成大型站点（这也是 Hugo 的主要卖点）。 (<a href="https://gohugo.io/?utm_source=chatgpt.com">gohugo.io</a>)</li><li><strong>单个二进制即可运行</strong>：Hugo 是一个可独立运行的二进制 CLI，安装/部署非常简单。 (<a href="https://github.com/gohugoio/hugo">GitHub</a>)</li><li><strong>基于文件的内容模型</strong>：内容通常用 Markdown（或其他格式）放在 <code>content/</code>，通过 front matter（YAML/TOML/JSON）控制元数据与 taxonomies（分类/标签）。 (<a href="https://gohugo.io/about/introduction/?utm_source=chatgpt.com">gohugo.io</a>)</li><li><strong>强大的模板系统</strong>：使用 Go template 引擎，支持短代码（shortcodes）、布局继承、局部模板、数据模板等。 (<a href="https://gohugo.io/about/introduction/?utm_source=chatgpt.com">gohugo.io</a>)</li><li><strong>内建资源处理（assets）</strong>：支持 image processing、SCSS/SASS 管道、JS/CSS 压缩、Fingerprinting 等静态资源管线。 (<a href="https://gohugo.io/about/introduction/?utm_source=chatgpt.com">gohugo.io</a>)</li><li><strong>多语言/国际化（i18n）支持</strong>：内建多语言站点支持。 (<a href="https://gohugo.io/about/introduction/?utm_source=chatgpt.com">gohugo.io</a>)</li><li><strong>丰富的主题生态</strong>：有官方/社区维护的主题库，可直接套用或作为起点。 (<a href="https://github.com/gohugoio/hugoThemesSiteBuilder?utm_source=chatgpt.com">GitHub</a>)</li></ul>



<h3>关键概念</h3>



<ul><li><strong>content/</strong>：你的 Markdown 文件和目录结构（每个文件是一个“页面”或文章）。 (<a href="https://gohugo.io/about/introduction/?utm_source=chatgpt.com">gohugo.io</a>)</li><li><strong>layouts/</strong>：模板目录（决定最终 HTML 的呈现）。 (<a href="https://gohugo.io/about/introduction/?utm_source=chatgpt.com">gohugo.io</a>)</li><li><strong>archetypes/</strong>：新建文章的模板（如 <code>hugo new</code> 时使用）。 (<a href="https://gohugo.io/about/introduction/?utm_source=chatgpt.com">gohugo.io</a>)</li><li><strong>shortcodes</strong>：在 Markdown 中使用的可复用模板片段（比如嵌入视频、响应式图集等）。 (<a href="https://gohugo.io/about/introduction/?utm_source=chatgpt.com">gohugo.io</a>)</li><li><strong>data/</strong>：可以放 JSON/YAML/TOML 的数据文件，在模板中直接读取并渲染（适合制作动态目录或外部数据驱动的页面）。 (<a href="https://gohugo.io/about/introduction/?utm_source=chatgpt.com">gohugo.io</a>)</li></ul>



<h3>内部实现 &amp; 技术亮点</h3>



<ul><li>用 <strong>Go</strong> 实现：跨平台构建、单二进制分发，编译后的二进制体积小、启动快。 (<a href="https://github.com/gohugoio/hugo">GitHub</a>)</li><li>模板基于 Go 的 <code>text/template</code>/<code>html/template</code>，性能与安全性好，但模板语法与某些其他 SSG（如 Liquid）略有差异，需要适应。 (<a href="https://gohugo.io/about/introduction/?utm_source=chatgpt.com">gohugo.io</a>)</li><li>静态资源处理（image resizing、Sass/SCSS、concat/minify、fingerprint）内置在 Hugo 的管道里，减少额外构建工具依赖。 (<a href="https://gohugo.io/about/introduction/?utm_source=chatgpt.com">gohugo.io</a>)</li></ul>



<h3>常用命令</h3>



<p>假设已安装 <code>hugo</code> 二进制（见安装文档）：</p>



<pre class="wp-block-code"><code># 新建站点
hugo new site mysite

# 本地预览（开发服务器，实时热重载）
hugo server -D

# 新建一篇文章（会根据 archetype 自动填 front matter）
hugo new posts/my-first-post.md

# 生成静态站点（输出到 public/）
hugo -v

# 指定输出目录
hugo --destination ./dist
</code></pre>



<p>（这些命令与教程在官方文档/入门页可查到）。 (<a href="https://gohugo.io/about/introduction/?utm_source=chatgpt.com">gohugo.io</a>)</p>



<h3>部署与托管</h3>



<p>Hugo 生成静态文件后可托管到任何静态站点主机或 CDN：</p>



<ul><li><strong>GitHub Pages</strong>（通过 gh-pages 或 actions 自动部署）。 (<a href="https://gohugo.io/host-and-deploy/host-on-github-pages/?utm_source=chatgpt.com">gohugo.io</a>)</li><li><strong>Netlify</strong>（支持自动构建、预览分支、表单、函数等）。 (<a href="https://docs.netlify.com/build/frameworks/framework-setup-guides/hugo/?utm_source=chatgpt.com">Netlify Docs</a>)</li><li><strong>Cloudflare Pages / AWS S3 + CloudFront / Vercel</strong> 等静态托管或边缘平台均可。 (<a href="https://developers.cloudflare.com/pages/framework-guides/deploy-a-hugo-site/?utm_source=chatgpt.com">Cloudflare Docs</a>)</li></ul>



<h3>生态与社区</h3>



<ul><li><strong>主题库</strong>：官方/社区主题集中在 themes.gohugo.io（也有 GitHub 仓库索引）。 (<a href="https://github.com/gohugoio/hugoThemesSiteBuilder?utm_source=chatgpt.com">GitHub</a>)</li><li><strong>活跃发布与维护</strong>：Hugo 发布频繁（可查看 Releases 页面与官方 News 列表），社区活跃。 最近若干次小版本和功能更新详见 Releases。 (<a href="https://github.com/gohugoio/hugo/releases?utm_source=chatgpt.com">GitHub</a>)</li><li><strong>文档齐全</strong>：官方文档覆盖安装、模板、管道、部署等，适合查阅与学习。 (<a href="https://gohugo.io/about/introduction/?utm_source=chatgpt.com">gohugo.io</a>)</li></ul>



<h3>优点 / 适用场景</h3>



<p><strong>优点</strong></p>



<ul><li>构建速度快（大型站点也能秒级完成）。 (<a href="https://gohugo.io/?utm_source=chatgpt.com">gohugo.io</a>)</li><li>无需运行时服务器（生成静态文件后可放 CDN），运维成本低。 (<a href="https://gohugo.io/about/introduction/?utm_source=chatgpt.com">gohugo.io</a>)</li><li>内置丰富的功能（图像处理、管道、多语言、taxonomy），减少外部工具依赖。 (<a href="https://gohugo.io/about/introduction/?utm_source=chatgpt.com">gohugo.io</a>)</li></ul>



<p><strong>适用场景</strong></p>



<ul><li>个人/团队博客、技术文档站、公司站、产品落地页、知识库（非交互式）等。 (<a href="https://gohugo.io/?utm_source=chatgpt.com">gohugo.io</a>)</li></ul>



<h3>缺点 / 需要注意的点</h3>



<ul><li><strong>不是动态应用框架</strong>：对需要用户交互（登录、动态评论、实时功能）的场景，需要额外接入服务（如 Netlify Functions、第三方评论、后台 API）。</li><li><strong>模板学习曲线</strong>：Go template 的语法/模板逻辑对新手可能有点不直观（尤其从 Liquid/Handlebars 转来）。 (<a href="https://gohugo.io/about/introduction/?utm_source=chatgpt.com">gohugo.io</a>)</li><li><strong>部分高级功能依赖构建时逻辑</strong>：例如需要 CMS（内容管理后台）可以用 Netlify CMS、Forestry、Netlify CMS 等第三方或 headless CMS 来配合。<br>（这些是选择 SSG 时通用的权衡点。）</li></ul>



<h3>实操建议</h3>



<ol><li>先在本地用 <code>hugo new site</code> + 一个社区主题 快速搭建并用 <code>hugo server -D</code> 调试。 (<a href="https://gohugo.io/about/introduction/?utm_source=chatgpt.com">gohugo.io</a>)</li><li>把站点托管在 Git 仓库，配置 CI（GitHub Actions / GitLab CI / Netlify）在 push 时自动 <code>hugo</code> 构建并部署到静态托管服务。 (<a href="https://gohugo.io/host-and-deploy/host-on-github-pages/?utm_source=chatgpt.com">gohugo.io</a>)</li><li>若要图像优化或 Sass 管理，优先尝试 Hugo 内置的资源管道，必要时再补充外部构建工具。 (<a href="https://gohugo.io/about/introduction/?utm_source=chatgpt.com">gohugo.io</a>)</li></ol>



<h3>参考与文档</h3>



<ul><li>官方 GitHub 仓库（源码、Issues、Releases）：gohugoio/hugo。 (<a href="https://github.com/gohugoio/hugo">GitHub</a>)</li><li>官方站点与文档（入门、模板、管道、部署）：gohugo.io。 (<a href="https://gohugo.io/?utm_source=chatgpt.com">gohugo.io</a>)</li><li>Releases（查看最近版本、变更日志）：GitHub Releases。 (<a href="https://github.com/gohugoio/hugo/releases?utm_source=chatgpt.com">GitHub</a>)</li><li>主题库（themes.gohugo.io / 主题索引仓库）。 (<a href="https://github.com/gohugoio/hugoThemesSiteBuilder?utm_source=chatgpt.com">GitHub</a>)</li></ul>
<p><a rel="nofollow" href="https://wangqianhong.com/2022/10/hugo-%e4%bb%8b%e7%bb%8d/">Hugo 介绍</a>最先出现在<a rel="nofollow" href="https://wangqianhong.com">wqh博客</a>。</p>
]]></content:encoded>
					
					<wfw:commentRss>https://wangqianhong.com/2022/10/hugo-%e4%bb%8b%e7%bb%8d/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Go pprof</title>
		<link>https://wangqianhong.com/2021/08/go-pprof/</link>
					<comments>https://wangqianhong.com/2021/08/go-pprof/#respond</comments>
		
		<dc:creator><![CDATA[wqh_work]]></dc:creator>
		<pubDate>Sat, 14 Aug 2021 01:51:28 +0000</pubDate>
				<category><![CDATA[技术文章]]></category>
		<category><![CDATA[Go]]></category>
		<guid isPermaLink="false">https://wangqianhong.com/?p=1685</guid>

					<description><![CDATA[<p>在平常的开发中，golang为我们提供了非常方便的性能测试工具&#8211;pprof 激活ppro&#8230; <a href="https://wangqianhong.com/2021/08/go-pprof/" class="more-link read-more" rel="bookmark">继续阅读 <span class="screen-reader-text">Go pprof</span><i class="fa fa-arrow-right"></i></a></p>
<p><a rel="nofollow" href="https://wangqianhong.com/2021/08/go-pprof/">Go pprof</a>最先出现在<a rel="nofollow" href="https://wangqianhong.com">wqh博客</a>。</p>
]]></description>
										<content:encoded><![CDATA[
<p>在平常的开发中，golang为我们提供了非常方便的性能测试工具&#8211;pprof</p>



<h3>激活pprof</h3>



<p>在main函数中，增加下面代码用来激活pprof：</p>



<pre class="wp-block-code"><code>go func() {
	http.ListenAndServe("localhost:10239", nil)
}()</code></pre>



<p>这段代码就是在程序中创建一个http的监听服务</p>



<h3>pprof在线查看</h3>



<p>当运行代码时，就可以在<a href="http://localhost:12003/debug/pprof/" target="_blank" rel="noreferrer noopener">http://localhost:10239/debug/pprof/</a>中查看相关数据：</p>



<pre class="wp-block-preformatted">/debug/pprof/
Types of profiles available:
Count Profile
146 allocs
0 block
0 cmdline
70 goroutine
146 heap
0 mutex
0 profile
10 threadcreate
0 trace
full goroutine stack dump
Profile Descriptions:</pre>



<p>allocs：在过去时间内分配的栈内存</p>



<p>block：导致同步阻塞的堆栈跟踪</p>



<p>cmdline：当前程序的命令行调用</p>



<p>goroutine：所有当前 goroutine 的堆栈跟踪</p>



<p>heap：在过去时间内分配的堆内存。 您可以指定 gc GET 参数以在获取堆样本之前运行 GC。</p>



<p>mutex：竞争互斥锁持有者的堆栈跟踪</p>



<p>profile：CPU profile。 您可以在 seconds GET 参数中指定持续时间。 获取profile文件后，使用 go tool pprof 命令调查配置文件。</p>



<p>threadcreate：导致创建新操作系统线程的堆栈跟踪</p>



<p>trace：当前程序执行的轨迹。 您可以在 seconds GET 参数中指定持续时间。 获取跟踪文件后，使用 go tool trace 命令调查跟踪。</p>



<h3>graphviz安装</h3>



<p>pprof 能够借助 grapgviz 图形化，生成一个 svg 格式的文件，直接在浏览器里打开。</p>



<p>下载链接<a href="https://graphviz.org/download/" target="_blank" rel="noreferrer noopener">https://graphviz.org/download/</a>，这里下载windows的对应版本windows_10_cmake_Release_graphviz-install-3.0.0-win64.exe，下载完成后安装即可。</p>



<p>也可以下载windows_10_msbuild_Release_graphviz-3.0.0-win32.zip，下载完成后解压，然后把解压路径配置到系统环境变量下面即可。</p>



<h3>pporf命令查看</h3>



<p>一般使用命令查看会更清楚些，在安装玩grapgviz之后，会自动在浏览器中打开,也可也使用web命令打开，下面是一些常用命令：</p>



<p>查看 30 秒内的 CPU 信息</p>



<pre class="wp-block-code"><code>go tool pprof http://localhost:10239/debug/pprof/profile?seconds=30</code></pre>



<p>查看堆栈调用信息</p>



<pre class="wp-block-code"><code>go tool pprof http://localhost:10239/debug/pprof/heap</code></pre>



<p>查看 goroutine 阻塞</p>



<pre class="wp-block-code"><code>go tool pprof http://localhost:10239/debug/pprof/block</code></pre>



<p>收集 5 秒内的执行路径</p>



<pre class="wp-block-code"><code>go tool pprof http://localhost:10239/debug/pprof/trace?seconds=5</code></pre>



<p>互斥锁的堆栈跟踪</p>



<pre class="wp-block-code"><code>go tool pprof http://localhost:10239/debug/pprof/mutex</code></pre>



<h3>查看远程服务器的pprof</h3>



<p>有时候我们需要查看远程服务器的性能情况，可以用下面的命令</p>



<pre class="wp-block-code"><code>go tool pprof -http localhost:10239 localhost:10239/debug/pprof/heap?seconds=30</code></pre>



<p>把localhost:10239改成对应的服务器IP和端口即可</p>
<p><a rel="nofollow" href="https://wangqianhong.com/2021/08/go-pprof/">Go pprof</a>最先出现在<a rel="nofollow" href="https://wangqianhong.com">wqh博客</a>。</p>
]]></content:encoded>
					
					<wfw:commentRss>https://wangqianhong.com/2021/08/go-pprof/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Linux搭建Golang开发环境</title>
		<link>https://wangqianhong.com/2021/07/linux%e6%90%ad%e5%bb%bagolang%e5%bc%80%e5%8f%91%e7%8e%af%e5%a2%83/</link>
					<comments>https://wangqianhong.com/2021/07/linux%e6%90%ad%e5%bb%bagolang%e5%bc%80%e5%8f%91%e7%8e%af%e5%a2%83/#comments</comments>
		
		<dc:creator><![CDATA[wqh_work]]></dc:creator>
		<pubDate>Sun, 25 Jul 2021 04:06:39 +0000</pubDate>
				<category><![CDATA[技术文章]]></category>
		<category><![CDATA[Centos]]></category>
		<category><![CDATA[Go]]></category>
		<guid isPermaLink="false">https://wangqianhong.com/?p=1673</guid>

					<description><![CDATA[<p>今天我们来学习如何在Linux环境下搭建Go开发环境，这里的系统是Centos。 下载安装包 打开官&#8230; <a href="https://wangqianhong.com/2021/07/linux%e6%90%ad%e5%bb%bagolang%e5%bc%80%e5%8f%91%e7%8e%af%e5%a2%83/" class="more-link read-more" rel="bookmark">继续阅读 <span class="screen-reader-text">Linux搭建Golang开发环境</span><i class="fa fa-arrow-right"></i></a></p>
<p><a rel="nofollow" href="https://wangqianhong.com/2021/07/linux%e6%90%ad%e5%bb%bagolang%e5%bc%80%e5%8f%91%e7%8e%af%e5%a2%83/">Linux搭建Golang开发环境</a>最先出现在<a rel="nofollow" href="https://wangqianhong.com">wqh博客</a>。</p>
]]></description>
										<content:encoded><![CDATA[
<p>今天我们来学习如何在Linux环境下搭建Go开发环境，这里的系统是Centos。</p>



<h3>下载安装包</h3>



<p>打开官方链接：<a href="https://golang.google.cn/dl/" target="_blank" rel="noreferrer noopener">https://golang.google.cn/dl/</a></p>



<p>下载对应的版本，这里选择go1.18.linux-amd64.tar.gz。</p>



<h3>安装Go</h3>



<p>把包上传到Centos，然后使用root账号进行安装，命令如下 ：</p>



<pre class="wp-block-code"><code>rm -rf /usr/local/go &amp;&amp; tar -C /usr/local -xzf go1.18.linux-amd64.tar.gz</code></pre>



<h3>配置环境变量</h3>



<p>打开<code>/etc/profile</code>文件，在文件最后加上一行：</p>



<pre class="wp-block-code"><code>export PATH=$PATH:/usr/local/go/bin</code></pre>



<p>保存退出，再使用命令使其生效：</p>



<pre class="wp-block-code"><code>source /etc/profile</code></pre>



<h3>Go开发配置</h3>



<p>使用命令验证是否安装成功：</p>



<pre class="wp-block-code"><code>go version</code></pre>



<p>如果看到go version go1.18 linux/amd6表示安装成功。</p>



<p>最后我们再配置一下go的中国镜像：</p>



<pre class="wp-block-code"><code>go env -w GOPROXY=https://goproxy.cn</code></pre>



<p>如果你是Windows开发环境可以参考我的另一篇博文：</p>



<figure class="wp-block-embed-wordpress wp-block-embed is-type-wp-embed is-provider-wqh博客"><div class="wp-block-embed__wrapper">
<blockquote class="wp-embedded-content" data-secret="tKiOhkpgyq"><a href="https://wangqianhong.com/2020/09/win10%e6%90%ad%e5%bb%bagolang%e5%bc%80%e5%8f%91%e7%8e%af%e5%a2%83/">Win10搭建Golang开发环境</a></blockquote><iframe class="wp-embedded-content" sandbox="allow-scripts" security="restricted" title="《Win10搭建Golang开发环境》—wqh博客" src="https://wangqianhong.com/2020/09/win10%e6%90%ad%e5%bb%bagolang%e5%bc%80%e5%8f%91%e7%8e%af%e5%a2%83/embed/#?secret=tKiOhkpgyq" data-secret="tKiOhkpgyq" width="500" height="282" frameborder="0" marginwidth="0" marginheight="0" scrolling="no"></iframe>
</div></figure>
<p><a rel="nofollow" href="https://wangqianhong.com/2021/07/linux%e6%90%ad%e5%bb%bagolang%e5%bc%80%e5%8f%91%e7%8e%af%e5%a2%83/">Linux搭建Golang开发环境</a>最先出现在<a rel="nofollow" href="https://wangqianhong.com">wqh博客</a>。</p>
]]></content:encoded>
					
					<wfw:commentRss>https://wangqianhong.com/2021/07/linux%e6%90%ad%e5%bb%bagolang%e5%bc%80%e5%8f%91%e7%8e%af%e5%a2%83/feed/</wfw:commentRss>
			<slash:comments>1</slash:comments>
		
		
			</item>
		<item>
		<title>chromedp Linux环境安装</title>
		<link>https://wangqianhong.com/2021/06/chromedp-linux%e7%8e%af%e5%a2%83%e5%ae%89%e8%a3%85/</link>
					<comments>https://wangqianhong.com/2021/06/chromedp-linux%e7%8e%af%e5%a2%83%e5%ae%89%e8%a3%85/#respond</comments>
		
		<dc:creator><![CDATA[wqh_work]]></dc:creator>
		<pubDate>Sun, 06 Jun 2021 01:38:00 +0000</pubDate>
				<category><![CDATA[技术文章]]></category>
		<category><![CDATA[Go]]></category>
		<guid isPermaLink="false">https://wangqianhong.com/?p=1500</guid>

					<description><![CDATA[<p>首先安装chromedp库： go get -u github.com/chromedp/chrom&#8230; <a href="https://wangqianhong.com/2021/06/chromedp-linux%e7%8e%af%e5%a2%83%e5%ae%89%e8%a3%85/" class="more-link read-more" rel="bookmark">继续阅读 <span class="screen-reader-text">chromedp Linux环境安装</span><i class="fa fa-arrow-right"></i></a></p>
<p><a rel="nofollow" href="https://wangqianhong.com/2021/06/chromedp-linux%e7%8e%af%e5%a2%83%e5%ae%89%e8%a3%85/">chromedp Linux环境安装</a>最先出现在<a rel="nofollow" href="https://wangqianhong.com">wqh博客</a>。</p>
]]></description>
										<content:encoded><![CDATA[
<p>首先安装chromedp库：</p>



<p><code>go get -u github.com/chromedp/chromedp</code></p>



<p>然后打开链接<a href="https://www.google.cn/chrome/" target="_blank" rel="noreferrer noopener">https://www.google.cn/chrome/</a>，拉到最下面选择其他平台，选择linux下载：</p>



<figure class="wp-block-image size-large"><img loading="lazy" width="1297" height="852" src="https://wangqianhong.com/wp-content/uploads/2021/08/chrome.png" alt="" class="wp-image-1501" srcset="https://wangqianhong.com/wp-content/uploads/2021/08/chrome.png 1297w, https://wangqianhong.com/wp-content/uploads/2021/08/chrome-768x504.png 768w, https://wangqianhong.com/wp-content/uploads/2021/08/chrome-457x300.png 457w" sizes="(max-width: 1297px) 100vw, 1297px" /></figure>



<p>最后把安装包上传到服务器，使用命令安装即可：</p>



<p><code>dnf localinstall google-chrome-stable_current_x86_64.rpm</code></p>
<p><a rel="nofollow" href="https://wangqianhong.com/2021/06/chromedp-linux%e7%8e%af%e5%a2%83%e5%ae%89%e8%a3%85/">chromedp Linux环境安装</a>最先出现在<a rel="nofollow" href="https://wangqianhong.com">wqh博客</a>。</p>
]]></content:encoded>
					
					<wfw:commentRss>https://wangqianhong.com/2021/06/chromedp-linux%e7%8e%af%e5%a2%83%e5%ae%89%e8%a3%85/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<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>
	</channel>
</rss>
