1. 题目描述
俄罗斯方块是一款由七种四格方块构成,控制方块下落的游戏。
方块又以下七种构成,分别称为I、L、J、O、S、T、Z型方块。
俄罗斯方块中出现的七种方块类型
开始时,一个随机的方块生成于游玩区域的上方,该游玩区域高20格、宽10格,同时缓慢落下。在落下时,玩家可以旋转方块或是以格子为单位左右移动方块,也可让方块加速落下。
当方块下落到游玩区域的最下方或落到其他方块上无法再向下移动时,就会固定在该处,此时一个新的随机方块会出现在游玩区域的上方缓慢落下,周而复始。
当区域中某一横行的格子全部由方块填满时,则该行会被消除。当固定的方块堆到区域最顶端而无法消除层数时,游戏就会结束。
2. 设计要求
⑴ 在内存中,设计数据结构存储游戏需要的数据。
⑵ 满足俄罗斯方块游戏的游戏规则。
3. 数据结构设计
在游戏运行过程中,需要在内存中保存方块的状态,可以设计以下数据结构来保存。
struct BlockType
{
char block[4][4]; // 方块当前状态
int x,y; // 方块的位置,x自上而下,y从左至右
}
同时,对于游戏的游玩区域,可以使用char类型的二维数组保存其游玩状态。
4. 算法设计
对于该款游戏,我们需要以下函数提供基本功能:
InitGame:初始化游戏的游玩状态
GetPlayerInput:获取玩家的输入并响应
BlockMove:处理方块的横向移动
BlockDown:处理方块的下落移动
BlockRotate:处理方块的旋转移动
GenerateBlock:在七种方块中选择生成一个随机的方块
Update:根据游玩状态绘制游戏画面
游戏的主要逻辑由BlockMove、BlockDown、BlockRotate构成。
BlockMove负责根据玩家的输入,进行方块的平移,在平移的同时也要判断是否和其他已经固定的方块或边界相撞,若相撞,则此次移动应该是无效的。BlockMove的函数设计如下:
其中board数据类型为游玩状态,数据类型为char类型的二维数组,nowBlock为当前控制的方块,movedBlock为移动后的方块,数据类型均为前文定义的BlockType,记录了方块的当前状态和位置。
BlockMove:平移玩家当前控制的方块 输入:平移的方向,这里以向左平移举例 输出:无 1. movedBlock=nowBlock; 2. 获取方块移动后的位置,movedBlock.y-=1; 3. 逐行逐列判断移动后的方块movedBlock是否于board中其他方块重合 或超出游玩区域,记为isBlocked; 4. 根据isBlocked判断: 4.1. 若isBlocked=true,不合法,本次操作无效 4.2. 若isBlocked=false,合法,令nowBlock=movedBlock; |
BlockRotate负责根据控制方块的旋转,在平移的同时也要判断是否和其他已经固定的方块或边界相撞,若相撞,则此次旋转应该是无效的。BlockRotate的函数设计如下:
其中board数据类型为游玩状态,数据类型为char类型的二维数组,nowBlock为当前控制的方块,rotatedBlock为旋转后的方块,数据类型均为前文定义的BlockType。
BlockRotate:旋转玩家当前控制的方块 输入:无 输出:无 1. rotatedBlock=nowBlock; 2. 逐行逐列对方块进行90度的旋转操作,记循环中行号为i列号为j: 2.1. rotatedBlock.block[i][j]= nowBlock.block[j][3-i]; 3. 逐行逐列判断旋转后的方块rotatedBlock是否于board中其他方块重 合或超出游玩区域,记为isBlocked; 4. 根据isBlocked判断: 4.1. 若isBlocked=true,不合法,本次操作无效 4.2. 若isBlocked=false,合法,令nowBlock=rotatedBlock; |
BlockDown函数负责控制当前方块的下落,在方块下落的同时也要判断是否落到其他方块上,若落到其他方块上则需要固定该方块,判断是否要消行,同时判断是否触碰到游玩区域上界导致游戏结束。
其中board数据类型为游玩状态,数据类型为char类型的二维数组,nextBlock为下一个玩家控制的方块、nowBlock为当前控制的方块,nxtTickBlock为下一时刻已下落的方块,数据类型均为BlockType。
BlockDown:控制当前方块的下落 输入:无 输出:无 1. nxtTickBlock=nowBlock; 2. 获取方块下落后的状态,nxtTickBlock.x++; 3. 逐行逐列判断下落后的方块nxtTickBlock是否于board中其他方块重 合或超出游玩区域,记为isBlocked; 4. 若碰撞,即isBlocked==true: 4.1. 根据nowBlock的状态,将块固定在board中; 4.2. 自底向上逐行判断是否满行,若满行则将该行消去,上方的行 下落,记消去了rowCnt行; 4.3. 根据nowBlock的状态位置,获取该块的最高点高度,记为x; 4.4. 若x+rowCnt<0:则表示固定并消行后的块仍高出了游玩区域, 执行游戏结束的相关逻辑; 4.5. nowBlock=nextBlock; 4.6. 生成新的nextBlock; 5. 未发生碰撞,方块正常下落,nowBlock=nxtTickBlock; |
5. 测试样例
无测试样例
6. 代码实现
// 编译器版本 g++8.1.0 // 编译参数 -Weffc++ -Wextra -Wall -std=c++11 #include <cstdio> #include <cstdlib> #include <cstring> #include <cmath> #include <ctime> #include <conio.h> #include <windows.h> using namespace std; /** * 常量定义 **/ const char BLOCK_I[2][4]= { {' ',' ',' ',' '}, {'#','#','#','#'} }; const char BLOCK_L[2][4]= { {'#',' ',' ',' '}, {'#','#','#',' '} }; const char BLOCK_J[2][4]= { {' ',' ','#',' '}, {'#','#','#',' '} }; const char BLOCK_O[2][4]= { {'#','#',' ',' '}, {'#','#',' ',' '} }; const char BLOCK_S[2][4]= { {' ','#','#',' '}, {'#','#',' ',' '} }; const char BLOCK_T[2][4]= { {'#','#','#',' '}, {' ','#',' ',' '} }; const char BLOCK_Z[2][4]= { {'#','#',' ',' '}, {' ','#','#',' '} }; /** * 数据类型定义 **/ // 方块数据类型 struct BlockType { char block[4][4]; // 方块形状 int x,y; // 块的位置 }; /** * 全局变量定义 **/ char Gboard[20][10]; // 10*20的画板 char GfixedBoard[20][10]; // 10*20的画板,记录了已经固定的块信息 BlockType GnextBlock; // 下一个方块 BlockType GnowBlock; // 当前方块 /** * 生成新块于nextBlock **/ void GenerateBlock() { GnextBlock= {}; int rndNum=rand()%7; switch(rndNum) { case 0: memcpy(GnextBlock.block[2],BLOCK_I,sizeof(BLOCK_I)); break; case 1: memcpy(GnextBlock.block[2],BLOCK_L,sizeof(BLOCK_L)); break; case 2: memcpy(GnextBlock.block[2],BLOCK_J,sizeof(BLOCK_J)); break; case 3: memcpy(GnextBlock.block[2],BLOCK_O,sizeof(BLOCK_O)); break; case 4: memcpy(GnextBlock.block[2],BLOCK_S,sizeof(BLOCK_S)); break; case 5: memcpy(GnextBlock.block[2],BLOCK_T,sizeof(BLOCK_T)); break; case 6: memcpy(GnextBlock.block[2],BLOCK_Z,sizeof(BLOCK_Z)); break; } GnextBlock.x=-4; GnextBlock.y=3; return; } /** * 绘制提示信息 **/ void UpdateHeader() { putchar('\n'); printf(" NEXT:\n"); printf(" "); for(int i=0; i<4; i++) putchar(GnextBlock.block[2][i]); putchar('\n'); printf(" "); for(int i=0; i<4; i++) putchar(GnextBlock.block[3][i]); putchar('\n'); putchar('\n'); return; } /** * 绘制方块 **/ void DrawBlock() { for(int i=0; i<20; i++) for(int j=0; j<10; j++) Gboard[i][j]=GfixedBoard[i][j]; for(int i=0; i<4; i++) for(int j=0; j<4; j++) if(GnowBlock.block[i][j]=='#'&&GnowBlock.x+i>=0&&GnowBlock.x+i<20&&GnowBlock.y+j>=0&&GnowBlock.y+j<10) { Gboard[GnowBlock.x+i][GnowBlock.y+j]=GnowBlock.block[i][j]; } return; } /** * 绘制画面 **/ void Update() { printf(" @----------@ \n"); for(int i=0; i<20; i++) { putchar(' '); putchar('@'); for(int j=0; j<10; j++) putchar(Gboard[i][j]); putchar('@'); putchar('\n'); } printf(" @----------@ \n"); return; } /** * 游戏初始化 **/ void InitGame() { // 初始化显示画面高度和宽度 system("mode con cols=20 lines=30"); system("cls"); // 初始化变量 memset(Gboard,' ',sizeof(Gboard)); memset(GfixedBoard,' ',sizeof(GfixedBoard)); srand(time(NULL)); GenerateBlock(); GnowBlock=GnextBlock; GenerateBlock(); UpdateHeader(); Update(); return; } /** * 结算画面 **/ void UpdateEnd() { system("cls"); putchar('\n'); printf("END"); system("pause"); InitGame(); return; } /** * 方块旋转 & 碰撞探测 **/ void BlockRotate() { // 获取方块旋转后的位置 BlockType rotatedBlock = GnowBlock; for(int i=0; i<4; i++) for(int j=0; j<4; j++) rotatedBlock.block[i][j]=GnowBlock.block[j][3-i]; // 碰撞检测 bool isBlocked = false; for(int i=0; i<4; i++) for(int j=0; j<4; j++) if(rotatedBlock.block[i][j]=='#'&&(rotatedBlock.x+i<0||rotatedBlock.x+i>=20||rotatedBlock.y+j<0||rotatedBlock.y+j>=10||GfixedBoard[rotatedBlock.x+i][rotatedBlock.y+j]=='#')) isBlocked=true; // 若未碰撞 更新 if (!isBlocked) { GnowBlock=rotatedBlock; system("cls"); UpdateHeader(); Update(); } return; } /** * 方块下落 & 碰撞探测 & 消块 & 判断结束 **/ void BlockDown() { // 获取方块下一刻的位置 BlockType nxtTickBlock = GnowBlock; nxtTickBlock.x++; // 若下一刻和边界重合或碰撞到其他块 bool isFixed = false; for(int i=0; i<4; i++) for(int j=0; j<4; j++) if(nxtTickBlock.block[i][j]=='#'&&(nxtTickBlock.x+i>=20||GfixedBoard[nxtTickBlock.x+i][nxtTickBlock.y+j]=='#')) isFixed=true; // 若碰撞 if(isFixed) { for(int i=0; i<4; i++) for(int j=0; j<4; j++) // 当前方块固定 if(GnowBlock.block[i][j]=='#'&&GnowBlock.x+i>=0&&GnowBlock.x+i<20&&GnowBlock.y+j>=0&&GnowBlock.y+j<10) GfixedBoard[GnowBlock.x+i][GnowBlock.y+j]=GnowBlock.block[i][j]; // 进行消块 int rowCnt = 0; for(int i=19; i>=0; i--) { bool isFull = true; for(int j=0; j<10; j++) if(GfixedBoard[i][j]!='#') { isFull=false; } if(isFull) { rowCnt++; for(int row = i; row>0; row--) for(int col = 0; col<10; col++) GfixedBoard[row][col]=GfixedBoard[row-1][col]; } } // 判断块的最高点-消掉的行数是否小于0,判断游戏是否结束 bool isEnd = false; for(int i=0; i<4; i++) for(int j=0; j<4; j++) if(GnowBlock.block[i][j]=='#'&&GnowBlock.x+i+rowCnt<0) isEnd=true; // 游戏结束调用 if(isEnd) { UpdateEnd(); } // 游戏没结束 生成新块 继续 else { GnowBlock=GnextBlock; GenerateBlock(); } } // 未碰撞 else { // 块下落 GnowBlock.x++; } return; } /** * 处理玩家移动操作 & 碰撞探测 **/ void BlockMove(int dy) { // 获取方块旋转后的位置 BlockType movedBlock = GnowBlock; movedBlock.y+=dy; // 碰撞检测 bool isBlocked = false; for(int i=0; i<4; i++) for(int j=0; j<4; j++) if(movedBlock.block[i][j]=='#'&&(movedBlock.x+i<0||movedBlock.x+i>=20||movedBlock.y+j<0||movedBlock.y+j>=10||GfixedBoard[movedBlock.x+i][movedBlock.y+j]=='#')) isBlocked=true; // 若未碰撞 更新 if (!isBlocked) { GnowBlock=movedBlock; system("cls"); UpdateHeader(); Update(); } return; } /** * 响应玩家操作 **/ DWORD WINAPI GetPlayerInput(LPVOID lpParameter) { while(true) { char ch = getch(); switch(ch) { case 'w': BlockRotate(); break; case 's': BlockDown(); break; case 'a': BlockMove(-1); break; case 'd': BlockMove(1); break; } int i=0; Sleep(100); } } /** * 程序入口 **/ int main() { // 初始化游戏 InitGame(); // 多线程获取用户输入 HANDLE thread = CreateThread(NULL, 0, GetPlayerInput, NULL, 0, NULL); while(true) { Sleep(500); system("cls"); BlockDown(); DrawBlock(); UpdateHeader(); Update(); } return 0; } |
7. 思考题
⑴ 请自行查阅参考资料,使用图形库等工具,实现俄罗斯方块游戏。