오늘 배운 내용
- 3주차 과제 1 - 스네이크 게임
- 3주차 과제 2 - 블랙잭 게임
- 박싱 언박싱
3주차 과제 1 - 스네이크 게임
namespace ConsoleApp1
{
internal class Program
{
static void Main(string[] args)
{
int score = 0;
Food remainFood = new Food(0, 0, "o");
Snake snake = new Snake(5, 10, "s");
while (snake.Move(Console.ReadKey().Key))
{
if (snake.Head.isConflict(remainFood))
{
remainFood.Clear();
snake.AddSnake();
remainFood = new Food(0, 0, "o");
score++;
}
snake.Draw();
}
Console.WriteLine($"게임 종료! 점수는 {score} 점 입니다.");
Console.WriteLine("Enter를 눌러 게임을 완전히 종료해 주세요");
while(Console.ReadKey().Key != ConsoleKey.Enter){}
}
}
public class Sprite
{
public int X { get; set; }
public int Y { get; set; }
public int BeforeX { get; set; }
public int BeforeY { get; set; }
public string Image { get; set; }
public Sprite(int _x, int _y, string _image)
{
X = _x;
Y = _y;
Image = _image;
}
public bool isConflict(Sprite _sprite)
{
if (X == _sprite.X && Y == _sprite.Y)
{
return true;
}
return false;
}
public void Draw()
{
Console.SetCursorPosition(X, Y);
Console.Write(Image);
}
public void Clear()
{
Console.SetCursorPosition(X, Y);
Console.Write(' ');
}
}
public class Food : Sprite
{
public Food(int _x, int _y, string _sprite) : base(_x, _y, _sprite)
{
Random ran = new Random();
X = ran.Next(1, Console.WindowWidth - 1);
Y = ran.Next(1, Console.WindowHeight - 1);
Draw();
}
}
// 구현하고 싶은것
// 뱀의 머리가 이동을하면 그뒤의 몸체가 한박자씩 늦게 따라온다.
// Snake 클래스는 Sprite 리스트를 가진다.
// 생성자에서는 sprite를 하나 생성한다.
// snake를 이동시키면 sprites 리스트 전체가 움직여야하는데?
// 역 순으로 돌면서 이전 index의 위치값을 가져온다.
public class Snake
{
List<Sprite> sprites = new List<Sprite>();
public Snake(int _x, int _y, string _image)
{
sprites.Add(new Sprite(_x,_y,_image));
Draw();
}
public Sprite Head { get { return sprites[0]; } }
public void AddSnake()
{
sprites.Add(new Sprite(sprites[sprites.Count - 1].X, sprites[sprites.Count - 1].Y, "s")); ;
sprites[sprites.Count - 1].Draw();
}
public bool Move(ConsoleKey _input)
{
Clear();
int nextX = 0;
int nextY = 0;
switch (_input)
{
case ConsoleKey.UpArrow:
nextY--;
break;
case ConsoleKey.DownArrow:
nextY++;
break;
case ConsoleKey.LeftArrow:
nextX--;
break;
case ConsoleKey.RightArrow:
nextX++;
break;
default:
return true;
}
for (int i = sprites.Count - 1; i > 0; i--)
{
sprites[i].X = sprites[i-1].X;
sprites[i].Y = sprites[i - 1].Y;
}
sprites[0].X += nextX;
sprites[0].Y += nextY;
return isConflict() ? false : true;
}
public void Draw()
{
foreach (Sprite sp in sprites)
{
sp.Draw();
}
}
public void Clear()
{
foreach (Sprite sp in sprites)
{
sp.Clear();
}
}
public bool isConflict()
{
if (sprites[0].X < 0 || sprites[0].X > Console.WindowWidth || sprites[0].Y < 0 || sprites[0].Y > Console.WindowHeight)
{
Console.WriteLine();
Console.WriteLine("벽에 부딫혀 게임이 종료되었습니다.");
return true;
}
foreach (Sprite sp in sprites)
{
foreach(Sprite sp2 in sprites)
{
if (sp != sp2 && sp.isConflict(sp2))
{
Console.WriteLine();
Console.WriteLine("자신의 몸에 부딪혀 게임이 종료 되었습니다.");
return true;
}
}
}
return false;
}
}
}
이 게임에는 스네이크, 푸드 두 가지가 있는데
푸드는 한개의 스프라이트로 구성되고, 스네이크는 스프라이트 리스트로 구성되어 있다.
키보드 방향키를 누르면 제일 처음으로 만들어진 뱀의 머리가 움직이게되고 푸드를 먹으면 뱀의 길이가 길어진다.
뱀의 길이가 길어지면 스프라이트 리스트에 스프라이트가 하나 추가되고 새로 추가된 스프라이트는 뱀의 머리를 따라다니게된다.
또, 뱀의 머리를 기준으로 뱀이 벽에 부딪혔는지 판단했다. 머리를 기준으로 한 이유는 모든 몸체는 머리를 따라가기 때문에
머리가 벽의 범위 밖으로 나가지 않았다면 몸체 또한 나갈일이 없기 떄문이라고 생각했고,
스프라이트 리스트에 있는 스프라이트들의 충돌여부를 판단해서 스스로의 몸에 부딪혔는지 판단했다.
이부분은 일단은 2중 for문으로 구현했는데 더 좋은 방법을 생각해보면 좋을 것 같다.
3주차 과제 2 - 블랙잭 게임
namespace ConsoleApp1
{
internal class Program
{
static void Main(string[] args)
{
bool GameEnd = false;
while (!GameEnd)
{
GameStart();
Console.WriteLine("게임을 다시 시작하려면 1, 종료하려면 2를 누르세요");
ConsoleKey Input = Console.ReadKey().Key;
while (Input!= ConsoleKey.D1 && Input != ConsoleKey.D2)
{
Console.WriteLine("게임을 다시 시작하려면 1, 종료하려면 2를 누르세요");
Input = Console.ReadKey().Key;
}
if (Input == ConsoleKey.D1)
{
Console.Clear();
}
else
{
GameEnd = true;
Console.WriteLine("게임을 종료 합니다");
}
}
}
static void GameStart()
{
Deck deck = new Deck(); // 덱 생성 카드 셔플 완료
Player player = new Player("player");
Dealer dealer = new Dealer("dealer");
player.Draw(deck.DrawCard());
player.Draw(deck.DrawCard());
dealer.Draw(deck.DrawCard());
dealer.Draw(deck.DrawCard());
Console.WriteLine();
// 1. 플레이어의 턴 입니다.
// 2. 선택지 선택 1. 카드를 받는다. 2. 완료 선언
// 3. 딜러의 턴 딜러의 카드 합이 17 미만이라면 카드를 받습니다. 아니라면 완료됩니다.
Console.WriteLine("턴을 진행합니다.");
while (!player.isCompleted || !dealer.isCompleted)
{
if (!player.isCompleted)
{
Console.WriteLine("플레이어의 턴입니다. 선택지를 골라주세요");
Console.WriteLine("1. 카드를 받는다.");
Console.WriteLine("2. 완료.");
int select;
while (int.TryParse(Console.ReadLine(), out select) == false && select < 0 && select > 2)
{
Console.WriteLine("1 또는 2를 입력해 주세요");
}
if (select == 1)
{
player.Draw(deck.DrawCard());
}
else
{
player.isCompleted = true;
}
}
if (!dealer.isCompleted)
{
if (dealer.Score < 17)
{
Console.WriteLine("딜러의 합이 17 미만이여서 딜러가 카드를 뽑습니다.");
dealer.Draw(deck.DrawCard());
}
}
}
Console.WriteLine();
if (player.Score == 21 && dealer.Score == 21)
{
Console.WriteLine("플레이어와 딜러 모두 21을 완성했습니다.");
}
else if (player.Score > 21 && dealer.Score > 21)
{
Console.WriteLine("플레이어와 딜러 모두 21을 초과하였습니다.");
}
else if ((player.Score > 21 && dealer.Score <= 21) || (player.Score < 21 && dealer.Score < 21 && player.Score < dealer.Score))
{
Console.WriteLine("딜러의 승리 입니다.");
}
else if ((player.Score <= 21 && dealer.Score > 21) || (player.Score < 21 && dealer.Score < 21 && player.Score > dealer.Score))
{
Console.WriteLine("플레이어의 승리 입니다.");
}
else
{
// 둘다 21아래인데 같을때
Console.WriteLine("동점 입니다.");
}
Console.WriteLine();
player.ShowHand();
dealer.ShowHand();
Console.WriteLine();
}
}
}
public enum CardSuit
{
Heart,
Diamond,
Spade,
Club,
}
class Card
{
CardSuit suit;
int cardNum;
public string Name
{
get
{
if (cardNum == 11)
{
return suit.ToString() + " J";
}
else if (cardNum == 12)
{
return suit.ToString() + " Q";
}
else if (cardNum == 13)
{
return suit.ToString() + " K";
}
return suit.ToString() + " " +cardNum.ToString();
}
}
public int Score
{
get
{
if (cardNum > 10)
{
return 10;
}
return cardNum;
}
}
public Card(CardSuit _suit, int _cardNum)
{
suit = _suit;
cardNum = _cardNum;
}
}
class Deck
{
Stack<Card> deck = new Stack<Card>();
public Deck()
{
CardSufffle();
}
public void CardSufffle()
{
List<Card> list = new List<Card>(); // 0~ 51까지의 인덱스
List<int> temp = Enumerable.Range(0, 52).ToList();
Random rand = new Random();
for (int i = 0; i < 4; i++)
{
for (int j = 1; j <= 13; j++)
{
list.Add(new Card((CardSuit)i, j));
}
}
while (temp.Count > 0)
{
int index = rand.Next(0, temp.Count-1); // temp에서 index 번째 요소를 선택
deck.Push(list[temp[index]]); // temp[index] 번째 카드를 stack 가장 아래에넣는다.
temp.Remove(temp[index]); // temp의 index 번째 요소를 제거
}
}
public Card DrawCard()
{
return deck.Pop();
}
}
class Player
{
protected List<Card> hand = new List<Card>();
public Player(string _name)
{
UserName = _name;
}
protected string UserName { get; set; }
public int Score { get; set; }
public bool isCompleted = false;
public void Draw(Card _card)
{
hand.Add(_card);
Console.WriteLine($"뽑은 카드는 {_card.Name} 입니다.");
Score += _card.Score;
Console.WriteLine($"현재 숫자의 합은 {Score} 입니다.");
Console.WriteLine();
if (Score > 21)
{
isCompleted = true;
Console.WriteLine("21점을 초과하였습니다. 딜러가 21점을 초과하지 않으면 패배합니다.");
Console.WriteLine();
}
}
public void ShowHand()
{
Console.Write($"{UserName}의 패 : ");
foreach(Card card in hand)
{
Console.Write(card.Name + ", ");
}
Console.WriteLine();
}
}
class Dealer : Player
{
public Dealer(string _name) : base(_name) { }
public new void Draw(Card _card)
{
hand.Add(_card);
Score += _card.Score;
if (Score >= 17)
{
isCompleted = true;
}
}
}
블랙잭 게임을 만들었다.
카드를 셔플할때 전에 Linq와 Random을 사용해서 카드를 셔플하는 예제를 만들어 봤었는데
이번에는 Linq를 사용하지 않고 전에 팀 카드 게임만들기를 할때 마지막에 적용했던 방식으로 만들어 보았다.
먼저 카드리스트를 준비하고 카드리스트에서 랜덤으로 한장씩 뽑아서 stack에 저장을 했고
그 결과 카드를 셔플한 효과를 만들어 낼 수 있었다.
박싱 언박싱
전에 IEnumerable 인터페이스에 대해서 공부하면서 제네릭 IEnumerable이 권장된다는 것을 알게되었고
일반 IEnumerable는 컬렉션에 데이터를 박싱, 언박싱 해야하기 떄문에 성능 저하와 타입 안정성이 떨어진다는 것도 알게되었다.
그때 박싱과 언박싱을 타입을 최상위 타입인 Object형으로 변환시켜서 형 변환을 하는것으로만 이해했는데
4주차 강의를 통해서 박싱과 언박싱은 값 - 참조 간의 형 변환이라는 것을 이제와서 눈치챘다.
생각해보니 그냥 단순한 형 변환의 경우는 int -> float 라던가 값 -> 값 의 형변환 이였고
박싱과 언박싱은 int -> Object 값 -> 참조 의 변환이였는데 가장 중요한 부분을 놓치고 넘어가 버렸다.
결국 박싱과 언박싱은 박싱 - 스택 메모리 영역에 있는 데이터를 힙 영역에 할당하거나 , 언박싱 - 힙 메모리 영역에 있는 데이터를 스택 영역에 할당하고
그 과정에서 메모리를 새로 할당하고 가비지 컬렉션으로 메모리를 정리하는 등의 비용도 필요하기 때문에 권장되지 않는다.
정리 :