我罗斯方块最终篇

这个作业属于哪个课程 2020年面向对象程序设计
这个作业要求在哪里 我罗斯方块最终篇
这个作业的目标 1.代码的git仓库链接 2.运行截图/运行视频 3.代码要点 4.收获与心得 5.依然存在的问题
作业正文 见下文 以及 本组组员的额外补充
其他参考文献 C语言俄罗斯方块
小组成员 031902522--杨潮湧 031902503--陈松庆
项目地址 github地址

代码的git仓库链接↑(请点击上方,欢迎试玩提意见哦)

运行截图/运行视频

视频链接在这里鸭

在这里我补充几点

视频中的画面只有程序运行的界面是由于录屏软件的原因,视频中有几次停下来是因为按了空格键暂停了,然后再次按下空格键,游戏继续。在暂停时,可以看到鼠标的光标,原因是本组做了一个简单的操作指南,写在记事本上,原来打算在暂停时用鼠标选取指南里的内容,提示本组接下来的操作,但由于只有录到程序界面,所以记事本的内容就看不到了。还有一点就是视频里展示的程序在本组在录视频前有做过一些背景上色方面的改动,还存在一些bug,算是最新版的,但不是最完善的一版。目前最完善的一版我们已经放在git仓库里了。最新版的录屏里出现了“L键的可用次数:”和“L键的可用次数:”后数字无法显示以及最后程序结束时没有弹窗出现等的问题,在git仓库里的那版里是没有的。

我们将会继续完善最新版,如果要试玩评估的话请以git仓库里的那版为准哦。

下方是运行截图

前几张是最新版的,但背景上色和某些字体显示有问题



后几张是最完善版本的,但还未给背景整体上色



代码要点&心得体会

对于采用window编程,本组有了进一步的理解。windows编程与我们平常进行的编程存在一些差别。如:我们平时调用的是window的cmd窗口(也就是平常说的程序黑框),而windows编程需要直接直接创建一个新的窗口,并直接在这个窗口上进行绘图、显示等界面操作,而这一部分其实便是渲染类需要做的工作。

首先来谈一谈程序主函数部分。

windows编程的主函数应该是

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE PreInstance, LPSTR lpCmdLine, int nCmdShow)
{
  return 0;
}

可以看到,主函数的形式就与平常一直在使用的int main()有着极大的差异。顺便说一句,在windows编程的学习中,给我留下的印象最深的就是无处不在的句柄。可以说,在windows编程中,句柄占据了极其重要的地位。什么是句柄?简单来说就是操作系统给予程序员的窗口接口。句柄的种类可以说是多种多样的,有HINSTANCE、HWND、HDC等等等等。它们的作用也各有不同。但它们也有相同点——利用句柄可以对不同的窗口或同一窗口的不同位置进行所需的操作。反过来说,绝大多数的对窗口的操作,都需要依托句柄才能完成。

接着是主函数的函数体部分。既然是对窗口进行操作的编程,当然首先需要进行窗口的显示。要完成一个窗口的显示,有一个最基本的流程。

第一,设计窗口类

g_hinstance = hinstance;
TCHAR szAppName[] = TEXT("Tetris");
WNDCLASS wc;
wc.style = CS_HREDRAW | CS_VREDRAW;
wc.lpfnWndProc = WndProc;
wc.cbClsExtra = 0;
wc.cbWndExtra = 0;
wc.hInstance = hinstance;
wc.hIcon = LoadIcon(NULL, IDI_APPLICATION);
wc.hCursor = LoadCursor(NULL, IDC_ARROW);
wc.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH);//wc.hbrBackground = CreateSolidBrush(RGB(255, 0, 0));
wc.lpszMenuName = NULL;
wc.lpszClassName = szAppName;

第二,注册窗口类

if (!RegisterClass(&wc))
{
	cout << "RegisterClass occurs errors!";
	return 0;
}

第三,创建窗口

hwnd = CreateWindow(szAppName,TEXT("els"),WS_OVERLAPPEDWINDOW,100, 50,1000, 650,NULL, NULL,hinstance,NULL);

第四,显示窗口

ShowWindow(hwnd, SW_SHOWNORMAL);

第五,更新窗口

UpdateWindow(hwnd);

到了这里,已经可以显示窗口了,但是,此时存在的问题是,窗口在程序运行时总是一闪而过,无法持续地进行显示。因此还需要最后一步。

