这是笔者用作练习C++的一个小项目,框架思路和程序上很大程度借鉴了牛客网-项目实战-2048小游戏,并在此基础上进行了功能上的拓展,增加了记录历史最高成绩和当前玩家得分两个功能。下面进行介绍
码云:https://gitee.com/hinzer/my-notes-of-C_plus/tree/master/project/
项目界面
我的系统环境是CentOS7.x,编译运行项目前,需要安装依赖
sudo yum install libncurses5-dev
编译命令
g++ demo.cpp -l ncurses -o 2048
一、问题描述
对于整个项目,从IPO(输入-处理-输出)的角度来看这个游戏
1.输入:用户按按键
2.处理规则:
一次只能合并相邻的两个数字,例如 [2 2 2 2] ,向右合并以后是 [空 空 4 4] ,不是 [空 空 空 8]
每次合并的时候,合并方向优先级高,例如 [空 2 2 2],向右合并以后是 [空 空 2 4],不是 [空 空 4 2]
判断游戏胜利或者失败
每次合并以后随机新出4的概率10%
3.输出:数字元素的动态变化和用户提示语句形成的图形界面
二、问题分析
1.随机产生两个数字元素在这个"方阵"上,通过按键的上下左右来控制数字的移动,实现数字的合并。因此数字特点是离散的、和二维的。
2.每次按下按键的时刻和键值是不确定的,将按键按下视为一个事件,然后程序再进行判断。因此是事件驱动模型。
3.事件的主要信息是,按键按下产生的键值和之后对二维数字方阵的结果。因此用二维数组保存最终的二维数据。
4.游戏主要是根据二维数组或者玩家的动作(按下按键)来判断游戏玩家的状态。因此用变量来记录玩家的状态。
综上,定义一个类Game2048来描述这个游戏,通过变量status和二维数组data来表示这个游戏的属性,再有就是类的方法表示游戏的动作,比如绘制图形界面相关方法、重启初始化方法、上下左右移动方法、判断游戏结束方法。
三、开发步骤
Step1.引入curses库 终端内绘制简单的图形用户界面,这里先让其对窗口进行一系列的初始化。
Step2.绘制游戏界面 将界面框架绘制好,具体在实现draw方法的时候,利用循环结构填充字符框架,除了布局整洁之外,还要注意保持合适的间距,后面用来填充数字元素。
Step3.游戏状态切换 实现processInput方法,捕获用户按下按键的键值,能够判断QRWASD几个字母,后面会实现具体的逻辑功能。
Step4.重启初始化游戏实现restart方法,当用户按下R键时,清空界面并随机生成两个数值元素作为初始元素。在随机生成数值元素的处理程序中,先定义一个vector容器,存储当前数组中的空元素的位置(将二维坐标转换成一维坐标值表示),然后在随机选择空位置上生成2or4的元素(使用rand()函数生成随机数,用时间作为随机数种子)。
Step5.向左移动 逐行遍历每一个元素data[i][j],将前一个元素设定为比较值compareValue,进行相邻元素之间的比较。如果相邻元素值相等,将结果当前要写入的位置currentWritePos,重新设定比较值compareValue并进行下一轮比较;如果相邻元素值不相等,就将比较值写回当前要写入的位置currentWritePos,并跟新比较值compareValue,进行下一次比较。直到这行遍历完成,将比较值再写回当前要写入的位置currentWritePos(这是为了保证右边的元素不会被"清除")
Step6.向其他方向上移动 这里用了一个颇为巧妙的方法,将数组逆时针翻转90度之后的新数组,再进行step5中的想左移动,相当于原数组向上移动。将移动后的结果再翻转回去,就有了向其他方向上的移动效果。
Step7.游戏判断胜负 任意地方出现2048数字,就算赢;如果矩阵被填满并且不能再移动,就算输。具体通过查询或相邻元素比较来判断。
四、拓展
记录历史最高分,并且显示当前玩家得分。
我这里通过读写文件的方式,完成玩家历史得分的显示和记录。每次有方向移动过程中,如果一对数字组合成功,就加10分。在图形界面刷新过程中,实时显示当前玩家分数为当前分数,和读取文件的分数为历史最高。判断玩家分数是否大于历史分数,是的话就跟新文件中记录的最大值。
五、完整程序
#include <string>
#include <vector>
#include <cstdlib>
#include <cstdio>
#include <ctime>
#include <curses.h>
#include <fstream>
using namespace std;
// 格子数
#define N 4
// 每个格子的字符长度
#define WIDTH 5
// 胜利条件
#define TARGET 2048
// 游戏状态
#define S_FAIL 0
#define S_WIN 1
#define S_NORMAL 2
#define S_QUIT 3
class Game2048
{
public:
Game2048():status(S_NORMAL)
{
myScore = 0;
setTestData();
}
int getStatus()
{
return status;
}
//处理按键
void processInput()
{
char ch = getch();
bool updated = false;
if( 'a' <= ch <= 'z' )
{
ch = ch -32;
}
if(ch == 'Q')
{
status = S_QUIT;
}
else if (ch == 'R')
{
restart();
}
else if (ch == 'A')
{//向左
updated = moveLeft();
}
else if (ch == 'W')
{//向上
rotate();
updated = moveLeft();
rotate();
rotate();
rotate();
}
else if (ch == 'D')
{//向右
rotate();
rotate();
updated = moveLeft();
rotate();
rotate();
}
else if (ch == 'S')
{//向下
rotate();
rotate();
rotate();
updated = moveLeft();
rotate();
}
if (updated)
{
randNew(); //随机产生一个新元素
if (isOver())
{
status = S_FAIL;
}
}
}
// 绘制游戏界面
void draw()
{
// 清理屏幕
clear();
// 居中偏移
const int offset = 12;
for (int i = 0; i <= N; ++i)
{
for (int j = 0; j <= N; ++j)
{
// 相交点
drawItem(i * 2, 1 + j * WIDTH + offset, '+');
// 竖线
if (i != N)
{
drawItem(i * 2 + 1, 1 + j * WIDTH + offset, '|');
}
// 横线
for (int k = 1; j != N && k < WIDTH; ++k)
{
drawItem(i * 2, 1 + j * WIDTH + k + offset, '-');
}
// 每个格子里的数
if (i != N && j != N)
{
drawNum(i * 2 + 1, (j + 1) * WIDTH + offset, data[i][j]);
}
}
}
drawString(1,offset-WIDTH,"highest:");
drawNum(1,offset,readDateToFile("score.txt"));
drawString(2,offset-WIDTH,"score:");
drawNum(2,offset,myScore);
// 提示文字
mvprintw(2 * N + 2, (5 * (N - 4) - 1) / 2, "W(UP),S(DOWN),A(LEFT),D(RIGHT),R(RESTART),Q(QUIT)");
mvprintw(2 * N + 3, 5 + 5 * (N - 4) / 2, "My Blog : https://blog.csdn.net/feit2417");
if (status == S_WIN)
{
mvprintw( N, 5 * N / 2 - 1, " YOU WIN,PRESS R TO CONTINUE ");
} else if (status == S_FAIL)
{
mvprintw( N, 5 * N / 2 - 1, " YOU LOSE,PRESS R TO CONTINUE ");
}
if (myScore > readDateToFile("score.txt"))
{
writeDateToFile("score.txt",myScore); //将分数记录到文件
}
}
// 方便调试, 随时设置数据
void setTestData()
{
for (int i = 0; i < N; ++i)
{
for (int j = 0; j < N; ++j)
{
data[i][j] = 16 << (i + j);
// data[i][j] = 2 << (1 + rand() % 7);
/*
data[i][0] = 2;
data[i][1] = 4;
data[i][2] = 8;
data[i][3] = 16;*/
}
}
}
private:
// 判断游戏已经结束
bool isOver()
{
for (int i = 0; i < N; ++i)
{
for (int j = 0; j < N; ++j)
{
// 有空位或者相邻有一样的都可以继续
if ((j + 1 < N) && (data[i][j] * data[i][j+1] == 0 || data[i][j] == data[i][j+1])) return false;
if ((i + 1 < N) && (data[i][j] * data[i+1][j] == 0 || data[i][j] == data[i+1][j])) return false;
}
}
return true;
}
//向左移动
bool moveLeft()
{
int tmp[N][N];
for (int i = 0; i < N; ++i)
{//逐行遍历
int compareValue = 0; //比较值
int currentWritePos = 0; //当前写入的位置
for (int j = 0; j < N; ++j)
{
tmp[i][j] = data[i][j];
if (0 == data[i][j])
{//元素为空,结束本次循环
continue;
}
if (0 == compareValue)
{
compareValue = data[i][j]; //设定比较值
}
else
{
if (compareValue == data[i][j])
{//相邻元素值相等
data[i][currentWritePos] = compareValue*2; //结果写入当前位置
compareValue = 0; //准备下一轮比较
++currentWritePos;
myScore += 10; //跟新分数
}
else
{
data[i][currentWritePos] = compareValue; //将比较值写回当前写入位置
compareValue = data[i][j]; //跟新比较值
++currentWritePos;
}
}
data[i][j] = 0; //清除该位置的记录
}
if (0 != compareValue)
{//考虑最右边的元素,保证不会被"清除"
data[i][currentWritePos] = compareValue;
}
}
for (int i = 0; i <= N; ++i)
{//看看左移前后数组是否有变化
for (int j = 0; j <= N; ++j)
{
if (tmp[i][j] != data[i][j])
return true;
}
}
return false;
}
// 矩阵逆时针旋转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] = data[j][N - 1 - i];
}
}
for (int i = 0; i < N; ++i) {
for (int j = 0; j < N; ++j) {
data[i][j] = tmp[i][j];
}
}
}
// 重新开始
void restart()
{
for (int i = 0; i < N; ++i)
{
for (int j = 0; j < N; ++j)
{
data[i][j] = 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 (data[i][j] == 0)
{
emptyPos.push_back(i * N + j);
}
}
}
if (emptyPos.size() == 0)
{
return false;
}
// 随机找个空位置
int value = emptyPos[rand() % emptyPos.size()];
// 10%的概率产生4
data[value / N][value % N] = rand() % 10 == 1 ? 4 : 2;
return true;
}
// 左上角为(0,0),在指定的位置画一个字符
void drawItem(int row, int col, char c)
{
move(row, col);
addch(c);
}
//在指定位置写一个字符串
void drawString(int row, int col, string str)
{
int num = str.size();
while (num--)
{
drawItem(row, col, str[num]);
--col;
}
}
// 游戏里的数字是右对齐,row, col是数字最后一位所在的位置
void drawNum(int row, int col, int num)
{
while (num > 0) {
drawItem(row, col, num % 10 + '0');
num /= 10;
--col;
}
}
//写数值到文件
void writeDateToFile(const char *path,int value)
{
ofstream of(path);
of << value;
of.close();
}
//读文件中的数值
int readDateToFile(const char *path)
{
ifstream inf(path);
int sb;
inf>>sb;
inf.close();
return sb;
}
private:
int myScore; //玩家得分
int status; //游戏状态
int data[N][N]; //阵盘
};
void initialize()
{
// ncurses初始化
initscr();
// 按键不需要输入回车直接交互
cbreak();
// 按键不显示
noecho();
// 隐藏光标
curs_set(0);
// 随机数
srand(time(NULL));
}
void shutdown() {
// ncurses清理
endwin();
}
int main(int argc, char const *argv[])
{
initialize();
Game2048 game;
do
{
game.draw();
game.processInput();
}while(S_QUIT != game.getStatus());
shutdown();
return 0;
}