栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > C/C++/C#

C++ QT 中国象棋代码及说明

C/C++/C# 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

C++ QT 中国象棋代码及说明

C++ QT 中国象棋代码及说明

本项目实现了棋盘和棋子的绘制、象棋的基本规则和基于最大最小值算法的n步人机对战。
这个象棋依然很初级,走5步的人工智能就会出现明显卡顿,而且还是下不过玩家(悲),因此更好的人工智能依然有待学习。
效果预览:

此处可以调节电脑思考层数

SingleGame::SingleGame(QWidget *parent) : Board(parent)
{
    _level = 5;
}

链接:https://pan.baidu.com/s/1szK50uwq5DFvBL1e7vgI6w
提取码:dame
说明部分只包含cpp文件部分。

文章目录
  • C++ QT 中国象棋代码及说明
    • 1.绘制棋盘
    • 2.移动棋子
    • 3.设置落子规则
      • 3.1 一些在规则制定能用到的中间函数
    • 3.2 各棋子的移动规则
      • 3.2.1 将的移动
      • 3.2.2 士的移动
      • 3.2.3 相的移动
      • 3.2.4 车的移动
      • 3.2.5 马的移动
      • 3.2.6 炮的移动
      • 3.2.7 兵的移动
    • 4.人机对战
      • 4.1 一步人机
      • 4.2 两步人机
      • 4.3 n步人机
      • 4.4 剪枝算法

1.绘制棋盘

使用QWidget、QPainter进行棋盘的绘制,QWidget类是所有的用户接口对象的基类,QPainter的核心功能是绘图。
在Board的无参构造函数中,初始32颗棋子的信息,这里需要注意,由于是交换着对战,所以实际上只有一个棋盘对象,_selectid表示当前选中的棋子坐标,_bRedTurn为True表示红方走棋,False的话表示黑方走棋。
画棋盘分为画线和画棋子,总的画界面函数如下:

    
    void paintEvent(QPaintEvent *);
    void drawPlate(QPainter& p);//绘制棋盘
    void drawPlace(QPainter& p);//绘制九宫格
    void drawInitPosition(QPainter& p);
    void drawInitPosition(QPainter& p, int row, int col);//绘制初始位置
    void drawStone(QPainter& p);
    void drawStone(QPainter& p, int id);//绘制棋子

实现后的代码如下

void Board::paintEvent(QPaintEvent *)
{
    int r = height()/20;
    _r = r;
    _off = QPoint(r+1, r+1);

    QPainter p(this);
    p.setRenderHints(QPainter::Antialiasing | QPainter::TextAntialiasing);

    p.save();
    drawPlate(p);
    p.restore();

    p.save();
    drawPlace(p);
    p.restore();

    p.save();
    drawInitPosition(p);
    p.restore();

    p.save();
    drawStone(p);
    p.restore();
}
void Board::drawPlate(QPainter &p)
{
    for(int i=0; i<10; ++i)
    {
        if(i==0 || i==9)
        {
            p.setPen(QPen(Qt::black, 3, Qt::SolidLine));
        }
        else
        {
            p.setPen(QPen(Qt::black, 1, Qt::SolidLine));
        }
        p.drawLine(center(i, 0), center(i, 8));
    }

    for(int i=0; i<9; ++i)
    {
        if(i==0 || i==8)
        {
            p.setPen(QPen(Qt::black, 3, Qt::SolidLine));
            p.drawLine(center(0, i), center(9, i));
        }
        else
        {
            p.setPen(QPen(Qt::black, 1, Qt::SolidLine));
            p.drawLine(center(0, i), center(4, i));
            p.drawLine(center(5, i), center(9, i));
        }
    }
}
void Board::drawInitPosition(QPainter &p, int row, int col)
{
    QPoint pt = center(row, col);
    QPoint off = QPoint(_r/6, _r/6);
    int len = _r/3;

    QPoint ptStart;
    QPoint ptEnd;

    if(col != 0)
    {
        
        ptStart = QPoint(pt.x() - off.x(), pt.y() - off.y());
        ptEnd = ptStart + QPoint(-len, 0);
        p.drawLine(ptStart, ptEnd);
        ptEnd = ptStart + QPoint(0, -len);
        p.drawLine(ptStart, ptEnd);

        
        ptStart = QPoint(pt.x() - off.x(), pt.y() + off.y());
        ptEnd = ptStart + QPoint(-len, 0);
        p.drawLine(ptStart, ptEnd);
        ptEnd = ptStart + QPoint(0, +len);
        p.drawLine(ptStart, ptEnd);
    }

    if(col != 8)
    {
        
        ptStart = QPoint(pt.x() + off.x(), pt.y() + off.y());
        ptEnd = ptStart + QPoint(+len, 0);
        p.drawLine(ptStart, ptEnd);
        ptEnd = ptStart + QPoint(0, +len);
        p.drawLine(ptStart, ptEnd);

        
        ptStart = QPoint(pt.x() + off.x(), pt.y() - off.y());
        ptEnd = ptStart + QPoint(+len, 0);
        p.drawLine(ptStart, ptEnd);
        ptEnd = ptStart + QPoint(0, -len);
        p.drawLine(ptStart, ptEnd);
    }
}
void Board::drawInitPosition(QPainter &p)
{
    drawInitPosition(p, 3, 0);
    drawInitPosition(p, 3, 2);
    drawInitPosition(p, 3, 4);
    drawInitPosition(p, 3, 6);
    drawInitPosition(p, 3, 8);

    drawInitPosition(p, 6, 0);
    drawInitPosition(p, 6, 2);
    drawInitPosition(p, 6, 4);
    drawInitPosition(p, 6, 6);
    drawInitPosition(p, 6, 8);

    drawInitPosition(p, 2, 1);
    drawInitPosition(p, 2, 7);

    drawInitPosition(p, 7, 1);
    drawInitPosition(p, 7, 7);
}

void Board::drawStone(QPainter &p)
{
    for(int i=0; i<32; i++)
    {
        drawStone(p, i);
    }
}
2.移动棋子

函数如下:

    void mouseReleaseEvent(QMouseEvent *);//mouseReleaseEvent和mousePressEvent一个是鼠标松开一个是鼠标点击,在这里可以随意使用
    void click(QPoint pt);//mouseReleaseEvent调用的函数,表示点棋盘上的这个点
    virtual void click(int id, int row, int col);//上一个click的重载,表示选中一个棋子
    void trySelectStone(int id);//点击棋子时调用
    void tryMoveStone(int killid, int row, int col);
    void moveStone(int moveid, int killid, int row, int col);//移动棋子,同时记录顺序
    void saveStep(int moveid, int killid, int row, int col, QVector& steps);//moveStone时记录step

实现后如下:

void Board::mouseReleaseEvent(QMouseEvent *ev)
{
    if(ev->button() != Qt::LeftButton)
    {
        return;
    }

    click(ev->pos());
}
void Board::click(int id, int row, int col)
{
    if(this->_selectid == -1)
    {
        trySelectStone(id);
    }
    else
    {
        tryMoveStone(id, row, col);
    }
}

void Board::click(QPoint pt)
{
    int row, col;
    bool bClicked = getClickRowCol(pt, row, col);
    if(!bClicked) return;

    int id = getStoneId(row, col);
    click(id, row, col);
}
void Board::trySelectStone(int id)
{
    if(id == -1)
        return;

    if(!canSelect(id)) return;

    _selectid = id;
    update();
}
void Board::moveStone(int moveid, int row, int col)
{
    _s[moveid]._row = row;
    _s[moveid]._col = col;

    _bRedTurn = !_bRedTurn;
}
void Board::saveStep(int moveid, int killid, int row, int col, QVector& 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);
}
3.设置落子规则

绘制完棋盘后就有了象棋的雏形,此时只能进行人人对战且游玩全靠自觉 ,因此下一步操作就是设置基本的落子规则。

GetRowCol()//取出moveid的行和列
relation()//获取关系值row1 - row的绝对值的10倍+col1-col的绝对值
isBottomSide()//判断棋子是否位于棋盘下方(下方上方由楚河汉界划分),方便进行网络在线下棋的开发
getStoneCountAtLine()//判断起子点和落子点是否在同一条直线上(车和炮的路径判断)


//最基本的能不能走判断(颜色是否相同)
bool canMove(int moveid, int killid, int row, int col);
//几种棋子落子规则的制定
bool canMoveChe(int moveid, int killid, int row, int col);
bool canMoveMa(int moveid, int killid, int row, int col);
bool canMovePao(int moveid, int killid, int row, int col);
bool canMoveBing(int moveid, int killid, int row, int col);
bool canMoveJiang(int moveid, int killid, int row, int col);
bool canMoveShi(int moveid, int killid, int row, int col);
bool canMoveXiang(int moveid, int killid, int row, int col);
bool canSelect(int id);// ID能否落子(落子顺序判断)
3.1 一些在规则制定能用到的中间函数

GetRowCol取出moveid的行和列

relation获取关系值row1 - row的绝对值的10倍+col1-col的绝对值

