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





5.00/5 (23投票s)
一系列关于两人策略数学游戏的第二部分文章
↵
引言
什么是 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 操作。例如,局面是
因此,要判断这是否是一个必败局,我们必须对每一行中的物品数量进行 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 个。
摘要
获胜所需的步骤是
- 轮到您时,将每行中的物品数量转换为二进制数并进行 XOR。
- 如果结果数字是
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); }
- 此外,我想解释如何绘制橡皮筋矩形。橡皮筋矩形是一个分组对象。
组使用橡皮筋来收集一组对象(铅笔)。
换句话说,pencil
对象之间是完全独立的(橡皮筋矩形总是可以取消分组——每个成员都几乎完全自主于其组中的其他成员)。左侧的图包含一组分组的对象(铅笔);
橡皮筋矩形技术通常用于在响应用户鼠标指针输入时界定一个选区。该术语用于描述以下情况:
- 按住鼠标左键,定义矩形的一个角
- 拖动鼠标并在定义矩形另一个角的点释放
矩形的 - 矩形在鼠标拖动时绘制,看起来像是
矩形被拉伸和收缩,就像一根橡皮筋
有关橡皮筋矩形的更多信息,请访问 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 );
}
}