C语言版2048双平台游戏

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/HaloTrriger/article/details/82696572

一、初衷

看到舍友玩这个游戏,思考了下觉得这个游戏可以用简单的代码实现,用表格当做界面,用数组保存值,读入玩家操作后刷新界面显示给玩家就可以了

二、游戏特色

  1. 支持双平台(Windows和Linux)
  2. 可以显示历史最高分,可选择重新开始或退出
  3. 可自行更改行列大小(宏:ROW,COL),改大了可能要玩很久才会输

三、游戏思路

  1. 读入玩家的操作,如果是上下左右中的一个操作,则让每个元素都进行如此操作。
  2. 在操作中判断那些元素需要挪动,那些元素需要合并
  3. 如果有元素合并了,则在空位置生成随机数(2、4)
  4. 刷新界面,重新显示格子。
  5. 如果每个元素都不能进行合并或挪动则结束游戏。(不能根据是否还有空格子判断游戏结束与否)

四、元素挪动或合并(用左移举例)

  1. 在每一行,第一列元素作为比较列。
  2. 如果第一列元素是0,即空位,后面的非空元素填补这个空位
  3. 如果第一列元素不空,但和后面第一个非空元素相等,相加合并值;且后面那个元素位置置空。比较列后移。
  4. 否则,比较列后移,用后面的非空元素覆盖比较列的值。如果当前列和比较列不同,则将比较列值空。
  5. 循环,继续和比较列元素判断。

五、代码

2048.h

#ifndef __2048_H__
#define __2048_H__
#endif //2048.h


#define _CRT_SECURE_NO_WARNINGS
#include<time.h> 
#include<stdio.h> 
#include<stdlib.h> 
#include<stdbool.h>

#ifdef _WIN32
/* 包含Windows平台相关函数,包括控制台界面清屏及光标设定等功能 */
#include<io.h>
#include<conio.h>
#include<windows.h>
#include<direct.h>
#include<Shlobj.h>

#else
/* 包含Linux平台相关函数,包括控制台界面清屏及光标设定等功能 */
#include <unistd.h>
#include <bits/signum.h>
#include <termio.h>
#include<signal.h>
#include<limits.h>

#define KEY_CODE_UP    0x41
#define KEY_CODE_DOWN  0x42
#define KEY_CODE_RIGHT 0x43
#define KEY_CODE_LEFT  0x44
#define KEY_CODE_QUIT  0x71

struct termios old_config; /* linux下终端属性配置备份 */

#endif //Win32

						   //表格所代表的的行和列可自行更改
#define ROW 5
#define COL 5
#define MAX_PATH 260
char historyBest[MAX_PATH]; //历史最高记录文件的路径
int board[ROW][COL];     /* 界面数组 */
int score;           /* 游戏得分 */
int highest_score;   /* 游戏最高分 */
bool if_random; /* 是否需要生成随机数标志,1表示需要,0表示不需要 */
bool game_over;    /* 是否游戏结束标志,1表示游戏结束,0表示游戏 */
bool if_exit; /* 是否准备退出游戏,1表示是,0表示否 */
bool if_restart; /*是否重新开始游戏*/

				 //游戏处理函数
void InitGame();    /* 初始化游戏 */
void ClearScreen();    /* 清屏 */
void RefreshBoard();    /* 刷新界面显示 */
void InitBoard();     /*初始化棋盘*/
void PlayGame();    /* 游戏循环 */
void EndGame(int signal); /* 结束游戏 */

int ReadKeyboard(); /*读取键盘操作*/

					//方向移动函数
void LeftMove();
void RightMove();
void UpMove();
void DownMove();

//游戏检测函数
void GenerateRandPosition();    /* 在空的随机位置生成一个数2/4概率1:1 */
void CheckGameOver(); /* 检测是否输掉游戏,设定游戏结束标志 */
int GetEmptyCount();   /* 获取游戏面板上空位置数量 */

2048.c

#include"2048.h"
/* 初始化游戏 */
void InitGame()
{
#ifdef _WIN32
	system("cls");
	char CurDir[MAX_PATH];
	_getcwd(CurDir, MAX_PATH);
	sprintf(historyBest, "%sbestScore.dat", CurDir);

#else
	/* 获取游戏存档路径,Linux下放在当前用户主目录下 */
	char CurDir[MAX_PATH];
	getcwd(CurDir, MAX_PATH);
	sprintf(historyBest, "%s/2048.txt", CurDir);

	tcgetattr(0, &old_config);              /* 获取终端属性 */
	struct termios new_config = old_config; /* 创建新的终端属性 */
	new_config.c_lflag &= ~ICANON;          /* 设置非正规模式 */
	new_config.c_lflag &= ~ECHO;            /* 关闭输入回显 */
	new_config.c_cc[VMIN] = 1;              /* 设置非正规模式下的最小字符数 */
	new_config.c_cc[VTIME] = 0;             /* 设置非正规模式下的读延时 */
	tcsetattr(0, TCSANOW, &new_config);     /* 设置新的终端属性 */
	printf("\033[?25l");

	signal(SIGINT, EndGame);
#endif

	/* 读取游戏最高分数 */
	FILE *fp = fopen(historyBest, "r");
	if (fp)
	{
		fread(&highest_score, sizeof(highest_score), 1, fp);
		fclose(fp);

	}
	else
	{
		highest_score = 0;
		fp = fopen(historyBest, "w");
		if (fp)
		{
			fwrite(&highest_score, sizeof(highest_score), 1, fp);
			fclose(fp);
		}
	}
}

void InitBoard()
{
	score = 0;
	if_random = true;
	game_over = false;
	if_exit = false;
	if_restart = false;
	/*表格全部置为0,防止不能生成随机位置 */
	for (int i = 0; i < ROW; ++i)
	{
		for (int j = 0; j < COL; ++j)
			board[i][j] = 0;
	}

	/* 游戏开始先随机生成一个2,其他均为0 */
	srand((unsigned)time(NULL));//生成种子
	int row = rand() % ROW;
	int col = rand() % COL;
	board[row][col] = 2;
	/* 再生成一个随机的2或4,概率之比1:1 */
	GenerateRandPosition();

	/* 刷新界面 */
	RefreshBoard();
}

// 刷新界面 函数定义
void RefreshBoard()
{
	ClearScreen();

	printf("\n\n\n\n");
	printf("                             GAME_NAME: 2048 \n\n");
	printf("                      SCORE: %5d     BEST: %5d\n", score, highest_score);
	printf("               --------------------------------------------------");

	/* 绘制方格和数字 */
	printf("\n\n                             ┌──");
	for (int i = 0; i < COL - 1; ++i)
		printf("──┬──");
	printf("──┐\n");


	for (int i = 0; i < ROW; ++i)
	{
		printf("                             │");
		for (int j = 0; j < COL; ++j)
		{
			if (board[i][j] != 0)
			{
				if (board[i][j] < 10)
				{
					printf("  %d │", board[i][j]);
				}
				else if (board[i][j] < 100)
				{
					printf(" %d │", board[i][j]);
				}
				else if (board[i][j] < 1000)
				{
					printf(" %d│", board[i][j]);
				}
				else if (board[i][j] < 10000)
				{
					printf("%4d│", board[i][j]);
				}
				else
				{
					//计算超出10000应该是2的多少次方如2^10形式 
					int n = board[i][j];
					for (int k = 1; k < 20; ++k)
					{
						n = n >> 1;
						if (n == 1)
						{
							printf("2^%2d│", k);
							break;
						}
					}
				}
			}
			else
				printf("    │");
		}

		if (i < COL - 1)
		{
			printf("\n                             ├──");
			for (int i = 0; i < COL - 1; ++i)
				printf("──┼──");
			printf("──┤\n");
		}
		else
		{
			printf("\n                             └──");
			for (int i = 0; i < COL - 1; ++i)
				printf("──┴──");
			printf("──┘\n");
		}
	}
	printf("\n");
	printf("               --------------------------------------------------\n");
	printf("                  [w]:UP [s]:Down [a]:Left [d]:Right [r]:Restart [q]:Exit ");

	if (GetEmptyCount() == 0)
	{
		CheckGameOver();

		/* 判断是否输掉游戏 */
		if (game_over == true)
		{
			//\b表示退格,与backspace不同的是不删除元素,下次输入从倒数第\b个元素开始覆盖
			printf("\r                      GAME OVER! TRY AGAIN? [y/n]:                         \b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b");

#ifdef _WIN32
			CONSOLE_CURSOR_INFO info = { 1, 1 };
			SetConsoleCursorInfo(GetStdHandle(STD_OUTPUT_HANDLE), &info);
#else
			printf("\033[?25h"); /* linux下的显示输入光标 */
#endif
		}
	}

	/* 判断是否准备退出游戏 */
	if (if_exit == true)
	{
		printf("\r                   DO YOU REALLY WANT TO QUIT THE GAME? [Y/N]:           \b\b\b\b\b\b\b\b\b\b");
#ifdef _WIN32
		CONSOLE_CURSOR_INFO info = { 1, 1 };
		SetConsoleCursorInfo(GetStdHandle(STD_OUTPUT_HANDLE), &info);
#else
		printf("\033[?25h"); /* linux下的显示输入光标 */
#endif
	}

	/* 判断是否重开游戏 */
	if (if_restart == true)
	{
		printf("\r                   DO YOU REALLY WANT TO RESTART THE GAME? [Y/N]:           \b\b\b\b\b\b\b\b\b\b");
#ifdef _WIN32
		CONSOLE_CURSOR_INFO info = { 1, 1 };
		SetConsoleCursorInfo(GetStdHandle(STD_OUTPUT_HANDLE), &info);
#else
		printf("\033[?25h"); /* linux下的显示输入光标 */
#endif
	}
	fflush(0); /* 刷新输出缓冲区 */
}

/* 开始游戏 函数定义 */
void PlayGame()
{
	while (1)
	{
		int operate = ReadKeyboard(); /* 接收标准输入流字符命令 */

									  /* 判断是否准备退出游戏 */
		if (if_exit == true)
		{
			if (operate == 'y' || operate == 'Y')
			{
				/* 退出游戏,清屏后退出 */
				ClearScreen();
				return;
			}
			else if (operate == 'n' || operate == 'N')
			{
				/* 取消退出 */
				if_exit = false;
				RefreshBoard();
				continue;
			}
			else
			{ //无效输入,继续进行循环,直到用户输入有效选择
				continue;
			}
		}

		/*是否是重新开始游戏*/
		if (if_restart == true)
		{
			if (operate == 'y' || operate == 'Y')
			{
				/* 重新游戏 */
				RefreshBoard();
				InitBoard();
			}
			else if (operate == 'n' || operate == 'N')
			{
				/* 取消重新开始 */
				if_restart = false;
				RefreshBoard();
				continue;
			}
			else
			{ //无效输入,继续进行循环,直到用户输入有效选择
				continue;
			}
		}

		/* 游戏已结束,判断是否需要继续*/
		if (game_over == true)
		{
			if (operate == 'y' || operate == 'Y')
			{
				InitGame();
				continue;
			}
			else if (operate == 'n' || operate == 'N')
			{
				ClearScreen();
				return;
			}
			else
			{
				continue;
			}
		}

		if_random = false; /* 先设定不默认需要生成随机数,需要时再设定为1 */

#ifdef _WIN32
						   /* 命令解析,除了上下左右箭头w,s,a,d字符代表上下左右右命令,q代表退出 */
		switch (operate)
		{
			//具体keycode可以用_getch函数输入打印查看
		case 'w':
		case 72:UpMove(); break;
		case 's':
		case 80:DownMove(); break;
		case 'a':
		case 75:LeftMove(); break;
		case 'd':
		case 77:RightMove(); break;
		case 'r':
			if_restart = true; break;
		case 'q':
		case 27:if_exit = true; break;
		default:continue;
		}

#else
		switch (operate)
		{
		case 'a':
		case KEY_CODE_LEFT:LeftMove();
			break;
		case 's':
		case KEY_CODE_DOWN:DownMove();
			break;
		case 'w':
		case KEY_CODE_UP:UpMove();
			break;
		case 'd':
		case KEY_CODE_RIGHT:RightMove();
			break;
		case 'r':
			if_restart = true; break;
		case KEY_CODE_QUIT:if_exit = true;
			break;
		default:continue;
		}
#endif

		/* 需要时更新最高分 */
		if (score > highest_score)
		{
			highest_score = score;
			FILE *fp = fopen(historyBest, "w");
			if (fp)
			{
				fwrite(&highest_score, sizeof(highest_score), 1, fp);
				fclose(fp);
			}
		}

		/* 默认为需要生成随机数时也同时需要刷新显示,反之亦然 */
		if (if_random == true)
		{
			GenerateRandPosition();
			RefreshBoard();
		}
		else if (if_exit == true)
		{
			RefreshBoard();
		}
		if (if_restart == true)
		{
			RefreshBoard();
		}
	}
}

/* 读取键盘操作符 */
int ReadKeyboard()
{
#ifdef _WIN32
	return _getch();//不回显函数,输入一个字符无需回车直接读入
#else
	int key_code;
	if (read(0, &key_code, 1) < 0)
	{
		return -1;
	}
	return key_code;
#endif
}

//左移
void LeftMove()
{
	int i;
	for (i = 0; i < ROW; ++i)
	{
		// 变量j为列标,变量k为待比较项的列标,循环每行进入判断
		for (int j = 1, k = 0; j < COL; ++j)
		{
			if (board[i][j] > 0) // 找出k后面第一个不为空的列项
			{
				if (board[i][k] == 0) /*k列为空,后面非空直接覆盖*/
				{
					// 情况2:k项为空,则把j项赋值给k项
					/*相当于j方块移动到k方块*/
					board[i][k] = board[i][j];
					board[i][j] = 0;
					if_random = true;
				}
				else if (board[i][k] == board[i][j]) /*k列非空且和后面非空项相等则合并*/
				{
					board[i][k++] *= 2;
					score += board[i][k];
					board[i][j] = 0;
					if_random = true;
				}
				else /*否则,k列后移,让后面非空项覆盖k列*/
				{
					++k;
					board[i][k] = board[i][j];
					if (j != k)
					{
						board[i][j] = 0;
						if_random = true;
					}
				}
			}
		}
	}
}

//右移
void RightMove()
{
	// 仿照左移操作,区别仅仅是j和k都反向遍历 
	for (int i = 0; i < ROW; ++i)
	{
		for (int j = COL - 2, k = COL - 1; j >= 0; --j)
		{
			if (board[i][j] > 0) {
				if (board[i][k] == board[i][j])
				{
					score += board[i][k--] *= 2;
					board[i][j] = 0;
					if_random = true;
				}
				else if (board[i][k] == 0)
				{
					board[i][k] = board[i][j];
					board[i][j] = 0;
					if_random = true;
				}
				else
				{
					board[i][--k] = board[i][j];
					if (j != k)
					{
						board[i][j] = 0;
						if_random = true;
					}
				}
			}
		}
	}
}

//上移
void UpMove()
{
	for (int i = 0; i < COL; ++i)
	{
		for (int j = 1, k = 0; j < ROW; ++j)
		{
			if (board[j][i] > 0)
			{
				if (board[k][i] == board[j][i])
				{
					score += board[k++][i] *= 2;
					board[j][i] = 0;
					if_random = true;
				}
				else if (board[k][i] == 0)
				{
					board[k][i] = board[j][i];
					board[j][i] = 0;
					if_random = true;
				}
				else
				{
					board[++k][i] = board[j][i];
					if (j != k)
					{
						board[j][i] = 0;
						if_random = true;
					}
				}
			}
		}
	}
}

