本项目实现了棋盘和棋子的绘制、象棋的基本规则和基于最大最小值算法的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 剪枝算法
使用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



