主要考察
模块划分; 文本文件读取 ;打印到控制台测试 ;
UI与业务分离 ;控制台交互 ;文件读取; 数据抽象;
程序运行效果
需求
用户输入字母表示方向,实现贪吃蛇游戏;
方向键通过输入字符(W A S D)来表示(上 左 下 右);
蛇的身体需要显示为 #;蛇的头需要显示为 @ ; 食物需要显示为 $;
规则:贪吃蛇在向前移动的时候碰到边缘和碰到蛇自己都算游戏结束;
输入文件 data.txt
data.txt 内容如下:
6 7
0 0 0 0 0 0 0
0 0 2 0 2 0 0
0 0 0 0 0 0 0
0 0 0 0 0 2 0
0 0 0 0 0 1 0
0 0 0 0 0 0 0
第一行,包括两个整数,表示游戏棋盘大小。分别表示行数和列数。
例如,上图中表示游戏大小为6行,7列。
后面的内容是一个行数乘以列数的二维数组。
数组的元素为 0 表示这里什么也没有。
数组的元素为 1 表示蛇的头。程序开始的时候,蛇没有身体,只有头。
数组的元素为 2 表示食物。程序开始的时候,可以有多个食物。
例如,上图中表示,蛇一开始位于棋盘的 第 5 行,第 6 列。同时有 3 个食物。
实现思路
文件加载
文件加载只需要按照文件格式的规定,读取对应的信息保存在内存模型变量中即可。
内存模型
内存模型设计的合理,符合对事物本周的抽象,程序代码就简单,易于理解。
反之,代码就会晦涩难懂。
游戏盘面二维数组
我们需要一个二维数组来存储游戏盘面。这个二维数组可以用 vector<vector<char>> m_playBoard 来表示。
蛇的身体队列
蛇会越来越长,身体的每个部分我们不希望只是单独的放在游戏盘面上。因为这样意味着每次更新蛇的位置的时候,找蛇的身体的每个位置都非常的麻烦。
我们把蛇的身体单独存放一份,放到一个队列里 list<pair<int, int>> m_snakeBody 。
链表的 front 表示蛇的头,back 表示蛇的尾巴。
蛇的移动
如上图所示,蛇的身体全部都用 1 表示。
那蛇的头呢?我们在绘制的时候单独处理蛇的头。这样有利于简化代码的设计。
实际上我们移动蛇的时候,只需要将尾巴上的 1 搬到 蛇的头的下一个将要移动到的地方就完成了蛇的整体移动。
这也是为何我们并没有在蛇的身体里用不同的值区分蛇的身体不同部位的原因。
如上图所示,蛇头向下方移动了一个位置,我们把原来尾巴搬到了蛇头新位置,就完成了蛇整体的移动。
这样做的好处就是,蛇的移动每次只需要搬蛇身体的尾巴一个元素。
移动蛇的身体总是固定的常数时间。计算量最大限度的降低了。程序也变的简单了。
游戏盘面和蛇身体的同步
由于蛇的移动变成了游戏盘面上把蛇的尾巴上的 1 搬运到蛇的头部的下一个将要移动到的位置,所以蛇的身体队列 m_snakeBody 里只需要存储蛇的身体的每一个元素在游戏盘面上的位置即可。
这就是为何 m_snakeBody 的每一个元素都是一个 pair<int, int> 原因了。
游戏控制
用户输入
玩家在键盘上输入一个表示方向的字母,这样程序就知道蛇应该往哪里移动了。
使用 GoAhead 来实现往前走一步。
游戏结束
当蛇往前走碰到了墙壁(超出了游戏盘面)的时候,游戏结束。
当蛇往前走碰到了自己的身体的一部分,游戏结束。
枚举类型 enum class
enum class 通常用来提供字面值常量。也就是一些固定值。比如,表示方向的东、南、西、北。表示空间的上、下、左、右、前、后。
在本游戏中,我们用来表示盘面上物品的类型:什么也没有,蛇的身体,食物。
enum 类型转换
枚举类型本身是数值类型,可以转换为 char , int 等数值类型。
例如:
char value = static_cast<char>(MatrixValueEnum::FOOD);
启动代码
#include <list>
#include <utility>
#include <fstream>
#include <sstream>
#include <iostream>
#include <random>//随机数
#include <chrono>//日期时间
#include <cassert>
using namespace std;
// 游戏的任意位置 只有三种情况:什么也没有;蛇的身体;食物
enum class MatrixValueEnum
{
NOTHING = '0', SNAKE_BODY = '1', FOOD = '2'
};
class Model
{
public:
size_t GetRow(void) { return m_playBoard.size(); }
size_t GetCol(void) { return m_playBoard.at(0).size(); }
void EatFood(pair<int, int> nextPosition)
{
//new snake head
m_snakeBody.push_front(nextPosition);
//直接吃掉,尾巴不用移动
m_playBoard[nextPosition.first][nextPosition.second] = static_cast<char>(MatrixValueEnum::SNAKE_BODY);
}
bool PushFoodAt(int row, int col)
{
bool result = false;
if (m_playBoard[row][col] == static_cast<char>(MatrixValueEnum::NOTHING))
{
m_playBoard[row][col] = static_cast<char>(MatrixValueEnum::FOOD);
result = true;
}
return result;
}
// 判断 (i,j) 处是否是一个食物
bool ExistFood(int row, int col) const
{
//返回 坐标(i,j)处是否是有蛇的食物可以吃
//(8) your code
return false;//wrong! you have to change it.
}
void IncreaseOnlyBody(pair<int, int> nextPosition)
{
m_snakeBody.push_front(nextPosition);
}
void AppendToBoard(const vector<char>& lineData)
{
m_playBoard.push_back(lineData);
}
//撞到墙壁或者蛇自己的身体就结束游戏
bool IsGameOver(int row, int col) const
{
//判断游戏是否已经结束了
// x y 是蛇的头打算要去的目的地,这个目的地会导致gomeover
// 比如超出了游戏界面(下标越界)
// 比如撞到了蛇的身体
//(5) your code : row out of range, col out of range, snake body ahead
return
false
;
}
// 计算蛇的头移动一次后的新坐标
std::pair<int, int> GetNextPosition(int row, int col) const
{
//根据蛇的头的位置,以及一个移动的向量 (i,j) 得到蛇头部打算要去的新目的地的坐标
auto old = GetCurrentPosition();
//(7) your code
auto x = -1;//wrong! you have to change it.
auto y = -1;//wrong! you have to change it.
return std::make_pair(x, y);
}
// 获取蛇的头的坐标
std::pair<int, int> GetCurrentPosition(void) const
{
//返回蛇 的头的坐标,是m_snakeBody的第一个元素的值
//(6) your code : snake body 's front element
auto front = pair<int, int>(-1, -1);//wrong! you have to change it.
return front;
}
void MoveOneStepTo(pair<int, int> nextPosition)
{
//snake tail move to snake new head
//new snake head
//尾巴移动
auto head = nextPosition;
auto tail = m_snakeBody.back();
m_playBoard[tail.first][tail.second] = static_cast<char>(MatrixValueEnum::NOTHING);
m_playBoard[head.first][head.second] = static_cast<char>(MatrixValueEnum::SNAKE_BODY);
m_snakeBody.push_front(head);
m_snakeBody.pop_back();
}
// 在界面上生成一个新的食物给蛇吃
void CreateFood(void)
{
// 生成一个新的食物给蛇来吃
// 随机生成一个新的位置,但是这个位置可能已经是蛇的身体了
// 所以,需要用一个循环不断的重复在一个新生成的随机位置放置食物
// 直到放置成功为止
do
{
auto seed = std::chrono::system_clock::now().time_since_epoch().count();
std::mt19937 g(seed); // mt19937 is a standard mersenne_twister_engine
//生成新的随机的坐标
//随机数的用法:https://blog.csdn.net/calmreason/article/details/72655060
//(9) your code
int row = g() % GetRow();
int col = -1;//wrong! you have to change it.
// 在新坐标处放置一个食物,记得检查可以放才能放
// 一旦放好,记得退出循环,让程序继续执行
//(10) your code : push food at row, col. if ok , break this loop.
} while (true);
}
//this function help your find bug easyly
void ShowBoardForTest(ostream& os) const
{
for (size_t row = 0; row < m_playBoard.size(); row++)
{
for (size_t col = 0; col < m_playBoard[row].size(); col++)
{
auto element = m_playBoard[row][col];
os << element << " ";
}
os << std::endl;
}
}
void ShowGame(ostream& os) const
{
auto headPosition = m_snakeBody.front();
for (size_t row = 0; row < m_playBoard.size(); row++)
{
for (size_t col = 0; col < m_playBoard[row].size(); col++)
{
auto element = m_playBoard[row][col];
//打印蛇的头,其实这个位置的内存值是蛇的身体;存储的时候部分蛇的头和身的身体
//,只有显示的时候才通过在队列中的位置区分
if (row == headPosition.first && col == headPosition.second)
{
os << "@" << " ";
}
//打印食物,内存中的 '2' 打印成 $
else if (element == static_cast<char>(MatrixValueEnum::FOOD))
{
os << "$" << " ";
}
//打印空白,内存中的 '0' 打印成 '_'
else if (element == static_cast<char>(MatrixValueEnum::NOTHING))
{
os << "_" << " ";
}
// snake body
else if (element == static_cast<char>(MatrixValueEnum::SNAKE_BODY))
{
os << "#" << " ";
}
else
{
assert(false);
}
}
os << std::endl;
}
}
public:
vector<vector<char>> m_playBoard;// 整个游戏的数据(二维数组)
list<pair<int, int>> m_snakeBody;// 蛇的身体数据
};
class Control
{
public:
// 用户输入一个字符(e/s/f/d),决定将蛇的头部往哪个方向移动
//bool GoAhead(char userInputDirection);// 核心函数
// 移动蛇的头的坐标(x,y) = (x,y) + (i,j)
bool GoAhead(char userInputDirection)
{
switch (userInputDirection)
{
case 'w':
case 'W':
return GoAhead(-1, 0);//up : row decrease 1, col not change
case 'a':
case 'A':
return GoAhead(0, -1);//left
case 'd':
case 'D':
return GoAhead(0, +1);//right
case 's':
case 'S':
return GoAhead(+1, 0);//down
default:
return true;
}
}
bool LoadPlayDataFromFile(istream& fin)
{
std::string line;
std::getline(fin, line);
std::istringstream iss(line);// 字符串流 https://zhuanlan.zhihu.com/p/441027904
int row_count = 0, column_count = 0;
//读取行数和列数
//(1) your code
//read board size : row and col .
//see https://blog.csdn.net/calmreason/article/details/126686679
for (int row = 0; row < row_count; row++)
{
std::vector<char> lineData;
std::getline(fin, line);
std::istringstream lineDataIss(line);
for (int column = 0; column < column_count; column++)
{
char element;
//读取一个元素
// (2) your code
// read data from istringstream , and store it to line data
// see : https://blog.csdn.net/calmreason/article/details/126736091
//将组成蛇的头1存放到蛇m_snakeBody容器中,蛇一开始没有身体,只有一个蛇头
//判断两个char相等即可
if (element == '1')
{
//(3) your code IncreaseOnlyBody
}
}
//将第一行数据存放到二维数组中,作为第一维的一个元素(子数组)
//(4) your code : AppendToBoard lineData
}
if (m_model.m_snakeBody.empty())
{
cout << "snake body is empty! init game failed." << endl;
assert(false);
return false;
}
return true;
}
bool GoAhead(int row, int col)
{
auto nextPosition = m_model.GetNextPosition(row, col);//垂直方向x不变,竖直方向y减少1
// 首先判断游戏是否已经结束
if (m_model.IsGameOver(nextPosition.first, nextPosition.second))
{
return false;
}
// 判断nextPosition 处是否有食物
// 如果有食物,就吃掉这个食物
// 并生成一个新的食物
if (m_model.ExistFood(nextPosition.first, nextPosition.second))
{
// (11) your code
// EatFood at nextPosition
// create one food use CreateFood
}
// 如果 nextPosition 处没有食物,就移动蛇的身体
else
{
// (12) your code
// MoveOneStepTo nextPosition
}
return true;
}
Model m_model;
};
class Snake
{
public:
// 从文件中加载界面数据,存放到内部容器中,再根据容器内容绘制界面
bool LoadPlayDataFromFile(const std::string& file)
{
std::ifstream fin(file);
if (!fin)
{
std::cout << "can not open file " << file << endl;
return false;
}
auto result = m_control.LoadPlayDataFromFile(fin);
return result;
}
// 开始游戏
void Play(void);
private:
// 打印贪吃蛇游戏,将界面和内存之间的数据对应起来
// 界面 内存
// '_' <== '0'
// '$' <== '2'
// '#' <== '1'
// '@' <== '1'
void PrintMatrix(void) const
{
//test code, remove it while your code is finished.
m_control.m_model.ShowBoardForTest(cout);
m_control.m_model.ShowGame(cout);
}
private:
Control m_control;
};
void Snake::Play(void)
{
while (true)
{
/*清屏,这不是C++的一部分,是系统调用。
这个语句执行的快慢与代码无关,与控制台用户自己设置的缓冲区大小有关。
*/
system("cls");
PrintMatrix();
std::cout << "direction: W(up) A(left) S(down) D(right)\n";
std::cout << "$: food\n";
std::cout << "@: snake head\n";
std::cout << "#: snake tail\n";
char direction;
std::cin >> direction;
//往前走一步,如果判断无法往前走到用户指定的位置,就退出程序
if (!m_control.GoAhead(direction))
{
std::cout << "Game Over!" << std::endl;
break;
}
}
}
int main(int argc, char** argv)
{
Snake snake;
if (snake.LoadPlayDataFromFile("data.txt"))
{
snake.Play();
}
else
{
cout << "LoadPlayDataFromFile data.txt failed!" << endl;
assert(false);
}
return 0;
}
开始吧!
如需答案和答疑:请私信。