문제 소개
https://www.acmicpc.net/problem/17081
지문 길이부터 정신이 나갈 것 같은 문제..
RPG Extreme 문제입니다.
윷놀이 문제도 굉장히 스트레스 받는 문제라고 생각했는데, 이건 진짜 장난아니네요
그래도 윷놀이보다 출력형식은 양심적이라 좋았습니다..
또 이 문제를 만들면서 진짜 게임 만드는 기분이라 꽤 재밌기도 했습니다.
디버깅은 짜증났지만요 ㅋㅋ
문제를 풀기 위해 노트에 "요약해서" 정리한 위 문제의 요구사항입니다...
풀이 과정
저같은 경우 이런 복잡한 구현 문제를 풀 때는 머리속으로 문제를 해결할 큰 틀을 잡고,
해당 틀에 맞춰 입력부, 구현부, 출력부를 나눕니다.
그리고 구현부분에서 어떻게 구현할지 다시 틀을 잡고, 해당 틀에 맞춰 주석을 쭉 작성합니다.
진짜 구현하기 시작할 때는 주석을 참고하며 꼼꼼하게 구현해나갑니다.
이 문제는 단순 구현이기 때문에 실수만 조심하면 (이게 어렵지만요..) 코드 작성은 어렵지 않습니다.
저는 각각의 보드의 기능을 '이벤트'로보고 이벤트를 처리하듯 했습니다.
def Event(row, col):
global character_pos_before, character_info, killer
event = grid[row][col]
# 만약 현재 위치 그대로 이벤트가 발생한 경우
if event == '@':
event = character_pos_before
else:
character_pos_before = event
# 이동한 캐릭터 지도에 표시
grid[row][col] = '@'
if event == '.':
return "blank"
elif event == '&':
return Battle(row, col)
elif event == 'B':
# 아이템 획득, 및 착용
get_item = item_info[row][col]
if get_item["type"] == "W":
character_info["weapon"] = get_item["val"]
elif get_item["type"] == "A":
character_info["armor"] = get_item["val"]
elif get_item["type"] == "O":
if character_info["accessory_count"] < 4:
if not character_info[get_item["val"]]:
character_info[get_item["val"]] = True
character_info["accessory_count"] += 1
# 상자를 열면 해당 칸이 빈칸이 된다.
character_pos_before = '.'
elif event == '^':
# 가시 데미지 분기
damage = 5
if character_info["DX"]:
damage = 1
# 데미지 계산
character_info["hp"] -= damage
# 사망 처리
if character_info["hp"] <= 0:
character_info["hp"] = 0
killer = "SPIKE TRAP"
return "dead"
elif event == 'M':
return Battle(row, col, True)
# 벽은 밖에서 처리
elif event == '#':
assert True
벽에 대한 이벤트는 외부에서 처리했습니다.
이동 없이 현재 자리에서 다시 이벤트를 진행하는 것이기 때문에 특이케이스 였거든요.
그리고 저는 이럴 때 assert 문을 종종 활용합니다.
혹시 코드에 구멍이 생겨 외부에서 처리되지 못하고 벽에대한 부분이 함수로 넘어왔을 때,
이를 런타임에러로 잡아내기 위함입니다. (디버깅 과정이든 제출에서 보든..ㅎㅎ)
그리고 전투에 대한 부분은 다시 함수를 뺐습니다.
전투에 대한 구현이 매우 복잡해질 것이라고 생각해, 이 함수에서 구현하면 너무 복잡할 것 같았거든요.
그럼 안그래도 디버깅하기 힘든 문젠데 나중에 디버깅하기가 더더 힘들어집니다.
def Battle(row, col, is_boss=False):
global character_pos_before, killer
is_first_atk = True
is_no_damage = False
ch_atk = character_info["atk"] + character_info["weapon"]
ch_def = character_info["def"] + character_info["armor"]
monster = monster_info[row][col]
monster_max_hp = monster["hp"]
# HU 장신구 효과 적용
if is_boss and character_info["HU"]:
# 최대 체력으로 회복
character_info["hp"] = character_info["max_hp"]
# 데미지 면역 활성화
is_no_damage = True
while True:
# 데미지 계산
if is_first_atk and character_info["CO"]:
if character_info["DX"]:
character_damage = max(1, 3*ch_atk - monster["def"])
else:
character_damage = max(1, 2*ch_atk - monster["def"])
is_first_atk = False
else:
character_damage = max(1, ch_atk - monster["def"])
monster_damage = max(1, monster["atk"] - ch_def)
# 캐릭터 공격
monster["hp"] -= character_damage
# 몬스터가 죽었을 때
if monster["hp"] <= 0:
# 캐릭터가 있던 곳은 빈칸이 된다.
character_pos_before = '.'
# "HR" 장신구 적용
if character_info["HR"]:
character_info["hp"] = min(character_info["hp"] + 3, character_info["max_hp"])
# 경험치 계산
if character_info["EX"]:
get_exp = int(monster["exp"]*1.2)
else:
get_exp = monster["exp"]
character_info["exp"] += get_exp
# 레벨 업
if character_info["exp"] >= character_info["max_exp"]:
character_info["level"] += 1
character_info["exp"] = 0
character_info["max_exp"] += 5
character_info["atk"] += 2
character_info["def"] += 2
character_info["max_hp"] += 5
character_info["hp"] = character_info["max_hp"]
if is_boss:
return "clear"
return "win"
# 몬스터 공격 (보스 몬스터 & HU 장신구 착용시 첫 공격은 면역)
if is_no_damage:
is_no_damage = False
else:
character_info["hp"] -= monster_damage
# 캐릭터가 죽었을 때
if character_info["hp"] <= 0:
character_info["hp"] = 0
monster_info[now_row][now_col]["hp"] = monster_max_hp
killer = monster["name"]
return "dead"
사실 계속 제출하자마자 틀리고 있었는데,
저는 틀린 이유가 몬스터에게 죽은뒤 체력이 음수가 됐음에도 음수 체력 처리를 안했기 때문이었습니다.
(예제 입력에서 거르지 못한 이유는, 예제 입력이 가시 함정에 의한 음수 체력만을 잡을 수 있는 예제 였습니다.)
그밖에 놓쳤던 부분은 일반 몬스터 뿐만 아니라 보스 몬스터를 잡고난 이후에도
HR 장신구 효과를 적용하는 것을 놓치고 있었습니다.
이 부분은 제 스스로 디버깅하면서 찾았습니다.
# 몬스터, 아이템 숫자 세기 & 캐릭터 초기 위치 파악
for i in range(row_size):
for j in range(col_size):
if grid[i][j] == '&' or grid[i][j] == 'M':
monster_count += 1
elif grid[i][j] == '@':
start_row, start_col = i, j
elif grid[i][j] == 'B':
item_box_count += 1
monster_info = [[dict() for _ in range(col_size)] for _ in range(row_size)]
for _ in range(monster_count):
r, c, s, w, a, h, e = input().split()
monster_info[int(r)-1][int(c)-1] = {
"name": s,
"atk": int(w),
"def": int(a),
"hp": int(h),
"exp": int(e)
}
item_info = [[dict() for _ in range(col_size)] for _ in range(row_size)]
for _ in range(item_box_count):
r, c, t, s = input().split()
if t == 'W' or t == 'A':
item_info[int(r) - 1][int(c) - 1] = {
"type": t,
"val": int(s)
}
else:
item_info[int(r) - 1][int(c) - 1] = {
"type": t,
"val": s
}
character_info = {
"level": 1,
"max_hp": 20,
"hp": 20,
"atk": 2,
"def": 2,
"exp": 0,
"max_exp": 5,
"weapon": 0,
"armor": 0,
"accessory_count": 0,
"HR": False,
"RE": False,
"CO": False,
"EX": False,
"DX": False,
"HU": False,
"CU": False
}
캐릭터, 아이템, 몬스터 정보는 이렇게 맵을 이용해 저장했습니다.
클래스를 활용하는 것도 물론 방법이겠지만, 전 파이썬으로 클래스를 다루는 건 아직 어색하더라구요..
now_row, now_col = start_row, start_col
result_message = ""
for cmd in command:
turn_count += 1
# 이동을 가정하고, 이전에 있던 캐릭터 위치에는 빈칸을 그려줌
# 만약 가시밭길 위에 있었다면 가시로 그려야 함
before_row, before_col = now_row, now_col
grid[before_row][before_col] = character_pos_before
if cmd == 'L':
now_col = max(0, now_col-1)
elif cmd == 'R':
now_col = min(col_size-1, now_col+1)
elif cmd == 'U':
now_row = max(0, now_row-1)
elif cmd == 'D':
now_row = min(row_size-1, now_row+1)
# 벽에 부딪힌 경우
if grid[now_row][now_col] == '#':
now_row, now_col = before_row, before_col
# 이벤트 발생
event_result = Event(now_row, now_col)
if event_result == 'clear':
result_message = "YOU WIN!"
break
elif event_result == 'dead':
# 부활 장신구 처리
if character_info["RE"]:
character_info["RE"] = False
character_info["accessory_count"] -= 1
character_info["hp"] = character_info["max_hp"]
# 게임 시작 위치로 이동
grid[now_row][now_col] = character_pos_before
now_row, now_col = start_row, start_col
character_pos_before = '.'
grid[now_row][now_col] = '@'
# 부활 장신구가 없을 경우 게임 끝
else:
grid[now_row][now_col] = character_pos_before
result_message = f"YOU HAVE BEEN KILLED BY {killer}.."
break
# 모든 커맨드 입력을 마쳤을 때
if result_message == "":
result_message = "Press any key to continue."
# 그리드 결과 출력
for i in range(row_size):
print("".join(grid[i]))
# 턴 수 출력
print(f"Passed Turns : {turn_count}")
# 캐릭터 정보 출력
print("LV", ":", character_info["level"])
print("HP", ":", str(character_info["hp"])+'/'+str(character_info["max_hp"]))
print("ATT", ":", str(character_info["atk"]) + '+' + str(character_info["weapon"]))
print("DEF", ":", str(character_info["def"]) + '+' + str(character_info["armor"]))
print("EXP", ":", str(character_info["exp"])+'/'+str(character_info["max_exp"]))
# 결과 메세지 출력
print(result_message)
함수를 호출하는 몸통 핵심 구현부와 출력부 입니다.
구현부에서는 사망 후에 now_row, now_col 위치만 스타팅 위치로 설정해두고
실제 grid 에서는 @ 위치를 옮기지 않았던 문제도 디버깅을 통해 스스로 찾아냈습니다.
출력부분에서는 사망시 @ 를 출력하지 않는 부분을 문제가 아닌 예제 출력을 통해서 찾아냈었네요..
사실 구현 문제는 설명하거나 적을 말이 별로 없습니다 ㅋㅋ
그렇다고 지난 계산기 문제처럼 이 문제도 단계단계별로 적자니 너무 복잡합니다..
이런 류의 문제는 디버깅할 생각을 하기보다, 처음부터 꼼꼼하게 작성할 생각을 하는게 중요해보입니다.
저는 귀찮아서 '이 부분은 복잡하니/귀찮으니 나중에 구현해야지~' 하고 넘겼던 부분들이
나중에 발목을 잡았던 경우가 많았습니다.
그런 부분은 넘어갈 때 넘어가더라도 꼭 주석으로라도 다 적어놓고 넘어가는 습관을 기르면 좋겠다는 생각이 듭니다.
정답 코드
row_size, col_size = map(int, input().split())
grid = []
for _ in range(row_size):
grid.append(list(input()))
command = list(input())
character_pos_before = '.'
killer = ""
monster_count = 0
item_box_count = 0
turn_count = 0
# 몬스터, 아이템 숫자 세기 & 캐릭터 초기 위치 파악
for i in range(row_size):
for j in range(col_size):
if grid[i][j] == '&' or grid[i][j] == 'M':
monster_count += 1
elif grid[i][j] == '@':
start_row, start_col = i, j
elif grid[i][j] == 'B':
item_box_count += 1
monster_info = [[dict() for _ in range(col_size)] for _ in range(row_size)]
for _ in range(monster_count):
r, c, s, w, a, h, e = input().split()
monster_info[int(r)-1][int(c)-1] = {
"name": s,
"atk": int(w),
"def": int(a),
"hp": int(h),
"exp": int(e)
}
item_info = [[dict() for _ in range(col_size)] for _ in range(row_size)]
for _ in range(item_box_count):
r, c, t, s = input().split()
if t == 'W' or t == 'A':
item_info[int(r) - 1][int(c) - 1] = {
"type": t,
"val": int(s)
}
else:
item_info[int(r) - 1][int(c) - 1] = {
"type": t,
"val": s
}
character_info = {
"level": 1,
"max_hp": 20,
"hp": 20,
"atk": 2,
"def": 2,
"exp": 0,
"max_exp": 5,
"weapon": 0,
"armor": 0,
"accessory_count": 0,
"HR": False,
"RE": False,
"CO": False,
"EX": False,
"DX": False,
"HU": False,
"CU": False
}
def Battle(row, col, is_boss=False):
global character_pos_before, killer
is_first_atk = True
is_no_damage = False
ch_atk = character_info["atk"] + character_info["weapon"]
ch_def = character_info["def"] + character_info["armor"]
monster = monster_info[row][col]
monster_max_hp = monster["hp"]
# HU 장신구 효과 적용
if is_boss and character_info["HU"]:
# 최대 체력으로 회복
character_info["hp"] = character_info["max_hp"]
# 데미지 면역 활성화
is_no_damage = True
while True:
# 데미지 계산
if is_first_atk and character_info["CO"]:
if character_info["DX"]:
character_damage = max(1, 3*ch_atk - monster["def"])
else:
character_damage = max(1, 2*ch_atk - monster["def"])
is_first_atk = False
else:
character_damage = max(1, ch_atk - monster["def"])
monster_damage = max(1, monster["atk"] - ch_def)
# 캐릭터 공격
monster["hp"] -= character_damage
# 몬스터가 죽었을 때
if monster["hp"] <= 0:
# 캐릭터가 있던 곳은 빈칸이 된다.
character_pos_before = '.'
# "HR" 장신구 적용
if character_info["HR"]:
character_info["hp"] = min(character_info["hp"] + 3, character_info["max_hp"])
# 경험치 계산
if character_info["EX"]:
get_exp = int(monster["exp"]*1.2)
else:
get_exp = monster["exp"]
character_info["exp"] += get_exp
# 레벨 업
if character_info["exp"] >= character_info["max_exp"]:
character_info["level"] += 1
character_info["exp"] = 0
character_info["max_exp"] += 5
character_info["atk"] += 2
character_info["def"] += 2
character_info["max_hp"] += 5
character_info["hp"] = character_info["max_hp"]
if is_boss:
return "clear"
return "win"
# 몬스터 공격 (보스 몬스터 & HU 장신구 착용시 첫 공격은 면역)
if is_no_damage:
is_no_damage = False
else:
character_info["hp"] -= monster_damage
# 캐릭터가 죽었을 때
if character_info["hp"] <= 0:
character_info["hp"] = 0
monster_info[now_row][now_col]["hp"] = monster_max_hp
killer = monster["name"]
return "dead"
def Event(row, col):
global character_pos_before, character_info, killer
event = grid[row][col]
# 만약 현재 위치 그대로 이벤트가 발생한 경우
if event == '@':
event = character_pos_before
else:
character_pos_before = event
# 이동한 캐릭터 지도에 표시
grid[row][col] = '@'
if event == '.':
return "blank"
elif event == '&':
return Battle(row, col)
elif event == 'B':
# 아이템 획득, 및 착용
get_item = item_info[row][col]
if get_item["type"] == "W":
character_info["weapon"] = get_item["val"]
elif get_item["type"] == "A":
character_info["armor"] = get_item["val"]
elif get_item["type"] == "O":
if character_info["accessory_count"] < 4:
if not character_info[get_item["val"]]:
character_info[get_item["val"]] = True
character_info["accessory_count"] += 1
# 상자를 열면 해당 칸이 빈칸이 된다.
character_pos_before = '.'
elif event == '^':
# 가시 데미지 분기
damage = 5
if character_info["DX"]:
damage = 1
# 데미지 계산
character_info["hp"] -= damage
# 사망 처리
if character_info["hp"] <= 0:
character_info["hp"] = 0
killer = "SPIKE TRAP"
return "dead"
elif event == 'M':
return Battle(row, col, True)
# 벽은 밖에서 처리
elif event == '#':
assert True
now_row, now_col = start_row, start_col
result_message = ""
for cmd in command:
turn_count += 1
# 이동을 가정하고, 이전에 있던 캐릭터 위치에는 빈칸을 그려줌
# 만약 가시밭길 위에 있었다면 가시로 그려야 함
before_row, before_col = now_row, now_col
grid[before_row][before_col] = character_pos_before
if cmd == 'L':
now_col = max(0, now_col-1)
elif cmd == 'R':
now_col = min(col_size-1, now_col+1)
elif cmd == 'U':
now_row = max(0, now_row-1)
elif cmd == 'D':
now_row = min(row_size-1, now_row+1)
# 벽에 부딪힌 경우
if grid[now_row][now_col] == '#':
now_row, now_col = before_row, before_col
# 이벤트 발생
event_result = Event(now_row, now_col)
if event_result == 'clear':
result_message = "YOU WIN!"
break
elif event_result == 'dead':
# 부활 장신구 처리
if character_info["RE"]:
character_info["RE"] = False
character_info["accessory_count"] -= 1
character_info["hp"] = character_info["max_hp"]
# 게임 시작 위치로 이동
grid[now_row][now_col] = character_pos_before
now_row, now_col = start_row, start_col
character_pos_before = '.'
grid[now_row][now_col] = '@'
# 부활 장신구가 없을 경우 게임 끝
else:
grid[now_row][now_col] = character_pos_before
result_message = f"YOU HAVE BEEN KILLED BY {killer}.."
break
# 모든 커맨드 입력을 마쳤을 때
if result_message == "":
result_message = "Press any key to continue."
# 그리드 결과 출력
for i in range(row_size):
print("".join(grid[i]))
# 턴 수 출력
print(f"Passed Turns : {turn_count}")
# 캐릭터 정보 출력
print("LV", ":", character_info["level"])
print("HP", ":", str(character_info["hp"])+'/'+str(character_info["max_hp"]))
print("ATT", ":", str(character_info["atk"]) + '+' + str(character_info["weapon"]))
print("DEF", ":", str(character_info["def"]) + '+' + str(character_info["armor"]))
print("EXP", ":", str(character_info["exp"])+'/'+str(character_info["max_exp"]))
# 결과 메세지 출력
print(result_message)
이 문제는 푸는데만 3시간은 걸린 것 같습니다.
연세대 2019 프로그래밍 경진대회 문제인데, 심지어 개인전이었습니다.
5시간짜리 대회에서 이 문제를 풀고도 다른 문제들을 푸시는 분들.. 정말 대단하다고 생각합니다..
'알고리즘 (PS) > BOJ' 카테고리의 다른 글
[백준] 17070 - 파이프 옮기기 1 (G5) (0) | 2021.11.14 |
---|---|
[백준] 17144 - 미세먼지 안녕! (G4) (0) | 2021.11.13 |
[백준] 20936 - 우선순위 계산기 (P4) (0) | 2021.08.18 |
[백준] 20129 - 뒤집힌 계산기 (G3) (2) | 2021.08.17 |
[백준] 2376 - 단말 정점들의 거리 (P5) (0) | 2021.08.14 |