1. 程式人生 > >象棋AI算法(二)

象棋AI算法(二)

根節點 說明 過去 發現 oot set 位置 fun ()

原文大神是用html5+js寫的關於象棋AI的博客,裏面重點講了棋子的著法,自己設計的評估函數和簡單的Minmax理論,沒有具體的講搜索算法,本文是對原文的學習和分析補充

一,棋子的著法
com.bylaw ={} 首先創建一個數組,用於存儲該棋子處於某一點時所能走到著點

(1)車:

com.bylaw.c = function (x,y,map,my){
	var d=[];
	//左側檢索 若存在棋子且顏色不同則push過去並結束循環,否則一步步push <span style="color:#ff0000;"> </span>
	for (var i=x-1; i>= 0; i--){
		if (map[y][i]) {
			if (com.mans[map[y][i]].my!=my) d.push([i,y]);
			break
		}else{
			d.push([i,y])	
		}
	}
	//右側檢索
	for (var i=x+1; i <= 8; i++){
		if (map[y][i]) {
			if (com.mans[map[y][i]].my!=my) d.push([i,y]);
			break
		}else{
			d.push([i,y])	
		}
	}
	//上檢索
	for (var i = y-1 ; i >= 0; i--){
		if (map[i][x]) {
			if (com.mans[map[i][x]].my!=my) d.push([x,i]);
			break
		}else{
			d.push([x,i])	
		}
	}
	//下檢索
	for (var i = y+1 ; i<= 9; i++){
		if (map[i][x]) {
			if (com.mans[map[i][x]].my!=my) d.push([x,i]);
			break
		}else{
			d.push([x,i])	
		}
	}
	return d;
}

算法分析:

分別向上,下,左,右四個方向搜索,若找到一個點且顏色與該棋子不同(敵對棋子),則將該點坐標記錄在d數組中,若某一方向上沒有其他棋子,將這一方向上所有坐標都記錄在d數組中。簡單來講:就是將以車這個棋子為中心的十字上的坐標都記錄在d數組中(你早這樣說多好~,開始說那麽多)

前提補充:

1,代碼中的map:

com.initMap = [
	[‘C0‘,‘M0‘,‘X0‘,‘S0‘,‘J0‘,‘S1‘,‘X1‘,‘M1‘,‘C1‘],
	[    ,    ,    ,    ,    ,    ,    ,    ,    ],
	[    ,‘P0‘,    ,    ,    ,    ,    ,‘P1‘,    ],
	[‘Z0‘,    ,‘Z1‘,    ,‘Z2‘,    ,‘Z3‘,    ,‘Z4‘],
	[    ,    ,    ,    ,    ,    ,    ,    ,    ],
	[    ,    ,    ,    ,    ,    ,    ,    ,    ],
	[‘z0‘,    ,‘z1‘,    ,‘z2‘,    ,‘z3‘,    ,‘z4‘],
	[    ,‘p0‘,    ,    ,    ,    ,    ,‘p1‘,    ],
	[    ,    ,    ,    ,    ,    ,    ,    ,    ],
	[‘c0‘,‘m0‘,‘x0‘,‘s0‘,‘j0‘,‘s1‘,‘x1‘,‘m1‘,‘c1‘]
];
這裏的字符串代表每個棋子的key值:

com.keys = {                                       //設定每類棋子的key值
	"c0":"c","c1":"c",
	"m0":"m","m1":"m",
	"x0":"x","x1":"x",
	"s0":"s","s1":"s",
	"j0":"j",
	"p0":"p","p1":"p",
	"z0":"z","z1":"z","z2":"z","z3":"z","z4":"z","z5":"z",
	
	"C0":"C","C1":"C",
	"M0":"M","M1":"M",
	"X0":"X","X1":"X",
	"S0":"S","S1":"S",
	"J0":"J",
	"P0":"P","P1":"P",
	"Z0":"Z","Z1":"Z","Z2":"Z","Z3":"Z","Z4":"Z","Z5":"Z",
}

2,my:

標記值:1代表紅色方(這裏指人。玩家永遠操縱紅色) ; -1代表AI

3,map[y][i]與d.push([i][y])

左方向上搜索,y坐標不變,x坐標遍歷,而體現在map當中(向上翻第一點),仔細看就會發現:第一個下標代表y值,第二個下標代表x值,其與坐標值正好相反

其他方向上以此類推。。。

(2)馬

com.bylaw.m = function (x,y,map,my){
	var d=[];
		//1點鐘方向  不絆馬腳  1點不存在棋子或1點棋子顏色不同  push 
		if ( y-2>= 0 && x+1<= 8 && !play.map[y-1][x] &&(!com.mans[map[y-2][x+1]] || com.mans[map[y-2][x+1]].my!=my)) d.push([x+1,y-2]);
		//2點
		if ( y-1>= 0 && x+2<= 8 && !play.map[y][x+1] &&(!com.mans[map[y-1][x+2]] || com.mans[map[y-1][x+2]].my!=my)) d.push([x+2,y-1]);
		//4點
		if ( y+1<= 9 && x+2<= 8 && !play.map[y][x+1] &&(!com.mans[map[y+1][x+2]] || com.mans[map[y+1][x+2]].my!=my)) d.push([x+2,y+1]);
		//5點
		if ( y+2<= 9 && x+1<= 8 && !play.map[y+1][x] &&(!com.mans[map[y+2][x+1]] || com.mans[map[y+2][x+1]].my!=my)) d.push([x+1,y+2]);
		//7點
		if ( y+2<= 9 && x-1>= 0 && !play.map[y+1][x] &&(!com.mans[map[y+2][x-1]] || com.mans[map[y+2][x-1]].my!=my)) d.push([x-1,y+2]);
		//8點
		if ( y+1<= 9 && x-2>= 0 && !play.map[y][x-1] &&(!com.mans[map[y+1][x-2]] || com.mans[map[y+1][x-2]].my!=my)) d.push([x-2,y+1]);
		//10點
		if ( y-1>= 0 && x-2>= 0 && !play.map[y][x-1] &&(!com.mans[map[y-1][x-2]] || com.mans[map[y-1][x-2]].my!=my)) d.push([x-2,y-1]);
		//11點
		if ( y-2>= 0 && x-1>= 0 && !play.map[y-1][x] &&(!com.mans[map[y-2][x-1]] || com.mans[map[y-2][x-1]].my!=my)) d.push([x-1,y-2]);

	return d;
}

算法分析:

當馬處於一點時,可以走的最多情況有8種方向,分別討論每個方向:如果不絆馬腳,且該方向上那著點沒有棋子或棋子顏色不同,則記錄該著點

圖例分析:

技術分享

有點醜,用畫圖做的,不要在意這些細節

(三)相

com.bylaw.x = function (x,y,map,my){
	var d=[];
	if (my===1){ //紅方  顏色不同,y的取值範圍不同,且不能過河
		//4點半  不絆象腳   4.5位置沒子或棋子顏色不同   push
		if ( y+2<= 9 && x+2<= 8 && !play.map[y+1][x+1] && (!com.mans[map[y+2][x+2]] || com.mans[map[y+2][x+2]].my!=my)) d.push([x+2,y+2]);
		//7點半
		if ( y+2<= 9 && x-2>= 0 && !play.map[y+1][x-1] && (!com.mans[map[y+2][x-2]] || com.mans[map[y+2][x-2]].my!=my)) d.push([x-2,y+2]);
		//1點半
		if ( y-2>= 5 && x+2<= 8 && !play.map[y-1][x+1] && (!com.mans[map[y-2][x+2]] || com.mans[map[y-2][x+2]].my!=my)) d.push([x+2,y-2]);
		//10點半
		if ( y-2>= 5 && x-2>= 0 && !play.map[y-1][x-1] && (!com.mans[map[y-2][x-2]] || com.mans[map[y-2][x-2]].my!=my)) d.push([x-2,y-2]);
	}else{
		//4點半
		if ( y+2<= 4 && x+2<= 8 && !play.map[y+1][x+1] && (!com.mans[map[y+2][x+2]] || com.mans[map[y+2][x+2]].my!=my)) d.push([x+2,y+2]);
		//7點半
		if ( y+2<= 4 && x-2>= 0 && !play.map[y+1][x-1] && (!com.mans[map[y+2][x-2]] || com.mans[map[y+2][x-2]].my!=my)) d.push([x-2,y+2]);
		//1點半
		if ( y-2>= 0 && x+2<= 8 && !play.map[y-1][x+1] && (!com.mans[map[y-2][x+2]] || com.mans[map[y-2][x+2]].my!=my)) d.push([x+2,y-2]);
		//10點半
		if ( y-2>= 0 && x-2>= 0 && !play.map[y-1][x-1] && (!com.mans[map[y-2][x-2]] || com.mans[map[y-2][x-2]].my!=my)) d.push([x-2,y-2]);
	}
	return d;
}


