俄罗斯方块Tetris(C基础,Linux终端)

文章目录

  • 俄罗斯方块Tetris(C基础,Linux终端)
    • 前言
    • 游戏说明
    • 游戏效果展示
    • 游戏程序实现步骤
        • 一.准备工作
          • 1.非阻塞型输入
          • 2.在屏幕上打印一个方块
        • 二.头文件、宏定义、全局变量、声明及主函数
          • 7种基本形状,以 L 型为例
        • 三.创建游戏界面
        • 四.打印整个界面
        • 五.随机出新的图形并打印
        • 六.方块的下降、左右移动及旋转
          • 旋转算法示意:
        • 七.用户输入及控制
        • 八.方块触底
        • 九.方块触顶(结束游戏)
        • 十.完结撒花
    • 完整代码

俄罗斯方块Tetris(C基础,Linux终端)

前言

学完了C语言基础后想写个经典游戏俄罗斯方块Tetris练练手吗?今天它来啦!代码量少,逻辑清晰,简单易懂,山顶洞人表示一眼就能学会! (结尾附完整代码)

游戏说明

1.按键 W :方块变形
2.按键 S :方块加速下落
3.按键 A :方块向左移动
4.按键 D :方块向右移动
5.按键:ctrl + C :结束运行
6.游戏区右侧信息区可显示下一个方块的形状,以及当前得分
7.快动手去拓展更多功能吧!!

游戏效果展示

游戏程序实现步骤

一.准备工作

1.非阻塞型输入

首先来了解一下什么是非阻塞型输入,非阻塞型输入跟操作系统有关。linux下C语言常用输入函数 scanf 、getchar 和 gets 通常都是阻塞式的,如果用户没有输入,程序就一直停在那儿。输入内容后,还需要用户点回车,同时,用户输入的信息,会在屏幕上回显出来。写俄罗斯方块的时候,我们不期望这种阻塞发生,我们希望按键后程序立刻响应,而不需要输入回车和屏幕回显。这不是我们编写程序的重点,只需了解即可。来看非阻塞型输入实验完整代码:(可直接复制尝试)
此段程序仅供做理解与测试用,ctrl+c结束运行:

#include
#include 
#define TTY_PATH    "/dev/tty"
#define STTY_US     "stty raw -echo -F "
#define STTY_DEF    "stty -raw echo -F "
int get_char();

int get_char()
{
    fd_set rfds;
    struct timeval tv;
    int ch = 0;
    FD_ZERO(&rfds);
    FD_SET(0, &rfds);
    tv.tv_sec = 0;
    tv.tv_usec = 10; 							//设置等待超时时间
	if (select(1, &rfds, NULL, NULL, &tv) > 0)  //检测键盘是否有输入
    {
        ch = getchar(); 
    }
    return ch;
}

int main()
{
    int ch = 0;
    system(STTY_US TTY_PATH);					//开启非阻塞输入
    while(1)
    {
        ch = get_char();
        if (ch==3)								//ctrl+c
		{
			system(STTY_DEF TTY_PATH);			//关闭非阻塞输入
			return 0;
		}
            printf("key = %d(%c)\n\r", ch, ch);
			usleep(1000*1000);					//微秒数     
    }
}

2.在屏幕上打印一个方块

Linux控制台特殊效果 linux下printf的特殊用法

void drawPoint(int x, int y,int mode,int color) 
{ 
	printf("\033[%d;%dH", y+1, x*2+1);//x,y参数为方块的位置 
	printf("\033[%d;%dm■ \033[0m",mode,color);//打印一个方块 
}
(1) 通用格式控制:mode (2) 前景色:color
0 重置所有属性 30 黑色
1 高亮/加粗 31 红色
2 暗淡 33 黄色
4 下划线 34 蓝色
5 闪烁 35 品红
7 反转 36 青色
8 隐藏 37 白色

二.头文件、宏定义、全局变量、声明及主函数

#include
#include  
#include 
#define TTY_PATH  "/dev/tty"
#define STTY_US  "stty raw -echo -F "
#define STTY_DEF  "stty -raw echo -F "
#define SCREEN_WIDTH 18
#define SCREEN_HEIGHT 20
#define INFO_AREA 6
#define SPEED 8

int map[SCREEN_HEIGHT][SCREEN_WIDTH] = {0};//定义二维数组作为平面直角坐标系,向右x增向下y增
struct Point_Delta			//中心点的偏移量,中心点以此延伸获得各形状点的坐标
{
	int delta_x;
	int delta_y;
};
struct Point_Delta shapes[7][4] = //7种基本方块形状
{
	{{0,0},{-1,0},{1,0},{0,1}},//T
	{{0,0},{0,-1},{0,1},{1,1}},//L
	{{0,0},{0,-1},{0,1},{-1,1}},//反L
	{{0,0},{0,-1},{-1,-1},{1,0}},//Z
	{{0,0},{-1,0},{0,-1},{1,-1}},//反Z
	{{0,0},{1,0},{0,1},{1,1}},//田
	{{0,0},{0,-1},{0,1},{0,2}},//|
};
int x,y,x_next,y_next,shapeIndex,shapeIndex_temp,shapeIndex_next;
int score,moveDown_count;
void showPoint(int x, int y,int bright,int color);
void showMap();
void setFrame();
void creatNewShape();
void showShape();
int moveDown();
void addToStatic();
void clearLines();
int isFullLine(int line);
void copyLine(int dst,int src);
void supplyLines(int line);
void userControl(char input);
void moveRight();
void moveLeft();
int overCheak();
void userControl(char input);
void moveRight();
void moveLeft();
int get_char();

int main()
{
		system(STTY_US TTY_PATH);//开启非阻塞输入
		setFrame();//初始化边框
		srand(time(0));//随机种子
		shapeIndex = rand()%7;//初始化一个形状的编号
		shapeIndex_next = rand()%7;//初始化下一个形状的编号
		creatNewShape();//创建新图形,重置定位点
		while(1)
		{
			system("clear");//清屏
			showShape();//打印当前的图形以及信息区下一个形状
			if(moveDown())//控制下降函数,触底判断为真
			{
				addToStatic();//把触底的图形添加至静态部分
				clearLines();//如果有一行满了则消除并加相应的分数
				shapeIndex = shapeIndex_next;//下一个图形的编号为信息区所显示的
				creatNewShape();
				shapeIndex_next = shapeIndex_temp;//新随机的下一个图形编号
			}
			showMap();//打印整个游戏界面
			if(overCheak())//检测方块触顶的函数,触顶判断为真,跳出循环
				break;
			fflush(stdout);//清理缓存,让终端立刻显示
			usleep(1000*40);//系统等待的微秒数,控制帧率
			char input = get_char();//获取按键输入
			userControl(input);//控制方块的左右移动,变形,加速下降
			if(input == 3)
			{
				system(STTY_DEF TTY_PATH);//关闭非阻塞输入
				return 0;
			}
		}
	return 0;
}
7种基本形状,以 L 型为例

俄罗斯方块Tetris(C基础,Linux终端)_第1张图片

三.创建游戏界面

void setFrame()//创建边框
{
	int i,j;
	for(j=0;j<SCREEN_WIDTH;j++)
	{
		map[0][j] = 1;//第1行   给坐标上点赋值是为了显示特定的点,判断碰撞以及区分开不同意义的点集
		map[SCREEN_HEIGHT-1][j] = 1;//最后一行
	}
	for(i=1;i<SCREEN_HEIGHT-1;i++)
	{
		map[i][0] = 1;//第一列
		map[i][SCREEN_WIDTH-INFO_AREA] = 1;//信息区
		map[i][SCREEN_WIDTH-1] = 1;//最后一列
	}
}

四.打印整个界面

