基于Qt开发的中国象棋 (1) 双人对战

本文介绍了使用Qt5.9开发一个双人对战的中国象棋游戏的过程,包括棋盘绘制、棋子生成、棋子移动、走棋规则、轮流下棋以及胜利判断。通过QPainter进行图形绘制,实现棋盘和棋子的显示,利用mouseReleaseEvent处理棋子移动,并编写逻辑判断棋子的合法移动。同时,实现了简单的胜利判断,当一方的老将被吃时,游戏结束。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

开发工具 Qt5.9

前置知识 , QPainter , paintEvent, mouseReleaseEvent

本次实现的是双人对战的象棋。需要实现的功能如下

  1. 棋盘的显示
  2. 棋子的生成
  3. 棋子的移动
  4. 走棋
  5. 轮流下棋
  6. 判断胜利

棋盘的显示

在这里插入图片描述
象棋的棋盘一般如上图所示, 包括9条竖线,10条横线和两个九宫格。两边的竖线是从上到下,其余竖线要分两次画分别画上面和下面。下面开始画
竖线和横线的代码

// 这里 _d 是一个小格子的长度
_d = std::min(this->width(), this->height())/11;
    int d = _d;
    // 10条横线
    for(int i=1; i<=10; i++)
    {
        painter.drawLine(QPoint(d, i*d), QPoint(9*d, i*d));
    }
    // 9条竖线
    for(int i=1; i<=9; i++)
    {
        if(i==1 || i==9){
            painter.drawLine(QPoint(i*d, d), QPoint(i*d, 10*d));
        }
        else {
            painter.drawLine(QPoint(i*d, d), QPoint(i*d, 5*d));
            painter.drawLine(QPoint(i*d, 6*d), QPoint(i*d, 10*d));
        }
    }

九宫格的代码

    // 九宫格
    painter.drawLine(QPoint(4*d, d), QPoint(6*d, 3*d));
    painter.drawLine(QPoint(6*d, d), QPoint(4*d, 3*d));
    // 下九宫
    painter.drawLine(QPoint(4*d, 10*d), QPoint(6*d, 8*d));
    painter.drawLine(QPoint(6*d, 10*d), QPoint(4*d, 8*d));

这样一个简易的棋盘就画好了
在这里插入图片描述

棋子的生成

棋盘画好了,下面需要画棋子,我们先把棋子类创建出来,先想一想棋子大致有什么属性。棋子需要知道自己在棋盘上面的位置,棋子需要一个类型(车马炮…),棋子有颜色之分,棋子有可能被吃。为了方便操作棋子还需要一个编号,所以创建棋子类如下

class Stone
{
public:
    enum TYPE { CHE, MA, XIANG, SHI, JIANG, PAO, BING};
    Stone();

    void init(int id);
    int row() { return _row; }
    int col() { return _col; }

    QString getText();

public:
    int _col;   // 棋子所在列
    int _row;   // 棋子所在行
    int _id;    // 棋子编号
    TYPE _type;  // 棋子类型
    bool _red;   // 棋子颜色
    bool _dead;  // 棋子是否被吃
};

象棋是有初始局面的,所以每个棋子需要初始化,初始化函数 init 如下。因为棋盘是对称的,象棋初始摆盘的位置数组只要 16就可以了。

// 初始化棋子
void Stone::init(int id)
{
    struct {
        int row, col;
        Stone::TYPE type;
    } pos[16] = {
        {0, 0, Stone::CHE},
        {0, 1, Stone::MA},
        {0, 2, Stone::XIANG},
        {0, 3, Stone::SHI},
        {0, 4, Stone::JIANG},
        {0, 5, Stone::SHI},
        {0, 6, Stone::XIANG},
        {0, 7, Stone::MA},
        {0, 8, Stone::CHE},

        {2, 1, Stone::PAO},
        {2, 7, Stone::PAO},
        {3, 0, Stone::BING},
        {3, 2, Stone::BING},
        {3, 4, Stone::BING},
        {3, 6, Stone::BING},
        {3, 8, Stone::BING}
    };

    _id = id;
    _dead = false;
    _red = id<16;

    if(id < 16)
    {
        _row = pos[id].row;
        _col = pos[id].col;
        _type = pos[id].type;
    }
    else
    {
        _row = 9-pos[id-16].row;
        _col = 8-pos[id-16].col;
        _type = pos[id-16].type;
    }
}

现在每一个棋子都初始化好了,接下来是将棋子画到棋盘上面。QPainter提供了画圆的函数,void QPainter::drawEllipse(const QPoint &center, int rx, int ry) 需要一个圆心。我们每个棋子都有行列的属性,这里的center是像素位置。所以我们需要一个转换函数将 行列坐标转换成 像素点。

QPoint Board::center(int col, int row)
{
    int x = (col+1)*_d;
    int y = (row+1)*_d;

    return QPoint(x, y);
}
画好圆后,我们填充上颜色并写入文本。一个棋子就好了,代码如下
    QPoint c = center(_stone[id].col(), _stone[id].row());
    //qDebug() <<"draw" <<  _stone[id]._row << " : " << _stone[id]._col;
    p->setPen(Qt::black);
    if(true == _stone[id]._red)
        p->setPen(Qt::red);
    p->drawEllipse(c, _d/2, _d/2);

    p->setFont(QFont("system", _d/2, 700));
    // 画字
    QRect rect = QRect(c.x()-_d/2, c.y()-_d/2, _d, _d);
    p->drawText(rect, _stone[id].getText(), QTextOption(Qt::AlignCenter));

在这里插入图片描述

棋子的移动

棋子的移动,其实就是棋子坐标的改变。当我们点击某个棋子后,然后再点击另一个位置。棋子的 col 和 row 值更新一下。然后重新绘一下棋盘,就实现了棋子的移动。 Qt提供了 mouseReleaseEvent() 事件,这个函数在鼠标点击后被调用,提供了 点击的坐标位置。我们重写这个函数,获取每次点击的位置,然后判断是否落在棋盘上,若落在棋盘上,将像素坐标转换成 col 和 row。 然后判断是否点击到某个棋子并记录该棋子的Id(Id 必 >= 0)。因为一次移动要点击两个位置,所以我们需要记住两个位置,一个是当前选中的棋子 _selectId,一个是当前点击的位置clickId。 每次判断之前是否有选中棋子,若有则 更新棋子的row 和 col,如果目标位置也有棋子(clickId > 0)就将 clickId 的那颗棋子的属性_dead 设置为 true。 若之前未选中棋子,则将 clickId 赋值给 selectId。 上述步骤做好以后,更新界面。
思路想好了,开始动手实现。 这里的 CanMove函数是判断可不可以移动。下一节说,先注释掉。 update() 函数会自动调用绘图事件。

// 鼠标点击释放事件
void Board::mouseReleaseEvent(QMouseEvent *e)
{
    QPoint curPos = e->pos();
    int col, row;   // 当前点击位置
    // 判断点是否落在棋盘
    bool ret = getCurPos(curPos, col, row);
    if(!ret) return;    // 落在棋盘外,忽略

    int clickId = -1;   // 本次点击的棋子
    // 获取本次点击的棋子
    clickId = getStoneId(row, col);

    // 如果是第一次点击到棋子
    if(_selectId == -1)
    {
        // 设置 _selectId
        _selectId = clickId;

    }
    else { // 如果之前点击了,实现移动和吃子逻辑

        if(clickId == -1)
        {
            // 可不可以移动
            // if(canMove(_selectId, row, col, clickId))
            {
                _stone[_selectId]._row = row;
                _stone[_selectId]._col = col;
                _redRound = !_redRound; // 切换回合
            }
            _selectId = -1;

        }
        else {
            if(_stone[_selectId]._red == _stone[clickId]._red)
            {
                _selectId = clickId;
            }else {
                // 可不可以吃
                // if(canMove(_selectId, row, col, clickId))
                {
                    _stone[_selectId]._row = row;
                    _stone[_selectId]._col = col;
                    _stone[clickId]._dead = true;
                    _redRound = !_redRound;
                }
                _selectId = -1;
            }
        }
    }
    update();
}

