|
| 1 | +# -*- coding: utf-8 -*- |
| 2 | +# 장애물 회피 게임 즉, 자율주행차:-D 게임을 구현합니다. |
| 3 | + |
| 4 | +import numpy as np |
| 5 | +import random |
| 6 | + |
| 7 | +import matplotlib.pyplot as plt |
| 8 | +import matplotlib.patches as patches |
| 9 | + |
| 10 | + |
| 11 | +class Game: |
| 12 | + def __init__(self, screen_width, screen_height, show_game=True): |
| 13 | + self.screen_width = screen_width |
| 14 | + self.screen_height = screen_height |
| 15 | + # 도로의 크기는 스크린의 반으로 정하며, 도로의 좌측 우측의 여백을 계산해둡니다. |
| 16 | + self.road_width = (screen_width / 2) |
| 17 | + self.road_left = self.road_width / 2 + 1 |
| 18 | + self.road_right = self.road_left + self.road_width - 1 |
| 19 | + |
| 20 | + # 자동차와 장애물의 초기 위치와, 장애물 각각의 속도를 정합니다. |
| 21 | + self.car = {"col": 0, "row": 2} |
| 22 | + self.block = [ |
| 23 | + {"col": 0, "row": 0, "speed": 1}, |
| 24 | + {"col": 0, "row": 0, "speed": 2}, |
| 25 | + ] |
| 26 | + |
| 27 | + self.total_reward = 0. |
| 28 | + self.current_reward = 0. |
| 29 | + self.total_game = 0 |
| 30 | + self.show_game = show_game |
| 31 | + |
| 32 | + if show_game: |
| 33 | + self.fig, self.axis = self.prepare_display() |
| 34 | + |
| 35 | + def prepare_display(self): |
| 36 | + """게임을 화면에 보여주기 위해 matplotlib 으로 출력할 화면을 설정합니다.""" |
| 37 | + fig, axis = plt.subplots(figsize=(4, 6)) |
| 38 | + fig.set_size_inches(4, 6) |
| 39 | + # 화면을 닫으면 프로그램을 종료합니다. |
| 40 | + fig.canvas.mpl_connect('close_event', exit) |
| 41 | + plt.axis((0, self.screen_width, 0, self.screen_height)) |
| 42 | + plt.tick_params(top='off', right='off', |
| 43 | + left='off', labelleft='off', |
| 44 | + bottom='off', labelbottom='off') |
| 45 | + |
| 46 | + plt.draw() |
| 47 | + # 게임을 진행하며 화면을 업데이트 할 수 있도록 interactive 모드로 설정합니다. |
| 48 | + plt.ion() |
| 49 | + plt.show() |
| 50 | + |
| 51 | + return fig, axis |
| 52 | + |
| 53 | + def get_state(self): |
| 54 | + """게임의 상태를 가져옵니다. |
| 55 | +
|
| 56 | + 게임의 상태는 screen_width x screen_height 크기로 각 위치에 대한 상태값을 가지고 있으며, |
| 57 | + 빈 공간인 경우에는 0, 사물이 있는 경우에는 1이 들어있는 1차원 배열입니다. |
| 58 | + 계산의 편의성을 위해 2차원 -> 1차원으로 변환하여 사용합니다. |
| 59 | + """ |
| 60 | + state = np.zeros((self.screen_width, self.screen_height)) |
| 61 | + |
| 62 | + state[self.car["col"], self.car["row"]] = 1 |
| 63 | + |
| 64 | + if self.block[0]["row"] < self.screen_height: |
| 65 | + state[self.block[0]["col"], self.block[0]["row"]] = 1 |
| 66 | + |
| 67 | + if self.block[1]["row"] < self.screen_height: |
| 68 | + state[self.block[1]["col"], self.block[1]["row"]] = 1 |
| 69 | + |
| 70 | + return state.reshape((-1, self.screen_width * self.screen_height)) |
| 71 | + |
| 72 | + def draw_screen(self): |
| 73 | + title = " Avg. Reward: %d Reward: %d Total Game: %d" % ( |
| 74 | + self.total_reward / self.total_game, |
| 75 | + self.current_reward, |
| 76 | + self.total_game) |
| 77 | + |
| 78 | + self.axis.clear() |
| 79 | + self.axis.set_title(title, fontsize=12) |
| 80 | + |
| 81 | + road = patches.Rectangle((self.road_left - 1, 0), self.road_width + 1, self.screen_height, linewidth=0, facecolor="#333333") |
| 82 | + # 자동차, 장애물들을 1x1 크기의 정사각형으로 그리도록하며, 좌표를 기준으로 중앙에 위치시킵니다. |
| 83 | + # 자동차의 경우에는 장애물과 충돌시 확인이 가능하도록 0.5만큼 아래쪽으로 이동하여 그립니다. |
| 84 | + car = patches.Rectangle((self.car["col"] - 0.5, self.car["row"] - 0.5), 1, 1, linewidth=0, facecolor="#00FF00") |
| 85 | + block1 = patches.Rectangle((self.block[0]["col"] - 0.5, self.block[0]["row"]), 1, 1, linewidth=0, facecolor="#0000FF") |
| 86 | + block2 = patches.Rectangle((self.block[1]["col"] - 0.5, self.block[1]["row"]), 1, 1, linewidth=0, facecolor="#FF0000") |
| 87 | + |
| 88 | + self.axis.add_patch(road) |
| 89 | + self.axis.add_patch(car) |
| 90 | + self.axis.add_patch(block1) |
| 91 | + self.axis.add_patch(block2) |
| 92 | + |
| 93 | + self.fig.canvas.draw() |
| 94 | + # 게임의 다음 단계 진행을 위해 matplot 의 이벤트 루프를 잠시 멈춥니다. |
| 95 | + plt.pause(0.0001) |
| 96 | + |
| 97 | + def reset(self): |
| 98 | + """자동차, 장애물의 위치와 보상값들을 초기화합니다.""" |
| 99 | + self.current_reward = 0 |
| 100 | + self.total_game += 1 |
| 101 | + |
| 102 | + self.car["col"] = int(self.screen_width / 2) |
| 103 | + |
| 104 | + self.block[0]["col"] = random.randrange(self.road_left, self.road_right + 1) |
| 105 | + self.block[0]["row"] = 0 |
| 106 | + self.block[1]["col"] = random.randrange(self.road_left, self.road_right + 1) |
| 107 | + self.block[1]["row"] = 0 |
| 108 | + |
| 109 | + self.update_block() |
| 110 | + |
| 111 | + def update_car(self, move): |
| 112 | + """액션에 따라 자동차를 이동시킵니다. |
| 113 | +
|
| 114 | + 자동차 위치 제한을 도로가 아니라 화면의 좌우측 끝으로 하고, |
| 115 | + 도로를 넘어가면 패널티를 주도록 학습해서 도로를 넘지 않게 만들면 더욱 좋을 것 같습니다. |
| 116 | + """ |
| 117 | + |
| 118 | + # 자동차의 위치가 도로의 좌측을 넘지 않도록 합니다: max(0, move) > 0 |
| 119 | + self.car["col"] = max(self.road_left, self.car["col"] + move) |
| 120 | + # 자동차의 위치가 도로의 우측을 넘지 않도록 합니다.: min(max, screen_width) < screen_width |
| 121 | + self.car["col"] = min(self.car["col"], self.road_right) |
| 122 | + |
| 123 | + def update_block(self): |
| 124 | + """장애물을 이동시킵니다. |
| 125 | +
|
| 126 | + 장애물이 화면 내에 있는 경우는 각각의 속도에 따라 위치 변경을, |
| 127 | + 화면을 벗어난 경우에는 다시 방해를 시작하도록 재설정을 합니다. |
| 128 | + """ |
| 129 | + reward = 0 |
| 130 | + |
| 131 | + if self.block[0]["row"] > 0: |
| 132 | + self.block[0]["row"] -= self.block[0]["speed"] |
| 133 | + else: |
| 134 | + self.block[0]["col"] = random.randrange(self.road_left, self.road_right + 1) |
| 135 | + self.block[0]["row"] = self.screen_height |
| 136 | + reward += 1 |
| 137 | + |
| 138 | + if self.block[1]["row"] > 0: |
| 139 | + self.block[1]["row"] -= self.block[1]["speed"] |
| 140 | + else: |
| 141 | + self.block[1]["col"] = random.randrange(self.road_left, self.road_right + 1) |
| 142 | + self.block[1]["row"] = self.screen_height |
| 143 | + reward += 1 |
| 144 | + |
| 145 | + return reward |
| 146 | + |
| 147 | + def is_gameover(self): |
| 148 | + # 장애물과 자동차가 충돌했는지를 파악합니다. |
| 149 | + # 사각형 박스의 충돌을 체크하는 것이 아니라 좌표를 체크하는 것이어서 화면에는 약간 다르게 보일 수 있습니다. |
| 150 | + if ((self.car["col"] == self.block[0]["col"] and |
| 151 | + self.car["row"] == self.block[0]["row"]) or |
| 152 | + (self.car["col"] == self.block[1]["col"] and |
| 153 | + self.car["row"] == self.block[1]["row"])): |
| 154 | + |
| 155 | + self.total_reward += self.current_reward |
| 156 | + |
| 157 | + return True |
| 158 | + else: |
| 159 | + return False |
| 160 | + |
| 161 | + def proceed(self, action): |
| 162 | + # action: 0: 좌, 1: 유지, 2: 우 |
| 163 | + # action - 1 을 하여, 좌표를 액션이 0 일 경우 -1 만큼, 2 일 경우 1 만큼 옮깁니다. |
| 164 | + self.update_car(action - 1) |
| 165 | + # 장애물을 이동시킵니다. 장애물이 자동차에 충돌하지 않고 화면을 모두 지나가면 보상을 얻습니다. |
| 166 | + escape_reward = self.update_block() |
| 167 | + # 움직임이 적을 경우에도 보상을 줘서 안정적으로 이동하는 것 처럼 보이게 만듭니다. |
| 168 | + stable_reward = 1. / self.screen_height if action == 1 else 0 |
| 169 | + # 게임이 종료됐는지를 판단합니다. 자동차와 장애물이 충돌했는지를 파악합니다. |
| 170 | + gameover = self.is_gameover() |
| 171 | + |
| 172 | + if gameover: |
| 173 | + # 장애물에 충돌한 경우 -2점을 보상으로 줍니다. 장애물이 두 개이기 때문입니다. |
| 174 | + # 장애물을 회피했을 때 보상을 주지 않고, 충돌한 경우에만 -1점을 주어도 됩니다. |
| 175 | + reward = -2 |
| 176 | + else: |
| 177 | + reward = escape_reward + stable_reward |
| 178 | + self.current_reward += reward |
| 179 | + |
| 180 | + if self.show_game: |
| 181 | + self.draw_screen() |
| 182 | + |
| 183 | + return reward, gameover |
0 commit comments