1. 程式人生 > >『HTML5實現人工智慧』小遊戲《井字棋》釋出,據說IQ上200才能贏【演算法&程式碼講解+資源打包下載】

『HTML5實現人工智慧』小遊戲《井字棋》釋出,據說IQ上200才能贏【演算法&程式碼講解+資源打包下載】

一,什麼是TicTacToe(井字棋)

本遊戲為在下用lufylegend開發的第二款小遊戲。此遊戲是大家想必大家小時候都玩過,因為玩它很簡單,只需要一張草稿紙和一隻筆就能開始遊戲,所以廣受兒童歡迎。可能我說了半天,對它名字不熟悉的朋友也不懂我在說神馬。那沒關係,我就引用Wiki(維基百科)的介紹作為大家對它名字的認識,順便也勾起我們兒時的回憶:

井字棋,大陸、臺灣又稱為井字遊戲、圈圈叉叉;另外也有打井遊戲、OX棋的稱呼,香港多稱井字過三關、過三關,是種紙筆遊戲。兩個玩家,一個打圈(O),一個打叉(X),輪流在3乘3的格上打自己的符號,最先以橫、直、斜連成一線則為勝。如果雙方都下得正確無誤,將得和局。這種遊戲實際上是由第一位玩家所控制,第一位玩家是攻,第二位玩家是守。第一位玩家在角位行第一子的話贏面最大(見圖一),第二位玩家若是在邊,角位下子,第一位玩家就可以以兩粒連線牽制著第二位玩家,然後製造“兩頭蛇”。


圖一

二,遊戲在哪裡玩?

相信大家看了介紹就對井字棋有了瞭解。現在我用html5配合開源遊戲引擎lufylegend開發出了這一款遊戲,並實現了人工智慧(AI)確保遊戲中玩家能棋縫對手。

接下來是遊戲線上試玩和下載原始碼的地址:

三,遊戲截圖



四,遊戲引擎

本遊戲運用國產的lufylegend引擎,版本為1.6.1,如果大家感興趣可以去官網看看

lufylegend官方網站:

lufylegend API文件:

上面有此引擎的下載和API介紹。關於用lufylegend開發遊戲的其他文章:

五,演算法&程式碼講解

先來個遊戲初始化:

init(30,"mylegend",390,420,main);

為了方便操作遊戲中的一些資料,我們設定許多變數:

var backLayer,chessLayer,overLayer;
var statusText = new LTextField();
var statusContent="您先請吧……";
var matrix = [
	[0,0,0],
	[0,0,0],
	[0,0,0]
];
var usersTurn = true;
var step = 0;
var title = "井字棋";
var introduction = ""
var infoArr = [title,introduction];

第一行是層變數;第二行是例項化的文字框物件,用來顯示文字;第三行是當前顯示資訊的文字,比如該哪方走,哪方贏了等,會根據不同情況改變。

matrix是用來儲存當前棋盤資料的陣列,如果下一步棋,就會更改其中資料,順便也說一下,為了區分【空白格子】,【我方下的位置】,【電腦下的位置】,我們用-1來代表【我方下的位置】,用0來代表【空白格子】,1來代表【電腦下的位置】;看官且記,這-1,0,1在棋盤陣列中便各有了代表意義。

userTurn是用來判斷玩家是否可以下棋;step是用來表示走的步數,用來判斷棋盤是否下滿;title,introduction還有infoArr原本是用來製作關於介面的,結果做到最後就算了,大家直接忽視掉吧。

接下來就是main函式,由於沒有圖片,所以就沒有載入部分了:

function main(){
	gameInit();
	addText();
	addLattice();	
}
main呼叫的幾個函式如下:
function gameInit(){
	initLayer();
	addEvent();
}
function addText(){
	statusText.size = 15;	
	statusText.weight = "bold";
	statusText.color = "white";
	statusText.text = statusContent;
	statusText.x = (LGlobal.width-statusText.getWidth())*0.5;
	statusText.y = 393;
	
	overLayer.addChild(statusText);
}
function addLattice(){
	backLayer.graphics.drawRect(10,"dimgray",[0,0,390,420],true,"dimgray");
	backLayer.graphics.drawRect(10,"dimgray",[0,0,390,390],true,"lavender");
	for(var i=0;i<3;i++){
		backLayer.graphics.drawLine(3,"dimgray",[130*i,0,130*i,390]);
	}
	for(var i=0;i<3;i++){
		backLayer.graphics.drawLine(3,"dimgray",[0,130*i,390,130*i]);
	}
}
解釋一下他們的功能。首先,gameInit是用來初始化遊戲的,包括初始化層一類的東西。addText是用來加下面文字的。addLattice使用來畫棋盤的。程式碼很簡單,參照lufylegend API文件看一下就能看懂。

接下來我們來看gameInit裡呼叫的函式:

function initLayer(){
	backLayer = new LSprite();
	addChild(backLayer);

	chessLayer = new LSprite();
	backLayer.addChild(chessLayer);

	overLayer = new LSprite();
	backLayer.addChild(overLayer);
}
function addEvent(){
	backLayer.addEventListener(LMouseEvent.MOUSE_DOWN,onDown);
}
initLayer是用來例項化層的,說明了一點就是例項化LSprite。addEvent用來加點選事件。

然後接下來就來看看事件觸發的onDown:

function onDown(){
	var mouseX,mouseY;
	mouseX = event.offsetX;
	mouseY = event.offsetY;

	var partX = Math.floor(mouseX/130);
	var partY = Math.floor(mouseY/130);
	if(matrix[partX][partY]==0){
		usersTurn=false;
		matrix[partX][partY]=-1;
		step++;
		update(partX,partY);
		
		if(win(partX,partY)){
			statusContent = "帥呆了,你贏啦!點選螢幕重開遊戲。";
			gameover();
			addText();
		}else if(isEnd()){
			statusContent = "平局啦~~點選螢幕重開遊戲。";
			gameover();
			addText();
		}else{
			statusContent = "電腦正在思考中……";
			addText();
			computerThink();
		}
	}
}
這個函式要做的就是先取出點選位置,然後根據點的位置下一顆棋,然後將在棋盤陣列中相應的位置設為-1,表示是我方走的,然後判斷:下了這一步棋後的勝負或者平局情況,並且呼叫相應的函式和顯示相應的文字。判斷贏,我們用win函式,程式碼如下:
function win(x,y){
	if(Math.abs(matrix[x][0]+matrix[x][1]+matrix[x][2])==3){
		return true;
	}
	if(Math.abs(matrix[0][y]+matrix[1][y]+matrix[2][y])==3){
		return true;
	}
	if(Math.abs(matrix[0][0]+matrix[1][1]+matrix[2][2])==3){
		return true;
	}
	if(Math.abs(matrix[2][0]+matrix[1][1]+matrix[0][2])==3){
		return true;
	}
	return false;
}
首先我們判斷第x行,第0,1,2列的數字相加的絕對值是否為3(由於這個函式在下面還要用到,所以我們要做得通用性,所以就用了絕對值)。為什麼等於3呢?因為看官是否記得我們上面說的:-1代表【我方下的位置】,0代表【空白格子】,1代表【電腦下的位置】。但凡是下了棋的地方,值總是1或者-1,所以假如有三個同一方棋子連在一起,那這幾個值加起來的絕對值一定是3。因此就返回true代表贏了。如果一直判斷到最後都沒有,就返回false,代表還沒有贏。

我們用isEnd判斷平局,程式碼如下:

function isEnd(){
	return step>=9;
}
程式碼很簡單,就是判斷棋盤佔滿沒有。

其中用到updata負責更新棋盤。程式碼如下:
function update(x,y){
	var v = matrix[x][y];
	if(v>0){
		chessLayer.graphics.drawArc(10,"green",[x*130+65,y*130+65,40,0,2*Math.PI]);
	}else if(v<0){
		chessLayer.graphics.drawLine(20,"#CC0000",[130*x+30,130*y+30,130*(x+1)-30,130*(y+1)-30]);
		chessLayer.graphics.drawLine(20,"#CC0000",[130*(x+1)-30,130*y+30,130*x+30,130*(y+1)-30]);
	}
}

以上的程式碼也很好理解,就是先取出畫的那一點是什麼,如果是我方畫的(在棋盤陣列就是-1),在判斷時,取出的值如果小於0,就畫個叉叉。如果大於0也就是代表電腦畫的(在棋盤陣列中代表1),就畫個圓。

onDown中還用到了gameOver函式,程式碼如下:
function gameover(){
	backLayer.removeEventListener(LMouseEvent.MOUSE_DOWN,onDown);
	backLayer.addEventListener(LMouseEvent.MOUSE_DOWN,function(){
		chessLayer.removeAllChild();
		backLayer.removeChild(chessLayer);
		backLayer.removeChild(overLayer);
		removeChild(backLayer);
		matrix = [
			[0,0,0],
			[0,0,0],
			[0,0,0]
		];
		step = 0;
		main();
		statusContent = "您先請吧……";
		addText();
	});
}
看似程式碼有點長,其實很簡單,就是簡單的移除介面上的一切物件,並且把一些值恢復為預設值。還有onDown中的computerThink函式,程式碼如下:
function computerThink(){
	var b = best();
	var x = b.x;
	var y = b.y;
	matrix[x][y]=1;
	step++;
	update(x,y);
	
	if(win(x,y)){
		statusContent = "哈哈你輸了!點選螢幕重開遊戲。";
		gameover();
		addText();
	}else if(isEnd()){
		statusContent = "平局啦~~點選螢幕重開遊戲。";
		gameover();
		addText();
	}else{
		statusContent = "該你了!!!";
		addText();
	}
}

