中国象棋(人机博弈)

项目效果展示

  

 

 

走法产生

  • 如何产生
  1. 走法产生是指将一个局面的所有可能的走法罗列出来的那一部分程序。也就是用来告诉其他部分下一部分可以网哪里走的模块。在象棋里,象可以走田,你就需要检查与这个象相关联的象位上是否有自己的棋,并且要检查其间的象眼上是否有棋子。而兵则要注意是不可后退并一次只能前进一步。假定现在有一个轮到红方走棋的局面。要列举出红方所有合乎规则的走法。
  2. 首先要扫面棋盘,如果某一个位置上是一个红方棋子,则根据该棋子的类型找出该棋子的所有可走位。例如该棋子是马,检查马的纵横和横纵方向上的距离分别为1和2的所有位置是否有红方棋子,如某位置没有,则还要检查该方向上与马的纵横距离均为1的位置是否有棋子。如没有,该位置就是前述红马的一个合法走步,其他的棋子也要经历各自的判断程序,最后找出所有合法的走步。
  3. 一个象棋程序走法产生的代码如下:
void GenerateAllMove(BYTE position[10][9],int nColor)
{
    int i,j;
    for(i = 0;i < 10;i++)  //循环扫描传入的9*10 BYTE 的棋盘 
    {
        for(j = 0;j < 9; j++)
        {
            if(GetColor(position[i][j]) == nColor)
			{//只处理指定颜色的棋子
				switch(position[i][j])
				{
				case RED_KING:
					GenR_KingMove(position,i,j); //产生红将的走法
				case BLACK_KING:
					GenR_KingMove(position,i,j);  //产生黑将的走法
				case RED_PAWN:
					GenR_PawnMove(position,i,j); //产生红兵的走法
				case BLACK_PAWN:
					GenB_PawnMove(position,i,j);  //产生黑兵的走法
				..............//其他棋子的类似处理
									
				}
			}
        }
    }//end fo switch
}//end of if

//产生黑兵的走法
void GenB_PawnMove(BYTE position[10][9],int i,int j)
{
	int x,y;
	y = i-1;
	x = j;
	//如果是向前方向并且没有本方棋子阻挡,就是合法走法
	if(y > 0&& ! IsSameSide(nChessID,position[x][y]))
		AddMove(j,i,x,y);  //将起点和终点加入可走步队列
	if(i < 5)
	{
		//如果兵以过河
		y=i;
		x=j+1; //向右横走
		if(x < 9 && !IsSameSide(nChessID,position[y][x]))
			AddMove(j,i,x,y);  //将起点和终点加入可走步队列
		x = j-1; //向左横走
		if(x >= 0&& !IsSameSide(nChessID,position[y][x]))
			AddMove(j,i,x,y);  //将起点和终点加入可走步队列
	}
}
void GenR_PawnMove(int position,int i,in tj);  //产生红兵的走法
{
	............
} 

上述c 程序片段基于9 * 10二维数组的棋盘定义,并隐含默认黑方初始在0到4行,红方初始在5到9行。在这个小小的片段里,我们可以看出仅仅一个兵的走法就需要做出若干判断后才可以完成。

        有些程序员为了去除函数调用的开销,将分别判断的小函数去掉,而将所有判断写在一个长长的switch当中来代替,这在一定程度上提高了走法产生的速度。

  • 效率问题
  1.   走法产生通常伴随着搜索进行,并且调用频繁。但是像中国象棋或国际象棋这样有复杂规则棋类,走法的产生往往导致大量繁琐的判断。这在一定程度上形成了程序的性能瓶颈。
  2. 在象棋中,每一种棋子的移动规则各有不同,这样的情形导致了较为复杂的判断。但是我们可以将每种棋子在某一位置上的最大可走步建成一个数据库。在产生走法时直接从中取出数据,进行少量判断就可以算出该棋子的合法走步。
  3. 象的走法是田字格,并且象眼要空白。一般的做法是这样:

1、先检查该棋子周围与该棋子横纵坐标差的绝对值均为2的位置是否落在己方半边棋盘上,如某个点超出己方半边棋盘,将其去除。

2、检查剩下的位置上是否有己方棋子,如有将其去除。

3、检查剩下的位置方向上与该棋子横纵坐标差的绝对值均为1的象眼上是否有棋子,如有将其去除。

4、剩下的位置即是合法走不。

而如果将象的可走位及象眼位置存入数据库,枚举象在某一位置的合法走步就只需如下操作:

1、从库中取出该位置上象的可走位置,检查可走位置上有无己方棋子。

2、从库中取出该位置上象的象眼位置,检查象眼上有无棋子。

显然,此操作的过程更为简单,但在去数据库中的数据时必须有快速的定址方法,否则次操作未必有速度上的优势。实际上,由于每一位置上只能有一个棋子,我们可以轻易地使用棋子位置作为数据库中的定址依据而快速的数据读取。

第一章        绘制棋盘

  • 画10条横线
  • 画9条竖线
  • 画将军的九宫格
#include "Board.h"
#include <QPainter>

Board::Board(QWidget *parent) :
    QWidget(parent)
{
}

void Board::paintEvent(QPaintEvent *)
{
    QPainter painter(this);
    int d = 40;
    // 画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, 1*d), QPoint(6*d, 3*d));
    painter.drawLine(QPoint(6*d, 1*d), QPoint(4*d, 3*d));
    
    painter.drawLine(QPoint(4*d, 8*d), QPoint(6*d, 10*d));
    painter.drawLine(QPoint(6*d, 8*d), QPoint(4*d, 10*d));


}

第二章         初始化棋子

  • 绘制32个棋子
#include "Board.h"
#include <QPainter>
#include <QMouseEvent>

Board::Board(QWidget *parent) :
    QWidget(parent)
{
    for(int i=0; i<32; ++i)
    {
        _s[i].init(i);
    }
    _selectid = -1;
    _bRedTurn = true;
}

void Board::paintEvent(QPaintEvent *)
{
    QPainter painter(this);
    int d = 40;
    _r = d/2;
    // 画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, 1*d), QPoint(6*d, 3*d));
    painter.drawLine(QPoint(6*d, 1*d), QPoint(4*d, 3*d));
    
    painter.drawLine(QPoint(4*d, 8*d), QPoint(6*d, 10*d));
    painter.drawLine(QPoint(6*d, 8*d), QPoint(4*d, 10*d));
    
    // 绘制32个棋子
    for(int i=0;i <32; ++i)
    {
        drawStone(painter, i);
    }

}

QPoint Board::center(int row, int col)
{
    QPoint ret;
    ret.rx() = (col+1)* _r*2;
    ret.ry() = (row+1)* _r*2;
    return ret;
}

QPoint Board::center(int id)
{
    return center(_s[id]._row, _s[id]._col);
}

