문제1 - 예제 123 + 예제의 심화

Item, UsableItem, Weapon, Player객체

public class Example
{
    public class Item
    {
        private string _name;
        
        public Item(string name)
        {
            _name = name;
        }
        public string Name
        {
            get { return _name; }
            set { _name = value; }
        }        
    }
    
    public class UsableItem : Example.Item
    {
        private int _price;
        private int _quantity;

        public UsableItem(string name, int price, int quantity) : base(name)
        {
            _price = price;
            _quantity = quantity;
        }

        public int Price
        {
            get { return _price; }
            set { _price = value; }
        }

        public int Quantity
        {
            get { return _quantity; }
            set { _quantity = value; }
        }

        public int TotalPrice()
        {
            return Price * Quantity;
        }
    }

    public class Weapon : Example.Item
    {
        public Weapon(string name, int attackRate) : base(name)
        {
            _attackRate = attackRate;
        }
        
        private int _attackRate;

        public int AttackRate
        {
            get {return _attackRate;}
            set { _attackRate = value; }
        }

        public void WeaponEnforcement()
        {
            AttackRate *= 2;
        }
    }            
    
    public class Player<T>
    {
        private T[] _inv;
        private bool _isCustomer;
        private int _count;
        
        // 30개 넘 많으니까 5개로,, 무료유저는 5개만
        public const int FreeMaxCount = 5;
        
        public Player()
        {
            _inv = new T[FreeMaxCount];
        }
        
        public string Name
        {
            get;
            set;
        }

        public uint Money
        {
            get;
            set;
        }

        public T[] Inventory
        {
            get { return _inv; }
        }
        
        public int Count
        {
            get { return _count; }
            private set { _count = value; }
        }
        
        public T this [int i]
        {
            get { return _inv[i];}
            set { _inv[i] = value; }
        }

        public bool IsCustomer
        {
            get { return _isCustomer;}
            set {  _isCustomer = value; }
        }

        // 결제 태도가 좋은지 설정
        public void WhenPayMoney()
        {
            IsCustomer = true;
        }

        public void AddItem(T item)
        {
            // 결재 태도가 바르지 못한 친구들은 가세요라
            if(IsCustomer == false && Count >= FreeMaxCount)
            {
                Console.WriteLine("창고가 꽉 찼습니다. 결제하세요");
                return;
            }
            
            if (IsCustomer && Count >= _inv.Length)
            {
                Array.Resize(ref _inv, _inv.Length * 2);
            }
            
            Inventory[Count++] = item;
        }
        
        // 전체 출력
        public void ShowInventory()
        {
            foreach (T i in Inventory)
            {
                if (i is UsableItem)
                {
                    PrintUsableItem(i as UsableItem);
                }

                if (i is Weapon)
                {
                    PrintWeapon(i as Weapon);
                }
            }
        }

        // 1번이면 무기 2번이면 아이템 출력
        private void ShowInventory(Item targetItem, int choice)
        {
            if (targetItem is Weapon && choice == 1)
            {
                PrintWeapon(targetItem as Weapon);
            }

            if (targetItem is UsableItem && choice == 2)
            {
                PrintUsableItem(targetItem as UsableItem);
            }
        }
        
        public void UseMyItem()
        {
            bool printTitle = true;
            Console.WriteLine("무기 목록은 1번, 사용 가능 아이템은 2번을 눌러주세요");
            ConsoleKeyInfo keyInfo = Console.ReadKey();
            Console.WriteLine();

            foreach (T someThing in Inventory)
            {
                if (!(someThing is Item))
                {
                    continue;
                }

                Item item = someThing as Item;
                
                if(keyInfo.Key == ConsoleKey.D1 && printTitle)
                {
                    printTitle = false;
                    Console.WriteLine("아이템명\t공격력");                    
                }
                else if(keyInfo.Key == ConsoleKey.D2 && printTitle)
                {
                    printTitle = false;
                    Console.WriteLine("아이템명\t가격\t갯수\t합계");                        
                }
                ShowInventory(item, int.Parse(keyInfo.KeyChar.ToString()));
            }
        }
    }
    
    public static void PrintUsableItem(UsableItem ui)
    {
        Console.WriteLine($"{ui.Name}\t{ui.Price}\t{ui.Quantity}\t{ui.TotalPrice()}");
    }
    
    public static void PrintWeapon(Weapon w)
    {
        Console.WriteLine($"{w.Name}\t{w.AttackRate}");        
    }
} 

Main.cs

Example.Player<Example.Item> player = new Example.Player<Example.Item>();
player.WhenPayMoney();

for (int i = 0; i < 10; i++)
{
    Example.Item usable = new Example.UsableItem("좋은아이템"+i, i*100, i);
    Example.Item weapon = new Example.Weapon("좋은무기"+(10-i), (10-i)*100);
    player.AddItem(usable);
    player.AddItem(weapon);
}

player.ShowInventory();
player.UseMyItem();

// 무기 갖고와서 강화 & 확인 & 출력
if (player.Inventory[1] is Example.Weapon)
{
    Example.Weapon tempWeapon = player.Inventory[1] as Example.Weapon;
    tempWeapon?.WeaponEnforcement();
    tempWeapon?.WeaponEnforcement();
    Example.PrintWeapon(tempWeapon);
}

구현할때 생각한점..?

  • Player의 인벤토리에 뭐가 들어갈지 모르니까, Player로 생성하여 안에 T값을 인벤토리에 전달하도록 수정.
  • ShowInventory를 다형성으로 구현해서 인자없이 넣으면 인벤토리 전체를 출력, 인자를 Item과 1,2번에 나눠서 받으면 해당 아이템이 UsableItem인지, Weapon인지 골라서 출력한다
    • ShowInventory에 출력하는 내용들을 제네릭에 따라 변경해 줘야하다보니, 중복되는 코드가 많다.
    • 따라서 PrintUsableItem과 PrintWeapon을 static 메서드로 뽑아서 각각 UsableItem을 넘기거나 Weapon을 받아서 호출 할 수 있게 바꿨다.

심화 문제 1

  • Gun이라는 클래스와 Projectile이라는 클래스를 만들겠습니다.
  • 그리고 Projectile 클래스를 상속받는 Bullet클래스와 Grenade 클래스를 만듭니다.
  • Grenade와 Bullet클래스에는 각각 필드로 int _damage 하나만 담아 두고, Gun 클래스에서 Bullet들을 담을 수 있는 List, Grenade들을 담을 수 있는 List, 총 두개의 리스트를 맴버변수로 가지게 합니다.

1-2

  • Gun의 생성자를 만들어서 해당 두 컨테이너들을 뉴할당 시켜줍니다.
  • 마지막으로, Gun에 컨테이너를 하나 더 추가하겠습니다.
  • 키값으로는 string을 받고, 내용으로는 방금 만들어진 List 컨테이너를 담는 Dictionary 필드를 만들어 주시기 바랍니다.

1-3

  • Gun의 생성자를 수정합니다.
  • 방금 만들어진 딕셔너리를 뉴할당 하고, Bullet을 담은 List와 Grenade를 담은 List 둘을 각각 “Bullet”, “Grenade” 이라는 키값으로 저장시켜주시기 바랍니다.
  • 이를 위해 리팩토링을 진행해주시길 바랍니다

1-4

  • Gun에 메소드를 하나 더 추가하겠습니다.
  • “Bullet” 이라는 인자값을 넘겨주면 총알이 담긴 리스트를 반환 받고, “Grenade”라는 인자값을 넘겨주면 수류탄이 담긴 리스트를 반환 받는 메소드를 작성하여 주세요

Gun.cs

public class Gun
{
    // 이것도 필요없잖아..
    // private List<Bullet> _bullets;
    // private List<Grenade> _grenade;
    private Dictionary<string, List<Projectile>> _dict; 
        
    public Gun()
    {
        List<Projectile> tempBullets = new List<Projectile>();
        List<Projectile> tempGrenade = new List<Projectile>();

        _dict = new Dictionary<string, List<Projectile>>
        {
            { "Bullet", tempBullets }, { "Grenade", tempGrenade }
        };

        // 형변환이 안되는디....
        // _bullets = (List<Bullet>)tempBullets;
        // _grenade = (List<Grenade>)tempGrenade;
    }

    public List<Bullet> GetBullet()
    {
        List<Bullet> tempBullets = new List<Bullet>();
        List<Projectile> tempDictValue = _dict["Bullet"];
        foreach (Projectile p in tempDictValue)
        {
            if (p is Bullet)
            {
                tempBullets.Add((Bullet)p);                    
            }
        }
        return tempBullets;
    }
    
