FireMonkey3D之中国象棋程序设计(五)置换表

这篇具有很好参考价值的文章主要介绍了FireMonkey3D之中国象棋程序设计(五)置换表。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

声明:本程序设计参考象棋巫师源码(开发工具dephi 11,建议用delphi 10.3以上版本)。  

这一章主要介绍置换表。本章目标:

  • 实现置换表;
  • 采用置换表走法、杀手走法等多种启发方式。

5.1 置换表   

  没有置换表,就称不上是完整的计算机博弈程序。在搜索过程中,某个搜索结果可能会出现这么多次,这浪费了很多时间。为避免重复搜索,保存搜索结果的表,就是置换表。由于哈希表的读写速度很快,通常置换表就由哈希表来实现。  

  置换表非常简单,以局面的 Zobrist Key mod HASH_SIZE 作为索引值。每个置换表项存储的内容无非就是:A. 深度,B. 标志,C. 分值,D. 最佳走法,E. Zobrist Lock 校验码。置换表结构:

{置换表结构}
type HashItem=record
  ucDepth, ucFlag:Byte;//深度、标志
  svl:SmallInt;        //分值
  wmv, wReserved:Word; //最佳走法
  dwLock0, dwLock1:Cardinal; //Zobrist检验码
end;

  置换表的处理函数也很传统——一个 ProbeHash 和一个 RecordHash 就足够了。

  先说 RecordHash,即便采用深度优先的替换策略,RecordHash 也非常简单,在判断深度后,将 Hash 表项中的每个值填上就是了。

  再看看 ProbeHash 是如何利用置换表信息的:

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

  (2) 是否能直接利用置换表中的结果,取决于两个因素:A. 深度是否达到要求,B. PV节点还需要考虑边界。

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

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

5.2 杀棋分数调整   

  增加了置换表以后,杀棋分数要进行调整——置换表中的分值不能是距离根节点的杀棋分值,而是距离当前(置换表项)节点的分值。所以当分值接近杀棋分数时,ProbeHash 和 RecordHash 都要做细微的调整:

  (1) 对于RecordHash:置换表项记录的杀棋步数 实际杀棋步数 置换表项距离根节点的步数;

  (2) 对于ProbeHash:实际杀棋步数 置换表项记录的杀棋步数 置换表项距离根节点的步数。 

5.3 杀手(Killer)走法   

  杀手走法就是兄弟节点中产生Beta截断的走法。根据国际象棋的经验,杀手走法产生截断的可能性极大。很显然,兄弟节点中的走法未必在当前节点下能走,所以在尝试杀手走法以前先要对它进行走法合理性的判断。在第二章里写过CanMove(走法是否合理)这个函数,这里它将大显身手。如果杀手走法确实产生截断了,那么后面耗时更多的 GenerateMove (生成所有走法)就可以不用执行了。

  如何保存和获取“兄弟节点中产生截断的走法”呢?我们可以把这个问题简单化——距离根节点步数(nDistance)同样多的节点,彼此都称为“兄弟”节点,换句话说,亲兄弟、堂表兄弟以及关系更疏远的兄弟都称为“兄弟”。

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

5.4 优化走法顺序

   利用各种信息渠道(如置换表、杀手走法、历史表等)来优化走法顺序的手段称为“启发”。第五章以前,我们只用历史表作启发,但从这个版本开始,我们采用了多种启发方式:

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

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

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

  这样,我们就可以构造一个状态机,来描述走法顺序的若干阶段:

  

 文章来源地址https://www.toymoban.com/news/detail-807414.html

 

 

 

 

  首先要定义走法结构:

{走法排序结构}
type SortStruct=record
  mvHash, mvKiller1, mvKiller2:Integer; // 置换表走法和两个杀手走法
  nPhase, nIndex, nGenMoves:Integer;    // 当前阶段,当前采用第几个走法,总共有几个走法
  mvs:TArray<Integer>;           // 所有的走法
  procedure Init(mvHash_:Integer);// 初始化,设定置换表走法和两个杀手走法
  function Next:Integer;  // 得到下一个走法
