1. 题目描述
推箱子的游戏规则是扮演工人的玩家,以“推”的方式推动箱子。玩家可以在没有阻碍物(如墙壁等的阻碍物)的情况下,向上、下、左、右的方向移动,将箱子移动到指定位置,当箱子都处于指定位置上时,即可过关。
地图上有若干个箱子,当玩家移动箱子时,需要满足以下条件:
⑴ 箱子只能以“推”的方式移动,不能以“拉”的方式移动,推到墙壁的箱子,玩家就不可以背对墙壁,把箱子拉回到空处。但如果玩家推至墙壁后,垂直墙壁的两侧没有阻碍物,则玩家可以朝这两个不同的方向推动箱子。
⑵ 一旦箱子被移动到角落,玩家没有任何方法再移动这个箱子。
⑶ 玩家不可同时推动两个或以上的箱子。假设玩家面前有一个箱子,箱子的正前方又有一个箱子,则这两个箱子是不能被推动的。
2. 设计要求
⑴ 在内存中,设计数据结构存储游戏数据。
⑵ 满足推箱子游戏的游戏规则。
3. 数据结构设计
在游戏运行时,需要将关卡信息加载到内存中,供玩家游玩,为了记录关卡的内容,需要一个数据结构与之对应,可以设计以下结构来存储关卡信息。
在这里我们人为规定,关卡中用‘#’来表示墙、用’.’表示空地、用’P’表示玩家、用’B’表示箱子、用’G’表示目标位置。
struct LevelType
{
int width; // 关卡宽度
int height; // 关卡高度
char map[20][21]; // 关卡内容,宽和高最高为20
}
其中,因为关卡是一个二维平面,对于平面上的任一点可以通过一个二元组来表示其位置,可以设计以下结构来表示一个点。同时又因为要对点进行判断相等或不等,所以需要用到运算符重载。
struct PointType
{
int x;
int y;
bool operator==(const PointType& p)
{
if(p.x==this->x&&p.y==this->y)
return true;
return false;
}
bool operator!=(const PointType& p)
{
return !(*this==p);
}
}
4. 算法设计
对于该款游戏,我们需要以下函数提供基本功能:
LoadLevelToGame:载入关卡数据到游戏中的函数
Update:负责绘制更新游戏的画面函数
PlayerMove:处理玩家移动的函数
CheckPlayerWin:判断玩家是否取得胜利的函数
较为重要的函数PlayerMove负责处理玩家的移动,算法设计如下:
其中的playerPos为前文定义的PointType类型,表示玩家的位置。
PlayerMove:处理玩家的移动 输入:玩家的移动方向,本例以向下移动举例 输出:无 1. 如果玩家的前进位置为空地: 1.1. 玩家的位置向下移动一格; 1.2. playerPos.x++,算法结束; 2. 如果玩家的前进位置为墙:忽略本次输入,结束算法; 3. 如果玩家的前进位置是箱子: 3.1. 判断箱子的的前进位置是墙或箱子:忽略本次输入,结束算法; 3.2. 判断箱子的前进位置是否为空地: 3.2.1. 箱子的位置向下移动一格; 3.2.2. 玩家的位置向下移动一格; 3.3.3. playerPos.x++,结束算法; |
其中CheckPlayerWin函数判断玩家是否胜利,也就是判断所有目标位置上方是否均为箱子,算法设计如下:
其中status的类型是char类型的二维数组,表示当前关卡的每一格状态。
其中level的类型是前文定义的LevelType类型,表示当前关卡的关卡信息。
CheckPlayerWin:判断玩家是否胜利 输入:无 输出:玩家是否胜利 1. isWin = true; 2. 逐行逐列判断,行号为i,i从0至level.height-1,列号为j,j从0至level.width-1: 2.1. 若status [i][j]为目标位置并且上方无箱子:isWin=False; 3. 如果isWin==true:输出玩家胜利; 4. 如果isWin==false:输出玩家还未取得胜利; 3.2. 返回选择关卡界面,算法结束; 4. 玩家当前未获得胜利,算法结束; |
5. 测试样例
其中‘#’是墙、用’.’是空地、用’P’是玩家、用’B’是箱子、用’G’是目标位置。
5 5
#####
#P#.#
#...#
#.BG#
#####
6 9
..####...
###..####
#.....B.#
#.#..#B.#
#.G.G#P.#
#########
6. 代码实现
// 编译器版本 g++8.1.0 // 编译参数 -Weffc++ -Wextra -Wall -std=c++11 #include <cstdio> #include <cstdlib> #include <conio.h> /** * 常量的定义 **/ // 关卡最大高度和宽度 const int MAXHEIGHT = 20; const int MAXWIDTH = 20; // 代表位移的四个常量(对应上下左右) const int DX[4]= {-1,1,0,0}; const int DY[4]= {0,0,-1,1}; /** * 数据类型定义 **/ // 坐标数据类型 struct PointType { int x; int y; bool operator==(const PointType& p) { if(p.x==this->x&&p.y==this->y) return true; return false; } bool operator!=(const PointType& p) { return !(*this==p); } }; // 关卡数据类型 struct LevelType { int height; // 关卡的高度,应当小于MAXHEIGHT int width; // 关卡的宽度,应当小于MAXWIDTH char levelMap[MAXHEIGHT][MAXWIDTH]; // 关卡每格数据,'P'代表玩家、'B'代表箱子,'G'代表目标点,'#'代表墙,'.'代表空地 }; /** * 全局变量定义 **/ LevelType Glevel[2]={ {5,5,{ "#####", "#P#.#", "#...#", "#.BG#", "#####"}}, {6,9,{ "..####...", "###..####", "#.....B.#", "#.#..#B.#", "#.G.G#P.#", "#########"}}, }; // 关卡的定义 int GlevelSelect; // 当前正在游玩的关卡 LevelType GnowLevel; // 游戏正在运行中的关卡状态 PointType Gplayer; // 玩家的位置信息 /** * 载入关卡数据至游戏中并初始化玩家位置 **/ void LoadLevelToGame(LevelType & level) { GnowLevel = level; for(int i=0; i<GnowLevel.height; i++) for(int j=0; j<GnowLevel.width; j++) if(GnowLevel.levelMap[i][j]=='P') Gplayer= {i,j}; return; } /** * 更新游戏画面 **/ void Update() { // 清空显示画面 system("cls"); // 绘制提示信息 printf("w:Up s:Down a:Left d:Right\n"); printf("r:Restart q:Quit\n\n"); // 绘制关卡信息 for(int i=0; i<GnowLevel.height; i++) printf("%s\n",GnowLevel.levelMap[i]); return; } /** * 选择关卡 **/ void LevelSelect() { // 绘制提示信息 system("cls"); printf("Please choose level.\n"); printf("0=Easy 1=Hard\n\n"); printf("Level:"); scanf("%d",&GlevelSelect); // 载入关卡数据 LoadLevelToGame(Glevel[GlevelSelect]); // 更新画面 Update(); return; } /** * 更新游戏胜利画面 **/ void UpdateWin() { // 清空显示画面 system("cls"); // 绘制提示信息 printf("YOU WIN!!!\n"); // 等待玩家确认 system("pause"); return; } /** * 游戏初始化 **/ void InitGame() { // 初始化显示画面高度和宽度 system("mode con cols=50 lines=20"); return; } /** * 玩家移动函数 * type:移动类型,与上文位移常量对应,0=上,1=下,2=左,3=右 **/ void PlayerMove(int type) { // 玩家下一格的前进方向 PointType toPoint = {Gplayer.x+DX[type],Gplayer.y+DY[type]}; // 玩家当前格位置与触碰到箱子的位置,便于恢复地图状态 PointType beforePlayer = Gplayer; switch(GnowLevel.levelMap[toPoint.x][toPoint.y]) { case '.': case 'G': // 如果下一格是空地或目标点,直接前进 Gplayer=toPoint; break; case '#': // 如果下一格是墙,忽略输入 break; case 'B': // 如果下一格是箱子,对箱子在进行一次判断 PointType boxToPoint = {toPoint.x+DX[type],toPoint.y+DY[type]}; switch(GnowLevel.levelMap[boxToPoint.x][boxToPoint.y]) { case '.': case 'G': // 如果下一格是空地或目标点,直接前进 GnowLevel.levelMap[boxToPoint.x][boxToPoint.y]='B'; // 玩家覆盖该位置 Gplayer=toPoint; break; // 其他状况忽略输入 } break; } // 更新地图上玩家位置 GnowLevel.levelMap[Gplayer.x][Gplayer.y]='P'; // 恢复玩家之前踩着的格子状态,仅存在目标点需要处理 if(beforePlayer!=Gplayer) { if(Glevel[GlevelSelect].levelMap[beforePlayer.x][beforePlayer.y]=='G') GnowLevel.levelMap[beforePlayer.x][beforePlayer.y]='G'; else GnowLevel.levelMap[beforePlayer.x][beforePlayer.y]='.'; } return; } /** * 获取用户输入 **/ void GetPlayerInput() { int input=getch(); switch(input) { case 'w': PlayerMove(0); break; case 's': PlayerMove(1); break; case 'a': PlayerMove(2); break; case 'd': PlayerMove(3); break; case 'r': LoadLevelToGame(Glevel[GlevelSelect]); break; case 'q': exit(0); break; default: break; } return; } /** * 判断玩家胜利 **/ bool CheckPlayerWin() { bool isWin=true; for(int i=0; i<GnowLevel.height; i++) for(int j=0; j<GnowLevel.width; j++) if(Glevel[GlevelSelect].levelMap[i][j]=='G'&&GnowLevel.levelMap[i][j]!='B') isWin=false; return isWin; } /** * 程序入口 **/ int main() { // 初始化游戏 InitGame(); while(true) { // 加载关卡界面 LevelSelect(); // 游戏开始 while(true) { // 获取用户输入并执行相关功能函数 GetPlayerInput(); // 更新画面 Update(); // 判断玩家是否胜利 if(CheckPlayerWin()) { // 显示胜利画面 UpdateWin(); // 返回到关卡选择界面 break; } } } return 0; } |
7. 思考题
⑴ 游戏的所有关卡信息放置于内存当中,造成了不必要的资源浪费。如何通过文件读取的方式,将关卡数据保存在外存中,每当玩家游玩时从外存中读取关卡数据至内存,降低内存资源的占用?
⑵ 在玩家游玩推箱子游戏时,可能会出现误操作或者是想反悔的情况,重新游玩关卡会导致不必要的时间浪费。需要使用何种数据结构来实现“上一步”的操作?又如何实现该功能呢?
⑶ 在目前的游戏程序中,需要玩家自己意识到该局游戏目前的状态无解,需要重新开始游戏。请思考并设计算法,使得本局游戏无解的时候主动提醒玩家游戏失败,重新开始游戏。