開源專案讀起來 | 關於圈圈叉叉的雅量

神Q超人
Starbugs Weekly 星巴哥技術專欄
10 min readDec 6, 2021
Photo by Jon Tyson on Unsplash

Hi!大家好,我是神 Q 超人!因為 StarBugs 的關係,所以平常沒事我就會讀一些有關於程式的技術文章,然後再把那些投稿到每週週刊的推薦文章中,也順便看看自己有沒有感興趣想要學習的東西。然後就在某天的下午我突然意識到

圈圈叉叉也太多了吧!

沒錯,就是那個童年遊戲圈圈叉叉,我幾乎每個禮拜都會看到有關於寫出一個圈圈叉叉的心得文章,發現這一點的我就到 GitHub 查詢一下,沒想到用寫一個圈圈叉叉來當作品練習的開發者,光是使用 JavaScript 語言寫的就有快 5 萬人。

https://github.com/search?q=tic+tac+toe

於是我就好奇了,判斷圈圈叉叉的邏輯那麼單純簡單,那這些近 5 萬人寫出來的圈圈叉叉又會有哪些不同呢?所以這篇文章就要和大家一起來讀幾篇同樣是寫圈圈叉叉的開源專案,看看開發者們對相同邏輯的思考方式有什麼不同!GO!GO!

我會怎麼做?

好的!在看其他開發者的邏輯之前,先來試想一下如果是我的話,會如何判斷在圈圈叉叉中到底是誰獲勝了。

如果是我大概會先定義一個 3 * 3 的二維陣列,接著讓圈和叉輪流放到二維陣列中一直到有玩家獲勝,至於獲勝的邏輯會分成三個部分寫,分別是判斷橫軸和縱軸有沒有連線的 checkVertical 和 checkHorizontal,以及判斷對角線有沒有連線的 checkDiagonal,只要這三個其中一個回傳 true 就代表目前的玩家贏了:

上方的邏輯相當單純,checkVertical 和 checkHorizontal 都可以用迴圈完成位置的判斷,而對角線的部分我就偷懶直接寫死位置的邏輯(反正圈圈叉叉大部分都是 3 * 3 嘛),接著只要在每一次玩家下完之後執行 checkIsWin 檢查一次是否獲勝就好:

X 的對角線贏了

好啦!那麼在自己初步思考寫出第一版後,就來看看其他開發者都是怎麼處理這段邏輯的吧!

其他人怎麼寫?

案例一

首先來看看 gabrielfroes/tic-tac-toe,會選擇它是因為在 Github 上的 JavaScript 圈圈叉叉遊戲中,它算是獲得滿多 star 的一個,而且還是用 VanillaJS 寫的,就閱讀上來說應該不會太難!

該專案的組成非常簡單,要說邏輯的話就只有在 js 資料夾中的一個 tic-tac-toe.js,而在點開後馬上就能看到的 board 和 winning_sequences:

https://github.com/gabrielfroes/tic-tac-toe/blob/d8e49ee2ac4b834af704c7c6e7c7157e96b0b878/js/tic-tac-toe.js#L5

看見 winning_sequences 之後就瞬間明白,雖然是用一維陣列的 board 放置圈圈叉叉的位置,但作者事先就定義了所有會贏的 index 組合,然後在每一次下完後去檢查當前下完的符號贏了沒:

https://github.com/gabrielfroes/tic-tac-toe/blob/d8e49ee2ac4b834af704c7c6e7c7157e96b0b878/js/tic-tac-toe.js#L61

這個方法利用了快查表的好處,先列出 8 種會贏的組合,之後每次都只需要跑 8 次迴圈就可以知道有沒有人獲勝,如果是我的判斷邏輯,每一次都要用雙迴圈判斷縱和橫軸,再多跑兩次確認對角線有沒有連線,幾次我就不算了,反正一定比這個多。

另外順帶一提,在 React 官方文件的教學中,也是用圈圈叉叉當作練習對象,然後也是用這種方式判斷輸贏的:

https://reactjs.org/tutorial/tutorial.html#declaring-a-winner

案例二

接下來是 AnthonyDM-Dev/LetsPlayToTicTacToe,這個專案是用 Vue 寫的,作者也是使用一維陣列紀錄當前的盤面,並且在每一步下完後,都把當前是誰下(圈或叉)的放到另一個叫做 history 的 array,並利用 Vue 裡面的 watch 在 history 改變時判斷是否獲勝,所以重點應該就在 watch 裡面:

https://github.com/AnthonyDM-Dev/LetsPlayToTicTacToe/blob/main/src/components/TicTacToe.vue#L160

在 watch 中可以看見最後是用 gameStatus.hasWinner 判斷有沒有玩家獲勝,gameStatus 的值是由 game.checkWinningCombos 來的,而 game.checkWinningCombos 在執行時又會接收一個由 game.calculateCombos 回傳的參數叫做 allWinningCombos。因此先來看看 game.calculateCombos 裡賣的是什麼藥吧:

https://github.com/AnthonyDM-Dev/LetsPlayToTicTacToe/blob/main/src/assets/tictactoe-library/game.js#L1

從 calculateCombos 的第一行註解,大概可以猜出它會依照傳進來的 gridSize 算出所有會贏的組合後回傳,執行結果也就是這麼一回事:

calculateCombos 的執行結果會回傳所有勝利的組合

取得所有會勝利的組合後,就一樣用 for 迴圈判斷當前符號有沒有符合其中一個勝利組合的所有 index,在案例一的這部分是寫死判斷,但在這裡用了 every,就不限於只能用在 3 * 3 的圈圈叉叉了:

https://github.com/AnthonyDM-Dev/LetsPlayToTicTacToe/blob/main/src/assets/tictactoe-library/game.js#L45

案例三

最後一個案例 gajayana/tictactoe 就比較特別了,與上方兩者不同的是,它分別為了兩個玩家各定義一個 array 去紀錄目前已經下在哪些位置:

https://github.com/gajayana/tictactoe/blob/master/src/store/game.js#L9

在玩家選擇要下在哪裡的時候,就會把該位置 push 到對應玩家的 array 中

https://github.com/gajayana/tictactoe/blob/master/src/components/Board.vue#L43

值得注意的是,在把位置寫入玩家的 array 時,它多做了排序這件事情,至於為什麼要這麼做我們待會再來看看:

https://github.com/gajayana/tictactoe/blob/master/src/store/game.js#L35

而 store 的 getters 裡面有個 getWinningConbinations 分別回傳了橫軸、縱軸和對角線會贏的三種組合,所以這位作者也是根據棋盤的大小動態產生派的:

https://github.com/gajayana/tictactoe/blob/master/src/store/game.js#L80

這個案例判斷的部分要一氣呵成,首先取出所有會獲勝的組合叫做 items,然後再把兩個玩家下的位置都取出來,分別去跑獲勝組合的迴圈,並用 lodash 的 intersectionWith 判斷符合獲勝組合的 index 有幾個,如果符合的 index 數量和目前遊玩的棋盤尺寸相同就獲勝了(如果尺寸是 3 * 3 就是要符合 3 個 index 相同等等),也是因為使用 lodash 的 intersectionWith 的關係,所以才需要將玩家下過的位置排序。

https://github.com/gajayana/tictactoe/blob/master/src/components/label/Winner.vue#L16

結束啦!看過了三個不同的圈圈叉叉專案,各位不知道有什麼感覺,就我個人而言,有兩點是我自己一開始沒有考慮到的:

  1. 就是覺得我把對角線想得太簡單啦!完全沒有考慮到不同尺寸的問題。
  2. 沒有想過如果是用快查表的話,就可以減少每一次下完需要判斷是否已經有人獲勝的迴圈次數。

少考慮到上方兩點說不定在面試時就被刷掉了。 😂

另外在讀其他人的專案時,也想到一些可以改進的地方:

  1. 在第一個專案的快查表如果改得和第二三個一樣,可以根據棋盤尺寸動態產生應該會更好。
  2. 第二三個專案都是在每一個玩家下完後,重新產生所有會獲勝的組合,應該可以改成只在遊戲開始的那瞬間取得一次就好,畢竟遊戲開始後就固定棋盤的尺寸了。
  3. 在第三個專案中最後是取得兩個玩家的 array 出來判斷有沒有人獲勝,但是這裡應該只需要判斷最後一個下的人就好,不需要兩個都判斷。

所以其實開源專案不需要去讀一些很困難的幾萬顆星的酷東西,就算是 0 顆星的專案,我們也可以在閱讀程式碼的過中學習到很多事情!像是我如果需要寫驗證,就會去看別人驗證怎麼做,如果覺得某個 UI 框架的樣式很漂亮,就去看爆它的 CSS 怎麼寫。整個 GitHub 都是我們的程式範例寶庫!推薦各位可以多多利用,反正看不懂就把它 clone 下來跑看看就對了。 😂

其實這篇文章的主題算是我很喜歡的,算是從很簡單的專案為例子,再一次讓了解到讀開源專案對我們軟體開發者有多重要!那麼最後再麻煩大家,如果文章裡有任何錯誤,或是對內容有任何問題和想法,通通都可以留言告訴我,我會盡快回覆和修正的,非常感謝!🙌

--

--