赞
踩
目录
对于2048这个小游戏大家应该都玩过,没玩过的话,可以点击下方的链接进去玩一下。
2048是一个风靡全球的益智类小游戏,通过上下左右控制来合并盘子里的数字,直到盘子里出现2048。
我们今天在Linux下用C++实现2048编程小项目,自己动手实践一下。
首先前提是,小伙伴们必须要有Linux操作系统,没有的小伙伴可以自己去网上百度安装一个。
我们先简单了解一下2048的原理:
1. 通过上下左右来控制合并盘子里的数字。
2. 每次合并只能合并相邻的两个数字,例如 [2 2 2 2] ,向右合并以后是 [空 空 4 4] ,不是 [空 空 空 8]。
3. 每次合并的时候,合并那个方向的数字优先级高,例如 [空 2 2 2],向右合并以后是 [空 空 2 4],不是 [空 空 4 2]。
4. 判断游戏胜利的条件是盘子里出现数字2048。
5. 判断游戏失败的条件是盘子里的数字占满了,此时盘子中没有出现数字2048,且相邻的数字之间不满足合并要求,例如
此时盘子里相邻的数字之间都不能再合并了。
6. 盘子中每次重新开始会随机在两个空的位置产生数字2或者4。
7. 盘子中每次合并后出现新数字4的概率为10%。
8. 每次移动之后会在空的位置中随机产生一个数字2或者4。
我们在Linux中依赖库函数<curses.h>来绘制图形界面和控制上下左右的移动操作,一般Linux中没有安装这个库,你可以输入这个操作命令安装这个<curses.h>库:$ apt-get install libncurses5-dev 安装完成之后,我们先简单了解一下这个库函数所包含的我们会使用到的函数。
initscr(); | ncurses初始化,初始化一下屏幕 |
cbreak(); | 按键不需要输入回车直接交互,也就是我输入一个字符不需要回车 ,输入函数就可以得到这个字符了 |
noecho(); | 按键不显示,使你输入的字符不在屏幕上显示,就和Linux输密码一样,屏幕上看不到你输入了什么 |
curs_set(0); | 隐藏输入光标,平时输入时,输入位置会有一个光标在一闪一闪的,这个函数将光标隐藏起来了。 |
endwin(); | ncurses清理,清理屏幕。 |
move(5,5); 和addch(ch); | 将输入光标移到屏幕坐标(5,5)的位置上,然后使你要输入的字符显示在屏幕位置(5,5)上 |
mvprintw(8,8,"2048小游戏"); | 在指定位置打印字符串,在坐标(8,8)处打印字符串“Game 2048” |
这几个就是我们要使用的库<curses.h>中的几个函数。库<curses.h>对中文不是很友好,比如函数mvprintw()中的字符串如果你用中文的话程序运行显示在屏幕上的就是一串乱码。
注意:函数move(row, column); 和函数mvprintw(row, column,"***"); 中的坐标row代表第几行,column代表第几空格。例如
- move(0,0);//将光标移到0*0的位置上
- addch(ch);//使你要输入的字符显示在屏幕位置0*0上
- mvprintw(1,2,"Game 2048");//在1*2的位置上打印一个字符串
这几个代码显示在屏幕上就是:
来仔细说明一下:
字符n的位置为(0,0),就是第0行第0个空格的位置;字符串“GAME 2048”中G的位置为(1,2),也就是第1行第2个空格的位置。所以说这两个函数是有坐标(0,0)的,也就是我们通常意义上的第一行第一个空格,但其实在函数中是第0行第0个空格的位置。
我们将这几个函数写成一个简易的程序来执行一下。代码如下所示:
- //step1.cpp
- #include<iostream>
- #include<curses.h>
- using namespace std;
-
- void initialize()
- {
- //ncurse初始化
- initscr(); //初始化一下屏幕
- //按键不需要输入回车直接交互
- cbreak();//也就是我输入一个字符不需要回车 输入函数就可以得到这个字符了
- //按键不显示
- noecho();//使你输入的字符不在屏幕上显示,就和Linux输密码一样 屏幕上看不到你输入了什么
- //隐藏光标
- curs_set(0);//将输入为止的光标隐藏
- }
- void shutdown()
- {
- //ncurses清理
- endwin();
- }
-
- int main()
- {
- initialize();
-
- char ch='n'; //将ch初始化为n
- do
- {
- move(5,5);//将光标移到5*5的位置上
- addch(ch);//使你要输入的字符显示在屏幕位置5*5上
- mvprintw(8,8,"Game 2048");//在2*2的位置上打印一个字符串 在指定位置打印字符串
- ch=getch();//获取你输入的字符 赋值给ch
- }while(ch!='Q'&&ch !='q');//判断循环是否继续
-
- shutdown();
- return 0;
- }
-
我们来看一下执行效果:
你随意按一个字符,屏幕上字符a就会变成你所按的字符,且字符a处没有光标闪烁。
在了解了我们所要使用的几个函数之后,我们创建一个游戏类Game,用来实现2048游戏的主体。接下来我们来绘制一下简易的盘子。我们可以用符号+、-、|来绘制盘子。绘制的大概效果图如下所示:
我们可以将函数move();和函数addch();包装成一个函数drawch(int row, int col, char c);调用这个函数时传入坐标和要显示的字符就可以了,代码如下所示:
- // 在指定的位置画一个字符
- void drawch(int row, int col, char c)
- {
- move(row, col);
- addch(c);
- }
我们结合函数drawch();和for循环就能画出来,因为符号的位置是有规律的,我们可以用for循环将坐标传入给函数drawch();让符号在指定的位置显示即可。代码如下所示:
- //step2.cpp
- #include<iostream>
- #include<curses.h>
- using namespace std;
-
- #define N 4 //格子数量
- #define W 5 //格子的宽度
-
- class Game
- {
- public:
- Game(){} //构造函数
- //绘制游戏界面
- void draw()
- {
- //清理屏幕
- clear();
- //居中偏移
- for(int i = 0; i<=N; ++i)
- {
- for(int j = 0; j<=N ;++j)
- {
- //绘制相交点
- drawch(i*2, j*W, '+');
-
- //绘制竖线
- if(i != N)
- {
- drawch(i*2+1, j*W, '|');
- }
-
- //绘制横线
- for(int k=0; j != N && k<N; ++k)
- {
- drawch(i*2, 1 + j*W + k,'-');
- }
- }
- }
- }
- private:
- // 在指定的位置画一个字符
- void drawch(int row, int col, char c)
- {
- move(row, col);
- addch(c);
- }
- };
-
- void initialize()
- {
- //ncurse初始化
- initscr();
- //按键不需要输入回车直接交互
- cbreak();
- //按键不显示
- noecho();
- //隐藏光标
- curs_set(0);
- }
- void shutdown()
- {
- //ncurses清理
- endwin();
- }
-
- int main()
- {
- initialize();//函数调用
- Game game;
- char ch='n';
- do
- {
- game.draw();//调用对象game的子函数绘制游戏界面
- ch = getch();
- }while(ch != 'q' && ch != 'Q');//当输入的字符为q或Q时退出游戏界面
-
- shutdown();
- return 0;
- }
运行结果为:
和step1.cpp相比较,step2.cpp创建了一个游戏类Game,将在屏幕指定位置上输出字符包装成了一个函数drawch(),并将其放在了游戏类Game中,这里我们还用到了预处理命令#define,其为宏定义命令,我们要画一个4*4的盘子,所以宏定义N代表格子数量4,我们设置各自中间的空格数为4个,也就是每次向后移动五个空格,在第五个空格输出字符‘|’,所以设置W代表格子的宽度5。然后利用for循环和函数drawch()编写一个函数draw(),来绘制游戏界面盘子。
我们观察上图,绘制出来的游戏界面,发现贴着右边,不是很美观,我们宏定义M来控制盘子向右移动的距离,我们来看一下修改后的代码:
- //step2.cpp
- #include<iostream>
- #include<curses.h>
- using namespace std;
-
- #define N 4 //格子数量
- #define W 5 //格子的宽度
- #define M 18 //盘子向右移动的距离
-
- class Game
- {
- public:
- Game(){} //构造函数
- //绘制游戏界面
- void draw()
- {
- //清理屏幕
- clear();
- //居中偏移
- for(int i = 0; i<=N; ++i)
- {
- for(int j = 0; j<=N ;++j)
- {
- //绘制相交点
- drawch(i*2, j*W + M, '+');
-
- //绘制竖线
- if(i != N)
- {
- drawch(i*2+1, j*W + M, '|');
- }
-
- //绘制横线
- for(int k=0; j != N && k<N; ++k)
- {
- drawch(i*2, 1 + j*W + M + k,'-');
- }
- }
- }
- }
- private:
- // 在指定的位置画一个字符
- void drawch(int row, int col, char c)
- {
- move(row, col);
- addch(c);
- }
- };
-
- void initialize()
- {
- //ncurse初始化
- initscr();
- //按键不需要输入回车直接交互
- cbreak();
- //按键不显示
- noecho();
- //隐藏光标
- curs_set(0);
- }
- void shutdown()
- {
- //ncurses清理
- endwin();
- }
-
- int main()
- {
- initialize();//函数调用
- Game game;
- char ch='n';
- do
- {
- game.draw();//调用对象game的子函数绘制游戏界面
- ch = getch();
- }while(ch != 'q' && ch != 'Q');//当输入的字符为q或Q时退出游戏界面
-
- shutdown();
- return 0;
- }
运行结果为:
这样看起来就舒服多了,接着我们要在屏幕合适位置显示一些提示信息,也就是,告诉玩家要怎么玩这个游戏,我们设置键盘上的W为向上合并数字,A为向左合并数字,S为向下合并数字,D为向右合并数字,Q为退出游戏,R为重新开始。这是我们就要用库函数<curses.h>中的函数mvpintw(),我们在游戏类Game的函数draw()中增加一行代码就可以了,增加的代码如下所示:
mvprintw(2 * (N + 1), 2 * N, "W(UP),S(DOWN),A(LEFT),D(RIGHT),R(RESTART),Q(QUIT)");
我们看一下效果:
我们要对玩家按键要做出反应,所以我们要将主函数进行修改一下,我们先看看之前的主函数:
- int main()
- {
- initialize();//函数调用
- Game game;
- char ch='n';
- do
- {
- game.draw();//调用对象game的子函数绘制游戏界面
- ch = getch();
- }while(ch != 'q' && ch != 'Q');//当输入的字符为q或Q时退出游戏界面
-
- shutdown();
- return 0;
- }
我们要对变量ch的处理写成类Game中的函数,对变量ch的处理的函数命名为processInput(),我们再增加一个Game类的成员变量status来控制游戏的状态,用函数getStatus(),返回status的值,我们宏定义S_FAIL表示失败、S_WIN表示胜利、S_NORMAL表示游戏正常进行、S_QUIT表示退出游戏。对其设置相应的值。 代码如下所示:
- #include<iostream>
- #include<curses.h>
- using namespace std;
-
- #define N 4 //格子数量
- #define W 5 //格子的宽度
- #define M 18 //盘子向右移动的距离 居中偏移量
- //游戏状态
- #define S_WIN 0 //胜利
- #define S_FAIL 1 //失败
- #define S_NORMAL 2 //游戏正常进行
- #define S_QUIT 3 //退出
-
- class Game
- {
- public:
- Game():status(S_NORMAL) {}
- //返回status的值
- int getStatus()
- {
- return status;
- }
-
- //处理按键
- void processInput()
- {
- char ch = getch();
- //将字母转化为大写
- if(ch >= 'a' && ch <= 'z')
- {
- ch -= 32;
- }
- if(ch == 'Q')
- {
- status = S_QUIT;
- }
- else
- {
- //目前先不做处理,只在输赢提示语中转换
- status = (status+1) % 2;
- }
- }
- //绘制游戏界面
- void draw()
- {
- //清理屏幕
- clear();
- for(int i = 0; i<=N; ++i)
- {
- for(int j = 0; j<=N ;++j)
- {
- //绘制相交点
- drawch(i*2, j*W + M, '+');
-
- //绘制竖线
- if(i != N)
- {
- drawch(i*2+1, j*W + M, '|');
- }
-
- //绘制横线
- for(int k=0; j != N && k<N; ++k)
- {
- drawch(i*2, 1 + j*W + M + k,'-');
- }
- }
- }
- mvprintw( 2*(N + 1), 2*N,"W(UP),S(DOWN),A(LEFT),D(RIGHT),R(RESTART),Q(QUIT)");
- if(status == S_WIN)
- {
- mvprintw(2*(N + 2), 4*N,"YOU WIN, PRESS R TO CONTINUE");
- }
- else if(status == S_FAIL)
- {
- mvprintw(2*(N + 2), 4*N,"YOU LOSE, PRESS R TO CONTINUE");
- }
- }
- private:
- void drawch(int row, int col, char c)
- {
- move(row, col);
- addch(c);
- }
- //游戏中的数字是右对齐,row,col是数字最后一位所在的位置
- void drawNum(int row, int col, int num)
- {
- while(num>0)
- {
- drawch(row, col, num%10+'0');
- num /= 10;
- --col;
- }
- }
- private:
- int status;
- };
- void initialize()
- {
- //ncurse初始化
- initscr();
- //按键不需要输入回车直接交互
- cbreak();
- //按键不显示
- noecho();
- //隐藏光标
- curs_set(0);
- }
- void shutdown()
- {
- //ncurses清理
- endwin();
- }
-
- int main()
- {
- initialize();
- Game game;
- do
- {
- game.draw();
- game.processInput();
- }while(game.getStatus() != S_QUIT);
-
- shutdown();
- return 0;
- }
-
运行结果如下所示:
现在我们使数字在盘子中正确的位置出现,对于要显示的数字,我们利用函数drawch();和while循环将数字按位的显示出来,大部分的人以前应该写过这样的一道题,将输入的数字123,倒序输出来,321。这种题很简单,我们将数字123倒序每位数转化为字符型输出出来,我们来看一下实现代码:
- #include<stdio.h>
-
- int main()
- {
- int num=123;
- char c;
- while(num!=0)
- {
- c=num%10 + '0';
- num/=10;
- printf("%c",c);
- }
- return 0;
- }
运行结果为:
这个代码我就不细细讲解了,稍微有点基础的人一下就能明白。
我们可以利用例题的解题思路,将数字显示在盘子中指定的位置,我们利用函数drawch();和for循环将数字化成字符显示在指定的位置,我们先创建一个函数setTestData(),这个函数是为了方便程序调制,随时设置数据。代码如下:
- //为了方便程序调制,随时设置数据
- //数组data定义在类中以声明
- void setTestData()
- {
- for(int i = 0; i < N; ++i)
- {
- for(int j = 0; j < N; ++j)
- {
- //设置交点值
- num[i][j] = 16<<i<<j;//左移一位相当于乘2,数组定义为类的私有成员函数
- }
- }
- }
数组设置好以后,我们将数组的元素显示在屏幕的盘子中,我们先看一下盘子:
数字显示,我们设置为右对齐,也就是如图中红色数字显示的那样,我们可以看出,数字是在符号 ‘ | ’ 的左边显示的,我们可以将从左往右数第二个符号 ‘ | ’ 的坐标的空格数减一,就可以得到数字个位数的坐标了,再减一就可以得到十位数的坐标了,以此类推就可以得到每位数字的坐标了,我们设置函数drawNum()对数字每一位求余并转化为字符,然后一位一位的画出来,我们来看一下实现代码:
- #include<iostream>
- #include<curses.h>
- using namespace std;
-
- #define N 4 //格子数量
- #define W 5 //格子的宽度
- #define M 18 //盘子向右移动的距离
- //游戏状态
- #define S_WIN 0 //胜利
- #define S_FAIL 1 //失败
- #define S_NORMAL 2 //游戏正常进行
- #define S_QUIT 3 //退出
-
- class Game
- {
- public:
- Game():status(S_NORMAL) { setTestData(); }//数组初始化
- //返回status的值
- int getStatus()
- {
- return status;
- }
-
- //处理按键
- void processInput()
- {
- char ch = getch();
- //将字母转化为大写
- if(ch >= 'a' && ch <= 'z')
- {
- ch -= 32;
- }
- if(ch == 'Q')
- {
- status = S_QUIT;
- }
- else
- {
- //目前先不做处理,只在输赢提示语中转换
- status = (status+1) % 2;
- }
- }
- //绘制游戏界面
- void draw()
- {
- //清理屏幕
- clear();
- for(int i = 0; i<=N; ++i)
- {
- for(int j = 0; j<=N ;++j)
- {
- //绘制相交点
- drawch(i*2, j*W + M, '+');
-
- //绘制竖线
- if(i != N)
- {
- drawch(i*2+1, j*W + M, '|');
- }
-
- //每个格子中的数字
- if(i!=N && j!=N)
- {
- drawNum(i*2+1, (j+1)*W - 1 + M, num[i][j]);
- }
-
- //绘制横线
- for(int k=0; j != N && k<N; ++k)
- {
- drawch(i*2, 1 + j*W + M + k,'-');
- }
- }
- }
- mvprintw( 2*(N + 1), 2*N,"W(UP),S(DOWN),A(LEFT),D(RIGHT),R(RESTART),Q(QUIT)");
- if(status == S_WIN)
- {
- mvprintw(2*(N + 2), 4*N,"YOU WIN, PRESS R TO CONTINUE");
- }
- else if(status == S_FAIL)
- {
- mvprintw(2*(N + 2), 4*N,"YOU LOSE, PRESS R TO CONTINUE");
- }
- }
- private:
- //初始化数组
- void setTestData()
- {
- for(int i = 0; i < N; ++i)
- {
- for(int j = 0; j<N; ++j)
- {
- num[i][j] = 8<<i<<i<<j;//左移一位也就是将数字乘2,移动N位,乘以2的N次方
- }
- }
- }
- void drawch(int row, int col, char c)
- {
- move(row, col);
- addch(c);
- }
- //游戏中的数字是右对齐,row,col是数字最后一位所在的位置
- void drawNum(int row, int col, int num)
- {
- while(num>0)
- {
- drawch(row, col, num%10+'0');
- num /= 10;
- --col;
- }
- }
- private:
- int status;
- int num[N][N];
- };
- void initialize()
- {
- //ncurse初始化
- initscr();
- //按键不需要输入回车直接交互
- cbreak();
- //按键不显示
- noecho();
- //隐藏光标
- curs_set(0);
- }
- void shutdown()
- {
- //ncurses清理
- endwin();
- }
-
- int main()
- {
- initialize();
- Game game;
- do
- {
- game.draw();
- game.processInput();
- }while(game.getStatus() != S_QUIT);
-
- shutdown();
- return 0;
- }
-
运行结果如图所示:
我们每次玩2048游戏时,开始的时候我们会发现,每次开始都是在盘子不同的位置出现两个数字2或者4,出现的位置是随机的,我们将每次重新开始调用的函数命名为restart(),我们来看一下实现的代码:
- // 重新开始
- void restart()
- {
- for (int i = 0; i < N; ++i)
- {
- for (int j = 0; j < N; ++j)
- {
- num[i][j] = 0;//将数组元素全部初始化为0
- }
- }
- randNew();
- randNew();//随机产生一个数字
- status = S_NORMAL;
- }
首先我们将数组元素全部初始化为0,然后我们调用函数randNew(),在随机空位置产生一个数字,调用这个函数两次就可以在不同的两个位置随机产生数字2或者4,函数randNew()的返回值类型为bool型,我们来看randNew()函数的代码:
- // 随机产生一个新的数字
- bool randNew()
- {
- vector<int> emptyPos;
- // 把空位置先存起来
- for (int i = 0; i < N; ++i)
- {
- for (int j = 0; j < N; ++j)
- {
- if (num[i][j] == 0)
- {
- emptyPos.push_back(i * N + j);//将数组中数值为0的元素的坐标保存到emptyPos中
- }
- }
- }
- if (emptyPos.size() == 0)
- {
- return false;
- }
- // 随机找个空位置
- int value = emptyPos[rand() % emptyPos.size()];
- // 10%的概率产生4
- num[value / N][value % N] = rand() % 10 == 1 ? 4 : 2;
- return true;
- }
这里创建一个vector对象emptyPos,用其保存数组中值为0的元素的坐标,然后判断一下emptyPos长度是否为空,如果是空的,说明数组中没有值为0的元素了,也就是,盘子中没有空位置了,返回false,代表游戏结束了,如果盘子中没有数字达到2048,说明你失败了,否则你胜利了,当然这是后面考虑的。如果盘子中还有空位置,这时我们就需要使用库函数<cstdlib>中,产生随机数的函数srand()和rand(),将这两个函数结合时间一起使用,不同的时间产生的随机数也是不一样的,我们将srand()函数放在我们自己创建的函数initialize()中,根据每次程序初始化的时间来随机产生数字。rand() % emptyPos.size() 的意思就是在vector对象emptyPos的所有元素中随机选取一个元素,在这里我们声明一个变量value来保存随机在vector对象emptyPos选择的元素,然后根据这个元素计算出这个元素在数组num中所代表的位置,由于value是int型变量,所以value/N得出来的是数组所在行,value%N得出来是数组所在列,具体是怎么实现的,我们看下图操作步骤:
我们得出盘子中的空位置之后,rand() % 10 == 1 ? 4 : 2;中的rand() % 10会随机产生整数0~9任意一个数,如果该数为1,则在盘子的空位置出生成数字4,否则是数字2,这样就达到了项目的要求在盘子空位置出现数字4的概率为10%,然后返回true。我们来看一下完整的代码:
- #include<iostream>
- #include<curses.h>
- #include<vector>
- #include<cstdlib>
- #include<ctime>
- using namespace std;
-
- #define N 4 //格子数量
- #define W 5 //格子的宽度
- #define M 18 //盘子向右移动的距离
- //游戏状态
- #define S_WIN 0 //胜利
- #define S_FAIL 1 //失败
- #define S_NORMAL 2 //游戏正常进行
- #define S_QUIT 3 //退出
-
- class Game
- {
- public:
- Game():status(S_NORMAL) { setTestData(); }//数组初始化
- //返回status的值
- int getStatus()
- {
- return status;
- }
- //处理按键
- void processInput()
- {
- char ch = getch();
- //将字母转化为大写
- if(ch >= 'a' && ch <= 'z')
- {
- ch -= 32;
- }
- if(ch == 'Q')//退出游戏
- {
- status = S_QUIT;
- }
- else if (ch == 'R') //重新开始
- {
- restart();
- }
- else
- {
- //目前先不做处理,只在输赢提示语中转换
- status = (status+1) % 2;
- }
- }
- //绘制游戏界面
- void draw()
- {
- //清理屏幕
- clear();
- for(int i = 0; i<=N; ++i)
- {
- for(int j = 0; j<=N ;++j)
- {
- //绘制相交点
- drawch(i*2, j*W + M, '+');
-
- //绘制竖线
- if(i != N)
- {
- drawch(i*2+1, j*W + M, '|');
- }
-
- //每个格子中的数字
- if(i!=N && j!=N)
- {
- drawNum(i*2+1, (j+1)*W - 1 + M, num[i][j]);
- }
-
- //绘制横线
- for(int k=0; j != N && k<N; ++k)
- {
- drawch(i*2, 1 + j*W + M + k,'-');
- }
- }
- }
- mvprintw( 2*(N + 1), 2*N,"W(UP),S(DOWN),A(LEFT),D(RIGHT),R(RESTART),Q(QUIT)");
- if(status == S_WIN)
- {
- mvprintw(2*(N + 2), 4*N,"YOU WIN, PRESS R TO CONTINUE");
- }
- else if(status == S_FAIL)
- {
- mvprintw(2*(N + 2), 4*N,"YOU LOSE, PRESS R TO CONTINUE");
- }
- }
- private:
- // 重新开始
- void restart()
- {
- for (int i = 0; i < N; ++i)
- {
- for (int j = 0; j < N; ++j)
- {
- num[i][j] = 0;//将数组元素全部初始化为0
- }
- }
- randNew();//随机产生一个数字
- randNew();
- status = S_NORMAL;
- }
- // 随机产生一个新的数字
- bool randNew()
- {
- vector<int> emptyPos;
- // 把空位置先存起来
- for (int i = 0; i < N; ++i)
- {
- for (int j = 0; j < N; ++j)
- {
- if (num[i][j] == 0) {
- emptyPos.push_back(i * N + j);//将数组中数值为0的元素的坐标保存到emptyPos中
- }
- }
- }
- if (emptyPos.size() == 0)
- {
- return false;
- }
-
- // 随机找个空位置
- int value = emptyPos[rand() % emptyPos.size()];
- // 10%的概率产生4
- num[value / N][value % N] = rand() % 10 == 1 ? 4 : 2;
- return true;
- }
- //初始化数组
- void setTestData()
- {
- for(int i = 0; i < N; ++i)
- {
- for(int j = 0; j<N; ++j)
- {
- num[i][j] = 8<<i<<i<<j;//左移一位也就是将数字乘2,移动N位,乘以2的N次方
- }
- }
- }
- void drawch(int row, int col, char c)
- {
- move(row, col);
- addch(c);
- }
- //游戏中的数字是右对齐,row,col是数字最后一位所在的位置
- void drawNum(int row, int col, int num)
- {
- while(num>0)
- {
- drawch(row, col, num%10+'0');
- num /= 10;
- --col;
- }
- }
- private:
- int status;
- int num[N][N];
- };
- void initialize()
- {
- //ncurse初始化
- initscr();
- //按键不需要输入回车直接交互
- cbreak();
- //按键不显示
- noecho();
- //隐藏光标
- curs_set(0);
- // 随机数
- srand(time(NULL));
- }
- void shutdown()
- {
- //ncurses清理
- endwin();
- }
-
- int main()
- {
- initialize();
- Game game;
- do
- {
- game.draw();
- game.processInput();
- }while(game.getStatus() != S_QUIT);
-
- shutdown();
- return 0;
- }
-
运行结果如下所示:(第一张为初始界面,第二张为按R键后的界面,盘子中随机两个位置产生数字)
完成了这些功能之后,我们接下来完成向左移动合并数字,我们先回忆一下项目要求:
1. 通过上下左右来控制合并盘子里的数字。
2. 每次合并只能合并相邻的两个数字,例如 [2 2 2 2] ,向右合并以后是 [空 空 4 4] ,不是 [空 空 空 8]。
3. 每次合并的时候,合并那个方向的数字优先级高,例如 [空 2 2 2],向右合并以后是 [空 空 2 4],不是 [空 空 4 2]。
4. 每次移动之后会在空的位置中随机产生一个数字2或者4。
我们设置键盘上的W为向上合并数字,A为向左合并数字,S为向下合并数字,D为向右合并数字,Q为退出游戏,R为重新开始。当玩家按下A时,盘子中的数字会左移,并且合并相邻且相同的数字,当玩家按下D时,盘子中的数字会右移,并且合并相邻且相同的数字,当玩家按下W时,盘子中的数字会上移,并且合并相邻且相同的数字,当玩家按下S时,盘子中的数字会下移,并且合并相邻且相同的数字。
我们完善一下处理按键的函数processInput():
- //处理按键
- void processInput()
- {
- char ch = getch();
- //将字母转化为大写
- if(ch >= 'a' && ch <= 'z')
- {
- ch -= 32;
- }
- if(status == S_NORMAL)//如果游戏正常进行
- {
- bool updated = false;//用来判断盘子中的数是否移动了或者合并了
- if(ch == 'A')
- {
- //向左移动
- updated = moveLeft();//如果盘子中的数发生了变化,会返回true,否则返回false
- }
- else if(ch == 'D')
- {
- //向右移动
- }
- else if(ch =='W')
- {
- //向上移动
- }
- else if(ch == 'S')
- {
- //向下移动
- }
- if(updated)//如果盘子中的数发生了变化,就在所有的空位置中,随机产生一个数字
- {
- randNew();//在盘子中的空位置中的随机一个位置产生一个新数
- }
- }
- if(ch == 'Q')//判断是否退出游戏
- {
- status = S_QUIT;
- }
- else if (ch == 'R')//判断是否重新开始游戏
- {
- restart();
- }
- }
向左移动的函数为moveLeft(),该函数返回值为bool类型,如果盘子中的数据发生了变化就返回true,否则就返回false,如果盘子中的数据没有发生变化,就不在盘子中随机产生新数,如果发生了变化就在盘子中的空位置中,随机一个空位置产生一个新数2或者4。我们来看一下代码:
- #include<iostream>
- #include<curses.h>
- #include<vector>
- #include<cstdlib>
- #include<ctime>
- using namespace std;
-
- #define N 4 //格子数量
- #define W 5 //格子的宽度
- #define M 18 //盘子向右移动的距离
- //游戏状态
- #define S_WIN 0 //胜利
- #define S_FAIL 1 //失败
- #define S_NORMAL 2 //游戏正常进行
- #define S_QUIT 3 //退出
- //胜利条件
- #define TARGET 2048
- class Game
- {
- public:
- Game():status(S_NORMAL) { setTestData(); }//数组初始化
- //返回status的值
- int getStatus()
- {
- return status;
- }
- //处理按键
- void processInput()
- {
- char ch = getch();
- //将字母转化为大写
- if(ch >= 'a' && ch <= 'z')
- {
- ch -= 32;
- }
- if(status == S_NORMAL)//如果游戏正常进行
- {
- bool updated = false;//用来判断盘子中的数是否移动了或者合并了
- if(ch == 'A')
- {
- //向左移动
- updated = moveLeft();//如果盘子中的数发生了变化,会返回true,否则返回false
- }
- else if(ch == 'D')
- {
- //向右移动
- }
- else if(ch =='W')
- {
- //向上移动
- }
- else if(ch == 'S')
- {
- //向下移动
- }
- if(updated)//如果盘子中的数发生了变化,就在所有的空位置中,随机产生一个数字
- {
- randNew();//在盘子中的空位置中的随机一个位置产生一个新数
- }
- }
- if(ch == 'Q')//判断是否退出游戏
- {
- status = S_QUIT;
- }
- else if (ch == 'R')//判断是否重新开始游戏
- {
- restart();
- }
- }
- //绘制游戏界面
- void draw()
- {
- //清理屏幕
- clear();
- //居中偏移
- //const int offs`et = 18;
- for(int i = 0; i<=N; ++i)
- {
- for(int j = 0; j<=N ;++j)
- {
- //绘制相交点
- drawch(i*2, j*W + M, '+');
-
- //绘制竖线
- if(i != N)
- {
- drawch(i*2+1, j*W + M, '|');
- }
-
- //每个格子中的数字
- if(i!=N && j!=N)
- {
- drawNum(i*2+1, (j+1)*W - 1 + M, num[i][j]);
- }
-
- //绘制横线
- for(int k=0; j != N && k<N; ++k)
- {
- drawch(i*2, 1 + j*W + M + k,'-');
- }
- }
- }
- mvprintw( 2*(N + 1), 2*N,"W(UP),S(DOWN),A(LEFT),D(RIGHT),R(RESTART),Q(QUIT)");
- if(status == S_WIN)
- {
- mvprintw(2*(N + 2), 4*N,"YOU WIN, PRESS R TO CONTINUE");
- }
- else if(status == S_FAIL)
- {
- mvprintw(2*(N + 2), 4*N,"YOU LOSE, PRESS R TO CONTINUE");
- }
- }
- private:
- //向左移动
- bool moveLeft()
- {
- int tmp[N][N];//保存移动之前的数组的值
- for(int i=0; i< N; ++i)
- {
- //逐行处理数据
- int currentWritePos = 0;//记录合并数字的前一个数字的位置
- int lastValue = 0;//记录合并数字的前一个数字的值
- for(int j=0; j<N;++j)
- {
- tmp[i][j] = num[i][j];
- if(num[i][j] == 0)
- {
- continue;//如果值为0,跳过此次循环,不执行下列操作,不为0,继续下列操作
- }
- if(lastValue == 0)
- {
- lastValue = num[i][j];//记录相同两个数合并时的前一个数字的值
- }
- else
- {
- if(lastValue == num[i][j])
- //如果lastValue的值与下一个不为0的值相同
- {
- num[i][currentWritePos] = lastValue*2;//这两个数的值相加
- lastValue = 0;//lastValue的值清空
- if(num[i][currentWritePos] == TARGET)//判断合并后的数字是否是2048,也就是判断是否胜利
- {
- status = S_WIN;
- }
- }
- //如果lastValue的值与下一个不为0的值相同
- else//如果lastValue的值与下一个不为0的值不相同
- {
- num[i][currentWritePos] = lastValue;//将lastValue对应的盘子中的数字向前移动
- lastValue = num[i][j];//lastValue的值更新为下一个不为0的值
- }
- ++currentWritePos;//执行到这一步说明num[i][currentWritePos]对应的盘子中的位置已有数值,所以currentWritePos的值加一,对应的盘子中的位置向右移动一位
- }
- num[i][j] = 0;//原本的值,要么向前移动了,要么和前面不为0的值合并了,所以要将此处的值赋值为0
- }
- if(lastValue != 0)//本行已经遍历到最后一个位置了,如果lastValue的值不为0,本行的后面没有值可以和lastValue合并了,所以将其赋值给currentWritePos对应的位置
- {
- num[i][currentWritePos] = lastValue;
- }
- }
- //判断数组的数据是否发生了变化,如果发生了变化,说明此次操作引起了数组的数据发生了移动或者合并,然后盘子中会产生新的数字,反之,说明盘子中的数没有移动,也没有合并,也就是此次操作没有引起变化,盘子中就不能随机生成新的数
- for(int i = 0; i < N;++i)
- {
- for(int j = 0; j < N;++j)
- {
- if(num[i][j] != tmp[i][j])
- {
- return true;
- }
- }
- }
- return false;
- }
- // 重新开始
- void restart()
- {
- for (int i = 0; i < N; ++i)
- {
- for (int j = 0; j < N; ++j)
- {
- num[i][j] = 0;//将数组元素初始化为0
- }
- }
- randNew();//随机产生一个数字
- randNew();
- status = S_NORMAL;
- }
- // 随机产生一个新的数字
- bool randNew()
- {
- vector<int> emptyPos;
- // 把空位置先存起来
- for (int i = 0; i < N; ++i)
- {
- for (int j = 0; j < N; ++j)
- {
- if (num[i][j] == 0)
- {
- emptyPos.push_back(i * N + j);
- }
- }
- }
- if (emptyPos.size() == 0)
- {
- return false;
- }
- // 随机找个空位置
- int value = emptyPos[rand() % emptyPos.size()];
- // 10%的概率产生4
- num[value / N][value % N] = rand() % 10 == 1 ? 4 : 2;
- return true;
- }
- //初始化数组
- void setTestData()
- {
- for(int i = 0; i < N; ++i)
- {
- for(int j = 0; j<N; ++j)
- {
- num[i][j] = 8<<i<<i<<j;//左移一位也就是将数字乘2,移动N位,乘以2的N次方
- }
- }
- }
- void drawch(int row, int col, char c)
- {
- move(row, col);
- addch(c);
- }
- //游戏中的数字是右对齐,row,col是数字最后一位所在的位置
- void drawNum(int row, int col, int num)
- {
- while(num>0)
- {
- drawch(row, col, num%10+'0');
- num /= 10;
- --col;
- }
- }
- private:
- int status;
- int num[N][N];
- };
- void initialize()
- {
- //ncurse初始化
- initscr();
- //按键不需要输入回车直接交互
- cbreak();
- //按键不显示
- noecho();
- //隐藏光标
- curs_set(0);
- // 随机数
- srand(time(NULL));
- }
- void shutdown()
- {
- //ncurses清理
- endwin();
- }
-
- int main()
- {
- initialize();
- Game game;
- do
- {
- game.draw();
- game.processInput();
- }while(game.getStatus() != S_QUIT);
-
- shutdown();
- return 0;
- }
-
假如我们有下图所示的第一个盘子的数据,我们现在要将其向上移动合并数字,利用数学思维,我们将其逆时针旋转90°之后,向左移,再逆时针旋转270°,这样我们就实现了向上移动合并数字;向右移动,我们就我们将其逆时针旋转180°之后,向左移,再逆时针旋转180°,这样就是实现了向右移;向下移动,我们就我们将其逆时针旋转270°之后,向左移,再逆时针旋转90°,这样就实现了向下移动。这样处理代码是实现起来就很简洁了,而且很巧妙。
我们来看一下实现逆时针旋转90°的代码:
- //数组逆时针旋转90度
- void rotate()
- {
- int tmp[N][N] = {0};
- for(int i = 0;i<N;++i)
- {
- for(int j = 0; j<N;++j)
- {
- tmp[i][j] = num[N-1-i];
- }
- }
- for(int i = 0;i<N;++i)
- {
- for(int j = 0; j<N;++j)
- {
- num[i][j] = tmp[i][j];
- }
- }
- }
我们还要增加一个函数isOver()判断盘子中如果还有空位,相邻元素之间还能合并,则返回true,否则返回false。代码如下所示:
- //判断游戏是否结束
- bool isOver()
- {
- for(int i = 0;i<N;++i)
- {
- for(int j = 0; j<N;++j)
- {
- //有空位或相邻相同 两个有一个成立
- if((j+1<N)&&(num[i][j]*num[i][j+1] == 0 || num[i][j]==num[i][j+1]))
- {
- return false;
- }
- if((i+1<N)&&(num[i][j]*num[i+1][j] == 0 || num[i][j] == num[i+1][j]))
- {
- return false;
- }
- }
- }
- return true;
- }
我们看一下完善之后的处理按键的函数processInput():
- //处理按键
- void processInput()
- {
- char ch = getch();
- //将字母转化为大写
- if(ch >= 'a' && ch <= 'z')
- {
- ch -= 32;
- }
- if(status == S_NORMAL)//如果游戏正常进行
- {
- bool updated = false;//用来判断盘子中的数是否移动了或者合并了
- if(ch == 'A')
- {
- //向左移动
- updated = moveLeft();//如果盘子中的数发生了变化,会返回true,否则返回false
- }
- else if(ch == 'D')
- {
- //向右移动
- rotate();
- rotate();
- updated = moveLeft();
- rotate();
- rotate();
- }
- else if(ch =='W')
- {
- //向上移动
- rotate();
- updated = moveLeft();
- rotate();
- rotate();
- rotate();
- }
- else if(ch == 'S')
- {
- //向下移动
- rotate();
- rotate();
- rotate();
- updated = moveLeft();
- rotate();
- }
- if(updated)//如果盘子中的数发生了变化,就在所有的空位置中,随机产生一个数字
- {
- randNew();//在盘子中的空位置中的随机一个位置产生一个新数
- if(isOver())//判断盘子中是否还有空位置,相邻元素之间是否还能合并
- {
- status = S_FAIL;
- }
- }
- }
- if(ch == 'Q')//判断是否退出游戏
- {
- status = S_QUIT;
- }
- else if (ch == 'R')//判断是否重新开始游戏
- {
- restart();
- }
- }
我们将新增的几个函数,加进去之后,我们的项目2048小游戏就圆满完成了。当然还能变得更加完美,结合数据库,保留每次的成绩,还有最高和最低成绩,等等,如果之后有时间在完善该功能。
- #include<iostream>
- #include<curses.h>
- #include<vector>
- #include<cstdlib>
- #include<ctime>
- using namespace std;
-
- #define N 4 //格子数量
- #define W 5 //格子的宽度
- #define M 18 //盘子向右移动的距离
- //游戏状态
- #define S_WIN 0 //胜利
- #define S_FAIL 1 //失败
- #define S_NORMAL 2 //游戏正常进行
- #define S_QUIT 3 //退出
- //胜利条件
- #define TARGET 2048
- class Game
- {
- public:
- Game():status(S_NORMAL) { setTestData(); }//数组初始化
- //返回status的值
- int getStatus()
- {
- return status;
- }
- //处理按键
- void processInput()
- {
- char ch = getch();
- //将字母转化为大写
- if(ch >= 'a' && ch <= 'z')
- {
- ch -= 32;
- }
- if(status == S_NORMAL)//如果游戏正常进行
- {
- bool updated = false;//用来判断盘子中的数是否移动了或者合并了
- if(ch == 'A')
- {
- //向左移动
- updated = moveLeft();//如果盘子中的数发生了变化,会返回true,否则返回false
- }
- else if(ch == 'D')
- {
- //向右移动
- rotate();
- rotate();
- updated = moveLeft();
- rotate();
- rotate();
- }
- else if(ch =='W')
- {
- //向上移动
- rotate();
- updated = moveLeft();
- rotate();
- rotate();
- rotate();
- }
- else if(ch == 'S')
- {
- //向下移动
- rotate();
- rotate();
- rotate();
- updated = moveLeft();
- rotate();
- }
- if(updated)//如果盘子中的数发生了变化,就在所有的空位置中,随机产生一个数字
- {
- randNew();//在盘子中的空位置中的随机一个位置产生一个新数
- if(isOver())//判断盘子中是否还有空位置,相邻元素之间是否还能合并
- {
- status = S_FAIL;
- }
- }
- }
- if(ch == 'Q')//判断是否退出游戏
- {
- status = S_QUIT;
- }
- else if (ch == 'R')//判断是否重新开始游戏
- {
- restart();
- }
- }
- //绘制游戏界面
- void draw()
- {
- //清理屏幕
- clear();
- //居中偏移
- //const int offs`et = 18;
- for(int i = 0; i<=N; ++i)
- {
- for(int j = 0; j<=N ;++j)
- {
- //绘制相交点
- drawch(i*2, j*W + M, '+');
-
- //绘制竖线
- if(i != N)
- {
- drawch(i*2+1, j*W + M, '|');
- }
-
- //每个格子中的数字
- if(i!=N && j!=N)
- {
- drawNum(i*2+1, (j+1)*W - 1 + M, num[i][j]);
- }
-
- //绘制横线
- for(int k=0; j != N && k<N; ++k)
- {
- drawch(i*2, 1 + j*W + M + k,'-');
- }
- }
- }
- mvprintw( 2*(N + 1), 2*N,"W(UP),S(DOWN),A(LEFT),D(RIGHT),R(RESTART),Q(QUIT)");
- if(status == S_WIN)
- {
- mvprintw(2*(N + 2), 4*N,"YOU WIN, PRESS R TO CONTINUE");
- }
- else if(status == S_FAIL)
- {
- mvprintw(2*(N + 2), 4*N,"YOU LOSE, PRESS R TO CONTINUE");
- }
- }
- private:
- //判断游戏是否结束
- bool isOver()
- {
- for(int i = 0;i<N;++i)
- {
- for(int j = 0; j<N;++j)
- {
- //有空位或相邻相同 两个有一个成立
- if((j+1<N)&&(num[i][j]*num[i][j+1] == 0 || num[i][j]==num[i][j+1]))
- {
- return false;
- }
- if((i+1<N)&&(num[i][j]*num[i+1][j] == 0 || num[i][j] == num[i+1][j]))
- {
- return false;
- }
- }
- }
- return true;
- }
- //数组逆时针旋转90度
- void rotate()
- {
- int tmp[N][N] = {0};
- for(int i = 0;i<N;++i)
- {
- for(int j = 0; j<N;++j)
- {
- tmp[i][j] = num[j][N-1-i];
- }
- }
- for(int i = 0;i<N;++i)
- {
- for(int j = 0; j<N;++j)
- {
- num[i][j] = tmp[i][j];
- }
- }
- }
- //向左移动
- bool moveLeft()
- {
- int tmp[N][N];//保存移动之前的数组的值
- for(int i=0; i< N; ++i)
- {
- //逐行处理数据
- int currentWritePos = 0;//记录合并数字的前一个数字的位置
- int lastValue = 0;//记录合并数字的前一个数字的值
- for(int j=0; j<N;++j)
- {
- tmp[i][j] = num[i][j];
- if(num[i][j] == 0)
- {
- continue;//如果值为0,跳过此次循环,不执行下列操作,不为0,继续下列操作
- }
- if(lastValue == 0)
- {
- lastValue = num[i][j];//记录相同两个数合并时的前一个数字的值
- }
- else
- {
- if(lastValue == num[i][j])
- //如果lastValue的值与下一个不为0的值相同
- {
- num[i][currentWritePos] = lastValue*2;//这两个数的值相加
- lastValue = 0;//lastValue的值清空
- if(num[i][currentWritePos] == TARGET)//判断合并后的数字是否是2048,也就是判断是否胜利
- {
- status = S_WIN;
- }
- }
- //如果lastValue的值与下一个不为0的值相同
- else//如果lastValue的值与下一个不为0的值不相同
- {
- num[i][currentWritePos] = lastValue;//将lastValue对应的盘子中的数字向前移动
- lastValue = num[i][j];//lastValue的值更新为下一个不为0的值
- }
- ++currentWritePos;//执行到这一步说明num[i][currentWritePos]对应的盘子中的位置已有数值,所以currentWritePos的值加一,对应的盘子中的位置向右移动一位
- }
- num[i][j] = 0;//原本的值,要么向前移动了,要么和前面不为0的值合并了,所以要将此处的值赋值为0
- }
- if(lastValue != 0)//本行已经遍历到最后一个位置了,如果lastValue的值不为0,本行的后面没有值可以和lastValue合并了,所以将其赋值给currentWritePos对应的位置
- {
- num[i][currentWritePos] = lastValue;
- }
- }
- //判断数组的数据是否发生了变化,如果发生了变化,说明此次操作引起了数组的数据发生了移动或者合并,然后盘子中会产生新的数字,反之,说明盘子中的数没有移动,也没有合并,也就是此次操作没有引起变化,盘子中就不能随机生成新的数
- for(int i = 0; i < N;++i)
- {
- for(int j = 0; j < N;++j)
- {
- if(num[i][j] != tmp[i][j])
- {
- return true;
- }
- }
- }
- return false;
- }
- // 重新开始
- void restart()
- {
- for (int i = 0; i < N; ++i)
- {
- for (int j = 0; j < N; ++j)
- {
- num[i][j] = 0;//将数组元素初始化为0
- }
- }
- randNew();//随机产生一个数字
- randNew();
- status = S_NORMAL;
- }
- // 随机产生一个新的数字
- bool randNew()
- {
- vector<int> emptyPos;
- // 把空位置先存起来
- for (int i = 0; i < N; ++i)
- {
- for (int j = 0; j < N; ++j)
- {
- if (num[i][j] == 0)
- {
- emptyPos.push_back(i * N + j);
- }
- }
- }
- if (emptyPos.size() == 0)
- {
- return false;
- }
- // 随机找个空位置
- int value = emptyPos[rand() % emptyPos.size()];
- // 10%的概率产生4
- num[value / N][value % N] = rand() % 10 == 1 ? 4 : 2;
- return true;
- }
- //初始化数组
- void setTestData()
- {
- for(int i = 0; i < N; ++i)
- {
- for(int j = 0; j<N; ++j)
- {
- num[i][j] = 8<<i<<i<<j;//左移一位也就是将数字乘2,移动N位,乘以2的N次方
- }
- }
- }
- void drawch(int row, int col, char c)
- {
- move(row, col);
- addch(c);
- }
- //游戏中的数字是右对齐,row,col是数字最后一位所在的位置
- void drawNum(int row, int col, int num)
- {
- while(num>0)
- {
- drawch(row, col, num%10+'0');
- num /= 10;
- --col;
- }
- }
- private:
- int status;
- int num[N][N];
- };
- void initialize()
- {
- //ncurse初始化
- initscr();
- //按键不需要输入回车直接交互
- cbreak();
- //按键不显示
- noecho();
- //隐藏光标
- curs_set(0);
- // 随机数
- srand(time(NULL));
- }
- void shutdown()
- {
- //ncurses清理
- endwin();
- }
-
- int main()
- {
- initialize();
- Game game;
- do
- {
- game.draw();
- game.processInput();
- }while(game.getStatus() != S_QUIT);
-
- shutdown();
- return 0;
- }
-
运行结果,以及部分操作图:
好吧,我玩不到2048。。。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。