Russian Roulette 미니게임 만들기

김준호·2024년 6월 21일

프로젝트 주제 선정

프로젝트 주제는 만들고싶은 프로그램 만들기였다.
파이썬을 배우기 시작하고 문법만 다 끝맞친 상태에서 너무 고민되었다.
내가 가진 이 재료들을 활용해서 최고의 OUTPUT을 뽑고싶고, 게다가 제작 난이도도 너무 쉽지 않고, 최대한 내가 가진 기술들을 활용해볼 수 있는게 뭐가 있을까 하는 와중에 유튜브를 뒤적거리다 '벅샷 룰렛'이라는 게임 영상을 보게 되었다.
이거다! 러시안 룰렛 자체의 로직은 생각보다 단순할 수 있는데, 이 '벅샷 룰렛'이라는 게임은 단순한 러시안 룰렛에 아이템을 제공해서 게임을 조금 더 전략적으로 생각할 수 있게 해주었다.
물론 실제 '벅샷 룰렛'이라는 게임은 기존 러시안 룰렛과는 달리 리볼버가 아닌 샷건으로 진행하면서 랜덤한 장탄수에 실탄과 공포탄을 채워넣으면서 플레이어 입장에서 계산할 것도 생기고 아이템도 생각하면서 플레이를 해야하다보니 훨씬 생각할게 많지만, 일단 저 '아이템'이라는 것을 사용할 수 있는 것에 포커스를 두고 제작해야겠다는 생각이 들었다.

첫 걸음.

일단 아무렇게나 막 두들겨 때려넣어보았다. 최대한 현재의 내 머릿 속에서 러시안 룰렛을 플레이할 때의 고려해야할 점을 생각해서 일단 코드부터 때려넣어봤는데, 결과는 당연하게도 에러 천지에, 원하는 출렵값도 나오지 않았다.
그래서 생각을 해봤다. 그림을 그릴 때도 스케치라는 작업을 거치는데, 일단 내가 구현해야할 부분들을 적어보자.


이렇게 하나하나 최대한 세분화를 하면서 적어봤다.
적어보니 조금이나마 어떤식으로 코드를 짜야할지 감이 잡혀 코드를 적었다.

스케치를 바탕으로 그려넣기

처음에 짰던 코드다 스케치에서 생각했던대로 하려고 했는데 심심해서 매 턴마다 챔버를 돌리는 형식으로 게임을 진행하려고 했는데 너무 루즈하고 판이 너무 길었다. 또한, 그림에서 보다싶이 아직 체크아이템은 넣지를 않았는데 이유는 어떤 방식으로 체크 아이템을 사용하는게 좋을지 감이 안왔다.

'벅샷 룰렛' 이라는 게임에서는 돋보기를 사용해서 샷건 약실에 들어있는 총알을 알 수 있게 해준다. 그리고 정해진 위치에 총알이 고정되어있다보니 알려주고 방아쇠를 당겨도 총알위치가 변하지 않아 보여준 그 값으로 나왔다. 그러나 매 턴마다 총알위치가 바뀌다보니 어떻게 알려줘야할지 감이 너무 안왔다.

또한, 체크아이템을 넣지 않은 상태에서 패스아이템만 주어지다보니 패스 아이템의 전략적인 사용이 너무 운이 되어버려 사용하더라도 이득이 확실히 느껴지지 않았다.
그래서 다시 짜보았다.

약간의 보완.

def set_game_pve():                                 # PVE 게임 초기 설정 함수
    item = {"pass" : 2, "check" : 1}                # 유저는 패스 2개, 체크 1개를 갖고 시작하기
    bullet_position = random.randint(1, 6)          # 총알 위치는 기존 러시안 룰렛처럼 리볼버를 바탕으로 1~6까지의 값에 랜덤으로 넣기
    return item, bullet_position
