一、问题描述
三连棋双方都是智力卓越的话,很容易完成平局;在人机对战情况下, 计算机的策略是保证自己不输,并等待玩家失误的机会,当玩家失误时候, 计算机抓住这个失误并取得胜利;
** 极大极小策略**
使用一个求值函数来对一个位置的好坏量化;能使得计算机获胜的位置,其值+1; 平局为0; 计算机输的-1;通过考察盘面能够确定这局棋输赢的位置叫做终端位置(终端位置:有时候只下到一半,计算机发现玩家失误了,计算机一定能赢了,不用等到完全落完棋子);
终端位置是使用immediateHumanWin函数或是immediateCompWin函数能判断的;
如果一个位置不是终端位置, 该位置的值通过递归假设双方都选择最优棋步确定(计算机和玩家都不失误),叫做极大极小策略;计算机试图使这个位置极大化, 玩家试图使这个位置极小化;
每次轮到计算机下棋的时候, 计算机都对棋盘中的空位做挨个做测试,得到每一个空位的值,从中选择最大的作为这一步的下子点;
每次轮到玩家要下棋的时候, 计算机都对棋盘的所有空位都挨个做测试,得到每一个空位的值,从中选择一个最小的值作为这一步的下子点;
二、代码分析
一个井字游戏代码的链接:
https://github.com/YinWenAtBIT/Data-Structure/tree/master/tic_tac_toe
https://blog.csdn.net/yw8355507/article/details/48868173
下面是自己写的代码,但是这个代码有错误,还没有找出,上面链接的代码是正确的,可以参考;
假设当前棋盘的情况如下图所示,将棋盘的9个空间分为两个集合, 其中Q表示已经落子的集合, S表示空闲集合;假设接下来轮到计算机下棋,计算机会在集合S中选择一个i点用于防止本轮的棋子;如何从集合中选择i点执行以下的策略——极大极小策略:
判断的标准是value变量,有三个取值:
1) 当计算机赢,设置COM_WIN为 1
2) 当平局, 设置DRAW为0
3) 当计算机输, 设置COM_LOSS为-1
#include <stdio.h>
#include <stdlib.h>
#define N 3
char Chess[N][N] = {
0};
const char COMP = 'O';
const char HUMAN = 'X';
//极大极小策略
//是计算机获胜的位置,可以得到值1; 平局0; 人获胜的值-1;
#define COMP_LOSS -1
#define DRAW 0
#define COMP_WIN 1
对于计算机,使用findCompMove返回S集合的value值
findCompMove函数分为三部分;
1) 当前集合Q和S导致棋子落满了, 是平局,返回value = DRAW;
2)在当前的棋面上, 在S集中选择一个i位置放置COMP, 如果放置之后计算机胜利,就返回,并且将i赋值给bestMove返回上一层调用;
3)如果本次无论如何放置都不会成功,就需要在集合S中的value值来判断;
假设value一开始为最低的COM_LOSS, 循环尝试S集合中的每一个点i, 递归调用计算(S - i)的value值得到responseValue, 得到最大的value值为value = max(value, respenseValue);
当能够得到一个最大的value的时候,记录下点i的位置为bestMove表明本次轮流下应该下子的点;最后, 将value的值做函数返回;
轮到计算机放置棋子时候findCompMove的函数, findCompMove 的目的是在集合S中找到一个点来放置COMP, 这个点就是bestMove;findCompMove 函数返回的是极大极小策略的比较标准,在递归调用中, 一般将比较标准的值的结果作为递归函数的返回值;
回溯算法在实现上需要循环与递归调用的结合, 循环保证对当前所有可能情况都去尝试, 递归保证了在不符合标准的时候(尝试失败的时候),递归调用函数返回,重新选择下一种情况重新尝试;
findCompMove 函数的代码如下:
int findCompMove(int *bestMove) {
int i, responseValue;
int dc; //无用的值
int value; //空闲点的值
if (fullChess())
value = DRAW;
//在空闲点集合S中选择一个点放置COMP,然后测试计算机是否胜利
else if (immediateCompWin(bestMove))
return COMP_WIN;
else {
value = COMP_LOSS;//空闲点的值初始化为最小
*bestMove = 1;
for (i = 1; i <= N * N; i++) {
if (isEmpty(i)) {
//S集合是现在棋盘上的空闲点
place(i, COMP); //假设在i位置落子
responseValue = findHumanMove(&dc);//测试(S-i) 空闲点的值
unPlace(i); //恢复位置i
//responseValue表示空闲点(S-i)时候的空闲点值
//value表示空闲点S时候的空闲先值
//根据极大极小值策略, 计算机是想让空闲点的值最大化,因此取得max(value, responseValue)
//如果在空闲点S中选择一个点i能使的value增大为responseValue,就在位置i上落子
//如果当前空闲空间S中任何一个i都不能使得空闲值增加, 就返回
if (responseValue > value)
{
value = responseValue;
*bestMove = i;
}
}
}
}
//函数返回空闲点的值
//value = 1, 表示剩余空闲点中又让计算机赢的组合,计算机有赢的可能
//value = 0, 计算机有平局的可能
//value = -1 计算机有输的可能
//由于有value = max(value, responseValue),因此上述三种情况有优先级
//只要存在计算机赢的可能, value就返回1
//当不存在计算机赢的机会, 但是只要存在平局的机会, value就返回0
//当即不存在计算机赢的机会, 也不存在平局的机会,value才返回-1;
return value;
}
findHumanMove 函数的代码实现如下:
int findHumanMove(int *bestMove) {
int i, responseValue;
int dc;
int value;
if (fullChess())
value = DRAW;
else if (immediateHumanWin(bestMove))
return COMP_LOSS;
else {
value = COMP_WIN;
*bestMove = 1;
for (i = 1; i <= N * N; i++) {
if (isEmpty(i)) {
place(i, HUMAN);
responseValue = findCompMove(&dc);
unPlace(i);
if (responseValue < value) {
value = responseValue;
*bestMove = i