****大学

《程序设计课程设计》

报告

1 课程设计需求

编写一个 2048 游戏,且使用图形界面。

游戏规则为:

① 游戏开始时,初始化一个 16 方格的棋盘,并在棋盘内随机出现两个数字,出现的数字只能是 2 或 4。

② 玩家可以选择上下左右四个方向,若棋盘内的数字出现位移或合并,视为有效移动。

③ 玩家选择的方向上若有相同的数字则合并,每次有效移动可以同时合并,但不可以连续合并。

④ 合并所得的所有新生成数字相加即为此次移动的有效得分。

⑤ 玩家选择的方向行或列前方有空格则出现位移。

⑥ 每有效移动一步,棋盘的空位随机出现一个数字(依然为 2 或 4)。

⑦ 棋盘被数字填满,无法进行有效移动,判负,游戏结束。

⑧ 棋盘上出现 2048,判胜,游戏结束。

2设计

(1)总体思路

根据上述需求,先利用随机数调整2和4的出现概率,再在棋盘中随机2个空位填补数字2或4,即初始化棋盘。

采用文件流相关操作记录历史最高分,若玩家从未玩过,则默认最高分为0。游戏过程中需要进行当前分数(Score)和历史最高分(Best)大小比较,以便随时更新历史最高分。

再利用循环结构实现玩家操作(重新开始,退出游戏,移动),移动操作需要实现上移、下移、左移、右移。

重新开始(设定为N或n):需要再次初始化棋盘。

退出游戏(设定为Z或z):结束程序运行进程。

移动操作(设定以W,S,A,D或者键盘自带方向键作为移动方向键):对移动操作是否有效进行判断,有效则累加此次移动的分数,并判断是否出现2048,出现则游戏胜利,否则在棋盘空位随机出现一个数字(依然为 2 或 4);在分数累加后与历史最高分数比较,判断是否更新历史最高分;当棋盘被填满且无法合并数字,即移动操作无效时,游戏结束。

游戏胜利:界面上方出现Win字样。

游戏失败:界面上方出现Lose字样。

(2)具体流程图如下:

(3)界面设计

采用easyx绘制图形界面:界面下方是4×4大小的棋盘,并对棋盘填充色彩,且不同数字对应不同色彩;上方是数据显示界面,显示当前分数和历史最高分,以及重新开始和退出游戏的操作提示。

表格1不同数字对应颜色

枚举的color数组对应下标

对应的数字

对应的RGB

t0

0

RGB(205, 193,180)

t1

2

RGB(238, 228,218)

t2

4

RGB(237, 224,200)

t3

8

RGB(242, 177,121)

t4

16

RGB(245, 149, 99)

t5

32

RGB(246, 124, 95)

t6

64

RGB(246, 94, 59)

t7

128

RGB(242, 177,121)

t8

256

RGB(237, 204, 97)

t9

512

RGB(255, 0, 128)

t10

1024

RGB(145, 0, 72)

t11

2048

RGB(242, 17, 158)

(4)构思

表格2常量汇总

常量

Row

Col

Width

Gap

数值

4

4

105 px

15 px

描述

棋盘行数

棋盘列数

单独一个正方形格子的边长

格子间的距离

表格3全局变量

全局变量

score

Best

table[Row][Col]

gameOver

类型

int

int

二维数组

bool

初始值

0

0

{ }

false

描述

当前总分

历史最佳分数

棋盘

判断游戏是否继续

3 项目实现与运行结果

调试结果和分析:

(1)首次运行,进入游戏:

可以看到在棋盘中随机2处出现数字2(因为设定出现2的概率大于4),历史最高分(Best)也是默认为0,因还未移动,所以当前得分(Score)也为0.

(2)移动数次后:

移动过程中分数一直变化,因为移动后Score>Best始终成立,所以Best随时跟随Score变化。

(3)游戏失败时,得分为660分(注:最后一次滑动是向右滑动):

因为此次滑动是向右边滑动,所以虽然上下方向可以合并2个16,但因为右滑,无法进行数字合并,而且棋盘已满,故游戏判负,显示Lose字样。

(4)再重新开始游戏:

重开后,棋盘也照样在随机2处出现数字。且历史最高分(Best)变为之前的660分,而当前得分(Score)为0.

(5)再次移动数次:

移动过程中,因为Score暂时未超过Best,所以Best不变,而Score变化。

当Score超过Best后,Best会随着Score一同增加。

(6)在中途时,选择重新开始:

在上一步得到880分后,重新开始(键入N或者n)后,棋盘随机2处出现数字,Best是之前的最高分880,Score为0.

