내일배움캠프/강의 과제

C# 문법 종합반 - 4주차 : 텍스트 RPG 게임 만들기

서보훈 2024. 9. 25. 15:43

4-1 간단한 텍스트 RPG

  1. 목표: 기본적인 턴 기반 RPG 게임을 만들어 봅니다.
  2. 과제 요구사항:
    • **ICharacter**라는 인터페이스를 정의하세요. 이 인터페이스는 다음의 프로퍼티를 가져야 합니다:
      • Name: 캐릭터의 이름
      • Health: 캐릭터의 현재 체력
      • Attack: 캐릭터의 공격력
      • IsDead: 캐릭터의 생사 상태 그리고 다음의 메서드를 가져야 합니다:
      • TakeDamage(int damage): 캐릭터가 데미지를 받아 체력이 감소하는 메서드
      • **Warrior**는 플레이어의 캐릭터를 나타내며, **Monster**는 몬스터를 나타냅니다.
    • ICharacter 인터페이스를 구현하는 **Warrior**와 **Monster**라는 두 개의 클래스를 만들어주세요.
      • Monster 클래스에서 파생된 **Goblin**과 **Dragon**이라는 두 개의 클래스를 추가로 만들어주세요.
    • **IItem**이라는 인터페이스를 정의하세요. 이 인터페이스는 다음의 프로퍼티를 가져야 합니다:
      • Name: 아이템의 이름 그리고 다음의 메서드를 가져야 합니다:
      • Use(Warrior warrior): 아이템을 사용하는 메서드, 이 메서드는 Warrior 객체를 파라미터로 받습니다.
    • IItem 인터페이스를 구현하는 **HealthPotion**과 **StrengthPotion**이라는 두 개의 클래스를 만들어주세요.
    • **Stage**라는 클래스를 만들어 주세요. 이 클래스는 플레이어와 몬스터, 그리고 보상 아이템들을 멤버 변수로 가지며, **Start**라는 메서드를 통해 스테이지를 시작하게 됩니다.
      • 스테이지가 시작되면, 플레이어와 몬스터가 교대로 턴을 진행합니다.
      • 플레이어나 몬스터 중 하나가 죽으면 스테이지가 종료되고, 그 결과를 출력해줍니다.
      • 스테이지가 끝날 때, 플레이어가 살아있다면 보상 아이템 중 하나를 선택하여 사용할 수 있습니다.
  3. 추가적인 요구사항:
    • 모든 코드는 C# 언어로 작성해주세요.
    • 코드에는 적절한 주석을 달아주세요.
    • 각 스테이지가 시작할 때 플레이어와 몬스터의 상태를 출력해주세요.
    • 각 턴이 진행될 때 천천히 보여지도록 **Thread.Sleep**을 사용하여 1초의 대기시간을 추가해주세요.

 

주어진 내용을 기반으로 텍스트 RPG를 만드는 과제입니다.

사용될 인터페이스들과 대략적인 게임 플레이 과정이 주어졌습니다.

 

더보기
namespace Quest_TextRPG
{
    internal class Program
    {
        static void Main(string[] args)
        {
            //몬스터 객체 생성
            Goblin goblin1 = new Goblin("고블린 1");
            Goblin goblin2 = new Goblin("고블린 2");
            Goblin goblin3 = new Goblin("고블린 3");
            Dragon dragon = new Dragon("드래곤");

            //스테이지 보상 생성
            List<IItem> noReward = new List<IItem>();
            List<IItem> Rewards = new List<IItem> { new HealthPotion(), new StrengthPotion() };

            //플레이어 객체 생성
            string playerName;
            
            Console.WriteLine("플레이어의 이름을 입력해주세요");
            playerName = Console.ReadLine();

            Warrior player = new Warrior(playerName);
            Console.Clear();

            Console.WriteLine("스테이지 1 시작");
            Thread.Sleep(1000);
            Console.Clear();

            //스테이지 1 생성 및 플레이
            Stage stage1 = new Stage(player, goblin1, noReward, 1);
            stage1.PlayStage();

            //사망시 게임 종료
            if(player.IsDead)
            {
                return;
            }

            Console.WriteLine("스테이지 2 시작");
            Thread.Sleep(1000);
            Console.Clear();

            //스테이지 2 생성 및 플레이
            Stage stage2 = new Stage(player, goblin2, Rewards, 2);
            stage2.PlayStage();

            //사망시 게임 종료
            if (player.IsDead)
            {
                return;
            }

            Console.WriteLine("스테이지 3 시작");
            Thread.Sleep(1000);
            Console.Clear();

            //스테이지 3 생성 및 플레이
            Stage stage3 = new Stage(player, goblin3, Rewards, 3);
            stage3.PlayStage();

            //사망시 게임 종료
            if (player.IsDead)
            {
                return;
            }

            Console.WriteLine("스테이지 4 시작");
            Thread.Sleep(1000);
            Console.Clear();

            //스테이지 4 생성 및 플레이
            Stage stage4 = new Stage(player, dragon, noReward, 4);
            stage4.PlayStage();

            //사망시 게임 종료
            if (player.IsDead)
            {
                return;
            }

            Console.WriteLine("모든 스테이지를 클리어했습니다!");
        }
    }

    //캐릭터 인터페이스 구현
    internal interface ICharacter
    {
        //이름
        public string Name { get;}
        //현재 체력
        public int Health {  get;}
        //공력력
        public int Attack {  get;}
        //사망 여부
        public bool IsDead {  get;}
        //데미지를 받을때 호출되는 함수
        public void TakeDamage(int damage);
    }

    //플레이어의 캐릭터
    internal class Warrior : ICharacter
    {
        //이름
        private string name;
        public string Name
        {
            get { return name; }
        }

        //체력
        private int maxHealth = 100;
        private int health;
        public int Health
        {
            get { return health; }
        }

        //공격력
        private int attack = 10;
        public int Attack
        {
            get { return attack; }
        }

        //사망여부
        public bool IsDead { get; private set; }

        //객체 생성시 초기화
        public Warrior(string _name)
        {
            name = _name;
            health = maxHealth;
            IsDead = false;
        }

        //데미지를 받을때 사용되는 함수
        public void TakeDamage(int damage)
        {
            Console.WriteLine($"{name} 이(가) {damage} 의 데미지를 받았습니다.");
            //체력에서 빼기
            health -= damage;
            //체력이 0이하가 되면 사망처리
            if(health <= 0)
            {
                health = 0;
                Console.WriteLine($"{name}이(가) 죽었습니다.");
                IsDead = true;
            }
        }

        //회복아이템 사용
        public void UseHealthPotion(int healAmount)
        {
            //임시로 회복된 체력량 계산
            int temp = health + healAmount;
            //회복이 최대체력을 넘지 않게 만들어줌
            health = (maxHealth < temp)? maxHealth : temp;
        }

        //공격력 아이템 사용
        public void UseStrengthPotion(int amount)
        {
            //수치만큼 공격력 증가
            attack += amount;
        }
    }

    internal abstract class Monster : ICharacter
    {
        //이름
        protected string name;
        public string Name
        {
            get { return name; }
        }

        //체력
        protected int maxHealth;
        protected int health;
        public int Health
        {
            get { return health; }
        }

        //공격력
        protected int attack;
        public int Attack
        {
            get { return attack; }
        }

        //사망여부
        public bool IsDead { get; protected set; }

        //몬스터가 사용할 기본 생성자
        public Monster(string _name, int _maxHealth, int _minAttack)
        {
            name = _name;
            maxHealth = _maxHealth;
            health = maxHealth;

            attack = new Random().Next(_minAttack, _minAttack + 5);
        }

        //공격받을때 사용되는 함수
        public void TakeDamage(int damage)
        {
            Console.WriteLine($"{name} 이(가) {damage} 의 데미지를 받았습니다.");
            health -= damage;
            if(health <= 0)
            {
                health = 0;
                Console.WriteLine($"{name}이(가) 죽었습니다.");
                IsDead = true;
            }
        }
    }

    internal class Goblin : Monster
    {
        //생성자를 통해 필드에 값 입력
        public Goblin(string _name) : base(_name, 15, 5) { }
    }

    internal class Dragon : Monster
    {
        //생성자를 통해 필드에 값 입력
        public Dragon(string _name) : base(_name, 100, 20) { }
    }

    internal interface IItem
    {
        //아이템 이름
        public string Name { get; }
        //효과 텍스트
        public string EffectText { get; }
        //아이템 사용시 호출되는 함수
        public void Use(Warrior player);
    }

    internal class HealthPotion : IItem
    {
        private string name = "회복 포션";
        public string Name { get { return name; } }

        //회복량
        private int healAmount = 50;

        //효과 텍스트
        private string effectText = "체력을 50 회복합니다.";
        public string EffectText { get { return effectText; } }

        //체력회복 함수 호출
        public void Use(Warrior player)
        {
            int temp = player.Health;
            player.UseHealthPotion(healAmount);
            Console.WriteLine($"{player.Name}의 체력이 회복됩니다.");
            Console.WriteLine($"{player.Name}의 체력 : {temp} -> {player.Health}");
            Thread.Sleep(1000);
        }
    }

    internal class StrengthPotion : IItem
    {
        private string name = "힘 포션";
        public string Name { get { return name; } }

        //공격력 증가량
        private int atkUpAmount = 10;

        //효과 텍스트
        private string effectText = "공격력이 10 증가합니다.";
        public string EffectText { get { return effectText; } }

        //공격력 증가 함수 호출
        public void Use(Warrior player)
        {
            Console.WriteLine($"{player.Name}의 공격력이 {atkUpAmount} 증가합니다");
            Console.WriteLine($"공격력 : {player.Attack} -> {player.Attack + atkUpAmount}");
            player.UseStrengthPotion(atkUpAmount);
            Thread.Sleep(1000);
        }
    }

    internal class Stage
    {
        //몬스터와 플레이어 객체
        private ICharacter player;
        private ICharacter monster;
        private List<IItem> rewardItems;

        private int nowStage;

        //객체를 생성할 때, 플레이어 객체를 가져옴
        public Stage(ICharacter _player, ICharacter _monster, List<IItem> _rewardItems, int _nowStage)
        {
            player = _player;
            monster = _monster;
            rewardItems = _rewardItems;
            nowStage = _nowStage;
        }

