[转载] 中国象棋软件-引擎实现(四)搜索算法

mane 2021-12-10 原文

对于棋类软件的搜索算法经前人的努力已形成了较为成熟的Alpha-Beta搜索算法以及其它一些辅助增强算法。所以小生在自己的程序中直接借鉴了Alpha-Beta搜索算法并辅以了历史启发。对此两者王小春的《PC 游戏编程(人机博弈)》和ElephantBoard的主页 http://www.elephantbase.net/上都有非常详细的介绍,所以这里我只简单介绍一下算法的主体思想:

首先对于棋类游戏存在一棵“博弈树”——树的每一个结点代表棋盘上的一个局面,对每一个局面(结点)根据不同的走法又产生不同的局面(生出新的结点),如此不断直到再无可选择的走法(棋局结束)。

现在假定甲乙两方下棋,甲胜的局面是一个极大值(一个正数),那么乙胜的局面则是一个极小值(极大值的负值),和棋的局面则是零值或是接近零的值。如此,则轮到甲走棋时他会选择生成结点的值最大的走法,相反轮到乙走棋时他会选择生成结点的值最小的走法(当然,搜索会向下推导多步后选择一个分值对当前下棋方有利的着法)。这就是“最大-最小”的基本思想。这样搜索函数可以通过搜索以当前局面为根结点、限定层数以内的整棵树来获得一个最佳的着法。不幸的是,博弈树相当庞大(它会成指数增长),因而搜索整棵树是一件相当费时的工作。

然而,下棋是一个你来我往的交替进行并且相互“较劲”的过程,由于每一方都会尽可能将局面导向对自己有利而对对方不利的形势。所以有些“暂时”看来很不错的局面由于可能会产生很糟糕的局面因而根本没有考虑的价值。所以当你看到某个局面有可能产生很糟糕的局面时(确切地说这里的“很糟糕”是与之前所分析的情况相比较而言的),你应当立刻停止对其剩余子结点的分析——不要对它再报任何幻想了,如果你选择了它,则你必将得到那个很糟糕的局面,甚至更糟……这样一来便可以很大程度上减少搜索的工作量,提高搜索效率。这称为“树的剪裁”。为了便于大家理解,下面我援引ElephantBoard的主页http://www.elephantbase.net/上所翻译的《Alpha-Beta搜索》中的一个“口袋的例子”,原文作者是Bruce Moreland (brucemo@seanet.com)。
口袋的例子:

比如你的死敌面前有很多口袋,他和你打赌赌输了,因此他必须从中给你一样东西,而挑选规则却非常奇怪:
每个口袋里有几件物品,你能取其中的一件,你来挑这件物品所在的口袋,而他来挑这个口袋里的物品。你要赶紧挑出口袋并离开,因为你不愿意一直做在那里翻口袋而让你的死敌盯着你。
假设你一次只能找一只口袋,在找口袋时一次只能从里面摸出一样东西。
很显然,当你挑出口袋时,你的死敌会把口袋里最糟糕的物品给你,因此你的目标是挑出“诸多最糟的物品当中是最好的”那个口袋。
你很容易把最小-最大原理运用到这个问题上。你是最大一方棋手,你将挑出最好的口袋。而你的死敌是最小一方棋手,他将挑出最好的口袋里尽可能差的物品。运用最小-最大原理,你需要做的就是挑一个有“最好的最差的”物品的口袋。
假设你可以估计口袋里每个物品的准确价值的话,最小-最大原理可以让你作出正确的选择。我们讨论的话题中,准确评价并不重要,因为它同最小-最大或Alpha-Beta的工作原理没有关系。现在我们假设你可以正确地评价物品。
最小-最大原理刚才讨论过,它的问题是效率太低。你必须看每个口袋里的每件物品,这就需要花很多时间。
那么怎样才能做得比最小-最大更高效呢?
我们从第一个口袋开始,看每一件物品,并对口袋作出评价。比方说口袋里有一只花生黄油三明治和一辆新汽车的钥匙。你知道三明治更糟,因此如果你挑了这只口袋就会得到三明治。事实上只要我们假设对手也会跟我们一样正确评价物品,那么口袋里的汽车钥匙就是无关紧要的了。
现在你开始翻第二个口袋,这次你采取的方案就和最小-最大方案不同了。你每次看一件物品,并跟你能得到的最好的那件物品(三明治)去比较。只要物品比三明治更好,那么你就按照最小-最大方案来办——去找最糟的,或许最糟的要比三明治更好,那么你就可以挑这个口袋,它比装有三明治的那个口袋好。
比方这个口袋里的第一件物品是一张20美元的钞票,它比三明治好。如果包里其他东西都没比这个更糟了,那么如果你选了这个口袋,它就是对手必须给你的物品,这个口袋就成了你的选择。
这个口袋里的下一件物品是六合装的流行唱片。你认为它比三明治好,但比20美元差,那么这个口袋仍旧可以选择。再下一件物品是一条烂鱼,这回比三明治差了。于是你就说“不谢了”,把口袋放回去,不再考虑它了。
无论口袋里还有什么东西,或许还有另一辆汽车的钥匙,也没有用了,因为你会得到那条烂鱼。或许还有比烂鱼更糟的东西(那么你看着办吧)。无论如何烂鱼已经够糟的了,而你知道挑那个有三明治的口袋肯定会更好。

“最大-最小”的思想再加上“对树的剪裁”就是Alpha-Beta搜索算法的核心。
下面就是CChessSearch.h的代码实现,其中涉及的“历史启发”和“着法排序”的具体实现会在下篇文章中给出。

// CChessSearch.h   

#include "HistoryHeuristic.h"
#include "SortMove.h"
#include "CChessEvaluate.h"   

/////////////////// Data Define ///////////////////////////////////////////////   

int nMaxSearchDepth;  // 最大搜索深度
CCHESSMOVE cmBestMove;  // 存储最佳走法   

/////////////////// Function Prototype ////////////////////////////////////////   

// 通过AlphaBeta搜索+历史启发搜索得到一部最佳着法并执行之
CCHESSMOVE SearchAGoodMove();   

// AlphaBeta搜索+历史启发,nDepth为搜索深度,
// alpha初始为极小值,beta初始为极大值
int AlphaBeta_HH( int nDepth, int alpha, int beta );   

// 判断游戏是否结束,若结束则根据当前下棋方返回相应的极值,否则返回0
//  当前下棋方胜则返回极大值,当前下棋方败则返回极小值(下棋方追求极大值)
int IsGameOver( int fWhoseTurn );   

// 执行着法,返回ptTo位置的棋子状况。即若吃掉子返回被吃掉的子,没有吃子则返回0
BYTE DoMove( CCHESSMOVE * move );   

// 撤销执行着法,恢复原位置的棋子状况。nCChessID为原ptTo位置的棋子状况
void UndoMove( CCHESSMOVE * move, BYTE nCChessID );   

////////////////// Programmer-Defined Function ////////////////////////////////   

CCHESSMOVE SearchAGoodMove()
{
  ResetHistoryTable();
  int i;
  i = AlphaBeta_HH( nMaxSearchDepth, -MaxValue, MaxValue );   

  DoMove( &cmBestMove );   

  return cmBestMove;
}   

int AlphaBeta_HH( int nDepth, int alpha, int beta )
{
  int nScore;
  int nCount;
  BYTE nCChessID;   

  int i;
  i = IsGameOver( ( nMaxSearchDepth - nDepth ) % 2 );
  if( i != 0 )  // 游戏结束
    return i;   

  if( nDepth == 0 )  // 叶子节点
    return Eveluate( ( nMaxSearchDepth - nDepth ) % 2 );   

  nCount = GenerateMove( ( nMaxSearchDepth - nDepth ) % 2 , nDepth );   

  for( i = 0; i < nCount; i ++ ) // 取历史表得分
  {
    MoveList[nDepth][i].nScore = GetHistoryScore( & MoveList[nDepth][i] );
  }   

  MergeSort( MoveList[nDepth], nCount );  // 对着法进行降序排序   

  int iBestmove = -1;   

  for( i = 0; i < nCount; i ++ )
  {   

    nCChessID = DoMove( & MoveList[nDepth][i] );  // 执行着法(生成新节点)
    nScore = - AlphaBeta_HH( nDepth - 1, -beta, -alpha );//递归调用AlphaBeta_HH
    UndoMove( & MoveList[nDepth][i], nCChessID );  // 撤销执行(删除节点)   

    if( nScore > alpha )
    {
      alpha = nScore;    // 保留最大值   

      if( nDepth == nMaxSearchDepth )
        cmBestMove = MoveList[nDepth][i];   

      iBestmove = i;  // 保存最佳走法的序号
    }
    if( alpha >= beta )
    {
      iBestmove = i;  // 保存最佳走法的序号
      break;
    }   

  }
  if( iBestmove != -1 )
    EnterHistoryScore( & MoveList[nDepth][iBestmove], nDepth ); //记录历史得分   

  return alpha;
}    

int IsGameOver( int fWhoseTurn )
{
  int x, y ;   

  if( fWhoseTurn == RED )  // 轮到红方下棋,只可能是红帅已经被吃
  {
    // 红方九宫
    for( x = 3; x <= 5; x ++ )
      for( y = 0; y <= 2; y ++ )
      {
        if( CChessBoard[x][y] == RED_K )
        {
          return 0;  // 红帅没被吃,则说明游戏尚未结束
        }
      }   

    return -MaxValue ;  // 返回失败极值(已验证应为 -MaxValue )   

  }
  else // 轮到黑方下棋,只可能是黑将已经被吃
  {
    // 黑方九宫
    for( x = 3; x <= 5; x ++ )
      for( y = 9; y >= 7; y -- )
      {
        if( CChessBoard[x][y] == BLACK_K )
        {
          return 0;  // 黑将没被吃,则说明游戏尚未结束
        }
      }   

    return -MaxValue ;  // 返回失败极值(已验证应为 -MaxValue )   

  }   

}   

