基于人类经验的五子棋(2)—补充规则和指出
基于之前建立的规则,来建立了一个初级的五子棋电脑,我们用规则来为棋盘上的每个位置评分,再从中找出评分最高的位置来选择;尽管有了一个良好的开端,但这仍然不能为它模拟一个"视觉"数据。
在进入下一步之前,我得提供一个事件,这个事件包含着一个经验,这个经验正是在这个五子棋目前状态中所有的。
我在上一篇位置提供了这些:
- 墙的优势计算【-5000】
- 当前位置在一行内是否有敌子(5格)的优势计算【-1】
- 与我子临格(5格)的优势计算【+2】
- 当前位置与我子关联(即临格1格)【+3】
- 如果另一边有子堵且可成3子【+3】,否则【+5】;
- 如果另一边有子堵且可成4子【+13】,否则【+20】;
- 成5子【+10000】
当时的我并没有处理“另一边有子堵且”。也就意味着我只处理了成3/4子的情况,而忽略了被堵的判断。不过,造成这种影响的确是因为五子棋入门潜,容易忽略这些,而在把程序做出来后,再去分析,才加入了这些功能。
所以,在进行项目之前,要尽量接触与项目有关的事物,以尽可能地完全实现项目中的行为细节。
好吧,那就从这里继续。接下来的问题是如何快速地添加这个实现。
void nodefraction(strplay**, int, int, short);
//计算优势,short是猜测对方下几步的次数
这个是我的计算优势的函数,strplay结构(或者说是类,但使用struct构造,而不是class)中存在着两个成员,分别记录着玩家优势分与电脑优势分。
与现在话题有关的、其内部实现:
//在此【CSDN】略有改动和从代码阅读上的优化
/*
static bool negative(strplay**node, int x, int y, int a, int b, int need, bool mode, bool how);
//查看该成子处另一端是否被堵。need为要看的方向:1为右-左;3为上-下;5为右上-左下;7为左上-右下返回true说明此位置被堵,被堵死则标记(只看5步);mode为false则不记录堵;how为true则为玩家
*/
//右边
linebool = false;//[bool]标记是否被堵,true是,false为否
int CommputerPtr = UserPtr = 0;//[int]UserPtr为玩家子计数;CommputerPtr为敌方子计数
CommputerUint = UserUint = false;//[bool]标记是否有电脑/玩家子出现过,true是,false否
//看这个位置以及接下来的4个位置
for (int i = 1, linea = a; linea < x && i < 5; ++i)
{
linea = a + i;//[int]
if (node[linea][b].mode == 0) //此处无子
break;//退出这个看右边的for,接下来是看左边
else
{
if (node[linea][b].mode == 1)//此处为玩家子
{
if (UserPtr < 4 && !CommputerUint)
++UserPtr;
else
break;
UserUint = true;
}
else //此处为电脑子
{
if (CommputerPtr < 4 && !UserUint)
++CommputerPtr;
else
break;
CommputerUint = true;
}
}
}
//左边
CommputerUint = UserUint = false;//标记是否有电脑/玩家子出现过,true是,false否
//看这个位置以及接下来的4个位置
for (int i = 1, linea = a; linea >= 0 && i < 5; ++i)
{
linea = a - i;
if (linea < 0)
break;
else if (node[linea][b].mode == 0)//此处无子
break;//退出这个看左边的for,然后其他方向,不过在此【CSDN】没有给出接下来的代码
else
{
if (node[linea][b].mode == 1)//此处为玩家子
{
if (UserPtr < 4 && !CommputerUint)
++UserPtr;
else
break;
UserUint = true;
}
else//此处为电脑子
{
if (CommputerPtr < 4 && !UserUint)
++CommputerPtr;
else
break;
CommputerUint = true;
}
}
}
linebool = negative(node, x, y, a, b, 1, true, true);//玩家记录被堵死位置
//玩家子成子数加分
switch (UserPtr) {//目前位置上的子是玩家子
case 2://成3子
if (linebool)//查看是否会被堵,会则为true
node[a][b].pinguser += 3;
else
node[a][b].pinguser += 7;
break;
case 3://成4子
if (linebool)
node[a][b].pinguser += 11;
else
node[a][b].pinguser += 500;
break;
case 4:node[a][b].pinguser += 10000;//5子+10000
break;
}
linebool = false;
linebool = negative(node, x, y, a, b, 1, true, false);//记录电脑被堵死位置
//电脑子成子数加分
switch (CommputerPtr) {//目前位置上的子是电脑子
case 2://成3子
if (linebool)
node[a][b].pingcommputer += 3;//3子+3
else
node[a][b].pingcommputer += 7;
break;
case 3://成4子
if (linebool)
node[a][b].pingcommputer += 11;//4子+11
else
node[a][b].pingcommputer += 500;
break;
case 4:node[a][b].pingcommputer += 10000;//5子+10000
break;
}
去除与这次话题无关的部分,改了一些,随后发现一些优化的地方也优化了,不过源代码我没动[懒得动]。
这是右-左的判断,每次判断的循环是对棋盘上的每个位置进行的。这里添加的函数negative:它判断这个成子处的两端是否全部都是空位,如果是则返回false,否则返回true标识被堵,如果被堵死,则记录到玩家/电脑的被堵死位置去。
基于上面的negative函数实现会发现一些问题:
- 对两端空位查看来判断是否被堵死的方法并非总是有效的。如一个成3子,但它的两端+1处都是敌子或墙;
- 记录堵死位置有潜在的不可逆风险。因为记录了不一定会删除(没人认为这有多么重要,并且在后面还会用到它,所以无法知道正确删除的时机)——如果在后续添加一个与此相联的功能: 比如用户想在游戏进行时自定义棋盘棋子,此时,玩家/电脑被堵死位置的信息仍然存在,又因为现在的目标是以最方便的方法到达现有目标,导致用户在完成他想做的事后,对于某些棋子位置电脑总是忽略它。
这个问题的解决方法是在软件开发文档中记录它。是的,你可以选择在合适的时候去处理它,但谁会知道这个数据对象会对后面的开发工作产生影响?现有对象是基于现有方案来实现处理的,所以根本无法从目前的要求考虑到对象的未来处理,从而到后面的功能添加时,发生了问题,也不一定能够准确分析出是因为某个数据对象造成的。所以,最好的方法是使negative函数不进行记录被堵死位置,取而代之为对此位置的评分记为最低。(这个方案相当不错,它既解决了以后对棋盘内容改动的潜在麻烦,也完成了功能需求。不过遗憾的是源代码中并没有这样做,因为现在改动可能会影响先前根据此来开发的功能,另外,这里其实还包含着一个经验,只是现在还不明显。)
现在的计算优势分函数完成了要求的更改。尽管这样,人机还是不能“正确”地下棋,有些明摆着的位置人机不给予理会,所以在用户眼里,他们无法理解这些行为,只能指出人机各种各样的错误来表达人机并不智能。(不过幸好我这时没把它推广)
由此可见,单纯的基于纳升均衡式数字化过于笨拙,应该添加一些“视觉”上的规则来纠正它。