C++ 150行代码实现俄罗斯方块

震惊!C++全程不压缩代码、包括注释实现俄罗斯方块竟只需要150行!

该程序思路和步骤均参考自b站 up主:你已经是大佬了快和萌新们聊天吧 大佬的视频。

在此向大佬表示诚挚的敬意与感谢。

视频传送门

由于被参考源代码的特殊原因和其他原因,本人硬是研究了将近11个小时才算基本了解了该程序的原理!

之后我又用C++根据理解重新写了这个程序,并添加了详细的注释。

这个程序让我深刻认识到了在数据结构和算法上,我还有多么长的路要走…

如果大家急于学习,请忽略以下内容并移步技术环节。

反思:
对于人们平时写程序而言,基本上只需要了解了程序的原理,即可动手写出一个具有这些功能的程序。

我认为,不论这个程序本身是多么的复杂,如果写出来的程序包含有大量重复、冗余代码、极高的时空复杂度以及很多的bug,那么这个程序依旧是一个糟糕的程序。

以我之前写的几个小程序为例,动辄上千行的代码看起来很多,但,我自己知道这里面大多数代码都没有什么技术含量,因为在写程序的过程中,我均采取了最暴力也是傻的解决办法,如,for循环上千次只为寻找到一个值、用很多不符合规范的代码去堵上一个bug等等。

虽然我是因为当时没有更好的解决方案才会去这么写,但我们必须要认识到,这种写法不过只是最低级的程序,如果把这种程序真正当作问题的解决方案来看的话,无疑是错误的。

其实在参考其他人的代码写这个程序之前,我用常规暴力写法几乎已经完成了一个俄罗斯方块程序,但是到最后又需要写一个常规写法需要很多lj代码才能实现的功能的时候,我便放弃了,因为我突然意识到:写简单的程序是没有出路的!

技术环节:
编译环境:Windows Visual Studio 2019

需求:
俄罗斯方块所有基本功能,包括:
7种不同的方块组合自动下落
方块旋转(一个键)
方块组合按键下落
方块组合左右移
一行全部为方块时消行

思路:
通过将node二维数组各方块位置组成不同方块组合(形状)与一维map数组的位置对应,且map数组下标加上方块组合距左、上墙的距离,其中距上墙距离用Y*10在一维数组中表示,通过Y每次循环+1即可得到方块组合自动下落,通过接收按键ad,修改X实现左移右移。

通过node数组排成特定规律,再将方块组合编号(形状)与特定相加数字传入move函数,即可与左右移功能使用同一个函数更改得到当前方块组合旋转后的形状,并限制旋转后的碰撞情况。

注意:
文中的“方块组”,是指小方块组成的,所有形状、方向的方块组合。
node数组将所有不同的方块组分别列出。

由于编译器原因,代码中_kbhit()和_getch()函数可能在其他编译器上编译会出现错误,解决办法是去掉函数前面的“_”。

运行效果:
运行效果

代码:

#include <iostream>
#include <conio.h>
#include <windows.h>
#include <ctime>
using namespace std;

int map[250];		//地图数组
int X = 4, Y = 1;	//方块组分别距左墙、上墙的距离

class Square		//方块组类
{
private:
	int node[28][4] =		//节点数组,规律存储各方块组的形状
	{//每四个数存储一个形状的方块组在地图中对应的信息
	 //其含义可以看作在平面直角坐标系中,但不存在可以随机访问的y轴,而最大X轴为10
	 //要访问特定y轴的数,就需要对x轴进行加减,x+n大于10的部分会在y+1轴重新计算,反之在y-1
		{ -1,0,1,-11 },{ 10,0,-10,-9 },{ 11,-1,0,1 },{ 9,10,0,-10 }, { -1,0,1,-9 },{ 10,11,0,-10 },{ 9,-1,0,1 },
		{ 0,10,-10,-11 }, { 9,10,0,1 },{ 11,0,1,-10 },{ 9,10,0,1 },{ 11,0,1,-10 }, { 10,11,-1,0 },{ 10,0,1,-9 },
		{ 10,11,-1,0 },{ 10,0,1,-9 },{ -1,0,1,-10 },{ 10,0,1,-10 },{ 10,-1,0,1 },{ 10,-1,0,-10 },{ 20,10,0,-10 },
		{ -1,0,1,2 }, { 20,10,0,-10 }, { -1,0,1,2 }, { 10,11,0,1 }, { 10,11,0,1 }, { 10,11,0,1 }, { 10,11,0,1 }
	}; 
	//方块组形状,循环
	int form = rand() % 1;
	bool sign = false;			//判断是否执行了方块组下方为边界或其他方块的标记

public:
	int& getform()				//获取和修改当前方块组的形状编号
	{
		return form;
	}

