文章目录
试试用字符串的方法来思考人机五子棋
1 说明
记得第一篇博客就是写的用C语言实现人机五子棋,完成后当时非常开心,但是现在看来,写得很尴尬。因为后期项目会有用到的缘故,让我再一次重新思考五子棋游戏的算法实现,思路其实和以前没多大变化,但在设计中,发现用字符串自带的方法可以让问题变得很简单(效率方面的话可能会有点影响,但可忽略不计)。
2 胜负判断(核心)
我的思路仍然是依次遍历4个方向(横’-’,竖’|’,左斜’/’,右斜"\"),根据这些方向是否出现5子就可判断输赢。那问题是如何实现呢?标题是用字符串解决,那么当然是使用字符串啦!棋盘我选用二维数组,char类型,'0’表示没有棋,'1’表示黑棋,'2’表示白棋。
具体思路:我可以通过循环获取当前棋子所在方向的字符串,然后通过查找子串的方式判断是否出现五个’1’或’2’,这里给出Java代码示例:
//x和y是坐标,index区分当前棋子颜色
public boolean isVictory(int x, int y, int index)
{
String panDuan = (index == 1 ? "11111" : "22222" );
String heng = ""; //-
for (int i = 0; i < 15; i++) heng += chessboard[i][y];
if (heng.indexOf(panDuan) != -1) return true;
String shu = ""; //|
for (int i = 0; i < 15; i++) shu += chessboard[x][i];
if (shu.indexOf(panDuan) != -1) return true;
int tempX = x, tempY = y;
String zhuoXie = chessboard[x][y] + ""; // '/'
tempX = x; tempY = y; //向上遍历
while (tempX++ < 14 && tempY-- > 0) zhuoXie = zhuoXie + chessboard[tempX][tempY];
tempX = x; tempY = y; //向下遍历
while (tempX-- > 0 && tempY++ < 14) zhuoXie = chessboard[tempX][tempY] + zhuoXie;
if (zhuoXie.indexOf(panDuan) != -1) return true;
String youxie = chessboard[x][y] + ""; // '\'
tempX = x; tempY = y; //向上遍历
while (tempX-- > 0 && tempY-- > 0) youxie = chessboard[tempX][tempY] + youxie;
tempX = x; tempY = y; //向下遍历
while (tempX++ < 14 && tempY++ < 14) youxie = youxie + chessboard[tempX][tempY];
if (youxie.indexOf(panDuan) != -1) return true;
return false;
}
需要注意的是斜方向字符串拼接的顺序(灵感来源于用字符串求回文串),另外代码命名不规范,并且有一个多余的代码,主要看思路吧
3 人机走法(核心)
我的思路仍然是通过遍历棋盘每个可以下棋的位置,根据网上给出的码表得出目前最高分的位置,此位置就是人机落子点。这里请务必先看看这位老师的讲解(当时就是看的这篇博客写的人机算法)。一直觉得这种方法很奇特,原理如此简单,但是效果很神奇( 也可能是我五子棋技术确实太菜了😄)。那问题是如何编写呢?
3.1 棋盘和码表制作
public static char[][] chessboard =new char[15][15];
/* x表示空格 A表示棋子 score中第一个是自己情况,第二个是阻止对方
成五:AAAAA 10000 1000
活四:XAAAAX 200 100
死四:XAAAA或AAAAX 50 20
活三:XAAAX 30 10
死三:XAAA或AAAX 8 5
活二:XAAX 2 1
死二:XAA或AAX 2 1
活一:XAX 1 0
死一:XA或AX 1 0
死绝: 全是A但小于5(使用A代替) -1 0
*/ //死绝情况说明:就是下了也没有比如说两端不可以下了,中间下了也没有5颗棋子,这里顺便把没出现在码表的情况也判断了
public static Map<String, Integer[]> policyTables = new HashMap<String, Integer[]>();
//保存每一步下棋步骤,可以悔棋,可以看出每一步顺序
public List<String> saveStep = new LinkedList<>();
static
{
String[] condition = { "AAAAA", "XAAAAX", "XAAAA", "AAAAX", "XAAAX", "XAAA",
"AAAX", "XAAX", "XAA", "AAX", "XAX", "XA", "AX", "A"};
Integer[][] score = {{100000,1000}, {200,100}, {50,20}, {50,20}, {30,10}, {8,5},
{8,5}, {2,1}, {2,1}, {2,1}, {1,0}, {1,0}, {1,0}, {-1,0}};
//初始化码表
for (int i = 0; i < condition.length; i++) policyTables.put(condition[i], score[i]);
//初始化棋盘
for (int i = 0; i < 15; i++) Arrays.fill(chessboard[i], '0');
}
其实也可以不使用码表,直接在代码中写,但是这样的话很不方便,分数满天飞,想增加点也不方便。码表使用键值对方式,值使用的数组,第一个元素是自己得分,第二个是阻止敌人后的得分。
3.2 进一步分析
现在我们有了棋盘也有了码表,那接下又该干啥呢?现在想象一下我就是一颗棋,棋盘摆在我的面前,我当前的位置如果我落下去了,会有哪些可能性?你可能会说那可能性太多了吧?但是其实大部分就是码表出现的情况,因此我把当前位置看成中心,像判断胜负那样去判断我当前位置 水平方向,竖直方向,两个斜方向是码表中的哪一种情况,把对应的得分累加起来,但不要忘了,我们还要考虑阻止敌人的情况,所以我们还要把自己当成敌人,然后使用同样的方法看这个位置下去后会对有多大影响,两次影响之和就是最终得分。看到这里你可能会晕,这可能是我的表述问题,但看代码可能就好了。另外,我们发现可能会写大量重复的代码,而且和胜负判断的代码也会使用到。我们还是把问题拆分成小问题好一点。第一步:先写一个函数,功能是通过传入的字符串,下标,得出在码表可以出现的键;第二步:类似胜负判断代码,水平方向,竖直方向,两个斜方向分别调用此函数,得出最终得分,最后返回;第三步:遍历棋盘,把每个空位都判断一遍,得出最高分并且返回坐标。下面是示例代码:
3.2.1 根据字符串匹配码表
public String getPolicyValue(String test, int x, char index)
{
char reversal = (index == '1' ? '2' : '1');
String ret = index + "";
//如果是当前标志,就继续; 否则退出,并且如果是墙就是死,是其他就是死,是空格就是活
for (int i = x + 1; i < test.length(); i++) //15
{
if (test.charAt(i) == index) ret = ret + index;
if (test.charAt(i) == reversal) break;
if (test.charAt(i) == '0')
{
ret = ret + 'X';
break;
}
}
for (int i = x - 1; i >= 0; i--)
{
if (test.charAt(i) == index) ret = index + ret;
if (test.charAt(i) == reversal) break;
if (test.charAt(i) == '0')
{
ret = 'X' + ret;
break;
}
}
ret = ret.replace(index, 'A');
if (ret.indexOf("AAAAA") != -1) return "AAAAA";
if (ret.indexOf("X") == -1) return "A";
return ret;
}
3.2.2 根据码表得出当前位置的分数
//chessboard是棋盘,x和y是棋子,index表明颜色,返回分数
public int getScore(int x, int y, char index)
{
char preventEnemy = (index == 1 ? '2' : '1' );
String heng = ""; //-
for (int i = 0; i < 15; i++) heng += chessboard[i][y];
String heng1 = getPolicyValue(heng, x, index);
String heng2 = getPolicyValue(heng, x, preventEnemy);
String shu = ""; //|
for (int i = 0; i < 15; i++) shu += chessboard[x][i];
String shu1 = getPolicyValue(shu, y, index);
String shu2 = getPolicyValue(shu, y, preventEnemy);
int tempX = x, tempY = y;
String zhuoXie = chessboard[x][y] + ""; // /
tempX = x; tempY = y; //向上遍历
while (tempX++ < 14 && tempY-- > 0) zhuoXie = zhuoXie + chessboard[tempX][tempY];
tempX = x; tempY = y; //向下遍历
int cnt = 0; //记录向下遍历了次数
while (tempX-- > 0 && tempY++ < 14)
{
zhuoXie = chessboard[tempX][tempY] + zhuoXie;
cnt++;
}
String zhuoXie1 = getPolicyValue(zhuoXie, cnt, index);
String zhuoXie2 = getPolicyValue(zhuoXie, cnt, preventEnemy);
String youxie = chessboard[x][y] + ""; // '\'
tempX = x; tempY = y; //向上遍历
cnt = 0;
while (tempX-- > 0 && tempY-- > 0)
{
youxie = chessboard[tempX][tempY] + youxie;
cnt++;
}
tempX = x; tempY = y; //向下遍历
while (tempX++ < 14 && tempY++ < 14) youxie = youxie + chessboard[tempX][tempY];
String youxie1= getPolicyValue(youxie, cnt, index);
String youxie2 =getPolicyValue(youxie, cnt, preventEnemy);
int score = -1;
String[] scoreList = {heng1, heng2, shu1, shu2, zhuoXie1, zhuoXie2, youxie1, youxie2};
for (String i : scoreList)
{
score += policyTables.get(i)[0];
score += policyTables.get(i)[1];
}
return score;
}
3.2.3 遍历棋盘返回最高分位置
public int[] calc()
{
int max = -10000;
int x = -1, y = -1;
for (int i = 0; i < 15; i++)
{
for (int j = 0; j < 15; j++)
{
if (chessboard[j][i] == '0')
{
int score = getScore(j, i, '2');
if (max < score)
{
max = score;
x = j;
y = i;
}
}
}
}
System.out.println("\n" + "当前人机落子点:" + x + "," + y);
return new int[]{x, y};
}
目前为止,人机算法全部实现,会发现大量使用了和胜负判断相同的代码,实际写可以把这些重复代码写成方法,这只是我的实现方式,网上还有许多方法,下面顺便也提供一下其他几个功能的实现。
4 悔棋功能
此功能比较简单,大致思路是创建一个容器,当我和人机每一次下棋的时候就把坐标和颜色push进去,当我使用悔棋功能的时候只需要从容器中pop出来,然后把棋盘对应位置的值修改为无棋就ok了,如果是c语言可能需要自己手写数据结构,其他很多语言都有对应的容器解决,后面会有Java代码。
5 存档读档
读档的规则依赖于存储,所以先说存档吧。存档功能也比较简单,但是可以有一个小的优化,那就是并不需要把整个棋盘状态都保存。可以只去存放有落子点的坐标和颜色,这样如果我们下了很少的步数,非常节约内存,最大也不过是255个。那我们可以把棋子封装成一个字符串,比如说保存棋子(位置是6,8,颜色是白)我们可以这样写:“06082”,比如说保存棋子(位置是12,14,颜色 是黑)我们可以这样写:“12141”。最后遍历容器把数据放入到文件中。
读档就很简单了,因为我们存档有明确的规则,所以我们只需要按照规则去读出来,然后把数据加入到对应的二维数组棋盘就ok了。这里我采用的是对象序列化和反序列化,后面会有Java代码。
6 封装一下
这里把刚刚的代码整理一下,写一个GobangUtil类,整的稍微专业一点点,如果是用Java语言都可以直接拿来用了!我们可以更加专注界面设计,这里有些代码做了一点点小规范,但是上面的代码没问题的话这个类也不在话下:
package com.hmj;
import java.io.*;
import java.util.*;
public class GobangUtil
{
public static final Character NO_CHESS = '0';
public static final Character BLACK_CHESS = '1';
public static final Character WHITE_CHESS = '2';
public static final String BLACK_WIN = "11111";
public static final String WHITE_WIN = "22222";
public static final Integer CHESSBOARD_SIZE = 15;
public static Map<String, Integer[]> policyTables = new HashMap<String, Integer[]>();
static
{
String[] condition = { "AAAAA", "XAAAAX", "XAAAA", "AAAAX", "XAAAX", "XAAA",
"AAAX", "XAAX", "XAA", "AAX", "XAX", "XA", "AX", "A"};
Integer[][] score = {{100000,1000}, {200,100}, {50,20}, {50,20}, {30,10}, {8,5},
{8,5}, {2,1}, {2,1}, {2,1}, {1,0}, {1,0}, {1,0}, {-1,0}};
//初始化码表
for (int i = 0; i < condition.length; i++) policyTables.put(condition[i], score[i]);
}
/**
* 获取一个SaveStep表 可以用来保存每一步下棋步骤,悔棋功能可以用到,也可以看出每一步顺序
* @return SaveStep表
*/
public static List<String> getSaveStep()
{
return new LinkedList<>();
}
/**
* 获取一个15*15的字符二维数组
* @return 返回一个Chessboard(用户需要)
*/
public static char[][] getChessboard()
{
char[][] chessboard =new char[CHESSBOARD_SIZE][CHESSBOARD_SIZE];
for (int i = 0; i < CHESSBOARD_SIZE; i++) Arrays.fill(chessboard[i], NO_CHESS);
return chessboard;
}
/**
* 判断胜负
* @param chessboard 当前棋盘
* @param x 棋子x坐标
* @param y 棋子y坐标
* @param index BLACK_CHESS或者WHITE_CHESS
* @return true表示胜利
*/
public static boolean isVictory(char[][] chessboard, int x, int y, char index)
{
String panDuan = (index == BLACK_CHESS ? BLACK_WIN : WHITE_WIN );
String heng = ""; //-
for (int i = 0; i < CHESSBOARD_SIZE; i++) heng += chessboard[i][y];
if (heng.indexOf(panDuan) != -1) return true;
String shu = ""; //|
for (int i = 0; i < CHESSBOARD_SIZE; i++) shu += chessboard[x][i];
if (shu.indexOf(panDuan) != -1) return true;
int tempX = x, tempY = y;
String zhuoXie = chessboard[x][y] + ""; // /
tempX = x; tempY = y; //向上遍历
while (tempX++ < CHESSBOARD_SIZE-1 && tempY-- > 0) zhuoXie = zhuoXie + chessboard[tempX][tempY];
tempX = x; tempY = y; //向下遍历
while (tempX-- > 0 && tempY++ < CHESSBOARD_SIZE-1) zhuoXie = chessboard[tempX][tempY] + zhuoXie;
if (zhuoXie.indexOf(panDuan) != -1) return true;
String youxie = chessboard[x][y] + ""; // '\'
tempX = x; tempY = y; //向上遍历
while (tempX-- > 0 && tempY-- > 0) youxie = chessboard[tempX][tempY] + youxie;
tempX = x; tempY = y; //向下遍历
while (tempX++ < CHESSBOARD_SIZE-1 && tempY++ < CHESSBOARD_SIZE-1) youxie = youxie + chessboard[tempX][tempY];
if (youxie.indexOf(panDuan) != -1) return true;
return false;
}
//返回的就是可以直接判断码表中情况的字符串
private static String getPolicyValue(String test, int x, char index)
{
char reversal = (index == BLACK_CHESS ? WHITE_CHESS : BLACK_CHESS);
String ret = index + "";
//如果是当前标志,就继续; 否则退出,并且如果是墙就是死,是其他就是死,是空格就是活
for (int i = x + 1; i < test.length(); i++)
{
if (test.charAt(i) == index) ret = ret + index;
if (test.charAt(i) == reversal) break;
if (test.charAt(i) == NO_CHESS) { ret = ret + 'X'; break;}
}
for (int i = x - 1; i >= 0; i--)
{
if (test.charAt(i) == index) ret = index + ret;
if (test.charAt(i) == reversal) break;
if (test.charAt(i) == NO_CHESS) { ret = 'X' + ret; break;}
}
ret = ret.replace(index, 'A');
if (ret.indexOf("AAAAA") != -1) return "AAAAA";
if (ret.indexOf("X") == -1) return "A";
return ret;
}
//chessboard是棋盘,x和y是棋子,index表明颜色,返回分数
private static int getScore(char[][] chessboard, int x, int y, char index)
{
char preventEnemy = (index == BLACK_CHESS ? WHITE_CHESS : BLACK_CHESS );
String heng = ""; //-
for (int i = 0; i < CHESSBOARD_SIZE; i++) heng += chessboard[i][y];
String heng1 = getPolicyValue(heng, x, index);
String heng2 = getPolicyValue(heng, x, preventEnemy);
String shu = ""; //|
for (int i = 0; i < CHESSBOARD_SIZE; i++) shu += chessboard[x][i];
String shu1 = getPolicyValue(shu, y, index);
String shu2 = getPolicyValue(shu, y, preventEnemy);
int tempX = x, tempY = y;
String zhuoXie = chessboard[x][y] + ""; // /
tempX = x; tempY = y; //向上遍历
while (tempX++ < CHESSBOARD_SIZE - 1 && tempY-- > 0) zhuoXie = zhuoXie + chessboard[tempX][tempY];
tempX = x; tempY = y; //向下遍历
int cnt = 0; //记录向下遍历了次数
while (tempX-- > 0 && tempY++ < CHESSBOARD_SIZE - 1)
{
zhuoXie = chessboard[tempX][tempY] + zhuoXie;
cnt++;
}
String zhuoXie1 = getPolicyValue(zhuoXie, cnt, index);
String zhuoXie2 = getPolicyValue(zhuoXie, cnt, preventEnemy);
String youxie = chessboard[x][y] + ""; // '\'
tempX = x; tempY = y; //向上遍历
cnt = 0;
while (tempX-- > 0 && tempY-- > 0)
{
youxie = chessboard[tempX][tempY] + youxie;
cnt++;
}
tempX = x; tempY = y; //向下遍历
while (tempX++ < CHESSBOARD_SIZE - 1 && tempY++ < CHESSBOARD_SIZE - 1) youxie = youxie + chessboard[tempX][tempY];
String youxie1= getPolicyValue(youxie, cnt, index);
String youxie2 =getPolicyValue(youxie, cnt, preventEnemy);
int score = -1;
String[] scoreList = {heng1, heng2, shu1, shu2, zhuoXie1, zhuoXie2, youxie1, youxie2};
for (String i : scoreList)
{
score += policyTables.get(i)[0];
score += policyTables.get(i)[1];
}
return score;
}
/**
* 判断人机最佳落子点
* @param chessboard 当前棋盘
* @return 一个int类型的二维数组,最优的位置
*/
public static int[] calc(char[][] chessboard)
{
int max = -10000;
int x = -1, y = -1;
for (int i = 0; i < CHESSBOARD_SIZE; i++)
{
for (int j = 0; j < CHESSBOARD_SIZE; j++)
{
if (chessboard[j][i] == NO_CHESS)
{
int score = getScore(chessboard, j, i, WHITE_CHESS);
if (max < score) { max = score; x = j; y = i;}
}
}
}
return new int[]{x, y};
}
/**
* 落子
* @param chessboard 当前棋盘
* @param saveStep 步骤保存表
* @param x x坐标
* @param y y坐标
* @param index BLACK_CHESS或者WHITE_CHESS
*/
public static void downChess(char[][] chessboard, List<String> saveStep, int x, int y, char index)
{
chessboard[x][y] = index;
saveStep.add(String.format("%02d %02d %c", x, y, index)); //"01 02 1"
}
/**
* 悔棋
* @param chessboard 棋盘
* @param saveStep 步骤保存表
*/
public static void back(char[][] chessboard, List<String> saveStep)
{
if (saveStep.size() >= 2)
{
for (int i = 0; i < 2; i++)
{
String lastSteps = saveStep.get(saveStep.size() - 1);
String[] str = lastSteps.split(" ");
chessboard[Integer.parseInt(str[0])][Integer.parseInt(str[1])] = NO_CHESS;
saveStep.remove(saveStep.size() - 1);
}
}
}
/**
* 存档功能
* @param chessboard 棋盘
* @param path 保存的路径
*/
public static void enCode(char[][] chessboard, String path)
{
List<String> saveChessboard = new ArrayList<>();
for (int i = 0; i < CHESSBOARD_SIZE; i++)
for (int j = 0; j < CHESSBOARD_SIZE; j++)
if (chessboard[i][j] != NO_CHESS)
saveChessboard.add(String.format("%02d %02d %c", i, j, chessboard[i][j]));
//序列化
try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(path)))
{
oos.writeObject(saveChessboard);
}
catch (Exception e)
{
e.printStackTrace();
}
}
/**
* 读档
* @param chessboard 棋盘
* @param path 读取的路径
* @return true表示成功读档
*/
public static boolean deCode(char[][] chessboard, String path)
{
//反序列化
try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream(path)))
{
List<String> ret = (List<String>)ois.readObject();
for (String i : ret)
{
String[] s = i.split(" ");
chessboard[Integer.parseInt(s[0])][Integer.parseInt(s[1])] = s[2].charAt(0);
}
return true;
}
catch (Exception e)
{
e.printStackTrace();
}
return false;
}
/**
* 当前位置有棋吗
* @param chessboard 棋盘
* @param x x坐标
* @param y y坐标
* @return true表示当前位置有棋
*/
public static boolean haveChess(char[][] chessboard, int x, int y)
{
return chessboard[x][y] != NO_CHESS;
}
/**
* 显示当前棋盘情况(GUI界面下可自己实现)
* @param chessboard 棋盘
*/
public static void showChessboard(char[][] chessboard)
{
for (int i = 0; i < CHESSBOARD_SIZE; i++)
{
for (int j = 0; j < CHESSBOARD_SIZE; j++)
{
String s = "";
if (chessboard[j][i] == BLACK_CHESS) s = "1";
else if (chessboard[j][i] == WHITE_CHESS) s = "2";
else s = "-";
System.out.printf("%-3s", s);
if (j == CHESSBOARD_SIZE - 1)
System.out.print(" "+ i);
}
System.out.println();
}
System.out.println();
for (int i = 0; i < CHESSBOARD_SIZE; i++) System.out.printf("%-3d", i);
System.out.println();
}
}
7 写一个测试类
把刚刚的代码加入到了项目中后,写一个简单命令行来爽一把,顺便把里面的方法都用用,我们可以根据这个例子加入图像化界面,这里可能会有一些小的bug,就当是留给大家的作业啦!
import java.util.List;
import java.util.Scanner;
public class GobangUtilTest
{
public static void main(String[] args)
{
System.out.println("欢迎来到人机五子棋测试\n" + "输入:\n" +
" 1 进入游戏\n" +
" 2 读档\n" +
" 3 退出游戏\n");
Scanner in = new Scanner(System.in);
char[][] chessboard = GobangUtil.getChessboard();
List<String> saveStep = GobangUtil.getSaveStep();
while (true)
{
switch (in.nextInt())
{
case 1: startGame(chessboard, saveStep); break;
case 2: deCode(chessboard, saveStep, "out.txt"); break;
case 3: System.exit(0); break;
default: System.out.println("请重新输入!");
}
}
}
//开始游戏
public static void startGame(char[][] chessboard, List<String> saveStep)
{
System.out.println("欢迎来到游戏!");
GobangUtil.showChessboard(chessboard);
try{
while (true)
{
System.out.println("back 悔棋; save 存档; exit 退出游戏");
Scanner s = new Scanner(System.in);
String[] ret = s.nextLine().split(" ");
//悔棋功能实现
if (ret[0].equals("back"))
{
GobangUtil.back(chessboard, saveStep);
GobangUtil.showChessboard(chessboard);
continue;
}
else if (ret[0].equals("save")) //存档功能实现
{
GobangUtil.enCode(chessboard, "out.txt");
GobangUtil.showChessboard(chessboard);
continue;
}
else if (ret[0].equals("exit")) //存档功能实现
{
System.exit(0);
}
int a = Integer.parseInt(ret[0]);
int b = Integer.parseInt(ret[1]);
if (GobangUtil.haveChess(chessboard, a, b))
{
System.out.println("不可重复落子!请重新输入!");
continue;
}
GobangUtil.downChess(chessboard, saveStep, a, b, '1');
if (GobangUtil.isVictory(chessboard, a, b, '1'))
{
System.out.println("你赢了");
GobangUtil.showChessboard(chessboard);
break;
}
int[] matchin = GobangUtil.calc(chessboard);
GobangUtil.downChess(chessboard, saveStep, matchin[0], matchin[1], '2');
System.out.println("人机当前落子位置:" + matchin[0] + "," + matchin[0]);
if (GobangUtil.isVictory(chessboard, matchin[0], matchin[1], '2'))
{
System.out.println("人机赢了!");
GobangUtil.showChessboard(chessboard);
break;
}
GobangUtil.showChessboard(chessboard);
}
}catch (Exception e){
System.out.println("输入导致程序发生异常啦!该背时!");
}
}
//读档(解码)
public static void deCode(char[][] chessboard, List<String> saveStep, String path)
{
if (GobangUtil.deCode(chessboard, path))
startGame(chessboard, saveStep);
else
{
System.out.println("读档失败");
return;
}
}
}
8 结语
这里做了一个抛砖引玉,有没有觉得字符串有时很神奇?大家也可以在评论区发表一下自己的看法哦😄!