(7)移动数次后再退出游戏:

进行数次移动操作后,Score为252分,然后退出游戏(键入Z或z),游戏关闭,并调出控制台(因为调试程序时,选择不关闭控制台;若想退出游戏后,直接退出所有程序,则需要在initgraph(500, 630)函数中传入第3个参数1,因为默认第三个参数为0,表示退出游戏后调出控制台)。

(8)游戏胜利时:

由于技术水平有限,暂时无法提供通关截屏。

至此,已基本将所有调试做完。

4 课程设计过程问题分析

(1)怎样利用easyx绘制图形化界面?

通过网上查询资料,主要在网站EasyX文档(https://docs.easyx.cn/zh-cn/reference),再浏览主要的绘制函数,包括填充背景色彩、设置字体颜色,大小、显示字符串等,一步步学以致用,并通过结合网站提供的实例,逐渐掌握使用的方法。

(2)完成数据的收集,以及构思整个程序如何书写。

主要是需要搜集每个数字对应颜色,可以直接上网收集每个数字所对应的颜色,但我选择使用色彩吸取相关工具,在实践中逐步掌握色彩吸取工具的用法,并越发熟练。然后是构思程序设计,在多次阅读完题设需求后,最好在画图工具上一步步梳理题干;理清程序的进行步骤,明白程序的进程;该用何种方式才能完成题目要求;在使用这种方法时,是否需要一个变量来跟进程序运行进程,以便对实现某些操作:比如此次课设,要随时检验移动的有效性以及游戏是否结束,所以我采用创建一个变量来控制,如果移动无效,该变量改变后,就可满足游戏结束的条件。

最终理清程序该如何进行后,得出具体的流程图,对书写程序有很大帮助。

(3)如何具体实现移动操作?

在实现移动操作的过程中,因为各种原因,导致程序异常、运行失败、崩溃等问题。主要在于如何实现合并数字的操作,而且不能在一次移动中连续合并数字。因为上移,下移,左移,右移都是一个原理,所以先挑选右移入手,其他的移动便不攻自破。

当数字下移时,需要考虑如下场景:相邻位置数字相同时的合并操作,如[2,2,4,4]à[0,4,0,8]这种情况;数字无法合并时,如[0,2,0,8]à[0,0,2,8]这种情况。

所以选择下面这种解法:

void moveRight()

{

for (int i = Row – 1; i >= 0;i–)

{

int t = Row – 1;

for (int next = Col – 2; next >= 0;next–)

{

if (table[i][next] !=0)

{

if (table[i][t] == 0)

{

table[i][t]= table[i][next];

table[i][next]= 0;

}

else if (table[i][next] ==table[i][t])

{

table[i][t]*= 2;

score+= table[i][t];

table[i][next]= 0;

t–;

}

else

{

table[i][t- 1] = table[i][next];

if (t – 1 != next)

{

table[i][next]= 0;

}

t–;

}

}

}

}

}

通过前后两个变量是否为0,是否相等,考虑各种情况下的右移合并操作。

5 总结与心得体会

通过本次课程设计,对C++语言的应用以及实操有了更多的了解,提高了自身的逻辑思维能力;在查找资料的过程中,逐渐学会如何自学,自学能力进一步加强;在此基础上,还学会了如何运用esayx工具绘制简易游戏界面,以及熟练掌握了色彩吸取相关工具的快捷使用方法;能通过些许代码实现需求,程序每次运行成功总能带来不少喜悦,加强了继续下去,不断攻克难题的信心。

具体代码:

#define _CRT_SECURE_NO_WARNINGS 1// VS高版本编译器需要#include#include#include#include#include#include #include // 图形化界面采用easyx#include #includeusing namespace std;#define Row 4// 行数#define Col 4// 列数#define Width 105// 格子边长#define Gap 15 // 格子间距enum color // 枚举相应颜色{t0 = RGB(205, 193, 180), // 0t1 = RGB(238, 228, 218), // 2t2 = RGB(237, 224, 200), // 4t3 = RGB(242, 177, 121), // 8t4 = RGB(245, 149, 99),// 16t5 = RGB(246, 124, 95),// 32t6 = RGB(246, 94, 59), // 64t7 = RGB(242, 177, 121), // 128t8 = RGB(237, 204, 97),// 256t9 = RGB(255, 0, 128), // 512t10 = RGB(145, 0, 72), // 1024t11 = RGB(242, 17, 158)// 2048};color colors[] = { t0,t1,t2,t3,t4,t5,t6,t7,t8,t9,t10,t11 }; // 对应数字的背景颜色int table[Row][Col] = {};int score = 0; // 当前总分int Best = 0;// 历史最佳分数void over();// 判断是否出现2048获胜bool gameOver = false;// 判断游戏是否继续void startagain();// 是否重新开始bool find0(); // 确认有无空位int random();// 出现随机数字2或4void init(int n); // 数字出现的个数void display(); // 展示void record();// 记录最高分void update();// 更新最高分void move();// 移动void moveUp();void moveDown();void moveLeft();void moveRight();int main(){initgraph(500, 630); // 窗口分辨率 init(2); // 初始化棋盘update();do {while (!gameOver){display();move();over();update();}if (gameOver){startagain();}} while (!gameOver);closegraph();return 0;}int random(){srand((unsigned int)time(NULL));if (rand() % 10 > 6)// 调整2和4出现概率{return 4;}else{return 2;}}bool find0() // 确认有无空位增加{for (int j = 0; j < Row; j++){for (int t = 0; t < Col; t++){if (table[j][t] == 0){return true;}}}settextcolor(RGB(252, 85, 49));settextstyle(100, 0, _T("微软雅黑"));outtextxy(Width * 2 - Gap * 3, Gap * 4, _T("Lose"));gameOver = true;return false;}void init(int n) // 数字出现的个数{srand((unsigned int)time(NULL));int init_row = 0;int init_col = 0;if (find0() == true){for (int i = 0; i < n; ){init_row = rand() % Row;init_col = rand() % Col;if (table[init_row][init_col] == 0){table[init_row][init_col] = random();i++;}}}}void over()// 判断是否2048获胜{for (int i = 0; i < Row; i++){for (int j = 0; j < Col; j++){if (table[i][j] == 2048){settextcolor(RGB(252, 85, 49));settextstyle(100, 0, _T("微软雅黑"));outtextxy(Width * 2 - Gap * 3, Gap * 4, _T("Win"));gameOver = true;}}}}void startagain() // 是否重新开始{char key = _getch();switch (key){case 'N':case 'n':{for (int i = 0; i < Row; i++){for (int j = 0; j < Col; j++){table[i][j] = 0;}}init(2); // 初始化score = 0;graphdefaults(); // 设置默认字体display();gameOver = false;break;}default:{break;}}}void display() // 展示{setbkcolor(RGB(187, 173, 160)); // 设置背景颜色cleardevice();for (int i = 0; i < Row; i++){for (int j = 0; j = 0; i--){int t = Col - 1;for (int next = Row - 2; next >= 0; next--){if (table[next][i] != 0){if (table[t][i] == 0){table[t][i] = table[next][i];table[next][i] = 0;}else if (table[next][i] == table[t][i]){table[t][i] *= 2;score += table[t][i];table[next][i] = 0;t--;}else{table[t - 1][i] = table[next][i];if (t - 1 != next){table[next][i] = 0;}t--;}}}}}void moveLeft(){for (int i = 0; i < Row; i++){int t = 0;for (int next = 1; next = 0; i--){int t = Row - 1;for (int next = Col - 2; next >= 0; next--){if (table[i][next] != 0){if (table[i][t] == 0){table[i][t] = table[i][next];table[i][next] = 0;}else if (table[i][next] == table[i][t]){table[i][t] *= 2;score += table[i][t];table[i][next] = 0;t--;}else{table[i][t - 1] = table[i][next];if (t - 1 != next){table[i][next] = 0;}t--;}}}}}void move(){char key = _getch();switch (key){case 'N':case 'n':{for (int i = 0; i < Row; i++){for (int j = 0; j < Col; j++){table[i][j] = 0;}}init(2); // 初始化score = 0;break;}case 'Z':case 'z':{gameOver = true;return;}case 'w':case 'W':case 72:{moveUp();init(1);break;}case 's':case 'S':case 80:{moveDown();init(1);break;}case 'a':case 'A':case 75:{moveLeft();init(1);break;}case 'd':case 'D':case 77:{moveRight();init(1);break;}default:{break;}}}void record() // 记录最高分{ofstream ofs("BestScore.text", ios::trunc);Best = score;ofs << Best << endl;ofs.close();}void update() // 更新最高分{ifstream ifs("BestScore.text", ios::in|ios::binary);if (!ifs.is_open()) // 判断文件是否存在{ofstream ofs("BestScore.text", ios::out);ofs << 0 << endl;ofs.close();Best = 0;return;}char bestchar[8];ifs.getline(bestchar, 8);string t = bestchar;Best = stoi(t);if (Best < score){ifs.close();record();}}