Skip to content

遊戲與 MLPlay 類別

LanKuDot edited this page Oct 5, 2020 · 2 revisions

這個章節介紹遊戲類別與 MLPlay 類別所需要提供的函式與功能。要注意每個提供的函式不能有無限迴圈,否則會造成 MLGame 無法持續執行。如何將一個現有的遊戲導入到 MLGame 中,可以參考貪食蛇的教學將遊戲導入 MLGame

遊戲類別 API

在 MLGame 中,遊戲需要提供一個遊戲類別,並含有指定的函式供 MLGame 的執行核心傳入或取得資料。

Game.__init__(init_arg_1, init_arg_2, ...)

初始化遊戲類別,所有遊戲的初始化內容都在這邊完成,像是初始化遊戲所需的 pygame 模組。

init_arg_x 是傳入給遊戲類別的初始化參數,與在 config.py 中設定的 GAME_PARAMS 順序一樣。如果沒有遊戲參數,則不用指定。例如,遊戲在 config.py 設定:

GAME_PARAMS = {
    "()": {...},
    "difficulty": {
        "choices": ("EASY", "NORMAL"),
    },
    "level": {
        "type": int,
    }
}

那麼,遊戲類別的 __init__() 第一個參數就是 difficulty,第二個參數是 level

$ python MLGame.py -i ml_play.py some_game EASY 2

class Game:
    def __init__(self, difficulty, level):  # Game("EASY", 2)
        ...

game.update(command: dict) -> str

遊戲的主要進行函式,每一次呼叫就讓遊戲更新一次(一個影格),遊戲更新畫面也是在這裡執行。

command 會傳入玩家的指令,資料類型為一個 dict。該 dict 的鍵值為機器學習端的名字,而其值就是該機器學習端傳來的指令:

# config.py
GAME_SETUP = {
    ...,
    "ml_clients" : [
        { "name": "ml_1P" },
        { "name": "ml_2P" }
    ]
}

# Game class
def update(self, command: dict):
    print(command.get("ml_1P")) # 輸出從 ml_1P 玩家送來的指令
    print(command.get("ml_2P")) # 輸出從 ml_2P 玩家送來的指令

如果該玩家來不及傳送或是沒有傳送指令的話,則得到的值會是 None

如果遊戲是動態玩家人數,也就是 GAME_SETUP 中的 dynamic_ml_clientsTrue 的話,則沒被使用的機器學習端不會被包含在 command 裡:

# config.py
GAME_SETUP = {
    ...,
    "dynamic_ml_clients": True,
    "ml_clients" : [
        { "name": "ml_1P" },
        { "name": "ml_2P" },
        { "name": "ml_3P" }
    ]
}

# Game class
def update(self, command: dict):
    print(command.get("ml_1P")) # 輸出從 ml_1P 玩家送來的指令
    print(command.get("ml_2P")) # 輸出從 ml_2P 玩家送來的指令
    print(command.get("ml_3P")) # 永遠是 None,因為鍵值 ml_3P 不存在於 command 裡
    print(command["ml_3P"]) # KeyError

建議使用 dict.get(key[, default]) 來取得 dict 裡面的值,避免因為 command 中沒有該鍵值而產生 KeyError

要注意的是 MLGame 並不會檢查玩家傳得指令是否有效,所以在使用之前建議先檢查其值並轉換成遊戲可用的指令。例如:

VALID_COMMANDS = ("UP", "DOWN", "LEFT", "RIGHT", "NONE")

class Game:
    ...
    def update(self, command: dict):
        if not command.get("ml_1P") or command.get("ml_1P") not in VALID_COMMANDS:
            command_1P = "NONE"
            ...

update() 在執行完後,需要回傳指定的字串來告知 MLGame 是否該繼續執行:

  • "RESET":重置遊戲,讓 MLGame 呼叫 reset()
  • "QUIT":結束遊戲,MLGame 會結束執行
class Game:
    ...
    def update(self, command):
        ...

        if game_status == "GAME_OVER":
            return "RESET"

如果不回傳任何值,或是不是上面兩個字串,則 MLGame 會繼續執行。

game.reset()

重置遊戲。在 MLGame 從 update() 收到 "RESET" 後,會直接呼叫這個函式來重置遊戲。

game.get_player_scene_info() -> dict

產生給玩家的場景資訊,供玩家判斷或是訓練模型使用。

回傳的場景資訊必須是一個 dict,其鍵值為對應的機器學習端名字,其值為該機器學習端要收到的檔景資訊,因此可以為不同的機器學習端提供不同的場景資訊(例如:只有它可以知道的資訊):

{
    "ml_1P": {
        "status": "GAME_ALIVE",
        "sensor_L": 10,
        "sensor_R": 5,
        "sensor_F": 2
    },
    "ml_2P": {
        "status": "GAME_ALIVE",
        "sensor_L": 7,
        "sensor_R": 7,
        "sensor_F": 12
    }
}

如果提供的場景資訊缺少給正在執行的機器學習端的資訊,則 MLGame 會直接回報錯誤並停止執行。但是如果是動態玩家數量的遊戲,則可以不提供場景資訊給未被啟用的機器學習端,像是沒有該機器學習端的欄位,或是給予 None,執行過程也不會被除存到紀錄檔中。

game.get_keyboard_command() -> dict

偵測按下的按鍵,並回傳對應的指令。這個函式用在 MLGame 的手動模式中,以模擬機器學習端的執行,用於產生紀錄檔。

get_keyboard_command() 所回傳的 dict 會直接傳給 update() 執行,因此回傳的格式必須與 MLGame 傳給 update() 的格式一樣:

def get_keyboard_command(self):
    key_pressed_list = pygame.key.get_pressed()

    if   key_pressed_list[pygame.K_PERIOD]: cmd_1P = "SERVE_TO_LEFT"
    elif key_pressed_list[pygame.K_SLASH]:  cmd_1P = "SERVE_TO_RIGHT"
    elif key_pressed_list[pygame.K_LEFT]:   cmd_1P = "MOVE_LEFT"
    elif key_pressed_list[pygame.K_RIGHT]:  cmd_1P = "MOVE_RIGHT"
    else: cmd_1P = "NONE"

    if   key_pressed_list[pygame.K_q]:  cmd_2P = "SERVE_TO_LEFT"
    elif key_pressed_list[pygame.K_e]:  cmd_2P = "SERVE_TO_RIGHT"
    elif key_pressed_list[pygame.K_a]:  cmd_2P = "MOVE_LEFT"
    elif key_pressed_list[pygame.K_d]:  cmd_2P = "MOVE_RIGHT"
    else: cmd_2P = "NONE"

    return {"ml_1P": cmd_1P, "ml_2P": cmd_2P}

MLPlay 類別 API

MLPlay 是玩家提供來玩遊戲的類別,一樣需要含有指定的函式供 MLGame 的機器學習端呼叫。所有執行於機器學習端的程式碼,都必須要有 MLPlay 類別。

MLPlay.__init__(init_arg_1, init_arg_2, ...)

初始化 MLPlay 類別。在這裡初始化所需要的模組、讀取資料等。

MLGame 的機器學習端在呼叫完這個函式後,才會告知遊戲端準備好了。

init_arg_x 是遊戲傳給玩家的參數,與 config.py 中的 GAME_SETUPml_clients 中指定的參數一樣(argskwargs),如果沒有,則不須提供。例如:遊戲在 config.py 中設定:

GAME_SETUP = {
    "game": some_game,
    "ml_clients": [
        { "name": "ml_1P", "args": ("1P",) },
        { "name": "ml_2P", "args": ("2P",) },
    ]
}

MLPlay 類別會需要一個參數來取得遊戲類別指定的值:

class MLPlay:
    def __init__(self, side):   # MLPlay("1P") for "ml_1P" player
        ...

mlplay.update(scene_info) -> command or "RESET"

玩家程式主要進行的函式。處理接受到的場景資訊,並回傳遊戲指定的指令或是 "RESET"

參數 scene_info 會是從遊戲類別傳送的場景資訊,即 game.get_player_scene_info() 所回傳的物件。

update() 回傳的指令會傳給遊戲類別使用,但如果沒有回傳任何物件(即 None),則不會傳給遊戲類別。如果回傳的是 "RESET",則 MLGame 的機器學習端會直接呼叫 reset() 來重設機器學習端,因此 update() 內要能夠判定遊戲是否結束,並告知重設。

class MLPlay:
    def update(self, scene_info):
        if scene_info["status"] == "GAME_OVER":
            return "RESET"

        # do something
        ...
        return "UP"

mlplay.reset()

重設機器學習端。在 update() 回傳 "RESET" 後會呼叫此函式。

MLGame 的機器學習端在呼叫完這個函式後,才會告知遊戲端它已經準備好下一輪遊戲了。