        public void PlayStage()
        {
            //스테이지 정보와 플레이어,몬스터 스텟 정보
            Console.WriteLine($"[스테이지{nowStage}]");
            int playerStatCursor = Console.GetCursorPosition().Top;
            Console.WriteLine($"이름 : {player.Name} | 현재 체력 : {player.Health} | 공격력 : {player.Attack}    ");
            Console.WriteLine();
            Console.WriteLine("몬스터 정보");
            int monsterStatCursor = Console.GetCursorPosition().Top;
            Console.WriteLine($"이름 : {monster.Name} | 체력 : {monster.Health} | 공격력 : {monster.Attack}     ");
            Console.WriteLine("---------------------------------------------------------------");
            int battleCursorPos = Console.GetCursorPosition().Top;
            int lastCursorPos;

            Thread.Sleep(1000);

            //둘중 하나가 죽을때까지 반복
            while(!player.IsDead && !monster.IsDead)
            {
                //플레이어의 턴부터 시작
                Console.WriteLine($"{player.Name} 의 턴");
                monster.TakeDamage(player.Attack);
                lastCursorPos = Console.GetCursorPosition().Top;
                Console.SetCursorPosition(0, monsterStatCursor);
                Console.WriteLine($"이름 : {monster.Name} | 체력 : {monster.Health} | 공격력 : {monster.Attack}     ");
                Console.SetCursorPosition(0, lastCursorPos);
                Thread.Sleep(1000);

                //몬스터 사망시 종료
                if(monster.IsDead == true)
                {
                    break;
                }

                //턴 정보 지우기
                ConsoleLineClear(lastCursorPos, battleCursorPos);

                //몬스터의 턴
                Console.WriteLine($"{monster.Name} 의 턴");
                player.TakeDamage(monster.Attack);
                lastCursorPos = Console.GetCursorPosition().Top;
                Console.SetCursorPosition(0, playerStatCursor);
                Console.WriteLine($"이름 : {player.Name} | 현재 체력 : {player.Health} | 공격력 : {player.Attack}     ");
                Console.SetCursorPosition(0, lastCursorPos);
                Thread.Sleep(1000);

                //턴 정보 지우기
                ConsoleLineClear(lastCursorPos, battleCursorPos);
            }

            //반복문 종료후, 플레이어 생존 여부를 통해 결과 출력
            StageEnd(player.IsDead);
        }

        //스테이지 종료
        private void StageEnd(bool isPlayerDead)
        {
            //플레이어 상태를 받아서 클리어 성공 여부 판정
            if(isPlayerDead == false)
            {
                Console.Clear();
                Console.WriteLine($"스테이지{nowStage} 클리어!");
                Console.WriteLine();

                //보상 아이템이 있으면 보상 주기
                if (rewardItems.Count > 0)
                {
                    Console.WriteLine("클리어 보상을 선택할 수 있습니다.");
                    //보상 앞에 숫자 띄우기
                    int selector = 1;
                    foreach(var item in rewardItems)
                    {
                        Console.WriteLine($"{selector}. {item.Name} : {item.EffectText}");
                        selector++;
                    }
                    Console.WriteLine();
                    //보상 선택 반복문 시작
                    while (true)
                    {
                        Console.WriteLine("사용할 아이템 번호를 입력해주세요");
                        //int 변수 재활용, 번호를 통해 아이템 선택
                        if (int.TryParse(Console.ReadLine(), out selector))
                        {
                            if(selector >= 1 && selector < rewardItems.Count + 1)
                            {
                                //플레이어가 인터페이스로 선언된 상태 -> Warrior로 형변환 시도
                                rewardItems[selector - 1].Use((Warrior)player);
                                //선택 완료, 반복문 종료
                                break;
                            }
                            else
                            {
                                Console.WriteLine("잘못된 선택입니다.");
                            }
                            
                        }
                        else
                        {
                            Console.WriteLine("잘못된 선택입니다.");
                        }
                    }
                    //다음 스테이지로 넘어갈 준비, 콘솔 모두 지우기
                    Thread.Sleep(1000);
                    Console.Clear();
                }
            }
            else
            {
                //플레이어 사망시 게임 오버
                Console.Clear();
                Console.WriteLine("게임 오버");
            }
        }

        //원하는 줄만 콘솔에서 지우기 위해 사용하느 함수
        private void ConsoleLineClear(int nowLine, int toClearLine)
        {
            int line = nowLine - toClearLine;
            for(int i = 0; i < line; i++)
            {
                Console.SetCursorPosition(0, Console.GetCursorPosition().Top - 1);
                Console.Write("\r                                                                       \r");
            }
        }
    }
}

인터페이스 ICharacter

캐릭터들이 사용할 인터페이스 구현부 입니다.

//캐릭터 인터페이스 구현
internal interface ICharacter
{
    //이름
    public string Name { get;}
    //현재 체력
    public int Health {  get;}
    //공력력
    public int Attack {  get;}
    //사망 여부
    public bool IsDead {  get;}
    //데미지를 받을때 호출되는 함수
    public void TakeDamage(int damage);
}

인터페이스는 이후 이 인터페이스를 사용하는 클래스에서 이 내용을 필수로 구현하도록 만들어 줍니다.

또한, 서로 다른 클래스에서 같은 상황에 호출되는 함수가 있을때 클래스 각각을 호출하여 메서드를 실행하는것이 아닌 인터페이스를 통해 메서드를 호출하요 사용할 수 있습니다.

 

인터페이스를활용하는 예시로 개인적으로는 IDamageable을 인터페이스로 만들어 활용하며, 이 인터페이스가 붙은 클래스는 체력이 존재하거나, 공격에 반응하여 파괴되는 오브젝트등에 사용하였습니다.


ICharacter 를 사용하는 클래스 - Warrior

인터페이스를 사용하는 클래스 입니다.

인터페이스의 프로퍼티의 내용을 저장하는 변수를 선언해주고, 프로퍼티를 통해서 해당 변수의 내용을 읽을수는 있지만 수정할수는 없게 만들어주었습니다.

TakeDamage(int damage) 의 내용도 제작하여 체력에 피해를 받고, 사망여부를 판정하도록 만들어주었습니다.

//플레이어의 캐릭터
internal class Warrior : ICharacter
{
    //이름
    private string name;
    public string Name
    {
        get { return name; }
    }

    //체력
    private int maxHealth = 100;
    private int health;
    public int Health
    {
        get { return health; }
    }

    //공격력
    private int attack = 10;
    public int Attack
    {
        get { return attack; }
    }

    //사망여부
    public bool IsDead { get; private set; }

    //객체 생성시 초기화
    public Warrior(string _name)
    {
        name = _name;
        health = maxHealth;
        IsDead = false;
    }