算法分析:

因為相不能過河,所以要按顏色分情況討論(不同顏色,y坐標不同)

而每種顏色的相都有四種可能著法,與馬類似:如果不絆象腳, 著點沒有棋子或棋子顏色不同,記錄

圖例分析:

技術分享

(四)士

com.bylaw.s = function (x,y,map,my){
	var d=[];
	if (my===1){ //紅方
		//4點半
		if ( y+1<= 9 && x+1<= 5 && (!com.mans[map[y+1][x+1]] || com.mans[map[y+1][x+1]].my!=my)) d.push([x+1,y+1]);
		//7點半
		if ( y+1<= 9 && x-1>= 3 && (!com.mans[map[y+1][x-1]] || com.mans[map[y+1][x-1]].my!=my)) d.push([x-1,y+1]);
		//1點半
		if ( y-1>= 7 && x+1<= 5 && (!com.mans[map[y-1][x+1]] || com.mans[map[y-1][x+1]].my!=my)) d.push([x+1,y-1]);
		//10點半
		if ( y-1>= 7 && x-1>= 3 && (!com.mans[map[y-1][x-1]] || com.mans[map[y-1][x-1]].my!=my)) d.push([x-1,y-1]);
	}else{
		//4點半
		if ( y+1<= 2 && x+1<= 5 && (!com.mans[map[y+1][x+1]] || com.mans[map[y+1][x+1]].my!=my)) d.push([x+1,y+1]);
		//7點半
		if ( y+1<= 2 && x-1>= 3 && (!com.mans[map[y+1][x-1]] || com.mans[map[y+1][x-1]].my!=my)) d.push([x-1,y+1]);
		//1點半
		if ( y-1>= 0 && x+1<= 5 && (!com.mans[map[y-1][x+1]] || com.mans[map[y-1][x+1]].my!=my)) d.push([x+1,y-1]);
		//10點半
		if ( y-1>= 0 && x-1>= 3 && (!com.mans[map[y-1][x-1]] || com.mans[map[y-1][x-1]].my!=my)) d.push([x-1,y-1]);
	}
	return d;
		
}


算法分析:

士不能出九宮格,x,y值都有限制。按顏色分情況討論。每種顏色各有4中可能著法:如果該著點沒棋子或棋子顏色不同,記錄

圖例分析:

這個簡單了,就不畫圖了~ ~ ~ ~

(五)將

com.bylaw.j = function (x,y,map,my){
	var d=[];
	var isNull=(function (y1,y2){        
		var y1=com.mans["j0"].y;         //紅帥的y
		var x1=com.mans["J0"].x;         //黑將的x
		var y2=com.mans["J0"].y;         //黑將的y
		for (var i=y1-1; i>y2; i--){
			if (map[i][x1]) return false;       //將與將之間非空,有子
		}
		return true;
	})();
	
	if (my===1){ //紅方
		//下
		if ( y+1<= 9  && (!com.mans[map[y+1][x]] || com.mans[map[y+1][x]].my!=my)) d.push([x,y+1]);
		//上
		if ( y-1>= 7 && (!com.mans[map[y-1][x]] || com.mans[map[y-1][x]].my!=my)) d.push([x,y-1]);
		//老將對老將的情況
		if ( com.mans["j0"].x == com.mans["J0"].x &&isNull) d.push([com.mans["J0"].x,com.mans["J0"].y]);      //x相等,且中間為空,push黑將的坐標
		
	}else{
		//下
		if ( y+1<= 2  && (!com.mans[map[y+1][x]] || com.mans[map[y+1][x]].my!=my)) d.push([x,y+1]);
		//上
		if ( y-1>= 0 && (!com.mans[map[y-1][x]] || com.mans[map[y-1][x]].my!=my)) d.push([x,y-1]);
		//老將對老將的情況
		if ( com.mans["j0"].x == com.mans["J0"].x &&isNull) d.push([com.mans["j0"].x,com.mans["j0"].y]);        //push紅帥的坐標
	}
	//右
	if ( x+1<= 5  && (!com.mans[map[y][x+1]] || com.mans[map[y][x+1]].my!=my)) d.push([x+1,y]);
	//左
	if ( x-1>= 3 && (!com.mans[map[y][x-1]] || com.mans[map[y][x-1]].my!=my))d.push([x-1,y]);
	return d;
}

