面向对象以及运用最大最小搜索的井字棋设计(附源码) 您所在的位置:网站首页 井字棋游戏方案设计 面向对象以及运用最大最小搜索的井字棋设计(附源码)

面向对象以及运用最大最小搜索的井字棋设计(附源码)

2024-07-10 05:17| 来源: 网络整理| 查看: 265

这里写图片描述 一:实验题目 井字棋游戏设计 利用面向对象程序设计的知识,通过设计board、player、game类,实现一个具有人人对弈、人机对弈以及机机对弈的井字棋游戏。 要求: ①对类设置和实现的要求 1.封装:需要对游戏中的数据进行相应的封装保护。 在井字棋中的棋盘数据可以被 player 读取并放置下一步棋,但 player 不应该有权 限对棋盘进行随意改动。 2.继承:playerHuman 和 playerComputer 派生于 player。 3.多态:game 类只调用 player。Player 可以作为虚基类,通过纯虚函数实现多态 ②对界面的要求 1. 界面清晰,交互友好,可以使玩家在不需要说明的情况下操作。 ③对程序稳定性的要求 1. 程序应该具有一定的容错性,可以在用户输入错误的情况下给出提示并且保持正常工作。

二:实验目的 ① 熟悉运用c++的封装、继承和多态。 ② 掌握c++通过虚函数实现多态的办法。 ③ 学习通过基类指针绑定派生类对象的方法。 ④ 熟悉工程的建立和测试。 ⑤ 掌握最大最小搜索法,进行博弈。

三:程序设计与测试 先大体说一下思路,具体算法后面补上。首先,分析了一下每个类应该具有的作用: Board类: 1.储存一个每个点可以有三种状态的3×3矩阵, 2.画出棋盘 3.访问棋盘上各点 4.设置棋盘上某一点 因此用一个3×3的int矩阵储存信息,-1和1代表已经落子,0代表未落子。然后设置Draw、Get、Set三个函数来分别画出、访问和设置棋盘。 Player类: 这是个比较开放的,为了方便,我的设置是这样的 1. name,根据不同时候创建的时候给一个name,方便输出结果,比如说,同样是player_human,假如在人人对弈中,他可能叫“player1”或者“player2”,而在人机对弈中,他就叫“You”,再加上一个Get_name的函数,这样在输出游戏结果的时候据更加合理而且方便。 2. movement,记录当前走子(1~9),本来设置了悔棋这个功能,但是觉得程序冗余了很多(每次下的时候都要问一句悔棋否?),一个这么几步的游戏没必要设置,所以后来纯粹是作为记录走子使用,可有可无。 3. mark,这是一个静态成员变量,用来记录当前有多少个玩家,作用下面说。 4. hand,这是个用来记录先后手的成员变量,hand的设定是这样的 hand = pow(-1,mark),也就是说先后手是根据玩家创建的顺序确定的,因为每个游戏只有两个玩家,所以他们的hand一定是一正一负的,而且game结束之后,player会被析构掉,所以保证了player一定不超过两个。 5.一个move的虚函数,用来确定落子,根据player类型不同,这个move函数实现不同,实现多态。 6.check函数,判定玩家落子是否正确,保证玩家不会在已经下了的位置或者棋盘外的位置下。

Player_human类:继承于player 1. 没有自己独有的成员变量 成员函数: 1. Read_input读入玩家的输入 2. Move实现:利用check函数保证输入合法后返回走子值

Player_weak_computer类: 1. move实现:产生一个1到9的随机数,用check函数检查到合法即返回。

Player_strong_computer类: 1. 利用极大极小搜索寻求最佳策略。(详细算法后面解释)

