demo-2048-c

程序设计语言:C语言
游戏:2048
玩法:用WSAD控制上下左右方向,按Q退出游戏;
当两个相同数字方块发生碰撞,它们会累加起来成为一个新的数字方块,出现数字方块为2048时即可通关
源代码地址: https://github.com/gitforhzc/demo-2048-c
编译源代码,运行demo./demo

Framwork

  • include
  • define
  • implement
  • main

design

include

  • stdafx.h # pre-compile include file

define

  • demo2048c.h
    • declare global variable
    • declare global function

implement

1
2
3
4
5
6
7
8
9
10
11
12
// file: stdafx.h
#ifndef _STDAFX_H

#include <curses.h>
#include <stdio.h>
#include <stdlib.h> // for srand() rand() exit()
#include <string.h> // for strcpy()
#include <time.h> // for time()
#include <unistd.h> // for sleep()

#define _STDAFX_H
#endif
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// file: demo2048c.h
#ifndef _DEMO2048C_H

/*
global variable
*/
// 4*4方格
int a[4][4];
// 方格里空格的个数, 标记已移动
int empty, moved;
// 标记退出情况:初始值0 游戏中,1 退出, 2 输, 3 赢
enum {playing, quit, lose, win} endflag;

/*
global function
*/
void init();
void add_number();
void draw();
void draw_one(int y, int x);
void play();
void left();
void right();
void up();
void down();
int game_over();
int test_lose();
void draw_over();

#define _DEMO2048C_H
#endif

main

1
2
3
4
5
6
7
8
9
10
11
// file: main.c
#include "stdafx.h"
#include "demo2048c.h"
int a[4][4] = {0};
int main()
{
init();
play();

return 0;
}

coding

编程思想:模块化
// file: demo2048c.c

#include “stdafx.h”

#include “demo2048c.h”

init()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// init 函数首先初始化屏幕,在方格中随机生成两个位置并放入随机数字2,4。
void init()
{
initscr();
cbreak();
noecho();
curs_set(0);

empty = sizeof(a) / sizeof(a[0][0]);
srand(time(0));
add_number();
add_number();
draw();

}

srand() — why call it only once?

add_number()

初始化数字和生成新数字整合成这一个函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// add_number 函数用于生成随机数
void add_number() {
int new_x, new_y, tmp;
// 随机空位置
do {
new_x = rand() % 4;
new_y = rand() % 4;
} while (a[new_y][new_x] != 0);

// 随机2,4数字
do {
tmp = rand() % 4;
} while (tmp == 0 || tmp == 2);
a[new_y][new_x] = tmp + 1;
empty--;
}

draw()

关键点在于横线和竖线for循环的对称性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// draw绘制方格和数字
void draw() {
int n, m, x, y;

clear();
for (n = 0; n < 9; n += 2) //横线
for (m = 0; m < 21; m++) {
move(n, m);
addch('-');
refresh();
}
for (m = 0; m < 22; m += 5) //竖线
for (n = 1; n < 8; n++) {
move(n, m);
addch('|');
refresh();
}
for (y = 0; y < 4; y++) //数字
for (x = 0; x < 4; x++) {
draw_one(y, x);
if (a[y][x] == 2048) { // 胜利条件,出现2048
endflag = win;
}
}
}

draw_one(int y, int x)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// draw_one 函数用于绘制单个数字
void draw_one(int y, int x)
{
int i, m, k, j;
char c[4] = {'0', '0', '0', '0'}; // 个位、十位、百位、千位

i = a[y][x];
m = 0;
// 提取各位数到c[0-3]
do {
j = i % 10;
c[m++] = j + '0';
i = i / 10;
}while(i > 0);

//输出c[0-3]到恰当的位置
m = 0;
k = (x + 1) * 5 - 1;
while(c[m] != '0') {
move(2*y+1, k);
addch(c[m++]);
k--;
}
}

