​7.3 项目3 贪吃蛇(控制台版) (A)​

C++自学精简实践教程 目录(必读)

主要考察

模块划分 / 文本文件读取

UI与业务分离 / 模块划分

控制台交互 / 数据抽象

需求

用户输入字母表示方向,实现贪吃蛇游戏

规则:碰到边缘和碰到蛇自己都算游戏结束

输入文件 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 个食物。

程序输出样式

运行效果

如下图所示,蛇的身体需要显示为 #蛇的头需要显示为 @食物需要显示为 $; 

实现思路

文件加载

文件加载只需要按照文件格式的规定,读取对应的信息保存在内存模型变量中即可。

所以,主要的问题在于应该如何设计内存模型(Model) 。

内存模型

内存模型设计的合理,符合对事物本周的抽象,程序代码就简单,易于理解。

反之,代码就会晦涩难懂。

游戏盘面二维数组

我们需要一个二维数组来存储游戏盘面。这个二维数组可以用 vector<vector<char>> m_playBoard 来表示。

蛇的身体队列

蛇会越来越长,身体的每个部分我们不希望只是单独的放在游戏盘面上。因为这样意味着每次更新蛇的位置的时候,找蛇的身体的每个位置都非常的麻烦。

我们把蛇的身体单独存放一份,放到一个队列里 queue<pair<int, int>> m_snakeBody 。队列是有方向的,这样就可以轻易的知道蛇的头在哪里。

蛇的移动

但是 queue 没有办法遍历元素,这样我们想让蛇往前走一步就好像变的不可能了。

真的是这样吗? 蛇需要每个元素都需要往前移动一步,才能完成蛇走一步吗? 

如上图所示,蛇的身体全部都用  1 表示,实际上我们移动蛇的时候,只需要将尾巴上的 1 搬到 蛇的头的下一个将要移动到的地方就完成了蛇的整体移动。

如上图所示,蛇头向下方移动了一个位置,我们把原来尾巴搬到了蛇头新位置,就完成了蛇整体的移动。

这样做的好处就是,蛇的移动每次只需要搬蛇身体的尾巴一个元素。移动蛇的身体总是固定的常数时间。计算量最大限度的降低了。程序也变的简单了。

游戏盘面和蛇身体的同步

由于蛇的移动变成了游戏盘面上把蛇的尾巴上的 1 搬运到蛇的头部的下一个将要移动到的位置,所以蛇的身体队列 m_snakeBody 里只需要存储蛇的身体的每一个元素在游戏盘面上的位置即可。

这就是为何 m_snakeBody 的每一个元素都是一个 pair<int, int> 原因了。

游戏控制

用户输入

玩家在键盘上输入一个表示方向的字母,这样程序就知道蛇应该往哪里移动了。

使用 GoAhead 来实现往前走一步。

游戏结束 

当蛇往前走碰到了墙壁(超出了游戏盘面)的时候,游戏结束。

当蛇往前走碰到了自己的身体的一部分,游戏结束。

枚举类型 enum class

enum class 通常用来提供字面值常量。也就是一些固定值。比如,表示方向的东、南、西、北。表示空间的上、下、左、右、前、后。

在本游戏中,我们用来表示盘面上物品的类型:什么也没有,蛇的身体,食物。

启动代码

