Vue.js 井字遊戲 - OOXX

大家好,我在地下城8樓,題目是3x3井字遊戲,直覺想到最簡單的方式就是暴力檢查,每個Row跟Column去檢查有沒有相等的,最後加上對角線兩條。也看到別樓有分享Alex1&10解法,覺得很高明。

不過我想看看有沒有不一樣的做法,相信一定有。剛剛講到Row跟Column,就讓我想到二維陣列。沒錯,整個井字窗格根本可以用座標來看,今天這個題目的解法會以二維陣列為主軸來解說作法,切版的部分則比較繁瑣且不難所以不會多提,有問題可以再跟我討論。

你可以在這裡看到我的成果,或是參考原始碼

規則

  • 先手為 O,後手為 X,某方獲勝時,上方會紀錄各方的獲勝戰績

  • 每回合結束後,會判定結果頁(平手、O獲勝、X獲勝)

  • 需符合 RWD,能在低螢幕解析度也能遊玩,介面不能超出 x 軸,至少在以下解析度能夠遊玩:

    1. iPhone SE 320px
    2. iPhone 8 375px
    3. 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。

雖然是小地方但可以增加樂趣,算是一點小優化,給有興趣的人參考。

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×