# 使用事件建立遊戲

## 課前測驗

[課前測驗](https://ashy-river-0debb7803.1.azurestaticapps.net/quiz/21?loc=zh_tw)

## 事件驅動程式設計

當我們建立專為瀏覽器設計的應用程式時，我們會提供 Graphical User Interface (GUI) 給用戶使用，在我們建立的格式上進行互動。最常見的互動方式是透過點擊或輸入在多樣的物件。開發者面臨的問題是，我們不了解用戶會何時對這些物件產生互動！

[事件驅動程式設計](https://zh.wikipedia.org/zh-tw/%E4%BA%8B%E4%BB%B6%E9%A9%85%E5%8B%95%E7%A8%8B%E5%BC%8F%E8%A8%AD%E8%A8%88)是一種程式設計的方式，以建立我們的 GUI。若拆解該名詞的話，我們知道主軸關鍵會是**事件（Event）**。根據 Merriam-Webster，[事件](https://www.merriam-webster.com/dictionary/event)定義為「將發生的事」。它能有效地解決我們面臨的問題。我們知道當用戶產生互動時，什麼程式必須回應其要求，只差在我們不知道用戶會何時產生互動。

藉由建立新的函式，我們可以標記這段將被運行的程式碼。我們回顧一下[程序式程式設計](https://zh.wikipedia.org/wiki/%E8%BF%87%E7%A8%8B%E5%BC%8F%E7%BC%96%E7%A8%8B)，函式會依照順序一行一行的被運行。這同樣也會被實踐在事件驅動程式設計上，差別在於**如何**去呼叫這些函式。

要處理這些事件：點擊按鈕、輸入字串等等，我們需註冊**事件監聽者（Event Listeners）**。事件監聽者是函式之一，負責回應當事件觸發時，提供相對應的回應。事件監聽者可以根據用戶的行為，更新使用者介面，呼叫伺服器，或是任何你想要它做的事。我們利用 [addEventListener](https://developer.mozilla.org/docs/Web/API/EventTarget/addEventListener) 新增事件監聽者，提供要被運行的函式。

> **注意** 值得注意我們有許多建立事件監聽者的方式。你可以使用匿名函式（anonymous functions），或是有名字的；你可以使用多種的快捷，好比直接設定 `click` 屬性，或使用 `addEventListener`。在我們練習過程中，主要專注在 `addEventLister` 與匿名函式上，它們可能是開發者最常見的網頁開發技巧。同時，也是彈性最高的： `addEventListener` 作用在任何事件，任何以參數方式輸入的事件名稱。

### 常見事件

創造應用時，這邊有[數種事件](https://developer.mozilla.org/docs/Web/Events)提供給你監聽。基本上，使用者在網頁上做的任何行為都會觸發事件，你需要花大量時間、大量精力確保它們有相對應的使用者體驗。幸運的是，你只需要處理少部分的事件類型。這邊是一些常見的事件類型，我們會使用其中兩種來建立遊戲：

- [點擊](https://developer.mozilla.org/docs/Web/API/Element/click_event)： 使用者點擊物件，通常會是按鈕或是連結。
- [右鍵選單](https://developer.mozilla.org/docs/Web/API/Element/contextmenu_event)： 使用者點擊滑鼠右鍵。
- [選取](https://developer.mozilla.org/docs/Web/API/Element/select_event)： 使用者標記特定文字。
- [輸入](https://developer.mozilla.org/docs/Web/API/Element/input_event)： 使用者輸入文字。

## 建立遊戲

現在我們藉由建立遊戲，了解事件是如何在 JavaScript 上運作的。我們的遊戲會測試玩家的打字技巧，一項程式開發員被忽略的技能之一。我們應該時刻練習打字技術！大致的遊戲流程如下：

- 玩家點擊「開始」按鈕並產生一行要被輸入的引文
- 玩家盡快地輸入這段文字到文字框中
  - 當單字輸入完畢時，立即標記下一個單字。
  - 當玩家打錯字時，將文字框轉為紅色。
  - 當玩家完成引文輸入時，顯示祝賀語與花費的時間。

讓我們開始建立遊戲，學習事件驅動吧！

### 檔案結構

我們總共需要三個檔案：**index.html**、**script.js** 與 **style.css**。我們來設定它們，以完成後續的步驟。

- 建立新的資料夾存放我們的遊戲，開啟 Console 或是終端機，輸入下列指令：

```bash
# Linux 或 macOS
mkdir typing-game && cd typing-game

# Windows
md typing-game && cd typing-game
```

- 打開文字編輯器 Visual Studio Code

```bash
code .
```

- 現在，在 Visual Studio Code 中新增三個檔案到資料夾中，分別為：
  - index.html
  - script.js
  - style.css

## 建立使用者介面

藉由回顧我們的需求，我們在 HTML 頁面上新增一些元素。這就像是看一份食譜，你需要對應的食材：

- 一個地方呈現將被輸入的引文
- 一個地方呈現任何訊息，好比祝賀文
- 一個玩家輸入的文字框
- 一個開始按鈕。

每一個物件都需要 ID ，讓 JavaScript 程式能控制它們。另外，在 HTML 檔案匯入 CSS 與 JavaScript 檔，我們等一下會編輯它們。

在新的 **index.html** 檔案中，加入下列程式碼：

```html
<!-- inside index.html -->
<html>
<head>
  <title>Typing game</title>
  <link rel="stylesheet" href="style.css">
</head>
<body>
  <h1>Typing game!</h1>
  <p>Practice your typing skills with a quote from Sherlock Holmes. Click **start** to begin!</p>
  <p id="quote"></p> <!-- This will display our quote -->
  <p id="message"></p> <!-- This will display any status messages -->
  <div>
    <input type="text" aria-label="current word" id="typed-value" /> <!-- The textbox for typing -->
    <button type="button" id="start">Start</button> <!-- To start the game -->
  </div>
  <script src="script.js"></script>
</body>
</html>
```

### 執行應用程式

最好的逐段開發模式是定期的確認程式結果。讓我們來執行現在的應用程式。Visual Studio Code 上有一個好用的擴充套件為 [Live Server](https://marketplace.visualstudio.com/items?itemName=ritwickdey.LiveServer)，它會在你儲存網頁檔案時，同時架設並更新瀏覽器上的網頁。

- 安裝 [Live Server](https://marketplace.visualstudio.com/items?itemName=ritwickdey.LiveServer)，點擊連結中的 **Install**
  - 瀏覽器要求開啟 Visual Studio Code，Visual Studio Code 會執行後續的安裝流程
  - 安裝完後，重啟 Visual Studio Code
- 一旦安裝完成，在 Visual Studio Code 下按下 Ctrl-Shift-P (或 Cmd-Shift-P) 開啟指令視窗。
- 輸入 **Live Server: Open with Live Server**
  - Live Server 會架設並發布你的網頁成果
- 開啟瀏覽器，前往 **https://localhost:5500**
- 現在你能看到你所做的網頁！

讓我們來為網頁增加更多功能。

## 加入 CSS

建立完 HTML 檔，現在我們為了造型加入 CSS。我們需要標記玩家需要輸入的單字，若單字輸入錯誤時需要改變文字框的顏色。利用兩組 class 來完成：

在檔案 **style.css** 加入下列語法：

```css
/* 在 style.css 中 */
.highlight {
  background-color: yellow;
}

.error {
  background-color: lightcoral;
  border: red;
}
```

✅ 處理 CSS 時，你可以規劃任何你想要的介面布局。花點時間讓你的網頁更迷人：

- 變更其他字型
- 改變標題顏色
- 改變物件大小

## JavaScript

建立完使用者介面後，我們要專注在 JavaScript 上，提供網頁邏輯處理的能力。我們將工作分為下列步驟：

- [建立常數](#建立常數)
- [事件監聽者 - 開始遊戲](#加入開始邏輯)
- [事件監聽者 - 輸入文字](#加入打字邏輯)

首先，我們先編輯檔案 **script.js**。

### 建立常數

加入一些變數給程式使用。同樣地，就像食譜一樣，我們需要的食材如下：

- 矩陣，儲存所有引文
- 空矩陣，儲存單一引文的所有單字
- 變數，儲存空矩陣的索引，標記玩家現在面對的單字
- 變數，紀錄玩家點擊開始時的時間

我們也需要將使用者介面上的物件做連結：

- 文字框 (**typed-value**)
- 顯示引文 (**quote**)
- 訊息欄 (**message**)

```javascript
// 在檔案 script.js 中
// 所有的引文內容
const quotes = [
    'When you have eliminated the impossible, whatever remains, however improbable, must be the truth.',
    'There is nothing more deceptive than an obvious fact.',
    'I ought to know by this time that when a fact appears to be opposed to a long train of deductions it invariably proves to be capable of bearing some other interpretation.',
    'I never make exceptions. An exception disproves the rule.',
    'What one man can invent another can discover.',
    'Nothing clears up a case so much as stating it to another person.',
    'Education never ends, Watson. It is a series of lessons, with the greatest for the last.',
];
// 儲存單字列表及目前要輸入的單字索引
let words = [];
let wordIndex = 0;
// 開始時間
let startTime = Date.now();
// 網頁物件連結
const quoteElement = document.getElementById('quote');
const messageElement = document.getElementById('message');
const typedValueElement = document.getElementById('typed-value');
```

✅ 試著加入更多的引文到你的遊戲中。

> **筆記** 我們可以接收任何物件，只要使用程式碼 `document.getElementById`。因為我們需要定期參考這些元素，所以使用常數來確認是否有單字輸入錯誤的問題。框架如 [Vue.js](https://vuejs.org/) 或 [React](https://reactjs.org/) 可以幫助你更好管理你的程式碼。

花點時間觀看下列關於 `const`、`let` 與 `var` 的影片。

[![變數類型](https://img.youtube.com/vi/JNIXfGiDWM8/0.jpg)](https://youtube.com/watch?v=JNIXfGiDWM8 "變數類型")

> 點擊上方圖片以觀賞關於變數的影片。

### 加入開始邏輯

為了開始我們的遊戲，玩家會點擊開始按鈕。當然，我們不知道何時玩家會開始遊戲，這就是為什麼我們使用[事件監聽者](https://developer.mozilla.org/docs/Web/API/EventTarget/addEventListener)到程式中。一個事件監聽者允許我們監看事件的觸發與對應的回應程式。在這個例子，我們希望當使用者點擊開始時，執行某些程式。

當玩家點擊 **start** 按鈕後，我們需要挑選一段引文、設定使用者介面並追蹤現在玩家的要輸入的單字與時間。下列為我們需要新增的程式碼，我們會在之後逐行解釋。

```javascript
// 在 script.js 末端
document.getElementById('start').addEventListener('click', () => {
  // 取得一行引文
  const quoteIndex = Math.floor(Math.random() * quotes.length);
  const quote = quotes[quoteIndex];
  // 將引文分成許多單字，存在矩陣中。
  words = quote.split(' ');
  // 重制單字索引來做追蹤
  wordIndex = 0;

  // 更新使用者介面
  // 建立 span 元素的矩陣，設定 class 用。
  const spanWords = words.map(function(word) { return `<span>${word} </span>`});
  // 轉換成字串並以 innerHTML 顯示引文
  quoteElement.innerHTML = spanWords.join('');
  // 標記第一個單字
  quoteElement.childNodes[0].className = 'highlight';
  // 清除訊息欄之前的訊息
  messageElement.innerText = '';

  // 設定文字框
  // 清除文字框
  typedValueElement.value = '';
  // 設定 focus
  typedValueElement.focus();
  // 設定事件驅動程式

  // 開始計時器
  startTime = new Date().getTime();
});
```

我們來分解程式碼吧！

- 設定單字追蹤
  - 使用 [Math.floor](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Math/floor) 和 [Math.random](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Math/random) 讓我們能隨機從矩陣 `quotes` 中挑選一行引文
  - 轉換 `quote` 成 `words` 組成的矩陣，追蹤目前玩家正在輸入的單字
  - `wordIndex` 設定為 0，玩家會從第一的單字開始輸入
- 設定使用者介面
  - 建立矩陣 `spanWords`，將每一個單字包在 `span` 元素中
    - 這讓我們能高光標記單字
  - `join` 矩陣來建立字串，我們可以在 `quoteElement` 上更新 `innerHTML`
    - 這會顯示引文給玩家檢視
  - 設定第一個 `span` 元素的 `className` 成 `highlight`，來標記單字呈黃色
  - 修改 `messageElement`的 `innerText` 成 `''`，這會清除訊息欄的內容
- 設定文字框
  - 清除目前 `typedValueElement` 的 `value` 
  - 設定 `typedValueElement` 成 `focus` 
- 呼叫 `getTime` 來啟始計時器

### 加入打字邏輯

當玩家開始打字時，`input` 事件會被觸發。對應的事件監聽者需要檢查玩家是否輸入正確的單字，監控目前的遊戲狀況。回到檔案 **script.js**，加入下方程式碼到檔案最下方。我們會在後續解釋程式碼。

```javascript
// script.js 最末端
typedValueElement.addEventListener('input', () => {
  // 取得目前的單字
  const currentWord = words[wordIndex];
  // 取得目前輸入的數值
  const typedValue = typedValueElement.value;

  if (typedValue === currentWord && wordIndex === words.length - 1) {
    // 句子最末端
    // 顯示成功
    const elapsedTime = new Date().getTime() - startTime;
    const message = `CONGRATULATIONS! You finished in ${elapsedTime / 1000} seconds.`;
    messageElement.innerText = message;
  } else if (typedValue.endsWith(' ') && typedValue.trim() === currentWord) {
    // 單字最末端
    // 清除輸入的數值，準備給新的單字使用
    typedValueElement.value = '';
    // 移動到下一個單字
    wordIndex++;
    // 重設所有引文子元素的 class 名稱
    for (const wordElement of quoteElement.childNodes) {
      wordElement.className = '';
    }
    // 標記新單字
    quoteElement.childNodes[wordIndex].className = 'highlight';
  } else if (currentWord.startsWith(typedValue)) {
    // 單字目前輸入正確
    // 標記下一個單字
    typedValueElement.className = '';
  } else {
    // 單字輸入錯誤
    typedValueElement.className = 'error';
  }
});
```

讓我們分解程式碼吧！我們開始取得目前的單字與玩家輸入的數值。我們建立一系列的邏輯，檢查引文是否輸入完成，單字是否輸入完成，單字是否正確、是否錯誤。

- 引文完成，檢查 `typedValue` 與 `currentWord` 相等且 `wordIndex` 與 `words` 的 `length` 減一相等。
  - 計算 `elapsedTime` ，利用目前時間減去 `startTime` 取得遊戲時長
  - `elapsedTime` 除以 1,000 ，轉化毫秒單位為秒單位
  - 顯示成功訊息
- 單字完成，以 `typedValue` 間的空白為界，檢查 `typedValue` 是否與 `currentWord` 相等
  - 設定 `typedElement` 的 `value` 成 `''` ，準備給下一個單字輸入進來
  - 增加 `wordIndex` 到下一個單字
  - 進迴圈，每一個 `quoteElement` 的 `childNodes` ，它們的 `className` 都被設為 `''` ，代表預設的單字呈現規則
  - 設定單字的 `className` 成 `highlight` 來標記為下一個被輸入的單字
- 單字目前輸入正確但未完成，從 `typedValue` 開始檢查 `currentWord` 
  - 確保清除 `typedValueElement` 的 `className`，顯示預設的呈現方式。
- 若此時輸入錯誤，我們加上錯誤規則
  - 設定 `typedValueElement` 的 `className` 成 `error`

## 測試你的應用程式

我們做到最後了！最後一步就是確保我們的應用程式運作正常。試試看！不要擔心程式出現錯誤，**所有的開發者**都會面臨錯誤。有需要時，檢查程式訊息並偵錯。

點擊按鈕 **start**，馬上開始輸入單字！你可以看看這預覽動畫。

![遊戲中的動畫](../../images/demo.gif)

---

## 🚀 挑戰

加入更多功能。

- 在完成遊戲時，關閉 `input` 事件監聽者；遊戲重新開始時，再重新開啟它。
- 當玩家完成引文時，關閉文字框
- 以對話窗格的方式顯示恭賀訊息
- 利用 [localStorage](https://developer.mozilla.org/docs/Web/API/Window/localStorage) 儲存最高分的資料

## 課後測驗

[課後測驗](https://ashy-river-0debb7803.1.azurestaticapps.net/quiz/22?loc=zh_tw)

## 複習與自學

在瀏覽器上閱讀[所有開發者可運用的事件](https://developer.mozilla.org/docs/Web/Events)，想想你能在什麼樣的場合使用各個事件。

## 作業

[建立一款新的鍵盤遊戲](assignment.zh-tw.md)
