****大学
《程序设计课程设计》
报告
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();}}