    //데미지를 받을때 사용되는 함수
    public void TakeDamage(int damage)
    {
        Console.WriteLine($"{name} 이(가) {damage} 의 데미지를 받았습니다.");
        //체력에서 빼기
        health -= damage;
        //체력이 0이하가 되면 사망처리
        if(health <= 0)
        {
            health = 0;
            Console.WriteLine($"{name}이(가) 죽었습니다.");
            IsDead = true;
        }
    }

    //회복아이템 사용
    public void UseHealthPotion(int healAmount)
    {
        //임시로 회복된 체력량 계산
        int temp = health + healAmount;
        //회복이 최대체력을 넘지 않게 만들어줌
        health = (maxHealth < temp)? maxHealth : temp;
    }

    //공격력 아이템 사용
    public void UseStrengthPotion(int amount)
    {
        //수치만큼 공격력 증가
        attack += amount;
    }
}

인터페이스 기능 이외에도 이 클래스에서 아이템을 사용할때 행동을 구현해두었습니다.

 


ICharacter를 사용하는 클래스 - Monster

Warrior 클래스와 마찬가지로 프로퍼티의 내용을 저장하는 변수와, 이를 읽기전용으로 반환해주는 프로퍼티로 써 구현해주었습니다.

또한 아이템을 사용하지 않는 클래스이기 때문에, 아이템 사용과 관련된 내용은 없습니다.

internal abstract class Monster : ICharacter
{
    //이름
    protected string name;
    public string Name
    {
        get { return name; }
    }

    //체력
    protected int maxHealth;
    protected int health;
    public int Health
    {
        get { return health; }
    }

    //공격력
    protected int attack;
    public int Attack
    {
        get { return attack; }
    }

    //사망여부
    public bool IsDead { get; protected set; }

    //몬스터가 사용할 기본 생성자
    public Monster(string _name, int _maxHealth, int _minAttack)
    {
        name = _name;
        maxHealth = _maxHealth;
        health = maxHealth;

        attack = new Random().Next(_minAttack, _minAttack + 5);
    }

    //공격받을때 사용되는 함수
    public void TakeDamage(int damage)
    {
        Console.WriteLine($"{name} 이(가) {damage} 의 데미지를 받았습니다.");
        health -= damage;
        if(health <= 0)
        {
            health = 0;
            Console.WriteLine($"{name}이(가) 죽었습니다.");
            IsDead = true;
        }
    }

Warrior 와 다른점은 생성자를 통해 이름, 체력, 공격력을 입력받아 사용하게 됩니다.

이 부분은 이후 몬스터의 새부 내용을 생성할때 사용하게 됩니다.


Monster의 자식클래스들

이 클래스들에 필요한 내용은 모두 Monster 클래스에 구현되어 있으며, 추가로 구현할 내용은 없습니다.

하지만 이름과 체력, 공격력은 클래스마다 다르게 구현해주어야합니다.

internal class Goblin : Monster
{
    //생성자를 통해 필드에 값 입력
    public Goblin(string _name) : base(_name, 15, 5) { }
}

internal class Dragon : Monster
{
    //생성자를 통해 필드에 값 입력
    public Dragon(string _name) : base(_name, 100, 20) { }
}

부모 클래스인 Monster의 생성자를 이용하여 이 클래스의 객체를 생성하는데, 여기서 몬스터가 표기될 이름을 제외한 몬스터의 체력과 공격력을 정해주고, 객체를 생성할 때 몬스터의 이름만 정해주게됩니다.

 


인터페이스 IItem

아이템이 사용하는 인터페이스입니다.

과제 내용에 포함된 이름과 Use(Warrior player) 함수 이외에도 아이템 효과를 반환하는 string 프로퍼티 EffectText 를 추가하여 아이템의 사용효과를 출력해줄 예정입니다.

internal interface IItem
{
    //아이템 이름
    public string Name { get; }
    //효과 텍스트
    public string EffectText { get; }
    //아이템 사용시 호출되는 함수
    public void Use(Warrior player);
}

IItem 인터페이스를 사용하는 클래스들

체력을 회복해주는 HealthPotion 클래스와, 공격력을 증가시켜주는 StrengthPotion 클래스입니다.

이전 ICharater을 사용하는 클래스와 마찬가지로, 프로퍼티의 내용을 저장할 변수와 읽기전용 프로퍼티를 만들어준 상태입니다.

internal class HealthPotion : IItem
{
    private string name = "회복 포션";
    public string Name { get { return name; } }

    //회복량
    private int healAmount = 50;

    //효과 텍스트
    private string effectText = "체력을 50 회복합니다.";
    public string EffectText { get { return effectText; } }

    //체력회복 함수 호출
    public void Use(Warrior player)
    {
        int temp = player.Health;
        player.UseHealthPotion(healAmount);
        Console.WriteLine($"{player.Name}의 체력이 회복됩니다.");
        Console.WriteLine($"{player.Name}의 체력 : {temp} -> {player.Health}");
        Thread.Sleep(1000);
    }
}

internal class StrengthPotion : IItem
{
    private string name = "힘 포션";
    public string Name { get { return name; } }

    //공격력 증가량
    private int atkUpAmount = 10;

    //효과 텍스트
    private string effectText = "공격력이 10 증가합니다.";
    public string EffectText { get { return effectText; } }

    //공격력 증가 함수 호출
    public void Use(Warrior player)
    {
        Console.WriteLine($"{player.Name}의 공격력이 {atkUpAmount} 증가합니다");
        Console.WriteLine($"공격력 : {player.Attack} -> {player.Attack + atkUpAmount}");
        player.UseStrengthPotion(atkUpAmount);
        Thread.Sleep(1000);
    }
}

두 클래스는 Use 함수의 기능이 다릅니다.

하지만 두 클래스는 모두 사용 아이템이라는 공통점이 있습니다.

사용아이템이라는 공통점으로 다른 기능을 묶어주는것이 인터페이스라고 생각하시면 되겠습니다.

 


게임이 진행될 Stage 클래스 

게임이 시작되고 전투가 진행될 클래스입니다.

아래는 클래스의 전체 코드입니다.

더보기
internal class Stage
{
    //몬스터와 플레이어 객체
    private ICharacter player;
    private ICharacter monster;
    private List<IItem> rewardItems;

