C語言版2048雙平臺遊戲
阿新 • • 發佈:2019-01-11
一、初衷
看到舍友玩這個遊戲,思考了下覺得這個遊戲可以用簡單的程式碼實現,用表格當做介面,用陣列儲存值,讀入玩家操作後重新整理介面顯示給玩家就可以了
二、遊戲特色
- 支援雙平臺(Windows和Linux)
- 可以顯示歷史最高分,可選擇重新開始或退出
- 可自行更改行列大小(巨集:ROW,COL),改大了可能要玩很久才會輸
三、遊戲思路
- 讀入玩家的操作,如果是上下左右中的一個操作,則讓每個元素都進行如此操作。
- 在操作中判斷那些元素需要挪動,那些元素需要合併
- 如果有元素合併了,則在空位置生成隨機數(2、4)
- 重新整理介面,重新顯示格子。
- 如果每個元素都不能進行合併或挪動則結束遊戲。(不能根據是否還有空格子判斷遊戲結束與否)
四、元素挪動或合併(用左移舉例)
- 在每一行,第一列元素作為比較列。
- 如果第一列元素是0,即空位,後面的非空元素填補這個空位
- 如果第一列元素不空,但和後面第一個非空元素相等,相加合併值;且後面那個元素位置置空。比較列後移。
- 否則,比較列後移,用後面的非空元素覆蓋比較列的值。如果當前列和比較列不同,則將比較列值空。
- 迴圈,繼續和比較列元素判斷。
五、程式碼
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;
}