    public List<Grenade> GetGrenade()
    {
        List<Grenade> tempGrenades = new List<Grenade>();
        List<Projectile> tempDictValue = _dict["Grenade"];
        foreach (Projectile p in tempDictValue)
        {
            if (p is Grenade)
            {
                tempGrenades.Add((Grenade)p);                    
            }
        }            
        return tempGrenades;
    }
    
    // ConvertAll 이용한 깔끔 버전?
    // 문제, 실수로 dict에 bullet에 Grenade를 넣으면 타입 캐스트 익셉션이 발생, 해결 할수가 있나?
    public List<Bullet> GetBulletV2()
    {
        List<Projectile> tempDictValue = _dict["Bullet"];
        List<Bullet> bullets = tempDictValue.ConvertAll(ConvertToDerived<Bullet>);
        return bullets;
    }
    
    public List<Grenade> GetGrenadesV2()
    {
        List<Projectile> tempDictValue = _dict["Grenade"];
        List<Grenade> grenades = tempDictValue.ConvertAll(ConvertToDerived<Grenade>);
        return grenades;
    }
    
    public static Projectile ConvertToParent<T>(T someProj)
    {
        if (someProj is Bullet)
        {
            return (Projectile)(someProj as Bullet);
        }

        if (someProj is Grenade)
        {
            return (Projectile)(someProj as Grenade);
        }

        // 없으면 널
        return null;
    }        
    
    // https://learn.microsoft.com/ko-kr/dotnet/api/system.converter-2?view=net-8.0
    // https://learn.microsoft.com/ko-kr/dotnet/csharp/programming-guide/generics/generic-methods
    private static T ConvertToDerived<T> (Projectile someProj) where T : Projectile
    {
        if (someProj is Bullet || someProj is Grenade)
        {
            return (T)someProj;
        }
        // 없으면 널
        return null;
    }        

    public List<Projectile> GetProjectileListByKey(string key)
    {
        if (_dict.ContainsKey(key))
        {
            return _dict[key];
        }

        Console.WriteLine("해당하는 키가 없습니다");
        return null;
    }
}

// 발사체
public abstract class Projectile
{
    protected int _damage;
    public abstract int Damage
    {
        get;
        set;
    }
    public abstract void PrintDamage();
}
public class Bullet : Projectile
{
    public override int Damage
    {
        get { return _damage; }
        set { _damage = value;  }
    }

    public override void PrintDamage()
    {
        Console.WriteLine("총알 데미지 : "+ Damage);
    }
}

public class Grenade : Projectile
{
    public override int Damage
    {
        get { return _damage; }
        set { _damage = value;  }
    }
    public override void PrintDamage()
    {
        Console.WriteLine("수류탄 데미지 : "+ Damage);
    }        
}

Main.cs

Gun gun = new Gun();
// 예제 쉽게 만들기 위해 추가
List<Projectile> bulletsList = gun.GetProjectileListByKey("Bullet");
List<Projectile> grenadeList = gun.GetProjectileListByKey("Grenade");

bulletsList.Add(new Bullet{Damage = 20});
bulletsList.Add(new Bullet{Damage = 30});
bulletsList.Add(new Bullet{Damage = 40});

grenadeList.Add(new Grenade{Damage = 10});
grenadeList.Add(new Grenade{Damage = 30});
grenadeList.Add(new Grenade{Damage = 50});

foreach (var b in gun.GetBullet())
{
    b.PrintDamage();
}

foreach (var b2 in gun.GetBulletV2())
{
    b2.PrintDamage();
}

foreach (var g2 in gun.GetGrenadesV2())
{
    g2.PrintDamage();
}

구현하면서 생각해본 내용들

  • Projectile -> 발사체를 추상 클래스로 만들고, 추상 프로퍼티, 프린트 메소드를 추상 메소드로 선언
  • 형변환 코드를 수동으로 구현 -> 리스트에 담기는것은 자동으로 다운캐스팅 / 업캐스팅이 안된다.
    • 좀 더 찾아보니 ConvertAll 이라는 메서드가 리스트에 구현되어있어, 그걸 쓰는것으로 구현함.
    • v2 버전으로 바꾸고 난 후? 깔끔은 한데 이런식으로 쓰는지? -> 선생님의 답변은 ConvertAll는 코스트가 많이 든다.
      • (아마도 새 배열을 만들고 복사하는것 외에도 연산이 추가적으로 있으니까?)
      • 게임 쪽에서는 v1 버전으로 하는것을 추천

