这是笔者用作练习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;
}