end;

 其中Next方法就是状态机的实现:

function SortStruct.Next:Integer;
var
  mv:Integer;
  Comparer: IComparer<Integer>;
  s,d:TPoint;
begin
  // "nPhase"表示着法启发的若干阶段,依次为:
  // 0. 置换表着法启发,完成后立即进入下一阶段;
  if nPhase=PHASE_HASH then
  begin
    nPhase:= PHASE_KILLER_1;
    if (mvHash<>0) then
      Exit(mvHash);
  end;
  // 1. 杀手着法启发(第一个杀手着法),完成后立即进入下一阶段;
  if nPhase=PHASE_KILLER_1 then
  begin
    nPhase:= PHASE_KILLER_2;
    s:=GetSrc(mvKiller1);d:=GetDest(mvKiller1);
    if (mvKiller1<> mvHash)and(mvKiller1<>0) and pcMove.CanMove(s,d) then
      Exit(mvKiller1);
  end;
  // 2. 杀手着法启发(第二个杀手着法),完成后立即进入下一阶段;
  if nPhase=PHASE_KILLER_2 then
  begin
    nPhase:= PHASE_GEN_MOVES;
    s:=GetSrc(mvKiller2);d:=GetDest(mvKiller2);
    if (mvKiller2 <>mvHash)and(mvKiller2<>0)and pcMove.canMove(s,d) then
      Exit(mvKiller2);
  end;
  // 3. 生成所有着法,完成后立即进入下一阶段;
  if nPhase=PHASE_GEN_MOVES then
  begin
    nPhase:= PHASE_REST;
    mvs:= pcMove.GenerateMoves;
    nGenMoves:=Length(mvs);
    Comparer := TComparer<Integer>.Construct(CompareHistory);
    TArray.Sort<Integer>(mvs,Comparer);
    nIndex:= 0;
  end;
  // 4. 对剩余着法做历史表启发;
  if nPhase=PHASE_REST then
  begin
    while (nIndex < nGenMoves) do
    begin
      mv:= mvs[nIndex];
      Inc(nIndex);
      if (mv<>mvHash)and(mv<>mvKiller1)and(mv<>mvKiller2) then
        Exit(mv);
    end;
  end;
  // 5. 没有着法了,返回零。
  Result:=0;
end;

  提取置换表和保存置换表的代码如下:

// 提取置换表项
function ProbeHash(vlAlpha,vlBeta,nDepth:Integer;var mv:Integer):Integer;
var
  bMate:Boolean; // 杀棋标志:如果是杀棋,那么不需要满足深度条件
  hsh:HashItem;
begin
  // 查询置换表分为以下几步:
  // 1.获取当前局面的置换表表项
  hsh:= Search.HashTable[pcMove.zobr.dwKey and (HASH_SIZE - 1)];
   // 2.判断置换表中的zobristLock校验码与当前局面是否一致
  if (hsh.dwLock0 <> pcMove.zobr.dwLock0) or(hsh.dwLock1 <> pcMove.zobr.dwLock1) then
  begin
    mv:= 0;
    Exit(-MATE_VALUE);
  end;
  mv:= hsh.wmv;
  bMate:= False;
  //3.如果是杀棋,返回与深度相关的杀棋分数。如果是长将或者和棋,返回-MATE_VALUE。
  if hsh.svl > WIN_VALUE then
  begin
    hsh.svl:=hsh.svl - pcMove.nDistance;
    bMate:= True;
  end
  else if hsh.svl < -WIN_VALUE then
  begin
    hsh.svl:=hsh.svl+ pcMove.nDistance;
    bMate:= True;
  end;
  //4.如果置换表中节点的搜索深度小于当前节点,查询失败
  if (hsh.ucDepth>= nDepth)or(bMate) then
  begin
    // 5.遇到一个beta节点,只能说明当前节点的值不小于hash.vl。
    // 如果正好hash.vl >= vlBeta,说明当前节点会产生beta阶段。否则,置换表查询失败,需要重新对该局面进行搜索。
    if hsh.ucFlag=HASH_BETA then
    begin
      if hsh.svl >=vlBeta  then Exit(hsh.svl)  else Exit(-MATE_VALUE);
    end
    // 6.遇到一个alpha节点,只能说明当前节点的值不大于hash.vl。
    // 如果正好hash.vl <= vlAlpha,说明当前节点又是一个alpha节点,并且值不大于hash.vl。否则,置换表查询失败,需要重新对该局面进行搜索。
    else if hsh.ucFlag=HASH_ALPHA then
    begin
      if hsh.svl <= vlAlpha then Exit(hsh.svl)  else Exit(-MATE_VALUE);
    end;
    Exit(hsh.svl);
  end;
  Result:=-MATE_VALUE;
end;
// 保存置换表项
procedure RecordHash(nFlag,vl,nDepth,mv:Integer);
var
  hsh:HashItem;
begin
  hsh:= Search.HashTable[pcMove.zobr.dwKey and (HASH_SIZE - 1)];// 获取当前局面的置换表表项
  if hsh.ucDepth > nDepth then    Exit; // 深度优先覆盖原则
  hsh.ucFlag:= nFlag;    // 节点类型
  hsh.ucDepth:= nDepth;  // 搜索深度
  // 如果是杀棋,需要将分值转换为与深度无关的分值。如果是长将或者和棋,又没有最佳走法,就不记入置换表。
  if vl > WIN_VALUE then
  begin
    hsh.svl:= vl + pcMove.nDistance;
  end
  else if vl < -WIN_VALUE then
  begin
    hsh.svl:= vl - pcMove.nDistance;
  end
  else
    hsh.svl:= vl;

  hsh.wmv:= mv;
  hsh.dwLock0:= pcMove.zobr.dwLock0;
  hsh.dwLock1:= pcMove.zobr.dwLock1;
  Search.HashTable[pcMove.zobr.dwKey and (HASH_SIZE - 1)]:= hsh;
end;

  SearchFull函数中生成所有走法,并逐一走这些走法被Next方法所替代:

{超出边界(Fail-Soft)的Alpha-Beta搜索过程}
function SearchFull(vlAlpha,vlBeta,nDepth:Integer;bNoNull:Boolean=False):Integer;
var
  vl, vlBest,mvBest,mvHash,nHashFlag,mv:Integer;
  s,d:TPoint;
  Sort:SortStruct;