심화 2 Queue 활용

  • Milk라는 클래스와 VendingMachine 이라는 클래스를 하나 만들겠습니다.
  • Milk의 맴버변수로, 유통기한을 나타내는 int를 작성해주시기 바랍니다.
  • VendingMachine 클래스에는 Queue를 활용하여 Milk를 담을 수 있는 컨테이너를 필드로 작성하여 주시기 바랍니다.
  • 벤딩머신의 메소드로, 우유를 집어넣는 코드와 우유를 꺼내는 기능을 작성하되, 꺼낼때는 콘솔에 유통기한 및 큐에 남아있는 갯수를 출력하는 기능을 작성해주시기 바랍니다.
  • 갯수가 0일때 우유를 꺼내는 기능을 호출하게 되면 꺼내는 대신 다른 멘트가 나오게끔 작성해주시길 바랍니다.

Milk.cs

public class Milk
{
    public int ExpiredDate
    {
        get;
        set;
    }

    public void ShowExpiredDate()
    {
        Console.WriteLine("우유의 유통기한 : " + ExpiredDate + "일");
    }
}

public class VendingMachine
{
    private Queue<Milk> _milks;

    public Queue<Milk> Milks
    {
        get { return _milks; }
    }

    public VendingMachine()
    {
        _milks = new Queue<Milk>();
    }

    public void InsertMilk(Milk milk)
    {
        _milks.Enqueue(milk);
    }

    public Milk GetMilk()
    {
        if (_milks.Count == 0)
        {
            Console.WriteLine("우유가 다 떨어졌습니다");
            return null;
        }
        
        Milk mymilk = _milks.Dequeue();
        mymilk.ShowExpiredDate();
        Console.WriteLine("===남은 우유 갯수 : " + _milks.Count + "===");
        
        return mymilk;
    }
}

Main.cs

// 심화 2
VendingMachine vm = new VendingMachine();
// 밀크 없을때 출력 여부
Milk m0 = vm.GetMilk();

// 널은 출력이 안되나봄.. 다행인가!?
Console.WriteLine(m0);

for (int i = 0; i < 5; i++)
{
    vm.InsertMilk(new Milk{ExpiredDate = i});
}

Milk m1 = vm.GetMilk();

// 그냥 남은 우유 출력해보기
foreach (var m in vm.Milks)
{
    m.ShowExpiredDate();
    // vm.GetMilk(); 요거는 출력 하는 중에 빠지기 때문에 안됨
    // foreach 가 돌고있는 을 수정하려고 하면 불변 객체이기 때문에 안된다고함.
}

while (vm.GetMilk() != null)
{
    // Q1. 다 떨어질때까지는 이런식으로 해야되나?
}

질문사항 정리

  • forEach에서는? in 이 돌때 안의 대상값을 const처럼 바꾼다. // 익셉션도 컬렉션이 수정되었습니다 형태로 나옴
  • for 에서는..? 먼가 다른 이유때문에 터지겠지? 렝스가 고정일텐데.. 수정한다거나? 먼가 잘못짠다거나
  • 큐나 스택에서 while을 돌려서 겟 밀크 하는것처럼 짜는것은?
    • 굳이 넣고 빼는것을 제한한건 이유가 있어서일텐데, 자료 구조형을 맞게 썼는지 먼저 판단이 필요할것같다.

심화 3 플레잉 카드 / 자료구조 구현

  • Shape라는 Enum을 만들어서 Spade, Heart, Clover, Diamond 네 가지를 요소로 가지게 합니다.
  • Card라는 클래스를 만듭니다.
  • 위 열거형을 Card클래스 맴버변수로 가지게 해주세요.
  • Card클래스에는 숫자를 나타내는 int형을 넣되, a는 1, J는 11, Q는 12, K는 13이라고 가정하겠습니다.

  • CardDeck이라는 클래스도 하나 제작하도록 하겠습니다.
  • Card 객체를 52개 담을 수 있는 ???(자료구조) 들을 활용하여 두 개 만드시되, 하나는 unusedCards, 하나는 usedCards 의 이름으로 만들겠습니다.

  • CardDeck 클래스의 생성자에 unusedCards 속 52개의 Card를 중복없이 뉴할당해서 넣어주는 코드를 작성하여 주시기 바랍니다.
  • 카드를 섞는 메소드를 작성하거나 생성자에서 바로 진행해주시기 바랍니다.

  • CardDeck 클래스에 아직 쓰지 않은 맨 위 카드를 보기만 하는 ShowTopCard 기능을 제작하여 주시기 바랍니다.
  • 이는 맨 상단의 카드를 보기만 할 뿐 카드를 소모하진 않습니다. DrawCard라는 메소드도 하나 제작하겠습니다.
  • 이 메소드는 아직 쓰지 않은 덱의 맨 위 카드를 반환받고 그와 동시에 usedCard에 쌓는 기능을 수행합니다.

