本文是以《遍历列举俄罗斯方块的所有形状》为基础,继续进行的程序编写。
之前已经遍历了类俄罗斯方块的形状,包括3、4、5个方块的组合。
接下来的目标是将一个完整的区域,例如7*10,分解为这些块组的组合。这么一说可能还是有些含糊不清,看一张图就明白了:

基本思路
下面是一个初步的思路:
1,先读取数目为3-5的方块组合;
2,列举出每个方块组合的旋转的情况;
3,对指定区域进行初始化;
4,选择一个方块组合,填入,判断合理性;如此重复;
5,若填充失败,则重试;
6,如此重复,直到恰好填充满指定区域;
7,展示结果。
以上步骤中,有一步是比较复杂的,就是如何判断填充失败。
我最开始的想法,是判断到最后一个块组,怎么也填不上去,试了所有块组,都失败,就是填充失败。
后来再想一想,其实可以更早就判断出失败,就是从左往右,从上往下填充,若是某一行没有填充满,就可以判断为失败了。
下面是具体的实现:
一,读取数目为3-5的方块组合;
这个组合是之前已经做好了,存放在一个文件里,这里,我们指定文件名为:
blockInfo-345.bin
并且就放在工程目录下。
然后是实现一个读取的功能:
int BlockDialog::readWholeBlockInfo(list<BlockGroup> &blocksListRead){
int ret = -1;
int blockNum = 0;
// 打开要读取的二进制文件
QFile file("blockInfo-345.bin");
if (!file.open(QIODevice::ReadOnly )) {
qDebug() << "无法打开文件";
return -1;
}
// 创建数据流对象并将其与文件关联起来
QDataStream out(&file);
// 从文件中读取数据
while(!file.atEnd()){
out >> blockNum;
BlockGroup *pBlock=new BlockGroup(blockNum);
pBlock->blockNum = blockNum;
qDebug() << pBlock->blockNum ;
for(int i=0;i<pBlock->blockNum;i++){
out >> pBlock->block[i].leftUp.rx();
out >> pBlock->block[i].leftUp.ry();
qDebug() << pBlock->block[i].leftUp.rx() << pBlock->block[i].leftUp.ry() ;
}
blocksListRead.push_back(*pBlock);
}
// 关闭文件
file.close();
ret = 0;
return ret;
}
二,列举出每个方块组合的旋转的情况;
最开始的想法,是先按方块组合的方块数目划分开,分别按方块的个数:3,4,5来处理旋转的图形,存储在不同的数组中。在后面使用的过程中,发现并不方便,所以还是将所有旋转后的形状,都存入一个大数组中了,只用一个索引就能找到每个形状,使用随机数来取各形状也很方便。
当然,旋转后的形状,是否是重复的,也要进行判断,丢弃重复的。
下面是具体实现:
//2,列举出所有旋转的形状
int loopNum=0;
for(auto it=blocksListRead.begin();it!=blocksListRead.end();it++){//遍历所有块�?
loopNum++;
qDebug()<<"loopNum="<<loopNum<<" blockNum="<< it->blockNum;
checkAddBlocksRotate(&(*it));
}
blockRotateNum = blockRotateList.size();
qDebug()<<"blockRotateNum="<< blockRotateNum;
// printAllBlockGroup();
里面有一个关键函数:
checkAddBlocksRotate()
作用是检查旋转形状的,如果是新形状就添加进数组中。具体实现如下:
void BlockDialog::checkAddBlocksRotate(BlockGroup *it){
BlockGroupRotates *blocksRotates = new BlockGroupRotates();
blocksRotates->pBlocksRotate[0]->flagNew=1;
blocksRotates->pBlocksRotate[0]->pBlockGroup=it;
//产生旋转的形状,并进行去重
BlockGroup *blockgroupTmp=new BlockGroup(it->blockNum);
*blockgroupTmp = *blocksRotates->pBlocksRotate[0]->pBlockGroup;
blockRotateList.push_back(*blocksRotates->pBlocksRotate[0]->pBlockGroup);
for(int i=1;i<4;i++){//每次旋转90度,检查3次,加之前的一次平移检查,即完成了360度的检查
BlockGroup *blockgroupTmp1=new BlockGroup(it->blockNum);
//1,对方块组旋转90度,就是(x,y) -> (y,-x)
RotateBlockGroup(blockgroupTmp);
//规范坐标
adjustCoordinates(blockgroupTmp);
*blockgroupTmp1 = *blockgroupTmp;
//对每次旋转的形状,进行去重判断(对本块组其他形状)
int flagRepeat=0;
for(int j=0;j<3;j++){//总共4种形状,最多需要比较3次
if(blocksRotates->pBlocksRotate[j]->flagNew==1){
//进行平移重复检查
if(1==checkTranslationRepeatability(blocksRotates->pBlocksRotate[j]->pBlockGroup,blockgroupTmp)){
flagRepeat=1;
break;
} else {
}
}
}
if(flagRepeat==0){//是一个新的形状,添加
blocksRotates->pBlocksRotate[i]->flagNew=1;
blocksRotates->pBlocksRotate[i]->pBlockGroup=blockgroupTmp1;
blockRotateList.push_back(*blocksRotates->pBlocksRotate[i]->pBlockGroup);
} else {
delete blockgroupTmp1;
}
}
}
三,对指定区域进行初始化;
这一步简单,直接看代码:
// 3,对指定区域进行初始化,赋初值为0
for(int y=0;y<areaHeight;y++){
for(int x=0;x<areaWidth;x++){
workArea[y][x]=0;
}
}
blockGroupResultList.clear(); //清空
四,选择一个方块组合,填入,判断合理性;如此重复;
这里选择方块组合,采用的是随机选择,避免总是拆分出相同的组合序列。
填入的方式:
填充顺序:先填充第一行,从左往右填充。第一行填充满了,填充第二行。如此递推。
选定一个方块组合后,先给这个块组的左上角的方块找一个位置,填入。然后,看随后几个方块的位置能否在工作区放下。
先看选择方块组合的实现:
BlockGroup* BlockDialog::getOneBlockGroup(){
//从列表中搜索,随机选择
int index = QRandomGenerator::global()->bounded(blockRotateNum);// 取随机整数
// 找到我们想访问的元素
auto it = blockRotateList.begin();
std::advance(it, index);
// printBlockGroup(&(*it));
return &(*it);
}
将指定的方块组合,填入工作区域,判断合理性。
这里是核心逻辑,初始化时将区域初始化为0了,代表没有填充,置为1就是代表已经填充了。若轮到填充一个块,而尝试多次也填充不上,就判断为失败。若填充上了,就设置填充标志。
//将指定的方块组合,填入工作区域,并判断合理性
//成功返回1,失败返回0
int BlockDialog::putBlockGroupInArea(BlockGroup * blockgroup, int workIndex){
int x=workIndex%areaWidth;
int y=workIndex/areaWidth;
for(int index=1; index < blockgroup->blockNum;index++){
int x_offset = blockgroup->block[index].leftUp.rx() - blockgroup->block[0].leftUp.rx();
int y_offset = blockgroup->block[index].leftUp.ry() - blockgroup->block[0].leftUp.ry();
if(((x+x_offset) < 0) || ((x+x_offset) >= areaWidth)){//x坐标越界
return 0;
}
if(((y+y_offset) < 0) || ((y+y_offset) >= areaHeight)){//y坐标越界
return 0;
}
if(workArea[y+y_offset][x+x_offset]==1){
return 0;//放不下,继续查找
}
}
for(int index=0; index < blockgroup->blockNum;index++){
int x_offset = blockgroup->block[index].leftUp.rx() - blockgroup->block[0].leftUp.rx();
int y_offset = blockgroup->block[index].leftUp.ry() - blockgroup->block[0].leftUp.ry();
workArea[y+y_offset][x+x_offset]=1;//可以存放,设置占用标志
}
return 1;
}
在每次填充一个块组后,要进行判断,是否填充完成。
五,下面是完成判断的代码:
//判断是否填充完成
//成功返回1,失败返回0
int BlockDialog::JudgeComplete(){
for(int y=0;y<areaHeight;y++){
for(int x=0;x<areaWidth;x++){
if(workArea[y][x]==0){
return 0;
}
}
}
return 1;
}
六,将以上的操作整合起来,如下:
void BlockDialog::on_splitButton_clicked()
{
//1,读文件,获取所有3-5的方块组合
readWholeBlockInfo(blocksListRead);
timerID=startTimer(1000);
//2,列举出所有旋转的形状
int loopNum=0;
for(auto it=blocksListRead.begin();it!=blocksListRead.end();it++){//遍历所有块�?
loopNum++;
qDebug()<<"loopNum="<<loopNum<<" blockNum="<< it->blockNum;
checkAddBlocksRotate(&(*it));
}
blockRotateNum = blockRotateList.size();
qDebug()<<"blockRotateNum="<< blockRotateNum;
// printAllBlockGroup();
// 把耗时任务丢到子线程!
QFuture<void> future = QtConcurrent::run([=]() {
// 这里面写 耗时操作! 不能操作 UI
while(1){
numTrySplit++;
qDebug()<<"onSplit : numTrySplit="<< numTrySplit;
// 子线程里发信号
emit progressUpdate(numTrySplit);
// 3,对指定区域进行初始化,例如3*3,赋值为0
for(int y=0;y<areaHeight;y++){
for(int x=0;x<areaWidth;x++){
workArea[y][x]=0;
}
}
blockGroupResultList.clear(); //清空
// 4,选择一个方块组合,填入,判断合理性;如此重复�?
BlockGroup* blockgroup;
int index = 0;
workIndex = getWorkIndex();
while(workIndex < (areaWidth*areaHeight)){
//选择一个方块组合,填入工作区域
int numRetry=0;
do{
blockgroup = getOneBlockGroup();
int ret = putBlockGroupInArea(blockgroup, workIndex);
// qDebug()<<"putBlockGroupInArea workIndex="<< workIndex<<" ret="<<ret<<" numRetry="<<numRetry;
if(ret == 1){
break;
}
numRetry++;
}while(numRetry<blockRotateNum);//对同一个位置,最多尝�?20�?
if(numRetry<blockRotateNum){
// qDebug()<<"push_back workIndex="<< workIndex<<" index="<<index;
//填充一个块组成功,则记录下�?
BlockGroupResult *blockGroupResult = new BlockGroupResult();
blockGroupResult->index = index;
blockGroupResult->xPos=workIndex%areaWidth;
blockGroupResult->yPos=workIndex/areaWidth;
blockGroupResult->pBlockGroup = blockgroup;
blockGroupResultList.push_back(*blockGroupResult);
index++;
} else {//尝试20次后仍然失败,则重头再来
break;
}
if(JudgeComplete()==1){//填充完成
//打印所有块,包括索引,在工作区域中的位置,以及组块形状信息�?
qDebug()<<"JudgeComplete : workIndex="<< workIndex<<" index="<<index;
printAllBlockGroupResult();
flagFlushResult=1;
return;//完成,退出此函数
}
workIndex = getWorkIndex();
}
}
});
// 开始监视(进度)
m_watcher->setFuture(future);
}
这里关联到界面上的一个按钮,在点击之后,执行拆分区域的算法。为了避免耗时操作影响界面的响应实时性,建立了一个子线程,来运行比较耗时的算法。添加了一个尝试次数的变量,显示在界面上,充当进度条指示的作用。
拆分完成后,将拆分结果展示在界面上。
这个是接收进度信号的槽函数:
// 进度信号来了,在这里更新界面
void BlockDialog::onProgressUpdate(int percent)
{
int percentLast = 0;
if(percentLast != percent){
flagFlushProgress = 1;
}
update(); // 你要的绘制也可以在这里调用,绝对不卡顿
}
七,最后,是展示结果
下面这个是画图形的函数:
void BlockDialog::paintEvent(QPaintEvent *event)
{
QPainter painter (this);//创建QPainter对象
QPen pen;
QBrush brush;
int row=0,column=0;
int W=this->width(); //绘图区宽度
int w=40;
int h=40;
painter.setRenderHint(QPainter::Antialiasing);
painter.setRenderHint(QPainter::TextAntialiasing);
//设置画笔
pen.setWidth(1); //线宽
pen.setStyle(Qt::SolidLine);//线的样式,实线、虚线等
pen.setCapStyle(Qt::FlatCap);//线端点样式
pen.setJoinStyle(Qt::BevelJoin);//线的连接点样式
painter.setPen(pen);
//设置画刷
// brush.setColor(QColor(255,255,128)); //画刷颜色
brush.setStyle(Qt::SolidPattern); //画刷填充样式
painter.setBrush(brush);
if(flagFlushProgress==1){
flagFlushProgress = 0;
//写字
pen.setColor(Qt::black); //划线颜色
painter.setPen(pen);
QString temp = QString("分割次数: %1").arg(numTrySplit);
painter.drawText(10,h+10,temp);
}
if(flagFlushResult==1){
flagFlushResult = 0;
//写字
pen.setColor(Qt::black); //划线颜色
painter.setPen(pen);
painter.drawText(10,h-10,"分割结果:");
QString temp = QString("分割次数: %1").arg(numTrySplit);
painter.drawText(10,h+10,temp);
//画区域分割的结果块组
row=0,column=0;
for(auto it=blockGroupResultList.begin();it!=blockGroupResultList.end();it++){
// qDebug()<<" blockPos="<< blockPos;
int blockNum = 5;//it->pBlockGroup->blockNum;
colorNum++;
pen.setColor(Qt::black); //划线颜色
painter.setPen(pen);
if((colorNum%COLOR_NUM)==0){
brush.setColor(Qt::yellow);//画刷颜色
} else if((colorNum%COLOR_NUM)==1){
brush.setColor(Qt::red);//画刷颜色
} else if((colorNum%COLOR_NUM)==2){
brush.setColor(Qt::green);//画刷颜色
} else if((colorNum%COLOR_NUM)==3){
brush.setColor(Qt::blue);//画刷颜色
} else if((colorNum%COLOR_NUM)==4){
brush.setColor(Qt::cyan);//画刷颜色
} else if((colorNum%COLOR_NUM)==5){
brush.setColor(Qt::magenta);//画刷颜色
} else if((colorNum%COLOR_NUM)==6){
brush.setColor(Qt::darkGreen);//画刷颜色
} else if((colorNum%COLOR_NUM)==7){
brush.setColor(Qt::darkYellow);//画刷颜色
} else if((colorNum%COLOR_NUM)==8){
brush.setColor(Qt::darkRed);//画刷颜色
} else if((colorNum%COLOR_NUM)==9){
brush.setColor(Qt::darkBlue);//画刷颜色
}
painter.setBrush(brush);
for(int j=0;j<it->pBlockGroup->blockNum;j++){
int x_offset = it->pBlockGroup->block[j].leftUp.rx() - it->pBlockGroup->block[0].leftUp.rx();
int y_offset = it->pBlockGroup->block[j].leftUp.ry() - it->pBlockGroup->block[0].leftUp.ry();
int x = (it->xPos + x_offset)*w + 1*w;
int y = (it->yPos + y_offset)*h + (1+1)*h;//大块的高为h , 后面部分为固定下移量
painter.drawRect(QRect(x, y, w, h));
// qDebug()<<"BGPos:("<<it->xPos<<","<<it->yPos<< ") x="<< it->pBlockGroup->block[j].leftUp.x() <<" y="<<it->pBlockGroup->block[j].leftUp.y()
// <<" offset: ("<<x_offset<<","<<y_offset<<")"<<" lastPos: ("<<x<<","<<y<<")";
}
column++;
// qDebug()<< " row="<< row <<" w="<<w<<" W="<<W<<" column="<<column;
//判断下一个块是否会超出窗体宽�?
if((column+1)*blockNum*w>W){
column=0;
row++;
}
}
QString str3 = QString::fromLocal8Bit("now time: ");
qDebug()<<str3 << QTime().currentTime();
}
}
至此,主要逻辑就都展示出来了。
八,调试过程中遇到的问题:
1,有相同颜色的不同块组遇到一起的情况,此时,看结果图,分不出这两个块组的形状。
解决思路:
可以为每一个块组画一个边界。
也可以简单处理,将COLOR_NUM 再增大一点。此处采用的是简单的方法。
2,新构建的字符串,在界面上显示,汉字乱码。
解决:
修改源文件为utf8格式。具体步骤:
顶部菜单:工具 → 选项 → 文本编辑器 → 显示
勾选 Display file encoding(显示文件编码)
确定后,编辑器右上角会显示当前文件编码(如 GBK、System、UTF-8)
然后,点击,修改即可。
3,调试时遇到不能显示中间进度值的情况,每次只能看到最后的一个值
原因:算法执行放在主循环中,没有执行完,不会更新界面。
解决:添加并发子任务,将耗时操作放入子任务重执行,这样不影响主界面的显示与响应。
4,发现每次分割的结果都是一样的
原因:rand()是伪随机。
解决:改为QT自带的随机函数QRandomGenerator,内部自动管理高精度随机种子,不会出现重复问题,无需手动srand。
5,执行拆分算法很慢
发现,竟然是调试过程中添加了sleep 导致的。去掉,就很快了,通常,在2秒之内找到有效的拆分方式。
九,完整的工程,可从如下地址获取: