当前位置: 代码迷 >> 综合 >> 01-0002 C++实现控制台五子棋[缺少AI]
  详细解决方案

01-0002 C++实现控制台五子棋[缺少AI]

热度:62   发布时间:2023-11-24 01:59:35.0

C++实现控制台的五子棋

  • 1.什么样子的程序?
    • 1.1 控制台 || 应用界面?
    • 1.2 面向过程 || 面向对象?
    • 1.3类的划分 && 信息存储?
    • 1.4 输入坐标 || 上下左右键?
  • 2.几个类的划分&&负责的功能
    • 2.1 Piece类
    • 2.2 Player类
    • 2.3 Chessboard类
    • 2.4 整体结构
  • 3.函数解释
    • 3.1 Piece* Player::drop_gobang(int x, int y);
    • 3.2 Player::~Player();
    • 3.3 void Chessboard::reset();
    • 3.4 void Chessboard::ini_board_arr();
    • 3.5 void Chessboard::show_board();
    • 3.6 bool Chessboard::referee(const Piece& p);
    • 3.7 main函数中的逻辑
  • 4.AI部分[有待实现]
  • 5.Gif展示
    • 5.1 棋盘展示
    • 5.2 下棋展示
  • 6.源码

1.什么样子的程序?

1.1 控制台 || 应用界面?

如果是控制台:绘制界面的函数需要自己写,不是很方便,在不同的电脑下显示的还有可能不一样,显示起来不是特别的美观,但是控制台的刷新很快,人眼一般难以识别,这个程序主要是为了写个五子棋,不求太多。

如果有应用界面:自然是美观,但是需要学习图形库,上次的那个EasyX是真个用的恶心,需要些界面与操作之间的交互,会比较麻烦,一大堆的字符转化也会让码代码的过程变得不愉快。

决定直接在控制台绘制界面,使用制表符来绘制棋盘。

1.2 面向过程 || 面向对象?

面向过程:一大堆的函数,main()中的逻辑会很长,写代码的时候很乱,但是可以下手就开始写,不用想太多,调试就好了。

面向对象:各司其职,不同的人设计出来的逻辑结构也不一样,C++推出三种编程模式:过程编程面向对象泛型编程,这其中有先后关系,使用面向对象会不会高级一点?会不会更加简单合理一点?

决定面向对象写这个程序。

1.3类的划分 && 信息存储?

棋子要有,玩家要有,棋盘要有,这是最基本的三个类。

玩家落子,棋盘容纳棋子,棋盘存储每个棋子的信息,棋盘甚至可以盘算哪一方赢了。玩家落子的时候,棋盘接收一个棋子然后改变棋盘的信息,玩家下了n=1个棋子。

棋盘的信息,自然而然的想到用二维数组[动态的],而玩家拥有的棋子信息,用vector、stack、list等存储都可以,我选择了vector

最终只设置上述三个类,使用动态二维数组存储棋子信息,使用vector来存储棋手落子的信息。

1.4 输入坐标 || 上下左右键?

输入坐标:设想了几种输入坐标的方式:带括号的输入数字,或者字母,中间用逗号或者是空格隔开,输入的检测需要额外的代码实现。

方向键:方向键会方便一点,但是这个时候需要设置光标提示玩家在屏幕中的位置,此时需要多来一部分代码实现,就会破坏棋盘绘制的逻辑。

最终决定用a-o的字幕表示棋盘行列,输出的时候不用考虑两位数占位问题。输入的格式为“ab”、“a b”,不带引号,中间可以有空格或者无空格。[利用了cin的特性]

相关链接:
链接1:10000以内unicode对照表
链接2:markdown修改字体、颜色
链接3:五子棋源码-C++

2.几个类的划分&&负责的功能

2.1 Piece类

代码:

#pragma once
class Piece {
    
public:int x;//x坐标int y;//y坐标char color;//颜色'B'or'W'Piece(int, int, char);//通过坐标与颜色初始化~Piece();//析构函数显式[说不定以后会有用]
};

备注:上述文件为Piece.h文件,是棋子类。文件实现起来很简单,此处不给出cpp文件的实现。由于方便操作,这里直接将所有的内容都设置成了public。

2.2 Player类

