# pingpong_ML
> F14071075 @2021
上一次的 Arkanoid game 筆記: [Arkanoid game](/551W3kEET0CBQm1YS3RLtg)
- 原始遊戲Github網址: https://github.com/LanKuDot/MLGame/tree/beta7.1.3/games/pingpong#readme
環境:
---
- Ubuntu 20.04
- git
- python 3.8.5
- pygame == 2.0.1
- sklearn == 0.22.2.post1
簡介:
---
- 遊戲介紹:
- 上方為2P, 下方為1P, 各自控制水平移動的板子,將球擊回去。
- 球落地算輸
- 球速會逐漸加速
- 本次實驗流程:
- 首先,寫出一個透過 Rule 判定與控制遊戲的程式碼,讓程式可以自己與自己對打。
- (也就是透過程式直接計算落點、控制板子移動)
- 1P、2P 都要由自己的程式控制。
- 這步驟很重要,成果會直接影響訓練結果
- 而後,使用 該程式 遊玩遊戲,將遊戲歷程紀錄成 .pickle 檔案保存
- 再透過 遊戲歷程.pickle 訓練 ML
- 最後用 ML 預測/控制遊戲水平移動的板子
- (透過另一個 "ml_play.py" 檔案,讀取剛剛訓練的 ML model 和 回傳指令給遊戲)
下載遊戲:
---
- 在terminal開啟想要儲存的資料夾後,使用git複製資料庫:
`git clone https://github.com/LanKuDot/MLGame.git`
- 手動遊玩:
- 進入到MLGame資料夾後,
`python MLGame.py -m pingpong HARD 3`
- 控制:

- 參數設定:
- `-m` 手動模式
- `-f 300` fps 300 模式
- `-r` 將遊戲記錄成`.pickle`檔案存放到 `MLGame/games/pingpong/log`資料夾內
使用程式操控遊戲:
---
- 在 MLGame/games/pingpong/ml 資料夾中新增 test01.py 檔案:
- 編輯`test01.py`
- 程式控制遊戲遊玩:
- `python3 MLGame.py -r -i test01.py -f 500 pingpong NORMAL 3`
- `python MLGame.py -i ml_play_template.py pingpong EASY 3`
Rule_Base 控制遊戲進行:
---
- 背景:
- 可取得之遊戲 Info
- ball_x
- ball_y
- platform_1P_x
- platform_1P_y
- platform_2P_x
- platform_2P_y
- frame
- blocker
- 可操控之遊戲 Cmd
- SERVE_TO_LEFT
- SERVE_TO_RIGHT
- MOVE_LEFT
- MOVE_RIGHT
- NONE
- RESET
- 遊戲長寬:
- 左到右: 0 ~ 200
- 上到下方平台: 0 ~ 400
- ball: 5
- platform: 40
- **結論**:
- **ball_x: 0 ~ 195**
- **ball_y: 0 ~ 395**
- **platform 實際涵蓋: platform ~ platform+40**
- 遊戲分析:
- ball 上升過程不重要
- ball 下降時與牆壁碰撞所構成的 x 軸運動為 簡諧運動SHM
- 半週期為 195
- 完整週期為 390
- 
> ## 注意!在實驗過程中,我發現牛頓在哭
> - 首先,這個模型理應是建立在 無摩擦力的完全碰撞 環境。
> - 所以我透過簡諧運動去預測球體落點。這本身應該沒有任何問題,但是卻很常發生“預測落點”和"實際落點"並不相符合的狀況。
> 
> - 花了很多時間檢查程式碼,卻沒有發現問題。正當我一籌莫展時,突然發現一個關鍵問題:
> 
> - 為什麼球以 +10 速度,從 190 衝撞牆壁(牆壁位於195位置)的時候,卻在 195 瞬間停止?而且從 195 反彈回來時,又重新獲得 10 的速度?! 理論上從 190 +10後 下一個點應該是撞到牆(195)反彈回 190 吧?
> - 而後我明白了。這是個"伸縮"牆壁。也就是 **每一次簡諧運動,牆壁的實際邊界都是不同的!!!**。而再修這這項謎團後,就可以真正預測出準確的落點了。(for側面牆壁: real_wall = 195 + ball_speed_x - ( 195 % ball_speed_x ))
> 
> 而後我發現,同理,下方的 1P 球拍(板子) 也會發生該問題。
> 
> - 拜託如果要把牛頓當空氣,先跟我說一下,我找這個Bug找了很久OAO
- 計算出落點:
- P1

- P2

