一、背景
上一篇文章介绍了readline的基本用法,这一篇通过readline读取按键对2048游戏的c语言实现;
二、思路
2.1 游戏介绍
关于2048游戏,网上找了一下介绍,你可以点击这里在线进行体验:
"《2048》的初始数字则是由2+2组成的基数4。在操作方面的不同则表现为一步一格的移动,变成更为爽快的一次到底。
相同数字的方况在靠拢、相撞时会相加。系统给予的数字方块不是2就是4,玩家要想办法在这小小的16格范围中凑出「2048」这个数字方块。"
看似简单的4x4方格游戏,梳理规则的时候发现还是比较繁琐的;
2.2 移动、合并规则
以向右移动为例,向右移动时,按照行(row)为单位各自进行处理:
- 移动完成后,需要保持有数字的方块都移动到最右侧,.eg: (0204)->(0024) / (2004)->(0024) / (2400)->(0024);
- 相邻的同等值方块进行一次合并动作,.eg: (0022)->(0004) / (0222) -> (0024) / (2222) -> (0044) / (2244) -> (0048);
- 相邻的意思为非零方块的相邻,即中间夹杂的零方块还需进行移动消除,.eg:(0202) -> (0004) / (2024) -> (0044);
- 移动、合并之后,在最左列某个零方块上产生一个2或4的新值,其中2出现的概率是4的两倍;
2.3 胜利、失败的界定
胜利则查找数组,若出现某个方格值大于等于2048则表示胜利;
失败的界定得符合以下两个条件:
- 4x4 数组方格都被填满,没有零值方格;
- 4x4 数组方格无法再相互合并,即相邻方块值各不相等;
3 实现
参考了文章[1]的思路,通过另外一个切入点,根据第二小节的描述,对移动、合并画出处理流程图如下:
开始代码的实现说明,先是定义了一个4x4的全局数组;
#define SIZE 4
static int grid[SIZE][SIZE] = {
{0,0,0,0},
{0,0,0,0},
{0,0,0,0},
{0,0,0,0},
};
定义了通用的移动、合并的宏,方便后续操作;
#define __move_a2b(a, b) if ( (a) != 0 ) { \
printf("Move %d to %d\n", (a), (b)); \
(b) = (a); \
(a) = 0; \
}
#define __merge_a2b(a, b) if ( (b) != 0 ) { \
printf("Merge %d to %d\n", (a), (b)); \
(b) *= 2; \
(a) = 0; \
}
#define __try_merge_a2b(a, b) if ( (b) == (a) ) { \
__merge_a2b(a, b); \
}
然后是对应流程图的右移函数的实现,(左移、上移、下移类似)
#define __for_each(i, n) for ( i = 0, n = random() % SIZE; i < SIZE; i++, n = (n + 1) % SIZE )
int grid_move_right()
{
int ix = 0, jx = 0, row = 0, col = 0;
printf("right\n");
/* Move */
for ( row = 0; row < SIZE; row++ ) {
for ( col = SIZE - 1; col > 0; col-- ) {
/* Fill zero by pulling */
for ( ix = 0; ix < col && grid[row][col] == 0; ix++ ) {
for ( jx = col; jx > 0; jx-- ) {
__move_a2b(grid[row][jx - 1], grid[row][jx]);
}
}
for ( ix = 0; ix < col - 1 && grid[row][col - 1] == 0; ix++ ) {
for ( jx = col - 1; jx > 0; jx-- ) {
__move_a2b(grid[row][jx - 1], grid[row][jx]);
}
}
__try_merge_a2b(grid[row][col - 1], grid[row][col]);
}
}
/* Generate one random cells in col[0] */
if ( is_won() || is_over() ) {
return;
}
__for_each(ix, row) {
if ( grid[row][0] == 0 ) {
grid[row][0] = __new_cell();
break;
}
}
grid_display(row, 0);
}
int is_won()
{
int ix = 0, jx = 0;
for ( ix = 0; ix < SIZE; ix++ ) {
for ( jx = 0; jx < SIZE; jx++ ) {
if ( grid[ix][jx] >= 2048 ) {
grid_display(ix, jx);
printf("[ You won! ]\n");
return 1;
}
}
}
return 0;
}
int is_over()
{
int ix = 0, jx = 0;
for ( ix = 0; ix < SIZE; ix++ ) {
for ( jx = 0; jx < SIZE; jx++ ) {
if ( ix < SIZE - 1 && grid[ix][jx] == grid[ix + 1][jx] ) {
return 0;
}
if ( jx < SIZE - 1 && grid[ix][jx] == grid[ix][jx + 1] ) {
return 0;
}
if ( grid[ix][jx] == 0 ) {
return 0;
}
}
}
printf("[ Game over! ]\n");
return 1;
}
如果游戏还没结束,那么还得产生一个新的随机块,随机函数实现如下:
/* 66.66% is 2; 33.33% is 4 */
#define __new_cell() \
(random() & 0x3 ? 2 : 4)
然后对应的显示函数,为了显示方便,对新产生的方块进行黄颜色标记处理
char *grid_generate(char *buffer, size_t size, int row, int col)
{
int ix = 0;
int jx = 0;
int len = 0;
#define __do_append(buffer, len, size, args...) do { \
len += snprintf(buffer + len, size - len, args); \
} while(0);
__do_append(buffer, len, size, "+----+----+----+----+\n");
for ( ix = 0; ix < SIZE; ix++ ) {
for ( jx = 0; jx < SIZE; jx++ ) {
if ( grid[ix][jx] ) {
if ( ix == row && jx == col ) {
__do_append(buffer, len, size, "|\033[40;33m%4d\033[0m", grid[ix][jx]);
}
else {
__do_append(buffer, len, size, "|%4d", grid[ix][jx]);
}
}
else {
__do_append(buffer, len, size, "| ");
}
}
__do_append(buffer, len, size, "|\n");
__do_append(buffer, len, size, "+----+----+----+----+\n");
}
return buffer;
}
void grid_display(int row, int col)
{
char line[2048] = {0};
fprintf(stderr, "%s", grid_generate(line, sizeof(line), row, col));
}
核心函数讲完了,最后说一下主函数,本例使用的readline库rl_bin_key()函数对按键进行绑定操作,
上下左右分别为(WSAD、KJHL),绑方向键的方法还没有找到;
// gcc -g -o 2048 2048.c -lreadline
int main(int argc, char *argv[])
{
char *pline = NULL;
char prompt[2048] = {0};
grid_init();
rl_bind_key('h', grid_move_left);
rl_bind_key('j', grid_move_up);
rl_bind_key('k', grid_move_down);
rl_bind_key('l', grid_move_right);
rl_bind_key('d', grid_move_left);
rl_bind_key('w', grid_move_up);
rl_bind_key('s', grid_move_down);
rl_bind_key('a', grid_move_right);
while ( 1 ) {
pline = readline("");
if ( !pline ) {
break;
}
free(pline);
}
return EXIT_SUCCESS;
}
四、结果分析
运行成功、失败的结果如下:
上手发现一次成功的难度较大,需后续增加回滚功能减低难度,要不一步走错基本gg
然后在显示方面可以优化,比如说每次都清屏再重新打印,并增加一些提示功能...
参考文章:
[1] http://www.cnblogs.com/judgeyoung/p/3760515.html
[2] https://github.com/gabrielecirulli/2048