在文章 強化學習實戰 | 自定義Gym環境 中 ,我們瞭解了一個簡單的環境應該如何定義,並使用 print 簡單地呈現了環境。在本文中,我們將學習自定義一個稍微複雜一點的環境——井字棋。回想一下井字棋遊戲:
- 這是一個雙人回合制博弈遊戲,雙方玩家使用的佔位符是不一樣的(圈/叉),動作編寫需要區分玩家
- 雙方玩家獲得的終局獎勵是不一樣的,勝方+1,敗方-1(除非平局+0),獎勵編寫需要區分玩家
- 終局的條件是:任意行 / 列 / 對角 佔滿了相同的佔位符 or 場上沒有空位可以佔位
- 從單個玩家的視角看,當前狀態 s 下采取動作 a 後,新的狀態 s_ 並不是後繼狀態,而是一個等待對手動作的中間狀態,真正的後繼狀態是對手動作之後產生的狀態 s'(除非採取動作 a 後遊戲直接結束),如下圖所示:
-
除了遊戲本身的機制,考慮到與gym的API介面格式的契合,通過外部迴圈控制遊戲程式是較方便的,所以env本身定義時不必要編寫控制遊戲程式 / 切換行動玩家的程式碼。另外,我們還需要更生動的環境呈現方式,而不是print!那麼,接下來我們就來實現上述的目標吧!
步驟1:新建檔案
來到目錄:D:\Anaconda\envs\pytorch1.1\Lib\site-packages\gym\envs\user,建立檔案 __init__.py 和 TicTacToe_env.py(還記得嗎?資料夾user是文章 強化學習實戰 | 自定義Gym環境 中我們建立的用來存放自定義環境的資料夾)。
步驟2:編寫 TicTacToe_env.py 和 __init__.py
gym內建了一個繪圖工具rendering,不過功能並不周全,想要繪製複雜的東西非常麻煩。本文不打算深入研究,只借助rendering中基本的線條 / 方塊 / 圓圈呈現環境(更生動的遊戲表現我們完全可以通過pygame來實現)。rendering是單幀繪製的,當呼叫env.render()時,將呈現當前 self.viewer.geoms 中所記錄的繪畫元素。環境的基本要素設計如下:
- 狀態:由二維的numpy.array表示,無佔位符值為0,有藍色佔位符值為1,有紅色佔位符值為-1。
- 動作:設計為一個字典,有著格式:action = {'mark':'blue', 'pos':(x, y)},其中'mark'表示佔位符的顏色,用以區分玩家,'pos'表示佔位符的位置。
- 獎勵:鎖定藍方視角,勝利+1,失敗-1,平局+0。
TicTacToe_env.py 的整體程式碼如下:
import gym import random import time import numpy as np from gym.envs.classic_control import rendering class TicTacToeEnv(gym.Env): def __init__(self): self.state = np.zeros([3, 3]) self.winner = None WIDTH, HEIGHT = 300, 300 self.viewer = rendering.Viewer(WIDTH, HEIGHT) def reset(self): self.state = np.zeros([3, 3]) self.winner = None self.viewer.geoms.clear() # 清空畫板中需要繪製的元素 self.viewer.onetime_geoms.clear() def step(self, action): # 動作的格式:action = {'mark':'circle'/'cross', 'pos':(x,y)}# 產生狀態 x = action['pos'][0] y = action['pos'][1] if action['mark'] == 'blue': self.state[x][y] = 1 elif action['mark'] == 'red': self.state[x][y] = -1 # 獎勵 done = self.judgeEnd() if done: if self.winner == 'blue': reward = 1 else: reward = -1 else: reward = 0 # 報告 info = {} return self.state, reward, done, info def judgeEnd(self): # 檢查兩對角 check_diag_1 = self.state[0][0] + self.state[1][1] + self.state[2][2] check_diag_2 = self.state[2][0] + self.state[1][1] + self.state[0][2] if check_diag_1 == 3 or check_diag_2 == 3: self.winner = 'blue' return True elif check_diag_1 == -3 or check_diag_2 == -3: self.winner = 'red' return True # 檢查三行三列 state_T = self.state.T for i in range(3): check_row = sum(self.state[i]) # 檢查行 check_col = sum(state_T[i]) # 檢查列 if check_row == 3 or check_col == 3: self.winner = 'blue' return True elif check_row == -3 or check_col == -3: self.winner = 'red' return True # 檢查整個棋盤是否還有空位 empty = [] for i in range(3): for j in range(3): if self.state[i][j] == 0: empty.append((i,j)) if empty == []: return True return False def render(self, mode='human'): SIZE = 100 # 畫分隔線 line1 = rendering.Line((0, 100), (300, 100)) line2 = rendering.Line((0, 200), (300, 200)) line3 = rendering.Line((100, 0), (100, 300)) line4 = rendering.Line((200, 0), (200, 300)) line1.set_color(0, 0, 0) line2.set_color(0, 0, 0) line3.set_color(0, 0, 0) line4.set_color(0, 0, 0) # 將繪畫元素新增至畫板中 self.viewer.add_geom(line1) self.viewer.add_geom(line2) self.viewer.add_geom(line3) self.viewer.add_geom(line4) # 根據self.state畫佔位符 for i in range(3): for j in range(3): if self.state[i][j] == 1: circle = rendering.make_circle(30) # 畫直徑為30的圓 circle.set_color(135/255, 206/255, 250/255) # mark = blue move = rendering.Transform(translation=(i * SIZE + 50, j * SIZE + 50)) # 建立平移操作 circle.add_attr(move) # 將平移操作新增至圓的屬性中 self.viewer.add_geom(circle) # 將圓新增至畫板中 if self.state[i][j] == -1: circle = rendering.make_circle(30) circle.set_color(255/255, 182/255, 193/255) # mark = red move = rendering.Transform(translation=(i * SIZE + 50, j * SIZE + 50)) circle.add_attr(move) self.viewer.add_geom(circle) return self.viewer.render(return_rgb_array=mode == 'rgb_array')
在 __init__.py 中引入類的資訊,新增:
from gym.envs.user.TicTacToe_env import TicTacToeEnv
步驟3:註冊環境
來到目錄:D:\Anaconda\envs\pytorch1.1\Lib\site-packages\gym,開啟 __init__.py,新增程式碼:
register( id="TicTacToeEnv-v0", entry_point="gym.envs.user:TicTacToeEnv", max_episode_steps=20, )
步驟4:測試環境
在測試程式碼中,我們在主迴圈中讓遊戲不斷地進行。藍紅雙方玩家以0.5s的間隔,隨機選擇空格子動作,程式碼如下:
import gym import random import time # 檢視所有已註冊的環境 # from gym import envs # print(envs.registry.all()) def randomAction(env_, mark): # 隨機選擇未佔位的格子動作 action_space = [] for i, row in enumerate(env_.state): for j, one in enumerate(row): if one == 0: action_space.append((i,j)) action_pos = random.choice(action_space) action = {'mark':mark, 'pos':action_pos} return action def randomFirst(): if random.random() > 0.5: # 隨機先後手 first_, second_ = 'blue', 'red' else: first_, second_ = 'red', 'blue' return first_, second_ env = gym.make('TicTacToeEnv-v0') env.reset() # 在第一次step前要先重置環境 不然會報錯 first, second = randomFirst() while True: # 先手行動 action = randomAction(env, first) state, reward, done, info = env.step(action) env.render() time.sleep(0.5) if done: env.reset() env.render() first, second = randomFirst() time.sleep(0.5) continue # 後手行動 action = randomAction(env, second) state, reward, done, info = env.step(action) env.render() time.sleep(0.5) if done: env.reset() env.render() first, second = randomFirst() time.sleep(0.5) continue
效果如下圖所示: