博弈算法实现简单五子棋

一、  问题介绍

实现交互式五子棋

采用博弈算法

二、  程序设计与算法分析

l   博弈问题简介:

– 双人对弈,轮流走步。

– 信息完备,双方所得到的信息是一样的。

– 零和,即对一方有利的棋,对另一方肯定是不利的,不存在对双方均有利或无利的棋。

l   博弈的特性:

– 两个棋手交替地走棋 ;

– 比赛的最终结果,是赢、输和平局中的一种;

– 可用图搜索技术进行,但效率很低;


– 博弈的过程,是寻找置对手于必败态的过程;

– 双方都无法干预对方的选择。

l   结论:五子棋问题属于博弈问题。

l   博弈问题的两种常用算法思路:

– 与或图直接搜索法:搜索枚举双方棋手的下棋方法,构建与或图直接搜索出答案 ;

u  与或图简介:

与或图是一个超图,节点间通过连接符连接。

u  与或图节点分类:

n  能解节点:

Ø  终节点是能解节点 


Ø  若非终节点有“或”子节点时,当且仅当其子节点至少有一能解时,该非终结点才能解。

Ø  若非终节点有“与”子节点时,当且仅当其子节点均能解时, 该非终节点才能解。

n  不能解节点

Ø  没有后裔的非终节点是不能解节点。


Ø  若非终节点有“或”子节点,当且仅当所有子节点均不能解时, 
该非终节点才不能解。 


Ø  若非终节点有“与”子节点时,当至少有一个子节点不能解时, 该非终节点才不能解。

u  与或图搜索算法:

n  1.建立搜索图G:=s,计算q(s)=h(s), If Goal(s)Then M(s, Solved) 


n  2.Until S 被标记为Solved, Do: 


n  3.Begin ;扩展 


n  4.G’:= Find(g) ; G’为根据连接符找到的待扩展局部解图 


n  5.n := G’中任一非终结点 


n  6.{nj}:=Expand(n), 计算q(nj)=h(nj),If Goal(nj) Then M(nj,solved) 


n  7.S:={n} ;回溯,修改指针和耗散值 


n  8.Until S为空, Do: 


n  9.Begin 


n  10.Remove(m, S) If m的子结点不在s中 ;子结点耗散值已确定 3/21/2014 第二章 与或图搜索

n  11.修改m的耗散值:
对m的每个连接符i{n1i,n2i,...,nki},计算qi(m)=Ci+q(n1i)+...+q(nki)q(m):=min qi (m)
修改m的指针到min qi (m)对应的连接符上
If(nji,Solved) THEN M(m,Solved) ;某一个连接符已解决

n  12.If M(m,Solved) or q(m)被修改 Then Add(ma,S), ma为m的所有先辈节点

n  13.End

n  14.End

u  两个过程:

n  图生成过程,即扩展节点,从最优的局部途中选择一个节点扩展

n  计算耗散值的过程
– 对当前的局部图从新计算耗散值

u  算法实现:

n  终结点为某一方胜利的状态:可用true或者false分别标记胜负

n  利用与或图搜索更新到根节点

n  若根节点为false表示当前棋局状态为必败情况。

n  若根节点为true表示当前棋局为必胜状态。

n  根节点总是尽量选择状态为true的子节点进行状态转移。

u  算法局限性:

n  搜索的没一层都要穷举所有可能的情况,很容易引起组合爆炸的问题。

 

– 极大极小搜索算法:利用优先深度的搜索,通过对局势进行评估来选择状态转移的方向。

u  极大极小搜索简介:

n  下棋的双方是对立的;

n  一方为“正方”,这类节点称为“MAX”节点; 


n  另一方为“反方”,这类节点称为“MIN”节点; 


n  正方从所有子节点中,选取具有最大评估值的节点进行状态转移; 


n  反方从其所有子节点中,选取具有最小评估值的节点惊醒状态转移; 


n  反复进行这种选取,就可以得到双方各个节点的评估值。这种确定棋步的方法,称为极大极小搜索法。 


u  对各个局面进行评估:

n  评估的目的:对后面的状态提前进行考虑,并且以各种状态的评估值为基础作出最好的走棋选择。       


n  评估的方法:用评价函数对棋局进行评估。赢的评估值设为+∞,输的评估值设为-∞,平局的评估值设为0。 


n  评估的标准:由于下棋的双方是对立的,只能选择其中一 方为评估的标准方。 
   所有评估站在正方的立场!

u  极大极小搜索算法:

n  1.T:=(s,max), Open:=(s),Close:=();


n  2.Lopp1:                ;扩展深度至d

n  3.If Open=() Then Goto Loop2


n  4.N:=First(Open),Remove(n,Open), Add(n,Close)


n  5.If F(n)=-∞, +∞, 0, Then GotoLoop1 ;                               可以被判定

Else {ni}:=Expand(n), Add({ni},T)


If d(ni)

Else 计算f(ni), Goto Loop1

n  6.Loop2:                                  ;赋值

n  7.If Close=() Then Goto Loop3

Else np:=First(Close)

n  8.If (np Is Max) And (F(npi)有值)                      ; npi是np的子结点,且都有值

Then f(np):=maxf(npi), Remove(np,Close)

If (np Is MIN)And (f(npi)有值)                           ; npi是np的子结点,且都有值

THEN f(np):=minf(npi), Remove(np,Close)


n  9.Goto Loop2

n  10.Loop3:


n  10.If f(s)有值 Then Exit(End or M(Move, T))                   ;s被赋值,结束该步

u  alpha-beta剪枝:在极小极大法中,必须求出所有终端节点的评估值,当预先考虑的棋步比较多时,计算量会大大增加。为了提高搜索的效率,引入了通过对评估值的上下限进行估计、从而减少需进行评估的节点范围。

n  MAX节点的评估下限值:

作为正方出现的MAX节点,假设它的MIN子节点有N个,那么当它的第一

个MIN子节点的评估值为alpha时,则对于其它的子节点,如果有高过alpha的, 就取那最高的值作为该MAX节点的评估值;如果没有,则该MAX节点的评估值为alpha。总之,该MAX节点的评估值不会低于alpha,这个alpha就称为该MAX节 点的评估下限值。

n  MIN节点的评估上限值:

作为反方出现的MIN节点,假设它的MAX子节点有N个,那么当它的第一 个MAX子节点的评估值为beta时,则对于其它子节点,如果有低于beta的,就取那个低于beta的值作为该MIN节点的评估值;如果没有,则该MIN节点的评估值取beta总之,该MIN节点的评估值不会高过beta这个beta就称为该MIN节点的评估上限值。

n  剪枝条件:

Ø  后辈节点的beta值≤祖先节点的alpha值时,alpha剪枝。

Ø  – 后辈节点的alpha值≥祖先节点的beta值时,beta剪枝。

 

l   五子棋算法分析:

u  因为每一步可走棋的位置很多,所以不适合直接使用与或图搜索,在这里使用了极大极小搜索,并用alpha-beta剪枝进行了优化。

u  为方便实现剪枝,而不是全部拓展所有节点,使用了dfs的搜索方式。

u  维护一个当前棋局的数组和一个当前节点下一层可能走的点的集合。因为涉及到回溯,需要一种高效的插入和删除的数据结构,我这里采用了HashSet来作为集合的数据结构。

u  将当前所有下过棋子的点的相邻点放入到下一层待搜索的集合中,而不是搜索整个棋盘,这样缩小了搜索的范围,减少了扩展的节点数。

u  评估函数使用当前局面连在一线上棋子的个数进行打分,连在一起的个数越多得分越高。

u  利用java的drawingPanel库实现了图形界面。附drawingPanel下载地址:http://www.buildingjavaprograms.com/DrawingPanel.java

 


import java.util.*;
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;

