(1)图形化界面。
(2)随机选取先手后手。
(3)可以人-计算机或计算机-计算机
Max-Min算法:
采用Max-Min算法进行对抗搜索,Max和Min双方均要对方失败,在井字棋中双方交替走子,根据评估函数的值更新Max和Min,在更新的过程中进行α-β剪枝,加快搜索速度。
评估函数e(s)代表Max状态的启发值,是对Max有利状态的估计。
α-β剪枝算法:
α-β剪枝中,α是Max至今为止的路径上所有选择点的最大值,β是Min中的最小值。它的基本思想是:边生成博弈树边计算评估各节点的倒推值,并且根据评估出的倒推值范围,及时停止扩展那些已无必要再扩展的子节点,即相当于剪去了博弈树上的一些分枝,从而节约了机器开销,提高了搜索效率。具体算法如下:
(1)使用Max-Min算法和α-β剪枝算法实现井字棋游戏。
(2)图形化界面,可以直观显示下棋过程。
(3)可以选取人先手或者计算机先手。
(4)可以人与计算机下棋或者计算机与计算机互博。
(1)计算机与计算机模块:调用计算机下棋模块,判断赢家模块和判断平局模块完成功能。
(2)人与计算机模块:调用人类下棋,计算机下棋,赢家判断和平局判断模块完成功能。
(3)人类下棋模块:获取当前鼠标点击的按钮,在对应按钮上显示棋子图片,根据按钮位置修改存储的棋盘信息。
(4)计算机下棋模块:调用Max-Min的α-β剪枝算法,获取搜索到的最有利于计算机的下棋点在相应位置上显示棋子图片。
(5)评估值计算模块:根据上面基本思想介绍的算法计算评估值,即所有空余位置填满计算机棋子后连成一条线的个数减去填满人类棋子后连成一条线的个数。如果电脑赢则返回极大值,人类赢则返回极小值。
(6)Max-Min的α-β剪枝模块:根据计算的评估值进行极大极小算法,不停的交换当前下棋的人进行搜索,在搜索的过程中如果遇到α>=β的情况就进行剪枝。
(7)游戏结束模块:当有一方赢得棋局时,对图形化界面进行重置,存储的棋盘信息也清零。
(8)先手判断模块:当点击计算机先手按钮时,调用计算机下棋模块先进行下棋。
(8)平局判断模块:当有九个格子都被走过且没有赢家时,弹出平局的提示信息对话框。
(9)判断赢家模块:分别对于水平,垂直,对角线进行判断是否有一方连成了一条线。
当用户点击棋盘区域进行下棋时,会触发按钮的消息响应函数,消息响应函数会调用人类与计算机下棋的模块,在人类下棋的部分,直接获取点击的按钮然后更改图片为棋子图片就行。而对于计算机下棋就需要调用Max-Min剪枝算法获取搜索到的下棋点进行下棋。下棋的过程中判断是否有输赢和平局的出现,如果出现则游戏结束重新开始。
当用户点击计算机-计算机下棋单选按钮时就会触发消息响应函数修改游戏类型为计算机-计算机类型代表的值,然后点击Next Step按钮界面会一步一步显示计算机之间下棋的过程。在下棋的过程中计算机的下棋点的计算都是基于Max-Min的α-β剪枝算法计算得到的点。
根据上述基本思想的介绍可以得到下面计算的流程图:
mainwindow.h
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
#include
#include
#include
#include
#include
#include
#include
using namespace std;
const int N = 1e3 + 5;
namespace Ui {
class MainWindow;
}
class MainWindow : public QMainWindow
{
Q_OBJECT
public:
explicit MainWindow(QWidget *parent = 0);
~MainWindow();
QList<QPushButton *> gridButton; //九个网格按钮
QRadioButton *humanVScom, *comVScom, *human_first, *com_first;
QRadioButton *level1, *level2, *level3;
QLabel *first_player, *level, *boundup, *boundunder;
QPushButton *next_step;
private:
Ui::MainWindow *ui;
void HumanAndComputer(QPushButton *button); //人与计算机
void ComputerAndComputer(); //计算机与计算机
int HumanChess(QPushButton *button); //人类下子
int ComputerChess(); //计算机下棋
int GetWinPeople(); //获取当前赢的人
void ComFirst(); //电脑是否先手
bool Tie(); //是否平局
void Print(); //打印棋盘
int MinMaxSolution(int depth, int alpha, int beta); //MinMax算法
int CalEvalute();
int chess[3][3]; //棋盘
int select_type = 0; //选择游戏类型:人-计算机为0,计算机-计算机为1
int select_level; // 选择难度等级
pair<int, int> best_point; //最优点
int cur_depth = 9; //当前搜索深度
int cur_player = 1; //当前玩家
public slots:
void ButtonClick(); //下棋按钮信号
void GameOver(); //游戏结束信号
void RadioButtonClick();//选择游戏类型
void NextbutClick(); //获得电脑下一步
};
#endif // MAINWINDOW_H
main.cpp
#include "mainwindow.h"
#include
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
MainWindow w;
w.show();
return a.exec();
}
mainwindow.cpp
#include "mainwindow.h"
#include "ui_mainwindow.h"
MainWindow::MainWindow(QWidget *parent) :
QMainWindow(parent),
ui(new Ui::MainWindow)
{
ui->setupUi(this);
memset(chess, 0, sizeof(chess)); //棋盘初始化
select_type = 0;
setWindowTitle("井字棋"); //设置窗体名
setFixedSize(1000, 610); //设置窗体大小
setWindowIcon(QIcon(":/logo.png")); //设置窗体图标
//设置窗体背景为白色
QPalette palette(this->palette());
palette.setColor(QPalette::Background, Qt::white);
this->setPalette(palette);
//上下分割线
boundup = new QLabel(this);
boundup->setStyleSheet("border-image:url(:/bound.png);");
boundup->setGeometry(30, 0, 540, 30);
boundunder = new QLabel(this);
boundunder->setStyleSheet("border-image:url(:/bound.png);");
boundunder->setGeometry(30, 570, 540, 30);
//设置九个网格图片
int x = 0, y = 0, w = 180, h = 180;
for(int i = 0; i < 3; ++ i){
for(int j = 0; j < 3; ++ j){
QPushButton *button = new QPushButton(this);
gridButton.push_back(button);
x = j * 180 + 30, y = i * 180 + 30;
button->setGeometry(x, y, w, h); //设置按钮位置和大小
button->setStyleSheet("border-image:url(:/grid.png);"); //设置按钮图片
connect(button, SIGNAL(clicked(bool)), this, SLOT(ButtonClick()));//为每个按钮添加响应函数
}
}
QPushButton *restart = new QPushButton(this); //重新开始按钮
restart->setGeometry(650, 15, 260, 65);
restart->setStyleSheet({"border-image:url(:/restart.png);"});
connect(restart, SIGNAL(clicked(bool)), this, SLOT(GameOver()));
next_step = new QPushButton(this); //电脑下一步
next_step->setGeometry(650, 75, 260, 65);
next_step->setStyleSheet({"border-image:url(:/nextstep.png);"});
connect(next_step, SIGNAL(clicked(bool)), this, SLOT(NextbutClick()));
//游戏类型
QLabel *game_type = new QLabel(this); //游戏类型标签
game_type->setGeometry(next_step->pos().x(), next_step->pos().y() + 75, 250, 65);
game_type->setStyleSheet({"border-image:url(:/selecttype.png);"});
//人与计算机
humanVScom = new QRadioButton(this);
humanVScom->setStyleSheet({"border-image:url(:/humvscom.png);"});
humanVScom->setGeometry(game_type->pos().x() - 10, game_type->pos().y() + 60, 145, 65);
humanVScom->setChecked(true); //默认人机
select_type = 0;
connect(humanVScom, SIGNAL(clicked(bool)), this, SLOT(RadioButtonClick()));
//计算机与计算机
comVScom = new QRadioButton(this);
comVScom->setStyleSheet({"border-image:url(:/comvscom.png);"});
comVScom->setGeometry(humanVScom->pos().x() + 145, humanVScom->pos().y(), 145, 65);
connect(comVScom, SIGNAL(clicked(bool)), this, SLOT(RadioButtonClick()));
QButtonGroup * VSgroup = new QButtonGroup(this);
VSgroup->addButton(humanVScom);
VSgroup->addButton(comVScom);
//选择先手
first_player = new QLabel(this); //选择先手标签
first_player->setGeometry(game_type->pos().x(), game_type->pos().y() + 130, 250, 65);
first_player->setStyleSheet({"border-image:url(:/selectfirst.png);"});
//人类先手
human_first = new QRadioButton(this);
human_first->setStyleSheet({"border-image:url(:/humfirst.png);"});
human_first->setGeometry(first_player->pos().x() - 10, first_player->pos().y() + 60, 145, 55);
connect(human_first, SIGNAL(clicked(bool)), this,SLOT(RadioButtonClick()));
human_first->setChecked(true); //默认人先手
//计算机先手
com_first = new QRadioButton(this);
com_first->setStyleSheet({"border-image:url(:/comfirst.png);"});
com_first->setGeometry(human_first->pos().x() + 145, human_first->pos().y(), 145, 55);
connect(com_first, SIGNAL(clicked(bool)), this,SLOT(RadioButtonClick()));
com_first->setChecked(false);
QButtonGroup* first_group = new QButtonGroup(this);
first_group->addButton(human_first);
first_group->addButton(com_first);
//困难级别
level = new QLabel(this);
level->setGeometry(first_player->pos().x(), first_player->pos().y() + 130, 300, 65);
level->setStyleSheet({"border-image:url(:/selectlevel.png);"});
//简单
level1 = new QRadioButton(this);
level1->setGeometry(level->pos().x() - 30, level->pos().y() + 60, 120, 55);
level1->setStyleSheet({"border-image:url(:/easy.png);"});
connect(level1, SIGNAL(clicked(bool)), this, SLOT(RadioButtonClick()));
//普通
level2 = new QRadioButton(this);
level2->setGeometry(level1->pos().x() + 120, level1->pos().y(), 120, 55);
level2->setStyleSheet({"border-image:url(:/medium.png);"});
connect(level2, SIGNAL(clicked(bool)), this, SLOT(RadioButtonClick()));
//困难
level3 = new QRadioButton(this);
level3->setGeometry(level2->pos().x() + 120, level2->pos().y(), 120, 55);
level3->setStyleSheet({"border-image:url(:/hard.png);"});
connect(level3, SIGNAL(clicked(bool)), this, SLOT(RadioButtonClick()));
level3->setChecked(true); // 默认困难
select_level = 0;
QButtonGroup * level_group = new QButtonGroup(this);
level_group->addButton(level1);
level_group->addButton(level2);
level_group->addButton(level3);
}
//棋盘响应函数
void MainWindow::ButtonClick()
{
QPushButton *button = qobject_cast<QPushButton*>(sender());
//qDebug()<<"类型:"<
HumanAndComputer(button);
}
void MainWindow::NextbutClick()
{
ComputerAndComputer();
}
//计算机与计算机
void MainWindow::ComputerAndComputer()
{
qDebug()<<"计算机与计算机被调用"<<endl;
cur_player = 1;
if(ComputerChess() == 1) --cur_depth;
if(cur_player == GetWinPeople()){
QMessageBox::information(this, "井字棋", "Computer One Win");
GameOver();
return;
}
else if(Tie()) return;
else cur_player = -1;
qDebug()<<"计算机与计算机one"<<best_point.first<<","<<best_point.second<<endl;
if(ComputerChess() == 1) --cur_depth;
if(cur_player == GetWinPeople()){
QMessageBox::information(this, "井字棋", "Computer Two Win");
GameOver();
return;
}
else if(Tie()) return;
else cur_player = 1;
qDebug()<<"计算机与计算机Two"<<best_point.first<<","<<best_point.second<<endl;
}
void MainWindow::RadioButtonClick()
{
QRadioButton *button = qobject_cast<QRadioButton*>(sender());
if(button == humanVScom) select_type = 0;
if(button == comVScom) select_type = 1;
else if(button == human_first) select_type = 0;
else if(button == com_first) select_type = 0;
else if(button == level1) select_level = 3;
else if(button == level2) select_level = 1;
else if(button == level3) select_level = 0;
qDebug()<<"类型2:"<<select_type<<endl;
GameOver();
}
//人与计算机
void MainWindow::HumanAndComputer(QPushButton *button)
{
//人类下棋
if(HumanChess(button) == 1) --cur_depth; //搜索深度减一
if(cur_player == GetWinPeople()){ //当前玩家赢了
QMessageBox::information(this, "井字棋", "Congratulations. You Win!");
GameOver();
return;
}
else if(Tie()) return; //平局
else cur_player = -1; //更改当前玩家为计算机
//计算机下棋
if(ComputerChess() == 1) --cur_depth;
if(cur_player == GetWinPeople()){
QMessageBox::information(this, "井字棋", "egrettably . Computer Win");
GameOver();
return;
}
else if(Tie()) return;
else cur_player = 1; //更改当前玩家为人类
}
//人类下棋
int MainWindow::HumanChess(QPushButton *button)
{
if(button == false) return -1;//下棋失败
int i = button->pos().x() / 180;
int j = (button->pos().y() - 30) / 180;
chess[j][i] = 1; //人类在点(i, j)下了一个子
button->setStyleSheet("border-image: url(:/circle.png);");
button->setEnabled(false); //只能下一次
return 1; //下棋成功
}
//电脑下棋
int MainWindow::ComputerChess()
{
MinMaxSolution(cur_depth, -N, N); //根据算法获得最优解
if(comVScom->isChecked()) chess[best_point.first][best_point.second] = cur_player;
else chess[best_point.first][best_point.second] = -1;
int point = (best_point.first * 3) + best_point.second;
for(int i = 0; i < 9; ++ i){//显示电脑下棋点
if(i == point){
if(cur_player == -1)
gridButton[i]->setStyleSheet("border-image: url(:/fork.png);");
else gridButton[i]->setStyleSheet("border-image: url(:/circle.png);");
gridButton[i]->setEnabled(false);
}
}
return 1; //下棋成功
}
//计算评估值
int MainWindow::CalEvalute()
{
int win_people = GetWinPeople();
//胜负已分
if(win_people == -1 ) return N; // 计算机胜返回最大值
if(win_people == 1 ) return -N; // 人类胜返回最小值
//胜负未分
int value = 0;
int new_chess[3][3]; //临时棋盘
//计算机
for(int i = 0; i < 3; ++ i){
for(int j = 0; j < 3; ++ j){
if(chess[i][j] == 0) new_chess[i][j] = -1;
else new_chess[i][j] = chess[i][j];
}
}
for(int i = 0; i < 3; ++ i) //对于水平方向
value = value - (new_chess[i][0] + new_chess[i][1] + new_chess[i][2])/3;
for(int i = 0; i < 3; ++ i) //对于垂直方向
value = value - (new_chess[0][i] + new_chess[1][i] + new_chess[2][i])/3;
value = value - (new_chess[0][0] + new_chess[1][1] + new_chess[2][2])/3;//主对角线
value = value - (new_chess[2][0] + new_chess[1][1] + new_chess[0][2])/3;//副对角线
//人类
for(int i = 0; i < 3; ++ i){
for(int j = 0; j < 3; ++ j){
if(chess[i][j] == 0) new_chess[i][j] = 1;
else new_chess[i][j] = chess[i][j];
}
}
for(int i = 0; i < 3; ++ i) //对于水平方向
value = value - (new_chess[i][0] + new_chess[i][1] + new_chess[i][2])/3;
for(int i = 0; i < 3; ++ i) //对于垂直方向
value = value - (new_chess[0][i] + new_chess[1][i] + new_chess[2][i])/3;
value = value - (new_chess[0][0] + new_chess[1][1] + new_chess[2][2])/3;//主对角线
value = value - (new_chess[2][0] + new_chess[1][1] + new_chess[0][2])/3;//副对角线
return value;
}
int MainWindow::MinMaxSolution(int depth, int alpha, int beta)
{
int cur_value = 0, best_value = 0, cnt = 0;
pair<int, int> location[10];
int win_people = GetWinPeople();
if(win_people == -1 || win_people == 1 ||depth == 0) {
// qDebug() << CalEvalute()<
return CalEvalute();
}
//深度未耗尽,给定初始值
if(cur_player == -1) best_value = -N;
else if(cur_player == 1) best_value = N;
//获取棋盘上剩余的位置
for(int i = 0; i < 3; ++ i){
for(int j = 0; j < 3; ++ j){
if(chess[i][j] == 0){
//qDebug()<
location[cnt].first = i;
location[cnt++].second = j;
}
}
}
cnt -= select_level; //困难级别
// qDebug()<
if(cnt < 1) {
best_point = location[0];
//qDebug()<<"0: "<
return best_value;
}
for(int i = 0; i < cnt; ++ i){
pair<int, int> cur_pos = location[i];
int x = cur_pos.first, y = cur_pos.second;
chess[x][y] = cur_player; //当前点下一个棋
cur_player = (cur_player == 1) ? -1 : 1;
cur_value = MinMaxSolution(depth - 1, alpha, beta); //向下递归
chess[x][y] = 0; //取消下棋
cur_player = (cur_player == 1) ? -1 : 1;
if(cur_player == -1){ // 当前玩家是Max节点
if(cur_value > best_value){
best_value = cur_value;
if(depth == cur_depth) best_point = cur_pos;
alpha = best_value;
}
if(beta <= alpha) return beta; // Max中上界小于下界 返回较小值
}
else if(cur_player == 1){ // 当前是Min节点
if(cur_value < best_value){
best_value = cur_value;
if(depth == cur_depth) best_point = cur_pos;
beta = best_value;
}
if(beta <= alpha) return alpha; // Min中上界小于下界 返回较大值
}
}
return best_value;
}
void MainWindow::GameOver() //游戏结束
{
for(int i = 0; i < 9; ++ i){
gridButton[i]->setStyleSheet("border-image: url(:/grid.png);");
gridButton[i]->setEnabled(true);
}
//棋盘重置
for(int i = 0; i < 3; ++ i){
for(int j = 0; j < 3; ++ j){
chess[i][j] = 0;
}
}
cur_depth = 9;
ComFirst();
}
//电脑先手
void MainWindow::ComFirst()
{
if(humanVScom->isChecked()){
if(com_first->isChecked()){//人机对战模式且电脑先手
ComputerChess();
--cur_depth;
cur_player = 1;
}
else cur_player = -1;
}
}
bool MainWindow::Tie() //平局
{
int cnt = 0; //有多少个格子走过
for(int i = 0; i < 3; ++ i){
for(int j = 0; j < 3; ++ j){
if(chess[i][j] != 0){
++cnt;
}
}
}
if(cnt == 9){ //9个格子都走过
QMessageBox::information(this, tr("井字棋"), tr("Draw!"), QMessageBox::Ok);
GameOver(); //游戏结束
return true;
}
return false;
}
int MainWindow::GetWinPeople() //判断输赢
{
int sum = 0;
for(int i = 0; i < 3; ++ i){//水平方向
sum = chess[i][0] +chess[i][1] + chess[i][2];
if( sum == 3) return 1;
else if(sum == -3) return -1;
}
for(int i = 0; i < 3; ++ i){//垂直方向
sum = chess[0][i] + chess[1][i] + chess[2][i];
if(sum == 3) return 1;
else if(sum == -3) return -1;
}
int psum = chess[0][0] + chess[1][1] + chess[2][2];//正对角线
int csum = chess[2][0] + chess[1][1] + chess[0][2];//副对角线
if(psum == 3 || csum == 3) return 1;
else if(psum == -3 || csum == -3) return -1;
return 0; //未决胜负
}
MainWindow::~MainWindow()
{
delete ui;
}
在此模式下,人类与计算机基本上总是平手,计算机几乎不会失误,当平手时系统弹出Draw信息提示对话框提示用户。
在此模式下,赢得游戏变得非常容易,几乎每次必赢,平局的局面很少
当选择计算机与计算机下棋时,往往总是平局,并且它们每次下棋的落子点都一样。因为它们都是使用极大极小的剪枝算法下棋所以每次获取的都是最有利于自己的点就会不断的平局。
CSDN下载:(https://download.csdn.net/download/qq_45364953/15172911)