23.03.27 TIL

최창수·2023년 3월 27일
0

오늘 한 것

  • 개인 과제: 각 직업별 특수공격 만들기
  • 개인 과제: 속도에 따른 우선공격 및 공격무효 기능 만들기
  • 개인 과제: 화면 전환 효과 만들기
  • 개인 과제: CSV이용해 몬스터 임의생성
  • 도움: 함수를 실행하지 않고 객체로 가져오기

과제 1. class: 직업별 특수 공격

  • 궁수: 적에게 마법 데미지, 확률적으로 적의 속도 감소, 자신의 속도 증가
  • 전사: 적에게 마법+물리 데미지, 자신에게 마법 데미지
  • 성직자: 적에게 마법 데미지, 확률적으로 자신의 체력, SP(skill point)회복

이렇게 서로 다른 효과를 만들기 위해서는 Player의 subclass인 이 각 클래스들에게 직접 새로운 특수공격 함수를 작성해주면 되지만 각 클래스에 따라 고정된 부분을 줄이고 싶었다.

시도: super() 사용하기

Player 클래스의self.mag_attack을 다음과 같이 모든 클래스에서 공통적으로 수행할 내용으로 작성하였다.

	def mag_attack(self, other):
    	#이미 쓰러졌음
        if self.hp == 0:
            print(f"{self.name}은 전투불능. 공격하지 못했습니다.")
            return
        # SP 부족
        if self.sp == 0:
            print("공격실패: SP가 부족합니다")
            return
        self.sp -= 1
        # 랜덤 데미지
        damage = rd.randint(self.mag - 2, self.mag + 2)
        # 적의 마법 방어력 적용
        damage = max(0, damage-other.mag_d)
        other.hp = max(other.hp - damage, 0)
        print(f"{self.name}의 공격! {other.name}에게 {damage}의 데미지를 입혔습니다.")
        if other.hp == 0:
            print(f"{other.name}이(가) 쓰러졌습니다.")

이후 각 클래스에서 다음과 같이 super().mag_attack(other)로 불러와 공통적으로 작동할 부분은 작성을 줄일 수 있었다.

	def mag_attack(self, other):
        if self.sp == 0:
            print("공격실패: SP가 부족합니다")
            return
        super().mag_attack(other)
        dice = rd.randint(1, 20)
        ...
        elif dice > 14:
            print('바람의 화신이 화살을 인도합니다')
            print('적의 다리가 느려집니다!')
            other.speed = max(other.speed-1, 0)
        elif dice > 11:
            print('바람의 화신이 주목하십니다! 당신의 속도가 빨라집니다!')
            self.speed = min(self.speed+1, 5)
        else:
            False

과제 2-1. 함수 객체: 일반공격 vs 특수공격

동기분중 한분이 몬스터가 랜덤하게 플레이어에게 일반공격과 특수 공격 중 한가지 공격을 가하는 코드를 짜고 싶었다고한다.

    def attack_or_skill(self, other):
        select = random.choice(self.attack(other), self.skill(other))
        return select

이에 따라 위와 같은 코드를 작성하였더니, 일반 공격과 특수공격을 동시에 하거나, 인자의 갯수를 초과하였다는 오류가 발생하였다.

시도 1. random.choice 인자 맞추기

해당 함수는 인자로 list등의 객체를 하나를 인자로 받는다. 일부 class에서는 인자로 2개의 객체를 넣고 있었다. 따라서 다음과 같이 수정해 보았다.

    def attack_or_skill(self, other):
        select = random.choice([self.attack(other), self.skill(other)])
        return select

이렇게 하자 인자의 갯수가 초과되었다는 오류는 사라졌다. 그러나 여전히 공격이 2번씩 진행되는 오류는 해결되지 않았다.

시도2. print()로 어디서 어떤 문제가 발생하는지 알아보기

어디서 문제가 발생하는지 알아보기 위해 짐작이 가는 곳인 random.choice함수 다음에 print(select)를 이용해 어떤 값이 반환되는지 확인한 결과 None이 반환되었다. 이를 통해 self.attack(other), self.skill(other)이 아무것도 반환하지 않는 함수임을 알 수 있었다.

    def attack_or_skill(self, other):
        select = random.choice([self.attack, self.skill])
        select(other)

원인과 해결: 함수를 실행하지 않고 이름(주소)으로 가져오기

위의 코드에서 각 함수(self.attack(other), self.skill(other))는 인자를 받아 실행되고, 아무것도 반환하지 않는 함수들이었다. 즉 함수 내에서 직접 상대 객체의 hp값에 접근하여 값을 수정하고 결과를 print로 출력하는 함수였다. 따라서 위 코드에서 둘은 해당 줄이 번역되자마자 실행되어 공격이 진행된 것이다.
이를 막기위해 다음과 같이 함수이름을 이용해 함수의 주소를 받아온뒤, 필요할 때 인자를 주고 실행시키는 방식으로 해결하였다.

    def attack_or_skill(self, other):
        select = random.choice([self.attack, self.skill])
        select(other)

2-2. 임의 순서로 함수 호출하기

공격 순서를 턴마다 바뀌게 하고 싶었다.

예시:
1턴: 플레이어의 속도가 적보다 느리므로 나중에 행동
2턴: 플레이어의 속도가 적보다 빨라졌으므로 먼저 행동
3턴: 플레이어의 속도가 적과 같으므로 랜덤한 순서로 행동

시도 1. 조건문 사용하기

import random as rd
...
            if p_s == e_s:
            	a=rd.randint(0,1)
                if a:
                	p_move(enemy) # 플레이어 선
                    enemy(player_entity)
                else:
                	enemy(player_entity)
                    p_move(enemy) # 플레이어 후
            elif p_s-e_s > 0:
                p_move(enemy)
                if (p_s-e_s)**2 <= rd.randint(1, 25):
                    enemy.attack(player_entity)
                else:
                    print(f"{enemy.name}는 움직이지 못했다.")
            else:
                enemy.attack(player_entity)
                if (e_s-p_s)**2 <= rd.randint(1, 25):
                    p_move(enemy)
                else:
                    print((f"{player_entity.name}는 움직이지 못했다."))

그러나 플레이어와 몬스터의 속도가 같을 때 임의 순서로 실행하는 경우 등 중복된 코드가 존재하는 구간이 너무 많다고 생각했다.

해결: 아까처럼 함수를 이름으로 불러와 보기

			# 속도가 같을 때
			if p_s == e_s:
                movelist = [enemy.attack, p_move]
                targets = [player_entity, enemy]
                index = rd.randint(0, 1)
                movelist[index](targets[index])
                movelist[(index+1) % 2](targets[(index+1) % 2])
            
            # 조건에 맞춰 first, second 변수에 함수 객체 갈아 끼우기
            
            else:
            	# 플레이어가 더 빠를 때
                if p_s-e_s > 0:
                    first = p_move
                    second = enemy.attack
                    first_arg = enemy
                    second_arg = player_entity
				# 플레이어가 더 느릴 때
                else:
                    first = enemy.attack
                    second = p_move
                    first_arg = player_entity
                    second_arg = enemy
                first(first_arg)
                # 속도차이로 인한 공격불능 체크
                if (p_s-e_s)**2 <= rd.randint(1, 25):
                    second(second_arg)
                else:
                    print(f"{first_arg.name}는 움직이지 못했다.")
			

파이썬에서 함수를 포함한 대부분의 요소는 객체로서 존재한다는 점을 상기하였다. 함수는 이름만 따오면 주소만 가져올 수 있다. 함수들의 주소만 불러온 뒤 순서에 따라 새로운 변수에 갈아 끼워 실행시켰다. 이렇게 하니 코드의 중복을 줄일 수 있었다.

과제 3. 화면 전환 효과 만들기

아래와 같이 일반적인 CLI에서 입출력이 계속 한줄 한줄 쌓이는 형식으로 만드니 게임 같지가 않았다. 이에 게임 화면이 전환되는 효과를 만들고 싶었다.

