將遊戲導入 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__.pygame 資料夾、ml 資料夾。將貪食蛇的遊戲程式碼放在 game 資料夾中。最後在 gameml 資料夾中個新增一個 __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 來執行機器學習模式:

因為沒有使用到蛇身位置的資訊,所以蛇會為了吃食物撞自己或是不知道自己想直接往後退而無法改變方向,造成遊戲結束。想要有一個能過關的蛇,就是玩家的任務了!