大家好,我在地下城8樓,題目是3x3井字遊戲,直覺想到最簡單的方式就是暴力檢查,每個Row跟Column去檢查有沒有相等的,最後加上對角線兩條。也看到別樓有分享Alex1&10解法,覺得很高明。
不過我想看看有沒有不一樣的做法,相信一定有。剛剛講到Row跟Column,就讓我想到二維陣列。沒錯,整個井字窗格根本可以用座標來看,今天這個題目的解法會以二維陣列為主軸來解說作法,切版的部分則比較繁瑣且不難所以不會多提,有問題可以再跟我討論。
規則
-
先手為 O,後手為 X,某方獲勝時,上方會紀錄各方的獲勝戰績
-
每回合結束後,會判定結果頁(平手、O獲勝、X獲勝)
-
需符合 RWD,能在低螢幕解析度也能遊玩,介面不能超出 x 軸,至少在以下解析度能夠遊玩:
- iPhone SE 320px
- iPhone 8 375px
- iPhone PLUS 414px
-
請使用瀏覽器離線儲存技術,將戰績保留起來
資料結構
為了讓對應到畫面的資料可以更直觀的操作跟查詢,我是這樣訂定格式的:
data(){
return {
...
currentPlayer:0,
gameStatus:[[null,null,null],
[null,null,null],
[null,null,null]]
}}
陣列第0個元素代表的是第一個Row,Row本身也是陣列,剛好由左到右對應到0、1、2的位置,(0,0)對應到最左上,(2,2)對應到最右下,這樣直觀多了,如下圖所示:
然後用0表示O、1表示X,與上述資料結構的currentPlayer一樣,null則表示尚未下棋的位置,不過這邊不建議用0來做任何的判斷,原因稍後再提。
判斷勝利者
因為我用的資料表示是: O為0,X為1,因此如果整列、整行或交叉相加都是0,或是3,代表有人勝出,這時我只要取得currentPlayer就知道獲勝的是誰了。
判斷勝利邏輯
要判斷目前賽況是否有人勝出,我們需要檢查的部分有:
- 每個Row(橫排)是否有連線
- 每個Column(直行)是否有連線
- 對角線是否有連線
判斷對角線的部分比較麻煩,但你應該可以看出,使用陣列的方式可以清楚區分出Row跟Column,讓檢查更方便,檢查的時間點是每次使用者點完要下的棋格之後的瞬間,檢查完最後切換使用者(玩家)。
methods:{
...
onSectionClick(x,y){
if (this.gameStatus[y][x]!==null) return //防止重複下棋
this.gameStatus[y][x] = this.currentPlayer//下棋
this.checkResult()//檢查賽況
this.togglePlayer()//<---下完後切換使用者
},}
因為是二維陣列所以要兩層for迴圈,注意這邊因為要先取用Row,所以陣列的操作必須先從y (也就是直排) 開始選取,而不是一般座標的表示方式(x,y),當然可以透過改變資料結構做出比較直觀的使用方式,不過因為沒有很困難,且為了解說方便,這邊就不做調整。
let status = this.gameStatus
for(let y=0; y<status.length; y++){
for(let x=0; x<status[y].length; x++ ){
// 用status[y][x]取得棋格元素 <-----重要
}
}
知道取得陣列的方式之後就可以開始逐行逐列檢查啦!
檢查Row / Column是否有連線
想要取得Row整行的和,每次的y必須固定,我是在x為0的時候,取得後面兩個x+1、x+2元素,相加後算出總和,。
methods:{
...
getSumOfRow(x,y){
let status = this.gameStatus
if(x!==0 ) return
return status[y][x]+
status[y][x+1]+
status[y][x+2]
},}
Coloum總和則為x y與上述相反:
getSumOfColumn(x,y){
let status = this.gameStatus
if(y!==0 ) return
return status[y][x]+
status[y+1][x]+
status[y+2][x]
},
取得總合後判斷是否有0或是3就知道有無勝出。
檢查對角線是否有連線
一樣是在迴圈一開始x為0以及y為0的時候做判斷,在取得總和之前,我先看看是不是整個對角線都已經被下過棋了,如果沒有,則直接return 跳出不做任何動作。
methods:{
getSumOfNegativeSlash(x,y){
let status = this.gameStatus
if(x!==0 || y!==0 ) return
if( !(this.isNumber(status[y][x]) &&
this.isNumber(status[y+1][x+1]) &&
this.isNumber(status[y+2][x+2]))) return
return status[y][x]+ status[y+1][x+1]+status[y+2][x+2]
},
}
另一個方向的對角線是同樣的做法,看到這裡的你應該可以推算出邏輯,如果還是不懂,可以參考文章開頭處的專案原始碼。
整合所有的判斷邏輯 - checkResult method
最後整個判斷邏輯如下,我把三種情況(Row / Column / 對腳線)個別寫下條件之後用OR串起來,只要狀況符合其中一種條件,就會結束賽局。
checkResult(){
let status = this.gameStatus
for(let y=0; y<status.length; y++){
for(let x=0; x<status[y].length; x++ ){
let sumOfCol = this.getSumOfColumn(x,y)
let sumOfRow = this.getSumOfRow(x,y)
if(
( this.isRowAllNumber(x,y) &&
sumOfRow=== 0||sumOfRow=== 3
)||(
this.isColumnAllNumber(x,y) &&
sumOfCol === 0||sumOfCol === 3
)||(
this.getSumOfNegativeSlash(x,y)===0 ||
this.getSumOfNegativeSlash(x,y)===3 ||
this.getSumOfPositiveSlash(x,y)===0 ||
this.getSumOfPositiveSlash(x,y)===3
)){
this.setScore()
this.goResultPage(this.currentPlayer)
}
}
}
this.checkIfFinish()
},
這邊有一個蠻麻煩的雷點就是js裡面null+0居然還是0,害我沒辦法直接正確判斷結果,導致我還寫了一個isRowAllNumber來判斷該列/欄是否全是數字,所以不推薦用0跟null搭配來當做相加的判斷。(你可以打開開發者工具試試看)
使用localStorage紀錄分數
這邊我使用localStorage來永久儲存比數,localStorage操作方式有以下幾種:
- localStorage.setItem(‘key’,‘content’)
- localStorage.getItem(‘key’)
- localStorage.removeItem(‘key’)
瀏覽器上有另外一個操作方式很類似的api叫做sessionStorage,差別就在於localStorage除非主動刪除,否則會永久保留在瀏覽器、sessionStorage則是在分頁關閉後就會被刪除。
我把上面三個api另外包成三個function,讓我在各個元件可以方便使用,統一使用方式,以下是setItem的包裝:
export const setStorage = (name, content) => {
if (!name) return;
if (typeof content !== "string") {
content = JSON.stringify(content);
}
window.localStorage.setItem(name, content);
};
getStorage、removeStorage則是類似的方式,在此就不多提。
多思考一些 - 重置分數按鈕
這邊思考的是,既然可以儲存戰績,那如果可以讓玩家手動清除分數,應該會更好用,很多遊戲也有這種功能。所以我將原本入口的START按鈕改為CONTINUE跟RESTART,如果兩邊分數為0才只顯示START。
雖然是小地方但可以增加樂趣,算是一點小優化,給有興趣的人參考。