begin
  // 一个Alpha-Beta完全搜索分为以下几个阶段
  // 1. 到达水平线,则调用静态搜索(注意:由于空步裁剪,深度可能小于零)
  if pcMove.nDistance>0 then
  begin
    if (nDepth <= 0)then
      Exit(SearchQuiesc(vlAlpha, vlBeta));
    // 1-1. 检查重复局面(注意:不要在根节点检查,否则就没有走法了)
    vl:= pcMove.RepStatus;
    if vl <> 0 then
      Exit(pcMove.RepValue(vl));
    // 1-2. 到达极限深度就返回局面评价
    if pcMove.nDistance = LIMIT_DEPTH then
      Exit(pcMove.Evaluate);
    // 1-3. 尝试置换表裁剪,并得到置换表走法
    vl:= ProbeHash(vlAlpha, vlBeta, nDepth, mvHash);
    if vl > -MATE_VALUE then
      Exit(vl);
    // 1-3. 尝试空步裁剪(根节点的Beta值是"MATE_VALUE",所以不可能发生空步裁剪)
    if (not bNoNull)and(pcMove.InCheck=False)and pcMove.NullOkay then
    begin
      pcMove.NullMove;
      vl:= -SearchFull(-vlBeta, 1 - vlBeta, nDepth - NULL_DEPTH - 1, True);//NO_NULL=True
      pcMove.UndoNullMove;
      if vl >= vlBeta then
        Exit(vl);
    end;
  end
  else
    mvHash:=0;
  // 2. 初始化最佳值和最佳走法
  nHashFlag:= HASH_ALPHA;
  vlBest:= -MATE_VALUE; // 这样可以知道,是否一个走法都没走过(杀棋)
  mvBest:=0;            // 这样可以知道,是否搜索到了Beta走法或PV走法,以便保存到历史表
  // 3. 初始化走法排序结构
  Sort.Init(mvHash);
  // 4. 逐一走这些走法,并进行递归
  with pcMove do
  while True do
  begin
    mv:=Sort.Next;
    if mv=0 then Break;
    s:=GetSrc(mv);d:=GetDest(mv);
    if MakeMove(s,d) then
    begin
      vl:= -SearchFull(-vlBeta, -vlAlpha, nDepth+InCheck.ToInteger-1);
      // 将军延伸(如果局面处于被将军的状态,或者只有一种回棋,多向下搜索一层)
      // 将军延伸或者只有一种走法也要延伸
      UndoMakeMove;
      // 5. 进行Alpha-Beta大小判断和截断
      if (vl > vlBest) then      // 找到最佳值(但不能确定是Alpha、PV还是Beta走法)
      begin
        vlBest:= vl;            // "vlBest"就是目前要返回的最佳值,可能超出Alpha-Beta边界
        if (vl >= vlBeta) then  // 找到一个Beta走法
        begin
          nHashFlag:= HASH_BETA;// 节点类型
          mvBest:= mv;          // Beta走法要保存到历史表
          break;                // Beta截断
        end;
        if (vl > vlAlpha) then  // 找到一个PV走法
        begin
          nHashFlag:= HASH_PV;  // 节点类型
          mvBest := mv;         // PV走法要保存到历史表
          vlAlpha:= vl;         // 缩小Alpha-Beta边界
        end;
      end;
    end;
  end;
  // 5. 所有走法都搜索完了,把最佳走法(不能是Alpha走法)保存到历史表,返回最佳值
  if  vlBest =-MATE_VALUE then
    Exit(pcMove.nDistance - MATE_VALUE); // 如果是杀棋,就根据杀棋步数给出评价
   // 记录到置换表
  RecordHash(nHashFlag, vlBest, nDepth, mvBest);
  if mvBest<>0 then
  begin
    // 如果不是Alpha走法,就将最佳走法保存到历史表
    SetBestMove(mvBest, nDepth);
     if pcMove.nDistance = 0 then // 搜索根节点时,总是有一个最佳走法(因为全窗口搜索不会超出边界),将这个走法保存下来
            Search.mvResult:=mvBest;
  end;
  Result:=vlBest;
end;

以上程序未经充分测试,发现问题请及时反馈。

下一章将实现以下目标:

  1. 实现开局库;
  2. 实现PVS(主要变例搜索);
  3. 把根节点的搜索单独处理,增加搜索的随机性;
  4. 克服由长将引起的置换表的不稳定性。

其他未说明的内容请参阅源码注释,如有问题,敬请指出。

本章节源码百度云盘:

链接:中国象棋程序设计(五)置换表

提取码:1234

 

 

 