public class Ai3{
	private static DrawingPanel panel=new DrawingPanel(700,700);
	private static Graphics g=panel.getGraphics();
	public static boolean isBlack=false;//标志棋子的颜色
	public static int[][] chessBoard=new int[17][17]; //棋盘棋子的摆放情况:0无子,1黑子,-1白子
	private static HashSet toJudge=new HashSet(); // ai可能会下棋的点
	private static int dr[]=new int[]{-1,1,-1,1,0,0,-1,1}; // 方向向量
	private static int dc[]=new int[]{1,-1,-1,1,-1,1,0,0}; //方向向量
	public static final int MAXN=1<<28;
	public static final int MINN=-MAXN; 
	private static int searchDeep=4;	//搜索深度
	private static final int size=15;	//棋盘大小
	public static boolean isFinished=false;

	public static void main(String[] args){
		MyMouseEvent myMouseEvent=new MyMouseEvent();
		panel.addMouseListener(myMouseEvent);
		initChessBoard();
	}

	// 初始化函数,汇图
	public static void initChessBoard(){

		isBlack=false;
		toJudge.clear();
		panel.clear();
		panel.setBackground(Color.GRAY);
		g.setColor(Color.BLACK);
		for(int i=45;i<=675;i+=45){
			g.drawLine(45,i,675,i);
			g.drawLine(i,45,i,675);
		}
		// 棋盘上的五个定位基本点,图中的小圆圈
		g.setColor(Color.BLACK);
		g.fillOval(353,353,14,14);
		g.fillOval(218,218,14,14);
		g.fillOval(488,218,14,14);
		g.fillOval(488,488,14,14);
		g.fillOval(218,488,14,14);
		// 初始化棋盘
		for(int i=1;i<=15;++i)
			for(int j=1;j<=15;++j)
				chessBoard[i][j]=0;
		// ai先手
		g.fillOval(337,337,45,45);
		chessBoard[8][8]=1;
		for(int i=0;i<8;++i)
			if(1<=8+dc[i] && 8+dc[i]<=size && 1<=8+dr[i] && 8+dr[i]<=size){
				Point now=new Point(8+dc[i],8+dr[i]);
				if(!toJudge.contains(now))
					toJudge.add(now);
			}
		isBlack=false;
	}

	// 通过点击事件,得到棋子位置进行下棋
	public static void putChess(int x,int y){
		if(isBlack)
			g.setColor(Color.BLACK);
		else 
			g.setColor(Color.WHITE);
		g.fillOval(x-22,y-22,45,45);
		chessBoard[y/45][x/45]=isBlack?1:-1;
		if(isEnd(x/45,y/45)){
			String s=Ai3.isBlack?"黑子胜":"白子胜";
			JOptionPane.showMessageDialog(null,s);
			isBlack=true;
			initChessBoard();
		}
		else{
			Point p=new Point(x/45,y/45);
			if(toJudge.contains(p))
				toJudge.remove(p);
			for(int i=0;i<8;++i){
				Point now=new Point(p.x+dc[i],p.y+dr[i]);
				if(1<=now.x && now.x<=size && 1<=now.y && now.y<=size && chessBoard[now.y][now.x]==0)
					toJudge.add(now);
			}

			// Iterator it=toJudge.iterator();
			// while(it.hasNext()){
			// 	Point now=(Point)it.next();
			// 	System.out.printf("%d\t%d\n",now.x,now.y);
			// }
			// System.out.printf("*******************************************************\n");
		}
	}

	// ai博弈入口函数
	public static void myAI(){
		Node node=new Node();
		dfs(0,node,MINN,MAXN,null);
		Point now=node.bestChild.p;
		// toJudge.remove(now);
		putChess(now.x*45,now.y*45);
		isBlack=false;
	}