void showPoint(int x, int y,int mode,int color)
{
	printf("\033[%d;%dH", y+1, x*2+1);
	printf("\033[%d;%dm■ \033[0m",mode,color);//打印一个方块
}
void showMap()//打印整个界面
{
	int i,j;
	for(i=0;i<SCREEN_HEIGHT;i++)
	{
		for(j=0;j<SCREEN_WIDTH;j++)
		{
			if(map[i][j] == 1)
				showPoint(j,i,1,30);//行标i,列表j与坐标系横纵坐标的值是相反的
			if(map[i][j] == 2)
				showPoint(j,i,1,33);
			if(j==SCREEN_WIDTH-INFO_AREA)
			{
				if(i==1)
					printf("Next:");
				if(i==7)
					printf("Score:");
				if(i==8)
					printf("    %3d",score);
			}
		}
	}
}

五.随机出新的图形并打印

void creatNewShape()
{
	x = (SCREEN_WIDTH-INFO_AREA)/2;//新形状的定位点,游戏区的中间
	y = 2;//避免越界
	shapeIndex_temp = rand()%7;
	x_next = SCREEN_WIDTH-INFO_AREA/2;//信息区下一个形状的定位点
	y_next = 4;
}
void showShape()
{
	int i;
	for(i=0;i<4;i++)
		showPoint(x+shapes[shapeIndex][i].delta_x,//x,y分别加上4个点相应的偏移量
			      y+shapes[shapeIndex][i].delta_y,1,31);
	for(i=0;i<4;i++)
	{ 
		showPoint(x_next+shapes[shapeIndex_next][i].delta_x,
				  y_next+shapes[shapeIndex_next][i].delta_y,1,31);
	}
}

六.方块的下降、左右移动及旋转

int moveDown()
{
	int i;
	if(moveDown_count<SPEED)//函数被调用SPEED次才会执行一次,用来控制下降速度
	{
		moveDown_count++;
		return 0;
	}
	moveDown_count = 0;
	for(i=0;i<4;i++)
	{
		int dx = x+shapes[shapeIndex][i].delta_x;
		int dy = y+shapes[shapeIndex][i].delta_y+1;//判断map[x][y]的下方是否已经有方块
		if(map[dy][dx] == 1||map[dy][dx] ==2)//如果有,不可移动
			return 1;
	}
	y++;
	return 0;
}
void moveRight()
{
	int i,rx,ry;//移动之前检测合法性,先判断移动后的点是否与边界或静态部分碰撞
	for(i=0;i<4;i++)
	{
		rx = x+shapes[shapeIndex][i].delta_x+1;
		ry = y+shapes[shapeIndex][i].delta_y;
		if(map[ry][rx]==1||map[ry][rx]==2)//不能移动
			return;
	}
	x++;//合法,则移动
}
void moveLeft()
{
	int i,lx,ly;
	for(i=0;i<4;i++)
	{
		lx = x+shapes[shapeIndex][i].delta_x-1;
		ly = y+shapes[shapeIndex][i].delta_y;
		if(map[ly][lx]==1||map[ly][lx]==2)//不能移动
			return;
	}
	x--;//合法,则移动
}
void chageShape()
{
	int i,newDelta_x[4],newDelta_y[4];
	if(shapeIndex==5)//田形状不需要旋转
		return;
	for(i=0;i<4;i++)
	{
		newDelta_x[i] = shapes[shapeIndex][i].delta_y;
		newDelta_y[i] = -shapes[shapeIndex][i].delta_x;
		if(map[y+newDelta_y[i]][x+newDelta_x[i]] == 1//在旋转之前要做合法性判断,旋转后是否会发生碰撞
			||map[y+newDelta_y[i]][x+newDelta_x[i]] == 2)
			return;
	}
	for(i=0;i<4;i++)//合法,则赋值完成旋转
	{
		shapes[shapeIndex][i].delta_x = newDelta_x[i];
		shapes[shapeIndex][i].delta_y = newDelta_y[i];
	}
}
旋转算法示意:

俄罗斯方块Tetris(C基础,Linux终端)_第2张图片

七.用户输入及控制

int get_char()	//简单了解即可
{
  fd_set rfds;
  struct timeval tv;
  int ch = 0;
  FD_ZERO(&rfds);
  FD_SET(0, &rfds);
  tv.tv_sec = 0;
  tv.tv_usec = 10; 
  if (select(1, &rfds, NULL, NULL, &tv) > 0)
  {
      ch = getchar(); 
  }
  return ch;
}
void userControl(char input)
{
	switch(input)
	{
		case 'w':
			chageShape();
			break;
		case 'a':
			moveLeft();
			break;
		case 's':
			moveDown_count+=SPEED;//加速下降
			break;
		case 'd':
			moveRight();
			break;
	}
}