算法分析:

將除了顏色不同導致y值不同外,還有種特殊情況:即老將見面。所以開始先寫個函數,判斷將與帥之間是否有其他棋子

接下來按顏色不同分情況討論上下兩種著法:重點 是y值的界定。以帥為例:帥在棋盤下方,y坐標只能取7,8,9.如果向下走,則取7,8,所以y值最大為8.上與其類似。而判斷完著法之後還要判斷是否老將見面的特殊情況:如果兩者x坐標相等且中間沒其他棋子,之間閃現過去搶人頭~ ~ ~然後victory

(六),炮

com.bylaw.p = function (x,y,map,my){
	var d=[];
	//左側檢索
	var n=0;
	for (var i=x-1; i>= 0; i--){
		if (map[y][i]) {                  //碰到子
			if (n==0){                    //若是第一個子,不用管,跳出本次循環,標記位加1
				n++;
				continue;
			}else{                       //若不是第一個子,判斷顏色若不同,push過去並結束循環
				if (com.mans[map[y][i]].my!=my) d.push([i,y]);
				break	
			}
		}else{                          //若一直碰不到子,將子走到最左
			if(n==0) d.push([i,y])	
		}
	}
	//右側檢索
	var n=0;
	for (var i=x+1; i <= 8; i++){
		if (map[y][i]) {
			if (n==0){
				n++;
				continue;
			}else{
				if (com.mans[map[y][i]].my!=my) d.push([i,y]);
				break	
			}
		}else{
			if(n==0) d.push([i,y])	
		}
	}
	//上檢索
	var n=0;
	for (var i = y-1 ; i >= 0; i--){
		if (map[i][x]) {
			if (n==0){
				n++;
				continue;
			}else{
				if (com.mans[map[i][x]].my!=my) d.push([x,i]);
				break	
			}
		}else{
			if(n==0) d.push([x,i])	
		}
	}
	//下檢索
	var n=0;
	for (var i = y+1 ; i<= 9; i++){
		if (map[i][x]) {
			if (n==0){
				n++;
				continue;
			}else{
				if (com.mans[map[i][x]].my!=my) d.push([x,i]);
				break	
			}
		}else{
			if(n==0) d.push([x,i])	
		}
	}
	return d;
}

算法分析:

跟車一樣,需要向4個方向上搜索

若該方向上沒棋子,則記錄該方向所有點坐標

若走著走著發現一個棋子,先冷靜一下(跳出本次循環),偷偷地看接下來該方向上有沒有敵方棋子,有,就可以越塔gank了。然後把敵方死的位置記錄下來留作紀念~ ~ ~

(七)卒

com.bylaw.z = function (x,y,map,my){
	var d=[];
	if (my===1){ //紅方
		//上
		if ( y-1>= 0 && (!com.mans[map[y-1][x]] || com.mans[map[y-1][x]].my!=my)) d.push([x,y-1]);
		//右
		if ( x+1<= 8 && y<=4  && (!com.mans[map[y][x+1]] || com.mans[map[y][x+1]].my!=my)) d.push([x+1,y]);    //y<4,即過河之後,才能左右移動
		//左
		if ( x-1>= 0 && y<=4 && (!com.mans[map[y][x-1]] || com.mans[map[y][x-1]].my!=my))d.push([x-1,y]);
	}else{
		//下
		if ( y+1<= 9  && (!com.mans[map[y+1][x]] || com.mans[map[y+1][x]].my!=my)) d.push([x,y+1]);
		//右
		if ( x+1<= 8 && y>=6  && (!com.mans[map[y][x+1]] || com.mans[map[y][x+1]].my!=my)) d.push([x+1,y]);
		//左
		if ( x-1>= 0 && y>=6 && (!com.mans[map[y][x-1]] || com.mans[map[y][x-1]].my!=my))d.push([x-1,y]);
	}
	
	return d;
}

算法分析:

同樣分情況討論。且由於卒不能後退所以只用判斷上,左,右三種情況。而卒由於過河後才能左右移動,所以左右的判斷除了x的界定還有y值的界定。最後跟車一樣如果該著點沒有棋子或該棋子顏色不同,記錄該點