    private int nowStage;

    //객체를 생성할 때, 플레이어 객체를 가져옴
    public Stage(ICharacter _player, ICharacter _monster, List<IItem> _rewardItems, int _nowStage)
    {
        player = _player;
        monster = _monster;
        rewardItems = _rewardItems;
        nowStage = _nowStage;
    }

    public void PlayStage()
    {
        //스테이지 정보와 플레이어,몬스터 스텟 정보
        Console.WriteLine($"[스테이지{nowStage}]");
        int playerStatCursor = Console.GetCursorPosition().Top;
        Console.WriteLine($"이름 : {player.Name} | 현재 체력 : {player.Health} | 공격력 : {player.Attack}    ");
        Console.WriteLine();
        Console.WriteLine("몬스터 정보");
        int monsterStatCursor = Console.GetCursorPosition().Top;
        Console.WriteLine($"이름 : {monster.Name} | 체력 : {monster.Health} | 공격력 : {monster.Attack}     ");
        Console.WriteLine("---------------------------------------------------------------");
        int battleCursorPos = Console.GetCursorPosition().Top;
        int lastCursorPos;

        Thread.Sleep(1000);

        //둘중 하나가 죽을때까지 반복
        while(!player.IsDead && !monster.IsDead)
        {
            //플레이어의 턴부터 시작
            Console.WriteLine($"{player.Name} 의 턴");
            monster.TakeDamage(player.Attack);
            lastCursorPos = Console.GetCursorPosition().Top;
            Console.SetCursorPosition(0, monsterStatCursor);
            Console.WriteLine($"이름 : {monster.Name} | 체력 : {monster.Health} | 공격력 : {monster.Attack}     ");
            Console.SetCursorPosition(0, lastCursorPos);
            Thread.Sleep(1000);

            //몬스터 사망시 종료
            if(monster.IsDead == true)
            {
                break;
            }

            //턴 정보 지우기
            ConsoleLineClear(lastCursorPos, battleCursorPos);

            //몬스터의 턴
            Console.WriteLine($"{monster.Name} 의 턴");
            player.TakeDamage(monster.Attack);
            lastCursorPos = Console.GetCursorPosition().Top;
            Console.SetCursorPosition(0, playerStatCursor);
            Console.WriteLine($"이름 : {player.Name} | 현재 체력 : {player.Health} | 공격력 : {player.Attack}     ");
            Console.SetCursorPosition(0, lastCursorPos);
            Thread.Sleep(1000);

            //턴 정보 지우기
            ConsoleLineClear(lastCursorPos, battleCursorPos);
        }

        //반복문 종료후, 플레이어 생존 여부를 통해 결과 출력
        StageEnd(player.IsDead);
    }

    //스테이지 종료
    private void StageEnd(bool isPlayerDead)
    {
        //플레이어 상태를 받아서 클리어 성공 여부 판정
        if(isPlayerDead == false)
        {
            Console.Clear();
            Console.WriteLine($"스테이지{nowStage} 클리어!");
            Console.WriteLine();

            //보상 아이템이 있으면 보상 주기
            if (rewardItems.Count > 0)
            {
                Console.WriteLine("클리어 보상을 선택할 수 있습니다.");
                //보상 앞에 숫자 띄우기
                int selector = 1;
                foreach(var item in rewardItems)
                {
                    Console.WriteLine($"{selector}. {item.Name} : {item.EffectText}");
                    selector++;
                }
                Console.WriteLine();
                //보상 선택 반복문 시작
                while (true)
                {
                    Console.WriteLine("사용할 아이템 번호를 입력해주세요");
                    //int 변수 재활용, 번호를 통해 아이템 선택
                    if (int.TryParse(Console.ReadLine(), out selector))
                    {
                        if(selector >= 1 && selector < rewardItems.Count + 1)
                        {
                            //플레이어가 인터페이스로 선언된 상태 -> Warrior로 형변환 시도
                            rewardItems[selector - 1].Use((Warrior)player);
                            //선택 완료, 반복문 종료
                            break;
                        }
                        else
                        {
                            Console.WriteLine("잘못된 선택입니다.");
                        }
                        
                    }
                    else
                    {
                        Console.WriteLine("잘못된 선택입니다.");
                    }
                }
                //다음 스테이지로 넘어갈 준비, 콘솔 모두 지우기
                Thread.Sleep(1000);
                Console.Clear();
            }
        }
        else
        {
            //플레이어 사망시 게임 오버
            Console.Clear();
            Console.WriteLine("게임 오버");
        }
    }

