はじめに
昔から某RPGが大好きで、いつか勇者になれたらと思った過去もありました。
今回は学習しているpythonを通して、RPGの作成をしながら学んでいきたいと思います。
勉強の復習にもつながるように、内容はある程度詳細として表記をしてみました。
要件定義
サーバーサイド:python
フロントエンド:tkinter
ゲーム構成
はじまりのまち
↓ドロップアイテムの選択
中間町
↓ドロップアイテムの選択
魔王を倒す
選択したドロップアイテムにより、主人公に補正がかかるので、最終戦での戦いやすさが変化する。
作成過程
gui_game.py
import tkinter as tk
from tkinter import messagebox
import time
import random
from PIL import Image, ImageTk # Pillowライブラリをインポート
from main import create_player, items_data, enemies, locations, calculate_damage
class RPGGameGUI:
def __init__(self, master):
self.master = master
master.title("勇者みならいの冒険")
master.geometry("800x600")
master.resizable(False, False)
self.player = None
self.current_location_key = None
self.current_enemy = None
self.current_enemy_key = None
# キャラクター画像のパスを定義
self.enemy_images = {
"ゴブリン": "img/ゴブリン.png",
"ゴブリンキング": "img/ゴブリン.png", # ゴブリンキングの画像がないため、一旦ゴブリンを代用
"オーク": "img/オーク.png",
"魔導士": "img/魔法使い.png", # ファイル名が「魔法使い.png」のため修正
"デビル": "img/デビル.png",
"魔王": "img/maou.png"
}
self.loaded_images = {} # 画像オブジェクトを保持するための辞書
self.create_widgets()
self.create_battle_widgets()
self.start_game_setup()
def create_widgets(self):
self.master.configure(bg="#2c3e50")
# 背景画像表示用のキャンバス
self.background_canvas = tk.Canvas(self.master, bg="#2c3e50", highlightthickness=0)
self.background_canvas.pack(fill="both", expand=True)
# 全てのUIフレームをキャンバスのウィンドウとして配置
self.message_frame = tk.Frame(self.background_canvas, bd=2, relief="solid", bg="#34495e", padx=10, pady=10)
self.message_frame_window = self.background_canvas.create_window(400, 50, window=self.message_frame, width=760, anchor="n") # 中央上
self.message_label = tk.Label(self.message_frame, text="ゲームを開始します...", wraplength=720,
justify="left", font=("Meiryo UI", 14), fg="#ecf0f1", bg="#34495e")
self.message_label.pack(pady=5, fill="both", expand=True)
self.status_frame = tk.Frame(self.background_canvas, bd=2, relief="solid", bg="#34495e", padx=10, pady=5)
self.status_frame_window = self.background_canvas.create_window(400, 150, window=self.status_frame, width=760, anchor="n") # 少し下
self.status_label = tk.Label(self.status_frame,
text="プレイヤーHP: 100/100 攻撃力: 10 防御力: 5",
font=("Meiryo UI", 12, "bold"), fg="#ecf0f1", bg="#34495e")
self.status_label.pack(pady=3)
self.choices_frame = tk.Frame(self.background_canvas, bd=2, relief="solid", bg="#34495e", padx=10, pady=10)
self.choices_frame_window = self.background_canvas.create_window(400, 250, window=self.choices_frame, width=760, height=300, anchor="n") # さらに下、高さ指定
self.choice_buttons = []
self.item_drop_buttons = []
def create_battle_widgets(self):
# 敵のステータス表示はキャンバスの下部に配置
self.enemy_status_label = tk.Label(self.background_canvas, text="",
font=("Meiryo UI", 12, "bold"), fg="#e74c3c", bg="#2c3e50")
self.enemy_status_label_window = self.background_canvas.create_window(400, 200, window=self.enemy_status_label, anchor="n") # 敵のステータス位置調整
self.battle_commands_frame = tk.Frame(self.background_canvas, bd=2, relief="solid", bg="#34495e", padx=10, pady=10)
self.battle_commands_frame_window = self.background_canvas.create_window(400, 500, window=self.battle_commands_frame, width=760, anchor="s") # コマンドを下部に
button_font = ("Meiryo UI", 12, "bold")
button_bg = "#2980b9"
button_fg = "#ecf0f1"
self.attack_button = tk.Button(self.battle_commands_frame, text="⚔️ たたかう",
command=self.player_attack, font=button_font, bg=button_bg, fg=button_fg,
activebackground="#3498db", activeforeground="#ecf0f1")
self.item_button = tk.Button(self.battle_commands_frame, text="🩹 どうぐ",
command=self.show_item_menu, font=button_font, bg=button_bg, fg=button_fg,
activebackground="#3498db", activeforeground="#ecf0f1")
self.escape_button = tk.Button(self.battle_commands_frame, text="🏃 にげる",
command=self.try_escape, font=button_font, bg=button_bg, fg=button_fg,
activebackground="#3498db", activeforeground="#ecf0f1")
# 初期状態では非表示
self.background_canvas.itemconfigure(self.enemy_status_label_window, state="hidden")
self.background_canvas.itemconfigure(self.battle_commands_frame_window, state="hidden")
def update_display(self):
"""現在のゲーム状態に基づいて表示を更新する (マップ画面用)"""
# 戦闘用ウィジェットを非表示に
self.background_canvas.itemconfigure(self.enemy_status_label_window, state="hidden")
self.background_canvas.itemconfigure(self.battle_commands_frame_window, state="hidden")
self.attack_button.pack_forget()
self.item_button.pack_forget()
self.escape_button.pack_forget()
self._clear_item_drop_buttons()
self._clear_enemy_image() # 戦闘終了後、敵画像をクリア
# 通常のUIフレームを表示
self.background_canvas.itemconfigure(self.message_frame_window, state="normal")
self.background_canvas.itemconfigure(self.status_frame_window, state="normal")
self.background_canvas.itemconfigure(self.choices_frame_window, state="normal")
if not self.player:
return
current_loc = locations[self.current_location_key]
self.message_label.config(text=f"--- 現在地: {current_loc['name']} ---\n{current_loc['description']}")
self.status_label.config(text=f"👨🦰 勇者{self.player['name']} HP: {self.player['hp']}/{self.player['max_hp']} 攻撃力: {self.player['attack']} 防御力: {self.player['defense']}")
# 回復スポットの場合の処理
if current_loc.get("healing_spot"):
if self.player["hp"] < self.player["max_hp"]:
self.player["hp"] = self.player["max_hp"]
self.message_label.config(text=f"--- 現在地: {current_loc['name']} ---\n{current_loc['description']}\n\n✨ 泉の力でHPが満タンになった!")
self.update_battle_display() # HP表示を即座に更新
else:
self.message_label.config(text=f"--- 現在地: {current_loc['name']} ---\n{current_loc['description']}\n\nすでにHPは満タンだ。")
for button in self.choice_buttons:
button.destroy()
self.choice_buttons.clear()
# 動的に選択肢を生成
current_choices = dict(current_loc["choices"]) # 元のchoicesをコピーして変更を加える
# 回復の泉への選択肢を動的に追加
if "東の森_戦闘" in self.player["cleared_enemies"] and \
"西の洞窟_戦闘" in self.player["cleared_enemies"] and \
self.current_location_key == "始まりの町":
current_choices["回復の泉へ向かう"] = "回復の泉"
if current_choices:
for i, (choice_text, next_location_key) in enumerate(current_choices.items()):
button = tk.Button(self.choices_frame, text=choice_text,
command=lambda key=next_location_key: self.move_to_location(key),
font=("Meiryo UI", 12), bg="#2980b9", fg="#ecf0f1",
activebackground="#3498db", activeforeground="#ecf0f1")
button.pack(pady=5, fill="x", padx=10)
self.choice_buttons.append(button)
status_button = tk.Button(self.choices_frame, text="📊 ステータスを見る", command=self.show_status,
font=("Meiryo UI", 12), bg="#7f8c8d", fg="#ecf0f1",
activebackground="#95a5a6", activeforeground="#ecf0f1")
status_button.pack(pady=5, fill="x", padx=10)
self.choice_buttons.append(status_button)
exit_button = tk.Button(self.choices_frame, text="🚪 ゲーム終了", command=self.master.quit,
font=("Meiryo UI", 12), bg="#c0392b", fg="#ecf0f1",
activebackground="#e74c3c", activeforeground="#ecf0f1")
exit_button.pack(pady=5, fill="x", padx=10)
self.choice_buttons.append(exit_button)
else:
if current_loc["enemy"]:
battle_start_button = tk.Button(self.choices_frame, text=f"⚔️ {current_loc['enemy']}と戦う!",
command=lambda: self.start_battle(current_loc['enemy']),
font=("Meiryo UI", 14, "bold"), bg="#e67e22", fg="#ecf0f1",
activebackground="#f39c12", activeforeground="#ecf0f1")
battle_start_button.pack(pady=20, fill="x", padx=10)
self.choice_buttons.append(battle_start_button)
else:
pass # Game clear / game over message if applicable
def start_game_setup(self):
self.message_label.config(text="勇者の名前を入力してください:")
self.name_entry = tk.Entry(self.choices_frame, font=("Meiryo UI", 12), bg="#ecf0f1", fg="#2c3e50", insertbackground="#2c3e50")
self.name_entry.pack(pady=10)
name_submit_button = tk.Button(self.choices_frame, text="名前を決定", command=self.process_name_input,
font=("Meiryo UI", 12, "bold"), bg="#27ae60", fg="#ecf0f1",
activebackground="#2ecc71", activeforeground="#ecf0f1")
name_submit_button.pack(pady=5)
self.choice_buttons.append(self.name_entry)
self.choice_buttons.append(name_submit_button)
def process_name_input(self):
player_name = self.name_entry.get().strip()
if not player_name:
messagebox.showwarning("入力エラー", "名前を入力してください!", parent=self.master)
return
self.player = create_player(player_name)
for widget in self.choice_buttons:
widget.destroy()
self.choice_buttons.clear()
self.message_label.config(text="冒険を始めるにあたり、どちらかのオーブを選んでください。\n✨ 金のオーブ (攻撃力アップ)\n🛡️ 銀のオーブ (防御力アップ)")
orb_button_1 = tk.Button(self.choices_frame, text="✨ 金のオーブ (攻撃力アップ)", command=lambda: self.choose_orb("1"),
font=("Meiryo UI", 12), bg="#f1c40f", fg="#2c3e50",
activebackground="#f39c12", activeforeground="#2c3e50")
orb_button_1.pack(pady=5, fill="x", padx=10)
self.choice_buttons.append(orb_button_1)
orb_button_2 = tk.Button(self.choices_frame, text="🛡️ 銀のオーブ (防御力アップ)", command=lambda: self.choose_orb("2"),
font=("Meiryo UI", 12), bg="#bdc3c7", fg="#2c3e50",
activebackground="#95a5a6", activeforeground="#2c3e50")
orb_button_2.pack(pady=5, fill="x", padx=10)
self.choice_buttons.append(orb_button_2)
def choose_orb(self, choice):
if choice == "1":
self.player["items"].append(items_data["金のオーブ"])
# ここは修正: main.pyのitems_dataに合わせてアクセスする
self.player["attack"] += items_data["金のオーブ"]["attack_boost"]
self.message_label.config(text="金のオーブを手に入れた!攻撃力がアップした!")
elif choice == "2":
self.player["items"].append(items_data["銀のオーブ"])
# ここは修正: main.pyのitems_dataに合わせてアクセスする
self.player["defense"] += items_data["銀のオーブ"]["defense_boost"]
self.message_label.config(text="銀のオーブを手に入れた!防御力がアップした!")
else:
self.message_label.config(text="無効な選択です。オーブは選べませんでした。")
for button in self.choice_buttons:
button.destroy()
self.choice_buttons.clear()
self.current_location_key = "始まりの町"
self.master.after(1000, self.update_display)
def move_to_location(self, next_location_key):
self.current_location_key = next_location_key
self.update_display()
def start_battle(self, enemy_key):
self.current_enemy_key = enemy_key
self.current_enemy = dict(enemies[enemy_key])
# 通常のUIフレームを非表示に
self.background_canvas.itemconfigure(self.message_frame_window, state="hidden")
self.background_canvas.itemconfigure(self.status_frame_window, state="hidden")
self.background_canvas.itemconfigure(self.choices_frame_window, state="hidden")
# 戦闘用ウィジェットを表示
self.background_canvas.itemconfigure(self.enemy_status_label_window, state="normal")
self.background_canvas.itemconfigure(self.battle_commands_frame_window, state="normal")
self.message_frame_window_battle = self.background_canvas.create_window(400, 50, window=self.message_frame, width=760, anchor="n") # メッセージフレームは戦闘中も表示
self.status_frame_window_battle = self.background_canvas.create_window(400, 150, window=self.status_frame, width=760, anchor="n") # ステータスフレームも戦闘中表示
self.attack_button.pack(side="left", expand=True, fill="x", padx=5, pady=5)
self.item_button.pack(side="left", expand=True, fill="x", padx=5, pady=5)
self.escape_button.pack(side="left", expand=True, fill="x", padx=5, pady=5)
# 敵の画像を背景に表示
self._load_and_display_enemy_image(enemy_key)
self.update_battle_display()
self.display_battle_message(f"--- ⚔️ {self.current_enemy['name']}が現れた! ⚔️ ---")
self.master.after(1000, self.player_turn)
def _load_and_display_enemy_image(self, enemy_key):
image_path = self.enemy_images.get(enemy_key)
if image_path:
try:
# 画像を読み込み、サイズ調整
original_image = Image.open(image_path)
# 画像が大きすぎる場合、適切なサイズにリサイズ(例: 300x300)
# アスペクト比を維持しつつリサイズ
width, height = original_image.size
max_size = 300
if width > max_size or height > max_size:
if width > height:
new_width = max_size
new_height = int(max_size * height / width)
else:
new_height = max_size
new_width = int(max_size * width / height)
resized_image = original_image.resize((new_width, new_height), Image.Resampling.LANCZOS)
else:
resized_image = original_image
tk_image = ImageTk.PhotoImage(resized_image)
self.loaded_images[enemy_key] = tk_image # 参照を保持
# 古い画像を削除
if hasattr(self, 'current_enemy_image_id'):
self.background_canvas.delete(self.current_enemy_image_id)
# キャンバスの中央に配置
self.current_enemy_image_id = self.background_canvas.create_image(400, 300, image=tk_image, anchor="center") # 中央に表示
self.background_canvas.tag_lower(self.current_enemy_image_id) # 他のウィジェットの下に配置
except FileNotFoundError:
print(f"Error: Image file not found for {enemy_key} at {image_path}")
except Exception as e:
print(f"Error loading image for {enemy_key}: {e}")
else:
self._clear_enemy_image() # 画像がない場合はクリア
def _clear_enemy_image(self):
if hasattr(self, 'current_enemy_image_id'):
self.background_canvas.delete(self.current_enemy_image_id)
del self.current_enemy_image_id
self.loaded_images.clear() # 保持していた画像参照もクリア
def update_battle_display(self):
self.status_label.config(text=f"👨🦰 勇者{self.player['name']} HP: {self.player['hp']}/{self.player['max_hp']} 攻撃力: {self.player['attack']} 防御力: {self.player['defense']}")
enemy_icon = "👹" # デフォルトアイコン
if self.current_enemy_key == "ゴブリン": enemy_icon = "🧌"
elif self.current_enemy_key == "ゴブリンキング": enemy_icon = "👑"
elif self.current_enemy_key == "オーク": enemy_icon = "🐷"
elif self.current_enemy_key == "魔導士": enemy_icon = "🧙"
elif self.current_enemy_key == "デビル": enemy_icon = "👿"
elif self.current_enemy_key == "魔王": enemy_icon = "😈"
self.enemy_status_label.config(text=f"{enemy_icon} 敵: {self.current_enemy['name']} HP: {self.current_enemy['hp']}/{enemies[self.current_enemy_key]['hp']}")
def display_battle_message(self, message):
current_message = self.message_label.cget("text")
lines = (current_message + "\n" + message).split('\n')
display_lines = lines[-5:]
self.message_label.config(text="\n".join(display_lines))
self.master.update_idletasks()
def player_turn(self):
self.update_battle_display()
self.attack_button.config(state=tk.NORMAL)
self.item_button.config(state=tk.NORMAL)
self.escape_button.config(state=tk.NORMAL)
self.display_battle_message("あなたのターンです。コマンドを選択してください:")
def player_attack(self):
self.attack_button.config(state=tk.DISABLED)
self.item_button.config(state=tk.DISABLED)
self.escape_button.config(state=tk.DISABLED)
self.display_battle_message(f"👨🦰 {self.player['name']}の攻撃!")
player_attack_power = self.player['attack']
damage = calculate_damage(player_attack_power, self.current_enemy['defense'])
self.current_enemy['hp'] -= damage
self.master.after(1000, lambda: self._display_player_damage(damage))
def _display_player_damage(self, damage):
self.display_battle_message(f"{self.current_enemy['name']}に{damage}のダメージを与えた!")
self.update_battle_display()
if self.current_enemy['hp'] <= 0:
self.check_battle_end()
else:
self.master.after(1000, self.enemy_attack)
def enemy_attack(self):
if self.current_enemy['hp'] <= 0:
self.check_battle_end()
return
self.display_battle_message(f"\n--- ⚡️ {self.current_enemy['name']}の攻撃! ⚡️ ---")
enemy_attack_message = ""
if "attack_patterns" in self.current_enemy and self.current_enemy["attack_patterns"]:
enemy_attack_message = random.choice(self.current_enemy['attack_patterns'])
self.display_battle_message(f"{self.current_enemy['name']}: {enemy_attack_message}")
else:
self.display_battle_message(f"{self.current_enemy['name']}は攻撃してきた!")
self.master.after(1500, self._perform_enemy_damage)
def _perform_enemy_damage(self):
damage = calculate_damage(self.current_enemy['attack'], self.player['defense'])
self.player['hp'] -= damage
self.display_battle_message(f"👨🦰 {self.player['name']}は{damage}のダメージを受けた!")
self.update_battle_display()
if self.player['hp'] <= 0:
self.check_battle_end()
else:
self.master.after(1000, self.player_turn)
def check_battle_end(self):
if self.player['hp'] <= 0:
self.display_battle_message(f"👨🦰 {self.player['name']}は力尽きた...")
messagebox.showerror("ゲームオーバー", "力尽きてしまった...", parent=self.master)
self.master.quit()
return
if self.current_enemy['hp'] <= 0:
self.display_battle_message(f"🎉 {self.current_enemy['name']}を倒した! 🎉")
current_loc_data = locations.get(self.current_location_key)
if current_loc_data and "clear_condition" in current_loc_data:
condition_key = current_loc_data["clear_condition"][0]
if condition_key not in self.player["cleared_enemies"]:
self.player["cleared_enemies"].append(condition_key)
print(f"DEBUG: cleared_enemies: {self.player['cleared_enemies']}")
self.master.after(1000, self._handle_item_drop)
return
def _handle_item_drop(self):
# main.py の enemies データ構造に合わせて 'drop_items' に変更
dropped_item_keys = enemies[self.current_enemy_key].get("drop_items", [])
if not dropped_item_keys:
self.display_battle_message("何もドロップしなかった...")
self.master.after(1000, lambda: self.end_battle("win"))
return
self.display_battle_message("ドロップアイテムがあります!どれを選びますか?")
self.attack_button.pack_forget()
self.item_button.pack_forget()
self.escape_button.pack_forget()
for item_key in dropped_item_keys:
item = items_data[item_key]
icon = "🎁"
if item.get("type") == "consumable":
icon = "🩹"
elif item.get("type") == "weapon":
icon = "⚔️"
elif item.get("type") == "armor":
icon = "🛡️"
elif item.get("type") == "permanent_boost":
icon = "✨"
elif item.get("type") == "special":
icon = "🏆"
item_description = item.get("description", "")
if item.get("type") == "weapon" and "attack_boost" in item:
item_description = f"攻撃力+{item['attack_boost']}"
elif item.get("type") == "armor" and "defense_boost" in item:
item_description = f"防御力+{item['defense_boost']}"
elif item.get("type") == "permanent_boost":
boost_info = []
if "attack_boost" in item:
boost_info.append(f"攻撃力+{item['attack_boost']}")
if "defense_boost" in item:
boost_info.append(f"防御力+{item['defense_boost']}")
item_description = ", ".join(boost_info)
item_button = tk.Button(self.battle_commands_frame,
text=f"{icon} {item['name']} ({item_description})",
command=lambda it=item: self._select_dropped_item(it),
font=("Meiryo UI", 12), bg="#e7b416", fg="#2c3e50",
activebackground="#f0d56b", activeforeground="#2c3e50")
item_button.pack(pady=5, fill="x", expand=True)
self.item_drop_buttons.append(item_button)
self.battle_commands_frame.pack(pady=10, padx=20, fill="x")
def _select_dropped_item(self, selected_item):
self.player['items'].append(selected_item)
self.display_battle_message(f"✨ {selected_item['name']}を手に入れた! ✨")
status_change_message = ""
item_type = selected_item.get('type')
if item_type == 'permanent_boost':
if 'attack_boost' in selected_item:
self.player['attack'] += selected_item['attack_boost']
status_change_message += f"攻撃力が{selected_item['attack_boost']}アップした!\n"
if 'defense_boost' in selected_item:
self.player['defense'] += selected_item['defense_boost']
status_change_message += f"防御力が{selected_item['defense_boost']}アップした!\n"
elif item_type == 'weapon':
if self.player['equipped_weapon']:
self.player['attack'] -= self.player['equipped_weapon'].get('attack_boost', 0)
status_change_message += f"{self.player['equipped_weapon']['name']}の装備を解除した。\n"
self.player['equipped_weapon'] = selected_item
self.player['attack'] += selected_item.get('attack_boost', 0)
status_change_message += f"{selected_item['name']}を装備した!攻撃力が{selected_item.get('attack_boost', 0)}アップした!\n"
elif item_type == 'armor':
if self.player['equipped_armor']:
self.player['defense'] -= self.player['equipped_armor'].get('defense_boost', 0)
status_change_message += f"{self.player['equipped_armor']['name']}の装備を解除した。\n"
self.player['equipped_armor'] = selected_item
self.player['defense'] += selected_item.get('defense_boost', 0)
status_change_message += f"{selected_item['name']}を装備した!防御力が{selected_item.get('defense_boost', 0)}アップした!\n"
if status_change_message:
self.display_battle_message(status_change_message.strip())
messagebox.showinfo("アイテム獲得", f"{selected_item['name']}を手に入れた!\n{status_change_message.strip()}", parent=self.master)
self._clear_item_drop_buttons()
self.master.after(500, lambda: self.end_battle("win"))
def _clear_item_drop_buttons(self):
for button in self.item_drop_buttons:
button.destroy()
self.item_drop_buttons.clear()
def end_battle(self, result):
# キャンバスに配置されたウィジェットを非表示に
self.background_canvas.itemconfigure(self.enemy_status_label_window, state="hidden")
self.background_canvas.itemconfigure(self.battle_commands_frame_window, state="hidden")
# パックされたボタンも非表示に
self.attack_button.pack_forget()
self.item_button.pack_forget()
self.escape_button.pack_forget()
self._clear_item_drop_buttons()
self._clear_enemy_image() # 戦闘終了時に敵画像をクリア
# 通常のUIフレームを表示に戻す
self.background_canvas.itemconfigure(self.message_frame_window, state="normal")
self.background_canvas.itemconfigure(self.status_frame_window, state="normal")
self.background_canvas.itemconfigure(self.choices_frame_window, state="normal")
if result == "win":
if self.current_enemy_key == "ゴブリン":
self.current_location_key = "北の砦"
elif self.current_enemy_key == "ゴブリンキング":
self.current_location_key = "古びた塔"
elif self.current_enemy_key == "オーク":
self.current_location_key = "奈落の淵"
elif self.current_enemy_key == "魔導士":
self.current_location_key = "奈落の淵"
elif self.current_enemy_key == "デビル":
self.current_location_key = "魔王城"
elif self.current_enemy_key == "魔王":
messagebox.showinfo("ゲームクリア!", "魔王を倒し、世界に平和が訪れた!\n勇者の伝説が今、始まった!", parent=self.master)
self.master.quit()
return
self.update_display()
self.display_battle_message("探索を続けよう!")
elif result == "lose":
pass
elif result == "escaped":
self.current_location_key = "始まりの町"
self.update_display()
self.display_battle_message("戦いを避け、始まりの町へ逃げ帰った。")
def show_item_menu(self):
self.attack_button.config(state=tk.DISABLED)
self.item_button.config(state=tk.DISABLED)
self.escape_button.config(state=tk.DISABLED)
self.display_battle_message("どのどうぐを使いますか?")
recoverable_items = [item for item in self.player['items'] if item.get('type') == 'consumable' and 'hp_recovery' in item]
if not recoverable_items:
self.display_battle_message("回復アイテムを持っていません!")
self.master.after(1000, self.player_turn)
return
self.attack_button.pack_forget()
self.item_button.pack_forget()
self.escape_button.pack_forget()
self.item_choice_buttons = []
for i, item in enumerate(recoverable_items):
item_button = tk.Button(self.battle_commands_frame,
text=f"🩹 {item['name']} (HP+{item['hp_recovery']})",
command=lambda it=item: self._use_item(it),
font=("Meiryo UI", 11), bg="#8e44ad", fg="#ecf0f1",
activebackground="#9b59b6", activeforeground="#ecf0f1")
item_button.pack(pady=3, fill="x", expand=True)
self.item_choice_buttons.append(item_button)
cancel_button = tk.Button(self.battle_commands_frame, text="キャンセル",
command=self._cancel_item_menu,
font=("Meiryo UI", 11), bg="#34495e", fg="#ecf0f1",
activebackground="#7f8c8d", activeforeground="#ecf0f1")
cancel_button.pack(pady=3, fill="x", expand=True)
self.item_choice_buttons.append(cancel_button)
def _use_item(self, item):
for btn in self.item_choice_buttons:
btn.destroy()
self.item_choice_buttons.clear()
self.attack_button.pack(side="left", expand=True, fill="x", padx=5, pady=5)
self.item_button.pack(side="left", expand=True, fill="x", padx=5, pady=5)
self.escape_button.pack(side="left", expand=True, fill="x", padx=5, pady=5)
if item['type'] == 'consumable' and 'hp_recovery' in item:
self.player['hp'] = min(self.player['max_hp'], self.player['hp'] + item['hp_recovery'])
self.player['items'].remove(item)
self.display_battle_message(f"🩹 {item['name']}を使った!HPが{item['hp_recovery']}回復した!")
self.update_battle_display()
else:
self.display_battle_message("そのアイテムは使えません。")
self.master.after(1000, self.enemy_attack)
def _cancel_item_menu(self):
for btn in self.item_choice_buttons:
btn.destroy()
self.item_choice_buttons.clear()
self.attack_button.pack(side="left", expand=True, fill="x", padx=5, pady=5)
self.item_button.pack(side="left", expand=True, fill="x", padx=5, pady=5)
self.escape_button.pack(side="left", expand=True, fill="x", padx=5, pady=5)
self.display_battle_message("アイテムの使用をキャンセルしました。")
self.master.after(500, self.player_turn)
def try_escape(self):
self.attack_button.config(state=tk.DISABLED)
self.item_button.config(state=tk.DISABLED)
self.escape_button.config(state=tk.DISABLED)
self.display_battle_message("🏃 逃走を試みる...")
self.master.update_idletasks()
if random.random() < 0.5:
self.master.after(1000, lambda: self.display_battle_message("💨 うまく逃げ切れた!"))
self.master.after(1500, lambda: messagebox.showinfo("逃走成功", "うまく逃げ切れた!", parent=self.master))
self.master.after(2000, lambda: self.end_battle("escaped"))
else:
self.master.after(1000, lambda: self.display_battle_message("❌ 逃げられなかった!"))
self.master.after(1500, self.enemy_attack)
def show_status(self):
status_text = (
f"名前: {self.player['name']}\n"
f"HP: {self.player['hp']}/{self.player['max_hp']}\n"
f"攻撃力: {self.player['attack']}\n"
f"防御力: {self.player['defense']}\n"
)
equipped_weapon_name = self.player["equipped_weapon"]["name"] if self.player["equipped_weapon"] else "なし"
equipped_armor_name = self.player["equipped_armor"]["name"] if self.player["equipped_armor"] else "なし"
status_text += f"装備: 武器 - {equipped_weapon_name}, 防具 - {equipped_armor_name}\n"
items_list = [item['name'] for item in self.player['items']]
status_text += f"所持アイテム: {', '.join(items_list) if items_list else 'なし'}"
messagebox.showinfo("📊 プレイヤー情報", status_text, parent=self.master)
# メイン処理
if __name__ == "__main__":
root = tk.Tk()
game_gui = RPGGameGUI(root)
root.mainloop()
Tkinter
pythonのライブラリの一つで、install不要のもの。 学習コストが少なく、簡易的なアプリのフロントであればおすすめ。 できること ウィンドウの生成 ボタン、メニュー、テキストボックス、ラベル、チェックボックス、ラジオボタンなどのウィジェットの配置や制御main.py
import random
import time
# --- ゲームデータ定義 ---
# プレイヤーの初期データ
def create_player(name):
return {
"name": f"勇者{name}",
"hp": 100,
"max_hp": 100,
"attack": 10,
"defense": 5,
"items": [],
"equipped_weapon": None,
"equipped_armor": None,
"cleared_enemies": [], # 新しく追加: 倒した敵のキーを記録するリスト
}
# アイテムデータ (変更なし)
items_data = {
"金のオーブ": {"name": "金のオーブ", "type": "orb", "attack_boost": 10, "description": "攻撃力が上がる不思議なオーブ。"},
"銀のオーブ": {"name": "銀のオーブ", "type": "orb", "defense_boost": 10, "description": "防御力が上がる不思議なオーブ。"},
"薬草": {"name": "薬草", "type": "consumable", "hp_recovery": 30, "description": "HPを30回復する。"},
"こんぼう": {"name": "こんぼう", "type": "weapon", "attack_boost": 5, "description": "ごく一般的なこんぼう。"},
"革の盾": {"name": "革の盾", "type": "armor", "defense_boost": 5, "description": "初心者向けの革の盾。"},
"勇者の証": {"name": "勇者の証", "type": "special", "description": "魔王を倒した勇者の証。"},
"おまもり": {"name": "おまもり", "type": "permanent_boost", "attack_boost": 10, "defense_boost": 30, "description": "持っているだけで守りが強くなるおまもり。"},
"戦いの護符": {"name": "戦いの護符", "type": "permanent_boost", "attack_boost": 50, "description": "持っているだけで攻撃力が高まる護符。"},
"秘薬": {"name": "秘薬", "type": "consumable", "hp_recovery": 80, "description": "HPを大きく回復する秘薬。"},
"勇者の剣": {"name": "勇者の剣", "type": "weapon", "attack_boost": 60, "description": "伝説の勇者が使ったとされる剣。"},
"鉄の鎧": {"name": "鉄の鎧", "type": "armor", "defense_boost": 50, "description": "頑丈な鉄でできた鎧。"},
"はねのブーツ": {"name": "はねのブーツ", "type": "armor", "defense_boost": 20, "description": "軽い素材でできたブーツ。"}
}
# 敵データ (変更なし)
enemies = {
"ゴブリン": {
"name": "ゴブリン", "hp": 50, "attack": 15, "defense": 3,
"attack_patterns": ["「キーキー!」と叫びながら攻撃!", "素早い動きで切りかかってきた!"],
"drop_items": ["薬草", "こんぼう"]
},
"ゴブリンキング": {
"name": "ゴブリンキング", "hp": 100, "attack": 25, "defense": 8,
"attack_patterns": ["「グガアアア!」と雄叫びを上げて殴りかかってきた!", "巨大な棍棒を振り下ろす!"],
"drop_items": ["鉄の鎧", "秘薬"]
},
"オーク": {
"name": "オーク", "hp": 80, "attack": 20, "defense": 6,
"attack_patterns": ["鈍重な一撃!", "「ブヒー!」と突進してきた!"],
"drop_items": ["革の盾", "薬草"]
},
"魔導士": {
"name": "魔導士", "hp": 70, "attack": 22, "defense": 4,
"attack_patterns": ["炎の魔法を唱えた!", "氷の魔法があなたを凍らせる!"],
"drop_items": ["はねのブーツ", "秘薬"]
},
"デビル": {
"name": "デビル", "hp": 120, "attack": 30, "defense": 10,
"attack_patterns": ["闇の力で襲いかかってきた!", "邪悪な波動を放つ!"],
"drop_items": ["おまもり", "戦いの護符"]
},
"魔王": {
"name": "魔王", "hp": 200, "attack": 40, "defense": 15,
"attack_patterns": ["絶望の淵に突き落とす一撃!", "世界の終わりを告げる闇の波動!"],
"drop_items": ["勇者の証", "勇者の剣"]
},
}
# 場所データ
locations = {
"始まりの町": {
"name": "始まりの町",
"description": "ここから冒険が始まる。人々の活気に満ちている。",
"choices": {
"東の森へ向かう": "東の森",
"西の洞窟へ向かう": "西の洞窟",
"北の砦へ向かう": "北の砦",
"古びた塔へ向かう": "古びた塔",
"奈落の淵へ向かう": "奈落の淵",
},
"enemy": None
},
"東の森": {
"name": "東の森",
"description": "薄暗い森の奥から、何かの気配がする...",
"choices": {
"奥へ進む": "東の森_戦闘",
"町へ戻る": "始まりの町"
},
"enemy": None
},
"東の森_戦闘": {
"name": "東の森の奥",
"description": "ゴブリンが待ち構えている!",
"choices": {},
"enemy": "ゴブリン",
"clear_condition": ["東の森_戦闘"] # この戦闘をクリアしたことを示すキー
},
"北の砦": {
"name": "北の砦",
"description": "かつては堅牢な砦だったが、今はオークが徘徊しているようだ。",
"choices": {
"奥へ進む": "北の砦_戦闘",
"町へ戻る": "始まりの町"
},
"enemy": None
},
"北の砦_戦闘": {
"name": "北の砦の奥",
"description": "オークが立ちふさがる!",
"choices": {},
"enemy": "オーク",
"clear_condition": ["北の砦_戦闘"]
},
"西の洞窟": {
"name": "西の洞窟",
"description": "冷たい風が吹き荒れる洞窟。奥には何かいるようだ。",
"choices": {
"奥へ進む": "西の洞窟_戦闘",
"町へ戻る": "始まりの町"
},
"enemy": None
},
"西の洞窟_戦闘": {
"name": "西の洞窟の奥",
"description": "ゴブリンキングが襲いかかる!",
"choices": {},
"enemy": "ゴブリンキング",
"clear_condition": ["西の洞窟_戦闘"]
},
"古びた塔": {
"name": "古びた塔",
"description": "今にも崩れそうな古い塔。魔導士の気配がする。",
"choices": {
"塔の奥へ": "古びた塔_戦闘",
"町へ戻る": "始まりの町"
},
"enemy": None
},
"古びた塔_戦闘": {
"name": "古びた塔の頂",
"description": "魔導士が詠唱を始めた!",
"choices": {},
"enemy": "魔導士",
"clear_condition": ["古びた塔_戦闘"]
},
"奈落の淵": {
"name": "奈落の淵",
"description": "暗く深い奈落の底。ここから魔王城へと続く道がある。",
"choices": {
"魔王城へ": "奈落の淵_戦闘",
"町へ戻る": "始まりの町"
},
"enemy": None
},
"奈落の淵_戦闘": {
"name": "奈落の淵の奥",
"description": "デビルが門番のように立ちはだかる!",
"choices": {},
"enemy": "デビル",
"clear_condition": ["奈落の淵_戦闘"]
},
"魔王城": {
"name": "魔王城",
"description": "ついに魔王城に辿り着いた。最深部には魔王が待ち受けている。",
"choices": {
"最深部へ": "魔王城_戦闘",
"奈落の淵へ戻る": "奈落の淵"
},
"enemy": None
},
"魔王城_戦闘": {
"name": "魔王の間",
"description": "魔王があなたを待ち構えていた!",
"choices": {},
"enemy": "魔王",
"clear_condition": ["魔王城_戦闘"]
},
# 回復の泉を追加
"回復の泉": {
"name": "回復の泉",
"description": "神秘的な光を放つ泉がある。全身の傷が癒されるのを感じる。",
"choices": {
"町へ戻る": "始まりの町"
},
"enemy": None,
"healing_spot": True # 回復スポットであることを示すフラグ
}
}
# --- ゲームシステム関数 ---
def display_player_status(player):
"""プレイヤーの現在のステータスを表示する関数 (コマンドライン版用)"""
print("----------------------------------------")
print(f"名前: {player['name']}")
print(f"HP: {player['hp']}/{player['max_hp']}")
print(f"攻撃力: {player['attack']}")
print(f"防御力: {player['defense']}")
equipped_weapon_name = player["equipped_weapon"]["name"] if player["equipped_weapon"] else "なし"
equipped_armor_name = player["equipped_armor"]["name"] if player["equipped_armor"] else "なし"
print(f"装備: 武器 - {equipped_weapon_name}, 防具 - {equipped_armor_name}")
print(f"所持アイテム: {[item['name'] for item in player['items']] if player['items'] else 'なし'}")
print("----------------------------------------")
def calculate_damage(attacker_attack, defender_defense):
"""ダメージを計算する関数"""
damage = max(1, attacker_attack - defender_defense + random.randint(-2, 2))
return damage
# この if ブロックの中に、これまでのゲーム開始処理とメインループを移動させる
if __name__ == "__main__":
# --- ゲーム開始処理 ---
print("----------------------------------------")
print(" 勇者みならいの冒険 ")
print("----------------------------------------")
player_name = input("勇者の名前を入力してください:")
player = create_player(player_name)
print("冒険を始めるにあたり、どちらかのオーブを選んでください。")
print("1: 金のオーブ (攻撃力アップ)")
print("2: 銀のオーブ (防御力アップ)")
orb_choice = input("選択してください (1/2): ")
if orb_choice == "1":
player["items"].append(items_data["金のオーブ"])
player["attack"] += items_data["金のオーブ"]["attack_boost"]
print("金のオーブを手に入れた!攻撃力がアップした!")
elif orb_choice == "2":
player["items"].append(items_data["銀のオーブ"])
player["defense"] += items_data["銀のオーブ"]["defense_boost"]
print("銀のオーブを手に入れた!防御力がアップした!")
else:
print("無効な選択です。オーブは選べませんでした。")
display_player_status(player)
current_location_key = "始まりの町" # 常に始まりの町から開始
print("\n--- 冒険が今、始まる! ---")
# --- ゲームメインループ ---
game_over = False
game_clear = False
while not game_over and not game_clear:
current_location = locations[current_location_key]
print(f"\n--- 現在地: {current_location['name']} ---")
print(current_location["description"])
# 回復スポットの場合の処理 (コマンドライン版)
if current_location.get("healing_spot"):
if player["hp"] < player["max_hp"]:
player["hp"] = player["max_hp"]
print("泉の力でHPが満タンになった!")
else:
print("すでにHPは満タンだ。")
# 場所に敵がいる場合、戦闘開始
if current_location["enemy"]:
enemy_key_in_location = current_location["enemy"] # これを保持
enemy = dict(enemies[enemy_key_in_location]) # 敵データをコピーして使う
print(f"\n--- {enemy['name']}が現れた! ---")
# コマンドライン版の戦闘ループ
while player['hp'] > 0 and enemy['hp'] > 0:
print(f"\n{player['name']} HP: {player['hp']} vs {enemy['name']} HP: {enemy['hp']}")
print("どうする? (1: たたかう, 2: どうぐ, 3: にげる)")
choice = input(">> ")
if choice == "1": # たたかう
print(f"{player['name']}の攻撃!")
time.sleep(1)
damage = calculate_damage(player['attack'], enemy['defense'])
enemy['hp'] -= damage
print(f"{enemy['name']}に{damage}のダメージを与えた!")
if enemy['hp'] <= 0:
break # 敵を倒したらループを抜ける
time.sleep(1)
print(f"\n{enemy['name']}の攻撃!")
time.sleep(1)
enemy_attack_pattern = random.choice(enemy['attack_patterns'])
print(f"{enemy['name']}: {enemy_attack_pattern}")
damage = calculate_damage(enemy['attack'], player['defense'])
player['hp'] -= damage
print(f"{player['name']}は{damage}のダメージを受けた!")
time.sleep(1)
elif choice == "2": # どうぐ
if not player["items"]:
print("アイテムを持っていません。")
continue
print("どのアイテムを使いますか?")
consumable_items = [item for item in player["items"] if item.get("type") == "consumable"]
if not consumable_items:
print("回復アイテムを持っていません。")
continue
for i, item in enumerate(consumable_items):
print(f"{i+1}: {item['name']} ({item['description']})")
item_choice = input("アイテム番号を入力 (0でキャンセル): ")
if item_choice.isdigit() and 1 <= int(item_choice) <= len(consumable_items):
chosen_item = consumable_items[int(item_choice)-1]
if "hp_recovery" in chosen_item:
player["hp"] = min(player["max_hp"], player["hp"] + chosen_item["hp_recovery"])
player["items"].remove(chosen_item)
print(f"{chosen_item['name']}を使った!HPが{chosen_item['hp_recovery']}回復した!")
else:
print("そのアイテムは使えません。")
else:
print("キャンセルしました。")
time.sleep(1)
# アイテム使用後も敵は攻撃
if enemy['hp'] > 0:
print(f"\n{enemy['name']}の攻撃!")
time.sleep(1)
enemy_attack_pattern = random.choice(enemy['attack_patterns'])
print(f"{enemy['name']}: {enemy_attack_pattern}")
damage = calculate_damage(enemy['attack'], player['defense'])
player['hp'] -= damage
print(f"{player['name']}は{damage}のダメージを受けた!")
time.sleep(1)
elif choice == "3": # にげる
print("逃げ出した!")
time.sleep(1)
if random.random() < 0.5: # 50%の確率で逃走成功
print("うまく逃げ切れた!")
battle_result = "escaped"
break # 戦闘ループを抜ける
else:
print("逃げられなかった!")
time.sleep(1)
# 逃げられなかった場合も敵は攻撃
if enemy['hp'] > 0:
print(f"\n{enemy['name']}の攻撃!")
time.sleep(1)
enemy_attack_pattern = random.choice(enemy['attack_patterns'])
print(f"{enemy['name']}: {enemy_attack_pattern}")
damage = calculate_damage(enemy['attack'], player['defense'])
player['hp'] -= damage
print(f"{player['name']}は{damage}のダメージを受けた!")
time.sleep(1)
else:
print("無効なコマンドです。")
continue
if player['hp'] <= 0:
battle_result = "lose"
break # プレイヤーが倒れたらループを抜ける
# コマンドライン版の戦闘結果処理
if player['hp'] <= 0:
print(f"{player['name']}は力尽きた...")
game_over = True
break
elif enemy['hp'] <= 0:
print(f"{enemy['name']}を倒した!")
# 倒した敵の場所キーを記録 (回復の泉開放条件のため)
if "clear_condition" in current_location and current_location["clear_condition"][0] not in player["cleared_enemies"]:
player["cleared_enemies"].append(current_location["clear_condition"][0])
# ドロップアイテム処理
if "drop_items" in enemies[enemy_key_in_location] and enemies[enemy_key_in_location]["drop_items"]:
print("\nドロップアイテムがあります!どれを選びますか?")
for i, item_key in enumerate(enemies[enemy_key_in_location]["drop_items"]):
item_info = items_data[item_key]
print(f"{i+1}: {item_info['name']} ({item_info['description']})")
while True:
drop_choice = input("選択してください (1/2): ")
if drop_choice.isdigit() and 1 <= int(drop_choice) <= len(enemies[enemy_key_in_location]["drop_items"]):
chosen_item_key = enemies[enemy_key_in_location]["drop_items"][int(drop_choice)-1]
chosen_item = items_data[chosen_item_key]
print(f"✨ {chosen_item['name']}を手に入れた! ✨")
player['items'].append(chosen_item)
# permanent_boost / weapon / armor タイプのアイテムの効果を適用
if chosen_item.get('type') == 'permanent_boost':
if 'attack_boost' in chosen_item:
player['attack'] += chosen_item['attack_boost']
print(f"攻撃力が{chosen_item['attack_boost']}アップした!")
if 'defense_boost' in chosen_item:
player['defense'] += chosen_item['defense_boost']
print(f"防御力が{chosen_item['defense_boost']}アップした!")
elif chosen_item.get('type') == 'weapon':
if player['equipped_weapon']:
player['attack'] -= player['equipped_weapon'].get('attack_boost', 0)
print(f"{player['equipped_weapon']['name']}の装備を解除した。")
player['equipped_weapon'] = chosen_item
player['attack'] += chosen_item.get('attack_boost', 0)
print(f"{chosen_item['name']}を装備した!攻撃力が{chosen_item.get('attack_boost', 0)}アップした!")
elif chosen_item.get('type') == 'armor':
if player['equipped_armor']:
player['defense'] -= player['equipped_armor'].get('defense_boost', 0)
print(f"{player['equipped_armor']['name']}の装備を解除した。")
player['equipped_armor'] = chosen_item
player['defense'] += chosen_item.get('defense_boost', 0)
print(f"{chosen_item['name']}を装備した!防御力が{chosen_item.get('defense_boost', 0)}アップした!")
break
else:
print("無効な選択です。もう一度入力してください。")
# 勝利後の場所遷移ロジック(コマンドライン版)
if current_location_key == "東の森_戦闘":
current_location_key = "北の砦"
elif current_location_key == "西の洞窟_戦闘":
current_location_key = "古びた塔"
elif current_location_key == "北の砦_戦闘":
current_location_key = "奈落の淵"
elif current_location_key == "古びた塔_戦闘":
current_location_key = "奈落の淵"
elif current_location_key == "奈落の淵_戦闘":
current_location_key = "魔王城"
if current_location_key == "魔王城_戦闘":
print("\n----------------------------------------")
print(" 魔王を倒した!世界に平和が訪れた!")
print(f" 勇者{player_name}の伝説が始まった!")
print("----------------------------------------")
game_clear = True
break
if "battle_result" in locals() and battle_result == "escaped":
pass
else:
continue
# 通常の場所移動選択肢の表示
print("\nどこへ移動しますか? (status: ステータス表示, exit: 終了)")
available_choices_nums = []
choice_mapping = {}
# 通常の選択肢を追加
for i, (choice_text, next_location_key) in enumerate(current_location["choices"].items()):
choice_num = str(i + 1)
print(f"{choice_num}: {choice_text}")
available_choices_nums.append(choice_num)
choice_mapping[choice_num] = next_location_key
# 回復の泉への選択肢を動的に追加
# 「東の森_戦闘」と「西の洞窟_戦闘」をクリアしているかチェック
if "東の森_戦闘" in player["cleared_enemies"] and "西の洞窟_戦闘" in player["cleared_enemies"] and current_location_key == "始まりの町":
# 既に選択肢になければ追加
if "回復の泉へ向かう" not in current_location["choices"]:
choice_num = str(len(available_choices_nums) + 1)
print(f"{choice_num}: 回復の泉へ向かう")
available_choices_nums.append(choice_num)
choice_mapping[choice_num] = "回復の泉"
user_input = input(">> ").strip()
if user_input.lower() == 'exit':
print("ゲームを終了します。また遊んでね!")
break
elif user_input.lower() == 'status':
display_player_status(player)
continue
if user_input in available_choices_nums:
current_location_key = choice_mapping[user_input]
else:
print("無効な選択です。")
continue
if game_over:
print("\n----------------------------------------")
print(" GAME OVER ")
print("----------------------------------------")
基本的に敵やアイテムなどの効果、ステータスについては辞書型で盛り込む形になっています。
課題
現状、正常に機能はするものの、名前が勇者勇者になっていたり、画像の大きさがあっていなかったりとまだGUI的にも改良余地があるので、いくつかに分けて投稿を進めようと思います。
今後改良したいところ:
金のオーブ、銀のオーブ→アイコン一のずれ
表示ログの左寄りを真ん中にする
名前の修正
画像の調整
戦闘コマンドの調整
参考