Last active
October 18, 2020 16:06
-
-
Save youngsoul/29321f190415b5f316b27d2088c197d2 to your computer and use it in GitHub Desktop.
Environment for grid based Reinforcement learning game
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import numpy as np | |
| from scipy.spatial import distance | |
| from PIL import Image, ImageDraw, ImageFont | |
| import cv2 | |
| import time | |
| class BaseBlob: | |
| game_blobs = [] | |
| wall_blobs = [] | |
| def __init__(self, size=20, bgr=(255,255,255)): | |
| self.size = size | |
| self.x = np.random.randint(0, self.size) | |
| self.y = np.random.randint(0, self.size) | |
| self.bgr = bgr | |
| def __str__(self): | |
| return f"{self.x}, {self.y}" | |
| def __sub__(self, rhs_blob): | |
| return (self.x-rhs_blob.x, self.y-rhs_blob.y) | |
| def __eq__(self, rhs_blob): | |
| return (self - rhs_blob) == (0,0) | |
| def action(self, choice): | |
| raise Exception("No Action was defined") | |
| def move(self, x,y): | |
| raise Exception("No Move was defined") | |
| class WallBlob(BaseBlob): | |
| def __init__(self, size=20, bgr=(192,192,192)): | |
| super().__init__(size, bgr) | |
| while (self.x, self.y) in BaseBlob.wall_blobs: | |
| self.x = np.random.randint(0, self.size) | |
| self.y = np.random.randint(0, self.size) | |
| BaseBlob.wall_blobs.append((self.x,self.y)) | |
| class Blob(BaseBlob): | |
| def __init__(self, size=20, bgr=(255,255,255)): | |
| super().__init__(size, bgr) | |
| # make sure that no blob overlaps with another | |
| while (self.x, self.y) in BaseBlob.game_blobs or (self.x, self.y) in BaseBlob.wall_blobs: | |
| self.x = np.random.randint(0, self.size) | |
| self.y = np.random.randint(0, self.size) | |
| BaseBlob.game_blobs.append((self.x, self.y)) | |
| def action(self, choice): | |
| """ | |
| Gives us 8 total movement options: | |
| - 0,1,2,3 | |
| - 0: upper right | |
| - 1: lower left | |
| - 2: upper left | |
| - 3: lower right | |
| - 4: up | |
| - 5: down | |
| - 6: left | |
| - 7: right | |
| :param choice: | |
| :return: | |
| """ | |
| if choice == 0: # upper right | |
| self.move(x=1, y=1) | |
| elif choice == 1: # lower left | |
| self.move(x=-1, y=-1) | |
| elif choice == 2: # upper left | |
| self.move(x=-1, y=1) | |
| elif choice == 3: # lower right | |
| self.move(x=1, y=-1) | |
| elif choice == 4: # up | |
| self.move(x=0, y=1) | |
| elif choice == 5: # down | |
| self.move(x=0, y=-1) | |
| elif choice == 6: # left | |
| self.move(x=-1, y=0) | |
| elif choice == 7: # right | |
| self.move(x=1, y=0) | |
| def move(self, x=-99, y=-99): | |
| """ | |
| :param x: amount in the x direction to move | |
| :param y: amount in the y direction to move | |
| :return: True - the move was possible, False - could not move either out of bounds or hit a wall | |
| """ | |
| # if no value for x, move random | |
| original_x = self.x | |
| original_y = self.y | |
| result = True # assume we can perform the action | |
| if x == -99: | |
| self.x += np.random.randint(-1,2) | |
| else: | |
| self.x += x | |
| # if no value for y, move random | |
| if y == -99: | |
| self.y += np.random.randint(-1,2) | |
| else: | |
| self.y += y | |
| # check to make sure we did not move to a wall | |
| for wall_blob in BaseBlob.wall_blobs: | |
| if wall_blob == (self.x, self.y): | |
| # if we moved onto a way, go back to where we were before hitting the wall | |
| self.x = original_x | |
| self.y = original_y | |
| result = False | |
| break | |
| # check for out of bounds | |
| if self.x < 0: | |
| self.x = 0 | |
| result = False | |
| elif self.x > self.size-1: | |
| self.x = self.size - 1 | |
| result = False | |
| if self.y < 0: | |
| self.y = 0 | |
| result = False | |
| elif self.y > self.size - 1: | |
| self.y = self.size - 1 | |
| result = False | |
| return result | |
| class _ActionSpace: | |
| number_of_actions = 8 | |
| """ | |
| Gives us 8 total movement options: | |
| - 0,1,2,3 | |
| - 0: upper right | |
| - 1: lower left | |
| - 2: upper left | |
| - 3: lower right | |
| - 4: up | |
| - 5: down | |
| - 6: left | |
| - 7: right | |
| """ | |
| def __init__(self): | |
| self.n = _ActionSpace.number_of_actions | |
| self.actionspace = [0,1,2,3,4,5,6,7] | |
| def action(self, choice, blob): | |
| """ | |
| Gives us 8 total movement options: | |
| - 0,1,2,3 | |
| - 0: upper right | |
| - 1: lower left | |
| - 2: upper left | |
| - 3: lower right | |
| - 4: up | |
| - 5: down | |
| - 6: left | |
| - 7: right | |
| :param choice: | |
| :return: True - if action was successful, False if not | |
| """ | |
| result = True | |
| if choice == 0: # upper right | |
| result = blob.move(x=1, y=1) | |
| elif choice == 1: # lower left | |
| result = blob.move(x=-1, y=-1) | |
| elif choice == 2: # upper left | |
| result = blob.move(x=-1, y=1) | |
| elif choice == 3: # lower right | |
| result = blob.move(x=1, y=-1) | |
| elif choice == 4: # up | |
| result = blob.move(x=0, y=1) | |
| elif choice == 5: # down | |
| result = blob.move(x=0, y=-1) | |
| elif choice == 6: # left | |
| result = blob.move(x=-1, y=0) | |
| elif choice == 7: # right | |
| result = blob.move(x=1, y=0) | |
| return result | |
| class _ObservationSpace: | |
| """ | |
| The observation space, or state space is: | |
| for an NxN grid: | |
| Distance calculation from player to win square, [above,below,inline] to win square, [left,right,inline] to win square, distance calculation from player to lose square, [above,below,inline] to lose square, [left,right,inline] to lose square, actions | |
| """ | |
| def __init__(self, size, number_of_blobs=2): | |
| """ | |
| :param size: | |
| :param number_of_blobs: typically 2, 1-win and 1-lose blob | |
| """ | |
| self.size = size | |
| self.max_distance_indexes = int(np.sqrt(self.size ** 2 + self.size ** 2)) | |
| total_space = () | |
| for _ in range(number_of_blobs): | |
| total_space = total_space + (self.max_distance_indexes, 3, 3,) | |
| total_space = total_space + (_ActionSpace.number_of_actions, ) | |
| self.dqn_observation_space = (size,size,3) | |
| self.observation_space = total_space | |
| def _distance(self, from_blob, to_blob): | |
| return int(distance.euclidean((to_blob.x, to_blob.y), (from_blob.x, from_blob.y))) | |
| def state(self, player_blob, win_blob, lose_blobs): | |
| d = self._distance(player_blob, win_blob) | |
| above_below = 0 # is this blob above, below or inline with the to_blob | |
| left_right = 0 # is this blob to the left, right or inline with the to_blob | |
| if win_blob.y > player_blob.y: | |
| above_below = 2 # win above player | |
| elif win_blob.y < player_blob.y: | |
| above_below = 1 # win below player | |
| if win_blob.x > player_blob.x: | |
| left_right = 2 # win blob to right | |
| elif win_blob.x < player_blob.x: | |
| left_right = 1 # win blob to the left | |
| state = (d, above_below, left_right) | |
| for lose_blob in lose_blobs: | |
| d = self._distance(player_blob, lose_blob) | |
| above_below = 0 # is this blob above, below or inline with the to_blob | |
| left_right = 0 # is this blob to the left, right or inline with the to_blob | |
| if lose_blob.y > player_blob.y: | |
| above_below = 2 | |
| elif lose_blob.y < player_blob.y: | |
| above_below = 1 | |
| if lose_blob.x > player_blob.x: | |
| left_right = 2 | |
| elif lose_blob.x < player_blob.x: | |
| left_right = 1 | |
| state = state + (d, above_below, left_right, ) | |
| return state | |
| class GridEnvironment: | |
| def __init__(self, size=20, lose_blob_count=1, number_of_wall_blobs=0): | |
| self.size = size | |
| self.actionspace = _ActionSpace() | |
| self.observationspace = _ObservationSpace(self.size, number_of_blobs=lose_blob_count+1) | |
| self.q_table = None | |
| self.win_blob = None | |
| self.lose_blobs = [] | |
| self.lose_blob_count = lose_blob_count | |
| self.player_blob = None | |
| self.MOVE_PENALTY = -1 | |
| self.LOSE_PENALTY = -300 | |
| self.WIN_REWARD = 25 | |
| self.NON_MOVE_PENALTY = self.MOVE_PENALTY * 4 | |
| self.draw_font = ImageFont.truetype("/Library/Fonts/Arial Narrow.ttf", 20) | |
| self.wall_blobs = [] | |
| if number_of_wall_blobs > 0: | |
| for wall_blob in range(0, number_of_wall_blobs): | |
| wall_blob = WallBlob(size=size) | |
| self.wall_blobs.append(wall_blob) | |
| def reset(self): | |
| BaseBlob.game_blobs = [] | |
| self.win_blob = Blob(size=self.size, bgr=(0,255,0)) | |
| self.player_blob = Blob(size=self.size, bgr=(255,175,0)) | |
| self.lose_blobs = [] | |
| for _ in range(self.lose_blob_count): | |
| self.lose_blobs.append(Blob(size=self.size, bgr=(0,0,255))) | |
| not_done = True | |
| while not_done: | |
| blobs_moved = [] | |
| for i, lose_blob in enumerate(self.lose_blobs): | |
| if self.win_blob - lose_blob == (0, 0): | |
| blobs_moved.append(i) | |
| if len(blobs_moved) > 0: | |
| for x in blobs_moved: | |
| self.lose_blobs[x] = Blob(size=self.size, bgr=(0,0,255)) | |
| else: | |
| not_done = False | |
| initial_state = self.observationspace.state(self.player_blob, self.win_blob, self.lose_blobs) | |
| return initial_state | |
| def create_q_table(self): | |
| self.q_table = np.random.uniform(size=self.observationspace.observation_space) | |
| return self.q_table | |
| def step(self, action_choice): | |
| """ | |
| Step the player | |
| :param action_choice: one of: [0,1,2,3,4,5,6,7] | |
| :return: next_state, reward | |
| """ | |
| action_result = self.actionspace.action(action_choice, self.player_blob) | |
| new_state = self.observationspace.state(self.player_blob, self.win_blob, self.lose_blobs) | |
| # Calculate Reward | |
| done = False | |
| for lose_blob in self.lose_blobs: | |
| if self.player_blob - lose_blob == (0,0): | |
| reward = self.LOSE_PENALTY | |
| done = True | |
| break | |
| if not done: | |
| if self.player_blob - self.win_blob == (0, 0): | |
| reward = self.WIN_REWARD | |
| done = True | |
| else: | |
| if action_result: | |
| # then we were able to move | |
| reward = self.MOVE_PENALTY | |
| else: | |
| # we could not perform the action. perhaps at the boundary or we hit a wall | |
| # we want to penalize these slightly more | |
| reward = self.NON_MOVE_PENALTY | |
| return new_state, reward, done | |
| def _draw_grid(self, image): | |
| step_count = self.size | |
| # Draw a grid | |
| draw = ImageDraw.Draw(image) | |
| y_start = 0 | |
| y_end = image.height | |
| step_size = int(image.width / step_count) | |
| for x in range(0, image.width, step_size): | |
| line = ((x, y_start), (x, y_end)) | |
| draw.line(line, fill=255) | |
| x_start = 0 | |
| x_end = image.width | |
| for y in range(0, image.height, step_size): | |
| line = ((x_start, y), (x_end, y)) | |
| draw.line(line, fill=255) | |
| def render(self, show=True, slow_render=0, done=False, episode_number = 0, move_number = 0, show_grid=False, total_episodes = None, episode_reward=None): | |
| """ | |
| :param show: True - display the game | |
| :param slow_render delay to slow the rendering down. 0 - do not slow down rendering | |
| :return: boolean: | |
| """ | |
| if show: | |
| game_canvas = np.zeros((self.size, self.size, 3), dtype=np.uint8) # starts an rbg of our size | |
| if self.win_blob - self.player_blob != (0,0): | |
| # if the win blob and player blog are in different places draw the | |
| # winning blob | |
| game_canvas[self.win_blob.x][self.win_blob.y] = self.win_blob.bgr # sets the food location tile to green color | |
| for lose_blob in self.lose_blobs: | |
| game_canvas[lose_blob.x][lose_blob.y] = lose_blob.bgr # sets the enemy location to red | |
| game_canvas[self.player_blob.x][self.player_blob.y] = self.player_blob.bgr # sets the player tile to blue | |
| for wall_blob in self.wall_blobs: | |
| game_canvas[wall_blob.x][wall_blob.y] = wall_blob.bgr # sets the wall location | |
| img = Image.fromarray(game_canvas, 'RGB') # reading to rgb. Apparently. Even tho color definitions are bgr. ??? | |
| img = img.resize((400, 400)) # resizing so we can see our agent in all its glory. | |
| if show_grid: | |
| self._draw_grid(img) | |
| if episode_number >= 0 and move_number >= 0: | |
| draw = ImageDraw.Draw(img) | |
| episode_str = f"Episode: {episode_number}" | |
| if total_episodes: | |
| episode_str += f" / {total_episodes}" | |
| episode_reward_str = "" | |
| if episode_reward is not None: | |
| episode_reward_str = f"Reward: {episode_reward}" | |
| summary_text = f"{episode_str} Move: {move_number} {episode_reward_str}" | |
| draw.text((0, 0), summary_text, font=self.draw_font) | |
| # print(summary_text) | |
| cv2.imshow("GridEnvironment", np.array(img)) # show it! | |
| if cv2.waitKey(1) & 0xFF == ord('q'): | |
| cv2.destroyAllWindows() | |
| if slow_render: | |
| time.sleep(slow_render) | |
| if done: | |
| time.sleep(1.0) | |
| def destroy(self): | |
| cv2.destroyAllWindows() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment