|
| 1 | +import tkinter as tk |
| 2 | +from tkinter import messagebox |
| 3 | +import ttkbootstrap as ttk |
| 4 | +from ttkbootstrap.constants import * |
| 5 | +import random |
| 6 | + |
| 7 | +# ----- Game Logic ----- |
| 8 | +class Player: |
| 9 | + def __init__(self, name): |
| 10 | + self.name = name |
| 11 | + self.hp = 100 |
| 12 | + self.max_hp = 100 |
| 13 | + self.level = 1 |
| 14 | + self.exp = 0 |
| 15 | + self.items = {"Potion": 2, "Elixir": 1, "Bomb": 1} |
| 16 | + self.status_effects = {} |
| 17 | + |
| 18 | + def gain_exp(self, amount): |
| 19 | + self.exp += amount |
| 20 | + if self.exp >= 100: |
| 21 | + self.level_up() |
| 22 | + |
| 23 | + def level_up(self): |
| 24 | + self.level += 1 |
| 25 | + self.max_hp += 20 |
| 26 | + self.hp = self.max_hp |
| 27 | + self.exp = 0 |
| 28 | + messagebox.showinfo("Level Up!", f"{self.name} reached level {self.level}!") |
| 29 | + |
| 30 | + def use_item(self, item_name, target_enemy=None): |
| 31 | + if self.items.get(item_name, 0) > 0: |
| 32 | + if item_name == "Potion": |
| 33 | + heal_amount = 50 |
| 34 | + self.hp = min(self.max_hp, self.hp + heal_amount) |
| 35 | + self.items[item_name] -= 1 |
| 36 | + return heal_amount, f"{self.name} used a Potion and restored {heal_amount} HP!" |
| 37 | + elif item_name == "Elixir": |
| 38 | + heal_amount = 100 |
| 39 | + self.hp = min(self.max_hp, self.hp + heal_amount) |
| 40 | + self.items[item_name] -= 1 |
| 41 | + return heal_amount, f"{self.name} used an Elixir and restored {heal_amount} HP!" |
| 42 | + elif item_name == "Bomb" and target_enemy: |
| 43 | + damage = 50 |
| 44 | + target_enemy.hp -= damage |
| 45 | + target_enemy.add_status('burn', 2) |
| 46 | + self.items[item_name] -= 1 |
| 47 | + return -damage, f"{self.name} used a Bomb on {target_enemy.name} for {damage} damage and inflicted burn!" |
| 48 | + return 0, f"No {item_name}s left!" |
| 49 | + |
| 50 | + def add_status(self, status_name, duration): |
| 51 | + self.status_effects[status_name] = duration |
| 52 | + |
| 53 | + def apply_statuses(self): |
| 54 | + total_damage = 0 |
| 55 | + if 'poison' in self.status_effects: |
| 56 | + dmg = 5 |
| 57 | + self.hp -= dmg |
| 58 | + total_damage += dmg |
| 59 | + self.status_effects['poison'] -= 1 |
| 60 | + if self.status_effects['poison'] <= 0: |
| 61 | + del self.status_effects['poison'] |
| 62 | + return total_damage |
| 63 | + |
| 64 | +class Enemy: |
| 65 | + def __init__(self, name, hp, exp_reward): |
| 66 | + self.name = name |
| 67 | + self.hp = hp |
| 68 | + self.max_hp = hp |
| 69 | + self.exp_reward = exp_reward |
| 70 | + self.status_effects = {} |
| 71 | + |
| 72 | + def add_status(self, status_name, duration): |
| 73 | + self.status_effects[status_name] = duration |
| 74 | + |
| 75 | + def apply_statuses(self): |
| 76 | + total_damage = 0 |
| 77 | + if 'burn' in self.status_effects: |
| 78 | + dmg = 10 |
| 79 | + self.hp -= dmg |
| 80 | + total_damage += dmg |
| 81 | + self.status_effects['burn'] -= 1 |
| 82 | + if self.status_effects['burn'] <= 0: |
| 83 | + del self.status_effects['burn'] |
| 84 | + return total_damage |
| 85 | + |
| 86 | +# ----- Game Engine ----- |
| 87 | +class GameEngine: |
| 88 | + def __init__(self, root): |
| 89 | + self.root = root |
| 90 | + self.root.title("Text-Based RPG") |
| 91 | + self.style = ttk.Style(theme="cosmo") |
| 92 | + |
| 93 | + self.player = None |
| 94 | + self.current_enemy = None |
| 95 | + |
| 96 | + self.frame = ttk.Frame(root, padding=20) |
| 97 | + self.frame.pack(fill=BOTH, expand=YES) |
| 98 | + |
| 99 | + self.label = ttk.Label(self.frame, text="Enter your hero's name:") |
| 100 | + self.label.pack(pady=5) |
| 101 | + |
| 102 | + self.entry = ttk.Entry(self.frame) |
| 103 | + self.entry.pack(pady=5) |
| 104 | + |
| 105 | + self.start_button = ttk.Button(self.frame, text="Start Adventure", command=self.start_game) |
| 106 | + self.start_button.pack(pady=10) |
| 107 | + |
| 108 | + # HP bars |
| 109 | + self.player_hp_bar = ttk.Progressbar(self.frame, length=300, maximum=100) |
| 110 | + self.enemy_hp_bar = ttk.Progressbar(self.frame, length=300, maximum=100) |
| 111 | + |
| 112 | + # Status labels |
| 113 | + self.player_status_label = ttk.Label(self.frame, text="Player Status: None") |
| 114 | + self.enemy_status_label = ttk.Label(self.frame, text="Enemy Status: None") |
| 115 | + |
| 116 | + # Canvas for attack/heal animations |
| 117 | + self.animation_canvas = tk.Canvas(self.frame, width=300, height=50, bg="white") |
| 118 | + self.animation_canvas.pack(pady=5) |
| 119 | + |
| 120 | + self.text_area = tk.Text(self.frame, height=10, state=DISABLED) |
| 121 | + self.text_area.pack(pady=10, fill=BOTH, expand=YES) |
| 122 | + |
| 123 | + self.action_frame = ttk.Frame(self.frame) |
| 124 | + self.action_frame.pack(pady=10) |
| 125 | + |
| 126 | + def start_game(self): |
| 127 | + name = self.entry.get().strip() |
| 128 | + if not name: |
| 129 | + messagebox.showwarning("Input Error", "Please enter a valid name.") |
| 130 | + return |
| 131 | + |
| 132 | + self.player = Player(name) |
| 133 | + self.entry.destroy() |
| 134 | + self.start_button.destroy() |
| 135 | + self.label.config(text=f"Welcome, {name}! Choose your action:") |
| 136 | + |
| 137 | + # Show HP bars and status labels |
| 138 | + self.player_hp_bar.pack(pady=5) |
| 139 | + self.enemy_hp_bar.pack(pady=5) |
| 140 | + self.player_status_label.pack() |
| 141 | + self.enemy_status_label.pack() |
| 142 | + |
| 143 | + self.show_main_actions() |
| 144 | + self.update_bars() |
| 145 | + |
| 146 | + def show_main_actions(self): |
| 147 | + for widget in self.action_frame.winfo_children(): |
| 148 | + widget.destroy() |
| 149 | + ttk.Button(self.action_frame, text="Explore", command=self.explore).pack(side=LEFT, padx=5) |
| 150 | + ttk.Button(self.action_frame, text="Show Stats", command=self.show_stats).pack(side=LEFT, padx=5) |
| 151 | + ttk.Button(self.action_frame, text="Quit", command=self.root.quit).pack(side=LEFT, padx=5) |
| 152 | + |
| 153 | + def explore(self): |
| 154 | + enemies = [Enemy("Goblin", 50, 20), Enemy("Orc", 80, 40), Enemy("Dragon", 150, 100)] |
| 155 | + self.current_enemy = random.choice(enemies) |
| 156 | + self.log(f"You encountered a {self.current_enemy.name} with {self.current_enemy.hp} HP!") |
| 157 | + self.enemy_hp_bar['maximum'] = self.current_enemy.max_hp |
| 158 | + self.enemy_hp_bar['value'] = self.current_enemy.hp |
| 159 | + self.show_combat_menu() |
| 160 | + self.update_status_labels() |
| 161 | + |
| 162 | + def show_combat_menu(self): |
| 163 | + for widget in self.action_frame.winfo_children(): |
| 164 | + widget.destroy() |
| 165 | + ttk.Button(self.action_frame, text="Attack", command=self.attack).pack(side=LEFT, padx=5) |
| 166 | + ttk.Button(self.action_frame, text="Defend", command=self.defend).pack(side=LEFT, padx=5) |
| 167 | + ttk.Button(self.action_frame, text="Use Item", command=self.show_inventory).pack(side=LEFT, padx=5) |
| 168 | + ttk.Button(self.action_frame, text="Run", command=self.run_away).pack(side=LEFT, padx=5) |
| 169 | + |
| 170 | + def attack(self): |
| 171 | + damage = random.randint(15, 30) |
| 172 | + self.animate_attack(target='enemy') |
| 173 | + self.current_enemy.hp -= damage |
| 174 | + self.log(f"You attack {self.current_enemy.name} for {damage} damage.") |
| 175 | + self.apply_status_effects() |
| 176 | + self.update_bars() |
| 177 | + self.check_battle_end() |
| 178 | + self.enemy_turn() |
| 179 | + |
| 180 | + def defend(self): |
| 181 | + self.log(f"{self.player.name} braces for the next attack, reducing damage taken!") |
| 182 | + self.apply_status_effects() |
| 183 | + self.update_bars() |
| 184 | + self.enemy_turn(defending=True) |
| 185 | + |
| 186 | + def show_inventory(self): |
| 187 | + inv_window = tk.Toplevel(self.root) |
| 188 | + inv_window.title("Inventory") |
| 189 | + for item, count in self.player.items.items(): |
| 190 | + if count > 0: |
| 191 | + def use_item_closure(item_name=item): |
| 192 | + amount, result = self.player.use_item(item_name, target_enemy=self.current_enemy) |
| 193 | + if amount > 0: |
| 194 | + self.animate_heal(amount) |
| 195 | + elif amount < 0: |
| 196 | + self.animate_attack(target='enemy') |
| 197 | + self.log(result) |
| 198 | + inv_window.destroy() |
| 199 | + self.apply_status_effects() |
| 200 | + self.update_bars() |
| 201 | + self.check_battle_end() |
| 202 | + self.enemy_turn() |
| 203 | + ttk.Button(inv_window, text=f"{item} x{count}", command=use_item_closure).pack(pady=5) |
| 204 | + |
| 205 | + def animate_attack(self, target='enemy'): |
| 206 | + bar = self.enemy_hp_bar if target=='enemy' else self.player_hp_bar |
| 207 | + original_color = bar.cget('style') |
| 208 | + flash_color = 'danger.Horizontal.TProgressbar' |
| 209 | + self.style.configure(flash_color, troughcolor='white', background='red') |
| 210 | + bar['style'] = flash_color |
| 211 | + self.root.after(200, lambda: bar.configure(style=original_color)) |
| 212 | + |
| 213 | + def animate_heal(self, amount): |
| 214 | + bar = self.player_hp_bar |
| 215 | + original_color = bar.cget('style') |
| 216 | + flash_color = 'success.Horizontal.TProgressbar' |
| 217 | + self.style.configure(flash_color, troughcolor='white', background='green') |
| 218 | + bar['style'] = flash_color |
| 219 | + self.root.after(200, lambda: bar.configure(style=original_color)) |
| 220 | + |
| 221 | + def run_away(self): |
| 222 | + success = random.random() > 0.3 |
| 223 | + if success: |
| 224 | + self.log("You successfully ran away!") |
| 225 | + self.show_main_actions() |
| 226 | + else: |
| 227 | + self.log("Failed to run away!") |
| 228 | + self.apply_status_effects() |
| 229 | + self.update_bars() |
| 230 | + self.enemy_turn() |
| 231 | + |
| 232 | + def enemy_turn(self, defending=False): |
| 233 | + if self.current_enemy.hp <= 0: |
| 234 | + return |
| 235 | + damage = random.randint(10, 25) |
| 236 | + if defending: |
| 237 | + damage //= 2 |
| 238 | + self.player.hp -= damage |
| 239 | + self.animate_attack(target='player') |
| 240 | + self.log(f"{self.current_enemy.name} attacks you for {damage} damage.") |
| 241 | + self.apply_status_effects() |
| 242 | + self.update_bars() |
| 243 | + self.check_player_defeat() |
| 244 | + |
| 245 | + def apply_status_effects(self): |
| 246 | + player_damage = self.player.apply_statuses() |
| 247 | + enemy_damage = self.current_enemy.apply_statuses() if self.current_enemy else 0 |
| 248 | + if player_damage > 0: |
| 249 | + self.animate_attack(target='player') |
| 250 | + self.log(f"{self.player.name} took {player_damage} damage from status effects!") |
| 251 | + if enemy_damage > 0: |
| 252 | + self.animate_attack(target='enemy') |
| 253 | + self.log(f"{self.current_enemy.name} took {enemy_damage} damage from status effects!") |
| 254 | + self.update_status_labels() |
| 255 | + |
| 256 | + def update_bars(self): |
| 257 | + if self.player: |
| 258 | + self.player_hp_bar['maximum'] = self.player.max_hp |
| 259 | + self.player_hp_bar['value'] = max(0, self.player.hp) |
| 260 | + if self.current_enemy: |
| 261 | + self.enemy_hp_bar['maximum'] = self.current_enemy.max_hp |
| 262 | + self.enemy_hp_bar['value'] = max(0, self.current_enemy.hp) |
| 263 | + |
| 264 | + def update_status_labels(self): |
| 265 | + player_status = ', '.join([f"{k}({v})" for k,v in self.player.status_effects.items()]) or 'None' |
| 266 | + enemy_status = ', '.join([f"{k}({v})" for k,v in self.current_enemy.status_effects.items()]) if self.current_enemy else 'None' |
| 267 | + self.player_status_label.config(text=f"Player Status: {player_status}") |
| 268 | + self.enemy_status_label.config(text=f"Enemy Status: {enemy_status}") |
| 269 | + |
| 270 | + def show_stats(self): |
| 271 | + items_list = ', '.join([f"{k} x{v}" for k, v in self.player.items.items() if v > 0]) |
| 272 | + status_list = ', '.join([f"{k} ({v} turns)" for k, v in self.player.status_effects.items()]) |
| 273 | + status_text = status_list if status_list else 'None' |
| 274 | + self.log(f"Name: {self.player.name} | HP: {self.player.hp}/{self.player.max_hp} | Level: {self.player.level} | EXP: {self.player.exp} | Items: {items_list} | Status: {status_text}") |
| 275 | + |
| 276 | + def check_battle_end(self): |
| 277 | + if self.current_enemy and self.current_enemy.hp <= 0: |
| 278 | + self.log(f"You defeated {self.current_enemy.name}!") |
| 279 | + self.player.gain_exp(self.current_enemy.exp_reward) |
| 280 | + self.current_enemy = None |
| 281 | + self.show_main_actions() |
| 282 | + self.update_bars() |
| 283 | + |
| 284 | + def check_player_defeat(self): |
| 285 | + if self.player.hp <= 0: |
| 286 | + self.log("You were defeated! Game Over.") |
| 287 | + messagebox.showinfo("Game Over", "You have fallen in battle.") |
| 288 | + self.root.quit() |
| 289 | + |
| 290 | + def log(self, message): |
| 291 | + self.text_area.config(state=NORMAL) |
| 292 | + self.text_area.insert(END, message + "\n") |
| 293 | + self.text_area.see(END) |
| 294 | + self.text_area.config(state=DISABLED) |
| 295 | + |
| 296 | +# ----- Main Application ----- |
| 297 | +if __name__ == "__main__": |
| 298 | + root = ttk.Window(themename="cosmo") |
| 299 | + app = GameEngine(root) |
| 300 | + root.mainloop() |
0 commit comments