八.方块触底

每次方块触底后:
1.把它添加到静态部分,把所有已经落定的方块成为静态区域(坐标上的点赋值为2)
2.判断是否有满行,有则消除(包含满行判断,行覆盖,行添补)
3.创建一个新的图形继续下落

void addToStatic()//添加到静态区域,把所有已经落定的方块称为静态区域
{
	int i;
	for(i = 0;i<4;i++)
		map[y+shapes[shapeIndex][i].delta_y][x+shapes[shapeIndex][i].delta_x] = 2;//静态的点值为2
}
void clearLines()//消除已经满的行
{
	int i,count = 0;
	for(i=SCREEN_HEIGHT-2;i>0;i--)//从最后一行往上遍历
	{
		if(isFullLine(i))
		{
			count++;//此处算法可参考下方程序段
			continue;
		}
		copyLine(i+count,i);//上面覆盖到下面
	}
	for(i=1;i<count;i++)
		supplyLines(i);
	score += count;//计分
}
int isFullLine(int line)//判断此行是否满
{
	int i;
	for(i=1;i<SCREEN_WIDTH-INFO_AREA-1;i++)//只算游戏区域
	{
		if(map[line][i]!=2)//此行没有满
			return 0;
	}
	return 1;				//此行全部为2
}
void copyLine(int dst,int src)//目标行,源行
{
	int i;
	for(i=1;i<SCREEN_WIDTH-INFO_AREA-1;i++)
		map[dst][i] = map[src][i];
}
void supplyLines(int line)//给i行补0
{
	int i;
	for(i=1;i<SCREEN_WIDTH-INFO_AREA-1;i++)
		map[line][i] = 0;
}

消行算法的原理可参考下方程序,不要与游戏程序混淆:

#include
int main()
{
	int a[6] = {0,2,3,1,4,1};//消除值为1的元素并在前面补0
	int i,count = 0;
	for(i=6-1;i>=0;i--)
	{
		if(a[i]==1)
		{
			count++;
			continue;
		}
		a[i+count] = a[i];
	}

	for(i=0;i<count;i++)
		a[i] = 0;

	for(i=0;i<6;i++)
		printf("%d ",a[i]);//运行程序输出结果为0 0 0 2 3 4
	printf("\n");
	return 0;
}

九.方块触顶(结束游戏)

int overCheak()//检测方块是否触顶
{
	int i,m,n;
	for(i=1;i<SCREEN_WIDTH-1;i++)
	{
		if(map[1][i]==2)
		{			
			system(STTY_DEF TTY_PATH);//关闭非阻塞输入
			return 1;
		}
	}
	return 0;
}

十.完结撒花

1.笔者因水平限制如有误导大家的地方非常抱歉哈
2.欢迎下方留言讨论
3.来“华清远见” http://www.hqyj.com/学习更多编程知识
俄罗斯方块Tetris(C基础,Linux终端)_第3张图片

完整代码

#include
#include  
#include 
#define TTY_PATH  "/dev/tty"
#define STTY_US  "stty raw -echo -F "
#define STTY_DEF  "stty -raw echo -F "
#define SCREEN_WIDTH 18
#define SCREEN_HEIGHT 20
#define INFO_AREA 6
#define SPEED 8