二 ,使用alpha-beta在所有著法當中搜索最佳著法

AI.getAlphaBeta = function (A, B, depth, map ,my) { 
	if (depth == 0) {
		return {"value":AI.evaluate(map , my)}; //當搜索深度為0是時調用局面評價函數; 
 	}
 	var moves = AI.getMoves(map , my ); //生成全部走法; 
 	<span style="color:#ff0000;">//這裏排序以後會增加效率

	for (var i=0; i < moves.length; i++) {</span>
		
		
  	//走這個走法;
		var move= moves[i];
		var key = move[4];
		var oldX= move[0];
		var oldY= move[1];
		var newX= move[2];
		var newY= move[3];
		var clearKey = map[ newY ][ newX ]||"";

		map[ newY ][ newX ] = key;                   //走,賦新值,刪除舊值
		delete map[ oldY ][ oldX ];
		play.mans[key].x = newX;
		play.mans[key].y = newY;
		
	  <span style="color:#ff0000;">if (clearKey=="j0"||clearKey=="J0") {        //被吃老將 
			play.mans[key]	.x = oldX;
			play.mans[key]	.y = oldY;
			map[ oldY ][ oldX ] = key;
			delete map[ newY ][ newX ];      //並不是真的走,所以這裏要撤銷
			if (clearKey){
				 map[ newY ][ newX ] = clearKey;
				
			}

			return {"key":key,"x":newX,"y":newY,"value":8888};
			</span>
	  }else { 
	  	var val = -AI.getAlphaBeta(-B, -A, depth - 1, map , -my).value;        //上面代表AI,這裏倒置,-my,代表人的著法,然後再從上面開始執行
			//val = val || val.value;
	
	  	//<span style="color:#ff0000;">撤消這個走法;  
			play.mans[key]	.x = oldX;
			play.mans[key]	.y = oldY;
			map[ oldY ][ oldX ] = key;
			delete map[ newY ][ newX ];
			if (clearKey){
				 map[ newY ][ newX ] = clearKey;
				 //play.mans[ clearKey ].isShow = true;
			}</span>
	  	if (val >= B) { 
				//將這個走法記錄到歷史表中; 
				//AI.setHistoryTable(txtMap,AI.treeDepth-depth+1,B,my);
				return {"key":key,"x":newX,"y":newY,"value":B}; 
			} 
			<span style="color:#ff0000;">if (val > A) { 
	    	A = val; //設置最佳走法, 
				if (AI.treeDepth == depth) var rootKey={"key":key,"x":newX,"y":newY,"value":A};
			} </span>
		} 
 	} 
	
	if (AI.treeDepth == depth) {//已經遞歸回根了
		if (!rootKey){
			//AI沒有最佳走法,說明AI被將死了,返回false
			return false;
		}else{
			//這個就是最佳走法;
			return rootKey;
		}
	}
 return {"key":key,"x":newX,"y":newY,"value":A}; 
}

簡化後的偽代碼(與上面代碼一一對應):

int AlphaBeta(int vlAlpha, int vlBeta, int nDepth) {
 if (nDepth == 0) {
  return 局面評價函數;
 }
 生成全部走法;
 <span style="color:#ff0000;">按歷史表排序全部走法;</span>
 for (每個生成的走法) {
  走這個走法;
  <span style="color:#ff0000;">if (被將軍) {
   撤消這個走法;
  } else</span> {
   int vl = -AlphaBeta(-vlBeta, -vlAlpha, nDepth - 1);
   <span style="color:#ff0000;">撤消這個走法;</span> 
   if (vl >= vlBeta) {
    <span style="color:#ff0000;">將這個走法記錄到歷史表中;</span>
    return vlBeta;
   }
   if (vl > vlAlpha) {
    <span style="color:#ff0000;">設置最佳走法;</span>
    vlAlpha = vl;
   }
  }
 }
 if (沒有走過任何走法) {                 //AI被將死
  return 殺棋的分數;
 }
 將最佳走法記錄到歷史表中;
 if (根節點) {
  最佳走法就是電腦要走的棋;
 }
 return vlAlpha;
}

這樣,簡單套用上一講講過的alpha-beta算法,就能搜索出相對來說最佳路徑來~ ~ ~

最後設置坐標就可以實現AI自動走棋或吃子了

象棋AI算法(二)