判断是否落在棋盘上 getCurPos 代码

// 当前点击是否有效
bool Board::getCurPos(QPoint pos, int &col, int &row)
{
    for(int i=0; i<10; i++)
    {
        for(int j=0; j<9; j++)
        {
            QPoint c = center(j, i);
            if(pos.x() < c.x()+_d/2 && pos.x() > c.x()-_d/2)
            {
                if(pos.y() < c.y()+_d/2 && pos.y() > c.y() - _d/2){
                    col = j;
                    row = i;
                    return true;
                }
            }
        }
    }
    return false;
}

获取本次点击的棋子Id getStoneId 代码

//判断该行列位置有没没棋子,有就返回ID,没有就返回-1
int Board::getStoneId(int row, int col)
{
    for (int i = 0; i < 32; i++)
        if (row == _stone[i]._row && col == _stone[i]._col && !_stone[i]._dead)
            return i; //有棋子,返回棋子ID
    return -1; //该行列位置没棋子
}

走棋

到目前为止,我们的象棋已经可以移动了,接下来就是象棋的规则,比如马走日,相飞田什么的。这部分代码贴在下面,不解释。有兴趣自己看

// 走棋逻辑
bool Board::canMove(int id, int row, int col, int clickId)
{
    switch (_stone[id]._type) {
    case Stone::JIANG:
        if (col > 2 && col < 6 &&
            ((_stone[id]._red && row < 3 || !_stone[id]._red && row > 6) && (abs(row - _stone[id]._row) + abs(col - _stone[id]._col) == 1)) //0+1=1
            || countAtLine(_stone[4]._row, _stone[4]._col, _stone[20]._row, _stone[20]._col) == 0)
            return true;
        break;
    case Stone::CHE:
        if (countAtLine(row, col, _stone[id]._row, _stone[id]._col) == 0)
            return true;
        break;
    case Stone::MA:
        if ( (abs(row-_stone[id]._row)==1 && abs(col-_stone[id]._col)==2 //左右跳
             && getStoneId(_stone[id]._row,(col+_stone[id]._col)>>1) == -1 //没拐脚
             ) || (abs(row-_stone[id]._row)==2 && abs(col-_stone[id]._col)==1 //上下跳
                   && getStoneId((row+_stone[id]._row)>>1,_stone[id]._col) == -1)) //没拐脚
            return true;
        break;
    case Stone::PAO:
        if (getStoneId(row, col) == -1 && countAtLine(row, col, _stone[id]._row, _stone[id]._col) == 0 //移动
            || getStoneId(row, col) != -1 && countAtLine(row, col, _stone[id]._row, _stone[id]._col) == 1) //吃子
            return true;
        break;
    case Stone::XIANG:
        if ((_stone[id]._red && row < 5 || !_stone[id]._red && row > 4) //没过河
            && abs(row - _stone[id]._row) == 2 && abs(col - _stone[id]._col) == 2 //象步
            && getStoneId((row+_stone[id]._row)>>1, (col+_stone[id]._col)>>1) == -1 ) //没象眼
            return true;
        break;
    case Stone::SHI:
        if (col > 2 && col < 6 && (_stone[id]._red && row < 3 || !_stone[id]._red && row > 6)
            && abs(row - _stone[id]._row) == 1 && abs(col - _stone[id]._col) == 1)
            return true;
        break;
    case Stone::BING:
        if (abs(row - _stone[id]._row) + abs(col - _stone[id]._col) != 1)
            break;
        if (_stone[id]._red) { //红棋在上
            if (row < _stone[id]._row) break;
            if (_stone[id]._row <= 4 && row == _stone[id]._row) break;
        } else { //黑棋在下
            if (row > _stone[id]._row) break;
            if (_stone[id]._row >= 5 && row == _stone[id]._row) break;
        }
        return true;
    }
    return false;
}


