본문 바로가기

C++/문제풀이 기록

[BOJ 26076, 무난함] RPG Extreme (C++)

 

구현량으로 유명한 문제다

정말 구현만 하면 되는 무난한 난이도이지만 실수 한번 하면 익스트림한 경험을 하게 된다

어떤 경우에 스파게티 코드가 생기는지 잘 알 수 있었다

하지만 차근차근 구현하고 자잘한 실수를 하지 않는다면 어렵지 않은 문제이다

 

그래서 아래의 과정을 거쳐 코드를 작성하며 실수를 하면 안된다.

 

1. 끝까지 읽고 계획하기

2. 실수 줄이기

3. 적절한 테스트 코드 삽입과 디버깅

 

ps가 아닌 실제 개발 프로젝트 진행 시에는 더욱 그렇기 때문에

다이어그램 / 명세서 / OOP / 테스트방법론 / 디버깅 툴 등등을 활용하는 것 같다....


코드를 다 짰는데 어느 부분에서 틀렸는지 모르겠어요 ㅠㅠ

만일 당신이 디버깅을 위해 이 글을 검색해 들어왔다면 내 글엔 별로 도움될 내용이 없으니 백준 질문게시판이나 뒤져봐라


어떤 경우에 스파게티 코드가 생기냐

내가 문제를 푼 순서인데 이렇게 하면 디버깅할 때 익스트림한 경험을 하게 된다

 

1. 입출력 보고 때려맞추는 식으로 전역변수를 이것저것 만들었다

2. 만들다 보니깐 변수가 더 필요하고 함수가 더 필요하다!! 그래서 변수를 더 만들고 함수도 더 만들고 기존 변수명이 어울리지 않는 것 같아서 기존 변수명을 새롭게 바꿨다

3. 만들다 보니깐 액세서리의 종류는 비트마스킹으로 관리하는 게 좋을 것 같고 몹/아이템/플레이어를 구조체로 관리하는 게 나을 것 같다!! 그래서 기존 변수들을 struct 안으로 집어넣고 struct를 관리하기 위한 함수를 추가로 만들었다

4. 만들다 보니깐 문제에서 요구하는 자잘자잘한 사항들이 내 코드와 맞지 않다!! 그런데 내 코드는 이미 5000B를 넘어가서 고치는 것도 쉽지 않다

5. 그래서 코드를 갈아치우고 새로 짰다

 

위와 같은 경험을 통해, 사전에 계획을 제대로 세우고 큰 틀을 잡아놓지 않으면 나중 가서 좃됨을 알 수 있었다


올바르게 문제에 접근하기

이 글을 읽고 있는 이상 백준에 나와 있는 문제 설명은 다 읽어보고 이해했을 거라고 생각하고 글을 쓰겠다

 

큰 덩어리를 파악하고 class나 struct를 만들어 객체지향으로 관리해야 실수를 줄일 수 있다

이 문제의 경우 이녀석들이 가장 큰 3개 덩어리니까 이녀석들로 struct를 만들자

enum buffs{HR,RE,CO,EX,DX,HU,CU};

struct player
{
    int R,C;
    
    int hp=20,atk=2,def=2,level=1,exp=0,weapon=0,armor=0;
    int buff=0;
};

struct mob
{
    int R,C,hp,atk,def,exp;
    string name;
    mob(int r,int c,string s,int w,int a,int h,int e){
        R=r;
        C=c;
        name=s;
        atk=w;
        def=a;
        hp=h;
        exp=e;
    }
};

struct item{
    int R,C,p=0,buff=0;
    char type=0;
    item(int r,int c,char t,string s){
        R=r;
        C=c;
        type=t;
        if(type=='W'){
            p=stoi(s);
        }
        else if(type=='A'){
            p=stoi(s);
        }
        else if(type=='O'){
            if(s=="HR"){buff=(1<<(int)HR);}
            else if(s=="RE"){buff=(1<<(int)RE);}
            else if(s=="CO"){buff=(1<<(int)CO);}
            else if(s=="EX"){buff=(1<<(int)EX);}
            else if(s=="DX"){buff=(1<<(int)DX);}
            else if(s=="HU"){buff=(1<<(int)HU);}
            else if(s=="CU"){buff=(1<<(int)CU);}
        }
    }
};

map<pii,mob> mobs;
map<pii,item> items;

그래서 기본 struct의 틀 3개를 이렇게 잡았다.

buff가 현재 보유한 액세서리 종류라고 생각하면 되고, 이 부분을 실수 없이 관리하기 위해 enum을 사용했다

그리고... mobs와 items 라는 이름의 map을 만들어 활용했는데. pair를 키로 사용할 경우 원소를 추가하거나 검색할 때 알 수 없는 동작을 해서 좀 애먹었다. 이 부분은 별개의 글로 분리해서 다음에 찾아보기 쉽게 해 놓을 예정

더보기

아래의 코드를 수정하며 실행해 보면 내가 어느 부분에서 애먹었는지 알 수 있다. 아마도 pair을 map의 key로 사용할 경우에 map의 operator []가 내 예상 밖의 연산을 수행하면서 오류가 발생하는 듯 하다 그래서 at 함수나 insert나 insert_or_assign함수를 활용해 map에 접근해야 했다. 아니면 operator []을 오버라이딩해서 사용해야 할 것이다

// #include <bits/stdc++.h>
// pair와 map을 같이 사용할 경우 map [] 가 제대로 작동하지 않음을 보여주는 코드
#include <iostream>
#include <map>
#define fastio cin.tie(0)->sync_with_stdio(0)
#define ll long long
#define pii pair<int,int>
using namespace std;

class mc
{
public:
    int A;
    int B;
    string S;
    mc(int a,int b,string s){
        A=a;
        B=b;
        S=s;
    };
};

struct ms
{
    int A;
    int B;
    string S;
    ms(int a,int b,string s){
        A=a;
        B=b;
        S=s;
    };
};


int main(void) {
    fastio;
    mc tc(1,2,"asd");
    ms ts(1,2,"asd");
    map<pii,mc> mmc;
    map<pii,ms> mms;

    // 정상적으로 작동
    // mmc.insert_or_assign({1,2},tc);
    // mms.insert_or_assign({1,2},ts);

    // 정상적으로 작동
    // mmc.insert({{1,3},tc});
    // mms.insert({{1,3},ts});

    // 에러
    // mmc[{1,2}]=tc;
    // mms[{1,2}]=ts;

    // 정상적으로 작동
    // cout<<mmc.at({1,2}).A<<endl;
    // cout<<mms.at({1,2}).B<<endl;

    // 에러
    // cout<<mmc[{1,2}].A<<endl;
    // cout<<mms[{1,2}].B<<endl;

	return 0;
}

struct로 기본적인 틀을 잡았다면 요소들 간 상호작용을 위한 함수를 만들어야 한다.

플레이어가 몹과 싸우면 hp나 level이나 exp가 변경되기 때문에 플레이어 정보를 참조하도록 해야 한다

플레이어가 아이템을 얻으면 weapon이나 armor이나 buff가 변경되기 때문에 플레이어 정보를 참조하도록 해야 한다

 

그래서 코드를 보면