    //원하는 줄만 콘솔에서 지우기 위해 사용하느 함수
    private void ConsoleLineClear(int nowLine, int toClearLine)
    {
        int line = nowLine - toClearLine;
        for(int i = 0; i < line; i++)
        {
            Console.SetCursorPosition(0, Console.GetCursorPosition().Top - 1);
            Console.Write("\r                                                                       \r");
        }
    }
}

사실상 게임이 진행되는 클래스입니다.

 

해당 클래스의 필드와 생성자 입니다.

//몬스터와 플레이어 객체
private ICharacter player;
private ICharacter monster;
private List<IItem> rewardItems;

private int nowStage;

//객체를 생성할 때, 플레이어 객체를 가져옴
public Stage(ICharacter _player, ICharacter _monster, List<IItem> _rewardItems, int _nowStage)
{
    player = _player;
    monster = _monster;
    rewardItems = _rewardItems;
    nowStage = _nowStage;
}

객체를 선언할 때, 클래스명이 아닌 인터페이스명으로 선언할 수 있습니다.

인터페이스를 가진 클래스는 해당 인터페이스가 가진 모든 기능을 가지고있기 때문에 클래스명 대신 사용할 수 있습니다.

 

또한 사용아이템을 지정하는 IItem 인터페이스를 통해 체력포션과 공격력 포션을 묶어 보상으로 보여주고, 사용할 예정입니다.

 

이후 생성자를 통해 필드에 선언한 인터페이스에 클래스 객체를 받아줍니다.

 

게임을 진행하는 함수 PlayStage() 입니다.

텍스트를 생성하는 내용이 주를 이루고 있는 상태입니다.

public void PlayStage()
{
    //스테이지 정보와 플레이어,몬스터 스텟 정보
    Console.WriteLine($"[스테이지{nowStage}]");
    int playerStatCursor = Console.GetCursorPosition().Top;
    Console.WriteLine($"이름 : {player.Name} | 현재 체력 : {player.Health} | 공격력 : {player.Attack}    ");
    Console.WriteLine();
    Console.WriteLine("몬스터 정보");
    int monsterStatCursor = Console.GetCursorPosition().Top;
    Console.WriteLine($"이름 : {monster.Name} | 체력 : {monster.Health} | 공격력 : {monster.Attack}     ");
    Console.WriteLine("---------------------------------------------------------------");
    int battleCursorPos = Console.GetCursorPosition().Top;
    int lastCursorPos;

    Thread.Sleep(1000);

    //둘중 하나가 죽을때까지 반복
    while(!player.IsDead && !monster.IsDead)
    {
        //플레이어의 턴부터 시작
        Console.WriteLine($"{player.Name} 의 턴");
        monster.TakeDamage(player.Attack);
        lastCursorPos = Console.GetCursorPosition().Top;
        Console.SetCursorPosition(0, monsterStatCursor);
        Console.WriteLine($"이름 : {monster.Name} | 체력 : {monster.Health} | 공격력 : {monster.Attack}     ");
        Console.SetCursorPosition(0, lastCursorPos);
        Thread.Sleep(1000);

        //몬스터 사망시 종료
        if(monster.IsDead == true)
        {
            break;
        }

        //턴 정보 지우기
        ConsoleLineClear(lastCursorPos, battleCursorPos);

        //몬스터의 턴
        Console.WriteLine($"{monster.Name} 의 턴");
        player.TakeDamage(monster.Attack);
        lastCursorPos = Console.GetCursorPosition().Top;
        Console.SetCursorPosition(0, playerStatCursor);
        Console.WriteLine($"이름 : {player.Name} | 현재 체력 : {player.Health} | 공격력 : {player.Attack}     ");
        Console.SetCursorPosition(0, lastCursorPos);
        Thread.Sleep(1000);

        //턴 정보 지우기
        ConsoleLineClear(lastCursorPos, battleCursorPos);
    }

    //반복문 종료후, 플레이어 생존 여부를 통해 결과 출력
    StageEnd(player.IsDead);
}

플레이어와 몬스터가 서로 턴을 주고받으며 공격하고, 둘중 하나의 IsDead 프로퍼티가 false가 될경우 반복문을 종료합닏 .

첫 턴은 플레이어의 턴인데, 이때 몬스터가 사망처리될경우 몬스터는 턴을 받을 수 없기때문에 즉시 반복문을 종료합니다.

//몬스터 사망시 종료
if(monster.IsDead == true)
{
    break;
}

 

반복문이 종료되면 플레이어의 사망 상태를 통해 스테이지 클리어 여부를 결정합니다.

//스테이지 종료
private void StageEnd(bool isPlayerDead)
{
    //플레이어 상태를 받아서 클리어 성공 여부 판정
    if(isPlayerDead == false)
    {
        Console.Clear();
        Console.WriteLine($"스테이지{nowStage} 클리어!");
        Console.WriteLine();

        //보상 아이템이 있으면 보상 주기
        if (rewardItems.Count > 0)
        {
            Console.WriteLine("클리어 보상을 선택할 수 있습니다.");
            //보상 앞에 숫자 띄우기
            int selector = 1;
            foreach(var item in rewardItems)
            {
                Console.WriteLine($"{selector}. {item.Name} : {item.EffectText}");
                selector++;
            }
            Console.WriteLine();
            //보상 선택 반복문 시작
            while (true)
            {
                Console.WriteLine("사용할 아이템 번호를 입력해주세요");
                //int 변수 재활용, 번호를 통해 아이템 선택
                if (int.TryParse(Console.ReadLine(), out selector))
                {
                    if(selector >= 1 && selector < rewardItems.Count + 1)
                    {
                        //플레이어가 인터페이스로 선언된 상태 -> Warrior로 형변환 시도
                        rewardItems[selector - 1].Use((Warrior)player);
                        //선택 완료, 반복문 종료
                        break;
                    }
                    else
                    {
                        Console.WriteLine("잘못된 선택입니다.");
                    }
                    
                }
                else
                {
                    Console.WriteLine("잘못된 선택입니다.");
                }
            }
            //다음 스테이지로 넘어갈 준비, 콘솔 모두 지우기
            Thread.Sleep(1000);
            Console.Clear();
        }
    }
    else
    {
        //플레이어 사망시 게임 오버
        Console.Clear();
        Console.WriteLine("게임 오버");
    }
}

스테이지 종료시, 플레이어의 IsDead 가 true일경우 게임 오버연출을 발생시키고 false일경우 보상이 있으면 보상을 지급합니다.

 

이때 보상 지급을 List<IItem> 에 저장된 인터페이스를 꺼내서 지급합니다.

if (int.TryParse(Console.ReadLine(), out selector))
{
    if(selector >= 1 && selector < rewardItems.Count + 1)
    {
        //플레이어가 인터페이스로 선언된 상태 -> Warrior로 형변환 시도
        rewardItems[selector - 1].Use((Warrior)player);
        //선택 완료, 반복문 종료
        break;
    }
    else
    {
        Console.WriteLine("잘못된 선택입니다.");
    }
    
}

IItem에 Use 함수가 구현되어있기 때문에, 해당 인터페이스로 구현된 2개의 아이템 클래스를 인터페이스를 통해 사용하게됩니다.

 

마지막으로, 전투중 원하는만큼 콘솔을 지우는 함수입니다.

//원하는 줄만 콘솔에서 지우기 위해 사용하느 함수
private void ConsoleLineClear(int nowLine, int toClearLine)
{
    int line = nowLine - toClearLine;
    for(int i = 0; i < line; i++)
    {
        Console.SetCursorPosition(0, Console.GetCursorPosition().Top - 1);
        Console.Write("\r                                                                       \r");
    }
}

-> 블로그 작성중 생각난 내용이지만, 기본 스텟표기 이후 if문을 사용하여 누구의 턴인지 구분하고 표기를 달리 했으면 되지 않았을까 하는 생각이 듭니다. 마지막 부분에 Console.Clear() 를 포함해서

 


Program클래스 Main함수

프로그램이 실행될 메인함수입니다.

메인함수에서 각 클래스들의 객체를 생성해주고 사용합니다.

더보기
internal class Program
{
    static void Main(string[] args)
    {
        //몬스터 객체 생성
        Goblin goblin1 = new Goblin("고블린 1");
        Goblin goblin2 = new Goblin("고블린 2");
        Goblin goblin3 = new Goblin("고블린 3");
        Dragon dragon = new Dragon("드래곤");

        //스테이지 보상 생성
        List<IItem> noReward = new List<IItem>();
        List<IItem> Rewards = new List<IItem> { new HealthPotion(), new StrengthPotion() };

        //플레이어 객체 생성
        string playerName;
        
        Console.WriteLine("플레이어의 이름을 입력해주세요");
        playerName = Console.ReadLine();

        Warrior player = new Warrior(playerName);
        Console.Clear();

        Console.WriteLine("스테이지 1 시작");
        Thread.Sleep(1000);
        Console.Clear();

        //스테이지 1 생성 및 플레이
        Stage stage1 = new Stage(player, goblin1, noReward, 1);
        stage1.PlayStage();

        //사망시 게임 종료
        if(player.IsDead)
        {
            return;
        }

        Console.WriteLine("스테이지 2 시작");
        Thread.Sleep(1000);
        Console.Clear();

        //스테이지 2 생성 및 플레이
        Stage stage2 = new Stage(player, goblin2, Rewards, 2);
        stage2.PlayStage();

        //사망시 게임 종료
        if (player.IsDead)
        {
            return;
        }

        Console.WriteLine("스테이지 3 시작");
        Thread.Sleep(1000);
        Console.Clear();

        //스테이지 3 생성 및 플레이
        Stage stage3 = new Stage(player, goblin3, Rewards, 3);
        stage3.PlayStage();

        //사망시 게임 종료
        if (player.IsDead)
        {
            return;
        }

        Console.WriteLine("스테이지 4 시작");
        Thread.Sleep(1000);
        Console.Clear();

        //스테이지 4 생성 및 플레이
        Stage stage4 = new Stage(player, dragon, noReward, 4);
        stage4.PlayStage();

        //사망시 게임 종료
        if (player.IsDead)
        {
            return;
        }

        Console.WriteLine("모든 스테이지를 클리어했습니다!");
    }
}

먼저 몬스터들의 객체를 생성합니다.

각 몬스터의 이름을 이때 정해주게됩니다.

//몬스터 객체 생성
Goblin goblin1 = new Goblin("고블린 1");
Goblin goblin2 = new Goblin("고블린 2");
Goblin goblin3 = new Goblin("고블린 3");
Dragon dragon = new Dragon("드래곤");

 

이후 스테이지 보상을 만들어줍니다.

스테이지 보상은 List형식으로 저장되어있으며, 각 리스트에 IItem 인터페이스를 가진 클래스가 저장됩니다.

//스테이지 보상 생성
List<IItem> noReward = new List<IItem>();
List<IItem> Rewards = new List<IItem> { new HealthPotion(), new StrengthPotion() };

 

플레이어의 객체를 생성하고, 게임을 시작합니다.

이때 사용자로부터 이름을 받아서 플레이어의 이름으로 사용합니다.

//플레이어 객체 생성
string playerName;

Console.WriteLine("플레이어의 이름을 입력해주세요");
playerName = Console.ReadLine();

Warrior player = new Warrior(playerName);
Console.Clear();

 

스테이지의 객체를 만들고, 게임을 플레이합니다.

이때 스테이지의 생성자에 만들어준 플레이어의 객체, 해당스테이지의 몬스터 객체, 보상 리스트, 스테이지 번호를 넣어줍니다.

Console.WriteLine("스테이지 1 시작");
Thread.Sleep(1000);
Console.Clear();

//스테이지 1 생성 및 플레이
Stage stage1 = new Stage(player, goblin1, noReward, 1);
stage1.PlayStage();

//사망시 게임 종료
if(player.IsDead)
{
    return;
}

스테이지가 종료되면 플레이어의 사망 여부를 판단하여 게임 진행 여부를 판단합니다.

 

이러한 형식으로 4스테이지까지 생성해주었습니다.

 

아래는 풀이와의 비교입니다.


더보기

1. 프로퍼티 사용

저는 프로퍼티의 내용을 저장하는 변수를 따로 선언하여 사용하였지만, 풀이에서는 프로퍼티에 직접 값을 저장하여 사용하였습니다.

 

또한 Attack 에 화살표 함수를 사용, 랜덤함수를 써서 공격력을 무작위로 정해주는 형태입니다.

 

// 몬스터 클래스
public class Monster : ICharacter
{
    public string Name { get; }
    public int Health { get; set; }
    public int Attack => new Random().Next(10, 20); // 공격력은 랜덤