- 但僅僅是依靠"球下降時計算出落點"這件事,不足以闖過 "HARD" 難度的遊戲。
- 因為在 HARD 難度時,中間會多出一個移動的漂浮台("blocker"),當球速隨時間上升後,若碰撞到漂浮台很可能會來不及移動球拍(板子)到球的落點
- 所以我們要多考慮三種狀況:
- 當球朝向遠離板子的方向行進時:
- 直接移動到 假設會撞到漂浮台並反彈回來的落點
- 透過 "伸縮"簡諧運動可以很簡單的計算出來若是撞到漂浮台反彈後的落點
- P1

- P2

- 若對方的球飛過來的過程中撞到漂浮台的側邊,導致行進路線與預測之落點不同:
- (我沒算這部分。祝好運^^)
- 並且,因為最後的模型**要和別人的模型對戰**,因此我想將之複雜化:**切球**
- 我在板子和球距離小於12的時候,控制板子向某個特定方向
- 要朝向哪個方向切球,是依據 (blocker_x < platform_x)&&(ball_speed > 0)決定
- 若結果為真,則向右切球:
- 用(blocker_x < platform_x)&&(ball_speed > 0)的原因是為了盡量減少打出去的球碰到漂浮版彈回來的次數

- Rule_base 結果呈現:
- 注意:**因為會切球,所以有時球的x軸速度會與y軸速度不同。**

> Rule Base 的遊戲控制寫法,將會極大的影響到後來使用 ML 訓練模組時的結果
遊戲紀錄檔(.pickle)處理:
---
- 遊戲執行時加上`-r`就會自動產生遊戲紀錄檔,而記錄檔會在`MLGame/games/pingpong/log`資料夾內
因為名稱都很亂,為了方便等等使用:
**批量編號改名(Linux):**
- 在terminal中,進入到`log`資料夾後,用指令
`i=1; for x in *; do mv $x $i.pickle; let i=i+1; done`
將所有檔案重新編號為 `1.pickle` `2.pickle` `3.pickle`...
- 改變之前:

- 改變之後:

上傳遊戲.pickle至google colab
---
- 將剛剛被重新編號過的`.pickle`檔案上傳至 Google Colab
- 
訓練Model
===
KNN
---
- **KNN 01:** (with ml_knn.py)
https://drive.google.com/file/d/1FHY1Qn__bXtTRn9kX37xSZfg4zp5p4Dc/view?usp=sharing
- 修改自上次遊戲之KNN
- Model 訓練架構:
- Input: `[目前ball_x, 目前ball_y, 目前platform_x, 195 - ball_x, ball_x - 195]`
- Output: `[0, 1, 2]` # 分別代表(NONE, MOVE_LEFT, MOVE_RIGHT)
- 訓練資料前處理:
- Train_X: 所有的 `[目前ball_x, 目前ball_y, 目前platform_x, 195 - ball_x, ball_x - 195]` 直接作為 input
- Train_Y: 讀取 data['ml_1P'/'ml_2P']['command']並且轉換為 0, 1, 2
- 總共使用 181 個 Rule_Base 遊玩 NORMAL 3 產生的 .pickle 檔案訓練
- Result:
- 儘管f1分數看起來很高,但事實上幾乎無法正常玩遊戲。

- 結果不盡理想。歸咎原因很可能是因為:
- 因為rule_base有使用到切球機制,導致不容易訓練(在球接近板子時會發生錯亂,朝向不正確的方向前進)
- 分數僅僅是代表"指令"是否吻合。無法完全代表實際的情況(板子位置是否相同)

- **KNN 02:** (with ml_knn_04.py)
https://drive.google.com/file/d/19yEu5vz8ipz-qwQJ-fX00LW8U4ILEGvq/view?usp=sharing
- 修改自**KNN 01**
- feature
- 增加 "對手platform_x" 以幫助 overfitting (...)
- Result:
- 儘管f1分數有所提升,但仍然無法到正常玩遊戲。

- 結果不盡理想。歸咎原因仍然與KNN 01相似。:


Random_Forest Classifier
---
- **RFC 01:** (with ml_knn.py)
https://drive.google.com/file/d/1FHY1Qn__bXtTRn9kX37xSZfg4zp5p4Dc/view?usp=sharing
- 修改自上次遊戲之KNN
- Model 訓練架構:
- Input: `[目前ball_x, 目前ball_y, 目前platform_x, 195 - ball_x, ball_x - 195]`
- Output: `[0, 1, 2]` # 分別代表(NONE, MOVE_LEFT, MOVE_RIGHT)
- 訓練資料前處理:
- Train_X: 所有的 `[目前ball_x, 目前ball_y, 目前platform_x, 195 - ball_x, ball_x - 195]` 直接作為 input
- Train_Y: 讀取 data['ml_1P'/'ml_2P']['command']並且轉換為 0, 1, 2
- 總共使用 **39** 個 Rule_Base 遊玩 NORMAL 3 產生的 .pickle 檔案訓練
- Result:
- 儘管f1分數看起來不會到太糟,但事實上連發球都接不到。

