오늘은 5주차 강의 알고리즘에 대해서 알아보고 정리를 진행한다.
알고리즘 정리는 좀 더 깊게 고민해야 하는 부분이 많아 오늘은 <알고리즘의 기본>만 작성하고,
이후 정리는 프로젝트 진행이 끝나고 시작할 생각이다.
[참고문서 : <쉽게 배우는 알고리즘>, <자바와 함께하는 자료구조의 이해>]
과제로 개인 프로젝트TextRPG의 필수 구현과 도전 구현을 시간이 되는 대로 진행할 것이며,
진행 과정과 트러블 슈팅을 위주로 정리해보았다.
정리 리마인드
아래 노션 링크에 배웠던 개념을 정리하였다.
아래는 금일 정리한 개념이다.
추가로 개인 프로젝트 구현 과정을 아래 노션에 정리하였다.
TextRPG 프로젝트 도전과제 구현 과정을 정리할 생각이다.
이전 필수 기능에 대한 구현은 위 노션 링크에 정리해두었고,
소스파일 또한 업로드 해두었다.
클래스로 이미 구현하였으니, 구조체로 만들어보자!
구조체에 대한 개념은 아래에 정리해 두었다
struct Item
{
public string Name { get; }
public int Attack { get; }
public int Defense { get; }
public string Description { get; }
public int Price { get; }
public bool Purchased { get; set; }
public bool Equipped { get; set; }
public Item(string name, int atk, int def, string desc, int price)
{
Name = name;
Attack = atk;
Defense = def;
Description = desc;
Price = price;
}
public string StatString()
{
if (Attack > 0) return $"공격력 +{Attack}";
if (Defense > 0) return $"방어력 +{Defense}";
return string.Empty;
}
}
이렇게 작성했는데
이럴 수가..
바로 생성자에 오류가 발생했다.

품절 상태와 장착 상태에 대한 초기화가 없어 그런 것 같다.
생성자로 구현할 때는 필요 없더만 구조체로 하니 왜 이런 에러가???
→ 구조체는 “값이 완전히 채워져야만 유효하다” 는 C# 언어 설계 원칙 때문에 사용자 정의 생성자에서Purchased·Equipped 같은 필드를 빠뜨리면 CS0171 (모든 필드를 할당해야 한다) 컴파일 오류가 발생한다.
TIP!
init 또는 readonly) 자동 프로퍼티를 선언하면,struct Item { public bool Equipped { get; init; } = false; } 처럼일단 생성자에 두 프로퍼티에 대한 초기화를 해주는 코드를 작성해서 해결했다.
근데 또 하나가 더 있었다.

이 CS0051 컴파일 에러는 뭘까…?
바로 마이크로소프트 홈페이지 문서로 찾아가보았다.

생략으로 인해 private가 되지 않도록 주의하라고…?
아! 위에서 보면 구조체 선언할 때 접근지정자를 생략해서 private로 되었다
구조체 선언에 public을 작성해주니 해결되었다.
근데 바로 문제가 생겼다.
아이템을 장착해도 상태 변화가 적용이 안된다.
두 가지를 의심해 볼만 하다
일단 다시 개념부터 생각해보자.
클래스와 구조체는 참조와 값 복사라는 점에서 차이가 있다.
→ 장착/구매할 때 로직을 바꾸어야할 수도?
→ 구입한 아이템이 인벤토리로 가는 부분도 생각해봐야한다.
그리고 리스트를 사용할 때도 박싱 문제도 있다. 일단 이건 덮어두자
그럼 장착할 때 코드에서 값 복사라는 점에서 코드를 수정해보자.
참조 형으로 했던 클래스와 달리 구조체는 값 복사이므로
구매나 장착 후 수정본을 재 할당 해야 한다.
따라서
//상점에서
_buyer.Gold -= item.Price;
item.Purchased = true;
_items[choice - 1] = item; //추가된 코드
_buyer.Inventory.AddItem(item);
Console.WriteLine("구매를 완료했습니다.");
//장착할 때
var item = _items[choice - 1];
item.Equipped = !item.Equipped;
_items[choice - 1] = item; // 수정본을 리스트에 다시 저장
Console.WriteLine(item.Equipped ? "장착했습니다." : "장착 해제했습니다.");
수정본을 리스트에 다시 저장해야 한다
이제 잘 돌아간다 🥲
배열로 관리하라는 말은 지금까지 리스트로 구현한 것들을 배열로 바꾸는 작업을 하면 되는 것 같다.
지금까지 리스트로 인벤토리에서의 아이템, 상점에서의 아이템을 관리하였다.
이걸 배열로 바꾼다면 먼저 리스트와 배열의 차이를 기반으로 설계 후 넘어가는 것이
위 구현할 때 겪었던 것처럼, 구조체로 바로 바꾸면서 오만가지 오류를 피할 수 있는 방법일 것 같다.
일단,
리스트와 달리 배열은 동적으로 용량을 관리할 수 없다
→ 상점과 인벤토리 슬롯 수를 고정 및 정의해야한다.
→ 그러면 비어있는 값은? 이것도 신경써서 구현해야한다.
또한 foreach문을 바꾸어야한다.
→이거야 뭐 for문 루프로 바꾸면 될 것 같다.
그리고 아이템 추가하는 메소드 additem()을 직접 구현해야한다
아…그리고 장착하는 메소드도 재구현해야한다. 배열은 실제 사용 범위를 관리하기 때문에 LINQ 메소드를 바로 쓸 수가 없다.
흠..리스트가 더 바람직한 구현인 것 같다.
아래는 배열로 재구현한 코드이다.
파일 링크(노션)
item에 새로 만들어주기만 하면 된다.