	bool move(int& Z, int L)
	{
		Z += L;		//形参1 += 形参2

		for (int i = 0; i < 4; i++)
		{
			int j = (node[form][i] + 11) % 10 - 1 + X;	//计算出小方块所在地图的列数
			//如果当前方块组修改之后有方块的x轴小于0或x轴大于9
			//或有方块的列数大于24
			//或更改方块组Y坐标后方块所在的位置上已经有其他方块
			//则撤回移动并返回1
			if (j < 0 || j > 9 || ((Y * 10 + X +node[form][i]) / 10) > 24 || map[Y * 10 + X +node[form][i]])
			{
				//如果方块组移动后纵坐标>24或下方有其他方块为真,则设置标记为true,否则标记为false
				(((Y * 10 + X +node[form][i]) / 10) > 24 || map[Y * 10 + X +node[form][i]])
					?
				sign = true : sign = false;
				
				Z -= L;		//因为撤回方块组移动会更改方块组的数值,
				//而判断方块组下方边界和其他方块需要的时方块组移动之后的数据
				//所以撤回撤回移动写在标记赋值下边

				return false;
			}
		}

		//i从后向前遍历地图数组,k用来累加地图一行中为1值的个数,为10则执行消行
		for (int i = 249, k = 1; i > 9; i--)		
		{
			if (i % 10 == 0)
				k = 0;
			if (map[i] == 1)
				k++;
			if (k == 10)
				for (int j = i / 10 * 10 + 9; j >= 0; j--)
					j - 10 >= 0 ? map[j] = map[j - 10] : map[j] = 0;
		}
		return true;	//正常移动返回true
	}

	void setmap()		//将方块组对应的地图位置赋值为-1
	{
		for (int i = 0; i < 4; i++)
			map[Y * 10 + X +node[form][i]] = -1;
	}

	void drop()		//下落
	{
		if (move(Y, 1)||(!sign))				//正常下落或方块组碰左右墙直接返回
			return;

		//方块组成功落地,执行地图对应位置赋值并重置下落方块组的操作

		for (int i = 0; i < 4; i++)				//将当前方块组在地图中对应的位置赋为1
			map[Y * 10 + X + node[form][i]] = 1;

		X = 4, Y = 1, form = rand() % 28;		//将方块组的位置和形状编号进行重置

		for (int i = 0; i < 4; i++)
			map[Y * 10 + X + node[form][i]] = -1;
	}
};

void HideCursor()			//光标隐藏
{
	CONSOLE_CURSOR_INFO cursor_info = { 1, 0 };
	SetConsoleCursorInfo(GetStdHandle(STD_OUTPUT_HANDLE), &cursor_info);
}

int main()
{
	system("mode con:cols=20 lines=25");	//设置控制台宽高,且因为需要用到控制台自动换行,控制台必为10宽,且因为map原因必为25高
	HideCursor();							//光标隐藏
	srand((unsigned)time(NULL));			//随鸡种子

	Square square;		//方块组类

	//游戏循环
	while (true)
	{
		if (_kbhit())	//键盘按下时
		{
			char key = _getch();
			switch (key)
			{
			case 'a':	//左移
				square.move(X,-1);	break;
			case 'd':	//右移
				square.move(X, 1);	break;
			case 's':	//下落
				if (Y < 2) break;	//方块重置时不可连按下落,否则有时会出现bug,原因不明
				square.drop();		break;
			case ' ':	//暂停
				_getch();			break;
			case 'w':	//旋转
				//旋转的本质是改变方块组的形状,将方块组编号传入move中修改,可对修改后的方块组进行越界检测
				//修改后方块组如果与方块1、下、左、右墙重合则会被取消改变
				int &tempf = square.getform();
				square.move(tempf, (tempf % 4) < 3 ? 1 : -3);//将(form % 4) < 3 ? 1 : -3)与form相加即可得到节点数组中与形状相对应的旋转形状,其中玄机,还请自悟
			}
		}
		square.setmap();			//将方块组对应在地图中的位置修改为-1

		//遍历地图数组,非0的位置输出■代表方块
		for (auto i = 0; i < 250; i++)
		{
			cout << (map[i] ? "■" : "  ");

			if(map[i] == -1)	//必须加if(map[i]==-1)的原因为:在方块组成功下落后,
				//地图对应位置会变为1来表示已经下落的方块,如果不加判断会将已经下落的方块重新置为0
				map[i] = 0;		//输出完方块后将当前地图值重新赋值为0
		}

		square.drop();			//方块组下落

		Sleep(150);
	}
	return 0;
}

由于目前本人对该程序还不深,不规范之处还请多多包含。

欢迎大家提出批评和建议。


感谢大家的支持。

原创文章 20 获赞 24 访问量 8352

猜你喜欢

转载自blog.csdn.net/qq_46239972/article/details/105212344