一、初衷
看到舍友玩这个游戏,思考了下觉得这个游戏可以用简单的代码实现,用表格当做界面,用数组保存值,读入玩家操作后刷新界面显示给玩家就可以了
二、游戏特色
- 支持双平台(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 historyBestScorePath[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(historyBestScorePath, "%sbestScore.dat", CurDir);
#else
/* 获取游戏存档路径,Linux下放在当前用户主目录下 */
char CurDir[MAX_PATH];
getcwd(CurDir, MAX_PATH);
sprintf(historyBestScorePath, "%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(historyBestScorePath, "r");
if (fp)
{
fread(&highest_score, sizeof(highest_score), 1, fp);
fclose(fp);
}
else
{
highest_score = 0;
fp = fopen(historyBestScorePath, "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')
{
game_over = false;
InitGame();
InitBoard();
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(historyBestScorePath, "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[i][j] == board[i + 1][j] || board[i][j]==0)
{
game_over = false;
return;
}
}
}
game_over = true;
}
main.c
#include"2048.h"
int main()
{
InitGame();
InitBoard();
PlayGame();
EndGame(0);
return 0;
}
makefile文件
main:main.c 2048.c
gcc main.c 2048.c -o main
clean:
rm -f main