int getitem(player& a, item& b){
    if(b.type=='W'){
        a.weapon=b.p;
        return 1;
    }
    else if(b.type=='A'){
        a.armor=b.p;
        return 1;
    }
    else if(b.type=='O'){
        if(a.buffcnt>=4)return 0;
        if(a.buff&b.buff){
            return 0;
        }
        a.buff+=b.buff;
        a.buffcnt++;
        return 1;
    }
    return 0;
}
int battle(player& a,mob& b){
    int attack_turn = 0;
    int turn = 1;
    int ahp=a.hp,bhp=b.hp;
    if(b.R==boss_R&&b.C==boss_C){
        if(a.buff&(1<<(int)HU)){
            ahp=a.max_hp;
        }
    }
    while (ahp>0&&bhp>0)
    {
        if(ahp==0||bhp==0)break;
        if(!attack_turn){
            int dmg=a.atk+a.weapon;
            if(turn==1){
                if(a.buff&(1<<(int)CO)){
                    if(a.buff&(1<<(int)DX)){
                        dmg=dmg*3;
                    }
                    else{
                        dmg=dmg*2;
                    }
                }
            }
            bhp-=max(1,dmg-b.def);
            // cout<<"myattack"<<max(1,dmg-b.defense)<<" "<<bhp<<"\n";
            attack_turn=!attack_turn;
        }
        else{
            if(b.R==boss_R&&b.C==boss_C&&turn==2&&a.buff&(1<<(int)HU)){
                attack_turn=!attack_turn;
            }
            else{
                ahp-=max(1,b.atk-(a.def+a.armor));
                // cout<<"enermy attack"<<max(1,b.attack-(a.defense+a.armor))<<" "<<ahp<<"\n";
                attack_turn=!attack_turn;
            }
        }
        turn++;
    }
    if(attack_turn){
        a.hp=ahp;
        int texp = b.exp;
        if(a.buff&(1<<(int)EX)){
            texp = floor(1.2*texp);
        }
        a.exp+=texp;
        if(a.buff&(1<<(int)HR)){
            a.hp+=3;
            a.hp=min(a.hp,a.max_hp);
        }
        a.levelup();
        return 1;
    }
    else{
        if(a.buff&(1<<(int)RE)){
            a.buffcnt--;
            a.buff-=(1<<(int)RE);
            a.R=a.R_begin;
            a.C=a.C_begin;
            a.hp=a.max_hp;
            return 0;
        }
        a.hp=0;
        a.isalive=0;
        printgame(a);
        cout<<"YOU HAVE BEEN KILLED BY "<<b.name<<"..\n";
        exit(0);
    }
    return 0;
}

이렇게 짜고 액세서리의 효과도 반영했다

함수가 올바르게 종료되면 1을 리턴하고, 올바르게 종료되지 않으면 0을 리턴하도록 했다

(게임 프로그래밍 코드 보면 스크립트를 죄다 if else 틀 안에 넣고 하던데 이 역시 버그를 줄이기 위함이라는 생각이 들었다)

 

그리고 입력(위 아래 좌 우)에 의한 플레이어의 이동에 따라 플레이어와 지도 위 사물들이 상호작용하도록 move 함수를 만들었다

bool move(player& a,char input){
    passed_turns++;
    int NR=a.R,NC=a.C;
    if(input=='L'){
        NC--;
    }
    else if(input=='R'){
        NC++;
    }
    else if(input=='U'){
        NR--;            
    }
    else if(input=='D'){
        NR++;
    }
    else return false;
    if(NR<=0||NC<=0||NR>N||NC>M){NR=a.R;NC=a.C;}
    if(arr[NR][NC]=='#'){//벽
        NR=a.R;NC=a.C;
    }
    int R=NR;int C=NC;
    a.R=R;a.C=C;
    if(arr[R][C]=='&'){//몹
        if(battle(a,mobs.at({R,C}))){
            arr[R][C]='.';
        }
        return 1;
    }
    if(arr[R][C]=='B'){//상자
        getitem(a,items.at({R,C}));
        arr[R][C]='.';
        return 1;
    }
    if(arr[R][C]=='^'){//스파이크
        if(a.buff&(1<<(int)DX)){//DX가 있다면 스파이크 데미지 1
            a.hp-=1;
        }
        else{
            a.hp-=5;
        }
        if(a.hp<=0){
            if(a.buff&(1<<(int)RE)){
                a.buff-=(1<<(int)RE);
                a.buffcnt--;
                a.hp=a.max_hp;
                a.R=a.R_begin;
                a.C=a.C_begin;
                return 1;
            }
            a.isalive=0;
            printgame(a);
            cout<<"YOU HAVE BEEN KILLED BY SPIKE TRAP.."<<"\n";
            exit(0);
        }
        return 1;
    }
    if(arr[R][C]=='M'){//보스
        if(battle(a,mobs.at({R,C}))){
            printgame(a);
            cout<<"YOU WIN!\n";
            exit(0);
        }
        return 1;
    }
    return 0;
}

지도를 탈출하지 않고 벽을 뚫지 않도록 플레이어의 위치를 수정한 뒤, 그 위치에 무엇이 있는가에 따라 플레이어와 사물이 상호작용하도록 코드를 짰다

마찬가지로 정상 동작 시 1을 리턴하고 이상하면 0을 리턴한다

 

마지막으로 게임의 현재 상황을 출력 형식에 맞게 출력하도록 printgame()함수를 만들었다

void printgame(player& a){
    for (int i = 1; i <= N; i++)
    {
        for (int j = 1; j <= M; j++)
        {
            if(a.isalive&&a.R==i&&a.C==j)cout<<'@';
            else cout<<arr[i][j];
        }
        cout<<"\n";
    }
    cout<<"Passed Turns : "<<passed_turns<<"\n";
    cout<<"LV : "<<a.level<<"\n";
    cout<<"HP : "<<a.hp<<"/"<<a.max_hp<<"\n";
    cout<<"ATT : "<<a.atk<<"+"<<a.weapon<<"\n";
    cout<<"DEF : "<<a.def<<"+"<<a.armor<<"\n";
    cout<<"EXP : "<<a.exp<<"/"<<a.level*5<<"\n";
}

이후 자잘자잘한 부분을 건드려 코드를 완성했고

헤메지 않도록 큰 틀을 잡아가면서 코드를 작성하니 사소한 문제는 있었으나 이후의 디버깅이 크게 어렵지 않았다

 

테스트케이스를 콘솔로 실행하고 버그를 잡는 과정도 알아보기가 힘들었는데,

cph(competitive programming helper) 확장 프로그램을 활용해 확인하니 그나마 나았다

cph를 활용할 경우 vscode 환경에서 이렇게 다수의 테스트케이스를 한번에 돌려볼 수 있다

 

아무튼 얻은 교훈은 이거다

1. 문제를 제대로 읽고 큰 틀만큼은 잘 잡아놔야 나중에 뭘 추가하거나 디버깅하기 편함

2. 실제 프로젝트의 경우엔 다이어그램이나 명세서나 객체지향을 적극적으로 활용해라

3. 다양한 툴의 도움을 받아라 (cph, 디버거)

4. 이거 아닌거같은데 생각이 들면 그냥 코드를 갈아치우자


그래서 전체 코드는 아래와 같음

적당히 돌아가는 걸 보고 구조 개선을 포기해서 내 코드도 그렇게 깔끔하지는 않다

아무튼 풀었으니 됐지 뭐

#include <bits/stdc++.h>
#define fastio cin.tie(0)->sync_with_stdio(0)
#define ll long long
#define pii pair<int,int>
using namespace std;

int N,M,passed_turns;
int boss_R,boss_C;
char arr[101][101];
enum buffs{HR,RE,CO,EX,DX,HU,CU};

struct player
{
    int R,C;
    
    int hp=20,atk=2,def=2,level=1,exp=0,weapon=0,armor=0;
    int buff=0;
    
    int max_hp=20;
    int buffcnt=0;
    int R_begin=0;
    int C_begin=0;
    int isalive=1;

    int levelup(){
        if(level*5>exp)return 0;
        level++;
        exp=0;
        max_hp+=5;
        hp=max_hp;
        atk+=2;def+=2;
        return 1;
    }
};

