MFC双人五子棋

一、 简介

本人最近用MFC制作了一个双人五子棋,实现了以下功能:
1.完全使用鼠标操作;
2.自动判断胜负;
3.悔棋;
4.保存和打开棋局。
下面,我与大家分享一下我的制作流程。
开发环境: Visual Studio 2019

二、程序编写

1. 创建资源文件

首先,创建双人五子棋解决方案,我的名称是GoBang。
创建对话框并调整对话框尺寸,我的尺寸是400*400。将对话框的标题改为“双人五子棋”,“边框”属性改为“对话框外框”,防止运行期间改变大小。

在对话框上放置6个Button控件,标题分别改为“开始游戏”(ID:IDC_START)“结束本局”(ID:IDC_ENDGAME)“退出”(ID:IDC_QUIT)“悔棋”(ID:IDC_REPENTANCE)“保存棋局”(ID:IDC_SAVE)“打开棋局”(ID:IDC_OPEN),将“结束本局”、“悔棋”和“保存”按钮设为最初禁用状态。
对话框到此设计完毕,棋盘我们会在后面使用代码绘制。最终的对话框效果如图所示:
MFC双人五子棋_第1张图片
然后,添加两个cursor光标资源,图案分别是一个黑棋和一个白棋。黑棋的ID是IDC_CURSOR1,白棋的ID是IDC_CURSOR2。

2. 判断胜负算法设计

双人五子棋由于是用户之间的对战,无需设计AI算法,所以算法很简单。它用到的主要算法只有一个:判断胜负。下面,我们就来设计判断胜负的算法。当然,下面是我自己的算法,肯定还有更好的算法,欢迎大家在评论区中为我指出缺点。
为了方便修改棋盘尺寸,我们定义了一个宏SIZE,它代表棋盘的行数和列数。

#define SIZE 15

下面我们开始创建变量。接下来的变量都放在GoBangDlg.h中作为对话框类的成员。首先,我们创建一个SIZE*SIZE的int二维数组(名为ChessBoard),这个数组就代表棋盘,每个元素的不同值代表棋盘交叉点的不同状态:-1为空,0为白,1为黑。再创建一个bool变量NowColor用来记录下一步棋的颜色,false(0)为白,true(1)为黑。接着创建一个bool变量IsPlaying记录是否正在游戏。代码如下:

bool IsPlaying;
bool NowColor;
int ChessBoard[SIZE][SIZE];//棋盘,-1为空,0为白,1为黑

由于计算机中数组下标是先行后列(先y轴后x轴),与我们平常的习惯(先列后行)不符,所以我们先创建一个函数转换这个差异。当然,这个函数也可以不写,但后面绘制棋子和判断鼠标位置的程序需要修改一下。

int CGoBangDlg::GetChessBoardColor(int nx, int ny)
{
     
	return ChessBoard[ny][nx];
}

接下来,我们开始创建算法的核心部分。我创建了一个函数,名为GetChessCount,这个名字很容易引起误解,这个函数的功能并不是获取棋盘上棋子的个数,而是获取指定坐标上的棋子在任意方向上相连的最大个数。举个例子,假如下面的棋盘中间的那一列最上方的棋子坐标为(5,5),那么GetChessCount(5,5)的值就为4(那个棋子横着连成了4个)。
MFC双人五子棋_第2张图片
该函数的实现代码如下:

int CGoBangDlg::GetChessCount(int nx, int ny)//获取指定棋子各个方向的同色棋子个数最大值
{
     
	int color = GetChessBoardColor(nx, ny);//获取指定点的棋子颜色
	if (color == -1)//空位
		return -1;

	int x = nx, y = ny;
	int m_max, count;
	while (--y >= 0 && GetChessBoardColor(x, y) == color);//获取这个棋子所在的y轴棋链中最下端的棋子坐标,注意行尾有分号,循环体什么也不做,y++并不是循环体
	y++;//由于上面的循环是先把y减1再判断,所以需要加1得到真正的坐标
	for (count = 1; (++y < SIZE) && (GetChessBoardColor(x, y) == color); count++);//获取y轴棋链的棋子个数,注意行尾有分号
	m_max = count;
	//y轴
	x = nx, y = ny;
	while (--x >= 0 && GetChessBoardColor(x, y) == color);
	x++;
	for (count = 1; ++x < SIZE && GetChessBoardColor(x, y) == color; count++);
	if (m_max < count)
		m_max = count;
	//x轴,代码意义同上
	x = nx, y = ny;
	while (x - 1 >= 0 && y - 1 >= 0 && GetChessBoardColor(x - 1, y - 1) == color)//这里由于是x-1而不是--x,所以行尾没有分号,x--,y--才是循环体,后面无需把x和y的值增加1,下同
		x--, y--;
	for (count = 1; x + 1 < SIZE && y + 1 < SIZE && GetChessBoardColor(x + 1, y + 1) == color; count++)
		x++, y++;
	if (m_max < count)
		m_max = count;
	//左下到右上,代码意义同上
	x = nx, y = ny;
	while (x - 1 >= 0 && y + 1 < SIZE && GetChessBoardColor(x - 1, y + 1) == color)
		x--, y++;
	for (count = 1; x + 1 < SIZE && y - 1 >= 0 && GetChessBoardColor(x + 1, y - 1) == color; count++)
		x++, y--;
	if (m_max < count)
		m_max = count;
	//左上到右下,代码意义同上
	return m_max;
}

接下来就简单了,只要用一个函数遍历棋盘,判断是否有五子连线就行了。不废话了,直接上代码:

int CGoBangDlg::GetWinner()//获取赢家,-1无,0白,1黑
{
     
	for (int i = 0; i < SIZE; i++)
	{
     
		for (int j = 0; j < SIZE; j++)
		{
     
			int color = GetChessBoardColor(i, j);
			if (color != -1)
			{
     
				if (GetChessCount(i, j) >= 5)
					return color;
			}
		}
	}
	return -1;
}

至此,双人五子棋判断胜负算法部分设计完毕,之后我们只需要每走一步棋判断一次胜负就可以了。
除了基本的判断胜负,我还增加了一个悔棋功能。我们创建一个整形变量index,用于记录上一步是第几步棋,再创建一个类型为CPoint或POINT的数组,用来记录每一步棋的坐标。

int index;
CPoint order[SIZE*SIZE];

悔棋的具体算法在后面会讲解。

3.创建事件处理程序及绘图函数

让我们先从简单的入手吧。我们先处理窗口的WM_CLOSE消息,当窗口收到WM_CLOSE消息时,判断游戏是否在进行,如果在进行则弹出对话框询问是否要退出否则直接退出。代码如下:

void CGoBangDlg::OnClose()
{
     
	if (!IsPlaying || MessageBoxW(L"正在游戏中,确定要退出吗?", L"双人五子棋", MB_YESNO | MB_ICONQUESTION) == IDYES)
		CDialogEx::OnClose();
}

这里用到了一个C++逻辑或运算符基本规则,那就是如果第一个表达式成立,就不会计算第二个表达式。所以如果!IsPlaying(游戏不在进行),就不会 弹出对话框,而是直接关闭。
“退出”按钮的BN_CLICKED消息和WM_CLOSE消息的处理程序基本相同,这里就不再赘述了。代码如下:

void CGoBangDlg::OnBnClickedQuit()
{
     
	if (!IsPlaying || MessageBoxW(L"正在游戏中,确定要退出吗?", L"双人五子棋", MB_YESNO | MB_ICONQUESTION) == IDYES)
		EndDialog(0);
}

接着,我们修改一下GoBangDlg的DoDataExchange函数,进行数据初始化。代码很简单,就不再解释。代码如下:

void CGoBangDlg::DoDataExchange(CDataExchange* pDX)
{
     
	IsPlaying = false;
	for (int i = 0; i < SIZE; i++)
	{
     
		for (int j = 0; j < SIZE; j++)
		{
     
			ChessBoard[i][j] = -1;
		}
	}//初始化棋盘
	CDialogEx::DoDataExchange(pDX);
}

然后,我们要修改WM_PAINT消息绘制棋盘和棋子。代码如下:

void CGoBangDlg::OnPaint()
{
     
	if (IsIconic())
	{
     
		CPaintDC dc(this); // 用于绘制的设备上下文

		SendMessage(WM_ICONERASEBKGND, reinterpret_cast<WPARAM>(dc.GetSafeHdc()), 0);

		// 使图标在工作区矩形中居中
		int cxIcon = GetSystemMetrics(SM_CXICON);
		int cyIcon = GetSystemMetrics(SM_CYICON);
		CRect rect;
		GetClientRect(&rect);
		int x = (rect.Width() - cxIcon + 1) / 2;
		int y = (rect.Height() - cyIcon + 1) / 2;

		// 绘制图标
		dc.DrawIcon(x, y, m_hIcon);
	}
	else
	{
     
		CPaintDC dc(this);
		CPen pen(PS_SOLID, 2, RGB(0, 0, 0));
		dc.SelectObject(pen);
		for (int i = 0; i < SIZE; i++)
		{
     
			dc.MoveTo(50, 50 + i * 50);
			dc.LineTo(750, 50 + i * 50);
		}//绘制棋盘横线
		for (int i = 0; i < SIZE; i++)
		{
     
			dc.MoveTo(50 + i * 50, 50);
			dc.LineTo(50 + i * 50, 750);
		}//绘制棋盘竖线
		for (int nx = 0; nx < SIZE; nx++)//遍历棋盘,绘制每一个棋子
		{
     
			for (int ny = 0; ny < SIZE; ny++)
			{
     

				int color = GetChessBoardColor(nx, ny);
				if (color == 0)//白棋
				{
     
					CBrush brush_w(RGB(255, 255, 255));
					const CPoint o(50 * nx + 50, 50 * ny + 50);//圆心
					dc.SelectObject(brush_w);
					dc.Ellipse(o.x - 15, o.y - 15, o.x + 15, o.y + 15);
				}
				else if (color == 1)//黑棋
				{
     
					CBrush brush_b(RGB(0, 0, 0));
					const CPoint o(50 * nx + 50, 50 * ny + 50);//圆心
					dc.SelectObject(brush_b);
					dc.Ellipse(o.x - 15, o.y - 15, o.x + 15, o.y + 15);
				}
			}
		}
	}
}

代码应该不难理解,用的是MFC的绘图工具,和GDI绘图类似,非常方便,不需要担心内存泄漏等问题,构造函数和析构函数会自动处理绘图对象的创建和删除。但有些MFC或Windows API开发经验的人可能会注意到一个问题:我用的是CPaintDC,相当于Windows API中的BeginPaint函数,这样只会绘制一次,绘制结束后不会再收到WM_PAINT消息,下棋后无法正常显示棋子。这个问题有道理。因为五子棋不需要一直重绘已经存在的棋子,如果一直重绘已经存在的棋子会大幅度降低程序性能,所以我在SetChessBoardColor函数中会绘制新下的棋子,已经绘制的棋子和棋盘不会改变,这个函数下面会讲到。这里之所以还添加绘制棋子的程序,是因为两种情况:
(1)游戏过程中,窗口被移出屏幕边缘或被最小化,恢复正常时窗口会收到WM_PAINT消息,如果不绘制棋子,则无法正常显示棋盘上已经存在的棋子。
(2)悔棋时由于无法直接擦除已经下了的棋子,需要调用Invalidate函数重新绘制,如果不绘制棋子,则无法正常显示棋盘上已经存在的棋子。

既然提到了SetChessBoardColor函数,那我们先来看看它的代码。这个函数不仅能修改ChessBoard,还能在屏幕上绘制出棋子,这样就不用重绘整个棋盘了。

void CGoBangDlg::SetChessBoardColor(int nx, int ny, int color)
{
     
	ChessBoard[ny][nx] = color;
	CDC* dc = this->GetDC();
	CPen pen(PS_SOLID, 2, RGB(0, 0, 0));
	dc->SelectObject(pen);
	if (color == 0)//白棋
	{
     
		CBrush brush_w(RGB(255, 255, 255));
		const CPoint o(50 * nx + 50, 50 * ny + 50);//圆心
		dc->SelectObject(brush_w);
		dc->Ellipse(o.x - 15, o.y - 15, o.x + 15, o.y + 15);
	}
	else if (color == 1)//黑棋
	{
     
		CBrush brush_b(RGB(0, 0, 0));
		const CPoint o(50 * nx + 50, 50 * ny + 50);//圆心
		dc->SelectObject(brush_b);
		dc->Ellipse(o.x - 15, o.y - 15, o.x + 15, o.y + 15);
	}
	else//清除该坐标棋子,需要重绘,用于悔棋
	{
     
		RECT rect;
		GetClientRect(&rect);
		InvalidateRect(&rect);
	}
}