void Board::drawStone(QPainter& painter, int id)
{
    if(_s[id]._dead)
        return;

    QPoint c = center(id);
    QRect rect = QRect(c.x()-_r, c.y()-_r, _r*2, _r*2);
    
    if(id ==_selectid)
        painter.setBrush(QBrush(Qt::gray));
    else
        painter.setBrush(QBrush(Qt::yellow));
    
    painter.setPen(Qt::black);
    
    painter.drawEllipse(center(id), _r, _r);
    
    if(_s[id]._red)
        painter.setPen(Qt::red);
    painter.setFont(QFont("system", _r, 700));
    
    painter.drawText(rect, _s[id].getText(), QTextOption(Qt::AlignCenter));

}
// 效率不高,应该改进
bool Board::getRowCol(QPoint pt, int &row, int &col)
{
    for(row=0; row<=9; row++)
    {
        for(col=0; col<=8; col++)
        {
            QPoint c = center(row, col);
            int dx = c.x() - pt.x();
            int dy = c.y() - pt.y();
            int dist = dx*dx+dy*dy;
            if(dist < _r*_r)
                return true;
        }
    }
    return false;
}
bool Board::canMove1(int moveid, int row, int col, int killid)
{
    /*
        1.首先目标位置在九宫内
        2.移动的步长是一个格子
    */
    if(_s[moveid]._red)
    {
        if(row > 2)return false;
    }
    else
    {
        if(row < 7)return false;
    }

    if(col < 3) return false;
    if(col > 5) return false;
    
    int dr = _s[moveid]._row - row;
    int dc = _s[moveid]._col - col;
    int d = abs(dr)*10 + abs(dc); // 12, 21   22   10, 1
    if(d == 1 || d == 10)
        return true;
    
    return false;

}
bool Board::canMove2(int moveid, int row, int col, int killid)
{
    if(_s[moveid]._red)
    {
        if(row > 2)return false;
    }
    else
    {
        if(row < 7)return false;
    }

    if(col < 3) return false;
    if(col > 5) return false;
    
    int dr = _s[moveid]._row - row;
    int dc = _s[moveid]._col - col;
    int d = abs(dr)*10 + abs(dc); // 12, 21   22   10, 1
    if(d == 11)
        return true;
    
    return false;

}
bool Board::canMove3(int moveid, int row, int col, int killid)
{
    return true;
}
bool Board::canMove4(int moveid, int row, int col, int killid)
{
    return true;
}
bool Board::canMove5(int moveid, int row, int col, int killid)
{
    return true;
}
bool Board::canMove6(int moveid, int row, int col, int killid)
{
    return true;
}
bool Board::canMove7(int moveid, int row, int col, int killid)
{
    return true;
}

bool Board::canMove(int moveid, int row, int col, int killid)
{
    if(_s[moveid]._red == _s[killid]._red)//moveid和killid颜色相同)
    {
        //换选择
        _selectid = killid;
        update();

        return false;
    }
    
    switch(_s[moveid]._type)
    {
    case Stone::JIANG:
        return canMove1(moveid, row, col, killid);
        break;
    case Stone::SHI:
        return canMove2(moveid, row, col, killid);
        break;
    case Stone::XIANG:
        return canMove3(moveid, row, col, killid);
        break;
    case Stone::CHE:
        return canMove4(moveid, row, col, killid);
        break;
    case Stone::MA:
        return canMove5(moveid, row, col, killid);
        break;
    case Stone::PAO:
        return canMove6(moveid, row, col, killid);
        break;
    case Stone::BING:
        return canMove7(moveid, row, col, killid);
        break;
    }
    
    return true;

}

void Board::mouseReleaseEvent(QMouseEvent *ev)
{
    QPoint pt = ev->pos();
    // 将pt转化成象棋的行列值
    // 判断这个行列值上面有没有棋子
    int row, col;
    bool bRet = getRowCol(pt, row, col);
    if(bRet == false) // 点到棋盘外
        return;

    int i;
    int clickid = -1;
    for(i=0;i<32;++i)
    {
        if(_s[i]._row == row && _s[i]._col == col && _s[i]._dead== false)
        {
            break;
        }
    }
    
    if(i<32)
    {
        clickid = i;
    
    }
    
    if(_selectid == -1)
    {
        if(clickid != -1)
        {
            if(_bRedTurn == _s[clickid]._red)
            {
                _selectid = clickid;
                update();
            }
        }
    }
    else
    {
        if(canMove(_selectid, row, col, clickid))
        {
            /*走棋*/
            _s[_selectid]._row = row;
            _s[_selectid]._col = col;
            if(clickid != -1)
            {
                _s[clickid]._dead = true;
            }
            _selectid = -1;
            _bRedTurn = !_bRedTurn;
            update();
        }
    }

}

第三章        走棋 象棋走棋规则和轮流规则

  • 返回象棋棋盘行列对应的行列值
  • 判断这个行列值上面有没有棋子
  • 点到棋盘外就false
void Board::mouseReleaseEvent(QMouseEvent *ev)
{
    QPoint pt = ev->pos();
    // 将pt转化成象棋的行列值
    // 判断这个行列值上面有没有棋子
    int row, col;
    bool bRet = getRowCol(pt, row, col);
    if(bRet == false) // 点到棋盘外
        return;

    int i;
    int clickid = -1;
    for(i=0;i<32;++i)
    {
        if(_s[i]._row == row && _s[i]._col == col && _s[i]._dead== false)
        {
            break;
        }
    }
    
    if(i<32)
    {
        clickid = i;
    
    }
    
    if(_selectid == -1)
    {
        if(clickid != -1)
        {
            if(_bRedTurn == _s[clickid]._red)
            {
                _selectid = clickid;
                update();
            }
        }
    }
    else
    {
        if(canMove(_selectid, row, col, clickid))
        {
            /*走棋*/
            _s[_selectid]._row = row;
            _s[_selectid]._col = col;
            if(clickid != -1)
            {
                _s[clickid]._dead = true;
            }
            _selectid = -1;
            _bRedTurn = !_bRedTurn;
            update();
        }
    }

}

第四章       

  • 一步象棋人工智能
  • 两步的人工智能
  • 走N步的人工智能
  • 优化-剪枝算法
  • 最小值最大值算法
#include "SingleGame.h"
#include <QTimer>
void SingleGame::click(int id, int row, int col)
{
    if(!this->_bRedTurn)
        return;

    Board::click(id, row, col);
    
    if(!this->_bRedTurn)
    {
        /* 启动0.1秒定时器,在0.1秒后电脑再思考 */
        QTimer::singleShot(100, this, SLOT(computerMove()));
    
    }

}
void SingleGame::computerMove()
{
    Step* step = getBestMove();
    moveStone(step->_moveid, step->_killid,
              step->_rowTo, step->_colTo);
    delete step;
    update();
}

void SingleGame::getAllPossibleMove(QVector<Step *> &steps)
{
    int min=16, max=32;
    if(this->_bRedTurn)
    {
        min = 0, max = 16;
    }
    for(int i=min; i<max; ++i)
    {
        if(_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(killid, i)) continue;
    
                if(canMove(i, killid, row, col))
                {
                    saveStep(i, killid, row, col, steps);
                }
            }
        }
    }

}

void SingleGame::fakeMove(Step* step)
{
    killStone(step->_killid);
    moveStone(step->_moveid, step->_rowTo, step->_colTo);
}

void SingleGame::unfakeMove(Step* step)
{
    reliveStone(step->_killid);
    moveStone(step->_moveid, step->_rowFrom, step->_colFrom);
}

/* 评价局面分 */
int SingleGame::calcScore()
{
    int redTotalScore = 0;
    int blackTotalScore = 0;
    //enum TYPE{CHE, MA, PAO, BING, JIANG, SHI, XIANG};
    static int chessScore[] = {100, 50, 50, 20, 1500, 10, 10};

    // 黑棋分的总数 - 红旗分的总数
    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;
        blackTotalScore += chessScore[_s[i]._type];
    }
    
    return blackTotalScore - redTotalScore;

}

int SingleGame::getMaxScore(int level, int curMinScore)
{
    if(level == 0) return calcScore();

    // 1.看看有那些步骤可以走
    QVector<Step*> steps;
    getAllPossibleMove(steps);   // 是红旗的possiblemove
    
    int maxScore = -100000;
    while(steps.count())
    {
        Step* step = steps.back();
        steps.removeLast();
    
        fakeMove(step);
        int score = getMinScore(level-1, maxScore);
        unfakeMove(step);
        delete step;
    
        if(score >= curMinScore)
        {
            while(steps.count())
            {
                Step* step = steps.back();
                steps.removeLast();
                delete step;
            }
            return score;
        }
        if(score > maxScore)
        {
            maxScore = score;
        }
    
    }
    return maxScore;

}

int SingleGame::getMinScore(int level, int curMaxScore)
{


    if(level == 0) return calcScore();
    
    // 1.看看有那些步骤可以走
    QVector<Step*> steps;
    getAllPossibleMove(steps);   // 是红旗的possiblemove
    
    int minScore = 100000;
    while(steps.count())
    {
        Step* step = steps.back();
        steps.removeLast();
    
        fakeMove(step);
        int score = getMaxScore(level-1, minScore);
        unfakeMove(step);
        delete step;
    
        if(score <= curMaxScore)
        {
            while(steps.count())
            {
                Step* step = steps.back();
                steps.removeLast();
                delete step;
            }
            return score;
        }
    
        if(score < minScore)
        {
            minScore = score;
        }
    
    }
    return minScore;

}