play()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
// 用 wsad 来表示上下左右方向
void play() {

int old_empty;
char ch;

while (1) {
moved = 0;
old_empty = empty;

ch = getch();
switch (ch) {
case 'A':
case 'a':
left();
break;
case 'D':
case 'd':
right();
break;
case 'W':
case 'w':
up();
break;
case 'S':
case 's':
down();
break;
case 'Q':
case 'q':
endflag = quit;
break;
default:
continue;
}

draw(); // 刷新

// 游戏失败
if (empty <= 0 && test_lose()) {
endflag = lose;
}

// 判断游戏结束条件
if (endflag != playing) {
game_over();
}

//生成新方块
if ((empty != old_empty) ||
(moved == 1)) { //修复了不移动或不消除方块也生成新方块的bug
add_number();
draw();
}
}
}

left()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
void left() {
int x, y, i;
//从左向右消去相同方块
for (y = 0; y < 4; y++)
for (x = 0; x < 4;) {
if (a[y][x] == 0) {
x++;
continue;
} else {
for (i = x + 1; i < 4; i++) {
if (a[y][i] == 0) {
continue;
} else {
if (a[y][x] == a[y][i]) {
a[y][x] += a[y][i];
a[y][i] = 0;
x = i + 1;
empty++;
break;
} else {
x = i;
break;
}
}
}
x = i;
}
}
//向左移动方块
for (y = 0; y < 4; y++)
for (x = 0; x < 4; x++) {
if (a[y][x] == 0) {
continue;
} else {
for (i = x; (i > 0) && (a[y][i - 1] == 0); i--) {
a[y][i - 1] = a[y][i];
a[y][i] = 0;
moved = 1;
}
}
}
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
void right() {
int x, y, i;
//从右向左消去相同方块
for (y = 0; y < 4; y++)
for (x = 3; x >= 0;) {
if (a[y][x] == 0) {
x--;
continue;
} else {
for (i = x - 1; i >= 0; i--) {
if (a[y][i] == 0) {
continue;
} else if (a[y][x] == a[y][i]) {
a[y][x] += a[y][i];
a[y][i] = 0;
x = i - 1;
empty++;
break;
} else {
x = i;
break;
}
}
x = i;
}
}
//向右移动方块
for (y = 0; y < 4; y++)
for (x = 3; x >= 0; x--) {
if (a[y][x] == 0) {
continue;
} else {
for (i = x; (i < 3) && (a[y][i + 1] == 0); i++) {
a[y][i + 1] = a[y][i];
a[y][i] = 0;
moved = 1;
}
}
}
}

up()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
void up() {
int x, y, i;
//从上向下消去相同方块
for (x = 0; x < 4; x++)
for (y = 0; y < 4;) {
if (a[y][x] == 0) {
y++;
continue;
} else {
for (i = y + 1; i < 4; i++) {
if (a[i][x] == 0) {
continue;
} else if (a[y][x] == a[i][x]) {
a[y][x] += a[i][x];
a[i][x] = 0;
y = i + 1;
empty++;
break;
} else {
y = i;
break;
}
}
y = i;
}
}
//向上移动方块
for (x = 0; x < 4; x++)
for (y = 0; y < 4; y++) {
if (a[y][x] == 0) {
continue;
} else {
for (i = y; (i > 0) && (a[i - 1][x] == 0); i--) {
a[i - 1][x] = a[i][x];
a[i][x] = 0;
moved = 1;
}
}
}
}

down()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
void down() {
int x, y, i;
//从下向上消去相同方块
for (x = 0; x < 4; x++)
for (y = 3; y >= 0;) {
if (a[y][x] == 0) {
y--;
continue;
} else {
for (i = y - 1; i >= 0; i--) {
if (a[i][x] == 0) {
continue;
} else if (a[y][x] == a[i][x]) {
a[y][x] += a[i][x];
a[i][x] = 0;
y = i - 1;
empty++;
break;
} else {
y = i;
break;
}
}
y = i;
}
}
//向下移动方块
for (x = 0; x < 4; x++)
for (y = 3; y >= 0; y--) {
if (a[y][x] == 0) {
continue;
} else {
for (i = y; (i < 3) && (a[i + 1][x] == 0); i++) {
a[i + 1][x] = a[i][x];
a[i][x] = 0;
moved = 1;
}
}
}
}

test_lose()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 判断矩阵各相邻数字是否相同,若都不同,则游戏失败
int test_lose() {
int y, x, j, i;
for (y = 0; y < 3; y++)
for (x = 0; x < 3; x++)
if (a[y][x] == a[y + 1][x] || a[y][x] == a[y][x + 1]) {
return 0;
}
x = 3;
for (y = 0; y < 3; y++)
if (a[y][3] == a[y + 1][3]) {
return 0;
}
y = 3;
for (x = 0; x < 3; x++)
if (a[3][x] == a[3][x + 1]) {
return 0;
}

return 1;
}

game_over()

1
2
3
4
5
6
7
8
// 游戏结束子函数
int game_over() {

draw_over();
sleep(1);
endwin();
exit(0);
}

draw_over()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// 游戏结束后绘制结束提醒
void draw_over() {
int y = 9, x = 0;
char output[30];

switch (endflag) {
case playing:
strcpy(output, "something wrong! ");
break;
case lose:
strcpy(output, "You lose! ");
break;
case win:
strcpy(output, "You win! ");
break;
case quit:
strcpy(output, "");
break;
}
strcat(output, "Enter any key to quit.");

for (int i = 0; output[i] != '\0'; i++, x++) {
move(y, x);
addch(output[i]);
}
refresh();
char chtmp = getch();
}

testing

caution: compile

我们用了一个文本界面的屏幕绘图库 ncurses,编译时需要加上 -lcurses 选项

1
sudo apt-get install libncurses5-dev

编译

1
gcc demo2048.c main.c -o demo -lcurses

error: re-define

1
2
/home/hzc/demo-2048-c/main.c:12: `a'被多次定义
/tmp/cctDvNiU.o:/home/hzc/demo-2048-c/demo2048c.c:11:第一次在此定义

solution: declare int a[4][4]; on demo2048c.h, define int a[4][4] = {0};on int main()

bonus

MarkDown-jump

markdown实现页内跳转:[](#jump)
[]内可任意文本,点击即跳转到jump处,jump必须是标题级纯英文字母

vim-ctags

ctags -R # 生成tags文件
ctrl+] 跳转到函数定义处
ctrl+T 回到函数被调用处

gdb-tty

tty命令可以指写输入输出的终端设备。如:tty /dev/ttyb
tty # echo /dev/pts/8
gdb demo-2048-c -tty /dev/pts/8

ncurses

[1] ncurses 文字模式下的特殊效果與遊標控制

curses 是一個建構於 termcap/terminfo 之上的通用程式庫; ncurses 是 curses 的改良版本

  • 在 C 程式最前面, 要 #include <curses/curses.h> 並且在連結時要加 -lncurses
  • 所有使用 ncurses 的程式, 一開始都要 initscr. 這會設定 ncurses 程式庫的初始狀態, 並且把程式執行之前的螢幕存起來.
  • 所有使用 ncurses 的程式, 結束時都要 endwin. 這會讓 ncurses 程式庫做一些撿場的動作, 例如把螢幕恢復成程式執行之前的狀態.
  • 為了提高執行效率, ncurses 中的大部分副程式所製造的效果, 都不是馬上會顯現出來的, 而是等到程式設計師呼叫 refresh 時才把先前所有指定的效果一次顯現出來.
  • cbreak 可讓終端機進入 “立即反應狀態”: 使用者只要敲一個鍵 (而不必按 Enter 鍵) 程式立即可以看到該字元 (透過 getch). 另一方面, noecho 可避免終端機當下就印出使用者敲進去的鍵, 而讓程式設計師自行安排在其他地方或時間才印出來 (甚或不印或印其他字串)

[2] FreeBSD Man Pages for ncurses

API manual

  • int curs_set(int visibility);
    The curs_set routine sets the cursor state is set to invisible, normal,
    or very visible for visibility equal to 0, 1, or 2 respectively.
  • int move(int y, int x);
    These routines move the cursor associated with the window to line y and column x.
    This routine does not move the physical cursor of the terminal until
    refresh is called. The position specified is relative to the
    upper left-hand corner of the window, which is (0,0).
  • int addch(const chtype ch);
    The addch, waddch, mvaddch and mvwaddch routines put the character ch
    into the given window at its current window position, which is then advanced.

update log

This project is based on https://www.shiyanlou.com/courses/running

  • code refactoring
  • integrate new number into one function, introduce add_number()
  • add game over warning, introduce draw_over()
  • add endflag,when to quit, lose or win
  • add lose, introduce test_lose()