当前位置: 首页 > news >正文

C++::基于minimax算法设计的三子棋游戏

基础的c语言三子棋相信很多人都做过了,碰巧今年电赛有个三子棋,就试试这个算法

三子棋的规则很简单:在一个3x3的格子板上,两名玩家轮流放置自己的棋子(通常是X和O)。第一个玩家成功在横向、纵向或对角线上连成一条由自己棋子组成的线(即三子连线)即为胜者。如果所有格子都被填满而没有人连成三子线,则游戏平局

而游戏的对弈模型是基本的零和博弈模型,minimax算法就是为了零和博弈而生的.

Minimax算法是一种用于零和博弈的决策算法,主要用于找到最佳策略。它通过递归地模拟所有可能的游戏状态来确定每个玩家的最佳行动。算法的核心是:

  1. 最大化:当前玩家(最大化者)试图使得其最终收益最大化,在这里指电脑.
  2. 最小化:对手(最小化者)试图使得当前玩家的收益最小化,在这里指玩家.

在每一步,算法评估所有可能的后续状态,选择能够在最终回合中达到最佳结果的行动。

一、游戏逻辑设计

1、头文件定义(board.h)

首先定义一个对象用于存储棋盘和调用方法。

#include <iostream>
#include <string>using namespace std;#define _computer_  'O'     //电脑棋子
#define _player_    'X'     //玩家棋子
#define _continue_  'C'     //继续游戏
#define _pease_    'P'      //平局
#define _empty_    ' '      //空棋子
#define ROW 3               //棋盘水平长度
#define COL 3               //棋盘垂直长度//棋盘对象
class Board
{//定义棋盘为 ROW X COL 大小char board[ROW][COL];public://初始化棋盘void InitBoard();//玩家移动bool Player_move(int x,int y);//打印棋盘void displayBoard();//电脑移动void Computer_move();//判断棋盘是否已经满了bool is_full();//minimax评分函数int evaluate();//minimax算法逻辑int minimax(int depth,bool is_maxing);//判断胜利方char is_win();
};//菜单
void menu();

*ps:为什么要把所有函数放在一个对象内,因为对象内可以直接调用board属性,省去传参

2、游戏逻辑设计(board.cpp)

(1)菜单、棋盘初始化、棋盘打印

//菜单
void menu()
{cout<<"***************************"<<endl;cout<<"***************************"<<endl;cout<<"******* 三子棋小游戏 *******"<<endl;cout<<"*******   1.play   *******"<<endl;cout<<"*******   0.exit   *******"<<endl;cout<<"***************************"<<endl;cout<<"***************************"<<endl;
}//初始化棋盘
void Board::InitBoard()
{//遍历每个格子,将其置为 _empty_for(int i=0;i<ROW;i++){for(int j=0;j<COL;j++){this->board[i][j] = _empty_;}}
}//打印棋盘
void Board::displayBoard() 
{//为了好看,加了点框string top = "┌";       //顶部三框string mid = "├";       //中间三框string bottom = "└";    //底部三框//添加中间隔层for(int i = 0; i < COL - 1; ++i) {top += "────┬";mid += "────┼";bottom += "────┴";}//尾部修缮top += "────┐";mid += "────┤";bottom += "────┘";cout << top << '\n';//遍历for(int i = 0; i < ROW; ++i) {if(i > 0)cout << mid << '\n';//每行的第一个隔层cout << "│  ";for(int j = 0; j < COL; ++j) {//将board中的数据插入隔层前cout << this->board[i][j] << " │  ";}//删除多余空格cout << "\b\b\b│\n"; }//打印底下隔层cout << bottom << '\n';}

(2)玩家下棋

//玩家下棋          获取外部输入的x,y下标
bool Board::Player_move(int  x,int y)
{//判断当前选定的棋盘位置是否为 _empty_if(board[x][y] == _empty_){//如果是空的将 _player_ 置换 board 对应位置的 _empty_board[x][y] = _player_;//下棋成功标签return true;    }else{cout<<"当前位置已有棋子,请重新选择"<<endl;system("pause");//下棋失败标签return false;}}

(3)电脑下棋

重要的来了,minimax算法的评分函数是判断当前棋盘的状态,给定评断分数,是minimax的结束条件

evaluate评分函数实现

//评分函数
int Board::evaluate(){//判断每行、每列状态for(int i=0;i< ROW;i++){if(board[i][0] == board[i][1] && board[i][1]== board[i][2] && board[i][0] != _empty_){return (board[i][0] == _computer_)? 1 : -1;}if(board[0][i] == board[1][i] && board[1][i] == board[2][i] && board[0][i] != _empty_){return (board[0][i] == _computer_)? 1 : -1;}}//判断斜线if(board[0][0] == board[1][1] && board[1][1] == board[2][2] && board[0][0] != _empty_)return (board[0][0] == _computer_)? 1 : -1;if(board[2][0] == board[1][1] && board[1][1] == board[0][2] && board[2][0]!= _empty_)return (board[2][0] == _computer_)? 1 : -1;//没有胜利条件return 0;}

注意,因为是作为在三子棋递归中的结束条件,所以只需要判断每行、每列、斜线是否连成一条线

,如果连成一条线的是MAX者的棋子,则返回的是 1 ,反之返回 -1,如果没有连成线,则返回 0 

Minimax算法实现

int Board::minimax(int depth,bool is_maxing)
{//评估棋盘状态 接收一个返回值(-1,1,0) 表示游戏结果int score = evaluate();//若结果为 1 或 -1 表示游戏结束 ,则退出递归if(score == 1 || score == -1)return score;//若判断棋盘为满的, 表示游戏平局,退出递归if(is_full())return 0;//如果当前是电脑回合,则最大化玩家if(is_maxing){//设置 最佳值 初始化为最小值double best_score = INT_MIN;//遍历棋盘上每一个为 _empty_ 的位置for(int i=0;i<ROW;i++){for(int j=0;j<COL;j++){//若当前棋盘位置为 _empty_if(board[i][j] == _empty_){//假设电脑在当前位置下棋board[i][j] = _computer_;//获取当前棋盘分数,递归minimax函数,并将下一回合切换到对手回合score = minimax(depth+1,false);//复位棋局board[i][j] = _empty_;//获取当前回合的最大值best_score = max(best_score,(double)score);}}}//返回最佳值return best_score;}else{//若当前回合是玩家,则最小化玩家//设置 最佳值 初始化为最大值double best_score = INT_MAX;//遍历棋盘上每一个为 _empty_ 的位置for(int i=0;i<ROW;i++){for(int j=0;j<COL;j++){//若当前位置为 _empty_if(board[i][j] == _empty_){//则假设玩家在当前位置下棋board[i][j] = _player_;//获取当前分数,递归minimax函数 并切换到电脑回合 score = minimax(depth+1,true);//复位当前棋局board[i][j] = _empty_;//获取当前回合的最小值best_score = min(best_score,(double)score);}}}//返回最佳值return best_score;}}

详细算法逻辑见:【从minimax到alpha-beta剪枝算法(上):minimax算法原理介绍】

Computer_move电脑下棋实现

//电脑下棋
void Board::Computer_move()
{//电脑回合,最大化玩家double best_val = INT_MIN;//设置初始最佳下棋位置为 -1,-1pair<int, int> best_move(-1, -1);//遍历当前棋盘上为 _empty_ 的位置for (int i = 0; i < 3; ++i) {for (int j = 0; j < 3; ++j) {//若当前位置为 _empty_if (board[i][j] == _empty_) {//执行minimax决策board[i][j] = _computer_;int move_val = minimax(0, false);board[i][j] = _empty_;// cout<<"move_val:>"<<move_val<<"\tbest_val:>"<<best_val<<"\t<x,y>:"<<i<<" "<<j<<endl;//若当前值大于最佳值if (move_val > best_val) {//则将最佳下棋位置替换为当前的位置best_move = std::make_pair(i, j);//更新最佳值best_val = move_val;}}}}//将棋盘对应最佳下棋位置替换为 _computer_this->board[best_move.first][best_move.second] = _computer_;
}

(4)判断胜利、判断平局

判断平局

bool Board::is_full()
{for(int i=0;i<ROW;i++){for(int j=0;j<COL;j++){if(board[i][j] == _empty_)return false;}}return true;
}

判断胜利

char Board::is_win()
{for(int i=0;i< ROW;i++){if(this->board[i][0] == this->board[i][1] && this->board[i][1] == this->board[i][2] &&this->board[i][0]!= _empty_){return this->board[i][0];}if(this->board[0][i] == this->board[1][i] && this->board[1][i] == this->board[2][i] &&this->board[0][i] != _empty_){return this->board[0][i];}}//判断斜线if(this->board[0][0] == this->board[1][1] && this->board[1][1] == this->board[2][2] && this->board[0][0]!= _empty_){return this->board[0][0];}if(this->board[2][0] == this->board[1][1] && this->board[1][1] == this->board[0][2] &&this->board[2][0]!= _empty_){return this->board[2][0];}if(is_full())return _pease_;return _continue_;}

3、主函数设计(main.cpp)

#include "board.h"void game()
{//清屏system("cls");//定义棋盘对象Board b;//初始化棋盘b.InitBoard();//游戏状态char ret;while (true){//展示棋盘system("cls");b.displayBoard();//玩家下棋int x,y;cout<<"请输入要下的位置<row col>:";cin>>x>>y;if(!b.Player_move(x,y))continue;//胜利判断ret = b.is_win();if(ret != _continue_)break;//电脑下棋b.Computer_move();//胜利判断ret = b.is_win();if(ret != _continue_)break;}//展示最后棋盘状态b.displayBoard();if(ret == _player_)cout<<"玩家胜利"<<endl;if(ret == _computer_)cout<<"电脑胜利"<<endl;if(ret == _pease_)cout<<"平局"<<endl;system("pause");system("cls");}int main()
{//菜单选择int input;while (true){menu();cout<<"请选择:>";cin>>input;switch (input){case 1:game();break;case 0:cout<<"游戏结束"<<endl;system("pause");return 0;break;default:cout<<"输入错误"<<endl;system("pause");system("cls");break;}}system("pause");
}

二、游戏展示

可以看到,程序完美运行。

但是有个问题,在我下第三个棋子(2,2)后,明明电脑已经连成两个棋子可以直接胜利,

却选择堵住(1,2)点位,这是因为递归中循环是从上往下依次遍历模拟下棋,然后在返回的值中从上往下选择,当找到(1,2)这个点位时,刚好是返回值是1,所以电脑选择在这一步下棋而不是在(2,1)处下棋

解决这个问题的办法就是使用α-β剪枝,当判断当前值已经最大时,直接break,而不是继续遍历

优化minimax算法实现

int Board::minimax(int depth, bool is_maxing, int alpha, int beta)
{double score = evaluate();if(score == 1 || score == -1)return score;if(is_full())return 0;if(is_maxing){double best_score = INT_MIN;for(int i = 0; i < ROW; i++){for(int j = 0; j < COL; j++){if(board[i][j] == _empty_){board[i][j] = _computer_;score = minimax(depth + 1, false, alpha, beta);board[i][j] = _empty_;best_score = max(best_score, score);//更新α值alpha = max((double)alpha, best_score);//减去β枝if(beta <= alpha)break; // Beta cutoff}}}return best_score;}else{double best_score = INT_MAX;for(int i = 0; i < ROW; i++){for(int j = 0; j < COL; j++){if(board[i][j] == _empty_){board[i][j] = _player_;score = minimax(depth + 1, true, alpha, beta);board[i][j] = _empty_;best_score = min(best_score, score);//更新β值beta = min((double)beta, best_score);//减去α枝if(beta <= alpha)break; // Alpha cutoff}}}return best_score;}
}

设置初始的α值为INT_MAX,设置初始β值为INT_MIN

优化算法后的程序为

可以看到,电脑已经完全实现了三子棋的功能


http://www.mrgr.cn/news/10873.html

相关文章:

  • 由浅入深package.json,发布一个优秀的npm包
  • 互换顺序表中的两个子表位置
  • 【开发笔记】Notepad++配置
  • 数据仓库建模的步骤-从需求分析到模型优化的全面指南
  • 【大数据】-- 插入或覆写动态分区数据(MaxCompute/Hive)
  • 华为OD机试-转盘寿司(C++ Java Python)
  • cloud compare 学习利用CC代码加快插件开发与总结(三)
  • 【机器学习工具库-一-传统机器学习sklearn库】
  • redis--主从复制,哨兵模式,Redis Cluster模式
  • MySQL 中的 distinct 和 group by 哪个效率更高
  • Android - lock/unlock bootloader
  • pikachu靶场XSS通关攻略
  • accelerate相关笔记
  • 基于Material Design风格开源的Avalonia UI控件库
  • ERROR: failed to create cluster: failed to list nodes
  • NVIDIA Jetson AGX Orin源码编译安装CV-CUDA
  • 关于Linux sudo授权的那点事
  • 《C++魔法:运算符重载的奇妙之旅》
  • Autosar(Davinci) --- ADT和IDT如何Mapping
  • Andrid异步更新UI:Handler(二)深入了解:Message你真的会创建?它是如何子线程和主线程通知?