当前时间:2021/3/1
项目网址:https://www.nowcoder.com/project/index/8
源码及教程(readme):https://git.nowcoder.com/68/2048
起步
安装依赖
# 依赖库
- linux: apt-get install libncurses5-dev
- Mac: brew install ncurses
- Windows:
1. 安装编译器MinGW,https://sourceforge.net/projects/mingw-w64/files/mingw-w64/mingw-w64-release/ 下载mingw-w64-install.exe(注意是exe,擅用ctrl+F) 5.0.4版本,解压到本地目录,例如 C:\mingw64,然后把C:\mingw64\bin 加入到系统设置的路径里,打开命令行控制台输入g++,确认有这个命令以保证安装是成功
2. 编译pdcurses库,https://sourceforge.net/projects/pdcurses/files/pdcurses/3.6/pdcurs36.zip/download 下载pdcurses后解压到C:\pdcurs36目录,命令行控制台cd到 C:\pdcurs36\wincon目录,运行 mingw32-make 命令编译pdcurses库,编译成功后目录下有多个demo的exe文件以及一个pdcurses.a文件,这个文件是库文件。
安装MinGW:首先确定下载的是exe,双击通过在线安装时,version选择5.1.0,架构选择x86-64,threads选择win32。安装完成后在cmd中输入gcc -v产生如下输出即可(输入g++后显示 fatal error: no input files,这是正确的,它在提示你没有给 g++ 命令一个输入)
下载pdcurses库并编译:没啥坑,找准目录即可,编译时cmd中节取如下:
运行
在【源码及教程(readme):https://git.nowcoder.com/68/2048】下载代码,在2048.cpp运行下述命令
g++ 2048.cpp C:\pdcurs36\wincon\pdcurses.a -I C:\pdcurs36\ -o 2048
编译成功后目录下出现2048.exe,双击后按R即可游戏
代码结构
仅一个cpp文件,300行的代码,其中一个类、两个函数、一个main函数
- class:Game2048
- 初始化函数 initialize()
- 关闭 shutdown()
- main()
- initialize() // 设置图形交互界面
- 初始化class: Game2048
- public:
- processInput() //处理按键
- draw() // 绘制界面
- setTestData() // 设置测试数据
- private:
- isOver() // 判断游戏结束
- moveLeft() // 向左边移动
- rotate() // 矩阵逆时针旋转90°
- restart() // 重新开始
- randNew() // 随机产生一个新的数字
- drawItem() // 左上角为原点,在指定位置绘制 char 字符
- drawNum() // 在指定位置绘制 int 字符
- public:
- 当 S_QUIT != game.getStatus() 时,循环 game.draw(); game.processInput();
- 结束shutdown()
项目运行样例
实现细节分析
想象一下2048的交互,无非以下几点:
- 读取用户输入,此输入仅有四种:上 下 左 右,其他输入视为无效
- 输入后,将数字按照输入方向移动到尽可能远的位置
- 当两个数字重复时合二为一
- 每次输入后在一个随机位置出现一个新的数字(数值不会太大)
- 作为一个游戏,应该有开始、结束,以及图形交互界面
下面将功能点和代码实现对应,如下:
1.读取用户输入
4.每次输入后在一个随机位置出现一个新的数字(数值不会太大)
class2048 - public - void processInput():
这里的要点有二:
- 只定义了左移动一个操作(函数),上移、下移、右移的实现依赖左移动和旋转矩阵实现
- 定义FLAG:updated,初始化为false;其接收 移动操作 的返回值,为true时 生成新的随机数 并 判断游戏是否结束
// 处理按键
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') {
// 向下移动 = 旋转270度,向左移动,再旋转90度
rotate();
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();
}
}
此外还需注意,上面代码在盘面发生变化后调用了randNew(),实现在随机位置出现新数字:
// 随机产生一个新的数字
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;
}
2.输入后,将数字按照输入方向移动到尽可能远的位置
3.当两个数字重复时合二为一
class2048 - public - bool moveLeft():
这里可以说是本项目的算法核心,需要完成的功能有以下几点
- 将数字左移到尽可能远的位置
- 若与左侧数字相同则将其合并
- 生成一个新的随机数字并加到矩阵中的随机位置
看一下代码,使用逐行计算,每一行从左到右遍历,其中维持两个变量:
- currentWritePos:待写格子
- lastValue:记录每一行的第一个与左值不同的值,需要注意的是,修改后的格子里的值是根据这个变量计算的
记录第一个非零值到 lastValue,若第二个非零值与lastValue相同则进行合并处理,若不同则仅更新lastValue
currentWritePos值得思考,因为我们遍历过程中的“当前”格子的标号用 j 表示,与“待写”格子是相区别的:
当当前格子为空时,不改变待写格子的位置;当当前格子非空时,将其数值加入lastvalue,不改变待写格子位置;当lastvalue非空、当前格子非空,判断两者数值是否相同,编辑待写格子内容并右移待写格子位置。
遍历过一行后,需要判断一下lastvalue是否有值,若有则需要将其填如待写
遍历完一个表格后,判断修改后的矩阵(data)和修改前的矩阵(tmp)是否相同,若不同(即发生了变化)则返回true,否则返回false
// 向左边移动, 返回值表示盘面是否有发生变化
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] = data[i][j];
if (data[i][j] == 0) {
continue;
}
if (lastValue == 0) {
lastValue = data[i][j];
} else {
if (lastValue == data[i][j]) {
data[i][currentWritePos] = lastValue * 2;
lastValue = 0;
if (data[i][currentWritePos] == TARGET) {
status = S_WIN;
}
} else {
data[i][currentWritePos] = lastValue;
lastValue = data[i][j];
}
++currentWritePos;
}
data[i][j] = 0;
}
if (lastValue != 0) {
data[i][currentWritePos] = 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;
}
5.作为一个游戏,应该有开始、结束,以及图形交互界面
逻辑呈现在main中,设置了表示游戏状态的标记位
// 游戏状态
#define S_FAIL 0
#define S_WIN 1
#define S_NORMAL 2
#define S_QUIT 3
每次处理用户输入后判断程序状态是否为 S_QUIT,若是则退出循环。
循环中执行draw绘图以及处理用户输入。
14:51完成