int map[SCREEN_HEIGHT][SCREEN_WIDTH] = {0};//定义二维数组作为平面直角坐标系,向右x增向下y增
struct Point_Delta							//中心点的偏移量,中心点以此延伸获得各形状点的坐标
{
	int delta_x;
	int delta_y;
};
struct Point_Delta shapes[7][4] = 
{
	{{0,0},{-1,0},{1,0},{0,1}},//T
	{{0,0},{0,-1},{0,1},{1,1}},//L
	{{0,0},{0,-1},{0,1},{-1,1}},//反L
	{{0,0},{0,-1},{-1,-1},{1,0}},//Z
	{{0,0},{-1,0},{0,-1},{1,-1}},//反Z
	{{0,0},{1,0},{0,1},{1,1}},//田
	{{0,0},{0,-1},{0,1},{0,2}},//|
};
int x,y,x_next,y_next,shapeIndex,shapeIndex_temp,shapeIndex_next;
int score,moveDown_count;
void showPoint(int x, int y,int bright,int color);
void showMap();
void setFrame();
void creatNewShape();
void showShape();
int moveDown();
void addToStatic();
void clearLines();
int isFullLine(int line);
void copyLine(int dst,int src);
void supplyLines(int line);
void userControl(char input);
void moveRight();
void moveLeft();
int overCheak();
void userControl(char input);
void moveRight();
void moveLeft();
int get_char();
int main()
{
		system(STTY_US TTY_PATH);//开启非阻塞输入
		setFrame();
		srand(time(0));
		shapeIndex = rand()%7;
		shapeIndex_next = rand()%7;
		creatNewShape();
		while(1)
		{
			system("clear");
			showShape();
			if(moveDown())
			{
				addToStatic();
				clearLines();
				shapeIndex = shapeIndex_next;
				creatNewShape();
				shapeIndex_next = shapeIndex_temp;
			}
			showMap();
			if(overCheak())
				break;
			fflush(stdout);//清理缓存,让终端立刻显示
			usleep(1000*40);//微秒数
			char input = get_char();
			userControl(input);
			if(input == 3)
			{
				system(STTY_DEF TTY_PATH);//关闭非阻塞输入
				return 0;
			}
		}
	return 0;
}
void setFrame()//创建边框
{
	int i,j;
	for(j=0;j<SCREEN_WIDTH;j++)
	{
		map[0][j] = 1;//第1行   给坐标上点赋值是为了显示特定的点,判断碰撞以及区分开不同意义的点集
		map[SCREEN_HEIGHT-1][j] = 1;//最后一行
	}
	for(i=1;i<SCREEN_HEIGHT-1;i++)
	{
		map[i][0] = 1;//第一列
		map[i][SCREEN_WIDTH-INFO_AREA] = 1;//信息区
		map[i][SCREEN_WIDTH-1] = 1;//最后一列
	}
}

void showPoint(int x, int y,int bright,int color)
{
	printf("\033[%d;%dH", y+1, x*2+1);
	printf("\033[%d;%dm■ \033[0m",bright,color);//打印一个方块
}
void showMap()
{
	int i,j;
	for(i=0;i<SCREEN_HEIGHT;i++)
	{
		for(j=0;j<SCREEN_WIDTH;j++)
		{
			if(map[i][j] == 1)
				showPoint(j,i,1,30);//行标i,列表j与坐标系横纵坐标的值是相反的
			if(map[i][j] == 2)
				showPoint(j,i,1,33);
			if(j==SCREEN_WIDTH-INFO_AREA)
			{
				if(i==1)
					printf("Next:");
				if(i==7)
					printf("Score:");
				if(i==8)
					printf("    %3d",score);
			}
		}
	}
}
void creatNewShape()
{
	x = (SCREEN_WIDTH-INFO_AREA)/2;//新形状的定位点,游戏区的中间
	y = 2;//避免越界
	shapeIndex_temp = rand()%7;
	x_next = SCREEN_WIDTH-INFO_AREA/2;
	y_next = 4;
}
void showShape()
{
	int i;
	for(i=0;i<4;i++)
		showPoint(x+shapes[shapeIndex][i].delta_x,
			      y+shapes[shapeIndex][i].delta_y,1,31);
	for(i=0;i<4;i++)
	{ 
		showPoint(x_next+shapes[shapeIndex_next][i].delta_x,
				  y_next+shapes[shapeIndex_next][i].delta_y,1,31);
	}
}
int moveDown()
{
	int i;
	if(moveDown_count<SPEED)
	{
		moveDown_count++;
		return 0;
	}
	moveDown_count = 0;
	for(i=0;i<4;i++)
	{
		int dx = x+shapes[shapeIndex][i].delta_x;
		int dy = y+shapes[shapeIndex][i].delta_y+1;//原来位置的map[x][y]已经是1
		if(map[dy][dx] == 1||map[dy][dx] ==2)
			return 1;//不可移动
	}
	y++;
	return 0;
}
void moveRight()
{
	int i,rx,ry;
	for(i=0;i<4;i++)
	{
		rx = x+shapes[shapeIndex][i].delta_x+1;
		ry = y+shapes[shapeIndex][i].delta_y;
		if(map[ry][rx]==1||map[ry][rx]==2)//不能移动
			return;
	}
	x++;
}
void moveLeft()
{
	int i,lx,ly;
	for(i=0;i<4;i++)
	{
		lx = x+shapes[shapeIndex][i].delta_x-1;
		ly = y+shapes[shapeIndex][i].delta_y;
		if(map[ly][lx]==1||map[ly][lx]==2)//不能移动
			return;
	}
	x--;
}
void chageShape()
{
	int i,newDelta_x[4],newDelta_y[4];
	if(shapeIndex==5)
		return;
	for(i=0;i<4;i++)
	{
		newDelta_x[i] = shapes[shapeIndex][i].delta_y;
		newDelta_y[i] = -shapes[shapeIndex][i].delta_x;
		if(map[y+newDelta_y[i]][x+newDelta_x[i]] == 1
			||map[y+newDelta_y[i]][x+newDelta_x[i]] == 2)
			return;
	}
	for(i=0;i<4;i++)
	{
		shapes[shapeIndex][i].delta_x = newDelta_x[i];
		shapes[shapeIndex][i].delta_y = newDelta_y[i];
	}
}
int get_char()	
{
  fd_set rfds;
  struct timeval tv;
  int ch = 0;
  FD_ZERO(&rfds);
  FD_SET(0, &rfds);
  tv.tv_sec = 0;
  tv.tv_usec = 10; 
  if (select(1, &rfds, NULL, NULL, &tv) > 0)
  {
      ch = getchar(); 
  }
  return ch;
}
void userControl(char input)
{
	switch(input)
	{
		case 'w':
			chageShape();
			break;
		case 'a':
			moveLeft();
			break;
		case 's':
			moveDown_count+=SPEED;
			break;
		case 'd':
			moveRight();
			break;
	}
}

void addToStatic()//添加到静态区域,把所有已经落定的方块称为静态区域
{
	int i;
	for(i = 0;i<4;i++)
		map[y+shapes[shapeIndex][i].delta_y][x+shapes[shapeIndex][i].delta_x] = 2;//静态的点值为2
}
void clearLines()//消除已经满的行
{
	int i,count = 0;
	for(i=SCREEN_HEIGHT-2;i>0;i--)//从最后一行往上遍历
	{
		if(isFullLine(i))
		{
			count++;
			continue;
		}
		copyLine(i+count,i);//上面覆盖到下面
	}
	for(i=1;i<count;i++)
		supplyLines(i);
	score += count;
}
int isFullLine(int line)
{
	int i;
	for(i=1;i<SCREEN_WIDTH-INFO_AREA-1;i++)//只算游戏区域
	{
		if(map[line][i]!=2)//此行没有满
			return 0;
	}
	return 1;				//此行全部为2
}
void copyLine(int dst,int src)//目标行,源行
{
	int i;
	for(i=1;i<SCREEN_WIDTH-INFO_AREA-1;i++)
		map[dst][i] = map[src][i];
}
void supplyLines(int line)//给i行补0
{
	int i;
	for(i=1;i<SCREEN_WIDTH-INFO_AREA-1;i++)
		map[line][i] = 0;
}
int overCheak()//检测是否游戏结束
{
	int i,m,n;
	for(i=1;i<SCREEN_WIDTH-1;i++)
	{
		if(map[1][i]==2)
		{			
			system(STTY_DEF TTY_PATH);//关闭非阻塞输入
			return 1;
		}
	}
	return 0;
}

你可能感兴趣的:(linux,c语言,linux,c语言)