大家好,我在六角地下城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,這個專案只會講述核心部分的邏輯,也就是如何產生題目,以及一些可能的問題點,其他細節部分希望你自己做做看,你可以在這裡看到我實做的成果。或是也可以直接參考原始碼。
規則流程-運算:
- 產生隨機兩個數字
- 產生運算符號(加減乘除)
- 等待使用者輸入
- 檢查正確答案與使用者輸入是否符合
- 產生下一道題目
規則流程-時間:
- 挑戰者按下開始後,切換頁面並開始倒數
- 倒數60秒
- 檢查現在秒數,根據不同秒數決定不同難度的題目及得分
- 60秒倒數完畢後,切換顯示頁面,不讓挑戰者繼續輸入
- 挑戰者查看得分,等待按下「再次挑戰」後重新此流程
需特別注意的規則細節:
因為數字是隨機產生,因此在某些情況下要注意,否則會有負數產生:
- 減法時,會有前後數字大小的問題
- 除法時,會有數字大小以及除不盡的問題
- 最後挑戰者重新開始時記得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,詳細可參考官方文件。
到這邊所有核心的邏輯都講完了,再提一次我並沒有從頭到尾講得很詳細,希望你試著自己去理清楚來龍去脈,自己識做看看,若還是看不懂也沒關係,可以參考我的原始碼,在自己做做看,有任何問題可以一起討論,感謝你的收看,下次見啦。