isBottomSide判断棋子是否位于棋盘下方(下方上方由楚河汉界划分),方便进行网络在线下棋的开发

getStoneCountAtLine判断起子点和落子点是否在同一条直线上(车和炮的路径判断)

#define GetRowCol(__row, __col, __id) 
    int __row = _s[__id]._row;
    int __col = _s[__id]._col
int Board::relation(int row1, int col1, int row, int col)
{
    return qAbs(row1-row)*10+qAbs(col1-col);
}
bool Board::isBottomSide(int id)
{
    return _bSide == _s[id]._red;
}
int Board::getStoneCountAtLine(int row1, int col1, int row2, int col2)
{
    int ret = 0;
    if(row1 != row2 && col1 != col2)
        return -1;
    if(row1 == row2 && col1 == col2)
        return -1;

    if(row1 == row2)
    {
        int min = col1 < col2 ? col1 : col2;
        int max = col1 < col2 ? col2 : col1;
        for(int col = min+1; col
            if(getStoneId(row1, col) != -1) ++ret;
        }
    }
    else
    {
        int min = row1 < row2 ? row1 : row2;
        int max = row1 < row2 ? row2 : row1;
        for(int row = min+1; row
            if(getStoneId(row, col1) != -1) ++ret;
        }
    }

    return ret;
}
3.2 各棋子的移动规则 3.2.1 将的移动

将的移动有两个特点:移动的目标位置在九宫内、移动的步长是一格

bool Board::canMoveJiang(int moveid, int killid, int row, int col)
{
    if (killid != -1 && _s[killid]._type == Stone::JIANG)
        return canMoveChe(moveid, killid, row, col);

    GetRowCol(row1, col1, moveid);
    int r = relation(row1, col1, row, col);
    if (r != 1 && r != 10)
        return false;

    if (col < 3 || col > 5)
        return false;
    if (isBottomSide(moveid))
    {
        if (row < 7)
            return false;
    }
    else
    {
        if (row > 2)
            return false;
    }
    return true;
}
3.2.2 士的移动

将和士基本类似,区别是r值, r=11时返回true,九宫内判断不变

bool Board::canMoveShi(int moveid, int, int row, int col)
{
    GetRowCol(row1, col1, moveid);
    int r = relation(row1, col1, row, col);
    if (r != 11)
        return false;

    if (col < 3 || col > 5)
        return false;
    if (isBottomSide(moveid))
    {
        if (row < 7)
            return false;
    }
    else
    {
        if (row > 2)
            return false;
    }
    return true;
}
3.2.3 相的移动

相的移动关键是找相眼

int rEye = (row + row1) / 2;
int cEye = (col + col1) / 2;

之后找相眼上有无棋子,有棋子return false

bool Board::canMoveXiang(int moveid, int, int row, int col)
{
    GetRowCol(row1, col1, moveid);
    int r = relation(row1, col1, row, col);
    if (r != 22)
        return false;

    int rEye = (row + row1) / 2;
    int cEye = (col + col1) / 2;
    if (getStoneId(rEye, cEye) != -1)
        return false;

    if (isBottomSide(moveid))
    {
        if (row < 4)
            return false;
    }
    else
    {
        if (row > 5)
            return false;
    }
    return true;
}
3.2.4 车的移动

关键是获取棋子起子和落子两坐标点之间有没有棋子,若为0返回true

bool Board::canMoveChe(int moveid, int, int row, int col)
{
    GetRowCol(row1, col1, moveid);
    int ret = getStoneCountAtLine(row1, col1, row, col);
    if (ret == 0)
        return true;
    return false;
}
3.2.5 马的移动

蹩马腿:马腿原则是相差=2的向量取中心值

若r=12即列相差=2,马腿的列坐标为列相加/2,行为原来棋子位于的行

若r=21即行相差=2,马腿的行坐标为列相加/2,行为原来棋子位于的列

if (r == 12)
{
    if (getStoneId(row1, (col + col1) / 2) != -1)
        return false;
}
else
{
    if (getStoneId((row + row1) / 2, col1) != -1)
        return false;
}
bool Board::canMoveMa(int moveid, int, int row, int col)
{
    GetRowCol(row1, col1, moveid);
    int r = relation(row1, col1, row, col);
    if (r != 12 && r != 21)
        return false;

    if (r == 12)
    {
        if (getStoneId(row1, (col + col1) / 2) != -1)
            return false;
    }
    else
    {
        if (getStoneId((row + row1) / 2, col1) != -1)
            return false;
    }

    return true;
}
3.2.6 炮的移动

