65.9K
CodeProject 正在变化。 阅读更多。
Home

C# 中的 2D“Nim”游戏开发和动画(第 2 部分)

starIconstarIconstarIconstarIconstarIcon

5.00/5 (23投票s)

2007年8月16日

CPOL

5分钟阅读

viewsIcon

70218

downloadIcon

2098

一系列关于两人策略数学游戏的第二部分文章

  

Screenshot - blue_pencil_step.jpg

在玩家通过(用一个橡皮筋矩形)选择了蓝色铅笔之后。

Screenshot - red_pencil_step.jpg

进一步,程序将用红色涂抹一些铅笔,并将它们从一个堆中移除。

引言

什么是 Nim 游戏

Nim 是一款两人策略数学游戏,玩家轮流从不同的堆中移除物品。在每一回合中,玩家必须至少移除一个物品,并且可以移除任意数量的物品,前提是所有物品都来自同一个堆。

Nim 的变体自古以来就已存在。据说该游戏起源于中国(它非常类似于中国游戏“剪石子”或“捡石子”),但起源不确定;最早的欧洲关于 Nim 的参考文献来自 16 世纪初。

它的现名由哈佛大学的 Charles L. Bouton 提出,他还在 1901 年开发了该游戏的完整理论,但名称的起源从未完全解释清楚。该名称可能源自德语的“nimm!”,意为“拿!”,或源自同义的旧式英语动词“nim”。有些人注意到将单词 NIM 颠倒并反向拼写会得到 WIN。

Nim 通常作为一种“反向赛制”(misere game)进行,即拿走最后一个物品的玩家输。Nim 也可以作为“标准赛制”(normal play game)进行,这意味着最后一步的玩家(即拿走最后一个物品的玩家)获胜。这被称为标准赛制,因为大多数游戏都遵循此约定,尽管 Nim 通常不遵循。

XOR 技巧助你赢得 Nim 游戏

虽然找到 Nim 的秘密策略需要一些高级数学知识,但运用该策略实际上只需要理解二进制数。

要赢得 Nim 游戏,您必须知道如何使用二进制运算 XOR,它代表“异或”。XOR 的实际含义是“x XOR y = 1,如果 x 或 y 是 1,但不是两者都是 1。” 因此,0 XOR 0 = 0,1 XOR 0 = 1,0 XOR 1 = 1,1 XOR 1 = 0。或者更简单地说,如果两个参数相同,XOR 操作的结果为 0,如果参数不同,则为 1。

对于 Nim 中的任何给定情况,都有一个数字决定该情况是否是必败局。要找到这个数字,您必须连续对每个堆中的物品数量执行 XOR 操作。例如,局面是

Screenshot - nim.jpg

因此,要判断这是否是一个必败局,我们必须对每一行中的物品数量进行 XOR,如下所示

1 XOR 3 XOR 5 

这用二进制表示为

001 XOR 011 XOR 101

现在我们必须对每个数字中的每一位进行 XOR,并将每个结果放在该数字列的下方。让我们从最右边的数字开始。

1 XOR 1 XOR 1   is the same as   (1 XOR 1) XOR 1

1 XOR 1 = 0,0 XOR 1 = 1。所以 1 XOR 1 XOR 1 = 1。

现在继续处理其余的列,直到我们得到一个新的二进制数。

001 XOR 011 XOR 101 = 111 

如果这个最终数字的所有位数都为零,则该局面是必败局!!!!!!! 如果轮到您而局面是必败局,您就麻烦了。然而,在这个例子中,数字不为零,所以我们可以将局面变成我们对手的必败局。

让我们取 XOR 行得到的数字 111,并尝试找到一行,当它与 111 XOR 时,会得到一个比该行之前小的数字。嗯,我们知道如果做 001 XOR 111,结果将大于 1,做 011 XOR 111 将大于 3,所以我们必须做 101 XOR 111,结果是 010,即十进制的 2,小于 5。因此,为了给对手一个必败局,您只需从 5 个物品的行中移除 3 个物品,剩下 2 个。

摘要

获胜所需的步骤是

  1. 轮到您时,将每行中的物品数量转换为二进制数并进行 XOR。
  2. 如果结果数字是 0,您几乎无法获胜。如果不是 0,则将其与某一行进行 XOR,并移动以在该行中留下相应数量的物品。

有关游戏数学理论的更多信息,请参阅 维基百科文章

Using the Code

以下代码演示了如何创建 Form。让我们在 Form 的客户端区域绘制一些东西。

  • 以下代码说明了动态创建和初始化这些铅笔堆的过程。为了保存和显示图像,我们使用交错数组(Jagged array)。
    private Bitmap[][] images;   // Bitmap Jagged Arrays
      /**************************************************************************/
     /* The method creates a matrix in the size 7 on 30 for the pencils        */ 
    /**************************************************************************/
    void CreateNew_Images()
    {
        //Allocation of memory for array bitmap
        images = new Bitmap[8][];
    
        for(int i=0; i<8; i++)
        {
          images[i] = new Bitmap [30];
    
          for(int j=0; j<30; j++)
          {
                   images[i][j]= new Bitmap(tempImage); //Load empty image
                   images[i][j].MakeTransparent();      //Without background
          }
        }
     } 
      /****************************************************************** ******/
     /* The method fills the matrix with gray pencils                         */
    /*************************************************************************/
    void Fill_Images()
    {
        int new_number_elements;
        Random rdm;
    
        for(int i=0; i < numberOfColumns; i++)
        {
            rdm =
                newRandom(unchecked((int)DateTime.Now.Ticks)); 
                new_number_elements =  rdm.Next(1,30);
            SumOfImages+ =  
                new_number_elements;   for(int
                j=
                0;j <  new_number_elements; j++)
            {
                images[i][j]= bitmapGray;
                images[i][j].MakeTransparent(); //Without background
            }
            numberLoadImages.SetValue(new_number_elements,i);
            System.Threading.Thread.Sleep(35);
        }
        Invalidate();
    }
  • 重写适当的方法

    当表单或控件的部分或全部被另一个表单或控件遮挡并需要重绘时,操作系统会调用表单或控件的 OnPaint 方法。一个 Graphics 对象作为 PaintEventArgs 参数传递给 OnPaint 方法。使用这个 Graphics 对象,您可以重绘被遮挡的显示部分。
    为什么是 OnPaint 方法?主要原因是我们可以轻松地在 OnPaint 方法中获取 Graphics 对象。在此方法中,我在鼠标单击后绘制对象的旋转。

          /*********************************************************************/
         /* The OnPaint method also allows derived classes to handle the event*/
        /* without attaching a delegate. This is the preferred technique for */
       /* handling the event in a derived class                             */ 
      /* One of the most responsible functions in this program!!!!         */
     /* This function re-draw the image on the main form !!               */
    /*********************************************************************/
    protected override void OnPaint(PaintEventArgs e) 
    {
        bool flag=false;
        //Output image for background
        e.Graphics.DrawImageUnscaled(PictureBitmap, 0, 0);
        //Output a sharper
        if(flag_colors==ColorsPen.Blue)
        {    
            //Output Blue Sharper
            bitmapBlueSharper.MakeTransparent(Color.White);
            e.Graphics.DrawImage(bitmapBlueSharper,((int)Xposition.GetValue(
                current_column)),0);
        }
        else if(flag_colors==ColorsPen.Red)
        {
            bitmapRedSharper.MakeTransparent(Color.White);
            e.Graphics.DrawImage(bitmapRedSharper,((int)Xposition.GetValue(
                current_column)),0);
        }
        //Output other pencils  
        int x=-160;
    
        for(int i= 0;i<8; i++,x+=120)
        {
            int y=-18;
            for(int j= 0;j<((int)numberLoadImages.GetValue(i)); j++)
            { 
                if(i==current_column && j==current_element_in_column)
                //if found pen for rotate   
                {    
                    flag=true;                        
                }
                else
                {
                    if(i== current_column &&j>current_element_in_column)
                    {
                        images[i][j]=this.bitmapGray;
                        e.Graphics.DrawImageUnscaled(images[i][j], xCurrentTop+x, 
                            yCurrentTop+y+posDown);
                        y-=18;
                        e.Graphics.ResetTransform();
                    }
                    else
                    { 
                        e.Graphics.DrawImageUnscaled(images[i][j], 
                            xCurrentTop+x, yCurrentTop+y);
                        y-=18;
                        e.Graphics.ResetTransform();
                    }
                }
            }                    
            yCurrentTop = yPrevTop;                    
        }    
        ///  Rotate the chosen pen  //////
        if(flag)
        {
            positionY=yCurrentTop+posY+
                images[current_column][current_element_in_column].Height/2;
            //Rotate pen
            e.Graphics.TranslateTransform( xCurrentTop+posX+
            images[current_column][current_element_in_column].Width/2,positionY); 
    
            e.Graphics.RotateTransform(angle);
            e.Graphics.ScaleTransform(1,(float) delta);
    
            images[current_column][current_element_in_column].MakeTransparent(
                Color.White);
    
            e.Graphics.DrawImage(
                images[current_column][current_element_in_column],
                new Rectangle(-images[current_column]
                [current_element_in_column].Width,
                -images[current_column][current_element_in_column].Height,
                images[current_column][current_element_in_column].Width,
                images[current_column][current_element_in_column].Height));
    
            //again output  sharper (Red/Blue)
            e.Graphics.ResetTransform();
            bitmapBlueSharper.MakeTransparent(Color.White);
            if(flag_colors==ColorsPen.Blue)
                e.Graphics.DrawImage(this.bitmapBlueSharper,
                    ((int)Xposition.GetValue(current_column)),0);
            else if(flag_colors==ColorsPen.Red )
                e.Graphics.DrawImage(this.bitmapRedSharper,
                    ((int)Xposition.GetValue(current_column)),0);
        }
        //When overriding OnPaint in a derived class, be sure to call the base 
        //class's 
        //OnPaint method so that registered delegates receive the event.
        base.OnPaint(e);        
    }
  • 此外,我想解释如何绘制橡皮筋矩形。

    Screenshot - rubber_rect.jpg

    橡皮筋矩形是一个分组对象。

    组使用橡皮筋来收集一组对象(铅笔)。
    换句话说,pencil 对象之间是完全独立的(橡皮筋矩形总是可以取消分组——每个成员都几乎完全自主于其组中的其他成员)。

    左侧的图包含一组分组的对象(铅笔);

