国际象棋:获取所有合法的国际象棋走法

11

我正在制作象棋游戏,几乎所有功能都已实现,但有一点需要解决:我需要让玩家无法将棋子移动到将军的位置。但我不知道该如何解决这个问题。

现在我使用伪代码生成有效的移动方式:

Class getMoveLocations(我将一个方格定义为棋盘上的一个位置):
如果这个位置在棋盘内,并且位于敌人棋子上,而模拟移动不会导致将军,则将此位置添加到可移动位置列表中。

问题出在如何检查棋盘是否处于“将军”状态。我的代码通过收集所有敌人的行动位置,并查看这些敌人的行动位置是否与国王的位置重叠来判断棋盘是否处于“将军”状态。

不幸的是,这就是无限循环开始的地方;为了收集所有敌人的行动位置,每个敌人可能的行动位置都需要确保其行动不会导致其处于将军状态。为了确保没有任何敌人的位置处于将军状态,还需要收集所有盟友的潜在行动位置等等。

我被卡住了,不知如何得到正常工作的算法。虽然我的代码在理论上是合乎逻辑的,但它无法实现。我对A)更有效地实现检查所有合法移动的方法感兴趣,或B)解决这个无限循环的方法。


你可以仅对那些会造成将军的着法运行递归部分(以确保它们是有效的),而不运行其他着法(无论它们是否无效都没有关系)。这样可以防止无限递归。 - assylias
你创建了一个名为 getMoveLocations 的类吗? - joshreesjones
1
你正在寻找可能的走法:"如果我在这里移动,对手在那里移动,然后如果我在对手移动到这里后再移动到那里......我会被将军吗?"?想象一下制作一个游戏,在这个游戏中,你有一个球反弹击打墙壁,而球是由玩家控制的,你不会限制他的运动到不会与墙壁碰撞的N路径(巨大的搜索),你会让他朝任何一个方向移动,直到他撞上墙壁。同样,在进行移动时只检查该移动是否会将你置于被将军状态,而不考虑所有棋子的可能移动(巨大搜索)。 - arynaq
我设置的方式是显示所有可能的移动。如果在尝试移动时弹出一个消息框并显示“非法移动!”,我觉得它会失去一些可读性。无论如何,这使得获取可能的移动更加高效,但并没有真正解决无限循环的问题。 - Mike
5个回答

9

有一种更有效的方法来确定一个方向是否被将军:你只需从国王的位置向外扫描并查看是否有可以攻击它的棋子。例如,从国王的位置开始,检查是否有任何敌方象在对角线上等。您根本不需要生成移动列表,因此递归也不必要。以下是一些伪代码:

function leftInCheck(board, sideToCheck) {

   // one of the four rays for bishop/queen attacks
   d := 0
   while (king rank + d, king file + d) is on the board
      piece := board[king rank + d][king file + d]
      if piece is an enemy bishop or queen
         return true
      if piece is not an empty square   // a piece blocks any potential
         break                          // attack behind it so we can stop
      d := d + 1

   // do this for all the other forms of attack
   ...

   return false
}

正如您所看到的,有一些代码重复,但您可以让它更短。我将其保留为简单易懂。您可以通过生成伪合法着法来生成合法着法,就像您现在正在做的那样,对每个着法进行操作,并省略会使您处于上述子程序中的着法。这还具有自然的吃过路兵的额外优势。以下是一些伪代码:

function legalMoves(board, sideToMove) {
   moveList := empty
   for each move in pseudoLegalMoves()
      make(move)
      if not leftInCheck(board, sideToMove)
         moveList.add(move)
      unmake(move) // you may not need this
   return moveList
}

对于王车易位,你仍然需要检查国王和车之间的方格是否受到攻击。幸运的是,你可以扩展上面的子程序以适用于国王以外的方格,这很容易。
我假设你没有使用比特棋盘或0x88,而是使用简单的数组表示。这使得实现合法移动生成(没有中间的伪合法移动)有点困难,因为它需要非常快速地生成攻击映射以确定被牵制的棋子。如果你有雄心壮志,这是一个可能性。
另外,我对这里的其他答案有些失望。我甚至不会向任何想要编写良好的移动生成器的人推荐我的答案(它只适用于那些不熟悉国际象棋编程的人)。这是一个已经被深入研究并且有着众所周知解决方案的主题,但是却在引发原创思路。当然,这并没有什么问题,但是为什么要重复发明轮子呢?最好研究一下已经确立的移动生成方法

他还需要特别处理王车易位。国王不能越过被攻击的空格,因此海报还需要检查国王侧易位的f1或f8,以及皇后侧易位的d1或d8。 - Eric Jablow
是的,并且顺便说一句。如果要完全合法地生成,这样做相当复杂。不过,如果他在伪合法生成后进行检查评估,所有这些都很容易处理,尽管速度稍慢。 - Zong
但是只有在王车易位时,发帖者才需要检查2个方格:目的地和中间空间。 - Eric Jablow
从国王的角度来看棋步是个好主意。然而,它比你描述的要稍微复杂一些。例如,敌方的主教需要沿着对角线移动,并且没有任何障碍物(或者将对角线视为在第一个棋子处结束)。 - Ted Hopp
是的,你说得对。我在思考如何利用“过路车”来揭示被挡住的射线攻击。 - Zong
@TedHopp 是的,那是暗示。否则,位棋盘表示法可以在单个时钟周期内检查移动合法性。哈哈。 - Zong

2

修改您的getMoveLocations程序,接受一个标志,指示是否考虑移动到将军状态。例如,如果一枚棋子被夹住了,它仍然可以移动以俘获对方国王。如果该标志设置为忽略将军风险,则跳过检查测试将会中断递归。

或者(等效地)编写一个单独的方法来生成移动,该方法忽略移动到将军状态的问题。将该程序用作“导致局面处于将军状态”测试的一部分。


我认为这种方法不适用于具有许多固定棋子的复杂位置。 固定的棋子之间可以存在循环关系。 - Zong
3
如果你能通过移动一个棋子立刻将对方国王困住,那么无论这个棋子是否被牵制,对方国王都处于“将军”的状态。为什么被牵制的棋子数量或者局面的复杂程度会有任何区别呢?关键在于,为了判断自己是否被“将军”,你只需测试对手是否可以在下一步将你的国王吃掉。 - Ted Hopp
1
我现在明白了,你是对的,我没有完全理解你所暗示的意思。既然我明白了,我必须说这需要额外的两层不必要的移动生成。这非常慢(分支因子为36,可能慢1000倍以上)。作为一个国际象棋程序员,我感到非常震惊。 - Zong
好的,再考虑一下,实际上只是多了一个层级,所以并不是太糟糕。我想如果 OP 只是在开发一个界面的话,这样做还是可以的。 - Zong
20世纪50年代撰写的有关计算机国际象棋算法的第一篇论文是Shannon Number的基础,即国际象棋游戏中可能走法的数量(或者换句话说,可能的国际象棋游戏数量)。这个数字约为10^120...大致相当于我们自己的10^35个宇宙中所包含的原子数量。Shannon提出,如果一个计算机可以每秒评估10^6个走法,那么该计算机需要花费10^90年才能得出最佳走法。 - scottb
显示剩余3条评论

2
如果你的棋子移动导致敌方棋子可以攻击到你的国王,那么这个走法将使你方被将军(即不允许)。注意,被将军和将对手置于将军的情况是不同的。当你将对手置于将军时,他们有机会进行反击。但如果你自己被将军,你就没有机会反击了。对手可以直接吃掉你的国王,而无论对手处于什么状况或是否被将军,这都是正确的走法。因此,要判断某个走法是否会将自己置于将军,只需看看是否有任何敌方棋子能够攻击到你的国王即可。不需要递归地解决未来可能的将军等问题——如果它们现在就能攻击到国王,那么这个走法就是无效的。

1

看起来有些发帖者不太了解国际象棋引擎,因此在争论之前,请仔细阅读并进行研究:

不要检查它们是否可以移动到目标位置,而是检查它们是否可以攻击该位置。你可能会在评估函数中用到这个技巧...

我不知道有多少引擎会这样做,但我知道Stockfish会(请参阅src文件夹中的evaluate.cpp),因此我认为这在好的引擎中相当普遍。如果它需要用于评估,那么您也可以将其用于移动生成。


如果您的算法接受了这个答案的逻辑,那么这将是一个比所有敌方有效移动更简单且可能更便宜的评估。对于国王的移动,它可以是一个相当直接的布尔运算:如果该空间是ChessPiece.KING的有效移动并且没有被敌方剩余棋子攻击,则它是一个有效的移动。如果ChessPiece.KING正在受到攻击并且没有有效的移动,则游戏以失败告终。 - scottb
除了王车易位、吃过路兵和兵的前进之外,能够移动到那里和能够攻击那里有什么区别?这几乎与原帖中所做的完全相同。 - Zong
我从未真正编写过国际象棋引擎,但我在这方面进行了大量研究,我认为这是最好的方法。我的主要担忧是OP似乎正在使用Java编写国际象棋引擎,他们似乎并不太关心性能... - Andrew W
1
@宗立,“攻击”(我使用的术语)意味着如果没有其他规则限制,它可以在那里移动。您需要这个功能来进行评估,例如,您可以通过“攻击”它们来保护您的棋子。 - Andrew W
我认为他们也不太关心性能,但Java可以非常快。虽然我理解你的意思,但我不明白这对OP有什么优势。如果不使用位板或0x88,你怎么比移动生成更快地完成呢?即使使用了这些技术,生成攻击图也是相当昂贵的,我认为很少有引擎这样做。而且,它的编码并不像描述高级概念那样简单。 - Zong

0
基于标题:国际象棋:获取所有合法的走棋方式
我想介绍一下对我来说效果很好的方法。
这需要一种验证pgn文件的方法,我正在使用pgn-extract,但它与UCI引擎和其他工具类似。
sudo apt install pgn-extract

这只能通过从有效的PGN开始,而不是简单的FEN字符串来实现。

在任何给定的回合中,无论棋子如何,最多不超过8⁴= 4096种组合,其中64种是不可能的(例如:c4c4永远不会有效)。

实际上,它要少得多,但这并不重要,我们正在测试它们所有,而不解析棋子类型。

通过生成整个组合的数组:

array(4032) {
  [0]=>
  string(4) "a1a2"
  [1]=>
  string(4) "a1a3"
  [2]=>
  string(4) "a1a4"
  [3]=>
  (...) 

将每个条目作为PGN文件中的最后一步进行推送。 如果验证失败,则该移动无效。

这很有效,考虑到所有特殊移动,如果将军等情况下没有有效移动。

当然,这样做非常粗暴,需要一些秒数。

enter image description here enter image description here

示例代码是php

#!/usr/bin/php
<?php
$cp = file_get_contents("SINGLE_GAME-FILE.pgn");
$M = [];$ALL = [];$a = "a";
for ($i = 0;$i < 8;$i++){
  for ($j = 1;$j < 9;$j++){
    $M[] = $a.$j;
  }
  $a++; 
}
$t=0;
for ($i = 0;$i < count($M);$i++){
  for ($j = 0;$j < count($M);$j++){
    if ($M[$i]  != $M[$j]){
      $move = $M[$i].$M[$j];
      $ALL[] = $move;
      $pgn = explode("\n",$cp);
      $g =   explode(" ",end($pgn));
      $pgn[count($pgn)-1] = "";
      $g[count($g)-1] = $move;$g[]= "*";
      file_put_contents("mgen.pgn",implode("\n",$pgn).implode(" ",$g));
      system("pgn-extract -r mgen.pgn 2> .gentest.fen");
      if (!preg_match_all('~\b(Failed|Unknown|Missing)\b~i',file_get_contents(".gentest.fen"))){
        // Valid move
        echo "$move ";$t++;
      }
    }
  }
}
echo "\nFOUND $t LEGAL MOVES\n"; 

示例pgn


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接