#include <list>
#include <utility>
#include <fstream>
#include <sstream>
#include <iostream>
#include <random>//随机数
#include <chrono>//日期时间
using namespace std;class Snake
{// 游戏的任意位置 只有三种情况:什么也没有;蛇的身体;食物enum class MatrixValueEnum{NOTHING = '0', SNAKE_BODY = '#', FOOD = '2'};
public:// 从文件中加载界面数据,存放到内部容器中,再根据容器内容绘制界面bool LoadPlayDataFromFile(const std::string& file);// 开始游戏void Play(void);
private:// 用户输入一个字符(e/s/f/d),决定将蛇的头部往哪个方向移动bool GoAhead(char userInputDirection);// 核心函数// 移动蛇的头的坐标(x,y) = (x,y) + (i,j)bool GoAhead(int i, int j);//撞到墙壁或者蛇自己的身体就结束游戏bool IsGameOver(int, int) const;// 获取蛇的头的坐标std::pair<int, int> GetCurrentPosition(void) const;// 计算蛇的头移动一次后的新坐标std::pair<int, int> GetNextPosition(int, int) const;// 打印贪吃蛇游戏void PrintMatrix(void) const;// 判断 (i,j) 处是否是一个食物bool ExistFood(int i, int j) const;// 在界面上生成一个新的食物给蛇吃void CreateFood(void);
private:std::vector<std::vector<char>> m_playMatrix;// 整个游戏的数据(二维数组)std::list<std::pair<int, int>> m_snakeBody;// 蛇的身体数据
};bool Snake::LoadPlayDataFromFile(const std::string& file)
{std::ifstream fin(file);if (!fin){std::cout << "can not open file " << file << endl;return false;}std::string line;std::getline(fin, line);std::istringstream iss(line);// 字符串流 https://zhuanlan.zhihu.com/p/441027904int row = 0, column = 0;//读取行数和列数//(1) your codefor (size_t i = 0; i < row; i++){std::vector<char> lineData;std::getline(fin, line);std::istringstream issLineData(line);for (size_t j = 0; j < column; j++){char data;//读取一个元素// (2) your code//将组成蛇的头#存放到蛇m_snakeBody容器中//在文件里,一开始蛇的身体只有一个头,需要把这个数据存起来//(3) your code  判断两个char相等即可// 参考:https://zhuanlan.zhihu.com/p/357348144}//将第一行数据存放到二维数组中,作为第一维的一个元素(子数组)//(4) your code}if (m_snakeBody.size() != 1){cout << "snake body is empty! init game failed." << endl;return false;}return true;
}bool Snake::IsGameOver(int x, int y) const
{//判断游戏是否已经结束了// x y 是蛇的头打算要去的目的地,这个目的地会导致gomeover// 比如超出了游戏界面(下标越界)// 比如撞到了蛇的身体//(5) your codereturn true;
}
std::pair<int, int> Snake::GetCurrentPosition(void) const
{//返回蛇 的头的坐标,是m_snakeBody的第一个元素的值//(6) your code  下面的代码需要自己修改,不可以直接使用std::pair<int, int> front;return front;
}
std::pair<int, int> Snake::GetNextPosition(int i, int j) const
{//根据蛇的头的位置,以及一个移动的向量 (i,j) 得到蛇头部打算要去的新目的地的坐标auto old = GetCurrentPosition();//(7) your code 下面的代码需要自己修改,不可以直接使用int x = 0;int y = 0;return std::make_pair(x, y);
}
bool Snake::ExistFood(int i, int j) const
{//返回 坐标(i,j)处是否是有蛇的食物可以吃//(8) your code 下面的代码需要自己修改,不可以直接使用return false;
}
void Snake::CreateFood(void)
{// 生成一个新的食物给蛇来吃// 随机生成一个新的位置,但是这个位置可能已经是蛇的身体了// 所以,需要用一个循环不断的重复在一个新生成的随机位置放置食物// 直到放置成功为止do{unsigned 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 x = 0;int y = 0;// 在新坐标处放置一个食物,记得检查可以放才能放// 一旦放好,记得退出循环,让程序继续执行//(10) your code} while (true);
}
bool Snake::GoAhead(char userInputDirection)
{switch (userInputDirection){case 'w':case 'W':return GoAhead(-1, 0);//upcase 'a':case 'A':return GoAhead(0, -1);//leftcase 'd':case 'D':return GoAhead(0, +1);//rightcase 's':case 'S':return GoAhead(+1, 0);//downdefault:return true;}
}
bool Snake::GoAhead(int i, int j)
{auto nextPosition = GetNextPosition(i, j);//垂直方向x不变,竖直方向y减少1// 首先判断游戏是否已经结束if (IsGameOver(nextPosition.first, nextPosition.second)){return false;}// 判断nextPosition 处是否有食物// 如果有食物,就吃掉这个食物// 并生成一个新的食物if (ExistFood(nextPosition.first, nextPosition.second)){// (11) your code//直接吃掉,尾巴不用移动m_playMatrix[nextPosition.first][nextPosition.second] = static_cast<char>(MatrixValueEnum::SNAKE_BODY);CreateFood();//随机生成一个食物}// 如果 nextPosition 处没有食物,就移动蛇的身体else{// (12) your code//尾巴移动 auto tail = m_snakeBody.back();m_playMatrix[tail.first][tail.second] = static_cast<char>(MatrixValueEnum::NOTHING);m_snakeBody.pop_back();}
}void Snake::Play(void)
{CreateFood();//随机生成一个食物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;//往前走一步,如果判断无法往前走到用户指定的位置,就退出程序// (13) your codeif (!GoAhead(direction)){std::cout << "Game Over!" << std::endl;break;}}
}
void Snake::PrintMatrix(void) const
{auto headPosition = m_snakeBody.front();for (size_t i = 0; i < m_playMatrix.size(); i++){for (size_t j = 0; j < m_playMatrix[i].size(); j++){if (i == headPosition.first && j == headPosition.second){std::cout << "@" << " ";}else if (m_playMatrix[i][j] == static_cast<char>(MatrixValueEnum::FOOD)){std::cout << "$" << " ";}else if (m_playMatrix[i][j] == static_cast<char>(MatrixValueEnum::NOTHING)){std::cout << "_" << " ";}else{std::cout << m_playMatrix[i][j] << " ";}}std::cout << std::endl;}
}int main(int argc, char** argv)
{Snake snake;if (snake.LoadPlayDataFromFile("data.txt")){snake.Play();}return 0;
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/95224.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Go 官方标准编译器中所做的优化

本文是对#102 Go 官方标准编译器中实现的优化集锦汇总[1] 内容的记录与总结. 优化1-4: 字符串和字节切片之间的转化 1.紧跟range关键字的 从字符串到字节切片的转换&#xff1b; package mainimport ( "fmt" "strings" "testing")var cs10086 s…

【强化学习】贝尔曼公式 - bellman equation

return作用 还是用这个迷宫游戏说。 首先明确&#xff0c;不撞墙到终点比撞墙到终点好。路径越短到终点越好。 不撞墙到终点比撞墙到终点好。你可以把撞墙这个reward设置成负数&#xff0c;不撞墙设置成0。那么在最终return进行累加的时候&#xff0c;不撞墙的return就会大。路…

【方案】基于视频与AI智能分析技术的城市轨道交通视频监控建设方案

一、背景分析 地铁作为重要的公共场所交通枢纽&#xff0c;流动性非常高、人员大量聚集&#xff0c;轨道交通需要利用视频监控系统来实现全程、全方位的安全防范&#xff0c;这也是保证地铁行车组织和安全的重要手段。调度员和车站值班员通过系统监管列车运行、客流情况、变电…

使用PAM保障开发运营安全

硬编码凭据和 DevOps 系统中缺乏凭据安全性是组织的巨大漏洞。以明文形式访问凭据的恶意内部人员可以在 IT 中建立和扩展其立足点 基础设施&#xff0c;构成巨大的数据被盗风险。 什么是PAM 特权访问管理 &#xff08;PAM&#xff09; 是指一组 IT 安全管理原则&#xff0c;可…

Redis之管道解读

目录 基本介绍 使用例子 管道对比 管道与原生批量命令对比 管道与事务对比 使用pipeline注意事项 基准测试 基本介绍 Redis是一种基于客户端-服务端模型以及请求/响应协议的TCP服务器。 这意味着请求通常按如下步骤处理&#xff1a; 客户端发送一个请求到服务器&am…

Go实现LogCollect:海量日志收集系统【下篇——开发LogTransfer】

Go实现LogAgent&#xff1a;海量日志收集系统【下篇】 0 前置文章 Go实现LogAgent&#xff1a;海量日志收集系统【上篇——LogAgent实现】 前面的章节我们已经完成了日志收集&#xff08;LogAgent&#xff09;&#xff0c;接下来我们需要将日志写入到kafka中&#xff0c;然后…

【C++入门】命名空间、缺省参数、函数重载、引用、内联函数

​&#x1f47b;内容专栏&#xff1a; C/C编程 &#x1f428;本文概括&#xff1a; C入门学习必备语法 &#x1f43c;本文作者&#xff1a; 阿四啊 &#x1f438;发布时间&#xff1a;2023.9.3 前言 C是在C的基础之上&#xff0c;容纳进去了面向对象编程思想&#xff0c;并增加…

【C++技能树】继承概念与解析

Halo&#xff0c;这里是Ppeua。平时主要更新C&#xff0c;数据结构算法&#xff0c;Linux与ROS…感兴趣就关注我bua&#xff01; 继承 0. 继承概念0.1 继承访问限定符 1. 基类和派生类对象赋值兼容转换2. 继承中的作用域3. 派生类中的默认成员函数4.友元5.继承中的静态成员6.菱…

如何使用Puppeteer进行新闻网站数据抓取和聚合

导语 Puppeteer是一个基于Node.js的库&#xff0c;它提供了一个高级的API来控制Chrome或Chromium浏览器。通过Puppeteer&#xff0c;我们可以实现各种自动化任务&#xff0c;如网页截图、PDF生成、表单填写、网络监控等。本文将介绍如何使用Puppeteer进行新闻网站数据抓取和聚…

Linux挖矿程序清除

1. 找到挖矿进程 2.找到病毒的文件地址 ls -l /proc/进程ID/exe3.删除文件命令 rm -rf 文件地址4.杀死挖矿进程 kill -9 进程ID

Python入门学习——Day2-控制流程

一、Python 控制流程 什么是控制流程&#xff1a; 在Python中&#xff0c;控制流程指的是根据不同的条件或规则来控制程序的执行顺序和逻辑。Python提供了多种控制流程的语句和结构&#xff0c;可以根据条件进行分支判断和循环迭代。 1.1 条件语句&#xff08;if-elif-else&…

Kafka知识点总结

常见名词 生产者和消费者 同一个消费组下的消费者订阅同一个topic时&#xff0c;只能有一个消费者收到消息 要想让订阅同一个topic的消费者都能收到信息&#xff0c;需将它们放到不同的组中 分区机制 启动方法 生成者和消费者监听客户端