//统计直线上棋子个数
int Board::countAtLine(int row1, int col1, int row2, int col2)
{
    int min, max, cnt = 0;
    if (row1 != row2 && col1 != col2)
        return -1;
    if (row1 == row2) {
        if (col1 < col2) {
            min = col1;
            max = col2;
        } else {
            min = col2;
            max = col1;
        }
        for (int col = min+1; col < max; col++)
            if (getStoneId(row1, col) >= 0)
                cnt++;
    } else if (col1 == col2) {
        if (row1 < row2) {
            min = row1;
            max = row2;
        } else {
            min = row2;
            max = row1;
        }
        for (int row = min+1; row < max; row++)
            if (getStoneId(row, col1) >= 0)
                cnt++;
    }
    return cnt;
}

轮流下棋

轮流下棋其实很简单,就是轮到红(黑)方的时候,点击黑(红)棋没有效果。 我们在棋盘类中加一个属性 _redRound 判断当前是否是红方,初始化为红方。这个值的修改在 mouseReleaseEvent这个函数里面修改。 如果当前点击的棋的颜色和当前回合不一样,这次的 鼠标点击事件不处理,任何一方走棋完成后,将_redRound的值取反。 修改后的 mouseReleaseEvent 代码如下
checkWin函数就是判断是否胜利,这个函数还没有写,下一节说。

// 鼠标点击释放事件
void Board::mouseReleaseEvent(QMouseEvent *e)
{
    QPoint curPos = e->pos();
    int col, row;   // 当前点击位置
    // 判断点是否落在棋盘
    bool ret = getCurPos(curPos, col, row);
    if(!ret) return;    // 落在棋盘外,忽略

    int clickId = -1;   // 本次点击的棋子
    // 获取本次点击的棋子
    clickId = getStoneId(row, col);

    // 如果是第一次点击到棋子
    if(_selectId == -1)
    {
        // 不是当前回合
        if(clickId != -1 && _stone[clickId]._red != _redRound) return;
        // 设置 _selectId
        _selectId = clickId;
    }
    else { // 如果之前点击了,实现移动和吃子逻辑
        if(clickId == -1)
        {
            // 可不可以移动
            if(canMove(_selectId, row, col, clickId))
            {
                _stone[_selectId]._row = row;
                _stone[_selectId]._col = col;
                _redRound = !_redRound; // 切换回合
            }
            _selectId = -1;
        }
        else {
            if(_stone[_selectId]._red == _stone[clickId]._red)
            {
                _selectId = clickId;
            }else {
                // 可不可以吃
                if(canMove(_selectId, row, col, clickId))
                {
                    _stone[_selectId]._row = row;
                    _stone[_selectId]._col = col;
                    _stone[clickId]._dead = true;
                    _redRound = !_redRound;
                }
                _selectId = -1;
            }
        }
    }
    update();
    // checkWin();
}

判断胜利

判断胜利就是判断,红方老将和黑方老将有没有被吃,被吃则对方赢。代码如下

void Board::checkWin()
{
    if (_stone[4]._dead) {
        QMessageBox::information(this, "结束啦", "黑胜");
        init();
    } else if (_stone[20]._dead) {
        QMessageBox::information(this, "结束啦", "红胜");
        init();
    }
}

这样一个双人对战的中国象棋就完成了。还比较简陋,没有悔棋和棋谱残局等功能。后面会添加人机对战和网络对战功能,至于悔棋和棋谱,残局这些功能也许会添加吧。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值