//下移
void DownMove()
{
	for (int i = 0; i < COL; ++i)
	{
		for (int j = ROW - 2, k = ROW - 1; j >= 0; --j)
		{
			if (board[j][i] > 0)
			{
				if (board[k][i] == board[j][i])
				{
					score += board[k--][i] *= 2;
					board[j][i] = 0;
					if_random = true;
				}
				else if (board[k][i] == 0)
				{
					board[k][i] = board[j][i];
					board[j][i] = 0;
					if_random = true;
				}
				else
				{
					board[--k][i] = board[j][i];
					if (j != k)
					{
						board[j][i] = 0;
						if_random = true;
					}
				}
			}
		}
	}
}

//清屏
void ClearScreen()
{
#ifdef _WIN32
	/* 重设光标输出位置清屏*/
	COORD pos = { 0, 0 };  //光标坐标
	SetConsoleCursorPosition(GetStdHandle(STD_OUTPUT_HANDLE), pos);
	CONSOLE_CURSOR_INFO info = { 1, 0 }; //
	SetConsoleCursorInfo(GetStdHandle(STD_OUTPUT_HANDLE), &info);
#else
	printf("\033c");     /* linux下的清屏命令 */
	printf("\033[?25l"); /* linux下的隐藏输入光标 */
#endif
}


/* 结束游戏 */
void EndGame(int signal)
{
#ifdef _WIN32
	system("cls");
	CONSOLE_CURSOR_INFO info = { 1, 1 };
	SetConsoleCursorInfo(GetStdHandle(STD_OUTPUT_HANDLE), &info);
#else
	if (signal == SIGINT)
	{
		printf("\n");
	}
	if (tcsetattr(0, TCSANOW, &old_config) != 0) /* 还原回旧的终端属性 */
		perror("tcsetattr");
	printf("\033[?25h"); /*恢复显示光标*/
#endif
	exit(0);
}



/* 生成随机数 函数定义 */
void GenerateRandPosition()
{
	srand((unsigned int)time(0));
	int n = rand() % GetEmptyCount(); /* 在第n个空位置生成随机数 */
	for (int i = 0; i < ROW; ++i)
	{
		for (int j = 0; j < COL; ++j)
		{
			if (board[i][j] == 0 && n-- == 0)
			{
				board[i][j] = ((rand() % 2 == 0) ? 2 : 4); /* 生成字2或4,生成概率为1:1 */
				return;
			}
		}
	}
}

/* 获取空位置数量 */
int GetEmptyCount()
{
	int n = 0;
	for (int i = 0; i < ROW; ++i)
	{
		for (int j = 0; j < COL; ++j)
		{
			if (board[i][j] == 0)
				++n;
		}
	}
	return n;
}

/* 检测游戏是否结束,如果上下或者左右都不能结合则游戏结束,0和0也是种结合,避免了游戏开始会直接结束*/
void CheckGameOver()
{
	for (int i = 0; i < ROW; ++i)
	{
		for (int j = 0; j < COL - 1; ++j)
		{
			//横向和纵向比较挨着的两个元素是否相等,若有相等则游戏不结束 
			//一方面保证了访问有效性,不会越界;一方面只需遍历一半的表格
			if (board[i][j] == board[i][j + 1] || board[j][i] == board[j + 1][i])
			{
				game_over = false;
				return;
			}
		}
	}
	game_over = true;
}


main.c

#include"2048.h"

int main()
{
	InitGame();
	InitBoard();
	PlayGame();
	EndGame(0);
	return 0;

}

为了便于在Linux下一键编译执行,增加了makefile
make.file

#include"2048.h"

int main()
{
	InitGame();
	InitBoard();
	PlayGame();
	EndGame(0);
	return 0;

}

六、游戏截图

这里写图片描述

最后,有疑问或者不懂的地方欢迎提问,谢谢!

猜你喜欢

转载自blog.csdn.net/HaloTrriger/article/details/82696572
今日推荐