代码:

#pragma once
#include "Piece.h"
#include <vector>
#include <string>
using namespace std;
class Player {
    
public:vector<Piece> p;//玩家拥有棋子char kind;//玩家是哪一方string p_inf;//玩家的个人信息,之后需要拆开,看着更加的正规一点,目前假定所有的个人信息都一样//char mode;//棋手是电脑还是普通玩家,模式选择的时候使用Player();Player(char);//规定玩家是哪一方~Player();//事实上要清楚vector中的元素,其他的不用管Piece * drop_gobang(int,int);//玩家落子,产生一个棋子加入队列,记录玩家落子的信息,并返回该棋子,在棋盘中有所显示。表示玩家要在哪里落子,步骤就是先生成一个棋子,然后将棋子放在那个位置,逻辑合理。
};

备注:上述文件为Player.h,而且注释已经很清楚了,为更加明确,需要格外的注意其中的玩家信息,在后期玩家有各种各样的信息,需要将其拆开,来记录玩家的所有信息,相应的构造函数需要新加重载。一定要仔细的看注释。析构函数需要单独写,后面会提到。

2.3 Chessboard类

代码:

#pragma once
#include "Piece.h"
#include <iostream>
using namespace std;
class Chessboard {
    
public:int x_axis;//横线数目int y_axis;//竖线数目int** board;//动态二维数组Chessboard();//设置默认参数,15*15,初始化动态二维数组的相关信息Chessboard(int,int);//设置可拓展棋盘void show_board();//绘制棋盘bool show_board(const Piece&);//重载,添加棋子并判断是否成功添加,成功返回true,否则返回false,修改二维数组并重新绘制棋盘void show_rule();//单纯的展示规则,按键之后刷新bool referee(const Piece&);//判断新的落子是否获胜,如果赢了,就展示获胜的信息void reset();//重置棋局
private:void ini_board_arr();//初始化棋盘,就最开始调用一次,设置成了私有
};

备注:上述文件为Chessboard.h,每一个变量或者函数的含义都在注释里面了,一定要仔细看注释

注意下面两函数:

bool show_board(const Piece&);//重载,添加棋子并判断是否成功添加,成功返回true,否则返回false,修改二维数组并重新绘制棋盘
bool referee(const Piece&);//判断新的落子是否获胜,如果赢了,就展示获胜的信息

函数1:返回bool类型是为了方便在main函数中写输入逻辑。可以设置一下返回int类型,判断是哪种输入错误,或者是改成void类型,在main函数中进行判断,然后直接输入。
函数2:判断是否有获胜的信息,任意一方获胜都应该停止棋局。这里无需传递int类型的值,当前棋手落子之后出现获胜局面一定是他获胜,不需要额外的信息。

2.4 整体结构

备注:参考上述三个类的之间的include关系,可以得到结构,此处不再给出。

3.函数解释

3.1 Piece* Player::drop_gobang(int x, int y);

//玩家落子
Piece* Player::drop_gobang(int x, int y) {
    Piece *piece = new Piece(x, y, this->kind);p.push_back(*piece);//这些内存后来都会在Player的析构函数中删除掉return piece;
}

备注:这里存储玩家棋子信息,可能对以后的AI有所帮助,但是实际上并没有太大作用,单纯方便拓展

3.2 Player::~Player();

//清空向量空间
Player::~Player() {
    vector<Piece>().swap(this->p);//与临时对象交换以释放内存
}

备注:Vector清空之后并不会减少容量,clear函数只是清空了每一个数据块之中的数据,但是这个空间还是vector的。上述方法是与临时对象交换,临时对象会在该函数周期结束之后自动销毁,而这个vector就会变成空,是一个效率挺高,代码挺少的方法。

3.3 void Chessboard::reset();

//重置棋盘数组
void Chessboard::reset() {
    for (int i = 0; i < x_axis; i++) {
    for (int j = 0; j < y_axis; j++) {
    if (i == 0 || j == 0 || i == x_axis - 1 || j == y_axis - 1)board[i][j] = 9;elseboard[i][j] = 0;}}
}

备注:加入棋盘是1515的,这里绘制的是1717的,在外围的一圈都标注成9,棋盘位置标注成0,如果是黑子,将会是1,白子是-1,是为了方便检测,才这样刻意设置。[大了一圈]

