zoukankan      html  css  js  c++  java
  • chineseChess

    最近学习了chineseChess的Qt实现,把一些东西总结一下:

    实现功能:

    1.人人对战

    2.人机对战

    3.网络版

    一、基础性工作:(人人对战)

    1、棋盘和棋子的绘制(QPinter,drawLine(QPoint(0,0),QPoint(0,9))):

      棋盘: 10行,9列,中间有楚河汉界;九宫格;兵和炮的梅花位置。

      棋子:32颗棋子都是由圆圈和汉字组成:drawStone(painter, i)

         注意:1、死棋不画  if(_s[id]._dead)   return;

            2、线:painter.setPen(Qt::black);   填充:painter.setBrush(QBrush(Qt::gray));  字体:painter.setFont(QFont("system", _r, 700)); 

               字:painter.drawText(rect, _s[id].getText(), QTextOption(Qt::AlignCenter));   getText()可用switch语句实现,返回QString类型。

    2、初始化棋盘

         每个棋子有自己的 _id , _row , _col , _type , _Red, _dead:

           棋子的id可由数组_s[32]存储,由于棋子的中心对称性,棋子的 id 分配红棋从左上角开始是从0到15,黑棋从右下角开始是从16到31; 

                        这么分配的优势,中心对称的两颗棋子,行列和为定值:row(red)+row(black)=9; col(red)+col(black)=8;

            _s[i]._red = id<16;

           _s[i].dead = false;

            type有七种:车、马、象、士、将、炮、兵

                       红棋的 _row , _col ,_type可定义结构体数组一一初始化,黑棋可利用对称性给出。

           棋子类型可使用枚举方法: enum TYPE{CHE, MA, XIANG, SHI, JIANG, PAO ,BING }_type;

           枚举类型,系统默认第一个元素为0,其后元素依次加1。 不可为枚举类型赋值,但可以在初始化时给其赋值。

       eg:       enum TYPE{CHE=2, MA, XIANG, SHI, JIANG, PAO ,BING };  则XIANG为4,SHI为5。

    3、走棋规则的定制

    七种棋判断是否可以移动,先不判断是否吃棋。

    canMove(int moveId, int killedId, int row, int col)

     利用switch(_s[moveId]._type)语句 返回 每种棋子的 canMoveType()函数。

     特点:

      车走直线,需判断 起始点 之间棋子个数是否为0;

      马走,需判断马蹄上是否有棋子;

      象走,需判断象眼上是否有棋子;且行走范围只有半边天;顶部和底部的象,规则和而不同。

      士,活动范围九宫格斜线;顶部和底部的士,规则和而不同。

      将,飞将:若killId != -1 && _s[killId]._type == Stone::JIANG,若两将之间棋子个数为0,return ture;九宫格直线;顶部和底部的将,规则和而不同。

      炮,计算 起始点 之间棋子个数。棋子个数为1,则killedId != -1;棋子个数为0,则killedId == -1.

      兵,不能后退,过河后才能横向走。顶部和底部的兵,规则和而不同。

    为了实现七种棋子的可走性判断,需要一些辅助函数:起始点的位置关系(利用权重求和即可)、起始点之间的棋子个数、初始化在底部的棋子判断。

     int relation(row1, col1, row, col)
    {
      return qAbs(col1 - col) + qAbs(10*(row1 - row) ); 
    }
     int getStoneConutAtLine(int row1, int col1, int row2, int col2)    //车和炮需要判断,特点:行和列仅有一个相同
    {
      if(row1 != row2 && col1 == col2)
      {
                 int min = row1 < row2 ? row1 : row2;
                 int max = row1 > row2 ? row1 : row2;
                 for(int row = min + 1; row < max; ++row)
                 {
                       if(getStoneId(row, col1) != -1)  ++count;  //遍历非死棋,返回 行列 满足所求的id
                 }        
      }
          else if(row1 == row2 && col1 != col2)
          {
                 int min = col1 < col2 ? col1 : col2;
                 int max = col1 > col2 ? col1 : col2;
                 for(int col = min + 1; col < max; ++col)
                 {
                       if(getStoneId(row1, col) != -1)  ++count;
                 }    
          }
          else 
                return -1;
    
      return count;
    }
    bool isBottomSide(int id)
    {
            return _bRedSibe == _s[id]._red;   //_bRedSide表示红棋在下边。网络版红黑边不同,初始化利用中心对称,rotate即可
    }

     4、走棋

    第一步:鼠标点击  到  棋子id  的转换

      利用mouseReleaseEvent(QMouseEvent *ev)事件将鼠标点击像素位置传给 ev,调用ev->pos()即可返回点击点的像素坐标;

      利用帮助函数(下面有实现)getClickRowCol( QPoint pt, int &row, int &col)可获取行列坐标;

      利用getStoneId(row, col)可获取棋子id,从而实现了由鼠标点击到棋子id的转换。

      利用virtual void click(int id, int row, int col)来实现棋子的移动;虚函数:可以在其他类型的作战中重新实现click函数,实现其多态性。

     注:

    void Board::click(int id, int row, int col)
    {
         trySelectStone(id);       //与执棋方颜色一致,可选。连续选择执棋方,换棋。选后,this->_selectid == id
         else if(this->_selectid != -1) //轮到执棋方走时,选择空白地或是对方棋子无效 
         { 
                 tryMove(id, row, col); 
         }
    }
    void trySelect(int id)  
    {
         if(canSelect(id))
         {
             _selectId = id;
             update();
    rerurn;
    }
    } bool canSelect(id) { return _bRedTurn == _s[id].red; }

    尝试走棋。分两种情况,点击无棋处尝试走棋,点击异色尝试吃棋。

    void tryMove(int killid, int row, int col)
    {
    bool ret = canMove(_selectId, killId, row, col);
        if(ret)
        {
            moveStone(_selectId, killId, row, col);
            _selectId = -1;     //_selectId初始化
            update();
        }
    }
    void Board::moveStone(int moveid, int killid, int row, int col)
    {
        saveStep(moveid, killid, row, col, _steps);   //保存步骤用于悔棋
    
        killStone(killid);     //吃棋
        moveStone(moveid, row, col);  //走棋,轮换
    }
    void Board::moveStone(int moveId, int row, int col)
    {
        _s[moveId]._row = row;
        _s[moveId]._col = col;
        _bRedTurn = !_bRedTurn;    //轮换走棋
    }
    void Board::killStone(int id)
    {
        if(id==-1) return;
        _s[id]._dead = true;
    }
    void Board::saveStep(int moveid, int killid, int row, int col, QVector<Step*>& steps)
    {
        GetRowCol(row1, col1, moveid);
        Step* step = new Step;
        step->_colFrom = col1;
        step->_colTo = col;
        step->_rowFrom = row1;
        step->_rowTo = row;
        step->_moveid = moveid;
        step->_killid = killid;
    
        steps.append(step);
    }

    help function:

    1、_id可以通过_s[_id]._row获取行坐标:设棋盘一格为d,简单起见,设棋盘与边界距离也为d(注意d为棋盘像素坐标)根据棋盘行列与d的关系

                              不难推出id对应的棋盘像素坐标。 eg.  point.rx() = (col + 1)*_r*2;

    2、由棋盘上的 点击点QPoint 确定属于那个棋位(row,col)。

                           一种可行的方法是:遍历棋盘各个行列坐标,由行列坐标转化为棋盘像素坐标,并求取与 点击点的像素坐标 的距离dist,若dist<_r,则改点行列式即为所求行列值。其中 _r 为棋盘一格长度的一半。

                           改进1:只需要遍历点击点附近的坐标,无需遍历整个棋盘。

                改进2:利用浮点型,整型转换直接找到  

            bool Board::getClickRowCol( QPoint pt, int &row, int &col)

            {  col = d * (   int ( pt.x()/(d*1.0) - 0.5 )  );

                row = d * (   int ( pt.y()/(d*1.0) - 0.5 )  );

                             if(row < 0 || row > 9 || col < 0 || col > 8)

                 rerurn false;  

               else  

                               return true;

                           }

                            注:返回值可用于判断行列式是否成功得到;利用引用行列值传递进去,可以在成功获取行列值后进行保存,用法挺好,学习一下。    

    二、人机对战(最大最小值算法,减枝优化算法)

    利用虚函数的性质,实现click(int id, int row, int col)函数的多态性。

    红棋走就调用父类的click(int id, int row, int col)函数,走完后转换走棋方,让电脑走棋。由于电脑在考虑多步时,时间很久容易阻塞主进程,所以在电脑走棋前可以调用定时器0.1s,让红旗完成走棋,重绘棋盘。

    电脑利用最大最小值算法按评分最大值的步骤进行移动,即:

    void SingleGame::computerMove()
    {
        Step* step = getBestMove();
        moveStone(step->_moveid, step->_killid, step->_rowTo, step->_colTo);
        delete step;   //防止内存泄露
        update();
    }

    最大最小值算法:在你选择的箱子中,找到每个箱子最小值中最大的那个箱子。

    步骤:

    1、找到所有可以走的步骤

    2、尝试走一步(选箱子)

    3、计算这一步中最小的分数

    4、最小分数>预定义的最大值(很小的值)就将该分数保存为当前的最大值。

    5、退回尝试走的一步,进行下一步尝试

    Step* SingleGame::getBestMove()
    {
        Step* ret = NULL;
        QVector<Step*> steps;
        getAllPossibleMove(steps);           //保存可能走的步子
        int maxInAllMinScore = -300000;
    
        while(steps.count())
        {
            Step* step = steps.last();
            steps.removeLast();
    
            fakeMove(step);
            int minScore = getMinScore(this->_level-1, maxInAllMinScore);  //电脑走后,人走的左右步骤后电脑可得的最低分返回
            unfakeMove(step);
    
            if(minScore > maxInAllMinScore)
            {
                if(ret) delete ret;
                ret = step;
                maxInAllMinScore = minScore;
            }
            else
            {
                delete step;
            }
        }
        return ret;
    }

    运用剪枝算法的最小分:

    int SingleGame::getMinScore(int level, int curMin)
    {
        if(level == 0)
            return score();
    
        QVector<Step*> steps;
        getAllPossibleMove(steps);
        int minInAllMaxScore = 300000;
    
        while(steps.count())
        {
            Step* step = steps.last();
            steps.removeLast();
    
            fakeMove(step);
            int maxScore = getMaxScore(level-1, minInAllMaxScore);
            unfakeMove(step);
            delete step;
    
            if(maxScore <= curMin)             //上层是找最小分中的最大分,所以该层所求分数小于上层的最大分,则其后就不必计算,即减枝
            {
                while(steps.count())
                {
                    Step* step = steps.last();
                    steps.removeLast();
                    delete step;
                }
                return maxScore;
            }
    
            if(maxScore < minInAllMaxScore)     //找该箱子的最小分,如果有更小的分数,则保存该分数。
            {
                minInAllMaxScore = maxScore;
            }
    
    
        }
        return minInAllMaxScore;
    }

    运用剪枝算法的最大分:

    int SingleGame::getMinScore(int level, int curMin)
    {
        if(level == 0)
            return score();
    
        QVector<Step*> steps;
        getAllPossibleMove(steps);
        int minInAllMaxScore = 300000;
    
        while(steps.count())
        {
            Step* step = steps.last();
            steps.removeLast();
    
            fakeMove(step);
            int maxScore = getMaxScore(level-1, minInAllMaxScore);
            unfakeMove(step);
            delete step;
    
            if(maxScore <= curMin)
            {
                while(steps.count())
                {
                    Step* step = steps.last();
                    steps.removeLast();
                    delete step;
                }
                return maxScore;
            }
    
            if(maxScore < minInAllMaxScore)
            {
                minInAllMaxScore = maxScore;
            }
    
    
        }
        return minInAllMaxScore;
    }

    评分:

    由于是电脑判断当前分数来走棋,所以将评分定义为黑棋现有分数-红棋现有分数。

    定义一个数组,分别存放相应七类棋的分数,统计一下相应颜色活期的总分,即为该方现有分数。

    统计所有可走的步骤:(可优化)

    _bRedTurn:遍历id为0到15的活棋子;

    !_bRedTurn:遍历id为16到31的活棋子。

    遇到死棋,跳过。

    走棋时遇到同色棋子跳过,遍历整个棋盘其余可走可吃的位置。

    void SingleGame::getAllPossibleMove(QVector<Step *> &steps)
    {
        int min, max;
        if(this->_bRedTurn)
        {
            min = 0, max = 16;
        }
        else
        {
            min = 16, max = 32;
        }
    
        for(int i=min;i<max; i++)
        {
            if(this->_s[i]._dead) continue;    //死棋不计
            for(int row = 0; row<=9; ++row)
            {
                for(int col=0; col<=8; ++col)
                {
                    int killid = this->getStoneId(row, col);
                    if(sameColor(i, killid)) continue;  //不杀同类
    
                    if(canMove(i, killid, row, col))
                    {
                        saveStep(i, killid, row, col, steps);
                    }
                }
            }
        }
    }
    void Board::saveStep(int moveid, int killid, int row, int col, QVector<Step*>& steps)  //使用容器对步伐进行保存
    {
        GetRowCol(row1, col1, moveid);
        Step* step = new Step;
        step->_colFrom = col1;
        step->_colTo = col;
        step->_rowFrom = row1;
        step->_rowTo = row;
        step->_moveid = moveid;
        step->_killid = killid;
    
        steps.append(step);
    }

    优化:

    可以对棋子分类别进行可行区域的计算。

    将:前后左右四个位置,满足:1、九宫格内;2、不杀同类;

    士:也是最多四个位置,条件同将;

    车:行为当前车的行,列从0到8;&& 列为当前车的列,行从0到9。条件:同类不杀,起始点间棋子个数为0。

    马:可走位置最多8个。条件:在棋盘内;马蹄无字;同类不吃。

    象:最多四个位置条件:在棋盘内;象眼无子;同类不吃。

    炮:遍历位置同车

    兵:最多三个位置。

    除了黑色部分,其余可以使用canMove()进行判断。

    三、网络版(客户端与服务器)

     客户端:

    连接服务器:connectToHost("host",port);

    发送请求:socket->write(" ");

    读数据:socket->readAll();

    断开连接:socket->close();

    服务器:

    监听:server->listen(QHostAddress::Any, port);

    挑选空闲服务器:socket = server->nextPendingConnection();

    服务(写数据):socket->write(" ");

    断开连接:socket->close();

    NetGame::NetGame(bool server, QWidget *parent) : Board(parent)
    {
        _server = NULL;                //初始化为空
        _socket = NULL;
        _bServer = server;            //在启动服务器和客户端时,初始化为true和false.
    
        if(_bServer)
        {
            _server = new QTcpServer(this);
            _server->listen(QHostAddress::Any, 9899);
            connect(_server, SIGNAL(newConnection()), this, SLOT(slotNewConnection()));
        }
        else
        {
            _socket = new QTcpSocket(this);
            _socket->connectToHost(QHostAddress("127.0.0.1"), 9899);
            connect(_socket, SIGNAL(readyRead()), this, SLOT(slotDataArrive()));
        }
    }

    初始化棋盘由服务器发送数据,客户端接收。

    1、初始化棋盘(flag为1,随机产生第二个数)

    走棋由click(int id, int row, int col)虚函数发送数据,服务器和客户端接收,对方按照棋盘中心对称的性质进行转换。

    2、走棋(flag为2,id, row, col)

    悔棋由back()虚函数执行,己方棋盘红黑各悔一步后,发送flag为3,对方棋盘红黑也各悔一步,保持同步。

    3、悔棋(flag为3)

    服务器的槽函数:

    void NetGame::slotNewConnection()
    {
        if(_socket) return;   //已选到服务器,则返回
    
        _socket = _server->nextPendingConnection(); //接线员连接下一个空闲服务器
        connect(_socket, SIGNAL(readyRead()), this, SLOT(slotDataArrive()));    //保证click函数调用后,客户端和服务器都可以接受slotDataArrive()槽函数
    
        /* 产生随机数来决定谁走红色 */
        bool bRedSide = qrand()%2>0;    //主程序使用qsrand()对种子进行了初始化
        init(bRedSide);
    
        /* 发送给对方 */
        QByteArray buf;
        buf.append(1);
        buf.append(bRedSide>0?0:1);      //若bRedSide为1,就将0存在buf数组的第二个位置,发送给客户端
        _socket->write(buf);
    }

    客户端的槽函数:

    void NetGame::slotDataArrive()
    {
        QByteArray buf = _socket->readAll();
        switch (buf.at(0)) {
        case 1:
            initFromNetwork(buf);       //flag为1,初始化棋盘
            break;
        case 2:
            clickFromNetwork(buf);      //flag为2,给对方发送走棋数据,对方需要根据棋盘的中心对称性质进行转换
            break;
        case 3:
            backFromNetwork(buf);       //flag为3,让对方棋盘红黑各悔一步,与己方保持一致
            break;
        default:
            break;
        }
    }

    flag对应的函数:

    void NetGame::backFromNetwork(QByteArray)
    {
        backOne();
        backOne();
    }
    void NetGame::clickFromNetwork(QByteArray buf)
    {
        Board::click(buf[1], 9-buf[2], 8-buf[3]);
    }
    void NetGame::initFromNetwork(QByteArray buf)
    {
        bool bRedSide = buf.at(1)>0?true:false;
        init(bRedSide);
    }

    走棋:虚函数click(int id, int row, int col)的重载:

    void NetGame::click(int id, int row, int col)
    {
        if(_bRedTurn != _bSide)       //!(轮到红棋走,红旗在下方)  ||  !(轮到黑棋走,黑旗在下方)
    return;
        Board::click(id, row, col); /* 发送给对方 */ 
    QByteArray buf;
    buf.append(
    2);
    buf.append(id);
    buf.append(row);
    buf.append(col);
    _socket
    ->write(buf);
    }

    悔棋:虚函数back()的重载

    void NetGame::back()
    {
        if(_bRedTurn != _bSide)    
            return;
        backOne();
        backOne();
    
        QByteArray buf;
        buf.append(3);
        _socket->write(buf);
    }
  • 相关阅读:
    2. Add Two Numbers
    1. Two Sum
    leetcode 213. 打家劫舍 II JAVA
    leetcode 48. 旋转图像 java
    leetcode 45. 跳跃游戏 II JAVA
    leetcode 42. 接雨水 JAVA
    40. 组合总和 II leetcode JAVA
    24. 两两交换链表中的节点 leetcode
    1002. 查找常用字符 leecode
    leetcode 23. 合并K个排序链表 JAVA
  • 原文地址:https://www.cnblogs.com/Lunais/p/5648591.html
Copyright © 2011-2022 走看看