턴제 RPG 게임 만들기

1. 개요

드디어! 어릴 적 부터 좋아했던 RPG게임이다. 물론 내가 지금까지 해 온 것과 비교해서는 초라한 것이지만, 어떻게 몬스터와 전투를 하는 지 내가 직접 개발을 하며 들여다 볼 수가 있다. 이번 프로젝트에 주요 키는 클래스를 사용하는 것이다. 사용자와 몬스터간 1:3 대결이고, 사용자는 자신의 턴에 일반 공격과 마법공격 2가지 행동을 할 수가 있고, 몬스터는 회복, 대기, 공격 3가지 행동을 할 수 있다. 턴제 방식으로 전투가 진행되며 사용자가 먼저 행동 할 수 있다. 모든 몬스터의 체력이 0 이면 승리 반대의 경우 패배 이다.

2. 로직

1️⃣ 먼저 이름, 체력, 공격력을 멤버변수로, 상대방 공격 하는 함수를 멤버함수로 갖는 부모 클래스 Object를 만든다. 참, 현재 자신의 체력을 보여주는 클래스 내장함수 __str__()도 써보았다.
2️⃣ Object클래스를 상속받고 마법 공격을 하는 멤버 함수를 갖는 Player클래스, 마찬가지로 Object를 부모로 상속받고 대기, 회복을 멤버 함수로 갖는 Monster클래스를 만든다.
3️⃣ 현재 전투 중인 객체들의 체력 현황을 보여주는 함수를 만든다.
4️⃣ 사용자의 입력이 올바른지 검증하는 함수를 따로 만든다.(예외처리)
5️⃣ 사용자 턴 함수를 만들어 사용자가 입력한 대로 행동하게 한다.
6️⃣ 사용자 턴 이후 몬스터들의 체력 상황을 확인함수를 통해 0이하인 객체는 전투가능 리스트에서 제거한다. 그리고 몬스터가 몇 남았는지 리턴한다.
7️⃣ 몬스터 턴 함수를 만들어 각 몬스터가 랜덤으로 3가지(공격, 회복, 대기) 중 하나의 행동을 하게 한다.
8️⃣ 몬스터 턴 이후 사용자의 체력 상황을 파악하여 게임 종료 유무를 판단한다.
9️⃣ 전체 전투 가능자 체력 상황 프린트 -> 사용자턴 -> 몬스터 체력 확인 -> 몬스터턴 -> 사용자 체력 확인
🔟 게임 종료 전까지 해당 내용을 반복한다.

3. 코드

import random


class Object:
    def __init__(self, name, hp, attack_damage):
        self.name = name
        self.hp = hp
        self.attack_damage = attack_damage

    def get_name(self):
        return self.name

    def be_attacked(self, attack_damage):
        self.hp -= attack_damage

    def get_hp(self):
        return self.hp

    def set_hp(self, hp):
        self.hp = hp

    def get_attack_damage(self):
        return self.attack_damage

    def attack(self, target):
        print(f"[{self.get_name()}] (이)가 [{target.name}] (을)를 일반공격을 했다.")
        target.be_attacked(self.get_attack_damage())
        print(f"{target} (-{self.get_attack_damage()})")

    def __str__(self):
        msg = f"[{self.get_name()}] 의 HP : {self.get_hp()}" if self.get_hp() > 0 else f"[{self.get_name()}] (은)는 죽었습니다."
        return msg


class Player(Object):
    def magic_attack(self, target):
        print(f"[{self.get_name()}] (이)가 [{target.get_name()}] (을)를 마법공격을 했다.")
        target.be_attacked(50)
        print(f"{target} (-50)")


class Monster(Object):
    def stay(self):
        print(f"[{self.get_name()}] (이)가 대기했습니다.")

    def heal(self):
        self.set_hp(self.get_hp() + 10)
        print(f"[{self.get_name()}] (이)가 자신의 체력을 10만큼 회복했다.")
        print(f"{self} (+10)")


