基于博弈搜索算法的智能五子棋设计
0.引言
- 在智能过程中,搜索是必不可少的,是人工智能中的一个基本问题。这是因为人工智能研究的主要是那些没有成熟方法可依的问题领域,需要一步一步搜索求解。游戏中如何找到对自己有利的局面就属于这类问题。在游戏(人机博弈)程序中博弈树搜索算法是其核心的部分,它与估值及规则(走法)构成一个完整的系统。
- 与其他棋类相比, 五子棋每一层搜索节点数量庞大,规则简单,估值函数可以做到比较细致。本文以五子棋为例研究人机博弈,在传统极大极小搜索算法结合估值函数方法的基础上,提出一系列优化搜索的措施,设计完成一个智能程度较高的五子棋游戏。
1.五子棋算法介绍及初步实现
1.1 估值函数
- 不同的棋型,其优先级不同。例如,四个棋子连成一线且还能继续落子的棋型(活四)显然要比只有三个棋子连成一线(活三或死三)好。要使计算机正确地做出这种判断, 就要把第一种棋型的估值设高。事实上,对于每一种特定的棋型,都需要相应的估值来反映其优劣情况。另外,由于搜索模块频繁地调用估值函数,为了尽可能地加快搜索速度, 估值函数应设计的越仔细越好。估值时,需要从四个方向上来考虑所下棋子对当前盘面的影响。这四个方向分别是以该棋子为出发点, 水平、竖直和两条为45度角和135度角的线。本文中约定以“A”代表黑子,“B”代表白子,“?”代表棋盘上空位。算法中关于棋子死活的规定如下:一方落子后,它的落子连成的一条线有两条不损伤的出路, 则称该棋型是活的。否则称该棋型是死的。比如关于活三的定义:不论对手如何落子,仍然至少有一种方法可以冲四。因此, B?AAA?B中的三个A,不能算是活三;B?AAA??B中的三个A,也不是活三,尽管它有可能成为活四。这样,棋型的估值设计才能比较细致。本文算法对特定棋型的估值如图1所示。
1.2 极大极小搜索与α-β剪枝
- 在博弈问题中,每次博弈可供选择的行动方案都有很多,因此会生成十分庞大的博弈树。一般只生成一定深度的博弈树,然后进行极小极大搜索。极大极小搜索是指:在一棵博弈树中,当轮到甲走时,甲定会选择子节点值最大的走法;而轮到乙走时,乙则会选择子节点值最小的走法。使用估值函数对博弈树的每一个局面进行估值后,就可以通过极大极小搜索在博弈树中寻找最佳的合法走法。
- 在搜索的过程中,实际上有搜索很多点是多余的。此时我引入了α-β剪枝算法,一开始α和β是负正无穷,α表示到目前为止路径上发现的MAX的最佳(即极大值)选择,β表示到目前为止路径上发现的MIN的最佳(即极小值)选择。α-β搜索中不断更新α和β的值,并且当某个节点的值分别比目前的MAX的α或者MIN的β的值更差时,剪裁此节点剩下的分支(即终止递归调用)。
- 刚开始每一个节点上都标明了可能的取值范围,先从B下面第一个叶节点3开始,这时作为MIN节点的B值最多为3,如下图2所示。B下的第二个值为12,比3大,由于B想要最小值,则不考虑12,但却是会拿12与3作比较,所以12也会被算法计算到,此时MIN还是为3,8同理。此时已观察了B的所有后继,确定了最终的B的值为3。当C下面第一个后继为2,则C的MIN最多为2,此时B的值为3大于C的2,由于A要选MAX值,最少就是3不会再考虑C了,即C的另外两个后继不论是取何值,均不会再考虑了,此时我们就把C的另外两个后继裁剪掉,这就是α-β剪枝的实例。D下面第一个值为14,则D的值最多为14(比B的3要大,则继续向后探索)。D继续向后探索,第二个值为5比14小,则D最大为5(此时仍比B的3大,继续向后探索),D的最后一个后继是2,则D最后的值为2,小于B的3,则最终MAX在根节点的决策是走向值为3的B节点。还可以把这个过程看作对MINIMAX公式的简化。根节点的值计算如下:
MINIMAX(root)=max(min(3,12,8), min(2,x,y),min(14,5,2))
= max( 3, min(2, x, y), 2)
= max(3, z, 2) 其中z = min(2, x, y) <= 2
= 3。
1.3胜负判断
- 在棋局的胜负是根据最后一个落子的情况来判断的。此时需要查看四个方向, 即以该棋子为出发点的水平, 竖直和两条分别为45度角和135度角的线, 看在这四个方向上的其它棋子是否能和最后落子构成连续五个棋子, 如果能的话, 则表示这盘棋局已经分出胜负。
- 实际上, 我们可以提前若干步预判当前棋局的胜负情况。本文算法采用了如下的规则对胜负进行预判, 提高了算法的智能。在甲和乙对弈的棋局中, 某个时刻轮到甲下棋时几种可能获胜的情况:
- 甲已有任意组活四, 或者甲已有任意组死四 : 一步获胜;
- 甲已有任意组活三, 或者甲已有多于一组的死三 : 两步获胜;
- 甲已有一组死三和任意组的活二 : 三步获胜。
2.五子棋博弈算法的优化
- 到目前为止,我们使用传统的极大极小搜索结合估值函数的五子棋算法完成一个简单的五子棋对弈程序。虽然估值尽力做到细致、全面,但由于极大极小搜索存在博弈树算法中普遍存在的一个缺点 — 随着搜索层数的增加,算法的效率大大下降。所以搜索的效率还是不理想,五子棋程序的“智力”也不高。
- 因此,在上述基础上,我继续思考,通过对极大极小搜索算法的优化与修正,针对五子棋本身的特点和规律,提出采取以下优化措施,显著地提高了五子棋程序对弈的水平和能力。
2.1减小搜索范围
- 五子棋棋盘大小为15×15,传统算法中计算机每走一步都要遍历整个棋盘,对于棋面上所有空位都进行试探性下子并估值,这样大大影响算法的效率。其实在某个时刻,棋盘上很多的位置都是可以不用去考虑的。根据五子棋的特点,可以产生一个棋面搜索范围。记录当前棋面所有棋子的最左最右最上最下点构成的矩形,我们认为下一步棋的位置不会脱离这个框3步以上。这样在棋子较少的时候,搜索节点的数量大大减少。可以将AI的速度提高一倍左右。
2.2使用置换表
- 我们一般用递归的方法实现博弈树,但是,递归的效率是低的,而且很明显,有很多重复搜索的节点,所以,我们可以用一个表,记录下所有搜索过节点的情况,然后只要遇到搜索到的节点,就可以直接得到结果。这个表就是一个置换表,利用Zobrist算法,进行Hash处理,使在表中查找的时间大大缩短,这样AI的速度又能提高一个数量级。
2.3利用多线程,提高AI速度
- 在开发五子棋游戏中,我利用多个线程,让算法实现并行计算,提高计算的速度。我在第一层用一个线程分配器把第二层的候选节点分配给多个线程,每个线程包含着从第二层一个候选节点开始的搜索,然后等所有线程结束后,将所有线程的结果进行汇总,选出最大值。并行的程序,可以大大提升人机博弈的流畅度,大大改善用户体验。
3.总结与展望
- 从计算思维导论课中的井字棋到五子棋,博弈场景更加复杂,本文介绍了智能五子棋游戏的设计实现方法,提出优化五子棋算法、提高系统智能的措施。由于采用上述优化策略,本人实现的五子棋程序在对弈的水平和搜索效率方面均有显著的提高。从此次实践我深刻领悟到算法与数据结构的魅力,由衷感叹:智能并不是遥不可及的,它来源于生活,只是对我们人类行为的一种模拟!
- 但是由于本文所述算法的固定性(不具备自我学习的能力),所以一旦玩家一次获胜,按照相同的走法,必然会再次获胜。但除了必杀招或者必防招,一个局面很多时候没有绝对最好的走法。为了避免博弈算法重蹈覆辙,加入学习算法势在必行。今后,我认为可以借鉴阿尔法狗大战李世石的案例,利用强化学习技术开发一款具备自我学习的智能化五子棋程序。只有这样智能五子棋才能从弱智能走向强智能,只有这样AI五子棋才能更有效地模拟人类博弈。
项目代码如下
//Board类
package AIGobang;
import java.awt.*;
import java.util.Arrays;
import java.util.Stack;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* @author Jianxin.You
*/
public class Board {
// 棋型信息
public static enum Level {
CON_5("长连", 0, new String[] { "11111", "22222" }, 100000), ALIVE_4(
"活四", 1, new String[] { "011110", "022220" }, 10000), GO_4(
"冲四", 2, new String[] { "011112|0101110|0110110",
"022221|0202220|0220220" }, 500), DEAD_4("死四", 3,
new String[] { "211112", "122221" }, -5), ALIVE_3("活三", 4,
new String[] { "01110|010110", "02220|020220" }, 200), SLEEP_3(
"眠三", 5, new String[] {
"001112|010112|011012|10011|10101|2011102",
"002221|020221|022021|20022|20202|1022201" }, 50), DEAD_3(
"死三", 6, new String[] { "21112", "12221" }, -5), ALIVE_2("活二",
7, new String[] { "00110|01010|010010", "00220|02020|020020" },
5), SLEEP_2("眠二", 8, new String[] {
"000112|001012|010012|10001|2010102|2011002",
"000221|002021|020021|20002|1020201|1022001" }, 3), DEAD_2(
"死二", 9, new String[] { "2112", "1221" }, -5), NULL("null", 10,
new String[] { "", "" }, 0);
private String name;
private int index;
private String[] regex;// 正则表达式
int score;// 分值
// 构造方法
private Level(String name, int index, String[] regex, int score) {
this.name = name;
this.index = index;
this.regex = regex;
this.score = score;
}
// 覆盖方法
@Override
public String toString() {
return this.name;
}
};
// 方向
private static enum Direction {
HENG, SHU, PIE, NA
};
// 位置分
private static int[][] position = {
{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 },
{ 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0 },
{ 0, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 0 },
{ 0, 1, 2, 3, 3, 3, 3, 3, 3, 3, 3, 3, 2, 1, 0 },
{ 0, 1, 2, 3, 4, 4, 4, 4, 4, 4, 4, 3, 2, 1, 0 },
{ 0, 1, 2, 3, 4, 5, 5, 5, 5, 5, 4, 3, 2, 1, 0 },
{ 0, 1, 2, 3, 4, 5, 6, 6, 6, 5, 4, 3, 2, 1, 0 },
{ 0, 1, 2, 3, 4, 5, 6, 7, 6, 5, 4, 3, 2, 1, 0 },
{ 0, 1, 2, 3, 4, 5, 6, 6, 6, 5, 4, 3, 2, 1, 0 },
{ 0, 1, 2, 3, 4, 5, 5, 5, 5, 5, 4, 3, 2, 1, 0 },
{ 0, 1, 2, 3, 4, 4, 4, 4, 4, 4, 4, 3, 2, 1, 0 },
{ 0, 1, 2, 3, 3, 3, 3, 3, 3, 3, 3, 3, 2, 1, 0 },
{ 0, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 0 },
{ 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0 },
{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } };
public static final int BOARD_SIZE = 15;// 棋盘格数
public static final int BT = BOARD_SIZE + 2;
public static final int CENTER = BOARD_SIZE / 2 + 1;// 中心点
private int minx, maxx, miny, maxy; // 当前棋局下所有棋子的最小x,最大x,最小y,最大y,用于缩小搜索落子点的范围
private int currentPlayer = 0;// 当前玩家
private Stack<Point> history;// 落子历史记录
private Chess[][] data;// 1-15
private Chess[] sorted;// 根据各点的落子估值从大到小排序的数组
public Board() {
data = new Chess[BT][BT];
for (int i = 0; i < BT; i++)
for (int j = 0; j < BT; j++) {
data[i][j] = new Chess(i, j);
if (i == 0 || i == BT - 1 || j == 0 || j == BT - 1)
data[i][j].setSide(Chess.BORDER);// 边界
}
history = new Stack<Point>();
}
public Board(Board b) {// 深度拷贝
Chess[][] b_data = b.getData();
Chess[] b_sorted = b.getSorted();
data = new Chess[BT][BT];
for (int i = 0; i < BT; i++)
for (int j = 0; j < BT; j++) {
data[i][j] = new Chess(i, j);
Chess c = b_data[i][j];
data[i][j].sum = c.sum;
data[i][j].defence = c.defence;
data[i][j].offense = c.offense;
data[i][j].side = c.side;
}
sorted = new Chess[b_sorted.length];
for (int i = 0; i < sorted.length; i++) {
Chess c = b_sorted[i];
sorted[i] = new Chess(c.x, c.y);
sorted[i].sum = c.sum;
sorted[i].defence = c.defence;
sorted[i].offense = c.offense;
sorted[i].side = c.side;
}
currentPlayer = b.getPlayer();
minx = b.minx;
maxx = b.maxx;
miny = b.miny;
maxy = b.maxy;
history = new Stack<Point>();
}
public void start() {
currentPlayer = Chess.BLACK;// 默认黑子先行
putChess(CENTER, CENTER);// 默认第一步落在中心
minx = maxx = miny = maxy = CENTER;
}
public void reset() {
for (int i = 1; i < BT - 1; i++)
for (int j = 1; j < BT - 1; j++) {
data[i][j].reset();
}
history.clear();
}
public Point undo() {// 悔棋
if (!history.isEmpty()) {
Point p1 = history.pop();
Point p2 = history.pop();
data[p1.x][p1.y].setSide(Chess.EMPTY);
data[p2.x][p2.y].setSide(Chess.EMPTY);
return history.peek();
}
return null;
}
public Chess[][] getData() {
return data;
}
public Chess[] getSorted() {
return sorted;
}
public int getPlayer() {
return currentPlayer;
}
public int[][] getHistory() {
int length = history.size();
int[][] array = new int[length][2];
for (int i = 0; i < length; i++)
for (int j = 0; j < 2; j++) {
Point p = history.get(i);
array[i][0] = (int) p.getX();
array[i][1] = (int) p.getY();
}
return array;
}
/**
* 在点(x,y)落子
*/
public boolean putChess(int x, int y) {
if (data[x][y].isEmpty()) {
// 棋盘搜索范围限制
minx = Math.min(minx, x);
maxx = Math.max(maxx, x);
miny = Math.min(miny, y);
maxy = Math.max(maxy, y);
data[x][y].setSide(currentPlayer);
history.push(new Point(x, y));
trogglePlayer();
sorted = getSortedChess(currentPlayer);// 重要
System.out.printf(" 【" + (char) (64 + x) + (16 - y) + "】");
return true;
}
return false;
}
private void trogglePlayer() {
currentPlayer = 3 - currentPlayer;
};
private int check(int x, int y, int dx, int dy, int chess) {
int sum = 0;
for (int i = 0; i < 4; ++i) {
x += dx;
y += dy;
if (x < 1 || x > BOARD_SIZE || y < 1 || y > BOARD_SIZE) {
break;
}
if (data[x][y].getSide() == chess) {
sum++;
} else {
break;
}
}
return sum;
}
public int isGameOver() {
if (!history.isEmpty()) {
int chess = (history.size() % 2 == 1) ? Chess.BLACK : Chess.WHITE;
Point lastStep = history.peek();
int x = (int) lastStep.getX();
int y = (int) lastStep.getY();
if (check(x, y, 1, 0, chess) + check(x, y, -1, 0, chess) >= 4) {
return chess;
}
if (check(x, y, 0, 1, chess) + check(x, y, 0, -1, chess) >= 4) {
return chess;
}
if (check(x, y, 1, 1, chess) + check(x, y, -1, -1, chess) >= 4) {
return chess;
}
if (check(x, y, 1, -1, chess) + check(x, y, -1, 1, chess) >= 4) {
return chess;
}
}
// 进行中
for (int i = 0; i < BOARD_SIZE; ++i) {
for (int j = 0; j < BOARD_SIZE; ++j)
if (data[i][j].isEmpty()) {
return 0;
}
}
// 平局
return 3;
}
/**
* 玩家(player)轮,根据各点的落子估值从大到小排序
*
*/
public Chess[] getSortedChess(int player) {
// 限制范围
int px = Math.max(minx - 5, 1);
int py = Math.max(miny - 5, 1);
int qx = Math.min(maxx + 5, Board.BT - 1);
int qy = Math.min(maxy + 5, Board.BT - 1);
Chess[] temp = new Chess[(qx - px + 1) * (qy - py + 1)];
int count = 0;
for (int x = px; x <= qx; x++) {
for (int y = py; y <= qy; y++) {
temp[count] = new Chess(x, y);
if (data[x][y].isEmpty()) {
data[x][y].clearDetail();
data[x][y].append("================================\n");
int o = getScore(x, y, player) + 1;// 攻击分,优先
data[x][y].append("\n");
int d = getScore(x, y, 3 - player);// 防守分
data[x][y].append("\n");
String cs = "【" + (char) (64 + x) + (16 - y) + "】 ";
data[x][y].append(cs).append(" 攻击:" + o).append(" 防守:" + d)
.append("\n\n");
data[x][y].offense = temp[count].offense = o;
data[x][y].defence = temp[count].defence = d;
data[x][y].sum = temp[count].sum = o + d;// 综合分
}
count++;
}
}
Arrays.sort(temp);
return temp;
}
/**
* 在点(x,y)落下棋子(chess)后的落子估值
*
*/
public int getScore(int x, int y, int chess) {
data[x][y].append("-");
Level l1 = getLevel(x, y, Direction.HENG, chess);
data[x][y].append("|");
Level l2 = getLevel(x, y, Direction.SHU, chess);
data[x][y].append("/");
Level l3 = getLevel(x, y, Direction.PIE, chess);
data[x][y].append("\\");
Level l4 = getLevel(x, y, Direction.NA, chess);
return level2Score(l1, l2, l3, l4) + position[x - 1][y - 1];
}
/**
* 在点(x,y)落下棋子(chess)后, 方向(direction)形成的棋型
*
*/
public Level getLevel(int x, int y, Direction direction, int chess) {
String seq, left = "", right = "";
if (direction == Direction.HENG) {
left = getHalfSeq(x, y, -1, 0, chess);
right = getHalfSeq(x, y, 1, 0, chess);
} else if (direction == Direction.SHU) {
left = getHalfSeq(x, y, 0, -1, chess);
right = getHalfSeq(x, y, 0, 1, chess);
} else if (direction == Direction.PIE) {
left = getHalfSeq(x, y, -1, 1, chess);
right = getHalfSeq(x, y, 1, -1, chess);
} else if (direction == Direction.NA) {
left = getHalfSeq(x, y, -1, -1, chess);
right = getHalfSeq(x, y, 1, 1, chess);
}
seq = left + chess + right;
String rseq = new StringBuilder(seq).reverse().toString();
data[x][y].append("\t" + seq + "\t");
// seq2Level
for (Level level : Level.values()) {
Pattern pat = Pattern.compile(level.regex[chess - 1]);
Matcher mat = pat.matcher(seq);
boolean rs1 = mat.find();
mat = pat.matcher(rseq);
boolean rs2 = mat.find();
if (rs1 || rs2) {
data[x][y].append(level.name).append("\n");
return level;
}
}
return Level.NULL;
}
private String getHalfSeq(int x, int y, int dx, int dy, int chess) {
String sum = "";
boolean isR = false;
if (dx < 0 || (dx == 0 && dy == -1))
isR = true;
for (int i = 0; i < 5; ++i) {
x += dx;
y += dy;
if (x < 1 || x > BOARD_SIZE || y < 1 || y > BOARD_SIZE) {
break;
}
if (isR) {
sum = data[x][y].getSide() + sum;
} else
sum = sum + data[x][y].getSide();
}
return sum;
}
/**
* 将各方向的棋型统计成初步的打分
*/
public int level2Score(Level l1, Level l2, Level l3, Level l4) {
int size = Level.values().length;
int[] levelCount = new int[size];
for (int i = 0; i < size; i++) {
levelCount[i] = 0;
}
levelCount[l1.index]++;
levelCount[l2.index]++;
levelCount[l3.index]++;
levelCount[l4.index]++;
int score = 0;
if (levelCount[Level.GO_4.index] >= 2
|| levelCount[Level.GO_4.index] >= 1
&& levelCount[Level.ALIVE_3.index] >= 1)// 双活4,冲4活三
score = 10000;
else if (levelCount[Level.ALIVE_3.index] >= 2)// 双活3
score = 5000;
else if (levelCount[Level.SLEEP_3.index] >= 1
&& levelCount[Level.ALIVE_3.index] >= 1)// 活3眠3
score = 1000;
else if (levelCount[Level.ALIVE_2.index] >= 2)// 双活2
score = 100;
else if (levelCount[Level.SLEEP_2.index] >= 1
&& levelCount[Level.ALIVE_2.index] >= 1)// 活2眠2
score = 10;
score = Math.max(
score,
Math.max(Math.max(l1.score, l2.score),
Math.max(l3.score, l4.score)));
return score;
}
}
//Brain类
package AIGobang;
/**
* @author Jianxin.You
*/
public class Brain {
private Board bd;
private int INFINITY = 1000000;
private int movex, movey;
private int level;// 深度
private int node;// 每层结点
public Brain(Board bd, int level, int node) {
this.bd = bd;
this.level = level;
this.node = node;
}
// 估值函数
public int[] findOneBestStep() {
Chess[] arr = bd.getSortedChess(bd.getPlayer());
Chess c = bd.getData()[arr[0].x][arr[0].y];
int[] result = { c.x, c.y };
return result;
}
// 估值函数+搜索树
public int[] findTreeBestStep() {
alpha_beta(0, bd, -INFINITY, INFINITY);
int[] result = { movex, movey };
return result;
}
// alpha-beta剪枝搜索算法
public int alpha_beta(int depth, Board board, int alpha, int beta) {
if (depth == level || board.isGameOver() != 0) {
Chess[] sorted = board.getSorted();
Chess move = board.getData()[sorted[0].x][sorted[0].y];
// 搜索树辅助输出
System.out.println("\t- " + "【" + (char) (64 + move.x)
+ (16 - move.y) + "】," + move.getSum());
return move.getSum();// 局面估分
}
// 对局面下得分最高的几个点进行拓展
Board temp = new Board(board);
Chess[] sorted = temp.getSorted();
int score;
for (int i = 0; i < node; i++) {
int x = sorted[i].x;
int y = sorted[i].y;
// 搜索树辅助输出
if (depth >= 1) {
System.out.println();
for (int k = 0; k < depth; k++)
System.out.printf("\t");
}
// 走这个走法
if (!temp.putChess(x, y))
continue;
if (sorted[i].getOffense() >= Board.Level.ALIVE_4.score) {
System.out.println("我们快要赢啦!");
score = INFINITY + 1;
} else if (sorted[i].getDefence() >= Board.Level.ALIVE_4.score) {
System.out.println("对方快要赢啦!");
score = INFINITY;
} else {
score = alpha_beta(depth + 1, temp, alpha, beta);
}
temp = new Board(board);// 撤消这个走法
if (depth % 2 == 0) {// MAX
if (score > alpha) {
alpha = score;
if (depth == 0) {
movex = x;
movey = y;
}
}
if (alpha >= beta) {
score = alpha;
// System.out.println(" beta剪枝");
return score;
}
} else {// MIN
if (score < beta) {
beta = score;
}
if (alpha >= beta) {
score = beta;
// System.out.println(" alpha剪枝");
return score;
}
}
}
return depth % 2 == 0 ? alpha : beta;
}
}
package AIGobang;
//Chess类
/**
* @author Jianxin.You
*/
public class Chess implements Comparable<Chess> {
public static final int BLACK = 1;
public static final int WHITE = 2;
public static final int BORDER = -1;
public static final int EMPTY = 0;
protected int x;
protected int y;
protected int offense;//攻击分
protected int defence;//防守分
protected int sum;//综合分
protected int side;//落子
private StringBuilder detail;//该点的落子估值
public Chess(int x, int y) {
this.x = x;
this.y = y;
detail = new StringBuilder();
}
public int getOffense() {
return offense;
}
public void setOffense(int offense) {
this.offense = offense;
}
public int getDefence() {
return defence;
}
public void setDefence(int defence) {
this.defence = defence;
}
public int getSum() {
return sum;
}
public void setSum(int sum) {
this.sum = sum;
}
public int getSide() {
return side;
}
public void setSide(int side) {
this.side = side;
}
public String getDetail() {
return detail.toString();
}
public StringBuilder append(String more) {
return this.detail.append(more);
}
//清空
public void reset() {
clearDetail();
offense = defence = sum = 0;
side = EMPTY;
}
public void clearDetail() {
detail = new StringBuilder();
}
public String toString() {
return String.format(x + "," + y + "-(" + (char) (64 + x) + ","
+ (16 - y) + ") " + offense + "," + defence + "," + sum);
}
public boolean isEmpty() {
return side == EMPTY ? true : false;
}
//重写比较,从大到小
@Override
public int compareTo(Chess o) {
if (o == null)
return 0;
int val1 = sum;
int val2 = o.getSum();
if (val1 == val2)
return 0;
else if (val1 < val2)
return 1;
else
return -1;
}
}
//GobangFrame类
package AIGobang;
import javax.swing.*;
import javax.swing.border.TitledBorder;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
/**
* @author Jianxin.You
*/
public class GobangFrame extends JFrame {
private static final long serialVersionUID = -7844061449912554572L;
JRadioButton manualBtn = new JRadioButton("双人");
JRadioButton halfAutoBtn = new JRadioButton("人机", true);
JRadioButton autoBtn = new JRadioButton("双机");
JCheckBox orderBtn = new JCheckBox("显示落子顺序");
JRadioButton oneBtn = new JRadioButton("估值函数");
JRadioButton treeBtn = new JRadioButton("估值函数+搜索树", true);
JComboBox<Integer> levelCombo = new JComboBox<Integer>(new Integer[] { 1,
2, 3 });
JComboBox<Integer> nodeCombo = new JComboBox<Integer>(new Integer[] { 3, 5,
10 });
JButton btn = new JButton("新游戏");
JButton undoBtn = new JButton("悔棋");
TextArea area = new TextArea();
GobangPanel panel = new GobangPanel(area);// 棋盘面板
public GobangFrame() {
super("智能五子棋");
add(panel, BorderLayout.WEST);
ButtonGroup grp_mode = new ButtonGroup();
grp_mode.add(manualBtn);
grp_mode.add(halfAutoBtn);
grp_mode.add(autoBtn);
ButtonGroup grp_alg = new ButtonGroup();
grp_alg.add(oneBtn);
grp_alg.add(treeBtn);
JPanel rightPanel = new JPanel();
area.setEditable(false);
rightPanel.setLayout(new BoxLayout(rightPanel, BoxLayout.Y_AXIS));
JPanel panel1 = new JPanel(new BorderLayout());
panel1.setBorder(new TitledBorder("在棋盘上单击鼠标右键,查看各点估值"));
panel1.add(area);
rightPanel.add(panel1);
JPanel optPanel = new JPanel();
optPanel.setLayout(new BoxLayout(optPanel, BoxLayout.Y_AXIS));
optPanel.setBorder(new TitledBorder("游戏设置"));
JPanel panel2 = new JPanel();
panel2.setBorder(new TitledBorder("模式"));
panel2.add(manualBtn);
panel2.add(halfAutoBtn);
panel2.add(autoBtn);
optPanel.add(panel2);
JPanel panel3 = new JPanel();
panel3.setBorder(new TitledBorder("智能"));
panel3.add(oneBtn);
panel3.add(treeBtn);
optPanel.add(panel3);
JPanel panel4 = new JPanel();
panel4.setBorder(new TitledBorder("搜索树"));
panel4.add(new JLabel("搜索深度"));
panel4.add(levelCombo);
panel4.add(new JLabel("每层节点"));
panel4.add(nodeCombo);
optPanel.add(panel4);
optPanel.add(btn);
rightPanel.add(optPanel);
JPanel panel5 = new JPanel();
panel5.setBorder(new TitledBorder("其他"));
panel5.add(orderBtn);
panel5.add(undoBtn);
rightPanel.add(panel5);
add(rightPanel);
btn.addActionListener(l);
orderBtn.addActionListener(l);
undoBtn.addActionListener(l);
setSize(900, 700);
setResizable(false);
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
setVisible(true);
}
private ActionListener l = new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
Object source = e.getSource();
if (source == btn) {
int mode = -1, intel = -1, level, node;
if (manualBtn.isSelected())
mode = GobangPanel.MANUAL;
else if (halfAutoBtn.isSelected())
mode = GobangPanel.HALF;
else if (autoBtn.isSelected())
mode = GobangPanel.AUTO;
if (oneBtn.isSelected())
intel = GobangPanel.EVAL;
else if (treeBtn.isSelected())
intel = GobangPanel.TREE;
level = (Integer) levelCombo.getSelectedItem();
node = (Integer) nodeCombo.getSelectedItem();
panel.startGame(mode, intel, level, node);
} else if (source == orderBtn) {
panel.troggleOrder();
} else if (source == undoBtn) {
panel.undo();
}
}
};
}
//GobangPanel类
package AIGobang;
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import java.util.Timer;
import java.util.TimerTask;
/**
* @author Jianxin.You
*/
public class GobangPanel extends JPanel {
private static final long serialVersionUID = 667503661521167626L;
private final int OFFSET = 40;// 棋盘偏移
private final int CELL_WIDTH = 40;// 棋格宽度
private int computerSide = Chess.BLACK;// 默认机器持黑
private int humanSide = Chess.WHITE;
private int cx = Board.CENTER, cy = Board.CENTER;
private boolean isShowOrder = false;// 显示落子顺序
private int[] lastStep;// 上一个落子点
private Board bd;// 棋盘,重要
private Brain br;// AI,重要
public static final int MANUAL = 0;// 双人模式
public static final int HALF = 1;// 人机模式
public static final int AUTO = 2;// 双机模式
public static final int EVAL = 3;// 估值函数
public static final int TREE = 4;// 估值函数+搜索树
private int mode;// 模式
private int intel;// 智能
private boolean isGameOver = true;
private TextArea area;
// 显示落子顺序
public void troggleOrder() {
isShowOrder = !isShowOrder;
repaint();
}
// 悔棋
public void undo() {
Point p = bd.undo();
lastStep[0] = p.x;
lastStep[1] = p.y;
repaint();
}
public GobangPanel(TextArea area) {
this.area = area;
lastStep = new int[2];
addMouseMotionListener(mouseMotionListener);
addMouseListener(mouseListener);
this.setBackground(Color.ORANGE);
setPreferredSize(new Dimension(650, 700));
bd = new Board();
}
public void paintComponent(Graphics g) {
Graphics2D g2d = (Graphics2D) g;
super.paintComponent(g2d);
g2d.setStroke(new BasicStroke(2));
g2d.setFont(new Font("April", Font.BOLD, 12));
// 画棋盘
drawBoard(g2d);
// 画天元和星
drawStar(g2d, Board.CENTER, Board.CENTER);
drawStar(g2d, (Board.BOARD_SIZE + 1) / 4, (Board.BOARD_SIZE + 1) / 4);
drawStar(g2d, (Board.BOARD_SIZE + 1) / 4,
(Board.BOARD_SIZE + 1) * 3 / 4);
drawStar(g2d, (Board.BOARD_SIZE + 1) * 3 / 4,
(Board.BOARD_SIZE + 1) / 4);
drawStar(g2d, (Board.BOARD_SIZE + 1) * 3 / 4,
(Board.BOARD_SIZE + 1) * 3 / 4);
// 画数字和字母
drawNumAndLetter(g2d);
// 画提示框
drawCell(g2d, cx, cy, 0);
if (!isGameOver) {
// 画所有棋子
for (int x = 1; x <= Board.BOARD_SIZE; ++x) {
for (int y = 1; y <= Board.BOARD_SIZE; ++y) {
drawChess(g2d, x, y, bd.getData()[x][y].getSide());
}
}
// 画顺序
if (isShowOrder)
drawOrder(g2d);
else {
if (lastStep[0] > 0 && lastStep[1] > 0) {
g2d.setColor(Color.RED);
g2d.fillRect((lastStep[0] - 1) * CELL_WIDTH + OFFSET
- CELL_WIDTH / 10, (lastStep[1] - 1) * CELL_WIDTH
+ OFFSET - CELL_WIDTH / 10, CELL_WIDTH / 5,
CELL_WIDTH / 5);
}
}
}
}
// 画棋盘
private void drawBoard(Graphics g2d) {
for (int x = 0; x < Board.BOARD_SIZE; ++x) {
g2d.drawLine(x * CELL_WIDTH + OFFSET, OFFSET, x * CELL_WIDTH
+ OFFSET, (Board.BOARD_SIZE - 1) * CELL_WIDTH + OFFSET);
}
for (int y = 0; y < Board.BOARD_SIZE; ++y) {
g2d.drawLine(OFFSET, y * CELL_WIDTH + OFFSET,
(Board.BOARD_SIZE - 1) * CELL_WIDTH + OFFSET, y
* CELL_WIDTH + OFFSET);
}
}
// 画天元和星
private void drawStar(Graphics g2d, int cx, int cy) {
g2d.fillOval((cx - 1) * CELL_WIDTH + OFFSET - 4, (cy - 1) * CELL_WIDTH
+ OFFSET - 4, 8, 8);
}
// 画数字和字母
private void drawNumAndLetter(Graphics g2d) {
FontMetrics fm = g2d.getFontMetrics();
int stringWidth, stringAscent;
stringAscent = fm.getAscent();
for (int i = 1; i <= Board.BOARD_SIZE; i++) {
String num = String.valueOf(Board.BOARD_SIZE - i + 1);
stringWidth = fm.stringWidth(num);
g2d.drawString(String.valueOf(Board.BOARD_SIZE - i + 1), OFFSET / 4
- stringWidth / 2, OFFSET + (CELL_WIDTH * (i - 1))
+ stringAscent / 2);
String letter = String.valueOf((char) (64 + i));
stringWidth = fm.stringWidth(letter);
g2d.drawString(String.valueOf((char) (64 + i)), OFFSET
+ (CELL_WIDTH * (i - 1)) - stringWidth / 2, OFFSET * 3 / 4
+ OFFSET + CELL_WIDTH * (Board.BOARD_SIZE - 1)
+ stringAscent / 2);
}
}
// 画棋子
private void drawChess(Graphics g2d, int cx, int cy, int player) {
if (player == 0)
return;
int size = CELL_WIDTH * 5 / 6;
g2d.setColor(player == Chess.BLACK ? Color.BLACK : Color.WHITE);
g2d.fillOval((cx - 1) * CELL_WIDTH + OFFSET - size / 2, (cy - 1)
* CELL_WIDTH - size / 2 + OFFSET, size, size);
}
// 画预选框
private void drawCell(Graphics g2d, int x, int y, int c) {// c 是style
int length = CELL_WIDTH / 4;
int xx = (x - 1) * CELL_WIDTH + OFFSET;
int yy = (y - 1) * CELL_WIDTH + OFFSET;
int x1, y1, x2, y2, x3, y3, x4, y4;
x1 = x4 = xx - CELL_WIDTH / 2;
x2 = x3 = xx + CELL_WIDTH / 2;
y1 = y2 = yy - CELL_WIDTH / 2;
y3 = y4 = yy + CELL_WIDTH / 2;
g2d.setColor(Color.RED);
g2d.drawLine(x1, y1, x1 + length, y1);
g2d.drawLine(x1, y1, x1, y1 + length);
g2d.drawLine(x2, y2, x2 - length, y2);
g2d.drawLine(x2, y2, x2, y2 + length);
g2d.drawLine(x3, y3, x3 - length, y3);
g2d.drawLine(x3, y3, x3, y3 - length);
g2d.drawLine(x4, y4, x4 + length, y4);
g2d.drawLine(x4, y4, x4, y4 - length);
}
// 画落子顺序
private void drawOrder(Graphics g2d) {
int[][] history = bd.getHistory();
if (history.length > 0) {
g2d.setColor(Color.RED);
for (int i = 0; i < history.length; i++) {
int x = history[i][0];
int y = history[i][1];
String text = String.valueOf(i + 1);
// 居中
FontMetrics fm = g2d.getFontMetrics();
int stringWidth = fm.stringWidth(text);
int stringAscent = fm.getAscent();
g2d.drawString(text, (x - 1) * CELL_WIDTH + OFFSET
- stringWidth / 2, (y - 1) * CELL_WIDTH + OFFSET
+ stringAscent / 2);
}
}
}
// 开始游戏
public void startGame(int mode, int intel, int level, int node) {
if (isGameOver) {
this.mode = mode;
this.intel = intel;
bd.reset();
area.setText("");
lastStep[0] = lastStep[1] = Board.CENTER;
br = new Brain(bd, level, node);
bd.start();
isGameOver = false;
JOptionPane.showMessageDialog(GobangPanel.this, "游戏开始!");
repaint();
if (mode == AUTO) {// 双机
Timer t = new Timer(true);
t.schedule(new ComputurTask(), 0, 500);
}
} else {
JOptionPane.showMessageDialog(GobangPanel.this,
"游戏进行中...这局完了再重新开始吧!");
}
}
// 鼠标移动
private MouseMotionListener mouseMotionListener = new MouseMotionAdapter() {
public void mouseMoved(MouseEvent e) {
int tx = Math.round((e.getX() - OFFSET) * 1.0f / CELL_WIDTH) + 1;
int ty = Math.round((e.getY() - OFFSET) * 1.0f / CELL_WIDTH) + 1;
if (tx != cx || ty != cy) {
if (tx >= 1 && tx <= Board.BOARD_SIZE && ty >= 1
&& ty <= Board.BOARD_SIZE) {
setCursor(new Cursor(Cursor.HAND_CURSOR));
repaint();
} else
setCursor(new Cursor(Cursor.DEFAULT_CURSOR));
cx = tx;
cy = ty;
}
}
};
// 鼠标点击
private MouseListener mouseListener = new MouseAdapter() {
public void mouseClicked(MouseEvent e) {
if (isGameOver) {
JOptionPane.showMessageDialog(GobangPanel.this, "请开始新游戏!");
return;
}
int x = Math.round((e.getX() - OFFSET) * 1.0f / CELL_WIDTH) + 1;
int y = Math.round((e.getY() - OFFSET) * 1.0f / CELL_WIDTH) + 1;
if (cx >= 1 && cx <= Board.BOARD_SIZE && cy >= 1
&& cy <= Board.BOARD_SIZE) {
if (mode == MANUAL) {// 双人
int mods = e.getModifiers();
if ((mods & InputEvent.BUTTON3_MASK) != 0) {// 鼠标右键
area.append(bd.getData()[x][y].getDetail());
} else if ((mods & InputEvent.BUTTON1_MASK) != 0)// 鼠标左键
putChess(x, y);
} else if (mode == HALF) {// 人机
if (bd.getPlayer() == humanSide) {
int mods = e.getModifiers();
if ((mods & InputEvent.BUTTON3_MASK) != 0) {// 鼠标右键
area.append(bd.getData()[x][y].getDetail());
} else if ((mods & InputEvent.BUTTON1_MASK) != 0) {// 鼠标左键
if (putChess(x, y)) {
System.out.println("\n----白棋完毕----");
if (intel == EVAL) {
int[] bestStep = br.findOneBestStep();// 估值函数AI
putChess(bestStep[0], bestStep[1]);
} else if (intel == TREE) {
int[] bestStep = br.findTreeBestStep();// 估值函数+搜索树AI
putChess(bestStep[0], bestStep[1]);
}
System.out.println("\n----黑棋完毕----");
}
}
}
}
}
}
};
private boolean putChess(int x, int y) {
if (bd.putChess(x, y)) {
lastStep[0] = x;// 保存上一步落子点
lastStep[1] = y;
repaint();
int winSide = bd.isGameOver();// 判断终局
if (winSide > 0) {
if (winSide == humanSide) {
JOptionPane.showMessageDialog(GobangPanel.this, "白方赢了!");
} else if (winSide == computerSide) {
JOptionPane.showMessageDialog(GobangPanel.this, "黑方赢了!");
} else {
JOptionPane.showMessageDialog(GobangPanel.this, "双方平手");
}
// 清除
bd.reset();
area.setText("");
isGameOver = true;
repaint();
return false;
}
return true;
}
return false;
}
// 双机
private class ComputurTask extends TimerTask {
@Override
public void run() {
int[] bestStep = br.findTreeBestStep();
if (!putChess(bestStep[0], bestStep[1]))
this.cancel();
}
}
}
//GobangTest类
package AIGobang;
public class GobangTest {
public static void main(String args[]) {
GobangFrame frame = new GobangFrame();
}
}