# Project 1: Sprouthon (Sprout Python in C++)
## 勘誤與補充更新列表
* 03/31:【新增】在作業每個部分加上「需要更新的變數列表」了。
* 03/31:【新增】Windows系統環境設置教學影片。
* 03/30:【更正】解釋地圖座標的圖片中最右下角的座標標示錯誤成 (16, 15) ,應為 (15, 14),現已更正。
* 03/30:【新增】提醒Windows系統下,路徑名不得有中文字。
* 03/30:【更正】Windows系統的make指令,應為`mingw32-make`。
<hr>
## 作業簡介
大家知道 「python」 這個詞除了是大家耳熟能詳的程式語言的名稱之外,其實也是蟒蛇的意思嗎?
這次的作業以貪吃蛇為原型,會讓大家進行遊戲的部分功能撰寫。遊戲過程中,玩家會操縱一隻愛吃蘋果的蟒蛇在 $15 \times 16$ 的棋盤方格上移動,透過吞食不同顏色的蘋果獲得相對應的分數,而遊戲目標就是盡可能地獲取高分。
遊戲畫面中除了蟒蛇所在的棋盤方格之外,在其右方還有實時更新的計分板。另外,在蟒蛇的頭撞到自己或是棋盤方格邊界時,遊戲將會結束,並進入遊戲結束畫面,顯示玩家此輪遊戲的得分數並且詢問玩家是否繼續遊戲。
在本次作業中,同學需要完成的部分是**分數更新**、**蘋果位置更新**、**蟒蛇位置更新**和**蟒蛇的自動移動模式**。