def use_pass_pve(item, turn):                                   # PVE 패스 아이템 사용 함수
    if item["pass"] > 0:                                        # 아이템 딕셔너리에 pass라는 키의 value가 0보다 크면 사용할 수 있도록 하기
        print("PASS 사용으로 이번턴을 넘어갑니다.")
        item["pass"] -= 1                                       # 아이템 딕셔너리의 pass키의 value를 하나 줄이기
        return turn + 1
    else:                                                       # pass 의 value가 0이 되었을 떄는 더 이상 사용하지 못하도록 하기
        print("PASS를 더 이상 사용할 수 없습니다.")
    return turn
def use_check_pve(bullet_position, item):                               # PVE 체크 아이템 사용 함수
    if item["check"] > 0:                                               # 아이템 딕셔너리에 check 의 value 값을 체크
        for i in range(5):                                              # time.sleep() 메소드를 활용하여 중간중간 출력값의 지연을 넣어 값 출력시 실제 확인하는 것처럼 느끼게 하기
            if i == 2:
                print("총을 확인하는 중입니다.")
                time.sleep(0.3)
            elif i == 4:
                print("*")
                print("")
                time.sleep(1.0)
            else:
                print("*")
                time.sleep(0.3)
        if bullet_position == 1:                                         # bullet_position이 1이면 check사용 후 shoot을 하면 사망하므로 쏘면 안된다는 메세지 출력
            print("쏘면 안됩니다.")
            item["check"] -= 1
        elif (bullet_position > 1) and (bullet_position < 4):            # bullet_position이 사망하기까지 2 ~ 3턴 정도의 여유가 있으면 아직은 괜찮다는 메세지 출력 
            print("아직 괜찮습니다.")
            item["check"] -= 1
        else:
            print("아주 괜찮습니다.")                                    # bullet_position이 4 ~ 6 턴 정도의 여유가 있으면 아주 괜찮다고 출력
            item["check"] -= 1
    else:
        print("CHECK를 더 이상 사용할 수 없습니다.")

처음에 스케치를 바탕으로 넣었던 코드에서 수정이 훨씬 원활하게 이뤄질 수 있도록 러시안 룰렛 게임 함수에 다 때려박지 않고 게임 시작 설정, 패스 아이템 사용, 체크 아이템 사용 함수를 따로 만들었다.

게임 시작 설정에서 과감히 매 턴마다 챔버를 돌리는 행위는 뺐다. 위에서 적어놓았다시피 게임이 너무 루즈해지고 피곤해지기 때문에 실제 러시안 룰렛처럼 리볼버 6개의 약실에 한 곳에다가 총알을 넣고 돌리는 방법처럼 랜덤변수를 구현해 한 턴마다 그 랜덤변수 값에서 - 1을 해 0이 되면 사망할 수 있게 구현했다.

이렇게 구현하니 체크아이템을 어떤 로직으로 그리고 구현해야할지 감이 잡혀 짜보았다.
체크 아이템 사용 함수 중간에 time.sleep()을 포문으로 돌면서 유저 입장에서 실제로 총을 확인해 알려주는 것처럼 구현해보았다.

이제 여기서 혼자하는게 아닌 각 함수에다가 붙여놓은 것처럼 pve 형식을 만들고 싶었다.

그리고 한계에 부딪혔다.

pve로 게임을 제작하려면 턴이라는 개념을 구현해야하는데 도대체 어떻게 구현해야할지 감조차도 안왔다.
어떤 개념으로 턴이 돌아가는 건지 그 로직이 상상조차 되지 않아 구글링을 했다.
간단하게 게임을 만드신 분들이 올려놓은 코드를 보면서 턴이라는 개념을 어떻게 구현했는지 이해하고 만들어내어 턴 계산해주는 함수도 넣었다.

def calculate_turn_pve(turn):            # PVE 턴 계산해주는 함수
    players = ["user", "bot"]
    return players[turn % 2]        # 턴을 사망하거나 패스하는 순간에 + 1 을 해주어 turn % 2 의 값이 0이면 user, 1이면 bot으로 돌아갈 수 있게 구현

이렇게 짜고 생각을 해보니 bot의 행동도 추가해주면 pve를 플레이하는 user 입장에서 훨씬 더 몰입감이 좋을 것 같아 짜보았다.

def bot_action():                                                   # 봇 액션 함수
    print("bot의 턴입니다.")                                        # 봇의 턴임을 공지
    time.sleep(1.0)                                                 # 약간의 지연을 가져 가독성 높이기
    probability = random.random()                                   # random.random 을 활용해 확률 이용하기 (0 ~ 0.99의 랜덤한 실수를 가져오는 변수)
    if probability <= 0.3:                                          # 확률을 3개의 부류로 나누어서 각각의 멘트가 대략 비슷한 확률을 가질 수 있게 함
        for j in range(7):
            if j == 3:
                print("bot이 생각에 빠집니다.")                     # bot이 고민하고, 생각하고 있다는 것을 약간의 지연과 로딩하는 듯한 텍스트로 구현
                time.sleep(0.5)
            elif j == 6:
                print(".")
                print("")
                time.sleep(1.0)
            else:
                print(".")
                time.sleep(0.5)
    elif (probability > 0.3) and (probability <= 0.7):
        for j in range(7):
            if j == 3:
                print("bot이 깊은 고민에 빠집니다.")
                time.sleep(0.5)
            elif j == 6:
                print(".")
                print("")
                time.sleep(1.0)
            else:
                print(".")
                time.sleep(0.5)
    elif probability > 0.7:
        for j in range(7):
            if j == 3:
                print("bot이 총알위치를 고민합니다.")
                time.sleep(0.5)
            elif j == 6:
                print(".")
                print("")
                time.sleep(1.0)
            else:
                print(".")
                time.sleep(0.5)

위의 체크 아이템 사용할 때랑 마찬가지로 bot이 쏠지 말지 고민하는 것처럼 느껴 user가 몰입감이 느껴지도록 time.sleep() 함수를 사용하였고 random.random() 함수로 출력할 멘트들도 랜덤값으로 출력될 수 있도록 매 번 똑같은 멘트가 나와 지루하게 만들지 않고 진짜 AI랑 하고 있는 것처럼 느끼게 만들고 싶어서 꾸며보았다.

그렇게해서

def Russian_Roulette_pve():
    while True:
        start_game = input("룰렛을 시작하시겠습니까?? (Y / N)")
        start_game.lower()                           # input 을 소문자로 치든 대문자로 치든 항상 입력받을 수 있게 input값을 소문자로 바꾸어서 if문 작성
        if start_game.lower() == "y":
            print("게임을 시작하겠습니다.")
            item, bullet_position = set_game_pve()            # 게임 초기 설정 함수 실행
            turn = 0
            
            while True:
                cur_turn = calculate_turn_pve(turn)           # 반복이 다시 돌아올 때마다 턴을 계산해서 user의 턴인지, bot의 턴인지 출력
                if cur_turn == "user":
                    print("-" * 20)                       # 가독성을 높이기 위해 경계선 출력
                    print("당신의 턴입니다.")
                    print("")                             # 가독성을 높이기 위해 라인 하나 띄워서 출력하기 위함
                    user_input = input("shoot ?? / item ?? / end ??")
                    user_input.lower()
                    if user_input.lower() == "shoot":
                        bullet_position -= 1             # shoot을 입력받은 후 bullet_position에서 - 1 을 해 0이 되면 게임을 끝나게 구현
                        if bullet_position == 0:
                            print("사망하셨습니다.")
                            print("=" * 20)
                            print("")
                            print("🦴 패배 🦴")               # 게임이 끝났을 때를 구분하기 위해 출력
                            break
                        else:
                            turn += 1                    # 생존하였을 때는 턴 + 1 을 해 다음 턴으로 넘어갈 수 있게 구현
                            print("")
                            print("생존하셨습니다!")
                            print("-" * 20)
                            print("")
        
    
                    elif user_input.lower() == "item":
                        print("")
                        item_input = input("pass ? / check ? / back?")
                        print("")
                        item_input.lower()
                        if item_input.lower() == "pass":
                            turn = use_pass_pve(item, turn)                      # pass 아이템 사용 함수
                            print("-" * 20)
                            print("")
                        elif item_input.lower() == "check":
                            use_check_pve(bullet_position, item)                 # check 아이템 사용 함수
                            print("")
                        elif item_input.lower() == "back":                   # item 사용을 안하고 싶어졌을 user의 변심을 고려해 back 구현
                            print("이전으로 돌아갑니다.")
                            continue
                        else:
                            print("잘못된 입력입니다.")                      # 오타를 고려해 구현
                            continue
    
                    elif user_input.lower() == "end":                        # 게임을 그만하고 싶을 수 있는 user를 위해 구현
                        print("게임을 마칩니다.")
                        break
                        
                    else:
                        print("잘못 입력하셨습니다.")                        # 오타를 고려해 구현
                        continue

                if cur_turn == "bot":
                    bot_action()                                             # bot 액션 함수 
                    bullet_position -= 1
                    if bullet_position == 0:
                            print("bot이 사망하였습니다!")
                            print("")
                            print("🎊 승리를 축하합니다!! 🎊")
                            print("=" * 20)
                            break
                    else:
                        turn += 1
                        print("")
                        print("bot이 생존하였습니다.")
                        continue

        elif start_game.lower() == "n":                      # 게임을 진행하기 싫은 user의 입장을 고려해 구현
            print("게임을 종료합니다.")
            break

        else:
            print("Y 혹은 N으로만 입력해주십시오.")          # Y / N 이외의 값은 입력하지 못하도록 구현
            continue

이렇게 완성했다. 스케치를 바탕으로 짠 코드에서 약간의 디테일과 살짝의 꾸미기를 넣어 완성해봤다.

FEED BACK

코치님께서 제 플레이한 것과 코드와 발표를 보고 "PVP도 있으면 좋겠다. 그리고 리볼버의 챔버 이미지를 넣어서 게임 진척도를 시각화하면 더 좋겠다" 라는 말씀을 해주셨다.
더 완벽하게 만들어보라고 채찍질을 해주심에 감사하다.
그럼 받은 피드백을 가지고 바로 해봅시다.
먼저 class를 배워서 사용하고 싶었는데 아직 안익숙해서 그런가 그냥 pvp모드 함수와 pve함수를 따로따로 만들었다.

def set_game_pvp(first_user, second_user):         # PVP 게임 초기 설정 함수
    user_one = first_user
    user_two = second_user
    user_one_item = {"pass" : 1, "check" : 1}      # 각 유저 당 패스 1개, 체크 1개를 가짐.
    user_two_item = {"pass" : 1, "check" : 1}
    bullet_position = random.randint(1, 6)
    return user_one, user_two, user_one_item, user_two_item, bullet_position
def use_pass_pvp(user_one_item, user_two_item, cur_turn, user_one, user_two, turn):          # PVP 패스 아이템 사용 함수
    if cur_turn == user_one:
        if user_one_item["pass"] > 0:
            print("PASS 사용으로 이번턴을 넘어갑니다.")                                      # 각 턴을 계산해서 패스 사용
            user_one_item["pass"] -= 1
            return turn + 1
        else:
            print("PASS를 더 이상 사용할 수 없습니다.")
        return turn

    if cur_turn == user_two:
        if user_two_item["pass"] > 0:
            print("PASS 사용으로 이번턴을 넘어갑니다.")
            user_two_item["pass"] -= 1
            return turn + 1
        else:
            print("PASS를 더 이상 사용할 수 없습니다.")
        return turn
def use_check_pvp(bullet_position, user_one_item, user_two_item, cur_turn, user_one, user_two):            # PVP 체크 사용 함수
    if cur_turn == user_one:
        if user_one_item["check"] > 0:
            for i in range(5):
                if i == 2:
                    print("총을 확인하는 중입니다.")
                    time.sleep(0.3)
                elif i == 4:
                    print("*")
                    print("")
                    time.sleep(1.0)
                else:
                    print("*")
                    time.sleep(0.3)
            if bullet_position == 1:
                print("쏘면 안됩니다.")
                user_one_item["check"] -= 1
            elif (bullet_position > 1) and (bullet_position < 4): 
                print("아직 괜찮습니다.")
                user_one_item["check"] -= 1
            else:
                print("아주 괜찮습니다.")
                user_one_item["check"] -= 1
        else:
            print("CHECK를 더 이상 사용할 수 없습니다.")

    if cur_turn == user_two:
        if user_two_item["check"] > 0:
            for i in range(5):
                if i == 2:
                    print("총을 확인하는 중입니다.")
                    time.sleep(0.3)
                elif i == 4:
                    print("*")
                    print("")
                    time.sleep(1.0)
                else:
                    print("*")
                    time.sleep(0.3)
            if bullet_position == 1:
                print("쏘면 안됩니다.")
                user_two_item["check"] -= 1
            elif (bullet_position > 1) and (bullet_position < 4): 
                print("아직 괜찮습니다.")
                user_two_item["check"] -= 1
            else:
                print("아주 괜찮습니다.")
                user_two_item["check"] -= 1
        else:
            print("CHECK를 더 이상 사용할 수 없습니다.")
def calculate_turn_pvp(user_one, user_two, turn):        # PVP 턴 계산해주는 함수
    players = [user_one, user_two]
    return players[turn % 2]
def Russian_Roulette_pvp():                                                           # PVP 러시안 룰렛
    first_user = input("이름을 입력해주세요.")                                        # 플레이할 유저들 닉네임 입력 받기
    second_user = input("이름을 입력해주세요.")
    print("러시안 룰렛 다이다이를 시작합니다.")
    user_one, user_two, user_one_item, user_two_item, bullet_position = set_game_pvp(first_user, second_user)
    turn = 0
    spin_chamber = 0
    print("")
    print_chamber(spin_chamber)
    print("")

    while True:
        cur_turn = calculate_turn_pvp(user_one, user_two, turn)
        if cur_turn == user_one:
            print("-" * 20)
            print(f"{user_one}의 턴입니다.")
            print("")
            user_one_input = input("shoot ? / item ?")
            user_one_input.lower()
            if user_one_input.lower() == "shoot":
                bullet_position -= 1
                spin_chamber += 1
                if bullet_position == 0:
                    print("사망하셨습니다.")
                    print("=" * 20)
                    print("    RESULT    ")
                    print(f"🏆 {user_two} 승리! 🏆")
                    print("")
                    print(f"🏴 {user_one} 패배! 🏴")
                    break
                else:
                    turn += 1
                    print("")
                    print_chamber(spin_chamber)
                    print("")
                    print("생존하셨습니다!")
                    print("")
                    continue
                    
            elif user_one_input.lower() == "item":
                print("")
                item_input = input("pass ? / check ? / back?")
                print("")
                item_input.lower()
                if item_input.lower() == "pass":
                    turn = use_pass_pvp(user_one_item, user_two_item, cur_turn, user_one, user_two, turn)
                    print("")
                elif item_input.lower() == "check":
                    use_check_pvp(bullet_position, user_one_item, user_two_item, cur_turn, user_one, user_two)
                    print("")
                elif item_input.lower() == "back":
                    print("이전으로 돌아갑니다.")
                    continue
                else:
                    print("잘못된 입력입니다.")
                    continue
            else:
                print("잘못 입력하셨습니다.")
                continue
        
        if cur_turn == user_two:
            print("-" * 20)
            print(f"{user_two}의 턴입니다.")
            print("")
            user_two_input = input("shoot ? / item ?")
            user_two_input.lower()
            if user_two_input.lower() == "shoot":
                bullet_position -= 1
                spin_chamber += 1
                if bullet_position == 0:
                    print("사망하셨습니다.")
                    print("=" * 20)
                    print("    RESULT    ")
                    print(f"🏆 {user_one} 승리! 🏆")
                    print("")
                    print(f"🏴 {user_two} 패배! 🏴")
                    break
                else:
                    turn += 1
                    print("")
                    print_chamber(spin_chamber)
                    print("")
                    print("생존하셨습니다!")
                    print("")
                    
            elif user_two_input.lower() == "item":
                print("")
                item_input = input("pass ? / check ? / back?")
                print("")
                item_input.lower()
                if item_input.lower() == "pass":
                    turn = use_pass_pvp(user_one_item, user_two_item, cur_turn, user_one, user_two, turn)
                    print("")
                elif item_input.lower() == "check":
                    use_check_pvp(bullet_position, user_one_item, user_two_item, cur_turn, user_one, user_two)
                    print("")
                elif item_input.lower() == "back":
                    print("이전으로 돌아갑니다.")
                    continue
                else:
                    print("잘못된 입력입니다.")
                    continue
            else:
                print("잘못 입력하셨습니다.")
                continue