스파르타의 검을 만들어 주었다. 절대 위에 따라한거 아니다.
다음과 같은 기능을 추가해보자

자 설계를 해보자
휴식하기라는 기능이 생긴거니까
새로운 클래스에 새로운 메소드를 구현하면되고
메소드 호출 후 조건문을 통해 사용자 입력에 따라 2가지 행동을 구현하면될 것 같다.
그리고 플레이어의 체력과 골드를 참조해야 된다.
그럼 윤곽이 잡혔으니 구현해보자
우선 Rest 클래스를 생성해서 휴식 기능을 구현하자.
public class Rest
{
public readonly Character Player;
private int expense = 500;
public Rest(Character player)
{
Player = player;
}
public void rest()
{
while (true)
{
Console.Clear();
Console.WriteLine("휴식하기\n");
Console.WriteLine($"{expense}G 를 내면 체력을 회복할 수 있습니다.(보유골드 : {Player.Gold})");
Console.WriteLine("");
Console.WriteLine("1. 휴식하기");
Console.WriteLine("0. 나가기");
Console.WriteLine("\n원하시는 행동을 입력해 주세요 : ");
string input = Console.ReadLine();
if (input == "0")
{
break;
}
else if (input == "1")
{
if (Player.Gold >= expense)
{
Console.WriteLine("휴식을 완료하였습니다.");
Player.Gold -= expense;
Player.Health = 100; //set 의 private 없애기
}
else Console.WriteLine("Gold가 부족합니다");
}
else
{
Console.WriteLine("잘못된 입력입니다.");
}
Game.Pause();
}
}
}
여기서 큰 문제는 없었지만
플레이어의 체력을 참조하는데 있어서
프로퍼티를 다음과 같이 구현했어서 수정했다.
public int Health { get; private set; } = 100; //private 삭제
이후 메인 루프에 휴식 기능을 추가한다.
이 때 휴식 클래스의 메소드를 호출하기 위해 참조하는 것 또한 놓치면 안된다
다음 코드를 메인에 추가한다.
private Rest _rest;
....
...
_rest = new Rest(_player);
...
private void MainLoop()
{
while (true)
{
Console.Clear();
Console.WriteLine("스파르타 마을에 오신 여러분 환영합니다.");
Console.WriteLine("이곳에서 던전으로 들어가기 전 활동을 할 수 있습니다.\n");
Console.WriteLine("1. 상태 보기");
Console.WriteLine("2. 인벤토리");
Console.WriteLine("3. 상점");
Console.WriteLine("4. 휴식하기\n");//추가된 기능 출력
Console.Write("원하시는 행동을 입력해주세요: ");
string input = Console.ReadLine();
switch (input)
{
case "1":
_player.ShowStatus();
break;
case "2":
_player.InventoryMenu();
break;
case "3":
_shop.ShopMenu();
break;
case "4":
_rest.rest();//추가된 선택지
break;
default:
Console.WriteLine("\n잘못된 입력입니다.");
Pause();
break;
}
}
}
잘 실행된다. 🫠
노션 블록 링크 <- 소스파일 있음

