【问题标题】:C# - which data structure to use - manipulate and store chess position in 180 bits?C# - 使用哪种数据结构 - 以 180 位操作和存储国际象棋位置?
【发布时间】:2012-10-12 07:40:22
【问题描述】:

我正在开发一个国际象棋应用程序,需要创建开局书,一个文件 可能包含数百万个动作和位置。我们有 64 个 squres,其中一些有 被碎片占据,而有些则是空的。让我们用以下位来表示我们的作品 (使用霍夫曼编码技术)。

              White          Black
-Empty        0

-Pawn         110           100

-Rook         11111         11110

-Knight       10110         10101

-Bishop       10100         11100

-Queen        111010        111011

-King         101110        101111

在初始位置,我们有 32 个正方形被不同的棋子占据,其中 32 个正方形被 空的。为了提高效率,我必须将位置存储在连续位中。件位将 按顺序放置在位数组中,从 a1 方格开始,然后是 a2 方格,..a8, 然后是 b1、b2...b8 正方形等等。

所以对于起始位置,这等于 32 x 1 + 16 x 3 + 12 x 5 + 4 x 6 = 164 位。

对于不同的游戏情况,还需要额外的 16 位,例如 是否启用castling,如果适用,定义enpassant square。 所以我需要大约 180 位(或 23 字节 = 184 位)来存储单个位置 棋盘。

现在的问题是我必须执行一些按位运算,所以想知道 该方案如何在我的代码中操作它以及如何存储在文件中。方法 我应该使用哪种数据结构。例如最大 long (数据类型)将仅包含 4 字节 = 64 位。我想避免使用字符串。任何机构都可以提出任何想法 继续这个。

我正在使用 C#.Net,框架 3.5。

【问题讨论】:

  • 如果您正在模拟一个标准的国际象棋游戏,为什么不用一棵树(每个分支都可能移动)?如果您只存储它与前一个状态之间的差异,您可以将打开序列的每个状态减少到 11-12 位(尽管为了效率我会选择 16 位)。
  • 我想回答,但我认为我的回答永远不会被接受。
  • @Jodrell,伟大的 cmets,谢谢。
  • 您也可以关闭您想要的答案与您提出的问题无关的任何问题,例如stackoverflow.com/questions/9362184/…

标签: c# bit-manipulation bitwise-operators chess


【解决方案1】:

对于国际象棋应用程序,最常用的结构是这样的;

  • 一个 ulong 64 位变量,其中每个位代表棋盘上的一个位置
  • 为每种颜色的棋子、骑士等单独变量。

所以

ulong WHITE_PAWNS = xxx;
ulong BLACK_PAWNS = xxx;

等等

这不是存储效率最高的版本,但可以让您非常快速地做很多事情。要将棋子和骑士放在一起,您可以这样做

white_pawns_and_knights = WHITE_PAWNS | WHITE_KNIGHTS;

阅读更多关于它的信息here(以及其他地方)。

我自己编写了一个国际象棋程序(用 Delphi 和部分 C# 编写),里面有很多好东西。这些是我保存在这个主题上的书签;

如果你想要的话,我还有一个用于对 ulong 进行快速位操作的 C# 单元。如果需要,请通过 ivotops#gmail#com 给我发邮件,并随时询问。

很遗憾,我时间太少,没有完成 C# 版本;-)

【讨论】:

    【解决方案2】:

    对于一本书,您可以使用这种方法;

    为每个板生成一个好的 64 位哈希码(google zobrist 哈希)

    有一个使用哈希码作为键的字典。

    所以你不存储实际的电路板。如果哈希码匹配,您可以假设它是相同的板(64 位冲突的可能性在小数点后十五位之后开始)。作为最后的检查测试是否允许在棋盘上进行开局,然后您就可以开始了。

    然后编写代码将字典保存为一个整体。这段代码可以做到这一点并适用于所有可枚举的容器;

     // save
     using (var fs = new FileStream(fileName, FileMode.Create))
            {
                var bw = new BinaryWriter(fs);
                foreach (var kvp in this)
                {
                    kvp.Key.AddToStream(bw);
                    kvp.Value.AddToStream(bw);
                }
            }
    
     // load
     using (var fs = new FileStream(fileName, FileMode.Open))
                {
                    var fslen = fs.Length;
                    var br = new BinaryReader(fs);
                    while (fs.Position < fslen)
                    {
                        var k = new Pattern();
                        var v = new BestMove();
                        k.ReadFromStream(br);
                        v.ReadFromStream(br);
                        Add(k, v);
                    }
                }
    

    这是生成 64 位 Zobrist 哈希的方法。这些应该存储在某个永久位置,以便以后可以在此答案底部显示的代码方法中重用它们。在这里,它们被存储为静态类的静态成员:

    internal static class HashKeys
    {
        internal static readonly UInt64[,] PieceSquareKeys = new UInt64[64,16];
        internal static readonly UInt64[] EnPassantKeys = new UInt64[64];
        internal static readonly UInt64 SideToMoveKey;
        internal static readonly UInt64 WhiteCastlingKingSideKey;
        internal static readonly UInt64 WhiteCastlingQueenSideKey;
        internal static readonly UInt64 BlackCastlingKingSideKey;
        internal static readonly UInt64 BlackCastlingQueenSideKey;
    
        // Constructor - generates pseudo-random numbers for Zobrist hashing.
        // The use of a CSPRNG is a good guaranteee of genuinely random numbers.
        static HashKeys()
        {
            RNGCryptoServiceProvider randomGenerator = new RNGCryptoServiceProvider();
            byte[] eightRandomBytes = new byte[8];
    
            try
            {
                for (Int32 i1 = 0; i1 < 64; i1++)
                {
                    for (Int32 i2 = 0; i1 < 16; i1++)
                    {
                        randomGenerator.GetBytes(eightRandomBytes);
                        PieceSquareKeys[i1, i2] = BitConverter.ToUInt64(eightRandomBytes, 0);
                    }
                    randomGenerator.GetBytes(eightRandomBytes);
                    EnPassantKeys[i1] = BitConverter.ToUInt64(eightRandomBytes, 0);
                }
    
                randomGenerator.GetBytes(eightRandomBytes);
                SideToMoveKey = BitConverter.ToUInt64(eightRandomBytes, 0);
                randomGenerator.GetBytes(eightRandomBytes);
                WhiteCastlingKingSideKey = BitConverter.ToUInt64(eightRandomBytes, 0);
    
                randomGenerator.GetBytes(eightRandomBytes);
                WhiteCastlingQueenSideKey = BitConverter.ToUInt64(eightRandomBytes, 0);
    
                randomGenerator.GetBytes(eightRandomBytes);
                BlackCastlingKingSideKey = BitConverter.ToUInt64(eightRandomBytes, 0);
    
                randomGenerator.GetBytes(eightRandomBytes);
                BlackCastlingQueenSideKey = BitConverter.ToUInt64(eightRandomBytes, 0);
            }
            finally
            {
                randomGenerator.Dispose();
            }
        }
    }
    

    这是如何生成代表棋盘位置的 64 位 Zobrish 散列(包括移动的边、易位权、过客等:

    // Init Zobrist position hash, used in transposition table and draw detection.
    // This will be incrementally updated during move make/unmake.
    internal static UInt64 InitPositionHash(byte[] squares, ComplexProperties propertyStore, byte sideToMove)
    {
        UInt64 positionHash = 0;
    
        // Calculate piece/square hashes.
        for (Int32 i = 0; i < 64; i++)
        {
            if (squares[i] != Constants.EMPTY)
            {
                positionHash ^= HashKeys.PieceSquareKeys[i, squares[i]];
            }
        }
    
        // Add side to move only if Black.
        if (sideToMove == Constants.BLACK)
        {
            positionHash ^= HashKeys.SideToMoveKey;
        }
    
        // Add en-passant square if applicable.
        if (propertyStore.EpSquare != 0)
        {
            positionHash ^= HashKeys.EnPassantKeys[propertyStore.EpSquare];
        }
    
        // White castling.
        switch (propertyStore.WhiteCastlingStatus)
        {
            case Constants.EnumCastlingStatus.CAN_CASTLE_BOTH:
                 positionHash ^= HashKeys.WhiteCastlingKingSideKey;
                 positionHash ^= HashKeys.WhiteCastlingQueenSideKey;
                 break;
            case Constants.EnumCastlingStatus.CAN_CASTLE_OO:
                 positionHash ^= HashKeys.WhiteCastlingKingSideKey;
                 break;
            case Constants.EnumCastlingStatus.CAN_CASTLE_OOO:
                 positionHash ^= HashKeys.WhiteCastlingQueenSideKey;
                 break;
            case Constants.EnumCastlingStatus.CANT_CASTLE:
                 break;
            default:
                 Debug.Assert(false, "White has an invalid castling status!");
                 break;
        }
    
        // Black castling.
        switch (propertyStore.BlackCastlingStatus)
        {
            case Constants.EnumCastlingStatus.CAN_CASTLE_BOTH:
                 positionHash ^= HashKeys.BlackCastlingKingSideKey;
                 positionHash ^= HashKeys.BlackCastlingQueenSideKey;
                 break;
            case Constants.EnumCastlingStatus.CAN_CASTLE_OO:
                 positionHash ^= HashKeys.BlackCastlingKingSideKey;
                 break;
            case Constants.EnumCastlingStatus.CAN_CASTLE_OOO:
                 positionHash ^= HashKeys.BlackCastlingQueenSideKey;
                 break;
            case Constants.EnumCastlingStatus.CANT_CASTLE:
                 break;
            default:
                 Debug.Assert(false, "Black has an invalid castling status!");
                 break;
        }
    
        return positionHash;
    }
    

    【讨论】:

    • 我认为您的方法最适合跟踪图书位置,因此我使用一些有用的 Zobrist 相关代码对其进行了更新。
    • @RoadWarrior:谢谢。尽管在使用您的代码生成它们后,他将需要存储随机哈希键。如果您使用这些哈希值保存开头簿,再次加载开头簿后,他将需要相同的哈希值才能在书中找到任何内容。
    • 没错。在我的代码中,它们存储为静态类的静态成员,该静态类在一次运行中工作。显然还需要长期存储。
    • +1 这是正确的做法;您不会在开局书中存储国际象棋位置(您可以假设树中的所有移动都从标准的开局位置开始)。
    【解决方案3】:

    我会去BitArray。它专门用于位操作,但包装了操作。

    但听起来您的需求已经足够专业化,可能与实现您自己的集合相关。

    【讨论】:

    • 感谢您的快速回复,是的,我已经对其进行了审核。但不知道,我将如何将它存储在文件中。它还支持在单字节边界上假设的方法。但我需要连续的 180 位而不用担心在使用按位运算符时的字节边界(但肯定在低端,这些将被视为字节,但我不想在编码时被这些边界限制)。例如,我想处理第一个字节的后 2 位和第二个字节的前 3 位。
    • 您可以通过索引运算符 byteArray[3] = false; byteArray[129] = true; 等单独访问这些位。该类是 [Serializable],因此您可以使用标准 .NET 序列化将其直接存储在一个紧凑的数据文件中:msdn.microsoft.com/en-us/library/ms973893.aspx
    【解决方案4】:

    在运行时你可以使用 BitArray 类。它非常有效,因为您可以存储自定义大小的位数组

    更新

    将 BitArray 保存到文件:

    private void save()
    {
        BitArray bits = new BitArray(164, true);
        byte[] bytes = new byte[21]; // 21 * 8 bits = 168 bits (last 4 bits are unused)
        bits.CopyTo(bytes, 0);
        for (int i = 0; i < 21; i++)
        {
            bytes[i] = ToByte(bits, i);
        }
    
        // now save your byte array to file
    }
    
    private byte ToByte(BitArray bits, int start) 
    {
        int sum = 0;
    
        if (bits[start])
            sum += 8;
    
        if (bits[start + 1])
            sum += 4;
    
        if (bits[start + 2])
            sum += 2;
    
        if (bits[start + 3])
            sum += 1;
    
        return Convert.ToByte(sum);
    }
    

    【讨论】:

    • 感谢 Mohsen,您能否提供任何示例代码,如何将其存储在文件中。并回读。
    猜你喜欢
    • 1970-01-01
    • 2020-10-24
    • 1970-01-01
    • 2021-04-24
    • 2020-05-18
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-03-07
    相关资源
    最近更新 更多