Step* SingleGame::getBestMove()
{
    /*
        2.试着走一下
        3.评估走的结果
        4.取最好的结果作为参考
    */

    // 1.看看有那些步骤可以走
    QVector<Step*> steps;
    getAllPossibleMove(steps);
    
    // 2.试着走一下
    // 3.评估走的结果
    int maxScore = -100000;
    Step* ret = NULL;
    while(steps.count())
    {
        Step* step = steps.back();
        steps.removeLast();
    
        fakeMove(step);
        int score = getMinScore(_level-1, maxScore);
        unfakeMove(step);
    
        if(score > maxScore)
        {
            maxScore = score;
            if(ret) delete ret;
            ret = step;
        }
        else
        {
            delete step;
        }
    }
    
    // 4.取最好的结果作为参考
    return ret;

}

第五章 

  •  网络版本的实现
  • android移植

以下是 NetGame.h:

#ifndef NETGAME_H
#define NETGAME_H

#include "Board.h"
#include <QTcpServer>
#include <QTcpSocket>

/*
    1) 执红方还是黑方,这个信息有服务器发出,客户端接收
    第一个字节固定是1,第二个字节是1,或者0,1表示接收方走红旗,0表示走黑棋
    2)点击信息
    第一个字节固定是2,第二个字节是row,第三个字节是col,第四个字节是点击的棋子id
*/
class NetGame : public Board
{
    Q_OBJECT
public:
    NetGame(bool server);
    ~NetGame();

    QTcpServer* _server;
    QTcpSocket* _socket;
    
    void click(int id, int row, int col);

public slots:
    void slotNewConnection();
    void slotRecv();

};

#endif // NETGAME_H

以下是NetGame.cpp :

#include "NetGame.h"
#include <QDebug>
NetGame::NetGame(bool server)
{
    _server = NULL;
    _socket = NULL;

    if(server)
    {
        _server = new QTcpServer(this);
        _server->listen(QHostAddress::Any, 9999);
    
        connect(_server, SIGNAL(newConnection()),
                this, SLOT(slotNewConnection()));
    }
    else
    {
        _socket = new QTcpSocket(this);
        _socket->connectToHost(QHostAddress("127.0.0.1"), 9999);
    
        connect(_socket, SIGNAL(readyRead()),
                this, SLOT(slotRecv()));
    }

}

NetGame::~NetGame()
{

}

void NetGame::click(int id, int row, int col)
{
    if(_selectid == -1 && id != -1)
    {
        if(_s[id]._red != _bSide)
            return;
    }

    Board::click(id, row, col);
    
    /* 发送给对方 */
    char buf[4];
    buf[0] = 2;
    buf[1] = 9-row;
    buf[2] = 8-col;
    buf[3] = id;
    _socket->write(buf, 4);

}

void NetGame::slotRecv()
{
    QByteArray ba = _socket->readAll();
    char cmd = ba[0];
    if(cmd == 1)
    {
        // 初始化
        char data = ba[1];
        init(data==1);
    }
    else if(cmd==2)
    {
        int row = ba[1];
        int col = ba[2];
        int id = ba[3];
        Board::click(id, row, col);
    }
}

void NetGame::slotNewConnection()
{
    if(_socket) return;

    _socket = _server->nextPendingConnection();
    connect(_socket, SIGNAL(readyRead()),
            this, SLOT(slotRecv()));
    
    /* 给对方发送数据 */
    char buf[2];
    buf[0] = 1;
    buf[1] = 0;
    _socket->write(buf, 2);
    
    init(buf[1]==0);

}

总结

人人对战,还是比较简单,只要能够做到精确计算坐标点,就可以实现。

最重要的是人机对战的方面,需要用到比如最大值最小值的算法,还要用到剪枝+优化的算法,还是比较难。第一次接触算法的人可能就比较简单,所以我想发表一下感言:懂数学的人真的能走遍天下。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值