⭐ 欢迎来到虫虫下载站! | 📦 资源下载 📁 资源专辑 ℹ️ 关于我们
⭐ 虫虫下载站

📄 解剖大象的眼睛——中国象棋程序设计探索(二):棋盘结构和着法生成器.htm

📁 象棋程序设计全资料集(介绍编写象棋程序的方法思路)
💻 HTM
📖 第 1 页 / 共 5 页
字号:
      face=Arial><STRONG>f5</STRONG></FONT></TD>
    <TD align=middle bgColor=#ff00ff><FONT 
      face=Arial><STRONG>f6</STRONG></FONT></TD>
    <TD align=middle bgColor=#ff00ff><FONT 
      face=Arial><STRONG>f7</STRONG></FONT></TD>
    <TD align=middle bgColor=#ff00ff><FONT 
      face=Arial><STRONG>f8</STRONG></FONT></TD>
    <TD align=middle bgColor=#ff00ff><FONT 
      face=Arial><STRONG>f9</STRONG></FONT></TD>
    <TD align=middle bgColor=#ff00ff><FONT 
      face=Arial><STRONG>fa</STRONG></FONT></TD>
    <TD align=middle bgColor=#ff00ff><FONT 
      face=Arial><STRONG>fb</STRONG></FONT></TD>
    <TD align=middle bgColor=#ff00ff><FONT 
      face=Arial><STRONG>fc</STRONG></FONT></TD>
    <TD align=middle bgColor=#ff00ff><FONT 
      face=Arial><STRONG>fd</STRONG></FONT></TD>
    <TD align=middle bgColor=#ff00ff><FONT 
      face=Arial><STRONG>fe</STRONG></FONT></TD>
    <TD align=middle bgColor=#ff00ff><FONT 
      face=Arial><STRONG>ff</STRONG></FONT></TD></TR></TBODY></TABLE></CENTER></DIV>