- 結果不呈現了。
Gradient_Boosting Classifier
---
- **GBC 01**
- 在此奉勸大家,寫完程式要再檢查一下,不要直接衝結果,會浪費很多時間
- 先看個結果
- Result:

- 有點糟,但好像也沒那麼糟、對吧?
- 結果是完全連一顆球都打不到。不巧這其實是我第一個實驗的模型,讓我以為我的整個架構是有問題的,但卻暫時想不到有什麼其他辦法來訓練模型。
- 結果是...手誤把 1P的資料打成2P的資料。訓練出來當然會有問題啊!!!
- 
- **GBC 02~03:** (with ml_knn.py)
https://drive.google.com/file/d/18yyBVkOxf8GCoCbcCrtZ6CjDbxCTvQ0w/view?usp=sharing
- 修改自上次遊戲之 GBC
- Model 訓練架構:
- Input: `[目前ball_x, 目前ball_y, 目前platform_x, 195 - ball_x, ball_x - 195]`
- Output: `[0, 1, 2]` # 分別代表(NONE, MOVE_LEFT, MOVE_RIGHT)
- 訓練資料前處理:
- Train_X: 所有的 `[目前ball_x, 目前ball_y, 目前platform_x, 195 - ball_x, ball_x - 195]` 直接作為 input
- Train_Y: 讀取 data['ml_1P'/'ml_2P']['command']並且轉換為 0, 1, 2
- 總共使用 **181** 個 Rule_Base 遊玩 NORMAL 3 產生的 .pickle 檔案訓練
- Result:
- f1分數達到 98%。

- 目標是直接overfitting 到 100%,所以並沒有在此停留。
- 直接前往下一個模型前進!
- **GBC 04**
https://drive.google.com/file/d/1cFcfWR4pSmsXSVRR87JTyTby9jyePV2c/view?usp=sharing
- 修改自 GBC 02~03
- 增加 feature :
- ball_speed_x
- ball_speed_y
- 對手的 platform_x
- (球速大於1) ? 1 : 0
- Result:
- 達到99%與100%的準確率

- 目標是直接overfitting 到完全 100%,所以並沒有在此停留。
- 直接前往下一個模型前進!
### 實驗最終模型: GBC05
- **Gradient_Boosting 05:** (with ml_knn_07.py)
https://drive.google.com/file/d/1SbXZoIxopzVoGupDYXWJ_blGZ4iqh_5G/view?usp=sharing
- 由GBC 04 增加 feature "blocker_x" 而成
- Model 訓練架構:
- Input: `[目前ball_x, 目前ball_y, 目前platform_x, ball_x - platform_x, platform_y - ball_y, ball_speed_x, ball_speed_y, platform_x_對手, ball_speed_y>0 ? 1 : 0, blocker_x]`
- Output: `[0, 1, 2]` # 分別代表(NONE, MOVE_LEFT, MOVE_RIGHT)
- 訓練資料前處理:
- Train_X: 所有的 `[目前ball_x, 目前ball_y, 目前platform_x, ball_x - platform_x, platform_y - ball_y, ball_speed_x, ball_speed_y, platform_x_對手, ball_speed_y>0 ? 1 : 0, blocker_x]` 直接作為 input
- Train_Y: 讀取 data['ml_1P'/'ml_2P']['command']並且轉換為 0, 1, 2
- 總共使用 181 個 Rule_Base 遊玩 HARD 產生的 .pickle 檔案訓練
- Feature 分析:
- 我認為是真的有用的feature:
- 目前ball_x
- 目前ball_y
- 目前platform_x
- ball_x - platform_x
- platform_y - ball_y
- ball_speed_x
- ball_speed_y
- ball_speed_y>0 ? 1 : 0
- 我認為只是來幫助我overfitting(拿及格成績的feature)(及格要求:五次遊戲中一半的結束速度達|15,15|以上):
- platform_x_對手
- blocker_x
- grid.best_params:
- learning_rate: 1.0
- max_depth: 13
### 能夠做到切球!!!
- 從速度上可以看到,X 速度時常和 Y 不同,代表該球有成功切球。
- 但也因為切球,所以可以發現有時球速會下降。(切球方向與球原動量方向相反)
- 大部分的結束速度都有不錯的水平

- Result:

> 補充: ML訓練過程小插曲:
> 1. 資料前處理惹禍
> 不知道為什麼,在不同的 ML 演算法訓練出來的model,在開始遊戲時的第一步,都不約而同的向右跑?
> 結果是因為資料前處理中,我是修改自上次的程式,沒有更改到第一筆資料的前處理,導致第一筆資料都相同。
> 2. 準確率很高,可是卻完全接不到球?
> 在訓練的過程中發生有好幾次的結果怪怪的,儘管準確率不差(90%以上),但是實際拿到遊戲遊玩的時候,卻甚至連一顆球都打不到?
> 結果是因為,程式是更改自github內的說明程式,但它移動的指令是[-1, 0, 1] 去區分, 而我卻是以 [0, 1, 2] 去區分。這樣當然遊戲結果會很慘淡,因為控制對照全錯了。
> 3. 這次的遊戲歷史資料(.pickle)不只有"NONE",竟然還會出現"None"這種東西?!
> 4. 如果你的 ml_play 會跑完一次就當機,代表你是使用上次的 ml_play 去修改的。注意一下 def update 裡面的scene_info["status"]這部分有更改:
> 
補充: 儲存Model到.pickle格式:
---
- Official: https://scikit-learn.org/stable/modules/model_persistence.html

- import: `import pickle`
- store model.pickle file:
```
f = open('model_name.pickle', 'wb')
pickle.dump(model, f)
f.close()
```
- read model.pickle file:
```
f = open('model_name.pickle', 'rb')
model = pickle.load(f)
f.close()
```
補充: 部分程式碼
---
rule.py:
- 1P

- 2P

為什麼要 Overfitting? 不是應該要避免這件事情發生嗎?
---
- 我上次沒辦法接受,為什麼要把模型train到100%
- 但這次我找了合理解釋:
- 不要跟自己的成績過不去 (x
- 可以用 Q-learning 中的 Q-table 概念來解釋 (O
- Q-Learning
- 透過更新(訓練) Q-Table 的方式,達到程式自己玩遊戲的效果。
- 而 Q-Table 就是,把所有狀況所有排列組合 列成表格(或State),然後透過遊戲過程不斷更新參數,將每個狀況(state)應該要做的最佳行為填入。
- Deep-Q Learning
- 用Nerual Network 取代 Q-Table
- 我就姑且理解它為 "ML-Q Learning" 吧!
- 透過 ML overfitting 訓練集的方式,取代 Q-Learning 中的 Q-Table
- 這樣對於 "Overfitting" 就可以說,我目的是做出Q-Table的model
- 這樣可以說服你合理化 overfitting 了嗎?
- 我覺得不行
- 這樣訓練的結果,導致其實訓練出來的遊戲並不是真的很像會自己玩遊戲
- 比較像是"重現遊戲紀錄"
- 因為若拿兩個人的模型來對決, 我發現基本上"**發球的那方會贏**",也就是模型只是在重現自己的遊戲紀錄而已,根本不知道如何判斷如何操控。
結論
===
在此實驗中,若以目前的實驗結果來看,rule_base絕對是最重要的一個環節。
若沒有良好的紀錄檔用來訓練,要超過及格線很難。
我的程式因為有加入切球的機制,且每次切球的方向都會隨狀態變得不太一樣,因此不容易進行overfitting的訓練。從在KNN訓練中的結果都很慘淡,就可以窺知一二。但在GradientBoostingClassifier的強力bagging下,還是做到了"切球"這件事情,真的很神奇!
心得
===
這次也花了很多時間做不同的嘗試,包括 rule_base 與 ML 模型的建構,

- 特別是 rule_base 的部分,因為要用 rule_base 訓練出一個可以打出速度超過(20,20)的程式,本身就需要照顧到不少小細節,而且還要去發掘並思考牛頓被當空氣的問題,導致這次實驗其實一半以上的時間都是花在這部分,而實際思考 ML 的時間卻並不多。
而對於評分機制,我認為可以有不同的設計方法。
- `"自己與自己對打結束時,速度超過(20, 20)“`這件事,
是否代表他訓練出來的模型就是比較好的模型,我認為有待商榷
- 而且就`"結束時的速度"`作為依據這件事,如我的模型中,因為有切球的因素,會導致球速一度超過20, 但最後又低於 20, 那我是否要選擇當球速超過20時,故意漏接球,使得結果超過(20, 20)?
- 或許可以改為,`"訓練出來的model將會與助教的程式做對打"`,而分數就依照對打的結果去換算,且助教的程式完全不公開也不能事先測試。我想這樣會更吸引人、更讓樂於嘗試不同的實驗方法。