将区域拆分成方块组合

本文是以《遍历列举俄罗斯方块的所有形状》为基础,继续进行的程序编写。

之前已经遍历了类俄罗斯方块的形状,包括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秒之内找到有效的拆分方式。

九,完整的工程,可从如下地址获取:

https://download.csdn.net/download/lintax/93005316