이름을 입력하세요: 김철수
클래스를 선택하세요
1: 궁수, 2: 전사, 3: 성직자
1
궁수를 선택하셨습니다
전투시작!
적의 공격! 내 HP -10!
...

시도 1. try-except

이를 위해 os 모듈의 system 함수를 사용하였다. 이것은 파이썬 코드내에서 CLI에 명령어를 입력하도록 한다.
이를 이용해 화면 전체를 지우는 명령을 내리려고 하였다.
그런데 각 OS마다 해당하는 명령어가 다르므로 다음과 같이 try-except문을 이용하려고 했다.

try:
	os.system('cls')
except:
	os.system('clear')

그러나 다음과 같은 오류가 발생하였다.

sh: cls: command not found

원인을 찾아본 결과 이 에러를 예외처리 구문으로 해결하지 못하는 이유는 에러가 파이선에서 발생한 것이 아니라 bash에서 발생하였기 때문이다.

해결:

에러를 발생시키지 않고 각 os에 맞게 작동하는 코드를 geeks for geeks에서 발견하였다.

if os.name == 'nt':
	os.system('cls')
else:
	os.system('clear')

os.name을 통해 현재 pyyhon이 어떤 os상에서 실행중인지 알 수 있다. nt는 윈도우 운영체제, posix는 리눅스, 맥os등의 name이다.

과제 4. 몬스터 랜덤생성하기

다양한 몬스터 종류를 간편하게 저장된 목록상에서 뽑아 랜덤하게 생성하고 싶었다.

시도 1. 자식 클래스 여러개 만들기

class Monster(Character):
    def __init__(self, type_, name, hp, power, speed, mag_d):
        super().__init__(name, hp, power, speed)
        self.type_ = type_
        self.mag_d = mag_d
        self.random_name()

    def random_name(self):
        ...
class Slime(Monster):
	def __init__():
	    ...
class Goblin(Monster):
	def __init__():
    	...

mon_class_list=[ class_ for class_ in Monster.__subclasses__()]
index=rd.randint(0,len(mon_class_list)-1)
...

서브 클래스를 여러개 만든 뒤, .__subclasses__()로 list를 뽑아 이 중 랜덤으로 선택한다.
이 방식은 몬스터 종류를 추가할때 마다 클래스를 새로 만들어야 한다는 단점이 있었다.

시도 2. list of dict 사용

클래스는 하나만 두고, 각 몬스터 종류마다 다른 수치의 hp, speed, mag_d(마법 방어력)을 입력하여 객체를 생성할 수 있도록 list of dictionary를 사용하려 했다. 그러나 이것보다 더 괜찮은 방법을 생각해냈다.

시도 3. CSV 사용

def make_monster():
    csv_file = open('monster_classes.csv', "r", encoding="utf-8")
    csv_data = csv.DictReader(csv_file)
    csv_data = list(csv_data)
    row = rd.choice(csv_data)
    monster_entity = Monster(row['class_name'], '', int(row['hp']), int(
        row['power']), int(row['speed']), int(row['mag_d']))
    csv_file.close()
    return monster_entity

파이선 파일 내에 변수로 저장하지 않고 csv파일로 분리하여 DictReader로 list of dictionary형태로 불러오는 형식으로 한다.
이렇게 하면 파이선 파일을 수정하지 않고, CSV만 편집하여 새로운 몬스터 종류를 만들어 낼 수 있다.

배운 점 정리

  • super().foo를 이용하여 부모 클래스의 함수 내용을 불러와 코드를 단축 할 수 있다.
  • 괄호와 인자 없이 함수이름을 이용하면 함수의 주소만 가져올 수 있다. 이를 이용해 함수를 list의 요소에 넣는 등 다양하게 사용할 수 있다.
  • os 모듈을 불러와 디렉토리 생성, 파일 조회 등의 다양한 기능을 수행할 수 있다. os모듈의 system함수는 터미널에 명령어를 전달할 수 있으며 이때 발생하는 오류는 python파일 범위밖에 있을 수 있다.
  • CSV 파일로 데이터를 분리하고 불러와 사용할 수 있다.
profile
Hallow Word!

0개의 댓글