[강화시스터즈 1기/프로젝트/환경세미나] 폭탄제거부대 01팀
init
| input | 그리드월드 사이즈, 지뢰 개수 | | — | — |
def __init__(self, gridworld_size:Tuple, num_mine:int):
self.gridworld_size = gridworld_size
self.nrow, self.ncol = self.gridworld_size
self.num_mine = num_mine
# 그리드월드의 좌표(튜플)의 리스트
# points == action space
self.points = []
for i in range(self.nrow):
for j in range(self.ncol):
self.points.append((i,j))
self.num_actions = len(self.points)
# 보상 딕셔너리
self.reward_dict = {'mine':-10, 'empty':1, 'clear':10}
self.points_unvisit = self.points.copy()
# 지뢰 랜덤으로 배정
self.mine_points = random.sample(self.points, self.num_mine)
# 그리드 월드 rendering (지뢰: 'M')
self.gridworld = np.full(shape=(self.nrow, self.ncol), fill_value=".")
for x,y in self.mine_points:
self.gridworld[x,y] = 'M'
# 지뢰 = True인 맵
self.mine_bool = (self.gridworld=='M')
# 주변 지뢰 개수를 표시한 맵 (지뢰 위치: -1)
self.map_answer = np.zeros(self.gridworld_size)
for x,y in self.points:
cnt = self.check_mine((x,y))
self.map_answer[x,y] = cnt
# state 맵
self.present_state = np.full((self.nrow, self.ncol), -2) # BFS로 탐색하지 않은 부분을 -2로 초기화
# 방문 맵(bool)
self.visited_map = (self.present_state == self.map_answer)
-
좌표 리스트
self.points = [] for i in range(self.nrow): for j in range(self.ncol): self.points.append((i,j)) self.num_actions = len(self.points)
- type:
list
- 그리드 월드의 모든 좌표를 튜플로 담은 좌표 리스트
self.points
== Action space
- type:
-
보상 딕셔너리
# 보상 딕셔너리 self.reward_dict = {'mine':-10, 'empty':1, 'clear':10}
- type :
dict
- 지뢰 : -10
- 지뢰X : 1
- 게임을 성공한 경우 : 10
- type :
-
맵 & 지뢰
self.points_unvisit = self.points.copy()
- type :
list
- 액션에 따라 이미 방문한 좌표를 제외한 좌표 리스트
self.mine_points = random.sample(self.points, self.num_mine)
- type :
list
- 주어진 지뢰 개수만큼 지뢰의 좌표를 랜덤으로 뽑음
self.gridworld = np.full(shape=(self.nrow, self.ncol), fill_value=".") for x,y in self.mine_points: self.gridworld[x,y] = 'M' self.mine_bool = (self.gridworld=='M')
self.gridworld
: 그리드 월드 렌더링 (str 형식)- type :
np.array
- 빈 곳 : .
- 지뢰 : M
- type :
self.mine_bool
: 지뢰 위치를 True로 하는 맵 (bool 형식)
self.map_answer = np.zeros(self.gridworld_size) for x,y in self.points: cnt = self.check_mine((x,y)) self.map_answer[x,y] = cnt
self.map_answer
: 각 좌표 별로 주변 지뢰 개수를 계산한 정답 맵- type :
np.array
self.check_mine(좌표)
: 클래스 내장 함수
- type :
self.present_state = np.full((self.nrow, self.ncol), -2) self.visited_map = (self.present_state == self.map_answer)
self.present_state
: 에이전트에게 전달하는 상태 맵- type :
np.array
- 탐색하지 않은 좌표를 -2로 표시
- 처음에는 전부 -2 상태
- type :
self.visited_map
: 방문 맵. (bool 형식)- type :
np.array
- 방문한 좌표 : True
- type :
- type :
맵 생성 관련 함수
check_mine
-
해당 좌표 근처 지뢰 개수를 구하는 함수
input 좌표 (Tuple) output 좌표 근처 8칸에 있는 지뢰 개수 (int)
def check_mine(self, coord:Tuple):
directions = [(-1, 0), (1, 0), (0, -1), (0, 1),
(-1, -1), (-1, 1), (1, -1), (1, 1)]
x, y = coord
result = 0
if self.mine_bool[x,y]:
result = -1
else:
for dx, dy in directions:
nx, ny = x + dx, y + dy
if 0 <= nx < self.nrow and 0 <= ny < self.ncol:
if self.mine_bool[nx, ny]:
result += 1
return int(result)
directions = [(-1, 0), (1, 0), (0, -1), (0, 1),
(-1, -1), (-1, 1), (1, -1), (1, 1)]
x, y = coord
result = 0
direction
: 좌표 주변 8칸을 탐색하기 위한 리스트x
,y
: 입력받은 좌표를 넣음result
: 주변 지뢰 개수 (초기값 : 0)
if self.mine_bool[x,y]:
result = -1
- 해당 좌표가 지뢰인 경우 -1 반환
else:
for dx, dy in directions:
nx, ny = x + dx, y + dy
if 0 <= nx < self.nrow and 0 <= ny < self.ncol:
if self.mine_bool[nx, ny]:
result += 1
- 주어진 좌표의 주변 8칸을 탐색하며 만약 탐색한 좌표에 지뢰가 있으면 result에 +1
- 맵 가장자리 고려
bfs_minesweeper
-
가려져있는 맵에서 클릭할 좌표에 따라 맵을 열어주는 함수
input 클릭할 좌표(Tuple) output 클릭한 좌표에 따라서 열린 맵(array)
def bfs_minesweeper(self, clicked_point:Tuple):
queue = deque([clicked_point])
directions = [(-1, 0), (1, 0), (0, -1), (0, 1),
(-1, -1), (-1, 1), (1, -1), (1, 1)]
# 방문 맵(bool)
visited = self.visited_map.copy()
result = self.present_state.copy()
while queue:
x, y = queue.popleft()
if visited[x, y]:
continue
visited[x, y] = True
result[x, y] = self.map_answer[x, y]
if self.map_answer[x,y] == 0:
for dx, dy in directions:
nx, ny = x + dx, y + dy
if 0 <= nx < self.nrow and 0 <= ny < self.ncol and not visited[nx, ny]:
queue.append((nx, ny))
self.visited_map = visited
return result
directions = [(-1, 0), (1, 0), (0, -1), (0, 1),
(-1, -1), (-1, 1), (1, -1), (1, 1)]
visited = self.visited_map.copy()
result = self.present_state.copy()
direction
: 좌표 주변 8칸을 탐색하기 위한 리스트visited
: 방문 맵. 현재 열려있는 좌표를 True로 하는 bool 맵result
: 현재 상태 맵에서 클릭한 좌표에 따라 열린 맵. (초기값 : 현재 상태 맵)
queue = deque([clicked_point])
while queue:
x, y = queue.popleft()
if visited[x, y]:
continue
visited[x, y] = True
result[x, y] = self.map_answer[x, y]
if self.map_answer[x,y] == 0:
for dx, dy in directions:
nx, ny = x + dx, y + dy
if 0 <= nx < self.nrow and 0 <= ny < self.ncol and not visited[nx, ny]:
queue.append((nx, ny))
- 초기 : 클릭한 좌표만
queue
에 들어있음 (deque 자료형) queue
에 모든 좌표를 탐색할 때까지queue.popleft()
: queue에 들어있는 좌표의 x, y값을 가져오면서 동시에 제거한다.- 이미 방문한 좌표면 종료
- 아니라면 방문 맵에서 해당 좌표를 True로 바꾸고,
result
맵에서 해당 좌표를 연다. - 만약 해당 좌표의 값이 0이라면 주변을 탐색해
queue
에 넣는다.- 맵 가장자리 고려, 방문 여부 고려
self.visited_map = visited
self.visited_map
업데이트
gridworld_reset
- 그리드월드 렌더링, 지뢰 위치 맵, 정답 맵, 현재 상태 맵을 리셋하는 내장 함수.
- 클래스 외부에서 사용하지 않음!!!
def gridworld_reset(self):
# 그리드 월드 rendering (지뢰: 'M')
self.gridworld = np.full(shape=(self.nrow, self.ncol), fill_value=".")
for x,y in self.mine_points:
self.gridworld[x,y] = 'M'
# 지뢰 = True인 맵
self.mine_bool = (self.gridworld=='M')
# 주변 지뢰 개수를 표시한 맵 (지뢰 위치: -1)
self.map_answer = np.zeros(self.gridworld_size)
for x,y in self.points:
cnt = self.check_mine((x,y))
self.map_answer[x,y] = cnt
self.present_state = np.full((self.nrow, self.ncol), -2)
# 방문 맵(bool)
self.visited_map = (self.present_state == self.map_answer)
move_mine
-
에이전트가 첫 번째로 선택한 action이 지뢰인 경우, 해당 좌표의 지뢰를 다른 곳으로 옮기는 함수
| input | action (Tuple) - 좌표 | | — | — |
def move_mine(self, action:Tuple):
empty_points = list(set(self.points) - set(self.mine_points))
new_mine = random.sample(empty_points, 1)
self.mine_points.remove(action)
self.mine_points.append(new_mine[0])
self.gridworld_reset()
empty_points = list(set(self.points) - set(self.mine_points))
new_mine = random.sample(empty_points, 1)
empty_points
: 지뢰가 없는 좌표 리스트- type :
list
- type :
new_mine
:empty_points
에서 좌표 하나를 뽑아 지뢰로 설정
self.mine_points.remove(action)
self.mine_points.append(new_mine[0])
self.gridworld_reset()
- 선택한 좌표를
self.mine_points
에서 제거 후 new_mine
을 추가- 새로운
self.mine_points
로 모든 맵 리셋
step
-
에이전트가 선택한 action에 따라 주어지는 next_state, reward, done, clear 를 제공하는 함수
input action (Tuple) - 좌표 output next_state(array), reward(int), done(bool), clear(bool)
def step(self, action:Tuple):
x, y = action
# 첫번째 action인 경우
if np.sum(self.visted_map) == 0 :
if action in self.mine_points:
# 만약 start 좌표에 지뢰가 있는 경우 옮기기
self.move_mine(action)
# action에 따라 계산된 state
next_state = self.bfs_minesweeper(action)
# action에 따라 바뀌는 방문 맵(bool)
self.visited_map = (next_state == self.map_answer)
# ======
# reward
if action in self.mine_points:
# 지뢰 밟은 경우 -> 지뢰찾기 실패
# 음수의 보상과 함께 에피소드 종료
reward = self.reward_dict['mine']
done = True
else :
reward = self.reward_dict['empty']
done = False
# ======
# 밟지 않은 좌표 개수 == 지뢰 개수 -> 지뢰찾기 성공
if np.sum(self.visited_map==True) == self.num_mine:
reward = self.reward_dict['clear']
clear = True # 성공했는지 여부 판단을 위해
done = True
else :
clear = False
# 현재 위치 업데이트, 경로 추가
self.present_state = next_state
return next_state, reward, done, clear
if np.sum(self.visted_map) == 0 :
if action in self.mine_points:
self.move_mine(action)
- 첫 번째 액션인 경우,
- 해당 좌표가 지뢰라면
- 해당 좌표의 지뢰를 다른 곳으로 옮긴다.
self.move_mine(좌표)
: 클래스 내장 함수
next_state = self.bfs_minesweeper(action)
- type :
np.array
- 액션에 따라 바뀐 state 맵
self.bfs_minesweeper(좌표)
: 클래스 내장 함수
if action in self.mine_points:
reward = self.reward_dict['mine']
done = True
else :
reward = self.reward_dict['empty']
done = False
- 선택한 좌표가 지뢰인 경우
self.reward_dict
에서‘mine’
에 해당하는 보상 제공 (-10)done
= True
- 지뢰가 아닌 경우
self.reward_dict
에서‘empty’
에 해당하는 보상 제공 (+1)done
= False
if np.sum(self.visited_map==True) == self.num_mine:
reward = self.reward_dict['clear']
clear = True # 성공했는지 여부 판단을 위해
done = True
else :
clear = False
- 지뢰찾기 성공
- 방문하지 않은 좌표의 개수 == 지뢰 개수
self.reward_dict
에서‘clear’
에 해당하는 보상 제공 (+10)done
= Trueclear
= True
- 지뢰찾기 성공 조건을 만족하지 못하면
clear
= False
self.present_state = next_state
self.present_state
업데이트
reset
- 에피소드마다 게임을 리셋하는 함수
def reset(self):
self.mine_points = random.sample(self.points, self.num_mine)
self.gridworld_reset()
- 지뢰 위치 리셋, 모든 맵 리셋
렌더링 관련 함수
render
-
입력받은 state 맵을 렌더링하는 함수
| input | state(np.array) | | — | — |
def render(self, state):
render_state = np.full(shape=(self.nrow, self.ncol), fill_value=".")
for (i,j) in self.points:
if state[i,j] == -2:
continue
elif state[i,j] == -1:
render_state[i,j] = "M"
else:
render_state[i,j] = state[i,j]
render_state = pd.DataFrame(render_state)
render_state = render_state.style.applymap(self.render_color)
display(render_state)
render_state = np.full(shape=(self.nrow, self.ncol), fill_value=".")
for (i,j) in self.points:
if state[i,j] == -2:
continue
elif state[i,j] == -1:
render_state[i,j] = "M"
else:
render_state[i,j] = state[i,j]
- 지뢰와 방문하지 않은 정보를 숫자로 가지고 있는 state 맵을 str 형식으로 바꿈
- 방문하지 않은 좌표 : .
- 지뢰 : M
- 지뢰가 아닌 방문한 좌표 : 해당 숫자
render_state = pd.DataFrame(render_state)
render_state = render_state.style.applymap(self.render_color)
display(render_state)
render_state
: 출력할 맵- type :
np.array
→pd.DataFrame
- 문자, 숫자마자 다르게 색깔을 입힌다.
self.render_color
: 클래스 내장 함수
- type :
render_answer
- 정답 맵을 렌더링하는 함수
def render_answer(self):
render_state = np.full(shape=(self.nrow, self.ncol), fill_value=".")
for (i,j) in self.points:
if self.map_answer[i,j] == -1:
render_state[i,j] = "M"
else:
render_state[i,j] = str(self.map_answer[i,j])
render_state = pd.DataFrame(render_state)
render_state = render_state.style.applymap(self.render_color)
display(render_state)
render_color
- 렌더링 시 색깔을 입히는 함수
def render_color(self, var):
color = {'0':'black', '1':"skyblue", '2':'lightgreen', '3':'red', '4':'violet', '5':'brown',
'6':'turquoise', '7':'grey', '8':'black', 'M':'white', '.':'black'}
return f"color: {color[var]}"
train 관련 함수
sample_10
- 10개의 게임 리스트를 만드는 함수
def sample_10(self):
sample_mine_points = []
for i in range(10):
self.mine_points = random.sample(self.points, self.num_mine)
sample_mine_points.append(self.mine_points)
return sample_mine_points
- 지뢰 좌표 리스트를 10개 만들어 리스트로 저장함
train_reset
-
10개의 샘플만을 이용해 train할 때 사용하는 리셋 함수
| input | sample (list) - 지뢰 좌표 리스트의 샘플 | | — | — |
def train_reset(self, samples:list):
self.mine_points = random.sample(samples, 1)[0]
self.gridworld_reset()
self.mine_points
를 samples에서 뽑아 사용함