3.4 void Chessboard::ini_board_arr();

//初始化棋盘数组--分配空间
void Chessboard::ini_board_arr() {
    board = new int* [x_axis];for (int i = 0; i < x_axis; i++) {
    board[i] = new int[y_axis];}reset();
}

备注:如上述方法,初始化动态二维数组,然后调用了reset()函数,再对数据进行初始化。[先分配空间,然后初始化数据]

3.5 void Chessboard::show_board();

//绘制棋盘,通过制表符等
void Chessboard::show_board() {
    //先不要管是什么字体,能用就好,最终是要直接读取鼠标信息的cout << " ";for (int i = 1; i < this->x_axis-1; i++) {
    cout << (char)(i + 97-1) << " ";}cout << endl;for (int i = 1; i < this->x_axis-1; i++) {
    for (int j = 0; j < this->y_axis-1; j++) {
    if (j == 0)cout << (char)(i + 97-1);else if (this->board[i][j] == 0) {
    if (j > 1 && j < this->y_axis - 2)cout << (i == 1 ? "┬─" : i == (x_axis - 2) ? "┴─" : "┼─");else if (j == 1)cout << (i == 1 ? "┌─" : i == (x_axis - 2) ? "└─" : "├─");else if(j==(y_axis-2))cout << (i == 1 ? "┐" : i == (x_axis - 2) ? "┘" : "┤");}else if (this->board[i][j] == 1)cout << "●";elsecout << "○";}cout << endl;}
}

步骤

  • 1.两个必要的循环,分别绘制行、列
  • 2.绘制行分为两部分,第一行,与剩下的15行。第一行是ASCII码转字符绘制每一列的标识,剩下的15行分别绘制[此时看列]。
  • 3.15列中的第一列是字母,转换后输出。
  • 4.第二列的第一行是什么,中间是什么,最后一行是什么。
  • 5.中间几列的第一行是什么,中间是什么,最后是什么。
  • 6.最后一列的第一行是什么,中间是什么,最后是什么。
  • 7.如果其中有一些地方有值,需要进行替换,分别对应黑子白子。

备注:步骤很白话,简单易懂,不解释。

cout << (i == 1 ? "┬─" : i == (x_axis - 2) ? "┴─" : "┼─");

备注:三元表达式接连使用,如果是第一行,就使用"┬─",否则判断是否是最后一行,如果是则"┴─",否则就是在中间,使用"┼─"。

3.6 bool Chessboard::referee(const Piece& p);

//落子后检查是否获胜,棋盘是0-16,其中0、16行列是余出来方便检测的
bool Chessboard::referee(const Piece& p) {
    bool dir=true;//判断正向查,还是反向查int cha_n=0;//判断折回查询的次数int t_x = p.x;//落子点的x坐标,存储起来,作为逆向查找的起点int t_y = p.y;//落子点的y坐标,存储起来,作为逆向查找的起点int t_r = this->board[t_x][t_y];//当前棋子所代表的值int d[4][2] = {
     {
    1,1},{
    1,-1},{
    0,1},{
    1,0} };//二维数组,规定查找的方向//分为四个方向进行检测stack<int> s;for (int i = 0; i < 4; i++) {
    stack<int>().swap(s);//清空栈内元素[清理缓存]s.push(1);//将当前棋子压入栈中cha_n = 0;//交换次数重置为0while (cha_n < 2) {
    //根据dir判断应该查找的方向,可以将三元表达式拆开来看,会变得很清楚if ((dir ? this->board[t_x+=d[i][0]][t_y+= d[i][1]] : this->board[t_x+=(-d[i][0])][t_y+=(-d[i][1])]) == t_r)s.push(1);//遇到一个相同颜色的棋子就入栈一个else {
    dir = !dir;//设置反向查找,第二次反向的时候刚好进入下一轮t_x = p.x;//反向的时候应当归位t_y = p.y;cha_n++;//反向一次,就记录一次}}if (s.size() >= 5)return true;}return false;
}