根据killid判断是不是吃子,若不吃子则ret路径与车一致,若吃子则中间必须有一个别的棋子

bool Board::canMovePao(int moveid, int killid, int row, int col)
{
    GetRowCol(row1, col1, moveid);
    int ret = getStoneCountAtLine(row, col, row1, col1);
    if (killid != -1)
    {
        if (ret == 1)
            return true;
    }
    else
    {
        if (ret == 0)
            return true;
    }
    return false;
}
3.2.7 兵的移动

isBottomSide判断过不过河,在BottomSide只能往前走,且目标行不能大于原始行代表不能后退

bool Board::canMoveBing(int moveid, int, int row, int col)
{
    GetRowCol(row1, col1, moveid);
    int r = relation(row1, col1, row, col);
    if (r != 1 && r != 10)
        return false;

    if (isBottomSide(moveid))
    {
        if (row > row1)
            return false;
        if (row1 >= 5 && row == row1)
            return false;
    }
    else
    {
        if (row < row1)
            return false;
        if (row1 <= 4 && row == row1)
            return false;
    }

    return true;
}
4.人机对战

此时我们已经有了一个较为完善的象棋游戏,但我们仍然可以用人机完善。

4.1 一步人机

首先枚举出所有可以走的方案,存下来,遍历所有可以走的方案,然后模拟一步走棋,计算当前局面的分数,注意回溯操作,选择出局面分数最高的走法,走棋

先获取所有情况:

void SingleGame::getAllPossibleMove(QVector &steps)
{
    for(int i=16;i<32; 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);
                }
            }
        }
    }
}

再计算局面分,给每个棋子定义分数,黑棋总分-红棋总分

fakeMove(step);//棋子先走过去
score= calculate();//计算得分
unfakeMove(step);//再走回来
//  enum TYPE{CHE, MA, PAO, BING, JIANG, SHI, XIANG};
static int s[] = {1000, 499, 501, 200, 15000, 100, 100};
for(int i=0;i<16;++i)
{
    if(_s[i]._dead) continue;
    redTotalscore += chessscore[_s[i]._type ] ;
}
for (int i=16;i<32;++i)
{
    if(_s[i]._dead) continue;
    redTotalscore += chessscore[_s[i]._type];
}
4.2 两步人机

最大最小值算法:
最大值最小值在象棋运用的很多,也是最基础的走棋算法:首先根据基础局面计算各个走棋的局面分,假如说电脑要计算三层,那么如图:

电脑想自己得分最高,电脑假设人下棋的时候要推断出最优的走法也就是得分最高的走法,那么人就会选择电脑走法得分最高的走法,然后人从这些走法中挑选出分值最小的走法给电脑,电脑又想从人给的这些走法中挑出值最大的哪一个,所以这就是最小值最大值算法

首先获取人的所有可能落子:

void SingleGame::getAllPossibleMove(QVector &steps)
{
    int min, max;
    if(this->_bRedTurn)
    {
        min = 0, max = 16;
    }
    else
    {
        min = 16, max = 32;
    }

    for(int i=min;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);
                }
            }
        }
    }
}

电脑取自己最小值(假设人不犯错),在最小值的下一步选最大值

int SingleGame::getMinScore(int level, int curMin)
{
    if(level == 0)
        return score();

    QVector 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;
}

(这里插入的递归在n步人机中解释)

4.3 n步人机
int SingleGame::getMaxScore(int level, int curMax)
{
    if(level == 0)
        return score();

    QVector steps;
    getAllPossibleMove(steps);
    int maxInAllMinScore = -300000;

    while(steps.count())
    {
        Step* step = steps.last();
        steps.removeLast();

        fakeMove(step);
        int minScore = getMinScore(level-1, maxInAllMinScore);
        unfakeMove(step);
        delete step;

        if(minScore >= curMax)
        {
            while(steps.count())
            {
                Step* step = steps.last();
                steps.removeLast();
                delete step;
            }
            return minScore;
        }
        if(minScore > maxInAllMinScore)
        {
            maxInAllMinScore = minScore;
        }


    }
    return maxInAllMinScore;
}

getMaxScore中调用了getMinScore,getMinScore中调用了getMaxScore,形成了一个递归

用level代表n重人机算法,此时的getMinScore和getMaxScore以level为参数,以level=0时结束递归,level=0时直接返回score

4.4 剪枝算法


其中可能2出现了一个比可能一小的数字,则其他尚未计算的可能就可以省去

代码实现就是在getMinScore和getMaxScore中再添加一个参数curMaxScore和curMinScore,在函数内传入maxScore和minScore

转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/931438.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号