该程序是TICTACTOE游戏,棋盘为3*3,3个棋子连起来即为赢。输入参数时,若输入的数字超过边界或在已有棋子的地方再输入,则提示出错,要求重新输入。
1. 将程序改写成类。
2. 将棋盘扩充成4*4。
3. 统计输赢结果并打印输出。
4. 选做:使游戏更生动。(输赢条件相同)
评定难易程度:B
设计一个玩家下棋的操作类,里面封装了棋盘﹑棋子类别和输赢次数等成员数据。
操作类的函数包括有初始化棋盘﹑下棋﹑判断输赢等。定义一个类的对象,在调用类中函数时直接使用对象名加函数名即可。
将两个玩家定义为两个数组用于区分他们的信息,分别用‘O’和‘X’代表两个玩家所用棋子,然后分别对两个玩家通过已定义的类的对象调用类中的函数,完成下棋过程。
进一步改进程序:
- 程序中用if else if格式的语句来判断玩家的输赢,当棋盘增大时,三子连珠的各种可能性成倍增加,使得语句非常繁琐,所以将其改为for语句的循环结构进行判断,当棋盘增大时,只需更改循环条件结束的参数即可,可灵活使用于各种尺寸的棋盘。
- 为防止用户误操作,特别是在姓名的输入时的失误,可以采用cin.getline函数读入在判断确认。
- 在完成第一步的基础上可将三子连珠扩充为标准的五子棋。
- 原程序的结构(见下页)
- 类的说明
类是对一组具有相同属性、相同行为的对象的抽象的描述,不占用内存空间。可以把类看作“理论上”的对象,也就是说,它为对象提供蓝图,但在内存中并不存在。从这个蓝图可以创建任何数量的对象。从累创建的所有对象都有相同的成员:属性、行为或方法。这些对象才是具体的独立的个体,占用存储空间,因此,对象又称作类的实例。
下面以具体程序说明。
在头文件之后定义了一个名为CGobang的类:
class CGobang
{
char board[4][4];//用数组来定义一个4*4的棋盘
public:
void Guize();//打印菜单总体情况,说明游戏规则和方法,无返回值
void Dayinqp();//打印棋盘,无返回值
void Xiaqi(char*,int &,int &,char);//下棋,无返回值
int Huosheng(int,int,char);//判断那个选手赢,返回值为一整形数
void Choice(char &);//是否再玩,无返回值
void Chongzhi();//重置棋盘,无返回值
};
在这个类中,定义了一个未经说明的数组被默认为私有成员,即char board[4][4],用它来存储玩家在下棋的过程中所输入的棋子的坐标,并以此来区分表示不同的两个玩家。另外,这个类中还说明了五个没有返回值的函数以及一个返回值为整形数的函数,对它们的定义将出现在主函数完成之后,所以本着“先定义,后调用”的原则,在类中的说明相当于自定义函数中的函数说明,这样可以使在主函数调用类中的公有函数时变得更加明了清晰。
在这个函数中,对类的对象的定义出现在了主函数第一行,即:
CGobang s;//定义类的一个对象s
对这个对象的定义的目的是,在下面不同玩家的下其中的不同操作中,可以直接通过这个对象来分别调用类中的任意公有成员函数,这样就避免了通过类名来调用函数时可能出现的冗长。可以说,在这个程序里,这个对象s起到了把主函数与类连接起来的桥梁作用。
一、类的封装。
首先,在源程序代码中出现的主要函数程序有:
void PrintBoard(); // 打印棋盘
void PrintInfo(); //输出游戏提示信息
void PrintStats(int, int, int); // 输出统计的胜负信息
void IfError(int&, int&); // 判断输入时候有错
void ChoiceOfChar(char&); // 询问用户是否在玩一局
void PromptTurnO(int&, int&); // 选择“O”的玩家下棋(先走)
void PromptTurnX(int&, int&); // 选择“X”的玩家下棋(后走)
char board[3][3]; // 棋盘
在我的程序中的主要函数程序为:
void Guize();//打印菜单总体情况,说明游戏规则和方法,无返回值
void Dayinqp();//打印棋盘,无返回值
void Xiaqi(char*,int &,int &,char);//下棋,无返回值
int Huosheng(int,int,char);//判断那个选手赢,返回值为一整形数
void Choice(char &);//是否再玩,无返回值
void Chongzhi();//重置棋盘,无返回值
以此来对比说明。
◎源代码中的关于打印棋盘以及规则提示的说明的函数没有变动。
◎去掉了关于输出统计胜负信息的函数,在主函数体中用三个一组的条件自增变量代替,这样减少了复杂的函数调用,说明以及定义,精简了程序,并且达到了预想的目的。
◎由于对玩家输入是否有错的判断在玩家每次确定下棋之后必须进行,所以如果像源代码一样把玩家下棋的函数和判断是否有误分开定义的话,在没下一步棋之后都要重复调用判断函数,显得十分复杂,所以把源代码中的判断是否有错的函数与玩家下棋合并在 void Xiaqi(char*,int &,int &,char)
中,对它们的定义在后面的类的成员函数的定义中一起定义,这样可以减少很多不必要的麻烦,起到了精简程序的作用。
◎源代码中把连个玩家分别定义为两个函数,用以区别先后手,然后在定义一个数组char board[3][3](由于源代码中显示的是3*3的棋盘,所以数组定义为board[3][3])来表示棋盘。在这点我做了比较大的改动,首先是定义了一个类中的私有成员数组,用以记录每位玩家所下棋子的坐标,这样既可以通过用不同的指针变量来指向键盘的方法来区分不同的玩家,亦可以同时用它表示整个棋盘。这样使程序得到了进一步的精简。
void CGobang::Xiaqi(char*player1,int & x,int & y,char c)
{
cout<<"该"<<player1<<"下棋了,请输入:"<<endl;
cout<<"您的棋子所在行:";
cin>>x;
cout<<" "<<" "<<" "<<" "<<"棋子所在列:";
cin>>y;
while(x>4||y>4||x<1||y<1||‘ ’!=board[x-1][y-1])
//输入数字超过边界或在已有棋子的地方再输入
//' '!=board[x-1][y-1]表示已有棋子
{
cout<<"对不起,您的输入有误,请重新输入!"<<endl;
cout<<"您的棋子所在行:";
cin>>x;
cout<<" "<<" "<<" "<<" "<<"棋子所在列:";
cin>>y;
}
board[x-1][y-1]=c;//在x行,y列处下O或X
}
◎由于需要重复使用空棋盘,我又增加了一个重置棋盘的函数void Chongzhi()来进行每次开始下棋时的棋盘清空重置。
void CGobang::Chongzhi()
{
for(int Row=0;Row<4;Row++)
for(int Column=0;Column<4;Column++)
board[Row][Column]=' ';
}
◎完成对主要函数的改动之后,把它们封装到类CGobang中即可。
二、将棋盘扩充为4*4
在类的封装中已经详细的将我改动过的棋盘说明了,所以,要把棋盘扩充为n*n只需把定义过的私有成员数组board[n][n]中的n改为所需数字即可。即,定义board[4][4],就完成了对棋盘4*4的扩充。
三、统计输出结果并打印输出。
同样,在类的封装中也已说明我对记录胜负结果以及输出的方法,下面给出源代码:
记录胜负结果:
int FirstWin=0,SecondWin=0,Draws=0,x,y,N;
在每次获胜、失败以及和棋的判断完成之后,令FirstWin SecondWin Draws分别依情况自增,达到记录胜负次数的目的。
打印输出:
//输出游戏输赢次数
cout<<player1<<"赢了"<<FirstWin<<"次"<<endl;
cout<<player2<<"赢了"<<SecondWin<<"次"<<endl;
cout<<"和"<<Draws<<"次"<<endl;
cout<<"谢谢使用。"<<endl;
cout<<"任意键继续。"<<endl;
即在主函数结束时输出已记录的数值即可完成。
- 主要源程序及注释
1.主函数
void main ()//主函数
{
CGobang s;//定义类的一个对象s
s.Guize();//通过已定义的类的对象s调用Guize函数,提示玩家如何进行游戏
char player1[20],player2[20];//定义两个玩家的姓名变量,分别用两个数组储存
int FirstWin=0,SecondWin=0,Draws=0,x,y,N;//说明变量,并对部分变量付初值,以待计算输赢结果
char choice='Y';
cin.ignore(20,'\n');//输入输出流,前面如果有输入把输入行所有字符
//取空,以便后面的输入从新的一行开始
cout<<"请输入第一个玩家姓名:";
cin.getline(player1,20);//连续读取数据,实现对玩家一命名
cout<<"请输入第二个玩家姓名:";
cin.getline(player2,20);//连续读取数据,实现对玩家二命名
while(choice=='Y'||choice=='y')//条件成立,开始下棋
{
s.Chongzhi();//通过对象s调用Chongzhi函数
N=0;//对变量付初值,表示已经下在棋盘上的妻子总数
while(N<=16)//在棋盘未满的条件下下棋
{
s.Dayinqp();//打印棋盘
s.Xiaqi(player1,x,y,'O');//调用下棋的运行函数
N++;//记录已下棋子数
if(s.Huosheng(x-1,y-1,'O'))//返回值不为0则条件成立
{
cout<<player1<<"赢了。"<<endl;
FirstWin++;//记录玩家一所赢局数
break;//终止本次循环
}
s.Dayinqp();//交换玩家下棋,下棋方法同上
s.Xiaqi(player2,x,y,'X');
N++;
if(s.Huosheng(x-1,y-1,'X'))
{
cout<<player2<<"赢了。"<<endl;
SecondWin++;//记录玩家二所赢局数
break;
}
if(N==16)//当棋盘满时,此局结束
{
cout<<"和棋!";
Draws++;//记录平局总数
break;
}
}
s.Choice(choice);//让玩家选择是否在玩一次
}
//输出游戏输赢次数
cout<<player1<<"赢了"<<FirstWin<<"次"<<endl;
cout<<player2<<"赢了"<<SecondWin<<"次"<<endl;
cout<<"和"<<Draws<<"次"<<endl;
cout<<"谢谢使用。"<<endl;
cout<<"任意键继续。"<<endl;
}
2.胜负条件的判定
//定义判断获胜条件的函数
/*此函数以下棋的点为坐标分别向8个方向判断是否能构成胜利条件,若能
返回值1,则该选手获胜,程序结束;若不能,返回值为0,继续循环下棋程序。*/
int CGobang::Huosheng(int x,int y,char c)
{
int count,a;
//从 左斜上 方向判断是否构成胜利条件
for(a=0,count=0;(x-a>=0)&&(y-a>=0)&&(board[x-a][y-a]==c);a++)
if(++count==3) return 1;
//从 右斜下 方向判断是否构成胜利条件
for(a=1;(x+a<4)&&(y+a<4)&&(board[x+a][y+a]==c);a++)
if(++count==3) return 1;
//从 上向下 方向判断是否构成胜利条件
for(a=0,count=0;(x-a>=0)&&(board[x-a][y]==c);a++)
if(++count==3) return 1;
//从 下向上 方向判断是否构成胜利条件
for(a=1;(x+a<4)&&(board[x+a][y]==c);a++)
if(++count==3) return 1;
//从 左至右 方向判断是否构成胜利条件
for(a=0,count=0;(y-a>=0)&&(board[x][y-a]==c);a++)
if(++count==3) return 1;
//从 右至左 方向判断是否构成胜利条件
for(a=1;(y+a<4)&&(board[x][y+a]==c);a++)
if(++count==3) return 1;
//从 右斜上 方向判断是否构成胜利条件
for(a=0,count=0;(x-a>=0)&&(y+a<4)&&(board[x-a][y+a]==c);a++)
if(++count==3) return 1;
//从 左斜下 方向判断是否构成胜利条件
for(a=1;(x+a<4)&&(y-a>=0)&&(board[x+a][y-a]==c);a++)
if(++count==3) return 1;
return 0;
}
◎设计过程中的疑难及解答方案
在这次课程设计的过程中,我遇到了不小的阻力,还有也从中学习到了很多以前没有掌握好或这根本没有掌握的知识。
在定义玩家名字的输入接受的时候我遇到了一个从未见过的函数cin.ignore(),这个函数是我从一个同样是输入存储用户姓名的程序里看到的,上面对它的注释是:“输入输出流,前面如果有输入把输入行所有字符去空,一边后面的输入从新的一行开始。”我对这个想了很长时间,但是一直没有明白。最后解决问题的方法是我通过网络查询找到了这个函数,在其他一些人那里得到了答案:“cin.ignore(a,ch)方法是从输入流(cin)中提取字符,提取的字符被忽略(ignore),不被使用。每抛弃一个字符,它都要计数和比较字符:如果计数值达到a或者被抛弃的字符是ch,则cin.ignore()函数执行终止;否则,它继续等待。它的一个常用功能就是用来清除以回车结束的输入缓冲区的内容,消除上一次输入对下一次输入的影响。比如可以这么用:cin.ignore(1024,'\n'),通常把第一个参数设置得足够大,这样实际上总是只有第二个参数'\n'起作用,所以这一句就是把回车(包括回车)之前的所以字符从输入缓冲(流)中清除出去。”这样我就对这个函数有了很清楚的认识,所以就将它应用到了我的设计中了。
在设计过程中最大的困难就是对胜负判断条件的改写。依据课本中程序设计思想的提示,我需要把源代码中的if else格式的语句改写成for循环语句,以便于后面对任意大棋盘的n子连珠程序的扩展。
起初我只是单单从for循环的简单语句中找答案,但是一直没有结果,我也觉得不应该那么简单,但是却找不到突破口。于是我向另一位同学请教,在我们俩的共同努力下,我们觉得应该把for循环语句与if条件语句结合使用,于是,我便得出了我现在的判断方法。下面举例详细说明:例如从左斜上方向判断
课本中的源代码是
else if (board[2][0]== 'O' && board[1][1]== 'O' && board[0][2]== 'O')
{
cout << "O player wins the game" <<endl;
FirstPlayer++;
break;
}
改动过后的代码是
for(a=0,count=0;(x-a>=0)&&(y-a>=0)&&(board[x-a][y-a]==c);a++)
if(++count==3) return 1;
需要注意的是,源代码只是局限于3*3的棋盘进行三子游戏的从左斜上的获胜判断,不论棋盘变化或者子数变化都会引起代码的非常大的变动,这样显然不利于棋盘以及游戏的扩充。而对于修改后的判断方法,如果棋盘变化,只需修改相应的a的值即可,如果游戏子数变化,那么只要把if(++count==3)中的3改掉就可以了。比如说五子棋可以改为if(++count==5)。
当小错误解决了以后,在重复运行程序的过程中,我测试了各种数据及不同数据类型的输入,发现在玩家输入坐标的时候如果不是int型的,就会导致程序的无效死循环,为了解决这个问题我才又引入了
while(!cin) //当输入的行坐标不是int型时
{
char str[20];
cin.clear();//清空输入缓冲区
cin.getline(str,20);//接收原输入数据
cout<<"非法输入,请输入一个整形数:";
cin>>x; //重新输入
}
这一程序,从而解决了这个问题。
◎设计的不足
我的设计已经完成了,但是其中还有很多不足之处,其中最明显的就是我没有完成课程设计要求中的选作项,起初,我是想按照课本程序设计思想中提到的设一个电脑玩家可以与人竞赛的程序的,但是我在思考之后还是决定不添加这个项目,原因是我并没有什么好的办法来完成这个程序段。这个问题也算这次设计过后的一个遗留问题吧,我会在以后的学习中努力掌握这个问题的解决方法的。
在这次课程设计中,我不只一次的从早上八点一直到下午六点一直坐在机房,调试已经写好的程序,当晚上拖着疲惫的身体走进食堂开始这一天第一顿饭时,我的心里虽然能很清楚的感受到身体的疲惫,但却一直充满了喜悦,这大概就是付出的喜悦吧。
还有,通过这次设计,我还掌握了很多有用但以前不知道的知识:首先,我比以前更加知道了注释的重要性,而且学到了一些关于注释的技巧,比如说,当一段注释的话比较长的时候,为了美观起见,可以用/*开头,在用*/结尾,这样就可以轻松完成一段比较长的注释了;其次,我还学到了很多有用的函数,比如,cin.ignore(),还有解决由于输入不正确而引起的无效循环这一诟病;另外,我还在反复的输入、调试和修改中进一步加强了对以前已有知识的掌握与运用,比如说对指针变量的应用,对数组的理解与应用(说实话,我还是第一次觉得指针与数组并用会让问题变得如此明了)等等。
还有一点很重要的就是,这些都是我一个人完成的,当遇到问题时仔细思考,如果没有结果就虚心求教,直到懂得个中缘由为止,看着完成的设计,一种自豪的喜悦充满了内心。真的要感谢老师给我们的这次实践机会。
总之,通过十天左右的摸索,我终于完成了这个课程设计,个人从中是获益匪浅,另外也希望请老师能为我改正不足之处。谢谢老师。
附1:程序运行界面(棋盘4*4的三子连珠游戏)
图一:初始界面
图二:提示玩家输入姓名
图三:开始游戏,名为a的玩家先手,提示玩家输入他的棋子所在行。
图四:交换玩家,由姓名为s的玩家下棋。
图五:当玩家输入的行或者列数不是在棋盘范围内的整形数或者输入的信息不是整形数时,提示玩家重新输入。
图六:当有一个玩家下的棋子符合胜利条件时,系统提示玩家胜利,并询问玩家是否继续进行游戏。
图七:当玩家选择不再进行游戏(即选择n)时,系统输出两位玩家分别获胜次数以及和棋次数。游戏结束。
附2:程序运行界面(棋盘12*12的五子棋游戏)
图一:游戏开始,提示玩家继续进行。
图二:请玩家输入姓名。
图三:打印出12*12的棋盘,请玩家输入棋子。
图四:交换玩家下棋。
图五:输入有误时提示玩家重新输入。
图六:有玩家获胜时,系统提示是否继续游戏。
图七:玩家选择不再游戏,系统输出总胜负情况,游戏结束。