진짜 코드가 정말 길었다.
이걸 어떻게든 줄여보고 싶었는데 내 지식의 한계였다.
그래서 그냥 했다.
코드를 효율적으로 짤 수 있는 단계는 아직 멀었다고 생각이 들었다....
일단 에러없고 머리 속으로 원했던 그림이 그대로 출력되니 만족이었다.
그리고 챔버 이미지의 시각화였는데 메모장에서 동그라미랑 X 가지고 공간체크하면서 한줄 한줄 리스트로 만들어 뽑는 방식으로 선택했다.

def print_chamber(spin_chamber):                               # 챔버 시각화 함수
    chamber_image_list = [
    ["    ⭕ ⭕  ", "   ⭕   ⭕  ", "    ⭕ ⭕  "],
    
    ["    ⭕ ❌  ", "   ⭕   ⭕  ", "    ⭕ ⭕  "],

    ["    ⭕ ❌  ", "   ⭕   ❌  ", "    ⭕ ⭕  "],

    ["    ⭕ ❌  ", "   ⭕   ❌  ", "    ⭕ ❌  "],

    ["    ⭕ ❌  ", "   ⭕   ❌  ", "    ❌ ❌  "],

    ["    ⭕ ❌  ", "   ❌   ❌  ", "    ❌ ❌  "]
]
    print(*chamber_image_list[spin_chamber], sep = "\n")

이렇게 해서 한발 쏠 때마다 X가 하나씩 생기면서 진척도를 표시할 수 있도록 만들어보았다.

끝.

생각보다 좋은 평가를 받았다.
다른 분들이 보시기에는 정말 하찮고 귀여운 코드들일 수도 있고 허점투성이에 보완할게 무척 많은 코드일 수도 있지만, 정말 하나하나 최대한 효율적으로 짜려고 노력했다.
그 과정 속에서 놓치기 쉬운 디테일들 하나를 놓치지 않으려고 최대한 머리를 굴렸고, '내가 진짜 이 게임을 플레이한다면..' 이라는 생각으로 놓친 점들을 찾아나갔다.

그래서 생각한게 봇도 아이템을 사용할 수 있게 짜보면 좋겠다고 해봤지만, 아직 나에게는 너무 무리여서 해보지는 못했지만, 나중에 한 번 도전을 해볼 생각이다.

대략 일주일 정도를 정말 이 프로젝트에 몰두한 것 같다.
다 만들고나서 생각이든 건 진짜 '지금 우리가 사용하고 있는 모든 프로그램들이 정말 프로그래머 혹은 개발자분들의 피, 땀, 눈물로 세워진 거구나'를 느꼈다.
다음에 프로그램 만들 일이 있으면 좀 더 가독성 좋게, 효율적으로 돌아갈 수 있게 짜보고 싶어졌다.
좋은 결과를 당연하다는 듯이 받아들일 수 있는 내가 될 때까지 달려봐야겠다.

profile
어렵지만 이겨내보겠습니다

0개의 댓글