    public bool IsDead => Health <= 0;

    public Monster(string name, int health)
    {
        Name = name;
        Health = health;
    }

    public void TakeDamage(int damage)
    {
        Health -= damage;
        if (IsDead) Console.WriteLine($"{Name}이(가) 죽었습니다.");
        else Console.WriteLine($"{Name}이(가) {damage}의 데미지를 받았습니다. 남은 체력: {Health}");
    }
}

 

2. 아이템 사용

저는 Warrior 클래스에 아이템 사용 함수를 구현하고 아이템의 Use 함수에서는 Warrior 클래스의 함수를 호출하는 방식으로 처리하였지만, 풀이에서는 각 아이템의 Use에서 Warrior의 체력과 공격력에 접근해 프로퍼티 값을 바꾸는 식으로 사용되었습니다.

// 전사 클래스
public class Warrior : ICharacter
{
    public string Name { get; }
    public int Health { get; set; }
    public int AttackPower { get; set; }

    public bool IsDead => Health <= 0;
    public int Attack => new Random().Next(10, AttackPower); // 공격력은 랜덤

    public Warrior(string name)
    {
        Name = name;
        Health = 100; // 초기 체력
        AttackPower = 20; // 초기 공격력
    }

    public void TakeDamage(int damage)
    {
        Health -= damage;
        if (IsDead) Console.WriteLine($"{Name}이(가) 죽었습니다.");
        else Console.WriteLine($"{Name}이(가) {damage}의 데미지를 받았습니다. 남은 체력: {Health}");
    }
}

// 체력 포션 클래스
public class HealthPotion : IItem
{
    public string Name => "체력 포션";

