五子棋(C++面向对象实现)

代码链接:
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,萌新入坑,在所难免,让大佬们见笑了。恳请各路高手多多指点批评

祝大家新年快乐!事事顺心!

  • 38
    点赞
  • 133
    收藏
    觉得还不错? 一键收藏
  • 8
    评论
在 C 语言中,没有原生的面向对象特性,但可以通过结构体和函数指针等方式来实现类似于面向对象的编程。以下是一个简单的五子棋游戏的实现,其中包含了棋盘、棋子和玩家等相关结构体,以及对应的函数指针实现的方法。 ```c #include <stdio.h> #include <stdlib.h> #include <string.h> #define BOARD_SIZE 15 #define MAX_NAME_LENGTH 50 typedef struct { int x; int y; } Point; typedef struct { char name[MAX_NAME_LENGTH]; char chess; } Player; typedef struct { char board[BOARD_SIZE][BOARD_SIZE]; Player *players[2]; int current_player; void (*print_board)(char board[BOARD_SIZE][BOARD_SIZE]); void (*place_chess)(char board[BOARD_SIZE][BOARD_SIZE], Point point, char chess); int (*check_win)(char board[BOARD_SIZE][BOARD_SIZE], Point point); Point (*get_input)(void); } Game; void print_board(char board[BOARD_SIZE][BOARD_SIZE]) { printf(" "); for (int i = 0; i < BOARD_SIZE; i++) { printf("%c ", i + 'A'); } printf("\n"); for (int i = 0; i < BOARD_SIZE; i++) { printf("%2d", i + 1); for (int j = 0; j < BOARD_SIZE; j++) { printf("%c ", board[i][j]); } printf("\n"); } } void place_chess(char board[BOARD_SIZE][BOARD_SIZE], Point point, char chess) { board[point.x][point.y] = chess; } int check_win(char board[BOARD_SIZE][BOARD_SIZE], Point point) { char chess = board[point.x][point.y]; // check horizontal int count = 1; for (int i = point.y - 1; i >= 0; i--) { if (board[point.x][i] == chess) { count++; } else { break; } } for (int i = point.y + 1; i < BOARD_SIZE; i++) { if (board[point.x][i] == chess) { count++; } else { break; } } if (count >= 5) return 1; // check vertical count = 1; for (int i = point.x - 1; i >= 0; i--) { if (board[i][point.y] == chess) { count++; } else { break; } } for (int i = point.x + 1; i < BOARD_SIZE; i++) { if (board[i][point.y] == chess) { count++; } else { break; } } if (count >= 5) return 1; // check diagonal count = 1; for (int i = point.x - 1, j = point.y - 1; i >= 0 && j >= 0; i--, j--) { if (board[i][j] == chess) { count++; } else { break; } } for (int i = point.x + 1, j = point.y + 1; i < BOARD_SIZE && j < BOARD_SIZE; i++, j++) { if (board[i][j] == chess) { count++; } else { break; } } if (count >= 5) return 1; // check anti-diagonal count = 1; for (int i = point.x - 1, j = point.y + 1; i >= 0 && j < BOARD_SIZE; i--, j++) { if (board[i][j] == chess) { count++; } else { break; } } for (int i = point.x + 1, j = point.y - 1; i < BOARD_SIZE && j >= 0; i++, j--) { if (board[i][j] == chess) { count++; } else { break; } } if (count >= 5) return 1; return 0; } Point get_input(void) { char input[MAX_NAME_LENGTH]; printf("Please input the position (e.g. \"A1\"): "); scanf("%s", input); Point point; point.x = input[1] - '1'; point.y = input[0] - 'A'; return point; } void play_game(Game *game) { while (1) { Player *player = game->players[game->current_player]; printf("%s's turn (%c)\n", player->name, player->chess); Point point = game->get_input(); if (game->board[point.x][point.y] != ' ') { printf("This position has been occupied, please choose another one.\n"); continue; } game->place_chess(game->board, point, player->chess); game->print_board(game->board); if (game->check_win(game->board, point)) { printf("%s wins!\n", player->name); break; } game->current_player = 1 - game->current_player; } } int main() { Game game; game.current_player = 0; game.print_board = print_board; game.place_chess = place_chess; game.check_win = check_win; game.get_input = get_input; Player player1 = {"Player 1", 'X'}; Player player2 = {"Player 2", 'O'}; game.players[0] = &player1; game.players[1] = &player2; memset(game.board, ' ', sizeof(game.board)); game.print_board(game.board); play_game(&game); return 0; } ``` 在上述代码中,我们定义了 `Point`、`Player` 和 `Game` 三个结构体,分别表示一个点的坐标、一个玩家和一个游戏。`Game` 结构体中包含了棋盘、两个玩家、当前玩家、打印棋盘、落子、胜利判断和获取输入等函数指针。通过这些函数指针,我们可以方便地实现不同的游戏逻辑。 在 `play_game` 函数中,我们通过循环来进行游戏,每次轮到一个玩家时,获取输入、落子、打印棋盘、判断胜利并切换到下一个玩家。如果有一方胜利,则结束游戏。
评论 8
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值