首先這個函式用了best函式,這個函式會返回一個要下的位置,然後我們把在棋盤陣列中相應的位置設定為1,並且把走的步數+1。然後在相應位置畫上。然後判斷是否贏了或者平局,或者沒贏沒輸沒平局。

best是電腦AI演算法部分,程式碼如下:
function best(){
	var bestx;
	var besty;
	var bestv=0;
	for(var x=0;x<3;x++){
		for(var y=0;y<3;y++){
			if(matrix[x][y]==0){
				matrix[x][y] = 1;
				step++;
				if(win(x,y)){
					step--;
					matrix[x][y] = 0;	
					return {'x':x,'y':y,'v':1000};
				}else if(isEnd()){
					step--;
					matrix[x][y]=0;	
					return {'x':x,'y':y,'v':0};
				}else{
					var v=worst().v;
					step--;
					matrix[x][y]=0;
					if(bestx==null || v>=bestv){
						bestx=x;
						besty=y;
						bestv=v;
					}
				}
			}
		}
	}
	return {'x':bestx,'y':besty,'v':bestv};
}
演算法的思路如下:首先我們遍歷棋盤陣列,然後判斷遍歷到的那格如果是空的(也就值是0)就先假設畫上,並且將在棋盤陣列中相應的位置設為1,表示電腦是下的,然後將走的步數+1。普通的操作就完了,接下來就是給下的這一步評分階段,程式碼如下:
if(win(x,y)){
	step--;
	matrix[x][y] = 0;	
	return {'x':x,'y':y,'v':1000};
}else if(isEnd()){
	step--;
	matrix[x][y]=0;	
	return {'x':x,'y':y,'v':0};
}else{
	var v=worst().v;
	step--;
	matrix[x][y]=0;
	if(bestx==null || v>=bestv){
		bestx=x;
		besty=y;
		bestv=v;
	}
}
首先我們判斷一下如果下了這一步,是否就贏了,如果是,就先把步數改回去,並且把棋盤陣列改為下這一步之前的棋盤陣列(因為我們在computerThink裡要改一道,所以先改回去,避免改重了),然後返回這一步的位置,並且評分為1000。最後這個過程用return來實現,return是神馬,我想就不用說了吧。判斷是否贏了,我們用了win函式,上面已經說過了。

但是萬一下了這一步沒贏怎麼辦,就接著判斷是否下了成平局,怎麼才能成平局呢?就是把整個棋盤佔滿且對方沒有贏,自己也沒有贏就是平局。由於如果別人贏了,就不會進行電腦AI,也就不會呼叫best函式,換句話說就是不可能進行到這一步;如果是電腦贏了,在上級判斷中已經做了相應操作而且用return已經推出函數了,也不會執行到此步,因此直接判斷佔滿沒有就可以了。因此用到isEnd函式,上面也用到過,並且講到過,這裡不羅嗦。

萬一上面的兩種情況都不對怎麼辦?那就隨便下個吧。但是隨便下也不能亂下。因此用到了worst來選擇“隨便下”最好的位置。程式碼如下:

function worst(){
	var bestx;
	var besty;
	var bestv = 0;
	for(var x=0;x<3;x++){
		for(var y=0;y<3;y++){
			if(matrix[x][y] == 0){
				matrix[x][y] = -1;
				step++;
				if(win(x,y)){
					step--;
					matrix[x][y] = 0;	
					return {'x':x,'y':y,'v':-1000};
				}else if(isEnd()){
					step--;
					matrix[x][y]=0;	
					return {'x':x,'y':y,'v':0};;
				}else{
					var v=best().v;
					step--;
					matrix[x][y]=0;
					if(bestx==null || v<=bestv){
						bestx=x;
						besty=y;
						bestv=v;
					}
				}
				
			}
		}
	}
	return {'x':bestx,'y':besty,'v':bestv};
}

這個函式和best是反著來的,它是假設下了某一步後,別人會贏或者平局。如果別人走那步會贏,就返回這個位置,把這個位置先佔住。平局和對方贏是一樣的原理,就是見哪裡不對就填哪裡。最後的判讀是在對方不可能贏的情況下采取的,就是通過best函式取最好的。這個best函式在上面講過了。不作解釋了~~

通過worst這個函式會返回幾個值,第一個和第二個是隨便下的位置,最後一個是評分。在best中我們把這幾個返回值接收到,並且通過評分判斷這個選擇是否比平局的結果還要差,再返回給computerThink這個函式來繪畫布局。因此這個過程很繞。大家要搞清楚關係,搞清楚了就不難了。

本次講解就講到這裡。多謝大家捧場!

若遊戲異常,請及時聯絡我。謝謝大家的支援!

----------------------------------------------------------------

歡迎大家轉載我的文章。

歡迎繼續關注我的部落格