def print_players_status(players):
    print("\n *********** 각 플레이어의 현재 상태 ***********")
    for player in players:
        print(player)


def only_return_right_command(msg, num_range):
    user_input = ""
    while not user_input:
        try:
            user_input = int(input(msg))
            if not isinstance(user_input, int) or (user_input < 1 or user_input > num_range):
                raise ValueError
        except ValueError:
            print("보기에 있는 숫자만 입력 가능합니다")
            user_input = ""
    return user_input


def player_turn(players):
    attack_method = only_return_right_command("어떻게 공격하시겠습니까? (1.일반 2.마법) ", 2)

    msg_for_who_attack = "누구를 공격하시겠습니까? ("
    for i in range(1, len(players)):
        msg_for_who_attack += f"{i}.{players[i].get_name()} "
    msg_for_who_attack = msg_for_who_attack.strip()
    msg_for_who_attack += ") "
    attack_target = only_return_right_command(msg_for_who_attack, len(players)-1)

    players[0].attack(players[attack_target]) if attack_method == 1 else players[0].magic_attack(players[attack_target])

    return players


def check_monsters(players):
    for i in range(1, len(players)):
        monster = players[i]
        if monster.get_hp() <= 0:
            players.pop(i)
            break
    return len(players) - 1


def monsters_turn(players):
    player = players[0]
    for i in range(1, len(players)):
        monster = players[i]
        action = random.randrange(3)
        if action == 0:
            monster.attack(player)
        elif action == 1:
            monster.heal()
        else:
            monster.stay()
        print()
    return players


def check_player(player):
    alive = True
    if player.get_hp() <= 0:
        alive = False

    return alive


warrior = Player("최중재", 100, 10)
mini_goblin = Monster("미니고블린", 10, 10)
goblin = Monster("고블린", 30, 30)
super_goblin = Monster("슈퍼고블린", 50, 50)

battle_players = [warrior, mini_goblin, goblin, super_goblin]

while True:
    print_players_status(battle_players)
    print("\n ************* 턴 시작 *************")

    print("플레이어 턴")
    battle_players = player_turn(battle_players)
    if check_monsters(battle_players) == 0:
        print("승리")
        break

    print("\n몬스터 턴")
    battle_players = monsters_turn(battle_players)
    if not check_player(battle_players[0]):
        print("패배")
        break

총평

복잡한 프로그램은 아니지만, 이 게임을 코딩하면서 클래스 사용법에 대해서 한 번 더 생각해보고 정리가 됐던 시간이었다. 위에 짠 것 중 사용자 및 몬스터들 객체의 상태 변화가 함수안에서 적용되고 따로 그 객체들을 리턴해주지 않았다. 각 객체들이 변화된 값들을 잘 가지고 있었기 때문이다. 그런데 이는 좋게 짠 것이 아니라는 말을 듣고 즉시 리턴으로 다시 받아 수정된 객체리스트를 기존 객체리스트에 다시 넣어줬다.

def monsters_turn(players):
    player = players[0]
    for i in range(1, len(players)):
        monster = players[i]
        action = random.randrange(3)
        if action == 0:
            monster.attack(player)
        elif action == 1:
            monster.heal()
        else:
            monster.stay()
        print()
    return players

# ...

battle_players = monsters_turn(battle_players)

예를 들면 이 함수다. 이전에는 리턴값이 없었지만 지금은 리턴 해줘서 다시 기존 변수에 넣어줬다.
게임 개발을 해보셨던 분의 코드를 봤었는데, 그 짧은 시간안에 많은 걸 담아두셨었다. 예를 들면서 시놉시스라던지, 게임 종료 후 다시 시작이라 던지 등등 정말 게임 요소들을 많이 집어넣으셨었다. 디테일도 놓지 않는 프로 정신에 감명을 받았다.