剪刀、石頭、布 (Rock paper scissors)
JS 小遊戲 - 剪刀石頭布 開發筆記
專案簡介
剪刀石頭布小遊戲
功能需求:
- 玩家選擇一種拳打出,電腦會隨機選一種出
- 將每回合的結果印在畫面上並記錄分數
- 最新的一回合會渲染在最前方
練習目標:
使用原生 JS 進行第一次開發,再用 Vue 重構一次
JS 版
HTML布局部分為:
html
<div class="selections">
<button type="button" class="selection" data-selection="rock">✊</button>
<button type="button" class="selection" data-selection="paper">✋</button>
<button type="button" class="selection" data-selection="scissors">✌️</button>
</div>
<div class="results">
<div>
You
<span class="result-score" data-your-score>0</span>
</div>
<div data-final-column>
Computer
<span class="result-score" data-computer-score>0</span>
</div>
</div>
第一步先獲取所有 DOM 元素
javascript
const selectionButtons = document.querySelectorAll('[data-selection]')
const finalColumn = document.querySelector('[data-final-column]')
const computerScoreSpan = document.querySelector('[data-computer-score]')
const yourScoreSpan = document.querySelector('[data-your-score]')
再來定義一個常量資料記錄三種拳的資訊
javascript
const SELECTIONS = [
{
name: 'rock',
emoji: '✊',
beats: 'scissors'
},
{
name: 'paper',
emoji: '✋',
beats: 'rock'
},
{
name: 'scissors',
emoji: '✌️',
beats: 'paper'
}
]
遍歷所有按鈕並掛上事件偵聽器綁定一個點擊事件
javascript
selectionButtons.forEach(button => {
button.addEventListener('click', (e) => {
const selectionName = button.dataset.selection
const selection = SELECTIONS.find(selection => selection.name === selectionName)
// 將玩家的選擇當作 makeSelection 的參數傳入
makeSelection(selection)
})
})
接下來要定義核心的方法
javascript
function makeSelection(selection) {
// 需求:
// #1 取得電腦的選擇
// #2 檢查雙方是否獲勝
// #3 將結果渲染到畫面
// #4 計算得分
}
取得電腦的選擇
randomSelection()
javascriptfunction randomSelection() { const randomIndex = Math.floor(Math.random() * SELECTIONS.length) return SELECTIONS[randomIndex] }
檢查是否獲勝
isWinner()
javascriptfunction isWinner(selection, opponentSelection) { return selection.beats === opponentSelection.name }
將結果渲染到畫面
addSelectionResult()
javascriptfunction addSelectionResult(selection, winner) { const div = document.createElement('div') div.innerText = selection.emoji div.classList.add('result-selection') if (winner) div.classList.add('winner') // 將生成的 div 透過 after() 插入到 data-final-column 的後面 finalColumn.after(div) }
計算得分
incrementScore()
javascriptfunction incrementScore(scoreSpan) { scoreSpan.innerText = parseInt(scoreSpan.innerText) + 1 }
完成業務:
javascript
function makeSelection(selection) {
const computerSelection = randomSelection()
const yourWinner = isWinner(selection, computerSelection)
const computerWinner = isWinner(computerSelection, selection)
// 註: 要從後面插入,所以要將電腦的結果先渲染在畫面
addSelectionResult(computerSelection, computerWinner)
addSelectionResult(selection, yourWinner)
if (yourWinner) incrementScore(yourScoreSpan)
if (computerWinner) incrementScore(computerScoreSpan)
}
Vue 版
在透過 Vue 重構此專案時沒注意踩了一個坑,由於是透過 v-for 遍歷所有結果的,如果沒有掛上唯一的 key 值會出現 bug。所以我額外引入了 nanoid 套件替每回合的結果掛上唯一的 id 值。
HTML的重構:
html
<div id="app">
<div class="selections">
<button
type="button"
class="selection"
v-for="item in SELECTIONS" :key="item.name"
@click="makeSelection(item)"
>
{{ item.emoji }}
</button>
</div>
<div class="results">
<div>
You
<span class="result-score">{{ yourScore }}</span>
</div>
<div>
Computer
<span class="result-score">{{ computerScore }}</span>
</div>
<div
class="result-selection"
:class="{winner: result?.winner}"
v-for="(result) in results" :key="result.id"
>
{{ result.emoji }}
</div>
</div>
</div>
JS 的重構:
引入 Vue 3 CDN 來開發
javascript
import { createApp, ref } from 'https://cdnjs.cloudflare.com/ajax/libs/vue/3.2.31/vue.esm-browser.min.js'
import { nanoid } from 'https://cdn.jsdelivr.net/npm/nanoid/nanoid.js'
const app = createApp({
setup() {
const SELECTIONS = [
{
name: 'rock',
emoji: '✊',
beats: 'scissors'
},
{
name: 'paper',
emoji: '✋',
beats: 'rock'
},
{
name: 'scissors',
emoji: '✌️',
beats: 'paper'
}
]
const yourScore = ref(0)
const computerScore = ref(0)
const results = ref([])
// player's choice
const makeSelection = (selection) => {
// #1 得到雙方出的拳資料
const yourSelection = { ...selection }
const computerSelection = randomSelection()
// #2 檢查是否獲勝
const yourWinner = isWinner(yourSelection, computerSelection)
const computerWinner = isWinner(computerSelection, yourSelection)
// #3 將結果推入 results
// #3-1 將獲勝者加上 winner 屬性
if (yourWinner) {
yourSelection.winner = true
yourScore.value++
}
if (computerWinner) {
computerSelection.winner = true
computerScore.value++
}
// #3-2 將結果掛上唯一 id
yourSelection.id = nanoid()
computerSelection.id = nanoid()
// #3-3 插入到陣列
results.value.unshift(computerSelection)
results.value.unshift(yourSelection)
}
// computer's choice (random)
const randomSelection = () => {
const randomIndex = Math.floor(Math.random() * SELECTIONS.length)
return { ...SELECTIONS[randomIndex] }
}
// check win
const isWinner = (selection, opponentSelection) => {
return selection.beats === opponentSelection.name
}
return {
SELECTIONS,
yourScore,
computerScore,
results,
makeSelection
}
}
})
app.mount('#app')
注意點:
由於物件的傳參考特性,如果不把傳入的選擇或是電腦隨機的選擇做一個拷貝,會造成 SELECTIONS 的資料遭到修改導致動態加入的 id 被寫進去造成遊戲崩潰,所以要先進行複製後再將結果推進 results 陣列中去做列表渲染。