Card.cs

    public enum Shape{Spade, Heart, Clover, Diamond, End}
    public enum CharNumber{A=1, J=11, Q, K, End}
    public class Card
    {
        private Shape _shape;
        private int _number;

        public Shape Shape
        {
            get { return _shape; }
        }

        public int Number
        {
            get { return _number; }
        }

        public Card(Shape shape, int number)
        {
            _shape = shape;
            _number = number;
        }
        
        public void PrintCard()
        {
            CharNumber charNumber;
            bool parsedNum = Enum.TryParse(Number.ToString(), out charNumber);
            Console.WriteLine($"Shape:{Shape}\tNumber:{(parsedNum ? charNumber.ToString() : Number.ToString())}");
        }        
    }

    public class CardDeck
    {
        public const int MaxCardNum = 52;
        private Stack<Card> _unusedCards;
        private Stack<Card> _usedCard;

        public CardDeck()
        {
            _unusedCards = new Stack<Card>();
            _usedCard = new Stack<Card>();
        }

        public Stack<Card> UnusedCards
        {
            get { return _unusedCards; }
        }
        
        public Stack<Card> UsedCards
        {
            get { return _usedCard; }
        }

        public List<Card> MakeInitDeck()
        {
            List<Card> initDeck = new List<Card>();
            for (int i = 0; i < (int)Shape.End; i++)
            {
                for (int j = 1; j < (int)CharNumber.End; j++)
                {
                    initDeck.Add(new Card((Shape)i, j));
                }
            }

            return initDeck;
        }
        
        public void ShuffleToStack(List<Card> initDeck)
        {
            Random random = new Random();
            while (initDeck.Count > 0)
            {
                // 랜덤으로 카운트에 있는 인덱스 카드를 찾아서 스택으로 푸쉬해버림
                int randomCard = random.Next(initDeck.Count);
                Card tempCard = initDeck[randomCard];
                initDeck.RemoveAt(randomCard);
                UnusedCards.Push(tempCard);
            }
        }

        public Card ShowTopCard()
        {
            return UnusedCards.Peek();
        }

        public Card DrawCard()
        {
            Card drawCard = UnusedCards.Pop();
            UsedCards.Push(drawCard);
            return drawCard;
        }
    }

Main.cs

// 심화 3
CardDeck cardDeck = new CardDeck();
cardDeck.ShuffleToStack(cardDeck.MakeInitDeck());

// 잘 됐는지 테스트
cardDeck.ShowTopCard().PrintCard(); // 맨 윗장 peek
cardDeck.DrawCard().PrintCard(); // 맨 윗장 pop 후 버리는 stack에 push
cardDeck.UnusedCards.Peek().PrintCard(); // 다음장 카드 확인
cardDeck.UsedCards.Peek().PrintCard(); // 버린 stack의 카드 확인
  • 마작 구현하면서 해본것들이라 어렵지는 않았지만 자료구조형 리팩토링 할때 참고해서 쓰면 좋을듯?
  • 셔플은 그냥 대충 구현함.. 성능으로 따지면 Object를 쓰지않고 배열 쓰는 마작버전이 더 좋을듯
// Fisher–Yates shuffle
// https://stackoverflow.com/questions/108819/best-way-to-randomize-an-array-with-net
public static void ShuffleDeck(Tiles.Tile[] tiles)
{
    int n = tiles.Length;
    Random random = new Random();
    while (n > 1)
    {
        int k = random.Next(n--);
        Tiles.Tile temp = tiles[n];
        tiles[n] = tiles[k];
        tiles[k] = temp;
    }
}