C语言小项目练习----2048小游戏 Day13
游戏说明:
棋盘:游戏在一个4×4的网格中进行,共 16 个格子(对应你代码中的
#define SIZE 4和int a[4][4])。初始状态:游戏开始时,系统会随机在网格中生成 2 个数字方块,数值通常是2或4(对应你代码中的
randnumber函数)。目标:通过移动方块,让格子里的数字通过合并,最终拼凑出一个2048的方块即为获胜(对应你代码里
check_game_status中判断a[i][j] == 2048的逻辑)。失败条件:当 16 个格子全部填满,且相邻的格子之间没有任何相同数字可以合并时,游戏结束(对应你代码
check_game_status最后的gameover = 1)。
通过键盘输入W/A/S/D(上下左右)控制所有方块同时向那个方向滑动。每次滑动遵循以下 3 个执行步骤
压缩(靠拢):所有方块向指定方向移动,中间的空白格被填满(代码中的
compress_left/up/right/down)。合并(相加):如果压缩后,同一行/列上出现两个相邻且数值相同的方块,它们就会合并成一个,数值相加(例如
2+2=4,4+4=8)。每次滑动,同一个方块只能参与一次合并(代码中的Merge_left/up/right/down)。补充新方块:每次有效移动(棋盘发生过变化)之后,系统会在空白格子中随机生成一个新方块(代码中的
if(moved) { randnumber(a); ... },正常概率是 90% 生成 2,10% 生成 4)。
一 准备工作
1 定义数组
2 打印网格数组(游戏界面)
//打印网格和数组 void printGame(int a[SIZE][SIZE]) { int i = 0; int j = 0; printf("---------------------\n"); for(i=0;i<SIZE;i++) { for(j=0;j<SIZE;j++) { printf("|"); if(a[i][j] ==0) { printf(" "); }else { printf("%4d",a[i][j]); } } printf("|\n"); printf("---------------------\n"); } }3 生成随机数(游戏开局先生成两个随机数)
//生成随机值 void randnumber(int a[SIZE][SIZE]) { int i = 0; int j = 0; int cnt = 0; for(i=0;i<SIZE;i++) { for(j=0;j<SIZE;j++) { if(a[i][j]==0) cnt++; } } if(cnt==0) { return; } int val = (rand()%10<7)? 2:4; int row; int col; do { row = rand()%4; col = rand()%4; }while(a[row][col]!=0); a[row][col] = val; }二 核心算法
步骤1:压缩操作
把一行里的格子全部推挤到最左边,不留空隙。这是 2048 移动的第一步。
逻辑:利用“双指针(读写指针)”思想。
定义pos = 0(写入位置)。
循环遍历原行row的每个元素(i从 0 到 3)。
只要遇到一个非 0 的数字,就把它“写”到row[pos]的位置,然后把pos加 1。
循环结束后,从pos直到数组末尾,全部填充为0。
效果:[2, 0, 0, 2]经过压缩会变成[2, 2, 0, 0],数字紧紧贴到左边。
//实现压缩操作 void compress_left(int row[SIZE]) { int i = 0; int pos = 0; while(i<SIZE) { if(row[i]!=0) { row[pos] =row[i]; pos++; } i++; } for(int j=pos;j<SIZE;j++) { row[j] = 0; } }步骤2:合并操作
挤压靠拢之后,需要判断有没有相邻且相同的数字,把它们加在一起。
逻辑
先调用
compress_left把数字挤到一侧。使用
for(i=0; i<SIZE-1; i++)遍历(特别注意:只遍历到SIZE-1防止越界)。判断:如果
row[i] == row[i+1]且都不为 0,则:row[i] = 2 * row[i](合并翻倍)。score += row[i](分数累加)。关键:
row[i+1] = 0(把后面的那个数字抹掉,标记为已经被合并)。最后,再次调用
compress_left。因为刚才抹掉了一个数字,中间又产生了空隙,需要再挤压一次。效果:经过完整的
Merge_left后,[2, 2, 2, 0]-> (压缩) ->[2, 2, 2, 0]-> (合并) ->[4, 0, 2, 0]-> (再次压缩) ->最终[4, 2, 0, 0]。
//往左边移动时相同时xiangjia void Merge_left(int row[SIZE]) { compress_left(row); int i; for(i=0;i<SIZE-1;i++) { if(row[i]==row[i+1] && row[i] !=0) { row[i] =2*row[i]; score+=row[i]; row[i+1] = 0; }else{ continue; } } compress_left(row); }步骤3:左移操作
把二维数组的每一行提取到一维数组row,并备份到old_row。对row执行压缩和合并(Merge_left)。比较row和old_row。若有差异,将row写回原二维数组,并标记moved=1。表示发生改变。四行处理完后,若moved==1,调用randnumber生成新方块。用check_game_status判定胜负。check_game_status这个函数在后面会进行介绍
void left_move(int a[SIZE][SIZE]) { int moved = 0; int i = 0; int j=0; int row[SIZE]; int old_row[SIZE]; for(i=0;i<SIZE;i++) { for(j=0;j<SIZE;j++) { row[j] = a[i][j]; old_row[j] = a[i][j]; } Merge_left(row); int change = 0; for(j=0;j<SIZE;j++) { if(row[j]!=old_row[j]) { change = 1; break; } } if(change) { for(j=0;j<SIZE;j++) { a[i][j] = row[j]; } moved=1; } } if(moved) { randnumber(a); check_game_status(a); } }右移与左移算法类似,就是换了个方向,不做赘述
步骤4:右移的压缩
//压缩 void compress_right(int row[SIZE]) { int i = SIZE-1; int pos = SIZE-1; while(i>=0) { if(row[i]!=0) { row[pos] =row[i]; pos--; } i--; } for(int j=0;j<=pos;j++) { row[j] = 0; } }步骤5:
int Merge_right(int row[SIZE]) { compress_right(row); int i; for(i=SIZE-1;i>0;i--) { if(row[i]==row[i-1] && row[i] !=0) { row[i] =2*row[i]; score+=row[i]; row[i-1] = 0; }else{ continue; } } compress_right(row); }步骤7:
void right_move(int a[SIZE][SIZE]) { int moved = 0; int i = 0; int j=0; int row[SIZE]; int old_row[SIZE]; for(i=0;i<SIZE;i++) { for(j=0;j<SIZE;j++) { row[j] = a[i][j]; old_row[j] = a[i][j]; } Merge_right(row); int change = 0; for(j=0;j<SIZE;j++) { if(row[j]!=old_row[j]) { change = 1; break; } } if(change) { for(j=0;j<SIZE;j++) { a[i][j] = row[j]; } moved=1; } } if(moved) { randnumber(a); check_game_status(a); } }上移操作会可以将列提取出来,当作行来看,此时的上移就相当于左移,所以可以直接套用左移的压缩 合并的算法,完成压缩合并之后将行又转化为列,就可以完成上移
步骤8:上移压缩
//上移压缩 void compress_up(int col[SIZE]) { int j = 0; int pos = 0; while(j<SIZE) { if(col[j]!=0) { col[pos] =col[j]; pos++; } j++; } for(int i=pos;i<SIZE;i++) { col[i] = 0; } }步骤9:上移合并
//上移合并 void Merge_up(int col[SIZE]) { compress_up(col); int j; for(j=0;j<SIZE-1;j++) { if(col[j]==col[j+1] && col[j] !=0) { col[j] =2*col[j]; score+=col[j]; col[j+1] = 0; }else{ continue; } } compress_up(col); }步骤10:上移
//上移 void up_move(int a[SIZE][SIZE]) { int moved = 0; int i = 0; int j=0; int col[SIZE]; int old_col[SIZE]; for(j=0;j<SIZE;j++) { for(i=0;i<SIZE;i++) { col[i] = a[i][j]; //将要处理的数组中的列提取出来 old_col[i] = a[i][j]; } Merge_up(col); int change = 0; for(i=0;i<SIZE;i++) { if(col[i]!=old_col[i]) { change = 1; break; } } if(change) { for(i=0;i<SIZE;i++) { a[i][j] = col[i]; } moved=1; } } if(moved) { randnumber(a); check_game_status(a); } }步骤11:下移的合并(同上,上移用左移的压缩合并算法,下移就可以用有移的压缩合并算法)
//下移压缩 void compress_down(int col[SIZE]) { int i = SIZE-1; int pos = SIZE-1; while(i>=0) { if(col[i]!=0) { col[pos] =col[i]; pos--; } i--; } for(int j=0;j<=pos;j++) { col[j] = 0; } } //右移动 int Merge_down(int col[SIZE]) { compress_down(col); int i; for(i=SIZE-1;i>0;i--) { if(col[i]==col[i-1] && col[i] !=0) { col[i] =2*col[i]; score+=col[i]; col[i-1] = 0; }else{ continue; } } compress_down(col); } //向下移动 void down_move(int a[SIZE][SIZE]) { int moved = 0; int i = 0; int j=0; int col[SIZE]; int old_col[SIZE]; for(j=0;j<SIZE;j++) { for(i=0;i<SIZE;i++) { col[i] = a[i][j]; old_col[i] = a[i][j]; } Merge_down(col); int change = 0; for(i=0;i<SIZE;i++) { if(col[i]!=old_col[i]) { change = 1; break; } } if(change) { for(i=0;i<SIZE;i++) { a[i][j] = col[i]; } moved=1; } } if(moved) { randnumber(a); check_game_status(a); } }步骤12:检查游戏状态
第一环节:检查是否胜利(达到 2048)
遍历整个二维数组。如果发现有任何一个格子的值等于2048,就将全局胜利标志win设为1。这里使用了break;仅跳出内层j的循环,外层i仍会继续走完。但因为win已经为 1,后续的检查环节对于最终结果已经没有影响了(主循环会因!win为假而结束)。
2. 第二环节:检查是否还有“空位”
再次遍历整个数组。如果发现有a[i][j] == 0(即空白格子),直接return结束这个函数。return意味着“当前还没输,还有回旋余地”,直接告诉主程序继续游戏。
3. 第三环节:检查“横向”相邻数字是否相同
遍历数组,检查同一行中,相邻左右两个格子a[i][j]和a[i][j+1]的值是否相同。如果发现相同的,说明还可以进行横向合并,还有活路,直接return结束函数。游戏继续。
4. 第四环节:检查“纵向”相邻数字是否相同
遍历数组,检查同一列中,相邻上下两个格子a[j][i]和a[j+1][i]的值是否相同。如果发现相同的,说明还可以进行纵向合并,有路可走,直接return结束函数。
5. 最终判定:触发游戏结束
如果函数执行到了这里,意味着上面 4 个判断全部都没有触发return。满盘(没有 0)、没有横向相同、也没有纵向相同。这是一个死局。于是设置gameover = 1,主循环检测到!gameover为假,游戏宣告失败并退出。
void check_game_status(int a[SIZE][SIZE]) { int i=0; int j=0; for(i=0;i<SIZE;i++) { for(j=0;j<SIZE;j++) { if(a[i][j]==2048) { win = 1; break; } } } for(i=0;i<SIZE;i++) { for(j=0;j<SIZE;j++) { if(a[i][j]==0) { return; } } } for(i=0;i<SIZE;i++) { for(j=0;j<SIZE;j++) { if(a[i][j]==a[i][j+1]) { return; } } } for(i=0;i<SIZE;i++) { for(j=0;j<SIZE;j++) { if(a[j][i]==a[j+1][i]) { return; } } } gameover = 1; }步骤13:主函数
1 .初始化设置
(1)int a[4][4]={0};:定义一个 4x4 的二维数组作为棋盘,并将所有元素初始化为0(表示空格)。
(2)srand((unsigned int)time(NULL));:使用当前时间作为随机数种子。这是为了确保每次运行程序时,系统生成的方块位置都是随机不重复的。
2.开局布阵
(1)randnumber(a);(执行第一次):在空棋盘上,随机生成第一个数字方块(2 或 4)。
(2)randnumber(a);(执行第二次):在空棋盘上,再随机生成第二个数字方块。(标准 2048 规则:开局一定有两个初始数字)。
3.主游戏循环
进入
(1)while(!win && !gameover)循环。只要没有赢且没有输,游戏就会一直卡在这个死循环里:printGame(a);:先把当前的棋盘状况渲染打印到屏幕上。(2)char ch = getch();挂起等待,直到玩家按下键盘上的某个键。(3)switch(ch):根据按下的键执行不同的分支。(4)(注意:在移动函数内部,会触发随机生成新方块randnumber,以及检查游戏状态check_game_status更改win或gameover的值)。(5)按s/S-> 调用down_move(a)执行下移计算。(6)按w/W-> 调用up_move(a)执行上移计算。(7)按d/D-> 调用right_move(a)执行右移计算。(8)按a/A-> 调用left_move(a)执行左移计算。
4.结束退出
(1)一旦在上述某个移动操作中,check_game_status发现了a[i][j] == 2048并把win改成1,或者发现了死局并把gameover改成1,while循环的条件就不满足了,循环立刻中断。
(2)return 0;:程序正常结束退出。
int main(int argc,const char *argv[]) { int a[4][4]={0}; srand((unsigned int)time(NULL)); randnumber(a); randnumber(a); while(!win && !gameover) { printGame(a); char ch = getch(); switch(ch) { case 'a': case 'A': left_move(a); break; case 'D': case 'd': right_move(a); break; case 'w': case 'W': up_move(a); break; case 's': case 'S': down_move(a); break; } } return 0; }写到这里,游戏功能就完成的差不多了。测试之后发现每次打印网格时,屏幕会往下滚动,而不是在原位置覆盖,影响游戏体验,需要将光标移回屏幕左上角。
如何实现这个操作?
------------先添加 printf("\033[2J");
------------添加 printf("\033[H");
运行结果:
