[TIL] 5일 차 - C# 강의 정리 및 문제 풀이

ChangBeom·2025년 1월 31일

TIL

목록 보기
6/53
post-thumbnail

[내용 정리]

<다중 상속을 사용하지 않는 이유>

  • <다이아몬드 문제 (Diamond Problem)>

    다중 상속을 허용하면 한 클래스가 두 개 이상의 부모 클래스로부터 동일한 멤버를 상속받을 수 있다. 이 경우, 같은 이름의 멤버를 가지고 있을 때 어떤 부모 클래스의 멤버를 사용해야 하는지 모호해진다. 그래서 이런 모호성을 해결하기 위한 규칙이 필요하게 되는데, 이로 인해 코드가 복잡해지고 가독성이 저하될 수 밖에 없다.

  • <설계의 복잡성 증가>

    다중 상속을 허용하면 클래스 간의 관계가 복잡해진다. 클래스가 다중 상속을 받을 경우, 어떤 클래스로부터 어떤 멤버를 상속받을지 결정해야한다. 이로 인해 클래스 간의 상속 관계를 파악하기 어려워지고 코드의 유지 보수성이 저하될 수 있다.

  • <이름 충돌과 충돌 해결의 어려움>

    다중 상속을 허용하면 여러 부모 클래스로부터 상속받은 멤버들의 이름이 충돌할 수 있다. 이러한 충돌을 해결하기 위해 충돌하는 멤버를 재정의해야 하거나 명시적으로 부모 클래스를 지정해야 할 수도 있다. 이는 코드의 복잡성을 증가시키고 오류 발생 가능성을 높인다.

  • <설계의 복잡성 증가>

    C#은 단일 상속을 통해 설계의 일관성과 단순성을 유지한다. 단일 상속을 통해 클래스 간의 관계를 명확하게 만들고 코드의 가독성과 이해도를 높일 수 있다. 또한 인터페이스를 사용하여 다중 상속이 필요한 경우에도 유사하게 구현할 수 있다.


<인터페이스를 사용하는 이유>

1. 코드의 재사용성

인터페이스를 사용하면 다른 클래스에서 해당 인터페이스를 구현하여 동일한 기능을 공유할 수 있다. 인터페이스를 통해 다양한 클래스가 동일한 동작을 수행할 수 있으므로 코드의 재사용성이 향상된다.

2. 다중 상속 제공

C#에서는 클래스는 단일 상속만을 지원하지만, 인터페이스는 다중 상속을 지원한다. 클래스가 여러 인터페이스를 구현함으로써 여러 개의 기능을 조합할 수 있다. 다중 상속을 통해 클래스는 더 다양한 동작을 수행할 수 있다.

3. 유연한 설계

인터페이스를 사용하면 클래스와 인터페이스 간에 느슨한 결합을 형성할 수 있다. 클래스는 인터페이스를 구현하기만 하면 되므로, 클래스의 내부 구현에 대한 변경 없이 인터페이스의 동작을 변경하거나 새로운 인터페이스를 추가할 수 있다. 이는 유연하고 확장 가능한 소프트웨어 설계를 가능하게 한다.


<인터페이스>

  • 인터페이스란?

    클래스가 구현해야 하는 멤버들을 정의하는 것 즉, 선언부로만 이루어진 유사 클래스이다.

  • 인터페이스의 특징

    • 인터페이스는 클래스의 일종이 아니며, 클래스에 대한 제약 조건을 명시하는 것이다.
    • 클래스가 인터페이스를 구현할 경우, 모든 인터페이스 멤버를 구현해야 한다.
    • 인터페이스는 다중 상속을 지원한다.

<예시> : 인터페이스 구현


//	인터페이스 및 멤버 정의
interface IMyInterface
{
	void Method1();
    int Method2(string str);
}

// 인터페이스 구현
class MyClass : IMyInterface
{
	public void Method1()
    {
    	//	Method1 기능 구현
    }

	public void Method2(string str)
    {
    	//	Method2 기능 구현
        return 0;
    }
}

<예시> : 다중 상속


//	인터페이스 1
public interface IItemPickable
{
	void PickUp();
}

//	인터페이스 2
public interface IDroppable
{
	void Drop();
}

//	아이템 클래스
public class Item : IItemPickable, IDroppable
{
	public string Name { get; set; }

    public void PickUp()
    {
    	Console.WriteLine($"아이템 {Name}을 주웠습니다.");
    }

    public void Drop()
    {
    	Console.WriteLine($"아이템 {Name}을 버렸습니다.");
    }
}

//	플레이어 클래스
public class Player
{
	public void InteracWithItem(IItemPickable item)
    {
    	item.PickUp();
    }

    public void DropItem(IDroppable item)
    {
    	item.Drop();
    }
}

//	게임 실행
static void Main()
{
	Player player = new Player();
    Item item = new Item { Name = "Sword" };

    //	아이템을 주울 수 있음
    player.InteracWithItem(item);

    //	아이템을 버릴 수 있음
    player.DropItem(item);
}

인터페이스 vs 추상클래스

  • 인터페이스의 장단점
    • 추상적인 동작만 정의
    • 다중 상속이 가능
    • 코드의 재사용성과 확장성을 향상
    • 클래스들 간의 결합도를 낮추고, 유연한 상호작용을 가능하게 함.
    • 인터페이스를 구현하는 클래스가 모든 동작을 구현해야 한다는 의무를 가지기 때문에 작업량이 증가할 수 있다는 단점이 존재
  • 추상 클래스의 장단점
    • 일부 동작의 구현을 가지며, 추상 메소드를 포함할 수 있다.
    • 단일 상속만 가능
    • 구현된 동작을 가지고 있기 때문에 하위 클래스에서 재정의하지 않아도 될 경우 유용
    • 다중 상속이 불가능하고 상속을 통해 밀접하게 결합된 클래스들을 형성하므로 유연성이 제한될 수 있음.

<열거형>

  • <열거형이란?>
    서로 관련된 상수들의 집합을 뜻한다. 각 상수는 정수 값으로 지정된다는 특징을 가지고 있다.

  • <사용하는 이유>

    1. 가독성
     열거형을 사용하면 일련의 연관된 상수들을 명명할 수 있다. 이를 통해 코드의 가독성이 향상되고, 상수를 사용할 때 실수로 잘못된 값을 할당하는 것을 방지할 수 있음.

    2. 자기 문서화 (Self-documenting)
     열거형은 의미 있는 이름을 사용하여 상수를 명명할 수 있다. 이를 통해 코드의 가독성이 향상되며, 상수의 의미를 명확하게 설명할 수 있음.

    3. 스위치 문과의 호환성
     열거형은 스위치 문과 함께 사용될 때 유용하다. 열거형을 사용하면 스위치 문에서 다양한 상수 값에 대한 분기를 쉽게 작성할 수 있다.

<예시> : 열거형 정의

enum MyEnum
{
	Value1,
    Value2,
    Value3 = 20	//	상수 값 지정 가능
}

<예시> : 열거형 사용

MyEnum myEnum = MyEnum.Value1;

<예시> : 열거형 형변환

int intValue = (int)MyEnum.Value1;	//	열거형 -> 정수
MyEnum enumValue = (MyEnum)intValue;	//	정수 -> 열거형

<예시> : 스위치문 사용 예제

switch(enumValue)
{
	case MyEnum.Value1:
    	//	Value1에 대한 처리
        break;
    case MyEnum.Value2:
    	//	Value2에 대한 처리
        break;
    case MyEnum.Value3:
    	//	Value3에 대한 처리
        break;
    default:
    	//	기본 처리
        brek;       

<예외 처리>

<예외란?>
프로그램 실행 중에 발생하는 예기치 않은 상황을 뜻한다.

<예외 처리의 필요성과 장점>

  • 예외 처리는 예외 상황에 대비하여 프로그램을 안정적으로 유지하는 데 도움을 준다.
  • 예외 처리를 통해 오류 상황을 적절히 처리하고, 프로그램의 실행을 계속할 수 있다.
  • 예외 처리는 프로그램의 디버깅을 용이하게 한다.

<예시> : 예외 처리 구현

try
{
	//	예외가 발생할 수 있는 코드
}
catch (ExceptionType1 ex)
{
	//	ExceptioniType1에 해당하는 예외 처리
}
catch (ExceptionType2 ex)
{
	//	ExceptioniType2에 해당하는 예외 처리
}
finally
{
	//	예외 발생 여부와 상관없이 항상 실행되는 코드
}
  • 여러 개의 catch 블록을 사용하여 다양한 예외 타입을 처리할 수 있다.
  • 다중 catch 블록을 사용하면 각각의 예외 타입에 따라 다른 예외 처리 코드를 작성할 수 있다.
  • catch 블록은 위에서부터 순서대로 실행되며, 예외 타입에 해당하는 첫 번째 catch 블록이 실행된다.
  • 예외 타입은 상속 관계에 있는 경우 상위 예외 타입의 catch 블록이 먼저 실행된다.
  • catch 블록에서는 예외 객체를 사용하여 예외에 대한 정보를 액세스할 수 있다. 예외 객체를 사용하여 예외의 타입, 메시지 등을 확인하고 처리할 수 있다.

<예시> : 사용자 정의 예외 처리

public class NegativeNumberException : Exception
{
	public NegativeNumberExceoption(string message) : base(message)
    {
    }
}

try
{
	int number = -10;
    if(number < 0)
    {
    	throw new NegativeNumberException("음수는 처리할 수 없습니다.");
    }
}
catch (NegativeNumberException ex)
{
	Console.WriteLine(ex.Message);
}
catch (Exception ex)
{
	Console.WriteLine("예외가 발생했습니다: " + ex.Message);
}

<박싱과 언박싱>

박싱 (Boxing)

  • 값형을 참조형으로 변환하는 과정
  • 값형 변수의 값을 메모리의 힙 영역에 할당된 객체로 래필
  • 박싱을 통해 값형이 참조형의 특징을 갖게 되며, 참조형 변수로 다뤄질 수 있다.
  • 박싱된 값형은 참조로 전달되므로 메모리 오버헤드가 발생할 수 있다.

언박싱 (Unboxing)

  • 박싱된 객체를 다시 값형으로 변환하는 과정
  • 박싱된 객체에서 값을 추출하여 값형 변수에 할당
  • 언박싱은 명시적으로 타입 캐스팅을 해야 하며, 런타임에서 타입 검사가 이루어진다.
  • 잘못된 형식으로 언박싱하면 런타임 에러가 발생할 수 있다.

특징

  • 박싱과 언박싱은 값형과 참조형 사이의 변환 작업이므로 성능에 영향을 미칠 수 있다. 반복적인 박싱과 언박싱은 성능 저하를 초래할 수 있다.
  • 박싱된 객체는 힙 영역에 할당되므로 가비지 컬렉션의 대상이 될 수 있다. 따라서 메모리 관리에 유의해야 한다.
  • 박싱된 객체와 원래의 값형은 서로 독립적이므로 값을 수정하더라도 상호간에 영향을 주지 않는다.

object는 .NET Common Type System (CTS)의 일부이며, 모든 클래스의 직간접적인 상위 클래스이다. 모든 클래스는 object에서 상속되며, object는 모든 형식을 참조할 수 있는 포괄적인 타입이다.

<예시> : 박싱과 언박싱

using System

class Program
{
	static void Main()
    {
    	//	값형
        int x = 10;
        int y = x;
        y = 20;
        Console.WriteLine("x = " + x);	//	출력 결과 : x = 10
        Console.WriteLine("y = " + y);	//	출력 결과 : y = 20

        //	참조형
        int[] arr1 = new int[] { 1, 2, 3 };
        int[] arr2 = arr1;
        arr2[0] = 4;
        Console.WriteLine("arr1[0] = " + arr1[0]);	//	출력 결과 : arr1[0] = 4
        Console.WriteLine("arr2[0] = " + arr2[0]);	//	출력 결과 : arr2[0] = 4

        //	박싱과 언박싱
        int num1 = 10;
        object obj = num1;	//	박싱
        int num2 = (int)obj;	//	언박싱
        Console.WriteLine("num1 = " + num1);	//	출력 결과 : num1 = 10
        Console.WriteLine("num2 = " + num2);	//	출력 결과 : num2 = 10
    }
}

<델리게이트 (Delegate)>

<델리게이트란?>

메소드를 참조하는 타입. 다른 프로그래밍 언어에서는 함수포인터라는 용어를 사용하기도 한다. 델리게이트를 이용하면 메소드를 매개변수로 전달하거나 변수에 할당할 수 있다.

<예시> : 델리게이트 구현

delegate int Calculate(int x, int y);

static int Add(int x, int y)
{
	return x + y;
}

class Program
{
	static void Main()
    {
    	//	메소드 등록
        Calculate calc = Add;

        //	델리게이트 사용
        int result = calc(3, 5);
        Console.WriteLine("결과 : " + result);

<예시> : 하나 이상의 메소드 등록하기

delegate void MyDelegate(string message);

static void Method1(string message)
{
	Console.WriteLine("Method1 : " + message);
}

static void Method2(string message)
{
	Console.WriteLine("Method2 : " + message);
}

class Program
{
	static void Main()
    {
    	//	델리게이트 인스턴스 생성 및 메소드 등록
        MyDelegate myDelegate = Method1;
        myDelegate += Method2;

        //	델리게이트 호출
        myDelegate("Hello!");

        Console.ReadKey();
    }
}

<람다 (Lambda)>

<람다란?>

익명 메소드를 만드는 방법이다. 람다를 사용하면 메소드의 이름 없이 메소드를 만들 수 있다. 람다는 델리게이트를 사용하여 변수에 할당하거나, 메소드의 매개변수로 전달할 수 있다.

<예시> : 람다 사용 예제

using System;

delegate void MyDelegate(string message);

class Program
{
	static void Main()
    {
    	//	델리게이트 인스턴스 생성 및 람다식 할당
        MyDelegate myDelegate = (message) =>
        {
        	Console.WriteLine("람다식을 통해 전달된 메시지 : " + message);
        }

        //	델리게이트 호출
        myDelegate("안녕하세요!");

        Console.ReadKey();
    }
}

<Func과 Action>

<Func, Action이란?>

  • Func과 Action은 델리게이트를 대체하는 미리 정의된 제네릭 형식이다.
  • Func은 값을 반환하는 메소드를 나타내는 델리게이트이다. 마지막 제네릭 형식 매개변수는 반환 타입을 나타낸다. 예를 들어, Func<int, string>은 int를 입력으로 받아 string을 반환하는 메소드를 나타낸다.
  • Action은 값을 반환하지 않는 메소드를 나타내는 델리게이트이다. Action은 매개변수를 받아들이지만, 반환 타입이 없다. 예를 들어, Action<int, string>은 int와 string을 입력으로 받고, 아무런 값을 반환하지 않는 메소드를 나타낸다.
  • Func 및 Action은 제네릭 형식으로 미리 정의되어 있어 매개변수와 반환 타입을 간결하게 표현할 수 있다.

<예시> : Func 예제

//	Func을 사용하여 두 개의 정수를 더하는 메소드
int Add(int x, int y)
{
	return x + y;
}

//	Func을 이용한 메소드 호출
Func<int, int, int> addFunc = Add;
int result = addFunc(3,5);
Console.WriteLine("결과 : " + result);

<예시> : Action 예제

//	Action을 사용하여 문자열을 출력하는 메소드
void PrintMessage(string message)
{
	Console.WriteLine(message);
}

//	Action을 이용한 메소드 호출
Action<string> printAction = PrintMessage;
PrintAction("Hello, World!");

<LINQ (Language Integrated Query)

<LINQ란?>

  • .NET 프레임워크에서 제공되는 쿼리 언어 확장
  • 데이터 소스(예 : 컬렉션, 데이터베이스, XML 문서 등)에서 데이터를 쿼리하고 조작하는데 사용된다.
  • 데이터베이스 쿼리와 유사한 방식으로 데이터를 필터링, 정렬, 그룹화, 조인 등 다양한 작업을 수행할 수 있다.
  • LINQ는 객체, 데이터베이스, XML문서 등 다양한 데이터 소스를 지원한다.

<예시> : LINQ 구조

var result = from 변수 in 데이터 소스
			 [where 조건식]
             [orerby 정렬식 [, 정렬식...]]
             [select];
  • var 키워드는 결과 값의 자료형을 자동으로 추론한다.
  • from 절에서는 데이터 소스를 지정한다.
  • where 절은 선택적으로 사용하며, 조건식을 지정하여 데이터를 필터링한다.
  • orderby 절은 선택적으로 사용하며, 정렬 방식을 지정한다.
  • select 절은 선택적으로 사용하며, 조회할 데이터를 지정한다.

<예시> : LINQ 사용 예제

//	데이터 소스 정의 (컬렉션)
List<int> numbers = new List<int> { 1, 2, 3, 4, 5 };

//	쿼리 작성 (선언적인 구문)
var evenNumbers = from num in numbers
				  where num % 2 == 0
                  select num;                  

//	쿼리 실행 및 결과 처리
foreach (var num in evenNumbers)
{
	Console.WriteLine(num);
}

<Nullable 형>

<Nullable 형이란?>

  • Nullable은 C#에서 null 값을 가질 수 있는 값형에 대한 특별한 형식이다.
  • 기본적으로 값형은 null을 허용하지 않는다.
  • 값형 변수에 null 값을 지정할 수 있는 방법을 제공하여 값형이나 구조체를 사용하는 프로그램에서 null 상태를 나타낼 수 있다. 주로 값형 변수가 null인지 아닌지를 확인하고 처리해야 할 때 유용하다.
  • 형식은 ? 연산자를 사용하여 선언된다. 예를 들어, int?는 int 형식에 null을 할당할 수 있는 Nullable 형식을 나타낸다.

<예시> : Nullable 형 사용 예제

//	Nullable 형식 변수 선언
int? nullableInt = null;
double? nullableDouble = 3.14;
bool? nullabbleBool = true;

//	값 할당 및 접근
nullableInt = 10;
int intValue = nullableInt.Value;

//	null 값 검사
if(nullableDouble.HasValue)
{
	Console.WriteLine("nullableDouble 값 : " + nullableDouble.Value);
}
else
{
	Console.WriteLine("nullableDouble은 null입니다.");
}

//	null 병합 연산자 사용
//	nullableInt ?? 0과 같이 사용되며, nullableInt가 null이면 0을 반환한다.
int nonNullableInt = nullableInt ?? 0;
Console.WriteLine("nonNullableInt 값 " + nonNullableInt);

<문자열 빌더 (StringBuilder)>

<StringBuilder 란?>

  • 문자열 조작
    StringBuilder는 Append(), Insert(), Replace(), Remove() 등 다양한 메소드를 제공하여 문자열에 대한 추가, 삽입, 치환, 삭제 작업을 수행할 수 있다.

  • 가변성
    StringBuilder는 내부 버퍼를 사용하여 문자열 조작을 수행하므로 크기를 동적으로 조정할 수 있다. 따라서 문자열의 크기가 늘어나거나 줄어들어도 추가적인 메모리 할당이 발생하지 않는다.

  • 효율적인 메모리 관리
    문자열 조작 시 StringBuilder는 내부 버퍼를 사용하여 문자열을 조작하므로, 반복적인 문자열 조작 작업이 발생해도 메모리 할당 및 해제 오버헤드가 적다.

<주요 메소드>

  • Append : 문자열을 뒤에 추가
  • Insert : 문자열을 지정한 위치에 삽입
  • Remove : 지정한 위치에서 문자열을 제거
  • Replace : 문자열의 일부를 다른 문자열로 대체
  • Clear : StringBuilder의 내용을 모두 지움

<예시> : StringBuilder 사용 예제

StringBuilder sb = new StringBuilder();

//	문자열 추가
sb.Append("Hello");
sb.Append(" ");
sb.Append("World");

//	문자열 삽입
sb.Insert(5, ", ");

//	문자열 치환
sb.Replace("World", "C#");

//	문자열 삭제
sb.Remove(5, 2);

//	완성된 문자열 출력
string result = sb.ToString();
Console.WriteLine(result);

[1. 블랙잭 게임]

* 블랙잭 게임은 카지노에서 흔히 볼 수 있는 카드 게임 중 하나입니다. 이번 과제에서는 간단한 콘솔 기반의 블랙잭 게임을 C#으로 구현해 보도록 하겠습니다.

<요구사항>

  • 블랙잭 게임은 1명의 플레이어와 1명의 딜러가 참여합니다.
  • 게임 시작 시, 플레이어와 딜러는 각각 두 장의 카드를 받습니다.
  • 플레이어는 21점이 넘지 않는 한 계속해서 카드를 더 받을 수 있습니다.
  • 딜러는 카드 합이 17점이 되거나 넘을 때까지 계속해서 카드를 받아야 합니다.
  • 카드를 더 이상 받지 않는 플레이어와 딜러 중 카드 합이 21점에 더 가까운 쪽이 승리합니다. 21점을 초과하면 패배합니다.
  • Card, Deck, Hand, Player, Dealer, Blackjack 등의 클래스를 활용해 구현해야 합니다.
using System;
using System.Collections.Generic;
using System.Threading;

public enum Suit { Hearts, Diamonds, Clubs, Spades }
public enum Rank { Two = 2, Three, Four, Five, Six, Seven, Eight, Nine, Ten, Jack, Queen, King, Ace }

// 카드 한 장을 표현하는 클래스
public class Card
{
    public Suit Suit { get; private set; }
    public Rank Rank { get; private set; }

    public Card(Suit s, Rank r)
    {
        Suit = s;
        Rank = r;
    }

    public int GetValue()
    {
        if ((int)Rank <= 10)
        {
            return (int)Rank;
        }
        else if ((int)Rank <= 13)
        {
            return 10;
        }
        else
        {
            return 11;
        }
    }

    public override string ToString()
    {
        return $"{Rank} of {Suit}";
    }
}

// 덱을 표현하는 클래스
public class Deck
{
    private List<Card> cards;

    public Deck()
    {
        cards = new List<Card>();

        foreach (Suit s in Enum.GetValues(typeof(Suit)))
        {
            foreach (Rank r in Enum.GetValues(typeof(Rank)))
            {
                cards.Add(new Card(s, r));
            }
        }

        Shuffle();
    }

    public void Shuffle()
    {
        Random rand = new Random();

        for (int i = 0; i < cards.Count; i++)
        {
            int j = rand.Next(i, cards.Count);
            Card temp = cards[i];
            cards[i] = cards[j];
            cards[j] = temp;
        }
    }

    public Card DrawCard()
    {
        Card card = cards[0];
        cards.RemoveAt(0);
        return card;
    }
}

// 패를 표현하는 클래스
public class Hand
{
    private List<Card> cards;

    public Hand()
    {
        cards = new List<Card>();
    }

    public void AddCard(Card card)
    {
        cards.Add(card);
    }

    public int GetTotalValue()
    {
        int total = 0;
        int aceCount = 0;

        foreach (Card card in cards)
        {
            if (card.Rank == Rank.Ace)
            {
                aceCount++;
            }
            total += card.GetValue();
        }

        while (total > 21 && aceCount > 0)
        {
            total -= 10;
            aceCount--;
        }

        return total;
    }

    public int GetCards(int num)
    {
        return cards[num].GetValue();
    }
}

// 플레이어를 표현하는 클래스
public class Player
{
    public Hand Hand { get; private set; }

    public Player()
    {
        Hand = new Hand();
    }

    public Card DrawCardFromDeck(Deck deck)
    {
        Card drawnCard = deck.DrawCard();
        Hand.AddCard(drawnCard);
        return drawnCard;
    }
}

public class Dealer : Player
{
    public void DealerDraw(Deck deck)
    {
        while (true)
        {
            if(Hand.GetTotalValue() > 17)
            {
                break;
            }

            Card cardDraw = DrawCardFromDeck(deck);
            Console.WriteLine($"딜러가 뽑은 카드 : {cardDraw}   딜러의 총합 점수 : {Hand.GetTotalValue()}");
            Thread.Sleep(1000);
        }
    }
}

public class Blackjack
{
    private Player player;
    private Dealer dealer;
    private Deck deck;

    public void Play()
    {
        player = new Player();
        dealer = new Dealer();
        deck = new Deck();

        Console.WriteLine();

        for(int i = 0; i < 2; i++)
        {
            player.DrawCardFromDeck(deck);
            dealer.DrawCardFromDeck(deck);
        }

        Console.WriteLine($"플레이어의 첫번째 카드는 {player.Hand.GetCards(0)}, 두번째 카드는 {player.Hand.GetCards(1)} 입니다. 현재 플레이어 총합 : {player.Hand.GetTotalValue()}");
        Console.WriteLine($"딜러의 첫번째 카드는 {dealer.Hand.GetCards(0)}, 두번째 카드는 {dealer.Hand.GetCards(1)} 입니다. 현재 딜러 총합 : {dealer.Hand.GetTotalValue()}");
        Console.WriteLine();

        while(true){
            if(player.Hand.GetTotalValue() > 21)
            {
                Console.WriteLine($"플레이어의 점수가 {player.Hand.GetTotalValue()}점으로 21점을 초과하였습니다. 당신은 패배하였습니다.");
                Environment.Exit(0);
            }
            else if(player.Hand.GetTotalValue() == 21)
            {
                Console.WriteLine($"블랙잭(21점)입니다. 당신이 승리하였습니다!");
                Environment.Exit(0);
            }

            Console.WriteLine($"행동을 선택해주세요. 현재 점수 : {player.Hand.GetTotalValue()} (1 또는 2를 입력해주세요.)");
            Console.WriteLine("1. 카드뽑기");
            Console.WriteLine("2. 딜러에게 턴 넘기기");

            Console.WriteLine();
            Console.Write("행동 입력 : ");
            int cmd = int.Parse(Console.ReadLine());
            Console.WriteLine();

            if (cmd == 1)
            {
                Card playerDrawCard = player.DrawCardFromDeck(deck);
                Console.WriteLine($"{playerDrawCard}를 뽑았습니다.");
                Console.WriteLine();
            }
            else if (cmd == 2)
            {
                Console.WriteLine("플레이어의 턴을 종료합니다.");
                Console.WriteLine();
                break;
            }
        }

        Console.WriteLine("딜러의 턴입니다.");
        dealer.DealerDraw(deck);

        if(dealer.Hand.GetTotalValue() > 17)
        {
            Console.WriteLine("딜러의 점수가 17점이 넘었으므로 딜러의 턴을 종료합니다.");
            Console.WriteLine();
        }

        if(dealer.Hand.GetTotalValue() > 21)
        {
            Console.WriteLine($"딜러의 점수가 {dealer.Hand.GetTotalValue()}점으로 21점을 초과하였습니다. 당신은 승리하였습니다!");
        }
        else if(player.Hand.GetTotalValue() > dealer.Hand.GetTotalValue())
        {
            Console.WriteLine($"플레이어의 점수({player.Hand.GetTotalValue()})가 딜러의 점수({dealer.Hand.GetTotalValue()})보다 높으므로 당신은 승리하였습니다!");
        }
        else if(player.Hand.GetTotalValue() == dealer.Hand.GetTotalValue())
        {
            Console.WriteLine($"플레이어의 점수({player.Hand.GetTotalValue()})와 딜러의 점수({dealer.Hand.GetTotalValue()})가 같으므로 당신은 패배하였습니다.");
        }
        else
        {
            Console.WriteLine($"플레이어의 점수({player.Hand.GetTotalValue()})가 딜러의 점수({dealer.Hand.GetTotalValue()})보다 낮으므로 당신은 패배하였습니다.");
        }
    }
}

class Program
{
    static void Main(string[] args)
    {
        Console.WriteLine("**블랙잭 게임**");
        Console.WriteLine("카드를 뽑아 21점을 초과하지 않으면서 딜러보다 21점에 가까이 만들면 이기는 게임입니다.");
        Console.WriteLine();
        Console.WriteLine("지금부터 게임을 시작하겠습니다.");

        Blackjack blackjack = new Blackjack();
        blackjack.Play();
    }
}
  • 구현한 내용을 하나씩 살펴보자.

    1. Dealer 클래스의 DealerDraw(Deck deck) 함수는 딜러가 17점이 넘을 때 까지 카드를 뽑는 함수이다. 딜러가 카드를 뽑은 후 어떤 카드를 뽑았는지, 현재 딜러의 점수가 몇 점인지 알려주는 것이 포인트이다. 좀 더 디테일을 추가하기 위해 Thread.Sleep()을 통해 1초에 한 번씩 카드를 뽑도록 만들었다.

    2. Blackjack 클래스의 Play() 함수는 전반적인 게임의 흐름을 구현한 함수이다. 먼저 게임이 시작되면 요구사항에 따라 플레이어 1명, 딜러 1명 그리고 덱을 하나 생성해준다. 그리고 각각 시작 패로 2장씩 뽑는다. 이후 플레이어는 자신의 점수와 딜러의 점수를 토대로 행동을 선택할 수 있다. 1번은 카드를 한 장 뽑는 것이고, 2번은 딜러에게 턴을 넘기는 것이다. 플레이어는 점수가 21점이 되면 블랙잭이므로 바로 승리하며, 21점이 넘게 되면 바로 패배하게된다. 딜러의 턴에는 딜러가 카드를 뽑으며 딜러의 점수가 17점이 넘으면 딜러의 턴을 종료한 후 점수를 비교하여 승패를 가린다.


[2. 간단한 텍스트 RPG]

목표 : 기본적인 턴 기반 RPG 게임을 만들어 봅니다.

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

namespace Text_RPG
{
    internal class Program
    {
        //  캐릭터의 기본 틀이 되는 인터페이스
        public interface ICharacter
        {
            string Name { get; set; }
            public int MaxHealth { get; set; }
            int Health { get; set; }
            int Attack { get; set; }
            bool IsDead { get; set; }

            void TakeDamage(int damage);
        }

        //  ICharacter 인터페이스를 상속받아 만든 Warrior 클래스. 플레이어의 캐릭터이다.
        public class Warrior : ICharacter
        {
            public string Name { get; set; }

            public int MaxHealth { get; set; }
            public int Health { get; set; }
            public int Attack { get; set; }
            public bool IsDead { get; set; }

            public Warrior(string name)
            {
                Name = name;
                MaxHealth = 100;
                Health = 100;
                Attack = 10;
                IsDead = false;
            }

            //  플레이어가 공격을 받았을 때 처리하는 메소드
            public void TakeDamage(int damage)
            {
                Console.WriteLine($"{Name}님은 {damage}의 데미지를 받았습니다.");
                Health -= damage;

                if (Health <= 0)
                {
                    IsDead = true;
                }

                if (IsDead)
                {
                    Console.WriteLine($"{Name}님은 사망하셨습니다.");
                }
                else
                {
                    Console.WriteLine($"{Name}님의 현재 체력 : {Health}");
                }
            }
        }

        //  몬스터의 기본이 되는 클래스이다. 해당 클래스는 ICharacter 인터페이스를 상속받아서 구현했다.
        public class Monster : ICharacter
        {
            public string Name { get; set; }
            public int MaxHealth { get; set; }
            public int Health { get; set; }
            public int Attack { get; set; }
            public bool IsDead { get; set; }

            public Monster(string name)
            {
                Name = name;
                MaxHealth = 0;
                Health = 0;
                Attack = 0;
                IsDead = false;
            }

            //  몬스터가 공격받았을 때 처리하는 메소드
            public void TakeDamage(int damage)
            {
                Console.WriteLine($"{Name}은(는) {damage}의 데미지를 받았습니다.");
                Health -= damage;

                if (Health <= 0)
                {
                    IsDead = true;
                }

                if (IsDead)
                {
                    Console.WriteLine($"{Name}은(는) 죽었습니다.");
                }
                else
                {
                    Console.WriteLine($"{Name}의 현재 체력 : {Health}");
                }
            }
        }

        //  Monster 클래스를 상속받은 Goblin 클래스
        public class Goblin : Monster
        {
            public Goblin(string name) : base(name)
            {
                MaxHealth = 50;
                Health = 50;
                Attack = 8;
                IsDead = false;
            }
        }

        //  Monter 클래스를 상속받은 Dragon 클래스
        public class Dragon : Monster
        {
            public Dragon(string name) : base(name)
            {
                MaxHealth = 200;
                Health = 200;
                Attack = 15;
                IsDead = false;
            }
        }

        //  아이템의 기본 틀이 되는 인터페이스
        public interface IItem
        {
            string Name { get; set; }
            int Eff { get; set; }

            void Use(Warrior warrior);

            void Info();
        }

        // 체력이 늘어나는 아이템을 구현한 클래스이다. IItem 인터페이스를 상속 받았다.
        public class HealthPotion : IItem
        {
            public string Name { get; set; }
            public int Eff { get; set; }

            public HealthPotion()
            {
                Name = "체력 포션";
                Eff = 100;
            }

            //  아이템을 사용했을 때 처리하는 메소드
            public void Use(Warrior warrior)
            {
                Console.WriteLine($"{Name}을(를) 사용하셨습니다. 체력이 영구적으로 {Eff}만큼 증가합니다.");
                warrior.MaxHealth += Eff;
                Console.WriteLine($"현재 {warrior.Name}의 최대 체력은 {warrior.MaxHealth}입니다.");
                Console.WriteLine();
            }

            //  해당 아이템의 정보를 알려주는 메소드
            public void Info()
            {
                Console.WriteLine($"영구적으로 체력을 {Eff}만큼 증가시킨다.");
            }
        }

        // 공격력이 늘어나는 아이템을 구현한 클래스이다. IItem 인터페이스를 상속 받았다.
        public class StrengthPotion : IItem
        {
            public string Name { get; set; }
            public int Eff { get; set; }

            public StrengthPotion()
            {
                Name = "힘 포션";
                Eff = 20;
            }

            //  아이템을 사용했을 때 처리하는 메소드
            public void Use(Warrior warrior)
            {
                Console.WriteLine($"{Name}을(를) 사용하셨습니다. 공격력이 영구적으로 {Eff}만큼 상승합니다.");
                warrior.Attack += Eff;
                Console.WriteLine($"현재 {warrior.Name}의 공격력은 {warrior.Attack}입니다.");
                Console.WriteLine();
            }

            //  해당 아이템의 정보를 알려주는 메소드
            public void Info()
            {
                Console.WriteLine($"영구적으로 공격력을 {Eff}만큼 증가시킨다.");
            }
        }

        //  스테이지를 관리하는 클래스. 가장 핵심인 스테이지의 흐름을 제어하는 메소드가 포함 되어있다.
        public class Stage
        {
            private ICharacter Player;
            private ICharacter Monster;
            private List<IItem> RewardItem;

            public Stage(ICharacter player, ICharacter monster, List<IItem> rewardItem)
            {
                Player = player;
                Monster = monster;
                RewardItem = rewardItem;
            }

            //  스테이지의 흐름을 제어하는 메소드.
            public void Start()
            {
                Console.WriteLine("스테이지가 시작되었습니다!");
                Console.WriteLine();
                Console.WriteLine("**해당 스테이지 몬스터 정보**");
                Console.WriteLine();
                Console.WriteLine($"이름 : {Monster.Name}");
                Console.WriteLine($"체력 : {Monster.Health}");
                Console.WriteLine($"공격력 : {Monster.Attack}");
                Console.WriteLine();
                Console.WriteLine("******************************");
                Console.WriteLine();

                Console.WriteLine($"**현재 {Player.Name}의 정보**");
                Console.WriteLine();
                Console.WriteLine($"이름 : {Player.Name}");
                Console.WriteLine($"체력 : {Player.Health}");
                Console.WriteLine($"공격력 : {Player.Attack}");
                Console.WriteLine();
                Console.WriteLine("******************************");
                Console.WriteLine();

                while (true)
                {
                    if (Player.IsDead)
                    {
                        Console.WriteLine("당신은 패배하였습니다.");
                        break;
                    }

                    Console.WriteLine($"{Player.Name}의 턴입니다.");
                    Monster.TakeDamage(Player.Attack);
                    Console.WriteLine();

                    if (Monster.IsDead)
                    {
                        Console.WriteLine($"{Monster.Name}을(를) 처치하였습니다.");
                        Console.WriteLine($"스테이지를 클리어하였습니다!");
                        Console.WriteLine();
                        StageClear();
                        break;
                    }
                    else
                    {
                        Console.WriteLine($"{Monster.Name}의 현재 체력은 {Monster.Health}입니다.");
                        Console.WriteLine();
                        Console.WriteLine("--------------------------------------------------------");
                        Console.WriteLine();
                    }
                    Thread.Sleep(1000);

                    Console.WriteLine($"{Monster.Name}의 턴입니다.");
                    Player.TakeDamage(Monster.Attack);
                    Console.WriteLine();

                    if (Player.IsDead)
                    {
                        Console.WriteLine("당신은 패배하였습니다.");
                        break;
                    }
                    else
                    {
                        Console.WriteLine($"{Player.Name}의 현재 체력은 {Player.Health}입니다.");
                        Console.WriteLine();
                        Console.WriteLine("--------------------------------------------------------");
                        Console.WriteLine();
                    }
                    Thread.Sleep(1000);

                }
            }

            //  스테이지를 클리어 했을 때 처리하는 메소드. 스테이지 보상으로 아이템을 획득하고 사용하는 처리를 한다.
            public void StageClear()
            {
                Console.WriteLine("보상 목록입니다.");
                Console.WriteLine();

                for(int i = 0; i < RewardItem.Count; i++)
                {
                    Console.WriteLine($"{i+1}. {RewardItem[i].Name}");
                    Console.WriteLine("<정보>");
                    RewardItem[i].Info();
                    Console.WriteLine();
                }

                Console.Write("사용할 아이템의 번호를 눌러주세요. : ");
                int itemIndex = int.Parse(Console.ReadLine());

                Console.WriteLine($"{RewardItem[itemIndex-1].Name}을(를) 선택하였습니다.");

                RewardItem[itemIndex-1].Use((Warrior)Player);

                Player.Health = Player.MaxHealth;
            }
        }

        static void Main(string[] args)
        {
            //  Main문이 시작되면 플레이어, 고블린, 드래곤, 스테이지를 생성한다.

            Warrior player = new Warrior("플레이어");
            Goblin goblin = new Goblin("고블린");
            Dragon dragon = new Dragon("드래곤");

            //  스테이지 1은 플레이어, 고블린, 보상(1. 체력포션 2. 힘 포션)으로 진행된다.
            Stage stage01 = new Stage(player, goblin, new List<IItem> { new HealthPotion(), new StrengthPotion() });
            stage01.Start();

            //  만약 스테이지 1에서 플레이어가 죽었을 경우 게임을 끝내는 조건문
            if (player.IsDead)
            {
                Environment.Exit(0);
            }

            //  스테이지 2는 플레이어, 드래곤, 보상(1. 힘 포션 2. 체력 포션)으로 진행된다.
            Stage stage02 = new Stage(player, dragon, new List<IItem> { new HealthPotion(), new StrengthPotion() });
            stage02.Start();

            //  만약 스테이지 2에서 플레이어가 죽었을 경우 게임을 끝내는 조건문
            if (player.IsDead)
            {
                Environment.Exit(0);
            }

            Console.WriteLine("모든 스테이지를 클리어하셨습니다. 당신의 승리입니다.");
        }
    }
}
  • 코드가 길어서 전부 하나씩 설명하는 것은 비효율적인 것 같다. 주석을 통해 해당 부분이 무슨 역할을 구현한 것인지 확인하자!

0개의 댓글