然后是CleanChessBoard函数,该函数清空ChessBoard数组和屏幕上的棋子。代码如下:

void CGoBangDlg::CleanChessBoard()
{
     
	for (int i = 0; i < SIZE; i++)
	{
     
		for (int j = 0; j < SIZE; j++)
		{
     
			ChessBoard[i][j] = -1;
		}
	}
	Invalidate();//刷新
}

然后我们来创建EndGame函数,它的功能是结束游戏。代码如下:

void CGoBangDlg::EndGame()
{
     
	CleanChessBoard();
	IsPlaying = false;
	index = -1;
	GetDlgItem(IDC_START)->SetWindowTextW(L"开始游戏");
	GetDlgItem(IDC_ENDGAME)->EnableWindow(FALSE);
	GetDlgItem(IDC_REPENTANCE)->EnableWindow(FALSE);
	GetDlgItem(IDC_SAVE)->EnableWindow(FALSE);
}

接下来,我们创建“开始游戏”按钮的BN_CLICKED消息。由于开始游戏与重玩的代码完全相同,所以我们就不再创建重玩按钮,而是通过修改“开始游戏”按钮的窗口标题实现。代码如下:

void CGoBangDlg::OnBnClickedStart()
{
     
	if (IsPlaying && MessageBoxW(L"确定要重玩吗?", L"双人五子棋", MB_YESNO | MB_ICONQUESTION) == IDNO)
		return;
	GetDlgItem(IDC_START)->SetWindowTextW(L"重玩");
	IsPlaying = true;
	NowColor = 1;//黑先
	index = -1;
	GetDlgItem(IDC_ENDGAME)->EnableWindow(TRUE);
	GetDlgItem(IDC_REPENTANCE)->EnableWindow(FALSE);
	GetDlgItem(IDC_SAVE)->EnableWindow(TRUE);
	CleanChessBoard();
}

这里用到了一个C++逻辑与运算符基本规则,那就是如果第一个表达式不成立,就不会计算第二个表达式。所以如果IsPlaying(游戏在进行)不成立,就不会 弹出对话框,而是直接进行下面的代码。
接着,我们创建“结束本局”的BN_CLICKED消息处理程序。因为我们以前已经写了EndGame函数,这里直接调用就行了。代码如下:

void CGoBangDlg::OnBnClickedEndgame()
{
     
	if (MessageBoxW(L"确定要结束本局吗?", L"双人五子棋", MB_YESNO | MB_ICONQUESTION) == IDYES)
		EndGame();
}

然后是对话框的WM_SETCURSOR消息处理程序。这个函数设置鼠标光标的状态,决定鼠标是黑子、白子还是普通。代码如下:

BOOL CGoBangDlg::OnSetCursor(CWnd* pWnd, UINT nHitTest, UINT message)
{
     
	POINT point;
	GetCursorPos(&point);
	ScreenToClient(&point);
	if (!IsPlaying || point.x < 40 || point.x>760 || point.y < 40 || point.y>760)//判断鼠标当前位置是否在棋盘里
		return CDialogEx::OnSetCursor(pWnd, nHitTest, message);
	if (NowColor == 1)//黑棋
		SetCursor(LoadCursorW(AfxGetApp()->m_hInstance, MAKEINTRESOURCE(IDC_CURSOR1)));
	else
		SetCursor(LoadCursorW(AfxGetApp()->m_hInstance, MAKEINTRESOURCE(IDC_CURSOR2)));
	return TRUE;
}

现在,我们要创建一个极其重要的消息处理函数——响应鼠标左键松开的消息处理函数,其中point是鼠标的坐标(以客户区为参照系)。这个函数的功能是在鼠标单击处放置棋子并判断胜负。代码如下:

void CGoBangDlg::OnLButtonUp(UINT nFlags, CPoint point)
{
     
	if (!IsPlaying || point.x < 40 || point.x>760 || point.y < 40 || point.y>760)//判断鼠标是否在棋盘里
		return;
	int x = int(round(point.x / 50.0) - 1);//round是四舍五入函数
	int y = int(round(point.y / 50.0) - 1);
	//将鼠标坐标转为数组下标
	if (GetChessBoardColor(x, y) != -1)//如果已有棋子
		return;
	SetChessBoardColor(x, y, NowColor);
	NowColor = (!NowColor);
	index++;
	order[index].x = x;
	order[index].y = y;
	//记录上一步的坐标,用于悔棋
	GetDlgItem(IDC_REPENTANCE)->EnableWindow(index > -1);
	//如果可以悔棋,取消禁用“悔棋”按钮,否则禁用“悔棋”按钮
	SendMessage(WM_SETCURSOR);//刷新鼠标
	//以上为放置棋子
	int winner = GetWinner();
	int count = 0;
	for (int i = 0; i < SIZE; i++)
	{
     
		for (int j = 0; j < SIZE; j++)
		{
     
			if (ChessBoard[i][j] != -1)
				count++;
		}
	}
	if (winner != -1||count == SIZE * SIZE)
	{
     
		if (winner == 0)
			MessageBoxW(L"白棋胜利!", L"双人五子棋", MB_OK | MB_ICONINFORMATION);
		else if (winner == 1)
			MessageBoxW(L"黑棋胜利!", L"双人五子棋", MB_OK | MB_ICONINFORMATION);
		else
			MessageBoxW(L"平局!", L"双人五子棋", MB_OK | MB_ICONINFORMATION);
		EndGame();
		return;
	}
	//判断胜负
}

最后的判断胜负要注意,五子棋除了一方打败另一方,还有一种情况:平局。平局也就是棋盘所有交叉点都下满了,但还没有连成五个子。这时候,我们必须结束游戏,否则棋局无法进行。

最后,我们创建悔棋按钮被按下的消息处理函数。代码如下:

void CGoBangDlg::OnBnClickedRepentance()
{
     
	SetChessBoardColor(order[index].x, order[index].y, -1);//清除上一步棋
	index--;
	GetDlgItem(IDC_REPENTANCE)->EnableWindow(index > -1);
	NowColor = (!NowColor);//改变棋子颜色
}

至此,双人五子棋的消息处理部分已经完成,保存和打开的函数我会单独介绍。

4.学习创建一种文件格式——打开和保存算法

任何一个可以存储信息的软件都有一种文件格式,如word的格式是doc或docx,powerpoint的格式是ppt或pptx等。如果你用记事本打开docx/pptx文档,会发现很简短的一篇文档用记事本打开都会变得很长,这是因为word在保存文字的同时还保存了很多其它属性,如字体、字号、行间距等等。当保存文件时,word会自动将用户输入的文件转化为docx格式,打开文件时又会按自己的文件格式读取文件,并转化为各种属性,这样就可以还原保存时的样子了。五子棋程序也不例外,为了保存完整的棋局状态,我们需要把内存中所有与棋局有关的变量都复制到磁盘中,这就需要我们专门设计一种文件格式。我设计的五子棋文件格式如下:
1.扩展名:gob。
2.文件由多个数字组成,保存了程序运行时的所有变量。
3.第1到第SIZE*SIZE(225)个数字:记录棋盘上每个交叉点的状态。(ChessBoard)
4.第SIZE*SIZE+1(226)个数字:记录下一步是哪一方走。(NowColor)
5.第SIZE*SIZE+2(227)个数字:记录已经走了多少步棋,供悔棋功能用。(index)
6.第SIZE*SIZE+3(228)到第(SIZE*SIZE+3+第226个数字*2)个数字:
每两个数为一组,记录每一步棋的x坐标和y坐标。(order)

例如:
-1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1
-1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1
-1 -1 -1 -1 -1 -1 1 -1 -1 -1 -1 -1 -1 -1 -1
-1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 0 -1 -1 -1
-1 -1 -1 0 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1
-1 -1 -1 1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1
-1 -1 -1 -1 -1 0 -1 -1 -1 -1 -1 -1 -1 -1 -1
-1 -1 -1 -1 -1 1 0 1 1 -1 -1 -1 -1 -1 -1
-1 -1 -1 -1 -1 -1 0 -1 -1 -1 -1 -1 -1 -1 -1
-1 -1 -1 1 -1 -1 -1 -1 -1 -1 1 -1 -1 -1 -1
-1 -1 -1 0 -1 -1 -1 -1 0 -1 -1 -1 -1 -1 -1
-1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1
-1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1
-1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1
-1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1
1
13
7 7
6 8
5 7
5 6
3 5
3 4
6 2
11 3
10 9
3 10
3 9
8 10
8 7
6 7
这就是一个完整的gob文件的内容。
下面,我们开始创建保存算法,这个算法用到了ofstream类。代码如下:

void CGoBangDlg::OnBnClickedSave()
{
     
	CFileDialog filedlg(FALSE);
	filedlg.m_ofn.lpstrFilter = L"五子棋文件(*.gob)\0*.gob\0\0";
	if (filedlg.DoModal() != IDOK)
		return;
	CString filename = filedlg.GetPathName();
	if (filedlg.GetFileExt() == L"")//如果用户没有输入扩展名
		filename += ".gob";
	std::ofstream outfile;
	outfile.open(CStringA(filename));
	if(!outfile)
	{
     
		MessageBoxW(L"保存失败!", L"双人五子棋", MB_OK | MB_ICONERROR);
		return;
	}
	for (int y = 0; y < 15; y++)
	{
     
		for (int x = 0; x < 15; x++)
		{
     
			outfile << GetChessBoardColor(x, y) << '\0';
		}
		outfile << '\r';
	}
	//输出ChessBoard数组
	outfile <<NowColor<<'\r'<< index << '\r';
	for (int i = 0; i <= index; i++)
		outfile << order[i].x << '\0' << order[i].y << '\r';
	outfile.close();
	//输出order数组
	MessageBoxW(L"保存成功!", L"双人五子棋", MB_OK | MB_ICONINFORMATION);
}

保存文件的算法比较简单,但读取文件就要麻烦一些了。我们来看看它的代码:

void CGoBangDlg::OnBnClickedOpen()
{
     
	CFileDialog filedlg(TRUE);
	filedlg.m_ofn.lpstrFilter = L"五子棋文件(*.gob)\0*.gob\0\0";
	if (filedlg.DoModal() != IDOK)
		return;

	CString filename = filedlg.GetPathName();
	if (filedlg.GetFileExt() == L"")
		filename += ".gob";
	std::ifstream infile;
	infile.open(CStringA(filename));
	if (!infile)
	{
     
		MessageBoxW(L"打开失败!", L"双人五子棋", MB_OK | MB_ICONERROR);
		return;
	}
	for (int y = 0; y < 15; y++)
	{
     
		for (int x = 0; x < 15; x++)
		{
     
			int t;
			infile >> t;
			infile.seekg(infile.tellg().operator+(1));
			ChessBoard[y][x] = t;
			//因为保存文件已经用了GetChessBoardColor函数,文件中的数字是正常顺序,不能使用SetChessBoardColor函数。而且,SetChessBoardColor遇到-1就会刷新棋盘,性能不好。
		}
	}
	Invalidate();//绘制棋盘和棋子
	infile >> NowColor;
	infile.seekg(infile.tellg().operator+(1));
	infile >> index;
	for (int i = 0; i <= index; i++)
	{
     
		infile.seekg(infile.tellg().operator+(1));
		infile >> order[i].x;
		infile.seekg(infile.tellg().operator+(1));
		infile>> order[i].y;
	}
	infile.close();
	GetDlgItem(IDC_START)->SetWindowTextW(L"重玩");
	GetDlgItem(IDC_ENDGAME)->EnableWindow(TRUE);
	GetDlgItem(IDC_REPENTANCE)->EnableWindow(index > 0);
	GetDlgItem(IDC_SAVE)->EnableWindow(TRUE);
	IsPlaying = true;
}

这里有一点要注意,那就是每读完一个数据就会有一行

infile.seekg(infile.tellg().operator+(1));

把指针向后移动一个字节。关于这个问题,我也很纳闷,正常的话ifstream类的函数会自动跳过空格,就像我们的cin一样,可如果没有这一行代码怎么也不行。也有可能是我的编译器问题,如果读者们在自己的电脑上运行有bug,那就把这一行代码删掉试试。

到现在,双人五子棋已经完成,我们可以邀请家人和朋友一起来玩了!这个程序我调试过很多次,目前没有发现bug,如果有人发现了漏洞或有更好的意见,欢迎大家在评论区提出。

三、程序截图

初始窗口状态:
MFC双人五子棋_第3张图片
游戏中:
MFC双人五子棋_第4张图片
悔棋功能:
MFC双人五子棋_第5张图片
某方获胜:
MFC双人五子棋_第6张图片
保存棋局:
MFC双人五子棋_第7张图片
打开棋局:
MFC双人五子棋_第8张图片
退出:
MFC双人五子棋_第9张图片

你可能感兴趣的:(c++,mfc)