오늘의 학습 키워드

struct, StackOverFlow

공부한 내용

리팩토링 구상

리팩토링을 하기 전에 구조 부터 문제라는 생각이 들어 간단한 시스템 흐름도를 만들어 보았다.

아래 그림으로 보면 별 문제가 없어보이지만..

문제 1 : 너무 많은 gameState

모든 상태를 gameState에 넣어놓고 현재 gameState에 따라 함수로 진행하는 방법이다.

이러면 분기가 하나 늘어날 때마다 gameState가 많아질 것이며 코드의 복잡도가 더 증가할 것이라고 판단했다.

이것에 대한 내가 생각한 아이디어 중 하나는 다음 단계로 이동할 때 현재의 씬을 스택에 담는 것이다.

스택에 담는다면 나가기 버튼을 눌렀을 때도 이전 씬을 담으니까 그 씬을 돌려주면 된다.

// 현재 게임 상태
    GameState gameState;

    // UI랑 게임 플레이랑 나눌 필요성이 있어보임
    // 문제점 : 게임 스테이트가 너무 많음
    // 아이디어 1: 연결 되어있는 것이라면 Stack으로 씬을 담는 것

    public enum GameState
    {
        Town,
        State,
        Inventory,
        Equipment,
        ItemSort,
        Shop,
        Purchase,
        Sell,
        Dungeon,
        INN,
    }

    public void Start()
    {
        JsonUtility.Save(character.Info);


        switch (gameState)
        {
            case GameState.Town:
                EnterTown();
                break;
            case GameState.State:
                ShowState();
                break;
            case GameState.Inventory:
                ShowInventory();
                break;
            case GameState.Equipment:
                ManageEquiment();
                break;
            case GameState.ItemSort:
                ItemSort();
                break;
            case GameState.Shop:
                ShowShop();
                break;
            case GameState.Purchase:
                PurchaseItem();
                break;
            case GameState.Sell:
                SellItem();
                break;
            case GameState.Dungeon:
                EnterDungeon();
                break;
            case GameState.INN:
                EnterINN();
                break;
            default:
                break;
        }
    }

gameState를 메인에서 선택하는 5가지로 줄이니 우측 스크롤 바 쪽에 빨간줄을 여럿 볼 수 있다. ㅠㅠ

나의 과오니 내가 치워야지..

* 추가 : 어차피 맨 처음에 마을으로 가니까 gameState가 꼭 필요하지 않다고 판단해서 gameState 관련 로직을 제거하게 됐다.

gameState 관련 로직을 빼도 나가기가 잘 되는 모습

 

문제 1-1 : 호출 스택 문제 (스택 오버 플로우)

만들면서 뭔가 쎄한 느낌이 들어서 확인해보니 마을로 가지 않고서는 호출 스택을 탈출할 수 없다는 문제가 있었다.

'함수 호출으로 넘어가는 것이 문제인가?' 라는 생각이 들어 바로 적용해보기로 했다.

그럼 그렇지 내가 너무 복잡하게 생각해서 돌아가는 경우였다. 그냥 return 하면 되는 쉬운 방법이 있었다니!

함수에서 다른 함수로 이동할 때 다시 이전 함수로 돌아가는 경우라면 그냥 return을 쓰면 된다. 어렵게 생각하지 말자!

 

문제 2 : gameState와 맞물려 있는 안내 UI

일단 만들고 나중에 고치자고 생각했지만 만들면서도 계속 반복되는 게 많아 불편한 마음이 들었던 UI도 문제였다.

아래의 2개의 코드는 각각 마을과 아이템 장착 관리인데 비슷한 부분이 많아 변수를 넘겨주는 방식으로 통합하려 한다.

Console.Write
(
    "스파르타 마을에 오신 여러분 환영합니다\n" +
    "이곳에서 던전으로 들어가기 전 활동을 할 수 있습니다.\n\n" +
    "1. 상태보기\n" +
    "2. 인벤토리\n" +
    "3. 상점\n" +
    "4. 던전입장\n" +
    "5. 휴식하기\n" +
    "\n" +
    "원하시는 행동을 입력해주세요.\n" +
    ">> "
);
Console.Write
(
    titleStr +
    "보유 중인 아이템을 관리할 수 있습니다.\n\n" +
    "[아이템 목록]\n" +
    itemListStr +
    choiceStr +
    "원하시는 행동을 입력해주세요.\n" +
    ">> "
);

형식이 비슷하니 하나에서 다 출력하도록 바꿨다.

void WriteUI(string title, string introduction, string result, string choice, string inputStr)
{
    Console.WriteLine(title + introduction + result + choice + inputStr);
}

 

문제 3 : 너무 많은 아이템 클래스

이전엔 Item 클래스를 상속 받는 객체를 따로 생성하여 받는 방식으로 구현 하였는데 객체가 생길 때마다 생성 해야 되는 문제가 있었다.

이미 Item 클래스에 SetInfo라는 함수가 있으니 생성자로 접근하여 세팅하도록 바꾸려고 한다.

불필요한 객체 생성을 줄이고 생성자로 정보를 넘겼다.

public ItemShop()
{
    ItemList.AddRange(new Item[] 
    { 
        new Item(ItemType.Armor, "수련자 갑옷", 0, 5, "수련에 도움을 주는 갑옷입니다.", 1000), 
        new Item(ItemType.Armor, "무쇠갑옷", 0, 5, "무쇠로 만들어져 튼튼한 갑옷입니다." , 600), 
        new Item(ItemType.Armor, "스파르타의 갑옷", 0, 15, "스파르타의 전사들이 사용했다는 전설의 갑옷입니다.", 3500), 
        new Item(ItemType.Weapon, "낡은 검", 2, 0, "쉽게 볼 수 있는 낡은 검입니다.", 600), 
        new Item(ItemType.Weapon, "청동 도끼", 5, 0, "어디선가 사용됐던 것 같은 도끼입니다.", 1500), 
        new Item(ItemType.Weapon, "스파르타의 창", 7, 0, "스파르타의 전사들이 사용했다는 전설의 창입니다.", 2400) 
    });
}

 

문제 4 : 변수 접근 제한

이전에 그냥 Get Set 프로퍼티로 접근했는데

데이터 저장을 위해 프로퍼티로 넘긴다고 하면 Set은 Private로 해주고 실수 방지를 위해 변수가 아닌 함수로만 통해 접근하는 것이 좋아 보였다.

구조체도 인터페이스 상속이 가능하다는 것을 이번에 튜터님께 질문 하면서 알게 되었다.

추가로 함수도 사용 가능하다.

public struct ItemInfo : IItem
{
    public ItemType Type { get; private set; }
    public string Name { get; private set; }
    public int AttackModifier { get; private set; }
    public int DefenseModifier { get; private set; }
    public string Description { get; private set; }
    public int Price { get; private set; }
    public bool IsEquiped { get; private set; }

    public void SetInfo(ItemType type, string name, int attack, int defense, string description, int price, bool isEquiped)
    {
        Type = type;
        Name = name;
        AttackModifier = attack;
        DefenseModifier = defense;
        Description = description;
        Price = price;
        IsEquiped = isEquiped;
    }

    public void Equip(bool isEquip)
    {
        IsEquiped = isEquip;
    }
}

 

 

오늘의 회고

 오늘은 리팩토링 하는 시간을 가졌다. 리팩토링을 끝내고 리드미 파일도 정리하고 시간이 남으면 알고리즘도 공부해보려 했는데 시간이 부족해 리팩토링도 다 하지 못했다. 아쉽지만 오히려 혼자 고민 해본 것과 튜터님께 질문 드린 것을 통해  배운 것이 많아서 아쉬울 건 없는 하루였던 것 같다.

 내일은 리팩토링(데이터 저장, 상태 콜백, 클래스 나누기)를 하고 리드미 파일을 작성하려고 한다. 내일도 파이팅!

 

 

 

오늘의 학습 키워드

JsonSerializer.Serialize

공부한 내용

목표 : 세이브와 로드

시도한 것 : 저장할 때 JsonSerializer.Serialize(클래스)로 불러오려고 했다.

문제 : JsonSerializer.Serialize는 클래스로 불러오면 클래스 안의 구조체까지는 접근하지 못한다. 또한 프로퍼티 값이어야 읽어올 수 있다.

해결 : 사용할 때 JsonSerializer.Serialize(클래스명.클래스구조체)로 세이브 했다.

public struct CharacterInfo
{
    public int Level { get; set; }
    public string Name { get; set; }
    public float Attack { get; set; }
    public float Defense { get; set; }
    public float Health { get; set; }
    public float Gold { get; set; }
    public bool IsDead { get; set; }
    public float MaxHealth { get; set; }
    public float DefaultAttack { get; set; }
    public float DefaultDefense { get; set; }
}
class JsonUtility
    {
        public static void Save<T>(T t)
        {
            string json = JsonSerializer.Serialize(t);
            File.WriteAllText(@"D:\data.json", json);
        }

        public static T Load<T>(string path)
        {
            var json = File.ReadAllText(path);
            if (json != null)
            {
                return JsonSerializer.Deserialize<T>(json);
            }
            else
            {
                return default(T);
            }
        }
    }
static void Main(string[] args)
{
    Console.ForegroundColor = ConsoleColor.Green;

    Game game = new Game();

    // TODO : 추후 캐릭터 선택창 만들 것
    // 시작 : 캐릭터를 선택하시오
    // 끝 : 캐릭터 인스턴스 생성
    character = new Warrior();

    var info = JsonUtility.Load<Character.CharacterInfo>(@"D:\data.json");
    if (info.Name != null)
    {
        character.Info = info;
    }

    while (true)
    {
        game.Start();
    }
}

개인 과제 질문 피드백

기능 구상이 어려울 때

- 처음 기능이 끝까지 갈 수가 없음
    - 나중에 바뀌기 마련
    - 개방 폐쇄 원칙(Open Close) : 확장에는 개방 수정에는 폐쇄
- 플로우 차트 만들어보기 (와이어 프레임)

 

변수 정하기

- 왼쪽과 오른쪽은 기획에서 달라질 수 있는 부분이다.

- 왼쪽이 필요한 경우라면 공격력이 필요한 방어구(가시방패)가 필요 한다던지 하는 경우

- 오른쪽은 그런 것이 필요하지 않을 때

가장 중요한 것은 왜 이 방식을 선택했는지에 대한 근거가 필요하다.

클래스를 어떻게 나누는가?

- 외부로 둔다면 영향을 줄 수 있는 범위가 커진다.
- 최대한 분리한다면 영향을 적게 준다. 하지만 접근하는데 불편할 수 있다.

- 이것도 상황 판단 하에 정하기 -> 장단점을 파악하면서 사용

 

추가 : if else와 try catch문 어떨 때 쓰는 게 좋은가

- 입력을 받았을 때 입력이 예상 가능하면 if문
- 입력이 예상 불가능 하면 try catch문

오늘의 회고

 오늘은 TextRPG 구현을 마무리하고 제출하는 날이었다. 시간이 촉박했기에 부랴부랴 만드느라 코드에 대한 고민이 조금 부족했던 것 같다. 다른 사람의 코드도 보면서 좋은 방법들이 있다면 질문해보고 내 것에 적용하는 시간(리팩토링)을 가져야겠다. 팀 프로젝트도 TextRPG의 연장선으로 진행한다고 하는데 좋은 방법들을 많이 적용하도록 하고 글로 또 남겨서 헷갈릴 때마다 다시 보고 해야겠다.

 내일은 이전에 언급했다싶이 리팩토링을 진행할 생각이다. 또한 깃허브에 ReadMe파일에 정리하는 시간을 가지도록 해야겠다. 내일도 파이팅!

 

 

깃허브 링크 :

https://github.com/JeongJuChan/TextRPG_Single

 

GitHub - JeongJuChan/TextRPG_Single

Contribute to JeongJuChan/TextRPG_Single development by creating an account on GitHub.

github.com

 

 

참고 :

https://www.csharpstudy.com/Data/Json-SystemTextJson.aspx

 

System.Text.Json - C# 프로그래밍 배우기 (Learn C# Programming)

System.Text.Json .NET Core 3.0에서 새로운 Json 지원 클래스로서 System.Text.Json가 내장(built-in) 되었으며, ASP.NET Core 3.0 에서 디폴트 Json Serializer가 서드파티의 Newtonsoft.Json에서 System.Text.Json 으로 변경되었다

www.csharpstudy.com

 

 

오늘의 학습 키워드

Int int.ComareTo(int value), List<T>.Sort(Comparison<T> comparison), Console.ForegroundColor

공부한 내용

목표 : 인벤토리를 내림차순으로 정렬

시도한 것 : List.Sort()와 람다를 사용하여 Int int.CompareTo(int value)로 앞의 int와 뒤의 int를 비교하여 정렬하였다.

문제 : 기본적으로 비교하면 오름차순으로 정렬된다.

해결 : -를 곱하고 반환해 반대로 정렬하게 하였다.(내림차순)

// 아이템 정렬
void ItemSort()
{
    string input = null;
    WriteInventoryInfo();

    input = Console.ReadLine();

    switch (input)
    {
        case "0":
            gameState = GameState.Inventory;
            break;
        case "1":
            inventory.Items.Sort((x, y) =>
            {
                // ComareTo
                // -는 내림차순
                return -x.Info.Name.Length.CompareTo(y.Info.Name.Length);
            });
            break;
        case "2":
            inventory.Items.Sort((x, y) =>
            {
                // ComareTo
                return -x.Info.IsEquiped.CompareTo(y.Info.IsEquiped);
            });
            break;
        case "3":
            inventory.Items.Sort((x, y) =>
            {
                // ComareTo
                return -x.Info.AttackModifier.CompareTo(y.Info.AttackModifier);
            });
            break;
        case "4":
            inventory.Items.Sort((x, y) =>
            {
                // ComareTo
                return -x.Info.DefenseModifier.CompareTo(y.Info.DefenseModifier);
            });
            break;
        default:
            Console.WriteLine("잘못된 입력입니다.\n");
            break;
    }
}

목표 : 콘솔 색 바꾸기

해결 : Console.ForegroundColor = ConsoleColor.Green;을 사용하여 콘솔 색을 초록색으로 바꾸었다.

 

오늘의 회고

 오늘은 상점 구매와 판매, 정렬을 구현하고 줄맞춤 버그를 수정했다. 정렬에서 Comparison을 사용하는데 좀 걸렸지만 int 값을 반환하는 것을 알고 내림차순으로 쉽게 정렬할 수 있었다. 줄 맞춤 버그는 어제부터 시달렸던 과제인데 한글이 2바이트를 먹는다는 것과 한글을 검출해서 공백에 패딩을 한 자리 주는 것으로 해결할 수 있었다.

 내일은 장착 개선, 던전입장, 휴식, 레벨업, 게임 저장을 해볼 생각이다. 이를 내일 제출(오후 6시)까지 구현하는 것은 어려울 수 있지만 최대한 구현해 볼 생각이고 이후로도 이번주 안에는 작업할 생각이다. 내일 열심히 해서 최대한 달성해보자. 내일도 파이팅!

 

 

참고 :

https://developer-talk.tistory.com/220

 