	// alpha beta dfs
	private static void dfs(int deep,Node root,int alpha,int beta,Point p){
		if(deep==searchDeep){
			root.mark=getMark();
			// System.out.printf("%d\t%d\t%d\n",p.x,p.y,root.mark);
			return;
		}
		ArrayList judgeSet=new ArrayList();
		Iterator it=toJudge.iterator();
		while(it.hasNext()){
			Point now=new Point((Point)it.next());
			judgeSet.add(now);
		}
		it=judgeSet.iterator();
		while(it.hasNext()){
			Point now=new Point((Point)it.next());
			Node node=new Node();
			node.setPoint(now);
			root.addChild(node);
			boolean flag=toJudge.contains(now);
			chessBoard[now.y][now.x]=((deep&1)==1)?-1:1;
			if(isEnd(now.x,now.y)){
				root.bestChild=node;
				root.mark=MAXN*chessBoard[now.y][now.x];
				chessBoard[now.y][now.x]=0;
				return;
			}

			boolean flags[]=new boolean[8]; //标记回溯时要不要删掉
			Arrays.fill(flags,true);
			for(int i=0;i<8;++i){
				Point next=new Point(now.x+dc[i],now.y+dr[i]);
				if(1<=now.x+dc[i] && now.x+dc[i]<=size && 1<=now.y+dr[i] && now.y+dr[i]<=size && chessBoard[next.y][next.x]==0){
					if(!toJudge.contains(next)){
						toJudge.add(next);
					}
					else flags[i]=false;
				}
			}
			
			if(flag) 
				toJudge.remove(now);
			dfs(deep+1,root.getLastChild(),alpha,beta,now);
			chessBoard[now.y][now.x]=0;
			if(flag)
				toJudge.add(now);
			for(int i=0;i<8;++i)
				if(flags[i])
					toJudge.remove(new Point(now.x+dc[i],now.y+dr[i]));
			// alpha beta剪枝
			// min层
			if((deep&1)==1){
				if(root.bestChild==null || root.getLastChild().markroot.bestChild.mark){
					root.bestChild=root.getLastChild();
					root.mark=root.bestChild.mark;
					if(root.mark==MAXN)
						root.mark-=deep;
					alpha=Math.max(root.mark,alpha);
				}
				if(root.mark>=beta)
					return;
			}
		}
		// if(deep==0) System.out.printf("******************************************\n");
	}

	public static int getMark(){
		int res=0;
		for(int i=1;i<=size;++i){
			for(int j=1;j<=size;++j){
				if(chessBoard[i][j]!=0){
					// 行
					boolean flag1=false,flag2=false;
					int x=j,y=i;
					int cnt=1;
					int col=x,row=y;
					while(--col>0 && chessBoard[row][col]==chessBoard[y][x]) ++cnt;
					if(col>0 && chessBoard[row][col]==0) flag1=true;
					col=x;row=y;
					while(++col<=size && chessBoard[row][col]==chessBoard[y][x]) ++cnt;
					if(col<=size && chessBoard[row][col]==0) flag2=true;
					if(flag1 && flag2)
						res+=chessBoard[i][j]*cnt*cnt;
					else if(flag1 || flag2) res+=chessBoard[i][j]*cnt*cnt/4; 
					if(cnt>=5) res=MAXN*chessBoard[i][j];
					// 列
					col=x;row=y;
					cnt=1;flag1=false;flag2=false;
					while(--row>0 && chessBoard[row][col]==chessBoard[y][x]) ++cnt;
					if(row>0 && chessBoard[row][col]==0) flag1=true;
					col=x;row=y;
					while(++row<=size && chessBoard[row][col]==chessBoard[y][x]) ++cnt;
					if(row<=size && chessBoard[row][col]==0) flag2=true;
					if(flag1 && flag2)
						res+=chessBoard[i][j]*cnt*cnt;
					else if(flag1 || flag2)
						res+=chessBoard[i][j]*cnt*cnt/4;
					if(cnt>=5) res=MAXN*chessBoard[i][j];
					// 左对角线
					col=x;row=y;
					cnt=1;flag1=false;flag2=false;
					while(--col>0 && --row>0 && chessBoard[row][col]==chessBoard[y][x]) ++cnt;
					if(col>0 && row>0 && chessBoard[row][col]==0) flag1=true;
					col=x;row=y;
					while(++col<=size && ++row<=size && chessBoard[row][col]==chessBoard[y][x]) ++cnt;
					if(col<=size && row<=size && chessBoard[row][col]==0) flag2=true;
					if(flag1 && flag2) 	
						res+=chessBoard[i][j]*cnt*cnt;
					else if(flag1 || flag2) res+=chessBoard[i][j]*cnt*cnt/4;
					if(cnt>=5) res=MAXN*chessBoard[i][j];
					// 右对角线
					col=x;row=y;
					cnt=1;flag1=false;flag2=false;
					while(++row<=size && --col>0 && chessBoard[row][col]==chessBoard[y][x]) ++cnt;
					if(row<=size && col>0 && chessBoard[row][col]==0) flag1=true;
					col=x;row=y;
					while(--row>0 && ++col<=size && chessBoard[row][col]==chessBoard[y][x]) ++cnt;
					if(row>0 && col<=size && chessBoard[i][j]==0) flag2=true;
					if(flag1 && flag2)
						res+=chessBoard[i][j]*cnt*cnt;
					else if(flag1 || flag2) res+=chessBoard[i][j]*cnt*cnt/4;
					if(cnt>=5) res=MAXN*chessBoard[i][j];
					
				}
			}
		}
		return res;
	}

	// for debug
	public static void debug(){
		for(int i=1;i<=size;++i){
			for(int j=1;j<=size;++j){
				System.out.printf("%d\t",chessBoard[i][j]);
			}
			System.out.println("");
		}
	}

	// 判断是否一方取胜
	public static boolean isEnd(int x,int y){
		// 判断一行是否五子连珠
		int cnt=1;
		int col=x,row=y;
		while(--col>0 && chessBoard[row][col]==chessBoard[y][x]) ++cnt;
		col=x;row=y;
		while(++col<=size && chessBoard[row][col]==chessBoard[y][x]) ++cnt;
		if(cnt>=5){
			isFinished=true;
			return true;
		}
		// 判断一列是否五子连珠
		col=x;row=y;
		cnt=1;
		while(--row>0 && chessBoard[row][col]==chessBoard[y][x]) ++cnt;
		col=x;row=y;
		while(++row<=size && chessBoard[row][col]==chessBoard[y][x]) ++cnt;
		if(cnt>=5){
			isFinished=true;
			return true;
		}
		// 判断左对角线是否五子连珠
		col=x;row=y;
		cnt=1;
		while(--col>0 && --row>0 && chessBoard[row][col]==chessBoard[y][x]) ++cnt;
		col=x;row=y;
		while(++col<=size && ++row<=size && chessBoard[row][col]==chessBoard[y][x]) ++cnt;
		if(cnt>=5){
			isFinished=true;
			return true;
		}
		// 判断右对角线是否五子连珠
		col=x;row=y;
		cnt=1;
		while(++row<=size && --col>0 && chessBoard[row][col]==chessBoard[y][x]) ++cnt;
		col=x;row=y;
		while(--row>0 && ++col<=size && chessBoard[row][col]==chessBoard[y][x]) ++cnt;
		if(cnt>=5){
			isFinished=true;
			return true;
		}
		return false;
	}
}



// 树节点
class Node{
	public Node bestChild=null;
	public ArrayList child=new ArrayList();
	public Point p=new Point();
	public int mark;
	Node(){
		this.child.clear();
		bestChild=null;
		mark=0;
	}
	public void setPoint(Point r){
		p.x=r.x;
		p.y=r.y;
	}
	public void addChild(Node r){
		this.child.add(r);
	}
	public Node getLastChild(){
		return child.get(child.size()-1);
	}
}

// 实现鼠标事件接口
class MyMouseEvent implements MouseListener{
	public void mouseClicked(MouseEvent e){
		int x=round(e.getX()),y=round(e.getY());
		if(x>=45 && x<=675 && y>=45 && y<=675 && Ai3.chessBoard[y/45][x/45]==0 && Ai3.isBlack==false){
			Ai3.putChess(x,y);
			if(!Ai3.isFinished){
				Ai3.isBlack=true;
				Ai3.myAI();
			}
			Ai3.isFinished=false;
		}
	}
	// 得到鼠标点击点附近的棋盘精准点
	public static int round(int x){
		return (x%45<22)?x/45*45:x/45*45+45;
	}
	public void mouseExited(MouseEvent e){}
	public void mouseEntered(MouseEvent e){}
	public void mouseReleased(MouseEvent e){}
	public void mousePressed(MouseEvent e){}
}


你可能感兴趣的:(人工智能实践)