將遊戲導入 MLGame - LanKuDot/MLGame GitHub Wiki
這個章節介紹如何將製作好的貪食蛇遊戲導入 MLGame 中,讓遊戲可以透過 MLGame 執行。
在開始這個教學之前,建議先看過以下文件,以了解需要提供什麼樣的功能:
建立遊戲類別
這邊會將 snake.py
中的內容逐步整理成一個遊戲類別,供 MLGame 使用。看過遊戲類別需要提供的 API 就可以知道遊戲類別需要的功能有:
- 初始化
- 偵測按鍵指令
- 更新遊戲
- 重設遊戲
- 提供場景資訊
初始化
在貪食蛇遊戲中要初始化的有 pygame
套件,另外要把在無限迴圈中會用到的物件變成類別的屬性,因為會在更新遊戲的功能中使用到。首先在 snake.py
中的 if __name__ == "__main__"
上方建立 Snake
類別,並先移動初始化的程式碼:
import pygame
from .gamecore import Scene
class Snake:
def ___init__(self):
self._scene = Scene()
self._pygame_init()
def _pygame_init(self):
pygame.display.init()
pygame.display.set_caption("Snake")
self._screen = pygame.display.set_mode(
(Scene.area_rect.width, Scene.area_rect.height + 25))
pygame.font.init()
self._font = pygame.font.Font(None, 22)
self._font_pos = (1, Scene.area_rect.width + 5)
if __name__ == "__main__":
...
Snake
類別中並沒有使用到 pygame.time.Clock
,是因為 MLGame 會幫助控制遊戲更新的頻率。
偵測按鍵指令
接著把偵測按鍵指令的程式碼移動到 Snake
類別的 get_keyboard_command()
函式:
def get_keyboard_command(self):
key_pressed_list = pygame.key.get_pressed()
if key_pressed_list[pygame.K_UP]: action = "UP"
elif key_pressed_list[pygame.K_DOWN]: action = "DOWN"
elif key_pressed_list[pygame.K_LEFT]: action = "LEFT"
elif key_pressed_list[pygame.K_RIGHT]: action = "RIGHT"
else: action = "NONE"
return {"ml": action}
注意回傳值必須是一個 dict,裡面存有對應每個機器學習端名字的指令。
更新遊戲
將 while
迴圈中更新遊戲相關的程式碼,放到 update()
中。MLGame 每一次都會執行遊戲類別的 update()
來讓遊戲持續進行:
def update(self, action_dict):
# Check the action
if action_dict["ml"] not in ("UP", "DOWN", "LEFT", "RIGHT", "NONE"):
action = "NONE"
# Pass the command to the scene and get the status
game_status = self._scene.update(action)
# If the game is over, send the reset signal
if game_status == "GAME_OVER":
print("Score: {}".format(self._scene.score))
return "RESET"
# Draw the scene
self._screen.fill((50, 50, 50))
self._screen.fill((0, 0, 0), Scene.area_rect)
self._scene.draw_gameobjects(self._screen)
# Draw score
font_surface = self._font.render(
"Score: {}".format(self._scene.score), True, (255, 255, 255))
self._screen.blit(font_surface, self._font_pos)
pygame.display.flip()
在 update()
的最前面多的程式碼是檢查傳入的指令的正確性。因為玩家的程式碼可能會傳錯誤的指令,所以當收到無效的指令時,就將指令視為 "NONE"
。在收到錯誤指令時,也可以輸出訊息提示玩家。另外在遊戲結束時,要回傳 "RESET"
字串,讓 MLGame 呼叫 reset()
來重設遊戲。
重設遊戲
將重設遊戲的程式碼放到 reset()
中:
def reset(self):
self._scene.reset()
執行遊戲
建立好遊戲類別 Snake
後,在 if __name__ = "__main__"
下改成建立一個 Snake
物件,讓遊戲依然可以透過這個檔案執行,獨立在 MLGame 外:
if __name__ == "__main__":
snake = Snake()
clock = pygame.time.Clock()
is_running = True
while is_running:
action = snake.get_keyboard_command()
result = snake.update(action)
if result == "RESET":
snake.reset()
# Wait FPS
clock.tick(10)
# If ESC key or 'X' button on the window is pressed, quit the loop.
for event in pygame.event.get():
if (event.type == pygame.QUIT or
(event.type == pygame.KEYDOWN and event.key == pygame.K_ESCAPE)):
is_running = False
break
提供場景資訊
場景資訊是給玩家程式碼判斷場景情況與產生紀錄檔用的,格式通常為一個 dict。以貪食蛇遊戲來說,就是包含食物和所有蛇身的位置,而管理這些物件的是 gamecore.py
中的 Scene
類別,所以先在這個類別上提供一個可以取得場景資訊的函式:
class Scene:
def __init__(self):
...
self._status = "GAME_ALIVE"
def update(self, action):
...
if (not Scene.area_rect.collidepoint(self._snake.head_pos) or
self._snake.is_body_pos(self._snake.head_pos)):
self._status = "GAME_OVER"
return self._status
def reset(self):
...
self._status = "GAME_ALIVE"
def get_scene_info(self):
scene_info = {
"status": self._status,
"snake_head": self._snake.head_pos,
"snake_body": [body.pos for body in self._snake],
"food": self._food.pos
}
return scene_info
場景資訊中需要包含遊戲狀態,讓玩家可以知道什麼時候遊戲結束,因此增加一個屬性 _status
來紀錄目前的遊戲狀態,而不是在 update()
中直接回傳。get_scene_info()
回傳的場景資訊會像這樣:
{
'status': 'GAME_ALIVE',
'snake_head': (160, 40),
'snake_body': [(150, 40), (140, 40), (130, 40)], # From head to tail
'food': (100, 60)
}
最後在 snake.py
中的 Snake
類別加入 get_player_scene_info()
讓 MLGame 可以取得給玩家的場景資訊:
class Snake:
...
def get_player_scene_info(self):
return {"ml": self._scene.get_scene_info()}
到此,貪食蛇遊戲準備好放到 MLGame 中了。
將遊戲導入 MLGame
加入遊戲資料夾
在 MLGame 的 games
資料夾下新增一個 snake
的子資料夾,並在其下新增 __init__.py
、game
資料夾、ml
資料夾。將貪食蛇的遊戲程式碼放在 game
資料夾中。最後在 game
與 ml
資料夾中個新增一個 __init__.py
檔案。整個資料夾結構如下:
games/
└── snake/
├── __init__.py
├── game/
│ ├── __init__.py
│ ├── gamecore.py
│ ├── gameobject.py
│ └── snake.py
└── ml/
└── __init__.py
config.py
設定 在 snake
資料夾下新增一個 config.py
來設定貪食蛇遊戲:
GAME_VERSION = "1.0"
GAME_PARAMS = {
"()": {
"prog": "snake",
"description": "A simple snake game",
"game_usage": "%(prog)s"
}
}
from .game.snake import Snake
GAME_SETUP = {
"game": Snake,
"ml_clients": [
{"name": "ml"}
]
}
貪食蛇遊戲沒有遊戲參數,所以 GAME_PARAMS
使用最簡單的格式。而貪食蛇遊戲只有一個玩家,所以要在 "ml_clients"
中指定一個機器學習端。
到這裡使用 python MLGame.py -f 10 -m snake
來以手動模式執行貪食蛇,看看能不能在 MLGame 下正常執行。使用 python MLGame.py snake -h
可以印出遊戲的幫助訊息。
機器學習端範例程式
如果要執行機器學習模式,就必須要在 ml
資料夾裡提供玩家程式。所以在這邊新增一個簡單的玩家程式供執行,同時作為機器學習端的範例程式。
在 ml
資料夾下新增一個 ml_play_template.py
:
class MLPlay:
def __init__(self):
pass
def update(self, scene_info):
if scene_info["status"] == "GAME_OVER":
return "RESET"
snake_head = scene_info["snake_head"]
food = scene_info["food"]
if snake_head[0] > food[0]:
return "LEFT"
elif snake_head[0] < food[0]:
return "RIGHT"
elif snake_head[1] > food[1]:
return "UP"
elif snake_head[1] < food[1]:
return "DOWN"
def reset(self):
pass
在 MLPlay
類別中初始化與 reset()
並不需要做任何事情,所以直接 pass
。在 update()
中,會判斷遊戲結束後,要告知 MLGame 的機器學習端重設以進行下一個回合。在這邊會簡單判斷蛇頭與食物的相對位置,並給予對應的指令。
使用 python MLGame.py -f 10 -i ml_play_template.py snake
來執行機器學習模式:
因為沒有使用到蛇身位置的資訊,所以蛇會為了吃食物撞自己或是不知道自己想直接往後退而無法改變方向,造成遊戲結束。想要有一個能過關的蛇,就是玩家的任務了!