开发要点
- 一次只能合并相邻的两个数字,例如 [2 2 2 2] ,向右合并以后是 [空 空 4 4] ,不是 [空 空 空 8]
- 每次合并的时候,合并方向优先级高,例如 [空 2 2 2],向右合并以后是 [空 空 2 4],不是 [空 空 4 2]
- 判断游戏胜利或者失败
- 每次合并以后随机新出4的概率10%
开发步骤
- 引入curses库
- 绘制游戏界面
- 游戏状态切换
- 重启初始化游戏
- 向左移动
- 向其他方向移动
- 游戏胜负判定
扩展
记录最高分
代码
#include <string>
#include <vector>
#include <cstdlib> // srand() rand()
#include <cstdio> // ?
#include <ctime>
#include <curses.h>
using namespace std;
// 格子数
#define N 4
// 每个格子的字符长度
#define WIDTH 5
// 游戏状态
#define S_FAIL 0
#define S_WIN 1
#define S_NORMAL 2
#define S_QUIT 3
// 胜利条件
#define TARGET 2048
class Game2048
{
public:
Game2048() : status(S_NORMAL)
{
setTestData();
}
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();
// 向下移动等同于把下方旋转成左方,向左移动后再旋转回下方
else if (ch == 'S')
{
rotate(); // 逆时针旋转270度
rotate();
rotate();
updated = moveLeft();
rotate();
}
else if (ch == 'D')
{
rotate();
rotate();
updated = moveLeft();
rotate();
rotate();
}
else if (ch == 'W')
{
rotate();
updated = moveLeft();
rotate();
rotate();
rotate();
}
if (updated)
{
randNew(); // 如果发生改变则产生新随机数
if (isOver()) // 判断游戏是否结束
status = S_FAIL;
}
}
if (ch == 'Q')
status = S_QUIT;
else if (ch == 'R')
restart();
//else
// status = (status + 1) % 3; // 遍历其他三种状态
}
// 绘制游戏界面
void draw()
{
// 清理屏幕
clear();
// 居中偏移
const int offset = 12;
for (int i = 0; i <= N; ++i)
{
for (int j = 0; j <= N; ++j)
{
// 相交点
drawItem(i * 2, j * WIDTH + 1 + offset, '+');
// 竖线
if (i != N)
drawItem(i * 2 + 1, j * WIDTH + 1 + offset, '|');
// 横线
for (int k = 1; j != N && k < WIDTH; ++k)
drawItem(i * 2, j * WIDTH + 1 + k + offset, '-');
// 格子里的数
if (i != N && j != N)
drawNum(i * 2 + 1, (j + 1) * WIDTH + offset, data[i][j]);
}
}
// 提示文字
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, 12 + 5 * (N - 4) / 2, "https://www.nowcoder.com");
// 输赢显示
if (status == S_FAIL)
mvprintw(N, 5 * N / 2 - 1, "YOU FAIL,PRESS R TO RESTART");
if (status == S_WIN)
mvprintw(N, 5 * N / 2 - 1, "YOU WIN,PRESS R TO RESTART");
}
// 设置测试数据
void setTestData()
{
for (int i = 0; i < N; ++i)
{
for (int j = 0; j < N; ++j)
{
data[i][j] = 16 << i << j; // 16*2^i*2^j
// data[i][j] = 2 << (1 + rand() % 7);
}
}
}
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 currentWriteIndex = 0;
// 保存两个数字组成对组的第一个数字
int lastValue = 0;
for (int j = 0; j < N; ++j) // 考虑3种情况
{
tmp[i][j] = data[i][j];
if (data[i][j] == 0) // 1
continue;
if (lastValue == 0) // 2
lastValue = data[i][j];
else // 3
{
if (lastValue == data[i][j])
{
data[i][currentWriteIndex] = lastValue * 2;
lastValue = 0;
if (data[i][currentWriteIndex] == TARGET) //出现2048则胜利
status = S_WIN;
}
else
{
data[i][currentWriteIndex] = lastValue;
lastValue = data[i][j];
}
++currentWriteIndex;
}
data[i][j] = 0; // 把查过的值赋0
}
if (lastValue != 0)
data[i][currentWriteIndex] = lastValue;
}
for (int i = 0; i < N; ++i)
{
for (int j = 0; j < N; ++j)
{
if (data[i][j] != tmp[i][j])
return true;
}
}
return false;
}
// 逆时针旋转90度
void rotate()
{
int tmp[N][N];
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.empty())
return false;
// 随机取一个空的位置
int value = emptyPos[rand() % emptyPos.size()];
// 新产生2或4,4的概率为10%
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);
}
// 游戏里的数字是右对齐,row col是数字最后一位所在的位置
void drawNum(int row, int col, int num)
{
while (num > 0)
{
drawItem(row, col, num % 10 + '0');
num /= 10;
--col;
}
}
private:
int data[N][N]; // 映射数组
int status;
};
void initialize()
{
// 初始化屏幕
initscr();
// 按键不需要输入回车直接交互
cbreak();
// 按键不显示
noecho();
// 隐藏光标
curs_set(0);
// 随机数
srand(time(NULL));
}
void shutdown()
{
// ncursrs清理
endwin();
}
int main()
{
initialize();
Game2048 game;
do
{
game.draw();
game.processInput();
} while (game.getStatus() != S_QUIT);
shutdown();
return 0;
}
总结
- 游戏开发要分清每一个步骤要实现的小功能(简单的画字符函数=》界面绘制=》游戏状态=》具体的实现逻辑),同时设置合理的测试函数去检验功能的实现完善与否。
- 注意宏定义的使用以方便后续程序的修改与扩展。
- 使用一个类来囊括所有应该包括的功能。