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

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;
			else
				board[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 << "●";
			else
				cout << "○";
		}
		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;//交换次数重置为0
		while (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") << "重新输入:";
                }
                else
                    draw_signal = true;//否则落子成功,会跳出循环↑
            }
            if (player_contral++ % 2 == 1)//判断是否是玩家1落子
                p = p1.drop_gobang((int)x - 97 + 1, (int)y - 97 + 1);//转成int类型,因为第一行第一列是边界,需要加上1
            else
                p = 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,可以直接导图工程文件,或者自己一个个的添加。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
// MainFrm.cpp : implementation of the CMainFrame class // #include "stdafx.h" #include "五子棋.h" #include "MainFrm.h" #ifdef _DEBUG #define new DEBUG_NEW #undef THIS_FILE static char THIS_FILE[] = __FILE__; #endif HCURSOR m_hcursor; ///////////////////////////////////////////////////////////////////////////// // CMainFrame IMPLEMENT_DYNCREATE(CMainFrame, CFrameWnd) BEGIN_MESSAGE_MAP(CMainFrame, CFrameWnd) //{{AFX_MSG_MAP(CMainFrame) ON_WM_CREATE() ON_WM_SETCURSOR() //}}AFX_MSG_MAP END_MESSAGE_MAP() static UINT indicators[] = { ID_SEPARATOR, // status line indicator ID_INDICATOR_CAPS, ID_INDICATOR_NUM, ID_INDICATOR_SCRL, }; ///////////////////////////////////////////////////////////////////////////// // CMainFrame construction/destruction CMainFrame::CMainFrame() { // TODO: add member initialization code here } CMainFrame::~CMainFrame() { } int CMainFrame::OnCreate(LPCREATESTRUCT lpCreateStruct) { if (CFrameWnd::OnCreate(lpCreateStruct) == -1) return -1; if (!m_wndToolBar.CreateEx(this, TBSTYLE_FLAT, WS_CHILD | WS_VISIBLE | CBRS_TOP | CBRS_GRIPPER | CBRS_TOOLTIPS | CBRS_FLYBY | CBRS_SIZE_DYNAMIC) || !m_wndToolBar.LoadToolBar(IDR_MAINFRAME)) { TRACE0("Failed to create toolbar\n"); return -1; // fail to create } if (!m_wndStatusBar.Create(this) || !m_wndStatusBar.SetIndicators(indicators, sizeof(indicators)/sizeof(UINT))) { TRACE0("Failed to create status bar\n"); return -1; // fail to create } // TODO: Delete these three lines if you don't want the toolbar to // be dockable m_wndToolBar.EnableDocking(CBRS_ALIGN_ANY); EnableDocking(CBRS_ALIGN_ANY); DockControlBar(&m_wndToolBar); return 0; } BOOL CMainFrame::PreCreateWindow(CREATESTRUCT& cs) { if( !CFrameWnd::PreCreateWindow(cs) ) return FALSE; // TODO: Modify the Window class or styles here by modifying // the CREATESTRUCT cs return TRUE; } ///////////////////////////////////////////////////////////////////////////// // CMainFrame diagnostics #ifdef _DEBUG void CMainFrame::AssertValid() const { CFrameWnd::AssertValid(); } void CMainFrame::Dump(CDumpContext& dc) const { CFrameWnd::Dump(dc); } #endif //_DEBUG ///////////////////////////////////////////////////////////////////////////// // CMainFrame message handlers BOOL CMainFrame::OnSetCursor(CWnd* pWnd, UINT nHitTest, UINT message) { // TODO: Add your message handler code here and/or call default BOOL br = CFrameWnd::OnSetCursor( pWnd, nHitTest,message) ; if(star==1) { if(flag==1) { m_hcursor= AfxGetApp()->LoadCursor(IDC_hei); SetCursor(m_hcursor); br = TRUE; } else if(flag==2) { m_hcursor= AfxGetApp()->LoadCursor(IDC_bai); SetCursor(m_hcursor); br = TRUE; } } return br; // return CFrameWnd::OnSetCursor(pWnd, nHitTest, message); }
好的,下面是一个简单的控制台图书管理系统的C++实现: ```cpp #include <iostream> #include <cstring> using namespace std; const int MAX_BOOKS = 100; class Book { private: string title; string author; int year; public: Book(string title = "", string author = "", int year = 0) { this->title = title; this->author = author; this->year = year; } string getTitle() { return title; } string getAuthor() { return author; } int getYear() { return year; } }; class Library { private: Book books[MAX_BOOKS]; int numBooks; public: Library() { numBooks = 0; } void addBook(string title, string author, int year) { if (numBooks < MAX_BOOKS) { books[numBooks++] = Book(title, author, year); cout << "Book added successfully!" << endl; } else { cout << "Library is full!" << endl; } } void listBooks() { if (numBooks == 0) { cout << "Library is empty!" << endl; } else { cout << "Library contents:" << endl; for (int i = 0; i < numBooks; i++) { cout << books[i].getTitle() << " by " << books[i].getAuthor() << " (" << books[i].getYear() << ")" << endl; } } } }; int main() { Library library; int choice; string title, author; int year; do { cout << "Select an option:" << endl; cout << "1. Add book" << endl; cout << "2. List books" << endl; cout << "3. Quit" << endl; cin >> choice; switch (choice) { case 1: cout << "Enter title, author, and year:" << endl; cin >> title >> author >> year; library.addBook(title, author, year); break; case 2: library.listBooks(); break; case 3: break; default: cout << "Invalid option!" << endl; break; } } while (choice != 3); return 0; } ``` 这个系统包含两个类:`Book`和`Library`。`Book`类表示一本书,包含标题、作者和年份等信息;`Library`类表示整个图书馆,包含一个`Book`类型的数组和一些基本的操作,如添加书籍和列出所有书籍。 在主函数中,我们使用一个循环来不断读取用户输入的选项,并执行相应的操作。用户可以选择添加一本书籍、列出所有书籍或退出系统。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值