Game类: 设计思路是,一个game需要一个棋盘和两名玩家,game的成员变量就是两个player指针组,加上一个board,以及一个计算第几轮的round变量。 成员函数: 1. 构造函数:需要变量是三个int值,分别指玩家选定的游戏选项。 2. Get_round:获取当前游戏回合数 3. Add_round:在玩家走子后,让回合数加一 4. Check:逐行逐列还有对角线检查有没有三子连线 5. Check_result:根据check的结果,判定当前局势(玩家1/2获胜,平局,进行中) 6. Get_result:根据check_result的结果,输出对应的语句,并且如果游戏结束返回true,否则返回false 7. Move:根据回合数交替调用对应玩家的move函数进行走棋,这里是整个game函数的一个关键,为了发挥player类多态的优势,上面game的成员变量是两个由player类指针指向的不同的派生player,然后就可以通过round回合数确定该轮的玩家,进而通过这个指针调用基类的move函数接口,最终调用对应派生类的move函数。 8. Main函数: 因为game函数设置的比较全面,所以主函数要做的工作非常少,就只要让玩家输入选取游戏模式,然后对应地实例化一个game,接下来就是让game一直move走棋,add_round,然后利用Get_result判断游戏是否结束,如果结束了再询 问玩家是否继续玩。

这里写图片描述

Main函数+game构造函数思路: 1:询问玩家是否开始游戏 2:选择模式:①人人对弈②人机对弈③机机对弈 3:再次选择模式:如果是人机对弈,则选择对弈弱电脑或强电脑,如果是机机对弈则选择弱弱、强强、强弱对弈 4:如果是人人对弈、机机对弈中的弱弱对弈或强强对弈则不需要选择谁先手,因为都一样。如果是人机对弈或机机对弈中的强弱对弈则会询问先手顺序。

下面详细说一下强AI用到的算法——极大极小搜索 极小极大的定义 Minimax算法 又名极小极大算法,是一种找出失败的最大可能性中的最小值的算法(即最小化对手的最大得益)。通常以递归形式来实现。 Minimax算法常用于棋类等由两方较量的游戏和程序。该算法是一个零总和算法,即一方要在可选的选项中选择将其优势最大化的选择,另一方则选择令对手优势最小化的一个,其输赢的总和为0(有点像能量守恒,就像本身两个玩家都有1点,最后输家要将他的1点给赢家,但整体上还是总共有2点)。井字棋博弈就是一个对极大极小搜索的经典应用。 使用Minimax算法首先要设定不同玩家的最大利益,比如X玩家为正无穷(+∞),O玩家的最大利益为负无穷(-∞),这样我们称X玩家为MAX(因为他总是追求更大的值),成O玩家为MIN(她总是追求更小的值),各自都为争取自己的最大获益而努力。这里要注意,同一个局面,无论对X玩家还是O玩家,它的评分都应该一样。

现在,让我们站在Min的立场来分析局势(当然,也可以选择Max立场,不过我的程序里面用的是Min,那就以Min为最大利益)。于是构建出来的博弈树如下(前面两层,由于对称性,只列举三种情况): 这里写图片描述 MAX总是会选择MIN获利中的最小值(对MAX最有利),同样MIN也会一样,选择对自己最有利的(即MAX有可能获得的最大值)。有点难理解,其实就是然后抢先把对对手有利的位置抢占了,举个例子,假如在一步走棋之后,对手有两种走法,一种是直接赢,一种是游戏继续,假设对手足够聪明,他肯定会选择前者,那么你的结局有两个,输或者游戏继续,为了获取你的最大利益(达到Min),你肯定要阻止对手达到最优局面(Max),所以记录不同走子得到的Max值,最后选取其中最小的一个。 整个过程不断往下深钻的,直到最底层(即叶节点)你才能往上回溯,确定那个是对你最有利的。 具体过程会像是这么一个样子的(网上插图): 这里写图片描述 一般来说,最大最小搜索都要设置搜索深度,当达到搜索深度都还没有得到游戏结果,就需要一个评估函数得出当前局面的评估值,评估函数和搜索深度很大程度上了AI的智能程度(另一个办法是通过剪枝把极大极小搜索优化为Alpha-Beta搜索,不过考虑到project说明里面已经给了套路智能的方法,也就是说完全可以依靠套路达到智能,搜索就没有意义了,因此Alpha-Beta剪枝到极端就变成了纯套路,只要搜索一层或者不用搜索,算法就失去意义了),因为井字棋可能局面不多,只有9!,因此我的评价函数就只有三种结果:输赢或者平局。这样的话,程序基本上是搜索到底,不过这里也造成了一个小“问题”,当局面为必胜的时候,AI有两个选择,①直接获胜②制造下一轮有两个获胜位置的局面,这时候因为两种情况的估值都是最有利的,因此AI会随机选择其中一个,当然这样的情况可以通过在评价函数在补充一个对二连环的评估,返回一个小于赢但是大于平局的评估值来避免,鉴于最终结局都是赢,而且这样更有趣味性,我保留了这个“小问题”。

程序测试图: 游戏设定选择: 这里写图片描述

游戏选择输入异常处理

这里写图片描述 游戏过程: 这里写图片描述

游戏过程输入异常处理: 这里写图片描述

AI走棋后等待玩家确认:

这里写图片描述 游戏结束的提示:

这里写图片描述

这里写图片描述

四:实验总结与心得 这次的实验算得上是一个小工程了,在试验中学到了不少实际中的工程方法。另外,整个程序的设计充分利用了C++面向对象设计的特点,对封装、继承、多态有了更深入的了解。下面是一些具体的收获: 1. 类的声明实现和主函数分离,工程上的需要,而且一些情况下,避免了代码重写,易于使用和维护。 2. 学会建立工程(虽然是sublime帮我link的)。 3. 学会了使用ifdef、endif语句和pragma once语句防止重编译。 4. 对面向对象程序设计有更深入的了解,懂得根据一个对象的属性和功能进行分析,设计合适的成员变量和成员函数实现。 5. 对C++的继承和多态,掌握更加熟练。 6. 掌握了极大极小算法和Alpha-Beta算法。

下面是一些实验中的小发现: 1. 因为实验中我的user数组是基类player*类型的,在悔棋功能设计中,为了区别human和computer(只有human有悔棋功能),我尝试使用sizeof(*user[0])这样的方式获得指针指向对象的大小(我在computer类里面故意加了一个无用的int数组,增加它的大小),结果发现,无论指针指向computer对象还是human对象,被指向的对象大小一样(不是指针大小,是对象大小!)然后我用*player_human和*player_computer指针分别实例化了human和computer对象,发现这次他们的大小是不一样的!回想起上次Homework写的实验报告,基类指针可以指向派生类对象,但是只继承了基类的成员变量和成员函数, 这里写图片描述 下面把上次实验报告写的重新回顾一下: 多态下的指针调用,这是个比较难搞的知识点,下面粗略讲一下。 1. 基类指针指向派生类,则只可以访问基类函数和基类的成员变量,因此一般会通过虚函数提供接口访问派生类成员变量。 2. 派生类指针指向基类,这样做有一定的风险,必须通过合法转换,因为派生类指针可以访问派生类的成员函数,而基类对象不一定有派生类的成员函数或成员变量。 3. 如果基类和派生类有重定义函数,即派生类重定义了继承的函数,则通过指针调用成员函数,取决于指针的初始类型(原因下面解释),如果指针声明时为基类指针,则会调用基类成员函数,反之调用派生类函数。 4. 如果基类和派生类有虚函数,则通过指针调用virtual函数,调用基类还是派生类的virtual取决于指针指向的对象类型。 ⑥函数重定义和虚函数的区别(解释上面) 最大的区别就是,重定义函数是静态绑定的,而virtual函数是动态绑定的,也就是说重定义函数在编译的时候根据声明的类型就已经确定下来了,而virtual会在程序运行的时候根据指针指向的对象类型而改变。好奇之下, 查了一下实现原理,虚函数表,编译原理层面,~卒,先mark down了。 2.#pragma once一句话就可以防止重编译,比#ifdef #def #endif来得方便。(不知道在其他编译器下可不可以)

源代码: board类

//board.h #pragma once #include #include #include using namespace std; #define cls() system("cls") class board { private: int chessboard[4][4]; // for convenience public: board() { for(int i = 1; i 3) return -2; return chessboard[row][col]; } void Set_board(int row,int col,int hand) { if(row > 3 || row < 1 || col >


【本文地址】

公司简介

联系我们

今日新闻

    推荐新闻

    专题文章
      CopyRight 2018-2019 实验室设备网 版权所有