oh….자 지금까지 했던 것처럼 설계부터 고민해보자
일단 보이는 것부터 생각해보자
판매하는 선택지를 만들고
판매하는 창을 호출하는 메소드를 구현하고
그 메소드에 구매하는 것처럼 번호를 입력받아 판매하고 골드를 받으면 될 것이다.
판매 가격은 기존 아이템 가격을 참조한 변수에 85/100을 곱하면 될 것이고 이 값을 다시 보유 골드에 넣으면 된다.
그리고 아이템 목록은 인벤토리에 보유 중인 아이템을 보여주어야 한다.
아 또 장착 중인 아이템을 판매하면 장착이 해제되니 이것도 분기를 생각해야한다.
일단 판매 메소드를 복붙해서 부분적으로 수정만 하면 될 것 같다.
if (int.TryParse(input, out int choice) && choice >= 1 && choice <= _items.Count)
{
var item = _items[choice - 1];
//필요 없는 부분은 주석처리
//if (item.Purchased)
//{
// Console.WriteLine("이미 구매한 아이템입니다.");
//}
//else if (_buyer.Gold >= item.Price)
//{
_buyer.Gold += item.Price * (85 / 100); //수정된 코드
item.Purchased = false; //수정된 코드 : true -> false
if (item.Equipped) //장착 중인 아이템이면
{
//장착 해체 및 리스트에 아이템 제거
}
else
{
//리스트에 아이템 제거
}
//}
//else
//{
Console.WriteLine("Gold 가 부족합니다.");
//}
조건문을 채우면 될 것 같은데..
리스트에 아이템을 제거해야 하니 새로운 메소드를 생성해야한다.
public void AddItem(Item item) => _items.Add(item);
//아이템 판매를 위한 아이템 제거 메소드
public void DeleteItem(Item item) => _items.Remove(item);
또한 아이템 목록을 인벤토리에서 가져와야한다.
구현은 했는데 버그가 생겼다.
판매하는 아이템의 판매가격이 0으로 되버린다.

원인은 85%가격을 만드는 코드에 있었다.
C#에서 85/100은 정수 나눗셈으로 계산되어 0이 되기 때문이다.

위 코드에서
((float)item.Price)*(85/100) 을 바꿔주자.
for (int i = 0; i < _buyer.Inventory._items.Count; i++)
{
var item = _items2[i];
int sellPrice = (int)(item.Price * 0.85f);
string equipTag = item.Equipped ? "[E]" : "";
Console.WriteLine($"- {i + 1} {equipTag}{item.Name,-14} | {item.StatString()} | {item.Description} | {sellPrice}G");
}
Console.WriteLine("\n0. 나가기");
Console.Write("\n원하시는 행동을 입력해주세요: ");
string input = Console.ReadLine();
if (input == "0") return;
if (int.TryParse(input, out int choice) && choice >= 1 && choice <= _items2.Count)
{
var item = _items2[choice - 1];
int sellPrice = (int)(item.Price * (85f / 100f));
if (item.Equipped) //장착 중인 아이템이면
{
//장착 해체 및 리스트에 아이템 제거
item.Equipped = false;
_buyer.Gold += sellPrice;
_buyer.Inventory.DeleteItem(item);
Console.WriteLine("판매를 완료했습니다.");
}
else
{
//리스트에 아이템 제거
_buyer.Gold += sellPrice;
_buyer.Inventory.DeleteItem(item);
Console.WriteLine("판매를 완료했습니다.");
}
}
else
{
Console.WriteLine("잘못된 입력입니다.");
}
이제 문제없다
다만 구입하고 다시 판매한 상품이 여전히 구매 완료로 표시되어 있는 문제가 발생했다.
판매한 상품은 다시 상점에서 구매가 가능한 상태로 만들어야 한다.
그러면 상점의 아이템 리스트를 참조하여 Purchased 값을 false 로 만들어 주면 된다.
구현 완료! 😵💫
알고리즘 강의를 통해서 알고리즘 기본 개념에 대해서 복습할 수 있었다.
TextRPG 프로젝트 개발 과정에서, 크고 작은 문제를 만나면서 학습자의 관점이 아닌 개발자의 관점에서 이 문제를 어떻게 바라보는지에 대해 고민하면서 어떻게든 스스로의 힘으로 해결해보려고 노력했다.
엉뚱한 삽질같은 고민과 접근법이 많았지만, 굳은 살이 배기고 어딜 파야하는이제 대한 인사이트가 조금씩 트이는 느낌이다.
개발을 통해서 지식으로 기억한 문법들이 "내 것"으로 체화되는 것 같다. 마냥 기억하고 있는 문법은 다시 찾아보고 비교하면서 구현하지만, 내 것이 된 것들은 그냥 자동을 구현이 되고 문제가 생겨도 바로 핵심적인 문제를 잡아내는 것 같다.
내일은 도전 과제( 장착 개선, 레벨업 기능, 던전 입장 기능, 게임 저장)을 구현하고 알고리즘 정리를 시작할 생각이다.