【井字游戲】做一款回憶童年的游戲


99% of information we read, we forget anyway. The best way to remember is to "DO".

體驗地址:http://www.hoohack.me/assets/tictactoe/

游戲完整的代碼在我的 github 上,有興趣也可以圍觀一下:TicTacToe,也希望大家可以點個 star。

緣起

最近在FreeCodeCamp上面學習前端知識,不知不覺已經學到了319課,現在遇到的一個小project是做一款井字游戲。說起井字游戲,真是滿滿的童年味道,還記得最瘋狂的時候是小時候跟同桌拿着一張草稿紙就能玩一節課,回到家跟弟弟也能繼續玩,對於沒有太多娛樂節目的童年來說,真是一款玩不厭的小游戲。這款游戲代碼比較簡單,主要是掌握算法的原理,但是也有一些需要注意的地方,於是想把自己遇到的問題記錄下來。

游戲界面

進入正題。項目的效果圖如下:

 

 

FreeCodeCamp上要求不能查看源碼來實現,於是便想着先把頁面做出來。看到井字格子,就想着用9個li,然后設置li的邊框作為井字線。於是用了一個div包住一個ul,里面有9個li。

游戲有一個開始界面可供選擇玩家的角色,然后選擇先手是哪一方,接着開始游戲。選擇界面做了一個遮罩層,里面提供給用戶選擇,選擇之后便把遮罩層隱藏並開始游戲。

井字游戲算法

算法參考了這篇文章。但里面的圖片看不到了,筆者根據自己的理解再解釋一遍,並配上一些圖片。

這次做的是人機對戰,因此就需要寫出比較智能的算法。首先,設計者要懂得玩游戲,有自己的策略,接下來就是將自己的策略付諸實現。

從下圖可以看到,整個棋盤可以連接處8條線,即一共有8種取勝可能:

 

在下棋過程中,一共有下面幾種狀態:
1、開局第一步
2、第二步
3、攻擊
4、防守
5、垃圾時間

 

1、開局第一步,這一步有兩種情況

A、如果先手是電腦,那么就將棋子下在中心位置,如圖:

 

B、如果先手是玩家,那么有下面三種情況要考慮
如果玩家在中心位置,那么電腦必須落在四個角位,因為如果不落在角位,那么就會出現必輸的情形。假設現在用1-9表示9個棋位。如下圖所示,如果玩家第一步在中心位置,第二步電腦落在第2位的棱位(圖中的2),第三步玩家只需要在第7或第9位下棋(圖中的3),第四步電腦必須在1或3位,第五步玩家跟進在7或9,則第六步電腦必須在1或3,那么第七步玩家只需要在4或者6下棋就可以贏了。

如果玩家在棱位/角位,那么電腦需要在中心位置下棋,在保證不輸的情況下反擊。

2、第二步棋(先角原則)

根據上面的分析,如果先手是玩家且玩家落棋在中心位置,為了避免必輸的情形,電腦需要落棋在角上。而如果先手是電腦,那么如果玩家落在棱位,電腦落在角位讓必輸的情形屬於玩家。

3、攻擊

檢測棋盤,如果有兩枚己方的棋子連在一起且連線中仍有空位,那么就落棋在該位。

4、防守

檢測棋盤,如果有對方的兩枚棋子連在一起且連線中仍有空位,那么就落棋在該位。

5、垃圾時間

當不需要攻擊也不需要防守的時候,那就隨便找個位置下棋,盡可能找到連線中還有兩個空位的位置。

特殊情況
有一種特殊情況是不能執行先角原則的,如下圖所示,第一步,玩家先下棋在1,第二步,電腦根據開局第一步的規則下棋在中心位置5,第三步,玩家在1的對角位置9下棋,根據先角原則,第四步電腦將落在3或者7的棋位,第五步玩家在7或者3的位置封堵電腦,那么此時電腦就輸了。唯有此種情況不能執行先角原則,所以在非攻擊且非防守的時候要先排除掉此情況。

 

具體實現

說了那么多,可能比較枯燥,下面介紹一下具體的代碼實現。

使用一個二維數組panel保存棋盤的狀態,1是電腦的值,-1是玩家的值。
winArr保存所有可能贏的8個棋位組合;維護computerWin和userWin,初始值等於winArr,當電腦或玩家每次下棋時,都分別更新這兩個數組,刪除掉不能贏的棋位組合。在更新panel的時候會分別更新computerWin和userWin。

核心的方法是play,play的執行步驟偽代碼如下:

如果可以攻擊
遍歷computerWin數組,找到可以攻擊的棋位,下棋,顯示是否贏了。
不能攻擊,如果需要防守
遍歷userWin,根據玩家可贏的組合,找出需要防守的棋位,下棋,更新panel;
不需要防守,如果是電腦先手的第一步
在中心位置下棋,更新panel;
不是先手第一步
如果中心位置沒有被占去,在中心位置下棋,更新panel;返回
如果是特殊情況,在棱位下棋,更新panel; 返回
如果角位仍有位置,選擇一個角位下棋,更新panel; 返回
最后一種情況,找到剩余的空位,優先選擇位於computerWin的空位,下棋,更新panel; 返回

 

play算法的實現如下:

if(canAttack()) {
console.log(
"attack");
var attackPos = findAttackPos();
updatePanel(attackPos, computerVal);
}
else if(needDefend()) {
console.log(
"defend");
var defendPos = findDefendPos();
updatePanel(defendPos, computerVal);
}
else if(firstStep()) {
console.log(
"first");
updatePanel(firstPos, computerVal);
running
= true;
}
else {
console.log(
"other");
if(panel[1][1] == 0) {
updatePanel(firstPos, computerVal);
return;
}
if(special()) {
console.log(
'special');
var pos = findSpecialPos();
updatePanel(pos, computerVal);
return;
}
var random = Math.floor(Math.random() * 2);
if(panel[0][0] == 0 && panel[2][2] == 0) {
var pos = (random == 0) ? 0 : 8;
updatePanel(pos, computerVal);
}
else if(panel[0][2] == 0 && panel[2][0] == 0) {
var pos = (random == 0) ? 2: 6;
updatePanel(pos, computerVal);
}
else {
var otherPos = findEmptyPos();
updatePanel(otherPos, computerVal);
}
}

 

總結

在編碼的過程中遇到的一個難題就是JavaScript的數組對象,我在第一次調用play方法開頭輸出panel的時候,得到的是play執行后panel的值,后來請教一位大神,發現是因為panel是一個對象,因為對象遍歷引用的都是同一塊內存地址,所以一旦有改變,就全部改了。如果直接使用下標輸出每一個值的話是可以得到初始的值的,也可以用JSON方法將數組字符串,然后打印出來查看結果。

另外,也學會了如何在JavaScript里面封裝一個類,將私有方法寫在類的外面,需要暴露的方法寫在類里面。當然,還有很多需要學習的地方。繼續學習。

有時候一些東西看起來很簡單,或者聽到了很多次,心里面覺得實現起來應該很簡單的,沒什么了不起,覺得不以為然,但只有真正去實踐出來的時候才能體會到其中的樂趣和思想,才能真正的掌握。所以,盡情的去DO。

 

本文較短,如果還有什么疑問或者建議,可以多多交流,原創文章,文筆有限,才疏學淺,文中若有不正之處,萬望告知。

如果本文對你有幫助,望大力點推薦。


注意!

本站转载的文章为个人学习借鉴使用,本站对版权不负任何法律责任。如果侵犯了您的隐私权益,请联系我们删除。



 
粤ICP备14056181号  © 2014-2021 ITdaan.com