到了这里,关于FireMonkey3D之中国象棋程序设计(五)置换表的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • FireMonkey3D之中国象棋程序设计(四)水平效应、检查重复局面

    声明:本程序设计参考象棋巫师源码(开发工具dephi 11,建议用delphi 10.3以上版本)。 上一章我们的程序终于会走棋了,不过很多时候它很低能。由于水平线效应,任何变化都只搜索固定的深度。还有,有时它会长将。我们能做哪些改进呢? 本章的目标: 用Zobrist校验码技术

    2024年01月20日
    浏览(38)
  • Qt CMake 中国象棋程序实现

    C++自学精简实践教程 目录(必读) C++数据结构与算法实现(目录) Qt 入门实战教程(目录) 为学习 Qt 的人提供一个合适的有一定难度的综合型练习项目。 在学会写代码之前,先看别人怎么写的代码。深入其中,扩展完善。 最大限度的模拟企业开发的真实场景。 运行效果 中

    2024年02月09日
    浏览(40)
  • 从0开始写中国象棋-创建棋盘与棋子

    考虑到象棋程序,其实就是数据结构与算法实现。 所以和界面相关的QT部分我们先放一放。 我们从控制台版本开始。这样大家更容易接受,也不影响开发。 后面我们会把控制台嫁接到QT上完成完整的游戏,那时候自然就水到渠成了。 中国象棋的棋盘是一个宽9列,长 5+5 = 10

    2024年02月07日
    浏览(37)
  • 中国象棋AI库AlphaZero_ChineseChess

    AlphaZero_ChineseChess是一个基于AlphaZero算法的中国象棋AI库,它是开源的,使用Python语言编写,托管在GitHub上。以下是对AlphaZero_ChineseChess库的详细介绍: 算法原理 AlphaZero_ChineseChess基于AlphaZero算法,这是一种基于自我对弈的强化学习算法,能够让AI自主学习棋局的优劣、评估策略

    2024年02月14日
    浏览(122)
  • C++实现双人中国象棋(一)——算法篇(附完整代码)

    最近突发奇想,要使用C++做一个双人象棋的程序,昨天肝了一天,终于把算法部分完成了,下面把开发过程中的经验分享一下。 开发环境:Visual Studio 2019 语言标准:C++11及以上 纠错:暂无 知识要求: 熟练掌握C++语言面向对象编程的知识(继承,多态) 掌握STL的基本操作 了

    2024年02月04日
    浏览(61)
  • C++900行代码实现中国象棋游戏规则以及相关功能

    本文章通过C++中的900行代码实现中国象棋游戏以及相关功能,主要的内容如下: 1.设置未进入游戏前的主页面; 2.绘制棋盘(如果有刚好尺寸的图片也可直接加载),包括棋盘网格,炮与兵的特殊标记绘制; 3.绘制和创建棋子,并令其初始化在棋盘的相应位置; 4.游戏开始,

    2024年02月09日
    浏览(49)
  • AI人工智能(调包侠)速成之路十五(中国象棋AI网络机器人:AI模型部署)

    神经网络模型动态加解密的技术这个以后再写吧  书接上文: AI人工智能(调包侠)速成之路十四(中国象棋AI网络机器人:AI技术综合应用实现) 神经网络模型的存储格式         我们训练的神经网络就是一堆模拟神经元的参数集合,给(他/她/它)一个输入信息就会得到

    2024年01月23日
    浏览(50)
  • 从0实现基于Alpha zero的中国象棋AI(会分为多个博客,此处讲解蒙特卡洛树搜索)

    ​ 题主对于阿尔法狗的实现原理好奇,加上毕业在即,因此选择中国象棋版的阿尔法zero,阿尔法zero是阿尔法狗的升级版。在完成代码编写的历程中,深刻感受到深度学习环境的恶劣,网络上固然资料繁多,但要么水平不行,不知所云,要么国外课程,门槛过高。因而碰壁良

    2024年02月06日
    浏览(52)
  • html Antv L7 + mapbox 实现3D地图 3D中国地图 不限于中国地图

    echarts的3D地图实在太丑了,还一堆bug 使用阿里的Antv可视化库L7,实现3D地图,底图是mapbox 参考示例:https://l7.antv.antgroup.com/zh/examples/polygon/3d#floatMap 如果不需要底图样式,可把Scene的style设置为blank 直接上代码了,vue的就不说了,项目是html的 mapbox依赖 L7依赖 body元素 实现

    2024年02月14日
    浏览(52)
  • echarts 5.0——3d中国地图和飞线

    echarts 5.0的版本样式语法与4.0及以下的语法有个别差异,使用旧语法浏览器会警告提示。 3d地图常用的实现方法有两种。一种是使用GL来实现,一种是使用叠层和阴影来实现,本文将使用叠层和阴影来实现。先看效果图: 1. 一定要使用 echarts 5.0及以上的版本; 2. echarts 5.0没有

    2024年04月13日
    浏览(36)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包