第六,消息循环

在这里我碰到过一种情况,当时觉得挺有意思的。如果单纯写一个死循环让程序不断地跑,窗口就能得以持续地显现,但现在有一个问题是,我现在就没法直接点击通过窗口右上角的叉号来关闭窗口,并且过不了几秒,这个程序会直接崩溃。因此,这个时候,就需要写一个消息循环。

MSG msg;
while (GetMessage(&msg, NULL, 0, 0))//使窗口持续地显现
{
	//将虚拟键消息转化为字符串消息
	TranslateMessage(&msg);
	//将消息分发给消息处理函数
	DispatchMessage(&msg);
	//如此,就可以使用鼠标拖动窗口
}
return 0;//退出程序

完成主函数的编写后,我们还需要再写一个函数。这个函数与程序在运行过程中所产生的各种信息密切相关。

编写回调函数

LRESULT CALLBACK WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
	PAINTSTRUCT ps;
	HDC hDC;
	switch (uMsg)
	{
	/*
	case WM_CREATE://窗口创建
		break;
	case WM_CLOSE://窗口关闭消息
		DestroyWindow(hWnd);//销毁窗口
		break;
	}
	return DefWindowProc(hWnd, uMsg, wParam, lParam);
}

可以说回调函数其实就是用于处理程序运行过程中的各种信息的。值得注意的一点是,如果单单向窗口发送关闭信息时,窗口是可以立即关闭的。但是,这个时候会出现一个很奇怪的现象,如果我们在任务管理器里寻找,就可以发现我们这个程序的exe文件仍然在运行着。也就是说,此时我表面上把这个窗口关闭了,只是让这个窗口不在屏幕上显示,并没有完成窗口的销毁工作。因此,我们需要添上这个这个信息处理,将这个最初由我们创建的窗口手动销毁:

LRESULT CALLBACK WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
	PAINTSTRUCT ps;
	HDC hDC;
	switch (uMsg)
	{
	/*
	case WM_CREATE://窗口创建
		break;
	case WM_CLOSE://窗口关闭消息
		DestroyWindow(hWnd);//销毁窗口
		break;
	case WM_DESTROY://窗口销毁消息
		PostQuitMessage(0);//直接发出一个窗口退出消息
		break;
	}
	return DefWindowProc(hWnd, uMsg, wParam, lParam);
}

这样,在我们发出关闭窗口的指令后,系统就会发出销毁窗口的信息,执行销毁窗口的的操作,当然我们还需要在这里调用相应函数来完成销毁工作。接着整个程序结束。

当然,除了添加响应窗口关闭消息和窗口销毁消息的函数之外,我们还需要添加响应其他消息的操作,如响应窗口绘制的消息、响应定时器的消息、响应键盘按键的消息。
如下:

LRESULT CALLBACK WndProc(HWND hWnd, UINT nmsg, WPARAM wParam, LPARAM lParam)
{
	PAINTSTRUCT ps;
	HDC hDC;
	switch (nmsg)
	{
	case WM_CREATE://该消息只产生一次
	{
		break;
	}
	case WM_TIMER:
	{
		break;
	}
	case WM_KEYDOWN://注意这个的位置,放在WM_PAINT的后面会造成按回车后闪退
	{
                break;
	}
	case WM_PAINT:
	{
		break;
	}
	case WM_DESTROY:
		PostQuitMessage(0);
		break;
	}
	return DefWindowProc(hWnd, nmsg, wParam, lParam);
}

在这里还要说明几点:
1.上述的思路是完全使用windows窗口编程,因此需要使用的主函数应该是这样:

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE PreInstance, LPSTR lpCmdLine, int nCmdShow)
{
      return 0;
}

最开始我在尝试使用该主函数时遇到了一些问题。在Visual Studio 2019上跑时,编译器给出了什么什么主函数批注不一致的报错。

此时需要进行这样的操作:

右键点击解决方案下第一行,选择属性->链接器->系统->在子系统那一栏设置为 窗口 (/SUBSYSTEM:WINDOWS) ->点击确定。

2.本组采用的并非是标准的windows编程,因此我们使用的主函数形式仍然是int main(),不过在主函数开头以及在return时返回值有所不同:

int main()
{
	HINSTANCE hinstance = GetModuleHandle(NULL);

         //其他语句

	return msg.wParam;
}

这样一来,程序整体框架就确定好了,接下来就是分别来实现各个类的功能了。

Block类

方块类有几个比较重要的数据成员,如g_square方块形状数组(二维数组),用两个下标来表示方块在背景中的位置,数组元素的值为1或0,表示该位置有无正在下落的方块。方块类中还有一个faker_bg数组,即代表假的背景,这个之后会在渲染类的设计思路中具体说明,简单讲就是方便方块的自动下落、旋转、左右移动、加速移动等操作。还有代表方块形状的shape。在每一次生成随机方块时,shape的值都会被相应地刷新。

生成随机方块

本组决定直接给出七种方块对应的初始化位置,对于方块的随机生成,本组只实现类型的随机生成,而暂未实现初始位置的随机(该功能应该可以继续添加)。

int Block::createrandomsquare()
{
	int n = rand() % 7;
	switch (n)
	{
	case 0: {
		g_square[0][0] = 1; g_square[0][1] = 1; g_square[0][2] = 0; g_square[0][3] = 0;
		g_square[1][0] = 0; g_square[1][1] = 1; g_square[1][2] = 1; g_square[1][3] = 0;
		line = 0;
		list = 3;
		break;
	}
	case 1: {
		g_square[0][0] = 0; g_square[0][1] = 1; g_square[0][2] = 0; g_square[0][3] = 0;
		g_square[1][0] = 1; g_square[1][1] = 1; g_square[1][2] = 1; g_square[1][3] = 0;
		line = 0;
		list = 3;
		break;
	}
	case 2: {
		g_square[0][0] = 0; g_square[0][1] = 1; g_square[0][2] = 1; g_square[0][3] = 0;
		g_square[1][0] = 1; g_square[1][1] = 1; g_square[1][2] = 0; g_square[1][3] = 0;
		line = 0;
		list = 3;
		break;
	}
	case 3: {
		g_square[0][0] = 1; g_square[0][1] = 0; g_square[0][2] = 0; g_square[0][3] = 0;
		g_square[1][0] = 1; g_square[1][1] = 1; g_square[1][2] = 1; g_square[1][3] = 0;
		line = 0;
		list = 3;
		break;
	}
	case 4: {
		g_square[0][0] = 0; g_square[0][1] = 0; g_square[0][2] = 1; g_square[0][3] = 0;
		g_square[1][0] = 1; g_square[1][1] = 1; g_square[1][2] = 1; g_square[1][3] = 0;
		line = 0;
		list = 3;
		break;
	}
	case 5: {
		g_square[0][0] = 0; g_square[0][1] = 1; g_square[0][2] = 1; g_square[0][3] = 0;
		g_square[1][0] = 0; g_square[1][1] = 1; g_square[1][2] = 1; g_square[1][3] = 0;
		break;
	}
	case 6: {
		g_square[0][0] = 1; g_square[0][1] = 1; g_square[0][2] = 1; g_square[0][3] = 1;
		g_square[1][0] = 0; g_square[1][1] = 0; g_square[1][2] = 0; g_square[1][3] = 0;
		line = 0;
		list = 4;
		break;
	}
	}
	shape = n;
	return n;
}

除此之外,为了方便实现对方块进行左右移、旋转等操作,本组决定在方块开始移动时给定一个相对坐标,这个相对坐标其实就是方块中某一个小方块的位置坐标,用list和line分别来表示该坐标在第几列、第几行。然后在方块进行移动时,通过list和line的值来确定方块上一个小方块的位置,从而推得其他方块的位置。进而在背景上直接对这些位置坐标进行一些相应的操作(尤其对于方块旋转的实现至关重要)。

方块自动下落

实现方块的自动下落,本组采用的是暴力遍历的方法。直接从下至上遍历整个背景数组(这里是先对方块类中的假背景数组进行操作,后续再将假背景数组复制到渲染类的真背景数组上),遇到值为1的元素,就将1值赋给该元素下方相邻的元素,并将该元素置0。(此时需要添加判断函数,防止越界)。当方块最下层下落到背景最底行或者叠加到其他方块上时,需要使方块停下来,并固定在背景上。此时需要添加一个转换函数,遍历背景数组,找到所有值为1的元素,并全部赋值为2,表示已经停住的方块,同时调用随机产生新方块的函数。

这里需要提到的是,在背景在方块自动下落时,line值相应加一,list值不变,表示竖直下落一行,在方块左移时,list值减一,line值不变,表示水平向左移动一次,同理,方块右移,list加一,line不变。方块的旋转则需要分情况来实现,一种旋转是专门针对一字型方块的,另一种则是针对剩下的其他方块。

旋转

本组实现方块旋转操作时,实质上是实现一个矩阵的转置。对于除一字型以及正方形(正方形方块无须实现旋转)以外的方块,其实可以看做是在一块3乘3的正方形区域内。这样的话,我们此时只需完成一个方阵的转置即可。由于要对方块所在的3乘3方形区域进行操作,此时就需要用到line和list了。在最初的line和list坐标的选定,本组决定对于除一字型和正方形方块以外的方块选取3乘3区域左上角的坐标作为list和line所代表的坐标,而对于一字型的方块,选择绿色块所在坐标作为list和line所代表的坐标。


可以看到,前五种方块在旋转之后,list和line的值无需发生改变,根据左上角的坐标在背景上将3*3的区域遍历复制一遍,再将复制好的数组根据转置的顺序贴回背景即可。这样就能很方便地实现方阵转置。

void Block::normalrotate()
{
	int num = 2;
	char temp[3][3];
	for (int i = 0; i < 3; i++)
	{
		for (int j = 0; j < 3; j++)
		{
			temp[i][j] = faker_bg[line + i][list + j];
		}
	}
	for (int i = 0; i < 3; i++)
	{
		for (int j = 0; j < 3; j++)
		{
			faker_bg[line + i][list + j] = temp[2 - j][i];
		}
	}
}

正方形方块无须旋转。
比较麻烦的是一字型的方块,需要考虑的情况比较多

void Block::linerotate()
{
	if (faker_bg[line][list - 1] == 1)//横变竖
	{
		faker_bg[line][list - 1] = 0;
		faker_bg[line][list + 1] = 0;
		faker_bg[line][list + 2] = 0;
		if (faker_bg[line + 1][list] == 2)
		{
			faker_bg[line - 1][list] = 1;
			faker_bg[line - 2][list] = 1;
			faker_bg[line - 3][list] = 1;
		}
		else if (faker_bg[line + 2][list] == 2)
		{
			faker_bg[line + 1][list] = 1;
			faker_bg[line - 1][list] = 1;
			faker_bg[line - 2][list] = 1;
		}
		else
		{
			faker_bg[line - 1][list] = 1;
			faker_bg[line + 1][list] = 1;
			faker_bg[line + 2][list] = 1;
		}
	}
	else//竖变横
	{
		faker_bg[line - 1][list] = 0;
		faker_bg[line + 1][list] = 0;
		faker_bg[line + 2][list] = 0;
		if (faker_bg[line][list + 1] == 2 || list == 9)
		{
			faker_bg[line][list - 1] = 1;
			faker_bg[line][list - 2] = 1;
			faker_bg[line][list - 3] = 1;
			list = list - 2;
		}
		else if (faker_bg[line][list + 2] == 2 || list == 8)
		{
			faker_bg[line][list + 1] = 1;
			faker_bg[line][list - 1] = 1;
			faker_bg[line][list - 2] = 1;
			list = list - 2;
		}
		else if (faker_bg[line][list - 1] == 2 || list == 0)
		{
			faker_bg[line][list + 1] = 1;
			faker_bg[line][list + 2] = 1;
			faker_bg[line][list + 3] = 1;
			list = list + 1;
		}
		else
		{
			faker_bg[line][list - 1] = 1;
			faker_bg[line][list + 1] = 1;
			faker_bg[line][list + 2] = 1;
		}

	}
}

左移右移

方块左移右移的操作就容易多了。拿方块的右移来说,可以从背景数组(二维数组)的右边向左边遍历,检查每一个元素,若元素值为1,则将该元素的值赋给其相邻右侧的元素,同时将该元素置为0。(当然此处应该添加判断是否越界)暴力遍历完整个背景之后,再将list加一,line值不变。方块左移同理,只不过应该从背景数组的左边向右边遍历,并在检测到1的元素时将1赋给该元素相邻左侧的元素(要考虑是否已经越界了),并将该元素置零。最后,list减一,line不变。

至于方块的加速下落,只需在击键之后立即再次调用方块下落函数,即可实现加速下落。

接着就是消行操作。本组同样采取暴力遍历的方法,直接从下至上检查每一行,遇到满行的情况,就将该行以上的背景数组进行这样的处理:从下至上,每一行的所有元素全部赋给下方相邻行。完成该操作后,需要再次检查起始行,如此往复,直至检索到背景最顶行为止。

方块类剩下的就是一些判断操作合法性的函数,如判断方块如果进行了指定的移动后会不会导致越界,会不会导致其他已经停住的方块收到破坏,等等等等。如此,对于方块的实现就基本上全部完成了。

Render类

本组决定将渲染类声明为方块类的友元,这样既可以保证方块类的封装性,也能保证渲染类对象可以更方便地访问方块类对象。渲染类的成员函数也不多,这里直接把声明放下面了:

class Block;//向前引用
class Render
{//把Render类声明为Block类的友元类之后,Render类的所有成员函数都是Block类的友元函数,使用时记得要带上一个Block&(Block类的引用)
private:
	int color;//颜色色号
	int hposition;
	int zposition;
	char g_bg[ROWS][COLS];//背景数组
public:
	Render(int h, int z,int C=-1) { hposition = h, zposition = z,color=C; }
	void paint(HDC hdc, Block& b);
	void paintsquare_1(HDC hdc);
	void paintsquare_2(HDC hdc);
	void paintsquare_3(HDC hdc);
	void copyblock(Block& b);
	void copybg(Block& b);//拷贝Block类的假背景地图到Render类的真背景地图
	bool judgeGameover();
};

渲染类的成员函数可以分成两种,第一种函数是用于与方块类对象产生联系的,包括复制方块类对象初始位置到渲染类的真背景数组上以及将方块类的假背景数组实时地复制到渲染类的真背景数组上。第二种函数是针对渲染类自己的真背景数组进行的窗口显示,这类函数的形参大多是一些操作句柄和一些消息。

在窗口上进行绘制,需要用到Rectangle()函数,这个函数是用来画矩形的,需要指定操作句柄,所画矩形的位置以及大小等。这里本组将画方块的函数写成两个,分别用于显示值为1位置上的方块以及用于显示值为2位置上的方块。分成两个函数的理由是:后续在对方块上色时,可以方便添加更多的色彩,使界面显得更加美观。
下面是绘制位置上值为1的方块的函数

void Render::paintsquare_1(HDC hdc)
{
	Rectangle(hdc, hposition, zposition, hposition + 300, zposition + +600);
	int R = 0, G = 0, B = 0;
	switch (color)
	{
	case 0: R = 238, G = 248, B = 173; break;//淡黄
	case 1: R = 149; G = 240; B = 172; break;//青
	case 2: R = 245; G = 171; B = 158; break;//浅红
	case 3: R = 125; G = 190; B = 255; break;//淡蓝
	case 4: R = 250; G = 224; B = 165; break;//淡橘
	case 5: R = 255; G = 159; B = 207; break;//淡粉
	case 6: R = 208; G = 162; B = 255; break;//淡紫
	}
	HBRUSH hNewBrush = CreateSolidBrush(RGB(R, G, B));
	HBRUSH hOldBrush = (HBRUSH)SelectObject(hdc, hNewBrush);
	for (int i = 0; i < ROWS; i++)
	{
		for (int j = 0; j < COLS; j++)
		{

			if (g_bg[i][j] == 1)
			{
				Rectangle(hdc, hposition + j * 30 + 1, i * 30 + 1, hposition + j * 30 + 30 - 1, i * 30 + 30 - 1);
			}
		}
	}
	hNewBrush = (HBRUSH)SelectObject(hdc, hOldBrush);
	DeleteObject(hNewBrush);
}

在函数里,已经添加好了上色操作。对于每一张方块,都有专属于其的颜色。给方块上色需要创建一个新的画刷,替换旧的画刷。需要注意的是,当为指定方块上好颜色之后,记得需要将旧的画刷再换回来,同时删除之前创建的新画刷。

游戏类

游戏类的数据成员只有一个timer,用于区分定时器。剩下的基本都是对程序运行时产生消息做出响应的函数,函数参数大多是操作句柄。同时为了实现游戏类与其他类之间消息的传递,本组还为游戏类的响应函数添加了其他类引用的形参,方便几个类之间进行联动操作。

值得一提的是,方块类中的自动下落函数的实现,也与游戏类密切相关。在这里就不得不说到定时器了。方块的下落指令其实就是由系统的定时器根据预定的时间间隔发出的。

在程序运行之后,方块并不会立即开始下落,而是会等待用户按下回车键才会开始下落。在这里写了一个函数来响应回车键:

void Game::onreturn()
{
	Begin = 1;
	SetTimer(hwnd, TIMER1, SPEED, NULL);
}

可以看到在函数中,开启了一个定时器,这样每隔一定的时间,定时器都会发出一次消息,我们只需要在接收到定时器消息时调用方块类与渲染类的相关函数,将方块位置的变化绘制到窗口上即可。需要注意的是,定时器在窗口关闭前,也应该被关闭,因此,需要在处理窗口关闭消息时将定时器关闭。

既然需要绘制界面,自然少不了响应绘制窗口消息的函数。因为绘制界面的函数已经在渲染类里实现了,因此,可以通过下面这个函数将游戏类、方块类、渲染类联系起来,共同完成界面绘制。

void OnPaint(HDC hDC)
{
	//创建兼容性DC(一张纸的ID)
	HDC hMemDC = CreateCompatibleDC(hDC);
	//创建一张纸
	HBITMAP hBitmapBack = CreateCompatibleBitmap(hDC, 300, 600);//后两个参数指的是后台画纸的宽和高
	//关联起来
	SelectObject(hMemDC, hBitmapBack);
	paintsquare_1(hMemDC);//显示方块1
        paintsquare_1(hMemDC);//显示方块2
	//传递
	BitBlt(hDC, 0, 0, 300, 600, hMemDC, 0, 0, SRCCOPY);//将后台纸上绘制的话直接一次传到屏幕上
	//释放DC
	DeleteObject(hBitmapBack);
	DeleteDC(hMemDC);
}

在这里,本组添加了双缓冲绘图。添加双缓冲绘图的原因是,当所要绘制的窗口较大时,屏幕在每次变化的时候会出现闪动的情况。为了提高游戏体验,引入双缓冲就很有必要了。双缓冲的原理是:计算机先在后台进行界面上图形的绘制,然后再将绘制好的整个窗口位图贴到窗口上。

在回调函数里,与定时器有关的功能还有暂停。本组将空格作为游戏暂停键使用。双方都可以使用暂停键。游戏暂停之后,方块停止移动,再按一次空格键,游戏重新开始,方块重新开始移动。原理其实也很简单,即:第一次敲击空格键,关闭定时器,第二次敲击空格键,重新开启一个定时器。同样的原理,同样也适用于以下情况:按回车之前,游戏结束之后,按键的敲击应该视为不起作用才行。因此,本组决定使用几个全局变量(Begin,Pause,Over)来作为各个按键响应操作是否执行的判断条件。

游戏类中响应其他操作的函数其实也都是同理:通过函数参数将其他类与游戏类整合在一起。说白了,游戏类的成员函数都是用来响应某些消息的,而在这些函数中,其实就是完成各个类之间的组织工作,使得各个类的功能可以有机地结合在一起,共同完成某项工作。

玩家类

本组决定将玩家类声明为方块类的友元类。
玩家类的成员比较少,数据成员目前主要有玩家积分、代表玩家输赢状态的一个标志值、玩家ID等等(后续还会添加一些数据成员),成员函数主要是几个统计消行数,展示以及修改玩家类数据成员的函数。
声明如下

class Player {
private:
	int score;
	int chance;
	static char judgevictory;
	string ID;
	char str[30];
public:
	Player(string id) ;
	char* getstr();
	int dispscore();
	int dispchance();
	void changeScore(int n);
	void changeChance(int n);
	void changeJ() ;
	char getJ() ;
	friend Player getWin(Player& p1, Player& p2);
};

仍存在的问题

1.在代码中,我们仍使用到了全局变量,而众所周知,全局变量会在一定程度上破坏类的封装性,本组将思考更好的算法以此尽量避免使用全局变量。
2.在界面优化方面还有许多地方可以做
3.在游戏结束判断的地方还有一些小bug
4.还未实现自身消行的同时给对方增加随机行
5.还未实现显示下一个方块的预告板
6.虽已分别实现单人模式和双人模式,但还未能实现双人模式与单人模式的自由切换

posted @ 2020-06-13 00:44  枭魈  阅读(241)  评论(4编辑  收藏  举报