该程序的实现步骤:
- 编写菜单
- 旋转规则、形状类Shape和格局类Board
- Shape类和Board类的构造函数
- 绘制方块
- 绘制方块掉落区外边界
- 处理方块旋转
- 方块移动和受阻判定
- 处理方块掉落及更新格局
- 堆积块消除条件判断
- 堆积块下落的实现
- 总体流程控制
- 按键捕捉和方块下落的频度控制
- 重绘堆积块
- 显示游戏结束+设计得分类
- 显示得分和在操作区外画方框
- 提示下一方块形状
- 添加游戏暂停功能
具体的视频教学链接:https://www.51zxw.net/Show.aspx?cid=717&id=79283。
C++代码:
#include<iostream>
#include<windows.h>//包含输入输出坐标
#include<conio.h>//使用getch()函数!!!但我选择的是getchar(),两个均在Menu()函数中
#include<time.h>//使用函数srand(time(0)),使程序有随机性
using namespace std;
HANDLE handle;//用于设计输出坐标
COORD crd;//用于设计输出坐标
const short LMARGIN = 7;//方块掉落范围的左边界
const short TMARGIN = 5;//方块掉落范围的上边界
char type[19][17] = {
//定义方块种类,以下形状均采用逆时针旋转
//Z型0-1
"1100011000000000","0010011001000000",
//S型2-3
"0110110000000000","0100011000100000",
//I型4-5
"0000111100000000","0100010001000100",
//方型6
"0000011001100000",
//T型7-10
"0100111000000000","0100110001000000","1110010000000000","1000110010000000",
//L型11-14
"0010111000000000","0110001000100000","1110100000000000","0100010001100000",
//J型15-18
"1000111000000000","0010001001100000","1110001000000000","0110010001000000"
};
char rotate_hinder[19][17] = {
//定义方块旋转受阻位置
//Z型0-1
"0010000001100000","1100000000100000",
//S型2-3
"0000001011100000","0010100011000000",
//I型4-5
"1111000001110111","1011101100110011",
//方型6
"0000000000000000",
//T型7-10
"1010000011000000","1010001000100000","0000100010000000","0100001001100000",
//L型11-14
"0100000011100000","1000110000000000","0000010011100000","1010101000000000",
//J型15-18
"0110000011100000","1010101000100000","0000110011000000","1000101000100000"
};
short Alter(short n) {
//旋转
switch (n) {
case 1:
case 3:
case 5:
return n - 1;
break;
case 6:
return 6;
break;
case 10:
case 14:
case 18:
return n - 3;
break;
default:
return n + 1;
break;
}
}
void SetPos(short x, short y) {
//传入坐标
crd.X = x;
crd.Y = y;
SetConsoleCursorPosition(handle, crd);//句柄要使用GetStdHandle函数
}
void SetColor(short front, short back) {
//前景色(指文字颜色),背景色
//二者均有0-15的常数来表示,背景色(左边4位)比前景色(右边4位)大16倍
SetConsoleTextAttribute(handle, front + back * 0x10);
}
void PrintGameOver() {
SetPos(12, 9);
SetColor(5, 6);
cout << " Game Over! ";
SetColor(7, 0);
}
class Score {
//得分类
public:
void Reset() {
ln = 0;
}
void Print() {
SetPos(4, 1);
SetColor(2, 7);
cout << "lines:" << ln;
SetColor(7, 0);
}
Score() {
Reset();
}
void Update(short n) {
ln += n;
}
private:
short ln;//得分
};
//Shape和Board互为友元
class Shape {
//方块形状的类,定义和实现分开写
public:
Shape(short, short, short);//第一个参数传入方框的颜色,后两个参数是x,y坐标
void Draw(bool, bool);//第一个参数为真执行绘制操作,为假执行擦除操作
//第二个参数为真按方块的颜色画,为假画成白色
friend class Board;
bool Rotate(Board &brd);//判断一个方块能否旋转,如果能,把旋转操作完成,并返回真
short Collision(Board &brd);//左侧、右侧、下方受阻时,返回值分别“位或”1、2、4
bool Move(short, Board &brd);//左移动第一个参数为1,右移动为2
bool Fall(Board &brd);//能下落返回true,不能下落返回false
void DrawElsewhere(short _x, short _y);//在操作区外显示一个方块的初始形状
private:
short present;//当前形状(取值为0-18)
short color;
short x;//左上角横坐标
short y;//左上角纵坐标
};
class Board {
//游戏框架
public:
void Clear() {
//设置数组元素的值
for (short i = 0; i < 22; i++) {
for (short j = 0; j < 14; j++) {
if (j > 1 && j < 12 && i < 20)
wall[i][j] = 0;
else
wall[i][j] = 1;
}
}
}
Board() {
Clear();
}
void DrawBoard() {
short i;
SetColor(0xe, 1);//前者代表黄色,后者背景色为黑色
for (i = 0; i < 21; i++) {
//绘制左边界
SetPos(LMARGIN - 2, TMARGIN + i - 1);//只绘制内层1
cout << "■";
}
for (i = 0; i < 21; i++) {
//绘制右边界
SetPos(LMARGIN + 20, TMARGIN + i - 1);
cout << "■";
}
for (i = 0; i < 11; i++) {
//绘制下边界,原程序为i<10
SetPos(LMARGIN + i * 2, TMARGIN + 19);
cout << "■";
}
SetColor(7, 0);
}
short SetBlockValue(Shape &shp) {
//对wall数组的值进行更新
//只需考虑被当前方块所占据的格子对应的wall数
for (short i = 0; i < 16; i++) {
//注意:如果不进行if判断,
//直接写wall[y + i / 4][x + i % 4] =type[shp.present][i] == '1',
//就会造成给4*4=16的格子全部赋值
if (type[shp.present][i] == '1') {
if (shp.y == 0)//检查是否game over
return -1;
wall[shp.y + i / 4][shp.x + i % 4] = true;
}
}
return 0;
}
short Drop(Shape &shp) {
//返回可以消除的行数
short i, n;//i循环变量,n用来保存检测过的被方块占用的格子数
short j;//用于遍历一整行检查是否有wall数组的元素为false
short k;//用于从下往上的逐行覆盖
short y, _y = -1;//y保存当前格子的行号,_y保存上一个格子的行号,初始化为一个不合理值
short lines = 0;//保存消除了多少行
short top;//检测有没有遍历到堆积块顶部
short l;//在覆盖的操作当中从左到右进行遍历
for (i = 0, n = 0; n < 4 && i < 16; i++) {
if (type[shp.present][i] == '1') {
y = shp.y + i / 4;
n++;
if (y == _y)
continue;//同一行的格子不用再检查
_y = y;//将当前格的行号赋给_y
for (j = 2; j < 12; j++) {
//0、1为左边界,12、13为右边界,不用检测
if (!wall[y][j])
break;
}
if (j == 12) {
//一整行已满,消除
lines++;//消除的行数更新
for (k = y; k > 0; k--) {
top = 0;
for (l = 2; l < 12; l++) {
wall[k][l] = wall[k - 1][l];//实现覆盖
top += wall[k - 1][l];
}
if (top == 0)//已经到达堆积块顶部,终止从下到上的遍历
break;
}
}
}
}
bottom_line = _y;
return lines;
}
void DrawPile() {
//重绘堆积块,由bottom_line指定需要重绘的最下面一行
for (short i = 0; i <= bottom_line; i++) {
//行遍历
for (short j = 2; j < 12; j++) {
//列遍历
SetPos(LMARGIN - 4 + j * 2, TMARGIN - 1 + i);
if (wall[i][j]) {
SetColor(7, 7);//白色,用于绘出
cout << "□";
}
else {
SetColor(0, 0);//黑色,用于擦除
cout << " ";
}
}
}
}
friend class Shape;
private:
bool wall[22][14];//取值为真的元素表明该位置已被占用,为假表明为空
short bottom_line;//保存当前可以消除的最下面一行的行号
};
Shape::Shape(short _color, short _x = 5, short _y = 0) :color(_color), x(_x), y(_y) {
//第一个参数颜色值决定方块形状的因素
switch (_color) {
case 1:
present = 0;
break;
case 2:
present = 2;
break;
case 3:
present = 4;
break;
case 4:
present = 6;
break;
case 5:
present = 7;
break;
case 6:
present = 11;
break;
default:
color = 8;
present = 15;
break;
}
}
void Shape::Draw(bool flag = true, bool active = true) {
if (!flag) //擦除
SetColor(0, 0);
else
if (active)
SetColor(color, color);//按自己颜色画
else
SetColor(7, 7);//已着地的地方画成白色
for (short i = 0; i < 16; i++) {
if (type[present][i] == '1') {
SetPos(LMARGIN - 4 + (x + i % 4) * 2, TMARGIN + y + i / 4 - 1);
cout << "□";
}
}
SetColor(7, 0);
}
bool Shape::Rotate(Board &brd) {
for (short i = 0; i < 16; i++) {
if (brd.wall[y + i / 4][x + i % 4] && rotate_hinder[present][i] == '1') {
//如果格局中一个格子为真且该位置在方块的自身坐标中是能够阻碍旋转的
return false;
}
}
Draw(false);
present = Alter(present);
Draw();
return true;
}
short Shape::Collision(Board &brd) {
short result = 0;
for (short i = 0; i < 16; i++) {
if (type[present][i] == '1') {
//该格子被当前方块占用
if (brd.wall[y + i / 4][x + i % 4 - 1])//左受阻
result |= 1;
if (brd.wall[y + i / 4][x + i % 4 + 1])//右受阻
result |= 2;
if (brd.wall[y + i / 4 + 1][x + i % 4])//下受阻
result |= 4;
}
}
return result;
}
bool Shape::Move(short direction, Board &brd) {
if (direction == 1 && !(Collision(brd) & 1)) {
//向左移动
Draw(false, true);//其实第一个参数为false时,可以不考虑第二个参数
x--;
Draw();
return true;
}
if (direction == 2 && !(Collision(brd) & 2)) {
//向右移动
Draw(false, true);//其实第一个参数为false时,可以不考虑第二个参数
x++;
Draw();
return true;
}
return false;
}
bool Shape::Fall(Board &brd) {
if (Collision(brd) & 4) {
//已经着地
Draw(true, false);//画成堆积块
return false;
}
Draw(false, true);
y++;
Draw();
return true;
}
void Shape::DrawElsewhere(short _x, short _y) {
short i;//循环变量
SetPos(_x, _y);
SetColor(7, 1);
cout << "Next:";
_x += 6;
SetColor(0, 0);
for (i = 0; i < 12; i++) {
//用于擦掉前一次绘制的方块
//因为type数组中4*4的最后一行均为0,所以i<12即可
SetPos(_x + i % 4 * 2, _y + i / 4);
cout << " ";
}
SetColor(color, color);
_y -= (present == 6 || present == 4);
for (i = 0; i < 12; i++) {
//用于绘出下一个方块
//因为type数组中4*4的最后一行均为0,所以i<12即可
if (type[present][i] == '1') {
SetPos(_x + i % 4 * 2, _y + i / 4);
cout << "□";
}
}
}
short Menu() {
//菜单函数
//选中开始游戏返回1,离开返回2
short choice = 1;//跟踪用户选中的选项
char c;//记录用户的按键信息
system("cls");//清屏
SetPos(9, 12);//设置坐标
cout << "┌───────────────┐";
SetPos(9, 13);//输出坐标往下移动一列
cout << "│ 俄 罗 斯 方 块│";
SetPos(9, 14);//输出坐标往下移动一列
cout << "└───────────────┘";
SetPos(7, 21);
cout << "按方向键控制方块";
SetPos(7, 23);
cout << "按空格键暂停/继续";
while (1) {
//死循环
SetPos(15, 16);
if (choice == 1)
SetColor(2, 7);//2绿色(前景色),7白色(背景色)
else
SetColor(2, 0);//2绿色(前景色),0黑色(背景色)
cout << " 开始游戏 ";
SetPos(15, 18);
if (choice == 2)
SetColor(2, 7);//2绿色(前景色),7白色(背景色)
else
SetColor(2, 0);//2绿色(前景色),0黑色(背景色)
cout << " 离 开 ";
c = _getch();
if (c == VK_RETURN)
break;//用户按下回车,离开循环体
else
if (c == 0)
choice = _getch();//按下的方向键,重新获得信息
if (c == 72 || c == 80)//72向下,80向上
choice = choice == 1 ? 2 : 1;
}
SetColor(7, 0);//恢复成黑底白字
return choice;
}
int main() {
system("mode con cols=35 lines=27");//控制台尺寸,35列27行
handle = GetStdHandle(STD_OUTPUT_HANDLE);//设置句柄
Board board;
short tmp;//用来保存一个方块消去大的行数
Shape *shp;
Shape *shp_next;
char cc;//用于清空输入流文件
Score score;
short times;//用来保存执行sleep函数执行的次数
srand(time(0));//每次运行时的方块不一样
while (Menu() == 1) {
//每执行一次循环体,就是游戏从开始到结束走了一遍
system("cls");//擦掉菜单内容
board.Clear();
board.DrawBoard();
score.Reset();//得分归0
score.Print();//显示得分
shp_next = new Shape(rand() % 7 + 1);
while (1) {
//每执行一次循环体,就是一个方块从产生到着地的过程
shp = shp_next;
shp_next = new Shape(rand() % 7 + 1);
shp_next->DrawElsewhere(17, 1);
times = 0;
while (1) {
//每执行一次循环体,就是捕捉一次按键行为的过程,每循环十次方块下落一格
shp->Draw();
if (GetAsyncKeyState(VK_LEFT) == -32767) {
//左方向键的虚拟键码
shp->Move(1, board);
}
if (GetAsyncKeyState(VK_RIGHT) == -32767) {
//右方向键的虚拟键码
shp->Move(2, board);
}
if (GetAsyncKeyState(VK_UP) == -32767) {
//向上方向键,执行旋转
shp->Rotate(board);
}
if (GetAsyncKeyState(VK_DOWN) == -32767) {
//向下方向键,加速掉落
shp->Fall(board);
}
if (GetAsyncKeyState(VK_SPACE) == -32767) {
//空格键,暂停
while (!(GetAsyncKeyState(VK_SPACE) == -32767))
Sleep(25);
}
Sleep(25);
times++;
if (times == 10) {
times = 0;
if (!shp->Fall(board))
break;
}
}
if (board.SetBlockValue(*shp) == -1) {
PrintGameOver();
fflush(stdin);
while (cc = getchar() != '\n'&&cc != EOF);
delete shp_next;
break;//回到Menu
}
if (tmp = board.Drop(*shp)) {
board.DrawPile();
score.Update(tmp);//累加得分
score.Print();//显示得分
}
delete shp;
}
}
return 0;
}
C++运行结果: