실습_zombiebattle

Enzeun
Enzeun
∙∙조회 8

using System;

using System.Buffers;

using System.ComponentModel;

using System.Diagnostics;

using System.Security.Cryptography.X509Certificates;

using System.Text.Json.Serialization;

class ZombieGame

{

csharp
// ══════════════════════════════════════════

// Step 1. 상수 + 열거형

// ══════════════════════════════════════════

// TODO: 게임에서 절대 바뀌지 않는 수치들을 상수로 선언하세요

// 플레이어의 최대 HP, 공격력, 방어력

const int PLAYER_MAX_HP = 100;

const int PLAYER_ATTACK = 25;

const int PLAYER_DEFENCE = 5;

// 스테이지당 좀비 수, 회복량, 최소 데미지

const int ZOMBIE_COUNT = 5;

const int HEAL_AMOUNT = 20;

const int MIN_DAMAGE = 1;

// TODO: 플레이어의 상태를 열거형으로 선언하세요

// 생존 / 위험(HP 30 이하) / 사망

enum PlayerState

{

    Alived,

    Danger,

    Dead

}

// TODO: 좀비의 등급을 열거형으로 선언하세요

// 일반 / 달리기(공격력 높음) / 탱커(HP 높음)

enum ZombieType

{

    Normal,

    Runner,

    Tanker

}

// ══════════════════════════════════════════

// Step 2. 인터페이스 (추상화)

// ══════════════════════════════════════════

// TODO: 데미지를 받을 수 있는 대상의 약속을 인터페이스로 선언하세요

// HP 조회, 생존 여부 확인, 데미지를 받는 기능이 필요합니다

public interface IDamageable

{

    public int GetHp();

    public bool IsAlive { get; }

    public void TakeDamage(int damage);

}

// TODO: 회복할 수 있는 대상의 약속을 인터페이스로 선언하세요

public interface IHealable

{

    public void Heal();

}

// TODO: 상태를 출력할 수 있는 대상의 약속을 인터페이스로 선언하세요

public interface IPrintable

{

    public void PrintStatus();

}

// ══════════════════════════════════════════

// Step 3. Zombie 클래스

// IDamageable, IPrintable을 구현합니다

// ══════════════════════════════════════════

class Zombie : IDamageable, IPrintable

{

    // TODO: 이름, HP, 최대HP, 공격력, 등급 프로퍼티를 선언하세요

    // 외부에서 읽을 수는 있지만 직접 바꿀 수는 없어야 합니다

    public string Name { get; private set; }

    public int MaxHp { get; private set; }

    public int Hp { get; private set; }

    public int Attack { get; private set; }

    public ZombieType Type { get; private set; }

    // TODO: 생존 여부를 반환하는 읽기 전용 프로퍼티를 선언하세요

    public bool IsAlive => Hp > 0;

    // TODO: 생성자를 작성하세요

    // 등급에 따라 HP와 공격력이 달라집니다

    // 탱커: HP 80, 공격력 8 / 달리기: HP 30, 공격력 20 / 일반: HP 50, 공격력 12

    public Zombie(ZombieType type, string name)

    {

        Type = type;

        Name = name;

        switch (type)

        {

            case ZombieType.Normal:

                initialize(50, 12);

                break;

            case ZombieType.Runner:

                initialize(30, 20);

                break;

            case ZombieType.Tanker:

                initialize(80, 8);

                break;

            default: break;

        }

        void initialize(int hp, int atk)

        {

            MaxHp = hp;

            Hp = MaxHp;

            Attack = atk;

        }

    }

    // TODO: 데미지를 받는 기능을 구현하세요

    // HP가 0 아래로 내려가지 않도록 처리하세요

    public void TakeDamage(int damage)

    {

        Hp -= damage;

        if (Hp <= 0)

        {

            Hp = 0;

            OnZombieDied?.Invoke(Name);

            OnZombieDied = null;

        }

    }

    public event Action<string> OnZombieDied;

    // TODO: 상태를 출력하는 기능을 구현하세요

    // 예시: [탱커] 썩은손 | HP: 80/80 | 공격력: 8

    public void PrintStatus()

    {

        string label = Type  ==ZombieType.Tanker ? "탱커" : Type==  ZombieType.Runner ? "러너" : "일반";

        Console.WriteLine($" [{label}] {Name} | HP: {Hp}/{MaxHp} | 공격력: {Attack}");

    }

    // 인터페이스 미구현 함수들 구현

    public int GetHp()

    {

        return Hp;

    }

}

// ══════════════════════════════════════════

// Step 4. Player 클래스 (핵심 — 이벤트 발행자)

//

// 델리게이트/이벤트 구조:

//   Player 안에 event를 선언합니다

//   HP가 바뀌거나 좀비를 처치할 때 event를 발행합니다

//   외부(Main)에서 event에 람다식으로 구독합니다

//

// event를 쓰는 이유:

//   외부에서 직접 Invoke할 수 없어야 하고

//   += / -= 로만 구독/해지할 수 있어야 하기 때문입니다

// ══════════════════════════════════════════

class Player : IDamageable, IHealable, IPrintable

{

    // TODO: 이름, HP, 최대HP, 공격력, 방어력 프로퍼티를 선언하세요

    public string Name { get; private set; }

    public int Hp { get; private set; }

    public int MaxHp { get; private set; }

    public int Attack { get; private set; }

    public int Defence { get; private set; }

    // TODO: 생존 여부 프로퍼티를 선언하세요

    public bool IsAlive => Hp > 0;

    // TODO: 현재 상태를 반환하는 프로퍼티를 선언하세요

    // HP 조건에 따라 Dead / Danger / Alive 중 하나를 반환합니다

    public PlayerState State

    {

        get

        {

            if (Hp <= 0)

            {

                return PlayerState.Dead;

            }

            else if (Hp < 30)

            {

                return PlayerState.Danger;

            }

            else

                return PlayerState.Alived;

        }

    }

    // TODO: 이벤트 두 개를 선언하세요

    //

    // 첫 번째: HP가 바뀔 때 발행하는 이벤트

    //   구독자에게 이전 HP와 현재 HP를 전달해야 합니다

    //   Action<int, int> 형태로 선언하세요 (이전HP, 현재HP)

    public event Action<int, int> OnHpChanged;

    //

    // 두 번째: 좀비를 처치했을 때 발행하는 이벤트

    //   구독자에게 처치한 좀비 이름을 전달해야 합니다

    //   Action<string> 형태로 선언하세요

    public event Action<string> OnZombieKilled;

    // TODO: 생성자를 작성하세요

    public Player(string name)

    {

        Name = name;

        MaxHp = PLAYER_MAX_HP;

        Hp = MaxHp;

        Attack = PLAYER_ATTACK;

        Defence = PLAYER_DEFENCE;

    }

    // TODO: 데미지를 받는 기능을 구현하세요

    // 방어력만큼 데미지를 줄이되 최소 MIN_DAMAGE는 보장합니다

    // HP가 바뀐 뒤 OnHpChanged 이벤트를 발행하세요

    //   발행 방법: OnHpChanged?.Invoke(이전HP, 현재HP)

    //   ?.Invoke 를 쓰는 이유: 구독자가 없으면(null) 오류 없이 무시됩니다

    // 예시: 준헌이(가) 3 피해를 받음 (HP: 97/100)

    public void TakeDamage(int damage)

    {

        int prevHp = Hp;

        int finalDamage = Math.Max((damage - Defence), MIN_DAMAGE);

        Hp = Math.Max(Hp - finalDamage, 0);

        Console.WriteLine($"{Name}이(가) {finalDamage} 피해를 받음 (HP: {Hp}/{MaxHp}");

        if (prevHp != Hp)

        {

            OnHpChanged?.Invoke(prevHp, Hp);

        }

    }

    // TODO: 회복 기능을 구현하세요

    // 최대 HP를 초과하지 않도록 처리하세요

    // 회복 후에도 OnHpChanged 이벤트를 발행하세요

    // 예시: 회복! 준헌 HP: 70/100

    public void Heal()

    {

        int prevHp = Hp;

        Hp = Math.Min(Hp + HEAL_AMOUNT, PLAYER_MAX_HP);

        Console.WriteLine($"회복! {Name} HP: {Hp}/{MaxHp}");

        if (prevHp != Hp)

        {

            OnHpChanged?.Invoke(prevHp, Hp);

        }

    }

    // TODO: 좀비를 처치했을 때 호출할 메서드를 작성하세요

    // OnZombieKilled 이벤트를 발행합니다

    // 전투 함수에서 좀비가 죽었을 때 이 메서드를 호출하세요

    public void NotifyZombieKilled(string zombieName)