备注:上述代码是我能想到的最简单的代码了,设定四个正方向,设置逆向查找的标志,设置一个栈来存储能够找到的信息,一般来说斜着五子的结果比较多,所以将斜方向判断放在前面,设置一个二维数组用来存储方向信息。每次反向的时候重置查找起始位置的信息,并将反向标志dir取反,记录反向的次数,如果是反向了两次就跳出循环,此时的位置信息,dir等已经被重置,判断栈里面的元素是不是超过5个,是的话就说明出现胜利的局面,不是的话进入下一轮循环[记得重置记录量][一定要仔细看]。
备注:由于之前设置棋盘的时候多设置了外面一圈,数字为9,所以在这个检测函数里面,不需要对边缘进行检测,因为始终都在圈内查找。

3.7 main函数中的逻辑

int main() {
    //如何在C++中输出Unicode编码的字符Chessboard board;//定义棋盘,调用空构造函数,进行初始化Player p1('B');//定义并初始化玩家一Player p2('W');//定义并初始化玩家二Piece* p;//临时棋子,指针类型bool win_signal;//胜利信号,用来判断是否当前局面可以定胜负bool draw_signal;//根据玩家输入判断是否成功下棋char x, y;//棋子的位置,需要输入int player_contral;//控制应该下棋的玩家system("mode con cols=33 lines=20");//初始化界面大小system("color 09");//修改界面字体的颜色,0是背景色,9是字体色board.show_rule();//展示五子棋的规则while (1) {
    //循环下棋win_signal = false;//初始化,刚开始无人胜利system("cls");//清屏player_contral = 1;//P1先落子board.reset();//重置棋盘信息board.show_board();//显示棋盘while (!win_signal) {
    cout << "请" << (player_contral % 2 == 1 ? "P1" : "P2") << "输入落子位置:";draw_signal = false;while (!draw_signal) {
    //落子不成功就循环cin >> x >> y;//读入两个数据cin.ignore();//清除当前行多余的数据if (x < 'a' || x>'o' || y < 'a' || y>'o') {
    //如果超出行列范围system("cls");//清屏board.show_board();//不改变信息,重新显示棋盘cout << "落子在棋局之外,请" << (player_contral % 2 == 1 ? "P1" : "P2")<<"重新输入:";}else if (board.board[(int)x - 97 + 1][(int)y - 97 + 1] != 0) {
    //如果输入的位置已经有棋子system("cls");//清屏board.show_board();//重示棋盘cout << "此处已落子,请" << (player_contral % 2 == 1 ? "P1" : "P2") << "重新输入:";}elsedraw_signal = true;//否则落子成功,会跳出循环↑}if (player_contral++ % 2 == 1)//判断是否是玩家1落子p = p1.drop_gobang((int)x - 97 + 1, (int)y - 97 + 1);//转成int类型,因为第一行第一列是边界,需要加上1elsep = p2.drop_gobang((int)x - 97 + 1, (int)y - 97 + 1);system("cls");board.show_board(*p);//落子,并重新展示信息win_signal = board.referee(*p);//是否获胜if (win_signal) {
    //如果获胜cout << ((player_contral - 1) % 2 == 1 ? "P1" : "P2") << "获胜!!!\n是否再来一局,按任意键继续...";cin.get();//吸收enter字符,好开始下一轮cin.get();//卡屏}}}}

备注:上面有详尽注释,要仔细看注释。

  1. 其中的cin.ignore()函数是为了在应对输入了3、4个char字符之后,只读入两个的情况。
  2. cin.get()函数可能在不同的系统上面,需要的数目不一样,根据测试结果进行添加。
  3. 其中的board.show_board(*p)的返回值没有用到,本应该判断输入情况的。
  4. 在之后的版本中应该将玩家输入的判断放在函数中,把模式的选择放在函数中,可能Chessboard类会变得很大
  5. 还有AI部分需要扩展,可能需要一个临时的二维数组,需要新建一个类,辅助其中一个玩家下棋。

4.AI部分[有待实现]

cin.get();//卡屏,有待实现

5.Gif展示

5.1 棋盘展示

在这里插入图片描述

5.2 下棋展示

在这里插入图片描述

6.源码

五子棋源码-C++[缺少AI]
备注:VS2019,可以直接导图工程文件,或者自己一个个的添加。