【 C语言俄罗斯方块 】
我们预计界面如下:
游戏运行时界面如下:
思路已有,每一步的实现通过代码备注:
《 C语言俄罗斯方块 》
#define _CRT_SECURE_NO_WARNINGS 1
#include <iostream>
#include <windows.h>
#include <conio.h>
#include <time.h>
using namespace std;
#define Block "■"
#define block "□" //由于 "■" 出现白边,吃边界的情况, 于是 又定义了一个"□"
#define White " "
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"
};
//旋转路径上,不能有阻挡物。
//用字符串来表示:”0010 0000 0110 0000”
//我们都以逆时针旋转为例:
//————————————————————————————————————————————————————
//受阻方式 二选一:①旋转受阻,②只是变形受阻
char rotate_hinder[19][17] =
{
/*-------—— ①定义方块旋转受阻位置 ------———*/
//Z 型 0 - 1 (两种)
"0010000001100000", "1100000000000000",
//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"
};
//char rotate_hinder[19][17] =
//{
// /*-------—— ②定义方块旋变形位置 ------———*/
// //Z 型 0 - 1 (两种)
// "0010000001000000", "1100000000000000",
// //S 型 2 - 3 (两种)
// "0000001000100000", "0010100000000000",
// //I 型 4 - 5 (两种)
// "0100000001000100", "0000101100000000",
// //方型 6 (一种)
// "0000000000000000",
// //T 型 7 - 10 (四种)
// "0000000001000000", "0000001000000000", "0000001001000000", "0100001000000000",
// //L 型11 - 14 (四种)
// "0100000000100000", "1000100000000000", "0000010001100000", "0010101000000000",
// //J 型15 - 18 (四种)
// "0100000011000000", "0000110000000000", "0000010001000000", "1000100000000000",
//};
//————————————————————————————————————————————————————
//记录旋转之后的数字
short Alter(short n)
{
switch (n)
{
case 1:
case 3:
case 5: //1 3 5 类型旋转之后的结果刚好等于他的前一种结果。 1→0,3→2,5→4
return n - 1;
break;
case 6:
return 6; //方形■ 不发生变化
break;
case 10:
case 14:
case 18:
return n - 3; //10→7,14→11,18→15
break;
default:
return n + 1; //其余情况等于它的下一个下标结果。
break;
}
}
void SetPos(short x, short y) //传入坐标
{
crd.X = x;
crd.Y = y;
SetConsoleCursorPosition(handle, crd); //句柄要先使用 GetStdHandle 函数,只使用一次,包含在主函数体内部运行一次就可以了。
}
//颜色:【0黑 1蓝 2绿 3浅蓝 4红 5紫 6黄 7白 8灰 9浅蓝 10绿 11淡蓝 12粉 13粉紫 14黄 15白 16灰】
void SetColor(short front, short back) //前景色,背景色
{
SetConsoleTextAttribute(handle, front + back * 0x10); //前景色(文字颜色),背景色 [0~15的范围],背景色比前景色大16倍
}
//游戏结束
void PrintGameOver()
{
SetPos(12, 9);
SetColor(4, 6);
cout << " Game Over! ";
SetColor(7, 0);
}
//得分
class Score
{
private:
short ln; //得分
public:
void Reset()
{
ln = 0;
}
void Print()
{
SetPos(19, 2);
SetColor(2, 7);
cout << " Lines: " << ln << " ";
SetColor(7, 0);
}
Score()
{
Reset();
}
void Update(short n)
{
ln += n;
}
};
//管理各种形状的类, Board Shape 要互为友元
class Shape
{
private:
short present; //当前形状:char type【19】[17](取值0~18)
short color;
short x; //左上角横坐标
short y; //左上角纵坐标
public:
Shape(short, short, short); //传入方块的(颜色,x, y)
void Draw(bool, bool); //第一个参数为真:绘制操作,为假:擦除操作。 第二个参数:为真:按方块的颜色画,为假:画成白色(如果已经到底了需要同意涂成白色)。
friend class Board; //声明:后面才有 Board
bool Rotate(Board& brd); //判断方块是否可以旋转,如果能:完成旋转,并返回真。 brd 是会随时变化的,所以需要 &brd
short Collision(Board& brd); //左侧、右侧、下方受阻时,分别返回,二进制[0000 0111]末尾对应的,"位或"==> 1 2 4 , [0101]=5 左有阻挡物又 着地,[0110]=6 右侧阻挡物又 着地
bool Move(short, Board& brd); //左移参数为:1, 右移参数为:2
bool Fall(Board& brd); //能否下落? 能:true, 不能返回:false
void DrawElseWhere(short _x, short _y); //提示下一个出现的方块。
};
//游戏框类
class Board
{
private:
bool wall[22][14]; //值为真,表示该位置已被占用,为假表示该位置为空。
short bottom_line; //保存可以进行消除位置的末行行号。
public:
void Clear() //重置数组元素的值,初始化
{
for (short i = 0; i < 22; i++) //行遍历
{
for (short j = 0; j < 14; j++) //列遍历
{
if (j > 1 && j < 12 && i < 20) //除去边框,实际可操控的空间为:[19][10]
{
wall[i][j] = 0;
}
else
wall[i][j] = 1;
}
}
}
Board()
{
Clear();
}
void DrawBoard()
{
short i;
SetColor(0xe, 0xe); // 0xe = 14
for (i = 0; i < 21; i++)//绘制边框左边界
{
SetPos(LMARGIN - 2, TMARGIN + i - 1); //LMARGIN -2 掉落范围 -2的位置
cout << Block;
}
for (i = 0; i < 21; i++)//绘制边框右边界
{
SetPos(LMARGIN + 20, TMARGIN + i - 1);
cout << Block;
}
for (i = 0; i < 10; i++)
{
SetPos(LMARGIN + i * 2, TMARGIN + 19); // * 2 两个横坐的标宽 = 一个宽字符
cout << Block;
}
SetColor(7, 0); //颜色恢复 白字 黑背景
}
//对wall数组值进行更新(更新堆积块)
short SetBlockValue(Shape& shp)
{
for (short i = 0; i < 16; i++)
{
if (type[shp.present][i] == '1') //表示4*4的格子中方块对应占据位置
{
if (shp.y == 0) //方块到顶了。游戏结束
return -1;
wall[shp.y + i / 4][shp.x + i % 4] = true; // 方块在实际游戏画面所在位置 + 方块在4*4 格子的位置 = 实际位置
}
}
return 0;
}
//返回可消除的行数
short Drop(Shape& shp)
{
//消除后,重绘范围:下落物体下方的堆积快不用重绘
//遍历4*4=16个格子,每个图形方块只占据4格,只要方块格子达到4就可以跳出循环。
short i = 0; //遍历 方块图形 Z S L... 的 4*4 格子
short n = 0; //组成图形的方块 一共4个
short y = 0; //当前格子行号
short _y = -1; //y前一格行号,初始化为一个不合理的值 -1,方便对比。
short j = 0; //j 用于遍历一整行,是否全满,如果有 false 表示未满。
short lines = 0;//记录消除多少行。
short k = 0; //从下往上 逐行覆盖
short top = 0; //检查是否遍历到 堆积快顶部。
short l = 0; //从左往右 逐列遍历(在覆盖的过程中)
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++)
{
if (!wall[y][j])
break;
}
if (j == 12) //一整行已经填满,消除该行
{
lines++; //消除的行数更新
for (k = y; k > 0; k--)
{
for (l = 2; l < 12; l++) //除去边界的一行
{
wall[k][l] = wall[k - 1][l];//上一行 ==> 覆盖下一行
top += wall[k - 1][l];
}
if (top == 0) //如果一行遍历完,top 的值还是0,就证明这一行已经没有堆积块,已经到堆积块顶部了。那么上面的空间就不要再进行判断了。break 跳出循环。
break;
}
}
}
}
bottom_line = _y; //要消除行的 下面不需要进行重绘。只需要重绘上面
return lines; //最后返回要消除的行号
}
//重绘堆积块
void DrawPile() //重绘堆积块,由 bottom_line 指定需要重绘的最下面一行,已经在 Drop 函数定义。
{
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 << block;
}
else
{
SetColor(0, 0); //黑色,用于擦除
cout << White;
}
}
}
}
friend class Shape; //wall 为私有,要访问私有,声明友元
};
//构造函数:在类定义的外部实现类的成员,类名+:: x=5 y=0 方块出现比较合适的默认值
Shape::Shape(short _color, short _x = 5, short _y = 0) :color(_color), x(_x), y(_y) //根据第一个参数的值来确定出现什么方块(这里方块与颜色进行固定绑定了)
{
switch (_color)
{
case 1: present = 0; break; //Z [0<==>1] 方块除去旋转后的结果,一共只有 7 种不同的形状。
case 2: present = 2; break; //S [2<==>3] 方块旋转时,位置会发生改变,但是方块样式不会发生改变(方块样式与颜色进行了绑定)
case 3: present = 4; break; //I [4<==>5] 注意:要改变 present 的值,只能用 Alter() 函数对其赋值,确保赋值的合法性。
case 4: present = 6; break; //O [ 6 ] 不发生改变
case 5: present = 7; break; //T [7<==>10]
case 6: present = 11; break; //L [11<==>14]
default:color = 8; present = 15; break; //J [15<==>18]其余情况,颜色统一赋值为8,方块初始形状为15
}
}
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') //方块种类:type 中对应的字符串 ==> 对应 Shape 中的 present 成员 (0~18范围)。
{
//LMARGIN -4(两个字符宽度才是显示屏上的一个正方形)
//x 坐标的:下标位, 但是由于两个宽的=一个正方格子宽度,实际宽度需要*2
//i%4 ,不同形状 Z S L... 在4*4的格子,%4 得到他在这个4*4格子中的 下标位置(从0开始)
// 格子中的 [坐标位] + [x] 的坐标位 = 这个方块在整个游戏框当中的坐标。
//y + i / 4 - 1 同理: -1防止穿过地面, i/4:4*4格子的横坐标, + y 游戏框 的坐标
SetPos(LMARGIN - 4 + (x + i % 4) * 2, TMARGIN + y + i / 4 - 1);
cout << Block; //位置和颜色都确定了之后输出方格。
}
}
SetColor(7, 16); //颜色恢复 白字 黑背景
}
bool Shape::Rotate(Board& brd) //旋转,成员函数::
{
for (short i = 0; i < 16; i++) //遍历图形方块 4*4的格子,是否有阻挡物,导致无法旋转
{
if (brd.wall[y + i / 4][x + i % 4] && rotate_hinder[present][i] == '1') //i/4:4*4格子的横坐标 ,&&能够阻碍旋转
{
//if() //待完善:L J I S Z //如果是靠墙,旋转,要退回一格在进行旋转
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') //该格子是否被当前方块占用,为'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 是擦除,第二个true表示画自身的颜色。(第一个为false时,可以不考虑第二个参数的值)。
x--;
Draw();
return true;
}
if (direction == 2 && !(Collision(brd) & 2)) //按的右, 并且可以 右移(没有阻挡物)
{
Draw(false, true);
x++;
Draw();
return true;
}
return false;
}
//下落
bool Shape::Fall(Board& brd)
{
if (Collision(brd) & 4) //已经着地
{
Draw(true, false); //画成堆积块,第一个参数为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, 0);
cout << " Next: ";
_x += 6;
SetColor(0, 0);
for (i = 0; i < 12; i++)//擦除前一次的方块,初始状态,最多只会占据前3行。无需变量4*4 =>16
{
SetPos(_x + i % 4 * 2, _y + i / 4);
cout << White;
}
SetColor(color, color); //设置绘制颜色
_y -= (present == 6 || present == 4);//当出现的图形为 6或4 为真,逻辑表达式结果为 1,否则为0。 y轴就达到了是否 y-1 的效果。(方型□,和长条型I 存在比其他图形存在低一行的问题)
for (i = 0; i < 12; i++)//绘出下一次出现的方块
{
if (type[present][i] == '1')
{
SetPos(_x + i % 4 * 2, _y + i / 4);
cout << Block;
}
}
}
//菜单:选中开始游戏返回1, 退出游戏返回0
short Menu()
{
//控制台窗口设置
//system("mode con cols=35 lines=27"); //窗口大小
system("title 俄罗斯方块"); //标题
//隐藏光标
HANDLE hOutput = GetStdHandle(STD_OUTPUT_HANDLE); //获得句柄(光标的信息)
//隐藏光标操作
CONSOLE_CURSOR_INFO CursorInfo;
GetConsoleCursorInfo(hOutput, &CursorInfo);//获取控制台光标信息
CursorInfo.bVisible = false; //隐藏控制台光标
SetConsoleCursorInfo(hOutput, &CursorInfo);//设置控制台光标状态
short choice = 1; //跟踪用户选项
char c; //记录用户按键信息
system("cls"); //清屏
SetPos(8, 5); //输出坐标 //┏ ━┃┗ ┛┓
cout << "┏━━━━━━━━━━━━━━━━┓"; // 16个 ━
SetPos(8, 6); //定位新坐标
cout << "┃ 俄 罗 斯 方 块 ┃";
SetPos(8, 7);
cout << "┗━━━━━━━━━━━━━━━━┛";
SetPos(7, 18);
cout << "控制提示:← → 移动";
SetPos(7, 20);
cout << "↑ 旋转, ↓ 快速下落";
SetPos(8, 22);
cout << "空格键:暂停/继续";
SetPos(8, 24);
cout << "Esc:强制退出游戏";
while (1) //等待用户选择
{
SetPos(12, 13);
if (choice == 1) SetColor(2, 7);//被选中时:前景色:2(绿色), 背景色:7(白色)
else SetColor(2, 0); //未选中时:前景色:2(绿色), 背景色:0(黑色)
cout << " 开始游戏 ";
SetPos(12, 15);
if (choice == 2) SetColor(2, 7);//被选中时:前景色:2(绿色), 背景色:7(白色)
else SetColor(2, 0); //未选中时:前景色:2(绿色), 背景色:0(黑色)
cout << " 退出游戏 ";
c = _getch(); //包涵头文件 #include <conio.h>
if (c == VK_RETURN) break; //用户按下了回车,离开循环体
else
if (c == 0) choice = _getch();
//如果按键为 ↓ 或 ↑ ,重新赋值 choice ,原本为 1 改为 2 ,否则改为 1 。开始游戏,退出游戏 上下交换着选择
if (c == 72 || c == 80) choice = choice == 1 ? 2 : 1; //用户按了上下键的反馈:让 coice 在1,2之间切换选择。
}
SetColor(7, 0); //回复黑底白字颜色
return choice; //返回选择结果
}
//int main()
//{
// //控制台窗口高度和宽度
// system("mode con cols=35 lines=27");
// handle = GetStdHandle(STD_OUTPUT_HANDLE);
//
//
// Board board;
// board.DrawBoard();
// //Shape shp(6, 5, 0);
// Shape shp(6); //有了默认值,不指定坐标也可以
// shp.Draw();
//
// //测试变形
// //Sleep(2000);
// //shp.Rotate(board);
//
// //测试移动
// while (shp.Move(1, board)) // 左移
// Sleep(250);
// while (shp.Move(2, board)) // 右移
// Sleep(250);
//
// //测试下落
// while (shp.Fall(board))
// Sleep(250);
//
// return 0;
//}
int main()
{
//控制台窗口高度和宽度
system("mode con cols=35 lines=27");
handle = GetStdHandle(STD_OUTPUT_HANDLE);
Board board;
short tmp; //用来保存一个方块消去的行数
Score score; //建立分数类 实例化
Shape* shp; //指向当前方块的 指针。
Shape* shp_next; //指向下一方块的 指针。
short times; //用来保存执行 Sleep() 函数的次数
srand(time(NULL)); //使游戏运行时,每次产生方块随机。
char ch; //用来清空输入流文件
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); //重新产生一个方块 //得到0~6 + 1, 默认参数 5, 0,其余颜色直接 default:color = 8;
shp_next->DrawElseWhere(4, 2); //显示出下一个出现的方块
times = 0;
while (1) //每执行一遍,循环体就是捕捉一次按键行为的过程。每10次循环,方块下落一格,直到低跳出循环。
{
shp->Draw();
if (GetAsyncKeyState(VK_LEFT) == -32767) //VK_LEFT 是左方向键 ← 的虚拟键码
shp->Move(1, board); //向左移动
if (GetAsyncKeyState(VK_RIGHT) == -32767) //VK_RIGHT 是右方向键 → 的虚拟键码
shp->Move(2, board); //向右移动
if (GetAsyncKeyState(VK_UP) == -32767) //VK_UP 向上方向键 ↑ 执行旋转 方块
shp->Rotate(board);
if (GetAsyncKeyState(VK_DOWN) == -32767) //VK_DOWN 向下方向键 ↓ 快速降落
shp->Fall(board);
if (GetAsyncKeyState(VK_SPACE) == -32767) //VK_SPACE 空格键:暂停/继续游戏
{
while (!(GetAsyncKeyState(VK_SPACE) == -32767))//如果没有再次按下空格键就继续等待。
Sleep(25);
}
if (GetAsyncKeyState(VK_ESCAPE) == -32767) //VK_ESCAPE ESC键:退出游戏
{
SetPos(13, 10);
SetColor(4, 6);
cout << "退出游戏!" << endl;
SetPos(1, 22);
return 0;
}
Sleep(25); //延时
times++;
if (times == 10) //每250毫秒 方块 下落一次
{
times = 0; //重置时间记录次数。
if (!shp->Fall(board)) //是否可以往下落。 Fall 函数已经封装了擦除,重绘行为。
break;
}
}
//判断是否结束
if (board.SetBlockValue(*shp) == -1) //更新堆积块
{
PrintGameOver(); //打印 游戏结束 图标
fflush(stdin); //清空:输入输出流
while ((ch = getchar()) != '\n' && ch != EOF) //getch();
{ ; } //不是回车,并且不是以文件结尾结束,就继续读
delete shp_next;
break;
}
//根据消行得分
if (tmp = board.Drop(*shp)) //当有堆积块被消除时才进行重绘 (消除行!=0)
{
board.DrawPile();
score.Update(tmp); //累加得分,tmp = 当前消除的行数
score.Print(); //显示得分
}
//...是否消行
delete shp; //释放已经堆叠的方块。然后重新产生新的方块。
}
}
return 0;
}