iT邦幫忙

2023 iThome 鐵人賽

DAY 30
0
Modern Web

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

30天React練功坊-攻克常見實務/面試問題 Day30: Memory Cards Game(interview question)

  • 分享至 

  • xImage
  •  
tags: ItIron2023 react

前言

我們昨天做了一個基本的井字遊戲,基本上除了css的部分有些困難之外,基本的邏輯實踐其實相當的單純,今天我們反其道而行,雖然一樣是做個小遊戲,但我們今天把重點放在邏輯的處理上,css的部分不用你煩惱!

本日題目

請你以這個codesandbox作為起頭並參考下方的instructions完成題目的要求。

請你打造一個滿足以下條件的記憶卡遊戲

  1. 一開始展示12張面朝下的牌,12張牌要能湊出6個成對且不相同的組合(例如(A-A, B-B)這樣算一對)
  2. 每當任何一張卡被點擊時,會翻面朝上
  3. 若點擊任兩張卡發現正面字母相同(也就是成對),則標記卡片為成對且將卡片保持朝上
  4. 承上,若兩張卡不相符,則500毫秒後將兩張卡蓋回
  5. 若所有的卡片都成對,終止遊戲並在下方顯示"You Win!"
  6. 請勿修改css檔案的任何內容與render部分的className,僅需處理邏輯的部分

最終期待的成果如下圖

day30-demo-gif

以下為基礎的starter code

const App = () => {
  const initialCards = ['A', 'A', 'B', 'B', 'C', 'C', 'D', 'D', 'E', 'E', 'F', 'F'];
  const [cards, setCards] = useState(initialCards);
  const [flipped, setFlipped] = useState(Array(12).fill(false));
  const [check, setCheck] = useState([]);
  const [completed, setCompleted] = useState([]);
  
  // Shuffle cards on mount
  useEffect(() => {
    setCards(initialCards.sort(() => Math.random() - 0.5));
  }, []);

  const handleFlip = (index) => {
    // TODO: Implement handleFlip
  };

  // TODO: Implement the logic to check for matching cards

  // TODO: Implement the logic to display the game status
  const gameStatus = "";

  return (
    <div className="container">
      {cards.map((card, index) => (
        <div className="card-container" key={index} onClick={() => handleFlip(index)}>
          <div className={`card ${flipped[index] || completed.includes(index) ? 'flip' : ''}`}>
            <div className={`front ${completed.includes(index) ? 'matched' : ''}`}></div>
            <div className={`back ${completed.includes(index) ? 'matched' : ''}`}>{card}</div>
          </div>
        </div>
      ))}
      {gameStatus}
    </div>
  );
};

export default App;
.container {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  grid-gap: 16px;
  margin: 20px;
}

.card-container {
  width: 100px;
  height: 100px;
  position: relative;
  perspective: 1000px;
  margin: 10px;
  display: inline-block;
}

.card {
  width: 100%;
  height: 100%;
  position: absolute;
  transform-style: preserve-3d;
  transition: transform 0.5s;
}

.card.flip {
  transform: rotateY(180deg);
}

.card .front,
.card .back {
  width: 100%;
  height: 100%;
  position: absolute;
  backface-visibility: hidden;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 24px;
  border: 1px solid #ccc;
  border-radius: 8px;
  background-color: #ccc;
}

.card .back {
  transform: rotateY(180deg);
  background-color: #4caf50;
  color: white;
}

.matched {
  background-color: #ffd700;
  color: black;
}

解答與基本解釋

這個題目稍稍有些難度,但我仍認為最難的地方在於css的處理上,好在這次題目我先替你處理完這部分了,有興趣的朋友可以觀察一下我是如何實踐翻轉的動畫,大致上的重點在於以下幾個屬性的使用

1. perspective: 1000px; => 讓3D效果足夠明顯但又不會太誇張
2. transform-style: preserve-3d; => 讓子層的front & back可以被定位在3D平面上
3. backface-visibility: hidden; => 讓卡片的一面不會同時顯示到另一面上
4. transform: rotateY(180deg); => 讓卡片以Y軸做180旋轉來實現翻轉效果或是藏住背面卡片的內容

有興趣的可以自行研究一下,雖然很有趣但並不是今天的重點。

我們首先來觀察一下題目的給你的幾個state,當然你可以隨意修改,不過你也是可以用既有的state完成題目的要求。

// 儲存一開始的12張卡,其中包含六個對子
const [cards, setCards] = useState(initialCards);

// 儲存目前每張卡是否翻開的狀態,用以決定下方是否要掛上對應的class
const [flipped, setFlipped] = useState(Array(12).fill(false));

// 儲存目前正在翻的卡片,當翻到兩張時開始檢查的相關邏輯
const [check, setCheck] = useState([]);

// 儲存所有已經成對的卡片,用以決定下方是否要掛上對應的class
const [completed, setCompleted] = useState([]);

其中所有的卡片我們都會以index作為紀錄,辨別目前該修改哪張卡片的狀態,理解這一點之後我們就可以先處理最重要的handleFlip函數了,內容相當的單純

  1. 檢查目前點擊的卡片是否已經翻開或是已經成對,若沒有才繼續以下的邏輯
  2. 將該卡片利用setFlipped設為翻開
  3. 將該卡片利用setCheck加入目前正在翻的卡片陣列
const handleFlip = (index) => {
  if (flipped[index] || completed.includes(index)) return;

  setFlipped((prev) => {
    const copy = [...prev];
    copy[index] = true;
    return copy;
  });

  setCheck((prev) => [...prev, index]);
};

接著我們要處理check的部分,每一次以兩張卡為限,每當翻開兩張時我們就要檢查是否兩張卡有成對,這邊有許多種做法,我提供其中一種最為直觀的解法,也就是利用一個useEffect去處理這個邏輯。

1. 先檢查是否目前已翻開兩張卡
2. 若已經翻開兩張卡,則確認是否成對並利用setCompleted更新狀態
3. 若沒有成對,則在半秒後將兩張卡利用setFlipped蓋上
4. 利用setCheck設為初始狀態

useEffect(() => {
  if (check.length === 2) {
    const [first, second] = check;
    if (cards[first] === cards[second]) {
      setCompleted([...completed, first, second]);
    } else {
      setTimeout(() => {
        setFlipped((prev) => {
          const copy = [...prev];
          copy[first] = false;
          copy[second] = false;
          return copy;
        });
      }, 500);
    }
    setCheck([]);
  }
}, [check]);

最後的部分就簡單多了,我們只要檢查completed的長度是否跟初始陣列一樣長就可以判斷遊戲是否結束囉。

 const gameStatus = completed.length === 12 ? "You Win!" : "";

總結

今天我們處理了一個翻牌遊戲的問題,在沒有先提供css支援以及starter code的情況下,要在面試的30分鐘內做完會相當有難度,不過仍不失為一個有趣的挑戰題目,整體上並沒有太過於複雜的邏輯,完全可以靠useState & useEffect兩個hook就達成題目的要求,我想你也慢慢了解為什麼我們前面花這麼多時間探討這兩個hook的相關錯誤了,他們就是最常被使用的。整個系列文章就到此為止,希望透過這30天的練習有讓你對react更有自信一些,面對相關的面試時也能更如魚得水一些!我們明年見囉!

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


上一篇
30天React練功坊-攻克常見實務/面試問題 Day29: Simple Tic-Tac-Toe game(interview question)
系列文
30天React練功坊-攻克常見實務/面試問題30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言