Vue.js小遊戲實作-60秒算數挑戰

大家好,我在六角地下城6f,這樓的挑戰是實作小遊戲,題目的邏輯是在六十秒內會產生隨機兩個數字去隨機做加減乘除, 答對就加一分,答錯就扣一分,秒數越少,題目越困難,得分就越高分。

題目詳細規則如下:

  • 0~20 秒為 1位數計算 (ex. 5-3)
  • 21~40 秒為 2 位數計算 (ex. 30*19)
  • 41~60 秒為 3 位數計算 (ex. 332+312)
  • 加減乘除規則請用隨機產生,不可寫死題目,60 秒內可無限次數答題。
  • 0~40 秒答對加一分
  • 41~60 秒答對加五分
  • 答錯扣一分,最多僅能扣到零分

我打算在這個專案做邏輯梳理的練習,因此會盡量把有機會重複使用的部分拆成methods,這個專案只會講述核心部分的邏輯,也就是如何產生題目,以及一些可能的問題點,其他細節部分希望你自己做做看,你可以在這裡看到我實做的成果。或是也可以直接參考原始碼

規則流程-運算:

  1. 產生隨機兩個數字
  2. 產生運算符號(加減乘除)
  3. 等待使用者輸入
  4. 檢查正確答案與使用者輸入是否符合
  5. 產生下一道題目

規則流程-時間:

  1. 挑戰者按下開始後,切換頁面並開始倒數
  2. 倒數60秒
  3. 檢查現在秒數,根據不同秒數決定不同難度的題目及得分
  4. 60秒倒數完畢後,切換顯示頁面,不讓挑戰者繼續輸入
  5. 挑戰者查看得分,等待按下「再次挑戰」後重新此流程

需特別注意的規則細節:

因為數字是隨機產生,因此在某些情況下要注意,否則會有負數產生:

  1. 減法時,會有前後數字大小的問題
  2. 除法時,會有數字大小以及除不盡的問題
  3. 最後挑戰者重新開始時記得clearInterval

專案實作 - 基本配置

這個專案我是使用vue-cli,因為規模並不是很大,也不能跳頁,因此不打算另外切出Component。

資料結構的部分,因為會有重置資料的需求,我把原本習慣寫在data()裡面的資料拉出來,放在獨立的function回傳,這樣我就可以輕易的拿到預設的資料。

// outside Vue instance
function initData(){
    return {
     pages:['start','game','result'],
     operatorList:['+','-','x','÷'],
    operator:'+',
    currentPage:'start',
    score: '000',
    time_remain:60,
    currentTime:'00:00',
    quizNumbers:[],    }
}

// inside Vue instance
export default {
  name: 'app',
  data(){
    return initData()
  }
}

已知狀態有三種,因此我直接根據不同狀態去顯示不同頁面,這邊寫法可以依照自己習慣去更改:

 <div :class="pages[0]" v-if="currentPage==pages[0]"  >
 ...
 </div>
 
  <div :class="pages[1]" v-if="currentPage==pages[1]" >
  ...
  </div>

  <div :class="pages[2]" v-if="currentPage==pages[2]" >
    ...
  </div>

專案實作 - 邏輯實現

倒數方法 - 使用setTimeInterval

要能夠時線倒數,代表必須每隔一秒改變畫面上的時間值,這裡用setTimeInterval最適合不過了,他會每隔一段時間去執行寫入的方法:

methods:{
    ...
     timeReducer(){
         let timer =  setInterval(()=>{
          if( this.time_remain>0){
            this.currentTime = this.convertSeconds(this.time_remain -=1 )
          }else{
            clearInterval(timer)
            this.currentPage= this.pages[2]
          }
        }, 1000)
        return timer
    },
    ...
}

但在使用完後記得要清除這個interval,因為他會一直執行,如果沒有清除,在挑戰者再次開始遊戲時,如果沒有清楚,就會有兩個interval重疊,遊戲就無法順利進行,這邊我是在倒數完之後在setTimeInterval裡面直接使用clearInterval清除。

隨機產生運算子

因為加減乘除運算子必須隨機產生,所以我將這四種運算方式寫在陣列裡面(operatorList),然後用JS的random方法去隨機挑選這個陣列的元素:


methods:{
    ...
    randomOperator(){
      let order = Math.floor(Math.random()*100) % 4 
      this.operator = this.operatorList[order]
    }
    ...
}

