注:项目设计时时间有限,并且独立完成,很多时间花在算法的编写上,界面设计和模块整合有待改正和优化。如果对您有帮助,客官不要吝惜star呀!
游戏界面展示:

一、前言
这是原项目的新整合。两年前,我为c++实践课程做了这个项目,但游戏本身有太多“不必要”的部分,比如登录或注册模块。这些对于项目的核心贡献来说是多余的,但有助于显示我的工作量让我拿个好成绩。
因此,现在我只保留了项目的核心部分:阿尔法-贝塔算法和Qt绘制的棋盘。
打开方式:使用Qt Creator 6.3打开项目,然后构建并运行代码。
项目内容:利用C++的知识和QT以及一些自行拓展的新知识,实现AI模式对战的五子棋游戏,尝试设计启发式算法,实现AI模式的五子棋游戏。
二、关键设计
2.1 游戏界面设计
首先我们设计一个类gamewindow,继承至qt的QMainWindow,数据成员为Chess类的指针。对于画出已下的棋子,Chess类的数据成员是个二维数组用来标记下棋位置及对应玩家,所以可以通过指针调用成员函数访问Chess类的二维数组获得下棋位置,之后用Qt的画笔和画刷实现绘画棋子,棋盘网格和棋盘的五个黑点等。
对于当前选择框的移动,这里采用鼠标移动事件,捕获捕获鼠标移动的位置,并在就近的位置画出红色的选择框。由于Qt的鼠标移动事件节省资源,实际的移动事件只有在按下鼠标才会捕捉,但为了设计界面红框随鼠标移动,需要setMouseTracking(true)。
对于选择位置后的下棋需要鼠标点击才可以下棋,这里的捕捉鼠标并需要按下鼠标才选择,所以并不需要setMouseTracking(true)。
对于最后执棋方最后落子位置采用全局的lastx和lasty来标记,采用Qt绘画时间在对应棋子中心画出红色十字。
学习参考:(1)https://blog.csdn.net/weixin_39788534/article/details/79496905
(2)https://blog.csdn.net/qq_19727693/article/details/105694194
2.2 AI模式下minmax及alphabet剪枝搜索算法设计
设计AI模式采用的是极大极小和剪枝算法,由于棋盘为15行15列,运算量巨大,这里采用了剪枝算法减少计算量。在待搜索队列地设计上,首先排除周围一颗棋子都没有的位置,再对敌方最后落子位置周围的位置插入搜素队列头部(他们周围是最可能我方落子地位置),来尽可能地剪枝。
此外,对于棋局局势的评估采用了启发式的搜索算法,如对于“活三”,“活四”等给出不同的评价,并根据我方得分减去对手得分来评估该落子位置。而且可以根据调整敌方得分及我方得分地比例,将AI设置为进攻型或者防守型。
评估模型如下(1代表棋子位置,0为空):
核心算法设计:minmax及剪枝搜索:
//负值极大算法搜索 alpha + beta剪枝, 合并极大节点和极小节点两种情况, 减少代码量
//在一盘棋局中, 若到棋手A走棋, alpha 相当于棋手A得到的最好的值, 对于棋手A的值, 从对手的角度看就要取负值.
int Chess::alpha_beta(int player, int h, int alpha, int beta)
//h搜索深度,player=1表示电脑,player=0表示人类
{
if (vec1.begin() != vec1.end() && vec2.begin() != vec2.end())//不空
{
vector<Position>::iterator pter1 = vec1.end()-1;
vector<Position>::iterator pter2 = vec2.end()-1; //判断最新添加后是否赢棋
if (h == 1 || judge_victory(*pter1,chessFlag1) || judge_victory(*pter2, chessFlag2))
{
//若到达深度 或是出现胜负
return evaluate(player);
}
}
if (vec3.begin() == vec3.end()) //尚未下棋。居中坐
{
vector<Position>::iterator vter = vec_blank.begin();
vec_blank.erase(vter + 7 * 15 + 7); //先删除居中棋子
(*p).row = 7; //第八行第八列,执先
(*p).col = 7;
return 0;
}
// vec_blank 为可以落子的集合
order(); //按搜索顺序排序 提高剪枝效率
//遍历每一个候选步
vector<Position>::iterator vter = vec_blank.begin();
while (vter != vec_blank.end())
{
int tempv = vter - vec_blank.begin();
//如果要评估的位置没有相邻的子, 则不去评估 减少计算
if (!has_neightnor(vter))
{
vter++;
continue;
}
Position *pos = new Position();
pos->col = (*vter).col;
pos->row = (*vter).row;
//判断是否为ai走棋
if (player){
vec1.push_back(*pos);
vec_blank.erase(vter);
}
else{
vec2.push_back(*pos);
vec_blank.erase(vter); //从空白记录删除
}
vec3.push_back(*pos);
//返回到上一层节点时,会给出分数的相反数(因为返回的值相当于是在对手的选择下对手对当前棋盘的评估分数
//而作为他的对立方, 要把分数取反
//alpha可以视为当前情况下,当前棋手可以得到的最好值,当前棋手得到最好值 == 对手不愿意接受的最差值
int value = -alpha_beta(!player, h - 1, -beta, -alpha);
vector<Position>::iterator vter1 = vec1.end() - 1;
vector<Position>::iterator vter2 = vec2.end() - 1;
vector<Position>::iterator vter3 = vec3.end() - 1;
vter = vec_blank.insert(vec_blank.begin() + tempv, *pos);
//从空白记录原先位置插入
//将刚才走棋移除
if (player) { //是电脑
vec1.erase(vter1);
}
else{ //是玩家
vec2.erase(vter2);
}
vec3.erase(vter3);
//vec3.erase(vter); 这么写不行,超出范围
delete pos;
if (value > alpha)
{
//当depth == DEPTH时, 由于在循环内不断迭代, 总会在考虑后三步棋的情况下逐渐找到最好的走子方式;
if (h == Depth) //找到路径了
{
p->row = (*vter).row;
p->col = (*vter).col;
}
//alpha + beta剪枝点
//当当前value > beta 时相当于 对手的 value < -alpha, 对手肯定不会考虑这个选择
if (value >= beta)
{
//因为当前 depth中仍需要遍历候选点, return 后直接返回上层, 上层将返回值取负值
//因为当调用该函数时alpah > - beta, 必定返回上层迭代时, value < alpha, 达到剪枝
return beta;
}
alpha = value; //更新alpha值
}
vter++;
}
return alpha;
}
备注:项目仓库地址