简要说明一下 EG1222 这个 semester 的 project 是要做一个简单的 MasterMind Game ,这个游戏的算法是一个数学主题演绎博弈的经典例子,当然我们要做的是这个游戏而不是研究参与这个游戏的过程。
通过这个例子我们可以了解一下面向对象编程的主要思想和方法。这也是我研究这个 project 的主要目的。
首先我们从需求上来分析一下这个程序的结构。
这是一个简单的程序,不需要多个 components ,只要一个 class ,或者说只要一个 namespace ( C# )。
这个 namespace 由几个模块构成: 1 、生成随机数; 2 、用户输入数据; 3 、用户数据和生成随机数比较; 4 、成绩显示。
在面向对象编程中,对象是可以多态和继承的,这就意味着代码的复用效率需要相当高。
当然在本例中,虽然使用的是面向对象的语言,但是实际结构和逻辑还是面向过程的,无论在面向对象编程还是在面向过程编程中,方法都是模块化编程的有力工具。复杂的算法中甚至需要方法中对自身的调用实现递归。
先说一下方法这个概念。
从大致上来说,方法类似于数学中的函数。注意,是仅仅类似。
方法,顾名思义,是可以用来执行的一个代码的集合,用来实现某种功能或者进行某种操作的。
方法在声明的时候是这样的格式:
修饰符 返回值类型 方法名 [ :类 ][ :接口 ][ :父方法 ] (形参)
{
……
}
方法在调用的时候的格式:
若有返回值,则
返回值 = 方法名(实际参数);
若无返回值,则:
方法名(实际参数);
这里,生成随机数在事件 BtnStart_Click 中可以实现
private void btnStart_Click(object sender, EventArgs e)
{
clearall();
for (int i = 0; i <= 3; i++)
{
int j = ro.Next(0, 6);
intRanNum[i] = j;
}
}
intRanNum 是一个数组,用来储存随机生成的数字,范围是 0~5 ,对应 6 种颜色。
对应颜色的方法如下:
string [] strColor = new string [6] { "Red" , "Orange" , "Yellow" , "Green" , "Blue" , "Indigo" };
这样构造了一个 string 类型的数组用来存储颜色,这样数字能和这些颜色对应 string 在这个数组中的 Index 对应起来,从而把数和颜色对应起来。同样这个数组也可以用来被用于用户猜测数据显示颜色的调用过程。
Clearall() 方法是用来清除所有控件的。在程序一开始这里执行一下用来恢复程序到初始状态。
给出代码如下:
public void clearall()
{
counter1 = -1; counter2 = -1; counter3 = -1; counter4 = -1;
counterGo = -1;
counterAns = 0;
for (int i = 0; i < 40; i++)
{
String strLabelName = "lbltry" + Convert .ToString(i + 1);
Label _label = Try.Controls[strLabelName] as Label ;
string strLabelName1 = "lblChk" + Convert .ToString(i + 1);
Label __label = Check.Controls[strLabelName1] as Label ;
__label.Text = "" ;
_label.BackColor = Color .Empty;
}
}
这个方法没有返回值,所以不需要 return 语句。
用户输入数据是可以很容易通过 label_Click 事件完成的,可以使用计数器来完成计数。要注意,需要用 % 来完成颜色的转换,否则会出现数组的溢出。
以第一位为例 :
private void lblguess1_Click(object sender, EventArgs e)
{
counter1++;
lblguess1.BackColor = System.Drawing.Color .FromName(strColor[counter1 %6]);
}
还是那个方法,显示的时候直接 Color.FromName() ,直接从那个 Color 数组中读取颜色然后显示。一行代码解决问题。
关键是比较。
先讲算法。
这里有三个概念。一是 O ,表示位置和颜色都对;一是 P ,表示颜色对,但位置不对;一是 X ,表示位置和颜色都不对。
从算法上来讲,比较的过程是要有优先级的。即,先比较 O ,再比较 P ,最后一种剩余的情况就是 X 。
比如正确答案是 1112 ,用户猜测的数据是 1212 ,如果是平行比较的话,会得出结果是 OPOO ,但实际的结果应该是 OXOO ,因为猜测的第二个数字“ 2 ”在答案中已经没有可以匹配的数据了。因而应该是 X ,但是因为没有优先比较 O ,而是顺序比较每人个数字,就会出现 X 这样的情况。
而优先比较完 O 之后,需要把比较过的数据删除,但是在数组中删除数据会很不方便,因为后面所有数据的索引都要改变,还要为此写一个特别的方法而且索引变了循环变量也不好控制。所以用一个讨巧的方法,把比较过的数据变成“ -1 ”,这样在下一轮比较的时候加一个条件限制一下 -1 的情况就可以实现把比较过的数据删除了。
同样的情况也适用于 P 的比较。假设答案还是是 1012 ,我们猜测了 2204 ,那么如果不优先比较 P 的结果是,我们会得到 PPPX ,但实际是 PXPX ,因为第二个“ 2 ”,在答案中已经没有可以匹配位置的数据。换句话说,答案中只有一个“ 2 ”,而猜测中有 2 个“ 2 ”,就只能出现一次 P ,否则就说明算法有错误。
因而,整理一下。
先比较 O ,然后把得到 O 的数据对改成 -1 ;
然后再比较 P ,条件是数据不 =-1 ,然后再把得到 P 的数据对改成 -1 ;
最后剩下的情况就是 X ,在我们的程序中 X 是无用的数据,因而不计数。可以作为调试参数使用。
整个比较的过程就是:
for (int i = 0; i <= 3; i++)
{
if (intInput[i] == intRan[i] && intRan[i] != -1 && intInput[i] != -1 )
{
counterO++;
intInput[i] = -1;
intRan[i] = -1;
}
}
for (int Ran = 0; Ran <= 3; Ran++)
{
for (int input = 0; input <= 3; input++)
{
if ((intRan[Ran] != -1) && (intInput[input] == intRan[Ran]) && (Ran != input) && (intInput[input] != -1))
{
counterP++;
intRan[Ran] = -1;
intInput[input] = -1;
}
else if (intRan[Ran] != -1 && intInput[input] != -1 && intInput[input] != intRan[Ran])
{
counterX++;
}
}
}
比较的时候,其实是两个数组中的数的比较,一个是 intInput ,一个是 intRan 。比较 O 的时候,因为没有位置问题,所以只用一层循环就可以实现遍历。而当比较 P 的时候,由于有位置问题,所以需要两层循环,来实现 intInput 里的每一个数在 intRan 中的遍历。
最后是显示问题。先说一个东西。
做这个 project 最烦人的地方就在于那么多的 label ,都要显示颜色,都要显示结果,一个一个写显然很烦人,而且有些 label 要写很多遍。错一个就会出问题。
所以我们想, label1,label2,label3 ,这些名字里变的只是最后那一位数字,能不能通过这些数字来组合 label 的名字实现一次多个 label 的设置或者调用呢?
于是有了这样的代码:
for (int i = 0; i <= 3; i++)
{
String strLabelName = "lbltry" + Convert .ToString(i + 1 + Row * 4);
Label _label = Try.Controls[strLabelName] as Label ;
_label.BackColor = Color .FromName(strColor [intInput[i]]);
}
第一行用来生成对应的 label 名称,比如现在是 Row = 1 ,也就是在第一行显示,那么名字就是 lbltry1 。
第二行的代码是生成一个 System.WindowForms.Form1.Label 类的实例,叫 _label ,这个实例就叫对象,这里是真正的面向对象编程。这个对象的功能是在 Try 这个 GroupBox 里,找到名叫 strLabelName 的控件,然后把这个对象转化成这个控件。我这么干讲有可能还是不能理解,尤其是对象、实例这样的概念。
这么说,假如人,是一个类,那么每一个人,具体的人,就是人这个类的一个实例,也就是一个对象。那么人出生的过程就是这个对象实例化的过程,然后现在要找到一个人,进行一些操作,就需要把这个人的名字转化成具体这个人这个对象,然后才能找到这个人。这样说稍微有点能理解了吧?
之后就是更改背景颜色的过程啦,不多讲。
有了这样的一个东西之后,显示的部分就非常容易写啦。
下面给出比较这个方法的代码:
public int CheckColor(int [] intInput, int [] intRan,int Row)
{
string [] strResult = new string [4];
int counterO = 0, counterP = 0, counterX = 0;
int intAns = 0;
for (int i = 0; i <= 3; i++)
{
String strLabelName = "lbltry" + Convert .ToString(i + 1 + Row * 4);
Label _label = Try.Controls[strLabelName] as Label ;
_label.BackColor = Color .FromName(strColor [intInput[i]]);
}
for (int i = 0; i <= 3; i++)
{
if (intInput[i] == intRan[i] && intRan[i] != -1 && intInput[i] != -1 )
{
counterO++;
intInput[i] = -1;
intRan[i] = -1;
}
}
for (int Ran = 0; Ran <= 3; Ran++)
{
for (int input = 0; input <= 3; input++)
{
if ((intRan[Ran] != -1) && (intInput[input] == intRan[Ran]) && (Ran != input) && (intInput[input] != -1))
{
counterP++;
intRan[Ran] = -1;
intInput[input] = -1;
}
else if (intRan[Ran] != -1 && intInput[input] != -1 && intInput[input] != intRan[Ran])
{
counterX++;
}
}
}
for (int i = 0; i < counterO + counterP; i++)
{
if (i < counterO) { strResult[i] = "O" ; }
else { strResult[i] = "P" ; }
}
for (int i = 0; i < counterO + counterP; i++)
{
String strLabelName = "lblChk" + Convert .ToString(i + 1+Row * 4 );
Label _label = Check.Controls[strLabelName] as Label ;
_label.Text = strResult[i];
}
for (int i = 0; i < 4;i++ )
{
if (strResult[i] == "O" )
{
intAns++;
}
}
return intAns;
}
intAns 用来计数,记录“ O ”的个数,当 intAns=4 的时候,就可以显示“ congratulations ”啦。
最后是 buttonGo 的代码,这里是程序的执行部分。但是由于前面有 clearall() 和 CheckColor(int[] intInput, int[] intRan, int Row) 这两个方法,代码完全可以复用,所以很简单的几行就可以:代码加注释,不解释了。
private void btnGo_Click(object sender, EventArgs e)
{
int [] copy = new int [4];
intRanNum.CopyTo(copy, 0);// 将原始数据复制到另一个数组中,比较完一次就恢复一次,否则比较过程中变成 -1 会影响原始数据。
intInputNum[0] = counter1 %6;
intInputNum[1] = counter2 %6;
intInputNum[2] = counter3 %6;
intInputNum[3] = counter4 %6;// 将用户猜测的数据读入 intInputNum 数组。
counterGo ++;
counterAns = CheckColor( intInputNum,copy,counterGo);// 这里就是调用比较的方法进行比较
if (counterAns == 4)
{
MessageBox .Show("Congratulations!" );
}
for (int i = 0; i <= 3; i++)
{
String strLabelName = "lblguess" + Convert .ToString(i + 1);
Label _label = groupBox1.Controls[strLabelName] as Label ;
_label.BackColor = Color .Empty;
}
counter1 = -1; counter2 = -1; counter3 = -1; counter4 = -1;
}
总体来说,完成基本的 MasterMind Game 的功能就需要这么多。大概 160 行左右的代码,加上必要的定义变量的语句的话。所以这样看来这个游戏还是一个初级,当然,可以加入的元素很多,比如自定义难度,计时,比如 UI 等等。
算法只有更好,没有最好。如有误,请指正。