1. 程式人生 > >C語言版2048雙平臺遊戲

C語言版2048雙平臺遊戲

一、初衷

看到舍友玩這個遊戲,思考了下覺得這個遊戲可以用簡單的程式碼實現,用表格當做介面,用陣列儲存值,讀入玩家操作後重新整理介面顯示給玩家就可以了

二、遊戲特色

  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;

}

六、遊戲截圖

這裡寫圖片描述

最後,有疑問或者不懂的地方歡迎提問,謝謝!