struct mob
{
    int R,C,hp,atk,def,exp;
    string name;
    mob(int r,int c,string s,int w,int a,int h,int e){
        R=r;
        C=c;
        name=s;
        atk=w;
        def=a;
        hp=h;
        exp=e;
    }
};

struct item{
    int R,C,p=0,buff=0;
    char type=0;
    item(int r,int c,char t,string s){
        R=r;
        C=c;
        type=t;
        if(type=='W'){
            p=stoi(s);
        }
        else if(type=='A'){
            p=stoi(s);
        }
        else if(type=='O'){
            if(s=="HR"){buff=(1<<(int)HR);}
            else if(s=="RE"){buff=(1<<(int)RE);}
            else if(s=="CO"){buff=(1<<(int)CO);}
            else if(s=="EX"){buff=(1<<(int)EX);}
            else if(s=="DX"){buff=(1<<(int)DX);}
            else if(s=="HU"){buff=(1<<(int)HU);}
            else if(s=="CU"){buff=(1<<(int)CU);}
        }
    }
};

map<pii,mob> mobs;
map<pii,item> items;
int mobs_size;
int items_size;

void printgame(player& a){
    for (int i = 1; i <= N; i++)
    {
        for (int j = 1; j <= M; j++)
        {
            if(a.isalive&&a.R==i&&a.C==j)cout<<'@';
            else cout<<arr[i][j];
        }
        cout<<"\n";
    }
    cout<<"Passed Turns : "<<passed_turns<<"\n";
    cout<<"LV : "<<a.level<<"\n";
    cout<<"HP : "<<a.hp<<"/"<<a.max_hp<<"\n";
    cout<<"ATT : "<<a.atk<<"+"<<a.weapon<<"\n";
    cout<<"DEF : "<<a.def<<"+"<<a.armor<<"\n";
    cout<<"EXP : "<<a.exp<<"/"<<a.level*5<<"\n";
}
int getitem(player& a, item& b){
    if(b.type=='W'){
        a.weapon=b.p;
        return 1;
    }
    else if(b.type=='A'){
        a.armor=b.p;
        return 1;
    }
    else if(b.type=='O'){
        if(a.buffcnt>=4)return 0;
        if(a.buff&b.buff){
            return 0;
        }
        a.buff+=b.buff;
        a.buffcnt++;
        return 1;
    }
    return 0;
}
int battle(player& a,mob& b){
    int attack_turn = 0;
    int turn = 1;
    int ahp=a.hp,bhp=b.hp;
    if(b.R==boss_R&&b.C==boss_C){
        if(a.buff&(1<<(int)HU)){
            ahp=a.max_hp;
        }
    }
    while (ahp>0&&bhp>0)
    {
        if(ahp==0||bhp==0)break;
        if(!attack_turn){
            int dmg=a.atk+a.weapon;
            if(turn==1){
                if(a.buff&(1<<(int)CO)){
                    if(a.buff&(1<<(int)DX)){
                        dmg=dmg*3;
                    }
                    else{
                        dmg=dmg*2;
                    }
                }
            }
            bhp-=max(1,dmg-b.def);
            // cout<<"myattack"<<max(1,dmg-b.defense)<<" "<<bhp<<"\n";
            attack_turn=!attack_turn;
        }
        else{
            if(b.R==boss_R&&b.C==boss_C&&turn==2&&a.buff&(1<<(int)HU)){
                attack_turn=!attack_turn;
            }
            else{
                ahp-=max(1,b.atk-(a.def+a.armor));
                // cout<<"enermy attack"<<max(1,b.attack-(a.defense+a.armor))<<" "<<ahp<<"\n";
                attack_turn=!attack_turn;
            }
        }
        turn++;
    }
    if(attack_turn){
        a.hp=ahp;
        int texp = b.exp;
        if(a.buff&(1<<(int)EX)){
            texp = floor(1.2*texp);
        }
        a.exp+=texp;
        if(a.buff&(1<<(int)HR)){
            a.hp+=3;
            a.hp=min(a.hp,a.max_hp);
        }
        a.levelup();
        return 1;
    }
    else{
        if(a.buff&(1<<(int)RE)){
            a.buffcnt--;
            a.buff-=(1<<(int)RE);
            a.R=a.R_begin;
            a.C=a.C_begin;
            a.hp=a.max_hp;
            return 0;
        }
        a.hp=0;
        a.isalive=0;
        printgame(a);
        cout<<"YOU HAVE BEEN KILLED BY "<<b.name<<"..\n";
        exit(0);
    }
    return 0;
}