    {

        OnZombieKilled?.Invoke(zombieName);

    }

    // TODO: 상태 출력 기능을 구현하세요

    // 예시: [플레이어] 준헌 | HP: 100/100

    // Danger 상태면 끝에 위험 표시를 추가하세요

    public void PrintStatus()

    {

        if (State == PlayerState.Danger)

        {

            Console.Write($"[플레이어] {Name} | HP: {Hp}/{MaxHp} !!DANGER!!");

        }

        else

            Console.Write($"[플레이어] {Name} | HP: {Hp}/{MaxHp}");

    }

    //나머지 인터페이스 미구현 함수 구현

    public int GetHp() => Hp;

}

// ══════════════════════════════════════════

// Step 5. 전투 함수들

// ══════════════════════════════════════════

// TODO: 플레이어가 좀비를 공격하는 함수를 작성하세요

// 탱커 좀비는 데미지가 절반입니다

// 좀비가 처치되면 player.NotifyZombieKilled(좀비이름)를 호출하세요

// 예시: 준헌 → 썩은손 공격! 12 피해

static void PlayerAttackZombie(Player player, Zombie target)

{

    int damage = player.Attack;

    if (target.Type == ZombieType.Tanker)  damage /= 2;

    damage = Math.Max(damage, MIN_DAMAGE);

    target.TakeDamage(damage);

    Console.WriteLine($"{[player.Name](http://player.Name)} 공격! {[target.Name](http://target.Name)}이 {damage} 피해를 입음!");

}

// TODO: 좀비가 플레이어를 공격하는 함수를 작성하세요

// 죽은 좀비는 공격하지 않습니다

// 예시: 썩은손 → 준헌 공격!

static void ZombieAttackPlayer(Zombie Zombie, Player player)

{

    if (!Zombie.IsAlive)

        return;

    player.TakeDamage(Zombie.Attack);

    Console.WriteLine($"{[Zombie.Name](http://Zombie.Name)} 공격! {[player.Name](http://player.Name)}이 {Zombie.Attack} 피해를 입음!");

}

// TODO: 인덱스를 받아서 좀비를 생성하는 함수를 작성하세요

// 인덱스 나머지로 등급을 결정하세요 (0:탱커 / 1:달리기 / 2:일반)

// 이름 목록: 썩은손, 피투성이, 절름발이, 괴물, 검은눈

static Zombie CreateZombie(int index)

{

    ZombieType type = ZombieType.Normal;

    string[] names = { "썩은손", "피투성이", "절름발이", "괴물", "검은눈" };

    switch (index)

    {

        case 0:

            type = ZombieType.Tanker;

            break;

        case 1:

            type = ZombieType.Runner;

            break;

        case 2:

            type = ZombieType.Normal;

            break;

        default:

            type = ZombieType.Normal;

            break;

    }

    return new Zombie(type, names[index % names.Length]);

}

// TODO: 전투 결과를 출력하는 함수를 작성하세요

// 승리면 클리어 메시지와 생존 HP를 출력하세요

// 패배면 게임 오버 메시지를 출력하세요

static void PrintBattleResult(Player player, bool isWon)

{

    if (isWon)

    {

        Console.WriteLine("플레이어 승리!");

        Console.WriteLine($"생존 Hp: {player.Hp}/{player.MaxHp}");

    }

    else

    {

        Console.WriteLine("패배하였습니다!");

        Console.WriteLine("게임 오버");

    }

}

// ══════════════════════════════════════════

// Step 6. 전투 루프

// ══════════════════════════════════════════

// TODO: 전투 전체 흐름을 담당하는 함수를 작성하세요

// 반환값은 승리 여부(bool)입니다

//

// 흐름:

//   좀비 배열을 만들고 채운 뒤 전체 상태를 출력합니다

//   플레이어가 살아있는 동안 반복합니다

//     살아있는 좀비가 없으면 승리를 반환합니다

//     살아있는 첫 번째 좀비와 1:1 전투를 진행합니다

//     플레이어가 위험 상태면 자동으로 회복합니다

//   반복이 끝나면 패배를 반환합니다

static bool BattleLoop(Player player)

{

    //좀비생성

    Zombie[] zombies = new Zombie[ZOMBIE_COUNT];

    for (int i = 0; i < ZOMBIE_COUNT; i++)

    {

        zombies[i] = CreateZombie(i);

        zombies[i].PrintStatus();

        zombies[i].OnZombieDied += player.NotifyZombieKilled;

    }

    while (player.IsAlive)

    {

        int aliveZombie = 0;

        for (int i = 0; i < ZOMBIE_COUNT; i++)

        {

            if (zombies[i].IsAlive)

            {

                aliveZombie++;

            }

        }

        if (aliveZombie == 0) return true;

        for (int i = 0; i < ZOMBIE_COUNT; i++)

        {

            if (!zombies[i].IsAlive)

                continue;

            PlayerAttackZombie(player, zombies[i]);

            if (zombies[i].IsAlive)

            {

                ZombieAttackPlayer(zombies[i], player);

                Thread.Sleep(2000);

            }

            else

            {

                Console.WriteLine("승리하였습니다!");

            }

            break;

        }

        if (player.State == PlayerState.Danger)

        {

            player.Heal();

        }

    }

    return false;

}

// ══════════════════════════════════════════

// Step 7. 통합 실행

//

// 이벤트 구독은 전부 여기서 합니다

// Player 안에 UI/로그 코드를 넣지 않습니다

// Player는 "이벤트를 발행"하고

// Main이 "어떻게 반응할지"를 결정합니다

// ══════════════════════════════════════════

static void Main()

{

    Console.OutputEncoding = System.Text.Encoding.UTF8;

    // TODO: 게임 타이틀을 출력하세요

    Console.WriteLine("좀비배틀");

    // TODO: 플레이어 이름을 입력받으세요

    string name = Console.ReadLine();

    // 비어있으면 "생존자"로 설정하세요

    if (string.IsNullOrEmpty(name))

    {

        name = "생존자";

    }

    // TODO: Player 객체를 생성하세요

    Player player = new Player(name);

    // TODO: OnHpChanged 이벤트에 람다식으로 두 가지 반응을 구독하세요

    //

    // 첫 번째 구독 — HP 막대 그래프 출력

    //   HP 비율을 계산해서 10칸짜리 막대로 출력합니다

    //   예시: [UI] [██████░░░░] 60/100

    //   힌트: new string('█', 채워진칸) + new string('░', 빈칸)

    //   player.OnHpChanged += (oldHp, newHp) => { ... }

    Action<int, int> hpBarHandler = (prevHp, newHp) =>

    {

        //현재 hp비율 계산

        float ratio = (float)newHp / player.MaxHp;

        int filled = (int)(ratio * 10);

        string bar = new string('█', filled) + new string('░', (10 - filled));

        Console.WriteLine($"[UI] [{bar}] {newHp}/{player.Hp}");

    };

    player.OnHpChanged += hpBarHandler;

    //

    // 두 번째 구독 — 전투 로그 출력

    //   HP 변화 내역을 출력합니다

    //   예시: [LOG] 준헌 HP: 100→70

    //   player.OnHpChanged += (oldHp, newHp) => { ... }

    player.OnHpChanged += (prevHp, newHp) =>

    {

        Console.WriteLine($"[LOG] {[player.Name](http://player.Name)} HP: {prevHp}->{newHp}");

    }; // 람다식은 해제가 불가능함.

    // TODO: OnZombieKilled 이벤트에 람다식으로 구독하세요

    //   처치 내역을 출력합니다

    //   예시: [킬로그] 썩은손 처치!

    //   player.OnZombieKilled += (name) => { ... }

    player.OnZombieKilled += (name) =>

    {

        Console.WriteLine($"[킬로그] {name} 처치!");

    };

    // TODO: 시작 메시지를 출력하세요

    // 예시: 준헌, 좀비 5마리를 처치하라!

    Console.WriteLine($"{[player.Name](http://player.Name)}, 좀비 {ZOMBIE_COUNT}마리를 처치하라!");

    // TODO: 전투를 실행하고 결과를 받으세요

    bool result = BattleLoop(player);

    // TODO: 전투 결과를 출력하세요

    PrintBattleResult(player, result);

    // TODO: HP 막대 그래프 구독을 해지하세요

    //

    // 주의: 람다식은 변수에 저장하지 않으면 해지할 수 없습니다

    //   해지가 필요한 구독은 람다식을 미리 변수에 저장한 뒤 등록하세요

    //   예시:

    //     Action<int, int> hpBarHandler = (oldHp, newHp) => { ... }

    //     player.OnHpChanged += hpBarHandler

    //     ... 나중에 ...

    //     player.OnHpChanged -= hpBarHandler

}

}