代码链接:
https://github.com/zhouzg/FiveChess/tree/master
https://download.csdn.net/download/dreamlike_zzg/10948075
VS2017,控制台输出字符显示棋盘。共定义的6个class,即棋子类(Chess)、棋盘类(ChessBoard)、棋手类(Player)、裁判类(Judge)、显示类(Displayer)、游戏类(Game)。有人机对弈人人对弈两种模式,时间原因AI下棋落子是随机的。
序:
建国70年2月1日周五,寒假首flag告拔——以C++编作五子棋。上学期,求学于雁西湖畔,受教于杨LX大师,获益匪浅,相见恨晚。作五子棋乃杨之令,然选课未果,虽遵令而学分犹不可得。当是时,期末众考威逼咄咄,不得顾,遂妥,暂留坑。1月29日始填坑,醉坑三日有余,今日坑毕时,如梦初醒,方觉此乃吾独作纯软project之首例,悲喜交加,不可断绝。何喜之有?首例终竣,从无生有,质变也。何悲之有?羞才疏学浅,恨竣工时晚,实愧也。感慨良多,情溢于言表,遂逐此文,以自勉。诚求各路贤士不吝赐教,请洒潘江,各倾陆海。
正文
程序按面向对象的方式编写,共定义的6个class,即棋子类(Chess)、棋盘类(ChessBoard)、棋手类(Player)、裁判类(Judge)、显示类(Displayer)、游戏类(Game)。其中棋手类作为基类派生出真人棋手类(Human)和电脑棋手类(AI)。由于本人写五子棋的目的为体会C++的面向对象编程,再加上个人时间能力有限,所以没有给AI什么策略,只让其随机落子。毕竟策略设计与面向对象编程关系不大。
接下来主要分享下本人关于这5个类设计的一点思考。在课上获得了些体会:C++比C语言难,不难在语法,而是难在对面向对象的理解和类的设计。类的设计有些艺术的味道,设计没有对错之分,但好的设计有很强的复用性,可以为以后的工作带来便利,节省时间。杨大师说,省时间就是省钱!
下面仅是我的分类设计,大家有不同想法欢迎留言讨论。
Chess类
定义棋子,棋子包含颜色、位置。
ChessBoard类
定义棋盘,主要包括棋盘的大小、每个位置的状态(是否有棋子)、添棋子和删棋子操作,棋谱,显示棋盘所需的字符。
Player类
定义棋手,主要包括棋手姓名、执棋子的颜色、下棋操作。Human和AI作为其派生类。
Judge类
判断落子棋子是否合法、是否有人获胜、执行悔棋操作。
Displayer类
负责所有的显示任务,包括显示棋盘和提示信息。
Game类
负责整个游戏的流程。
接下来谈一下设计类时让我有些纠结的地方
“悔棋”由哪个类来负责?
这个是上课讨论的一大焦点,主要分为两派:1.由棋手类负责。因为悔棋请求是由棋手发起的,而且现实中把棋盘上的棋子拿掉也需要人来执行。2.由裁判类负责。因为棋盘的状态变动都需要裁判类判断是否合法,而且悔棋需所有棋手都同意才行,有时不能悔棋,需要第三方调解。
个人观点:悔棋请求由玩家发起,撤回棋子由棋盘类负责,而裁判类决定是否撤棋子。
首先本人不同意第一个观点,因为撤棋子不一定非要玩家执行。因为悔棋时撤回的棋子是固定的,即最新下出的那步棋,这是由当前棋局决定的,所以不需棋手来额外判断。如果把该程序复用到多人棋类游戏中,由棋手类负责悔棋的话,悔棋时需要每个棋手设法获取自己所有落子的顺序,并轮流执行撤棋操作,感觉好麻烦。
若想悔棋多次,则需要获取棋盘中每个棋子的先后顺序,也就是要维护一个棋谱,而棋谱和棋盘状态均是棋盘类的成员,所以很自然就让棋盘类来执行撤棋操作。
悔棋需要所有玩家都同意才可执行,所以需要第三方来协调,这是裁判的职能。
这样设计给人的感觉是我们的棋盘桌是“电动”的,有个按钮,按下则棋盘自动把最近的那几步棋撤走。而按钮的掌控权在裁判手里,他会根据局势判断是否按下这个按钮。
还有一点,判断悔棋是否可以执行是在Judge的成员函数内完成?还是在游戏进行的流程中完成(即Game类中)?我是按照后者实现的。考虑到程序的复用性,我希望除Game类外的其他类的功能尽可能的独立于游戏流程。这样将程序修改为其他棋类时,由于不同的棋局进行流程可能不同,所以Game类的修改无法避免,但其他类的改动会相对较小。
“棋手轮换”在哪里实现?
棋盘类游戏都是玩家轮流下出棋子。所以Player *current_player(指向当前要出棋的玩家)所指向的对象要不断改变。“棋手轮换”在哪里实现与“判断悔棋是否可以执行” 在哪里实现面临的问题相似。由Judge来决定当前轮到谁出棋似乎很合理,但是“轮到谁”是游戏进行的流程决定的,轮到谁了就是谁,是很客观的,无需裁决。五子棋流程简单,两个玩家一替一次交换就行了,两种方式实现起来都不困难,但对于较复杂的游戏来说,可能出现次序中途有变动的情况(比如“大富翁”里强制休息一轮,“飞行棋”先到终点的玩家不必参加下一轮)。这时Judge想要判断下一步轮到谁了,还要从游戏进行情况来判断,需要许多描述游戏当前状况的参数,直接在Game类的游戏流程里实现似乎更方便些。
为何需要Displayer类?
本程序是在控制台里输出,直接cout就行了,为何要搞个显示类,再在Displayer的成员函数中使用cout?岂不多此一举?这里是为了方便修改为其他显示方式,修改时只需将成员函数中的cout替换就行了。
Player类的设计
棋手分为真人玩家(Human)和电脑玩家(AI),均从Player类中派生得到。Human与AI的区别有两处:1. Human可以悔棋,AI不用2.落子方式不同,即成员函数GiveChess()不同。区别1容易处理,主要谈谈区别2。Human出棋需要手动输入棋子坐标,需要传参,即Human.GiveChess(Position p),而AI出棋不需要传参,根据棋局便可决定落子位置,即AI.GiveChess( )或AI.GiveChess(ChessBoard board)。这就导致了两个派生类存在同名不同参的成员函数,给多态性的保证造成影响。
Player类里将GiveChess定义为纯虚函数virtual Chess GiveChess( ) = 0;在派生类中直接分别重写Chess Human::GiveChess(Position p)和Chess AI::GiveChess( )的话,无法对基类虚函数覆盖,只是将其隐藏。执行下列语句必然error,
Player *ptr;
Human human;
AI ai;
ptr=&ai;
ptr->GiveChess();
ptr=&human;
ptr->GiveChess(position);
这个问题卡了我好久,也让我回想起了杨大师的一句话,意思是C++的类继承机制使派生类能加能改不能删,即可以增加成员,修改基类成员函数,但是不能丢弃基类成员。就是有些基类成员对派生类无用,但派生类依然要将其保留下来。杨大师善于举例子、打比方,士兵的坐骑由战马演变成了坦克,可以把战马看做基类,派生出了坦克类,从外表看坦克与马差异巨大,但是把坦克的盖子掀开,里面还“藏”了匹马。我这里就是想把基类的GiveChess丢弃,增加不同参数的同名函数。好像扯远了,哈哈。
为了解决这个问题,我把基类的虚函数这样写:
virtual Chess GiveChess(Position p = { -1,-1 }) = 0;
由于含有默认参数,调用AI::GiveChess( )时不需要传入参数,当然随便传入一个Position参数也没事儿,反正函数内部用不到这个Position。这样就从形式上解决了问题,虽然连自己都感觉怪怪的。
我还征求了其他同学的看法,有人说有时基类的源码不能随意修改,不能基类成员函数加默认参数。给出的方案是把AI::GiveChess需要的参数(position p)中作为新的数据成员加入AI类,在加一个成员函数AI::SetPosition(Position p)负责修改position p的值。在调用AI::GiveChess( )前先调用一下下AI::SetPosition(Position p)就行了。但是这也存在问题,ptr->SetPosition( )会报错,因为基类里没有SetPosition成员函数。该方案还是不行。
同名不同参的函数如何实现多态?这个本人目前还没有其他好的办法。换个思路吧。也许我的类设计本事就有些问题,非要C++去做他不善于做的事。那就改吧!睡前想到一个方案,干脆基类Player中写两个虚函数:
virtual Chess GiveChess(Position p) = 0;
virtual Chess GiveChess() = 0;
也不区分Human和AI类了,只派生一个Human_AI类,在Human_AI类里把两个函数都重写,需要真人就只调用GiveChess(Position p),需要电脑人就只调用GiveChess()。这样设计也有其合理性:真人下棋时会需要电脑提示,相当于让电脑替自己下一步棋,所以真人棋手GiveChess()和GiveChess(Position p)都需要。
字数逼近3K了,想说的也差不多了,总结一下吧。这次写五子棋的初衷已经达到了,的确对面向对象编程有了初步的体会。面向对象设计分类可以抽象出相对独立的模块,有助于大型项目的维护,也带来了更强的复用性。就这个五子棋而言,用C语言按照面向过程方式写或许更容易,甚至一个main函数就搞定了,但是复用性就要大打折扣了。但用C++,以后想要写个其他什么棋类游戏,只需修改那6个类的部分成员就行了,整体框架基本不变。
另外,写纯软程序感觉就是爽,定位导致出错的问题所在比写单片机程序快多了。不过过去写的单片机程序也不是一点用都没,在写游戏流程时,跟写单片机时似曾相识,一些经验同样适用。
好了,就到这吧!文笔不好,赘述颇多,感谢您的耐心阅读,谢谢!目前本人对C++的体会只有这些,可能很low,萌新入坑,在所难免,让大佬们见笑了。恳请各路高手多多指点批评。
祝大家新年快乐!事事顺心!