<DL>
  <DT>  在中国象棋里,短程棋子<FONT face="Times New Roman">(</FONT>短兵器<FONT 
  face="Times New Roman">)</FONT>指的是除车和炮以外的其他棋子,它们的着法都有固定的增量<FONT 
  face="Times New Roman">(</FONT>行的增量,列的增量<FONT 
  face="Times New Roman">)</FONT>,因此处理起来非常简单,也是着法生成技术的基础。例如马有<FONT 
  face="Times New Roman">8</FONT>个着法,增量分别是±<FONT 
  face="Times New Roman">0x0e</FONT>、±<FONT 
  face="Times New Roman">0x12</FONT>、±<FONT 
  face="Times New Roman">0x1f</FONT>和±<FONT 
  face="Times New Roman">0x21</FONT>,红方的过河兵有<FONT 
  face="Times New Roman">3</FONT>个着法,增量分别是<FONT face=Symbol>-</FONT><FONT 
  face="Times New Roman">0x10</FONT>和±<FONT face="Times New Roman">0x01</FONT>。 
  <DT>  <FONT 
  face="Times New Roman">16x16</FONT>的扩展棋盘如上图所示,底色是红色的格子都被标上“出界”的标记,目标格在这些格子上就说明着法无效。这样,马的着法产生就非常简单了: 

  <DD>  
  <DD>const int cnKnightMoveTab[8] = {-0x21, -0x1f, -0x12, -0x0e, +0x0e, +0x12, 
  +0x1f, +0x21}; 
  <DD>const int cnHorseLegTab[8] = {-0x10, -0x10, -0x01, +0x01, -0x01, +0x01, 
  +0x10, +0x10}; 
  <DD>  
  <DD>for (i = MyFirstHorse; i &lt; MyLastHorse; i ++) { 
  <DD> // 在ElephantEye的Pieces[48]中,红方的MyFirstHorse为21,MyLastHorse为22。 
  <DD> SrcSq = ucsqPieces[i]; 
  <DD> if (SrcSq != 0) { 
  <DD>  for (j = 0; j &lt; 8; j ++) { 
  <DD>   DstSq = SrcSq + cnKnightMoveTab[j]; 
  <DD>   LegSq = SrcSq + cnHorseLegTab[j]; 
  <DD>   if (cbcInBoard[DstSq] &amp;&amp; (ucpcSquares[DstSq] &amp; MyPieceMask) 
  == 0 &amp;&amp; ucpcSquares[LegSq] == 0) { 
  <DD>    MoveList[MoveNum].Src = SrcSq; 
  <DD>    MoveList[MoveNum].Dst = DstSq; 
  <DD>    MoveNum ++; 
  <DD>   } 
  <DD>  } 
  <DD> } 
  <DD>} 
  <DT>  
  <DT>  上面的代码是着法生成器的典型写法,用了两层循环,第一层循环用来确定要走的棋子,第二层循环用来确定棋子走到的目标格。如果要加快程序的运行速度,第二个循环可以拆成顺序结构。这个代码还加入了蹩马腿的判断,马腿的位置增量由<FONT 
  face="Times New Roman">ccHorseLegTab[j]</FONT>给出。 
  <DT>  其它棋子的着法也同样处理,只要注意帅<FONT face="Times New Roman">(</FONT>将<FONT 
  face="Times New Roman">)</FONT>和仕<FONT face="Times New Roman">(</FONT>士<FONT 
  face="Times New Roman">)</FONT>把<FONT 
  face="Times New Roman">InBoard[DstSq]</FONT>改为<FONT 
  face="Times New Roman">InFort[DstSq]</FONT>就可以了。而对于兵和象等需要考虑是否能过河的棋子,判断是否过河的方法非常简单:红方是<FONT 
  face="Times New Roman">(SrcSq/DstSq &amp; 0x80) != 0</FONT>,黑方是<FONT 
  face="Times New Roman">(SrcSq/DstSq &amp; 0x80) == 0</FONT>。 
  <DT>  <FONT 
  face="Times New Roman">Pieces[48]</FONT>这个扩展的棋子数组比较难以理解,实际上用了“屏蔽位”的设计,即<FONT 
  face="Times New Roman">1</FONT>位表示红子<FONT 
  face="Times New Roman">(16)</FONT>,<FONT 
  face="Times New Roman">1</FONT>位表示黑子<FONT 
  face="Times New Roman">(32)</FONT>。因此<FONT 
  face="Times New Roman">0</FONT>到<FONT 
  face="Times New Roman">16</FONT>没有作用,<FONT 
  face="Times New Roman">16</FONT>到<FONT 
  face="Times New Roman">31</FONT>代表红方棋子<FONT 
  face="Times New Roman">(16</FONT>代表帅,<FONT 
  face="Times New Roman">17</FONT>和<FONT 
  face="Times New Roman">18</FONT>代表仕,依此类推,直到<FONT 
  face="Times New Roman">27</FONT>到<FONT 
  face="Times New Roman">31</FONT>代表兵<FONT face="Times New Roman">)</FONT>,<FONT 
  face="Times New Roman">32</FONT>到<FONT 
  face="Times New Roman">47</FONT>代表黑方棋子<FONT 
  face="Times New Roman">(</FONT>在红方基础上加<FONT 
  face="Times New Roman">16)</FONT>。这样,棋盘数组<FONT 
  face="Times New Roman">Squares[256]</FONT>中的每个元素的意义就明确了,<FONT 
  face="Times New Roman">0</FONT>代表没有棋子,<FONT 
  face="Times New Roman">16</FONT>到<FONT 
  face="Times New Roman">31</FONT>代表红方棋子,<FONT 
  face="Times New Roman">32</FONT>到<FONT 
  face="Times New Roman">47</FONT>代表黑方棋子。这样表示的好处就是:它可以快速判断棋子的颜色,<FONT 
  face="Times New Roman">(Piece &amp; 16)</FONT>可以判断是否为红方棋子,<FONT 
  face="Times New Roman">(Piece &amp; 32)</FONT>可以判断是否为黑方棋子。 
  <DT>  “屏蔽位”的设计不仅仅限制在判断红方棋子还是黑方棋子,如果在棋子数组上再多加<FONT 
  face="Times New Roman">7</FONT>个屏蔽位,就可以对每个兵种作快速判断,例如判断是否是红兵,不需要用<FONT 
  face="Times New Roman">(Piece &gt;= 27 &amp;&amp; Piece &lt;= 
  31)</FONT>,而只要简单的<FONT face="Times New Roman">(Piece &amp; 
  WhitePawnBitMask)</FONT>即可。这样的话,棋子数组的大小就增加到<FONT 
  face="Times New Roman">2^12=4096</FONT>个了,其中<FONT 
  face="Times New Roman">9</FONT>个屏蔽位,还有<FONT 
  face="Times New Roman">3</FONT>位表示同兵种棋子的编号<FONT 
  face="Times New Roman">(</FONT>注意兵有<FONT 
  face="Times New Roman">5</FONT>个,所以必须占据<FONT 
  face="Times New Roman">3</FONT>位<FONT 
  face="Times New Roman">)</FONT>。事实上,确实有象棋程序是使用<FONT 
  face="Times New Roman">Pieces[4096]</FONT>的。 
  <DT>  
  <DT><FONT face=Arial size=4><STRONG>2.5 </STRONG></FONT><FONT face=楷体_GB2312 
  size=4><STRONG>着法预生成数组</STRONG></FONT> 
  <DT>  
  <DT>  上面提到的着法生成技术,在速度上并不是最快的。我们仍旧以马的着法为例,在很多情况下,马会处于棋盘的边缘,所以往往着法只有很少,而并不需要对每个马都作<FONT 
  face="Times New Roman">8</FONT>次是否出界的判断。因此,对于每个短程子力,都给定一个<FONT 
  face="Times New Roman">[256][4]</FONT>到<FONT 
  face="Times New Roman">[256][9]</FONT>不等的数组,它们保存着棋子可以到达的绝对位置,这些数组称为“着法预生成数组”。例如,<FONT 
  face="Times New Roman">ElephantEye</FONT>里用了<FONT 
  face="Times New Roman">ucsqKnightMoves[256][12]</FONT>和<FONT 
  face="Times New Roman">ucsqHorseLegs[256][8]</FONT>,前一个数组的第二个维度之所以大于<FONT 
  face="Times New Roman">8</FONT>,是因为着法生成器依次读取数组中的值,读到<FONT 
  face="Times New Roman">0</FONT>就表示不再有着法<FONT 
  face="Times New Roman">(12</FONT>则是为了对齐地址<FONT 
  face="Times New Roman">)</FONT>。程序基本上是这样的: 
  <DD>  
  <DD>for (i = MyFirstHorse; i &lt;= MyLastHorse; i ++) { 
  <DD> SrcSq = ucsqPieces[i]; 
  <DD> if (SrcSq != 0) { 
  <DD>  j = 0; 
  <DD>  DstSq = ucsqKnightMoves[SrcSq][j]; 
  <DD>  while (DstSq != 0) { 
  <DD>   LegSq = ucsqHorseLegs[SrcSq][j]; 
  <DD>   if (!(ucpcSquares[DstSq] &amp; MyPieceMask) &amp;&amp; 
  ucpcSquares[LegSq] == 0) { 
  <DD>    MoveList[MoveNum].Src = SrcSq; 
  <DD>    MoveList[MoveNum].Dst = DstSq; 
  <DD>    MoveNum ++; 
  <DD>   } 
  <DD>   j ++; 
  <DD>   DstSq = ucsqHorseMoves[SrcSq][j]; 
  <DD>  } 
  <DD> } 
  <DD>} 
  <DT>  
  <DT>  和前一个程序一样,这个程序也同样用了两层循环,不同之处在于第二个循环读取的是着法预生成数组,<FONT 
  face="Times New Roman">DstSq</FONT>从<FONT 
  face="Times New Roman">ucsqHorseMoves[256][12]</FONT>中读出,<FONT 
  face="Times New Roman">LegSq</FONT>从<FONT 
  face="Times New Roman">ucsqHorseLegs[256][8]</FONT>中读出。 
  <DT>  
  <DT><FONT face=Arial size=4><STRONG>2.6 </STRONG></FONT><FONT face=楷体_GB2312 
  size=4><STRONG>位行和位列</STRONG></FONT> 
  <DT>  
  <DT>  车和炮的着法分为吃子和不吃子两种,这两种着法生成器原则上是分开的,因此分为车炮不吃子、车吃子和炮吃子三个部分。不吃子的着法可以沿着上下左右四条射线逐一生成<FONT 
  face="Times New Roman">(</FONT>即并列做<FONT 
  face="Times New Roman">4</FONT>个循环<FONT 
  face="Times New Roman">)</FONT>。我们感兴趣的是吃子的着法,因为静态搜索只需要生成这种着法,能否不用循环就能做到?<FONT 
  face="Times New Roman">ElephantEye</FONT>几乎就做到了。 
  <DT>  “位行”和“位列”是目前比较流行的着法生成技术,但仅限于车和炮的着法,它是否有速度上的优势还很难说,但是设计程序时可以减少一层循环,这个思想就已经比较领先了。以“位”的形式记录棋盘上某一行所有的格子的状态<FONT 
  face="Times New Roman">(</FONT>仅仅指是否有子<FONT 
  face="Times New Roman">)</FONT>,就称为“位行”<FONT 
  face="Times New Roman">(BitRank)</FONT>,与之对应的是“位列”<FONT 
  face="Times New Roman">(BitFile)</FONT>,棋盘结构应该包含<FONT 
  face="Times New Roman">10</FONT>个位行和<FONT 
  face="Times New Roman">9</FONT>个位列,即: 
  <DD>  
  <DD>struct PositionStruct { 
  <DD> …… 
  <DD> unsigned short wBitFiles[16]; 
  <DD> unsigned short wBitRanks[16]; 
  <DD> …… 
  <DD>}; 
  <DT>  
  <DT>  值得注意的是,它仅仅是棋盘的附加信息,“棋盘<FONT 
  face="Times New Roman">-</FONT>棋子联系数组”仍旧是必不可少的。它的运作方式有点和“棋盘<FONT 
  face="Times New Roman">-</FONT>棋子联系数组”类似: 
  <DT>  <FONT face="Times New Roman">(1) 
  </FONT>同时用位行数组和位列数组表示棋盘上的棋子分布信息,位行数组和位列数组之间可以互相转换; 
  <DT>  <FONT face="Times New Roman">(2) </FONT>随时保持这两个数组之间的联系,棋子移动时必须同时更新这两个数组。 

  <DT>  因此,移走或放入一颗棋子时,必须在位行和位列上置位: 
  <DD>  
  <DD><FONT face=宋体>void AddPiece(int Square, int Piece) {</FONT> 
  <DD> …… 
  <DD> x = Square % 16; 
  <DD> y = Square / 16; 
  <DD> wBitFiles[x] = 1 &lt;&lt; (y - 3); 
  <DD> wBitRanks[y] = 1 &lt;&lt; (x - 3); 
  <DD> …… 
  <DD><FONT face=宋体>}</FONT> 
  <DT>  
  <DT>  车和炮是否能吃子<FONT face="Times New Roman">(</FONT>暂时不管吃到的是我方棋子还是敌方棋子<FONT 
  face="Times New Roman">)</FONT>,只取决于它所在的行和列上的每个格子上是否有棋子,而跟棋子的颜色和兵种无关,因此这些信息完全反映在位行和位列中。预置一个“能吃到的格子”的数组,以位行或位列为指标查找数组,就可以立即知道车或炮能吃哪个子了。预置数组到底有多大呢? 

  <DD>  
  <DD>// 某列各个位置的车或炮(10)在各种棋子排列下(1024)能走到的最上边或最下边的格子 
  <DD>unsigned char ucsqFileMoveNonCap[10][1024][2];  // 不吃子 
  <DD>unsigned char ucsqFileMoveRookCap[10][1024][2];  // 车吃子 
  <DD>unsigned char ucsqFileMoveCannonCap[10][1024][2]; // 炮吃子 
  <DD>// 某列各个位置的车或炮(9)在各种棋子排列下(512)能走到的最左边或最右边的格子 
  <DD>unsigned char ucsqRankMoveNonCap[9][512][2]; 
  <DD>…… 
  <DT>  
  <DT>  数组中的值记录的是目标格子的偏移值,即相对于该行或列第一个格子的编号。产生吃子着法很简单,以车吃子位例: 
  <DD>  
  <DD>for (i = MyFirstRook; i &lt;= MyLastRook; i ++) { 
  <DD> SrcSq = ucsqPieces[i]; 
  <DD> if (SrcSq != -1) { 
  <DD>  x = SrcSq % 16; 
  <DD>  y = SrcSq / 16; 
  <DD>  DstSq = ucsqFileMoveRookCap[y - 3][wBitFiles[x]][0]; // 得到向上吃子的目标格 
  <DD>  if (DstSq != 0) { 
  <DD>   DstSq += x * 16; // 注意:第x列的第一个格子总是x * 16。 
  <DD>   MoveList[MoveNum].Src = SrcSq; 
  <DD>   MoveList[MoveNum].Dst = DstSq; 
  <DD>   MoveNum ++; 
  <DD>  } 
  <DD>  …… // 再把FileMoveRookCap[...][...][0]替换成[...][...][1],得到向下吃子的着法 
  <DD>  DstSq = ucsqRankMoveRookCap[x - 3][wBitRanks[y]][0]; // 得到向左吃子的目标格 
  <DD>  if (DstSq != 0) { 
  <DD>   DstSq += y; // 注意:第y行的第一个格子总是y。 
  <DD>   MoveList[MoveNum].Src = SrcSq; 
  <DD>   MoveList[MoveNum].Dst = DstSq; 
  <DD>   MoveNum ++; 
  <DD>  } 
  <DD>  …… // 再把RankMoveRookCap[...][...][0]替换成[...][...][1],得到向右吃子的着法 
  <DD> } 
  <DD>} 
  <DT>  
  <DT><FONT face=Arial size=4><STRONG>2.7 </STRONG></FONT><FONT face=楷体_GB2312 
  size=4><STRONG>着法合理性的判断</STRONG></FONT> 
  <DT>  
  <DT>  <FONT face="Times New Roman">ElephantEye</FONT>搜索每个结点时,着法都有四个来源:<FONT 
  face="Times New Roman">(1) </FONT>置换表,<FONT face="Times New Roman">(2) 
  </FONT>吃子着法生成器,<FONT face="Times New Roman">(2) </FONT>杀手着法表,<FONT 

⌨️ 快捷键说明

复制代码 Ctrl + C
搜索代码 Ctrl + F
全屏模式 F11
切换主题 Ctrl + Shift + D
显示快捷键 ?
增大字号 Ctrl + =
减小字号 Ctrl + -