JS 30 是由加拿大的全端工程師 Wes Bos 免費提供的 JavaScript 簡單應用課程,課程主打 No Frameworks、No Compilers、No Libraries、No Boilerplate 在30天的30部教學影片裡,建立30個JavaScript的有趣小東西。
另外,Wes Bos 也很無私地在 Github 上公開了所有 JS 30 課程的程式碼,有興趣的話可以去 fork 或下載。
最後一天要實作的內容是"網頁版打地鼠"。(下面是實際網頁效果的gif圖)

<h1>是頁面的標題,旁邊的<span class="score">是打地鼠遊戲的記分板。
<button>在滑鼠點擊時,會呼叫startGame()方法,開始打地鼠遊戲。
<div class="game">是打地鼠遊戲的主體,內部有<dvi class="hole">共計六個洞,每個洞(.hole)都有一隻初始被隱藏的地鼠(<div class="mole">)。
<h1>Whack-a-mole! <span class="score">0</span></h1>
<button onClick="startGame()">Start!</button>
<div class="game">
    <div class="hole hole1">
      <div class="mole"></div>
    </div>
    <div class="hole hole2">
      <div class="mole"></div>
    </div>
    <div class="hole hole3">
      <div class="mole"></div>
    </div>
    <div class="hole hole4">
      <div class="mole"></div>
    </div>
    <div class="hole hole5">
      <div class="mole"></div>
    </div>
    <div class="hole hole6">
      <div class="mole"></div>
    </div>
</div>
初始每一隻地鼠(.mole)都是採用絕對定位(position:absolute),然後將top指定為100%,把地鼠(.mole)隱藏起來。
在遊戲過程中,我們會為地洞(.hole)添加.up這個 CSS class 選擇器,讓地鼠探頭出來給我們打 XD。
.mole {
  /*上略...*/
  position: absolute;
  top: 100%;
  /*下略...*/
}
.hole.up .mole {
  top: 0;
}
宣告常數holes取得所有的地洞(.hole),資料型態是 NodeList。
宣告常數scoreBoard取得頁面中顯示的分數(.score)。
宣告常數mole取得所有的地鼠(.mole),資料型態是 NodeList。
const holes = document.querySelectorAll('.hole'); //NodeList
const scoreBoard = document.querySelector('.score');
const moles = document.querySelectorAll('.mole');
撰寫ranTime()幫我們決定地鼠出現的持續時間並給定參數min、max作為出現持續時間範圍的最小、最大值。
下面用Math.random()隨機產生一個介於0~1的數字,然後把它乘上(max-min)再加上min,最後用Math.round()四捨五入得到隨機的出現持續時間。
function randTime(min,max){
    return Math.round(Math.random() * (max-min) + min);
}
randomHole()幫我們隨機選擇地鼠出現的洞,為避免接連選到兩次一樣的洞,所以另外宣告變數lastHole,來幫我們記住上一次出現的洞。
在方法裡,首先要傳入所有洞穴(holes,NodeList),接著一樣用Math.random()隨機產生0~1的數字並乘上holes的長度後,呼叫Math.floor()無條件捨去小數點,取得一個隨機的index放入常數idx中。
然後,宣告的常數hole就可以用這個idx,隨機取得holes中的一個洞穴。
為避免選到和上次一樣的洞穴,利用條件判斷hole是不是跟lastHole相同,如果相同就遞迴呼叫randomHole(),直至選到不同的洞為止。
在方法的最後,把這次的結果放到lastHole中,之後回傳被隨機選到的hole。
let lastHole;
function randomHole(holes){
    const idx = Math.floor(Math.random() * holes.length);
    const hole = holes[idx]
    if(hole === lastHole){
      console.log('You got the same hole.');
      return randomHole(holes);
    }
    lastHole = hole;
    return hole;
}
宣告變數timeup作為遊戲是否已經結束的flag。
peep()是讓地鼠從洞穴探頭出來的關鍵!!!
在peep()的最一開始,宣告常數time並取得由randTime(200,1000)隨機產生的地鼠持續出現時間,接著宣告常數hole取得由randomHole(holes)隨機挑出的地洞。
隨機挑出的hole會被添加.up這個 class,目的是讓躲在洞中的地鼠探出頭來。
如果超過地鼠持續出現的時間,地鼠就應該要重新回到洞中。所以這邊使用setTimeout(),在經過地鼠出現持續時間time後,移除hole上的.up,讓地鼠順利回家。在遊戲未結束(timeup = false)且前一隻地鼠已經回家的狀態下,我們會重新呼叫peep(),讓下一隻地鼠探頭出來。
let timeup = false;
function peep(){
    const time = randTime(200,1000);
    const hole = randomHole(holes);
    hole.classList.add('up');
    setTimeout(()=>{
      hole.classList.remove('up');
      if(!timeup) peep();
    },time);
}
宣告變數score給定初始值0,用來幫我們算分數。
startGame()可以讓我們在點擊頁面上的button後,立即開始打地鼠遊戲。
在方法的一開始,先把記分板(scoreBoard)歸零,然後把代表結束遊戲與否的timeup設成flase,同時也把計算的分數(score)歸零。
上面都做完後,呼叫peep()讓地鼠開始探頭出來給我們打,接著利用setTimeout()訂定打地鼠遊戲的時間限制,這邊設定遊戲時間為15秒(15000毫秒),15秒後把timeup設為true並提示使用者遊戲結束。
let score = 0;
function startGame(){
    scoreBoard.textContent = 0;
    timeup = false;
    score = 0;
    peep();
    setTimeout(()=>{
      timeup = true;
      alert("時間到,遊戲結束!!!");
    } , 15000);
}
最後一部分要來處理的是"當地鼠被點擊到,頁面上的得分要加1,然後地鼠要縮回洞中"。
我們可以為每一隻地鼠(mole)註冊click event listener以bonk()作為event handler。
在bonk()裡,首先判斷點擊是不是"人為"的,如果是使用者點擊觸發,則e.isTrusted會回傳true,而如果是用像是script之類的去觸發click event,則會回傳false並直接停止往下執行方法。
接著,在每一次點擊成功後,把分數(score)加1並移除加到地鼠(mole)上的.up,讓地鼠回到洞中,之後更新頁面上的得分(scoreBoard)就完成了。
function bonk(e){
    if(!e.isTrusted) return; //cheater
    score++;
    this.classList.remove('up');
    scoreBoard.textContent = score;
}
moles.forEach(mole => mole.addEventListener('click',bonk));
Math.random()
Math.round()
Math.floor()
setTimeout()
Element.classList
Node.textContent
Event.isTrusted
 (ps. 有空應該會再發一篇參賽心得吧! XD)
 (ps. 有空應該會再發一篇參賽心得吧! XD)