iT邦幫忙

2023 iThome 鐵人賽

DAY 29
0
Modern Web

30天React練功坊-攻克常見實務/面試問題系列 第 29

30天React練功坊-攻克常見實務/面試問題 Day29: Simple Tic-Tac-Toe game(interview question)

  • 分享至 

  • xImage
  •  
tags: ItIron2023 react

前言

我們昨天處理了一個實務上常見的面試問題,要你根據某份提供的api文件以及一些指示完成題目的要求,今天我們再來看另一種可能的面試情境吧!

本日題目

請觀察這個codesandbox以及下方的instructions。

題目目標

  1. 建立一個簡易的React Tic-Tac-Toe遊戲
  2. 遊戲應由兩位玩家X & O輪流進行
  3. 你需要在有人獲勝的當下便宣告贏家並終止遊戲
  4. 畫面的呈現應予提供的圖檔越接近越好
  5. Reset Game按鈕應重置整個遊戲,包含版面與輪到哪一位玩家

遊戲初始畫面

day29-starter-image

遊戲結束畫面

day29-finish-image

下方是基本的starter code,可以隨意修改所有的內容。

const App = () => {
  // Initialize board with null values
  const [board, setBoard] = useState(Array(9).fill(null));
  const [isXNext, setIsXNext] = useState(true);

  // TODO: Function to handle click on a cell
  const handleClick = (index) => {
    // Implement your code here
  };


  return (
    <div className="App">
      <h1>Tic-Tac-Toe</h1>
      <div className="board">
        {board.map((cell, index) => (
          <div className="cell" key={index} onClick={() => handleClick(index)}>
            {cell}
          </div>
        ))}
      </div>
    </div>
  );
};

export default App;

解答與基本解釋

井字遊戲可說是前端面試題非常常見的題目之一了,有許多公司的前測是利用一些測試平台,這個題目往往都會收錄在裡面,我自己就碰到兩次。 這次的題目僅提供你需求、截圖以及一些基本的指示,絕大多數的部分都讓你自由發揮,通常會期待你在約半小時內處理完這個問題。

這類的情境我強烈建議你先處理最基本的UI部分,這也是這個考題最有可能難倒人的地方了,畢竟一個井字可能不是這麼好想像該怎麼處理,下方我提供我自己的解法,大致原理在於利用:not避免在最後一行以及最後一列加入border

.App {
  /* 先將頁面元素置中 */
  text-align: center;
  font-family: "Arial", sans-serif;  
  height: 100vh;
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
}

.board {
  /* 排出三欄排版 */  
  display: grid;
  grid-template-columns: repeat(3, 100px);
}

.cell {
  width: 100px;
  height: 100px;
  display: flex;
  justify-content: center;
  align-items: center;
  font-size: 32px;
}

/* 只在非最後一欄的位置加上右邊的邊線 */
.cell:not(:nth-child(3n)) {
  border-right: 5px solid black;
}

/* 只在非最後一列的位置加上底邊的邊線,n + 7表示包含第七個以後的所有元素 */
.cell:not(:nth-child(n + 7)) {
  border-bottom: 5px solid black;
}

這麼一來對我來說最困難的地方就已經結束了,剩下的部分我們先處理放子,也就是點擊cell時需要做的處理。

const handleClick = (index) => {
  // 若該位置已經有值時不讓遊戲繼續進行
  if (board[index]) return;

  const newBoard = board.slice();
  newBoard[index] = isXNext ? "X" : "O";
  // 更新board並輪到下一位玩家  
  setBoard(newBoard);
  setIsXNext(!isXNext);
};

// 在下方render的部分把handleClick function掛在每一個cell上
<div className="board">
  {board.map((value, index) => (
    <div className="cell" key={index} onClick={() => handleClick(index)}>
      {value}
    </div>
  ))}
</div>

接著就是處理宣告贏家跟顯示狀態的部分,這邊我們會需要一個checkWinner function,在每一次的渲染時去更新顯示的狀態,會有三種可能

  1. 輪到X了
  2. 輪到O了
  3. 有贏家了

這也是這個題目第二難的部分,不過只要你先將所有可能勝利的組合都寫出來,之後再每一次檢查時都確認是否滿足其中一個組合,你就可以輕易地知道目前是否有贏家,下方是其中一種寫法。

const checkWinner = (board) => {
  // Possible winning combinations
  const winningCombinations = [
    [0, 1, 2],
    [3, 4, 5],
    [6, 7, 8],
    [0, 3, 6],
    [1, 4, 7],
    [2, 5, 8],
    [0, 4, 8],
    [2, 4, 6]
  ];

  for (let i = 0; i < winningCombinations.length; i++) {
    const [a, b, c] = winningCombinations[i];
    // 檢查三個位置是否都是同一個值,若都相同表示有贏家  
    if (board[a] && board[a] === board[b] && board[a] === board[c]) {
      return board[a];
    }
  }
  // 走到這就跑完每個勝利組合都沒找到贏家,回傳null表示遊戲還沒結束  
  return null;
};

// 利用status變數去提示使用者現在遊戲的狀態
const winner = checkWinner(board);
const status = winner
  ? `Winner: ${winner}`
  : `Next Player: ${isXNext ? "X" : "O"}`;
return (
  <div className="App">
    <h1>Tic-Tac-Toe</h1>
    <div className="status">{status}</div> // 加入status顯示
    <div className="board">
      {board.map((value, index) => (
        <div className="cell" key={index} onClick={() => handleClick(index)}>
          {value}
        </div>
      ))}
    </div>
  </div>
);

同時別忘了更新上方的handleClick函數,若我們現在已經有贏家了,那遊戲也不該繼續進行下去

const handleClick = (index) => {
  // 若該位置已經有值或有贏家時時不讓遊戲繼續進行
  if (board[index] || checkWinner(board)) return;

  const newBoard = board.slice();
  newBoard[index] = isXNext ? "X" : "O";
  // 更新board並輪到下一位玩家  
  setBoard(newBoard);
  setIsXNext(!isXNext);
};

剩下的Reset Game就不用多說了,增加一個handler讓點擊按鈕時重置回原本的狀態即可。

const resetBoard = () => {
  // 回復原本預設的狀態   
  setBoard(Array(9).fill(null));
  setIsXNext(true);
};

總結

今天的題目你肯定在學習時有見過各種教學影片,很遺憾的這慢慢變成了你可能會在面試時見到的東西,除了切版的部分稍微困難一點之外,只要你釐清勝利條件的部分,這個題目理論上不會花你太多的時間! 全仰賴你一開始state怎麼去設計,這部分每個人的解法都會有些微的差異,若你覺得我提供的範本太過於難懂你也可以試著用相同的邏輯改寫看看!剩下最後一天啦,明天的遊戲會再更有難度一些!

本文章同步發布於個人部落格,有興趣的朋友也可以來逛逛~!


上一篇
30天React練功坊-攻克常見實務/面試問題 Day28: Fetch all synonyms for given word(interview question)
下一篇
30天React練功坊-攻克常見實務/面試問題 Day30: Memory Cards Game(interview question)
系列文
30天React練功坊-攻克常見實務/面試問題30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言