## 前置作業與編譯教學
本部分會介紹如何在 MacOS 和 Windows 上成功執行這次的作業。
### MacOS
#### 下載並解壓縮作業檔案
使用 MacOS 的同學請到[這個連結](https://drive.google.com/file/d/1GzKePeM0eSI7GcGaLDAE2_3ou4eW80xb/view?usp=share_link)下載並解壓縮檔案。使用 Windows 系統的同學請往下滑到 Windows 的下載點下載檔案喔!
完成之後會得到叫做 `HW1_Sprouthon_Mac` 的資料夾。
#### 安裝 VSCode 、 G++ 和 Homebrew
首先,會想要大家在 VSCode 上編輯與編譯這次的作業檔案,還沒安裝好 VSCode 的可以看[這支影片](https://www.youtube.com/watch?v=bGNPqYjorPI)(感謝北區講師詹挹辰錄製影片><)。
預設大家在看完這支影片之後會裝好 g++ 和 homebrew,大家可以將下面這兩個指令分別複製到你的 terminal。
```
g++ -v
brew -v
```
如果 terminal 有顯示出你的 g++ 和 homebrew 的版本的文字,那麼就是有安裝好了。如果沒有安裝好就再回去重看一次影片XD
#### 安裝 CMake
在你的 terminal 輸入以下指令:
```
brew install cmake
```
接著可以輸入以下指令檢查 CMake 有沒有被安裝好:
```
cmake --version
```
如果有輸出類似這種東西就代表安裝好了:
```
cmake version 3.29.0
```
#### 安裝 Make
在你的 terminal 輸入以下指令:
```shell
brew install make
```
接著可以輸入以下指令檢查 Make 有沒有被安裝好:
```shell
make --version
```
如果有輸出類似這種東西就代表安裝好了:
```
GNU Make 4.4.1
...
```
#### 安裝函式庫(SFML)
這部分我們一樣透過 homebrew 來進行,請打以下指令:
```
brew install SFML
```
homebrew 將會幫你將適合你的系統的檔案下載下來,放在等等用到的 CMake 找得到的地方。如果真的很想知道你的檔案被儲存到哪裡,可以打以下指令:
```
brew info sfml
```
#### 使用 CMake 編譯檔案
請在 VSCode 裡面打開這次作業的目錄 HW1_Sprouthon,並且打開你的 terminal,確認一下你目前在 terminal 中的位置在 HW1_Sprouthon,如果不是的話可以利用 cd 的指令移動。
接著要請大家在這個目錄裡新增一個叫做 build 的目錄,可以使用指令或是直接利用 VSCode 的介面新增目錄。指令的話請看這邊:
```
mkdir build
```
接著請大家進入 build 這個目錄。請輸入指令:
```
cd build
```
這時候你應該會發現你已經進入 build 這個目錄中了。接著要請大家輸入以下指令:
```
cmake ..
```
接著再請大家輸入指令:
```
make
```
就會有一些綠綠的(?)的東西跳出來,像是這樣:

這就代表你的檔案編譯好了,接著只要繼續在 build 這個目錄中輸入以下指令就可以執行檔案了:
```
./Sprouthon
```
之後大家需要重新編譯與執行檔案的話就請你使用 cd 進入到 build 這個目錄中,然後輸入以下兩個指令就行了:
```
make
./Sprouthon
```
### Windows
以下教學亦有[影片](https://youtu.be/dVvt6Kesvfc)版本,步驟大致相同。
#### 下載並解壓縮作業檔案
使用 Windows 系統的同學請到[這個連結](https://drive.google.com/file/d/1BAUauHs7-lSAuvQsd3rc8K-6fSwDjQ9H/view?usp=share_link)下載並解壓縮檔案。使用 MacOS 系統的同學請往上滑到 MacOS 的下載點下載檔案喔!
完成之後會得到叫做 `HW1_Sprouthon_Win` 的資料夾。
**注意:完整路徑內不能有中文字。** 有些人的使用者名稱會是中文的,所以建議把解壓縮後的資料夾放在 `C:\HW1_Sprouthon_Win` 或 `D:\HW1_Sprouthon_Win`。
#### 安裝 Visual Studio Code
本次作業中,我們使用目前時下功能最強大的文字編輯器之一 Visual Studio Code(簡稱VS Code)。
:::spoiler 題外話
當然,要用 CodeBlocks 或 Dev-C++,甚至 Windows 內建的記事本也不是不行,只是有些步驟會比較麻煩,所以本次教學還是以 VS Code 為主。而且 VS Code 還有個好處是他跨平台,所以就算你是用 Windows、你左邊的同學是用 Linux、你右邊的同學是用 Mac,你們都可以用一樣的文字編輯器,就算直接用別人的電腦,也不需要重新熟悉別的編輯器。
:::
<br>
請前往 [Visual Studio Code 官網](https://code.visualstudio.com/Download)下載安裝檔。

執行安裝檔後一直按下一步即可。
#### 設定 Visual Studio Code
請開啟 VS Code ,選擇左邊工具列的「Extensions」。搜尋「C/C++ Extension Pack」並安裝此 extension pack。這些 extension 會提供一些額外的功能,讓大家在寫 C++ code 時感到更方便。

#### 安裝 CMake
前往 [CMake 官網](https://cmake.org/download/)下載安裝檔,版本選擇 Windows x64 Installer。

執行安裝檔,除了下圖中這一步要特別選擇「Add CMake to the system PATH for ...」兩者之一,其他步都可以一直按下一步。

#### 安裝 MSYS2
前往 [MSYS2 官網](https://www.msys2.org/#installation)下載安裝檔。

執行安裝檔並一直按下一步即可。
<!-- 安裝完成後或許會跳出一個類似下圖的 terminal,桃紅色的字寫著 **UCRT64**。請不要使用這個 terminal,否則等等編譯會編不起來。
 -->
#### 安裝 GCC 和 make
在 Windows 選單搜尋「MSYS MINGW64」並執行它,或是直接執行 `C:\msys64\mingw64.exe` (假設你安裝在預設位置)。注意此時跳出的 terminal ,桃紅色的字應為 **MINGW64** ,如下圖。

輸入 `pacman -S mingw-w64-x86_64-gcc mingw-w64-x86_64-make` 並按照指示安裝這相關套件。
安裝結束後分別輸入 `g++ --version` 和 `mingw32-make --version` ,如果有看到程式輸出版本號和一些版權資訊,就代表安裝成功了!

啊如果出現 `command not found` 之類的,就代表安裝可能哪裡出了問題。
#### 設定環境變數 Path
按 Windows + R 跳出「執行」視窗,輸入 `sysdm.cpl` 叫出「系統內容」視窗。接著到「進階 > 環境變數 > (使用者名稱) 的使用者變數 > Path」,新增一行 `C:\msys64\mingw64\bin`。

**做完這步後,記得把所有的 terminal 和 VS Code 關掉重開才會生效。**
#### 使用 CMake 建置本作業
回到 VS Code,點選頂端工具列的「File > Open Folder」,並開啟剛才解壓縮後的 HW1_Sprouthon 目錄。

點選頂端工具列的「Terminal > New Terminal」,並在終端機輸入 `mkdir build`。這一行指令會新增一個名為 `build` 的資料夾。
在終端機執行 `cd build` 。這就是在檔案總管「點進一個資料夾」的終端機版。
接著執行 `cmake .. -G "MinGW Makefiles"`。這步會花一點時間,完成後會顯示類似以下的訊息:
```
C:\Users\user\Downloads\HW1_Sprouthon\build> cmake .. -G "MinGW Makefiles"
-- The C compiler identification is GNU 13.2.0
-- The CXX compiler identification is GNU 13.2.0
...
-- Configuring done (28.9s)
-- Generating done (0.9s)
-- Build files have been written to: C:/Users/user/Downloads/HW1_Sprouthon/build
```
再接著執行 `cmake --build . -j`。這步是讓 `cmake` 依照我們寫好的規則,去編譯本專案中的各種程式碼,所以也會花一點時間。完成後會出現以下的訊息:
```
C:\Users\user\Downloads\HW1_Sprouthon\build> cmake --build . -j
[ 1%] Building CXX object _deps/sfml-build/src/SFML/Main/CMakeFiles/sfml-main.dir/MainWin32.cpp.obj
...
[100%] Linking CXX executable Sprouthon.exe
[100%] Built target Sprouthon
```
此時 `build` 目錄裡應該會跑出 `Sprouthon.exe`。不過在執行他之前,我們要先到 `HW1_Sprouthon\build\_deps\sfml-build\lib` 把所有附檔名是 `.dll` 的「應用程式擴充」檔案複製到 `HW1_Sprouthon\build` 底下。

恭喜各位,至此如果每一步都成功,我們終於可以執行遊戲 `Sprouthon.exe` 了!
**最後要請各位注意:之後每次修改程式碼後,記得要把 `Sprouthon.exe` 關掉,接著重新跑一次 `cmake --build . -j` ,新的程式碼才會被編譯為可執行檔喔!**
## 遊戲規則
### 遊戲操控
使用鍵盤上的上下左右方向鍵操縱蟒蛇移動。
### 遊戲模式
若在遊戲主畫面中按下 X 鍵會進入到玩家操縱模式,也就是正常的透過上下左右的方向鍵操縱蟒蛇移動;若在遊戲主畫面中按下 C 鍵則會進入到自動模式,蟒蛇會根據寫好的程式碼移動。
### 蘋果生成與變色規則
蘋果的顏色變化規則是這樣的:剛開始生成的蘋果的顏色都是紅色的,在 $15$ 幀之後會變成橘色,並且再過 $15$ 幀之後會變成黃色,直到蟒蛇吃到這顆蘋果之後再在隨機的位置生成一顆紅色的蘋果。
### 遊戲分數計算
蟒蛇只要頭部位置與蘋果重疊,也就是吃到蘋果,就會獲得分數。紅色、橘色、黃色蘋果分別對應到 $3$ 分、$2$ 分、$1$ 分。
在記分板上中,從上至下的數字分別代表總分、吞食紅色蘋果數、吞食橘色蘋果數、吞食黃色蘋果數。
<center>

</center>\
### 蟒蛇身體顏色
每當蟒蛇吞食了一顆蘋果,在蟒蛇的尾端會長出一節新的身體,這個身體的顏色是根據吞食的蘋果顏色決定的,比如說它吞了一個橘色的蘋果,那麼他就會長出一節橘色的身體。
### 遊戲地圖的座標編號

如上圖所示,遊戲地圖橫向為 X 軸,縱向為 Y 軸,原點(0, 0)位於左上角。
### 遊戲結束條件
#### 遊戲勝利
如果成功地讓蟒蛇的身體佔滿遊戲地圖的每一個格子,那麼就達成遊戲勝利的條件,會出現 You Win! 的遊戲終止畫面。
<center>

</center>\
#### 遊戲失敗
如果在遊戲過程中:
- 蟒蛇的頭部超出遊戲地圖上下左右的邊界。
- 蟒蛇的頭部與其他身體部位重疊,也就是撞到自己。
那麼便會判定為遊戲失敗,出現 Game Over 畫面。
<center>

</center>\
## 檔案介紹
這次作業大家會需要完成的部分包括了四個函式,`scoreboard.cpp` 中的 `update_scoreboard_score()`、 `apple.cpp` 中的 `spawn_apple()`、`python.cpp` 中的 `update_python()` 和 `auto_mode()`。另外,大家可能會發現在這次作業的資料夾中,有個叫做 `setting.hpp` 的檔案。它事實上是一個 header file(標頭檔),這在之後的課程中會介紹到,大家就先這樣想:只要在其他的 `.cpp` 檔案中有引用 `setting.h` 就可以使用其中定義的一些變數、值或是結構(struct)。
接下來這邊會介紹一下 `setting.hpp` 中大家會用到的參數:
#### 常用參數:
1. **col_num** 和 **row_num**

**col_num** 代表的是行數,總共 $16$ 行。**row_num** 代表的是列數,總共 $15$ 行。
2. **frame_rate**
**frame_rate** 被定義在 `setting.h` 的最上方,目前設定為 $5.0f$,代表每秒鐘會更新 $5$ 幀。如果大家在測試時有需要加快或是放慢蟒蛇的移動速率(尤其是最後一部分,撰寫 `auto_mode()` 的地方很需要調快),可以自行調整 **frame_rate** 後方的數字,比如說調整成 $10.0f$ 就代表每秒鐘更新 $10$ 幀。
#### 結構成員介紹:
這部分介紹大家會用到的三個結構:**Python**、**Apple** 和 **ScoreBoard**。在這些結構中有很多與這份作業使用的 SFML 有關的東西,他們會長成 **sf::** 這種樣子,因為大家不會用到,就忽視他們就好!
1. **Python**
```cpp=
struct Python {
sf::Sprite sprites[PYTHON_MAX_LENGTH] ;
sf::Texture body_images[3] ;
sf::Texture head ;
int length, x[PYTHON_MAX_LENGTH], y[PYTHON_MAX_LENGTH], direction[PYTHON_MAX_LENGTH], colors[PYTHON_MAX_LENGTH] ;
bool board_occupied[col_num][row_num] ;
} ;
```
- **.length**
蟒蛇長度(包含蟒蛇的頭)。比如說如果現在蟒蛇只有頭,那麼 **.length** 應該等於 $1$;如果現在除了頭之外,還有 $3$ 節身體,那麼 **.length** 應該等於 $4$。
- **.x[ ]** 和 **.y[ ]**
儲存的是蟒蛇每一節身體的所在座標,**.x[ ]** 和 **.y[ ]** 顧名思義分別儲存蟒蛇身體的 X 座標和 Y 座標,不清楚遊戲地圖座標如何編號的同學可以回去上面遊戲規則看看。索引值 $0$ 儲存的是蟒蛇頭部的座標,索引值 $1$ 儲存的是緊接在頭部之後的第 $1$ 個身體的座標,其他以此類推。
- **.direction[ ]**:
儲存的是蟒蛇每一節身體的面向,跟上方的座標一樣,索引值 $0$ 儲存的是蟒蛇頭部的面向,索引值 $i$ 儲存的是從頭部往後數第 $i$ 個身體的面向。陣列中儲存的面向由數字 $0、1、 2、3$ 分別表示左、上、右、下,不清楚的可以看下方的附圖。這個陣列中儲存的面向會影響到每節身體的畫面顯示,所以在之後的函式實作中都要記得更新 **.direction[ ]** 中的值。
<center>

</center>
- **.colors[ ]**:
儲存的是每節身體的顏色,頭部沒有顏色的區別,所以索引值 $0$ 的位置就不用管它,第 $1$ 節身體的顏色就儲存在索引值 $1$ 的位置,第 $i$ 節身體就儲存在索引值 $i$ 的地方。另外,陣列中儲存的值是由 $0、1、2$ 分別表示紅色、橘色、黃色的身體。至於哪節身體部位應該要是什麼顏色有在上面的遊戲規則部分解說了。
- **.board_occupied[ ][ ]**
此二維陣列的索引值分別填入 X 座標和 Y 座標,代表的是遊戲地圖中 (X, Y) 那格格子有沒有被蟒蛇的身體部位(包括頭部)佔用,如果沒有則應儲存 $false$;反之,應儲存 $true$。之後在撰寫蟒蛇的移動或是蟒蛇的新身體生成的程式碼時,都需要注意更新 **.board_occupied[ ][ ]**。
2. **Apple**
```cpp=
struct Apple {
sf::Texture images[3] ;
sf::Sprite sprite ;
int life_time, x, y, color ;
} ;
```
- **.life_time**
儲存目前的蘋果從生成開始算起,經歷了幾幀畫面。
- **.x 和 .y**
儲存目前生成的蘋果位置。關於遊戲地圖的座標表示法在上方的遊戲規則有做介紹了。
- **.color**
儲存目前蘋果的顏色。數字 $0、1、2$ 分別代表紅色、橘色、黃色。
3. **ScoreBoard**
```cpp=
struct ScoreBoard {
sf::Font font ;
sf::Text text ;
sf::Texture images[4] ;
sf::Sprite sprite[4] ;
sf::Text scores[4] ;
sf::Text game_over ;
sf::Text restart ;
int score_num[4] ;
} ;
```
- **.score_num[ ]**:在這部分之中,索引值 $0、1、2、3$ 分別儲存總分、吞食紅色蘋果數、吞食橘色蘋果數、吞食黃色蘋果數。
最後也要向大家介紹一些宣告在 `main.cpp` 之中的全域變數:
- **python**、**apple**、**score_board**:這三個變數分別是用上一段提到的結構 **Python**、**Apple**、**ScoreBoard** 作為變數型別宣告的,之後大家會修改或取用它們結構中的成員。比較有趣的是我後來才發現計分板的英文 scoreboard 是一個字,不是兩個字XD
- **current_direction**:這個變數會儲存 $0$ 到 $3$ 之間的整數,分別對應的方位與上方介紹 **Python** 結構中成員 **.direction[ ]** 的部分相同。此變數會根據玩家的鍵盤輸入改變,代表的是蟒蛇接下來會往哪個方向移動。每一幀都需要根據 **current_direction** 對蟒蛇的位置進行更新。
- **game_running**:以此變數紀錄遊戲是否還要繼續進行。如果儲存的值為 $false$ 代表遊戲將結束;如果儲存的值為 $true$ 則代表遊戲將繼續進行。
- **win_game**:以此變數紀錄玩家是否達成遊戲的勝利條件。如果儲存的值為 $false$ 代表遊戲失敗;如果儲存的值為 $true$ 則代表遊戲勝利。
- **game_mode**:以此變數紀錄遊戲模式。如果儲存的值為 $false$ 則代表手動模式;如果儲存的值為 $true$ 則代表自動移動模式。
## 作業要求
在這次作業需要完成的四個函式之中,會推薦大家以以下的順序完成作業:
1. `scoreboard.cpp` 中的 `update_scoreboard_score()`
2. `apple.cpp` 中的 `spawn_apple()`
3. `python.cpp` 中的 `update_python()`
4. `python.cpp` 中的 `auto_mode()`
在作業程式碼需要大家完成的部分都有一個 **// TODO** 的區塊,並附上簡短說明。請大家把程式碼寫在 **// TODO** 下方標註 **// code here** 的區塊(如果看 **// code here** 不順眼可以把它刪掉沒關係)。
### 1. update_scoreboard_score()
```cpp=
void update_scoreboard_score() {
extern Apple apple ;
extern Python python ;
extern ScoreBoard score_board ;
apple.life_time++ ;
if (apple.x == python.x[0] && apple.y == python.y[0]) {
// TODO: Update scoreboard.
// code here
spawn_apple() ;
}
}
```
在這部分,請用目前蘋果的顏色做判斷,為計分板做更新。請記得更新 $2$ 個數值:總分和對應的蘋果顆數。$紅色蘋果數 \times 3 + 橘色蘋果數 \times 2 + 黃色蘋果數 \times 1$ 應等於總分。
:::success
#### 需要更新的變數列表:
- score_board.score_num[ ]
:::
### 2. spawn_apple()
```cpp=
void spawn_apple() {
extern Apple apple ;
extern Python python ;
apple.life_time = 0 ;
apple.color = 0 ;
// set random position
std::random_device dev ;
std::mt19937 rng(dev()) ; // Mersenne Twister engine
std::uniform_int_distribution<int> dist6(1, col_num * row_num - python.length) ;
int random_num = dist6(rng) ;
// TODO: Update apple.x and apple.y according to random_num.
// code here
apple.x = 0 ;
apple.y = 0 ;
// load images
apple.images[0].loadFromFile("../images/apple_red.png") ;
apple.images[1].loadFromFile("../images/apple_orange.png") ;
apple.images[2].loadFromFile("../images/apple_yellow.png") ;
apple.sprite.setTexture(apple.images[0]) ;
apple.sprite.setPosition(apple.x * grid_length + board_left, apple.y * grid_length + board_top) ;
}
```
這個函式是用來在隨機的位置生成蘋果的。程式碼中,**random\_num** 介於 $1$ 和**扣除蛇的長度之後剩餘的格子數**。
我們會從左至右,從上至下數格子,並且跳過蟒蛇所在的格子,從 $1$ 開始數 **random\_num** 格,這個第 **random\_num** 格就是你需要生成蘋果的位置。
目前程式碼中是直接將 **apple.x** 和 **apple.y** 都直接設為 $0$,所以大家如果還沒撰寫這部分的程式碼的話,蘋果會固定在左上角生成。大家需要做的是將 **apple.x** 和 **apple.y** 更改成你透過 **random\_num** 推算出來的座標位置。
舉例來說,假設說現在蟒蛇的蛇身位置分佈圖如下圖,並且現在 **random\_num** 為 $35$,那麼你需要生成的蘋果的位置就是下圖中被藍筆圈起來的格子,也就是座標(9, 2)的格子。

:::success
#### 需要更新的變數列表:
- **apple.x** 、 **apple.y**
:::
### 3. update_python()
```cpp=
void update_python() {
extern Python python ;
extern int current_direction ;
extern bool game_running ;
extern Apple apple ;
extern bool win_game ;
// TODO 4: You might need to store information about the last segment of python's body for later use.
// code here
// TODO 1: Update the positions for python's body by copying the direction, x-axis, and y-axis from the i-th segment to the (i+1)-th segment.
// TODO 1: Also, you should update python.board_occupied
// code here
// TODO 2: Modify the following code to handle situations where python can't directly turn around.
// update head
if (current_direction == 0) {
python.x[0] -= 1 ;
} else if (current_direction == 1) {
python.y[0] -= 1 ;
} else if (current_direction == 2) {
python.x[0] += 1 ;
} else if (current_direction == 3) {
python.y[0] += 1 ;
}
python.direction[0] = current_direction ;
// TODO 3: Check if the game is over. If not, update python.board_occupied.
// code here
// TODO 4: Add a new body part to python if python's head is at the same position as the apple.
// TODO 4: Remember to set the position, direction, and color for the new body part.
// TODO 4: Also, remember to update python.length and python.board_occupied.
// code here
// check python.length
if (python.length == row_num * col_num) {
game_running = false ;
win_game = true ;
}
}
```
這部分算是作業的重頭戲,總共有 $4$ 個 **TODO** 要做,實作的順序推薦大家由 **TODO 1** 依序寫到 **TODO 4**。
另外,在這個函式中只幫大家完成蟒蛇頭部的移動,且不包括遊戲結束的判斷,所以在完全沒有撰寫程式碼的情況下,大家會看到一個蟒蛇的頭跑來跑去,甚至可以跑出遊戲地圖!
- **TODO 1:蟒蛇身體更新(不含頭部)**
在這部分需要大家實作蟒蛇身體的更新(不包括頭部)。實際上更新的方式就是讓蟒蛇的下一節身體照抄上一節身體的位置與面向資訊,達到一個移動的效果,比如說 **python[8].x** 就該更新成 **python[7].x** 原本儲存的值,**python[1].direction** 也該更新成 **python[0].direction** 原本儲存的值。另外,也需要大家更新 **python.board_occupied[ ][ ]**,如果有蟒蛇不再佔用的格子請記得設為 $false$。
:::success
#### TODO 1 需要更新的變數列表:
- **python.direction[ ]**
- **python.x[ ]**
- **python.y[ ]**
- **python.board_occupied[ ][ ]**
這部分的變數更新都只需要考慮身體部位(不含頭部),也就是索引值 $1$ 到 (**python.length** - $1$) 的部分。
:::
- **TODO 2:蟒蛇頭部更新**
在這部分我們要處理的只有蟒蛇的頭部(蛇頭......舌頭......?)。一般來說我們只要根據 **current_direction** 去更新頭部的座標資訊就好。
比如說 **current_direction** 是 $0$ ,代表現在蟒蛇頭部該向左,那麼我們就會把 **python.x[0]** 扣掉一,並且將 **python.direction[0]** 設為跟 **current_direction** 一樣的 $0$。
而這部分是作業中已經提供給大家的程式碼中處理好的狀況。
但是細心的同學會發現說在「蟒蛇有除了頭部以外的身體部位」的狀況下,如果上一幀蟒蛇是向左移動,而這一幀是向右移動,那麼蟒蛇一定會撞到緊接在頭部之後的第一節身體。這種因為上一幀和這一幀的移動方向相反而造成蟒蛇會撞到自己身體的情況我們稱之為「直接迴轉」。
要避免掉這種玩家笨笨的「直接迴轉」的狀況,遊戲機制規定如果你根據 **current_direction** 和上一幀的方向(也就是更新前的 **python.direction[0]**)發現玩家「直接迴轉」了,那麼我們在這一幀就該無視掉 **current_direction**,讓蟒蛇頭部根據上一幀的面向移動,也就是直接讓蟒蛇根據更新前的 **python.direction[0]** 移動。
這部分不管是新增程式碼或是修改既有的程式碼都可以,只要達成:
- 只有蟒蛇頭部時可以「直接迴轉」。
- 有了身體部位時不能「直接迴轉」,讓蟒蛇頭部照著上一幀的移動方向移動。
這兩個條件就好。
:::success
#### TODO 2 需要更新的變數列表:
- **python.direction[0]**
- **python.x[0]**
- **python.y[0]**
也就是頭部的面向和座標。
:::
- **TODO 3:遊戲結束判斷**
如果上方的 **TODO 1** 和 **TODO 2** 都完成之後,就處理完整條蟒蛇的位置更新了!接下來要做的就是判斷遊戲是否 Game Over。實際上遊戲 Game Over 會發生在兩種情況底下:
- 更新後的蟒蛇頭部超出遊戲地圖。
- 更新後的蟒蛇頭部與更新後的任一身體部位重疊。
只要發生上述任一狀況,請將全域變數 **game_running** 改為 $false$,讓遊戲 Game Over;否則,請更新 **TODO 2** 之中沒有請大家更新的 **python.board_occupied[ ][ ]** ,將頭部更新後的位置設成 $true$。
:::success
#### TODO 3 需要更新的變數列表:
- 如果遊戲結束:**game_running**
- 如果遊戲沒有結束:**python.board_occupied[ ][ ]**
:::
- **TODO 4:生成新的身體部位**
如果上方的 **TODO 1**、**TODO 2** 和 **TODO 3** 都完成之後,基本上就完成蟒蛇的所有更新了,接下來要做的就是判斷更新過後的蟒蛇頭部是否與現在的蘋果位置重疊。如果重疊了,那麼我們就該根據遊戲規則讓蟒蛇生成新的身體部位。
各位需要更新的包括 **python.length**、新生成身體部位的 X 和 Y 座標、新生成身體部位的面向、新生成身體部位的顏色和 **python.board_occupied[][]** 。
- **python.length** 要記得加一。
- 新生成身體部位的 X、Y 座標值和新生成身體部位的面向會照抄上一幀中(也就是更新前)最後一節身體的資訊。大家會發現函式最上方也有個 **TODO 4** 的註解,請在那邊先將上一幀最後一節身體的資訊記錄下來,儲存下來給這邊要生成身體部位的我們使用。
- 新生成身體部位的顏色和蘋果的顏色相同。
- 請記得用將新生成身體部位的座標更新 **python.board_occupied[ ][ ]。**
:::success
#### TODO 4 需要更新的變數列表:
- **python.length**
- **python.x[ ]** 和 **python.y[ ]**
- **python.direction[ ]**
- **python.colors[ ]**
- **python.board_occupied[ ][ ]**
:::
### 4. auto_mode()
```cpp=
void auto_mode() {
extern Python python ;
extern int current_direction ;
// TODO: Set current_direction for auto mode. The direction you set will change python's position in the next frame.
// TODO: For example, suppose Python is currently positioned at (x, y), and you set current_direction to 0. Then, in the next frame, Python will move to (x - 1, y).
// Example
// if ((python.x[0] + python.y[0]) % 2) {
// current_direction = 1 ;
// } else {
// current_direction = 2 ;
// }
}
```
在這部分會需要大家撰寫程式,設法讓蟒蛇透過某種設計好的移動方式達到遊戲勝利的條件:讓蟒蛇的長度達到 **col_num $\times$ row_num**,也就是讓蟒蛇的身體佔滿整個版面。其中一種方式如下圖所示,但是大家還是可以發想自己的方法,只要能達到遊戲勝利的條件就好!

實際上如果要讓蟒蛇根據你設定的移動方式移動,只要更改 **current_direction** 就好,蟒蛇在你設定好 **current_direction** 之後就會照著其方向移動。
需要注意的是,`auto_mode()` 這個函式會在每一幀開始前被呼叫,所以大家需要做的是根據每一幀的位置資訊去更改 **current_direction** 的值。
如果不太知道怎麼下手的話可以參考目前被註解起來的範例程式碼,大家可能會根據 **python.x[ ]** 和 **python.y[ ]** 進行條件判斷,規劃在哪種情況下 **current_direction** 應該被設定成什麼樣子。範例程式碼所做的是讓蟒蛇交替著往上方和右方移動。
最後就是在測試這部分程式碼的時候,有可能會嫌蟒蛇跑太慢,這時候就可以參考上面檔案介紹中寫的方法將遊戲的幀率調高,將 **frame_rate** 調成 $500.0f$ 會是一個不錯的數值。
:::success
#### 需要更新的變數列表:
- **current_direction**
:::
## 作業配分
- #### update_scoreboard_score() - $10$ 分
- 成功更改分數但是總分統計錯誤或是吞食蘋果顆數統計錯誤:得 $5$ 分
- 完全正確:得 $10$ 分
- #### spawn_apple() - $25$ 分
- 成功使蘋果於隨機位置生成但是位置與題目要求不符:得 $10$ 分
- 完全正確:得 $25$ 分
- #### update_python() - $35$ 分
- **TODO 1**:佔 $10$ 分
- **TODO 2**:佔 $10$ 分
- **TODO 3**:佔 $5$ 分
- **TODO 4**:佔 $10$ 分
這部分一樣會視情況對於每個 **TODO** 給部分分。
- #### auto_mode() - $28$ 分
- 成功使蟒蛇根據講師提供的解法或相似解法移動並達成遊戲勝利:得 $25$ 分
- 成功達成遊戲勝利並且解法與講師所提供的解法不同:得 $28$ 分
解法與講師解法相似的定義:講師的解法的上下、左右翻轉或是順逆時針變化。
- #### coding style - $2$ 分
這部分不會對大家太苛刻,主要是希望大家可以在寫程式的時候注重一下自己的程式碼可讀性。要拿到這部分的分數可以注意 coding style 的前後一致性(比如說不要一下大括號換行一下不換行)和程式碼的空行與空格(適時的空行分出程式碼的段落;適時的空格使程式碼不至於太擁擠難讀)。
## 作業繳交
### 作業死線
**2024/04/25 23:59**
### 繳交格式
請大家將作業壓縮成一個 .zip 檔繳交,檔案解壓縮之後的架構應該會像是下面那樣。上傳資料夾請以你的 Sprout OJ ID 命名,他大概會長這樣:
```
9999
├── /src
├── apple.cpp
├── game.cpp
├── grid.cpp
├── main.cpp
├── python.cpp
├── scoreboard.cpp
└── setting.hpp
└── (report.pdf)
```
請注意不要把你的 `build` 和 `CMakeLists.txt` 放上來,因為我會在我的電腦上重新編譯一次。另外也請不要把一些奇奇怪怪的檔案,比如說 VSCode 自己生成的 `.vscode` 檔案上傳。
至於 `report.pdf` 的部分是可有可無的,這個檔案的目的主要是讓你闡述你達成了哪些作業目標但是沒達成哪些作業目標,確保我在評分時不會誤判,算是對你分數的一種保障。如果你很確定你的作業完全寫完了或是不在乎成績,也可以不用交這個檔案,大家就開心就好!如果要繳交這個檔案的話請把這個報告的排版排得好看一點,拜託拜託,不然我會看得很辛苦><。檔案格式可以根據上面的評分標準一一條列,你每個 part 做了什麼,應該拿到哪些部分分。
確認好檔案沒問題,並且壓縮成 .zip 檔案之後就可以到[這個表單](https://forms.gle/AX3x5gheAfuV8eN98)上傳你的作業了!