Sunday Coders 3주차
Sunday Coders는 매주 일요일마다 1~4시간 정도 현존하는 AI를 활용하여 취미로 코딩을 하는 모임이다.
코딩 문외한인 내가 AI를 활용해서 어디까지 코딩을 할 수 있는지 궁금하기도 하고, 취미생활이 필요한거 같아 시작하게 되었다.
3주차인 이유는, 실제로 작업한지는 2주가 넘었는데, 작업물을 기록하는 방식이 필요하다고 생각하여 블로그 글을 올리기 시작했다.
3주차 작업물 : Python 텍스트 기반 게임
사용한 AI 툴 : GPT o3, Gemini 2.5 pro
1. AI 전용 프롬프트 짜기 : Gemini 2.5 pro 모델 사용
"나만의 Python 텍스트 기반 게임 만들기용 AI"를 만들기 위해 프롬프트 (명령어)를 짜야 했다.
AI는 AI가 잘안다(?) 는 생각으로, Gemini에게 AI에게 먹일 프롬프트를 짜게 명령했다.
" I want to create a simple text-based adventure game using Python. I am a complete beginner with no prior coding experience. Please help me by generating a game concept suitable for coding within approximately 4-5 hours. The game plan should include:
1. **A Core Story/Theme:** A simple narrative or setting for the adventure.
2. **NPCs:** 2-3 Non-Player Characters with basic roles, dialogue snippets, or purposes within the story.
3. **Quests/Objectives:** 1-2 simple quests or goals for the player to achieve.
4. **Basic Combat:** A very simple combat mechanic (e.g., player vs. one type of monster, turn-based, simple stats like HP).
5. **Step-by-Step Python Coding Guide:** Provide a clear, step-by-step guide on how to implement this game in Python, assuming I know absolutely nothing about coding. Break down the process into small, manageable steps (e.g., setting up the environment, handling player input, creating rooms/locations, defining items, implementing NPC interactions, coding the combat loop, defining win/lose conditions). **Crucially, before generating the full game plan and code steps, please ask me clarifying questions to better tailor the game to my interests.** Ask me about potential themes (fantasy, sci-fi, mystery, everyday life?), preferred game complexity (even within the 4-5 hour limit), desired player actions (look, go, talk, take, use, fight?), or any other details you need to create a more personalized and achievable game plan for me. Engage in a dialogue first to refine the concept. 마지막에는 한국어로 모든 답변은 번역해주세요"
그렇게 해서 나온 전용 프롬프트가 위의 프롬프트다. 이 프롬프트를 GPT 의 최근 모델인 o3에 먹여서 작업을 진행해보았다.
2. AI 가 짜준 코드를 Visual studio code에 입력 : GPT o3 모델 사용
GPT가 알려준 가벼운 주제를 사용한 Python 텍스트 기반 게임 개발을 위해 VSC코드를 깔았다.
코딩을 모르는 사람이 차근차근 따라갈 수 있도록, 물어보면서 어떻게 하면 될지 방법을 GPT가 알려주면 단계별로 진행했다.
GPT가 1차적으로 짜준 코드를 VSC에 붙여넣었고, 여기서 친구의 조언에 따라 게임 플레이를 tkinker 로 할 수 있게끔 변경했다.
* tkinker : 메모장처럼 네모난 창 안에 글·버튼·입력칸 등을 띄우고 그 안에서 텍스트를 주고받는 식의 아주 가벼운 GUI(Game User Interface) 프로그램을 만드는 데 쓰는 거랍니다..
/
# moon_adventure_tk.py
# =======================================
# Tkinter GUI 버전 – 달의 파편 텍스트 RPG
# =======================================
import json, os, sys, textwrap, tkinter as tk
from tkinter.scrolledtext import ScrolledText
# ---------- 데이터 클래스 ----------
class Room:
def __init__(self, 이름, 설명):
self.name = 이름
self.desc = 설명
self.exits = {}
self.items = []
self.npc = None
self.monster = None
class NPC:
def __init__(self, 이름, 대사, on_talk=None):
self.name = 이름
self.dialog = 대사
self.on_talk = on_talk
class Monster:
def __init__(self, 이름, hp, atk):
self.name, self.hp, self.atk = 이름, hp, atk
class Player:
def __init__(self):
self.hp = 20
self.atk = 5
self.inv = []
self.room = None
self.flags = {"have_shard": False, "herb_given": False}
player = Player()
# ---------- 월드 구축 ----------
def build_world():
숲 = Room("숲 빈터", "달빛이 비추는 고요한 공터다. 은둔자가 콧노래를 흥얼거린다.")
게이트 = Room("고대 게이트", "무너진 석문이 동쪽 길을 막고 있다. 요정 한 마리가 날아다닌다.")
동굴 = Room("수정 동굴", "벽면 가득 수정이 빛난다. 동굴 고블린이 이를 드러내고 있다!")
제단 = Room("달 제단", "차가운 대리석 제단. 중앙에 파편을 꽂을 자리가 있다.")
숲.exits = {"east": 게이트}
게이트.exits = {"west": 숲, "down": 동굴}
동굴.exits = {"up": 게이트}
제단.exits = {"west": 동굴}
숲.items.append("치유 약초")
동굴.items.append("달의 파편")
숲.npc = NPC("은둔자",
"은둔자: \"해가 지기 전에 달의 파편을 찾아오게나. 허리에 좋을 약초도 구해주면 더 좋고…\"",
on_talk=lambda: give_herb())
게이트.npc = NPC("요정",
"요정: \"히히! 반짝이는 파편은 아래 동굴에 있어. 고블린 조심해!\"")
동굴.monster = Monster("동굴 고블린", 15, 4)
제단.npc = None
return 숲, 게이트, 동굴, 제단
def give_herb():
if "치유 약초" in player.inv and not player.flags["herb_given"]:
player.inv.remove("치유 약초")
player.hp = min(player.hp + 10, 20)
player.flags["herb_given"] = True
sprint("은둔자: \"고맙네! (HP +10)\"")
# ---------- Tkinter 초기화 ----------
root = tk.Tk()
root.title("달의 파편 - 텍스트 어드벤처")
text_area = ScrolledText(root, wrap=tk.WORD, width=80, height=24, state="disabled")
text_area.pack(padx=8, pady=8, fill="both", expand=True)
entry = tk.Entry(root, width=80)
entry.pack(padx=8, pady=(0,8), fill="x")
entry.focus()
def sprint(msg="\n"):
"""텍스트 영역에 메시지 출력"""
text_area.config(state="normal")
text_area.insert(tk.END, textwrap.fill(msg, 76) + "\n")
text_area.see(tk.END)
text_area.config(state="disabled")
# ---------- 저장 / 불러오기 ----------
def save_game():
data = {
"hp": player.hp, "inv": player.inv,
"room": player.room.name, "flags": player.flags
}
with open("savegame.json", "w", encoding="utf-8") as f:
json.dump(data, f, ensure_ascii=False)
sprint("[게임 저장 완료]")
def load_game(room_lookup):
if not os.path.exists("savegame.json"):
sprint("저장 파일이 없습니다.")
return
with open("savegame.json", encoding="utf-8") as f:
data = json.load(f)
player.hp = data["hp"]
player.inv = data["inv"]
player.flags = data["flags"]
player.room = room_lookup[data["room"]]
sprint("[게임 불러오기 완료]")
cmd_look()
# ---------- 명령 함수 ----------
def cmd_look():
sprint(f"\n== {player.room.name} ==")
sprint(player.room.desc)
if player.room.items:
sprint("◆ 보이는 것: " + ", ".join(player.room.items))
if player.room.npc:
sprint(f"◆ {player.room.npc.name} 이(가) 있다.")
if player.room.monster and player.room.monster.hp > 0:
sprint(f"◆ {player.room.monster.name} 이(가) 으르렁거린다!")
sprint("◆ 출구: " + ", ".join(player.room.exits.keys()))
def cmd_go(direction):
if direction not in player.room.exits:
sprint("그 방향으론 갈 수 없습니다.")
return
player.room = player.room.exits[direction]
cmd_look()
def cmd_take(item):
if item not in player.room.items:
sprint("그런 것은 여기 없습니다.")
return
player.room.items.remove(item)
player.inv.append(item)
sprint(f"{item} 을(를) 주웠습니다.")
def cmd_use(item):
if item not in player.inv:
sprint("그 아이템을 가지고 있지 않습니다.")
return
if item == "달의 파편" and player.room.name == "달 제단":
player.flags["have_shard"] = True
sprint("파편을 제단에 꽂았습니다! 빛이 퍼집니다.")
check_win()
elif item == "치유 약초":
sprint("누군가에게 건네주면 도움이 될 것 같습니다.")
else:
sprint("아무 일도 일어나지 않습니다.")
def cmd_talk(name):
npc = player.room.npc
if not npc or npc.name != name:
sprint("그런 인물은 여기 없습니다.")
return
sprint(npc.dialog)
if npc.on_talk:
npc.on_talk()
def fight_round(mon):
mon.hp -= player.atk
sprint(f"{mon.name} 을(를) 공격! (상대 HP -{player.atk})")
if mon.hp <= 0:
sprint(f"{mon.name} 이(가) 쓰러졌습니다.")
return
player.hp -= mon.atk
sprint(f"{mon.name} 의 반격! (내 HP -{mon.atk}) 현재 HP: {player.hp}")
if player.hp <= 0:
sprint("당신은 쓰러졌습니다… 게임 오버.")
root.quit()
def cmd_fight():
mon = player.room.monster
if not mon or mon.hp <= 0:
sprint("싸울 상대가 없습니다.")
return
sprint(f"{mon.name} 과(와) 전투 시작!")
while mon.hp > 0 and player.hp > 0:
root.update() # GUI 멈춤 방지
fight_round(mon)
def check_win():
if player.flags["have_shard"]:
guardian = NPC("수호령", "수호령: \"빛이 돌아왔다. 영웅이여 축하하노라!\"")
player.room.npc = guardian
sprint("\n*** 축하합니다! 게임 클리어! ***")
root.quit()
# ---------- 입력 처리 ----------
def handle_command(event=None):
raw = entry.get().strip()
entry.delete(0, tk.END)
if not raw:
return
sprint(f"> {raw}") # 플레이어가 입력한 명령 echo
words = raw.lower().split()
verb = words[0]
args = words[1:]
if verb == "look": cmd_look()
elif verb == "go" and args: cmd_go(args[0])
elif verb == "take" and args: cmd_take(" ".join(args))
elif verb == "use" and args: cmd_use(" ".join(args))
elif verb == "talk" and args: cmd_talk(args[0])
elif verb == "fight": cmd_fight()
elif verb == "save": save_game()
elif verb == "load": load_game(name_to_room)
elif verb == "quit": root.quit()
else:
sprint("알 수 없는 명령입니다.")
entry.bind("<Return>", handle_command)
# ---------- 게임 시작 ----------
숲, 게이트, 동굴, 제단 = build_world()
name_to_room = {r.name: r for r in [숲, 게이트, 동굴, 제단]}
player.room = 숲
cmd_look()
root.mainloop()
3. AI 가 짜준 코드 디테일 삽입, 버그 수정 : Gemini 2.5 pro, GPT o3 모델 사용
GPT가 짜준 코드에 간단하게 몇번 수정을 해서 게임이 돌아는 갔는데, 문제가 발생했다.
게임의 디테일적 부분이 떨어진다는 것이었다...
ex) 고블린(적)이 공격을 안하고 맞기만 한다던지, 영어 명령만 인식한다던지, 메시지만 계속 출력되는 오류발생 등..
아무리 텍스트 게임이더라도 기본적인 게임의 재미는 필요할 것이라고 생각해서, 디테일을 첨부하기 시작했다.
편의성 (한국어 명령어 추가, 적 캐릭터 HP 추가 등), 게임성 (적이 공격하기 시작, 스킬 추가 등)
점점... 하나하나씩 추가가 되니 코드가 길어지기 시작하고... GPT o3모델에 과부하가 왔는지 중간중간에 코드가 끊기기 시작했다.
코드를 단위별로 끊어서 수정을 해나갈 순 있으나 귀찮은 나머지 Gemini 2.5 pro의 도움을 받기로 결심했다.
Gemini 2.5 pro에 새롭게 물어본 김에 추가해야할 편의성이 더 있는지 먼저 물어보고, 계속 게임을 테스트하면서 추가하면 좋을 사항 이나 오류가 발생하면 코드를 AI한테 물어봐서 수정하게 만들었다.
Gemini 2.5 pro와 25번 넘게 코드를 수정하여 얻은 코드다...
(결과로 나온 화면에서 몇 차례 개선하려고 AI로 코드를 수정했으나, 시간이 더 소요될 거 같아 이거로 일단 마무리하기로 했다.)
# moon_adventure_final_improved_v24_mod.py
# ==========================================
# 달의 파편 – 최종판 (개선판 v24 기반 수정)
# ------------------------------------------
# • 수호자 첫 조우 시 혼잣말 메시지 추가 (1회 출력)
# • 고블린 퇴치 시 메시지 변경 ("동쪽" -> "오른쪽")
# ==========================================
import tkinter as tk
from tkinter.scrolledtext import ScrolledText
from tkinter import font as tkFont
import textwrap, random
# ────────── GUI 초기화 ──────────
root = tk.Tk(); root.title("달의 파편 – 최종판 (개선판 v24 수정)") # 제목 수정
text = ScrolledText(root, wrap=tk.WORD, width=80, height=24, state="disabled")
text.pack(padx=8, pady=8, fill="both", expand=True)
entry = tk.Entry(root, width=80); entry.pack(padx=8, pady=(0,8), fill="x"); entry.focus()
# --- 서식 태그 설정 ---
default_font = tkFont.nametofont("TkDefaultFont")
bold_font = tkFont.Font(**default_font.configure()); bold_font.configure(weight="bold")
text.tag_configure("bold", font=bold_font)
large_bold_font = tkFont.Font(**default_font.configure()); default_size = default_font.actual()["size"]; large_bold_font.configure(weight="bold", size=default_size + 2)
text.tag_configure("large_bold", font=large_bold_font)
highlight_font = tkFont.Font(**default_font.configure()); highlight_font.configure(weight="bold", size=default_size + 1)
text.tag_configure("action_highlight", font=highlight_font)
# ----------------------
LOG_MAX = 40
# sprint 함수 (v23 방식 유지)
def sprint(msg="\n", tags=None):
text.config(state="normal")
current_lines = int(text.index('end-1c').split('.')[0])
if current_lines >= LOG_MAX * 2: lines_to_delete = current_lines - LOG_MAX; text.delete("1.0", f"{lines_to_delete}.0")
start_index = text.index(tk.END + "-1c")
if msg == "\n": text.insert(tk.END, "\n")
else:
for ln in msg.split("\n"):
wrapped_lines_list = textwrap.fill(ln, 76).split('\n')
for i, line_part in enumerate(wrapped_lines_list): text.insert(tk.END, line_part); text.insert(tk.END, "\n")
text.insert(tk.END, "\n") # 추가 빈 줄
end_index = text.index(tk.END + "-1c")
if tags and start_index != end_index: text.tag_add(tags, start_index, end_index)
text.see(tk.END); text.config(state="disabled")
# ────────── 기본 클래스 ──────────
class Room:
def __init__(self, name, desc): self.name, self.desc = name, desc; self.exits: dict[str, 'Room'] = {}; self.items: list[str] = []; self.npc: list[NPC] = []; self.monster: 'Monster|None' = None
class NPC:
def __init__(self, name, dialog, on_talk=None): self.name, self.dialog, self.on_talk = name, dialog, on_talk
class Monster:
def __init__(self, name, hp, atk): self.name = name; self.hp = self.max_hp = hp; self.atk = atk; self.is_boss = False
class Player:
def __init__(self):
self.max_hp, self.hp = 30, 30
self.max_mp, self.mp = 10, 10
self.atk = 5
self.skills = {"베기": {"cost": 0, "dmg": 6}}
# 시작 시 약초 0개, 숲에서 주워야 함
self.inv = {"HP포션": 2, "MP포션": 2, "치유 약초": 0}
self.room: Room | None = None
self.flags = {
"shard": False,
"herb_given": False,
"goblin_dead": False,
"boss_dead": False,
"saw_guardian_message": False, # <--- 수호자 첫 조우 플래그 추가
"game_over": False
}
player = Player()
# ────────── 방향 매핑 ──────────
# 사용자가 제공한 매핑 유지
eng2ko = {"north":"위","south":"남","east":"오른쪽","west":"왼쪽","up":"위","down":"아래"}
ko2eng = {v:k for k,v in eng2ko.items()}
# ────────── 월드 구축 ──────────
def build_world():
숲 = Room("숲 빈터", "달빛이 비추는 고요한 공터다.")
게이트 = Room("고대 게이트", "무너진 석문. 요정이 깔깔댄다.")
동굴 = Room("수정 동굴", "수정이 반짝인다.")
제단 = Room("달 제단", "대리석 제단 위에 파편 홈이 있다.")
숲.exits = {"east": 게이트}; 게이트.exits = {"west": 숲, "down": 동굴}
동굴.exits = {"up": 게이트, "east": 제단}; 제단.exits = {"west": 동굴}
# 시작 시 약초는 땅에 떨어져 있음 (인벤 0개)
숲.items.append("치유 약초")
숲.npc.append(NPC("은둔자", "은둔자: \"달의 파편을 찾아오게. 허리엔 약초도 좋고!\"", on_talk=hermit_talk))
게이트.npc.append(NPC("요정", "요정: \"동굴의 고블린을 물리쳐야 오른쪽 제단으로 갈 수 있어!\"")) # 요정 대사도 일관성 위해 수정
동굴.monster = Monster("동굴 고블린", 35, 5)
제단_몬스터 = Monster("달의 수호자", 45, 6); 제단_몬스터.is_boss = True; 제단.monster = 제단_몬스터
return 숲, 게이트, 동굴, 제단
# ────────── NPC 로직 ──────────
def hermit_talk():
if player.flags["herb_given"]: sprint("은둔자: \"이미 허리가 편하다네. 강장제 잘 쓰고 있나?\"")
elif player.inv.get("치유 약초", 0) > 0:
sprint("은둔자: \"허리가 영 불편하군... 약초가 있다면 도움이 될 텐데.\"")
sprint(" (약초를 주려면: 주기 은둔자 치유 약초)", tags="bold") # 힌트 볼드
else: sprint("은둔자: \"약초가 있으면 좋겠네…\"")
# ────────── 출력 및 상태 ──────────
action_labels = {"보기":"보기", "이동":"이동", "줍기":"줍기", "사용":"사용", "대화":"대화", "전투":"전투", "주기":"주기"}
available_commands = ["보기", "이동 [방향]", "줍기 [아이템]", "사용 [아이템]", "대화 [대상]", "주기 [대상] [아이템]", "전투", "소지품", "도움말", "종료"]
def status_bar():
inv_str = ", ".join(f"{k}:{v}" for k, v in player.inv.items() if v > 0) if any(v > 0 for v in player.inv.values()) else "없음"
sprint(f"[HP {player.hp}/{player.max_hp} | MP {player.mp}/{player.max_mp} | 스킬 {list(player.skills)} | 인벤 {inv_str}]")
def show_room():
r = player.room;
if not r: return
sprint(f"\n== {r.name} ==\n{r.desc}")
show_actions()
# 수정된 show_actions 함수
def show_actions():
if player.flags["game_over"]: return
r = player.room;
if not r: return
has_summary_info = False
if r.items: sprint("◆ 보이는 것: " + ", ".join(r.items)); has_summary_info = True
if r.npc:
for npc in r.npc:
if npc.name == "은둔자":
if not player.flags["herb_given"]: sprint("◆ 지팡이에 기댄 은둔자가 허리를 만지며 당신을 바라봅니다. (대화 가능)")
else: sprint("◆ 은둔자가 한결 편안해진 표정으로 앉아 있습니다.")
elif npc.name == "요정": sprint("◆ 작은 요정이 당신 주위를 맴돌며 무언가 말하려는 듯합니다. (대화 가능)")
else: sprint(f"◆ {npc.name}이(가) 이곳에 있습니다. (대화 가능)")
has_summary_info = True
current_monster = r.monster; combat_possible = current_monster and current_monster.hp > 0
if combat_possible:
# ================== 수호자 첫 조우 메시지 시작 ==================
if current_monster.name == "달의 수호자" and not player.flags.get("saw_guardian_message", False):
sprint("\n(저 강력한 수호자를 물리쳐야만 달의 파편을 얻고, 이 혼란을 끝낼 수 있을 것 같다...)\n", tags="bold")
player.flags["saw_guardian_message"] = True # 메시지 출력 플래그 설정
# ================== 수호자 첫 조우 메시지 끝 ====================
# 기존 몬스터 정보 출력
sprint(f"◆ {current_monster.name} (HP {current_monster.hp}) 이(가) 있습니다."); has_summary_info = True
exits = [];
for direction_eng in r.exits:
# 고블린 생존 시 동굴 -> 제단(east) 이동 불가 조건 확인
if r.name == "수정 동굴" and direction_eng == "east" and not player.flags["goblin_dead"]: continue
# 매핑된 한글 방향 추가 (eng2ko 딕셔너리 사용)
exits.append(eng2ko.get(direction_eng, direction_eng)) # 매핑 없으면 영어 그대로 표시
if exits : sprint("◆ 출구: " + ", ".join(exits)); has_summary_info = True
elif not has_summary_info and not combat_possible : sprint("이 방에는 특별한 것이 없어 보입니다.")
# --- 가능한 행동 목록 생성 ---
acts = ["보기", "소지품", "도움말"]
can_move = False;
# 이동 가능한 출구 목록 (고블린 조건 포함)
possible_exits_eng = [e for e in r.exits if not (r.name == "수정 동굴" and e == "east" and not player.flags["goblin_dead"])]
if possible_exits_eng: can_move = True
if can_move: acts.append("이동")
if r.items: acts.append("줍기")
if any(v > 0 for v in player.inv.values()): acts.append("사용")
if r.npc: acts.append("대화")
# 주기 조건: 인벤에 아이템 있고, 은둔자가 있고, 약초를 아직 안 줬고, '치유 약초' 소지 시
if any(v > 0 for v in player.inv.values()) and \
any(n.name == "은둔자" for n in r.npc) and \
not player.flags["herb_given"] and \
player.inv.get("치유 약초", 0) > 0:
acts.append("주기")
if combat_possible: acts.append("전투")
acts.append("종료")
# --- 행동 강조 로직 ---
highlight_keys = set()
if r.name == "숲 빈터":
if "치유 약초" in r.items: highlight_keys.add("줍기")
if any(n.name == "은둔자" for n in r.npc) and not player.flags["herb_given"]: highlight_keys.add("대화")
if player.inv.get("치유 약초", 0) > 0 and any(n.name == "은둔자" for n in r.npc) and not player.flags["herb_given"]: highlight_keys.add("주기")
if "east" in possible_exits_eng: highlight_keys.add("이동") # 영어 키로 확인
elif r.name == "고대 게이트":
if any(n.name == "요정" for n in r.npc): highlight_keys.add("대화")
if "west" in possible_exits_eng or "down" in possible_exits_eng: highlight_keys.add("이동")
elif r.name == "수정 동굴":
if combat_possible: highlight_keys.add("전투")
if player.flags["goblin_dead"] and "east" in possible_exits_eng: highlight_keys.add("이동")
if "up" in possible_exits_eng: highlight_keys.add("이동")
elif r.name == "달 제단":
if combat_possible: highlight_keys.add("전투") # 수호자 있을 때 전투 강조
if player.flags["boss_dead"] and player.inv.get("달의 파편", 0) > 0: highlight_keys.add("사용") # 보스 잡고 파편 있으면 사용 강조
if "west" in possible_exits_eng: highlight_keys.add("이동")
# --- 행동 목록 출력 ---
text.config(state="normal")
action_line_start = text.index(tk.END + "-1c"); text.insert(tk.END, "행동: "); is_first = True
unique_acts_display = []
for act_key in acts:
display_name = action_labels.get(act_key, act_key)
if display_name not in unique_acts_display: unique_acts_display.append(display_name)
for act_display_name in unique_acts_display:
# 표시 이름에서 원래 키 찾기 (action_labels 역참조 또는 직접 비교)
act_key = next((k for k, v in action_labels.items() if v == act_display_name), act_display_name)
if not is_first: text.insert(tk.END, " / ")
should_highlight = act_key in highlight_keys
if should_highlight:
tag_start = text.index(tk.END + "-1c"); text.insert(tk.END, act_display_name); tag_end = text.index(tk.END + "-1c")
text.tag_add("action_highlight", tag_start, tag_end)
else: text.insert(tk.END, act_display_name)
is_first = False
text.insert(tk.END, "\n\n"); text.config(state="disabled")
status_bar()
# ────────── 명령 헬퍼 ──────────
def add_skill(name, cost, dmg): player.skills[name] = {"cost": cost, "dmg": dmg}; sprint(f"새로운 스킬을 배웠다! [{name}] (MP {cost}, 데미지 {dmg})")
def game_over(message=""): (message and sprint(message)); sprint("Game Over."); player.flags["game_over"] = True; entry.config(state="disabled")
def game_win(message=""): (message and sprint(message)); sprint("*** 당신은 임무를 완수했습니다! 축하합니다! ***"); player.flags["game_over"] = True; entry.config(state="disabled")
# ────────── 명령 실행 ──────────
def do_look(): show_room()
def do_help():
sprint("< 사용 가능한 명령어 >");
general_commands = [cmd for cmd in available_commands if cmd not in ("예", "아니오")]
for cmd_desc in general_commands: sprint(f"- {cmd_desc}")
sprint("- 특정 행동 중 '이전' 입력 시 취소 가능 (예: 이동 방향 묻기)")
def do_inventory():
sprint("< 소지품 >");
items_to_show = {k: v for k, v in player.inv.items() if v > 0}
if not items_to_show: sprint("가진 아이템이 없습니다.")
else:
text.config(state="normal")
for item, count in items_to_show.items():
line_start = text.index(tk.END + "-1c"); text.insert(tk.END, f"- ")
# 달 제단에 있고, 보스 잡았을 때만 '달의 파편' 강조
if item == "달의 파편" and player.flags["boss_dead"] and player.room and player.room.name == "달 제단":
tag_start = text.index(tk.END + "-1c"); text.insert(tk.END, item); tag_end = text.index(tk.END + "-1c")
text.tag_add("action_highlight", tag_start, tag_end)
else: text.insert(tk.END, item)
text.insert(tk.END, f": {count}개\n\n")
text.config(state="disabled")
# use_item 함수 (v24 최종 회복량 적용)
def use_item(item_name):
# 아이템 이름 부분 일치 찾기
found_item_key = None
item_name_lower = item_name.lower()
for inv_item in player.inv:
if player.inv[inv_item] > 0 and item_name_lower in inv_item.lower():
found_item_key = inv_item
break
if not found_item_key: sprint(f"'{item_name}'(와)과 비슷한 아이템이 없습니다."); return False
used = False; msg = ""
if found_item_key == "치유 약초":
heal_amount = 5
if player.hp < player.max_hp: player.hp = min(player.max_hp, player.hp + heal_amount); msg = f"치유 약초를 사용하여 HP {heal_amount} 회복! (현재 HP: {player.hp}/{player.max_hp})"; used = True
else: msg = "HP가 이미 가득 찼습니다."
elif found_item_key == "HP포션":
heal_amount = 15
if player.hp < player.max_hp: player.hp = min(player.max_hp, player.hp + heal_amount); msg = f"HP {heal_amount} 회복! (현재 HP: {player.hp}/{player.max_hp})"; used = True
else: msg = "HP가 이미 가득 찼습니다."
elif found_item_key == "MP포션":
restore_amount = 15
if player.mp < player.max_mp: player.mp = min(player.max_mp, player.mp + restore_amount); msg = f"MP {restore_amount} 회복! (현재 MP: {player.mp}/{player.max_mp})"; used = True
else: msg = "MP가 이미 가득 찼습니다."
elif found_item_key == "강장제":
heal_amount = 20
if player.hp < player.max_hp: player.hp = min(player.max_hp, player.hp + heal_amount); msg = f"강장제 사용! HP {heal_amount} 회복! (현재 HP: {player.hp}/{player.max_hp})"; used = True
else: msg = "HP가 이미 가득 찼습니다."
elif found_item_key == "달의 파편":
if player.room and player.room.name == "달 제단":
if player.flags["boss_dead"]:
player.flags["shard"] = True
game_win("\n*** 제단에 달의 파편을 꽂자 달빛이 세상을 감쌉니다! ***")
return True # game_win 호출 시 True 반환
else: msg = "수호자가 아직 제단을 지키고 있어 파편을 사용할 수 없습니다!"; return False
else: msg = "달의 파편은 달 제단에서만 사용할 수 있습니다."; return False
else: msg = f"'{found_item_key}' 아이템은 사용할 수 없거나 효과가 없습니다."; return False
sprint(msg, tags="bold")
if used:
player.inv[found_item_key] -= 1
# 전투 중 사용 시 상태 즉시 업데이트
if pending.get("type") == "battle_choice":
status_bar()
return True
else: return False
# do_move 함수
def do_move(dir_ko):
dir_eng = ko2eng.get(dir_ko); r = player.room
if not r: return False # 플레이어 방 정보 없을 시 예외 처리
if not dir_eng or dir_eng not in r.exits: sprint("그 방향으론 갈 수 없습니다."); return False
if r.name == "수정 동굴" and dir_eng == "east" and not player.flags["goblin_dead"]: sprint("고블린이 오른쪽 길을 막고 있습니다!"); return False # 메시지 일관성
player.room = r.exits[dir_eng]; show_room(); return True
# do_take 함수 (부분 일치)
def do_take(item_name):
r = player.room; found_item = None
if not r: return False
item_name_lower = item_name.lower()
for item in r.items:
if item_name_lower in item.lower(): found_item = item; break
if not found_item: sprint(f"'{item_name}'(와)과 비슷한 아이템이 없습니다."); return False
r.items.remove(found_item); player.inv[found_item] = player.inv.get(found_item, 0) + 1
sprint(f"{found_item} 획득!"); status_bar(); return True
# talk_to 함수 (부분 일치)
def talk_to(npc_name):
r = player.room
if not r or not r.npc: sprint("여기에는 대화할 인물이 없습니다."); return False # 방/NPC 존재 확인
npc = next((n for n in r.npc if npc_name.lower() in n.name.lower()), None)
if not npc: sprint(f"'{npc_name}'(와)과 비슷한 이름의 인물은 없습니다."); return False
sprint(npc.dialog);
if npc.on_talk: npc.on_talk()
return True
# do_give 함수 (부분 일치)
def do_give(args):
if len(args) < 2: sprint("무엇을 누구에게 줄까요? (예: 주기 은둔자 치유 약초)"); return False
npc_name = args[0]; item_name = " ".join(args[1:])
r = player.room
if not r or not r.npc: sprint("여기에는 인물이 없습니다."); return False # 방/NPC 존재 확인
npc = next((n for n in r.npc if npc_name.lower() in n.name.lower()), None)
if not npc: sprint(f"여기에는 '{npc_name}'(와)과 비슷한 이름의 인물이 없습니다."); return False
found_item_key = None
item_name_lower = item_name.lower()
for inv_item in player.inv:
if player.inv[inv_item] > 0 and item_name_lower in inv_item.lower():
found_item_key = inv_item
break
if not found_item_key: sprint(f"'{item_name}'(와)과 비슷한 아이템이 없습니다."); return False
if npc.name == "은둔자" and found_item_key == "치유 약초":
if player.flags["herb_given"]: sprint("은둔자: \"이미 받았다네. 고맙네.\""); return False
else:
player.inv["치유 약초"] -= 1
player.inv["강장제"] = player.inv.get("강장제", 0) + 1; player.flags["herb_given"] = True
sprint("당신은 은둔자에게 치유 약초를 건넸습니다.")
sprint("은둔자: \"오, 고맙네! 허리가 한결 낫군. 여기 내가 만든 강장제를 가져가게.\""); status_bar()
# 상태 변경 후 방 정보 갱신 (NPC 상태 메시지 변경 위해)
# show_actions() # 주석 처리 유지 (필요 시 해제)
return True
else: sprint(f"{npc.name}에게 '{found_item_key}' 아이템을 줄 수 없습니다."); return False
# ────────── 전투 관련 함수 ──────────
# monster_attack 함수
def monster_attack(mon):
if not mon or mon.hp <= 0: return;
dmg = mon.atk + random.randint(-1, 1);
player.hp -= dmg
sprint(f"{mon.name}의 공격! {dmg} 피해 (내 HP: {player.hp}/{player.max_hp})", tags="bold");
if player.hp <= 0:
game_over("당신은 쓰러졌다…") # 게임 오버 처리
# player_skill 함수 (부분 일치)
def player_skill(mon, skill_name):
found_skill_key = None
skill_name_lower = skill_name.lower()
for skill in player.skills:
if skill_name_lower in skill.lower():
found_skill_key = skill
break
if not found_skill_key: sprint("그런 스킬은 없습니다."); return False
data = player.skills[found_skill_key]; cost = data["cost"]; dmg_base = data["dmg"]
dmg = dmg_base + random.randint(-1,2) # 데미지 변동
if player.mp < cost: sprint("MP가 부족합니다!"); return False
player.mp -= cost; mon.hp -= dmg
sprint(f"[{found_skill_key}] 스킬 사용! {mon.name}에게 {dmg} 데미지 (남은 HP: {mon.hp}/{mon.max_hp})", tags="bold")
# 몬스터 HP 0 이하 체크는 resolve_pending에서 함
return True
# 수정된 handle_monster_defeat 함수
def handle_monster_defeat(mon):
sprint(f"\n★ {mon.name}을(를) 물리쳤습니다! 더 이상 위협적이지 않습니다.")
# 현재 방의 몬스터가 쓰러뜨린 몬스터와 동일한 경우에만 제거
if player.room and player.room.monster is mon:
player.room.monster = None
if mon.name == "동굴 고블린":
player.flags["goblin_dead"] = True;
if "강타" not in player.skills: # 스킬 중복 추가 방지
add_skill("강타", cost=2, dmg=10);
# ================== 메시지 수정 ==================
sprint("고블린을 물리치고 오른쪽으로 가는 길이 열렸다!") # "동쪽" -> "오른쪽"
# ===============================================
elif mon.is_boss: # is_boss 플래그로 보스 판별
player.flags["boss_dead"] = True;
if "달의 파편" not in player.inv or player.inv.get("달의 파편", 0) == 0: # 파편 중복 추가 방지
player.inv["달의 파편"] = player.inv.get("달의 파편", 0) + 1
sprint("수호자가 쓰러지며 빛나는 '달의 파편'을 남겼습니다. (달의 파편 획득!)")
else:
sprint("수호자가 쓰러졌지만, 이미 달의 파편을 가지고 있습니다.")
sprint("(이 파편이라면... 제단의 홈에 맞을지도 모르겠군. 어서 가보자!)")
status_bar() # 파편 획득 후 상태바 업데이트
# initiate_battle 함수
def initiate_battle(mon):
sprint(f"\n★ {mon.name} 과(와) 전투 시작!")
pending.update(type="battle_choice", monster=mon)
prompt_battle_action(full_info=True)
# prompt_battle_action 함수
def prompt_battle_action(full_info=False):
status_bar()
mon = pending.get("monster")
if mon: sprint(f"== {mon.name} | HP {mon.hp}/{mon.max_hp} ==") # 몬스터 상태 표시
if full_info:
skill_list = ", ".join(f"{name}({data['cost']}MP)" for name, data in player.skills.items());
# 전투 중 사용 가능한 회복/버프 아이템만 표시
item_list = ", ".join(f"{k}:{v}" for k, v in player.inv.items() if v > 0 and k in ["HP포션", "MP포션", "치유 약초", "강장제"])
item_list = item_list if item_list else "없음"
sprint(f"사용 가능 스킬: [{skill_list}]");
sprint(f"사용 가능 아이템: [{item_list}]")
sprint("행동: 스킬명 / 아이템명 / 도주 (예: 베기, HP포션, 도주)")
# ────────── 펜딩(Pending) 상태 및 입력 처리 ──────────
pending = {"type": None}
# resolve_pending 함수 (v24 기반, 부분 일치, '이전' 처리 개선)
def resolve_pending(raw_input: str) -> bool:
p_type = pending.get("type")
if not p_type: return False
raw_input_lower = raw_input.strip().lower()
# --- 모든 펜딩 상태에서 '이전' 처리 ---
if raw_input_lower == "이전":
action_name = {"move_prompt": "이동", "take_prompt": "줍기", "use_prompt": "아이템 사용",
"talk_prompt": "대화", "give_prompt": "주기", "battle_choice": "전투 행동 선택"}.get(p_type, "행동")
sprint(f"{action_name}을(를) 취소했습니다.")
pending.clear()
if p_type == "battle_choice":
prompt_battle_action(full_info=True) # 전투 취소 시 다시 행동 선택 요청
else:
show_actions() # 일반 상태 복귀
return True # '이전' 입력 처리 완료
# --- 각 펜딩 타입별 처리 ---
action_complete = True # 기본값 True, 실패 시 False로 변경됨
if p_type == "battle_choice":
mon = pending.get("monster")
# 몬스터가 없거나 이미 죽은 경우의 안전장치
if not mon or mon.hp <= 0:
pending.clear(); show_actions(); return True
player_took_turn = False; monster_died_this_turn = False
matched_action = False
# 1. 스킬 사용 시도
found_skill_key = None
for skill in player.skills:
if raw_input_lower in skill.lower(): found_skill_key = skill; break
if found_skill_key:
skill_success = player_skill(mon, found_skill_key)
if skill_success:
player_took_turn = True
if mon.hp <= 0: monster_died_this_turn = True
matched_action = True
else: # 스킬 실패 (MP 부족 등)
prompt_battle_action(full_info=False); return True # 입력 다시 받기
# 2. 아이템 사용 시도 (스킬이 아닐 경우)
elif not matched_action:
found_item_key = None
usable_items = ["HP포션", "MP포션", "치유 약초", "강장제"] # 전투 중 사용 가능 아이템
for item in player.inv:
if player.inv[item] > 0 and item in usable_items and raw_input_lower in item.lower():
found_item_key = item; break
if found_item_key:
if use_item(found_item_key): # use_item 함수는 내부적으로 부분 매칭 처리
player_took_turn = True
# 아이템 사용으로 몬스터가 죽는 경우는 없음
matched_action = True
else: # 아이템 사용 실패 (HP/MP 풀 등)
prompt_battle_action(full_info=False); return True # 입력 다시 받기
# 3. 도주 시도 (스킬/아이템 아닐 경우)
elif raw_input_lower == "도주":
matched_action = True
if mon.is_boss:
sprint("강력한 기운 때문에 도망칠 수 없습니다!"); prompt_battle_action(full_info=False); return True
else:
sprint("전투에서 도망쳤습니다!"); pending.clear(); show_actions(); return True
# 4. 유효하지 않은 입력
if not matched_action:
sprint("잘못된 행동입니다. 스킬명, 아이템명, 또는 '도주'/'이전'을 입력하세요.")
prompt_battle_action(full_info=False); return True # 입력 다시 받기
# --- 몬스터 턴 진행 ---
if player_took_turn and not monster_died_this_turn and player.hp > 0:
monster_attack(mon) # 몬스터 공격 (내부에서 플레이어 사망 시 game_over 호출)
# --- 전투 상태 확인 및 다음 단계 ---
if monster_died_this_turn: # 플레이어 턴에 몬스터 사망
handle_monster_defeat(mon)
pending.clear()
show_actions()
elif player.hp <= 0: # 몬스터 턴에 플레이어 사망
# game_over는 monster_attack에서 호출됨
pending.clear() # 게임 종료 상태이므로 추가 액션 불필요
elif mon and mon.hp > 0: # 둘 다 생존
prompt_battle_action(full_info=False) # 다음 플레이어 턴 요청
return True # battle_choice 처리 완료 (종료 또는 다음 입력 대기)
# --- 다른 펜딩 타입 처리 ---
elif p_type == "move_prompt": action_complete = do_move(raw_input) # 한글 방향 그대로 전달
elif p_type == "take_prompt": action_complete = do_take(raw_input) # 부분 일치 do_take에서 처리
elif p_type == "use_prompt": action_complete = use_item(raw_input) # 부분 일치 use_item에서 처리
elif p_type == "talk_prompt": action_complete = talk_to(raw_input) # 부분 일치 talk_to에서 처리
elif p_type == "give_prompt":
# '주기'는 인자가 2개 필요하므로 handle_command에서 처리하는 것이 더 적합
sprint("주기 명령은 '주기 [대상] [아이템]' 형식으로 입력해주세요.")
pending.clear(); show_actions(); return True # 취소하고 다시 입력 받기
# --- 최종 처리 ---
# action_complete가 True/False면 pending 해제
if action_complete is not None:
pending.clear()
if not action_complete: # 작업 실패 시
show_actions() # 실패 메시지 후 현재 상태 다시 표시
# 성공(True) 시에는 각 do_ 함수 내에서 상태 갱신 (show_room 등)
return True
else: # 예외적인 경우 (None 반환 등)
pending.clear(); show_actions(); return True
# 여기까지 오면 안 됨
return False
# ────────── handle_command 함수 ──────────
def handle_command(event=None):
if player.flags["game_over"]: return
raw = entry.get().strip(); entry.delete(0, tk.END)
if not raw: return
sprint("=" * 70) # 턴 구분선
# --- Pending 상태 우선 처리 ---
if pending.get("type"):
# resolve_pending이 True를 반환하면 입력 처리 완료된 것
if resolve_pending(raw):
return
# --- 일반 명령어 처리 ---
sprint(f"> {raw}")
parts = raw.split(); verb = parts[0].lower() if parts else "" # 명령어 소문자화
args = parts[1:] if len(parts) > 1 else []; r = player.room
command_known = True; action_needs_prompt = True # 기본적으로 다음 프롬프트 필요
# 명령어 처리
if verb == "보기": do_look(); action_needs_prompt = False # 보기는 자체적으로 show_actions 호출
elif verb == "도움말": do_help(); # 도움말 후엔 프롬프트 필요
elif verb == "소지품": do_inventory(); # 소지품 후엔 프롬프트 필요
elif verb == "이동":
if not args: # 방향 입력 요구
if not r: possible_exits = []
else: possible_exits = [eng2ko.get(e,e) for e in r.exits if not (r.name == "수정 동굴" and e == "east" and not player.flags["goblin_dead"])]
if not possible_exits: sprint("이동할 수 있는 출구가 없습니다.")
else: sprint("어느 방향으로 이동하시겠습니까? (" + ", ".join(possible_exits) + ") ('이전' 입력 시 취소)"); pending.update(type="move_prompt"); action_needs_prompt = False # pending 상태 진입
else: # 방향 입력 받음
if not do_move(args[0]): action_needs_prompt = True # 이동 실패 시 프롬프트 필요
else: action_needs_prompt = False # 이동 성공 시 do_move에서 show_room 호출
elif verb == "줍기":
if not args: # 아이템 입력 요구
if not r or not r.items: sprint("주울 아이템이 없습니다.")
else: sprint("어떤 아이템을 주우시겠습니까? (" + ", ".join(r.items) + ") ('이전' 입력 시 취소)"); pending.update(type="take_prompt"); action_needs_prompt = False # pending 상태 진입
else: # 아이템 입력 받음
# do_take 성공/실패는 반환값으로, 상태 표시는 내부에서 함
if not do_take(" ".join(args)): action_needs_prompt = True # 실패 시 프롬프트 필요
else: action_needs_prompt = True # 성공해도 다음 행동 위해 프롬프트 필요 (줍고 나서 다른 행동 가능)
elif verb == "사용":
if not args: # 아이템 입력 요구
usable_items_inv = [k for k,v in player.inv.items() if v > 0]
if not usable_items_inv: sprint("사용할 아이템이 없습니다.")
else: sprint("어떤 아이템을 사용하시겠습니까? (" + ", ".join(usable_items_inv) + ") ('이전' 입력 시 취소)"); pending.update(type="use_prompt"); action_needs_prompt = False # pending 상태 진입
else: # 아이템 입력 받음
# use_item 성공/실패는 반환값으로, 상태 표시는 내부에서 함 (전투 중)
if not use_item(" ".join(args)): action_needs_prompt = True # 실패 시 프롬프트 필요
else: action_needs_prompt = True # 성공해도 다음 행동 위해 프롬프트 필요
elif verb == "대화":
if not args: # 대상 입력 요구
if not r or not r.npc: sprint("대화할 인물이 없습니다.")
else: sprint("누구와 대화하시겠습니까? (" + ", ".join(n.name for n in r.npc) + ") ('이전' 입력 시 취소)"); pending.update(type="talk_prompt"); action_needs_prompt = False # pending 상태 진입
else: # 대상 입력 받음
# talk_to 성공/실패는 반환값으로, 대화 내용은 내부에서 출력
if not talk_to(" ".join(args)): action_needs_prompt = True # 실패 시 프롬프트 필요
else: action_needs_prompt = True # 성공해도 다음 행동 위해 프롬프트 필요
elif verb == "주기":
if len(args) < 2: # 인자 부족
# 자동 주기 로직 (은둔자에게 약초 주기)
r = player.room
can_auto_give = False
if r and any(n.name == "은둔자" for n in r.npc) and \
player.inv.get("치유 약초", 0) > 0 and \
not player.flags["herb_given"]:
can_auto_give = True
if can_auto_give:
sprint("(가지고 있는 치유 약초를 은둔자에게 자동으로 건넵니다...)")
if not do_give(["은둔자", "치유 약초"]): action_needs_prompt = True
else: action_needs_prompt = False # 성공 시 do_give에서 status_bar 호출
else:
sprint("무엇을 누구에게 줄까요? (예: 주기 은둔자 치유 약초)")
# 필요 시 여기서 pending 설정 가능
# pending.update(type="give_prompt")
# action_needs_prompt = False
else: # 인자 충분 (대상, 아이템)
# do_give 성공/실패는 반환값으로, 상태 표시는 내부에서 함
if not do_give(args): action_needs_prompt = True # 실패 시 프롬프트 필요
else: action_needs_prompt = False # 성공 시 do_give에서 status_bar 호출
elif verb == "전투":
mon = r.monster if r else None
if not mon or mon.hp <= 0: sprint("싸울 대상이 없습니다.")
else: initiate_battle(mon); action_needs_prompt = False # 전투 시작, pending 상태 진입
elif verb == "종료": root.quit(); action_needs_prompt = False
else: # 알 수 없는 명령어
sprint(f"알 수 없는 명령입니다: '{raw}'. 사용 가능한 명령어는 '도움말'을 입력하여 확인하세요.")
command_known = False; action_needs_prompt = True # 실패했으므로 프롬프트 필요
# --- 명령 처리 후 상태 업데이트 ---
# pending 상태가 아니고, 게임오버가 아니고, 추가 프롬프트가 필요한 경우
if not pending.get("type") and not player.flags["game_over"] and action_needs_prompt:
show_actions() # 현재 방 상태와 가능한 행동 다시 표시
# ────────── Tkinter 이벤트 바인딩 ──────────
entry.bind("<Return>", handle_command)
# ────────── 게임 시작 ──────────
숲, 게이트, 동굴, 제단 = build_world()
player.room = 숲
sprint("==========================================")
sprint("달의 파편 - 최종판 (개선판 v24 수정)") # 버전 업데이트
sprint("==========================================")
sprint()
objective = "게임 목표: 달의 수호자를 물리치고 ★달의 파편★을 얻어 제단의 홈에 사용하세요!"
sprint(objective, tags="large_bold")
sprint()
sprint("숲 속 빈터에서 모험이 시작됩니다...")
sprint("무엇을 할까요? ('도움말' 입력 시 명령어 확인)")
show_room() # 게임 시작 시 첫 방 정보 표시
# ────────── Tkinter 메인 루프 시작 ──────────
root.mainloop()
4. 후기
구독하고 있는 AI를 최대한 활용해서 개발해보니, 게임개발자들의 위대함을 깨달을 수 있었다.. (특히 코딩, 콘텐츠 개발쪽)
그래도 꽤 흥미가 있었던 작업이라서 매주 코딩개발이 아니더라도 매주 AI를 활용해서 여러가지 취미생활을 해봐야겠다.
혹시나 게임을 해보고 싶으신 분이 계신다면 exe 파일은 별도로 공유드리겠습니다.