基于QT实现的中国象棋
源码在文章末尾,开源自提
1. 课程设计的目的
了解和掌握C++语言的基本语法和面向对象编程思想,同时通过实践来加深对C++语言的理解和应用。具体目标是实现一个简单的中国象棋游戏,包括界面设计、程序逻辑实现和人机博弈功能。通过完成这个课设,可以提高自己的编程能力,能够利用所学的基本知识和技能,解决简单的面向对象程序设计问题,能够熟练地使用C++的特点继承,封装,多态,加深对面向对象编程思想的理解,同时也为学习其他高级语言和开发其他复杂应用程序打下坚实的基础。
2.课程设计的要求
- 界面设计:实现一个简单的游戏界面,用户通过该界面选择游戏模式,开始游戏,悔棋,重新开始,计时等。
- 游戏规则:包括棋盘的布局,棋子的移动,吃棋子的规则,判断是否被将军的规则,以及输赢判断的规则。
- 人工智能算法:实现一个基础的AI算法,让计算机具有一定的智能,算两步以内的走法,在一定程度上协助用户提升游戏体验度。
- 数据结构:使用C++数据结构和面向对象程序设计思想,实现棋盘,棋子等对象,并进行一定程度的封装,以提高代码的复用和可读性。
- 程序性能和稳定:保证程序的性能和稳定性,防止程序出现崩溃和卡顿等问题,尤其是AI部分。
- 代码规范:要求程序代码设计规范,注释清晰,模板化设计,易于理解和维护。
3、项目亮点
主要亮点:
在实现AI算法的计算步数时,使用了剪枝优化算法。
图解:
4.课程设计报告内容
4.1 整体的游戏的流程图
4.2 游戏整体的类层次结构图
4.3 重点代码描述
1)类与类之间的关系
Qobject与QpaintDevice是基类,Voice,Step,Qwidget是其的派生类,其中Qwidget具有多继承,而QDailog,Board是前面派生类的派生类,ChooseMain和HMMode是强两个派生类的派生类。整个UML图中自己创建的类有Voice,Step,Board,ChooseMain,HMMode,其中Board类属于是核心类。这上图中既有多继承也有单继承,同还具有多层继承。这在很大程度上实现了代码的重用,将类的概念按照逻辑层次分层,便于组织管理代码。
Board类作为自己编写的核心类,内部主要定义了从画棋盘一直到判定游戏结束的核心代码,以下就是具体的核心代码的描述:
2)棋盘,棋子的绘制方法
主要定义了
paintEvent(QPaintEvent *),
drawInitPosition(QPainter &p, int row, int col),
drawStone(QPainter& painter, int id),
这三个函数以及他们的部分重载函数,通过QT所提供的类中的
drawLine(),
drawText(),
drawEllipse()
等方法实现绘制棋盘,棋盘上的楚河汉界字样,以及棋子和棋子的名字。棋子的类型则是通过枚举的方式列举出。
3)棋子的走步规则
int Board::movableRange(int row1, int col1, int row, int col){
//该函数是各类棋子行走的限定位置作用的。
return abs(row1-row)*10+abs(col1-col);}
int Board::getStoneCountAtLine(int row1, int col1, int row2, int col2 ) //计算两点之间有几个棋子
bool Board::isBottomSide(int id){
//用来判断是否是底部
return _bSide == _s[id]._red;}
//该函数主移动函数,以下都要先进入该函数判断是否是同色和棋子的类型在具体调用以下具体的走步函数
bool canMove(int moveid, int row, int col, int killid);
//该函数函数是限定将军的走法,将军除了飞将的情况下,只能在宫格内以步长为一的限定行走,其行走规则满足通过
//movableRange计算出来等于1或10的,并且在一定范围之内移动。将军的飞将功能就是通过下面车走步实现的。
bool canMoveJIANG(int moveid, int row, int col, int killid);
//该函数是限定士的走法的,通过movableRange计算出来等于11的位置移动,其范围的限定和将军一样。
bool canMoveSHI(int moveid, int row, int col, int killid);
//该函数是限定相的走步,通过movableRange计算出来等于22的位置移动,并且还需要考虑是否有相脚,相脚的计算是通过
//getStoneId(int row, int col)函数去判断某个位置是否有棋子的方式,去判断是否有相脚。
bool canMoveXIANG(int moveid, int row, int col, int killid);
//该函数是限定相的走步,通过movableRange计算出来等于12或者21的位置移动,并且还需要考虑是否有马脚,马脚的计算也
//是通过getStoneId(int row, int col)函数去判断某个位置是否有棋子的方式,去判断是否有马脚。
bool canMoveMA(int moveid, int row, int col, int killid);
//该函数是限定车的走法的,通过getStoneCountAtLine(int row1, int col1, int row, int col)去计算两点之间是否有棋子,
//没有则是指哪去哪。
bool canMoveCHE(int moveid, int row, int col, int killid);
//该函数是限定炮的走法的,通过getStoneCountAtLine(int row1, int col1, int row, int col)去计算两点之间是否有棋子,
//没有则是指哪去哪。如果有,当且仅当只有一个并且有敌人可以吃的情况下可以走动。
bool canMovePAO(int moveid, int row, int col, int killid);
//该函数是限定兵的走法的,通过movableRange去限定步长和位置,兵在没有过河之前只能向前走,
//过了河之后可以左右,前进,就是不能向后走。
bool canMoveBING(int moveid, int row, int col, int killid);
4)鼠标点击事件——>本身就是一个虚函数,通过在自定义重载的方式重新实现点击功能
void Board :: mouseReleaseEvent(QMouseEvent *ev){
click(ev->pos());
}
5)点击选棋子,移动棋子,吃棋子
//主要是判断鼠标点击的位置是否有棋子
void Board::click(QPoint pt) {
int row, col;
bool bRet = isChecked(pt,row,col);
if(!bRet)
return;
clickStone(getStoneId(row, col), row, col);
}
//主要是先具体的点击走棋功能
void Board::clickStone(int clickid, int row, int col){
if(_selectid == -1)
seleteStone(clickid);
else
testMoveStone(clickid, row, col);
}
//主要实现选棋子
void Board::seleteStone(int id)
{
if(id == -1 )return;
if(!isBeSeleted(id))return;
_selectid = id;
update();
}
//判断选中的是否是回合内的棋子
bool Board::isBeSeleted(int id){
return _bRedTurn == _s[id]._red;
}
//实现棋子的移动在坐标上的变化
void Board::moveStone(int moveid, int row, int col){
_s[moveid]._row = row;
_s[moveid]._col = col;
_bRedTurn = !_bRedTurn;
}
//具体的实现移动逻辑过程
void Board::moveStone(int moveid, int killid, int row, int col){
saveStep(moveid, killid, row, col, _steps);
killStone(killid);
moveStone(moveid, row, col);
whoWin();
if(killid== -1)
_voice.voiceMove(); //移动音效
else
_voice.voiceEat(); //吃子音效
if(isGeneral(_bRedTurn)){
if(isHongMen() != 0){
_voice.voiceGeneral();
if(!isHaveAntherStep()){
if(!_bRedTurn){
winMessageBox("提示", "绝杀!!!红方胜利");
}
winMessageBox("提示", "绝杀!!!黑方胜利");
}
}
else
backOne();
}
if(isGeneral()){
backOne();
}
}
//主要是实现移动棋子
void Board::testMoveStone(int killid, int row, int col){
if(killid != -1 && isSameColor(_selectid, killid)){
seleteStone(killid);
return;
}
if(canMove(_selectid, row, col, killid)){
moveStone(_selectid, killid, row, col);
_selectid = -1;
update();
}
}
//吃棋子功能
void Board::killStone(int id){
if(id==-1) return;
_s[id]._dead = true;
}
//判断选中的棋子与要吃的棋子是否是相同颜色。
bool Board::isSameColor(int moveid, int killid){
if(killid == -1 || moveid == -1)return false;
return isRed(moveid)== isRed(killid);
}
6)悔棋功能,走步的保存
//悔棋的逻辑方法
void Board::back(Step *step){
/*
* 1.复活死去的棋子
* 2.棋子往回移动一步
*/
reliveStone(step->_killid);
moveStone(step->_moveid, step->_rowFrom, step->_colFrom);
}
//悔一步棋
void Board::backOne(){
if(this->_steps.size() == 0) return;
Step* step = this->_steps.last();
_steps.removeLast();
back(step);
update();
delete step;
}
//悔棋
void Board::back(){
backOne();
}
//复活棋子
void Board::reliveStone(int id){
if(id==-1) return;
_s[id]._dead = false;
}
//保存通过Qvector存放移动棋子的位置
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);
}
7)判断被将军,不能产生鸿门宴,拿到所有棋子的走法,通过判断在将军的状态下是否还有起可以移动。
//计算两个将军之间有没有棋子间接证明是否处于鸿门宴
int Board::isHongMen(){
if(_s[4]._dead || _s[20]._dead)
return 0;
return getStoneCountAtLine(_s[4]._row, _s[4]._col, _s[20]._row, _s[20]._col);
}
//判断自己走的棋子是否会让自己将军被将军
bool Board::isGeneral(){
int generalId=20;
if(!_bRedTurn)
generalId=4;
int row= _s[generalId]._row;
int col= _s[generalId]._col;
for(int i=0; i<32; ++i){
if(canMove(i,row,col,generalId) && !_s[i]._dead){
return true;
}
}
return false;
}
//判断移动的棋子是否会让对方将军处于被将军的状态
bool Board:: isGeneral(bool _bRedTurn){
int generalId;
if(_bRedTurn){
generalId=4;
int row= _s[generalId]._row;
int col= _s[generalId]._col;
for(int i=0; i<32; ++i)
if((canMove(i,row,col,generalId) && !_s[i]._dead)){
return true;
}
}else{
generalId=20;
int row= _s[generalId]._row;
int col= _s[generalId]._col;
for(int i=0; i<32; ++i)
if((canMove(i,row,col,generalId) && !_s[i]._dead)){
return true;
}
}
return 0;
}
//拿到红黑棋的所有走法的可能。
void Board::getAllPossibleMove(QVector<Step *> &steps1){
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(isSameColor(i, killid)) continue;
if(canMove(i, row, col, killid)){
Board::saveStep(i, killid, row, col, steps1);
}
}
}
}
}
//判断拿到的所有棋子走法的可能,判断是否在移动后不被将军
bool Board :: isHaveAntherStep(){
QVector<Step *> steps1;
bool isGen = false;
if(_bRedTurn){
getAllPossibleMove(steps1);
for(QVector<Step*> :: iterator it = steps1.begin();it != steps1.end();it++){
Step* step = *it;
fakeMove(step);
if(isGeneral()){
unFakeMove(step);
continue;
}
else{
isGen = true;
unFakeMove(step);
}
}
}else {
getAllPossibleMove(steps1);
for(QVector<Step*> :: iterator it = steps1.begin();it != steps1.end();it++){
Step* step = *it;
fakeMove(step);
if(isGeneral()){
unFakeMove(step);
continue;
}
else{
isGen = true;
unFakeMove(step);
}
}
}
return isGen;
}
//试探性的走一步
void Board::fakeMove(Step *step){
killStone(step->_killid);
moveStone(step->_moveid, step->_rowTo, step->_colTo);
}
//悔试探性走的棋子
void Board::unFakeMove(Step *step){
reliveStone(step->_killid);
moveStone(step->_moveid, step->_rowFrom, step->_colFrom);
}
8)计时,开始游戏,暂停,重新开始,返回菜单信号与槽,以及连接。
//实现时间的更新
void updateTime();
//连接,四个参数分别是发送者,信号,接收者,槽
connect(_timer,SIGNAL(timeout()),this,SLOT(updateTime()));
ui->setupUi(this);
//实现点击开始按键
void on_pushButton_start_clicked();
//实现点击重置按键
void on_pushButton_reset_clicked();
//实现重新开始按键
void on_pushButton_restart_clicked();
//实现悔棋按键
void on_pushButton_back_clicked();
//实现返回菜单按键
void on_pushButton_toMenu_clicked();
9)实现简单AI的HMMode类,由于继承内部的函数只有以下几个
//重载父类中的该方法从而实现选棋子,走棋子的功能
void HMMode::clickStone(int checkedID, int row, int col)
//计算最好的局面分
int HMMode::calcScore()
//拿到最好的移动位置
Step* HMMode::getBestMove()
//拿到对机器最小损失的位置
int HMMode::getLossMinScore()
4.4 实验结果
1)选择游戏模式
图3-3
2)进入双人模式
图3-4
3)开始游戏
图3-5
3)判断输赢
图3-6
4)通过点击返回主菜单
图3-7
5)进入单机模式
图3-8
6)开始游戏
图3-9
4.5设计总结
组员姓名:XXXX
通过这次课程设计,更加深入的了解了 qt的使用,以及掌握了C++语言的基本语法和面向对象编程思想,同时通过实践来加深对C++语言的理解和应用。通过这次的课设一定程度上提高了自己的编程能力,能够利用所学的基本知识和技能,解决简单的面向对象程序设计问题,能够熟练地使用C++的特点继承,封装,多态,加深对面向对象编程思想的理解,同时也为学习其他高级语言和开发其他复杂应用程序打下坚实的基础。
在此过程中也遇到不少的挫折,问题是宛如翻山越岭一般,一波接着一波的来,主要遇到比较棘手花的时间比较长的是以下
问题:
1)在设计马脚的时候,由于简单认为相脚一样设计,忽略了相脚不论怎么样都是中心点,为马脚不是中心点,不能将两种情况放在同一分支里讨论。
解决方法:两种情况需要单独讨论。
2)在设计将军的状态不能移动不能解除被将军状态的棋子时,由于刚开始只是考虑了移动棋子可以造成对方被将军的情形,却忘记了移动棋子也有可能给自己造成被将军的状态。从而导致被将军是一直可以移动其他的不能解除被将军的棋子。
解决方法:在原来的判断将军函数基础上重载一个不带参数的,判断移动棋子对自己将军是否造成被将军的状态,从而通过这个函数去限制走棋的选择。
3)在设计绝杀判断输赢的时候,一直以为只要移动一步棋子不能解除将军就是没路可走,导致一直判断出错,却忘记了可以移动的棋子可能不止一个,没有很好的应用当时先AI建议算法是写的拿到所有棋子的可能的走法的函数,去通过迭代遍历判断是否有存在可以移动的棋子。
解决方法:在原先的写的拿到所有棋子可能的走法的函数基础上,在写一个通过迭代方式遍历找是否有可以移动的棋子。
4)在AI简易算步骤的方法时,由于粗心将红黑棋子的ID对调了,误认为0-15是黑棋,导致一开始的AI下棋是移动的都是自己的棋子,而黑动不了。
解决方法:将其ID判断对调以下即可。
以上是课设上花费时间比较长的部分,也是自己抓耳挠腮最频繁的时候,通过这次的课设,暴露出自己思维不够发散的缺点,在一个牛角尖上死命地转,却忘了退后一步去想问题,在增长经验的同时,也让我更加有耐性,更加享受调试过程,调试成功后的快感,在以后的学习路上又添加了一笔不错的风景线。
参考文献
[1]马石安.面向对象程序设计教程(C++语言描述)(第3版).北京:清华大学出版社, 2018.
[2]胡然.C++ QT程序设计工程实训教程.北京:电子工业出版社, 2018.6
[3]王小春.PC游戏编程:人机博弈.重庆:重庆大学出版社, 2018.6