C语言小结---2048游戏实现

C语言小结---2048游戏实现

游戏核心逻辑实现

一行数字向左合并

void merge_left(int *p) 71 { 72 int i=0; 73 int j=0; 74 for(i=0;i<4;i++) 75 { 76 if(p[i]!=0) 77 { 78 for(j=i+1;j<4;++j) 79 { 80 if(p[i]==p[j]) //匹配相同值,合并 81 { 82 p[i]*=2; 83 p[j]=0; 84 break; //合并一次退出,避免双重合并 85 }else if(p[j]!=0) //碰到非0且不相同值,说明此数不可能合并,退出循环避免跨数合并 86 { 87 break; 88 } //p【j】等于0,j++继续找 89 } 90 } 91 } 92 }

从左边p[0]开始向右寻找与p[0]相同数字,0值跳过,遇到非0且不等的数说明此数无法合并,直接退出后看p[1]依次查看下去

一行数字向左移动

94 void compress_left(int p[]) 95 { 96 int i=0; 97 int j=0; 98 for(i=0;i<4;++i) 99 { 100 if(p[i]!=0) 101 { 102 for(j=i-1;j>=0;--j) 103 { 104 if(p[j]==0) 105 { 106 p[j]=p[i]; 107 p[i]=0; 108 --i; 109 } 110 } 111 } 112 } 113 }

通过i外层循环寻找非0数,找到之后同过内层循环让非0数向左移动(非0数左边是0的情况下),通过--i让p[i]逐渐“冒泡”到左边

通过ai,获得一个更高效且符合规范的算法(上述算法在循环内改变了条件值i)

void compress_left(int p[], int n) { int write = 0; // 下一个非零元素要写入的位置 for (int read = 0; read < n; ++read) { if (p[read] != 0) { p[write++] = p[read]; } } // 剩余位置补零 while (write < n) { p[write++] = 0; } }

此算法通过双指针寻找非0值和0位,巧妙处在于即使p[0]有值,也只是p[0]赋值给p[0]。write每次写入后自动+1(先用后加)
read只遍历一遍数组,而且只赋值一次而不消除原处的数据,所以write之后的数据需要清零

数组向左移动

115 void left_move(int p[][4]) 116 { 117 int i; 118 for(i=0;i<4;++i) 119 { 120 merge_left(&p[i][0]); 121 compress_left(&p[i][0]); 122 } 123 }

通过函数复用轻松完成了整个数组的左移

数组向右移动

易得将数组左右翻转之后向左移动再翻转可以获得向右移动的效果

125 void reverse_row(int m[4]) 126 { 127 int row[4]={}; 128 int i=0; 129 for(i=0;i<4;++i) 130 { 131 row[i]=m[3-i]; 132 } 133 for(i=0;i<4;++i) 134 { 135 m[i]=row[i]; 136 } 137 }

翻转数组(一行)

void right_move(int p[][4]) 140 { 141 int i; 142 for(i=0;i<4;++i) 143 { 144 reverse_row(&p[i][0]); 145 merge_left(&p[i][0]); 146 compress_left(&p[i][0]); 147 reverse_row(&p[i][0]); 148 } 149 }

循环同样是对一行数据进行操作的,所以通过翻转实现了右移
重写向右合并与向右移动函数虽然已有前例对照,但是实现代码的效率还是不如翻转好

数组向上移动

有了以上经验,我们知道可以将数组改造成便于向左一行一行移动的结构即可

151 void up_move(int p[][4]) 152 { 153 int colmn[4]; 154 int i,j; 155 for(i=0;i<4;++i) 156 { 157 for(j=0;j<4;j++) 158 { 159 colmn[j]=p[j][i]; 160 } 161 merge_left(colmn); 162 compress_left(colmn); 163 for(j=0;j<4;++j) 164 { 165 p[j][i]=colmn[j]; 166 } 167 } 168 } 169

通过外循环(i)内部的两次循环(j),一次外循环就将一列数据取出、移动然后填回

数组向下移动

170 void down_move(int p[][4]) 171 { 172 int colmn[4]; 173 int i,j; 174 for(i=0;i<4;++i) 175 { 176 for(j=0;j<4;++j) 177 { 178 colmn[j]=p[j][i]; 179 } 180 reverse_row(colmn); 181 merge_left(colmn); 182 compress_left(colmn); 183 reverse_row(colmn); 184 for(j=0;j<4;++j) 185 { 186 p[j][i]=colmn[j]; 187 } 188 } 189 }

与向上移动类型,添加了两次翻转

判断失败或者胜利

void check_game_status(int p[][4]) 214 { 215 win=0; 216 int i,j; 217 for(i=0;i<4;++i) 218 { 219 for(j=0;j<4;++j) 220 { 221 if(p[i][j]==2048) 222 { 223 win=1; 224 return; //游戏胜利 225 } 226 } 227 } 228 for(i=0;i<4;++i) 229 { 230 for(j=0;j<4;++j) 231 { 232 if(p[i][j]==0) 233 { 234 return; //游戏继续 235 } 236 } 237 } 238 for(i=0;i<4;++i) 239 { 240 for(j=0;j<4;++j) 241 { 242 if(j==3 && i!=3) 243 { 244 if(p[i][j]==p[i+1][j]) 245 { 246 return; //游戏继续 247 } 248 }else if(i==3 && j!=3) 249 { 250 if(p[i][j]==p[i][j+1]) 251 { 252 return; //游戏继续 253 } 254 }else if(i!=3 && j!=3) 255 { 256 if(p[i][j]==p[i][j+1]) 257 { 258 return; //游戏继续 259 } 260 if(p[i][j]==p[i+1][j]) 261 { 262 return; //游戏继续 263 } 264 } 265 266 } 267 } 268 game_over=1; 269 }

优先判断是否有2048(胜利条件),防止2048出现后游戏继续
有空格直接判断游戏继续
无空格则判断是否有相同且相邻的数,有则继续,无则结束
我的判断相同相邻值算法略显复杂,插入一个更优秀的算法

for (int i = 0; i < 4; ++i) { for (int j = 0; j < 4; ++j) { // 检查右邻居(j+1 < 4) if (j + 1 < 4 && p[i][j] == p[i][j + 1]) return; // 检查下邻居(i+1 < 4) if (i + 1 < 4 && p[i][j] == p[i + 1][j]) return; } }

算法原理相同,我为了防止数组越界写的较为复杂,这是一种更简易且易理解的算法

对数组初始化

37 void init(int (*p)[4]) 38 { 39 srand(time(NULL)); 40 int init_val1,init_val2; 41 memset(p,0,sizeof(int[4][4])); 42 if(rand()%10==1) 43 { 44 init_val1=4; 45 }else 46 { 47 init_val1=2; 48 } 49 if(rand()%10==1) 50 { 51 init_val2=4; 52 }else 53 { 54 init_val2=2; 55 } 56 p[rand()%4][rand()%4]=init_val1; 57 int flage=1; 58 while(flage) 59 { 60 int n=rand()%4; 61 int m=rand()%4; 62 if(p[n][m]==0) 63 { 64 flage=0; 65 p[n][m]=init_val2; 66 } 67 } 68 }

初始化一局游戏只需要一次,所以这里顺便取出随机种子值(种子值一次运行只能取一次,否则时间戳相同,第一个随机数也是相同的)
这里通过对10求余,概率取得(0~9)十个结果,所以初始值有10%的概率为4,90%的概率为2
再通过flag检测是否两个初始值填入一个位置

填入随机值

10 void generate_random(int (*p)[4]) 11 { 12 int i,j; 13 int q[16][2]={}; 14 int t=0; 15 for(i=0;i<4;++i) 16 { 17 for(j=0;j<4;++j) 18 { 19 if(p[i][j]==0) 20 { 21 q[t][0]=i; 22 q[t][1]=j; 23 t++; 24 } 25 } 26 } 27 if(t==0) 28 return; 29 int r=rand()%t; 30 if(rand()%10>=7) 31 { 32 p[q[r][0]][q[r][1]]=4; 33 }else 34 { 35 p[q[r][0]][q[r][1]]=2; 36 } 37 }

创建一个数组存储所有空白格的行列坐标
从t个空白格随机选择一个
以 30% 的概率生成 4,70% 的概率生成 2