[C#]List 속성별로 정렬

이번 포스팅에서는 C#에서 속성별로 List 객체를 정렬하는 방법을 설명합니다. LINQ OrderBy() 메서드를 사용하여 정렬된 새로운 List를 생성하는 방법이 있으며, 기존 List를 정렬하는 List에 내장된 Sort

developer-talk.tistory.com

 

오늘의 학습 키워드

string.PadRight(int num, char character), Encoding.Unicode.GetByteCount(string str)

 

공부한 내용

문제 : 오늘은 인벤토리 구현 중에 문제를 맞닥뜨렸다. 그것은 아이템 목록을 정렬하는 것인데 여러 아이템을 정렬된 상태로 출력 하는 것이었다.

시도한 것 1 : 처음엔 그냥 저번에 배웠듯이 PadRight를 사용하기로 했다. 하지만 아래에 나와있듯이 자꾸 무쇠 갑옷이 삐져나오는 문제가 생겼다.

찾아본 원인 : C#에서는 한글과 다른 문자들의 바이트 크기가 달라서 생기는 문제라는 것이다. (한글 2바이트, 다른 것 1바이트)

또 다른 문제 : 하지만 생각했던 대로 바이트 개수가 나오지 않았다. (한글 3바이트, 다른 것 1바이트)

원인 : 이는 Encoding.Default가 UTF-8로 잡혀있어서 한글은 3바이트로 나오던 것이다.

시도한 것 2 : 그래서 Encoding.Unicode로 지정했더니 모든 문자가 2바이트로 나오게 되었다.

마지막 문제 : 한글이 2문자를 차지하니 한글과 공백을 구분하는 로직이 필요하다.

시도한 것 3 : 한글을 EncodingDefault.GetByteCount == 3인지 검사해 개수를 세고 -1한 값을 패딩을 줄 전체 바이트 길이에서 빼줬다. (한글이 2글자 공백이 1글자를 차지하므로 공백일 시 패딩을 1개씩 늘림)

string ArrangeInfo(string str, int totalLength, char padChar = ' ')
{
    // 한글 2바이트
    int krByteLength = totalLength;
    int byteCount = Encoding.Unicode.GetByteCount(str);
    int krCount = 0;

    string[] strArr = str.Split();

    foreach (var c in str)
    {
        // 한글이면 count++;
        if (Encoding.Default.GetByteCount(c.ToString()) == 3)
        {
            krCount++;
        }
    }

    // 정렬이 안 되는 문제 발생
    // 2바이트니 / 2 할 때 한글의 개수가 홀수면 1 짝수면 2씩 감소시키기
    // 12
    //byteCount = krCount * 2;

    // 공백, 영어, 숫자 1바이트
    // 한글은 2바이트 크기는 2배
    // 한글 10글자 기준 30바이트
    // 한글 4개 12바이트
    // 8 8 10 10 14
    //int bytes = Encoding.Unicode.GetByteCount("무쇠갑옷");
    //int bytes1 = Encoding.Unicode.GetByteCount("낡은 검");
    //int bytes2 = Encoding.Unicode.GetByteCount("린넨 셔츠");
    //int bytes3 = Encoding.Unicode.GetByteCount("나무 망치");
    //int bytes4 = Encoding.Unicode.GetByteCount("스파르타의 창");

    // * 조심 : Default시 UTF-8로 잡혀서 3바이트로 나옴 
    // int bytes = Encoding.Default.GetByteCount("무쇠갑옷");
    string tempStr = str.PadRight(totalLength + totalLength - krCount, padChar);

    return tempStr;
}

 

 

오늘의 회고

 오늘은 인벤토리와 장착 기능 구현을 마무리 했다. 추가적으로 구현할 기능들이 여럿 보이지만 일단 선택 구현 사항부터 마무리 지으려고 한다. 오늘은 문자열 정렬 쪽에서 시간을 많이 뺏긴 것 같은데 그만큼 내가 모르고 있었다는 것이니까 다음 번에는 틀리지 않도록 다시 공부해봐야겠다.

 내일은 콘솔 쪽을 꾸미고 인벤토리를 정렬하는 부분을 좀 해봐야겠다. 정렬 쪽은 특히나 게임 쪽에서 자주 나오는 개념이니 만큼 모른다면 확실히 알아가고 구현이 가능하다면 어떻게 하면 더 나은 구현 방법일지를 고민해봐야겠다. 내일도 파이팅!

 

 

참고 : 

https://holjjack.tistory.com/145

 

[C#] 한글 2byte 로 계산 하는 방법

한글을 2byte로 계산하기위해서 기본적으로 이해를 하셔야 하는 부분은 바로 Unicode 입니다. 해당 부분에 대한 이해없이 단순히 Encoding을 이용해서 Byte 수를 계산한다면, 한글자당 2~3 으로 byte수가

holjjack.tistory.com

 

오늘의 학습 키워드

ToString("D2"), string.PadLeft(int num, char character), 인터페이스

 

공부한 내용

문자열 공백 0출력

저번에 배웠던 ToString("N2")는 소숫점을 나타낸다면 ToString("D2")를 하면 공백이 0으로 채워진다는 것을 배웠다.

void ShowState()
{
    string input = null;
    while (input != "0")
    {
        Console.Write
        (
            "상태 보기\n" +
            "캐릭터의 정보가 표시됩니다\n\n" +
            $"Lv. {character.Level.ToString("D2")}\n" +
            $"Chad ( {character.Name} )\n" +
            $"공격력 : {character.Attack}\n" +
            $"방어력 : {character.Defense}\n" +
            $"체력 : {character.Health}\n" +
            $"Gold : {character.Gold} G\n\n" +
            "0. 나가기\n\n" +
            "원하시는 행동을 입력해주세요.\n" +
            ">> "
        );

        input = Console.ReadLine();
    }

    Console.WriteLine();
}

아이템 인터페이스

인터페이스에 관해 궁금한 점이 있었는데 어떨 때 인터페이스를 사용하는지 궁금했다.

검색도 해보고 고민을 해봤지만 이건 내 영역을 넘어선 것 같아서 튜터님께 언제 사용하면 좋을지에 대해 질문을 드렸다.

인터페이스 사용과 관련하여 다음 내용을 참고하면 좋다.

1. 인터페이스는 공통적인 행위를 묶을 때 사용하면 좋다.

2. 로직이 조금이라도 들어간다면 클래스로 빼는 것이 덜 복잡할 수 있다.(인터페이스 안에서는 구현이 불가능하므로)

3. 최소한의 단위로 만들어야지 코드가 보기 좋다.

interface IItem
{
    void Equip(bool isEquip);
}

보기 안 좋은 코드일 수 있지만 일단 구현해보는 것으로 하고 나중에 리팩토링을 해봐야겠다고 생각했다.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace TextRPG_Single
{
    class Item : IItem
    {
        public enum ItemType
        {
            Weapon,
            Armor,
        }

        public struct ItemInfo
        {
            public ItemType Type { get; set; }
            public string Name { get; set; }
            public int AttackModifier { get; set; }
            public int DefenseModifier { get; set; }
            public bool IsEquiped { get; set; }
        }

        public ItemInfo Info = new ItemInfo();
        

        protected Item(ItemType type, string name, int attack, int defense, bool isEquiped)
        {
            Info.Type = type;
            Info.Name = name;
            Info.AttackModifier = attack;
            Info.DefenseModifier = defense;
            Info.IsEquiped = isEquiped;
        }

        public void Equip(bool isEquip)
        {
            Info.IsEquiped = isEquip;
        }
    }
}

 

오늘의 회고

 튜터님께 질문드리기 전에 내가 고민했던 것들을 정리 했었는데 막상 앞에서 질문하려니 부끄럽고 말도 잘 못드렸던 것 같다. 하지만 친절하게 잘 설명해주려고 하시고 동기부여도 해주시니 공부할 맛이 났다. 사실 엄청나게 깨달음을 주신 건 아니지만 코드엔 정답은 없고 상황마다 다르며 직접 구현해보고 내가 경험으로 쌓아가는 것이라 먼저 해보라는 말씀이 제일 기억에 남았던 것 같다. 무작정 짜보고 왜 이게 안 좋은지 판단하고 고치는 작업을 통해 내가 더 성장할 수 있는 기회를 주신 것 같아서 더 감사하다는 생각이 들었다.

 오늘 질문을 하면서 느낀 점은 튜터님이 있고 질문을 편하게 드릴 수 있다는 점이 얼마나 좋은 환경인지 조금이라도 깨닫게 되는 계기가 된 것 같다. 항상 열심히 해주시는 튜터님들, 매니저님들, 동료들을 생각하며 나도 또한 열심히 해야겠다. 내일도 파이팅!

 

 

참고 : 

https://acpi.tistory.com/122

 

[C#] 숫자앞에 0붙이기

1. PadLeft 함수 사용 int value = 100; string result = Convert.ToString(value).PadLeft(5, '0'); MessageBox.Show(result); 2. string.Format 함수 사용 int value = 100; string result = string.Format("{0:D5}", value); MessageBox.Show(result); 3. ToString

acpi.tistory.com

 

오늘의 학습 키워드

재귀

 

공부한 내용

Flood_Fill 문제

구현 방법 : 알고리즘 해결을 위해 생각했던 것이 좌표를 리스트에 넣어놓고 상하좌우를 값이 같은지 체크하면서 반복하면서 뻗어가는 방식이었다.

문제 : 해당 과제에서의 문제가 발생한 부분은 리스트 안에 다음 타일의 중복체크를 안 해서 스택 오버 플로우가 발생했다. 해해결 : FloodFillNext 메서드에 중복체크하는 구문을 통해 이를 해결하였다.

* HashSet이 List보다 더 런타임 속도가 빨라서 이후에 수정하였다.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace sparta
{
    class Flood_Fill
    {
        // 2차원 배열이 주어지고 특정 좌표와 색이 주어진다.
        // 특정 좌표의 값을 바꾸는데 특정 좌표로부터 4차원(위,아래,왼,오른쪽)과
        // 연결되어있는 같은 값을 모두 color의 숫자로 바꾼다.
        static void Main(string[] args)
        {
            FloodFill(new int[][] { new int[] { 1, 1, 1 }, new int[] { 1, 1, 0 }, new int[] { 1, 0, 1 } }, 1, 1, 2);

        }

        private static void FloodFill(int[][] image, int sr, int sc, int color)
        {
            HashSet<(int, int)> posSet = new HashSet<(int, int)>();
            posSet.Add((sr, sc));
            FloodFillNext(image, posSet, sr, sc);

            foreach (var item in posSet)
            {
                image[item.Item1][item.Item2] = color;
            }

            //return image;
        }

        private static void FloodFillNext(int[][] image, HashSet<(int, int)> posSet, int sr, int sc)
        {
            int next = -1;

            if (sr > 0 && image[sr][sc] == image[sr - 1][sc])
            {
                next = sr - 1;
                if (!posSet.Contains((next, sc)))
                {
                    posSet.Add((next, sc));
                    FloodFillNext(image, posSet, next, sc);
                }

            }
            if (sr + 1 < image.Length && image[sr][sc] == image[sr + 1][sc])
            {
                next = sr + 1;
                if (!posSet.Contains((next, sc)))
                {
                    posSet.Add((next, sc));
                    FloodFillNext(image, posSet, next, sc);
                }
            }
            if (sc > 0 && image[sr][sc] == image[sr][sc - 1])
            {
                next = sc - 1;
                if (!posSet.Contains((sr, next)))
                {
                    posSet.Add((sr, next));
                    FloodFillNext(image, posSet, sr, next);
                }
            }
            if (sc + 1 < image[0].Length && image[sr][sc] == image[sr][sc + 1])
            {
                next = sc + 1;
                if (!posSet.Contains((sr, next)))
                {
                    posSet.Add((sr, next));
                    FloodFillNext(image, posSet, sr, next);
                }
            }
        }
    }
}

https://leetcode.com/problems/flood-fill/description/

 

Flood Fill - LeetCode

Can you solve this real interview question? Flood Fill - An image is represented by an m x n integer grid image where image[i][j] represents the pixel value of the image. You are also given three integers sr, sc, and color. You should perform a flood fill

leetcode.com

 

 

오늘의 회고

 오늘은 알고리즘 강의와 여러 알고리즘 문제들을 풀면서 공부하는 시간을 가졌다. 아직 많이 모자라고 부족한 점도 많지만 문제를 분석하고 정리해둔 후 코드로 바꾼다는 접근법을 알게된 것만으로 많이 배웠다고 생각한다. 알고리즘 풀 때는 비슷한 유형의 문제들을 연속적으로 풀어서 그 유형에 익숙해지도록 해야겠다.

 강의 과제로 문제들을 풀면서 힌트도 보고 했는데 내일은 힌트를 보지 않고 강의에 나온 문제들을 다시 복습하는 시간을 가져야겠다. 내일도 파이팅!

오늘의 학습 키워드

Task

 

공부한 내용

스네이크 게임

스네이크 게임 구현

스파르타 동료분의 코드를 참고하여 진행하였습니다.

using System;
using System.Collections.Generic;
using System.Threading.Tasks;

namespace SnakeGame
{
    class Program
    {
        static int score = 0;
        static int highScore = 0;
        static int foodCount = -1;

        static Map map;
        static SnakeHead player;
        static ConsoleKeyInfo input;
        static Vector2Int direction = new Vector2Int(1, 0);

        static bool applicationQuit = false;
        static bool isEnterPressed = false;

        static void Main(string[] args)
        {
            Init();
            Route();
            while (!applicationQuit);
        }

        private static async void Route()
        {
            bool isCrashed = false;

            Task task = Task.Run(() =>
            {
                while (true)
                {
                    input = Console.ReadKey(true);
                    if (input.Key == ConsoleKey.Escape)
                    {
                        applicationQuit = true;
                        break;
                    }
                    else if (input.Key == ConsoleKey.Enter)
                    {
                        isEnterPressed = true;
                    }
                }
            });

            while (!applicationQuit)
            {
                await Task.Delay(100);

                if (isCrashed)
                {
                    Console.Clear();
                    map.DrawMap();
                    Console.WriteLine("===============================================================");
                    Console.WriteLine($"점수 : {score}  최고 점수 : {highScore}  먹은 수 : {foodCount}");
                    Console.WriteLine("===============================================================");
                    Console.WriteLine("죽었습니다. 다시 시작 : Enter");

                    if (isEnterPressed)
                    {
                        Reset();
                        Init();
                        isEnterPressed = false;
                        isCrashed = false;
                    }
                }
                else
                {
                    switch (input.Key)
                    {
                        case ConsoleKey.RightArrow:
                            direction = new Vector2Int(1, 0);
                            break;
                        case ConsoleKey.LeftArrow:
                            direction = new Vector2Int(-1, 0);
                            break;
                        case ConsoleKey.UpArrow:
                            direction = new Vector2Int(0, -1);
                            break;
                        case ConsoleKey.DownArrow:
                            direction = new Vector2Int(0, 1);
                            break;
                    }

                    isCrashed = Update();
                }
            }
        }

        private static void Reset()
        {
            score = 0;
            foodCount = -1;

            map = null;
            player = null;
            direction = new Vector2Int(1, 0);
        }

        static bool Update()
        {
            Console.SetCursorPosition(0,0);
            if (!map.IsFoodExist())
            {
                foodCount++;
                map.SpawnRandomFood();
            }

            bool isCrashed = player.TryMove(map, direction);
            map.DrawMap();
            Console.WriteLine("===============================================================");
            Console.WriteLine($"점수 : {score}  최고 점수 : {highScore}  먹은 수 : {foodCount}");
            Console.WriteLine("===============================================================");
            Console.WriteLine("ESC를 누르면 종료");
            score++;
            if (score > highScore)
            {
                highScore = score;
            }

            isEnterPressed = false;
            return isCrashed;
        }

        private static void Init()
        {
            int mapX = 50, mapY = 25;
            map = new Map(mapX, mapY);
            player = new SnakeHead(new Vector2(mapX / 2, mapY / 2));
        }

        public enum ObjectType
        {
            BLANK,
            WALL,
            FOOD,
            SNAKE_BODY,
            SNAKE_HEAD,
        }

        public struct Vector2
        {
            public int X { get; set; }
            public int Y { get; set; }

            public Vector2(int x, int y)
            {
                X = x;
                Y = y;
            }
        }

        public struct Vector2Int
        {
            private int x, y;

            public int X 
            {   
                get => x; 
                set
                {
                    if (value > 1)
                    {
                        x = 1;
                    }
                    else if (value < -1)
                    {
                        x = -1;
                    }
                    else
                    {
                        x = value;
                    }
                } 
            }

            public int Y
            {
                get => y;
                set
                {
                    if (value > 1)
                    {
                        y = 1;
                    }
                    else if (value < -1)
                    {
                        y = -1;
                    }
                    else
                    {
                        y = value;
                    }
                }
            }
            
            public Vector2Int(int x, int y)
            {
                this.x = x;
                this.y = y;
            }
        }

        public class Transform
        {
            public Vector2 position = new Vector2(0, 0);
            public Vector2Int rotation = new Vector2Int(0, 0);
        }

        class Object
        {
            public Transform trans = new Transform();
            public ObjectType objType;

            public Object(Vector2 pos, Vector2Int rot, ObjectType type)
            {
                trans.position = pos;
                trans.rotation = rot;

                objType = type;
            }
        }

        class Map
        {
            public int X { get; private set; }
            public int Y { get; private set; }

            public Object[][] tiles { get; private set; }

            public Map(int x, int y)
            {
                X = x;
                Y = y;

                // 세로줄이 y개인 2차원 배열
                tiles = new Object[Y][];

                for (int i = 0; i < Y; i++)
                {
                    // 안에 가로줄의 요소가 x개인 배열
                    tiles[i] = new Object[X];
                }

                for (int i = 0; i < Y; i++)
                {
                    for (int j = 0; j < X; j++)
                    {
                        Vector2 pos = new Vector2(X, Y);
                        Vector2Int rot = new Vector2Int(0, 0);
                        Object tile;

                        // 벽 만들기
                        if (i == 0 || i + 1 == Y || j == 0 || j + 1 == X) // 경계 부분이라면
                        {
                            if (i == 0)
                            {
                                if (j == 0)
                                {
                                    rot = new Vector2Int(-1, 1);
                                }
                                else if (j + 1 == X)
                                {
                                    rot = new Vector2Int(1, 1);
                                }
                                else
                                {
                                    rot = new Vector2Int(0, 1);
                                }
                            }
                            // -1
                            else if (i + 1 == Y)
                            {
                                if (j == 0)
                                {
                                    rot = new Vector2Int(-1, -1);
                                }
                                else if (j + 1 == X)
                                {
                                    rot = new Vector2Int(1, -1);
                                }
                                else
                                {
                                    rot = new Vector2Int(0, -1);
                                }
                            }
                            else
                            {
                                if (j == 0)
                                {
                                    rot = new Vector2Int(-1, 0);
                                }
                                else if (j + 1 == X)
                                {
                                    rot = new Vector2Int(1, 0);
                                }
                            }

                            tile = new Object(pos, rot, ObjectType.WALL);
                        }
                        else // 경계 부분이 아니라면 기본 Vector2Int(0,0);
                        {
                            tile = new Object(pos, rot, ObjectType.BLANK);
                        }

                        tiles[i][j] = tile;
                    }
                }
            }

            public void DrawMap()
            {
                for (int i = 0; i < Y; i++)
                {
                    for (int j = 0; j < X; j++)
                    {
                        if (tiles[i][j].objType == ObjectType.BLANK)
                        {
                            Console.Write(' ');
                        }
                        else if (tiles[i][j].objType == ObjectType.FOOD)
                        {
                            Console.Write('$');
                        }
                        else if (tiles[i][j].objType == ObjectType.SNAKE_HEAD)
                        {
                            Console.Write('@');
                        }
                        else if (tiles[i][j].objType == ObjectType.SNAKE_BODY)
                        {
                            Console.Write('#');
                        }
                        else if (tiles[i][j].objType == ObjectType.WALL)
                        {
                            if (tiles[i][j].trans.rotation.X == -1 && tiles[i][j].trans.rotation.Y == 0
                                || tiles[i][j].trans.rotation.X == 1 && tiles[i][j].trans.rotation.Y == 0)
                            {
                                Console.Write('|');
                            }
                            else if (tiles[i][j].trans.rotation.Y == -1 && tiles[i][j].trans.rotation.X == 0
                                || tiles[i][j].trans.rotation.Y == 1 && tiles[i][j].trans.rotation.X == 0)
                            {
                                Console.Write('-');
                            }
                            else
                            {
                                Console.Write('+');
                            }
                        }
                    }
                    Console.WriteLine();
                }
            }

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

                while (true)
                {
                    int ranX = rand.Next(0, X);
                    int ranY = rand.Next(0, Y);

                    if (tiles[ranY][ranX].objType == ObjectType.BLANK)
                    {
                        tiles[ranY][ranX].objType = ObjectType.FOOD;
                        break;
                    }
                }
            }

            public bool IsFoodExist()
            {
                for (int i = 0; i < Y; i++)
                {
                    for (int j = 0; j < X; j++)
                    {
                        if (tiles[i][j].objType == ObjectType.FOOD)
                        {
                            return true;
                        }
                    }
                }

                return false;
            }
        }

        class SnakeHead : Object
        {
            List<Object> body = new List<Object>();

            public SnakeHead(Vector2 startPos) : base(startPos, new Vector2Int(1, 0), ObjectType.SNAKE_HEAD)
            {
                body.Add(this);
                for (int i = 1; i < 4; i++)
                {
                    body.Add(new Object(new Vector2(trans.position.X - i, trans.position.Y), 
                        trans.rotation, ObjectType.SNAKE_BODY));
                }
            }

            public bool TryMove(Map map, Vector2Int dir)
            {
                bool isCrashed = false;

                Vector2 lastBodyPosition = body[body.Count - 1].trans.position;

                // 머리 다음 부분을 제외한 몸통 이동
                for (int i = body.Count - 1; i > 1; i--)
                {
                    body[i].trans.rotation = body[i - 1].trans.rotation;

                    body[i].trans.position = 
                        new Vector2(body[i].trans.position.X + body[i].trans.rotation.X,
                        body[i].trans.position.Y + body[i].trans.rotation.Y);

                    map.tiles[body[i].trans.position.Y][body[i].trans.position.X].objType = 
                        body[i].objType;
                }

                body[1].trans.rotation = trans.rotation;
                body[1].trans.position =
                    new Vector2(body[1].trans.position.X + body[1].trans.rotation.X,
                    body[1].trans.position.Y + body[1].trans.rotation.Y);

                map.tiles[body[1].trans.position.Y][body[1].trans.position.X].objType = 
                    body[1].objType;

                trans.rotation = dir;
                trans.position.X += trans.rotation.X;
                trans.position.Y += trans.rotation.Y;

                if (map.tiles[trans.position.Y + dir.Y][trans.position.X + dir.X].objType == 
                    ObjectType.WALL)
                {
                    isCrashed = true;
                }
                else if (map.tiles[trans.position.Y + dir.Y][trans.position.X + dir.X].objType ==
                    ObjectType.SNAKE_BODY)
                {
                    isCrashed = true;
                }
                else if (map.tiles[trans.position.Y + dir.Y][trans.position.X + dir.X].objType ==
                    ObjectType.FOOD)
                {
                    Object tail = body[body.Count - 1];
                    body.Add(new Object(new Vector2(tail.trans.position.X - tail.trans.rotation.X, tail.trans.position.Y - tail.trans.rotation.Y),
                        body[body.Count - 1].trans.rotation, ObjectType.SNAKE_BODY));
                    lastBodyPosition = body[body.Count - 1].trans.position;
                }

                map.tiles[trans.position.Y][trans.position.X].objType = objType;

                map.tiles[lastBodyPosition.Y][lastBodyPosition.X].objType = ObjectType.BLANK;

                return isCrashed;
            }
        }
    }
}

블랙잭

딜러와 겨루는 블랙잭 게임을 구현했습니다.

나의 조건 : 21이 목표, 21을 넘어가면 짐, 1~13의 카드뭉치 4개 => 52장

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace sparta
{
    class 블랙잭
    {
        static int cardCount = 3;
        static int targetSum = 21;

        static void Main(string[] args)
        {
            List<int> cardSet = new List<int>();
            int[] cards = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13 };
            for (int i = 0; i < 4; i++)
            {
                cardSet.AddRange(cards);
            }

            // 1 ~ 13
            List<int> playerCards = new List<int>();
            int playerSum = 0;

            List<int> dealerCards = new List<int>();
            int dealerSum = 0;

            Random rand = new Random();

            for (int i = 0; i < cardCount; i++)
            {
                int randomIndex = rand.Next(0, cardSet.Count);
                playerCards.Add(cardSet[randomIndex]);
                cardSet.RemoveAt(randomIndex);
                WriteCards(playerCards);

                randomIndex = rand.Next(0, cardSet.Count);
                dealerCards.Add(cardSet[randomIndex]);
                cardSet.RemoveAt(randomIndex);
                WriteCards(dealerCards);

                playerSum = CalculateSum(playerCards);
                dealerSum = CalculateSum(dealerCards);

                if (playerSum > targetSum && dealerSum > targetSum)
                {
                    Console.WriteLine("둘 다 합이 목표 숫자를 넘었습니다. 무승부입니다.");
                    break;
                }
                else if (dealerSum > targetSum)
                {
                    Console.WriteLine("플레이어의 승입니다.");
                    break;
                }
                else if (playerSum > targetSum)
                {
                    Console.WriteLine("딜러의 승입니다.");
                    break;
                }

                if (i == cardCount - 1)
                {
                    if (playerSum == dealerSum)
                    {
                        Console.WriteLine("합이 같습니다. 무승부입니다.");
                    }
                    else if (playerSum > dealerSum)
                    {
                        Console.WriteLine("플레이어의 승입니다.");
                    }
                    else
                    {
                        Console.WriteLine("딜러의 승입니다.");
                    }
                }
            }
        }

        private static void WriteCards(List<int> cards)
        {
            foreach (var card in cards)
            {
                Console.Write($"{card} ");
            }
            Console.WriteLine();
        }

        private static int CalculateSum(List<int> cards)
        {
            int sum = 0;
            foreach (var card in cards)
            {
                sum += card;
            }

            return sum;
        }
    }
}

 

오늘의 회고

 오늘은 강의 듣느랴 기능 구현 해보느랴 코드 이해하느랴 정신이 없었다. 하지만 하나하나 문제가 풀려가는 것을 보고 꽤나 보람을 느낀 하루였던 것 같다. 이렇게만 꾸준히 노력하면 나도 언젠가 코딩 고수가 되지 않을까 하는 기대를 품으며 내일도 달려보도록 하겠다.

 내일은 알고리즘 강의 듣는 것이 목표인데 먼저 알고리즘 분야 별로 접근법이나 빅오 표기법을 위주로 공부하도록 해야겠다. 내일도 파이팅!

 

 

참고 :

https://potatopotatopotato.tistory.com/15

 

C# 콘솔로 스네이크 게임 찍어내기

숙제라서 만들었다 만들고 보니까 껌뻑껌뻑 거리는데 예전에 WINAPI 할때 더블 버퍼링 생각이 났다 어렵진 않았는데 시간 박치기 구현 노가다라서 피곤했다.. 저녁까지 밖에 나갔다 왔는데 집에

potatopotatopotato.tistory.com

 

한 주 회고

깃 데스크탑만 쓰다가 깃 배쉬도 써보고

카드 게임 리소스를 위해 팀원들과 사진 공유도 해보고

연출적인 부분을 맡아 여러가지 시도를 해보았다. 씬 페이딩이나 원의 방정식을 이용한 카드 세팅이라던지 처음엔 이해하기 어려웠지만 구현하고 나서는 다음에 또 써먹어야지 하는 생각이 들었다.

ppt 작업은 정민님이 해주셔서 부담감 없이 발표를 할 수 있게 되었다. 압도적 감사!

 

다음 주 목표

C# 과제들이 많던데 최대한 이해하면서 구현하는 방향으로 진행해야겠다. C# 과제 구현이 끝난다면 개인 공부(최적화, 쉐이더, 게임 수학)도 간간히 진행할 생각이다.  

오늘의 학습 키워드

문자열

 

공부한 내용

문자열 형식 지정

string.format(string format, string args[]);

문자열 대소 비교

string str1 = "Apple";
string str2 = "Banana";
// compare < 0 일 땐 str1 < str2, compare == 0 일 땐 str1 == str2, compare > 0 일 땐 str1 > str2
// A가 B보다 작으므로 음수 나옴
int compare = string.Compare(str1, str2);

문자열 갱신

boardUI가 갱신이 되지 않는 버그 수정

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace sparta
{
    class 틱택토_게임
    {
        static void Main(string[] args)
        {
            int[] board = new int[9];
            int player = 1;
            int enemy = 2;
            Random random = new Random();

            string boardUI = $"-------\n-{board[0]}-{board[1]}-{board[2]}-\n-{board[3]}-{board[4]}-{board[5]}-\n-{board[6]}-{board[7]}-{board[8]}-\n-------";

            Console.WriteLine(boardUI);
            int user = 0;
            bool result = false;

            while (true)
            {
                int tempNum = -1;
                tempNum = ChooseNum(board);
                // 1 : 012 036 048
                // 2 : 012 147
                // 3 : 012 258 246
                // 4 : 345 036
                // 5 : 345 147 048 246
                // 6 : 345 258
                // 7 : 678 036 246
                // 8 : 678 147
                // 9 : 678 258 048

                // 012 345 678 036 147 258 048 246
                board[tempNum] = player;
                boardUI = $"-------\n-{board[0]}-{board[1]}-{board[2]}-\n-{board[3]}-{board[4]}-{board[5]}-\n-{board[6]}-{board[7]}-{board[8]}-\n-------";
                Console.WriteLine(boardUI);
                result = CheckBingo(board, tempNum);

                if (result)
                {
                    user = player;
                    break;
                }

                tempNum = SelectRandomNum(random, board);
                board[tempNum] = enemy;
                boardUI = $"-------\n-{board[0]}-{board[1]}-{board[2]}-\n-{board[3]}-{board[4]}-{board[5]}-\n-{board[6]}-{board[7]}-{board[8]}-\n-------";
                Console.WriteLine(boardUI);
                result = CheckBingo(board, tempNum);

                if (result)
                {
                    user = enemy;
                    break;
                }
            }

            WriteResult(user);
        }
    }
}

바보 같이 string을 갱신해주지 않아서 생긴 문제였다.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace sparta
{
    class 틱택토_게임
    {
        static void Main(string[] args)
        {
            int[] board = new int[9];
            int player = 1;
            int enemy = 2;
            Random random = new Random();

            string boardUI = $"-------\n-{board[0]}-{board[1]}-{board[2]}-\n-{board[3]}-{board[4]}-{board[5]}-\n-{board[6]}-{board[7]}-{board[8]}-\n-------";

            Console.WriteLine(boardUI);
            int user = 0;
            bool result = false;

            while (true)
            {
                int tempNum = -1;
                tempNum = ChooseNum(board);
                // 1 : 012 036 048
                // 2 : 012 147
                // 3 : 012 258 246
                // 4 : 345 036
                // 5 : 345 147 048 246
                // 6 : 345 258
                // 7 : 678 036 246
                // 8 : 678 147
                // 9 : 678 258 048

                // 012 345 678 036 147 258 048 246
                board[tempNum] = player;
                boardUI = $"-------\n-{board[0]}-{board[1]}-{board[2]}-\n-{board[3]}-{board[4]}-{board[5]}-\n-{board[6]}-{board[7]}-{board[8]}-\n-------";
                Console.WriteLine(boardUI);
                result = CheckBingo(board, tempNum);

                if (result)
                {
                    user = player;
                    break;
                }

                tempNum = SelectRandomNum(random, board);
                board[tempNum] = enemy;
                boardUI = $"-------\n-{board[0]}-{board[1]}-{board[2]}-\n-{board[3]}-{board[4]}-{board[5]}-\n-{board[6]}-{board[7]}-{board[8]}-\n-------";
                Console.WriteLine(boardUI);
                result = CheckBingo(board, tempNum);

                if (result)
                {
                    user = enemy;
                    break;
                }
            }

            WriteResult(user);
        }
    }
}

 

오늘의 회고

 오늘은 12시간 중 10~11시간 정도는 몰입한 것 같다. 구현이 오래 걸리는 문제나 내가 모르는 부분이 있어 정보를 찾는데 시간이 오래 걸렸는데 그 과정이 순탄치는 않았지만 새로 알게된 내용이 많아 재밌었던 것 같다.

 내일은 과제로 있는 스네이크 게임과 블랙잭 구현을 마무리 할 것이다.

+ Recent posts