    public void Use(Warrior warrior)
    {
        Console.WriteLine("체력 포션을 사용합니다. 체력이 50 증가합니다.");
        warrior.Health += 50;
        if (warrior.Health > 100) warrior.Health = 100;
    }
}

 

3. 스테이지 클리어 시 행동

저는 플레이어의 사망 여부를 직접 함수에 넘겨서 스테이지 클리어 여부를 결정하였는데 풀이에서는 델리게이트를 사용하여 둘중 하나가 죽었을때 죽은쪽의 인터페이스를 넘겨주는 방식으로 구현되었습니다.

public class Stage
{
    private ICharacter player; // 플레이어
    private ICharacter monster; // 몬스터
    private List<IItem> rewards; // 보상 아이템들

    // 이벤트 델리게이트 정의
    public delegate void GameEvent(ICharacter character);
    public event GameEvent OnCharacterDeath; // 캐릭터가 죽었을 때 발생하는 이벤트

    public Stage(ICharacter player, ICharacter monster, List<IItem> rewards)
    {
        this.player = player;
        this.monster = monster;
        this.rewards = rewards;
        OnCharacterDeath += StageClear; // 캐릭터가 죽었을 때 StageClear 메서드 호출
    }

    // 스테이지 시작 메서드
    public void Start()
    {
        Console.WriteLine($"스테이지 시작! 플레이어 정보: 체력({player.Health}), 공격력({player.Attack})");
        Console.WriteLine($"몬스터 정보: 이름({monster.Name}), 체력({monster.Health}), 공격력({monster.Attack})");
        Console.WriteLine("----------------------------------------------------");

        while (!player.IsDead && !monster.IsDead) // 플레이어나 몬스터가 죽을 때까지 반복
        {
            // 플레이어의 턴
            Console.WriteLine($"{player.Name}의 턴!");
            monster.TakeDamage(player.Attack);
            Console.WriteLine();
            Thread.Sleep(1000);  // 턴 사이에 1초 대기

            if (monster.IsDead) break;  // 몬스터가 죽었다면 턴 종료

            // 몬스터의 턴
            Console.WriteLine($"{monster.Name}의 턴!");
            player.TakeDamage(monster.Attack);
            Console.WriteLine();
            Thread.Sleep(1000);  // 턴 사이에 1초 대기
        }

        // 플레이어나 몬스터가 죽었을 때 이벤트 호출
        if (player.IsDead)
        {
            OnCharacterDeath?.Invoke(player);
        }
        else if (monster.IsDead)
        {
            OnCharacterDeath?.Invoke(monster);
        }
    }

 

이에따라 클리어시 함수가 매개변수로 받은 내용이 Monster 클래스인지의 여부를통해 클리판정을 발생시킵니다.

// 스테이지 클리어 메서드
    private void StageClear(ICharacter character)
    {
        if (character is Monster)
        {
            Console.WriteLine($"스테이지 클리어! {character.Name}를 물리쳤습니다!");

            // 플레이어에게 아이템 보상
            if (rewards != null)
            {
                Console.WriteLine("아래의 보상 아이템 중 하나를 선택하여 사용할 수 있습니다:");
                foreach (var item in rewards)
                {
                    Console.WriteLine(item.Name);
                }

                Console.WriteLine("사용할 아이템 이름을 입력하세요:");
                string input = Console.ReadLine();

                // 선택된 아이템 사용
                IItem selectedItem = rewards.Find(item => item.Name == input);
                if (selectedItem != null)
                {
                    selectedItem.Use((Warrior)player);
                }
            }

            player.Health = 100; // 각 스테이지마다 체력 회복
        }
        else
        {
            Console.WriteLine("게임 오버! 패배했습니다...");
        }
    }