橡皮筋矩形技术通常用于在响应用户鼠标指针输入时界定一个选区。该术语用于描述以下情况:

  1. 按住鼠标左键,定义矩形的一个角
  2. 拖动鼠标并在定义矩形另一个角的点释放
    矩形的
  3. 矩形在鼠标拖动时绘制,看起来像是
    矩形被拉伸和收缩,就像一根橡皮筋

有关橡皮筋矩形的更多信息,请访问 Microsoft 的 MSDN 页面 此处

以下示例代码演示了如何使用橡皮筋矩形

    /**********************************************************************/
   /*The method takes the p1 and p2 coordinates of the starting and      */
  /*ending points  converts and normalizes the points and draws the     */ 
 /*rubber rectangle                                                    */ 
/**********************************************************************/    
private void MyDrawReversibleRectangle( Point p1, Point p2 )
{
    // Convert the points to screen coordinates.
    p1 = PointToScreen( p1 );
    p2 = PointToScreen( p2 );

    Rectangle rc = new Rectangle(p1.X, p1.Y, p2.X - p1.X, p2.Y - p1.Y);
    // Draw the reversible frame.
    ControlPaint.DrawReversibleFrame( rc, Color.Black, FrameStyle.Thick );
}          
  /**********************************************************************/
 /* Event of moving the mouse-device                                   */ 
/**********************************************************************/
private void Form1_MouseMove(object sender, 
    System.Windows.Forms.MouseEventArgs e)
{
    if(this.timer1.Enabled==true || this.timer2.Enabled==true)
        return;
    //  called when the mouse is moved                  
    if( rubber_rect_flag ) // if it is required to draw rubber rectangle 
    {
        MyDrawReversibleRectangle( FirstPoint, SecondPoint );
        // Update last point.
        SecondPoint = new Point(e.X,e.Y); 
        MyDrawReversibleRectangle( FirstPoint, SecondPoint );
    }
}
© . All rights reserved.