取得不同位數的數字

依照上面整理的規則,我們第一個會需要處理的部份是隨機產生數字,JS裡面可以用Math.random() 去解決,但要注意這個方法回傳的值是介於0~1之間的浮點數,後面要自己去調整才能拿到自己想要的格式。

以及要考量到可能會有需要拿不同位數數字的需求,所以在這個method我用digits當作參數來表示欲取得的位數:

 // inside Vue instance 
 ...methods:{
     ...
     getDigits(digits){
      switch(digits){
        case 1 : return Math.floor((Math.random() * 10) + 1) ; 
        case 2 : return Math.floor((Math.random() * 100) + 10) ; 
        case 3 : return Math.floor((Math.random() * 100) + 100) ; 
      }
    },
 } 

取得數字之後的處理

加上setQuizNumber方法,根據不同時間取得不同位數的數字:

    //setQuizNumber
    let digit;
      switch (true){
        case (this.time_remain>=40 && this.time_remain<=60): {
              digit = 1
              break; 
            }
        case (this.time_remain>=20 && this.time_remain<=40): {
              digit = 2
              break;
            }
        case (this.time_remain>=0 && this.time_remain<=20): {
              digit = 3
            break;
        }    
      }
      
      let result = []
      let firstNum=this.getDigits(digit); 
      let secondNum=this.getDigits(digit); 

我們不想要答案有負數或小數點產生,所以在減法時,必須注意第一個數字是否大於第二個,至於加法跟乘法就沒有這個問題,所以我加上判斷,如果第一個數大於第二個,就做swap交換:

    //setQuizNumber
      if(firstNum < secondNum){
        let temp = firstNum
        firstNum = secondNum 
        secondNum = temp 
      }

最後為了在除法時,讓兩數是可以整除的,所以必須確保第二個數字是第一個數字的因數,以及不能有質數出現,我加入的判斷質數的方法isPrime跟取得數字所有因數的方法getFactors,以及後面會有從因數裡面雖機挑選一個數字的需求,所已加入陣列隨機挑選元素的方法getArrayRandomItem:

methods:{
    ...
    isPrime(num) {
      for(var i = 2; i < num; i++)
        if(num % i === 0) return false;
      return num > 1;
    },
    getFactors(number,digits){
      let factors =  Array
        .from(Array(number + 1), (_, i) => i)
        .filter(i => number % i === 0)

      if(digits){
        factors = factors.filter(factor=>(factor+'').length===digits)
      }
        return factors
    },
    getArrayRandomItem(array){
      return  array[Math.floor(Math.random()*array.length)];
    },
    ... 
}

最後過濾掉質數後,從第一個數字的因數(陣列)裡面去隨機選出一個數當作第二個數字,就可以送出了:

 //setQuizNumber
 if(this.operator=='÷' && ( firstNum % secondNum!==0)){
        while(this.isPrime(firstNum)) firstNum = this.getDigits(digit); 
        while(this.isPrime(secondNum)) secondNum = this.getDigits(digit); 
        
        secondNum = this.getArrayRandomItem(this.getFactors(firstNum,digit))
        
      } 
      result = result.concat([firstNum,secondNum]) 
      
      this.quizNumbers = result

做到這裡產生題目的邏輯就完成了。

再次挑戰- 重置所有資料

還記得我一開始把原始狀態拉出來單獨放在function裡嗎?現在就可以拿來用了,使用Object.assign,可以直接對目標物件赴值,如果屬性重複,以參數越後面屬性的值為主(後面寫入的會蓋掉前面的),可以參考 官方文件,順帶一提Object.assign不支援深度拷貝,如果你要複製的物件裡面還要物件,要特別注意,他只會複製那個物件的參考,後面你只要改動到該子物件,則所有該物件的參考都會跟著一起被改變。(參考官方文件

methods:{
    ...
    onResetClick(){
      Object.assign(this.$data, initData());
      this.currentPage = this.pages[0]
    },
    ... 
}

this.$data是Vue裡面提供取得data物件的api,詳細可參考官方文件
到這邊所有核心的邏輯都講完了,再提一次我並沒有從頭到尾講得很詳細,希望你試著自己去理清楚來龍去脈,自己識做看看,若還是看不懂也沒關係,可以參考我的原始碼,在自己做做看,有任何問題可以一起討論,感謝你的收看,下次見啦。

Your browser is out-of-date!

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

×