bool move(player& a,char input){
    passed_turns++;
    int NR=a.R,NC=a.C;
    if(input=='L'){
        NC--;
    }
    else if(input=='R'){
        NC++;
    }
    else if(input=='U'){
        NR--;            
    }
    else if(input=='D'){
        NR++;
    }
    else return false;
    if(NR<=0||NC<=0||NR>N||NC>M){NR=a.R;NC=a.C;}
    if(arr[NR][NC]=='#'){//벽
        NR=a.R;NC=a.C;
    }
    int R=NR;int C=NC;
    a.R=R;a.C=C;
    if(arr[R][C]=='&'){//몹
        if(battle(a,mobs.at({R,C}))){
            arr[R][C]='.';
        }
        return 1;
    }
    if(arr[R][C]=='B'){//상자
        getitem(a,items.at({R,C}));
        arr[R][C]='.';
        return 1;
    }
    if(arr[R][C]=='^'){//스파이크
        if(a.buff&(1<<(int)DX)){//DX가 있다면 스파이크 데미지 1
            a.hp-=1;
        }
        else{
            a.hp-=5;
        }
        if(a.hp<=0){
            if(a.buff&(1<<(int)RE)){
                a.buff-=(1<<(int)RE);
                a.buffcnt--;
                a.hp=a.max_hp;
                a.R=a.R_begin;
                a.C=a.C_begin;
                return 1;
            }
            a.isalive=0;
            printgame(a);
            cout<<"YOU HAVE BEEN KILLED BY SPIKE TRAP.."<<"\n";
            exit(0);
        }
        return 1;
    }
    if(arr[R][C]=='M'){//보스
        if(battle(a,mobs.at({R,C}))){
            printgame(a);
            cout<<"YOU WIN!\n";
            exit(0);
        }
        return 1;
    }
    return 0;
}

int main(void) {
    fastio;
    player P;
    string moves;
    cin>>N>>M;
    for (int i = 1; i <= N; i++)
    {
        for (int j = 1; j <= M; j++)
        {
            cin>>arr[i][j];
            if(arr[i][j]=='@'){
                P.R=i;
                P.C=j;
                P.R_begin=i;
                P.C_begin=j;
                arr[i][j]='.';
            }
            else if(arr[i][j]=='M'||arr[i][j]=='&'){
                mobs_size++;
                if(arr[i][j]=='M'){
                    boss_R=i;
                    boss_C=j;
                }
            }
            else if(arr[i][j]=='B'){
                items_size++;
            }
        }
    }
    cin>>moves;
    for (int i = 0; i < mobs_size; i++)
    {
        int R,C,W,A,H,E;string S;
        cin>>R>>C>>S>>W>>A>>H>>E;
        mob tmp(R,C,S,W,A,H,E);
        mobs.insert_or_assign({R,C},tmp);
    }
    for (int i = 0; i < items_size; i++)
    {
        int R,C;char T;string S;
        cin>>R>>C>>T>>S;
        item tmp(R,C,T,S);
        items.insert_or_assign({R,C},tmp);
    }
    for (char i:moves)
    {
        move(P,i);
    }
    printgame(P);
    cout<<"Press any key to continue.";
	return 0;
}