BYTE DoMove( CCHESSMOVE * move )
{
  BYTE nCChessID;   

  //保留目标位置的棋子状况
  nCChessID = CChessBoard[ move->ptTo.x ][ move->ptTo.y ] ;   

  //移动子到目标位置
  CChessBoard[ move->ptTo.x ][ move->ptTo.y ]
    = CChessBoard[ move->ptFrom.x ][ move->ptFrom.y ] ;
  CChessBoard[ move->ptFrom.x ][ move->ptFrom.y ] = 0 ;   

  return nCChessID;
}   

void UndoMove( CCHESSMOVE * move, BYTE nCChessID )
{
  //将子移动回原处
  CChessBoard[ move->ptFrom.x ][ move->ptFrom.y ]
    = CChessBoard[ move->ptTo.x ][ move->ptTo.y ] ;   

  //恢复目标位置的子
  CChessBoard[ move->ptTo.x ][ move->ptTo.y ] = nCChessID ;
}   

// end of CChessSearch.h

版权声明:本文为mane原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://www.cnblogs.com/mane/p/6257888.html

[转载] 中国象棋软件-引擎实现(四)搜索算法的更多相关文章

  1. 前端编码规范

    最佳原则 坚持制定好的代码规范。 无论团队人数多少,代码应该同出一门。 命名规则 项目命名 全部采用小写方式, […]...

  2. 转载 | 李笑来《一小时建立终生受用的阅读操作系统》

    想起一篇旧文,和我最近所追求的「化繁为简」相互印证,特此整理留存。 我不懂赚钱营销号什么的,也不关心李笑来割了 […]...

  3. [转载] 中国象棋软件-引擎实现(一)概述

    2005年6月我系第二批科技小组的项目正式确定为实现一款中国象棋对弈软件。基本功能包括人机对战、网络对战。我负 […]...

  4. [转载] 平面/网页设计中色彩搭配 配色理论 色彩构成详解(一)

      在本篇文章中,我将讲述色彩理论的基础知识以及 3种简单的配色方案,以让你们在为 Web 站点选择色彩时能有 […]...

  5. 【手把手教你】入门量化回测最强神器backtrader(一) –转载

      原创 Python金融量化 2020-04-01 08:58:19 1引言   目前基于Python的量化 […]...

  6. 转载——我的华工四年

    2007年06月20日 星期三 02:04 2007-06-20 2:00 文章的作者xiaoyu,是全系每个 […]...

  7. iOS App自动化性能采集(转载)

    前言对于iOS总体生态是比较封闭的,相比Android没有像adb这种可以查看内存、cpu的命令.在日常做性能测试,需要借助xcode中instruments查看内存、cpu等数据.但是借助instruments比较麻烦、又不能提供命令...

  8. iOS App 代码覆盖率数据自动化采集(转载)

    实践这里我是基于XcodeCoverage这个工具实现的,目前这个工具只支持Objective-C的覆盖率数据采集,暂时不支持Swift。打覆盖率包1、首先将项目clone到本地,项目地址如下:https://github.com/j...

随机推荐

  1. HTTP协议

                             HTTP协议 随着web2.0时代的到来,互联网从c/s架构 […]...

  2. vue 项目集成 husky+commitlint+stylelint

    最近刚换了新工作,这两天也没有业务上的需求,做了一些前端工程化方面的东西。要在现有的项目中集成 husky+c […]...

  3. charles 验证工具

    本文参考:charles 验证工具 验证工具/validate 验证工具 Charles可以通过发送到W3C […]...

  4. 一个简单,强大的macOS数据库管理器

    SQLPro Studio是多用途的数据库客户端,允许您连接到MySQL,MSSQL,Oracle和Postg […]...

  5. python合并多个excel

    @目录前言代码编写1.导包2.定义位置和表头3.获取要合并的所有exce表格4.打开Exce文件5.获取exce文件下的所有sheet6.获取sheet下有多少行数据7.获取sheet下的数据8.主函数完整代码报错修改另外最后前言1.工...

  6. Python django实现简单的邮件系统发送邮件功能

    Python django实现简单的邮件系统发送邮件功能 本文实例讲述了Python django实现简单的邮 […]...

  7. 如何用R来定制个性化PPT

    ReporteRs包可以创建word,ppt,html文档。它可以格式化R的输出:如可编辑的矢量图,复杂的表格 […]...

  8. install mplayer on ubuntu

    安装 Mplayer 1 sudo apt-get install mplayer mplayer-fonts […]...

展开目录

目录导航